plugin-webpack5.js 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  1. const { resolveCompiler } = require('./compiler')
  2. const qs = require('querystring')
  3. const id = 'vue-loader-plugin'
  4. const NS = 'vue-loader'
  5. const BasicEffectRulePlugin = require('webpack/lib/rules/BasicEffectRulePlugin')
  6. const BasicMatcherRulePlugin = require('webpack/lib/rules/BasicMatcherRulePlugin')
  7. const RuleSetCompiler = require('webpack/lib/rules/RuleSetCompiler')
  8. const UseEffectRulePlugin = require('webpack/lib/rules/UseEffectRulePlugin')
  9. const objectMatcherRulePlugins = []
  10. try {
  11. const ObjectMatcherRulePlugin = require('webpack/lib/rules/ObjectMatcherRulePlugin')
  12. objectMatcherRulePlugins.push(
  13. new ObjectMatcherRulePlugin('assert', 'assertions'),
  14. new ObjectMatcherRulePlugin('descriptionData')
  15. )
  16. } catch (e) {
  17. const DescriptionDataMatcherRulePlugin = require('webpack/lib/rules/DescriptionDataMatcherRulePlugin')
  18. objectMatcherRulePlugins.push(new DescriptionDataMatcherRulePlugin())
  19. }
  20. const ruleSetCompiler = new RuleSetCompiler([
  21. new BasicMatcherRulePlugin('test', 'resource'),
  22. new BasicMatcherRulePlugin('mimetype'),
  23. new BasicMatcherRulePlugin('dependency'),
  24. new BasicMatcherRulePlugin('include', 'resource'),
  25. new BasicMatcherRulePlugin('exclude', 'resource', true),
  26. new BasicMatcherRulePlugin('conditions'),
  27. new BasicMatcherRulePlugin('resource'),
  28. new BasicMatcherRulePlugin('resourceQuery'),
  29. new BasicMatcherRulePlugin('resourceFragment'),
  30. new BasicMatcherRulePlugin('realResource'),
  31. new BasicMatcherRulePlugin('issuer'),
  32. new BasicMatcherRulePlugin('compiler'),
  33. ...objectMatcherRulePlugins,
  34. new BasicEffectRulePlugin('type'),
  35. new BasicEffectRulePlugin('sideEffects'),
  36. new BasicEffectRulePlugin('parser'),
  37. new BasicEffectRulePlugin('resolve'),
  38. new BasicEffectRulePlugin('generator'),
  39. new UseEffectRulePlugin()
  40. ])
  41. class VueLoaderPlugin {
  42. apply(compiler) {
  43. const normalModule = compiler.webpack
  44. ? compiler.webpack.NormalModule
  45. : require('webpack/lib/NormalModule')
  46. // add NS marker so that the loader can detect and report missing plugin
  47. compiler.hooks.compilation.tap(id, (compilation) => {
  48. const normalModuleLoader =
  49. normalModule.getCompilationHooks(compilation).loader
  50. normalModuleLoader.tap(id, (loaderContext) => {
  51. loaderContext[NS] = true
  52. })
  53. })
  54. const rules = compiler.options.module.rules
  55. let rawVueRule
  56. let vueRules = []
  57. for (const rawRule of rules) {
  58. // skip rules with 'enforce'. eg. rule for eslint-loader
  59. if (rawRule.enforce) {
  60. continue
  61. }
  62. vueRules = match(rawRule, 'foo.vue')
  63. if (!vueRules.length) {
  64. vueRules = match(rawRule, 'foo.vue.html')
  65. }
  66. if (vueRules.length > 0) {
  67. if (rawRule.oneOf) {
  68. throw new Error(
  69. `[VueLoaderPlugin Error] vue-loader 15 currently does not support vue rules with oneOf.`
  70. )
  71. }
  72. rawVueRule = rawRule
  73. break
  74. }
  75. }
  76. if (!vueRules.length) {
  77. throw new Error(
  78. `[VueLoaderPlugin Error] No matching rule for .vue files found.\n` +
  79. `Make sure there is at least one root-level rule that matches .vue or .vue.html files.`
  80. )
  81. }
  82. // get the normalized "use" for vue files
  83. const vueUse = vueRules
  84. .filter((rule) => rule.type === 'use')
  85. .map((rule) => rule.value)
  86. // get vue-loader options
  87. const vueLoaderUseIndex = vueUse.findIndex((u) => {
  88. return /^vue-loader|(\/|\\|@)vue-loader/.test(u.loader)
  89. })
  90. if (vueLoaderUseIndex < 0) {
  91. throw new Error(
  92. `[VueLoaderPlugin Error] No matching use for vue-loader is found.\n` +
  93. `Make sure the rule matching .vue files include vue-loader in its use.`
  94. )
  95. }
  96. // make sure vue-loader options has a known ident so that we can share
  97. // options by reference in the template-loader by using a ref query like
  98. // template-loader??vue-loader-options
  99. const vueLoaderUse = vueUse[vueLoaderUseIndex]
  100. vueLoaderUse.ident = 'vue-loader-options'
  101. vueLoaderUse.options = vueLoaderUse.options || {}
  102. // for each user rule (expect the vue rule), create a cloned rule
  103. // that targets the corresponding language blocks in *.vue files.
  104. const refs = new Map()
  105. const clonedRules = rules
  106. .filter((r) => r !== rawVueRule)
  107. .map((rawRule) =>
  108. cloneRule(rawRule, refs, langBlockRuleCheck, langBlockRuleResource)
  109. )
  110. // fix conflict with config.loader and config.options when using config.use
  111. delete rawVueRule.loader
  112. delete rawVueRule.options
  113. rawVueRule.use = vueUse
  114. // rule for template compiler
  115. const templateCompilerRule = {
  116. loader: require.resolve('./loaders/templateLoader'),
  117. resourceQuery: (query) => {
  118. if (!query) {
  119. return false
  120. }
  121. const parsed = qs.parse(query.slice(1))
  122. return parsed.vue != null && parsed.type === 'template'
  123. },
  124. options: vueLoaderUse.options
  125. }
  126. // for each rule that matches plain .js files, also create a clone and
  127. // match it against the compiled template code inside *.vue files, so that
  128. // compiled vue render functions receive the same treatment as user code
  129. // (mostly babel)
  130. const { is27 } = resolveCompiler(compiler.options.context)
  131. let jsRulesForRenderFn = []
  132. if (is27) {
  133. const skipThreadLoader = true
  134. jsRulesForRenderFn = rules
  135. .filter(
  136. (r) =>
  137. r !== rawVueRule &&
  138. (match(r, 'test.js').length > 0 || match(r, 'test.ts').length > 0)
  139. )
  140. .map((rawRule) => cloneRule(rawRule, refs, jsRuleCheck, jsRuleResource, skipThreadLoader))
  141. }
  142. // global pitcher (responsible for injecting template compiler loader & CSS
  143. // post loader)
  144. const pitcher = {
  145. loader: require.resolve('./loaders/pitcher'),
  146. resourceQuery: (query) => {
  147. if (!query) {
  148. return false
  149. }
  150. const parsed = qs.parse(query.slice(1))
  151. return parsed.vue != null
  152. },
  153. options: {
  154. cacheDirectory: vueLoaderUse.options.cacheDirectory,
  155. cacheIdentifier: vueLoaderUse.options.cacheIdentifier
  156. }
  157. }
  158. // replace original rules
  159. compiler.options.module.rules = [
  160. pitcher,
  161. ...jsRulesForRenderFn,
  162. ...(is27 ? [templateCompilerRule] : []),
  163. ...clonedRules,
  164. ...rules
  165. ]
  166. }
  167. }
  168. const matcherCache = new WeakMap()
  169. function match(rule, fakeFile) {
  170. let ruleSet = matcherCache.get(rule)
  171. if (!ruleSet) {
  172. // skip the `include` check when locating the vue rule
  173. const clonedRawRule = { ...rule }
  174. delete clonedRawRule.include
  175. ruleSet = ruleSetCompiler.compile([clonedRawRule])
  176. matcherCache.set(rule, ruleSet)
  177. }
  178. return ruleSet.exec({
  179. resource: fakeFile
  180. })
  181. }
  182. const langBlockRuleCheck = (query, rule) => {
  183. return (
  184. query.type === 'custom' || !rule.conditions.length || query.lang != null
  185. )
  186. }
  187. const langBlockRuleResource = (query, resource) => `${resource}.${query.lang}`
  188. const jsRuleCheck = (query) => {
  189. return query.type === 'template'
  190. }
  191. const jsRuleResource = (query, resource) =>
  192. `${resource}.${query.ts ? `ts` : `js`}`
  193. let uid = 0
  194. function cloneRule(rawRule, refs, ruleCheck, ruleResource, skipThreadLoader) {
  195. const compiledRule = ruleSetCompiler.compileRule(
  196. `clonedRuleSet-${++uid}`,
  197. rawRule,
  198. refs
  199. )
  200. // do not process rule with enforce
  201. if (!rawRule.enforce) {
  202. const ruleUse = compiledRule.effects
  203. .filter((effect) => effect.type === 'use')
  204. .map((effect) => effect.value)
  205. // fix conflict with config.loader and config.options when using config.use
  206. delete rawRule.loader
  207. delete rawRule.options
  208. // Filter out `thread-loader` from the `use` array.
  209. // Mitigate https://github.com/vuejs/vue/issues/12828
  210. // Note this won't work if the `use` filed is a function
  211. if (skipThreadLoader && Array.isArray(ruleUse)) {
  212. const isThreadLoader = (loader) => loader === 'thread-loader' || /\/node_modules\/thread-loader\//.test(loader)
  213. rawRule.use = ruleUse.filter(useEntry => {
  214. const loader = typeof useEntry === 'string' ? useEntry : useEntry.loader
  215. return !isThreadLoader(loader)
  216. })
  217. } else {
  218. rawRule.use = ruleUse
  219. }
  220. }
  221. let currentResource
  222. const res = {
  223. ...rawRule,
  224. resource: (resources) => {
  225. currentResource = resources
  226. return true
  227. },
  228. resourceQuery: (query) => {
  229. if (!query) {
  230. return false
  231. }
  232. const parsed = qs.parse(query.slice(1))
  233. if (parsed.vue == null) {
  234. return false
  235. }
  236. if (!ruleCheck(parsed, compiledRule)) {
  237. return false
  238. }
  239. const fakeResourcePath = ruleResource(parsed, currentResource)
  240. for (const condition of compiledRule.conditions) {
  241. // add support for resourceQuery
  242. const request =
  243. condition.property === 'resourceQuery' ? query : fakeResourcePath
  244. if (condition && !condition.fn(request)) {
  245. return false
  246. }
  247. }
  248. return true
  249. }
  250. }
  251. delete res.test
  252. if (rawRule.rules) {
  253. res.rules = rawRule.rules.map((rule) =>
  254. cloneRule(rule, refs, ruleCheck, ruleResource)
  255. )
  256. }
  257. if (rawRule.oneOf) {
  258. res.oneOf = rawRule.oneOf.map((rule) =>
  259. cloneRule(rule, refs, ruleCheck, ruleResource)
  260. )
  261. }
  262. return res
  263. }
  264. VueLoaderPlugin.NS = NS
  265. module.exports = VueLoaderPlugin