Skip to content

Commit

Permalink
feat: support experimental inline match resource (#2046)
Browse files Browse the repository at this point in the history
  • Loading branch information
h-a-n-a committed Jun 1, 2023
1 parent 6ad8056 commit 3149f6d
Show file tree
Hide file tree
Showing 15 changed files with 335 additions and 69 deletions.
12 changes: 12 additions & 0 deletions .github/workflows/ci.yml
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
Expand Up @@ -4,6 +4,10 @@
- [Documentation](https://vue-loader.vuejs.org)

## v17.1+ 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
@@ -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
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",
"dev-example": "node example/devServer.js --config example/webpack.config.js --inline --hot",
Expand Down
85 changes: 76 additions & 9 deletions src/index.ts
Expand Up @@ -20,7 +20,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 @@ -51,6 +56,7 @@ export interface VueLoaderOptions {
exposeFilename?: boolean
appendExtension?: boolean
enableTsInTemplate?: boolean
experimentalInlineMatchResource?: boolean

isServerBuild?: boolean
}
Expand Down Expand Up @@ -90,18 +96,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 @@ -167,10 +178,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 @@ -184,13 +208,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 @@ -205,12 +243,23 @@ 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(
Expand Down Expand Up @@ -283,9 +332,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
66 changes: 61 additions & 5 deletions src/pitcher.ts
@@ -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,12 +144,28 @@ function genProxyModule(

function genRequest(
loaders: (Loader | string)[],
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('!')
Expand Down
23 changes: 15 additions & 8 deletions src/plugin.ts
@@ -1,19 +1,26 @@
import webpack from 'webpack'
import type { Compiler } from 'webpack'
import { testWebpack5 } from './util'

declare class VueLoaderPlugin {
static NS: string
apply(compiler: Compiler): void
}

let Plugin: typeof VueLoaderPlugin
const NS = 'vue-loader'

if (webpack.version && webpack.version[0] > '4') {
// webpack5 and upper
Plugin = require('./pluginWebpack5').default
} else {
// webpack4 and lower
Plugin = require('./pluginWebpack4').default
class Plugin {
static NS = NS
apply(compiler: Compiler) {
let Ctor: typeof VueLoaderPlugin
if (testWebpack5(compiler)) {
// webpack5 and upper
Ctor = require('./pluginWebpack5').default
} else {
// webpack4 and lower
Ctor = require('./pluginWebpack4').default
}
new Ctor().apply(compiler)
}
}

export default Plugin

0 comments on commit 3149f6d

Please sign in to comment.