Skip to content

Commit bf339d6

Browse files
jeromehanh-a-n-ahaoqunjiang
authored
feat: v15 support experimental inline match resource (#2058)
Co-authored-by: Hana <[email protected]> Co-authored-by: Haoqun Jiang <[email protected]>
1 parent 4f5727d commit bf339d6

16 files changed

+325
-89
lines changed

.github/workflows/ci.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,4 @@ jobs:
4343
run: pnpm install --no-frozen-lockfile
4444

4545
- name: Run unit tests for webpack 5
46-
run: pnpm run test
46+
run: pnpm run test && pnpm run test:match-resource

lib/codegen/customBlocks.js

+17-4
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
const qs = require('querystring')
2-
const { attrsToQuery } = require('./utils')
2+
const { attrsToQuery, genMatchResource } = require('./utils')
33

44
module.exports = function genCustomBlocksCode(
5+
loaderContext,
56
blocks,
67
resourcePath,
78
resourceQuery,
8-
stringifyRequest
9+
stringifyRequest,
10+
enableInlineMatchResource
911
) {
1012
return (
1113
`\n/* custom blocks */\n` +
@@ -17,11 +19,22 @@ module.exports = function genCustomBlocksCode(
1719
? `&issuerPath=${qs.escape(resourcePath)}`
1820
: ''
1921
const inheritQuery = resourceQuery ? `&${resourceQuery.slice(1)}` : ''
22+
const externalQuery = block.attrs.src ? `&external` : ``
2023
const query = `?vue&type=custom&index=${i}&blockType=${qs.escape(
2124
block.type
22-
)}${issuerQuery}${attrsQuery}${inheritQuery}`
25+
)}${issuerQuery}${attrsQuery}${inheritQuery}${externalQuery}`
26+
27+
let customRequest
28+
29+
if (enableInlineMatchResource) {
30+
customRequest = stringifyRequest(
31+
genMatchResource(loaderContext, src, query, block.attrs.lang)
32+
)
33+
} else {
34+
customRequest = stringifyRequest(src + query)
35+
}
2336
return (
24-
`import block${i} from ${stringifyRequest(src + query)}\n` +
37+
`import block${i} from ${customRequest}\n` +
2538
`if (typeof block${i} === 'function') block${i}(component)`
2639
)
2740
})

lib/codegen/styleInjection.js

+16-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const { attrsToQuery } = require('./utils')
1+
const { attrsToQuery, genMatchResource } = require('./utils')
22
const hotReloadAPIPath = JSON.stringify(require.resolve('vue-hot-reload-api'))
33
const nonWhitespaceRE = /\S+/
44

@@ -10,7 +10,8 @@ module.exports = function genStyleInjectionCode(
1010
stringifyRequest,
1111
needsHotReload,
1212
needsExplicitInjection,
13-
isProduction
13+
isProduction,
14+
enableInlineMatchResource
1415
) {
1516
let styleImportsCode = ``
1617
let styleInjectionCode = ``
@@ -22,13 +23,23 @@ module.exports = function genStyleInjectionCode(
2223
function genStyleRequest(style, i) {
2324
const src = style.src || resourcePath
2425
const attrsQuery = attrsToQuery(style.attrs, 'css')
25-
const inheritQuery = `&${loaderContext.resourceQuery.slice(1)}`
26+
const lang = String(style.attrs.lang || 'css')
27+
const inheritQuery = loaderContext.resourceQuery.slice(1)
28+
? `&${loaderContext.resourceQuery.slice(1)}`
29+
: ''
2630
// make sure to only pass id not src importing so that we don't inject
2731
// duplicate tags when multiple components import the same css file
2832
const idQuery = !style.src || style.scoped ? `&id=${id}` : ``
2933
const prodQuery = isProduction ? `&prod` : ``
30-
const query = `?vue&type=style&index=${i}${idQuery}${prodQuery}${attrsQuery}${inheritQuery}`
31-
return stringifyRequest(src + query)
34+
const externalQuery = style.src ? `&external` : ``
35+
const query = `?vue&type=style&index=${i}${idQuery}${prodQuery}${attrsQuery}${inheritQuery}${externalQuery}`
36+
let styleRequest
37+
if (enableInlineMatchResource) {
38+
styleRequest = stringifyRequest(genMatchResource(loaderContext, src, query, lang))
39+
} else {
40+
styleRequest = stringifyRequest(src + query)
41+
}
42+
return styleRequest
3243
}
3344

3445
function genCSSModulesCode(style, request, i) {

lib/codegen/utils.js

+28
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,31 @@ exports.attrsToQuery = (attrs, langFallback) => {
1818
}
1919
return query
2020
}
21+
22+
exports.genMatchResource = (context, resourcePath, resourceQuery, lang) => {
23+
resourceQuery = resourceQuery || ''
24+
25+
const loaders = []
26+
const parsedQuery = qs.parse(resourceQuery.slice(1))
27+
28+
// process non-external resources
29+
if ('vue' in parsedQuery && !('external' in parsedQuery)) {
30+
const currentRequest = context.loaders
31+
.slice(context.loaderIndex)
32+
.map((obj) => obj.request)
33+
loaders.push(...currentRequest)
34+
}
35+
const loaderString = loaders.join('!')
36+
37+
return `${resourcePath}${lang ? `.${lang}` : ''}${resourceQuery}!=!${
38+
loaderString ? `${loaderString}!` : ''
39+
}${resourcePath}${resourceQuery}`
40+
}
41+
42+
exports.testWebpack5 = (compiler) => {
43+
if (!compiler) {
44+
return false
45+
}
46+
const webpackVersion = compiler.webpack && compiler.webpack.version
47+
return Boolean(webpackVersion && Number(webpackVersion.split('.')[0]) > 4)
48+
}

lib/index.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ declare namespace VueLoader {
1818
cacheIdentifier?: string
1919
prettify?: boolean
2020
exposeFilename?: boolean
21+
experimentalInlineMatchResource?: boolean
2122
}
2223
}
2324

lib/index.js

+42-15
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ const qs = require('querystring')
44
const plugin = require('./plugin')
55
const selectBlock = require('./select')
66
const loaderUtils = require('loader-utils')
7-
const { attrsToQuery } = require('./codegen/utils')
7+
const {
8+
attrsToQuery,
9+
testWebpack5,
10+
genMatchResource
11+
} = require('./codegen/utils')
812
const genStylesCode = require('./codegen/styleInjection')
913
const { genHotReloadCode } = require('./codegen/hotReload')
1014
const genCustomBlocksCode = require('./codegen/customBlocks')
@@ -38,14 +42,16 @@ module.exports = function (source) {
3842
sourceMap,
3943
rootContext,
4044
resourcePath,
41-
resourceQuery = ''
45+
resourceQuery: _resourceQuery = '',
46+
_compiler
4247
} = loaderContext
43-
44-
const rawQuery = resourceQuery.slice(1)
45-
const inheritQuery = `&${rawQuery}`
48+
const isWebpack5 = testWebpack5(_compiler)
49+
const rawQuery = _resourceQuery.slice(1)
50+
const resourceQuery = rawQuery ? `&${rawQuery}` : ''
4651
const incomingQuery = qs.parse(rawQuery)
4752
const options = loaderUtils.getOptions(loaderContext) || {}
48-
53+
const enableInlineMatchResource =
54+
isWebpack5 && Boolean(options.experimentalInlineMatchResource)
4955
const isServer = target === 'node'
5056
const isShadow = !!options.shadowMode
5157
const isProduction =
@@ -111,29 +117,47 @@ module.exports = function (source) {
111117
// let isTS = false
112118
const { script, scriptSetup } = descriptor
113119
if (script || scriptSetup) {
114-
// const lang = script?.lang || scriptSetup?.lang
120+
const lang = script.lang || (scriptSetup && scriptSetup.lang)
115121
// isTS = !!(lang && /tsx?/.test(lang))
122+
const externalQuery =
123+
script && !scriptSetup && script.src ? `&external` : ``
116124
const src = (script && !scriptSetup && script.src) || resourcePath
117125
const attrsQuery = attrsToQuery((scriptSetup || script).attrs, 'js')
118-
const query = `?vue&type=script${attrsQuery}${inheritQuery}`
119-
const request = stringifyRequest(src + query)
126+
const query = `?vue&type=script${attrsQuery}${resourceQuery}${externalQuery}`
127+
128+
let scriptRequest
129+
if (enableInlineMatchResource) {
130+
scriptRequest = stringifyRequest(
131+
genMatchResource(loaderContext, src, query, lang || 'js')
132+
)
133+
} else {
134+
scriptRequest = stringifyRequest(src + query)
135+
}
120136
scriptImport =
121-
`import script from ${request}\n` + `export * from ${request}` // support named exports
137+
`import script from ${scriptRequest}\n` + `export * from ${scriptRequest}` // support named exports
122138
}
123139

124140
// template
125141
let templateImport = `var render, staticRenderFns`
126142
let templateRequest
127143
if (descriptor.template) {
128144
const src = descriptor.template.src || resourcePath
145+
const externalQuery = descriptor.template.src ? `&external` : ``
129146
const idQuery = `&id=${id}`
130147
const scopedQuery = hasScoped ? `&scoped=true` : ``
131148
const attrsQuery = attrsToQuery(descriptor.template.attrs)
132149
// const tsQuery =
133150
// options.enableTsInTemplate !== false && isTS ? `&ts=true` : ``
134-
const query = `?vue&type=template${idQuery}${scopedQuery}${attrsQuery}${inheritQuery}`
135-
const request = (templateRequest = stringifyRequest(src + query))
136-
templateImport = `import { render, staticRenderFns } from ${request}`
151+
const query = `?vue&type=template${idQuery}${scopedQuery}${attrsQuery}${resourceQuery}${externalQuery}`
152+
if (enableInlineMatchResource) {
153+
templateRequest = stringifyRequest(
154+
// TypeScript syntax in template expressions is not supported in Vue 2, so the lang is always 'js'
155+
genMatchResource(loaderContext, src, query, 'js')
156+
)
157+
} else {
158+
templateRequest = stringifyRequest(src + query)
159+
}
160+
templateImport = `import { render, staticRenderFns } from ${templateRequest}`
137161
}
138162

139163
// styles
@@ -147,7 +171,8 @@ module.exports = function (source) {
147171
stringifyRequest,
148172
needsHotReload,
149173
isServer || isShadow, // needs explicit injection?
150-
isProduction
174+
isProduction,
175+
enableInlineMatchResource
151176
)
152177
}
153178

@@ -173,10 +198,12 @@ var component = normalizer(
173198

174199
if (descriptor.customBlocks && descriptor.customBlocks.length) {
175200
code += genCustomBlocksCode(
201+
loaderContext,
176202
descriptor.customBlocks,
177203
resourcePath,
178204
resourceQuery,
179-
stringifyRequest
205+
stringifyRequest,
206+
enableInlineMatchResource
180207
)
181208
}
182209

lib/loaders/pitcher.js

+54-6
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const selfPath = require.resolve('../index')
55
const templateLoaderPath = require.resolve('./templateLoader')
66
const stylePostLoaderPath = require.resolve('./stylePostLoader')
77
const { resolveCompiler } = require('../compiler')
8+
const { testWebpack5 } = require('../codegen/utils')
89

910
const isESLintLoader = (l) => /(\/|\\|@)eslint-loader/.test(l.path)
1011
const isNullLoader = (l) => /(\/|\\|@)null-loader/.test(l.path)
@@ -53,6 +54,7 @@ module.exports.pitch = function (remainingRequest) {
5354
const options = loaderUtils.getOptions(this)
5455
const { cacheDirectory, cacheIdentifier } = options
5556
const query = qs.parse(this.resourceQuery.slice(1))
57+
const isWebpack5 = testWebpack5(this._compiler)
5658

5759
let loaders = this.loaders
5860

@@ -78,7 +80,7 @@ module.exports.pitch = function (remainingRequest) {
7880
return
7981
}
8082

81-
const genRequest = (loaders) => {
83+
const genRequest = (loaders, lang) => {
8284
// Important: dedupe since both the original rule
8385
// and the cloned rule would match a source import request.
8486
// also make sure to dedupe based on loader path.
@@ -89,6 +91,8 @@ module.exports.pitch = function (remainingRequest) {
8991
// path AND query to be safe.
9092
const seen = new Map()
9193
const loaderStrings = []
94+
const enableInlineMatchResource =
95+
isWebpack5 && options.experimentalInlineMatchResource
9296

9397
loaders.forEach((loader) => {
9498
const identifier =
@@ -101,6 +105,14 @@ module.exports.pitch = function (remainingRequest) {
101105
loaderStrings.push(request)
102106
}
103107
})
108+
if (enableInlineMatchResource) {
109+
return loaderUtils.stringifyRequest(
110+
this,
111+
`${this.resourcePath}${lang ? `.${lang}` : ''}${
112+
this.resourceQuery
113+
}!=!-!${[...loaderStrings, this.resourcePath + this.resourceQuery].join('!')}`
114+
)
115+
}
104116

105117
return loaderUtils.stringifyRequest(
106118
this,
@@ -111,15 +123,51 @@ module.exports.pitch = function (remainingRequest) {
111123

112124
// Inject style-post-loader before css-loader for scoped CSS and trimming
113125
if (query.type === `style`) {
126+
if (isWebpack5 && this._compiler.options.experiments && this._compiler.options.experiments.css) {
127+
// If user enables `experiments.css`, then we are trying to emit css code directly.
128+
// Although we can target requests like `xxx.vue?type=style` to match `type: "css"`,
129+
// it will make the plugin a mess.
130+
if (!options.experimentalInlineMatchResource) {
131+
this.emitError(
132+
new Error(
133+
'`experimentalInlineMatchResource` should be enabled if `experiments.css` enabled currently'
134+
)
135+
)
136+
return ''
137+
}
138+
139+
if (query.inline || query.module) {
140+
this.emitError(
141+
new Error(
142+
'`inline` or `module` is currently not supported with `experiments.css` enabled'
143+
)
144+
)
145+
return ''
146+
}
147+
148+
const loaderString = [stylePostLoaderPath, ...loaders]
149+
.map((loader) => {
150+
return typeof loader === 'string' ? loader : loader.request
151+
})
152+
.join('!')
153+
154+
const styleRequest = loaderUtils.stringifyRequest(
155+
this,
156+
`${this.resourcePath}${query.lang ? `.${query.lang}` : ''}${
157+
this.resourceQuery
158+
}!=!-!${loaderString}!${this.resourcePath + this.resourceQuery}`
159+
)
160+
return `@import ${styleRequest};`
161+
}
162+
114163
const cssLoaderIndex = loaders.findIndex(isCSSLoader)
115164
if (cssLoaderIndex > -1) {
116165
const afterLoaders = loaders.slice(0, cssLoaderIndex + 1)
117166
const beforeLoaders = loaders.slice(cssLoaderIndex + 1)
118-
const request = genRequest([
119-
...afterLoaders,
120-
stylePostLoaderPath,
121-
...beforeLoaders
122-
])
167+
const request = genRequest(
168+
[...afterLoaders, stylePostLoaderPath, ...beforeLoaders],
169+
query.lang || 'css'
170+
)
123171
// console.log(request)
124172
return query.module
125173
? `export { default } from ${request}; export * from ${request}`

0 commit comments

Comments
 (0)