pitcher.js 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. const qs = require('querystring')
  2. const loaderUtils = require('loader-utils')
  3. const hash = require('hash-sum')
  4. const selfPath = require.resolve('../index')
  5. const templateLoaderPath = require.resolve('./templateLoader')
  6. const stylePostLoaderPath = require.resolve('./stylePostLoader')
  7. const { resolveCompiler } = require('../compiler')
  8. const { testWebpack5 } = require('../codegen/utils')
  9. const isESLintLoader = (l) => /(\/|\\|@)eslint-loader/.test(l.path)
  10. const isNullLoader = (l) => /(\/|\\|@)null-loader/.test(l.path)
  11. const isCSSLoader = (l) => /(\/|\\|@)css-loader/.test(l.path)
  12. const isCacheLoader = (l) => /(\/|\\|@)cache-loader/.test(l.path)
  13. const isPitcher = (l) => l.path !== __filename
  14. const isPreLoader = (l) => !l.pitchExecuted
  15. const isPostLoader = (l) => l.pitchExecuted
  16. const dedupeESLintLoader = (loaders) => {
  17. const res = []
  18. let seen = false
  19. loaders.forEach((l) => {
  20. if (!isESLintLoader(l)) {
  21. res.push(l)
  22. } else if (!seen) {
  23. seen = true
  24. res.push(l)
  25. }
  26. })
  27. return res
  28. }
  29. const shouldIgnoreCustomBlock = (loaders) => {
  30. const actualLoaders = loaders.filter((loader) => {
  31. // vue-loader
  32. if (loader.path === selfPath) {
  33. return false
  34. }
  35. // cache-loader
  36. if (isCacheLoader(loader)) {
  37. return false
  38. }
  39. return true
  40. })
  41. return actualLoaders.length === 0
  42. }
  43. module.exports = (code) => code
  44. // This pitching loader is responsible for intercepting all vue block requests
  45. // and transform it into appropriate requests.
  46. module.exports.pitch = function (remainingRequest) {
  47. const options = loaderUtils.getOptions(this)
  48. const { cacheDirectory, cacheIdentifier } = options
  49. const query = qs.parse(this.resourceQuery.slice(1))
  50. const isWebpack5 = testWebpack5(this._compiler)
  51. let loaders = this.loaders
  52. // if this is a language block request, eslint-loader may get matched
  53. // multiple times
  54. if (query.type) {
  55. // if this is an inline block, since the whole file itself is being linted,
  56. // remove eslint-loader to avoid duplicate linting.
  57. if (/\.vue$/.test(this.resourcePath)) {
  58. loaders = loaders.filter((l) => !isESLintLoader(l))
  59. } else {
  60. // This is a src import. Just make sure there's not more than 1 instance
  61. // of eslint present.
  62. loaders = dedupeESLintLoader(loaders)
  63. }
  64. }
  65. // remove self
  66. loaders = loaders.filter(isPitcher)
  67. // do not inject if user uses null-loader to void the type (#1239)
  68. if (loaders.some(isNullLoader)) {
  69. return
  70. }
  71. const genRequest = (loaders, lang) => {
  72. // Important: dedupe since both the original rule
  73. // and the cloned rule would match a source import request.
  74. // also make sure to dedupe based on loader path.
  75. // assumes you'd probably never want to apply the same loader on the same
  76. // file twice.
  77. // Exception: in Vue CLI we do need two instances of postcss-loader
  78. // for user config and inline minification. So we need to dedupe baesd on
  79. // path AND query to be safe.
  80. const seen = new Map()
  81. const loaderStrings = []
  82. const enableInlineMatchResource =
  83. isWebpack5 && options.experimentalInlineMatchResource
  84. loaders.forEach((loader) => {
  85. const identifier =
  86. typeof loader === 'string' ? loader : loader.path + loader.query
  87. const request = typeof loader === 'string' ? loader : loader.request
  88. if (!seen.has(identifier)) {
  89. seen.set(identifier, true)
  90. // loader.request contains both the resolved loader path and its options
  91. // query (e.g. ??ref-0)
  92. loaderStrings.push(request)
  93. }
  94. })
  95. if (enableInlineMatchResource) {
  96. return loaderUtils.stringifyRequest(
  97. this,
  98. `${this.resourcePath}${lang ? `.${lang}` : ''}${
  99. this.resourceQuery
  100. }!=!-!${[...loaderStrings, this.resourcePath + this.resourceQuery].join('!')}`
  101. )
  102. }
  103. return loaderUtils.stringifyRequest(
  104. this,
  105. '-!' +
  106. [...loaderStrings, this.resourcePath + this.resourceQuery].join('!')
  107. )
  108. }
  109. // Inject style-post-loader before css-loader for scoped CSS and trimming
  110. if (query.type === `style`) {
  111. if (isWebpack5 && this._compiler.options.experiments && this._compiler.options.experiments.css) {
  112. // If user enables `experiments.css`, then we are trying to emit css code directly.
  113. // Although we can target requests like `xxx.vue?type=style` to match `type: "css"`,
  114. // it will make the plugin a mess.
  115. if (!options.experimentalInlineMatchResource) {
  116. this.emitError(
  117. new Error(
  118. '`experimentalInlineMatchResource` should be enabled if `experiments.css` enabled currently'
  119. )
  120. )
  121. return ''
  122. }
  123. if (query.inline || query.module) {
  124. this.emitError(
  125. new Error(
  126. '`inline` or `module` is currently not supported with `experiments.css` enabled'
  127. )
  128. )
  129. return ''
  130. }
  131. const loaderString = [stylePostLoaderPath, ...loaders]
  132. .map((loader) => {
  133. return typeof loader === 'string' ? loader : loader.request
  134. })
  135. .join('!')
  136. const styleRequest = loaderUtils.stringifyRequest(
  137. this,
  138. `${this.resourcePath}${query.lang ? `.${query.lang}` : ''}${
  139. this.resourceQuery
  140. }!=!-!${loaderString}!${this.resourcePath + this.resourceQuery}`
  141. )
  142. return `@import ${styleRequest};`
  143. }
  144. const cssLoaderIndex = loaders.findIndex(isCSSLoader)
  145. if (cssLoaderIndex > -1) {
  146. const afterLoaders = loaders.slice(0, cssLoaderIndex + 1)
  147. const beforeLoaders = loaders.slice(cssLoaderIndex + 1)
  148. const request = genRequest(
  149. [...afterLoaders, stylePostLoaderPath, ...beforeLoaders],
  150. query.lang || 'css'
  151. )
  152. // console.log(request)
  153. return query.module
  154. ? `export { default } from ${request}; export * from ${request}`
  155. : `export * from ${request}`
  156. }
  157. }
  158. // for templates: inject the template compiler & optional cache
  159. if (query.type === `template`) {
  160. const path = require('path')
  161. const cacheLoader =
  162. cacheDirectory && cacheIdentifier
  163. ? [
  164. `${require.resolve('cache-loader')}?${JSON.stringify({
  165. // For some reason, webpack fails to generate consistent hash if we
  166. // use absolute paths here, even though the path is only used in a
  167. // comment. For now we have to ensure cacheDirectory is a relative path.
  168. cacheDirectory: (path.isAbsolute(cacheDirectory)
  169. ? path.relative(process.cwd(), cacheDirectory)
  170. : cacheDirectory
  171. ).replace(/\\/g, '/'),
  172. cacheIdentifier: hash(cacheIdentifier) + '-vue-loader-template'
  173. })}`
  174. ]
  175. : []
  176. const preLoaders = loaders.filter(isPreLoader)
  177. const postLoaders = loaders.filter(isPostLoader)
  178. const { is27 } = resolveCompiler(this.rootContext, this)
  179. const request = genRequest([
  180. ...cacheLoader,
  181. ...postLoaders,
  182. ...(is27 ? [] : [templateLoaderPath + `??vue-loader-options`]),
  183. ...preLoaders
  184. ])
  185. // the template compiler uses esm exports
  186. return `export * from ${request}`
  187. }
  188. // if a custom block has no other matching loader other than vue-loader itself
  189. // or cache-loader, we should ignore it
  190. if (query.type === `custom` && shouldIgnoreCustomBlock(loaders)) {
  191. return ``
  192. }
  193. // When the user defines a rule that has only resourceQuery but no test,
  194. // both that rule and the cloned rule will match, resulting in duplicated
  195. // loaders. Therefore it is necessary to perform a dedupe here.
  196. const request = genRequest(loaders)
  197. return `import mod from ${request}; export default mod; export * from ${request}`
  198. }