Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: v15 support experimental inline match resource #2058

Merged
merged 10 commits into from
Oct 18, 2023
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,4 @@ jobs:
run: pnpm install --no-frozen-lockfile

- name: Run unit tests for webpack 5
run: pnpm run test
run: pnpm run test && pnpm run test:match-resource
21 changes: 17 additions & 4 deletions lib/codegen/customBlocks.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
const qs = require('querystring')
const { attrsToQuery } = require('./utils')
const { attrsToQuery, genMatchResource } = require('./utils')

module.exports = function genCustomBlocksCode(
loaderContext,
blocks,
resourcePath,
resourceQuery,
stringifyRequest
stringifyRequest,
enableInlineMatchResource
) {
return (
`\n/* custom blocks */\n` +
Expand All @@ -17,11 +19,22 @@ module.exports = function genCustomBlocksCode(
? `&issuerPath=${qs.escape(resourcePath)}`
: ''
const inheritQuery = resourceQuery ? `&${resourceQuery.slice(1)}` : ''
const externalQuery = block.attrs.src ? `&external` : ``
const query = `?vue&type=custom&index=${i}&blockType=${qs.escape(
block.type
)}${issuerQuery}${attrsQuery}${inheritQuery}`
)}${issuerQuery}${attrsQuery}${inheritQuery}${externalQuery}`

let customRequest

if (enableInlineMatchResource) {
customRequest = stringifyRequest(
genMatchResource(loaderContext, src, query, block.attrs.lang)
)
} else {
customRequest = stringifyRequest(src + query)
}
return (
`import block${i} from ${stringifyRequest(src + query)}\n` +
`import block${i} from ${customRequest}\n` +
`if (typeof block${i} === 'function') block${i}(component)`
)
})
Expand Down
21 changes: 16 additions & 5 deletions lib/codegen/styleInjection.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const { attrsToQuery } = require('./utils')
const { attrsToQuery, genMatchResource } = require('./utils')
const hotReloadAPIPath = JSON.stringify(require.resolve('vue-hot-reload-api'))
const nonWhitespaceRE = /\S+/

Expand All @@ -10,7 +10,8 @@ module.exports = function genStyleInjectionCode(
stringifyRequest,
needsHotReload,
needsExplicitInjection,
isProduction
isProduction,
enableInlineMatchResource
) {
let styleImportsCode = ``
let styleInjectionCode = ``
Expand All @@ -22,13 +23,23 @@ module.exports = function genStyleInjectionCode(
function genStyleRequest(style, i) {
const src = style.src || resourcePath
const attrsQuery = attrsToQuery(style.attrs, 'css')
const inheritQuery = `&${loaderContext.resourceQuery.slice(1)}`
const lang = String(style.attrs.lang || 'css')
const inheritQuery = loaderContext.resourceQuery.slice(1)
? `&${loaderContext.resourceQuery.slice(1)}`
: ''
// make sure to only pass id not src importing so that we don't inject
// duplicate tags when multiple components import the same css file
const idQuery = !style.src || style.scoped ? `&id=${id}` : ``
const prodQuery = isProduction ? `&prod` : ``
const query = `?vue&type=style&index=${i}${idQuery}${prodQuery}${attrsQuery}${inheritQuery}`
return stringifyRequest(src + query)
const externalQuery = style.src ? `&external` : ``
const query = `?vue&type=style&index=${i}${idQuery}${prodQuery}${attrsQuery}${inheritQuery}${externalQuery}`
let styleRequest
if (enableInlineMatchResource) {
styleRequest = stringifyRequest(genMatchResource(loaderContext, src, query, lang))
} else {
styleRequest = stringifyRequest(src + query)
}
return styleRequest
}

function genCSSModulesCode(style, request, i) {
Expand Down
28 changes: 28 additions & 0 deletions lib/codegen/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,31 @@ exports.attrsToQuery = (attrs, langFallback) => {
}
return query
}

exports.genMatchResource = (context, resourcePath, resourceQuery, lang) => {
resourceQuery = resourceQuery || ''

const loaders = []
const parsedQuery = qs.parse(resourceQuery.slice(1))

// process non-external resources
if ('vue' in parsedQuery && !('external' in parsedQuery)) {
const currentRequest = context.loaders
.slice(context.loaderIndex)
.map((obj) => obj.request)
loaders.push(...currentRequest)
}
const loaderString = loaders.join('!')

return `${resourcePath}${lang ? `.${lang}` : ''}${resourceQuery}!=!${
loaderString ? `${loaderString}!` : ''
}${resourcePath}${resourceQuery}`
}

exports.testWebpack5 = (compiler) => {
if (!compiler) {
return false
}
const webpackVersion = compiler.webpack && compiler.webpack.version
return Boolean(webpackVersion && Number(webpackVersion.split('.')[0]) > 4)
}
1 change: 1 addition & 0 deletions lib/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ declare namespace VueLoader {
cacheIdentifier?: string
prettify?: boolean
exposeFilename?: boolean
experimentalInlineMatchResource?: boolean
}
}

Expand Down
57 changes: 42 additions & 15 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ const qs = require('querystring')
const plugin = require('./plugin')
const selectBlock = require('./select')
const loaderUtils = require('loader-utils')
const { attrsToQuery } = require('./codegen/utils')
const {
attrsToQuery,
testWebpack5,
genMatchResource
} = require('./codegen/utils')
const genStylesCode = require('./codegen/styleInjection')
const { genHotReloadCode } = require('./codegen/hotReload')
const genCustomBlocksCode = require('./codegen/customBlocks')
Expand Down Expand Up @@ -38,14 +42,16 @@ module.exports = function (source) {
sourceMap,
rootContext,
resourcePath,
resourceQuery = ''
resourceQuery: _resourceQuery = '',
_compiler
} = loaderContext

const rawQuery = resourceQuery.slice(1)
const inheritQuery = `&${rawQuery}`
const isWebpack5 = testWebpack5(_compiler)
const rawQuery = _resourceQuery.slice(1)
const resourceQuery = rawQuery ? `&${rawQuery}` : ''
const incomingQuery = qs.parse(rawQuery)
const options = loaderUtils.getOptions(loaderContext) || {}

const enableInlineMatchResource =
isWebpack5 && Boolean(options.experimentalInlineMatchResource)
const isServer = target === 'node'
const isShadow = !!options.shadowMode
const isProduction =
Expand Down Expand Up @@ -111,29 +117,47 @@ module.exports = function (source) {
// let isTS = false
const { script, scriptSetup } = descriptor
if (script || scriptSetup) {
// const lang = script?.lang || scriptSetup?.lang
const lang = script.lang || (scriptSetup && scriptSetup.lang)
// isTS = !!(lang && /tsx?/.test(lang))
const externalQuery =
script && !scriptSetup && script.src ? `&external` : ``
const src = (script && !scriptSetup && script.src) || resourcePath
const attrsQuery = attrsToQuery((scriptSetup || script).attrs, 'js')
const query = `?vue&type=script${attrsQuery}${inheritQuery}`
const request = stringifyRequest(src + query)
const query = `?vue&type=script${attrsQuery}${resourceQuery}${externalQuery}`

let scriptRequest
if (enableInlineMatchResource) {
scriptRequest = stringifyRequest(
genMatchResource(loaderContext, src, query, lang || 'js')
)
} else {
scriptRequest = stringifyRequest(src + query)
}
scriptImport =
`import script from ${request}\n` + `export * from ${request}` // support named exports
`import script from ${scriptRequest}\n` + `export * from ${scriptRequest}` // support named exports
}

// template
let templateImport = `var render, staticRenderFns`
let templateRequest
if (descriptor.template) {
const src = descriptor.template.src || resourcePath
const externalQuery = descriptor.template.src ? `&external` : ``
const idQuery = `&id=${id}`
const scopedQuery = hasScoped ? `&scoped=true` : ``
const attrsQuery = attrsToQuery(descriptor.template.attrs)
// const tsQuery =
// options.enableTsInTemplate !== false && isTS ? `&ts=true` : ``
const query = `?vue&type=template${idQuery}${scopedQuery}${attrsQuery}${inheritQuery}`
const request = (templateRequest = stringifyRequest(src + query))
templateImport = `import { render, staticRenderFns } from ${request}`
const query = `?vue&type=template${idQuery}${scopedQuery}${attrsQuery}${resourceQuery}${externalQuery}`
if (enableInlineMatchResource) {
templateRequest = stringifyRequest(
// TypeScript syntax in template expressions is not supported in Vue 2, so the lang is always 'js'
genMatchResource(loaderContext, src, query, 'js')
)
} else {
templateRequest = stringifyRequest(src + query)
}
templateImport = `import { render, staticRenderFns } from ${templateRequest}`
}

// styles
Expand All @@ -147,7 +171,8 @@ module.exports = function (source) {
stringifyRequest,
needsHotReload,
isServer || isShadow, // needs explicit injection?
isProduction
isProduction,
enableInlineMatchResource
)
}

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

if (descriptor.customBlocks && descriptor.customBlocks.length) {
code += genCustomBlocksCode(
loaderContext,
descriptor.customBlocks,
resourcePath,
resourceQuery,
stringifyRequest
stringifyRequest,
enableInlineMatchResource
)
}

Expand Down
60 changes: 54 additions & 6 deletions lib/loaders/pitcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const selfPath = require.resolve('../index')
const templateLoaderPath = require.resolve('./templateLoader')
const stylePostLoaderPath = require.resolve('./stylePostLoader')
const { resolveCompiler } = require('../compiler')
const { testWebpack5 } = require('../codegen/utils')

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

let loaders = this.loaders

Expand All @@ -78,7 +80,7 @@ module.exports.pitch = function (remainingRequest) {
return
}

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

loaders.forEach((loader) => {
const identifier =
Expand All @@ -101,6 +105,14 @@ module.exports.pitch = function (remainingRequest) {
loaderStrings.push(request)
}
})
if (enableInlineMatchResource) {
return loaderUtils.stringifyRequest(
this,
`${this.resourcePath}${lang ? `.${lang}` : ''}${
this.resourceQuery
}!=!-!${[...loaderStrings, this.resourcePath + this.resourceQuery].join('!')}`
)
}

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

// Inject style-post-loader before css-loader for scoped CSS and trimming
if (query.type === `style`) {
if (isWebpack5 && this._compiler.options.experiments && this._compiler.options.experiments.css) {
// If user enables `experiments.css`, then we are trying to emit css code directly.
// Although we can target requests like `xxx.vue?type=style` to match `type: "css"`,
// it will make the plugin a mess.
if (!options.experimentalInlineMatchResource) {
this.emitError(
new Error(
'`experimentalInlineMatchResource` should be enabled if `experiments.css` enabled currently'
)
)
return ''
}

if (query.inline || query.module) {
this.emitError(
new Error(
'`inline` or `module` is currently not supported with `experiments.css` enabled'
)
)
return ''
}

const loaderString = [stylePostLoaderPath, ...loaders]
.map((loader) => {
return typeof loader === 'string' ? loader : loader.request
})
.join('!')

const styleRequest = loaderUtils.stringifyRequest(
this,
`${this.resourcePath}${query.lang ? `.${query.lang}` : ''}${
this.resourceQuery
}!=!-!${loaderString}!${this.resourcePath + this.resourceQuery}`
)
return `@import ${styleRequest};`
}

const cssLoaderIndex = loaders.findIndex(isCSSLoader)
if (cssLoaderIndex > -1) {
const afterLoaders = loaders.slice(0, cssLoaderIndex + 1)
const beforeLoaders = loaders.slice(cssLoaderIndex + 1)
const request = genRequest([
...afterLoaders,
stylePostLoaderPath,
...beforeLoaders
])
const request = genRequest(
[...afterLoaders, stylePostLoaderPath, ...beforeLoaders],
query.lang || 'css'
)
// console.log(request)
return query.module
? `export { default } from ${request}; export * from ${request}`
Expand Down