Skip to content

Commit 59eebca

Browse files
18566246732haoqunjiang
authored andcommitted
feat: add support for webpack5 (#1613)
1 parent 92c02cb commit 59eebca

File tree

3 files changed

+365
-160
lines changed

3 files changed

+365
-160
lines changed

Diff for: lib/plugin-webpack4.js

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

Diff for: lib/plugin-webpack5.js

+193
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
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+
// 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+
131+
function cloneRule (rawRule, refs) {
132+
const rules = ruleSetCompiler.compileRules('ruleSet', [{
133+
rules: [rawRule]
134+
}], refs)
135+
let currentResource
136+
137+
const conditions = rules[0].rules
138+
.map(rule => rule.conditions)
139+
// shallow flat
140+
.reduce((prev, next) => prev.concat(next), [])
141+
142+
// do not process rule with enforce
143+
if (!rawRule.enforce) {
144+
const ruleUse = rules[0].rules
145+
.map(rule => rule.effects
146+
.filter(effect => effect.type === 'use')
147+
.map(effect => effect.value)
148+
)
149+
// shallow flat
150+
.reduce((prev, next) => prev.concat(next), [])
151+
152+
// fix conflict with config.loader and config.options when using config.use
153+
delete rawRule.loader
154+
delete rawRule.options
155+
rawRule.use = ruleUse
156+
}
157+
158+
const res = Object.assign({}, rawRule, {
159+
resource: resources => {
160+
currentResource = resources
161+
return true
162+
},
163+
resourceQuery: query => {
164+
const parsed = qs.parse(query.slice(1))
165+
if (parsed.vue == null) {
166+
return false
167+
}
168+
if (!conditions) {
169+
return false
170+
}
171+
const fakeResourcePath = `${currentResource}.${parsed.lang}`
172+
for (const condition of conditions) {
173+
// add support for resourceQuery
174+
const request = condition.property === 'resourceQuery' ? query : fakeResourcePath
175+
if (condition && !condition.fn(request)) {
176+
return false
177+
}
178+
}
179+
return true
180+
}
181+
})
182+
183+
delete res.test
184+
185+
if (rawRule.oneOf) {
186+
res.oneOf = rawRule.oneOf.map(rule => cloneRule(rule, refs))
187+
}
188+
189+
return res
190+
}
191+
192+
VueLoaderPlugin.NS = NS
193+
module.exports = VueLoaderPlugin

0 commit comments

Comments
 (0)