plugin-webpack4.js 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. const qs = require('querystring')
  2. const RuleSet = require('webpack/lib/RuleSet')
  3. const id = 'vue-loader-plugin'
  4. const NS = 'vue-loader'
  5. class VueLoaderPlugin {
  6. apply (compiler) {
  7. // add NS marker so that the loader can detect and report missing plugin
  8. if (compiler.hooks) {
  9. // webpack 4
  10. compiler.hooks.compilation.tap(id, compilation => {
  11. const normalModuleLoader = compilation.hooks.normalModuleLoader
  12. normalModuleLoader.tap(id, loaderContext => {
  13. loaderContext[NS] = true
  14. })
  15. })
  16. } else {
  17. // webpack < 4
  18. compiler.plugin('compilation', compilation => {
  19. compilation.plugin('normal-module-loader', loaderContext => {
  20. loaderContext[NS] = true
  21. })
  22. })
  23. }
  24. // use webpack's RuleSet utility to normalize user rules
  25. const rawRules = compiler.options.module.rules
  26. const { rules } = new RuleSet(rawRules)
  27. // find the rule that applies to vue files
  28. let vueRuleIndex = rawRules.findIndex(createMatcher(`foo.vue`))
  29. if (vueRuleIndex < 0) {
  30. vueRuleIndex = rawRules.findIndex(createMatcher(`foo.vue.html`))
  31. }
  32. const vueRule = rules[vueRuleIndex]
  33. if (!vueRule) {
  34. throw new Error(
  35. `[VueLoaderPlugin Error] No matching rule for .vue files found.\n` +
  36. `Make sure there is at least one root-level rule that matches .vue or .vue.html files.`
  37. )
  38. }
  39. if (vueRule.oneOf) {
  40. throw new Error(
  41. `[VueLoaderPlugin Error] vue-loader 15 currently does not support vue rules with oneOf.`
  42. )
  43. }
  44. // get the normlized "use" for vue files
  45. const vueUse = vueRule.use
  46. // get vue-loader options
  47. const vueLoaderUseIndex = vueUse.findIndex(u => {
  48. return /^vue-loader|(\/|\\|@)vue-loader/.test(u.loader)
  49. })
  50. if (vueLoaderUseIndex < 0) {
  51. throw new Error(
  52. `[VueLoaderPlugin Error] No matching use for vue-loader is found.\n` +
  53. `Make sure the rule matching .vue files include vue-loader in its use.`
  54. )
  55. }
  56. // make sure vue-loader options has a known ident so that we can share
  57. // options by reference in the template-loader by using a ref query like
  58. // template-loader??vue-loader-options
  59. const vueLoaderUse = vueUse[vueLoaderUseIndex]
  60. vueLoaderUse.ident = 'vue-loader-options'
  61. vueLoaderUse.options = vueLoaderUse.options || {}
  62. // for each user rule (expect the vue rule), create a cloned rule
  63. // that targets the corresponding language blocks in *.vue files.
  64. const clonedRules = rules
  65. .filter(r => r !== vueRule)
  66. .map(cloneRule)
  67. // global pitcher (responsible for injecting template compiler loader & CSS
  68. // post loader)
  69. const pitcher = {
  70. loader: require.resolve('./loaders/pitcher'),
  71. resourceQuery: query => {
  72. const parsed = qs.parse(query.slice(1))
  73. return parsed.vue != null
  74. },
  75. options: {
  76. cacheDirectory: vueLoaderUse.options.cacheDirectory,
  77. cacheIdentifier: vueLoaderUse.options.cacheIdentifier
  78. }
  79. }
  80. // replace original rules
  81. compiler.options.module.rules = [
  82. pitcher,
  83. ...clonedRules,
  84. ...rules
  85. ]
  86. }
  87. }
  88. function createMatcher (fakeFile) {
  89. return (rule, i) => {
  90. // #1201 we need to skip the `include` check when locating the vue rule
  91. const clone = Object.assign({}, rule)
  92. delete clone.include
  93. const normalized = RuleSet.normalizeRule(clone, {}, '')
  94. return (
  95. !rule.enforce &&
  96. normalized.resource &&
  97. normalized.resource(fakeFile)
  98. )
  99. }
  100. }
  101. function cloneRule (rule) {
  102. const { resource, resourceQuery } = rule
  103. // Assuming `test` and `resourceQuery` tests are executed in series and
  104. // synchronously (which is true based on RuleSet's implementation), we can
  105. // save the current resource being matched from `test` so that we can access
  106. // it in `resourceQuery`. This ensures when we use the normalized rule's
  107. // resource check, include/exclude are matched correctly.
  108. let currentResource
  109. const res = Object.assign({}, rule, {
  110. resource: {
  111. test: resource => {
  112. currentResource = resource
  113. return true
  114. }
  115. },
  116. resourceQuery: query => {
  117. const parsed = qs.parse(query.slice(1))
  118. if (parsed.vue == null) {
  119. return false
  120. }
  121. if (resource && parsed.lang == null) {
  122. return false
  123. }
  124. const fakeResourcePath = `${currentResource}.${parsed.lang}`
  125. if (resource && !resource(fakeResourcePath)) {
  126. return false
  127. }
  128. if (resourceQuery && !resourceQuery(query)) {
  129. return false
  130. }
  131. return true
  132. }
  133. })
  134. if (rule.rules) {
  135. res.rules = rule.rules.map(cloneRule)
  136. }
  137. if (rule.oneOf) {
  138. res.oneOf = rule.oneOf.map(cloneRule)
  139. }
  140. return res
  141. }
  142. VueLoaderPlugin.NS = NS
  143. module.exports = VueLoaderPlugin