diff --git a/.eslintrc.js b/.eslintrc.js index 49846c1f5e9bc..20fa7bdb2a490 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -330,6 +330,7 @@ module.exports = { 'packages/react-server-dom-webpack/**/*.js', 'packages/react-server-dom-turbopack/**/*.js', 'packages/react-server-dom-parcel/**/*.js', + 'packages/react-server-dom-vite/**/*.js', 'packages/react-server-dom-fb/**/*.js', 'packages/react-test-renderer/**/*.js', 'packages/react-debug-tools/**/*.js', @@ -484,6 +485,9 @@ module.exports = { parcelRequire: 'readonly', }, }, + { + files: ['packages/react-server-dom-vite/**/*.js'], + }, { files: ['packages/scheduler/**/*.js'], globals: { diff --git a/ReactVersions.js b/ReactVersions.js index 79e23e1d4b109..5e52c9be1d5c1 100644 --- a/ReactVersions.js +++ b/ReactVersions.js @@ -41,6 +41,7 @@ const stablePackages = { 'react-server-dom-webpack': ReactVersion, 'react-server-dom-turbopack': ReactVersion, 'react-server-dom-parcel': ReactVersion, + 'react-server-dom-vite': ReactVersion, 'react-is': ReactVersion, 'react-reconciler': '0.33.0', 'react-refresh': '0.18.0', diff --git a/fixtures/flight-vite/.gitignore b/fixtures/flight-vite/.gitignore new file mode 100644 index 0000000000000..af27ee2d4915a --- /dev/null +++ b/fixtures/flight-vite/.gitignore @@ -0,0 +1,3 @@ +dist +test-results +tsconfig.tsbuildinfo diff --git a/fixtures/flight-vite/README.md b/fixtures/flight-vite/README.md new file mode 100644 index 0000000000000..2b33016db9614 --- /dev/null +++ b/fixtures/flight-vite/README.md @@ -0,0 +1,28 @@ +# flight-vite + +Basic RSC app ported from [`@hiogawa/vite-rsc`](https://github.com/hi-ogawa/vite-plugins/tree/main/packages/rsc), +which was made on top of `react-server-dom-webpack`. + +## Code structure + +- `basic/{browser,ssr,rsc,plugin}.ts` + - baseline "framework-less" Vite plugin and runtime helpers +- `vite.config.ts`, `src` + - RSC framework/application consuming `react-server-dom-vite` APIs and `basic` plugin/runtime + +## How to test + +```sh +# setup +yarn build-dep +yarn + +# development +yarn dev --force +yarn test-e2e + +# production +yarn build +yarn preview +yarn test-e2e-preview +``` diff --git a/fixtures/flight-vite/basic/README.md b/fixtures/flight-vite/basic/README.md new file mode 100644 index 0000000000000..68f08ba3b7c03 --- /dev/null +++ b/fixtures/flight-vite/basic/README.md @@ -0,0 +1 @@ +Baseline plugins and runtime helpers based on https://github.com/hi-ogawa/vite-plugins/tree/main/packages/rsc diff --git a/fixtures/flight-vite/basic/browser.ts b/fixtures/flight-vite/basic/browser.ts new file mode 100644 index 0000000000000..8d6e49aa6eba1 --- /dev/null +++ b/fixtures/flight-vite/basic/browser.ts @@ -0,0 +1,27 @@ +// @ts-ignore +import clientReferences from 'virtual:vite-rsc/client-references'; + +export function loadModule(id: string) { + if (import.meta.env.DEV) { + // @ts-ignore + return __vite_rsc_raw_import__(/* @vite-ignore */ id); + } else { + return clientReferences[id](); + } +} + +export function findSourceMapURL(filename: string, environmentName: string) { + if (!import.meta.env.DEV) return null; + const url = new URL('/__vite_rsc_source_map', window.location.origin); + url.searchParams.set('filename', filename); + url.searchParams.set('environmentName', environmentName); + return url.toString(); +} + +export const callServer = (...args: any[]) => callServer_(...args); + +let callServer_: any; + +export function setCallServer(fn: any) { + callServer_ = fn; +} diff --git a/fixtures/flight-vite/basic/plugin.ts b/fixtures/flight-vite/basic/plugin.ts new file mode 100644 index 0000000000000..36956a16e769f --- /dev/null +++ b/fixtures/flight-vite/basic/plugin.ts @@ -0,0 +1,798 @@ +import { + defaultServerConditions, + EnvironmentModuleNode, + Rollup, + RunnableDevEnvironment, + parseAstAsync, + type Plugin, + type ResolvedConfig, + type ViteDevServer, + DevEnvironment, + isCSSRequest, +} from 'vite'; +import {createRequestListener} from '@mjackson/node-fetch-server'; +import path from 'node:path'; +import type {ModuleRunner} from 'vite/module-runner'; +import assert from 'node:assert'; +import fs from 'node:fs'; +import {fileURLToPath} from 'node:url'; +import {createHash} from 'node:crypto'; +import {normalizeViteImportAnalysisUrl} from './vite-utils'; +import {transformWrapExport} from './transforms/wrap'; +import {transformProxyExport} from './transforms/proxy'; + +// global state for build orchestration and dev runtimes. +// this plugin assumes server code runs on the same node runtime as vite cli process, +// but framework can do differently, for example, to run ssr/rsc environment on Cloudflare worker environment. +let clientReferences: Record = {}; +let serverReferences: Record = {}; +let server: ViteDevServer; +let config: ResolvedConfig; +let viteSsrRunner: ModuleRunner; +let viteRscRunner: ModuleRunner; + +export default function vitePluginRsc(rscOptions: { + entries: { + browser: string; + rsc: string; + ssr: string; + }; +}): Plugin[] { + return [ + { + // + // basic environment configuration + // + name: 'rsc', + config() { + return { + appType: 'custom', + environments: { + client: { + build: { + manifest: true, + outDir: 'dist/client', + rollupOptions: { + input: {index: 'virtual:vite-rsc/browser-entry'}, + }, + }, + optimizeDeps: { + include: [ + 'react-dom/client', + 'react-server-dom-vite/client.browser', + ], + }, + }, + ssr: { + build: { + outDir: 'dist/ssr', + rollupOptions: { + input: {index: rscOptions.entries.ssr}, + }, + }, + }, + rsc: { + resolve: { + conditions: ['react-server', ...defaultServerConditions], + noExternal: ['react', 'react-dom', 'react-server-dom-vite'], + }, + optimizeDeps: { + include: [ + 'react', + 'react/jsx-runtime', + 'react/jsx-dev-runtime', + 'react-server-dom-vite/server.edge', + ], + }, + build: { + outDir: 'dist/rsc', + rollupOptions: { + input: {index: rscOptions.entries.rsc}, + }, + }, + }, + }, + builder: { + sharedPlugins: true, + async buildApp(builder) { + // pre-pass build to scan client/server references. + // this can reach only environment transitions up-to 3 hops + // (server -> "use client" -> "use server" -> "use client"). + builder.environments.rsc!.config.build.write = false; + builder.environments.ssr.config.build.write = false; + await builder.build(builder.environments.rsc!); + await builder.build(builder.environments.ssr!); + builder.environments.rsc!.config.build.write = true; + builder.environments.ssr.config.build.write = true; + + await builder.build(builder.environments.rsc!); + await builder.build(builder.environments.client!); + await builder.build(builder.environments.ssr!); + }, + }, + }; + }, + configResolved(config_) { + config = config_; + }, + configureServer(server_) { + server = server_; + viteSsrRunner = (server.environments.ssr as RunnableDevEnvironment) + .runner; + viteRscRunner = (server.environments.rsc as RunnableDevEnvironment) + .runner; + (globalThis as any).__viteSsrRunner = viteSsrRunner; + + return () => { + server.middlewares.use(async (req, res, next) => { + try { + const mod = await viteRscRunner.import(rscOptions.entries.rsc); + createRequestListener(mod.default)(req, res); + } catch (e) { + next(e); + } + }); + }; + }, + async configurePreviewServer(server) { + const mod = await import( + /* @vite-ignore */ path.resolve(`dist/rsc/index.js`) + ); + const handler = createRequestListener(mod.default); + + // disable compressions since it breaks html streaming + // https://github.com/vitejs/vite/blob/9f5c59f07aefb1756a37bcb1c0aff24d54288950/packages/vite/src/node/preview.ts#L178 + server.middlewares.use((req, _res, next) => { + delete req.headers['accept-encoding']; + next(); + }); + + return () => { + server.middlewares.use(async (req, res, next) => { + try { + handler(req, res); + } catch (e) { + next(e); + } + }); + }; + }, + async hotUpdate(ctx) { + if (isCSSRequest(ctx.file)) return; + + const ids = ctx.modules.map(mod => mod.id).filter(v => v !== null); + if (ids.length === 0) return; + + // Check updates in server module graph other than client references + // and send it to browser for refetching rsc. + // Client reference update is handled by browser on its own. + const cliendIds = new Set(Object.values(clientReferences)); + const isClientReference = ids.some(id => cliendIds.has(id)); + if (!isClientReference) { + if (this.environment.name === 'rsc') { + ctx.server.environments.client.hot.send({ + type: 'custom', + event: 'rsc:update', + data: { + file: ctx.file, + }, + }); + } + } + }, + }, + { + // + // virtual module to allow rsc environment to access ssr environment + // + name: 'rsc:virtual:vite-rsc/import-ssr', + resolveId(source) { + if (source === 'virtual:vite-rsc/import-ssr') { + return { + id: `\0` + source, + // externalize `dist/rsc/...` import as relative path in ssr build + external: this.environment.mode === 'build', + }; + } + }, + load(id) { + if (id === '\0virtual:vite-rsc/import-ssr') { + assert(this.environment.mode === 'dev'); + return `export default () => __viteSsrRunner.import(${JSON.stringify(rscOptions.entries.ssr)})`; + } + }, + renderChunk(code, chunk) { + if (code.includes('\0virtual:vite-rsc/import-ssr')) { + const replacement = path.relative( + path.join( + this.environment.config.build.outDir, + chunk.fileName, + '..', + ), + path.join(config.environments.ssr.build.outDir, 'index.js'), + ); + code = code.replace('\0virtual:vite-rsc/import-ssr', replacement); + return {code}; + } + return; + }, + }, + createVirtualPlugin('vite-rsc/browser-entry', function () { + let code = ''; + code += `import "virtual:vite-rsc/rsc-css-browser";\n`; + if (this.environment.mode === 'dev') { + // ensure react hmr globas before running user react code + code += ` + import RefreshRuntime from "/@react-refresh"; + RefreshRuntime.injectIntoGlobalHook(window); + window.$RefreshReg$ = () => {}; + window.$RefreshSig$ = () => (type) => type; + window.__vite_plugin_react_preamble_installed__ = true; + await import(${JSON.stringify(rscOptions.entries.browser)}); + `; + } else { + code += `import ${JSON.stringify(rscOptions.entries.browser)};\n`; + } + return code; + }), + { + // + // virtual module to allow ssr/rsc environments to access browser asset urls + // + name: 'rsc:virtual:vite-rsc/assets-manifest', + resolveId(source) { + if (source === 'virtual:vite-rsc/assets-manifest') { + return { + id: `\0` + source, + external: this.environment.mode === 'build', + }; + } + }, + load(id) { + if (id === '\0virtual:vite-rsc/assets-manifest') { + assert(this.environment.name !== 'client'); + const manifest: AssetsManifest = { + entry: { + bootstrapModules: ['/@id/__x00__virtual:vite-rsc/browser-entry'], + deps: { + js: [], + css: [], + }, + }, + clientReferenceDeps: {}, + }; + return `export default ${JSON.stringify(manifest, null, 2)}`; + } + }, + // client build + generateBundle(_options, bundle) { + if (this.environment.name === 'client') { + const assetDeps = collectAssetDeps(bundle); + const clientReferenceDeps: Record = {}; + for (const [key, id] of Object.entries(clientReferences)) { + const deps = assetDeps[id]?.deps; + if (deps) { + clientReferenceDeps[key] = deps; + } + } + const entry = assetDeps['\0virtual:vite-rsc/browser-entry']!; + const manifest: AssetsManifest = { + entry: { + bootstrapModules: [`/${entry.chunk.fileName}`], + deps: entry.deps, + }, + clientReferenceDeps, + }; + this.emitFile({ + type: 'asset', + fileName: '__vite_rsc_assets_manifest.js', + source: `export default ${JSON.stringify(manifest, null, 2)}`, + }); + } + }, + // non-client builds can load assets manifest as external + renderChunk(code, chunk) { + if (code.includes('\0virtual:vite-rsc/assets-manifest')) { + assert(this.environment.name !== 'client'); + const replacement = path.relative( + path.join( + this.environment.config.build.outDir, + chunk.fileName, + '..', + ), + path.join( + config.environments.client!.build.outDir, + '__vite_rsc_assets_manifest.js', + ), + ); + code = code.replace( + '\0virtual:vite-rsc/assets-manifest', + replacement, + ); + return {code}; + } + return; + }, + }, + { + // make `AsyncLocalStorage` available globally for React request context on edge build + // (e.g. React.cache, ssr preload) + name: 'inject-async-local-storage', + async configureServer() { + const __viteRscAyncHooks = await import('node:async_hooks'); + (globalThis as any).AsyncLocalStorage = + __viteRscAyncHooks.AsyncLocalStorage; + }, + banner(chunk) { + if ( + (this.environment.name === 'ssr' || + this.environment.name === 'rsc') && + this.environment.mode === 'build' && + chunk.isEntry + ) { + return `\ + import * as __viteRscAyncHooks from "node:async_hooks"; + globalThis.AsyncLocalStorage = __viteRscAyncHooks.AsyncLocalStorage; + `; + } + return ''; + }, + }, + { + // inject dynamic import last to avoid Vite adding `?import` query to client references + // TODO: we should fix https://github.com/vitejs/vite/pull/14866 + name: 'rsc:patch-browser-raw-import', + transform: { + order: 'post', + handler(code) { + if (code.includes('__vite_rsc_raw_import__')) { + return code.replace('__vite_rsc_raw_import__', 'import'); + } + }, + }, + }, + ...vitePluginUseClient({clientReferences}), + ...vitePluginUseServer({serverReferences}), + ...vitePluginFindSourceMapURL({endpoint: '/__vite_rsc_source_map'}), + ...vitePluginRscCss({entries: rscOptions.entries}), + ]; +} + +// +// use client / server transforms +// + +function vitePluginUseClient({ + clientReferences, +}: { + clientReferences: Record; +}): Plugin[] { + return [ + { + name: 'use-client-transform', + async transform(code, id) { + if (this.environment.name === 'rsc') { + if (code.includes('use client')) { + const ast = await parseAstAsync(code); + const referenceKey = normalizeReferenceId(id, 'client'); + const result = transformProxyExport(ast, { + directive: 'use client', + code, + runtime: name => + `$$register({}, ${JSON.stringify(referenceKey)}, ${JSON.stringify(name)})`, + }); + if (!result) return; + clientReferences[referenceKey] = id; + const {output} = result; + output.prepend( + `import { registerClientReference as $$register } from "react-server-dom-vite/server.edge";\n`, + ); + return {code: output.toString(), map: {mappings: ''}}; + } + } + }, + }, + createVirtualPlugin('vite-rsc/client-references', function () { + if (this.environment.mode === 'dev') { + return `export default {}`; + } + const code = generateDynamicImportCode(clientReferences); + return {code, map: null}; + }), + ]; +} + +function vitePluginUseServer({ + serverReferences, +}: { + serverReferences: Record; +}): Plugin[] { + return [ + { + name: 'use-server-transform', + async transform(code, id) { + if (code.includes('use server')) { + const ast = await parseAstAsync(code); + const referenceKey = normalizeReferenceId(id, 'rsc'); + if (this.environment.name === 'rsc') { + const result = transformWrapExport(ast, { + directive: 'use server', + code, + runtime: (value, name) => + `$$register(${value}, ${JSON.stringify(referenceKey)}, ${JSON.stringify(name)})`, + }); + if (!result) return; + const output = result?.output; + serverReferences[referenceKey] = id; + output.prepend( + `import { registerServerReference as $$register } from "react-server-dom-vite/server.edge";\n`, + ); + return { + code: output.toString(), + map: output.generateMap({hires: 'boundary'}), + }; + } else { + const result = transformProxyExport(ast, { + directive: 'use server', + code, + runtime: name => + `$$register(` + + `${JSON.stringify(referenceKey + '#' + name)},` + + `$$callServer, ` + + `undefined, ` + + `$$findSourceMapURL, ` + + `${JSON.stringify(name)})`, + }); + if (!result) return; + const output = result.output; + serverReferences[referenceKey] = id; + const isBrowser = this.environment.name === 'client'; + output.prepend( + `import { createServerReference as $$register } from "react-server-dom-vite/client.${isBrowser ? 'browser' : 'edge'}";\n` + + `import { callServer as $$callServer, findSourceMapURL as $$findSourceMapURL } from "/basic/${isBrowser ? 'browser' : 'ssr'}";\n`, + ); + return { + code: output.toString(), + map: output.generateMap({hires: 'boundary'}), + }; + } + } + }, + }, + createVirtualPlugin('vite-rsc/server-references', function () { + if (this.environment.mode === 'dev') { + return `export default {}`; + } + const code = generateDynamicImportCode(serverReferences); + return {code, map: null}; + }), + ]; +} + +function hashString(v: string) { + return createHash('sha256').update(v).digest().toString('hex').slice(0, 12); +} + +function normalizeReferenceId(id: string, name: 'client' | 'rsc') { + // build + if (config.command === 'build') { + return hashString(path.relative(config.root, id)); + } + + // dev + // align with how Vite import analysis would rewrite id + const environment = server.environments[name]!; + return normalizeViteImportAnalysisUrl(environment, id); +} + +function createVirtualPlugin(name: string, load: Plugin['load']) { + name = 'virtual:' + name; + return { + name: `rsc:virtual-${name}`, + resolveId(source, _importer, _options) { + return source === name ? '\0' + name : undefined; + }, + load(id, options) { + if (id === '\0' + name) { + return (load as Function).apply(this, [id, options]); + } + }, + } satisfies Plugin; +} + +function generateDynamicImportCode(map: Record) { + let code = Object.entries(map) + .map( + ([key, id]) => + `${JSON.stringify(key)}: () => import(${JSON.stringify(id)}),`, + ) + .join('\n'); + return `export default {${code}};\n`; +} + +// +// collect client reference dependency chunk for modulepreload +// + +export type AssetsManifest = { + entry: {bootstrapModules: string[]; deps: AssetDeps}; + clientReferenceDeps: Record; +}; + +export type AssetDeps = { + js: string[]; + css: string[]; +}; + +function collectAssetDeps(bundle: Rollup.OutputBundle) { + const map: Record = {}; + for (const chunk of Object.values(bundle)) { + if (chunk.type === 'chunk' && chunk.facadeModuleId) { + map[chunk.facadeModuleId] = { + chunk, + deps: collectAssetDepsInner(chunk.fileName, bundle), + }; + } + } + return map; +} + +function collectAssetDepsInner( + fileName: string, + bundle: Rollup.OutputBundle, +): AssetDeps { + const visited = new Set(); + const css: string[] = []; + + function recurse(k: string) { + if (visited.has(k)) return; + visited.add(k); + const v = bundle[k]; + assert(v); + if (v.type === 'chunk') { + css.push(...(v.viteMetadata?.importedCss ?? [])); + for (const k2 of v.imports) { + recurse(k2); + } + } + } + + recurse(fileName); + return { + js: [...visited].map(file => `/${file}`), + css: [...new Set(css)].map(file => `/${file}`), + }; +} + +// +// findSourceMapURL support +// https://github.com/facebook/react/pull/29708 +// https://github.com/facebook/react/pull/30741 +// + +function vitePluginFindSourceMapURL({endpoint}: {endpoint: string}): Plugin[] { + return [ + { + name: 'rsc:findSourceMapURL', + apply: 'serve', + configureServer(server) { + server.middlewares.use(async (req, res, next) => { + const url = new URL(req.url!, `http://localhost`); + if (url.pathname === endpoint) { + let filename = url.searchParams.get('filename')!; + let environmentName = url.searchParams.get('environmentName')!; + try { + const map = await findSourceMapURL( + server, + filename, + environmentName, + ); + res.setHeader('content-type', 'application/json'); + if (!map) res.statusCode = 404; + res.end(JSON.stringify(map ?? {})); + } catch (e) { + next(e); + } + return; + } + next(); + }); + }, + }, + ]; +} + +async function findSourceMapURL( + server: ViteDevServer, + filename: string, + environmentName: string, +): Promise { + // this is likely server external (i.e. outside of Vite processing) + if (filename.startsWith('file://')) { + filename = fileURLToPath(filename); + if (fs.existsSync(filename)) { + // line-by-line identity source map + const content = fs.readFileSync(filename, 'utf-8'); + return { + version: 3, + sources: [filename], + sourcesContent: [content], + mappings: 'AAAA' + ';AACA'.repeat(content.split('\n').length), + }; + } + return; + } + + // server component stack, replace log, `registerServerReference`, etc... + let mod: EnvironmentModuleNode | undefined; + let map: + | NonNullable['map'] + | undefined; + if (environmentName === 'Server') { + mod = server.environments.rsc!.moduleGraph.getModuleById(filename); + // React extracts stacktrace via resetting `prepareStackTrace` on the server + // and let browser devtools handle the mapping. + // https://github.com/facebook/react/blob/4a36d3eab7d9bbbfae62699989aa95e5a0297c16/packages/react-server/src/ReactFlightStackConfigV8.js#L15-L20 + // This means it has additional +2 line offset due to Vite's module runner + // function wrapper. We need to correct it just like Vite handles it internally. + // https://github.com/vitejs/vite/blob/d94e7b25564abb81ab7b921d4cd44d0f0d22fec4/packages/vite/src/shared/utils.ts#L58-L69 + // https://github.com/vitejs/vite/blob/d94e7b25564abb81ab7b921d4cd44d0f0d22fec4/packages/vite/src/node/ssr/fetchModule.ts#L142-L146 + map = mod?.transformResult?.map; + if (map && map.mappings) { + map = {...map, mappings: (';;' + map.mappings) as any}; + } + } + + const base = server.config.base.slice(0, -1); + + // `createServerReference(... findSourceMapURL ...)` called on browser + if (environmentName === 'Client') { + try { + const url = new URL(filename).pathname.slice(base.length); + mod = server.environments.client.moduleGraph.urlToModuleMap.get(url); + map = mod?.transformResult?.map; + } catch (e) {} + } + + if (mod && map) { + // fix sources to match Vite's module url on browser + return {...map, sources: [base + mod.url]}; + } +} + +// +// css support +// - code split for each client reference +// - single bundle for rsc environment +// + +export function vitePluginRscCss({ + entries, +}: { + entries: {rsc: string}; +}): Plugin[] { + function collectCss(environment: DevEnvironment, entryId: string) { + const visited = new Set(); + const cssIds = new Set(); + + function recurse(id: string) { + if (visited.has(id)) { + return; + } + visited.add(id); + const mod = environment.moduleGraph.getModuleById(id); + for (const next of mod?.importedModules ?? []) { + if (next.id) { + if (isCSSRequest(next.id)) { + cssIds.add(next.id); + } else { + recurse(next.id); + } + } + } + } + + recurse(entryId); + + const hrefs = [...cssIds].map(id => + normalizeViteImportAnalysisUrl(server.environments.client, id), + ); + return {ids: [...cssIds], hrefs}; + } + + async function collectCssByUrl( + environment: DevEnvironment, + entryUrl: string, + ) { + const entryMod = await environment.moduleGraph.getModuleByUrl(entryUrl); + return collectCss(environment, entryMod!.id!); + } + + function invalidateModule(environment: DevEnvironment, id: string) { + const mod = environment.moduleGraph.getModuleById(id); + if (mod) { + environment.moduleGraph.invalidateModule(mod); + } + } + + // collect during rsc build and pass it to browser build. + const rscCssIdsBuild = new Set(); + + return [ + { + name: 'rsc:css', + hotUpdate(ctx) { + if (this.environment.name === 'rsc' && ctx.modules.length > 0) { + // simple virtual invalidation to ensure fresh css list + invalidateModule( + server.environments.ssr, + '\0virtual:vite-rsc/css/rsc', + ); + invalidateModule( + server.environments.client, + '\0virtual:vite-rsc/rsc-css-browser', + ); + } + }, + transform(_code, id) { + if ( + this.environment.mode === 'build' && + this.environment.name === 'rsc' + ) { + if (isCSSRequest(id)) { + rscCssIdsBuild.add(id); + } + } + }, + }, + createVirtualPlugin('vite-rsc/rsc-css', async function () { + assert(this.environment.name === 'rsc'); + if (this.environment.mode === 'build') { + // during build, css are injected through AssetsManifest.entry.deps.css + return `export default []`; + } + const {hrefs} = await collectCssByUrl( + server.environments.rsc!, + entries.rsc, + ); + return `export default ${JSON.stringify(hrefs, null, 2)}`; + }), + createVirtualPlugin('vite-rsc/rsc-css-browser', async function () { + assert(this.environment.name === 'client'); + let ids: string[]; + if (this.environment.mode === 'build') { + ids = [...rscCssIdsBuild]; + } else { + const collected = await collectCssByUrl( + server.environments.rsc!, + entries.rsc, + ); + ids = collected.ids; + } + ids = ids.map(id => id.replace(/^\0/, '')); + return ids.map(id => `import ${JSON.stringify(id)};\n`).join(''); + }), + { + name: 'rsc:css/dev-ssr-virtual', + resolveId(source) { + if (source.startsWith('virtual:vite-rsc/css/dev-ssr/')) { + return '\0' + source; + } + }, + async load(id) { + if (id.startsWith('\0virtual:vite-rsc/css/dev-ssr/')) { + id = id.slice('\0virtual:vite-rsc/css/dev-ssr/'.length); + const mod = + await server.environments.ssr.moduleGraph.getModuleByUrl(id); + if (!mod?.id || !mod?.file) { + return `export default []`; + } + const {hrefs} = collectCss(server.environments.ssr, mod.id); + // invalidate virtual module on file change to reflect added/deleted css import + this.addWatchFile(mod.file); + return `export default ${JSON.stringify(hrefs)}`; + } + }, + }, + ]; +} diff --git a/fixtures/flight-vite/basic/rsc.tsx b/fixtures/flight-vite/basic/rsc.tsx new file mode 100644 index 0000000000000..6894e53dbf5d5 --- /dev/null +++ b/fixtures/flight-vite/basic/rsc.tsx @@ -0,0 +1,54 @@ +// @ts-ignore +import serverReferences from 'virtual:vite-rsc/server-references'; +// @ts-ignore +import assetsManifest from 'virtual:vite-rsc/assets-manifest'; + +export {assetsManifest}; + +export function loadModule(id: string) { + if (import.meta.env.DEV) { + return import(/* @vite-ignore */ id); + } else { + return serverReferences[id](); + } +} + +export async function importSsr(): Promise { + const mod = await import('virtual:vite-rsc/import-ssr' as any); + if (import.meta.env.DEV) { + return mod.default(); + } else { + return mod; + } +} + +export async function Resources({nonce}: {nonce?: string}) { + let {css, js} = assetsManifest.entry.deps as {css: string[]; js: string[]}; + + if (import.meta.env.DEV) { + // for build, css in rsc environment is included in client entry css + const rscCss = await import('virtual:vite-rsc/rsc-css' as string); + css = [...css, ...rscCss.default]; + } + + const cssLinks = css.map(href => ( + + )); + + const jsLinks = js.map(href => ( + + )); + + // https://vite.dev/guide/features.html#content-security-policy-csp + // this is used by inline style during dev, + // but this is essentially meaningless when allowing `style-src 'unsafe-inline'` + const viteCsp = nonce && ; + + return ( + <> + {cssLinks} + {jsLinks} + {viteCsp} + + ); +} diff --git a/fixtures/flight-vite/basic/ssr.ts b/fixtures/flight-vite/basic/ssr.ts new file mode 100644 index 0000000000000..baddd03f03497 --- /dev/null +++ b/fixtures/flight-vite/basic/ssr.ts @@ -0,0 +1,50 @@ +// @ts-ignore +import clientReferences from 'virtual:vite-rsc/client-references'; +// @ts-ignore +import assetsManifest from 'virtual:vite-rsc/assets-manifest'; + +import * as ReactDOM from 'react-dom'; +import type {AssetDeps} from './plugin'; + +export {assetsManifest}; + +export async function loadModule(id: string) { + if (import.meta.env.DEV) { + const mod = await import(/* @vite-ignore */ id); + const modCss = await import( + /* @vite-ignore */ '/@id/__x00__virtual:vite-rsc/css/dev-ssr/' + id + ); + return wrapResourceProxy(mod, {js: [], css: modCss.default}); + } else { + const mod = await clientReferences[id](); + return wrapResourceProxy(mod, assetsManifest.clientReferenceDeps[id]); + } +} + +// trigger ssr preload/preinit on module getter access (i.e. requireModule) instead async module loading (i.e. preloadModule) +// since async module loading is cached on production. +function wrapResourceProxy(mod: any, deps: AssetDeps) { + return new Proxy(mod, { + get(target, p, receiver) { + if (p in mod) { + if (deps) { + for (const href of deps.js) { + ReactDOM.preloadModule(href, { + as: 'script', + // vite doesn't allow configuring crossorigin at the moment, so we can hard code it as well. + // https://github.com/vitejs/vite/issues/6648 + crossOrigin: '', + }); + } + for (const href of deps.css) { + ReactDOM.preinit(href, {as: 'style'}); + } + } + } + return Reflect.get(target, p, receiver); + }, + }); +} + +export const findSourceMapURL = undefined; +export const callServer = undefined; diff --git a/fixtures/flight-vite/basic/transforms/README.md b/fixtures/flight-vite/basic/transforms/README.md new file mode 100644 index 0000000000000..964a5fd91544c --- /dev/null +++ b/fixtures/flight-vite/basic/transforms/README.md @@ -0,0 +1 @@ +simplified version of https://github.com/hi-ogawa/vite-plugins/tree/main/packages/transforms diff --git a/fixtures/flight-vite/basic/transforms/proxy.ts b/fixtures/flight-vite/basic/transforms/proxy.ts new file mode 100644 index 0000000000000..d2185c9e248e0 --- /dev/null +++ b/fixtures/flight-vite/basic/transforms/proxy.ts @@ -0,0 +1,73 @@ +import type {Node, Program} from 'estree'; +import MagicString from 'magic-string'; +import {hasDirective} from './utils'; + +export function transformProxyExport( + ast: Program, + options: { + directive: string; + code: string; + runtime: (name: string) => string; + }, +) { + if (!hasDirective(ast.body, options.directive)) { + return; + } + + const output = new MagicString(options.code); + const exportNames: string[] = []; + + function handleExport(node: Node, names: string[]) { + exportNames.push(...names); + const newCode = names + .map( + name => + (name === 'default' ? `export default` : `export const ${name} =`) + + ` /* #__PURE__ */ ${options.runtime(name)};\n`, + ) + .join(''); + output.update(node.start, node.end, newCode); + } + + for (const node of ast.body) { + if (node.type === 'ExportNamedDeclaration') { + if (node.declaration) { + if ( + node.declaration.type === 'FunctionDeclaration' || + node.declaration.type === 'ClassDeclaration' + ) { + /** + * export function foo() {} + */ + handleExport(node, [node.declaration.id.name]); + } else if (node.declaration.type === 'VariableDeclaration') { + /** + * export const foo = 1, bar = 2 + */ + const decl = node.declaration.declarations[0]; + if (decl && decl.id.type === 'Identifier') { + handleExport(node, [decl.id.name]); + } + } else { + node.declaration satisfies never; + } + continue; + } + } + + /** + * export default function foo() {} + * export default class Foo {} + * export default () => {} + */ + if (node.type === 'ExportDefaultDeclaration') { + handleExport(node, ['default']); + continue; + } + + // remove all other nodes + output.remove(node.start, node.end); + } + + return {exportNames, output}; +} diff --git a/fixtures/flight-vite/basic/transforms/utils.ts b/fixtures/flight-vite/basic/transforms/utils.ts new file mode 100644 index 0000000000000..f07b2b2af04aa --- /dev/null +++ b/fixtures/flight-vite/basic/transforms/utils.ts @@ -0,0 +1,22 @@ +import type {Program} from 'estree'; + +// rollup ast's node span +declare module 'estree' { + interface BaseNode { + start: number; + end: number; + } +} + +export function hasDirective( + body: Program['body'], + directive: string, +): boolean { + return !!body.find( + stmt => + stmt.type === 'ExpressionStatement' && + stmt.expression.type === 'Literal' && + typeof stmt.expression.value === 'string' && + stmt.expression.value === directive, + ); +} diff --git a/fixtures/flight-vite/basic/transforms/wrap.ts b/fixtures/flight-vite/basic/transforms/wrap.ts new file mode 100644 index 0000000000000..49570540e3107 --- /dev/null +++ b/fixtures/flight-vite/basic/transforms/wrap.ts @@ -0,0 +1,80 @@ +import type {Program} from 'estree'; +import MagicString from 'magic-string'; +import {hasDirective} from './utils'; + +export function transformWrapExport( + ast: Program, + options: { + directive: string; + code: string; + runtime: (value: string, name: string) => string; + }, +) { + if (!hasDirective(ast.body, options.directive)) { + return; + } + + const output = new MagicString(options.code); + const exportNames: string[] = []; + + function handleExport(start: number, end: number, names: string[]) { + // move and update code in a way that + // `registerServerReference` position maps to original action position e.g. + // + // [input] + // export async function f() { ... } + // ^^^^^^ + // + // [output] + // async function f() { ... } + // f = registerServerReference(f, ...) << maps to original "export" token + // export { f } << + // + const newCode = names + .map( + name => + `${name} = /* #__PURE__ */ ${options.runtime(name, name)};\n` + + `export { ${name} };\n`, + ) + .join(''); + output.update(start, end, newCode); + output.move(start, end, options.code.length); + } + + for (const node of ast.body) { + // named exports + if (node.type === 'ExportNamedDeclaration') { + if (node.declaration) { + if ( + node.declaration.type === 'FunctionDeclaration' || + node.declaration.type === 'ClassDeclaration' + ) { + /** + * export function foo() {} + */ + handleExport(node.start, node.declaration.start, [ + node.declaration.id.name, + ]); + } else if (node.declaration.type === 'VariableDeclaration') { + /** + * export const foo = 1, bar = 2 + */ + if (node.declaration.kind === 'const') { + // replace `const` with `let` to override variable + output.update( + node.declaration.start, + node.declaration.start + 5, + 'let', + ); + } + const decl = node.declaration.declarations[0]; + if (decl && decl.id.type === 'Identifier') { + handleExport(node.start, node.declaration.start, [decl.id.name]); + } + } + } + } + } + + return {exportNames, output}; +} diff --git a/fixtures/flight-vite/basic/vite-utils.ts b/fixtures/flight-vite/basic/vite-utils.ts new file mode 100644 index 0000000000000..c5bd88fac188f --- /dev/null +++ b/fixtures/flight-vite/basic/vite-utils.ts @@ -0,0 +1,128 @@ +// import analysis logic copied from vite +// since there's no proper API exposed yet +// cf. https://github.com/vitejs/vite/pull/19950 + +import fs from 'node:fs'; +import path from 'node:path'; +import type {DevEnvironment, Rollup} from 'vite'; + +const VALID_ID_PREFIX = `/@id/`; + +const NULL_BYTE_PLACEHOLDER = `__x00__`; + +const FS_PREFIX = `/@fs/`; + +function wrapId(id: string): string { + return id.startsWith(VALID_ID_PREFIX) + ? id + : VALID_ID_PREFIX + id.replace('\0', NULL_BYTE_PLACEHOLDER); +} + +// function unwrapId(id: string): string { +// return id.startsWith(VALID_ID_PREFIX) +// ? id.slice(VALID_ID_PREFIX.length).replace(NULL_BYTE_PLACEHOLDER, "\0") +// : id; +// } + +function withTrailingSlash(path: string): string { + if (path[path.length - 1] !== '/') { + return `${path}/`; + } + return path; +} + +const postfixRE = /[?#].*$/; +function cleanUrl(url: string): string { + return url.replace(postfixRE, ''); +} + +function splitFileAndPostfix(path: string): { + file: string; + postfix: string; +} { + const file = cleanUrl(path); + return {file, postfix: path.slice(file.length)}; +} + +const windowsSlashRE = /\\/g; +function slash(p: string): string { + return p.replace(windowsSlashRE, '/'); +} + +const isWindows = + typeof process !== 'undefined' && process.platform === 'win32'; + +function injectQuery(url: string, queryToInject: string): string { + const {file, postfix} = splitFileAndPostfix(url); + const normalizedFile = isWindows ? slash(file) : file; + return `${normalizedFile}?${queryToInject}${postfix[0] === '?' ? `&${postfix.slice(1)}` : /* hash only */ postfix}`; +} + +// function joinUrlSegments(a: string, b: string): string { +// if (!a || !b) { +// return a || b || ""; +// } +// if (a.endsWith("/")) { +// a = a.substring(0, a.length - 1); +// } +// if (b[0] !== "/") { +// b = "/" + b; +// } +// return a + b; +// } + +function normalizeResolvedIdToUrl( + environment: DevEnvironment, + url: string, + resolved: Rollup.PartialResolvedId, +): string { + const root = environment.config.root; + const depsOptimizer = environment.depsOptimizer; + + // normalize all imports into resolved URLs + // e.g. `import 'foo'` -> `import '/@fs/.../node_modules/foo/index.js'` + if (resolved.id.startsWith(withTrailingSlash(root))) { + // in root: infer short absolute path from root + url = resolved.id.slice(root.length); + } else if ( + depsOptimizer?.isOptimizedDepFile(resolved.id) || + // vite-plugin-react isn't following the leading \0 virtual module convention. + // This is a temporary hack to avoid expensive fs checks for React apps. + // We'll remove this as soon we're able to fix the react plugins. + (resolved.id !== '/@react-refresh' && + path.isAbsolute(resolved.id) && + fs.existsSync(cleanUrl(resolved.id))) + ) { + // an optimized deps may not yet exists in the filesystem, or + // a regular file exists but is out of root: rewrite to absolute /@fs/ paths + url = path.posix.join(FS_PREFIX, resolved.id); + } else { + url = resolved.id; + } + + // if the resolved id is not a valid browser import specifier, + // prefix it to make it valid. We will strip this before feeding it + // back into the transform pipeline + if (url[0] !== '.' && url[0] !== '/') { + url = wrapId(resolved.id); + } + + return url; +} + +export function normalizeViteImportAnalysisUrl( + environment: DevEnvironment, + id: string, +): string { + let url = normalizeResolvedIdToUrl(environment, id, {id}); + + // https://github.com/vitejs/vite/blob/c18ce868c4d70873406e9f7d1b2d0a03264d2168/packages/vite/src/node/plugins/importAnalysis.ts#L416 + if (environment.config.consumer === 'client') { + const mod = environment.moduleGraph.getModuleById(id); + if (mod && mod.lastHMRTimestamp > 0) { + url = injectQuery(url, `t=${mod.lastHMRTimestamp}`); + } + } + + return url; +} diff --git a/fixtures/flight-vite/e2e/basic.test.ts b/fixtures/flight-vite/e2e/basic.test.ts new file mode 100644 index 0000000000000..e65a42396d5c5 --- /dev/null +++ b/fixtures/flight-vite/e2e/basic.test.ts @@ -0,0 +1,190 @@ +import fs from 'node:fs'; +import {type Page, expect, test} from '@playwright/test'; +import { + createEditor, + createReloadChecker, + testNoJs, + waitForHydration, +} from './helper'; + +test('basic', async ({page}) => { + await page.goto('./'); + await waitForHydration(page); +}); + +test('client component', async ({page}) => { + await page.goto('./'); + await waitForHydration(page); + await page.getByRole('button', {name: 'Client Counter: 0'}).click(); + await page.getByRole('button', {name: 'Client Counter: 1'}).click(); +}); + +test('server action @js', async ({page}) => { + await page.goto('./'); + await waitForHydration(page); + await using _ = await createReloadChecker(page); + await testAction(page); +}); + +testNoJs('server action @nojs', async ({page}) => { + await page.goto('./'); + await testAction(page); +}); + +async function testAction(page: Page) { + await page.getByRole('button', {name: 'Server Counter: 0'}).click(); + await expect( + page.getByRole('button', {name: 'Server Counter: 1'}), + ).toBeVisible(); + await page.getByRole('button', {name: 'Server Reset'}).click(); + await expect( + page.getByRole('button', {name: 'Server Counter: 0'}), + ).toBeVisible(); +} + +testNoJs('module preload on ssr @build', async ({page}) => { + await page.goto('./'); + const srcs = await Promise.all( + (await page.locator(`head >> link[rel="modulepreload"]`).all()).map(s => + s.getAttribute('href'), + ), + ); + const viteManifest = JSON.parse( + fs.readFileSync('dist/client/.vite/manifest.json', 'utf-8'), + ); + const file = '/' + viteManifest['src/routes/client.tsx'].file; + expect(srcs).toContain(file); +}); + +test('client hmr @dev', async ({page}) => { + await page.goto('./'); + await waitForHydration(page); + await page.getByRole('button', {name: 'Client Counter: 0'}).click(); + await expect( + page.getByRole('button', {name: 'Client Counter: 1'}), + ).toBeVisible(); + + using editor = createEditor('src/routes/client.tsx'); + editor.edit(s => s.replace('Client Counter', 'Client [edit] Counter')); + await expect( + page.getByRole('button', {name: 'Client [edit] Counter: 1'}), + ).toBeVisible(); + + // check next ssr is also updated + const res = await page.goto('/'); + expect(await res?.text()).toContain('Client [edit] Counter'); +}); + +test('server hmr @dev', async ({page}) => { + await page.goto('./'); + await waitForHydration(page); + await page.getByRole('button', {name: 'Client Counter: 0'}).click(); + await expect( + page.getByRole('button', {name: 'Client Counter: 1'}), + ).toBeVisible(); + + using editor = createEditor('src/routes/root.tsx'); + editor.edit(s => s.replace('Server Counter', 'Server [edit] Counter')); + await expect( + page.getByRole('button', {name: 'Server [edit] Counter: 0'}), + ).toBeVisible(); + await expect( + page.getByRole('button', {name: 'Client Counter: 1'}), + ).toBeVisible(); +}); + +test('useActionState @js', async ({page}) => { + await page.goto('./'); + await waitForHydration(page); + await using _ = await createReloadChecker(page); + await testUseActionState(page); +}); + +testNoJs('useActionState @nojs', async ({page}) => { + await page.goto('./'); + await testUseActionState(page); +}); + +async function testUseActionState(page: Page) { + await expect(page.getByTestId('use-action-state')).toContainText( + 'test-useActionState: 0', + ); + await page.getByTestId('use-action-state').click(); + await expect(page.getByTestId('use-action-state')).toContainText( + 'test-useActionState: 1', + ); +} + +test('temporary references @js', async ({page}) => { + await page.goto('./'); + await waitForHydration(page); + await page.getByRole('button', {name: 'test-temporary-reference'}).click(); + await expect(page.getByTestId('temporary-reference')).toContainText( + 'result: [server [client]]', + ); +}); + +test('hydrate while streaming @js', async ({page}) => { + await page.goto('./suspense', {waitUntil: 'commit'}); + await waitForHydration(page); + await expect(page.getByTestId('suspense')).toContainText('suspense-fallback'); + await expect(page.getByTestId('suspense')).toContainText('suspense-resolved'); +}); + +test('css client @js', async ({page}) => { + await page.goto('./'); + await waitForHydration(page); + await expect(page.locator('.test-style-client')).toHaveCSS( + 'color', + 'rgb(250, 150, 0)', + ); +}); + +testNoJs('css client @nojs', async ({page}) => { + await page.goto('./'); + await expect(page.locator('.test-style-client')).toHaveCSS( + 'color', + 'rgb(250, 150, 0)', + ); +}); + +test('css client hmr @dev', async ({page}) => { + await page.goto('./'); + await waitForHydration(page); + await using _ = await createReloadChecker(page); + using editor = createEditor('src/routes/client.css'); + editor.edit(s => s.replaceAll('rgb(250, 150, 0)', 'rgb(150, 250, 0)')); + await expect(page.locator('.test-style-client')).toHaveCSS( + 'color', + 'rgb(150, 250, 0)', + ); +}); + +test('css server @js', async ({page}) => { + await page.goto('./'); + await waitForHydration(page); + await expect(page.locator('.test-style-server')).toHaveCSS( + 'color', + 'rgb(0, 200, 100)', + ); +}); + +testNoJs('css server @nojs', async ({page}) => { + await page.goto('./'); + await expect(page.locator('.test-style-server')).toHaveCSS( + 'color', + 'rgb(0, 200, 100)', + ); +}); + +test('css server hmr @dev', async ({page}) => { + await page.goto('./'); + await waitForHydration(page); + await using _ = await createReloadChecker(page); + using editor = createEditor('src/routes/root.css'); + editor.edit(s => s.replaceAll('rgb(0, 200, 100)', 'rgb(0, 100, 200)')); + await expect(page.locator('.test-style-server')).toHaveCSS( + 'color', + 'rgb(0, 100, 200)', + ); +}); diff --git a/fixtures/flight-vite/e2e/helper.ts b/fixtures/flight-vite/e2e/helper.ts new file mode 100644 index 0000000000000..17c4541ea10eb --- /dev/null +++ b/fixtures/flight-vite/e2e/helper.ts @@ -0,0 +1,43 @@ +import {readFileSync, writeFileSync} from 'fs'; +import test, {type Page, expect} from '@playwright/test'; +import assert from 'node:assert'; + +export const testNoJs = test.extend({ + javaScriptEnabled: ({}, use) => use(false), +}); + +export async function waitForHydration(page: Page) { + await expect(page.getByTestId('hydrated')).toHaveText('[hydrated: 1]'); +} + +export async function createReloadChecker(page: Page) { + // inject custom meta + await page.evaluate(() => { + const el = document.createElement('meta'); + el.setAttribute('name', 'x-reload-check'); + document.head.append(el); + }); + + return { + [Symbol.asyncDispose]: async () => { + // check if meta is preserved + await expect(page.locator(`meta[name="x-reload-check"]`)).toBeAttached({ + timeout: 1, + }); + }, + }; +} + +export function createEditor(filepath: string) { + const init = readFileSync(filepath, 'utf-8'); + return { + edit(editFn: (data: string) => string) { + const next = editFn(init); + assert(next !== init); + writeFileSync(filepath, next); + }, + [Symbol.dispose]() { + writeFileSync(filepath, init); + }, + }; +} diff --git a/fixtures/flight-vite/package.json b/fixtures/flight-vite/package.json new file mode 100644 index 0000000000000..25c5bb4009a36 --- /dev/null +++ b/fixtures/flight-vite/package.json @@ -0,0 +1,30 @@ +{ + "name": "flight-vite", + "private": true, + "type": "module", + "scripts": { + "build-dep": "cd ../.. && RELEASE_CHANNEL=experimental node ./scripts/rollup/build.js react-server-dom-vite/ --type=NODE_DEV,NODE_PROD", + "dev": "vite", + "build": "vite build --app", + "preview": "vite preview", + "test-e2e": "playwright test", + "test-e2e-preview": "E2E_PREVIEW=1 playwright test" + }, + "dependencies": { + "@mjackson/node-fetch-server": "^0.6.1", + "react": "experimental", + "react-dom": "experimental", + "react-server-dom-vite": "file:../../build/node_modules/react-server-dom-vite", + "rsc-html-stream": "^0.0.6" + }, + "devDependencies": { + "@playwright/test": "^1.52.0", + "@types/estree": "^1.0.7", + "@types/react": "latest", + "@types/react-dom": "latest", + "@vitejs/plugin-react": "^4.4.1", + "magic-string": "^0.30.17", + "vite": "^6.3.3", + "vite-plugin-inspect": "^11.0.1" + } +} diff --git a/fixtures/flight-vite/playwright.config.ts b/fixtures/flight-vite/playwright.config.ts new file mode 100644 index 0000000000000..adbce51150cdd --- /dev/null +++ b/fixtures/flight-vite/playwright.config.ts @@ -0,0 +1,32 @@ +import {defineConfig, devices} from '@playwright/test'; + +const port = Number(process.env.E2E_PORT || 6174); +const isPreview = Boolean(process.env.E2E_PREVIEW); +const command = isPreview + ? `yarn preview --port ${port}` + : `yarn dev --port ${port}`; + +export default defineConfig({ + testDir: 'e2e', + use: { + trace: 'on-first-retry', + }, + projects: [ + { + name: 'chromium', + use: { + ...devices['Desktop Chrome'], + viewport: null, + deviceScaleFactor: undefined, + }, + }, + ], + webServer: { + command, + port, + }, + grepInvert: isPreview ? /@dev/ : /@build/, + forbidOnly: !!process.env['CI'], + retries: process.env['CI'] ? 2 : 0, + reporter: 'list', +}); diff --git a/fixtures/flight-vite/public/favicon.ico b/fixtures/flight-vite/public/favicon.ico new file mode 100644 index 0000000000000..4aff076603f8f Binary files /dev/null and b/fixtures/flight-vite/public/favicon.ico differ diff --git a/fixtures/flight-vite/src/entry.browser.tsx b/fixtures/flight-vite/src/entry.browser.tsx new file mode 100644 index 0000000000000..a306c8d3998e2 --- /dev/null +++ b/fixtures/flight-vite/src/entry.browser.tsx @@ -0,0 +1,127 @@ +// @ts-ignore +import * as ReactClient from 'react-server-dom-vite/client.browser'; +import React from 'react'; +import ReactDomClient from 'react-dom/client'; +import {rscStream} from 'rsc-html-stream/client'; +import type {RscPayload} from './entry.rsc'; +import {findSourceMapURL, loadModule, setCallServer} from '../basic/browser'; + +ReactClient.setPreloadModule(loadModule); + +async function main() { + const callServer = async (id: string, args: unknown) => { + const temporaryReferences = ReactClient.createTemporaryReferenceSet(); + const payload = await ReactClient.createFromFetch( + fetch(window.location.href, { + method: 'POST', + body: await ReactClient.encodeReply(args, {temporaryReferences}), + headers: { + 'x-rsc-action': id, + }, + }), + {...rscOptions, temporaryReferences}, + ); + setPayload(payload); + return payload.returnValue; + }; + setCallServer(callServer); + const rscOptions = {callServer, findSourceMapURL}; + + let setPayload: (v: RscPayload) => void; + const initialPayload: RscPayload = await ReactClient.createFromReadableStream( + rscStream, + rscOptions, + ); + + async function onNavigation() { + const url = new URL(window.location.href); + const payload = await ReactClient.createFromFetch( + fetch(url), + rscOptions, + ); + setPayload(payload); + } + + function BrowserRoot() { + const [payload, setPayload_] = React.useState(initialPayload); + + React.useEffect(() => { + setPayload = v => React.startTransition(() => setPayload_(v)); + }, [setPayload_]); + + React.useEffect(() => { + return listenNavigation(() => onNavigation()); + }, []); + + return payload.root; + } + + const browserRoot = ( + + + + ); + + ReactDomClient.hydrateRoot(document, browserRoot, { + formState: initialPayload.formState, + }); + + if (import.meta.hot) { + import.meta.hot.on('rsc:update', async () => { + const payload = await ReactClient.createFromFetch( + fetch(window.location.href), + rscOptions, + ); + setPayload(payload); + }); + } +} + +function listenNavigation(onNavigation: () => void) { + window.addEventListener('popstate', onNavigation); + + const oldPushState = window.history.pushState; + window.history.pushState = function (...args) { + const res = oldPushState.apply(this, args); + onNavigation(); + return res; + }; + + const oldReplaceState = window.history.replaceState; + window.history.replaceState = function (...args) { + const res = oldReplaceState.apply(this, args); + onNavigation(); + return res; + }; + + function onClick(e: MouseEvent) { + let link = (e.target as Element).closest('a'); + if ( + link && + link instanceof HTMLAnchorElement && + link.href && + (!link.target || link.target === '_self') && + link.origin === location.origin && + !link.hasAttribute('download') && + e.button === 0 && // left clicks only + !e.metaKey && // open in new tab (mac) + !e.ctrlKey && // open in new tab (windows) + !e.altKey && // download + !e.shiftKey && + !e.defaultPrevented + ) { + e.preventDefault(); + history.pushState(null, '', link.href); + } + } + document.addEventListener('click', onClick); + + return () => { + document.removeEventListener('click', onClick); + window.removeEventListener('popstate', onNavigation); + window.history.pushState = oldPushState; + window.history.replaceState = oldReplaceState; + }; +} + +main(); diff --git a/fixtures/flight-vite/src/entry.rsc.tsx b/fixtures/flight-vite/src/entry.rsc.tsx new file mode 100644 index 0000000000000..11fa7b12f7f21 --- /dev/null +++ b/fixtures/flight-vite/src/entry.rsc.tsx @@ -0,0 +1,100 @@ +// @ts-ignore +import * as ReactServer from 'react-server-dom-vite/server.edge'; +import type React from 'react'; +import type {ReactFormState} from 'react-dom/client'; +import {Root} from './routes/root'; +import {importSsr, loadModule, Resources} from '../basic/rsc'; + +ReactServer.setPreloadModule(loadModule); + +export type RscPayload = { + root: React.ReactNode; + formState?: ReactFormState; + returnValue?: unknown; +}; + +export default async function handler(request: Request): Promise { + const nonce = process.env.DISABLE_NONCE ? undefined : crypto.randomUUID(); + + function RscRoot() { + return ( + <> + + + + ); + } + + return renderRsc(request, , {nonce}); +} + +async function renderRsc( + request: Request, + root: React.ReactNode, + options: {nonce?: string}, +): Promise { + const url = new URL(request.url); + const isAction = request.method === 'POST'; + + // override with ?__rsc and ?__html for quick debugging + const isRscRequest = + (!request.headers.get('accept')?.includes('text/html') && + !url.searchParams.has('__html')) || + url.searchParams.has('__rsc'); + + // action + let returnValue: unknown | undefined; + let formState: ReactFormState | undefined; + let temporaryReferences: unknown | undefined; + if (isAction) { + const actionId = request.headers.get('x-rsc-action'); + if (actionId) { + // client stream request + const contentType = request.headers.get('content-type'); + const body = contentType?.startsWith('multipart/form-data') + ? await request.formData() + : await request.text(); + temporaryReferences = ReactServer.createTemporaryReferenceSet(); + const args = await ReactServer.decodeReply(body, {temporaryReferences}); + const action = await ReactServer.loadServerAction(actionId); + returnValue = await action.apply(null, args); + } else { + // progressive enhancement + const formData = await request.formData(); + const decodedAction = await ReactServer.decodeAction(formData); + const result = await decodedAction(); + formState = await ReactServer.decodeFormState(result, formData); + } + } + + const rscPayload: RscPayload = {root, formState, returnValue}; + const rscOptions = {temporaryReferences, nonce: options.nonce}; + const stream = ReactServer.renderToReadableStream(rscPayload, rscOptions); + + if (isRscRequest) { + return new Response(stream, { + headers: { + 'Content-Type': 'text/x-component;charset=utf-8', + }, + }); + } + + const ssr = await importSsr(); + const htmlStream = await ssr.renderHtml({ + url, + stream, + formState, + nonce: options.nonce, + }); + const headers = new Headers({'Content-Type': 'text/html;charset=utf-8'}); + if (options.nonce) { + headers.set( + 'Content-Security-Policy', + `default-src 'self'; object-src 'none';` + + // allow unsafe-eval during dev for `createFakeFunction` eval. + `script-src 'self' 'nonce-${options.nonce}' ${import.meta.env.DEV ? `'unsafe-eval'` : ''};` + + `style-src 'self' 'unsafe-inline'`, + ); + } + return new Response(htmlStream, {headers}); +} diff --git a/fixtures/flight-vite/src/entry.ssr.tsx b/fixtures/flight-vite/src/entry.ssr.tsx new file mode 100644 index 0000000000000..9adb147f75606 --- /dev/null +++ b/fixtures/flight-vite/src/entry.ssr.tsx @@ -0,0 +1,45 @@ +// @ts-ignore +import * as ReactClient from 'react-server-dom-vite/client.edge'; +import React from 'react'; +import type {ReactFormState} from 'react-dom/client'; +// @ts-ignore +import * as ReactDomServer from 'react-dom/server.edge'; +import {injectRSCPayload} from 'rsc-html-stream/server'; +import type {RscPayload} from './entry.rsc'; + +import {assetsManifest, loadModule} from '../basic/ssr'; + +ReactClient.setPreloadModule(loadModule); + +export async function renderHtml({ + url, + stream, + formState, + nonce, +}: { + url: URL; + stream: ReadableStream; + formState?: ReactFormState; + nonce?: string; +}) { + const [stream1, stream2] = stream.tee(); + + let payload: Promise; + function SsrRoot() { + payload ??= ReactClient.createFromReadableStream(stream1, { + nonce, + }); + return React.use(payload).root; + } + + const htmlStream = await ReactDomServer.renderToReadableStream(, { + bootstrapModules: url.search.includes('__nojs') + ? [] + : assetsManifest.entry.bootstrapModules, + nonce, + // @ts-ignore + formState, + }); + + return htmlStream.pipeThrough(injectRSCPayload(stream2, {nonce})); +} diff --git a/fixtures/flight-vite/src/routes/action-from-client/action.tsx b/fixtures/flight-vite/src/routes/action-from-client/action.tsx new file mode 100644 index 0000000000000..72f02f76682ff --- /dev/null +++ b/fixtures/flight-vite/src/routes/action-from-client/action.tsx @@ -0,0 +1,17 @@ +'use server'; + +export async function testAction() { + console.log('[test-action-from-client]'); +} + +export async function testActionState(prev: number) { + return prev + 1; +} + +export async function testActionTemporaryReference(node: React.ReactNode) { + return ( + + [server {node}] + + ); +} diff --git a/fixtures/flight-vite/src/routes/action-from-client/client.tsx b/fixtures/flight-vite/src/routes/action-from-client/client.tsx new file mode 100644 index 0000000000000..7cc7a88b50aca --- /dev/null +++ b/fixtures/flight-vite/src/routes/action-from-client/client.tsx @@ -0,0 +1,44 @@ +'use client'; + +import React from 'react'; +import { + testAction, + testActionState, + testActionTemporaryReference, +} from './action'; + +export function TestActionFromClient() { + return ( +
+ +
+ ); +} + +export function TestUseActionState() { + const [state, formAction] = React.useActionState(testActionState, 0); + + return ( +
+ +
+ ); +} + +export function TestTemporaryReference() { + const [result, setResult] = React.useState('(none)'); + + return ( +
+
{ + setResult(await testActionTemporaryReference([client])); + }}> + +
+
result: {result}
+
+ ); +} diff --git a/fixtures/flight-vite/src/routes/action.tsx b/fixtures/flight-vite/src/routes/action.tsx new file mode 100644 index 0000000000000..4e9df583c8ab0 --- /dev/null +++ b/fixtures/flight-vite/src/routes/action.tsx @@ -0,0 +1,15 @@ +'use server'; + +let serverCounter = 0; + +export async function getServerCounter() { + return serverCounter; +} + +export async function changeServerCounter(change: number) { + serverCounter += change; +} + +export async function resetServerCounter() { + serverCounter = 0; +} diff --git a/fixtures/flight-vite/src/routes/client.css b/fixtures/flight-vite/src/routes/client.css new file mode 100644 index 0000000000000..954574061f6ee --- /dev/null +++ b/fixtures/flight-vite/src/routes/client.css @@ -0,0 +1,3 @@ +.test-style-client { + color: rgb(250, 150, 0); +} diff --git a/fixtures/flight-vite/src/routes/client.tsx b/fixtures/flight-vite/src/routes/client.tsx new file mode 100644 index 0000000000000..59aeef1f343e6 --- /dev/null +++ b/fixtures/flight-vite/src/routes/client.tsx @@ -0,0 +1,26 @@ +'use client'; + +import React from 'react'; +import './client.css'; + +export function ClientCounter(): React.ReactElement { + const [count, setCount] = React.useState(0); + return ( + + ); +} + +export function Hydrated() { + const hydrated = React.useSyncExternalStore( + React.useCallback(() => () => {}, []), + () => true, + () => false, + ); + return [hydrated: {hydrated ? 1 : 0}]; +} + +export function TestStyleClient() { + return test-style-client; +} diff --git a/fixtures/flight-vite/src/routes/root.css b/fixtures/flight-vite/src/routes/root.css new file mode 100644 index 0000000000000..0386f937c881a --- /dev/null +++ b/fixtures/flight-vite/src/routes/root.css @@ -0,0 +1,3 @@ +.test-style-server { + color: rgb(0, 200, 100); +} diff --git a/fixtures/flight-vite/src/routes/root.tsx b/fixtures/flight-vite/src/routes/root.tsx new file mode 100644 index 0000000000000..e8a985c006b8e --- /dev/null +++ b/fixtures/flight-vite/src/routes/root.tsx @@ -0,0 +1,73 @@ +import React from 'react'; +import { + changeServerCounter, + resetServerCounter, + getServerCounter, +} from './action'; +import {ClientCounter, Hydrated, TestStyleClient} from './client'; +import { + TestActionFromClient, + TestTemporaryReference, + TestUseActionState, +} from './action-from-client/client'; +import './root.css'; + +export function Root(props: {url: URL}) { + return ( + + + vite-rsc + + +

Test

+
+ + +
+ +
test-style-server
+
+ test-suspense{' '} + {props.url.pathname === '/suspense' && } +
+
+ test-console-replay{' '} + {props.url.pathname === '/console-replay' && } +
+ + + + + + + + ); +} + +function ServerCounter() { + return ( +
+ + +
+ ); +} + +function TestConsoleReplay() { + console.log('[test-console-replay]'); + return
; +} + +function TestSuspense() { + async function Sleep() { + await new Promise(r => setTimeout(r, 1000)); + return suspense-resolved; + } + return ( + + suspense-fallback}> + + + + ); +} diff --git a/fixtures/flight-vite/tsconfig.json b/fixtures/flight-vite/tsconfig.json new file mode 100644 index 0000000000000..eeb2d95d9f938 --- /dev/null +++ b/fixtures/flight-vite/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "allowImportingTsExtensions": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "skipLibCheck": true, + "verbatimModuleSyntax": true, + "noEmit": true, + "moduleResolution": "Bundler", + "module": "ESNext", + "target": "ESNext", + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "types": ["vite/client"], + "jsx": "react-jsx" + } +} diff --git a/fixtures/flight-vite/vite.config.ts b/fixtures/flight-vite/vite.config.ts new file mode 100644 index 0000000000000..0c410fb16dd67 --- /dev/null +++ b/fixtures/flight-vite/vite.config.ts @@ -0,0 +1,25 @@ +import react from '@vitejs/plugin-react'; +import {defineConfig} from 'vite'; +import rsc from './basic/plugin'; +import Inspect from 'vite-plugin-inspect'; + +export default defineConfig({ + clearScreen: false, + build: { + minify: false, + }, + plugins: [ + react(), + rsc({ + entries: { + browser: '/src/entry.browser.tsx', + rsc: '/src/entry.rsc.tsx', + ssr: '/src/entry.ssr.tsx', + }, + }), + Inspect(), + ], + optimizeDeps: { + include: ['rsc-html-stream/client'], + }, +}) as any; diff --git a/fixtures/flight-vite/yarn.lock b/fixtures/flight-vite/yarn.lock new file mode 100644 index 0000000000000..1fc719717094e --- /dev/null +++ b/fixtures/flight-vite/yarn.lock @@ -0,0 +1,922 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@ampproject/remapping@^2.2.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4" + integrity sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.24" + +"@babel/code-frame@^7.26.2": + version "7.26.2" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.26.2.tgz#4b5fab97d33338eff916235055f0ebc21e573a85" + integrity sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ== + dependencies: + "@babel/helper-validator-identifier" "^7.25.9" + js-tokens "^4.0.0" + picocolors "^1.0.0" + +"@babel/compat-data@^7.26.8": + version "7.26.8" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.26.8.tgz#821c1d35641c355284d4a870b8a4a7b0c141e367" + integrity sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ== + +"@babel/core@^7.26.10": + version "7.26.10" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.26.10.tgz#5c876f83c8c4dcb233ee4b670c0606f2ac3000f9" + integrity sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ== + dependencies: + "@ampproject/remapping" "^2.2.0" + "@babel/code-frame" "^7.26.2" + "@babel/generator" "^7.26.10" + "@babel/helper-compilation-targets" "^7.26.5" + "@babel/helper-module-transforms" "^7.26.0" + "@babel/helpers" "^7.26.10" + "@babel/parser" "^7.26.10" + "@babel/template" "^7.26.9" + "@babel/traverse" "^7.26.10" + "@babel/types" "^7.26.10" + convert-source-map "^2.0.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.3" + semver "^6.3.1" + +"@babel/generator@^7.26.10", "@babel/generator@^7.27.0": + version "7.27.0" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.27.0.tgz#764382b5392e5b9aff93cadb190d0745866cbc2c" + integrity sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw== + dependencies: + "@babel/parser" "^7.27.0" + "@babel/types" "^7.27.0" + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + jsesc "^3.0.2" + +"@babel/helper-compilation-targets@^7.26.5": + version "7.27.0" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.0.tgz#de0c753b1cd1d9ab55d473c5a5cf7170f0a81880" + integrity sha512-LVk7fbXml0H2xH34dFzKQ7TDZ2G4/rVTOrq9V+icbbadjbVxxeFeDsNHv2SrZeWoA+6ZiTyWYWtScEIW07EAcA== + dependencies: + "@babel/compat-data" "^7.26.8" + "@babel/helper-validator-option" "^7.25.9" + browserslist "^4.24.0" + lru-cache "^5.1.1" + semver "^6.3.1" + +"@babel/helper-module-imports@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz#e7f8d20602ebdbf9ebbea0a0751fb0f2a4141715" + integrity sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw== + dependencies: + "@babel/traverse" "^7.25.9" + "@babel/types" "^7.25.9" + +"@babel/helper-module-transforms@^7.26.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz#8ce54ec9d592695e58d84cd884b7b5c6a2fdeeae" + integrity sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw== + dependencies: + "@babel/helper-module-imports" "^7.25.9" + "@babel/helper-validator-identifier" "^7.25.9" + "@babel/traverse" "^7.25.9" + +"@babel/helper-plugin-utils@^7.25.9": + version "7.26.5" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz#18580d00c9934117ad719392c4f6585c9333cc35" + integrity sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg== + +"@babel/helper-string-parser@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz#1aabb72ee72ed35789b4bbcad3ca2862ce614e8c" + integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA== + +"@babel/helper-validator-identifier@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz#24b64e2c3ec7cd3b3c547729b8d16871f22cbdc7" + integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ== + +"@babel/helper-validator-option@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz#86e45bd8a49ab7e03f276577f96179653d41da72" + integrity sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw== + +"@babel/helpers@^7.26.10": + version "7.27.0" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.27.0.tgz#53d156098defa8243eab0f32fa17589075a1b808" + integrity sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg== + dependencies: + "@babel/template" "^7.27.0" + "@babel/types" "^7.27.0" + +"@babel/parser@^7.1.0", "@babel/parser@^7.20.7", "@babel/parser@^7.26.10", "@babel/parser@^7.27.0": + version "7.27.0" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.27.0.tgz#3d7d6ee268e41d2600091cbd4e145ffee85a44ec" + integrity sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg== + dependencies: + "@babel/types" "^7.27.0" + +"@babel/plugin-transform-react-jsx-self@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.25.9.tgz#c0b6cae9c1b73967f7f9eb2fca9536ba2fad2858" + integrity sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-react-jsx-source@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.25.9.tgz#4c6b8daa520b5f155b5fb55547d7c9fa91417503" + integrity sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/template@^7.26.9", "@babel/template@^7.27.0": + version "7.27.0" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.27.0.tgz#b253e5406cc1df1c57dcd18f11760c2dbf40c0b4" + integrity sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA== + dependencies: + "@babel/code-frame" "^7.26.2" + "@babel/parser" "^7.27.0" + "@babel/types" "^7.27.0" + +"@babel/traverse@^7.25.9", "@babel/traverse@^7.26.10": + version "7.27.0" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.27.0.tgz#11d7e644779e166c0442f9a07274d02cd91d4a70" + integrity sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA== + dependencies: + "@babel/code-frame" "^7.26.2" + "@babel/generator" "^7.27.0" + "@babel/parser" "^7.27.0" + "@babel/template" "^7.27.0" + "@babel/types" "^7.27.0" + debug "^4.3.1" + globals "^11.1.0" + +"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.25.9", "@babel/types@^7.26.10", "@babel/types@^7.27.0": + version "7.27.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.27.0.tgz#ef9acb6b06c3173f6632d993ecb6d4ae470b4559" + integrity sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg== + dependencies: + "@babel/helper-string-parser" "^7.25.9" + "@babel/helper-validator-identifier" "^7.25.9" + +"@esbuild/aix-ppc64@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.3.tgz#014180d9a149cffd95aaeead37179433f5ea5437" + integrity sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ== + +"@esbuild/android-arm64@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.3.tgz#649e47e04ddb24a27dc05c395724bc5f4c55cbfe" + integrity sha512-XelR6MzjlZuBM4f5z2IQHK6LkK34Cvv6Rj2EntER3lwCBFdg6h2lKbtRjpTTsdEjD/WSe1q8UyPBXP1x3i/wYQ== + +"@esbuild/android-arm@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.3.tgz#8a0f719c8dc28a4a6567ef7328c36ea85f568ff4" + integrity sha512-PuwVXbnP87Tcff5I9ngV0lmiSu40xw1At6i3GsU77U7cjDDB4s0X2cyFuBiDa1SBk9DnvWwnGvVaGBqoFWPb7A== + +"@esbuild/android-x64@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.3.tgz#e2ab182d1fd06da9bef0784a13c28a7602d78009" + integrity sha512-ogtTpYHT/g1GWS/zKM0cc/tIebFjm1F9Aw1boQ2Y0eUQ+J89d0jFY//s9ei9jVIlkYi8AfOjiixcLJSGNSOAdQ== + +"@esbuild/darwin-arm64@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.3.tgz#c7f3166fcece4d158a73dcfe71b2672ca0b1668b" + integrity sha512-eESK5yfPNTqpAmDfFWNsOhmIOaQA59tAcF/EfYvo5/QWQCzXn5iUSOnqt3ra3UdzBv073ykTtmeLJZGt3HhA+w== + +"@esbuild/darwin-x64@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.3.tgz#d8c5342ec1a4bf4b1915643dfe031ba4b173a87a" + integrity sha512-Kd8glo7sIZtwOLcPbW0yLpKmBNWMANZhrC1r6K++uDR2zyzb6AeOYtI6udbtabmQpFaxJ8uduXMAo1gs5ozz8A== + +"@esbuild/freebsd-arm64@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.3.tgz#9f7d789e2eb7747d4868817417cc968ffa84f35b" + integrity sha512-EJiyS70BYybOBpJth3M0KLOus0n+RRMKTYzhYhFeMwp7e/RaajXvP+BWlmEXNk6uk+KAu46j/kaQzr6au+JcIw== + +"@esbuild/freebsd-x64@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.3.tgz#8ad35c51d084184a8e9e76bb4356e95350a64709" + integrity sha512-Q+wSjaLpGxYf7zC0kL0nDlhsfuFkoN+EXrx2KSB33RhinWzejOd6AvgmP5JbkgXKmjhmpfgKZq24pneodYqE8Q== + +"@esbuild/linux-arm64@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.3.tgz#3af0da3d9186092a9edd4e28fa342f57d9e3cd30" + integrity sha512-xCUgnNYhRD5bb1C1nqrDV1PfkwgbswTTBRbAd8aH5PhYzikdf/ddtsYyMXFfGSsb/6t6QaPSzxtbfAZr9uox4A== + +"@esbuild/linux-arm@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.3.tgz#e91cafa95e4474b3ae3d54da12e006b782e57225" + integrity sha512-dUOVmAUzuHy2ZOKIHIKHCm58HKzFqd+puLaS424h6I85GlSDRZIA5ycBixb3mFgM0Jdh+ZOSB6KptX30DD8YOQ== + +"@esbuild/linux-ia32@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.3.tgz#81025732d85b68ee510161b94acdf7e3007ea177" + integrity sha512-yplPOpczHOO4jTYKmuYuANI3WhvIPSVANGcNUeMlxH4twz/TeXuzEP41tGKNGWJjuMhotpGabeFYGAOU2ummBw== + +"@esbuild/linux-loong64@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.3.tgz#3c744e4c8d5e1148cbe60a71a11b58ed8ee5deb8" + integrity sha512-P4BLP5/fjyihmXCELRGrLd793q/lBtKMQl8ARGpDxgzgIKJDRJ/u4r1A/HgpBpKpKZelGct2PGI4T+axcedf6g== + +"@esbuild/linux-mips64el@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.3.tgz#1dfe2a5d63702db9034cc6b10b3087cc0424ec26" + integrity sha512-eRAOV2ODpu6P5divMEMa26RRqb2yUoYsuQQOuFUexUoQndm4MdpXXDBbUoKIc0iPa4aCO7gIhtnYomkn2x+bag== + +"@esbuild/linux-ppc64@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.3.tgz#2e85d9764c04a1ebb346dc0813ea05952c9a5c56" + integrity sha512-ZC4jV2p7VbzTlnl8nZKLcBkfzIf4Yad1SJM4ZMKYnJqZFD4rTI+pBG65u8ev4jk3/MPwY9DvGn50wi3uhdaghg== + +"@esbuild/linux-riscv64@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.3.tgz#a9ea3334556b09f85ccbfead58c803d305092415" + integrity sha512-LDDODcFzNtECTrUUbVCs6j9/bDVqy7DDRsuIXJg6so+mFksgwG7ZVnTruYi5V+z3eE5y+BJZw7VvUadkbfg7QA== + +"@esbuild/linux-s390x@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.3.tgz#f6a7cb67969222b200974de58f105dfe8e99448d" + integrity sha512-s+w/NOY2k0yC2p9SLen+ymflgcpRkvwwa02fqmAwhBRI3SC12uiS10edHHXlVWwfAagYSY5UpmT/zISXPMW3tQ== + +"@esbuild/linux-x64@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.3.tgz#a237d3578ecdd184a3066b1f425e314ade0f8033" + integrity sha512-nQHDz4pXjSDC6UfOE1Fw9Q8d6GCAd9KdvMZpfVGWSJztYCarRgSDfOVBY5xwhQXseiyxapkiSJi/5/ja8mRFFA== + +"@esbuild/netbsd-arm64@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.3.tgz#4c15c68d8149614ddb6a56f9c85ae62ccca08259" + integrity sha512-1QaLtOWq0mzK6tzzp0jRN3eccmN3hezey7mhLnzC6oNlJoUJz4nym5ZD7mDnS/LZQgkrhEbEiTn515lPeLpgWA== + +"@esbuild/netbsd-x64@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.3.tgz#12f6856f8c54c2d7d0a8a64a9711c01a743878d5" + integrity sha512-i5Hm68HXHdgv8wkrt+10Bc50zM0/eonPb/a/OFVfB6Qvpiirco5gBA5bz7S2SHuU+Y4LWn/zehzNX14Sp4r27g== + +"@esbuild/openbsd-arm64@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.3.tgz#ca078dad4a34df192c60233b058db2ca3d94bc5c" + integrity sha512-zGAVApJEYTbOC6H/3QBr2mq3upG/LBEXr85/pTtKiv2IXcgKV0RT0QA/hSXZqSvLEpXeIxah7LczB4lkiYhTAQ== + +"@esbuild/openbsd-x64@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.3.tgz#c9178adb60e140e03a881d0791248489c79f95b2" + integrity sha512-fpqctI45NnCIDKBH5AXQBsD0NDPbEFczK98hk/aa6HJxbl+UtLkJV2+Bvy5hLSLk3LHmqt0NTkKNso1A9y1a4w== + +"@esbuild/sunos-x64@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.3.tgz#03765eb6d4214ff27e5230af779e80790d1ee09f" + integrity sha512-ROJhm7d8bk9dMCUZjkS8fgzsPAZEjtRJqCAmVgB0gMrvG7hfmPmz9k1rwO4jSiblFjYmNvbECL9uhaPzONMfgA== + +"@esbuild/win32-arm64@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.3.tgz#f1c867bd1730a9b8dfc461785ec6462e349411ea" + integrity sha512-YWcow8peiHpNBiIXHwaswPnAXLsLVygFwCB3A7Bh5jRkIBFWHGmNQ48AlX4xDvQNoMZlPYzjVOQDYEzWCqufMQ== + +"@esbuild/win32-ia32@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.3.tgz#77491f59ef6c9ddf41df70670d5678beb3acc322" + integrity sha512-qspTZOIGoXVS4DpNqUYUs9UxVb04khS1Degaw/MnfMe7goQ3lTfQ13Vw4qY/Nj0979BGvMRpAYbs/BAxEvU8ew== + +"@esbuild/win32-x64@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.3.tgz#b17a2171f9074df9e91bfb07ef99a892ac06412a" + integrity sha512-ICgUR+kPimx0vvRzf+N/7L7tVSQeE3BYY+NhHRHXS1kBuPO7z2+7ea2HbhDyZdTephgvNvKrlDDKUexuCVBVvg== + +"@jridgewell/gen-mapping@^0.3.5": + version "0.3.8" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz#4f0e06362e01362f823d348f1872b08f666d8142" + integrity sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA== + dependencies: + "@jridgewell/set-array" "^1.2.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.24" + +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + +"@jridgewell/set-array@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280" + integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== + +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a" + integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== + +"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": + version "0.3.25" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" + integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + +"@mjackson/node-fetch-server@^0.6.1": + version "0.6.1" + resolved "https://registry.yarnpkg.com/@mjackson/node-fetch-server/-/node-fetch-server-0.6.1.tgz#a576ba088093fa978dca2cb448924bf9b172bd21" + integrity sha512-9ZJnk/DJjt805uv5PPv11haJIW+HHf3YEEyVXv+8iLQxLD/iXA68FH220XoiTPBC4gCg5q+IMadDw8qPqlA5wg== + +"@playwright/test@^1.52.0": + version "1.52.0" + resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.52.0.tgz#267ec595b43a8f4fa5e444ea503689629e91a5b8" + integrity sha512-uh6W7sb55hl7D6vsAeA+V2p5JnlAqzhqFyF0VcJkKZXkgnFcVG9PziERRHQfPLfNGx1C292a4JqbWzhR8L4R1g== + dependencies: + playwright "1.52.0" + +"@polka/url@^1.0.0-next.24": + version "1.0.0-next.29" + resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.29.tgz#5a40109a1ab5f84d6fd8fc928b19f367cbe7e7b1" + integrity sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww== + +"@rollup/rollup-android-arm-eabi@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.0.tgz#d964ee8ce4d18acf9358f96adc408689b6e27fe3" + integrity sha512-+Fbls/diZ0RDerhE8kyC6hjADCXA1K4yVNlH0EYfd2XjyH0UGgzaQ8MlT0pCXAThfxv3QUAczHaL+qSv1E4/Cg== + +"@rollup/rollup-android-arm64@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.0.tgz#9b5e130ecc32a5fc1e96c09ff371743ee71a62d3" + integrity sha512-PPA6aEEsTPRz+/4xxAmaoWDqh67N7wFbgFUJGMnanCFs0TV99M0M8QhhaSCks+n6EbQoFvLQgYOGXxlMGQe/6w== + +"@rollup/rollup-darwin-arm64@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.0.tgz#ef439182c739b20b3c4398cfc03e3c1249ac8903" + integrity sha512-GwYOcOakYHdfnjjKwqpTGgn5a6cUX7+Ra2HeNj/GdXvO2VJOOXCiYYlRFU4CubFM67EhbmzLOmACKEfvp3J1kQ== + +"@rollup/rollup-darwin-x64@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.0.tgz#d7380c1531ab0420ca3be16f17018ef72dd3d504" + integrity sha512-CoLEGJ+2eheqD9KBSxmma6ld01czS52Iw0e2qMZNpPDlf7Z9mj8xmMemxEucinev4LgHalDPczMyxzbq+Q+EtA== + +"@rollup/rollup-freebsd-arm64@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.0.tgz#cbcbd7248823c6b430ce543c59906dd3c6df0936" + integrity sha512-r7yGiS4HN/kibvESzmrOB/PxKMhPTlz+FcGvoUIKYoTyGd5toHp48g1uZy1o1xQvybwwpqpe010JrcGG2s5nkg== + +"@rollup/rollup-freebsd-x64@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.0.tgz#96bf6ff875bab5219c3472c95fa6eb992586a93b" + integrity sha512-mVDxzlf0oLzV3oZOr0SMJ0lSDd3xC4CmnWJ8Val8isp9jRGl5Dq//LLDSPFrasS7pSm6m5xAcKaw3sHXhBjoRw== + +"@rollup/rollup-linux-arm-gnueabihf@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.0.tgz#d80cd62ce6d40f8e611008d8dbf03b5e6bbf009c" + integrity sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA== + +"@rollup/rollup-linux-arm-musleabihf@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.0.tgz#75440cfc1e8d0f87a239b4c31dfeaf4719b656b7" + integrity sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg== + +"@rollup/rollup-linux-arm64-gnu@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.0.tgz#ac527485ecbb619247fb08253ec8c551a0712e7c" + integrity sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg== + +"@rollup/rollup-linux-arm64-musl@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.0.tgz#74d2b5cb11cf714cd7d1682e7c8b39140e908552" + integrity sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ== + +"@rollup/rollup-linux-loongarch64-gnu@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.0.tgz#a0a310e51da0b5fea0e944b0abd4be899819aef6" + integrity sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg== + +"@rollup/rollup-linux-powerpc64le-gnu@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.0.tgz#4077e2862b0ac9f61916d6b474d988171bd43b83" + integrity sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw== + +"@rollup/rollup-linux-riscv64-gnu@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.0.tgz#5812a1a7a2f9581cbe12597307cc7ba3321cf2f3" + integrity sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA== + +"@rollup/rollup-linux-riscv64-musl@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.0.tgz#973aaaf4adef4531375c36616de4e01647f90039" + integrity sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ== + +"@rollup/rollup-linux-s390x-gnu@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.0.tgz#9bad59e907ba5bfcf3e9dbd0247dfe583112f70b" + integrity sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw== + +"@rollup/rollup-linux-x64-gnu@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.0.tgz#68b045a720bd9b4d905f462b997590c2190a6de0" + integrity sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ== + +"@rollup/rollup-linux-x64-musl@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.0.tgz#8e703e2c2ad19ba7b2cb3d8c3a4ad11d4ee3a282" + integrity sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw== + +"@rollup/rollup-win32-arm64-msvc@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.0.tgz#c5bee19fa670ff5da5f066be6a58b4568e9c650b" + integrity sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ== + +"@rollup/rollup-win32-ia32-msvc@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.0.tgz#846e02c17044bd922f6f483a3b4d36aac6e2b921" + integrity sha512-+m03kvI2f5syIqHXCZLPVYplP8pQch9JHyXKZ3AGMKlg8dCyr2PKHjwRLiW53LTrN/Nc3EqHOKxUxzoSPdKddA== + +"@rollup/rollup-win32-x64-msvc@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.0.tgz#fd92d31a2931483c25677b9c6698106490cbbc76" + integrity sha512-lpPE1cLfP5oPzVjKMx10pgBmKELQnFJXHgvtHCtuJWOv8MxqdEIMNtgHgBFf7Ea2/7EuVwa9fodWUfXAlXZLZQ== + +"@types/babel__core@^7.20.5": + version "7.20.5" + resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.5.tgz#3df15f27ba85319caa07ba08d0721889bb39c017" + integrity sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA== + dependencies: + "@babel/parser" "^7.20.7" + "@babel/types" "^7.20.7" + "@types/babel__generator" "*" + "@types/babel__template" "*" + "@types/babel__traverse" "*" + +"@types/babel__generator@*": + version "7.27.0" + resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.27.0.tgz#b5819294c51179957afaec341442f9341e4108a9" + integrity sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg== + dependencies: + "@babel/types" "^7.0.0" + +"@types/babel__template@*": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.4.4.tgz#5672513701c1b2199bc6dad636a9d7491586766f" + integrity sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + +"@types/babel__traverse@*": + version "7.20.7" + resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.20.7.tgz#968cdc2366ec3da159f61166428ee40f370e56c2" + integrity sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng== + dependencies: + "@babel/types" "^7.20.7" + +"@types/estree@1.0.7", "@types/estree@^1.0.7": + version "1.0.7" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.7.tgz#4158d3105276773d5b7695cd4834b1722e4f37a8" + integrity sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ== + +"@types/react-dom@latest": + version "19.1.2" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-19.1.2.tgz#bd1fe3b8c28a3a2e942f85314dcfb71f531a242f" + integrity sha512-XGJkWF41Qq305SKWEILa1O8vzhb3aOo3ogBlSmiqNko/WmRb6QIaweuZCXjKygVDXpzXb5wyxKTSOsmkuqj+Qw== + +"@types/react@latest": + version "19.1.2" + resolved "https://registry.yarnpkg.com/@types/react/-/react-19.1.2.tgz#11df86f66f188f212c90ecb537327ec68bfd593f" + integrity sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw== + dependencies: + csstype "^3.0.2" + +"@vitejs/plugin-react@^4.4.1": + version "4.4.1" + resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-4.4.1.tgz#d7d1e9c9616d7536b0953637edfee7c6cbe2fe0f" + integrity sha512-IpEm5ZmeXAP/osiBXVVP5KjFMzbWOonMs0NaQQl+xYnUAcq4oHUBsF2+p4MgKWG4YMmFYJU8A6sxRPuowllm6w== + dependencies: + "@babel/core" "^7.26.10" + "@babel/plugin-transform-react-jsx-self" "^7.25.9" + "@babel/plugin-transform-react-jsx-source" "^7.25.9" + "@types/babel__core" "^7.20.5" + react-refresh "^0.17.0" + +ansis@^3.17.0: + version "3.17.0" + resolved "https://registry.yarnpkg.com/ansis/-/ansis-3.17.0.tgz#fa8d9c2a93fe7d1177e0c17f9eeb562a58a832d7" + integrity sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg== + +birpc@^2.0.19: + version "2.3.0" + resolved "https://registry.yarnpkg.com/birpc/-/birpc-2.3.0.tgz#e5a402dc785ef952a2383ef3cfc075e0842f3e8c" + integrity sha512-ijbtkn/F3Pvzb6jHypHRyve2QApOCZDR25D/VnkY2G/lBNcXCTsnsCxgY4k4PkVB7zfwzYbY3O9Lcqe3xufS5g== + +browserslist@^4.24.0: + version "4.24.4" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.4.tgz#c6b2865a3f08bcb860a0e827389003b9fe686e4b" + integrity sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A== + dependencies: + caniuse-lite "^1.0.30001688" + electron-to-chromium "^1.5.73" + node-releases "^2.0.19" + update-browserslist-db "^1.1.1" + +bundle-name@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/bundle-name/-/bundle-name-4.1.0.tgz#f3b96b34160d6431a19d7688135af7cfb8797889" + integrity sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q== + dependencies: + run-applescript "^7.0.0" + +caniuse-lite@^1.0.30001688: + version "1.0.30001715" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001715.tgz#bd325a37ad366e3fe90827d74062807a34fbaeb2" + integrity sha512-7ptkFGMm2OAOgvZpwgA4yjQ5SQbrNVGdRjzH0pBdy1Fasvcr+KAeECmbCAECzTuDuoX0FCY8KzUxjf9+9kfZEw== + +convert-source-map@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" + integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== + +csstype@^3.0.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" + integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== + +debug@^4.1.0, debug@^4.3.1, debug@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a" + integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA== + dependencies: + ms "^2.1.3" + +default-browser-id@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/default-browser-id/-/default-browser-id-5.0.0.tgz#a1d98bf960c15082d8a3fa69e83150ccccc3af26" + integrity sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA== + +default-browser@^5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/default-browser/-/default-browser-5.2.1.tgz#7b7ba61204ff3e425b556869ae6d3e9d9f1712cf" + integrity sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg== + dependencies: + bundle-name "^4.1.0" + default-browser-id "^5.0.0" + +define-lazy-prop@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz#dbb19adfb746d7fc6d734a06b72f4a00d021255f" + integrity sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg== + +electron-to-chromium@^1.5.73: + version "1.5.142" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.142.tgz#1de55d0d19b24b07768c4bfc90f41bd7f248d043" + integrity sha512-Ah2HgkTu/9RhTDNThBtzu2Wirdy4DC9b0sMT1pUhbkZQ5U/iwmE+PHZX1MpjD5IkJCc2wSghgGG/B04szAx07w== + +error-stack-parser-es@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz#e6a1655dd12f39bb3a85bf4c7088187d78740327" + integrity sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA== + +esbuild@^0.25.0: + version "0.25.3" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.25.3.tgz#371f7cb41283e5b2191a96047a7a89562965a285" + integrity sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q== + optionalDependencies: + "@esbuild/aix-ppc64" "0.25.3" + "@esbuild/android-arm" "0.25.3" + "@esbuild/android-arm64" "0.25.3" + "@esbuild/android-x64" "0.25.3" + "@esbuild/darwin-arm64" "0.25.3" + "@esbuild/darwin-x64" "0.25.3" + "@esbuild/freebsd-arm64" "0.25.3" + "@esbuild/freebsd-x64" "0.25.3" + "@esbuild/linux-arm" "0.25.3" + "@esbuild/linux-arm64" "0.25.3" + "@esbuild/linux-ia32" "0.25.3" + "@esbuild/linux-loong64" "0.25.3" + "@esbuild/linux-mips64el" "0.25.3" + "@esbuild/linux-ppc64" "0.25.3" + "@esbuild/linux-riscv64" "0.25.3" + "@esbuild/linux-s390x" "0.25.3" + "@esbuild/linux-x64" "0.25.3" + "@esbuild/netbsd-arm64" "0.25.3" + "@esbuild/netbsd-x64" "0.25.3" + "@esbuild/openbsd-arm64" "0.25.3" + "@esbuild/openbsd-x64" "0.25.3" + "@esbuild/sunos-x64" "0.25.3" + "@esbuild/win32-arm64" "0.25.3" + "@esbuild/win32-ia32" "0.25.3" + "@esbuild/win32-x64" "0.25.3" + +escalade@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== + +fdir@^6.4.4: + version "6.4.4" + resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.4.4.tgz#1cfcf86f875a883e19a8fab53622cfe992e8d2f9" + integrity sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg== + +fsevents@2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + +fsevents@~2.3.2, fsevents@~2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +gensync@^1.0.0-beta.2: + version "1.0.0-beta.2" + resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" + integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== + +globals@^11.1.0: + version "11.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== + +is-docker@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-3.0.0.tgz#90093aa3106277d8a77a5910dbae71747e15a200" + integrity sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ== + +is-inside-container@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-inside-container/-/is-inside-container-1.0.0.tgz#e81fba699662eb31dbdaf26766a61d4814717ea4" + integrity sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA== + dependencies: + is-docker "^3.0.0" + +is-wsl@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-3.1.0.tgz#e1c657e39c10090afcbedec61720f6b924c3cbd2" + integrity sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw== + dependencies: + is-inside-container "^1.0.0" + +js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +jsesc@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d" + integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA== + +json5@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== + +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + +magic-string@^0.30.17: + version "0.30.17" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.17.tgz#450a449673d2460e5bbcfba9a61916a1714c7453" + integrity sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA== + dependencies: + "@jridgewell/sourcemap-codec" "^1.5.0" + +mrmime@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/mrmime/-/mrmime-2.0.1.tgz#bc3e87f7987853a54c9850eeb1f1078cd44adddc" + integrity sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ== + +ms@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +nanoid@^3.3.8: + version "3.3.11" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b" + integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w== + +node-releases@^2.0.19: + version "2.0.19" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.19.tgz#9e445a52950951ec4d177d843af370b411caf314" + integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw== + +ohash@^2.0.11: + version "2.0.11" + resolved "https://registry.yarnpkg.com/ohash/-/ohash-2.0.11.tgz#60b11e8cff62ca9dee88d13747a5baa145f5900b" + integrity sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ== + +open@^10.1.0: + version "10.1.2" + resolved "https://registry.yarnpkg.com/open/-/open-10.1.2.tgz#d5df40984755c9a9c3c93df8156a12467e882925" + integrity sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw== + dependencies: + default-browser "^5.2.1" + define-lazy-prop "^3.0.0" + is-inside-container "^1.0.0" + is-wsl "^3.1.0" + +pathe@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/pathe/-/pathe-2.0.3.tgz#3ecbec55421685b70a9da872b2cff3e1cbed1716" + integrity sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w== + +perfect-debounce@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/perfect-debounce/-/perfect-debounce-1.0.0.tgz#9c2e8bc30b169cc984a58b7d5b28049839591d2a" + integrity sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA== + +picocolors@^1.0.0, picocolors@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + +picomatch@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.2.tgz#77c742931e8f3b8820946c76cd0c1f13730d1dab" + integrity sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg== + +playwright-core@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.52.0.tgz#238f1f0c3edd4ebba0434ce3f4401900319a3dca" + integrity sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg== + +playwright@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.52.0.tgz#26cb9a63346651e1c54c8805acfd85683173d4bd" + integrity sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw== + dependencies: + playwright-core "1.52.0" + optionalDependencies: + fsevents "2.3.2" + +postcss@^8.5.3: + version "8.5.3" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.3.tgz#1463b6f1c7fb16fe258736cba29a2de35237eafb" + integrity sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A== + dependencies: + nanoid "^3.3.8" + picocolors "^1.1.1" + source-map-js "^1.2.1" + +react-dom@experimental: + version "0.0.0-experimental-197d6a04-20250424" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-0.0.0-experimental-197d6a04-20250424.tgz#ea244a8b4329b204cd308fd748b67637f6f03eea" + integrity sha512-k8W9K9/sQlkydX+gVPgmFVNVqhU5YwtWAtCshWZYpLbmMWp6Za3T0FaFYeGRBjsgpqh+4fRPtQG1MNU+gvrN8w== + dependencies: + scheduler "0.0.0-experimental-197d6a04-20250424" + +react-refresh@^0.17.0: + version "0.17.0" + resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.17.0.tgz#b7e579c3657f23d04eccbe4ad2e58a8ed51e7e53" + integrity sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ== + +"react-server-dom-vite@file:../../build/node_modules/react-server-dom-vite": + version "19.1.0" + +react@experimental: + version "0.0.0-experimental-197d6a04-20250424" + resolved "https://registry.yarnpkg.com/react/-/react-0.0.0-experimental-197d6a04-20250424.tgz#83398ac57a5322b2ca7aedea6645c94c4585261a" + integrity sha512-2BWhl33tYx2nVLahXumIvAmyMKQOuXKRj/DjV1gnBTRT31PV7ttMeOUDtR1Pwt648rI4ooCKyMV4wqxZotyOAA== + +rollup@^4.34.9: + version "4.40.0" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.40.0.tgz#13742a615f423ccba457554f006873d5a4de1920" + integrity sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w== + dependencies: + "@types/estree" "1.0.7" + optionalDependencies: + "@rollup/rollup-android-arm-eabi" "4.40.0" + "@rollup/rollup-android-arm64" "4.40.0" + "@rollup/rollup-darwin-arm64" "4.40.0" + "@rollup/rollup-darwin-x64" "4.40.0" + "@rollup/rollup-freebsd-arm64" "4.40.0" + "@rollup/rollup-freebsd-x64" "4.40.0" + "@rollup/rollup-linux-arm-gnueabihf" "4.40.0" + "@rollup/rollup-linux-arm-musleabihf" "4.40.0" + "@rollup/rollup-linux-arm64-gnu" "4.40.0" + "@rollup/rollup-linux-arm64-musl" "4.40.0" + "@rollup/rollup-linux-loongarch64-gnu" "4.40.0" + "@rollup/rollup-linux-powerpc64le-gnu" "4.40.0" + "@rollup/rollup-linux-riscv64-gnu" "4.40.0" + "@rollup/rollup-linux-riscv64-musl" "4.40.0" + "@rollup/rollup-linux-s390x-gnu" "4.40.0" + "@rollup/rollup-linux-x64-gnu" "4.40.0" + "@rollup/rollup-linux-x64-musl" "4.40.0" + "@rollup/rollup-win32-arm64-msvc" "4.40.0" + "@rollup/rollup-win32-ia32-msvc" "4.40.0" + "@rollup/rollup-win32-x64-msvc" "4.40.0" + fsevents "~2.3.2" + +rsc-html-stream@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/rsc-html-stream/-/rsc-html-stream-0.0.6.tgz#4943c36021b0724334a1325a5732c0c9fedaa771" + integrity sha512-oZUJ5AH0oDo9QywxD9yMY6N5Z3VwX2YfQg0FanNdCmvXmO0itTfv7BMkbMSwxg7JmBjYmefU8DTW0EcLsePPgQ== + +run-applescript@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/run-applescript/-/run-applescript-7.0.0.tgz#e5a553c2bffd620e169d276c1cd8f1b64778fbeb" + integrity sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A== + +scheduler@0.0.0-experimental-197d6a04-20250424: + version "0.0.0-experimental-197d6a04-20250424" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.0.0-experimental-197d6a04-20250424.tgz#ca47e90feec36d058d8db9878a8b138a0f1604c8" + integrity sha512-kfVmZoI1SPFz8g24coy9aJTc/tdPjLHhs34QT5HW0+jJl2Kg5pjL1tlzM9aztfxLORQ3vS4Okg5FejbB3EVBYg== + +semver@^6.3.1: + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + +sirv@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/sirv/-/sirv-3.0.1.tgz#32a844794655b727f9e2867b777e0060fbe07bf3" + integrity sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A== + dependencies: + "@polka/url" "^1.0.0-next.24" + mrmime "^2.0.0" + totalist "^3.0.0" + +source-map-js@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" + integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== + +tinyglobby@^0.2.13: + version "0.2.13" + resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.13.tgz#a0e46515ce6cbcd65331537e57484af5a7b2ff7e" + integrity sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw== + dependencies: + fdir "^6.4.4" + picomatch "^4.0.2" + +totalist@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/totalist/-/totalist-3.0.1.tgz#ba3a3d600c915b1a97872348f79c127475f6acf8" + integrity sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ== + +unplugin-utils@^0.2.4: + version "0.2.4" + resolved "https://registry.yarnpkg.com/unplugin-utils/-/unplugin-utils-0.2.4.tgz#56e4029a6906645a10644f8befc404b06d5d24d0" + integrity sha512-8U/MtpkPkkk3Atewj1+RcKIjb5WBimZ/WSLhhR3w6SsIj8XJuKTacSP8g+2JhfSGw0Cb125Y+2zA/IzJZDVbhA== + dependencies: + pathe "^2.0.2" + picomatch "^4.0.2" + +update-browserslist-db@^1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz#348377dd245216f9e7060ff50b15a1b740b75420" + integrity sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw== + dependencies: + escalade "^3.2.0" + picocolors "^1.1.1" + +vite-dev-rpc@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/vite-dev-rpc/-/vite-dev-rpc-1.0.7.tgz#e81567c4e5b7e7d8074af56a2120bd0ea61cbdb7" + integrity sha512-FxSTEofDbUi2XXujCA+hdzCDkXFG1PXktMjSk1efq9Qb5lOYaaM9zNSvKvPPF7645Bak79kSp1PTooMW2wktcA== + dependencies: + birpc "^2.0.19" + vite-hot-client "^2.0.4" + +vite-hot-client@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/vite-hot-client/-/vite-hot-client-2.0.4.tgz#db383e0337c758fbabf14dad26f9a1bcb9e9e175" + integrity sha512-W9LOGAyGMrbGArYJN4LBCdOC5+Zwh7dHvOHC0KmGKkJhsOzaKbpo/jEjpPKVHIW0/jBWj8RZG0NUxfgA8BxgAg== + +vite-plugin-inspect@^11.0.1: + version "11.0.1" + resolved "https://registry.yarnpkg.com/vite-plugin-inspect/-/vite-plugin-inspect-11.0.1.tgz#2c84d74cb65e712ef3dcc4f877072383c91a1507" + integrity sha512-aABw7eGTr9Cmbn9RAs76e0BztVUFDl6a2R+/IJXpoUZxjx5YHB0P+Em3ZTWzpIPZzuRj28tAMblvcUyhgJc4aQ== + dependencies: + ansis "^3.17.0" + debug "^4.4.0" + error-stack-parser-es "^1.0.5" + ohash "^2.0.11" + open "^10.1.0" + perfect-debounce "^1.0.0" + sirv "^3.0.1" + unplugin-utils "^0.2.4" + vite-dev-rpc "^1.0.7" + +vite@^6.3.3: + version "6.3.3" + resolved "https://registry.yarnpkg.com/vite/-/vite-6.3.3.tgz#497392c3f2243194e4dbf09ea83e9a3dddf49b88" + integrity sha512-5nXH+QsELbFKhsEfWLkHrvgRpTdGJzqOZ+utSdmPTvwHmvU6ITTm3xx+mRusihkcI8GeC7lCDyn3kDtiki9scw== + dependencies: + esbuild "^0.25.0" + fdir "^6.4.4" + picomatch "^4.0.2" + postcss "^8.5.3" + rollup "^4.34.9" + tinyglobby "^0.2.13" + optionalDependencies: + fsevents "~2.3.3" + +yallist@^3.0.2: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-vite.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-vite.js new file mode 100644 index 0000000000000..244dd8200fb21 --- /dev/null +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-vite.js @@ -0,0 +1,18 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export {default as rendererVersion} from 'shared/ReactVersion'; +export const rendererPackageName = 'react-server-dom-vite'; + +export * from 'react-client/src/ReactFlightClientStreamConfigWeb'; +export * from 'react-client/src/ReactClientConsoleConfigBrowser'; +export * from 'react-server-dom-vite/src/client/ReactFlightClientConfigBundlerVite'; +export * from 'react-server-dom-vite/src/client/ReactFlightClientConfigTargetViteBrowser'; +export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM'; +export const usedWithSSR = false; diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-edge-vite.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-edge-vite.js new file mode 100644 index 0000000000000..c20fd883daae2 --- /dev/null +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-edge-vite.js @@ -0,0 +1,18 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export {default as rendererVersion} from 'shared/ReactVersion'; +export const rendererPackageName = 'react-server-dom-vite'; + +export * from 'react-client/src/ReactFlightClientStreamConfigWeb'; +export * from 'react-client/src/ReactClientConsoleConfigServer'; +export * from 'react-server-dom-vite/src/client/ReactFlightClientConfigBundlerVite'; +export * from 'react-server-dom-vite/src/client/ReactFlightClientConfigTargetViteServer'; +export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM'; +export const usedWithSSR = true; diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-vite.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-vite.js new file mode 100644 index 0000000000000..2758839ac72c2 --- /dev/null +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-vite.js @@ -0,0 +1,18 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export {default as rendererVersion} from 'shared/ReactVersion'; +export const rendererPackageName = 'react-server-dom-vite'; + +export * from 'react-client/src/ReactFlightClientStreamConfigNode'; +export * from 'react-client/src/ReactClientConsoleConfigServer'; +export * from 'react-server-dom-vite/src/client/ReactFlightClientConfigBundlerVite'; +export * from 'react-server-dom-vite/src/client/ReactFlightClientConfigTargetViteServer'; +export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM'; +export const usedWithSSR = true; diff --git a/packages/react-server-dom-vite/README.md b/packages/react-server-dom-vite/README.md new file mode 100644 index 0000000000000..fd0771f4d7b41 --- /dev/null +++ b/packages/react-server-dom-vite/README.md @@ -0,0 +1,5 @@ +# react-server-dom-vite + +Experimental React Flight bindings for DOM using Vite. + +**Use it at your own risk.** diff --git a/packages/react-server-dom-vite/client.browser.js b/packages/react-server-dom-vite/client.browser.js new file mode 100644 index 0000000000000..945ceed7f394a --- /dev/null +++ b/packages/react-server-dom-vite/client.browser.js @@ -0,0 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export * from './src/client/ReactFlightDOMClientBrowser'; diff --git a/packages/react-server-dom-vite/client.edge.js b/packages/react-server-dom-vite/client.edge.js new file mode 100644 index 0000000000000..0ba1d6b6b0457 --- /dev/null +++ b/packages/react-server-dom-vite/client.edge.js @@ -0,0 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export * from './src/client/ReactFlightDOMClientEdge'; diff --git a/packages/react-server-dom-vite/client.js b/packages/react-server-dom-vite/client.js new file mode 100644 index 0000000000000..2dad5bb513872 --- /dev/null +++ b/packages/react-server-dom-vite/client.js @@ -0,0 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export * from './client.browser'; diff --git a/packages/react-server-dom-vite/client.node.js b/packages/react-server-dom-vite/client.node.js new file mode 100644 index 0000000000000..c2e364f42f133 --- /dev/null +++ b/packages/react-server-dom-vite/client.node.js @@ -0,0 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export * from './src/client/ReactFlightDOMClientNode'; diff --git a/packages/react-server-dom-vite/index.js b/packages/react-server-dom-vite/index.js new file mode 100644 index 0000000000000..744321cb065f9 --- /dev/null +++ b/packages/react-server-dom-vite/index.js @@ -0,0 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +throw new Error('Use react-server-dom-vite/client instead.'); diff --git a/packages/react-server-dom-vite/npm/client.browser.js b/packages/react-server-dom-vite/npm/client.browser.js new file mode 100644 index 0000000000000..4c0bf841ae5e7 --- /dev/null +++ b/packages/react-server-dom-vite/npm/client.browser.js @@ -0,0 +1,7 @@ +'use strict'; + +if (process.env.NODE_ENV === 'production') { + module.exports = require('./cjs/react-server-dom-vite-client.browser.production.js'); +} else { + module.exports = require('./cjs/react-server-dom-vite-client.browser.development.js'); +} diff --git a/packages/react-server-dom-vite/npm/client.edge.js b/packages/react-server-dom-vite/npm/client.edge.js new file mode 100644 index 0000000000000..e90cef7cd7ac2 --- /dev/null +++ b/packages/react-server-dom-vite/npm/client.edge.js @@ -0,0 +1,7 @@ +'use strict'; + +if (process.env.NODE_ENV === 'production') { + module.exports = require('./cjs/react-server-dom-vite-client.edge.production.js'); +} else { + module.exports = require('./cjs/react-server-dom-vite-client.edge.development.js'); +} diff --git a/packages/react-server-dom-vite/npm/client.js b/packages/react-server-dom-vite/npm/client.js new file mode 100644 index 0000000000000..89d93a7a7920f --- /dev/null +++ b/packages/react-server-dom-vite/npm/client.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = require('./client.browser'); diff --git a/packages/react-server-dom-vite/npm/client.node.js b/packages/react-server-dom-vite/npm/client.node.js new file mode 100644 index 0000000000000..6ee1e7e24fe0f --- /dev/null +++ b/packages/react-server-dom-vite/npm/client.node.js @@ -0,0 +1,7 @@ +'use strict'; + +if (process.env.NODE_ENV === 'production') { + module.exports = require('./cjs/react-server-dom-vite-client.node.production.js'); +} else { + module.exports = require('./cjs/react-server-dom-vite-client.node.development.js'); +} diff --git a/packages/react-server-dom-vite/npm/index.js b/packages/react-server-dom-vite/npm/index.js new file mode 100644 index 0000000000000..ef54db443ffa2 --- /dev/null +++ b/packages/react-server-dom-vite/npm/index.js @@ -0,0 +1,12 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +'use strict'; + +throw new Error('Use react-server-dom-parcel/client instead.'); diff --git a/packages/react-server-dom-vite/npm/server.browser.js b/packages/react-server-dom-vite/npm/server.browser.js new file mode 100644 index 0000000000000..988a0c1addbb9 --- /dev/null +++ b/packages/react-server-dom-vite/npm/server.browser.js @@ -0,0 +1,18 @@ +'use strict'; + +var s; +if (process.env.NODE_ENV === 'production') { + s = require('./cjs/react-server-dom-vite-server.browser.production.js'); +} else { + s = require('./cjs/react-server-dom-vite-server.browser.development.js'); +} + +exports.renderToReadableStream = s.renderToReadableStream; +exports.decodeReply = s.decodeReply; +exports.decodeAction = s.decodeAction; +exports.decodeFormState = s.decodeFormState; +exports.registerClientReference = s.registerClientReference; +exports.registerServerReference = s.registerServerReference; +exports.createTemporaryReferenceSet = s.createTemporaryReferenceSet; +exports.loadServerAction = s.loadServerAction; +exports.setPreloadModule = s.setPreloadModule; diff --git a/packages/react-server-dom-vite/npm/server.edge.js b/packages/react-server-dom-vite/npm/server.edge.js new file mode 100644 index 0000000000000..01aa30a9e9504 --- /dev/null +++ b/packages/react-server-dom-vite/npm/server.edge.js @@ -0,0 +1,19 @@ +'use strict'; + +var s; +if (process.env.NODE_ENV === 'production') { + s = require('./cjs/react-server-dom-vite-server.edge.production.js'); +} else { + s = require('./cjs/react-server-dom-vite-server.edge.development.js'); +} + +exports.renderToReadableStream = s.renderToReadableStream; +exports.decodeReply = s.decodeReply; +exports.decodeReplyFromAsyncIterable = s.decodeReplyFromAsyncIterable; +exports.decodeAction = s.decodeAction; +exports.decodeFormState = s.decodeFormState; +exports.registerClientReference = s.registerClientReference; +exports.registerServerReference = s.registerServerReference; +exports.createTemporaryReferenceSet = s.createTemporaryReferenceSet; +exports.loadServerAction = s.loadServerAction; +exports.setPreloadModule = s.setPreloadModule; diff --git a/packages/react-server-dom-vite/npm/server.js b/packages/react-server-dom-vite/npm/server.js new file mode 100644 index 0000000000000..13a632e641179 --- /dev/null +++ b/packages/react-server-dom-vite/npm/server.js @@ -0,0 +1,6 @@ +'use strict'; + +throw new Error( + 'The React Server Writer cannot be used outside a react-server environment. ' + + 'You must configure Node.js using the `--conditions react-server` flag.' +); diff --git a/packages/react-server-dom-vite/npm/server.node.js b/packages/react-server-dom-vite/npm/server.node.js new file mode 100644 index 0000000000000..2ab959626645d --- /dev/null +++ b/packages/react-server-dom-vite/npm/server.node.js @@ -0,0 +1,19 @@ +'use strict'; + +var s; +if (process.env.NODE_ENV === 'production') { + s = require('./cjs/react-server-dom-vite-server.node.production.js'); +} else { + s = require('./cjs/react-server-dom-vite-server.node.development.js'); +} + +exports.renderToPipeableStream = s.renderToPipeableStream; +exports.decodeReplyFromBusboy = s.decodeReplyFromBusboy; +exports.decodeReply = s.decodeReply; +exports.decodeAction = s.decodeAction; +exports.decodeFormState = s.decodeFormState; +exports.registerClientReference = s.registerClientReference; +exports.registerServerReference = s.registerServerReference; +exports.createTemporaryReferenceSet = s.createTemporaryReferenceSet; +exports.loadServerAction = s.loadServerAction; +exports.setPreloadModule = s.setPreloadModule; diff --git a/packages/react-server-dom-vite/npm/static.browser.js b/packages/react-server-dom-vite/npm/static.browser.js new file mode 100644 index 0000000000000..59bf007a421d0 --- /dev/null +++ b/packages/react-server-dom-vite/npm/static.browser.js @@ -0,0 +1,12 @@ +'use strict'; + +var s; +if (process.env.NODE_ENV === 'production') { + s = require('./cjs/react-server-dom-vite-server.browser.production.js'); +} else { + s = require('./cjs/react-server-dom-vite-server.browser.development.js'); +} + +if (s.unstable_prerender) { + exports.unstable_prerender = s.unstable_prerender; +} diff --git a/packages/react-server-dom-vite/npm/static.edge.js b/packages/react-server-dom-vite/npm/static.edge.js new file mode 100644 index 0000000000000..523d59623f1a9 --- /dev/null +++ b/packages/react-server-dom-vite/npm/static.edge.js @@ -0,0 +1,12 @@ +'use strict'; + +var s; +if (process.env.NODE_ENV === 'production') { + s = require('./cjs/react-server-dom-vite-server.edge.production.js'); +} else { + s = require('./cjs/react-server-dom-vite-server.edge.development.js'); +} + +if (s.unstable_prerender) { + exports.unstable_prerender = s.unstable_prerender; +} diff --git a/packages/react-server-dom-vite/npm/static.js b/packages/react-server-dom-vite/npm/static.js new file mode 100644 index 0000000000000..13a632e641179 --- /dev/null +++ b/packages/react-server-dom-vite/npm/static.js @@ -0,0 +1,6 @@ +'use strict'; + +throw new Error( + 'The React Server Writer cannot be used outside a react-server environment. ' + + 'You must configure Node.js using the `--conditions react-server` flag.' +); diff --git a/packages/react-server-dom-vite/npm/static.node.js b/packages/react-server-dom-vite/npm/static.node.js new file mode 100644 index 0000000000000..b445972037392 --- /dev/null +++ b/packages/react-server-dom-vite/npm/static.node.js @@ -0,0 +1,12 @@ +'use strict'; + +var s; +if (process.env.NODE_ENV === 'production') { + s = require('./cjs/react-server-dom-vite-server.node.production.js'); +} else { + s = require('./cjs/react-server-dom-vite-server.node.development.js'); +} + +if (s.unstable_prerenderToNodeStream) { + exports.unstable_prerenderToNodeStream = s.unstable_prerenderToNodeStream; +} diff --git a/packages/react-server-dom-vite/package.json b/packages/react-server-dom-vite/package.json new file mode 100644 index 0000000000000..238d606259d6a --- /dev/null +++ b/packages/react-server-dom-vite/package.json @@ -0,0 +1,85 @@ +{ + "name": "react-server-dom-vite", + "description": "React Server Components bindings for DOM using Vite. This is intended to be integrated into meta-frameworks. It is not intended to be imported directly.", + "version": "19.1.0", + "keywords": [ + "react" + ], + "homepage": "https://reactjs.org/", + "bugs": "https://github.com/facebook/react/issues", + "license": "MIT", + "files": [ + "LICENSE", + "README.md", + "index.js", + "client.js", + "client.browser.js", + "client.edge.js", + "client.node.js", + "server.js", + "server.browser.js", + "server.edge.js", + "server.node.js", + "static.js", + "static.browser.js", + "static.edge.js", + "static.node.js", + "cjs/" + ], + "exports": { + ".": "./index.js", + "./client": { + "workerd": "./client.edge.js", + "deno": "./client.edge.js", + "worker": "./client.edge.js", + "node": "./client.node.js", + "edge-light": "./client.edge.js", + "browser": "./client.browser.js", + "default": "./client.browser.js" + }, + "./client.browser": "./client.browser.js", + "./client.edge": "./client.edge.js", + "./client.node": "./client.node.js", + "./server": { + "react-server": { + "workerd": "./server.edge.js", + "deno": "./server.browser.js", + "node": "./server.node.js", + "edge-light": "./server.edge.js", + "browser": "./server.browser.js" + }, + "default": "./server.js" + }, + "./server.browser": "./server.browser.js", + "./server.edge": "./server.edge.js", + "./server.node": "./server.node.js", + "./static": { + "react-server": { + "workerd": "./static.edge.js", + "deno": "./static.browser.js", + "node": "./static.node.js", + "edge-light": "./static.edge.js", + "browser": "./static.browser.js" + }, + "default": "./static.js" + }, + "./static.browser": "./static.browser.js", + "./static.edge": "./static.edge.js", + "./static.node": "./static.node.js", + "./src/*": "./src/*.js", + "./package.json": "./package.json" + }, + "main": "index.js", + "repository": { + "type": "git", + "url": "https://github.com/facebook/react.git", + "directory": "packages/react-server-dom-vite" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "react": "^19.1.0", + "react-dom": "^19.1.0" + } +} diff --git a/packages/react-server-dom-vite/server.browser.js b/packages/react-server-dom-vite/server.browser.js new file mode 100644 index 0000000000000..b7d59b2ab3d56 --- /dev/null +++ b/packages/react-server-dom-vite/server.browser.js @@ -0,0 +1,19 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export { + renderToReadableStream, + decodeReply, + decodeAction, + decodeFormState, + registerClientReference, + registerServerReference, + createTemporaryReferenceSet, + loadServerAction, +} from './src/server/react-flight-dom-server.browser'; diff --git a/packages/react-server-dom-vite/server.edge.js b/packages/react-server-dom-vite/server.edge.js new file mode 100644 index 0000000000000..d50448cc19bbc --- /dev/null +++ b/packages/react-server-dom-vite/server.edge.js @@ -0,0 +1,20 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export { + renderToReadableStream, + decodeReply, + decodeReplyFromAsyncIterable, + decodeAction, + decodeFormState, + registerClientReference, + registerServerReference, + createTemporaryReferenceSet, + loadServerAction, +} from './src/server/react-flight-dom-server.edge'; diff --git a/packages/react-server-dom-vite/server.js b/packages/react-server-dom-vite/server.js new file mode 100644 index 0000000000000..83d8b8a017ff2 --- /dev/null +++ b/packages/react-server-dom-vite/server.js @@ -0,0 +1,13 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +throw new Error( + 'The React Server cannot be used outside a react-server environment. ' + + 'You must configure Node.js using the `--conditions react-server` flag.', +); diff --git a/packages/react-server-dom-vite/server.node.js b/packages/react-server-dom-vite/server.node.js new file mode 100644 index 0000000000000..21a6ab96808cd --- /dev/null +++ b/packages/react-server-dom-vite/server.node.js @@ -0,0 +1,20 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export { + renderToPipeableStream, + decodeReplyFromBusboy, + decodeReply, + decodeAction, + decodeFormState, + registerClientReference, + registerServerReference, + createTemporaryReferenceSet, + loadServerAction, +} from './src/server/react-flight-dom-server.node'; diff --git a/packages/react-server-dom-vite/src/ReactFlightViteReferences.js b/packages/react-server-dom-vite/src/ReactFlightViteReferences.js new file mode 100644 index 0000000000000..70c633dae3fac --- /dev/null +++ b/packages/react-server-dom-vite/src/ReactFlightViteReferences.js @@ -0,0 +1,123 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {ReactClientValue} from 'react-server/src/ReactFlightServer'; + +export type ServerReference = T & { + $$typeof: symbol, + $$id: string, + $$bound: null | Array, + $$location?: Error, +}; + +// eslint-disable-next-line no-unused-vars +export type ClientReference = { + $$typeof: symbol, + $$id: string, + $$name: string, +}; + +const CLIENT_REFERENCE_TAG = Symbol.for('react.client.reference'); +const SERVER_REFERENCE_TAG = Symbol.for('react.server.reference'); + +export function isClientReference(reference: Object): boolean { + return reference.$$typeof === CLIENT_REFERENCE_TAG; +} + +export function isServerReference(reference: Object): boolean { + return reference.$$typeof === SERVER_REFERENCE_TAG; +} + +export function registerClientReference( + proxyImplementation: any, + id: string, + exportName: string, +): ClientReference { + return Object.defineProperties(proxyImplementation, { + $$typeof: {value: CLIENT_REFERENCE_TAG}, + $$id: {value: id + '#' + exportName}, + }); +} + +// $FlowFixMe[method-unbinding] +const FunctionBind = Function.prototype.bind; +// $FlowFixMe[method-unbinding] +const ArraySlice = Array.prototype.slice; +function bind(this: ServerReference): any { + // $FlowFixMe[prop-missing] + const newFn = FunctionBind.apply(this, arguments); + if (this.$$typeof === SERVER_REFERENCE_TAG) { + if (__DEV__) { + const thisBind = arguments[0]; + if (thisBind != null) { + console.error( + 'Cannot bind "this" of a Server Action. Pass null or undefined as the first argument to .bind().', + ); + } + } + const args = ArraySlice.call(arguments, 1); + const $$typeof = {value: SERVER_REFERENCE_TAG}; + const $$id = {value: this.$$id}; + const $$bound = {value: this.$$bound ? this.$$bound.concat(args) : args}; + return Object.defineProperties( + (newFn: any), + __DEV__ + ? { + $$typeof, + $$id, + $$bound, + $$location: { + value: this.$$location, + configurable: true, + }, + bind: {value: bind, configurable: true}, + } + : { + $$typeof, + $$id, + $$bound, + bind: {value: bind, configurable: true}, + }, + ); + } + return newFn; +} + +export function registerServerReference( + reference: ServerReference, + id: string, + exportName: string, +): ServerReference { + const $$typeof = {value: SERVER_REFERENCE_TAG}; + const $$id = { + value: id + '#' + exportName, + configurable: true, + }; + const $$bound = {value: null, configurable: true}; + return Object.defineProperties( + (reference: any), + __DEV__ + ? { + $$typeof, + $$id, + $$bound, + $$location: { + value: Error('react-stack-top-frame'), + configurable: true, + }, + bind: {value: bind, configurable: true}, + } + : { + $$typeof, + $$id, + $$bound, + bind: {value: bind, configurable: true}, + }, + ); +} diff --git a/packages/react-server-dom-vite/src/client/ReactFlightClientConfigBundlerVite.js b/packages/react-server-dom-vite/src/client/ReactFlightClientConfigBundlerVite.js new file mode 100644 index 0000000000000..e7f458be33757 --- /dev/null +++ b/packages/react-server-dom-vite/src/client/ReactFlightClientConfigBundlerVite.js @@ -0,0 +1,112 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type { + FulfilledThenable, + RejectedThenable, + Thenable, +} from 'shared/ReactTypes'; + +import type {ImportMetadata} from '../shared/ReactFlightImportMetadata'; + +import {ID, NAME} from '../shared/ReactFlightImportMetadata'; + +export type ClientManifest = null; +export type ServerManifest = null; +export type SSRModuleMap = null; +export type ModuleLoading = null; +export type ServerConsumerModuleMap = null; +export type ServerReferenceId = string; + +export opaque type ClientReferenceMetadata = ImportMetadata; + +// eslint-disable-next-line no-unused-vars +export opaque type ClientReference = [ + /* id */ string, + /* name */ string, + /* promise */ Thenable | null, +]; + +export function prepareDestinationForModule( + moduleLoading: ModuleLoading, + nonce: ?string, + metadata: ClientReferenceMetadata, +) {} + +export function resolveClientReference( + bundlerConfig: ServerConsumerModuleMap, + metadata: ClientReferenceMetadata, +): ClientReference { + return [metadata[ID], metadata[NAME], null]; +} + +export function resolveServerReference( + bundlerConfig: ServerManifest, + ref: ServerReferenceId, +): ClientReference { + const idx = ref.lastIndexOf('#'); + const id = ref.slice(0, idx); + const name = ref.slice(idx + 1); + return [id, name, null]; +} + +const asyncModuleCache: Map> = new Map(); + +export function preloadModule( + metadata: ClientReference, +): null | Thenable { + // cache same module id for build. + if (!__DEV__) { + const existingPromise = asyncModuleCache.get(metadata[ID]); + if (existingPromise) { + metadata[2] = existingPromise; + if (existingPromise.status === 'fulfilled') { + return null; + } + return existingPromise; + } + } + const promise = preloadModuleFn(metadata[ID]); + promise.then( + value => { + const fulfilledThenable: FulfilledThenable = (promise: any); + fulfilledThenable.status = 'fulfilled'; + fulfilledThenable.value = value; + }, + reason => { + const rejectedThenable: RejectedThenable = (promise: any); + rejectedThenable.status = 'rejected'; + rejectedThenable.reason = reason; + }, + ); + metadata[2] = promise; + if (!__DEV__) { + asyncModuleCache.set(metadata[ID], promise); + } + return promise; +} + +export function requireModule(metadata: ClientReference): T { + const promise = metadata[2]; + if (promise) { + if (promise.status === 'fulfilled') { + return promise.value[metadata[NAME]]; + } + if (promise.status === 'rejected') { + throw promise.reason; + } + } + throw new Error('invalid reference'); +} + +let preloadModuleFn: any; + +export function setPreloadModule(fn: any) { + preloadModuleFn = fn; +} diff --git a/packages/react-server-dom-vite/src/client/ReactFlightClientConfigTargetViteBrowser.js b/packages/react-server-dom-vite/src/client/ReactFlightClientConfigTargetViteBrowser.js new file mode 100644 index 0000000000000..ae0ee6efcba96 --- /dev/null +++ b/packages/react-server-dom-vite/src/client/ReactFlightClientConfigTargetViteBrowser.js @@ -0,0 +1,18 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {ModuleLoading} from './ReactFlightClientConfigBundlerVite'; + +export function prepareDestinationWithChunks( + moduleLoading: ModuleLoading, + bundles: Array, + nonce: ?string, +) { + // In the browser we don't need to prepare our destination since the browser is the Destination +} diff --git a/packages/react-server-dom-vite/src/client/ReactFlightClientConfigTargetViteServer.js b/packages/react-server-dom-vite/src/client/ReactFlightClientConfigTargetViteServer.js new file mode 100644 index 0000000000000..025feb0df5d0a --- /dev/null +++ b/packages/react-server-dom-vite/src/client/ReactFlightClientConfigTargetViteServer.js @@ -0,0 +1,16 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {ModuleLoading} from './ReactFlightClientConfigBundlerVite'; + +export function prepareDestinationWithChunks( + moduleLoading: ModuleLoading, + chunks: Array, + nonce: ?string, +) {} diff --git a/packages/react-server-dom-vite/src/client/ReactFlightDOMClientBrowser.js b/packages/react-server-dom-vite/src/client/ReactFlightDOMClientBrowser.js new file mode 100644 index 0000000000000..2cba537725d61 --- /dev/null +++ b/packages/react-server-dom-vite/src/client/ReactFlightDOMClientBrowser.js @@ -0,0 +1,169 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {Thenable} from 'shared/ReactTypes.js'; +import type { + Response as FlightResponse, + FindSourceMapURLCallback, +} from 'react-client/src/ReactFlightClient'; +import type {ReactServerValue} from 'react-client/src/ReactFlightReplyClient'; + +import { + createResponse, + getRoot, + reportGlobalError, + processBinaryChunk, + close, + injectIntoDevTools, +} from 'react-client/src/ReactFlightClient'; + +import {processReply} from 'react-client/src/ReactFlightReplyClient'; + +export { + createServerReference, + registerServerReference, +} from 'react-client/src/ReactFlightReplyClient'; + +import type {TemporaryReferenceSet} from 'react-client/src/ReactFlightTemporaryReferences'; + +export {createTemporaryReferenceSet} from 'react-client/src/ReactFlightTemporaryReferences'; +export type {TemporaryReferenceSet}; + +export {setPreloadModule} from '../client/ReactFlightClientConfigBundlerVite'; + +function startReadingFromStream( + response: FlightResponse, + stream: ReadableStream, +): void { + const reader = stream.getReader(); + function progress({ + done, + value, + }: { + done: boolean, + value: ?any, + ... + }): void | Promise { + if (done) { + close(response); + return; + } + const buffer: Uint8Array = (value: any); + processBinaryChunk(response, buffer); + return reader.read().then(progress).catch(error); + } + function error(e: any) { + reportGlobalError(response, e); + } + reader.read().then(progress).catch(error); +} + +type CallServerCallback = (string, args: A) => Promise; + +export type Options = { + callServer?: CallServerCallback, + temporaryReferences?: TemporaryReferenceSet, + findSourceMapURL?: FindSourceMapURLCallback, + replayConsoleLogs?: boolean, + environmentName?: string, +}; + +export function createFromReadableStream( + stream: ReadableStream, + options?: Options, +): Thenable { + const response: FlightResponse = createResponse( + null, // bundlerConfig + null, // serverReferenceConfig + null, // moduleLoading + options && options.callServer ? options.callServer : undefined, + undefined, // encodeFormAction + undefined, // nonce + options && options.temporaryReferences + ? options.temporaryReferences + : undefined, + __DEV__ && options && options.findSourceMapURL + ? options.findSourceMapURL + : undefined, + __DEV__ ? (options ? options.replayConsoleLogs !== false : true) : false, // defaults to true + __DEV__ && options && options.environmentName + ? options.environmentName + : undefined, + ); + startReadingFromStream(response, stream); + return getRoot(response); +} + +export function createFromFetch( + promiseForResponse: Promise, + options?: Options, +): Thenable { + const response: FlightResponse = createResponse( + null, // bundlerConfig + null, // serverReferenceConfig + null, // moduleLoading + options && options.callServer ? options.callServer : undefined, + undefined, // encodeFormAction + undefined, // nonce + options && options.temporaryReferences + ? options.temporaryReferences + : undefined, + __DEV__ && options && options.findSourceMapURL + ? options.findSourceMapURL + : undefined, + __DEV__ ? (options ? options.replayConsoleLogs !== false : true) : false, // defaults to true + __DEV__ && options && options.environmentName + ? options.environmentName + : undefined, + ); + promiseForResponse.then( + function (r) { + startReadingFromStream(response, (r.body: any)); + }, + function (e) { + reportGlobalError(response, e); + }, + ); + return getRoot(response); +} + +export function encodeReply( + value: ReactServerValue, + options?: {temporaryReferences?: TemporaryReferenceSet, signal?: AbortSignal}, +): Promise< + string | URLSearchParams | FormData, +> /* We don't use URLSearchParams yet but maybe */ { + return new Promise((resolve, reject) => { + const abort = processReply( + value, + '', // formFieldPrefix + options && options.temporaryReferences + ? options.temporaryReferences + : undefined, + resolve, + reject, + ); + if (options && options.signal) { + const signal = options.signal; + if (signal.aborted) { + abort((signal: any).reason); + } else { + const listener = () => { + abort((signal: any).reason); + signal.removeEventListener('abort', listener); + }; + signal.addEventListener('abort', listener); + } + } + }); +} + +if (__DEV__) { + injectIntoDevTools(); +} diff --git a/packages/react-server-dom-vite/src/client/ReactFlightDOMClientEdge.js b/packages/react-server-dom-vite/src/client/ReactFlightDOMClientEdge.js new file mode 100644 index 0000000000000..3e9f1db1d390b --- /dev/null +++ b/packages/react-server-dom-vite/src/client/ReactFlightDOMClientEdge.js @@ -0,0 +1,172 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {Thenable, ReactCustomFormAction} from 'shared/ReactTypes.js'; + +import type { + Response as FlightResponse, + FindSourceMapURLCallback, +} from 'react-client/src/ReactFlightClient'; +import type {ReactServerValue} from 'react-client/src/ReactFlightReplyClient'; + +import { + createResponse, + getRoot, + reportGlobalError, + processBinaryChunk, + close, +} from 'react-client/src/ReactFlightClient'; + +import { + processReply, + createServerReference as createServerReferenceImpl, +} from 'react-client/src/ReactFlightReplyClient'; + +export {registerServerReference} from 'react-client/src/ReactFlightReplyClient'; + +import type {TemporaryReferenceSet} from 'react-client/src/ReactFlightTemporaryReferences'; + +export {createTemporaryReferenceSet} from 'react-client/src/ReactFlightTemporaryReferences'; +export type {TemporaryReferenceSet}; + +export {setPreloadModule} from '../client/ReactFlightClientConfigBundlerVite'; +import type {ModuleLoading} from '../client/ReactFlightClientConfigBundlerVite'; + +function noServerCall() { + throw new Error( + 'Server Functions cannot be called during initial render. ' + + 'This would create a fetch waterfall. Try to use a Server Component ' + + 'to pass data to Client Components instead.', + ); +} + +export function createServerReference, T>( + id: string, +): (...A) => Promise { + return createServerReferenceImpl(id, noServerCall); +} + +type EncodeFormActionCallback = ( + id: any, + args: Promise, +) => ReactCustomFormAction; + +export type Options = { + moduleLoading?: ModuleLoading, + nonce?: string, + encodeFormAction?: EncodeFormActionCallback, + temporaryReferences?: TemporaryReferenceSet, + findSourceMapURL?: FindSourceMapURLCallback, + replayConsoleLogs?: boolean, + environmentName?: string, +}; + +function createResponseFromOptions(options?: Options) { + return createResponse( + options && options.nonce ? {nonce: options && options.nonce} : null, // bundlerConfig + null, // serverReferenceConfig + options && options.moduleLoading ? options.moduleLoading : null, + noServerCall, + options ? options.encodeFormAction : undefined, + options && typeof options.nonce === 'string' ? options.nonce : undefined, + options && options.temporaryReferences + ? options.temporaryReferences + : undefined, + __DEV__ && options && options.findSourceMapURL + ? options.findSourceMapURL + : undefined, + __DEV__ && options ? options.replayConsoleLogs === true : false, // defaults to false + __DEV__ && options && options.environmentName + ? options.environmentName + : undefined, + ); +} + +function startReadingFromStream( + response: FlightResponse, + stream: ReadableStream, +): void { + const reader = stream.getReader(); + function progress({ + done, + value, + }: { + done: boolean, + value: ?any, + ... + }): void | Promise { + if (done) { + close(response); + return; + } + const buffer: Uint8Array = (value: any); + processBinaryChunk(response, buffer); + return reader.read().then(progress).catch(error); + } + function error(e: any) { + reportGlobalError(response, e); + } + reader.read().then(progress).catch(error); +} + +export function createFromReadableStream( + stream: ReadableStream, + options?: Options, +): Thenable { + const response: FlightResponse = createResponseFromOptions(options); + startReadingFromStream(response, stream); + return getRoot(response); +} + +export function createFromFetch( + promiseForResponse: Promise, + options?: Options, +): Thenable { + const response: FlightResponse = createResponseFromOptions(options); + promiseForResponse.then( + function (r) { + startReadingFromStream(response, (r.body: any)); + }, + function (e) { + reportGlobalError(response, e); + }, + ); + return getRoot(response); +} + +export function encodeReply( + value: ReactServerValue, + options?: {temporaryReferences?: TemporaryReferenceSet, signal?: AbortSignal}, +): Promise< + string | URLSearchParams | FormData, +> /* We don't use URLSearchParams yet but maybe */ { + return new Promise((resolve, reject) => { + const abort = processReply( + value, + '', + options && options.temporaryReferences + ? options.temporaryReferences + : undefined, + resolve, + reject, + ); + if (options && options.signal) { + const signal = options.signal; + if (signal.aborted) { + abort((signal: any).reason); + } else { + const listener = () => { + abort((signal: any).reason); + signal.removeEventListener('abort', listener); + }; + signal.addEventListener('abort', listener); + } + } + }); +} diff --git a/packages/react-server-dom-vite/src/client/ReactFlightDOMClientNode.js b/packages/react-server-dom-vite/src/client/ReactFlightDOMClientNode.js new file mode 100644 index 0000000000000..40fdfb4022e6e --- /dev/null +++ b/packages/react-server-dom-vite/src/client/ReactFlightDOMClientNode.js @@ -0,0 +1,88 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {Thenable, ReactCustomFormAction} from 'shared/ReactTypes.js'; +import type { + Response, + FindSourceMapURLCallback, +} from 'react-client/src/ReactFlightClient'; +import type {Readable} from 'stream'; + +import { + createResponse, + getRoot, + reportGlobalError, + processBinaryChunk, + close, +} from 'react-client/src/ReactFlightClient'; + +import {createServerReference as createServerReferenceImpl} from 'react-client/src/ReactFlightReplyClient'; +import type {ModuleLoading} from './ReactFlightClientConfigBundlerVite'; + +export {registerServerReference} from 'react-client/src/ReactFlightReplyClient'; + +export {setPreloadModule} from '../client/ReactFlightClientConfigBundlerVite'; + +function noServerCall() { + throw new Error( + 'Server Functions cannot be called during initial render. ' + + 'This would create a fetch waterfall. Try to use a Server Component ' + + 'to pass data to Client Components instead.', + ); +} + +export function createServerReference, T>( + id: string, +): (...A) => Promise { + return createServerReferenceImpl(id, noServerCall); +} + +type EncodeFormActionCallback = ( + id: any, + args: Promise, +) => ReactCustomFormAction; + +export type Options = { + moduleLoading?: ModuleLoading, + nonce?: string, + encodeFormAction?: EncodeFormActionCallback, + findSourceMapURL?: FindSourceMapURLCallback, + replayConsoleLogs?: boolean, + environmentName?: string, +}; + +export function createFromNodeStream( + stream: Readable, + options?: Options, +): Thenable { + const response: Response = createResponse( + options && options.nonce ? {nonce: options && options.nonce} : null, // bundlerConfig + null, // serverReferenceConfig + options && options.moduleLoading ? options.moduleLoading : null, + noServerCall, + options ? options.encodeFormAction : undefined, + options && typeof options.nonce === 'string' ? options.nonce : undefined, + undefined, // TODO: If encodeReply is supported, this should support temporaryReferences + __DEV__ && options && options.findSourceMapURL + ? options.findSourceMapURL + : undefined, + __DEV__ && options ? options.replayConsoleLogs === true : false, // defaults to false + __DEV__ && options && options.environmentName + ? options.environmentName + : undefined, + ); + stream.on('data', chunk => { + processBinaryChunk(response, chunk); + }); + stream.on('error', error => { + reportGlobalError(response, error); + }); + stream.on('end', () => close(response)); + return getRoot(response); +} diff --git a/packages/react-server-dom-vite/src/server/ReactFlightDOMServerBrowser.js b/packages/react-server-dom-vite/src/server/ReactFlightDOMServerBrowser.js new file mode 100644 index 0000000000000..2c9bb5b5e7251 --- /dev/null +++ b/packages/react-server-dom-vite/src/server/ReactFlightDOMServerBrowser.js @@ -0,0 +1,209 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {ReactClientValue} from 'react-server/src/ReactFlightServer'; +import type {ReactFormState, Thenable} from 'shared/ReactTypes'; +import { + preloadModule, + requireModule, + resolveServerReference, + type ServerReferenceId, +} from '../client/ReactFlightClientConfigBundlerVite'; + +import { + createRequest, + createPrerenderRequest, + startWork, + startFlowing, + stopFlowing, + abort, +} from 'react-server/src/ReactFlightServer'; + +import { + createResponse, + close, + getRoot, +} from 'react-server/src/ReactFlightReplyServer'; + +import { + decodeAction as decodeActionImpl, + decodeFormState as decodeFormStateImpl, +} from 'react-server/src/ReactFlightActionServer'; + +export { + registerClientReference, + registerServerReference, +} from '../ReactFlightViteReferences'; + +import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences'; + +export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences'; +export type {TemporaryReferenceSet}; + +type Options = { + environmentName?: string | (() => string), + filterStackFrame?: (url: string, functionName: string) => boolean, + identifierPrefix?: string, + signal?: AbortSignal, + temporaryReferences?: TemporaryReferenceSet, + onError?: (error: mixed) => void, + onPostpone?: (reason: string) => void, +}; + +export function renderToReadableStream( + model: ReactClientValue, + options?: Options, +): ReadableStream { + const request = createRequest( + model, + null, + options ? options.onError : undefined, + options ? options.identifierPrefix : undefined, + options ? options.onPostpone : undefined, + options ? options.temporaryReferences : undefined, + __DEV__ && options ? options.environmentName : undefined, + __DEV__ && options ? options.filterStackFrame : undefined, + ); + if (options && options.signal) { + const signal = options.signal; + if (signal.aborted) { + abort(request, (signal: any).reason); + } else { + const listener = () => { + abort(request, (signal: any).reason); + signal.removeEventListener('abort', listener); + }; + signal.addEventListener('abort', listener); + } + } + const stream = new ReadableStream( + { + type: 'bytes', + start: (controller): ?Promise => { + startWork(request); + }, + pull: (controller): ?Promise => { + startFlowing(request, controller); + }, + cancel: (reason): ?Promise => { + stopFlowing(request); + abort(request, reason); + }, + }, + // $FlowFixMe[prop-missing] size() methods are not allowed on byte streams. + {highWaterMark: 0}, + ); + return stream; +} + +type StaticResult = { + prelude: ReadableStream, +}; + +export function prerender( + model: ReactClientValue, + options?: Options, +): Promise { + return new Promise((resolve, reject) => { + const onFatalError = reject; + function onAllReady() { + const stream = new ReadableStream( + { + type: 'bytes', + start: (controller): ?Promise => { + startWork(request); + }, + pull: (controller): ?Promise => { + startFlowing(request, controller); + }, + cancel: (reason): ?Promise => { + stopFlowing(request); + abort(request, reason); + }, + }, + // $FlowFixMe[prop-missing] size() methods are not allowed on byte streams. + {highWaterMark: 0}, + ); + resolve({prelude: stream}); + } + const request = createPrerenderRequest( + model, + null, + onAllReady, + onFatalError, + options ? options.onError : undefined, + options ? options.identifierPrefix : undefined, + options ? options.onPostpone : undefined, + options ? options.temporaryReferences : undefined, + __DEV__ && options ? options.environmentName : undefined, + __DEV__ && options ? options.filterStackFrame : undefined, + ); + if (options && options.signal) { + const signal = options.signal; + if (signal.aborted) { + const reason = (signal: any).reason; + abort(request, reason); + } else { + const listener = () => { + const reason = (signal: any).reason; + abort(request, reason); + signal.removeEventListener('abort', listener); + }; + signal.addEventListener('abort', listener); + } + } + startWork(request); + }); +} + +const serverManifest = null; + +export function decodeReply( + body: string | FormData, + options?: {temporaryReferences?: TemporaryReferenceSet}, +): Thenable { + if (typeof body === 'string') { + const form = new FormData(); + form.append('0', body); + body = form; + } + const response = createResponse( + serverManifest, + '', + options ? options.temporaryReferences : undefined, + body, + ); + const root = getRoot(response); + close(response); + return root; +} + +export function decodeAction(body: FormData): Promise<() => T> | null { + return decodeActionImpl(body, serverManifest); +} + +export function decodeFormState( + actionResult: S, + body: FormData, +): Promise | null> { + return decodeFormStateImpl(actionResult, body, serverManifest); +} + +export function loadServerAction any>(id: string): Promise { + const reference = resolveServerReference(serverManifest, id); + return Promise.resolve(reference) + .then(() => preloadModule(reference)) + .then(() => { + const fn = requireModule(reference); + if (typeof fn !== 'function') { + throw new Error('Server actions must be functions'); + } + return fn; + }); +} diff --git a/packages/react-server-dom-vite/src/server/ReactFlightDOMServerEdge.js b/packages/react-server-dom-vite/src/server/ReactFlightDOMServerEdge.js new file mode 100644 index 0000000000000..666c3457c8982 --- /dev/null +++ b/packages/react-server-dom-vite/src/server/ReactFlightDOMServerEdge.js @@ -0,0 +1,258 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {ReactClientValue} from 'react-server/src/ReactFlightServer'; +import type {ReactFormState, Thenable} from 'shared/ReactTypes'; +import { + preloadModule, + requireModule, + resolveServerReference, + type ServerReferenceId, +} from '../client/ReactFlightClientConfigBundlerVite'; + +import {ASYNC_ITERATOR} from 'shared/ReactSymbols'; + +import { + createRequest, + createPrerenderRequest, + startWork, + startFlowing, + stopFlowing, + abort, +} from 'react-server/src/ReactFlightServer'; + +import { + createResponse, + close, + getRoot, + reportGlobalError, + resolveField, + resolveFile, +} from 'react-server/src/ReactFlightReplyServer'; + +import { + decodeAction as decodeActionImpl, + decodeFormState as decodeFormStateImpl, +} from 'react-server/src/ReactFlightActionServer'; + +export { + registerClientReference, + registerServerReference, +} from '../ReactFlightViteReferences'; + +import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences'; + +export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences'; +export type {TemporaryReferenceSet}; + +type Options = { + environmentName?: string | (() => string), + filterStackFrame?: (url: string, functionName: string) => boolean, + identifierPrefix?: string, + signal?: AbortSignal, + temporaryReferences?: TemporaryReferenceSet, + onError?: (error: mixed) => void, + onPostpone?: (reason: string) => void, +}; + +export function renderToReadableStream( + model: ReactClientValue, + options?: Options, +): ReadableStream { + const request = createRequest( + model, + null, + options ? options.onError : undefined, + options ? options.identifierPrefix : undefined, + options ? options.onPostpone : undefined, + options ? options.temporaryReferences : undefined, + __DEV__ && options ? options.environmentName : undefined, + __DEV__ && options ? options.filterStackFrame : undefined, + ); + if (options && options.signal) { + const signal = options.signal; + if (signal.aborted) { + abort(request, (signal: any).reason); + } else { + const listener = () => { + abort(request, (signal: any).reason); + signal.removeEventListener('abort', listener); + }; + signal.addEventListener('abort', listener); + } + } + const stream = new ReadableStream( + { + type: 'bytes', + start: (controller): ?Promise => { + startWork(request); + }, + pull: (controller): ?Promise => { + startFlowing(request, controller); + }, + cancel: (reason): ?Promise => { + stopFlowing(request); + abort(request, reason); + }, + }, + // $FlowFixMe[prop-missing] size() methods are not allowed on byte streams. + {highWaterMark: 0}, + ); + return stream; +} + +type StaticResult = { + prelude: ReadableStream, +}; + +export function prerender( + model: ReactClientValue, + options?: Options, +): Promise { + return new Promise((resolve, reject) => { + const onFatalError = reject; + function onAllReady() { + const stream = new ReadableStream( + { + type: 'bytes', + start: (controller): ?Promise => { + startWork(request); + }, + pull: (controller): ?Promise => { + startFlowing(request, controller); + }, + cancel: (reason): ?Promise => { + stopFlowing(request); + abort(request, reason); + }, + }, + // $FlowFixMe[prop-missing] size() methods are not allowed on byte streams. + {highWaterMark: 0}, + ); + resolve({prelude: stream}); + } + const request = createPrerenderRequest( + model, + null, + onAllReady, + onFatalError, + options ? options.onError : undefined, + options ? options.identifierPrefix : undefined, + options ? options.onPostpone : undefined, + options ? options.temporaryReferences : undefined, + __DEV__ && options ? options.environmentName : undefined, + __DEV__ && options ? options.filterStackFrame : undefined, + ); + if (options && options.signal) { + const signal = options.signal; + if (signal.aborted) { + const reason = (signal: any).reason; + abort(request, reason); + } else { + const listener = () => { + const reason = (signal: any).reason; + abort(request, reason); + signal.removeEventListener('abort', listener); + }; + signal.addEventListener('abort', listener); + } + } + startWork(request); + }); +} + +const serverManifest = null; + +export function decodeReply( + body: string | FormData, + options?: {temporaryReferences?: TemporaryReferenceSet}, +): Thenable { + if (typeof body === 'string') { + const form = new FormData(); + form.append('0', body); + body = form; + } + const response = createResponse( + serverManifest, + '', + options ? options.temporaryReferences : undefined, + body, + ); + const root = getRoot(response); + close(response); + return root; +} + +export function decodeReplyFromAsyncIterable( + iterable: AsyncIterable<[string, string | File]>, + options?: {temporaryReferences?: TemporaryReferenceSet}, +): Thenable { + const iterator: AsyncIterator<[string, string | File]> = + iterable[ASYNC_ITERATOR](); + + const response = createResponse( + serverManifest, + '', + options ? options.temporaryReferences : undefined, + ); + + function progress( + entry: + | {done: false, +value: [string, string | File], ...} + | {done: true, +value: void, ...}, + ) { + if (entry.done) { + close(response); + } else { + const [name, value] = entry.value; + if (typeof value === 'string') { + resolveField(response, name, value); + } else { + resolveFile(response, name, value); + } + iterator.next().then(progress, error); + } + } + function error(reason: Error) { + reportGlobalError(response, reason); + if (typeof (iterator: any).throw === 'function') { + // The iterator protocol doesn't necessarily include this but a generator do. + // $FlowFixMe should be able to pass mixed + iterator.throw(reason).then(error, error); + } + } + + iterator.next().then(progress, error); + + return getRoot(response); +} + +export function decodeAction(body: FormData): Promise<() => T> | null { + return decodeActionImpl(body, serverManifest); +} + +export function decodeFormState( + actionResult: S, + body: FormData, +): Promise | null> { + return decodeFormStateImpl(actionResult, body, serverManifest); +} + +export function loadServerAction any>(id: string): Promise { + const reference = resolveServerReference(serverManifest, id); + return Promise.resolve(reference) + .then(() => preloadModule(reference)) + .then(() => { + const fn = requireModule(reference); + if (typeof fn !== 'function') { + throw new Error('Server actions must be functions'); + } + return fn; + }); +} diff --git a/packages/react-server-dom-vite/src/server/ReactFlightDOMServerNode.js b/packages/react-server-dom-vite/src/server/ReactFlightDOMServerNode.js new file mode 100644 index 0000000000000..1f678cb7be48e --- /dev/null +++ b/packages/react-server-dom-vite/src/server/ReactFlightDOMServerNode.js @@ -0,0 +1,310 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type { + Request, + ReactClientValue, +} from 'react-server/src/ReactFlightServer'; +import type {Destination} from 'react-server/src/ReactServerStreamConfigNode'; +import type {Busboy} from 'busboy'; +import type {Writable} from 'stream'; +import type {ReactFormState, Thenable} from 'shared/ReactTypes'; +import type {ServerReferenceId} from '../client/ReactFlightClientConfigBundlerVite'; + +import {Readable} from 'stream'; +import { + createRequest, + createPrerenderRequest, + startWork, + startFlowing, + stopFlowing, + abort, +} from 'react-server/src/ReactFlightServer'; + +import { + createResponse, + reportGlobalError, + close, + resolveField, + resolveFileInfo, + resolveFileChunk, + resolveFileComplete, + getRoot, +} from 'react-server/src/ReactFlightReplyServer'; + +import { + decodeAction as decodeActionImpl, + decodeFormState as decodeFormStateImpl, +} from 'react-server/src/ReactFlightActionServer'; +import { + preloadModule, + requireModule, + resolveServerReference, +} from '../client/ReactFlightClientConfigBundlerVite'; + +export { + registerClientReference, + registerServerReference, +} from '../ReactFlightViteReferences'; + +import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences'; + +export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences'; +export type {TemporaryReferenceSet}; + +function createDrainHandler(destination: Destination, request: Request) { + return () => startFlowing(request, destination); +} + +function createCancelHandler(request: Request, reason: string) { + return () => { + stopFlowing(request); + abort(request, new Error(reason)); + }; +} + +type Options = { + environmentName?: string | (() => string), + filterStackFrame?: (url: string, functionName: string) => boolean, + onError?: (error: mixed) => void, + onPostpone?: (reason: string) => void, + identifierPrefix?: string, + temporaryReferences?: TemporaryReferenceSet, +}; + +type PipeableStream = { + abort(reason: mixed): void, + pipe(destination: T): T, +}; + +export function renderToPipeableStream( + model: ReactClientValue, + options?: Options, +): PipeableStream { + const request = createRequest( + model, + null, + options ? options.onError : undefined, + options ? options.identifierPrefix : undefined, + options ? options.onPostpone : undefined, + options ? options.temporaryReferences : undefined, + __DEV__ && options ? options.environmentName : undefined, + __DEV__ && options ? options.filterStackFrame : undefined, + ); + let hasStartedFlowing = false; + startWork(request); + return { + pipe(destination: T): T { + if (hasStartedFlowing) { + throw new Error( + 'React currently only supports piping to one writable stream.', + ); + } + hasStartedFlowing = true; + startFlowing(request, destination); + destination.on('drain', createDrainHandler(destination, request)); + destination.on( + 'error', + createCancelHandler( + request, + 'The destination stream errored while writing data.', + ), + ); + destination.on( + 'close', + createCancelHandler(request, 'The destination stream closed early.'), + ); + return destination; + }, + abort(reason: mixed) { + abort(request, reason); + }, + }; +} + +function createFakeWritable(readable: any): Writable { + // The current host config expects a Writable so we create + // a fake writable for now to push into the Readable. + return ({ + write(chunk) { + return readable.push(chunk); + }, + end() { + readable.push(null); + }, + destroy(error) { + readable.destroy(error); + }, + }: any); +} + +type PrerenderOptions = { + environmentName?: string | (() => string), + filterStackFrame?: (url: string, functionName: string) => boolean, + onError?: (error: mixed) => void, + onPostpone?: (reason: string) => void, + identifierPrefix?: string, + temporaryReferences?: TemporaryReferenceSet, + signal?: AbortSignal, +}; + +type StaticResult = { + prelude: Readable, +}; + +export function prerenderToNodeStream( + model: ReactClientValue, + options?: PrerenderOptions, +): Promise { + return new Promise((resolve, reject) => { + const onFatalError = reject; + function onAllReady() { + const readable: Readable = new Readable({ + read() { + startFlowing(request, writable); + }, + }); + const writable = createFakeWritable(readable); + resolve({prelude: readable}); + } + + const request = createPrerenderRequest( + model, + null, + onAllReady, + onFatalError, + options ? options.onError : undefined, + options ? options.identifierPrefix : undefined, + options ? options.onPostpone : undefined, + options ? options.temporaryReferences : undefined, + __DEV__ && options ? options.environmentName : undefined, + __DEV__ && options ? options.filterStackFrame : undefined, + ); + if (options && options.signal) { + const signal = options.signal; + if (signal.aborted) { + const reason = (signal: any).reason; + abort(request, reason); + } else { + const listener = () => { + const reason = (signal: any).reason; + abort(request, reason); + signal.removeEventListener('abort', listener); + }; + signal.addEventListener('abort', listener); + } + } + startWork(request); + }); +} + +const serverManifest = null; + +export function decodeReplyFromBusboy( + busboyStream: Busboy, + options?: {temporaryReferences?: TemporaryReferenceSet}, +): Thenable { + const response = createResponse( + serverManifest, + '', + options ? options.temporaryReferences : undefined, + ); + let pendingFiles = 0; + const queuedFields: Array = []; + busboyStream.on('field', (name, value) => { + if (pendingFiles > 0) { + // Because the 'end' event fires two microtasks after the next 'field' + // we would resolve files and fields out of order. To handle this properly + // we queue any fields we receive until the previous file is done. + queuedFields.push(name, value); + } else { + resolveField(response, name, value); + } + }); + busboyStream.on('file', (name, value, {filename, encoding, mimeType}) => { + if (encoding.toLowerCase() === 'base64') { + throw new Error( + "React doesn't accept base64 encoded file uploads because we don't expect " + + "form data passed from a browser to ever encode data that way. If that's " + + 'the wrong assumption, we can easily fix it.', + ); + } + pendingFiles++; + const file = resolveFileInfo(response, name, filename, mimeType); + value.on('data', chunk => { + resolveFileChunk(response, file, chunk); + }); + value.on('end', () => { + resolveFileComplete(response, name, file); + pendingFiles--; + if (pendingFiles === 0) { + // Release any queued fields + for (let i = 0; i < queuedFields.length; i += 2) { + resolveField(response, queuedFields[i], queuedFields[i + 1]); + } + queuedFields.length = 0; + } + }); + }); + busboyStream.on('finish', () => { + close(response); + }); + busboyStream.on('error', err => { + reportGlobalError( + response, + // $FlowFixMe[incompatible-call] types Error and mixed are incompatible + err, + ); + }); + return getRoot(response); +} + +export function decodeReply( + body: string | FormData, + options?: {temporaryReferences?: TemporaryReferenceSet}, +): Thenable { + if (typeof body === 'string') { + const form = new FormData(); + form.append('0', body); + body = form; + } + const response = createResponse( + serverManifest, + '', + options ? options.temporaryReferences : undefined, + body, + ); + const root = getRoot(response); + close(response); + return root; +} + +export function decodeAction(body: FormData): Promise<() => T> | null { + return decodeActionImpl(body, serverManifest); +} + +export function decodeFormState( + actionResult: S, + body: FormData, +): Promise | null> { + return decodeFormStateImpl(actionResult, body, serverManifest); +} + +export function loadServerAction any>(id: string): Promise { + const reference = resolveServerReference(serverManifest, id); + return Promise.resolve(reference) + .then(() => preloadModule(reference)) + .then(() => { + const fn = requireModule(reference); + if (typeof fn !== 'function') { + throw new Error('Server actions must be functions'); + } + return fn; + }); +} diff --git a/packages/react-server-dom-vite/src/server/ReactFlightServerConfigViteBundler.js b/packages/react-server-dom-vite/src/server/ReactFlightServerConfigViteBundler.js new file mode 100644 index 0000000000000..9e5b4172c3821 --- /dev/null +++ b/packages/react-server-dom-vite/src/server/ReactFlightServerConfigViteBundler.js @@ -0,0 +1,66 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {ReactClientValue} from 'react-server/src/ReactFlightServer'; +import type {ImportMetadata} from '../shared/ReactFlightImportMetadata'; + +import type { + ClientReference, + ServerReference, +} from '../ReactFlightViteReferences'; + +export type {ClientReference, ServerReference}; + +export type ClientManifest = null; +export type ServerReferenceId = string; +export type ClientReferenceMetadata = ImportMetadata; +export type ClientReferenceKey = string; + +export { + isClientReference, + isServerReference, +} from '../ReactFlightViteReferences'; + +export function getClientReferenceKey( + reference: ClientReference, +): ClientReferenceKey { + return reference.$$id; +} + +export function resolveClientReferenceMetadata( + config: ClientManifest, + clientReference: ClientReference, +): ClientReferenceMetadata { + const ref = clientReference.$$id; + const idx = ref.lastIndexOf('#'); + const id = ref.slice(0, idx); + const name = ref.slice(idx + 1); + return [id, name]; +} + +export function getServerReferenceId( + config: ClientManifest, + serverReference: ServerReference, +): ServerReferenceId { + return serverReference.$$id; +} + +export function getServerReferenceBoundArguments( + config: ClientManifest, + serverReference: ServerReference, +): null | Array { + return serverReference.$$bound; +} + +export function getServerReferenceLocation( + config: ClientManifest, + serverReference: ServerReference, +): void | Error { + return serverReference.$$location; +} diff --git a/packages/react-server-dom-vite/src/server/react-flight-dom-server.browser.js b/packages/react-server-dom-vite/src/server/react-flight-dom-server.browser.js new file mode 100644 index 0000000000000..862a72356fdcf --- /dev/null +++ b/packages/react-server-dom-vite/src/server/react-flight-dom-server.browser.js @@ -0,0 +1,21 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export { + renderToReadableStream, + prerender as unstable_prerender, + decodeReply, + decodeAction, + decodeFormState, + registerClientReference, + registerServerReference, + createTemporaryReferenceSet, + loadServerAction, +} from './ReactFlightDOMServerBrowser'; +export {setPreloadModule} from '../client/ReactFlightClientConfigBundlerVite'; diff --git a/packages/react-server-dom-vite/src/server/react-flight-dom-server.edge.js b/packages/react-server-dom-vite/src/server/react-flight-dom-server.edge.js new file mode 100644 index 0000000000000..ff0cbb69ae55f --- /dev/null +++ b/packages/react-server-dom-vite/src/server/react-flight-dom-server.edge.js @@ -0,0 +1,22 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export { + renderToReadableStream, + prerender as unstable_prerender, + decodeReply, + decodeReplyFromAsyncIterable, + decodeAction, + decodeFormState, + registerClientReference, + registerServerReference, + createTemporaryReferenceSet, + loadServerAction, +} from './ReactFlightDOMServerEdge'; +export {setPreloadModule} from '../client/ReactFlightClientConfigBundlerVite'; diff --git a/packages/react-server-dom-vite/src/server/react-flight-dom-server.node.js b/packages/react-server-dom-vite/src/server/react-flight-dom-server.node.js new file mode 100644 index 0000000000000..bdf1b32e09fd1 --- /dev/null +++ b/packages/react-server-dom-vite/src/server/react-flight-dom-server.node.js @@ -0,0 +1,22 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export { + renderToPipeableStream, + prerenderToNodeStream as unstable_prerenderToNodeStream, + decodeReplyFromBusboy, + decodeReply, + decodeAction, + decodeFormState, + registerClientReference, + registerServerReference, + createTemporaryReferenceSet, + loadServerAction, +} from './ReactFlightDOMServerNode'; +export {setPreloadModule} from '../client/ReactFlightClientConfigBundlerVite'; diff --git a/packages/react-server-dom-vite/src/shared/ReactFlightImportMetadata.js b/packages/react-server-dom-vite/src/shared/ReactFlightImportMetadata.js new file mode 100644 index 0000000000000..d1ce8cfc591d7 --- /dev/null +++ b/packages/react-server-dom-vite/src/shared/ReactFlightImportMetadata.js @@ -0,0 +1,21 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +// This is the parsed shape of the wire format which is why it is +// condensed to only the essentialy information +export type ImportMetadata = [ + // eslint does not understand Flow tuple syntax. + /* eslint-disable */ + id: string, + name: string, + /* eslint-enable */ +]; + +export const ID = 0; +export const NAME = 1; diff --git a/packages/react-server-dom-vite/static.browser.js b/packages/react-server-dom-vite/static.browser.js new file mode 100644 index 0000000000000..3281fed6ea29c --- /dev/null +++ b/packages/react-server-dom-vite/static.browser.js @@ -0,0 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export {unstable_prerender} from './src/server/react-flight-dom-server.browser'; diff --git a/packages/react-server-dom-vite/static.edge.js b/packages/react-server-dom-vite/static.edge.js new file mode 100644 index 0000000000000..b1a96317ae9b3 --- /dev/null +++ b/packages/react-server-dom-vite/static.edge.js @@ -0,0 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export {unstable_prerender} from './src/server/react-flight-dom-server.edge'; diff --git a/packages/react-server-dom-vite/static.js b/packages/react-server-dom-vite/static.js new file mode 100644 index 0000000000000..83d8b8a017ff2 --- /dev/null +++ b/packages/react-server-dom-vite/static.js @@ -0,0 +1,13 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +throw new Error( + 'The React Server cannot be used outside a react-server environment. ' + + 'You must configure Node.js using the `--conditions react-server` flag.', +); diff --git a/packages/react-server-dom-vite/static.node.js b/packages/react-server-dom-vite/static.node.js new file mode 100644 index 0000000000000..345f4123c9f09 --- /dev/null +++ b/packages/react-server-dom-vite/static.node.js @@ -0,0 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export {unstable_prerenderToNodeStream} from './src/server/react-flight-dom-server.node'; diff --git a/packages/react-server/src/forks/ReactFlightServerConfig.dom-browser-vite.js b/packages/react-server/src/forks/ReactFlightServerConfig.dom-browser-vite.js new file mode 100644 index 0000000000000..ff8a95515137f --- /dev/null +++ b/packages/react-server/src/forks/ReactFlightServerConfig.dom-browser-vite.js @@ -0,0 +1,25 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {Request} from 'react-server/src/ReactFlightServer'; +import type {ReactComponentInfo} from 'shared/ReactTypes'; + +export * from 'react-server-dom-vite/src/server/ReactFlightServerConfigViteBundler'; +export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM'; + +export const supportsRequestStorage = false; +export const requestStorage: AsyncLocalStorage = (null: any); + +export const supportsComponentStorage = false; +export const componentStorage: AsyncLocalStorage = + (null: any); + +export * from '../ReactFlightServerConfigDebugNoop'; + +export * from '../ReactFlightStackConfigV8'; diff --git a/packages/react-server/src/forks/ReactFlightServerConfig.dom-edge-vite.js b/packages/react-server/src/forks/ReactFlightServerConfig.dom-edge-vite.js new file mode 100644 index 0000000000000..a0bf0e29fa85f --- /dev/null +++ b/packages/react-server/src/forks/ReactFlightServerConfig.dom-edge-vite.js @@ -0,0 +1,41 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ +import type {Request} from 'react-server/src/ReactFlightServer'; +import type {ReactComponentInfo} from 'shared/ReactTypes'; + +export * from 'react-server-dom-vite/src/server/ReactFlightServerConfigViteBundler'; +export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM'; + +// For now, we get this from the global scope, but this will likely move to a module. +export const supportsRequestStorage = typeof AsyncLocalStorage === 'function'; +export const requestStorage: AsyncLocalStorage = + supportsRequestStorage ? new AsyncLocalStorage() : (null: any); + +export const supportsComponentStorage: boolean = + __DEV__ && supportsRequestStorage; +export const componentStorage: AsyncLocalStorage = + supportsComponentStorage ? new AsyncLocalStorage() : (null: any); + +// We use the Node version but get access to async_hooks from a global. +import type {HookCallbacks, AsyncHook} from 'async_hooks'; +export const createAsyncHook: HookCallbacks => AsyncHook = + typeof async_hooks === 'object' + ? async_hooks.createHook + : function () { + return ({ + enable() {}, + disable() {}, + }: any); + }; +export const executionAsyncId: () => number = + typeof async_hooks === 'object' ? async_hooks.executionAsyncId : (null: any); + +export * from '../ReactFlightServerConfigDebugNode'; + +export * from '../ReactFlightStackConfigV8'; diff --git a/packages/react-server/src/forks/ReactFlightServerConfig.dom-node-vite.js b/packages/react-server/src/forks/ReactFlightServerConfig.dom-node-vite.js new file mode 100644 index 0000000000000..a17186077edab --- /dev/null +++ b/packages/react-server/src/forks/ReactFlightServerConfig.dom-node-vite.js @@ -0,0 +1,30 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import {AsyncLocalStorage} from 'async_hooks'; + +import type {Request} from 'react-server/src/ReactFlightServer'; +import type {ReactComponentInfo} from 'shared/ReactTypes'; + +export * from 'react-server-dom-vite/src/server/ReactFlightServerConfigViteBundler'; +export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM'; + +export const supportsRequestStorage = true; +export const requestStorage: AsyncLocalStorage = + new AsyncLocalStorage(); + +export const supportsComponentStorage = __DEV__; +export const componentStorage: AsyncLocalStorage = + supportsComponentStorage ? new AsyncLocalStorage() : (null: any); + +export {createHook as createAsyncHook, executionAsyncId} from 'async_hooks'; + +export * from '../ReactFlightServerConfigDebugNode'; + +export * from '../ReactFlightStackConfigV8'; diff --git a/scripts/rollup/bundles.js b/scripts/rollup/bundles.js index 824a1bbd52fc2..ba226c9b55eb7 100644 --- a/scripts/rollup/bundles.js +++ b/scripts/rollup/bundles.js @@ -689,6 +689,70 @@ const bundles = [ externals: ['react', 'react-dom'], }, + /******* React Server DOM Vite Server *******/ + { + bundleTypes: [NODE_DEV, NODE_PROD], + moduleType: RENDERER, + entry: 'react-server-dom-vite/src/server/react-flight-dom-server.browser', + name: 'react-server-dom-vite-server.browser', + condition: 'react-server', + global: 'ReactServerDOMServer', + minifyWithProdErrorCodes: false, + wrapWithModuleBoundaries: false, + externals: ['react', 'react-dom'], + }, + { + bundleTypes: [NODE_DEV, NODE_PROD], + moduleType: RENDERER, + entry: 'react-server-dom-vite/src/server/react-flight-dom-server.node', + name: 'react-server-dom-vite-server.node', + condition: 'react-server', + global: 'ReactServerDOMServer', + minifyWithProdErrorCodes: false, + wrapWithModuleBoundaries: false, + externals: ['react', 'util', 'async_hooks', 'react-dom'], + }, + { + bundleTypes: [NODE_DEV, NODE_PROD], + moduleType: RENDERER, + entry: 'react-server-dom-vite/src/server/react-flight-dom-server.edge', + name: 'react-server-dom-vite-server.edge', + condition: 'react-server', + global: 'ReactServerDOMServer', + minifyWithProdErrorCodes: false, + wrapWithModuleBoundaries: false, + externals: ['react', 'util', 'async_hooks', 'react-dom'], + }, + + /******* React Server DOM Vite Client *******/ + { + bundleTypes: [NODE_DEV, NODE_PROD], + moduleType: RENDERER, + entry: 'react-server-dom-vite/client.browser', + global: 'ReactServerDOMClient', + minifyWithProdErrorCodes: false, + wrapWithModuleBoundaries: false, + externals: ['react', 'react-dom'], + }, + { + bundleTypes: [NODE_DEV, NODE_PROD], + moduleType: RENDERER, + entry: 'react-server-dom-vite/client.node', + global: 'ReactServerDOMClient', + minifyWithProdErrorCodes: false, + wrapWithModuleBoundaries: false, + externals: ['react', 'react-dom', 'util'], + }, + { + bundleTypes: [NODE_DEV, NODE_PROD], + moduleType: RENDERER, + entry: 'react-server-dom-vite/client.edge', + global: 'ReactServerDOMClient', + minifyWithProdErrorCodes: false, + wrapWithModuleBoundaries: false, + externals: ['react', 'react-dom'], + }, + /******* React Server DOM ESM Server *******/ { bundleTypes: [NODE_DEV, NODE_PROD], diff --git a/scripts/shared/inlinedHostConfigs.js b/scripts/shared/inlinedHostConfigs.js index 1b2d3027ddf0d..1fda9154cbba8 100644 --- a/scripts/shared/inlinedHostConfigs.js +++ b/scripts/shared/inlinedHostConfigs.js @@ -232,6 +232,47 @@ module.exports = [ isFlowTyped: true, isServerSupported: true, }, + { + shortName: 'dom-node-vite', + entryPoints: [ + 'react-server-dom-vite/client.node', + 'react-server-dom-vite/src/server/react-flight-dom-server.node', + ], + paths: [ + 'react-dom', + 'react-dom-bindings', + 'react-dom/client', + 'react-dom/profiling', + 'react-dom/server', + 'react-dom/server.node', + 'react-dom/static', + 'react-dom/static.node', + 'react-dom/src/server/react-dom-server.node', + 'react-dom/src/server/ReactDOMFizzServerNode.js', // react-dom/server.node + 'react-dom/src/server/ReactDOMFizzStaticNode.js', + 'react-dom-bindings/src/server/ReactDOMFlightServerHostDispatcher.js', + 'react-dom-bindings/src/server/ReactFlightServerConfigDOM.js', + 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js', + 'react-server-dom-vite', + 'react-server-dom-vite/client.node', + 'react-server-dom-vite/server', + 'react-server-dom-vite/server.node', + 'react-server-dom-vite/static', + 'react-server-dom-vite/static.node', + 'react-server-dom-vite/src/client/ReactFlightDOMClientNode.js', // react-server-dom-vite/client.node + 'react-server-dom-vite/src/client/ReactFlightClientConfigBundlerVite.js', + 'react-server-dom-vite/src/server/react-flight-dom-server.node', + 'react-server-dom-vite/src/server/ReactFlightDOMServerNode.js', // react-server-dom-vite/src/server/react-flight-dom-server.node + 'react-devtools', + 'react-devtools-core', + 'react-devtools-shell', + 'react-devtools-shared', + 'shared/ReactDOMSharedInternals', + 'react-server/src/ReactFlightServerConfigDebugNode.js', + ], + isFlowTyped: true, + isServerSupported: true, + }, { shortName: 'dom-bun', entryPoints: ['react-dom/src/server/react-dom-server.bun.js'], @@ -347,6 +388,40 @@ module.exports = [ isFlowTyped: true, isServerSupported: true, }, + { + shortName: 'dom-browser-vite', + entryPoints: [ + 'react-server-dom-vite/client.browser', + 'react-server-dom-vite/src/server/react-flight-dom-server.browser', + ], + paths: [ + 'react-dom', + 'react-dom/client', + 'react-dom/profiling', + 'react-dom/server', + 'react-dom/server.node', + 'react-dom-bindings', + 'react-dom-bindings/src/server/ReactDOMFlightServerHostDispatcher.js', + 'react-dom-bindings/src/server/ReactFlightServerConfigDOM.js', + 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js', + 'react-server-dom-vite', + 'react-server-dom-vite/client', + 'react-server-dom-vite/client.browser', + 'react-server-dom-vite/server.browser', + 'react-server-dom-vite/static.browser', + 'react-server-dom-vite/src/client/ReactFlightDOMClientBrowser.js', // react-server-dom-vite/client.browser + 'react-server-dom-vite/src/client/ReactFlightClientConfigBundlerVite.js', + 'react-server-dom-vite/src/server/react-flight-dom-server.browser', + 'react-server-dom-vite/src/server/ReactFlightDOMServerBrowser.js', // react-server-dom-vite/src/server/react-flight-dom-server.browser + 'react-devtools', + 'react-devtools-core', + 'react-devtools-shell', + 'react-devtools-shared', + 'shared/ReactDOMSharedInternals', + ], + isFlowTyped: true, + isServerSupported: true, + }, { shortName: 'dom-edge-webpack', entryPoints: [ @@ -468,6 +543,45 @@ module.exports = [ isFlowTyped: true, isServerSupported: true, }, + { + shortName: 'dom-edge-vite', + entryPoints: [ + 'react-server-dom-vite/client.edge', + 'react-server-dom-vite/src/server/react-flight-dom-server.edge', + ], + paths: [ + 'react-dom', + 'react-dom/src/ReactDOMReactServer.js', + 'react-dom-bindings', + 'react-dom/client', + 'react-dom/profiling', + 'react-dom/server.edge', + 'react-dom/static.edge', + 'react-dom/unstable_testing', + 'react-dom/src/server/react-dom-server.edge', + 'react-dom/src/server/ReactDOMFizzServerEdge.js', // react-dom/server.edge + 'react-dom/src/server/ReactDOMFizzStaticEdge.js', + 'react-dom-bindings/src/server/ReactDOMFlightServerHostDispatcher.js', + 'react-dom-bindings/src/server/ReactFlightServerConfigDOM.js', + 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js', + 'react-server-dom-vite', + 'react-server-dom-vite/client.edge', + 'react-server-dom-vite/server.edge', + 'react-server-dom-vite/static.edge', + 'react-server-dom-vite/src/client/ReactFlightDOMClientEdge.js', // react-server-dom-vite/client.edge + 'react-server-dom-vite/src/client/ReactFlightClientConfigBundlerVite.js', + 'react-server-dom-vite/src/server/react-flight-dom-server.edge', + 'react-server-dom-vite/src/server/ReactFlightDOMServerEdge.js', // react-server-dom-vite/src/server/react-flight-dom-server.edge + 'react-devtools', + 'react-devtools-core', + 'react-devtools-shell', + 'react-devtools-shared', + 'shared/ReactDOMSharedInternals', + 'react-server/src/ReactFlightServerConfigDebugNode.js', + ], + isFlowTyped: true, + isServerSupported: true, + }, { shortName: 'dom-node-esm', entryPoints: [