plugin-webpack5.js 7.0 KB

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