@funstack/static 0.0.5-alpha.0 → 0.0.6

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.
@@ -1,38 +1,47 @@
1
1
  import { extractIDFromModulePath } from "./rscModule.mjs";
2
+ import { urlPathToFileCandidates } from "../util/urlPath.mjs";
2
3
  import { devMainRscPath } from "./request.mjs";
3
4
  import { stripBasePath } from "../util/basePath.mjs";
4
5
  import { defer, deferRegistry } from "./defer.mjs";
5
6
  import { generateAppMarker } from "./marker.mjs";
7
+ import { resolveApp, resolveRoot } from "./resolveEntry.mjs";
6
8
  import { jsx } from "react/jsx-runtime";
7
9
  import { ssr } from "virtual:funstack/config";
8
10
  import { renderToReadableStream } from "@vitejs/plugin-rsc/rsc";
9
11
 
10
12
  //#region src/rsc/entry.tsx
11
- async function loadEntries() {
12
- const Root = (await import("virtual:funstack/root")).default;
13
- const App = (await import("virtual:funstack/app")).default;
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.");
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.");
16
- return {
17
- Root,
18
- App
19
- };
13
+ async function loadEntriesList() {
14
+ const getEntries = (await import("virtual:funstack/entries")).default;
15
+ const result = getEntries();
16
+ const entries = [];
17
+ for await (const entry of result) entries.push(entry);
18
+ return entries;
20
19
  }
21
20
  /**
22
- * Entrypoint to serve HTML response in dev environment
21
+ * Find the entry matching a URL path from a list of entries.
23
22
  */
24
- async function serveHTML() {
25
- const timings = [];
23
+ function findEntryForUrlPath(entries, urlPath) {
24
+ const candidates = urlPathToFileCandidates(urlPath);
25
+ for (const candidate of candidates) {
26
+ const entry = entries.find((e) => e.path === candidate);
27
+ if (entry) return entry;
28
+ }
29
+ }
30
+ /**
31
+ * Renders a single entry to an HTML response.
32
+ */
33
+ async function renderEntryToResponse(entry, timings) {
26
34
  const marker = generateAppMarker();
27
- const entriesStart = performance.now();
28
- const { Root, App } = await loadEntries();
29
- timings.push(`entries;dur=${performance.now() - entriesStart}`);
35
+ const resolveStart = performance.now();
36
+ const Root = await resolveRoot(entry.root);
37
+ const appNode = await resolveApp(entry.app);
38
+ timings.push(`resolve;dur=${performance.now() - resolveStart}`);
30
39
  const ssrModuleStart = performance.now();
31
40
  const ssrEntryModule = await import.meta.viteRsc.loadModule("ssr");
32
41
  timings.push(`ssr-module;dur=${performance.now() - ssrModuleStart}`);
33
42
  if (ssr) {
34
43
  const rscStart = performance.now();
35
- const rootRscStream = renderToReadableStream({ root: /* @__PURE__ */ jsx(Root, { children: /* @__PURE__ */ jsx(App, {}) }) });
44
+ const rootRscStream = renderToReadableStream({ root: /* @__PURE__ */ jsx(Root, { children: appNode }) });
36
45
  timings.push(`rsc;dur=${performance.now() - rscStart}`);
37
46
  const ssrStart = performance.now();
38
47
  const ssrResult = await ssrEntryModule.renderHTML(rootRscStream, {
@@ -51,7 +60,7 @@ async function serveHTML() {
51
60
  } else {
52
61
  const rscStart = performance.now();
53
62
  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, {}) }) });
63
+ const clientRscStream = renderToReadableStream({ root: /* @__PURE__ */ jsx(Root, { children: appNode }) });
55
64
  timings.push(`rsc;dur=${performance.now() - rscStart}`);
56
65
  const ssrStart = performance.now();
57
66
  const ssrResult = await ssrEntryModule.renderHTML(shellRscStream, {
@@ -70,6 +79,22 @@ async function serveHTML() {
70
79
  });
71
80
  }
72
81
  }
82
+ /**
83
+ * Entrypoint to serve HTML response in dev environment.
84
+ * Accepts a Request to determine which entry to render based on URL path.
85
+ */
86
+ async function serveHTML(request) {
87
+ const timings = [];
88
+ const entriesStart = performance.now();
89
+ const entries = await loadEntriesList();
90
+ timings.push(`entries;dur=${performance.now() - entriesStart}`);
91
+ const entry = findEntryForUrlPath(entries, stripBasePath(new URL(request.url).pathname));
92
+ if (!entry) return new Response("Not Found", {
93
+ status: 404,
94
+ headers: { "Content-type": "text/plain" }
95
+ });
96
+ return renderEntryToResponse(entry, timings);
97
+ }
73
98
  var ServeRSCError = class extends Error {
74
99
  status;
75
100
  constructor(message, status) {
@@ -82,17 +107,23 @@ function isServeRSCError(error) {
82
107
  return error instanceof Error && error.name === "ServeRSCError";
83
108
  }
84
109
  /**
85
- * Servers an RSC stream response
110
+ * Serves an RSC stream response
86
111
  */
87
112
  async function serveRSC(request) {
88
113
  const timings = [];
89
114
  const pathname = stripBasePath(new URL(request.url).pathname);
90
115
  if (pathname === devMainRscPath) {
91
116
  const entriesStart = performance.now();
92
- const { Root, App } = await loadEntries();
117
+ const entries = await loadEntriesList();
93
118
  timings.push(`entries;dur=${performance.now() - entriesStart}`);
119
+ const entry = entries[0];
120
+ if (!entry) throw new ServeRSCError("No entries defined", 404);
121
+ const resolveStart = performance.now();
122
+ const Root = await resolveRoot(entry.root);
123
+ const appNode = await resolveApp(entry.app);
124
+ timings.push(`resolve;dur=${performance.now() - resolveStart}`);
94
125
  const rscStart = performance.now();
95
- const rootRscStream = renderToReadableStream({ root: /* @__PURE__ */ jsx(Root, { children: /* @__PURE__ */ jsx(App, {}) }) });
126
+ const rootRscStream = renderToReadableStream({ root: /* @__PURE__ */ jsx(Root, { children: appNode }) });
96
127
  timings.push(`rsc;dur=${performance.now() - rscStart}`);
97
128
  return new Response(rootRscStream, {
98
129
  status: 200,
@@ -128,27 +159,39 @@ async function serveRSC(request) {
128
159
  }
129
160
  }
130
161
  /**
131
- * Build handler
162
+ * Build handler — iterates over all entries and returns per-entry results
163
+ * along with the shared defer registry.
132
164
  */
133
165
  async function build() {
134
- const marker = generateAppMarker();
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
- }
145
- return {
146
- html: (await (await import.meta.viteRsc.loadModule("ssr")).renderHTML(rootRscStream, {
166
+ const getEntries = (await import("virtual:funstack/entries")).default;
167
+ const ssrEntryModule = await import.meta.viteRsc.loadModule("ssr");
168
+ const results = [];
169
+ for await (const entry of getEntries()) {
170
+ const Root = await resolveRoot(entry.root);
171
+ const appNode = await resolveApp(entry.app);
172
+ const marker = generateAppMarker();
173
+ let rootRscStream;
174
+ let appRscStream;
175
+ if (ssr) {
176
+ rootRscStream = renderToReadableStream({ root: /* @__PURE__ */ jsx(Root, { children: appNode }) });
177
+ appRscStream = renderToReadableStream({ root: /* @__PURE__ */ jsx(Root, { children: appNode }) });
178
+ } else {
179
+ rootRscStream = renderToReadableStream({ root: /* @__PURE__ */ jsx(Root, { children: /* @__PURE__ */ jsx("span", { id: marker }) }) });
180
+ appRscStream = renderToReadableStream({ root: appNode });
181
+ }
182
+ const ssrResult = await ssrEntryModule.renderHTML(rootRscStream, {
147
183
  appEntryMarker: marker,
148
184
  build: true,
149
185
  ssr
150
- })).stream,
151
- appRsc: appRscStream,
186
+ });
187
+ results.push({
188
+ path: entry.path,
189
+ html: ssrResult.stream,
190
+ appRsc: appRscStream
191
+ });
192
+ }
193
+ return {
194
+ entries: results,
152
195
  deferRegistry
153
196
  };
154
197
  }
@@ -1 +1 @@
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":"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"}
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\";\nimport { urlPathToFileCandidates } from \"../util/urlPath\";\nimport { resolveRoot, resolveApp } from \"./resolveEntry\";\nimport type { EntryDefinition, GetEntriesResult } from \"../entryDefinition\";\n\nexport type RscPayload = {\n root: React.ReactNode;\n};\n\nexport type EntryBuildResult = {\n path: string;\n html: ReadableStream<Uint8Array>;\n appRsc: ReadableStream<Uint8Array>;\n};\n\nimport { ssr as ssrEnabled } from \"virtual:funstack/config\";\n\nasync function loadEntriesList(): Promise<EntryDefinition[]> {\n const getEntries = (await import(\"virtual:funstack/entries\")).default;\n const result: GetEntriesResult = getEntries();\n const entries: EntryDefinition[] = [];\n for await (const entry of result) {\n entries.push(entry);\n }\n return entries;\n}\n\n/**\n * Find the entry matching a URL path from a list of entries.\n */\nfunction findEntryForUrlPath(\n entries: EntryDefinition[],\n urlPath: string,\n): EntryDefinition | undefined {\n const candidates = urlPathToFileCandidates(urlPath);\n for (const candidate of candidates) {\n const entry = entries.find((e) => e.path === candidate);\n if (entry) {\n return entry;\n }\n }\n return undefined;\n}\n\n/**\n * Renders a single entry to an HTML response.\n */\nasync function renderEntryToResponse(\n entry: EntryDefinition,\n timings: string[],\n): Promise<Response> {\n const marker = generateAppMarker();\n\n const resolveStart = performance.now();\n const Root = await resolveRoot(entry.root);\n const appNode = await resolveApp(entry.app);\n timings.push(`resolve;dur=${performance.now() - resolveStart}`);\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: <Root>{appNode}</Root>,\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: <Root>{appNode}</Root>,\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\n/**\n * Entrypoint to serve HTML response in dev environment.\n * Accepts a Request to determine which entry to render based on URL path.\n */\nexport async function serveHTML(request: Request): Promise<Response> {\n const timings: string[] = [];\n\n const entriesStart = performance.now();\n const entries = await loadEntriesList();\n timings.push(`entries;dur=${performance.now() - entriesStart}`);\n\n const url = new URL(request.url);\n const urlPath = stripBasePath(url.pathname);\n const entry = findEntryForUrlPath(entries, urlPath);\n\n if (!entry) {\n return new Response(\"Not Found\", {\n status: 404,\n headers: { \"Content-type\": \"text/plain\" },\n });\n }\n\n return renderEntryToResponse(entry, timings);\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 * Serves 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 // For HMR, re-render the first entry (single-entry mode) or index.html entry\n const entriesStart = performance.now();\n const entries = await loadEntriesList();\n timings.push(`entries;dur=${performance.now() - entriesStart}`);\n\n // Use the first entry for HMR re-fetch\n const entry = entries[0];\n if (!entry) {\n throw new ServeRSCError(\"No entries defined\", 404);\n }\n\n const resolveStart = performance.now();\n const Root = await resolveRoot(entry.root);\n const appNode = await resolveApp(entry.app);\n timings.push(`resolve;dur=${performance.now() - resolveStart}`);\n\n const rscStart = performance.now();\n const rootRscStream = renderToReadableStream<RscPayload>({\n root: <Root>{appNode}</Root>,\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 — iterates over all entries and returns per-entry results\n * along with the shared defer registry.\n */\nexport async function build() {\n const getEntries = (await import(\"virtual:funstack/entries\")).default;\n\n const ssrEntryModule = await import.meta.viteRsc.loadModule<\n typeof import(\"../ssr/entry\")\n >(\"ssr\");\n\n const results: EntryBuildResult[] = [];\n for await (const entry of getEntries()) {\n const Root = await resolveRoot(entry.root);\n const appNode = await resolveApp(entry.app);\n\n const marker = generateAppMarker();\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: <Root>{appNode}</Root>,\n });\n appRscStream = renderToReadableStream<RscPayload>({\n root: <Root>{appNode}</Root>,\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: appNode,\n });\n }\n\n const ssrResult = await ssrEntryModule.renderHTML(rootRscStream, {\n appEntryMarker: marker,\n build: true,\n ssr: ssrEnabled,\n });\n\n results.push({\n path: entry.path,\n html: ssrResult.stream,\n appRsc: appRscStream,\n });\n }\n\n return {\n entries: results,\n deferRegistry,\n };\n}\n\nexport { defer } from \"./defer\";\n\nif (import.meta.hot) {\n import.meta.hot.accept();\n}\n"],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ import { createElement } from "react";
2
+
3
+ //#region src/rsc/resolveEntry.ts
4
+ /**
5
+ * Resolves the root field of an EntryDefinition to a concrete React component.
6
+ */
7
+ async function resolveRoot(root) {
8
+ return (typeof root === "function" ? await root() : await root).default;
9
+ }
10
+ /**
11
+ * Checks whether a value is an AppModule (has a `default` property that is a function).
12
+ */
13
+ function isAppModule(value) {
14
+ return typeof value === "object" && value !== null && "default" in value && typeof value.default === "function";
15
+ }
16
+ /**
17
+ * Resolves the app field of an EntryDefinition to a React node.
18
+ */
19
+ async function resolveApp(app) {
20
+ if (typeof app === "function") return createElement((await app()).default);
21
+ if (isAppModule(app)) return createElement(app.default);
22
+ const resolved = await app;
23
+ if (isAppModule(resolved)) return createElement(resolved.default);
24
+ return resolved;
25
+ }
26
+
27
+ //#endregion
28
+ export { resolveApp, resolveRoot };
29
+ //# sourceMappingURL=resolveEntry.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolveEntry.mjs","names":[],"sources":["../../src/rsc/resolveEntry.ts"],"sourcesContent":["import { createElement } from \"react\";\nimport type { EntryDefinition } from \"../entryDefinition\";\n\n/**\n * Resolves the root field of an EntryDefinition to a concrete React component.\n */\nexport async function resolveRoot(\n root: EntryDefinition[\"root\"],\n): Promise<React.ComponentType<{ children: React.ReactNode }>> {\n const module = typeof root === \"function\" ? await root() : await root;\n return module.default;\n}\n\n/**\n * Checks whether a value is an AppModule (has a `default` property that is a function).\n */\nfunction isAppModule(\n value: unknown,\n): value is { default: React.ComponentType } {\n return (\n typeof value === \"object\" &&\n value !== null &&\n \"default\" in value &&\n typeof (value as Record<string, unknown>).default === \"function\"\n );\n}\n\n/**\n * Resolves the app field of an EntryDefinition to a React node.\n */\nexport async function resolveApp(\n app: EntryDefinition[\"app\"],\n): Promise<React.ReactNode> {\n if (typeof app === \"function\") {\n // Lazy import: () => Promise<{ default: Component }>\n const module = await app();\n return createElement(module.default);\n }\n if (isAppModule(app)) {\n // Sync module object: { default: Component }\n return createElement(app.default);\n }\n // Could be a Promise<AppModule> or a ReactNode (including Promise<ReactNode>).\n // Await it and check the resolved value.\n const resolved = await app;\n if (isAppModule(resolved)) {\n return createElement(resolved.default);\n }\n // ReactNode (JSX of a server component, or a resolved ReactNode)\n return resolved;\n}\n"],"mappings":";;;;;;AAMA,eAAsB,YACpB,MAC6D;AAE7D,SADe,OAAO,SAAS,aAAa,MAAM,MAAM,GAAG,MAAM,MACnD;;;;;AAMhB,SAAS,YACP,OAC2C;AAC3C,QACE,OAAO,UAAU,YACjB,UAAU,QACV,aAAa,SACb,OAAQ,MAAkC,YAAY;;;;;AAO1D,eAAsB,WACpB,KAC0B;AAC1B,KAAI,OAAO,QAAQ,WAGjB,QAAO,eADQ,MAAM,KAAK,EACE,QAAQ;AAEtC,KAAI,YAAY,IAAI,CAElB,QAAO,cAAc,IAAI,QAAQ;CAInC,MAAM,WAAW,MAAM;AACvB,KAAI,YAAY,SAAS,CACvB,QAAO,cAAc,SAAS,QAAQ;AAGxC,QAAO"}
@@ -34,7 +34,7 @@ async function renderHTML(rscStream, options) {
34
34
  bootstrapScriptContent,
35
35
  nonce: options?.nonce
36
36
  });
37
- } catch (e) {
37
+ } catch {
38
38
  status = 500;
39
39
  htmlStream = await renderToReadableStream(/* @__PURE__ */ jsx("html", { children: /* @__PURE__ */ jsx("body", { children: /* @__PURE__ */ jsx("noscript", { children: "Internal Server Error: SSR failed" }) }) }), {
40
40
  bootstrapScriptContent: `globalThis.__NO_HYDRATE=1;` + bootstrapScriptContent,
@@ -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 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"}
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 {\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;SACI;AAGN,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"}
@@ -0,0 +1,17 @@
1
+ //#region src/util/urlPath.ts
2
+ /**
3
+ * Maps a URL path to candidate file names for matching against entry paths.
4
+ *
5
+ * - `/` → `["index.html"]`
6
+ * - `/about` → `["about.html", "about/index.html"]`
7
+ * - `/blog/post-1` → `["blog/post-1.html", "blog/post-1/index.html"]`
8
+ */
9
+ function urlPathToFileCandidates(urlPath) {
10
+ if (urlPath === "/" || urlPath === "") return ["index.html"];
11
+ const stripped = urlPath.replace(/^\//, "").replace(/\/$/, "");
12
+ return [`${stripped}.html`, `${stripped}/index.html`];
13
+ }
14
+
15
+ //#endregion
16
+ export { urlPathToFileCandidates };
17
+ //# sourceMappingURL=urlPath.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"urlPath.mjs","names":[],"sources":["../../src/util/urlPath.ts"],"sourcesContent":["/**\n * Maps a URL path to candidate file names for matching against entry paths.\n *\n * - `/` → `[\"index.html\"]`\n * - `/about` → `[\"about.html\", \"about/index.html\"]`\n * - `/blog/post-1` → `[\"blog/post-1.html\", \"blog/post-1/index.html\"]`\n */\nexport function urlPathToFileCandidates(urlPath: string): string[] {\n if (urlPath === \"/\" || urlPath === \"\") {\n return [\"index.html\"];\n }\n const stripped = urlPath.replace(/^\\//, \"\").replace(/\\/$/, \"\");\n return [`${stripped}.html`, `${stripped}/index.html`];\n}\n"],"mappings":";;;;;;;;AAOA,SAAgB,wBAAwB,SAA2B;AACjE,KAAI,YAAY,OAAO,YAAY,GACjC,QAAO,CAAC,aAAa;CAEvB,MAAM,WAAW,QAAQ,QAAQ,OAAO,GAAG,CAAC,QAAQ,OAAO,GAAG;AAC9D,QAAO,CAAC,GAAG,SAAS,QAAQ,GAAG,SAAS,aAAa"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@funstack/static",
3
- "version": "0.0.5-alpha.0",
3
+ "version": "0.0.6",
4
4
  "description": "FUNSTACK static library",
5
5
  "type": "module",
6
6
  "repository": {
@@ -23,6 +23,10 @@
23
23
  "types": "./dist/index.d.mts",
24
24
  "import": "./dist/index.mjs"
25
25
  },
26
+ "./entries": {
27
+ "types": "./dist/entryDefinition.d.mts",
28
+ "import": "./dist/entryDefinition.mjs"
29
+ },
26
30
  "./server": {
27
31
  "types": "./dist/entries/server.d.mts",
28
32
  "import": "./dist/entries/server.mjs"
@@ -42,24 +46,24 @@
42
46
  "author": "uhyo <uhyo@uhy.ooo>",
43
47
  "license": "MIT",
44
48
  "devDependencies": {
45
- "@playwright/test": "^1.58.1",
46
- "@types/node": "^25.1.0",
47
- "@types/react": "^19.2.10",
49
+ "@playwright/test": "^1.58.2",
50
+ "@types/node": "^25.2.3",
51
+ "@types/react": "^19.2.14",
48
52
  "@types/react-dom": "^19.2.3",
49
- "jsdom": "^27.4.0",
53
+ "jsdom": "^28.1.0",
50
54
  "react": "^19.2.4",
51
55
  "react-dom": "^19.2.4",
52
- "tsdown": "^0.20.1",
56
+ "tsdown": "^0.20.3",
53
57
  "typescript": "^5.9.3",
54
58
  "vite": "^7.3.1",
55
59
  "vitest": "^4.0.18"
56
60
  },
57
61
  "dependencies": {
58
62
  "@funstack/skill-installer": "^1.0.0",
59
- "@vitejs/plugin-rsc": "^0.5.17",
60
- "react-error-boundary": "^6.1.0",
63
+ "@vitejs/plugin-rsc": "^0.5.19",
64
+ "react-error-boundary": "^6.1.1",
61
65
  "rsc-html-stream": "^0.0.7",
62
- "srvx": "^0.10.1"
66
+ "srvx": "^0.11.4"
63
67
  },
64
68
  "peerDependencies": {
65
69
  "react": "^19.2.3",
@@ -73,6 +77,7 @@
73
77
  "test": "vitest",
74
78
  "test:run": "vitest run",
75
79
  "test:e2e": "playwright test --config e2e/playwright.config.ts",
80
+ "test:e2e:dev": "playwright test --config e2e/playwright-dev.config.ts",
76
81
  "typecheck": "tsc --noEmit"
77
82
  }
78
83
  }
@@ -1 +0,0 @@
1
- import { DeferredComponent, RegistryContext } from "./clientWrapper.mjs";