plugin-webpack5.js 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. const qs = require('querystring')
  2. const id = 'vue-loader-plugin'
  3. const NS = 'vue-loader'
  4. const BasicEffectRulePlugin = require('webpack/lib/rules/BasicEffectRulePlugin')
  5. const BasicMatcherRulePlugin = require('webpack/lib/rules/BasicMatcherRulePlugin')
  6. const RuleSetCompiler = require('webpack/lib/rules/RuleSetCompiler')
  7. const UseEffectRulePlugin = require('webpack/lib/rules/UseEffectRulePlugin')
  8. const ruleSetCompiler = new RuleSetCompiler([
  9. new BasicMatcherRulePlugin('test', 'resource'),
  10. new BasicMatcherRulePlugin('include', 'resource'),
  11. new BasicMatcherRulePlugin('exclude', 'resource', true),
  12. new BasicMatcherRulePlugin('resource'),
  13. new BasicMatcherRulePlugin('conditions'),
  14. new BasicMatcherRulePlugin('resourceQuery'),
  15. new BasicMatcherRulePlugin('realResource'),
  16. new BasicMatcherRulePlugin('issuer'),
  17. new BasicMatcherRulePlugin('compiler'),
  18. new BasicEffectRulePlugin('type'),
  19. new BasicEffectRulePlugin('sideEffects'),
  20. new BasicEffectRulePlugin('parser'),
  21. new BasicEffectRulePlugin('resolve'),
  22. new UseEffectRulePlugin()
  23. ])
  24. class VueLoaderPlugin {
  25. apply (compiler) {
  26. // add NS marker so that the loader can detect and report missing plugin
  27. compiler.hooks.compilation.tap(id, compilation => {
  28. const normalModuleLoader = require('webpack/lib/NormalModule').getCompilationHooks(compilation).loader
  29. normalModuleLoader.tap(id, loaderContext => {
  30. loaderContext[NS] = true
  31. })
  32. })
  33. const rules = compiler.options.module.rules
  34. let rawVueRules
  35. let vueRules = []
  36. for (const rawRule of rules) {
  37. // skip the `include` check when locating the vue rule
  38. const clonedRawRule = Object.assign({}, rawRule)
  39. delete clonedRawRule.include
  40. const ruleSet = ruleSetCompiler.compile([{
  41. rules: [clonedRawRule]
  42. }])
  43. vueRules = ruleSet.exec({
  44. resource: 'foo.vue'
  45. })
  46. if (!vueRules.length) {
  47. vueRules = ruleSet.exec({
  48. resource: 'foo.vue.html'
  49. })
  50. }
  51. if (vueRules.length > 0) {
  52. if (rawRule.oneOf) {
  53. throw new Error(
  54. `[VueLoaderPlugin Error] vue-loader 15 currently does not support vue rules with oneOf.`
  55. )
  56. }
  57. rawVueRules = rawRule
  58. break
  59. }
  60. }
  61. if (!vueRules.length) {
  62. throw new Error(
  63. `[VueLoaderPlugin Error] No matching rule for .vue files found.\n` +
  64. `Make sure there is at least one root-level rule that matches .vue or .vue.html files.`
  65. )
  66. }
  67. // get the normlized "use" for vue files
  68. const vueUse = vueRules.filter(rule => rule.type === 'use').map(rule => rule.value)
  69. // get vue-loader options
  70. const vueLoaderUseIndex = vueUse.findIndex(u => {
  71. return /^vue-loader|(\/|\\|@)vue-loader/.test(u.loader)
  72. })
  73. if (vueLoaderUseIndex < 0) {
  74. throw new Error(
  75. `[VueLoaderPlugin Error] No matching use for vue-loader is found.\n` +
  76. `Make sure the rule matching .vue files include vue-loader in its use.`
  77. )
  78. }
  79. // make sure vue-loader options has a known ident so that we can share
  80. // options by reference in the template-loader by using a ref query like
  81. // template-loader??vue-loader-options
  82. const vueLoaderUse = vueUse[vueLoaderUseIndex]
  83. vueLoaderUse.ident = 'vue-loader-options'
  84. vueLoaderUse.options = vueLoaderUse.options || {}
  85. // for each user rule (expect the vue rule), create a cloned rule
  86. // that targets the corresponding language blocks in *.vue files.
  87. const refs = new Map()
  88. const clonedRules = rules
  89. .filter(r => r !== rawVueRules)
  90. .map((rawRule) => cloneRule(rawRule, refs))
  91. // fix conflict with config.loader and config.options when using config.use
  92. delete rawVueRules.loader
  93. delete rawVueRules.options
  94. rawVueRules.use = vueUse
  95. // global pitcher (responsible for injecting template compiler loader & CSS
  96. // post loader)
  97. const pitcher = {
  98. loader: require.resolve('./loaders/pitcher'),
  99. resourceQuery: query => {
  100. const parsed = qs.parse(query.slice(1))
  101. return parsed.vue != null
  102. },
  103. options: {
  104. cacheDirectory: vueLoaderUse.options.cacheDirectory,
  105. cacheIdentifier: vueLoaderUse.options.cacheIdentifier
  106. }
  107. }
  108. // replace original rules
  109. compiler.options.module.rules = [
  110. pitcher,
  111. ...clonedRules,
  112. ...rules
  113. ]
  114. }
  115. }
  116. let uid = 0
  117. function cloneRule (rawRule, refs) {
  118. const rules = ruleSetCompiler.compileRules(`clonedRuleSet-${++uid}`, [{
  119. rules: [rawRule]
  120. }], refs)
  121. let currentResource
  122. const conditions = rules[0].rules
  123. .map(rule => rule.conditions)
  124. // shallow flat
  125. .reduce((prev, next) => prev.concat(next), [])
  126. // do not process rule with enforce
  127. if (!rawRule.enforce) {
  128. const ruleUse = rules[0].rules
  129. .map(rule => rule.effects
  130. .filter(effect => effect.type === 'use')
  131. .map(effect => effect.value)
  132. )
  133. // shallow flat
  134. .reduce((prev, next) => prev.concat(next), [])
  135. // fix conflict with config.loader and config.options when using config.use
  136. delete rawRule.loader
  137. delete rawRule.options
  138. rawRule.use = ruleUse
  139. }
  140. const res = Object.assign({}, rawRule, {
  141. resource: resources => {
  142. currentResource = resources
  143. return true
  144. },
  145. resourceQuery: query => {
  146. const parsed = qs.parse(query.slice(1))
  147. if (parsed.vue == null) {
  148. return false
  149. }
  150. if (!conditions) {
  151. return false
  152. }
  153. const fakeResourcePath = `${currentResource}.${parsed.lang}`
  154. for (const condition of conditions) {
  155. // add support for resourceQuery
  156. const request = condition.property === 'resourceQuery' ? query : fakeResourcePath
  157. if (condition && !condition.fn(request)) {
  158. return false
  159. }
  160. }
  161. return true
  162. }
  163. })
  164. delete res.test
  165. if (rawRule.rules) {
  166. res.rules = rawRule.rules.map(rule => cloneRule(rule, refs))
  167. }
  168. if (rawRule.oneOf) {
  169. res.oneOf = rawRule.oneOf.map(rule => cloneRule(rule, refs))
  170. }
  171. return res
  172. }
  173. VueLoaderPlugin.NS = NS
  174. module.exports = VueLoaderPlugin