diff --git a/packages/plugin-rsc/e2e/basic.test.ts b/packages/plugin-rsc/e2e/basic.test.ts index 1306ee31..b821ac32 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( diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index 08565c09..33f896c2 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' @@ -992,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) { @@ -1923,6 +1939,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 +2187,38 @@ 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), + 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) + // } + // 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 } @@ -2153,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( @@ -2188,6 +2285,65 @@ export default function RemoveDuplicateServerCss() { ] } +function generateResourcesCode2( + deps: { styles: Record; js: string[] }, + manager: RscPluginManager, +) { + const ResourcesFn = ( + React: typeof import('react'), + deps: { styles: Record; js: string[] }, + RemoveDuplicateServerCss?: React.FC, + ) => { + return function Resources() { + return React.createElement(React.Fragment, null, [ + ...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/' + 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, + }, + 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', + }), + ]) + } + } + + 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(deps)}, + RemoveDuplicateServerCss, +); +` +} + function generateResourcesCode(depsCode: string, manager: RscPluginManager) { const ResourcesFn = ( React: typeof import('react'), diff --git a/packages/plugin-rsc/src/plugins/shared.ts b/packages/plugin-rsc/src/plugins/shared.ts index a603d9bc..2b67fb54 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) {