Skip to content

Commit

Permalink
feat: support experimental inline match resource (vuejs#2046)
Browse files Browse the repository at this point in the history
  • Loading branch information
h-a-n-a authored and freddy38510 committed Sep 6, 2023
1 parent c1e6267 commit cd07b1c
Show file tree
Hide file tree
Showing 15 changed files with 382 additions and 122 deletions.
12 changes: 12 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,15 @@ jobs:
cache: 'yarn'
- run: yarn install
- run: yarn test

test-webpack5-inline-match-resource:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set node version to 16
uses: actions/setup-node@v2
with:
node-version: 16
cache: 'yarn'
- run: yarn install
- run: yarn test:match-resource
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
- [Documentation](https://vue-loader.vuejs.org)

## v17.3+ Only Options

- `experimentalInlineMatchResource: boolean`: enable [Inline matchResource](https://webpack.js.org/api/loaders/#inline-matchresource) for rule matching for vue-loader.

## v16+ Only Options

- `reactivityTransform: boolean`: enable [Vue Reactivity Transform](https://github.com/vuejs/rfcs/discussions/369) (SFCs only).
Expand Down
10 changes: 9 additions & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
console.log(`running tests with webpack ${process.env.WEBPACK4 ? '4' : '5'}...`)
const isWebpack4 = process.env.WEBPACK4

console.log(
`running tests with webpack ${isWebpack4 ? '4' : '5'}${
!isWebpack4 && process.env.INLINE_MATCH_RESOURCE
? ' with inline match resource enabled'
: ''
}...`,
)

module.exports = {
preset: 'ts-jest',
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
"build": "tsc",
"pretest": "tsc",
"test": "jest",
"pretest:match-resource": "tsc",
"test:match-resource": "INLINE_MATCH_RESOURCE=true jest",
"pretest:webpack4": "tsc",
"test:webpack4": "WEBPACK4=true jest --forceExit",
"dev-example": "node example/devServer.js --config example/webpack.config.js --inline --hot",
Expand Down
104 changes: 86 additions & 18 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@ import { formatError } from './formatError'
import VueLoaderPlugin from './plugin'
import { canInlineTemplate } from './resolveScript'
import { setDescriptor } from './descriptorCache'
import { getOptions, stringifyRequest as _stringifyRequest } from './util'
import {
getOptions,
stringifyRequest as _stringifyRequest,
genMatchResource,
testWebpack5,
} from './util'

export { VueLoaderPlugin }

Expand Down Expand Up @@ -53,6 +58,7 @@ export interface VueLoaderOptions {
exposeFilename?: boolean
appendExtension?: boolean
enableTsInTemplate?: boolean
experimentalInlineMatchResource?: boolean

isServerBuild?: boolean
}
Expand All @@ -64,7 +70,7 @@ const exportHelperPath = require.resolve('./exportHelper')

export default function loader(
this: LoaderContext<VueLoaderOptions>,
source: string
source: string,
) {
const loaderContext = this

Expand All @@ -77,8 +83,8 @@ export default function loader(
loaderContext.emitError(
new Error(
`vue-loader was used without the corresponding plugin. ` +
`Make sure to include VueLoaderPlugin in your webpack config.`
)
`Make sure to include VueLoaderPlugin in your webpack config.`,
),
)
errorEmitted = true
}
Expand All @@ -92,18 +98,23 @@ export default function loader(
rootContext,
resourcePath,
resourceQuery: _resourceQuery = '',
_compiler,
} = loaderContext

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

const isServer = options.isServerBuild ?? target === 'node'
const isProduction =
mode === 'production' || process.env.NODE_ENV === 'production'

const filename = resourcePath.replace(/\?.*$/, '')

const { descriptor, errors } = parse(source, {
filename,
sourceMap,
Expand Down Expand Up @@ -133,7 +144,7 @@ export default function loader(
const id = hash(
isProduction
? shortFilePath + '\n' + source.replace(/\r\n/g, '\n')
: shortFilePath
: shortFilePath,
)

// if the query has a type field, this is a language block request
Expand All @@ -146,7 +157,7 @@ export default function loader(
options,
loaderContext,
incomingQuery,
!!options.appendExtension
!!options.appendExtension,
)
}

Expand All @@ -169,10 +180,23 @@ export default function loader(
if (script || scriptSetup) {
const lang = script?.lang || scriptSetup?.lang
isTS = !!(lang && /tsx?/.test(lang))
const externalQuery = Boolean(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}${resourceQuery}`
const scriptRequest = stringifyRequest(src + query)
const query = `?vue&type=script${attrsQuery}${resourceQuery}${externalQuery}`

let scriptRequest: string

if (enableInlineMatchResource) {
scriptRequest = stringifyRequest(
genMatchResource(this, src, query, lang || 'js'),
)
} else {
scriptRequest = stringifyRequest(src + query)
}

scriptImport =
`import script from ${scriptRequest}\n` +
// support named exports
Expand All @@ -186,13 +210,27 @@ export default function loader(
const useInlineTemplate = canInlineTemplate(descriptor, isProduction)
if (descriptor.template && !useInlineTemplate) {
const src = descriptor.template.src || resourcePath
const externalQuery = Boolean(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}${tsQuery}${attrsQuery}${resourceQuery}`
templateRequest = stringifyRequest(src + query)
const query = `?vue&type=template${idQuery}${scopedQuery}${tsQuery}${attrsQuery}${resourceQuery}${externalQuery}`

if (enableInlineMatchResource) {
templateRequest = stringifyRequest(
genMatchResource(
this,
src,
query,
options.enableTsInTemplate !== false && isTS ? 'ts' : 'js',
),
)
} else {
templateRequest = stringifyRequest(src + query)
}

templateImport = `import { ${renderFnName} } from ${templateRequest}`
propsToAttach.push([renderFnName, renderFnName])
}
Expand All @@ -209,19 +247,29 @@ export default function loader(
.forEach((style, i) => {
const src = style.src || resourcePath
const attrsQuery = attrsToQuery(style.attrs, 'css')
const lang = String(style.attrs.lang || 'css')
// make sure to only pass id when necessary 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 inlineQuery = asCustomElement ? `&inline` : ``
const query = `?vue&type=style&index=${i}${idQuery}${inlineQuery}${attrsQuery}${resourceQuery}`
const styleRequest = stringifyRequest(src + query)
const externalQuery = Boolean(style.src) ? `&external` : ``
const query = `?vue&type=style&index=${i}${idQuery}${inlineQuery}${attrsQuery}${resourceQuery}${externalQuery}`

let styleRequest
if (enableInlineMatchResource) {
styleRequest = stringifyRequest(
genMatchResource(this, src, query, lang),
)
} else {
styleRequest = stringifyRequest(src + query)
}

if (style.module) {
if (asCustomElement) {
loaderContext.emitError(
new Error(
`<style module> is not supported in custom element mode.`
)
`<style module> is not supported in custom element mode.`,
),
)
}
if (!hasCSSModules) {
Expand All @@ -234,7 +282,7 @@ export default function loader(
i,
styleRequest,
style.module,
needsHotReload
needsHotReload,
)
} else {
if (!isServer) {
Expand Down Expand Up @@ -300,9 +348,27 @@ export default function loader(
const issuerQuery = block.attrs.src
? `&issuerPath=${qs.escape(resourcePath)}`
: ''
const query = `?vue&type=custom&index=${i}${blockTypeQuery}${issuerQuery}${attrsQuery}${resourceQuery}`

const externalQuery = Boolean(block.attrs.src) ? `&external` : ``
const query = `?vue&type=custom&index=${i}${blockTypeQuery}${issuerQuery}${attrsQuery}${resourceQuery}${externalQuery}`

let customRequest

if (enableInlineMatchResource) {
customRequest = stringifyRequest(
genMatchResource(
this,
src as string,
query,
block.attrs.lang as string,
),
)
} 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}(script)`
)
})
Expand Down Expand Up @@ -332,7 +398,9 @@ export default function loader(
if (!propsToAttach.length) {
code += `\n\nconst __exports__ = script;`
} else {
code += `\n\nimport exportComponent from ${stringifyRequest(exportHelperPath)}`
code += `\n\nimport exportComponent from ${stringifyRequest(
exportHelperPath,
)}`
code += `\nconst __exports__ = /*#__PURE__*/exportComponent(script, [${propsToAttach
.map(([key, val]) => `['${key}',${val}]`)
.join(',')}])`
Expand Down
70 changes: 63 additions & 7 deletions src/pitcher.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { LoaderDefinitionFunction, LoaderContext } from 'webpack'
import * as qs from 'querystring'
import { stringifyRequest } from './util'
import { getOptions, stringifyRequest, testWebpack5 } from './util'
import { VueLoaderOptions } from '.'

const selfPath = require.resolve('./index')
Expand Down Expand Up @@ -58,7 +58,40 @@ export const pitch = function () {
})

// Inject style-post-loader before css-loader for scoped CSS and trimming
const isWebpack5 = testWebpack5(context._compiler)
const options = (getOptions(context) || {}) as VueLoaderOptions
if (query.type === `style`) {
if (isWebpack5 && context._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) {
context.emitError(
new Error(
'`experimentalInlineMatchResource` should be enabled if `experiments.css` enabled currently',
),
)
return ''
}

if (query.inline || query.module) {
context.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('!')
return `@import "${context.resourcePath}${
query.lang ? `.${query.lang}` : ''
}${context.resourceQuery}!=!-!${loaderString}!${context.resource}";`
}
const cssLoaderIndex = loaders.findIndex(isCSSLoader)
if (cssLoaderIndex > -1) {
// if inlined, ignore any loaders after css-loader and replace w/ inline
Expand All @@ -71,7 +104,8 @@ export const pitch = function () {
return genProxyModule(
[...afterLoaders, stylePostLoaderPath, ...beforeLoaders],
context,
!!query.module || query.inline != null
!!query.module || query.inline != null,
(query.lang as string) || 'css',
)
}
}
Expand All @@ -84,15 +118,21 @@ export const pitch = function () {

// Rewrite request. Technically this should only be done when we have deduped
// loaders. But somehow this is required for block source maps to work.
return genProxyModule(loaders, context, query.type !== 'template')
return genProxyModule(
loaders,
context,
query.type !== 'template',
query.ts ? 'ts' : (query.lang as string),
)
}

function genProxyModule(
loaders: (Loader | string)[],
context: LoaderContext<VueLoaderOptions>,
exportDefault = true
exportDefault = true,
lang = 'js',
) {
const request = genRequest(loaders, context)
const request = genRequest(loaders, lang, context)
// return a proxy module which simply re-exports everything from the
// actual request. Note for template blocks the compiled module has no
// default export.
Expand All @@ -104,15 +144,31 @@ function genProxyModule(

function genRequest(
loaders: (Loader | string)[],
context: LoaderContext<VueLoaderOptions>
lang: string,
context: LoaderContext<VueLoaderOptions>,
) {
const isWebpack5 = testWebpack5(context._compiler)
const options = (getOptions(context) || {}) as VueLoaderOptions
const enableInlineMatchResource =
isWebpack5 && options.experimentalInlineMatchResource

const loaderStrings = loaders.map((loader) => {
return typeof loader === 'string' ? loader : loader.request
})
const resource = context.resourcePath + context.resourceQuery

if (enableInlineMatchResource) {
return stringifyRequest(
context,
`${context.resourcePath}${lang ? `.${lang}` : ''}${
context.resourceQuery
}!=!-!${[...loaderStrings, resource].join('!')}`,
)
}

return stringifyRequest(
context,
'-!' + [...loaderStrings, resource].join('!')
'-!' + [...loaderStrings, resource].join('!'),
)
}

Expand Down
Loading

0 comments on commit cd07b1c

Please sign in to comment.