You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

163 lines
4.9 KiB

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