@rsc-kit/core 0.18.1 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/action.d.ts +16 -2
- package/dist/action.js +21 -12
- package/dist/action.js.map +1 -1
- package/dist/apiPrerender.js +25 -7
- package/dist/apiPrerender.js.map +1 -1
- package/dist/clientPackages.d.ts +30 -0
- package/dist/clientPackages.js +234 -0
- package/dist/clientPackages.js.map +1 -0
- package/dist/compress.d.ts +12 -0
- package/dist/compress.js +134 -0
- package/dist/compress.js.map +1 -0
- package/dist/compressRuntime.d.ts +8 -0
- package/dist/compressRuntime.js +62 -0
- package/dist/compressRuntime.js.map +1 -0
- package/dist/files.d.ts +20 -0
- package/dist/files.js +38 -0
- package/dist/files.js.map +1 -1
- package/dist/formSubmit.d.ts +1 -0
- package/dist/formSubmit.js +14 -0
- package/dist/formSubmit.js.map +1 -0
- package/dist/host.d.ts +23 -0
- package/dist/host.js +175 -31
- package/dist/host.js.map +1 -1
- package/dist/hostCalls.d.ts +15 -0
- package/dist/hostCalls.js +75 -8
- package/dist/hostCalls.js.map +1 -1
- package/dist/js/Form.d.ts +21 -4
- package/dist/js/Form.js +110 -83
- package/dist/js/Form.js.map +1 -1
- package/dist/js/Link.d.ts +5 -5
- package/dist/js/Link.js.map +1 -1
- package/dist/js/RedirectBoundary.js.map +1 -1
- package/dist/js/RouteErrorBoundary.d.ts +6 -2
- package/dist/js/RouteErrorBoundary.js +10 -0
- package/dist/js/RouteErrorBoundary.js.map +1 -1
- package/dist/js/SlotBoundary.d.ts +7 -1
- package/dist/js/SlotBoundary.js +9 -1
- package/dist/js/SlotBoundary.js.map +1 -1
- package/dist/js/createViteRscApp.d.ts +1 -0
- package/dist/js/createViteRscApp.js +54 -5
- package/dist/js/createViteRscApp.js.map +1 -1
- package/dist/js/errors.d.ts +3 -1
- package/dist/js/errors.js +26 -3
- package/dist/js/errors.js.map +1 -1
- package/dist/js/formEncoding.d.ts +72 -3
- package/dist/js/formEncoding.js +284 -20
- package/dist/js/formEncoding.js.map +1 -1
- package/dist/js/navigate.d.ts +6 -3
- package/dist/js/navigate.js +57 -7
- package/dist/js/navigate.js.map +1 -1
- package/dist/js/nuqs.js.map +1 -1
- package/dist/js/router.d.ts +3 -3
- package/dist/js/router.js.map +1 -1
- package/dist/js/updateStore.js +8 -2
- package/dist/js/updateStore.js.map +1 -1
- package/dist/manifest.d.ts +6 -0
- package/dist/manifest.js.map +1 -1
- package/dist/openapi.d.ts +74 -0
- package/dist/openapi.js +172 -0
- package/dist/openapi.js.map +1 -0
- package/dist/redirect.d.ts +4 -4
- package/dist/redirect.js.map +1 -1
- package/dist/request.d.ts +56 -2
- package/dist/request.js +68 -4
- package/dist/request.js.map +1 -1
- package/dist/routes.d.ts +17 -6
- package/dist/routes.js.map +1 -1
- package/dist/testing.d.ts +14 -0
- package/dist/testing.js +56 -2
- package/dist/testing.js.map +1 -1
- package/dist/useSsr.d.ts +11 -0
- package/dist/useSsr.js +15 -0
- package/dist/useSsr.js.map +1 -1
- package/dist/vite.d.ts +69 -1
- package/dist/vite.js +481 -51
- package/dist/vite.js.map +1 -1
- package/package.json +5 -1
package/dist/files.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"files.js","sourceRoot":"","sources":["../src/files.ts"],"names":[],"mappings":"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","sourcesContent":["// Reading built output off a disk.\n//\n// Kept apart from the host on purpose: it is the only part of serving an RSC\n// app that assumes a filesystem, and plenty of places to run one do not have\n// one. Nothing in `@rsc-kit/core/host` imports this module, so a bundle for a\n// host without a filesystem carries no reference to `node:fs` at all.\n//\n// Assets are not here any more — Nitro publishes and serves those. What is\n// left is the prerendered output: written by the build, read by the server it\n// generates, and copied by a static export.\n\nimport { cp, mkdir, readdir, readFile, writeFile } from 'node:fs/promises'\nimport { dirname, join } from 'node:path'\n\n/**\n * Read what the prerenderer wrote, from a directory.\n *\n * Missing is an answer, not an error: a partial prerender is a valid state,\n * and anything not found here is rendered on demand instead.\n */\nexport function prerenderedFrom(dir: string) {\n /**\n * What is on disk, listed once.\n *\n * A miss used to be a thrown ENOENT, and misses are the common case: the\n * host asks for `{url}.html`, then `{url}.ppr.html`, then the route's\n * pattern — so every request to a page served by a pattern shell threw and\n * caught two exceptions before finding anything. Measured at 3,000 req/s\n * against 28,000 for the same page read from memory.\n *\n * Contents are cached for the life of the process. A build's output does\n * not change under a running server - a deploy restarts it, and a server\n * that outlived a rebuild would be serving a stale engine against new\n * pages anyway - so the first read is the only one. Under load the same\n * page used to be opened, read and closed thousands of times a second,\n * which a profile showed as the one cost on a stored page that was ours\n * rather than React's. Capped by bytes, so a site with more pages than\n * memory holds the hot ones and re-reads the rest.\n */\n let present: Promise<Set<string>> | null = null\n\n const CONTENT_CAP_BYTES = 64 * 1024 * 1024\n const contents = new Map<string, string>()\n let held = 0\n\n const remember = (name: string, text: string): void => {\n const size = text.length * 2\n\n if (size > CONTENT_CAP_BYTES / 4) return\n\n while (held + size > CONTENT_CAP_BYTES && contents.size > 0) {\n const oldest = contents.keys().next().value as string\n held -= contents.get(oldest)!.length * 2\n contents.delete(oldest)\n }\n\n contents.set(name, text)\n held += size\n }\n\n const listing = async (): Promise<Set<string>> => {\n present ??= readdir(dir, { recursive: true })\n .then((names) => new Set(names.map((n) => String(n).replace(/\\\\/g, '/'))))\n // An absent directory is an empty one: a partial prerender is a valid\n // state, and everything falls through to being rendered.\n .catch(() => new Set<string>())\n\n return await present\n }\n\n return async (name: string): Promise<string | null> => {\n if (name.includes('..')) return null\n\n if (!(await listing()).has(name)) return null\n\n const cached = contents.get(name)\n\n if (cached !== undefined) return cached\n\n try {\n const text = await readFile(join(dir, name), 'utf-8')\n\n remember(name, text)\n\n return text\n } catch {\n return null\n }\n }\n}\n\n/**\n * The same reader, for a directory whose absolute path is not known until the\n * server runs.\n *\n * Nitro bundles the generated entry to a different depth depending on the\n * preset — `.output/server/index.mjs` for one, `.output/server/_ssr/rsc.mjs`\n * for another — so neither a fixed relative path nor a path baked in at build\n * time survives: the first is wrong for half the presets, and the second stops\n * being true the moment `.output/` is copied to the machine that serves it.\n *\n * So the caller passes its own `import.meta.url` and the directory is found by\n * walking up from it. A few levels, bounded, and resolved once.\n *\n * prerendered: prerenderedBeside(import.meta.url, 'rsc-static')\n *\n * Not finding it is a valid state and not an error: nothing was frozen, or\n * this host has no filesystem to have frozen it on. Every page renders live,\n * which is what happens today everywhere.\n */\nexport function prerenderedBeside(moduleUrl: string, dirName: string, levels = 4) {\n let reader: Promise<(name: string) => Promise<string | null>> | null = null\n\n const resolveDir = async (): Promise<(name: string) => Promise<string | null>> => {\n let dir: string | null = null\n\n try {\n const { fileURLToPath } = await import('node:url')\n\n dir = dirname(fileURLToPath(moduleUrl))\n } catch {\n // No filesystem, or no file: url to speak of. The inline module below\n // is the only place the pages can be.\n }\n\n if (dir !== null) {\n for (let i = 0; i <= levels; i++) {\n const candidate = join(dir, ...Array(i).fill('..'), dirName)\n\n try {\n await readdir(candidate)\n\n return prerenderedFrom(candidate)\n } catch {\n // Not at this level. Keep walking.\n }\n }\n }\n\n const inline = await inlinePrerendered(moduleUrl, `${dirName}-inline.mjs`, levels)\n\n return inline ? async (name) => inline[name] ?? null : async () => null\n }\n\n return async (name: string): Promise<string | null> => {\n reader ??= resolveDir()\n\n return await (await reader)(name)\n }\n}\n\n/**\n * The frozen pages as one module, for a runtime with no filesystem.\n *\n * A Worker cannot readdir. Its modules are what wrangler uploaded, and a\n * sibling .mjs beside the bundle is one of them - so the build writes every\n * stored file into `rsc-static-inline.mjs` as strings, and this walks up from\n * the caller the way the disk reader does, importing rather than reading.\n * The specifier is computed, and marked for the bundler to leave alone: at\n * bundle time the file does not exist yet, because it is written by the\n * prerender that runs the bundle.\n *\n * Two forms of the specifier, because runtimes disagree. workerd resolves a\n * relative string against the importing module's name and refuses a url;\n * Node and Bun want the url, since a relative string would resolve against\n * this file rather than the caller's.\n */\nasync function inlinePrerendered(\n moduleUrl: string,\n fileName: string,\n levels: number,\n): Promise<Record<string, string> | null> {\n for (let i = 0; i <= levels; i++) {\n const relative = './' + '../'.repeat(i) + fileName\n\n for (const specifier of [relative, safeUrl(relative, moduleUrl)]) {\n if (!specifier) continue\n\n try {\n const loaded = (await import(/* @vite-ignore */ specifier)) as { default?: Record<string, string> }\n\n if (loaded.default && typeof loaded.default === 'object') return loaded.default\n } catch {\n // Not there, or not at this level.\n }\n }\n }\n\n return null\n}\n\nfunction safeUrl(relative: string, base: string): string | null {\n try {\n return new URL(relative, base).href\n } catch {\n return null\n }\n}\n\n/** The name the build writes the inline module under, beside the stored pages' directory. */\nexport function inlineModuleName(dirName: string): string {\n return `${dirName}-inline.mjs`\n}\n\n/**\n * Every stored file under `dir`, as the source of the inline module.\n * Strings only: a frozen page, its flight payload, its meta, an api answer.\n */\nexport async function inlineModuleSource(dir: string): Promise<string> {\n const { readdir: list, readFile: read } = await import('node:fs/promises')\n const entries: Record<string, string> = {}\n\n const walk = async (sub: string): Promise<void> => {\n for (const entry of await list(join(dir, sub), { withFileTypes: true })) {\n const rel = sub ? `${sub}/${entry.name}` : entry.name\n\n if (entry.isDirectory()) await walk(rel)\n else entries[rel] = await read(join(dir, rel), 'utf-8')\n }\n }\n\n await walk('')\n\n return (\n '// @generated by the RSC build: the stored pages, for a runtime with no filesystem.\\n' +\n 'export default ' +\n JSON.stringify(entries) +\n '\\n'\n )\n}\n\nfunction contentTypeOf(pathname: string): string {\n if (pathname.endsWith('.js')) return 'text/javascript; charset=utf-8'\n if (pathname.endsWith('.css')) return 'text/css; charset=utf-8'\n if (pathname.endsWith('.map')) return 'application/json; charset=utf-8'\n if (pathname.endsWith('.svg')) return 'image/svg+xml'\n if (pathname.endsWith('.woff2')) return 'font/woff2'\n\n return 'application/octet-stream'\n}\n\n/**\n * Write build output into a directory.\n *\n * The sink `prerender` and `exportSite` take. Names are relative and may\n * contain directories — `docs/index.html` — so each one's parent is created\n * as it goes.\n */\nexport function writeTo(dir: string): ((name: string, contents: string) => Promise<void>) & { dir: string } {\n const write = async (name: string, contents: string): Promise<void> => {\n const path = join(dir, name)\n\n await mkdir(dirname(path), { recursive: true })\n await writeFile(path, contents)\n }\n\n // Where it wrote, for a caller that has to read it back. The embeddable\n // module has to list these files, and being told the directory a second time\n // is a second place for it to be wrong.\n return Object.assign(write, { dir })\n}\n\n/**\n * Copy the built browser bundle into an exported site.\n *\n * Separate from the export itself because copying a tree of files is a\n * filesystem operation by nature — a deploy that uploads them to a bucket\n * has its own way to do that, and passes its own callback instead.\n */\nexport function copyAssets(from: string, to: string, url = '/assets/') {\n return async (): Promise<void> => {\n const at = url.replace(/^\\/+|\\/+$/g, '')\n\n if (at === '') return\n\n const target = join(to, at)\n\n await mkdir(dirname(target), { recursive: true })\n await cp(from, target, { recursive: true })\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"files.js","sourceRoot":"","sources":["../src/files.ts"],"names":[],"mappings":"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","sourcesContent":["// Reading built output off a disk.\n//\n// Kept apart from the host on purpose: it is the only part of serving an RSC\n// app that assumes a filesystem, and plenty of places to run one do not have\n// one. Nothing in `@rsc-kit/core/host` imports this module, so a bundle for a\n// host without a filesystem carries no reference to `node:fs` at all.\n//\n// Assets are not here any more — Nitro publishes and serves those. What is\n// left is the prerendered output: written by the build, read by the server it\n// generates, and copied by a static export.\n\nimport { cp, mkdir, readdir, readFile, writeFile } from 'node:fs/promises'\nimport { dirname, join } from 'node:path'\n\n/**\n * Read what the prerenderer wrote, from a directory.\n *\n * Missing is an answer, not an error: a partial prerender is a valid state,\n * and anything not found here is rendered on demand instead.\n */\nexport function prerenderedFrom(dir: string) {\n /**\n * What is on disk, listed once.\n *\n * A miss used to be a thrown ENOENT, and misses are the common case: the\n * host asks for `{url}.html`, then `{url}.ppr.html`, then the route's\n * pattern — so every request to a page served by a pattern shell threw and\n * caught two exceptions before finding anything. Measured at 3,000 req/s\n * against 28,000 for the same page read from memory.\n *\n * Contents are cached for the life of the process. A build's output does\n * not change under a running server - a deploy restarts it, and a server\n * that outlived a rebuild would be serving a stale engine against new\n * pages anyway - so the first read is the only one. Under load the same\n * page used to be opened, read and closed thousands of times a second,\n * which a profile showed as the one cost on a stored page that was ours\n * rather than React's. Capped by bytes, so a site with more pages than\n * memory holds the hot ones and re-reads the rest.\n */\n let present: Promise<Set<string>> | null = null\n\n const CONTENT_CAP_BYTES = 64 * 1024 * 1024\n const contents = new Map<string, string>()\n let held = 0\n\n const remember = (name: string, text: string): void => {\n const size = text.length * 2\n\n if (size > CONTENT_CAP_BYTES / 4) return\n\n while (held + size > CONTENT_CAP_BYTES && contents.size > 0) {\n const oldest = contents.keys().next().value as string\n held -= contents.get(oldest)!.length * 2\n contents.delete(oldest)\n }\n\n contents.set(name, text)\n held += size\n }\n\n const listing = async (): Promise<Set<string>> => {\n present ??= readdir(dir, { recursive: true })\n .then((names) => new Set(names.map((n) => String(n).replace(/\\\\/g, '/'))))\n // An absent directory is an empty one: a partial prerender is a valid\n // state, and everything falls through to being rendered.\n .catch(() => new Set<string>())\n\n return await present\n }\n\n return async (name: string): Promise<string | null> => {\n if (name.includes('..')) return null\n\n if (!(await listing()).has(name)) return null\n\n const cached = contents.get(name)\n\n if (cached !== undefined) return cached\n\n try {\n const text = await readFile(join(dir, name), 'utf-8')\n\n remember(name, text)\n\n return text\n } catch {\n return null\n }\n }\n}\n\n/**\n * The same reader, for a directory whose absolute path is not known until the\n * server runs.\n *\n * Nitro bundles the generated entry to a different depth depending on the\n * preset — `.output/server/index.mjs` for one, `.output/server/_ssr/rsc.mjs`\n * for another — so neither a fixed relative path nor a path baked in at build\n * time survives: the first is wrong for half the presets, and the second stops\n * being true the moment `.output/` is copied to the machine that serves it.\n *\n * So the caller passes its own `import.meta.url` and the directory is found by\n * walking up from it. A few levels, bounded, and resolved once.\n *\n * prerendered: prerenderedBeside(import.meta.url, 'rsc-static')\n *\n * Not finding it is a valid state and not an error: nothing was frozen, or\n * this host has no filesystem to have frozen it on. Every page renders live,\n * which is what happens today everywhere.\n */\n/**\n * Where a compiled binary puts the frozen pages.\n *\n * `bun build --compile` embeds what is imported statically and nothing a\n * computed import names, so the walk below finds no directory and no\n * module inside a binary. The build writes `compile.mjs` beside the\n * server: it imports the inline module by name - which embeds it - and\n * hands the pages over here before importing the server. Shared as a\n * global under a registered symbol, because the wrapper and this module\n * are bundled apart.\n */\nexport const EMBEDDED_PAGES = Symbol.for('rsc-kit.embedded-pages')\n\nfunction embeddedPages(): Record<string, string> | null {\n const pages = (globalThis as Record<symbol, unknown>)[EMBEDDED_PAGES]\n\n return pages && typeof pages === 'object' ? (pages as Record<string, string>) : null\n}\n\nexport function prerenderedBeside(moduleUrl: string, dirName: string, levels = 4) {\n let reader: Promise<(name: string) => Promise<string | null>> | null = null\n\n const resolveDir = async (): Promise<(name: string) => Promise<string | null>> => {\n const embedded = embeddedPages()\n\n if (embedded) return async (name) => embedded[name] ?? null\n\n let dir: string | null = null\n\n try {\n const { fileURLToPath } = await import('node:url')\n\n dir = dirname(fileURLToPath(moduleUrl))\n } catch {\n // No filesystem, or no file: url to speak of. The inline module below\n // is the only place the pages can be.\n }\n\n if (dir !== null) {\n for (let i = 0; i <= levels; i++) {\n const candidate = join(dir, ...Array(i).fill('..'), dirName)\n\n try {\n await readdir(candidate)\n\n return prerenderedFrom(candidate)\n } catch {\n // Not at this level. Keep walking.\n }\n }\n }\n\n const inline = await inlinePrerendered(moduleUrl, `${dirName}-inline.mjs`, levels)\n\n return inline ? async (name) => inline[name] ?? null : async () => null\n }\n\n return async (name: string): Promise<string | null> => {\n reader ??= resolveDir()\n\n return await (await reader)(name)\n }\n}\n\n/**\n * The frozen pages as one module, for a runtime with no filesystem.\n *\n * A Worker cannot readdir. Its modules are what wrangler uploaded, and a\n * sibling .mjs beside the bundle is one of them - so the build writes every\n * stored file into `rsc-static-inline.mjs` as strings, and this walks up from\n * the caller the way the disk reader does, importing rather than reading.\n * The specifier is computed, and marked for the bundler to leave alone: at\n * bundle time the file does not exist yet, because it is written by the\n * prerender that runs the bundle.\n *\n * Two forms of the specifier, because runtimes disagree. workerd resolves a\n * relative string against the importing module's name and refuses a url;\n * Node and Bun want the url, since a relative string would resolve against\n * this file rather than the caller's.\n */\nasync function inlinePrerendered(\n moduleUrl: string,\n fileName: string,\n levels: number,\n): Promise<Record<string, string> | null> {\n for (let i = 0; i <= levels; i++) {\n const relative = './' + '../'.repeat(i) + fileName\n\n for (const specifier of [relative, safeUrl(relative, moduleUrl)]) {\n if (!specifier) continue\n\n try {\n const loaded = (await import(/* @vite-ignore */ specifier)) as { default?: Record<string, string> }\n\n if (loaded.default && typeof loaded.default === 'object') return loaded.default\n } catch {\n // Not there, or not at this level.\n }\n }\n }\n\n return null\n}\n\nfunction safeUrl(relative: string, base: string): string | null {\n try {\n return new URL(relative, base).href\n } catch {\n return null\n }\n}\n\n/**\n * The entry a binary is compiled from, written beside the server.\n *\n * Two imports, in this order: the inline module first, so its pages are\n * handed over before the server's first request; the server second. Bun\n * embeds both. `bun build --compile .output/server/compile.mjs`.\n */\nexport function compileEntrySource(dirName: string): string {\n return (\n '// The entry to compile into one binary: bun build --compile .output/server/compile.mjs\\n' +\n '//\\n' +\n '// A binary carries what is imported by name. The server reads its frozen pages\\n' +\n '// through a computed import, which a compile cannot see - imported here, they\\n' +\n '// travel inside the binary and are handed to the server before it starts.\\n' +\n `import pages from \"./${inlineModuleName(dirName)}\";\\n` +\n '\\n' +\n `globalThis[Symbol.for(\"rsc-kit.embedded-pages\")] = pages;\\n` +\n '\\n' +\n 'await import(\"./index.mjs\");\\n'\n )\n}\n\n/** The name the build writes the inline module under, beside the stored pages' directory. */\nexport function inlineModuleName(dirName: string): string {\n return `${dirName}-inline.mjs`\n}\n\n/**\n * Every stored file under `dir`, as the source of the inline module.\n * Strings only: a frozen page, its flight payload, its meta, an api answer.\n */\nexport async function inlineModuleSource(dir: string): Promise<string> {\n const { readdir: list, readFile: read } = await import('node:fs/promises')\n const entries: Record<string, string> = {}\n\n const walk = async (sub: string): Promise<void> => {\n for (const entry of await list(join(dir, sub), { withFileTypes: true })) {\n const rel = sub ? `${sub}/${entry.name}` : entry.name\n\n if (entry.isDirectory()) await walk(rel)\n else entries[rel] = await read(join(dir, rel), 'utf-8')\n }\n }\n\n await walk('')\n\n return (\n '// @generated by the RSC build: the stored pages, for a runtime with no filesystem.\\n' +\n 'export default ' +\n JSON.stringify(entries) +\n '\\n'\n )\n}\n\nfunction contentTypeOf(pathname: string): string {\n if (pathname.endsWith('.js')) return 'text/javascript; charset=utf-8'\n if (pathname.endsWith('.css')) return 'text/css; charset=utf-8'\n if (pathname.endsWith('.map')) return 'application/json; charset=utf-8'\n if (pathname.endsWith('.svg')) return 'image/svg+xml'\n if (pathname.endsWith('.woff2')) return 'font/woff2'\n\n return 'application/octet-stream'\n}\n\n/**\n * Write build output into a directory.\n *\n * The sink `prerender` and `exportSite` take. Names are relative and may\n * contain directories — `docs/index.html` — so each one's parent is created\n * as it goes.\n */\nexport function writeTo(dir: string): ((name: string, contents: string) => Promise<void>) & { dir: string } {\n const write = async (name: string, contents: string): Promise<void> => {\n const path = join(dir, name)\n\n await mkdir(dirname(path), { recursive: true })\n await writeFile(path, contents)\n }\n\n // Where it wrote, for a caller that has to read it back. The embeddable\n // module has to list these files, and being told the directory a second time\n // is a second place for it to be wrong.\n return Object.assign(write, { dir })\n}\n\n/**\n * Copy the built browser bundle into an exported site.\n *\n * Separate from the export itself because copying a tree of files is a\n * filesystem operation by nature — a deploy that uploads them to a bucket\n * has its own way to do that, and passes its own callback instead.\n */\nexport function copyAssets(from: string, to: string, url = '/assets/') {\n return async (): Promise<void> => {\n const at = url.replace(/^\\/+|\\/+$/g, '')\n\n if (at === '') return\n\n const target = join(to, at)\n\n await mkdir(dirname(target), { recursive: true })\n await cp(from, target, { recursive: true })\n }\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function submitForm(action: (formData: FormData) => Promise<unknown>, _previous: unknown, formData: FormData): Promise<unknown>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use server";
|
|
2
|
+
// What a <Form> posts to before it has a runtime.
|
|
3
|
+
//
|
|
4
|
+
// React seats the answer to a form posted without javascript into the form
|
|
5
|
+
// that posted it - through useActionState, which hands the action a
|
|
6
|
+
// (previousState, formData) pair. A Form's action takes the FormData alone,
|
|
7
|
+
// and every action written against the guide does; so the form is registered
|
|
8
|
+
// with this, bound to its real action, and this is the pair-shaped function
|
|
9
|
+
// React calls. The action is a server reference travelling as a bound
|
|
10
|
+
// argument, decoded back to itself on the way in.
|
|
11
|
+
export async function submitForm(action, _previous, formData) {
|
|
12
|
+
return await action(formData);
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=formSubmit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"formSubmit.js","sourceRoot":"","sources":["../src/formSubmit.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,kDAAkD;AAClD,EAAE;AACF,2EAA2E;AAC3E,oEAAoE;AACpE,4EAA4E;AAC5E,6EAA6E;AAC7E,4EAA4E;AAC5E,sEAAsE;AACtE,kDAAkD;AAElD,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,MAAgD,EAChD,SAAkB,EAClB,QAAkB;IAElB,OAAO,MAAM,MAAM,CAAC,QAAQ,CAAC,CAAC;AAChC,CAAC","sourcesContent":["\"use server\";\n\n// What a <Form> posts to before it has a runtime.\n//\n// React seats the answer to a form posted without javascript into the form\n// that posted it - through useActionState, which hands the action a\n// (previousState, formData) pair. A Form's action takes the FormData alone,\n// and every action written against the guide does; so the form is registered\n// with this, bound to its real action, and this is the pair-shaped function\n// React calls. The action is a server reference travelling as a bound\n// argument, decoded back to itself on the way in.\n\nexport async function submitForm(\n action: (formData: FormData) => Promise<unknown>,\n _previous: unknown,\n formData: FormData,\n): Promise<unknown> {\n return await action(formData);\n}\n"]}
|
package/dist/host.d.ts
CHANGED
|
@@ -32,6 +32,18 @@ export interface RscEngine {
|
|
|
32
32
|
}[], loadings?: string[], parallelSlots?: Record<string, string>, slotOverrides?: Record<string, unknown>, nonce?: string, pageKey?: string, bootstrap?: boolean): Promise<{
|
|
33
33
|
htmlStream: ReadableStream;
|
|
34
34
|
}>;
|
|
35
|
+
/**
|
|
36
|
+
* A form posted to the page's own url before the page had a runtime to
|
|
37
|
+
* catch it: React wrote the action's id into the form, and this runs that
|
|
38
|
+
* action from the posted fields, then renders the page with the result.
|
|
39
|
+
* Optional: an engine built by an older plugin answers such a post 404.
|
|
40
|
+
*/
|
|
41
|
+
handleRscFormPost?(component: string, props: Record<string, unknown>, layouts: {
|
|
42
|
+
component: string;
|
|
43
|
+
props: Record<string, unknown>;
|
|
44
|
+
}[], loadings: string[], parallelSlots: Record<string, string>, slotOverrides: Record<string, unknown>, nonce: string | undefined, pageKey: string, bootstrap: boolean, formData: FormData): Promise<{
|
|
45
|
+
htmlStream: ReadableStream;
|
|
46
|
+
}>;
|
|
35
47
|
/**
|
|
36
48
|
* Finish a shell frozen at build time, against data that exists now.
|
|
37
49
|
*
|
|
@@ -148,6 +160,17 @@ export interface RscHostOptions {
|
|
|
148
160
|
* through actions; a proxy in front usually has its own limit too.
|
|
149
161
|
*/
|
|
150
162
|
maxActionBody?: number;
|
|
163
|
+
/**
|
|
164
|
+
* gzip what this host answers, for a request that accepts it.
|
|
165
|
+
*
|
|
166
|
+
* On by default where the runtime has a compressor - Node and Bun - and
|
|
167
|
+
* never on a Worker, where the platform compresses. Behind a CDN or a
|
|
168
|
+
* proxy that compresses, the proxy sees an already-encoded answer and
|
|
169
|
+
* leaves it; a bun or node server answering the internet by itself sent
|
|
170
|
+
* every byte raw, which was a port's whole mobile performance story. Off
|
|
171
|
+
* for a deployment that would rather its proxy did it.
|
|
172
|
+
*/
|
|
173
|
+
compress?: boolean;
|
|
151
174
|
}
|
|
152
175
|
/**
|
|
153
176
|
* Whether a browser was told it could post this action.
|
package/dist/host.js
CHANGED
|
@@ -24,8 +24,9 @@ import { withRevalidation } from "./revalidate.js";
|
|
|
24
24
|
*/
|
|
25
25
|
export { revalidate } from "./revalidate.js";
|
|
26
26
|
import { currentNotFound, withRedirect } from "./redirect.js";
|
|
27
|
+
import { compressed } from "./compress.js";
|
|
27
28
|
import { withCache } from "./cache.js";
|
|
28
|
-
import { withRequest, withResponseDraft } from "./request.js";
|
|
29
|
+
import { takeAfterWork, withRequest, withResponseDraft } from "./request.js";
|
|
29
30
|
/**
|
|
30
31
|
* @internal For a host adapter that embeds the engine. An app imports this
|
|
31
32
|
* from `@rsc-kit/core/redirect` - the one the guides teach, and the one an
|
|
@@ -68,6 +69,21 @@ function inScript(value) {
|
|
|
68
69
|
.replace(/\u2028/g, "\\u2028")
|
|
69
70
|
.replace(/\u2029/g, "\\u2029");
|
|
70
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Whether the browser itself asked, for a document.
|
|
74
|
+
*
|
|
75
|
+
* Sec-Fetch-Mode is what every current browser sends and nothing else does;
|
|
76
|
+
* the Accept fallback is for the ones that do not, and for a test's Request.
|
|
77
|
+
* A payload request never counts, whatever it accepts.
|
|
78
|
+
*/
|
|
79
|
+
function isNavigation(request) {
|
|
80
|
+
if (request.headers.has(HEADER.rsc))
|
|
81
|
+
return false;
|
|
82
|
+
const mode = request.headers.get("sec-fetch-mode");
|
|
83
|
+
if (mode)
|
|
84
|
+
return mode === "navigate";
|
|
85
|
+
return (request.headers.get("accept") ?? "").includes("text/html");
|
|
86
|
+
}
|
|
71
87
|
function redirectResponse(to, isPayloadRequest) {
|
|
72
88
|
if (isPayloadRequest) {
|
|
73
89
|
return new Response(null, {
|
|
@@ -272,6 +288,7 @@ function matchPage(routes, url) {
|
|
|
272
288
|
export function createRscHandler(options) {
|
|
273
289
|
const { engine, assets, version } = options;
|
|
274
290
|
const maxActionBody = options.maxActionBody ?? DEFAULT_MAX_ACTION_BODY;
|
|
291
|
+
const compress = options.compress ?? true;
|
|
275
292
|
// Annotated rather than inferred: the narrowing below is lost inside the
|
|
276
293
|
// closures that use it, and every one of them runs after the throw.
|
|
277
294
|
const manifest = options.manifest ?? engine.manifest?.();
|
|
@@ -357,6 +374,52 @@ export function createRscHandler(options) {
|
|
|
357
374
|
// everything below shares it: middleware, layouts, the page, and an action. A
|
|
358
375
|
// guard that reads the session and a layout that reads it again are one
|
|
359
376
|
// query, not two.
|
|
377
|
+
/**
|
|
378
|
+
* The fields of a form posted to a page, or null for anything else.
|
|
379
|
+
*
|
|
380
|
+
* A POST with a form body, from this origin, to a url that is a page and
|
|
381
|
+
* not the action endpoint, with an engine that can run what the form
|
|
382
|
+
* names. Read here so the decision is made once; the action reads the
|
|
383
|
+
* same fields from what is returned.
|
|
384
|
+
*/
|
|
385
|
+
async function formPostOf(request, url) {
|
|
386
|
+
if (request.method !== "POST" || !engine.handleRscFormPost)
|
|
387
|
+
return null;
|
|
388
|
+
if (request.headers.has(HEADER.rsc))
|
|
389
|
+
return null;
|
|
390
|
+
const type = request.headers.get("content-type") ?? "";
|
|
391
|
+
if (!/^(?:application\/x-www-form-urlencoded|multipart\/form-data)/i.test(type))
|
|
392
|
+
return null;
|
|
393
|
+
if (!matchPage(routes, url))
|
|
394
|
+
return null;
|
|
395
|
+
if (!actionOriginAllowed(request, url))
|
|
396
|
+
return null;
|
|
397
|
+
try {
|
|
398
|
+
return await request.formData();
|
|
399
|
+
}
|
|
400
|
+
catch {
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* What a stored answer's compressed bytes are kept under.
|
|
406
|
+
*
|
|
407
|
+
* The build, the url, and the value of every request header the answer
|
|
408
|
+
* says it varies on - which is what tells the document for /login apart
|
|
409
|
+
* from the payload for /login. Keyed by the path alone, the payload
|
|
410
|
+
* request found the document's bytes waiting and hydration decoded HTML
|
|
411
|
+
* as Flight, silently, on every stored page in production.
|
|
412
|
+
*/
|
|
413
|
+
function storedKey(request, response) {
|
|
414
|
+
const url = new URL(request.url);
|
|
415
|
+
const varies = (response.headers.get("Vary") ?? "")
|
|
416
|
+
.split(",")
|
|
417
|
+
.map((name) => name.trim().toLowerCase())
|
|
418
|
+
.filter((name) => name && name !== "*" && name !== "accept-encoding")
|
|
419
|
+
.sort()
|
|
420
|
+
.map((name) => `${name}=${request.headers.get(name) ?? ""}`);
|
|
421
|
+
return [version ?? "", url.pathname + url.search, ...varies].join("\n");
|
|
422
|
+
}
|
|
360
423
|
return async function handle(request) {
|
|
361
424
|
return await withRequest(request, () => withCache(() =>
|
|
362
425
|
// Open for the whole request and sealed the moment an answer exists,
|
|
@@ -391,6 +454,26 @@ export function createRscHandler(options) {
|
|
|
391
454
|
// off for a team whose policy strips every framework identifier.
|
|
392
455
|
if (identify)
|
|
393
456
|
response.headers.set("X-Powered-By", "rsc-kit");
|
|
457
|
+
// Work after() queued, now that the answer exists. A Worker keeps
|
|
458
|
+
// the isolate alive only for what is handed to waitUntil - Nitro's
|
|
459
|
+
// Cloudflare preset puts the execution context on the request - so
|
|
460
|
+
// it goes there where it can; a process keeps a detached promise.
|
|
461
|
+
const pending = takeAfterWork();
|
|
462
|
+
if (pending) {
|
|
463
|
+
const context = request.context;
|
|
464
|
+
if (typeof context?.waitUntil === "function")
|
|
465
|
+
context.waitUntil(pending);
|
|
466
|
+
}
|
|
467
|
+
// Last, over the finished answer, headers and all. A stored answer
|
|
468
|
+
// is the same bytes for everyone and is compressed once, keyed by
|
|
469
|
+
// the build and the url it was stored for.
|
|
470
|
+
if (compress) {
|
|
471
|
+
const answer = await compressed(request, response, servedFrom.get(response) === "stored" ? storedKey(request, response) : undefined);
|
|
472
|
+
const from = servedFrom.get(response);
|
|
473
|
+
if (answer !== response && from)
|
|
474
|
+
servedFrom.set(answer, from);
|
|
475
|
+
return answer;
|
|
476
|
+
}
|
|
394
477
|
return response;
|
|
395
478
|
})));
|
|
396
479
|
};
|
|
@@ -453,7 +536,23 @@ export function createRscHandler(options) {
|
|
|
453
536
|
const stored = await frozenApi(request, url, api);
|
|
454
537
|
if (stored)
|
|
455
538
|
return stored;
|
|
456
|
-
|
|
539
|
+
// A redirect() thrown from the handler is the route's answer, not a
|
|
540
|
+
// fault: a real Location, because whoever asked is meant to go there
|
|
541
|
+
// - a browser that followed a link to this route, or a fetch of an
|
|
542
|
+
// export that lives on a signed url. notFound() is its 404.
|
|
543
|
+
return await withRedirect(async (taken) => {
|
|
544
|
+
try {
|
|
545
|
+
return await engine.handleApiRoute(api.route.name, request, api.params, allowFor(api.route));
|
|
546
|
+
}
|
|
547
|
+
catch (error) {
|
|
548
|
+
const redirected = taken();
|
|
549
|
+
if (redirected)
|
|
550
|
+
return redirectResponse(redirected, false);
|
|
551
|
+
if (currentNotFound())
|
|
552
|
+
return new Response("Not found", { status: 404 });
|
|
553
|
+
throw error;
|
|
554
|
+
}
|
|
555
|
+
});
|
|
457
556
|
}
|
|
458
557
|
if (request.method === "GET" && url.pathname === HEADER.queryPath) {
|
|
459
558
|
// Same check as an action, for a smaller reason: a cross-origin page
|
|
@@ -475,7 +574,14 @@ export function createRscHandler(options) {
|
|
|
475
574
|
url.pathname === HEADER.pprResumePath) {
|
|
476
575
|
return await servePprResume(request, url, options.prerendered);
|
|
477
576
|
}
|
|
478
|
-
|
|
577
|
+
// A form submitted before the page had a runtime: the browser posts it
|
|
578
|
+
// to the page's own url, as React wrote it, with the action's id among
|
|
579
|
+
// the fields. The action runs and the page renders with the result -
|
|
580
|
+
// what the guide promises for a form that had to work without
|
|
581
|
+
// javascript. Same-origin, as an action is; anything else that is not
|
|
582
|
+
// a read is not this host's.
|
|
583
|
+
const formPost = await formPostOf(request, url);
|
|
584
|
+
if (!formPost && request.method !== "GET" && request.method !== "HEAD")
|
|
479
585
|
return null;
|
|
480
586
|
// One named region of this page, asked for without mutating anything to
|
|
481
587
|
// earn it. What an action invalidated does not come through here — that
|
|
@@ -498,7 +604,7 @@ export function createRscHandler(options) {
|
|
|
498
604
|
// document puts the entire page inside that region, and answering an
|
|
499
605
|
// interception with it replaces the page the modal was opening over.
|
|
500
606
|
const match = matchPage(routes, url);
|
|
501
|
-
if (options.prerendered) {
|
|
607
|
+
if (options.prerendered && !formPost) {
|
|
502
608
|
// A guarded route can still be frozen: whether the content is the same
|
|
503
609
|
// for everyone, and whether this caller may see it, are different
|
|
504
610
|
// questions. The build answers the first; this answers the second, and
|
|
@@ -534,11 +640,13 @@ export function createRscHandler(options) {
|
|
|
534
640
|
// still available. Nothing is buffered to make that true.
|
|
535
641
|
let htmlStream;
|
|
536
642
|
try {
|
|
537
|
-
({ htmlStream } =
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
643
|
+
({ htmlStream } = formPost
|
|
644
|
+
? await engine.handleRscFormPost(match.route.component, props, layouts, match.route.loadings, match.route.slots, {}, undefined, url.pathname, true, formPost)
|
|
645
|
+
: await engine.handleRscHtmlStream(match.route.component, props, layouts, match.route.loadings, match.route.slots, {}, undefined, url.pathname,
|
|
646
|
+
// A route that ships no runtime gets no bootstrap and no segment
|
|
647
|
+
// boundary — the boundary is itself a client component, so leaving
|
|
648
|
+
// it in means no page could ever be JS-free.
|
|
649
|
+
true));
|
|
542
650
|
}
|
|
543
651
|
catch (error) {
|
|
544
652
|
// A rejected shell is how a redirect above every boundary arrives:
|
|
@@ -585,9 +693,13 @@ export function createRscHandler(options) {
|
|
|
585
693
|
"Content-Type": HTML_TYPE,
|
|
586
694
|
[HEADER.layouts]: chain.join(","),
|
|
587
695
|
Vary: VARY_ON_RSC,
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
696
|
+
// The answer to a post is the result of something that happened
|
|
697
|
+
// once; nothing may keep it.
|
|
698
|
+
"Cache-Control": formPost
|
|
699
|
+
? "no-store"
|
|
700
|
+
: match.route.middleware?.length
|
|
701
|
+
? PER_CLIENT
|
|
702
|
+
: REVALIDATE,
|
|
591
703
|
}),
|
|
592
704
|
});
|
|
593
705
|
});
|
|
@@ -918,13 +1030,19 @@ export function createRscHandler(options) {
|
|
|
918
1030
|
const payload = variant ?? (await read(`${key}.flight`));
|
|
919
1031
|
if (payload === null)
|
|
920
1032
|
return null;
|
|
1033
|
+
// As cacheable as the document it boots: the same build-time bytes for
|
|
1034
|
+
// everyone, unless a guard above the route decides who may have them.
|
|
1035
|
+
// Marked no-store, the service worker refused to keep it, and a
|
|
1036
|
+
// precached page rendered offline and never hydrated - the markup was
|
|
1037
|
+
// there and the payload it boots from was not.
|
|
1038
|
+
const guarded = matchPage(routes, url)?.route.middleware?.length ?? 0;
|
|
921
1039
|
return new Response(payload, {
|
|
922
1040
|
headers: withVersion({
|
|
923
1041
|
"Content-Type": FLIGHT_TYPE,
|
|
924
1042
|
[HEADER.segmentDepth]: String(variant ? shared : 0),
|
|
925
1043
|
[HEADER.layouts]: chain.join(","),
|
|
926
1044
|
Vary: VARY_ON_RSC,
|
|
927
|
-
"Cache-Control": PER_CLIENT,
|
|
1045
|
+
"Cache-Control": guarded ? PER_CLIENT : REVALIDATE,
|
|
928
1046
|
}),
|
|
929
1047
|
});
|
|
930
1048
|
}
|
|
@@ -1117,12 +1235,8 @@ export function createRscHandler(options) {
|
|
|
1117
1235
|
// A redirect is a refusal here. Where it was going is told rather than
|
|
1118
1236
|
// followed, so a client can decide for itself.
|
|
1119
1237
|
const redirected = taken();
|
|
1120
|
-
if (redirected)
|
|
1121
|
-
return
|
|
1122
|
-
status: 401,
|
|
1123
|
-
headers: { "X-RSC-Redirect": redirected.location },
|
|
1124
|
-
});
|
|
1125
|
-
}
|
|
1238
|
+
if (redirected)
|
|
1239
|
+
return apiRedirect(request, redirected);
|
|
1126
1240
|
// A visitor who may not use this endpoint has not caused a server
|
|
1127
1241
|
// error, and answering 500 makes a guarded route indistinguishable
|
|
1128
1242
|
// from a broken one. Null means the middleware threw something that is
|
|
@@ -1135,15 +1249,29 @@ export function createRscHandler(options) {
|
|
|
1135
1249
|
});
|
|
1136
1250
|
}
|
|
1137
1251
|
const redirected = taken();
|
|
1138
|
-
if (redirected)
|
|
1139
|
-
return
|
|
1140
|
-
status: 401,
|
|
1141
|
-
headers: { "X-RSC-Redirect": redirected.location },
|
|
1142
|
-
});
|
|
1143
|
-
}
|
|
1252
|
+
if (redirected)
|
|
1253
|
+
return apiRedirect(request, redirected);
|
|
1144
1254
|
return null;
|
|
1145
1255
|
});
|
|
1146
1256
|
}
|
|
1257
|
+
/**
|
|
1258
|
+
* A guard's redirect on a route.ts, answered for whoever asked.
|
|
1259
|
+
*
|
|
1260
|
+
* A browser that navigated here - followed a link to the route, typed the
|
|
1261
|
+
* url - is sent on with a real Location; a 401 would show it "Unauthorized"
|
|
1262
|
+
* over a page it cannot see. Code that fetched the route is told instead:
|
|
1263
|
+
* fetch follows a Location on its own and would hand back the login page's
|
|
1264
|
+
* html as the endpoint's answer, so it gets the 401 with the destination in
|
|
1265
|
+
* a header, and decides for itself.
|
|
1266
|
+
*/
|
|
1267
|
+
function apiRedirect(request, to) {
|
|
1268
|
+
if (isNavigation(request))
|
|
1269
|
+
return redirectResponse(to, false);
|
|
1270
|
+
return new Response("Unauthorized", {
|
|
1271
|
+
status: 401,
|
|
1272
|
+
headers: { "X-RSC-Redirect": to.location },
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1147
1275
|
async function handleQuery(request, url) {
|
|
1148
1276
|
if (!engine.handleQuery)
|
|
1149
1277
|
return null;
|
|
@@ -1216,12 +1344,28 @@ export function createRscHandler(options) {
|
|
|
1216
1344
|
: undefined;
|
|
1217
1345
|
// Scoped to this action: revalidate() called anywhere inside it, at any
|
|
1218
1346
|
// depth, marks here and nowhere else — two requests can be in flight and
|
|
1219
|
-
// marking is per-request state.
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1347
|
+
// marking is per-request state. And redirect(): the guide says "throw
|
|
1348
|
+
// from the action and the client follows it", and the client does
|
|
1349
|
+
// follow an X-RSC-Redirect on an action's response — but the signal the
|
|
1350
|
+
// throw raises was never caught here, so every login that redirected
|
|
1351
|
+
// after signing in was a 500. Caught, it is the instruction the client
|
|
1352
|
+
// already knows how to read.
|
|
1353
|
+
return await withRedirect(async (redirected) => {
|
|
1354
|
+
let stream;
|
|
1355
|
+
try {
|
|
1356
|
+
({ stream } = await withRevalidation((taken) => engine.handleAction(actionId, body, contentType, page, taken)));
|
|
1357
|
+
}
|
|
1358
|
+
catch (error) {
|
|
1359
|
+
const to = redirected();
|
|
1360
|
+
if (to)
|
|
1361
|
+
return redirectResponse(to, true);
|
|
1362
|
+
throw error;
|
|
1363
|
+
}
|
|
1364
|
+
return new Response(stream, {
|
|
1365
|
+
headers: withVersion({
|
|
1366
|
+
"Content-Type": "text/x-component; charset=utf-8",
|
|
1367
|
+
}),
|
|
1368
|
+
});
|
|
1225
1369
|
});
|
|
1226
1370
|
}
|
|
1227
1371
|
}
|