From c1bbac92eb1dedbec61a6330cec83015f794664b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Oct 2025 07:48:02 +0000 Subject: [PATCH 1/5] Initial plan From ebcc684bfe6e0d9a55260d04aa59af926a04e061 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Oct 2025 08:02:31 +0000 Subject: [PATCH 2/5] Use plugin hook filter internally - plugin-react: Remove redundant filter check since hook filter handles it - plugin-react-swc: Add hook filters for default file extensions (.tsx, .ts, .mts, .jsx, .mdx) - Conditional filter application: Only use filters when parserConfig is not custom Co-authored-by: hi-ogawa <4232207+hi-ogawa@users.noreply.github.com> --- packages/plugin-react-swc/src/index.ts | 128 ++++++++++++++++++------- packages/plugin-react/src/index.ts | 3 - 2 files changed, 91 insertions(+), 40 deletions(-) diff --git a/packages/plugin-react-swc/src/index.ts b/packages/plugin-react-swc/src/index.ts index 0a83a329..90d81ae9 100644 --- a/packages/plugin-react-swc/src/index.ts +++ b/packages/plugin-react-swc/src/index.ts @@ -169,34 +169,69 @@ const react = (_options?: Options): Plugin[] => { ] } }, - async transform(code, _id, transformOptions) { - const id = _id.split('?')[0] - const refresh = !transformOptions?.ssr && !hmrDisabled + transform: options.parserConfig + ? // When custom parserConfig is provided, we can't add a filter + // because the user controls which files are handled + async (code, _id, transformOptions) => { + const id = _id.split('?')[0] + const refresh = !transformOptions?.ssr && !hmrDisabled - const result = await transformWithOptions( - id, - code, - options.devTarget, - options, - viteCacheRoot, - { - refresh, - development: true, - runtime: 'automatic', - importSource: options.jsxImportSource, - }, - ) - if (!result) return - if (!refresh) return result + const result = await transformWithOptions( + id, + code, + options.devTarget, + options, + viteCacheRoot, + { + refresh, + development: true, + runtime: 'automatic', + importSource: options.jsxImportSource, + }, + ) + if (!result) return + if (!refresh) return result - const newCode = addRefreshWrapper( - result.code, - '@vitejs/plugin-react-swc', - id, - options.reactRefreshHost, - ) - return { code: newCode ?? result.code, map: result.map } - }, + const newCode = addRefreshWrapper( + result.code, + '@vitejs/plugin-react-swc', + id, + options.reactRefreshHost, + ) + return { code: newCode ?? result.code, map: result.map } + } + : { + // Add filter for default extensions: .tsx, .ts, .mts, .jsx, .mdx + filter: { id: /\.(tsx?|mts|jsx|mdx)(?:$|\?)/ }, + async handler(code, _id, transformOptions) { + const id = _id.split('?')[0] + const refresh = !transformOptions?.ssr && !hmrDisabled + + const result = await transformWithOptions( + id, + code, + options.devTarget, + options, + viteCacheRoot, + { + refresh, + development: true, + runtime: 'automatic', + importSource: options.jsxImportSource, + }, + ) + if (!result) return + if (!refresh) return result + + const newCode = addRefreshWrapper( + result.code, + '@vitejs/plugin-react-swc', + id, + options.reactRefreshHost, + ) + return { code: newCode ?? result.code, map: result.map } + }, + }, }, options.plugins || options.useAtYourOwnRisk_mutateSwcOptions ? { @@ -209,18 +244,37 @@ const react = (_options?: Options): Plugin[] => { configResolved(config) { viteCacheRoot = config.cacheDir }, - transform: (code, _id) => - transformWithOptions( - _id.split('?')[0], - code, - 'esnext', - options, - viteCacheRoot, - { - runtime: 'automatic', - importSource: options.jsxImportSource, + transform: options.parserConfig + ? // When custom parserConfig is provided, we can't add a filter + // because the user controls which files are handled + (code, _id) => + transformWithOptions( + _id.split('?')[0], + code, + 'esnext', + options, + viteCacheRoot, + { + runtime: 'automatic', + importSource: options.jsxImportSource, + }, + ) + : { + // Add filter for default extensions: .tsx, .ts, .mts, .jsx, .mdx + filter: { id: /\.(tsx?|mts|jsx|mdx)(?:$|\?)/ }, + handler: (code, _id) => + transformWithOptions( + _id.split('?')[0], + code, + 'esnext', + options, + viteCacheRoot, + { + runtime: 'automatic', + importSource: options.jsxImportSource, + }, + ), }, - ), } : { name: 'vite:react-swc', diff --git a/packages/plugin-react/src/index.ts b/packages/plugin-react/src/index.ts index f96d26cf..9f9893b5 100644 --- a/packages/plugin-react/src/index.ts +++ b/packages/plugin-react/src/index.ts @@ -3,7 +3,6 @@ import { fileURLToPath } from 'node:url' import { readFileSync } from 'node:fs' import type * as babelCore from '@babel/core' import type { ParserOptions, TransformOptions } from '@babel/core' -import { createFilter } from 'vite' import * as vite from 'vite' import type { Plugin, ResolvedConfig } from 'vite' import { @@ -107,7 +106,6 @@ const compilerAnnotationRE = /['"]use memo['"]/ export default function viteReact(opts: Options = {}): Plugin[] { const include = opts.include ?? defaultIncludeRE const exclude = opts.exclude ?? defaultExcludeRE - const filter = createFilter(include, exclude) const jsxImportSource = opts.jsxImportSource ?? 'react' const jsxImportRuntime = `${jsxImportSource}/jsx-runtime` @@ -247,7 +245,6 @@ export default function viteReact(opts: Options = {}): Plugin[] { }, async handler(code, id, options) { const [filepath] = id.split('?') - if (!filter(filepath)) return const ssr = options?.ssr === true const babelOptions = (() => { From c2fc9a379bd1924d73980d76fc0ce56ca977ad16 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Oct 2025 08:05:20 +0000 Subject: [PATCH 3/5] Make regex patterns more explicit for better readability Change tsx? to (tsx|ts) to make it clearer that both extensions are matched Co-authored-by: hi-ogawa <4232207+hi-ogawa@users.noreply.github.com> --- packages/plugin-react-swc/src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/plugin-react-swc/src/index.ts b/packages/plugin-react-swc/src/index.ts index 90d81ae9..466ef6bf 100644 --- a/packages/plugin-react-swc/src/index.ts +++ b/packages/plugin-react-swc/src/index.ts @@ -202,7 +202,7 @@ const react = (_options?: Options): Plugin[] => { } : { // Add filter for default extensions: .tsx, .ts, .mts, .jsx, .mdx - filter: { id: /\.(tsx?|mts|jsx|mdx)(?:$|\?)/ }, + filter: { id: /\.(tsx|ts|mts|jsx|mdx)(?:$|\?)/ }, async handler(code, _id, transformOptions) { const id = _id.split('?')[0] const refresh = !transformOptions?.ssr && !hmrDisabled @@ -261,7 +261,7 @@ const react = (_options?: Options): Plugin[] => { ) : { // Add filter for default extensions: .tsx, .ts, .mts, .jsx, .mdx - filter: { id: /\.(tsx?|mts|jsx|mdx)(?:$|\?)/ }, + filter: { id: /\.(tsx|ts|mts|jsx|mdx)(?:$|\?)/ }, handler: (code, _id) => transformWithOptions( _id.split('?')[0], From b1d3676984106695c8be54d6a8b11f73a4d51a80 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Oct 2025 08:19:44 +0000 Subject: [PATCH 4/5] Update plugin-rsc to use plugin hook filters internally - Revert changes to plugin-react and plugin-react-swc (per review feedback) - Add @rolldown/pluginutils dependency to plugin-rsc - Add hook filters to transform hooks: rsc:vite-client-raw-import, rsc:inject-async-local-storage, rsc:rsc-css-self-accept - Add hook filters to resolveId hook: rsc:react-server-dom-webpack-alias - Add hook filters to load hooks: virtual modules for assets-manifest and client-references - Remove redundant conditional checks where hook filters now handle the filtering Co-authored-by: hi-ogawa <4232207+hi-ogawa@users.noreply.github.com> --- packages/plugin-react-swc/src/index.ts | 128 ++++++--------------- packages/plugin-react/src/index.ts | 3 + packages/plugin-rsc/package.json | 1 + packages/plugin-rsc/src/plugin.ts | 149 +++++++++++++------------ pnpm-lock.yaml | 3 + 5 files changed, 122 insertions(+), 162 deletions(-) diff --git a/packages/plugin-react-swc/src/index.ts b/packages/plugin-react-swc/src/index.ts index 466ef6bf..0a83a329 100644 --- a/packages/plugin-react-swc/src/index.ts +++ b/packages/plugin-react-swc/src/index.ts @@ -169,69 +169,34 @@ const react = (_options?: Options): Plugin[] => { ] } }, - transform: options.parserConfig - ? // When custom parserConfig is provided, we can't add a filter - // because the user controls which files are handled - async (code, _id, transformOptions) => { - const id = _id.split('?')[0] - const refresh = !transformOptions?.ssr && !hmrDisabled + async transform(code, _id, transformOptions) { + const id = _id.split('?')[0] + const refresh = !transformOptions?.ssr && !hmrDisabled - const result = await transformWithOptions( - id, - code, - options.devTarget, - options, - viteCacheRoot, - { - refresh, - development: true, - runtime: 'automatic', - importSource: options.jsxImportSource, - }, - ) - if (!result) return - if (!refresh) return result - - const newCode = addRefreshWrapper( - result.code, - '@vitejs/plugin-react-swc', - id, - options.reactRefreshHost, - ) - return { code: newCode ?? result.code, map: result.map } - } - : { - // Add filter for default extensions: .tsx, .ts, .mts, .jsx, .mdx - filter: { id: /\.(tsx|ts|mts|jsx|mdx)(?:$|\?)/ }, - async handler(code, _id, transformOptions) { - const id = _id.split('?')[0] - const refresh = !transformOptions?.ssr && !hmrDisabled - - const result = await transformWithOptions( - id, - code, - options.devTarget, - options, - viteCacheRoot, - { - refresh, - development: true, - runtime: 'automatic', - importSource: options.jsxImportSource, - }, - ) - if (!result) return - if (!refresh) return result - - const newCode = addRefreshWrapper( - result.code, - '@vitejs/plugin-react-swc', - id, - options.reactRefreshHost, - ) - return { code: newCode ?? result.code, map: result.map } - }, + const result = await transformWithOptions( + id, + code, + options.devTarget, + options, + viteCacheRoot, + { + refresh, + development: true, + runtime: 'automatic', + importSource: options.jsxImportSource, }, + ) + if (!result) return + if (!refresh) return result + + const newCode = addRefreshWrapper( + result.code, + '@vitejs/plugin-react-swc', + id, + options.reactRefreshHost, + ) + return { code: newCode ?? result.code, map: result.map } + }, }, options.plugins || options.useAtYourOwnRisk_mutateSwcOptions ? { @@ -244,37 +209,18 @@ const react = (_options?: Options): Plugin[] => { configResolved(config) { viteCacheRoot = config.cacheDir }, - transform: options.parserConfig - ? // When custom parserConfig is provided, we can't add a filter - // because the user controls which files are handled - (code, _id) => - transformWithOptions( - _id.split('?')[0], - code, - 'esnext', - options, - viteCacheRoot, - { - runtime: 'automatic', - importSource: options.jsxImportSource, - }, - ) - : { - // Add filter for default extensions: .tsx, .ts, .mts, .jsx, .mdx - filter: { id: /\.(tsx|ts|mts|jsx|mdx)(?:$|\?)/ }, - handler: (code, _id) => - transformWithOptions( - _id.split('?')[0], - code, - 'esnext', - options, - viteCacheRoot, - { - runtime: 'automatic', - importSource: options.jsxImportSource, - }, - ), + transform: (code, _id) => + transformWithOptions( + _id.split('?')[0], + code, + 'esnext', + options, + viteCacheRoot, + { + runtime: 'automatic', + importSource: options.jsxImportSource, }, + ), } : { name: 'vite:react-swc', diff --git a/packages/plugin-react/src/index.ts b/packages/plugin-react/src/index.ts index 9f9893b5..f96d26cf 100644 --- a/packages/plugin-react/src/index.ts +++ b/packages/plugin-react/src/index.ts @@ -3,6 +3,7 @@ import { fileURLToPath } from 'node:url' import { readFileSync } from 'node:fs' import type * as babelCore from '@babel/core' import type { ParserOptions, TransformOptions } from '@babel/core' +import { createFilter } from 'vite' import * as vite from 'vite' import type { Plugin, ResolvedConfig } from 'vite' import { @@ -106,6 +107,7 @@ const compilerAnnotationRE = /['"]use memo['"]/ export default function viteReact(opts: Options = {}): Plugin[] { const include = opts.include ?? defaultIncludeRE const exclude = opts.exclude ?? defaultExcludeRE + const filter = createFilter(include, exclude) const jsxImportSource = opts.jsxImportSource ?? 'react' const jsxImportRuntime = `${jsxImportSource}/jsx-runtime` @@ -245,6 +247,7 @@ export default function viteReact(opts: Options = {}): Plugin[] { }, async handler(code, id, options) { const [filepath] = id.split('?') + if (!filter(filepath)) return const ssr = options?.ssr === true const babelOptions = (() => { diff --git a/packages/plugin-rsc/package.json b/packages/plugin-rsc/package.json index a6c50a78..3894d656 100644 --- a/packages/plugin-rsc/package.json +++ b/packages/plugin-rsc/package.json @@ -40,6 +40,7 @@ }, "dependencies": { "@remix-run/node-fetch-server": "^0.11.0", + "@rolldown/pluginutils": "1.0.0-beta.45", "es-module-lexer": "^1.7.0", "estree-walker": "^3.0.3", "magic-string": "^0.30.21", diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index 4851bbab..6f012576 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -56,6 +56,7 @@ import { validateImportPlugin } from './plugins/validate-import' import { vitePluginFindSourceMapURL } from './plugins/find-source-map-url' import { parseCssVirtual, toCssVirtual, parseIdQuery } from './plugins/shared' import { stripLiteral } from 'strip-literal' +import { code, exactRegex, id, prefixRegex } from '@rolldown/pluginutils' const isRolldownVite = 'rolldownVersion' in vite @@ -247,13 +248,12 @@ export function vitePluginRscMinimal( { name: 'rsc:vite-client-raw-import', transform: { + filter: { code: '__vite_rsc_raw_import__' }, order: 'post', handler(code) { - if (code.includes('__vite_rsc_raw_import__')) { - // inject dynamic import last to avoid Vite adding `?import` query - // to client references (and browser mode server references) - return code.replace('__vite_rsc_raw_import__', 'import') - } + // inject dynamic import last to avoid Vite adding `?import` query + // to client references (and browser mode server references) + return code.replace('__vite_rsc_raw_import__', 'import') }, }, }, @@ -689,12 +689,10 @@ export default function vitePluginRsc( // Alias plugin to redirect vendored react-server-dom imports to user's package when available name: 'rsc:react-server-dom-webpack-alias', resolveId: { + filter: { id: prefixRegex(`${PKG_NAME}/vendor/react-server-dom/`) }, order: 'pre', async handler(source, importer, options) { - if ( - hasReactServerDomWebpack && - source.startsWith(`${PKG_NAME}/vendor/react-server-dom/`) - ) { + if (hasReactServerDomWebpack) { const newSource = source.replace( `${PKG_NAME}/vendor/react-server-dom`, 'react-server-dom-webpack', @@ -861,8 +859,9 @@ export default function vitePluginRsc( return `\0` + source } }, - load(id) { - if (id === '\0virtual:vite-rsc/assets-manifest') { + load: { + filter: { id: exactRegex('\0virtual:vite-rsc/assets-manifest') }, + handler(id) { assert(this.environment.name !== 'client') assert(this.environment.mode === 'dev') const entryUrl = assetsURL( @@ -874,7 +873,7 @@ export default function vitePluginRsc( clientReferenceDeps: {}, } return `export default ${JSON.stringify(manifest, null, 2)}` - } + }, }, // client build generateBundle(_options, bundle) { @@ -1089,6 +1088,7 @@ function globalAsyncLocalStoragePlugin(): Plugin[] { { name: 'rsc:inject-async-local-storage', transform: { + filter: { code: 'AsyncLocalStorage' }, handler(code) { if ( (this.environment.name === 'ssr' || @@ -1260,76 +1260,83 @@ function vitePluginUseClient( return '\0' + source } }, - load(id) { - if (id === '\0virtual:vite-rsc/client-references') { - // not used during dev - if (this.environment.mode === 'dev') { - return { code: `export default {}`, map: null } - } - // no custom chunking needed for scan - if (manager.isScanBuild) { - let code = `` + load: { + filter: { id: prefixRegex('\0virtual:vite-rsc/client-references') }, + handler(id) { + if (id === '\0virtual:vite-rsc/client-references') { + // not used during dev + if (this.environment.mode === 'dev') { + return { code: `export default {}`, map: null } + } + // no custom chunking needed for scan + if (manager.isScanBuild) { + let code = `` + for (const meta of Object.values( + manager.clientReferenceMetaMap, + )) { + code += `import ${JSON.stringify(meta.importId)};\n` + } + return { code, map: null } + } + let code = '' + // group client reference modules by `clientChunks` option + manager.clientReferenceGroups = {} for (const meta of Object.values(manager.clientReferenceMetaMap)) { - code += `import ${JSON.stringify(meta.importId)};\n` + // no server chunk is associated when the entire "use client" module is tree-shaken + if (!meta.serverChunk) continue + let name = + useClientPluginOptions.clientChunks?.({ + id: meta.importId, + normalizedId: manager.toRelativeId(meta.importId), + serverChunk: meta.serverChunk, + }) ?? meta.serverChunk + // ensure clean virtual id to avoid interfering with other plugins + name = cleanUrl(name.replaceAll('..', '__')) + const group = (manager.clientReferenceGroups[name] ??= []) + group.push(meta) + meta.groupChunkId = `\0virtual:vite-rsc/client-references/group/${name}` } - return { code, map: null } - } - let code = '' - // group client reference modules by `clientChunks` option - manager.clientReferenceGroups = {} - for (const meta of Object.values(manager.clientReferenceMetaMap)) { - // no server chunk is associated when the entire "use client" module is tree-shaken - if (!meta.serverChunk) continue - let name = - useClientPluginOptions.clientChunks?.({ - id: meta.importId, - normalizedId: manager.toRelativeId(meta.importId), - serverChunk: meta.serverChunk, - }) ?? meta.serverChunk - // ensure clean virtual id to avoid interfering with other plugins - name = cleanUrl(name.replaceAll('..', '__')) - const group = (manager.clientReferenceGroups[name] ??= []) - group.push(meta) - meta.groupChunkId = `\0virtual:vite-rsc/client-references/group/${name}` - } - debug('client-reference-groups', manager.clientReferenceGroups) - for (const [name, metas] of Object.entries( - manager.clientReferenceGroups, - )) { - const groupVirtual = `virtual:vite-rsc/client-references/group/${name}` - for (const meta of metas) { - code += `\ + debug('client-reference-groups', manager.clientReferenceGroups) + for (const [name, metas] of Object.entries( + manager.clientReferenceGroups, + )) { + const groupVirtual = `virtual:vite-rsc/client-references/group/${name}` + for (const meta of metas) { + code += `\ ${JSON.stringify(meta.referenceKey)}: async () => { const m = await import(${JSON.stringify(groupVirtual)}); return m.export_${meta.referenceKey}; }, ` + } } + code = `export default {${code}};\n` + return { code, map: null } } - code = `export default {${code}};\n` - return { code, map: null } - } - // re-export client reference modules from each group - if (id.startsWith('\0virtual:vite-rsc/client-references/group/')) { - const name = id.slice( - '\0virtual:vite-rsc/client-references/group/'.length, - ) - const metas = manager.clientReferenceGroups[name] - assert(metas, `unknown client reference group: ${name}`) - let code = `` - for (const meta of metas) { - // pick only renderedExports to tree-shake unused client references - const exports = meta.renderedExports - .map((name) => `${name}: import_${meta.referenceKey}.${name},\n`) - .sort() - .join('') - code += ` + // re-export client reference modules from each group + if (id.startsWith('\0virtual:vite-rsc/client-references/group/')) { + const name = id.slice( + '\0virtual:vite-rsc/client-references/group/'.length, + ) + const metas = manager.clientReferenceGroups[name] + assert(metas, `unknown client reference group: ${name}`) + let code = `` + for (const meta of metas) { + // pick only renderedExports to tree-shake unused client references + const exports = meta.renderedExports + .map( + (name) => `${name}: import_${meta.referenceKey}.${name},\n`, + ) + .sort() + .join('') + code += ` import * as import_${meta.referenceKey} from ${JSON.stringify(meta.importId)}; export const export_${meta.referenceKey} = {${exports}}; ` + } + return { code, map: null } } - return { code, map: null } - } + }, }, }, { @@ -2034,13 +2041,13 @@ function vitePluginRscCss( name: 'rsc:rsc-css-self-accept', apply: 'serve', transform: { + filter: { id: /\.css(\?|&)direct/, code: /\S/ }, order: 'post', handler(_code, id, _options) { if ( this.environment.name === 'client' && this.environment.mode === 'dev' && - isCSSRequest(id) && - directRequestRE.test(id) + isCSSRequest(id) ) { const mod = this.environment.moduleGraph.getModuleById(id) if (mod && !mod.isSelfAccepting) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5e159f54..27f45838 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -439,6 +439,9 @@ importers: '@remix-run/node-fetch-server': specifier: ^0.11.0 version: 0.11.0 + '@rolldown/pluginutils': + specifier: 1.0.0-beta.45 + version: 1.0.0-beta.45 es-module-lexer: specifier: ^1.7.0 version: 1.7.0 From 9fc14015d247913d89357a0d9ae63e45ed0c58ae Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Oct 2025 08:23:11 +0000 Subject: [PATCH 5/5] Remove redundant isCSSRequest check in rsc:rsc-css-self-accept The filter already matches CSS files, so this check is redundant Co-authored-by: hi-ogawa <4232207+hi-ogawa@users.noreply.github.com> --- packages/plugin-rsc/src/plugin.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index 6f012576..502715a5 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -2046,8 +2046,7 @@ function vitePluginRscCss( handler(_code, id, _options) { if ( this.environment.name === 'client' && - this.environment.mode === 'dev' && - isCSSRequest(id) + this.environment.mode === 'dev' ) { const mod = this.environment.moduleGraph.getModuleById(id) if (mod && !mod.isSelfAccepting) {