plugin-webpack5.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  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. const enableInlineMatchResource =
  103. vueLoaderUse.options.experimentalInlineMatchResource
  104. // for each user rule (expect the vue rule), create a cloned rule
  105. // that targets the corresponding language blocks in *.vue files.
  106. const refs = new Map()
  107. const clonedRules = rules
  108. .filter((r) => r !== rawVueRule)
  109. .map((rawRule) =>
  110. cloneRule(rawRule, refs, langBlockRuleCheck, langBlockRuleResource)
  111. )
  112. // fix conflict with config.loader and config.options when using config.use
  113. delete rawVueRule.loader
  114. delete rawVueRule.options
  115. rawVueRule.use = vueUse
  116. // rule for template compiler
  117. const templateCompilerRule = {
  118. loader: require.resolve('./loaders/templateLoader'),
  119. resourceQuery: (query) => {
  120. if (!query) {
  121. return false
  122. }
  123. const parsed = qs.parse(query.slice(1))
  124. return parsed.vue != null && parsed.type === 'template'
  125. },
  126. options: vueLoaderUse.options
  127. }
  128. // for each rule that matches plain .js files, also create a clone and
  129. // match it against the compiled template code inside *.vue files, so that
  130. // compiled vue render functions receive the same treatment as user code
  131. // (mostly babel)
  132. const { is27 } = resolveCompiler(compiler.options.context)
  133. let jsRulesForRenderFn = []
  134. if (is27) {
  135. const skipThreadLoader = true
  136. jsRulesForRenderFn = rules
  137. .filter(
  138. (r) =>
  139. r !== rawVueRule &&
  140. (match(r, 'test.js').length > 0 || match(r, 'test.ts').length > 0)
  141. )
  142. .map((rawRule) => cloneRule(rawRule, refs, jsRuleCheck, jsRuleResource, skipThreadLoader))
  143. }
  144. // global pitcher (responsible for injecting template compiler loader & CSS
  145. // post loader)
  146. const pitcher = {
  147. loader: require.resolve('./loaders/pitcher'),
  148. resourceQuery: (query) => {
  149. if (!query) {
  150. return false
  151. }
  152. const parsed = qs.parse(query.slice(1))
  153. return parsed.vue != null
  154. },
  155. options: vueLoaderUse.options
  156. }
  157. // replace original rules
  158. if (enableInlineMatchResource) {
  159. // Match rules using `vue-loader`
  160. const vueLoaderRules = rules.filter((rule) => {
  161. const matchOnce = (use) => {
  162. let loaderString = ''
  163. if (!use) {
  164. return loaderString
  165. }
  166. if (typeof use === 'string') {
  167. loaderString = use
  168. } else if (Array.isArray(use)) {
  169. loaderString = matchOnce(use[0])
  170. } else if (typeof use === 'object' && use.loader) {
  171. loaderString = use.loader
  172. }
  173. return loaderString
  174. }
  175. const loader = rule.loader || matchOnce(rule.use)
  176. return (
  177. loader === require('../package.json').name ||
  178. loader.startsWith(require.resolve('./index'))
  179. )
  180. })
  181. compiler.options.module.rules = [
  182. pitcher,
  183. ...rules.filter((rule) => !vueLoaderRules.includes(rule)),
  184. ...(is27 ? [templateCompilerRule] : []),
  185. ...clonedRules,
  186. ...vueLoaderRules
  187. ]
  188. } else {
  189. compiler.options.module.rules = [
  190. pitcher,
  191. ...jsRulesForRenderFn,
  192. ...(is27 ? [templateCompilerRule] : []),
  193. ...clonedRules,
  194. ...rules
  195. ]
  196. }
  197. }
  198. }
  199. const matcherCache = new WeakMap()
  200. function match(rule, fakeFile) {
  201. let ruleSet = matcherCache.get(rule)
  202. if (!ruleSet) {
  203. // skip the `include` check when locating the vue rule
  204. const clonedRawRule = { ...rule }
  205. delete clonedRawRule.include
  206. ruleSet = ruleSetCompiler.compile([clonedRawRule])
  207. matcherCache.set(rule, ruleSet)
  208. }
  209. return ruleSet.exec({
  210. resource: fakeFile
  211. })
  212. }
  213. const langBlockRuleCheck = (query, rule) => {
  214. return (
  215. query.type === 'custom' || !rule.conditions.length || query.lang != null
  216. )
  217. }
  218. const langBlockRuleResource = (query, resource) => `${resource}.${query.lang}`
  219. const jsRuleCheck = (query) => {
  220. return query.type === 'template'
  221. }
  222. const jsRuleResource = (query, resource) =>
  223. `${resource}.${query.ts ? `ts` : `js`}`
  224. let uid = 0
  225. function cloneRule(rawRule, refs, ruleCheck, ruleResource, skipThreadLoader) {
  226. const compiledRule = ruleSetCompiler.compileRule(
  227. `clonedRuleSet-${++uid}`,
  228. rawRule,
  229. refs
  230. )
  231. // do not process rule with enforce
  232. if (!rawRule.enforce) {
  233. const ruleUse = compiledRule.effects
  234. .filter((effect) => effect.type === 'use')
  235. .map((effect) => effect.value)
  236. // fix conflict with config.loader and config.options when using config.use
  237. delete rawRule.loader
  238. delete rawRule.options
  239. // Filter out `thread-loader` from the `use` array.
  240. // Mitigate https://github.com/vuejs/vue/issues/12828
  241. // Note this won't work if the `use` filed is a function
  242. if (skipThreadLoader && Array.isArray(ruleUse)) {
  243. const isThreadLoader = (loader) => loader === 'thread-loader' || /\/node_modules\/thread-loader\//.test(loader)
  244. rawRule.use = ruleUse.filter(useEntry => {
  245. const loader = typeof useEntry === 'string' ? useEntry : useEntry.loader
  246. return !isThreadLoader(loader)
  247. })
  248. } else {
  249. rawRule.use = ruleUse
  250. }
  251. }
  252. let currentResource
  253. const res = {
  254. ...rawRule,
  255. resource: (resources) => {
  256. currentResource = resources
  257. return true
  258. },
  259. resourceQuery: (query) => {
  260. if (!query) {
  261. return false
  262. }
  263. const parsed = qs.parse(query.slice(1))
  264. if (parsed.vue == null) {
  265. return false
  266. }
  267. if (!ruleCheck(parsed, compiledRule)) {
  268. return false
  269. }
  270. const fakeResourcePath = ruleResource(parsed, currentResource)
  271. for (const condition of compiledRule.conditions) {
  272. // add support for resourceQuery
  273. const request =
  274. condition.property === 'resourceQuery' ? query : fakeResourcePath
  275. if (condition && !condition.fn(request)) {
  276. return false
  277. }
  278. }
  279. return true
  280. }
  281. }
  282. delete res.test
  283. if (rawRule.rules) {
  284. res.rules = rawRule.rules.map((rule) =>
  285. cloneRule(rule, refs, ruleCheck, ruleResource)
  286. )
  287. }
  288. if (rawRule.oneOf) {
  289. res.oneOf = rawRule.oneOf.map((rule) =>
  290. cloneRule(rule, refs, ruleCheck, ruleResource)
  291. )
  292. }
  293. return res
  294. }
  295. VueLoaderPlugin.NS = NS
  296. module.exports = VueLoaderPlugin