1 | const qs = require('querystring')
|
2 | const id = 'vue-loader-plugin'
|
3 | const NS = 'vue-loader'
|
4 | const BasicEffectRulePlugin = require('webpack/lib/rules/BasicEffectRulePlugin')
|
5 | const BasicMatcherRulePlugin = require('webpack/lib/rules/BasicMatcherRulePlugin')
|
6 | const RuleSetCompiler = require('webpack/lib/rules/RuleSetCompiler')
|
7 | const UseEffectRulePlugin = require('webpack/lib/rules/UseEffectRulePlugin')
|
8 |
|
9 | const 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 |
|
26 | class VueLoaderPlugin {
|
27 | apply (compiler) {
|
28 |
|
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 |
|
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 |
|
75 | const vueUse = vueRules.filter(rule => rule.type === 'use').map(rule => rule.value)
|
76 |
|
77 |
|
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 |
|
90 |
|
91 |
|
92 | const vueLoaderUse = vueUse[vueLoaderUseIndex]
|
93 | vueLoaderUse.ident = 'vue-loader-options'
|
94 | vueLoaderUse.options = vueLoaderUse.options || {}
|
95 |
|
96 |
|
97 |
|
98 | const refs = new Map()
|
99 | const clonedRules = rules
|
100 | .filter(r => r !== rawVueRules)
|
101 | .map((rawRule) => cloneRule(rawRule, refs))
|
102 |
|
103 |
|
104 | delete rawVueRules.loader
|
105 | delete rawVueRules.options
|
106 | rawVueRules.use = vueUse
|
107 |
|
108 |
|
109 |
|
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 |
|
123 | compiler.options.module.rules = [
|
124 | pitcher,
|
125 | ...clonedRules,
|
126 | ...rules
|
127 | ]
|
128 | }
|
129 | }
|
130 |
|
131 | let uid = 0
|
132 | function 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 |
|
141 | .reduce((prev, next) => prev.concat(next), [])
|
142 |
|
143 |
|
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 |
|
151 | .reduce((prev, next) => prev.concat(next), [])
|
152 |
|
153 |
|
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 |
|
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 |
|
197 | VueLoaderPlugin.NS = NS
|
198 | module.exports = VueLoaderPlugin
|