plugin-webpack5.js 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  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. jsRulesForRenderFn = rules
  134. .filter(
  135. (r) =>
  136. r !== rawVueRule &&
  137. (match(r, 'test.js').length > 0 || match(r, 'test.ts').length > 0)
  138. )
  139. .map((rawRule) => cloneRule(rawRule, refs, jsRuleCheck, jsRuleResource))
  140. }
  141. // global pitcher (responsible for injecting template compiler loader & CSS
  142. // post loader)
  143. const pitcher = {
  144. loader: require.resolve('./loaders/pitcher'),
  145. resourceQuery: (query) => {
  146. if (!query) {
  147. return false
  148. }
  149. const parsed = qs.parse(query.slice(1))
  150. return parsed.vue != null
  151. },
  152. options: {
  153. cacheDirectory: vueLoaderUse.options.cacheDirectory,
  154. cacheIdentifier: vueLoaderUse.options.cacheIdentifier
  155. }
  156. }
  157. // replace original rules
  158. compiler.options.module.rules = [
  159. pitcher,
  160. ...jsRulesForRenderFn,
  161. ...(is27 ? [templateCompilerRule] : []),
  162. ...clonedRules,
  163. ...rules
  164. ]
  165. }
  166. }
  167. const matcherCache = new WeakMap()
  168. function match(rule, fakeFile) {
  169. let ruleSet = matcherCache.get(rule)
  170. if (!ruleSet) {
  171. // skip the `include` check when locating the vue rule
  172. const clonedRawRule = { ...rule }
  173. delete clonedRawRule.include
  174. ruleSet = ruleSetCompiler.compile([clonedRawRule])
  175. matcherCache.set(rule, ruleSet)
  176. }
  177. return ruleSet.exec({
  178. resource: fakeFile
  179. })
  180. }
  181. const langBlockRuleCheck = (query, rule) => {
  182. return (
  183. query.type === 'custom' || !rule.conditions.length || query.lang != null
  184. )
  185. }
  186. const langBlockRuleResource = (query, resource) => `${resource}.${query.lang}`
  187. const jsRuleCheck = (query) => {
  188. return query.type === 'template'
  189. }
  190. const jsRuleResource = (query, resource) =>
  191. `${resource}.${query.ts ? `ts` : `js`}`
  192. let uid = 0
  193. function cloneRule(rawRule, refs, ruleCheck, ruleResource) {
  194. const compiledRule = ruleSetCompiler.compileRule(
  195. `clonedRuleSet-${++uid}`,
  196. rawRule,
  197. refs
  198. )
  199. // do not process rule with enforce
  200. if (!rawRule.enforce) {
  201. const ruleUse = compiledRule.effects
  202. .filter((effect) => effect.type === 'use')
  203. .map((effect) => effect.value)
  204. // fix conflict with config.loader and config.options when using config.use
  205. delete rawRule.loader
  206. delete rawRule.options
  207. rawRule.use = ruleUse
  208. }
  209. let currentResource
  210. const res = {
  211. ...rawRule,
  212. resource: (resources) => {
  213. currentResource = resources
  214. return true
  215. },
  216. resourceQuery: (query) => {
  217. if (!query) {
  218. return false
  219. }
  220. const parsed = qs.parse(query.slice(1))
  221. if (parsed.vue == null) {
  222. return false
  223. }
  224. if (!ruleCheck(parsed, compiledRule)) {
  225. return false
  226. }
  227. const fakeResourcePath = ruleResource(parsed, currentResource)
  228. for (const condition of compiledRule.conditions) {
  229. // add support for resourceQuery
  230. const request =
  231. condition.property === 'resourceQuery' ? query : fakeResourcePath
  232. if (condition && !condition.fn(request)) {
  233. return false
  234. }
  235. }
  236. return true
  237. }
  238. }
  239. delete res.test
  240. if (rawRule.rules) {
  241. res.rules = rawRule.rules.map((rule) =>
  242. cloneRule(rule, refs, ruleCheck, ruleResource)
  243. )
  244. }
  245. if (rawRule.oneOf) {
  246. res.oneOf = rawRule.oneOf.map((rule) =>
  247. cloneRule(rule, refs, ruleCheck, ruleResource)
  248. )
  249. }
  250. return res
  251. }
  252. VueLoaderPlugin.NS = NS
  253. module.exports = VueLoaderPlugin