diff --git a/fixtures/flight-vite/scripts/build.js b/fixtures/flight-vite/scripts/build.js index 7821b2e325584..7f4f9708638ee 100644 --- a/fixtures/flight-vite/scripts/build.js +++ b/fixtures/flight-vite/scripts/build.js @@ -168,6 +168,11 @@ async function build() { external: ['react', 'react-dom', 'react-server-dom-vite'], }, }); + + // copy assets from react-server build to static build, this includes stylesheets improted from server components + await fs.promises.cp('build/react-server/assets', 'build/static/assets', { + recursive: true, + }); } build(); diff --git a/fixtures/flight-vite/server/manifest.js b/fixtures/flight-vite/server/manifest.js new file mode 100644 index 0000000000000..029e926c96f01 --- /dev/null +++ b/fixtures/flight-vite/server/manifest.js @@ -0,0 +1,52 @@ +'use strict'; + +/** + * Traverses the module graph and collects assets for a given chunk + * + * @param manifest Client manifest + * @param id Chunk id + * @param assetMap Cache of assets + * @returns Array of asset URLs + */ +const findAssetsInManifest = (manifest, id, assetMap = new Map()) => { + function traverse(id) { + const cached = assetMap.get(id); + if (cached) { + return cached; + } + const chunk = manifest[id]; + if (!chunk) { + return []; + } + const assets = [ + ...(chunk.assets || []), + ...(chunk.css || []), + ...(chunk.imports?.flatMap(traverse) || []), + ]; + const imports = chunk.imports?.flatMap(traverse) || []; + const all = [...assets, ...imports].filter(Boolean); + all.push(chunk.file); + assetMap.set(id, all); + return Array.from(new Set(all)); + } + return traverse(id); +}; + +const findAssetsInModuleNode = moduleNode => { + const seen = new Set(); + function traverse(node) { + if (seen.has(node.url)) { + return []; + } + seen.add(node.url); + + const imports = [...node.importedModules].flatMap(traverse) || []; + imports.push(node.url); + return Array.from(new Set(imports)); + } + return traverse(moduleNode); +}; + +module.exports = { + findAssetsInManifest, +}; diff --git a/fixtures/flight-vite/server/region.js b/fixtures/flight-vite/server/region.js index 8b8cf8d0d0faa..351b521f60cc3 100644 --- a/fixtures/flight-vite/server/region.js +++ b/fixtures/flight-vite/server/region.js @@ -64,6 +64,11 @@ async function createApp() { return viteServer.ssrLoadModule(id); }; + const {collectStyles} = require('./styles.js'); + globalThis.__vite_find_assets__ = async entries => { + return Object.keys(await collectStyles(viteServer, entries)); + }; + loadModule = async entry => { return await viteServer.ssrLoadModule( path.isAbsolute(entry) @@ -92,10 +97,18 @@ async function createApp() { globalThis.__vite_module_cache__ = new Map(); globalThis.__vite_require__ = id => { + console.log({id}); return import( path.join(process.cwd(), 'build', 'react-server', id + '.js') ); }; + const {findAssetsInManifest} = require('./manifest.js'); + + globalThis.__vite_find_assets__ = async entries => { + return findAssetsInManifest(reactServerManifest, entries[0]).filter( + asset => asset.endsWith('.css') + ); + }; } async function renderApp(res, returnValue) { diff --git a/fixtures/flight-vite/server/styles.js b/fixtures/flight-vite/server/styles.js new file mode 100644 index 0000000000000..72976228ec8fe --- /dev/null +++ b/fixtures/flight-vite/server/styles.js @@ -0,0 +1,103 @@ +'use strict'; + +const path = require('node:path'); + +async function findDeps(vite, node, deps) { + // since `ssrTransformResult.deps` contains URLs instead of `ModuleNode`s, this process is asynchronous. + // instead of using `await`, we resolve all branches in parallel. + const branches = []; + + async function add(node) { + if (!deps.has(node)) { + deps.add(node); + await findDeps(vite, node, deps); + } + } + + async function add_by_url(url) { + const node = await vite.moduleGraph.getModuleByUrl(url); + + if (node) { + await add(node); + } + } + + if (node.ssrTransformResult) { + if (node.ssrTransformResult.deps) { + node.ssrTransformResult.deps.forEach(url => + branches.push(add_by_url(url)) + ); + } + + // if (node.ssrTransformResult.dynamicDeps) { + // node.ssrTransformResult.dynamicDeps.forEach(url => branches.push(add_by_url(url))); + // } + } else { + node.importedModules.forEach(node => branches.push(add(node))); + } + + await Promise.all(branches); +} + +// Vite doesn't expose this so we just copy the list for now +// https://github.com/vitejs/vite/blob/3edd1af56e980aef56641a5a51cf2932bb580d41/packages/vite/src/node/plugins/css.ts#L96 +const STYLE_ASSET_REGEX = /\.(css|less|sass|scss|styl|stylus|pcss|postcss)$/; +const MODULE_STYLE_ASSET_REGEX = + /\.module\.(css|less|sass|scss|styl|stylus|pcss|postcss)$/; + +async function collectStyles(devServer, match) { + const styles = {}; + const deps = new Set(); + try { + for (const file of match) { + const resolvedId = await devServer.pluginContainer.resolveId(file); + + if (!resolvedId) { + console.log('not found'); + continue; + } + + const id = resolvedId.id; + + const normalizedPath = path.resolve(id).replace(/\\/g, '/'); + let node = devServer.moduleGraph.getModuleById(normalizedPath); + if (!node) { + const absolutePath = path.resolve(file); + await devServer.ssrLoadModule(absolutePath); + node = await devServer.moduleGraph.getModuleByUrl(absolutePath); + + if (!node) { + console.log('not found'); + return; + } + } + + await findDeps(devServer, node, deps); + } + } catch (e) { + console.error(e); + } + + for (const dep of deps) { + const parsed = new URL(dep.url, 'http://localhost/'); + const query = parsed.searchParams; + + if (STYLE_ASSET_REGEX.test(dep.file ?? '')) { + try { + const mod = await devServer.ssrLoadModule(dep.url); + // if (module_STYLE_ASSET_REGEX.test(dep.file)) { + // styles[dep.url] = env.cssModules?.[dep.file]; + // } else { + styles[dep.url] = mod.default; + // } + } catch { + // this can happen with dynamically imported modules, I think + // because the Vite module graph doesn't distinguish between + // static and dynamic imports? TODO investigate, submit fix + } + } + } + return styles; +} + +module.exports = {collectStyles}; diff --git a/fixtures/flight-vite/src/App.jsx b/fixtures/flight-vite/src/App.jsx index 0b6dbfaa9af2c..62460e3c3235c 100644 --- a/fixtures/flight-vite/src/App.jsx +++ b/fixtures/flight-vite/src/App.jsx @@ -14,6 +14,17 @@ const REACT_REFRESH_PREAMBLE = ` window.__vite_plugin_react_preamble_installed__ = true `; +async function Assets() { + const styles = await __vite_find_assets__(['src/App.jsx']); + return ( + <> + {styles.map(key => ( + + ))} + > + ); +} + export default async function App() { const res = await fetch('http://localhost:3001/todos'); const todos = await res.json(); @@ -23,7 +34,6 @@ export default async function App() {