plugin.js 4.6 KB

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