UNPKG

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