@funstack/static 0.0.2 → 0.0.4
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/README.md +12 -3
- package/dist/bin/skill-installer.d.mts +1 -0
- package/dist/bin/skill-installer.mjs +12 -0
- package/dist/bin/skill-installer.mjs.map +1 -0
- package/dist/build/buildApp.mjs +4 -3
- package/dist/build/buildApp.mjs.map +1 -1
- package/dist/build/rscProcessor.mjs +9 -3
- package/dist/build/rscProcessor.mjs.map +1 -1
- package/dist/client/entry.d.mts +1 -0
- package/dist/client/entry.mjs +17 -9
- package/dist/client/entry.mjs.map +1 -1
- package/dist/client/globals.mjs.map +1 -1
- package/dist/docs/FAQ.md +5 -0
- package/dist/docs/GettingStarted.md +180 -0
- package/dist/docs/MigratingFromViteSPA.md +321 -0
- package/dist/docs/api/Defer.md +110 -0
- package/dist/docs/api/FunstackStatic.md +184 -0
- package/dist/docs/index.md +22 -0
- package/dist/docs/learn/HowItWorks.md +109 -0
- package/dist/docs/learn/LazyServerComponents.md +120 -0
- package/dist/docs/learn/OptimizingPayloads.md +107 -0
- package/dist/docs/learn/RSC.md +179 -0
- package/dist/docs/learn/SSR.md +104 -0
- package/dist/entries/client.d.mts +1 -1
- package/dist/entries/rsc-client.d.mts +2 -2
- package/dist/entries/rsc-client.mjs +2 -2
- package/dist/entries/server.d.mts +2 -2
- package/dist/plugin/index.d.mts +17 -1
- package/dist/plugin/index.d.mts.map +1 -1
- package/dist/plugin/index.mjs +10 -1
- package/dist/plugin/index.mjs.map +1 -1
- package/dist/rsc/defer.d.mts +18 -3
- package/dist/rsc/defer.d.mts.map +1 -1
- package/dist/rsc/defer.mjs +34 -14
- package/dist/rsc/defer.mjs.map +1 -1
- package/dist/rsc/entry.d.mts.map +1 -1
- package/dist/rsc/entry.mjs +85 -20
- package/dist/rsc/entry.mjs.map +1 -1
- package/dist/rsc-client/clientWrapper.d.mts +3 -3
- package/dist/rsc-client/clientWrapper.d.mts.map +1 -1
- package/dist/rsc-client/clientWrapper.mjs +2 -2
- package/dist/rsc-client/clientWrapper.mjs.map +1 -1
- package/dist/rsc-client/entry.d.mts +1 -1
- package/dist/rsc-client/entry.mjs +1 -1
- package/dist/ssr/entry.d.mts +2 -0
- package/dist/ssr/entry.d.mts.map +1 -1
- package/dist/ssr/entry.mjs +6 -2
- package/dist/ssr/entry.mjs.map +1 -1
- package/package.json +26 -11
- package/skills/funstack-static-knowledge/SKILL.md +44 -0
package/dist/plugin/index.mjs
CHANGED
|
@@ -4,9 +4,10 @@ import path from "node:path";
|
|
|
4
4
|
import rsc from "@vitejs/plugin-rsc";
|
|
5
5
|
|
|
6
6
|
//#region src/plugin/index.ts
|
|
7
|
-
function funstackStatic({ root, app, publicOutDir = "dist/public" }) {
|
|
7
|
+
function funstackStatic({ root, app, publicOutDir = "dist/public", ssr = false, clientInit }) {
|
|
8
8
|
let resolvedRootEntry = "__uninitialized__";
|
|
9
9
|
let resolvedAppEntry = "__uninitialized__";
|
|
10
|
+
let resolvedClientInitEntry;
|
|
10
11
|
return [
|
|
11
12
|
{
|
|
12
13
|
name: "@funstack/static:config-pre",
|
|
@@ -28,6 +29,7 @@ function funstackStatic({ root, app, publicOutDir = "dist/public" }) {
|
|
|
28
29
|
configResolved(config) {
|
|
29
30
|
resolvedRootEntry = path.resolve(config.root, root);
|
|
30
31
|
resolvedAppEntry = path.resolve(config.root, app);
|
|
32
|
+
if (clientInit) resolvedClientInitEntry = path.resolve(config.root, clientInit);
|
|
31
33
|
},
|
|
32
34
|
configEnvironment(_name, config) {
|
|
33
35
|
if (config.optimizeDeps?.include) config.optimizeDeps.include = config.optimizeDeps.include.map((entry) => {
|
|
@@ -41,10 +43,17 @@ function funstackStatic({ root, app, publicOutDir = "dist/public" }) {
|
|
|
41
43
|
resolveId(id) {
|
|
42
44
|
if (id === "virtual:funstack/root") return "\0virtual:funstack/root";
|
|
43
45
|
if (id === "virtual:funstack/app") return "\0virtual:funstack/app";
|
|
46
|
+
if (id === "virtual:funstack/config") return "\0virtual:funstack/config";
|
|
47
|
+
if (id === "virtual:funstack/client-init") return "\0virtual:funstack/client-init";
|
|
44
48
|
},
|
|
45
49
|
load(id) {
|
|
46
50
|
if (id === "\0virtual:funstack/root") return `export { default } from "${resolvedRootEntry}";`;
|
|
47
51
|
if (id === "\0virtual:funstack/app") return `export { default } from "${resolvedAppEntry}";`;
|
|
52
|
+
if (id === "\0virtual:funstack/config") return `export const ssr = ${JSON.stringify(ssr)};`;
|
|
53
|
+
if (id === "\0virtual:funstack/client-init") {
|
|
54
|
+
if (resolvedClientInitEntry) return `import "${resolvedClientInitEntry}";`;
|
|
55
|
+
return "";
|
|
56
|
+
}
|
|
48
57
|
}
|
|
49
58
|
},
|
|
50
59
|
{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/plugin/index.ts"],"sourcesContent":["import path from \"node:path\";\nimport type { Plugin } from \"vite\";\nimport rsc from \"@vitejs/plugin-rsc\";\nimport { buildApp } from \"../build/buildApp\";\nimport { serverPlugin } from \"./server\";\n\nexport interface FunstackStaticOptions {\n /**\n * Root component of the page.\n * The file should `export default` a React component that renders the whole page.\n * (`<html>...</html>`).\n */\n root: string;\n /**\n * Entry point of your application.\n * The file should `export default` a React component that renders the application content.\n */\n app: string;\n /**\n * Output directory for build.\n *\n * @default dist/public\n */\n publicOutDir?: string;\n}\n\nexport default function funstackStatic({\n root,\n app,\n publicOutDir = \"dist/public\",\n}: FunstackStaticOptions): (Plugin | Plugin[])[] {\n let resolvedRootEntry: string = \"__uninitialized__\";\n let resolvedAppEntry: string = \"__uninitialized__\";\n\n return [\n {\n name: \"@funstack/static:config-pre\",\n // Placed early because the rsc plugin sets the outDir to the default value\n config(config) {\n return {\n environments: {\n client: {\n build: {\n outDir:\n config.environments?.client?.build?.outDir ?? publicOutDir,\n },\n },\n },\n };\n },\n },\n serverPlugin(),\n rsc({\n entries: {\n rsc: \"@funstack/static/entries/rsc\",\n ssr: \"@funstack/static/entries/ssr\",\n client: \"@funstack/static/entries/client\",\n },\n serverHandler: false,\n }),\n {\n name: \"@funstack/static:config\",\n configResolved(config) {\n resolvedRootEntry = path.resolve(config.root, root);\n resolvedAppEntry = path.resolve(config.root, app);\n },\n // Needed for properly bundling @vitejs/plugin-rsc for browser.\n // See: https://github.com/vitejs/vite-plugin-react/tree/79bf57cc8b9c77e33970ec2e876bd6d2f1568d5d/packages/plugin-rsc#using-vitejsplugin-rsc-as-a-framework-packages-dependencies\n configEnvironment(_name, config) {\n if (config.optimizeDeps?.include) {\n config.optimizeDeps.include = config.optimizeDeps.include.map(\n (entry) => {\n if (entry.startsWith(\"@vitejs/plugin-rsc\")) {\n entry = `@funstack/static > ${entry}`;\n }\n return entry;\n },\n );\n }\n },\n },\n {\n name: \"@funstack/static:virtual-entry\",\n resolveId(id) {\n if (id === \"virtual:funstack/root\") {\n return \"\\0virtual:funstack/root\";\n }\n if (id === \"virtual:funstack/app\") {\n return \"\\0virtual:funstack/app\";\n }\n },\n load(id) {\n if (id === \"\\0virtual:funstack/root\") {\n return `export { default } from \"${resolvedRootEntry}\";`;\n }\n if (id === \"\\0virtual:funstack/app\") {\n return `export { default } from \"${resolvedAppEntry}\";`;\n }\n },\n },\n {\n name: \"@funstack/static:build\",\n async buildApp(builder) {\n await buildApp(builder, this);\n },\n },\n ];\n}\n"],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/plugin/index.ts"],"sourcesContent":["import path from \"node:path\";\nimport type { Plugin } from \"vite\";\nimport rsc from \"@vitejs/plugin-rsc\";\nimport { buildApp } from \"../build/buildApp\";\nimport { serverPlugin } from \"./server\";\n\nexport interface FunstackStaticOptions {\n /**\n * Root component of the page.\n * The file should `export default` a React component that renders the whole page.\n * (`<html>...</html>`).\n */\n root: string;\n /**\n * Entry point of your application.\n * The file should `export default` a React component that renders the application content.\n */\n app: string;\n /**\n * Output directory for build.\n *\n * @default dist/public\n */\n publicOutDir?: string;\n /**\n * Enable server-side rendering of the App component.\n * When false, only the Root shell is SSR'd and the App renders client-side.\n * When true, both Root and App are SSR'd and the client hydrates.\n *\n * @default false\n */\n ssr?: boolean;\n /**\n * Path to a module that runs on the client side before React hydration.\n * Use this for client-side instrumentation like Sentry, analytics, or feature flags.\n * The module is imported for its side effects only (no exports needed).\n */\n clientInit?: string;\n}\n\nexport default function funstackStatic({\n root,\n app,\n publicOutDir = \"dist/public\",\n ssr = false,\n clientInit,\n}: FunstackStaticOptions): (Plugin | Plugin[])[] {\n let resolvedRootEntry: string = \"__uninitialized__\";\n let resolvedAppEntry: string = \"__uninitialized__\";\n let resolvedClientInitEntry: string | undefined;\n\n return [\n {\n name: \"@funstack/static:config-pre\",\n // Placed early because the rsc plugin sets the outDir to the default value\n config(config) {\n return {\n environments: {\n client: {\n build: {\n outDir:\n config.environments?.client?.build?.outDir ?? publicOutDir,\n },\n },\n },\n };\n },\n },\n serverPlugin(),\n rsc({\n entries: {\n rsc: \"@funstack/static/entries/rsc\",\n ssr: \"@funstack/static/entries/ssr\",\n client: \"@funstack/static/entries/client\",\n },\n serverHandler: false,\n }),\n {\n name: \"@funstack/static:config\",\n configResolved(config) {\n resolvedRootEntry = path.resolve(config.root, root);\n resolvedAppEntry = path.resolve(config.root, app);\n if (clientInit) {\n resolvedClientInitEntry = path.resolve(config.root, clientInit);\n }\n },\n // Needed for properly bundling @vitejs/plugin-rsc for browser.\n // See: https://github.com/vitejs/vite-plugin-react/tree/79bf57cc8b9c77e33970ec2e876bd6d2f1568d5d/packages/plugin-rsc#using-vitejsplugin-rsc-as-a-framework-packages-dependencies\n configEnvironment(_name, config) {\n if (config.optimizeDeps?.include) {\n config.optimizeDeps.include = config.optimizeDeps.include.map(\n (entry) => {\n if (entry.startsWith(\"@vitejs/plugin-rsc\")) {\n entry = `@funstack/static > ${entry}`;\n }\n return entry;\n },\n );\n }\n },\n },\n {\n name: \"@funstack/static:virtual-entry\",\n resolveId(id) {\n if (id === \"virtual:funstack/root\") {\n return \"\\0virtual:funstack/root\";\n }\n if (id === \"virtual:funstack/app\") {\n return \"\\0virtual:funstack/app\";\n }\n if (id === \"virtual:funstack/config\") {\n return \"\\0virtual:funstack/config\";\n }\n if (id === \"virtual:funstack/client-init\") {\n return \"\\0virtual:funstack/client-init\";\n }\n },\n load(id) {\n if (id === \"\\0virtual:funstack/root\") {\n return `export { default } from \"${resolvedRootEntry}\";`;\n }\n if (id === \"\\0virtual:funstack/app\") {\n return `export { default } from \"${resolvedAppEntry}\";`;\n }\n if (id === \"\\0virtual:funstack/config\") {\n return `export const ssr = ${JSON.stringify(ssr)};`;\n }\n if (id === \"\\0virtual:funstack/client-init\") {\n if (resolvedClientInitEntry) {\n return `import \"${resolvedClientInitEntry}\";`;\n }\n return \"\";\n }\n },\n },\n {\n name: \"@funstack/static:build\",\n async buildApp(builder) {\n await buildApp(builder, this);\n },\n },\n ];\n}\n"],"mappings":";;;;;;AAwCA,SAAwB,eAAe,EACrC,MACA,KACA,eAAe,eACf,MAAM,OACN,cAC+C;CAC/C,IAAI,oBAA4B;CAChC,IAAI,mBAA2B;CAC/B,IAAI;AAEJ,QAAO;EACL;GACE,MAAM;GAEN,OAAO,QAAQ;AACb,WAAO,EACL,cAAc,EACZ,QAAQ,EACN,OAAO,EACL,QACE,OAAO,cAAc,QAAQ,OAAO,UAAU,cACjD,EACF,EACF,EACF;;GAEJ;EACD,cAAc;EACd,IAAI;GACF,SAAS;IACP,KAAK;IACL,KAAK;IACL,QAAQ;IACT;GACD,eAAe;GAChB,CAAC;EACF;GACE,MAAM;GACN,eAAe,QAAQ;AACrB,wBAAoB,KAAK,QAAQ,OAAO,MAAM,KAAK;AACnD,uBAAmB,KAAK,QAAQ,OAAO,MAAM,IAAI;AACjD,QAAI,WACF,2BAA0B,KAAK,QAAQ,OAAO,MAAM,WAAW;;GAKnE,kBAAkB,OAAO,QAAQ;AAC/B,QAAI,OAAO,cAAc,QACvB,QAAO,aAAa,UAAU,OAAO,aAAa,QAAQ,KACvD,UAAU;AACT,SAAI,MAAM,WAAW,qBAAqB,CACxC,SAAQ,sBAAsB;AAEhC,YAAO;MAEV;;GAGN;EACD;GACE,MAAM;GACN,UAAU,IAAI;AACZ,QAAI,OAAO,wBACT,QAAO;AAET,QAAI,OAAO,uBACT,QAAO;AAET,QAAI,OAAO,0BACT,QAAO;AAET,QAAI,OAAO,+BACT,QAAO;;GAGX,KAAK,IAAI;AACP,QAAI,OAAO,0BACT,QAAO,4BAA4B,kBAAkB;AAEvD,QAAI,OAAO,yBACT,QAAO,4BAA4B,iBAAiB;AAEtD,QAAI,OAAO,4BACT,QAAO,sBAAsB,KAAK,UAAU,IAAI,CAAC;AAEnD,QAAI,OAAO,kCAAkC;AAC3C,SAAI,wBACF,QAAO,WAAW,wBAAwB;AAE5C,YAAO;;;GAGZ;EACD;GACE,MAAM;GACN,MAAM,SAAS,SAAS;AACtB,UAAM,SAAS,SAAS,KAAK;;GAEhC;EACF"}
|
package/dist/rsc/defer.d.mts
CHANGED
|
@@ -3,6 +3,18 @@ import { ReactElement, ReactNode } from "react";
|
|
|
3
3
|
//#region src/rsc/defer.d.ts
|
|
4
4
|
interface DeferEntry {
|
|
5
5
|
state: DeferEntryState;
|
|
6
|
+
name?: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Options for the defer function.
|
|
10
|
+
*/
|
|
11
|
+
interface DeferOptions {
|
|
12
|
+
/**
|
|
13
|
+
* Optional name for debugging purposes.
|
|
14
|
+
* In development: included in the RSC payload file name.
|
|
15
|
+
* In production: logged when the payload file is emitted.
|
|
16
|
+
*/
|
|
17
|
+
name?: string;
|
|
6
18
|
}
|
|
7
19
|
interface LoadedDeferEntry extends DeferEntry {
|
|
8
20
|
state: Exclude<DeferEntryState, {
|
|
@@ -24,7 +36,7 @@ type DeferEntryState = {
|
|
|
24
36
|
};
|
|
25
37
|
declare class DeferRegistry {
|
|
26
38
|
#private;
|
|
27
|
-
register(element: ReactElement, id: string): void;
|
|
39
|
+
register(element: ReactElement, id: string, name?: string): void;
|
|
28
40
|
load(id: string): LoadedDeferEntry | undefined;
|
|
29
41
|
has(id: string): boolean;
|
|
30
42
|
/**
|
|
@@ -34,6 +46,7 @@ declare class DeferRegistry {
|
|
|
34
46
|
loadAll(): AsyncGenerator<{
|
|
35
47
|
id: string;
|
|
36
48
|
data: string;
|
|
49
|
+
name?: string;
|
|
37
50
|
}, void, unknown>;
|
|
38
51
|
}
|
|
39
52
|
/**
|
|
@@ -42,9 +55,11 @@ declare class DeferRegistry {
|
|
|
42
55
|
* During the client side rendering, fetching of the payload will be
|
|
43
56
|
* deferred until the returned ReactNode is actually rendered.
|
|
44
57
|
*
|
|
58
|
+
* @param element - The React element to defer.
|
|
59
|
+
* @param options - Optional configuration for the deferred payload.
|
|
45
60
|
* @returns A ReactNode that virtually contains the result of rendering the given component.
|
|
46
61
|
*/
|
|
47
|
-
declare function defer(element: ReactElement): ReactNode;
|
|
62
|
+
declare function defer(element: ReactElement, options?: DeferOptions): ReactNode;
|
|
48
63
|
//#endregion
|
|
49
|
-
export { DeferRegistry, defer };
|
|
64
|
+
export { DeferOptions, DeferRegistry, defer };
|
|
50
65
|
//# sourceMappingURL=defer.d.mts.map
|
package/dist/rsc/defer.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"defer.d.mts","names":[],"sources":["../../src/rsc/defer.tsx"],"
|
|
1
|
+
{"version":3,"file":"defer.d.mts","names":[],"sources":["../../src/rsc/defer.tsx"],"mappings":";;;UAMiB,UAAA;EACf,KAAA,EAAO,eAAA;EACP,IAAA;AAAA;;;;UAMe,YAAA;EANf;;;AAMF;;EAME,IAAA;AAAA;AAAA,UAGe,gBAAA,SAAyB,UAAA;EACxC,KAAA,EAAO,OAAA,CAAQ,eAAA;IAAmB,KAAA;EAAA;AAAA;AAAA,KAG/B,eAAA;EAEC,KAAA;EACA,OAAA,EAAS,YAAA;AAAA;EAGT,KAAA;EACA,MAAA,EAAQ,cAAA,CAAe,UAAA;AAAA;EAGvB,KAAA;EACA,IAAA;AAAA;EAGA,KAAA;EACA,KAAA;AAAA;AAAA,cAeO,aAAA;EAAA;EAGX,QAAA,CAAS,OAAA,EAAS,YAAA,EAAc,EAAA,UAAY,IAAA;EAI5C,IAAA,CAAK,EAAA,WAAa,gBAAA;EAwClB,GAAA,CAAI,EAAA;EAtEsB;;;;EA8EnB,OAAA,CAAA,GAAO,cAAA;;;;;;;;;AAvDhB;;;;;;;iBA2IgB,KAAA,CACd,OAAA,EAAS,YAAA,EACT,OAAA,GAAU,YAAA,GACT,SAAA"}
|
package/dist/rsc/defer.mjs
CHANGED
|
@@ -2,16 +2,26 @@ import { getPayloadIDFor } from "./rscModule.mjs";
|
|
|
2
2
|
import { drainStream } from "../util/drainStream.mjs";
|
|
3
3
|
import { jsx } from "react/jsx-runtime";
|
|
4
4
|
import { renderToReadableStream } from "@vitejs/plugin-rsc/react/rsc";
|
|
5
|
-
import {
|
|
5
|
+
import { DeferredComponent } from "#rsc-client";
|
|
6
6
|
|
|
7
7
|
//#region src/rsc/defer.tsx
|
|
8
|
+
/**
|
|
9
|
+
* Sanitizes a name for use in file paths.
|
|
10
|
+
* Replaces non-alphanumeric characters with underscores and limits length.
|
|
11
|
+
*/
|
|
12
|
+
function sanitizeName(name) {
|
|
13
|
+
return name.replace(/[^a-zA-Z0-9_-]/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "").slice(0, 50);
|
|
14
|
+
}
|
|
8
15
|
var DeferRegistry = class {
|
|
9
16
|
#registry = /* @__PURE__ */ new Map();
|
|
10
|
-
register(element, id) {
|
|
11
|
-
this.#registry.set(id, {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
17
|
+
register(element, id, name) {
|
|
18
|
+
this.#registry.set(id, {
|
|
19
|
+
state: {
|
|
20
|
+
element,
|
|
21
|
+
state: "pending"
|
|
22
|
+
},
|
|
23
|
+
name
|
|
24
|
+
});
|
|
15
25
|
}
|
|
16
26
|
load(id) {
|
|
17
27
|
const entry = this.#registry.get(id);
|
|
@@ -58,7 +68,11 @@ var DeferRegistry = class {
|
|
|
58
68
|
*/
|
|
59
69
|
async *loadAll() {
|
|
60
70
|
const errors = [];
|
|
61
|
-
const loadedEntries = Array.from(this.#registry, ([id, entry]) => [
|
|
71
|
+
const loadedEntries = Array.from(this.#registry, ([id, entry]) => [
|
|
72
|
+
id,
|
|
73
|
+
this.#loadEntry(entry),
|
|
74
|
+
entry.name
|
|
75
|
+
]);
|
|
62
76
|
if (loadedEntries.length === 0) return;
|
|
63
77
|
const completed = [];
|
|
64
78
|
let waiting;
|
|
@@ -68,19 +82,21 @@ var DeferRegistry = class {
|
|
|
68
82
|
remainingCount--;
|
|
69
83
|
waiting?.();
|
|
70
84
|
};
|
|
71
|
-
for (const [id, loadedEntry] of loadedEntries) (async () => {
|
|
85
|
+
for (const [id, loadedEntry, name] of loadedEntries) (async () => {
|
|
72
86
|
try {
|
|
73
87
|
switch (loadedEntry.state.state) {
|
|
74
88
|
case "streaming":
|
|
75
89
|
onComplete({
|
|
76
90
|
id,
|
|
77
|
-
data: await drainStream(loadedEntry.state.stream)
|
|
91
|
+
data: await drainStream(loadedEntry.state.stream),
|
|
92
|
+
name
|
|
78
93
|
});
|
|
79
94
|
break;
|
|
80
95
|
case "ready":
|
|
81
96
|
onComplete({
|
|
82
97
|
id,
|
|
83
|
-
data: loadedEntry.state.data
|
|
98
|
+
data: loadedEntry.state.data,
|
|
99
|
+
name
|
|
84
100
|
});
|
|
85
101
|
break;
|
|
86
102
|
case "error":
|
|
@@ -111,12 +127,16 @@ const deferRegistry = new DeferRegistry();
|
|
|
111
127
|
* During the client side rendering, fetching of the payload will be
|
|
112
128
|
* deferred until the returned ReactNode is actually rendered.
|
|
113
129
|
*
|
|
130
|
+
* @param element - The React element to defer.
|
|
131
|
+
* @param options - Optional configuration for the deferred payload.
|
|
114
132
|
* @returns A ReactNode that virtually contains the result of rendering the given component.
|
|
115
133
|
*/
|
|
116
|
-
function defer(element) {
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
134
|
+
function defer(element, options) {
|
|
135
|
+
const name = options?.name;
|
|
136
|
+
const sanitizedName = name ? sanitizeName(name) : void 0;
|
|
137
|
+
const id = getPayloadIDFor(sanitizedName ? `${sanitizedName}-${crypto.randomUUID()}` : crypto.randomUUID());
|
|
138
|
+
deferRegistry.register(element, id, name);
|
|
139
|
+
return /* @__PURE__ */ jsx(DeferredComponent, { moduleID: id });
|
|
120
140
|
}
|
|
121
141
|
|
|
122
142
|
//#endregion
|
package/dist/rsc/defer.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"defer.mjs","names":["#registry","#loadEntry"],"sources":["../../src/rsc/defer.tsx"],"sourcesContent":["import type { ReactElement, ReactNode } from \"react\";\nimport { renderToReadableStream } from \"@vitejs/plugin-rsc/react/rsc\";\nimport {
|
|
1
|
+
{"version":3,"file":"defer.mjs","names":["#registry","#loadEntry"],"sources":["../../src/rsc/defer.tsx"],"sourcesContent":["import type { ReactElement, ReactNode } from \"react\";\nimport { renderToReadableStream } from \"@vitejs/plugin-rsc/react/rsc\";\nimport { DeferredComponent } from \"#rsc-client\";\nimport { drainStream } from \"../util/drainStream\";\nimport { getPayloadIDFor } from \"./rscModule\";\n\nexport interface DeferEntry {\n state: DeferEntryState;\n name?: string;\n}\n\n/**\n * Options for the defer function.\n */\nexport interface DeferOptions {\n /**\n * Optional name for debugging purposes.\n * In development: included in the RSC payload file name.\n * In production: logged when the payload file is emitted.\n */\n name?: string;\n}\n\nexport interface LoadedDeferEntry extends DeferEntry {\n state: Exclude<DeferEntryState, { state: \"pending\" }>;\n}\n\ntype DeferEntryState =\n | {\n state: \"pending\";\n element: ReactElement;\n }\n | {\n state: \"streaming\";\n stream: ReadableStream<Uint8Array>;\n }\n | {\n state: \"ready\";\n data: string;\n }\n | {\n state: \"error\";\n error: unknown;\n };\n\n/**\n * Sanitizes a name for use in file paths.\n * Replaces non-alphanumeric characters with underscores and limits length.\n */\nfunction sanitizeName(name: string): string {\n return name\n .replace(/[^a-zA-Z0-9_-]/g, \"_\")\n .replace(/_+/g, \"_\")\n .replace(/^_|_$/g, \"\")\n .slice(0, 50);\n}\n\nexport class DeferRegistry {\n #registry = new Map<string, DeferEntry>();\n\n register(element: ReactElement, id: string, name?: string) {\n this.#registry.set(id, { state: { element, state: \"pending\" }, name });\n }\n\n load(id: string): LoadedDeferEntry | undefined {\n const entry = this.#registry.get(id);\n if (!entry) {\n return undefined;\n }\n return this.#loadEntry(entry);\n }\n\n #loadEntry(entry: DeferEntry): LoadedDeferEntry {\n const { state } = entry;\n switch (state.state) {\n case \"pending\": {\n const stream = renderToReadableStream<ReactNode>(state.element);\n const [stream1, stream2] = stream.tee();\n entry.state = { state: \"streaming\", stream: stream1 };\n (async () => {\n const chunks: string[] = [];\n const decoder = new TextDecoder();\n for await (const chunk of stream2) {\n chunks.push(decoder.decode(chunk, { stream: true }));\n }\n chunks.push(decoder.decode());\n entry.state = {\n state: \"ready\",\n data: chunks.join(\"\"),\n };\n })().catch((error) => {\n entry.state = { state: \"error\", error };\n });\n return entry as LoadedDeferEntry;\n }\n case \"streaming\":\n case \"ready\":\n case \"error\": {\n return entry as LoadedDeferEntry;\n }\n }\n state satisfies never;\n }\n\n has(id: string): boolean {\n return this.#registry.has(id);\n }\n\n /**\n * Iterates over all entries in parallel.\n * Yields results as each stream completes.\n */\n async *loadAll() {\n const errors: unknown[] = [];\n\n // Phase 1: Start all entries loading\n const loadedEntries = Array.from(\n this.#registry,\n ([id, entry]) => [id, this.#loadEntry(entry), entry.name] as const,\n );\n\n if (loadedEntries.length === 0) return;\n\n type Result = { id: string; data: string; name?: string };\n\n // Completion queue\n const completed: Array<Result | { error: unknown }> = [];\n let waiting: (() => void) | undefined;\n let remainingCount = loadedEntries.length;\n\n const onComplete = (result: Result | { error: unknown }) => {\n completed.push(result);\n remainingCount--;\n waiting?.();\n };\n\n // Phase 2: Start all operations (each pushes to queue when done)\n for (const [id, loadedEntry, name] of loadedEntries) {\n (async () => {\n try {\n switch (loadedEntry.state.state) {\n case \"streaming\":\n onComplete({\n id,\n data: await drainStream(loadedEntry.state.stream),\n name,\n });\n break;\n case \"ready\":\n onComplete({ id, data: loadedEntry.state.data, name });\n break;\n case \"error\":\n onComplete({ error: loadedEntry.state.error });\n break;\n }\n } catch (error) {\n onComplete({ error });\n }\n })();\n }\n\n // Phase 3: Yield from queue as results arrive\n while (remainingCount > 0 || completed.length > 0) {\n if (completed.length === 0) {\n await new Promise<void>((r) => {\n waiting = r;\n });\n waiting = undefined;\n }\n for (const result of completed.splice(0)) {\n if (\"error\" in result) {\n errors.push(result.error);\n } else {\n yield result;\n }\n }\n }\n\n if (errors.length > 0) {\n throw new AggregateError(errors);\n }\n }\n}\n\nexport const deferRegistry = new DeferRegistry();\n\n/**\n * Renders given Server Component into a separate RSC payload.\n *\n * During the client side rendering, fetching of the payload will be\n * deferred until the returned ReactNode is actually rendered.\n *\n * @param element - The React element to defer.\n * @param options - Optional configuration for the deferred payload.\n * @returns A ReactNode that virtually contains the result of rendering the given component.\n */\nexport function defer(\n element: ReactElement,\n options?: DeferOptions,\n): ReactNode {\n const name = options?.name;\n const sanitizedName = name ? sanitizeName(name) : undefined;\n const rawId = sanitizedName\n ? `${sanitizedName}-${crypto.randomUUID()}`\n : crypto.randomUUID();\n const id = getPayloadIDFor(rawId);\n deferRegistry.register(element, id, name);\n\n return <DeferredComponent moduleID={id} />;\n}\n"],"mappings":"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"}
|
package/dist/rsc/entry.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"entry.d.mts","names":[],"sources":["../../src/rsc/entry.tsx"],"
|
|
1
|
+
{"version":3,"file":"entry.d.mts","names":[],"sources":["../../src/rsc/entry.tsx"],"mappings":";;;KAQY,UAAA;EACV,IAAA,EAAM,KAAA,CAAM,SAAA;AAAA;;;;iBA2BQ,SAAA,CAAA,GAAa,OAAA,CAAQ,QAAA;AAAA,cA+ErC,aAAA,SAAsB,KAAA;EAC1B,MAAA;cACY,OAAA,UAAiB,MAAA;AAAA;AAAA,iBAOf,eAAA,CAAgB,KAAA,YAAiB,KAAA,IAAS,aAAA;;;;iBAOpC,QAAA,CAAS,OAAA,EAAS,OAAA,GAAU,OAAA,CAAQ,QAAA;AAlBzD;;;AAAA,iBA2FqB,KAAA,CAAA,GAAK,OAAA"}
|
package/dist/rsc/entry.mjs
CHANGED
|
@@ -4,30 +4,71 @@ import { stripBasePath } from "../util/basePath.mjs";
|
|
|
4
4
|
import { defer, deferRegistry } from "./defer.mjs";
|
|
5
5
|
import { generateAppMarker } from "./marker.mjs";
|
|
6
6
|
import { jsx } from "react/jsx-runtime";
|
|
7
|
+
import { ssr } from "virtual:funstack/config";
|
|
7
8
|
import { renderToReadableStream } from "@vitejs/plugin-rsc/rsc";
|
|
8
9
|
|
|
9
10
|
//#region src/rsc/entry.tsx
|
|
10
|
-
async function
|
|
11
|
+
async function loadEntries() {
|
|
11
12
|
const Root = (await import("virtual:funstack/root")).default;
|
|
12
13
|
const App = (await import("virtual:funstack/app")).default;
|
|
13
14
|
if (Root === void 0) throw new Error("Failed to load RSC root entry module. Check your entry file to ensure it has a default export.");
|
|
14
15
|
if (App === void 0) throw new Error("Failed to load RSC app entry module. Check your entry file to ensure it has a default export.");
|
|
15
|
-
return
|
|
16
|
+
return {
|
|
17
|
+
Root,
|
|
18
|
+
App
|
|
19
|
+
};
|
|
16
20
|
}
|
|
17
21
|
/**
|
|
18
22
|
* Entrypoint to serve HTML response in dev environment
|
|
19
23
|
*/
|
|
20
24
|
async function serveHTML() {
|
|
25
|
+
const timings = [];
|
|
21
26
|
const marker = generateAppMarker();
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
const entriesStart = performance.now();
|
|
28
|
+
const { Root, App } = await loadEntries();
|
|
29
|
+
timings.push(`entries;dur=${performance.now() - entriesStart}`);
|
|
30
|
+
const ssrModuleStart = performance.now();
|
|
31
|
+
const ssrEntryModule = await import.meta.viteRsc.loadModule("ssr");
|
|
32
|
+
timings.push(`ssr-module;dur=${performance.now() - ssrModuleStart}`);
|
|
33
|
+
if (ssr) {
|
|
34
|
+
const rscStart = performance.now();
|
|
35
|
+
const rootRscStream = renderToReadableStream({ root: /* @__PURE__ */ jsx(Root, { children: /* @__PURE__ */ jsx(App, {}) }) });
|
|
36
|
+
timings.push(`rsc;dur=${performance.now() - rscStart}`);
|
|
37
|
+
const ssrStart = performance.now();
|
|
38
|
+
const ssrResult = await ssrEntryModule.renderHTML(rootRscStream, {
|
|
39
|
+
appEntryMarker: marker,
|
|
40
|
+
build: false,
|
|
41
|
+
ssr: true
|
|
42
|
+
});
|
|
43
|
+
timings.push(`ssr;dur=${performance.now() - ssrStart}`);
|
|
44
|
+
return new Response(ssrResult.stream, {
|
|
45
|
+
status: ssrResult.status,
|
|
46
|
+
headers: {
|
|
47
|
+
"Content-type": "text/html",
|
|
48
|
+
"Server-Timing": timings.join(", ")
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
} else {
|
|
52
|
+
const rscStart = performance.now();
|
|
53
|
+
const shellRscStream = renderToReadableStream({ root: /* @__PURE__ */ jsx(Root, { children: /* @__PURE__ */ jsx("span", { id: marker }) }) });
|
|
54
|
+
const clientRscStream = renderToReadableStream({ root: /* @__PURE__ */ jsx(Root, { children: /* @__PURE__ */ jsx(App, {}) }) });
|
|
55
|
+
timings.push(`rsc;dur=${performance.now() - rscStart}`);
|
|
56
|
+
const ssrStart = performance.now();
|
|
57
|
+
const ssrResult = await ssrEntryModule.renderHTML(shellRscStream, {
|
|
58
|
+
appEntryMarker: marker,
|
|
59
|
+
build: false,
|
|
60
|
+
ssr: false,
|
|
61
|
+
clientRscStream
|
|
62
|
+
});
|
|
63
|
+
timings.push(`ssr;dur=${performance.now() - ssrStart}`);
|
|
64
|
+
return new Response(ssrResult.stream, {
|
|
65
|
+
status: ssrResult.status,
|
|
66
|
+
headers: {
|
|
67
|
+
"Content-type": "text/html",
|
|
68
|
+
"Server-Timing": timings.join(", ")
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
31
72
|
}
|
|
32
73
|
var ServeRSCError = class extends Error {
|
|
33
74
|
status;
|
|
@@ -44,27 +85,44 @@ function isServeRSCError(error) {
|
|
|
44
85
|
* Servers an RSC stream response
|
|
45
86
|
*/
|
|
46
87
|
async function serveRSC(request) {
|
|
88
|
+
const timings = [];
|
|
47
89
|
const pathname = stripBasePath(new URL(request.url).pathname);
|
|
48
90
|
if (pathname === devMainRscPath) {
|
|
49
|
-
const
|
|
91
|
+
const entriesStart = performance.now();
|
|
92
|
+
const { Root, App } = await loadEntries();
|
|
93
|
+
timings.push(`entries;dur=${performance.now() - entriesStart}`);
|
|
94
|
+
const rscStart = performance.now();
|
|
95
|
+
const rootRscStream = renderToReadableStream({ root: /* @__PURE__ */ jsx(Root, { children: /* @__PURE__ */ jsx(App, {}) }) });
|
|
96
|
+
timings.push(`rsc;dur=${performance.now() - rscStart}`);
|
|
50
97
|
return new Response(rootRscStream, {
|
|
51
98
|
status: 200,
|
|
52
|
-
headers: {
|
|
99
|
+
headers: {
|
|
100
|
+
"content-type": "text/x-component;charset=utf-8",
|
|
101
|
+
"Server-Timing": timings.join(", ")
|
|
102
|
+
}
|
|
53
103
|
});
|
|
54
104
|
}
|
|
55
105
|
const moduleId = extractIDFromModulePath(pathname);
|
|
56
106
|
if (!moduleId) throw new ServeRSCError(`Invalid RSC module path: ${pathname}`, 404);
|
|
107
|
+
const deferLoadStart = performance.now();
|
|
57
108
|
const entry = deferRegistry.load(moduleId);
|
|
58
109
|
if (!entry) throw new ServeRSCError(`RSC component not found: ${moduleId}`, 404);
|
|
110
|
+
timings.push(`defer-load;dur=${performance.now() - deferLoadStart}`);
|
|
59
111
|
const { state } = entry;
|
|
60
112
|
switch (state.state) {
|
|
61
113
|
case "streaming": return new Response(state.stream, {
|
|
62
114
|
status: 200,
|
|
63
|
-
headers: {
|
|
115
|
+
headers: {
|
|
116
|
+
"content-type": "text/x-component;charset=utf-8",
|
|
117
|
+
"Server-Timing": timings.join(", ")
|
|
118
|
+
}
|
|
64
119
|
});
|
|
65
120
|
case "ready": return new Response(state.data, {
|
|
66
121
|
status: 200,
|
|
67
|
-
headers: {
|
|
122
|
+
headers: {
|
|
123
|
+
"content-type": "text/x-component;charset=utf-8",
|
|
124
|
+
"Server-Timing": timings.join(", ")
|
|
125
|
+
}
|
|
68
126
|
});
|
|
69
127
|
case "error": throw new ServeRSCError(`Failed to load RSC component: ${state.error}`, 500);
|
|
70
128
|
}
|
|
@@ -74,14 +132,21 @@ async function serveRSC(request) {
|
|
|
74
132
|
*/
|
|
75
133
|
async function build() {
|
|
76
134
|
const marker = generateAppMarker();
|
|
77
|
-
const Root =
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
135
|
+
const { Root, App } = await loadEntries();
|
|
136
|
+
let rootRscStream;
|
|
137
|
+
let appRscStream;
|
|
138
|
+
if (ssr) {
|
|
139
|
+
rootRscStream = renderToReadableStream({ root: /* @__PURE__ */ jsx(Root, { children: /* @__PURE__ */ jsx(App, {}) }) });
|
|
140
|
+
appRscStream = renderToReadableStream({ root: /* @__PURE__ */ jsx(Root, { children: /* @__PURE__ */ jsx(App, {}) }) });
|
|
141
|
+
} else {
|
|
142
|
+
rootRscStream = renderToReadableStream({ root: /* @__PURE__ */ jsx(Root, { children: /* @__PURE__ */ jsx("span", { id: marker }) }) });
|
|
143
|
+
appRscStream = renderToReadableStream({ root: /* @__PURE__ */ jsx(App, {}) });
|
|
144
|
+
}
|
|
81
145
|
return {
|
|
82
146
|
html: (await (await import.meta.viteRsc.loadModule("ssr")).renderHTML(rootRscStream, {
|
|
83
147
|
appEntryMarker: marker,
|
|
84
|
-
build: true
|
|
148
|
+
build: true,
|
|
149
|
+
ssr
|
|
85
150
|
})).stream,
|
|
86
151
|
appRsc: appRscStream,
|
|
87
152
|
deferRegistry
|
package/dist/rsc/entry.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"entry.mjs","names":[],"sources":["../../src/rsc/entry.tsx"],"sourcesContent":["import \"./defer\";\nimport { renderToReadableStream } from \"@vitejs/plugin-rsc/rsc\";\nimport { devMainRscPath } from \"./request\";\nimport { generateAppMarker } from \"./marker\";\nimport { deferRegistry } from \"./defer\";\nimport { extractIDFromModulePath } from \"./rscModule\";\nimport { stripBasePath } from \"../util/basePath\";\n\nexport type RscPayload = {\n root: React.ReactNode;\n};\n\nasync function devMainRSCStream() {\n const Root = (await import(\"virtual:funstack/root\")).default;\n const App = (await import(\"virtual:funstack/app\")).default;\n\n // Sanity check; this may happen when user-provided entry file\n // does not have a default export.\n if (Root === undefined) {\n throw new Error(\n \"Failed to load RSC root entry module. Check your entry file to ensure it has a default export.\",\n );\n }\n if (App === undefined) {\n throw new Error(\n \"Failed to load RSC app entry module. Check your entry file to ensure it has a default export.\",\n );\n }\n\n const rootRscStream = renderToReadableStream<RscPayload>({\n root: (\n <Root>\n <App />\n </Root>\n ),\n });\n return rootRscStream;\n}\n\n/**\n * Entrypoint to serve HTML response in dev environment\n */\nexport async function serveHTML(): Promise<Response> {\n const marker = generateAppMarker();\n\n const rootRscStream = await devMainRSCStream();\n\n const ssrEntryModule = await import.meta.viteRsc.loadModule<\n typeof import(\"../ssr/entry\")\n >(\"ssr\");\n const ssrResult = await ssrEntryModule.renderHTML(rootRscStream, {\n appEntryMarker: marker,\n build: false,\n });\n\n // respond html\n return new Response(ssrResult.stream, {\n status: ssrResult.status,\n headers: {\n \"Content-type\": \"text/html\",\n },\n });\n}\n\nclass ServeRSCError extends Error {\n status: 404 | 500;\n constructor(message: string, status: 404 | 500) {\n super(message);\n this.name = \"ServeRSCError\";\n this.status = status;\n }\n}\n\nexport function isServeRSCError(error: unknown): error is ServeRSCError {\n return error instanceof Error && error.name === \"ServeRSCError\";\n}\n\n/**\n * Servers an RSC stream response\n */\nexport async function serveRSC(request: Request): Promise<Response> {\n const url = new URL(request.url);\n const pathname = stripBasePath(url.pathname);\n if (pathname === devMainRscPath) {\n // root RSC stream is requested\n const rootRscStream = await devMainRSCStream();\n return new Response(rootRscStream, {\n status: 200,\n headers: {\n \"content-type\": \"text/x-component;charset=utf-8\",\n },\n });\n }\n\n const moduleId = extractIDFromModulePath(pathname);\n if (!moduleId) {\n throw new ServeRSCError(`Invalid RSC module path: ${pathname}`, 404);\n }\n\n const entry = deferRegistry.load(moduleId);\n if (!entry) {\n throw new ServeRSCError(`RSC component not found: ${moduleId}`, 404);\n }\n const { state } = entry;\n switch (state.state) {\n case \"streaming\": {\n return new Response(state.stream, {\n status: 200,\n headers: {\n \"content-type\": \"text/x-component;charset=utf-8\",\n },\n });\n }\n case \"ready\": {\n return new Response(state.data, {\n status: 200,\n headers: {\n \"content-type\": \"text/x-component;charset=utf-8\",\n },\n });\n }\n case \"error\": {\n throw new ServeRSCError(\n `Failed to load RSC component: ${state.error}`,\n 500,\n );\n }\n }\n}\n\n/**\n * Build handler\n */\nexport async function build() {\n const marker = generateAppMarker();\n\n const Root = (await import(\"virtual:funstack/root\")).default;\n const App = (await import(\"virtual:funstack/app\")).default;\n\n const rootRscStream = renderToReadableStream<RscPayload>({\n root: (\n <Root>\n <span id={marker} />\n </Root>\n ),\n });\n\n const appRscStream = renderToReadableStream<RscPayload>({\n root: <App />,\n });\n\n const ssrEntryModule = await import.meta.viteRsc.loadModule<\n typeof import(\"../ssr/entry\")\n >(\"ssr\");\n\n const ssrResult = await ssrEntryModule.renderHTML(rootRscStream, {\n appEntryMarker: marker,\n build: true,\n });\n\n return {\n html: ssrResult.stream,\n appRsc: appRscStream,\n deferRegistry,\n };\n}\n\nexport { defer } from \"./defer\";\n\nif (import.meta.hot) {\n import.meta.hot.accept();\n}\n"],"mappings":";;;;;;;;;AAYA,eAAe,mBAAmB;CAChC,MAAM,QAAQ,MAAM,OAAO,0BAA0B;CACrD,MAAM,OAAO,MAAM,OAAO,yBAAyB;AAInD,KAAI,SAAS,OACX,OAAM,IAAI,MACR,iGACD;AAEH,KAAI,QAAQ,OACV,OAAM,IAAI,MACR,gGACD;AAUH,QAPsB,uBAAmC,EACvD,MACE,oBAAC,kBACC,oBAAC,QAAM,GACF,EAEV,CAAC;;;;;AAOJ,eAAsB,YAA+B;CACnD,MAAM,SAAS,mBAAmB;CAElC,MAAM,gBAAgB,MAAM,kBAAkB;CAK9C,MAAM,YAAY,OAHK,MAAM,OAAO,KAAK,QAAQ,WAE/C,MAAM,EAC+B,WAAW,eAAe;EAC/D,gBAAgB;EAChB,OAAO;EACR,CAAC;AAGF,QAAO,IAAI,SAAS,UAAU,QAAQ;EACpC,QAAQ,UAAU;EAClB,SAAS,EACP,gBAAgB,aACjB;EACF,CAAC;;AAGJ,IAAM,gBAAN,cAA4B,MAAM;CAChC;CACA,YAAY,SAAiB,QAAmB;AAC9C,QAAM,QAAQ;AACd,OAAK,OAAO;AACZ,OAAK,SAAS;;;AAIlB,SAAgB,gBAAgB,OAAwC;AACtE,QAAO,iBAAiB,SAAS,MAAM,SAAS;;;;;AAMlD,eAAsB,SAAS,SAAqC;CAElE,MAAM,WAAW,cADL,IAAI,IAAI,QAAQ,IAAI,CACG,SAAS;AAC5C,KAAI,aAAa,gBAAgB;EAE/B,MAAM,gBAAgB,MAAM,kBAAkB;AAC9C,SAAO,IAAI,SAAS,eAAe;GACjC,QAAQ;GACR,SAAS,EACP,gBAAgB,kCACjB;GACF,CAAC;;CAGJ,MAAM,WAAW,wBAAwB,SAAS;AAClD,KAAI,CAAC,SACH,OAAM,IAAI,cAAc,4BAA4B,YAAY,IAAI;CAGtE,MAAM,QAAQ,cAAc,KAAK,SAAS;AAC1C,KAAI,CAAC,MACH,OAAM,IAAI,cAAc,4BAA4B,YAAY,IAAI;CAEtE,MAAM,EAAE,UAAU;AAClB,SAAQ,MAAM,OAAd;EACE,KAAK,YACH,QAAO,IAAI,SAAS,MAAM,QAAQ;GAChC,QAAQ;GACR,SAAS,EACP,gBAAgB,kCACjB;GACF,CAAC;EAEJ,KAAK,QACH,QAAO,IAAI,SAAS,MAAM,MAAM;GAC9B,QAAQ;GACR,SAAS,EACP,gBAAgB,kCACjB;GACF,CAAC;EAEJ,KAAK,QACH,OAAM,IAAI,cACR,iCAAiC,MAAM,SACvC,IACD;;;;;;AAQP,eAAsB,QAAQ;CAC5B,MAAM,SAAS,mBAAmB;CAElC,MAAM,QAAQ,MAAM,OAAO,0BAA0B;CACrD,MAAM,OAAO,MAAM,OAAO,yBAAyB;CAEnD,MAAM,gBAAgB,uBAAmC,EACvD,MACE,oBAAC,kBACC,oBAAC,UAAK,IAAI,SAAU,GACf,EAEV,CAAC;CAEF,MAAM,eAAe,uBAAmC,EACtD,MAAM,oBAAC,QAAM,EACd,CAAC;AAWF,QAAO;EACL,OANgB,OAJK,MAAM,OAAO,KAAK,QAAQ,WAE/C,MAAM,EAE+B,WAAW,eAAe;GAC/D,gBAAgB;GAChB,OAAO;GACR,CAAC,EAGgB;EAChB,QAAQ;EACR;EACD;;AAKH,IAAI,OAAO,KAAK,IACd,QAAO,KAAK,IAAI,QAAQ"}
|
|
1
|
+
{"version":3,"file":"entry.mjs","names":["ssrEnabled"],"sources":["../../src/rsc/entry.tsx"],"sourcesContent":["import \"./defer\";\nimport { renderToReadableStream } from \"@vitejs/plugin-rsc/rsc\";\nimport { devMainRscPath } from \"./request\";\nimport { generateAppMarker } from \"./marker\";\nimport { deferRegistry } from \"./defer\";\nimport { extractIDFromModulePath } from \"./rscModule\";\nimport { stripBasePath } from \"../util/basePath\";\n\nexport type RscPayload = {\n root: React.ReactNode;\n};\n\nimport { ssr as ssrEnabled } from \"virtual:funstack/config\";\n\nasync function loadEntries() {\n const Root = (await import(\"virtual:funstack/root\")).default;\n const App = (await import(\"virtual:funstack/app\")).default;\n\n // Sanity check; this may happen when user-provided entry file\n // does not have a default export.\n if (Root === undefined) {\n throw new Error(\n \"Failed to load RSC root entry module. Check your entry file to ensure it has a default export.\",\n );\n }\n if (App === undefined) {\n throw new Error(\n \"Failed to load RSC app entry module. Check your entry file to ensure it has a default export.\",\n );\n }\n return { Root, App };\n}\n\n/**\n * Entrypoint to serve HTML response in dev environment\n */\nexport async function serveHTML(): Promise<Response> {\n const timings: string[] = [];\n const marker = generateAppMarker();\n\n const entriesStart = performance.now();\n const { Root, App } = await loadEntries();\n timings.push(`entries;dur=${performance.now() - entriesStart}`);\n\n const ssrModuleStart = performance.now();\n const ssrEntryModule = await import.meta.viteRsc.loadModule<\n typeof import(\"../ssr/entry\")\n >(\"ssr\");\n timings.push(`ssr-module;dur=${performance.now() - ssrModuleStart}`);\n\n if (ssrEnabled) {\n // SSR on: single RSC stream with full tree\n const rscStart = performance.now();\n const rootRscStream = renderToReadableStream<RscPayload>({\n root: (\n <Root>\n <App />\n </Root>\n ),\n });\n timings.push(`rsc;dur=${performance.now() - rscStart}`);\n\n const ssrStart = performance.now();\n const ssrResult = await ssrEntryModule.renderHTML(rootRscStream, {\n appEntryMarker: marker,\n build: false,\n ssr: true,\n });\n timings.push(`ssr;dur=${performance.now() - ssrStart}`);\n\n return new Response(ssrResult.stream, {\n status: ssrResult.status,\n headers: {\n \"Content-type\": \"text/html\",\n \"Server-Timing\": timings.join(\", \"),\n },\n });\n } else {\n // SSR off: shell RSC for SSR, full RSC for client\n const rscStart = performance.now();\n const shellRscStream = renderToReadableStream<RscPayload>({\n root: (\n <Root>\n <span id={marker} />\n </Root>\n ),\n });\n const clientRscStream = renderToReadableStream<RscPayload>({\n root: (\n <Root>\n <App />\n </Root>\n ),\n });\n timings.push(`rsc;dur=${performance.now() - rscStart}`);\n\n const ssrStart = performance.now();\n const ssrResult = await ssrEntryModule.renderHTML(shellRscStream, {\n appEntryMarker: marker,\n build: false,\n ssr: false,\n clientRscStream,\n });\n timings.push(`ssr;dur=${performance.now() - ssrStart}`);\n\n return new Response(ssrResult.stream, {\n status: ssrResult.status,\n headers: {\n \"Content-type\": \"text/html\",\n \"Server-Timing\": timings.join(\", \"),\n },\n });\n }\n}\n\nclass ServeRSCError extends Error {\n status: 404 | 500;\n constructor(message: string, status: 404 | 500) {\n super(message);\n this.name = \"ServeRSCError\";\n this.status = status;\n }\n}\n\nexport function isServeRSCError(error: unknown): error is ServeRSCError {\n return error instanceof Error && error.name === \"ServeRSCError\";\n}\n\n/**\n * Servers an RSC stream response\n */\nexport async function serveRSC(request: Request): Promise<Response> {\n const timings: string[] = [];\n const url = new URL(request.url);\n const pathname = stripBasePath(url.pathname);\n if (pathname === devMainRscPath) {\n // root RSC stream is requested (HMR re-fetch always sends full tree)\n const entriesStart = performance.now();\n const { Root, App } = await loadEntries();\n timings.push(`entries;dur=${performance.now() - entriesStart}`);\n\n const rscStart = performance.now();\n const rootRscStream = renderToReadableStream<RscPayload>({\n root: (\n <Root>\n <App />\n </Root>\n ),\n });\n timings.push(`rsc;dur=${performance.now() - rscStart}`);\n\n return new Response(rootRscStream, {\n status: 200,\n headers: {\n \"content-type\": \"text/x-component;charset=utf-8\",\n \"Server-Timing\": timings.join(\", \"),\n },\n });\n }\n\n const moduleId = extractIDFromModulePath(pathname);\n if (!moduleId) {\n throw new ServeRSCError(`Invalid RSC module path: ${pathname}`, 404);\n }\n\n const deferLoadStart = performance.now();\n const entry = deferRegistry.load(moduleId);\n if (!entry) {\n throw new ServeRSCError(`RSC component not found: ${moduleId}`, 404);\n }\n timings.push(`defer-load;dur=${performance.now() - deferLoadStart}`);\n\n const { state } = entry;\n switch (state.state) {\n case \"streaming\": {\n return new Response(state.stream, {\n status: 200,\n headers: {\n \"content-type\": \"text/x-component;charset=utf-8\",\n \"Server-Timing\": timings.join(\", \"),\n },\n });\n }\n case \"ready\": {\n return new Response(state.data, {\n status: 200,\n headers: {\n \"content-type\": \"text/x-component;charset=utf-8\",\n \"Server-Timing\": timings.join(\", \"),\n },\n });\n }\n case \"error\": {\n throw new ServeRSCError(\n `Failed to load RSC component: ${state.error}`,\n 500,\n );\n }\n }\n}\n\n/**\n * Build handler\n */\nexport async function build() {\n const marker = generateAppMarker();\n const { Root, App } = await loadEntries();\n\n let rootRscStream: ReadableStream<Uint8Array>;\n let appRscStream: ReadableStream<Uint8Array>;\n\n if (ssrEnabled) {\n // SSR on: both streams have full tree\n rootRscStream = renderToReadableStream<RscPayload>({\n root: (\n <Root>\n <App />\n </Root>\n ),\n });\n appRscStream = renderToReadableStream<RscPayload>({\n root: (\n <Root>\n <App />\n </Root>\n ),\n });\n } else {\n // SSR off: root stream has shell, app stream has App only\n rootRscStream = renderToReadableStream<RscPayload>({\n root: (\n <Root>\n <span id={marker} />\n </Root>\n ),\n });\n appRscStream = renderToReadableStream<RscPayload>({\n root: <App />,\n });\n }\n\n const ssrEntryModule = await import.meta.viteRsc.loadModule<\n typeof import(\"../ssr/entry\")\n >(\"ssr\");\n\n const ssrResult = await ssrEntryModule.renderHTML(rootRscStream, {\n appEntryMarker: marker,\n build: true,\n ssr: ssrEnabled,\n });\n\n return {\n html: ssrResult.stream,\n appRsc: appRscStream,\n deferRegistry,\n };\n}\n\nexport { defer } from \"./defer\";\n\nif (import.meta.hot) {\n import.meta.hot.accept();\n}\n"],"mappings":";;;;;;;;;;AAcA,eAAe,cAAc;CAC3B,MAAM,QAAQ,MAAM,OAAO,0BAA0B;CACrD,MAAM,OAAO,MAAM,OAAO,yBAAyB;AAInD,KAAI,SAAS,OACX,OAAM,IAAI,MACR,iGACD;AAEH,KAAI,QAAQ,OACV,OAAM,IAAI,MACR,gGACD;AAEH,QAAO;EAAE;EAAM;EAAK;;;;;AAMtB,eAAsB,YAA+B;CACnD,MAAM,UAAoB,EAAE;CAC5B,MAAM,SAAS,mBAAmB;CAElC,MAAM,eAAe,YAAY,KAAK;CACtC,MAAM,EAAE,MAAM,QAAQ,MAAM,aAAa;AACzC,SAAQ,KAAK,eAAe,YAAY,KAAK,GAAG,eAAe;CAE/D,MAAM,iBAAiB,YAAY,KAAK;CACxC,MAAM,iBAAiB,MAAM,OAAO,KAAK,QAAQ,WAE/C,MAAM;AACR,SAAQ,KAAK,kBAAkB,YAAY,KAAK,GAAG,iBAAiB;AAEpE,KAAIA,KAAY;EAEd,MAAM,WAAW,YAAY,KAAK;EAClC,MAAM,gBAAgB,uBAAmC,EACvD,MACE,oBAAC,kBACC,oBAAC,QAAM,GACF,EAEV,CAAC;AACF,UAAQ,KAAK,WAAW,YAAY,KAAK,GAAG,WAAW;EAEvD,MAAM,WAAW,YAAY,KAAK;EAClC,MAAM,YAAY,MAAM,eAAe,WAAW,eAAe;GAC/D,gBAAgB;GAChB,OAAO;GACP,KAAK;GACN,CAAC;AACF,UAAQ,KAAK,WAAW,YAAY,KAAK,GAAG,WAAW;AAEvD,SAAO,IAAI,SAAS,UAAU,QAAQ;GACpC,QAAQ,UAAU;GAClB,SAAS;IACP,gBAAgB;IAChB,iBAAiB,QAAQ,KAAK,KAAK;IACpC;GACF,CAAC;QACG;EAEL,MAAM,WAAW,YAAY,KAAK;EAClC,MAAM,iBAAiB,uBAAmC,EACxD,MACE,oBAAC,kBACC,oBAAC,UAAK,IAAI,SAAU,GACf,EAEV,CAAC;EACF,MAAM,kBAAkB,uBAAmC,EACzD,MACE,oBAAC,kBACC,oBAAC,QAAM,GACF,EAEV,CAAC;AACF,UAAQ,KAAK,WAAW,YAAY,KAAK,GAAG,WAAW;EAEvD,MAAM,WAAW,YAAY,KAAK;EAClC,MAAM,YAAY,MAAM,eAAe,WAAW,gBAAgB;GAChE,gBAAgB;GAChB,OAAO;GACP,KAAK;GACL;GACD,CAAC;AACF,UAAQ,KAAK,WAAW,YAAY,KAAK,GAAG,WAAW;AAEvD,SAAO,IAAI,SAAS,UAAU,QAAQ;GACpC,QAAQ,UAAU;GAClB,SAAS;IACP,gBAAgB;IAChB,iBAAiB,QAAQ,KAAK,KAAK;IACpC;GACF,CAAC;;;AAIN,IAAM,gBAAN,cAA4B,MAAM;CAChC;CACA,YAAY,SAAiB,QAAmB;AAC9C,QAAM,QAAQ;AACd,OAAK,OAAO;AACZ,OAAK,SAAS;;;AAIlB,SAAgB,gBAAgB,OAAwC;AACtE,QAAO,iBAAiB,SAAS,MAAM,SAAS;;;;;AAMlD,eAAsB,SAAS,SAAqC;CAClE,MAAM,UAAoB,EAAE;CAE5B,MAAM,WAAW,cADL,IAAI,IAAI,QAAQ,IAAI,CACG,SAAS;AAC5C,KAAI,aAAa,gBAAgB;EAE/B,MAAM,eAAe,YAAY,KAAK;EACtC,MAAM,EAAE,MAAM,QAAQ,MAAM,aAAa;AACzC,UAAQ,KAAK,eAAe,YAAY,KAAK,GAAG,eAAe;EAE/D,MAAM,WAAW,YAAY,KAAK;EAClC,MAAM,gBAAgB,uBAAmC,EACvD,MACE,oBAAC,kBACC,oBAAC,QAAM,GACF,EAEV,CAAC;AACF,UAAQ,KAAK,WAAW,YAAY,KAAK,GAAG,WAAW;AAEvD,SAAO,IAAI,SAAS,eAAe;GACjC,QAAQ;GACR,SAAS;IACP,gBAAgB;IAChB,iBAAiB,QAAQ,KAAK,KAAK;IACpC;GACF,CAAC;;CAGJ,MAAM,WAAW,wBAAwB,SAAS;AAClD,KAAI,CAAC,SACH,OAAM,IAAI,cAAc,4BAA4B,YAAY,IAAI;CAGtE,MAAM,iBAAiB,YAAY,KAAK;CACxC,MAAM,QAAQ,cAAc,KAAK,SAAS;AAC1C,KAAI,CAAC,MACH,OAAM,IAAI,cAAc,4BAA4B,YAAY,IAAI;AAEtE,SAAQ,KAAK,kBAAkB,YAAY,KAAK,GAAG,iBAAiB;CAEpE,MAAM,EAAE,UAAU;AAClB,SAAQ,MAAM,OAAd;EACE,KAAK,YACH,QAAO,IAAI,SAAS,MAAM,QAAQ;GAChC,QAAQ;GACR,SAAS;IACP,gBAAgB;IAChB,iBAAiB,QAAQ,KAAK,KAAK;IACpC;GACF,CAAC;EAEJ,KAAK,QACH,QAAO,IAAI,SAAS,MAAM,MAAM;GAC9B,QAAQ;GACR,SAAS;IACP,gBAAgB;IAChB,iBAAiB,QAAQ,KAAK,KAAK;IACpC;GACF,CAAC;EAEJ,KAAK,QACH,OAAM,IAAI,cACR,iCAAiC,MAAM,SACvC,IACD;;;;;;AAQP,eAAsB,QAAQ;CAC5B,MAAM,SAAS,mBAAmB;CAClC,MAAM,EAAE,MAAM,QAAQ,MAAM,aAAa;CAEzC,IAAI;CACJ,IAAI;AAEJ,KAAIA,KAAY;AAEd,kBAAgB,uBAAmC,EACjD,MACE,oBAAC,kBACC,oBAAC,QAAM,GACF,EAEV,CAAC;AACF,iBAAe,uBAAmC,EAChD,MACE,oBAAC,kBACC,oBAAC,QAAM,GACF,EAEV,CAAC;QACG;AAEL,kBAAgB,uBAAmC,EACjD,MACE,oBAAC,kBACC,oBAAC,UAAK,IAAI,SAAU,GACf,EAEV,CAAC;AACF,iBAAe,uBAAmC,EAChD,MAAM,oBAAC,QAAM,EACd,CAAC;;AAaJ,QAAO;EACL,OAPgB,OAJK,MAAM,OAAO,KAAK,QAAQ,WAE/C,MAAM,EAE+B,WAAW,eAAe;GAC/D,gBAAgB;GAChB,OAAO;GACFA;GACN,CAAC,EAGgB;EAChB,QAAQ;EACR;EACD;;AAKH,IAAI,OAAO,KAAK,IACd,QAAO,KAAK,IAAI,QAAQ"}
|
|
@@ -3,10 +3,10 @@ import React from "react";
|
|
|
3
3
|
|
|
4
4
|
//#region src/rsc-client/clientWrapper.d.ts
|
|
5
5
|
declare const RegistryContext: React.Context<DeferRegistry | undefined>;
|
|
6
|
-
interface
|
|
6
|
+
interface DeferredComponentProps {
|
|
7
7
|
moduleID: string;
|
|
8
8
|
}
|
|
9
|
-
declare const
|
|
9
|
+
declare const DeferredComponent: React.FC<DeferredComponentProps>;
|
|
10
10
|
//#endregion
|
|
11
|
-
export {
|
|
11
|
+
export { DeferredComponent, RegistryContext };
|
|
12
12
|
//# sourceMappingURL=clientWrapper.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"clientWrapper.d.mts","names":[],"sources":["../../src/rsc-client/clientWrapper.tsx"],"
|
|
1
|
+
{"version":3,"file":"clientWrapper.d.mts","names":[],"sources":["../../src/rsc-client/clientWrapper.tsx"],"mappings":";;;;cAUa,eAAA,EAAe,KAAA,CAAA,OAAA,CAAA,aAAA;AAAA,UAIlB,sBAAA;EACR,QAAA;AAAA;AAAA,cAGW,iBAAA,EAAmB,KAAA,CAAM,EAAA,CAAG,sBAAA"}
|
|
@@ -5,7 +5,7 @@ import React, { createContext, use } from "react";
|
|
|
5
5
|
|
|
6
6
|
//#region src/rsc-client/clientWrapper.tsx
|
|
7
7
|
const RegistryContext = createContext(void 0);
|
|
8
|
-
const
|
|
8
|
+
const DeferredComponent = ({ moduleID }) => {
|
|
9
9
|
const registry = use(RegistryContext);
|
|
10
10
|
const modulePath = getModulePathFor(moduleID);
|
|
11
11
|
if (registry) {
|
|
@@ -40,5 +40,5 @@ function getClientRSCStream(modulePath) {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
//#endregion
|
|
43
|
-
export {
|
|
43
|
+
export { DeferredComponent, RegistryContext };
|
|
44
44
|
//# sourceMappingURL=clientWrapper.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"clientWrapper.mjs","names":[],"sources":["../../src/rsc-client/clientWrapper.tsx"],"sourcesContent":["import React from \"react\";\nimport {\n createFromFetch,\n createFromReadableStream,\n} from \"@vitejs/plugin-rsc/browser\";\nimport { getModulePathFor } from \"../rsc/rscModule\";\nimport { createContext, use } from \"react\";\nimport type { LoadedDeferEntry, DeferRegistry } from \"../rsc/defer\";\nimport { withBasePath } from \"../util/basePath\";\n\nexport const RegistryContext = createContext<DeferRegistry | undefined>(\n undefined,\n);\n\ninterface
|
|
1
|
+
{"version":3,"file":"clientWrapper.mjs","names":[],"sources":["../../src/rsc-client/clientWrapper.tsx"],"sourcesContent":["import React from \"react\";\nimport {\n createFromFetch,\n createFromReadableStream,\n} from \"@vitejs/plugin-rsc/browser\";\nimport { getModulePathFor } from \"../rsc/rscModule\";\nimport { createContext, use } from \"react\";\nimport type { LoadedDeferEntry, DeferRegistry } from \"../rsc/defer\";\nimport { withBasePath } from \"../util/basePath\";\n\nexport const RegistryContext = createContext<DeferRegistry | undefined>(\n undefined,\n);\n\ninterface DeferredComponentProps {\n moduleID: string;\n}\n\nexport const DeferredComponent: React.FC<DeferredComponentProps> = ({\n moduleID,\n}) => {\n const registry = use(RegistryContext);\n const modulePath = getModulePathFor(moduleID);\n if (registry) {\n const entry = registry.load(moduleID);\n if (!entry) {\n throw new Error(`Module entry not found for ID '${moduleID}'`);\n }\n return getRSCStreamFromRegistry(entry);\n }\n const stream = getClientRSCStream(withBasePath(modulePath));\n return use(stream);\n};\n\nconst moduleToStreamMap = new Map<string, Promise<React.ReactNode>>();\n\nasync function getRSCStreamFromRegistry(\n entry: LoadedDeferEntry,\n): Promise<React.ReactNode> {\n switch (entry.state.state) {\n case \"streaming\": {\n return createFromReadableStream<React.ReactNode>(entry.state.stream);\n }\n case \"ready\": {\n const { readable, writable } = new TransformStream<\n Uint8Array,\n Uint8Array\n >();\n const writer = writable.getWriter();\n const encoder = new TextEncoder();\n await writer.write(encoder.encode(entry.state.data));\n await writer.close();\n return createFromReadableStream<React.ReactNode>(readable);\n }\n case \"error\": {\n return Promise.reject(entry.state.error);\n }\n }\n}\n\nfunction getClientRSCStream(modulePath: string) {\n let stream = moduleToStreamMap.get(modulePath);\n if (!stream) {\n stream = createFromFetch<React.ReactNode>(fetch(modulePath));\n moduleToStreamMap.set(modulePath, stream);\n }\n return stream;\n}\n"],"mappings":";;;;;;AAUA,MAAa,kBAAkB,cAC7B,OACD;AAMD,MAAa,qBAAuD,EAClE,eACI;CACJ,MAAM,WAAW,IAAI,gBAAgB;CACrC,MAAM,aAAa,iBAAiB,SAAS;AAC7C,KAAI,UAAU;EACZ,MAAM,QAAQ,SAAS,KAAK,SAAS;AACrC,MAAI,CAAC,MACH,OAAM,IAAI,MAAM,kCAAkC,SAAS,GAAG;AAEhE,SAAO,yBAAyB,MAAM;;AAGxC,QAAO,IADQ,mBAAmB,aAAa,WAAW,CAAC,CACzC;;AAGpB,MAAM,oCAAoB,IAAI,KAAuC;AAErE,eAAe,yBACb,OAC0B;AAC1B,SAAQ,MAAM,MAAM,OAApB;EACE,KAAK,YACH,QAAO,yBAA0C,MAAM,MAAM,OAAO;EAEtE,KAAK,SAAS;GACZ,MAAM,EAAE,UAAU,aAAa,IAAI,iBAGhC;GACH,MAAM,SAAS,SAAS,WAAW;GACnC,MAAM,UAAU,IAAI,aAAa;AACjC,SAAM,OAAO,MAAM,QAAQ,OAAO,MAAM,MAAM,KAAK,CAAC;AACpD,SAAM,OAAO,OAAO;AACpB,UAAO,yBAA0C,SAAS;;EAE5D,KAAK,QACH,QAAO,QAAQ,OAAO,MAAM,MAAM,MAAM;;;AAK9C,SAAS,mBAAmB,YAAoB;CAC9C,IAAI,SAAS,kBAAkB,IAAI,WAAW;AAC9C,KAAI,CAAC,QAAQ;AACX,WAAS,gBAAiC,MAAM,WAAW,CAAC;AAC5D,oBAAkB,IAAI,YAAY,OAAO;;AAE3C,QAAO"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { DeferredComponent, RegistryContext } from "./clientWrapper.mjs";
|
package/dist/ssr/entry.d.mts
CHANGED
|
@@ -4,8 +4,10 @@ import { DeferRegistry } from "../rsc/defer.mjs";
|
|
|
4
4
|
declare function renderHTML(rscStream: ReadableStream<Uint8Array>, options: {
|
|
5
5
|
appEntryMarker: string;
|
|
6
6
|
build: boolean;
|
|
7
|
+
ssr?: boolean;
|
|
7
8
|
nonce?: string;
|
|
8
9
|
deferRegistry?: DeferRegistry;
|
|
10
|
+
clientRscStream?: ReadableStream<Uint8Array>;
|
|
9
11
|
}): Promise<{
|
|
10
12
|
stream: ReadableStream<Uint8Array>;
|
|
11
13
|
status?: number;
|
package/dist/ssr/entry.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"entry.d.mts","names":[],"sources":["../../src/ssr/entry.tsx"],"
|
|
1
|
+
{"version":3,"file":"entry.d.mts","names":[],"sources":["../../src/ssr/entry.tsx"],"mappings":";;;iBAWsB,UAAA,CACpB,SAAA,EAAW,cAAA,CAAe,UAAA,GAC1B,OAAA;EACE,cAAA;EACA,KAAA;EACA,GAAA;EACA,KAAA;EACA,aAAA,GAAgB,aAAA;EAChB,eAAA,GAAkB,cAAA,CAAe,UAAA;AAAA,IAElC,OAAA;EAAU,MAAA,EAAQ,cAAA,CAAe,UAAA;EAAa,MAAA;AAAA"}
|
package/dist/ssr/entry.mjs
CHANGED
|
@@ -24,7 +24,8 @@ async function renderHTML(rscStream, options) {
|
|
|
24
24
|
});
|
|
25
25
|
}
|
|
26
26
|
let bootstrapScriptContent = "";
|
|
27
|
-
if (options.build) bootstrapScriptContent += `globalThis.${appClientManifestVar}={
|
|
27
|
+
if (options.build) if (options.ssr) bootstrapScriptContent += `globalThis.${appClientManifestVar}={stream:"${rscPayloadPlaceholder}"};\n`;
|
|
28
|
+
else bootstrapScriptContent += `globalThis.${appClientManifestVar}={marker:"${options.appEntryMarker}",stream:"${rscPayloadPlaceholder}"};\n`;
|
|
28
29
|
bootstrapScriptContent += await import.meta.viteRsc.loadBootstrapScriptContent("index");
|
|
29
30
|
let htmlStream;
|
|
30
31
|
let status;
|
|
@@ -41,7 +42,10 @@ async function renderHTML(rscStream, options) {
|
|
|
41
42
|
});
|
|
42
43
|
}
|
|
43
44
|
let responseStream = htmlStream;
|
|
44
|
-
if (!options.build)
|
|
45
|
+
if (!options.build) {
|
|
46
|
+
const streamToInject = options.clientRscStream ?? rscStream2;
|
|
47
|
+
responseStream = responseStream.pipeThrough(injectRSCPayload(streamToInject, { nonce: options?.nonce }));
|
|
48
|
+
}
|
|
45
49
|
return {
|
|
46
50
|
stream: responseStream,
|
|
47
51
|
status
|
package/dist/ssr/entry.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"entry.mjs","names":[],"sources":["../../src/ssr/entry.tsx"],"sourcesContent":["import { createFromReadableStream } from \"@vitejs/plugin-rsc/ssr\";\nimport { use } from \"react\";\nimport { renderToReadableStream } from \"react-dom/server.edge\";\nimport { injectRSCPayload } from \"rsc-html-stream/server\";\nimport type { RscPayload } from \"../rsc/entry\";\nimport { appClientManifestVar } from \"../client/globals\";\nimport { rscPayloadPlaceholder } from \"../build/rscPath\";\nimport { preload } from \"react-dom\";\nimport type { DeferRegistry } from \"../rsc/defer\";\nimport { RegistryContext } from \"#rsc-client\";\n\nexport async function renderHTML(\n rscStream: ReadableStream<Uint8Array>,\n options: {\n appEntryMarker: string;\n build: boolean;\n nonce?: string;\n deferRegistry?: DeferRegistry;\n },\n): Promise<{ stream: ReadableStream<Uint8Array>; status?: number }> {\n const [rscStream1, rscStream2] = rscStream.tee();\n\n let payload: Promise<RscPayload> | undefined;\n function SsrRoot() {\n // Tip: calling `createFromReadableStream` inside a component\n // makes `preinit`/`preload` work properly.\n payload ??= createFromReadableStream<RscPayload>(rscStream1);\n if (options.build) {\n preload(rscPayloadPlaceholder, {\n crossOrigin: \"anonymous\",\n as: \"fetch\",\n });\n }\n return (\n <RegistryContext value={options.deferRegistry}>\n {use(payload).root}\n </RegistryContext>\n );\n }\n\n let bootstrapScriptContent: string = \"\";\n if (options.build) {\n bootstrapScriptContent += `globalThis.${appClientManifestVar}={marker:\"${options.appEntryMarker}\",stream:\"${rscPayloadPlaceholder}\"};\\n`;\n }\n bootstrapScriptContent +=\n await import.meta.viteRsc.loadBootstrapScriptContent(\"index\");\n\n let htmlStream: ReadableStream<Uint8Array>;\n let status: number | undefined;\n try {\n htmlStream = await renderToReadableStream(<SsrRoot />, {\n bootstrapScriptContent,\n nonce: options?.nonce,\n });\n } catch (e) {\n // In this case, RSC payload is still sent to client and we let client render from scratch anyway.\n // This triggers the error boundary on client side.\n status = 500;\n htmlStream = await renderToReadableStream(\n <html>\n <body>\n <noscript>Internal Server Error: SSR failed</noscript>\n </body>\n </html>,\n {\n bootstrapScriptContent:\n `globalThis.__NO_HYDRATE=1;` + bootstrapScriptContent,\n nonce: options?.nonce,\n },\n );\n }\n\n let responseStream = htmlStream;\n\n if (!options.build) {\n responseStream = responseStream.pipeThrough(\n injectRSCPayload(
|
|
1
|
+
{"version":3,"file":"entry.mjs","names":[],"sources":["../../src/ssr/entry.tsx"],"sourcesContent":["import { createFromReadableStream } from \"@vitejs/plugin-rsc/ssr\";\nimport { use } from \"react\";\nimport { renderToReadableStream } from \"react-dom/server.edge\";\nimport { injectRSCPayload } from \"rsc-html-stream/server\";\nimport type { RscPayload } from \"../rsc/entry\";\nimport { appClientManifestVar } from \"../client/globals\";\nimport { rscPayloadPlaceholder } from \"../build/rscPath\";\nimport { preload } from \"react-dom\";\nimport type { DeferRegistry } from \"../rsc/defer\";\nimport { RegistryContext } from \"#rsc-client\";\n\nexport async function renderHTML(\n rscStream: ReadableStream<Uint8Array>,\n options: {\n appEntryMarker: string;\n build: boolean;\n ssr?: boolean;\n nonce?: string;\n deferRegistry?: DeferRegistry;\n clientRscStream?: ReadableStream<Uint8Array>;\n },\n): Promise<{ stream: ReadableStream<Uint8Array>; status?: number }> {\n const [rscStream1, rscStream2] = rscStream.tee();\n\n let payload: Promise<RscPayload> | undefined;\n function SsrRoot() {\n // Tip: calling `createFromReadableStream` inside a component\n // makes `preinit`/`preload` work properly.\n payload ??= createFromReadableStream<RscPayload>(rscStream1);\n if (options.build) {\n preload(rscPayloadPlaceholder, {\n crossOrigin: \"anonymous\",\n as: \"fetch\",\n });\n }\n return (\n <RegistryContext value={options.deferRegistry}>\n {use(payload).root}\n </RegistryContext>\n );\n }\n\n let bootstrapScriptContent: string = \"\";\n if (options.build) {\n if (options.ssr) {\n // SSR on: no marker needed, client hydrates full document\n bootstrapScriptContent += `globalThis.${appClientManifestVar}={stream:\"${rscPayloadPlaceholder}\"};\\n`;\n } else {\n // SSR off: marker needed for client to find mount point\n bootstrapScriptContent += `globalThis.${appClientManifestVar}={marker:\"${options.appEntryMarker}\",stream:\"${rscPayloadPlaceholder}\"};\\n`;\n }\n }\n bootstrapScriptContent +=\n await import.meta.viteRsc.loadBootstrapScriptContent(\"index\");\n\n let htmlStream: ReadableStream<Uint8Array>;\n let status: number | undefined;\n try {\n htmlStream = await renderToReadableStream(<SsrRoot />, {\n bootstrapScriptContent,\n nonce: options?.nonce,\n });\n } catch (e) {\n // In this case, RSC payload is still sent to client and we let client render from scratch anyway.\n // This triggers the error boundary on client side.\n status = 500;\n htmlStream = await renderToReadableStream(\n <html>\n <body>\n <noscript>Internal Server Error: SSR failed</noscript>\n </body>\n </html>,\n {\n bootstrapScriptContent:\n `globalThis.__NO_HYDRATE=1;` + bootstrapScriptContent,\n nonce: options?.nonce,\n },\n );\n }\n\n let responseStream = htmlStream;\n\n // Inject RSC payload into HTML for client consumption.\n // In dev: always inject (client reads from inline stream).\n // In build+SSR: skip (HTML already has full content, client hydrates directly).\n // In build+no-SSR: skip (client fetches RSC from separate file).\n if (!options.build) {\n const streamToInject = options.clientRscStream ?? rscStream2;\n responseStream = responseStream.pipeThrough(\n injectRSCPayload(streamToInject, {\n nonce: options?.nonce,\n }),\n );\n }\n\n return { stream: responseStream, status };\n}\n"],"mappings":";;;;;;;;;;;AAWA,eAAsB,WACpB,WACA,SAQkE;CAClE,MAAM,CAAC,YAAY,cAAc,UAAU,KAAK;CAEhD,IAAI;CACJ,SAAS,UAAU;AAGjB,cAAY,yBAAqC,WAAW;AAC5D,MAAI,QAAQ,MACV,SAAQ,uBAAuB;GAC7B,aAAa;GACb,IAAI;GACL,CAAC;AAEJ,SACE,oBAAC;GAAgB,OAAO,QAAQ;aAC7B,IAAI,QAAQ,CAAC;IACE;;CAItB,IAAI,yBAAiC;AACrC,KAAI,QAAQ,MACV,KAAI,QAAQ,IAEV,2BAA0B,cAAc,qBAAqB,YAAY,sBAAsB;KAG/F,2BAA0B,cAAc,qBAAqB,YAAY,QAAQ,eAAe,YAAY,sBAAsB;AAGtI,2BACE,MAAM,OAAO,KAAK,QAAQ,2BAA2B,QAAQ;CAE/D,IAAI;CACJ,IAAI;AACJ,KAAI;AACF,eAAa,MAAM,uBAAuB,oBAAC,YAAU,EAAE;GACrD;GACA,OAAO,SAAS;GACjB,CAAC;UACK,GAAG;AAGV,WAAS;AACT,eAAa,MAAM,uBACjB,oBAAC,oBACC,oBAAC,oBACC,oBAAC,wBAAS,sCAA4C,GACjD,GACF,EACP;GACE,wBACE,+BAA+B;GACjC,OAAO,SAAS;GACjB,CACF;;CAGH,IAAI,iBAAiB;AAMrB,KAAI,CAAC,QAAQ,OAAO;EAClB,MAAM,iBAAiB,QAAQ,mBAAmB;AAClD,mBAAiB,eAAe,YAC9B,iBAAiB,gBAAgB,EAC/B,OAAO,SAAS,OACjB,CAAC,CACH;;AAGH,QAAO;EAAE,QAAQ;EAAgB;EAAQ"}
|