From e2f90add8677db6c43ed26db7f5a9f311be20e56 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 18 Sep 2025 18:52:47 +0900 Subject: [PATCH 1/4] refactor(rsc): use inline style for server css during dev --- packages/plugin-rsc/src/plugin.ts | 125 ++++++++++++++++++++++++++++-- 1 file changed, 117 insertions(+), 8 deletions(-) diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index 08565c09e..cb1763f3d 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -36,6 +36,7 @@ import { cleanUrl, directRequestRE, evalValue, + injectQuery, normalizeViteImportAnalysisUrl, prepareError, } from './plugins/vite-utils' @@ -1923,6 +1924,55 @@ function vitePluginRscCss( return { ids: [...cssIds], hrefs, visitedFiles: [...visitedFiles] } } + async function collectCss2( + environment: DevEnvironment, + clientEnvironment: DevEnvironment, + entryId: string, + ) { + const visited = new Set() + const cssIds = new Set() + const visitedFiles = new Set() + + function recurse(id: string) { + if (visited.has(id)) { + return + } + visited.add(id) + const mod = environment.moduleGraph.getModuleById(id) + if (mod?.file) { + visitedFiles.add(mod.file) + } + for (const next of mod?.importedModules ?? []) { + if (next.id) { + if (isCSSRequest(next.id)) { + if (hasSpecialCssQuery(next.id)) { + continue + } + cssIds.add(next.id) + } else { + recurse(next.id) + } + } + } + } + + recurse(entryId) + + const styles: Record = {} + for (const id of cssIds) { + try { + const result = await clientEnvironment.transformRequest( + injectQuery(id, 'direct'), + ) + styles[id] = result?.code ?? '' + } catch (e) { + console.error(`[collectCss failed '${id}']`, e) + } + } + + return { ids: [...cssIds], styles, visitedFiles: [...visitedFiles] } + } + function getRscCssTransformFilter({ id, code, @@ -2122,23 +2172,33 @@ function vitePluginRscCss( } } }, - load(id) { + async load(id) { const { server } = manager const parsed = parseCssVirtual(id) if (parsed?.type === 'rsc') { assert(this.environment.name === 'rsc') const importer = parsed.id if (this.environment.mode === 'dev') { - const result = collectCss(server.environments.rsc!, importer) + const result = await collectCss2( + server.environments.rsc!, + server.environments.client, + importer, + ) for (const file of [importer, ...result.visitedFiles]) { this.addWatchFile(file) } - const cssHrefs = result.hrefs.map((href) => href.slice(1)) - const deps = assetsURLOfDeps({ css: cssHrefs, js: [] }, manager) - return generateResourcesCode( - serializeValueWithRuntime(deps), - manager, - ) + return generateResourcesCode2(result.styles, manager) + + // const result = collectCss(server.environments.rsc!, importer) + // for (const file of [importer, ...result.visitedFiles]) { + // this.addWatchFile(file) + // } + // const cssHrefs = result.hrefs.map((href) => href.slice(1)) + // const deps = assetsURLOfDeps({ css: cssHrefs, js: [] }, manager) + // return generateResourcesCode( + // serializeValueWithRuntime(deps), + // manager, + // ) } else { const key = manager.toRelativeId(importer) manager.serverResourcesMetaMap[importer] = { key } @@ -2188,6 +2248,55 @@ export default function RemoveDuplicateServerCss() { ] } +function generateResourcesCode2( + styles: Record, + manager: RscPluginManager, +) { + const ResourcesFn = ( + React: typeof import('react'), + styles: Record, + RemoveDuplicateServerCss?: React.FC, + ) => { + return function Resources() { + return React.createElement(React.Fragment, null, [ + ...Object.entries(styles).map(([id, content]) => + React.createElement( + 'style', + { + key: 'css:' + id, + rel: 'stylesheet', + precedence: 'vite-rsc/importer-resources', + // href: href, + // 'data-rsc-css-href': href, + }, + content, + ), + ), + RemoveDuplicateServerCss && + React.createElement(RemoveDuplicateServerCss, { + key: 'remove-duplicate-css', + }), + ]) + } + } + + return ` +import __vite_rsc_react__ from "react"; + +${ + manager.config.command === 'serve' + ? `import RemoveDuplicateServerCss from "virtual:vite-rsc/remove-duplicate-server-css";` + : `const RemoveDuplicateServerCss = undefined;` +} + +export const Resources = (${ResourcesFn.toString()})( + __vite_rsc_react__, + ${JSON.stringify(styles)}, + RemoveDuplicateServerCss, +); +` +} + function generateResourcesCode(depsCode: string, manager: RscPluginManager) { const ResourcesFn = ( React: typeof import('react'), From d56ded8a0e1719606852ce808353e0372a6e4b43 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 18 Sep 2025 19:05:49 +0900 Subject: [PATCH 2/4] fix: add href and precedence for head hoisting --- packages/plugin-rsc/src/plugin.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index cb1763f3d..3422be949 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -2264,10 +2264,11 @@ function generateResourcesCode2( 'style', { key: 'css:' + id, - rel: 'stylesheet', + // https://react.dev/reference/react-dom/components/style#rendering-an-inline-css-stylesheet + href: 'vite-rsc/importer-resources/' + id, precedence: 'vite-rsc/importer-resources', - // href: href, - // 'data-rsc-css-href': href, + // https://github.com/vitejs/vite/blob/dfd8d8aebec412f56346d078bb00170807f0883e/packages/vite/src/client/client.ts#L504 + 'data-vite-dev-id': id, }, content, ), From a25198f756f488ee8c4a7f058160a4a61e19dff3 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Fri, 19 Sep 2025 09:35:09 +0900 Subject: [PATCH 3/4] test: update --- packages/plugin-rsc/e2e/basic.test.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/plugin-rsc/e2e/basic.test.ts b/packages/plugin-rsc/e2e/basic.test.ts index 1306ee314..b821ac320 100644 --- a/packages/plugin-rsc/e2e/basic.test.ts +++ b/packages/plugin-rsc/e2e/basic.test.ts @@ -739,13 +739,13 @@ function defineTest(f: Fixture) { 'link[rel="stylesheet"][data-precedence="vite-rsc/client-reference"]', ), ).toHaveCount(0) - await expect( - page - .locator( - 'link[rel="stylesheet"][data-precedence="vite-rsc/importer-resources"]', - ) - .nth(0), - ).toBeAttached() + // await expect( + // page + // .locator( + // 'link[rel="stylesheet"][data-precedence="vite-rsc/importer-resources"]', + // ) + // .nth(0), + // ).toBeAttached() await expect( page .locator( From 1b8f1277d134644ec5639355ea683c865451702e Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Fri, 19 Sep 2025 10:21:30 +0900 Subject: [PATCH 4/4] wip --- packages/plugin-rsc/src/plugin.ts | 62 ++++++++++++++++++++--- packages/plugin-rsc/src/plugins/shared.ts | 2 +- 2 files changed, 55 insertions(+), 9 deletions(-) diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index 3422be949..33f896c29 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -993,6 +993,21 @@ export default assetsManifest.bootstrapScriptContent; async function () { assert(this.environment.mode === 'dev') let code = '' + code += `;(${() => { + const nodes = document.querySelectorAll('style') + nodes.forEach((node) => { + if ( + node.dataset.precedence?.startsWith( + 'vite-rsc/importer-resources/', + ) + ) { + const id = node.dataset.precedence.slice( + 'vite-rsc/importer-resources/'.length, + ) + node.dataset.viteDevId = id + } + }) + }})();` // enable hmr only when react plugin is available const resolved = await this.resolve('/@react-refresh') if (resolved) { @@ -2187,8 +2202,13 @@ function vitePluginRscCss( for (const file of [importer, ...result.visitedFiles]) { this.addWatchFile(file) } - return generateResourcesCode2(result.styles, manager) - + const jsHrefs = [ + `/@id/__x00__${toCssVirtual({ id: importer, type: 'rsc-browser' })}`, + ] + return generateResourcesCode2( + { styles: result.styles, js: jsHrefs }, + manager, + ) // const result = collectCss(server.environments.rsc!, importer) // for (const file of [importer, ...result.visitedFiles]) { // this.addWatchFile(file) @@ -2213,6 +2233,23 @@ function vitePluginRscCss( ` } } + if (parsed?.type === 'rsc-browser') { + assert(this.environment.name === 'client') + assert(this.environment.mode === 'dev') + const importer = parsed.id + const result = collectCss(server.environments.rsc!, importer) + for (const file of [importer, ...result.visitedFiles]) { + this.addWatchFile(file) + } + let code = result.ids + .map((id) => id.replace(/^\0/, '')) + .map((id) => `import ${JSON.stringify(id)};\n`) + .join('') + // ensure hmr boundary at this virtual since otherwise non-self accepting css + // (e.g. css module) causes full reload + code += `if (import.meta.hot) { import.meta.hot.accept() }\n` + return code + } }, }, createVirtualPlugin( @@ -2249,30 +2286,39 @@ export default function RemoveDuplicateServerCss() { } function generateResourcesCode2( - styles: Record, + deps: { styles: Record; js: string[] }, manager: RscPluginManager, ) { const ResourcesFn = ( React: typeof import('react'), - styles: Record, + deps: { styles: Record; js: string[] }, RemoveDuplicateServerCss?: React.FC, ) => { return function Resources() { return React.createElement(React.Fragment, null, [ - ...Object.entries(styles).map(([id, content]) => + ...Object.entries(deps.styles).map(([id, content]) => React.createElement( 'style', { key: 'css:' + id, // https://react.dev/reference/react-dom/components/style#rendering-an-inline-css-stylesheet href: 'vite-rsc/importer-resources/' + id, - precedence: 'vite-rsc/importer-resources', + precedence: 'vite-rsc/importer-resources/' + id, + // TODO: hoisted style doesn't support arbitrary attributes so they are injected in browser entry // https://github.com/vitejs/vite/blob/dfd8d8aebec412f56346d078bb00170807f0883e/packages/vite/src/client/client.ts#L504 - 'data-vite-dev-id': id, + // 'data-vite-dev-id': id, }, content, ), ), + ...deps.js.map((href: string) => + React.createElement('script', { + key: 'js:' + href, + type: 'module', + async: true, + src: href, + }), + ), RemoveDuplicateServerCss && React.createElement(RemoveDuplicateServerCss, { key: 'remove-duplicate-css', @@ -2292,7 +2338,7 @@ ${ export const Resources = (${ResourcesFn.toString()})( __vite_rsc_react__, - ${JSON.stringify(styles)}, + ${JSON.stringify(deps)}, RemoveDuplicateServerCss, ); ` diff --git a/packages/plugin-rsc/src/plugins/shared.ts b/packages/plugin-rsc/src/plugins/shared.ts index a603d9bc6..2b67fb54c 100644 --- a/packages/plugin-rsc/src/plugins/shared.ts +++ b/packages/plugin-rsc/src/plugins/shared.ts @@ -1,6 +1,6 @@ type CssVirtual = { id: string - type: 'ssr' | 'rsc' + type: 'ssr' | 'rsc' | 'rsc-browser' } export function toCssVirtual({ id, type }: CssVirtual) {