UNPKG

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