Skip to content

Commit

Permalink
feat: v15 support experimental inline match resource (#2058)
Browse files Browse the repository at this point in the history
Co-authored-by: Hana <andywangsy@gmail.com>
Co-authored-by: Haoqun Jiang <haoqunjiang@gmail.com>
  • Loading branch information
3 people committed Oct 18, 2023
1 parent 4f5727d commit bf339d6
Show file tree
Hide file tree
Showing 16 changed files with 325 additions and 89 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
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
@@ -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
@@ -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
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
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
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
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

0 comments on commit bf339d6

Please sign in to comment.