@funstack/static 0.0.10 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/bin/skill-installer.mjs +5 -4
- package/dist/bin/skill-installer.mjs.map +1 -1
- package/dist/build/buildApp.mjs +7 -8
- package/dist/build/buildApp.mjs.map +1 -1
- package/dist/build/contentHash.mjs +1 -1
- package/dist/build/dependencyGraph.mjs +1 -1
- package/dist/build/dependencyGraph.mjs.map +1 -1
- package/dist/build/rscPath.mjs +3 -4
- package/dist/build/rscPath.mjs.map +1 -1
- package/dist/build/rscProcessor.mjs +4 -4
- package/dist/build/rscProcessor.mjs.map +1 -1
- package/dist/build/validateEntryPath.mjs +1 -1
- package/dist/client/entry.d.mts +1 -1
- package/dist/client/entry.mjs +2 -3
- package/dist/client/entry.mjs.map +1 -1
- package/dist/client/error-boundary.mjs +2 -4
- package/dist/client/error-boundary.mjs.map +1 -1
- package/dist/client/globals.mjs +2 -4
- package/dist/client/globals.mjs.map +1 -1
- package/dist/docs/GettingStarted.md +10 -6
- package/dist/docs/MigratingFromViteSPA.md +5 -5
- package/dist/docs/{learn → advanced}/MultipleEntrypoints.md +14 -42
- package/dist/docs/{learn → advanced}/SSR.md +3 -3
- package/dist/docs/api/EntryDefinition.md +2 -2
- package/dist/docs/api/FunstackStatic.md +26 -7
- package/dist/docs/index.md +5 -2
- package/dist/docs/learn/DeferAndActivity.md +3 -3
- package/dist/docs/learn/HowItWorks.md +3 -3
- package/dist/docs/learn/LazyServerComponents.md +3 -3
- package/dist/docs/learn/OptimizingPayloads.md +4 -4
- package/dist/docs/learn/RSC.md +3 -3
- package/dist/entries/client.d.mts +1 -1
- package/dist/entries/client.mjs +1 -2
- package/dist/entries/rsc-client.mjs +1 -3
- package/dist/entries/rsc.mjs +1 -2
- package/dist/entries/server.mjs +1 -2
- package/dist/entries/ssr.mjs +1 -2
- package/dist/entryDefinition.mjs +1 -1
- package/dist/index.mjs +1 -2
- package/dist/plugin/getRSCEntryPoint.mjs +1 -1
- package/dist/plugin/getRSCEntryPoint.mjs.map +1 -1
- package/dist/plugin/index.d.mts +14 -0
- package/dist/plugin/index.d.mts.map +1 -1
- package/dist/plugin/index.mjs +6 -5
- package/dist/plugin/index.mjs.map +1 -1
- package/dist/plugin/server.mjs +1 -2
- package/dist/plugin/server.mjs.map +1 -1
- package/dist/rsc/defer.d.mts.map +1 -1
- package/dist/rsc/defer.mjs +3 -3
- package/dist/rsc/defer.mjs.map +1 -1
- package/dist/rsc/entry.mjs +4 -5
- package/dist/rsc/entry.mjs.map +1 -1
- package/dist/rsc/marker.mjs +1 -1
- package/dist/rsc/request.mjs +1 -1
- package/dist/rsc/resolveEntry.mjs +1 -2
- package/dist/rsc/resolveEntry.mjs.map +1 -1
- package/dist/rsc/rscModule.mjs +8 -8
- package/dist/rsc/rscModule.mjs.map +1 -1
- package/dist/rsc-client/clientWrapper.mjs +2 -3
- package/dist/rsc-client/clientWrapper.mjs.map +1 -1
- package/dist/rsc-client/entry.d.mts +1 -0
- package/dist/rsc-client/entry.mjs +2 -4
- package/dist/ssr/entry.mjs +2 -2
- package/dist/ssr/entry.mjs.map +1 -1
- package/dist/util/basePath.mjs +1 -1
- package/dist/util/drainStream.mjs +1 -1
- package/dist/util/urlPath.mjs +1 -1
- package/package.json +5 -5
- package/skills/funstack-static-knowledge/SKILL.md +1 -1
package/README.md
CHANGED
|
@@ -41,14 +41,14 @@ For detailed API documentation and guides, visit the **[Documentation](https://s
|
|
|
41
41
|
|
|
42
42
|
### :robot: FUNSTACK Static Skill
|
|
43
43
|
|
|
44
|
-
FUNSTACK Static provides an Agent Skill to feed your AI agents with knowledge about this framework.
|
|
44
|
+
FUNSTACK Static provides an Agent Skill to feed your AI agents with knowledge about this framework. Run the following command to add the skill to the project:
|
|
45
45
|
|
|
46
46
|
```sh
|
|
47
|
-
npx funstack-static-skill-installer
|
|
47
|
+
npx -p @funstack/static funstack-static-skill-installer
|
|
48
48
|
# or
|
|
49
|
-
yarn funstack-static-skill-installer
|
|
49
|
+
yarn dlx -p @funstack/static funstack-static-skill-installer
|
|
50
50
|
# or
|
|
51
|
-
pnpm funstack-static-skill-installer
|
|
51
|
+
pnpm --package @funstack/static dlx funstack-static-skill-installer
|
|
52
52
|
# or, if you use skills CLI (https://skills.sh/)
|
|
53
53
|
npx skills add uhyo/funstack-static
|
|
54
54
|
```
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
#! /usr/bin/env node
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
3
4
|
import { install } from "@funstack/skill-installer";
|
|
4
|
-
|
|
5
5
|
//#region src/bin/skill-installer.ts
|
|
6
|
-
const
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const resolved = path.resolve(__dirname, "../../skills/funstack-static-knowledge");
|
|
7
8
|
console.log("Installing skill from:", resolved);
|
|
8
9
|
await install(resolved);
|
|
9
|
-
|
|
10
10
|
//#endregion
|
|
11
|
-
export {
|
|
11
|
+
export {};
|
|
12
|
+
|
|
12
13
|
//# sourceMappingURL=skill-installer.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"skill-installer.mjs","names":[],"sources":["../../src/bin/skill-installer.ts"],"sourcesContent":["#! /usr/bin/env node\n\nimport { install } from \"@funstack/skill-installer\";\nimport path from \"node:path\";\n\nconst
|
|
1
|
+
{"version":3,"file":"skill-installer.mjs","names":[],"sources":["../../src/bin/skill-installer.ts"],"sourcesContent":["#! /usr/bin/env node\n\nimport { install } from \"@funstack/skill-installer\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n// Resolve relative to this script (dist/bin/) so it works\n// both when installed locally and via npx -p / pnpm dlx / yarn dlx.\nconst resolved = path.resolve(\n __dirname,\n \"../../skills/funstack-static-knowledge\",\n);\n\nconsole.log(\"Installing skill from:\", resolved);\n\nawait install(resolved);\n"],"mappings":";;;;;AAMA,MAAM,YAAY,KAAK,QAAQ,cAAc,OAAO,KAAK,IAAI,CAAC;AAG9D,MAAM,WAAW,KAAK,QACpB,WACA,yCACD;AAED,QAAQ,IAAI,0BAA0B,SAAS;AAE/C,MAAM,QAAQ,SAAS"}
|
package/dist/build/buildApp.mjs
CHANGED
|
@@ -7,9 +7,8 @@ import { checkDuplicatePaths, validateEntryPath } from "./validateEntryPath.mjs"
|
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
9
9
|
import { pathToFileURL } from "node:url";
|
|
10
|
-
|
|
11
10
|
//#region src/build/buildApp.ts
|
|
12
|
-
async function buildApp(builder, context) {
|
|
11
|
+
async function buildApp(builder, context, options) {
|
|
13
12
|
const { config } = builder;
|
|
14
13
|
const entry = await import(pathToFileURL(path.join(config.environments.rsc.build.outDir, "index.js")).href);
|
|
15
14
|
const baseDir = config.environments.client.build.outDir;
|
|
@@ -26,8 +25,8 @@ async function buildApp(builder, context) {
|
|
|
26
25
|
const dummyStream = new ReadableStream({ start(controller) {
|
|
27
26
|
controller.close();
|
|
28
27
|
} });
|
|
29
|
-
const { components, idMapping } = await processRscComponents(deferRegistry.loadAll(), dummyStream, context);
|
|
30
|
-
for (const result of entries) await buildSingleEntry(result, idMapping, baseDir, base, context);
|
|
28
|
+
const { components, idMapping } = await processRscComponents(deferRegistry.loadAll(), dummyStream, options.rscPayloadDir, context);
|
|
29
|
+
for (const result of entries) await buildSingleEntry(result, idMapping, baseDir, base, options.rscPayloadDir, context);
|
|
31
30
|
for (const { finalId, finalContent, name } of components) await writeFileNormal(path.join(baseDir, getModulePathFor(finalId).replace(/^\//, "")), finalContent, context, name);
|
|
32
31
|
}
|
|
33
32
|
function normalizeBase(base) {
|
|
@@ -42,15 +41,15 @@ function replaceIdsInContent(content, idMapping) {
|
|
|
42
41
|
for (const [oldId, newId] of idMapping) if (oldId !== newId) result = result.replaceAll(oldId, newId);
|
|
43
42
|
return result;
|
|
44
43
|
}
|
|
45
|
-
async function buildSingleEntry(result, idMapping, baseDir, base, context) {
|
|
44
|
+
async function buildSingleEntry(result, idMapping, baseDir, base, rscPayloadDir, context) {
|
|
46
45
|
const { path: entryPath, html, appRsc } = result;
|
|
47
46
|
const htmlContent = await drainStream(html);
|
|
48
47
|
const appRscContent = replaceIdsInContent(await drainStream(appRsc), idMapping);
|
|
49
48
|
const mainPayloadHash = await computeContentHash(appRscContent);
|
|
50
|
-
const mainPayloadPath = base === "" ? getRscPayloadPath(mainPayloadHash) : base + getRscPayloadPath(mainPayloadHash);
|
|
49
|
+
const mainPayloadPath = base === "" ? getRscPayloadPath(mainPayloadHash, rscPayloadDir) : base + getRscPayloadPath(mainPayloadHash, rscPayloadDir);
|
|
51
50
|
const finalHtmlContent = htmlContent.replaceAll(rscPayloadPlaceholder, mainPayloadPath);
|
|
52
51
|
await writeFileNormal(path.join(baseDir, entryPath), finalHtmlContent, context);
|
|
53
|
-
await writeFileNormal(path.join(baseDir, getRscPayloadPath(mainPayloadHash).replace(/^\//, "")), appRscContent, context);
|
|
52
|
+
await writeFileNormal(path.join(baseDir, getRscPayloadPath(mainPayloadHash, rscPayloadDir).replace(/^\//, "")), appRscContent, context);
|
|
54
53
|
}
|
|
55
54
|
async function writeFileNormal(filePath, data, context, name) {
|
|
56
55
|
await mkdir(path.dirname(filePath), { recursive: true });
|
|
@@ -58,7 +57,7 @@ async function writeFileNormal(filePath, data, context, name) {
|
|
|
58
57
|
context.info(`[funstack] Writing ${filePath}${nameInfo}`);
|
|
59
58
|
await writeFile(filePath, data);
|
|
60
59
|
}
|
|
61
|
-
|
|
62
60
|
//#endregion
|
|
63
61
|
export { buildApp };
|
|
62
|
+
|
|
64
63
|
//# sourceMappingURL=buildApp.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"buildApp.mjs","names":[],"sources":["../../src/build/buildApp.ts"],"sourcesContent":["import { mkdir, writeFile } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { pathToFileURL } from \"node:url\";\nimport type { ViteBuilder, MinimalPluginContextWithoutEnvironment } from \"vite\";\nimport { rscPayloadPlaceholder, getRscPayloadPath } from \"./rscPath\";\nimport { getModulePathFor } from \"../rsc/rscModule\";\nimport { processRscComponents } from \"./rscProcessor\";\nimport { computeContentHash } from \"./contentHash\";\nimport { drainStream } from \"../util/drainStream\";\nimport { validateEntryPath, checkDuplicatePaths } from \"./validateEntryPath\";\nimport type { EntryBuildResult } from \"../rsc/entry\";\n\nexport async function buildApp(\n builder: ViteBuilder,\n context: MinimalPluginContextWithoutEnvironment,\n) {\n const { config } = builder;\n // import server entry\n const entryPath = path.join(config.environments.rsc.build.outDir, \"index.js\");\n const entry: typeof import(\"../rsc/entry\") = await import(\n pathToFileURL(entryPath).href\n );\n\n const baseDir = config.environments.client.build.outDir;\n const base = normalizeBase(config.base);\n\n const { entries, deferRegistry } = await entry.build();\n\n // Validate all entry paths\n const paths: string[] = [];\n for (const result of entries) {\n const error = validateEntryPath(result.path);\n if (error) {\n throw new Error(error);\n }\n paths.push(result.path);\n }\n const dupError = checkDuplicatePaths(paths);\n if (dupError) {\n throw new Error(dupError);\n }\n\n // Process all deferred components once across all entries.\n // We pass a dummy empty stream since we handle per-entry RSC payloads separately.\n const dummyStream = new ReadableStream<Uint8Array>({\n start(controller) {\n controller.close();\n },\n });\n const { components, idMapping } = await processRscComponents(\n deferRegistry.loadAll(),\n dummyStream,\n context,\n );\n\n // Write each entry's HTML and RSC payload\n for (const result of entries) {\n await buildSingleEntry(result
|
|
1
|
+
{"version":3,"file":"buildApp.mjs","names":[],"sources":["../../src/build/buildApp.ts"],"sourcesContent":["import { mkdir, writeFile } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { pathToFileURL } from \"node:url\";\nimport type { ViteBuilder, MinimalPluginContextWithoutEnvironment } from \"vite\";\nimport { rscPayloadPlaceholder, getRscPayloadPath } from \"./rscPath\";\nimport { getModulePathFor } from \"../rsc/rscModule\";\nimport { processRscComponents } from \"./rscProcessor\";\nimport { computeContentHash } from \"./contentHash\";\nimport { drainStream } from \"../util/drainStream\";\nimport { validateEntryPath, checkDuplicatePaths } from \"./validateEntryPath\";\nimport type { EntryBuildResult } from \"../rsc/entry\";\n\nexport async function buildApp(\n builder: ViteBuilder,\n context: MinimalPluginContextWithoutEnvironment,\n options: { rscPayloadDir: string },\n) {\n const { config } = builder;\n // import server entry\n const entryPath = path.join(config.environments.rsc.build.outDir, \"index.js\");\n const entry: typeof import(\"../rsc/entry\") = await import(\n pathToFileURL(entryPath).href\n );\n\n const baseDir = config.environments.client.build.outDir;\n const base = normalizeBase(config.base);\n\n const { entries, deferRegistry } = await entry.build();\n\n // Validate all entry paths\n const paths: string[] = [];\n for (const result of entries) {\n const error = validateEntryPath(result.path);\n if (error) {\n throw new Error(error);\n }\n paths.push(result.path);\n }\n const dupError = checkDuplicatePaths(paths);\n if (dupError) {\n throw new Error(dupError);\n }\n\n // Process all deferred components once across all entries.\n // We pass a dummy empty stream since we handle per-entry RSC payloads separately.\n const dummyStream = new ReadableStream<Uint8Array>({\n start(controller) {\n controller.close();\n },\n });\n const { components, idMapping } = await processRscComponents(\n deferRegistry.loadAll(),\n dummyStream,\n options.rscPayloadDir,\n context,\n );\n\n // Write each entry's HTML and RSC payload\n for (const result of entries) {\n await buildSingleEntry(\n result,\n idMapping,\n baseDir,\n base,\n options.rscPayloadDir,\n context,\n );\n }\n\n // Write all deferred component payloads\n for (const { finalId, finalContent, name } of components) {\n const filePath = path.join(\n baseDir,\n getModulePathFor(finalId).replace(/^\\//, \"\"),\n );\n await writeFileNormal(filePath, finalContent, context, name);\n }\n}\n\nfunction normalizeBase(base: string): string {\n const normalized = base.endsWith(\"/\") ? base.slice(0, -1) : base;\n return normalized === \"/\" ? \"\" : normalized;\n}\n\n/**\n * Replaces temporary IDs with final hashed IDs in content.\n */\nfunction replaceIdsInContent(\n content: string,\n idMapping: Map<string, string>,\n): string {\n let result = content;\n for (const [oldId, newId] of idMapping) {\n if (oldId !== newId) {\n result = result.replaceAll(oldId, newId);\n }\n }\n return result;\n}\n\nasync function buildSingleEntry(\n result: EntryBuildResult,\n idMapping: Map<string, string>,\n baseDir: string,\n base: string,\n rscPayloadDir: string,\n context: MinimalPluginContextWithoutEnvironment,\n) {\n const { path: entryPath, html, appRsc } = result;\n\n // Drain HTML stream to string\n const htmlContent = await drainStream(html);\n\n // Drain and process RSC payload: replace temp IDs with final hashed IDs\n const rawAppRscContent = await drainStream(appRsc);\n const appRscContent = replaceIdsInContent(rawAppRscContent, idMapping);\n\n // Compute hash for this entry's RSC payload\n const mainPayloadHash = await computeContentHash(appRscContent);\n const mainPayloadPath =\n base === \"\"\n ? getRscPayloadPath(mainPayloadHash, rscPayloadDir)\n : base + getRscPayloadPath(mainPayloadHash, rscPayloadDir);\n\n // Replace placeholder with final hashed path\n const finalHtmlContent = htmlContent.replaceAll(\n rscPayloadPlaceholder,\n mainPayloadPath,\n );\n\n // entryPath is already a file name (e.g. \"index.html\", \"about.html\")\n await writeFileNormal(\n path.join(baseDir, entryPath),\n finalHtmlContent,\n context,\n );\n\n // Write RSC payload with hashed filename\n await writeFileNormal(\n path.join(\n baseDir,\n getRscPayloadPath(mainPayloadHash, rscPayloadDir).replace(/^\\//, \"\"),\n ),\n appRscContent,\n context,\n );\n}\n\nasync function writeFileNormal(\n filePath: string,\n data: string,\n context: MinimalPluginContextWithoutEnvironment,\n name?: string,\n) {\n await mkdir(path.dirname(filePath), { recursive: true });\n const nameInfo = name ? ` (${name})` : \"\";\n context.info(`[funstack] Writing ${filePath}${nameInfo}`);\n await writeFile(filePath, data);\n}\n"],"mappings":";;;;;;;;;;AAYA,eAAsB,SACpB,SACA,SACA,SACA;CACA,MAAM,EAAE,WAAW;CAGnB,MAAM,QAAuC,MAAM,OACjD,cAFgB,KAAK,KAAK,OAAO,aAAa,IAAI,MAAM,QAAQ,WAAW,CAEnD,CAAC;CAG3B,MAAM,UAAU,OAAO,aAAa,OAAO,MAAM;CACjD,MAAM,OAAO,cAAc,OAAO,KAAK;CAEvC,MAAM,EAAE,SAAS,kBAAkB,MAAM,MAAM,OAAO;CAGtD,MAAM,QAAkB,EAAE;AAC1B,MAAK,MAAM,UAAU,SAAS;EAC5B,MAAM,QAAQ,kBAAkB,OAAO,KAAK;AAC5C,MAAI,MACF,OAAM,IAAI,MAAM,MAAM;AAExB,QAAM,KAAK,OAAO,KAAK;;CAEzB,MAAM,WAAW,oBAAoB,MAAM;AAC3C,KAAI,SACF,OAAM,IAAI,MAAM,SAAS;CAK3B,MAAM,cAAc,IAAI,eAA2B,EACjD,MAAM,YAAY;AAChB,aAAW,OAAO;IAErB,CAAC;CACF,MAAM,EAAE,YAAY,cAAc,MAAM,qBACtC,cAAc,SAAS,EACvB,aACA,QAAQ,eACR,QACD;AAGD,MAAK,MAAM,UAAU,QACnB,OAAM,iBACJ,QACA,WACA,SACA,MACA,QAAQ,eACR,QACD;AAIH,MAAK,MAAM,EAAE,SAAS,cAAc,UAAU,WAK5C,OAAM,gBAJW,KAAK,KACpB,SACA,iBAAiB,QAAQ,CAAC,QAAQ,OAAO,GAAG,CAC7C,EAC+B,cAAc,SAAS,KAAK;;AAIhE,SAAS,cAAc,MAAsB;CAC3C,MAAM,aAAa,KAAK,SAAS,IAAI,GAAG,KAAK,MAAM,GAAG,GAAG,GAAG;AAC5D,QAAO,eAAe,MAAM,KAAK;;;;;AAMnC,SAAS,oBACP,SACA,WACQ;CACR,IAAI,SAAS;AACb,MAAK,MAAM,CAAC,OAAO,UAAU,UAC3B,KAAI,UAAU,MACZ,UAAS,OAAO,WAAW,OAAO,MAAM;AAG5C,QAAO;;AAGT,eAAe,iBACb,QACA,WACA,SACA,MACA,eACA,SACA;CACA,MAAM,EAAE,MAAM,WAAW,MAAM,WAAW;CAG1C,MAAM,cAAc,MAAM,YAAY,KAAK;CAI3C,MAAM,gBAAgB,oBADG,MAAM,YAAY,OAAO,EACU,UAAU;CAGtE,MAAM,kBAAkB,MAAM,mBAAmB,cAAc;CAC/D,MAAM,kBACJ,SAAS,KACL,kBAAkB,iBAAiB,cAAc,GACjD,OAAO,kBAAkB,iBAAiB,cAAc;CAG9D,MAAM,mBAAmB,YAAY,WACnC,uBACA,gBACD;AAGD,OAAM,gBACJ,KAAK,KAAK,SAAS,UAAU,EAC7B,kBACA,QACD;AAGD,OAAM,gBACJ,KAAK,KACH,SACA,kBAAkB,iBAAiB,cAAc,CAAC,QAAQ,OAAO,GAAG,CACrE,EACD,eACA,QACD;;AAGH,eAAe,gBACb,UACA,MACA,SACA,MACA;AACA,OAAM,MAAM,KAAK,QAAQ,SAAS,EAAE,EAAE,WAAW,MAAM,CAAC;CACxD,MAAM,WAAW,OAAO,KAAK,KAAK,KAAK;AACvC,SAAQ,KAAK,sBAAsB,WAAW,WAAW;AACzD,OAAM,UAAU,UAAU,KAAK"}
|
|
@@ -8,7 +8,7 @@ async function computeContentHash(content) {
|
|
|
8
8
|
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
9
9
|
return Array.from(new Uint8Array(hashBuffer)).map((b) => b.toString(16).padStart(2, "0")).join("").slice(0, 16);
|
|
10
10
|
}
|
|
11
|
-
|
|
12
11
|
//#endregion
|
|
13
12
|
export { computeContentHash };
|
|
13
|
+
|
|
14
14
|
//# sourceMappingURL=contentHash.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dependencyGraph.mjs","names":[],"sources":["../../src/build/dependencyGraph.ts"],"sourcesContent":["/**\n * Result of topological sort.\n */\nexport interface SortResult {\n /** Components that can be processed in dependency order */\n sorted: string[];\n /** Components stuck in cycles (cannot determine stable order) */\n inCycle: string[];\n}\n\n/**\n * Finds which IDs from the known set are referenced in the given content.\n */\nexport function findReferencedIds(\n content: string,\n allKnownIds: Set<string>,\n): Set<string> {\n const referenced = new Set<string>();\n for (const id of allKnownIds) {\n if (content.includes(id)) {\n referenced.add(id);\n }\n }\n return referenced;\n}\n\n/**\n * Performs topological sort using Kahn's algorithm.\n * Returns both the sorted nodes and any nodes that are part of cycles.\n *\n * @param dependencies - Map of node ID to the set of IDs it depends on (references)\n */\nexport function topologicalSort(\n dependencies: Map<string, Set<string>>,\n): SortResult {\n const allNodes = new Set(dependencies.keys());\n\n // Calculate in-degree for each node (how many other nodes reference it)\n const inDegree = new Map<string, number>();\n for (const node of allNodes) {\n inDegree.set(node, 0);\n }\n\n for (const [_node, deps] of dependencies) {\n for (const dep of deps) {\n if (allNodes.has(dep)) {\n inDegree.set(dep, (inDegree.get(dep) ?? 0) + 1);\n }\n }\n }\n\n // Start with nodes that have in-degree 0 (no one references them)\n const queue: string[] = [];\n for (const [node, degree] of inDegree) {\n if (degree === 0) {\n queue.push(node);\n }\n }\n\n const sorted: string[] = [];\n\n while (queue.length > 0) {\n const node = queue.shift()!;\n sorted.push(node);\n\n // Decrement in-degree of nodes this node depends on\n const deps = dependencies.get(node);\n if (deps) {\n for (const dep of deps) {\n if (allNodes.has(dep)) {\n const newDegree = (inDegree.get(dep) ?? 1) - 1;\n inDegree.set(dep, newDegree);\n if (newDegree === 0) {\n queue.push(dep);\n }\n }\n }\n }\n }\n\n // Nodes not in sorted result are part of cycles\n const inCycle: string[] = [];\n for (const node of allNodes) {\n if (!sorted.includes(node)) {\n inCycle.push(node);\n }\n }\n\n return { sorted, inCycle };\n}\n"],"mappings":";;;;AAaA,SAAgB,kBACd,SACA,aACa;CACb,MAAM,6BAAa,IAAI,KAAa;AACpC,MAAK,MAAM,MAAM,YACf,KAAI,QAAQ,SAAS,GAAG,CACtB,YAAW,IAAI,GAAG;AAGtB,QAAO;;;;;;;;AAST,SAAgB,gBACd,cACY;CACZ,MAAM,WAAW,IAAI,IAAI,aAAa,MAAM,CAAC;CAG7C,MAAM,2BAAW,IAAI,KAAqB;AAC1C,MAAK,MAAM,QAAQ,SACjB,UAAS,IAAI,MAAM,EAAE;AAGvB,MAAK,MAAM,CAAC,OAAO,SAAS,aAC1B,MAAK,MAAM,OAAO,KAChB,KAAI,SAAS,IAAI,IAAI,CACnB,UAAS,IAAI,MAAM,SAAS,IAAI,IAAI,IAAI,KAAK,EAAE;CAMrD,MAAM,QAAkB,EAAE;AAC1B,MAAK,MAAM,CAAC,MAAM,WAAW,SAC3B,KAAI,WAAW,EACb,OAAM,KAAK,KAAK;CAIpB,MAAM,SAAmB,EAAE;AAE3B,QAAO,MAAM,SAAS,GAAG;EACvB,MAAM,OAAO,MAAM,OAAO;AAC1B,SAAO,KAAK,KAAK;EAGjB,MAAM,OAAO,aAAa,IAAI,KAAK;AACnC,MAAI
|
|
1
|
+
{"version":3,"file":"dependencyGraph.mjs","names":[],"sources":["../../src/build/dependencyGraph.ts"],"sourcesContent":["/**\n * Result of topological sort.\n */\nexport interface SortResult {\n /** Components that can be processed in dependency order */\n sorted: string[];\n /** Components stuck in cycles (cannot determine stable order) */\n inCycle: string[];\n}\n\n/**\n * Finds which IDs from the known set are referenced in the given content.\n */\nexport function findReferencedIds(\n content: string,\n allKnownIds: Set<string>,\n): Set<string> {\n const referenced = new Set<string>();\n for (const id of allKnownIds) {\n if (content.includes(id)) {\n referenced.add(id);\n }\n }\n return referenced;\n}\n\n/**\n * Performs topological sort using Kahn's algorithm.\n * Returns both the sorted nodes and any nodes that are part of cycles.\n *\n * @param dependencies - Map of node ID to the set of IDs it depends on (references)\n */\nexport function topologicalSort(\n dependencies: Map<string, Set<string>>,\n): SortResult {\n const allNodes = new Set(dependencies.keys());\n\n // Calculate in-degree for each node (how many other nodes reference it)\n const inDegree = new Map<string, number>();\n for (const node of allNodes) {\n inDegree.set(node, 0);\n }\n\n for (const [_node, deps] of dependencies) {\n for (const dep of deps) {\n if (allNodes.has(dep)) {\n inDegree.set(dep, (inDegree.get(dep) ?? 0) + 1);\n }\n }\n }\n\n // Start with nodes that have in-degree 0 (no one references them)\n const queue: string[] = [];\n for (const [node, degree] of inDegree) {\n if (degree === 0) {\n queue.push(node);\n }\n }\n\n const sorted: string[] = [];\n\n while (queue.length > 0) {\n const node = queue.shift()!;\n sorted.push(node);\n\n // Decrement in-degree of nodes this node depends on\n const deps = dependencies.get(node);\n if (deps) {\n for (const dep of deps) {\n if (allNodes.has(dep)) {\n const newDegree = (inDegree.get(dep) ?? 1) - 1;\n inDegree.set(dep, newDegree);\n if (newDegree === 0) {\n queue.push(dep);\n }\n }\n }\n }\n }\n\n // Nodes not in sorted result are part of cycles\n const inCycle: string[] = [];\n for (const node of allNodes) {\n if (!sorted.includes(node)) {\n inCycle.push(node);\n }\n }\n\n return { sorted, inCycle };\n}\n"],"mappings":";;;;AAaA,SAAgB,kBACd,SACA,aACa;CACb,MAAM,6BAAa,IAAI,KAAa;AACpC,MAAK,MAAM,MAAM,YACf,KAAI,QAAQ,SAAS,GAAG,CACtB,YAAW,IAAI,GAAG;AAGtB,QAAO;;;;;;;;AAST,SAAgB,gBACd,cACY;CACZ,MAAM,WAAW,IAAI,IAAI,aAAa,MAAM,CAAC;CAG7C,MAAM,2BAAW,IAAI,KAAqB;AAC1C,MAAK,MAAM,QAAQ,SACjB,UAAS,IAAI,MAAM,EAAE;AAGvB,MAAK,MAAM,CAAC,OAAO,SAAS,aAC1B,MAAK,MAAM,OAAO,KAChB,KAAI,SAAS,IAAI,IAAI,CACnB,UAAS,IAAI,MAAM,SAAS,IAAI,IAAI,IAAI,KAAK,EAAE;CAMrD,MAAM,QAAkB,EAAE;AAC1B,MAAK,MAAM,CAAC,MAAM,WAAW,SAC3B,KAAI,WAAW,EACb,OAAM,KAAK,KAAK;CAIpB,MAAM,SAAmB,EAAE;AAE3B,QAAO,MAAM,SAAS,GAAG;EACvB,MAAM,OAAO,MAAM,OAAO;AAC1B,SAAO,KAAK,KAAK;EAGjB,MAAM,OAAO,aAAa,IAAI,KAAK;AACnC,MAAI;QACG,MAAM,OAAO,KAChB,KAAI,SAAS,IAAI,IAAI,EAAE;IACrB,MAAM,aAAa,SAAS,IAAI,IAAI,IAAI,KAAK;AAC7C,aAAS,IAAI,KAAK,UAAU;AAC5B,QAAI,cAAc,EAChB,OAAM,KAAK,IAAI;;;;CAQzB,MAAM,UAAoB,EAAE;AAC5B,MAAK,MAAM,QAAQ,SACjB,KAAI,CAAC,OAAO,SAAS,KAAK,CACxB,SAAQ,KAAK,KAAK;AAItB,QAAO;EAAE;EAAQ;EAAS"}
|
package/dist/build/rscPath.mjs
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { getModulePathFor, getPayloadIDFor } from "../rsc/rscModule.mjs";
|
|
2
|
-
|
|
3
2
|
//#region src/build/rscPath.ts
|
|
4
3
|
/**
|
|
5
4
|
* Placeholder used during SSR (will be replaced after hash is computed)
|
|
@@ -8,10 +7,10 @@ const rscPayloadPlaceholder = "__FUNSTACK_RSC_PAYLOAD_PATH__";
|
|
|
8
7
|
/**
|
|
9
8
|
* Generate final path from content hash (reuses same folder as deferred payloads)
|
|
10
9
|
*/
|
|
11
|
-
function getRscPayloadPath(contentHash) {
|
|
12
|
-
return getModulePathFor(getPayloadIDFor(contentHash));
|
|
10
|
+
function getRscPayloadPath(contentHash, rscPayloadDir) {
|
|
11
|
+
return getModulePathFor(getPayloadIDFor(contentHash, rscPayloadDir));
|
|
13
12
|
}
|
|
14
|
-
|
|
15
13
|
//#endregion
|
|
16
14
|
export { getRscPayloadPath, rscPayloadPlaceholder };
|
|
15
|
+
|
|
17
16
|
//# sourceMappingURL=rscPath.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rscPath.mjs","names":[],"sources":["../../src/build/rscPath.ts"],"sourcesContent":["import { getModulePathFor, getPayloadIDFor } from \"../rsc/rscModule\";\n\n/**\n * Placeholder used during SSR (will be replaced after hash is computed)\n */\nexport const rscPayloadPlaceholder = \"__FUNSTACK_RSC_PAYLOAD_PATH__\";\n\n/**\n * Generate final path from content hash (reuses same folder as deferred payloads)\n */\nexport function getRscPayloadPath(contentHash: string): string {\n return getModulePathFor(getPayloadIDFor(contentHash));\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"rscPath.mjs","names":[],"sources":["../../src/build/rscPath.ts"],"sourcesContent":["import { getModulePathFor, getPayloadIDFor } from \"../rsc/rscModule\";\n\n/**\n * Placeholder used during SSR (will be replaced after hash is computed)\n */\nexport const rscPayloadPlaceholder = \"__FUNSTACK_RSC_PAYLOAD_PATH__\";\n\n/**\n * Generate final path from content hash (reuses same folder as deferred payloads)\n */\nexport function getRscPayloadPath(\n contentHash: string,\n rscPayloadDir: string,\n): string {\n return getModulePathFor(getPayloadIDFor(contentHash, rscPayloadDir));\n}\n"],"mappings":";;;;;AAKA,MAAa,wBAAwB;;;;AAKrC,SAAgB,kBACd,aACA,eACQ;AACR,QAAO,iBAAiB,gBAAgB,aAAa,cAAc,CAAC"}
|
|
@@ -2,16 +2,16 @@ import { getPayloadIDFor } from "../rsc/rscModule.mjs";
|
|
|
2
2
|
import { drainStream } from "../util/drainStream.mjs";
|
|
3
3
|
import { computeContentHash } from "./contentHash.mjs";
|
|
4
4
|
import { findReferencedIds, topologicalSort } from "./dependencyGraph.mjs";
|
|
5
|
-
|
|
6
5
|
//#region src/build/rscProcessor.ts
|
|
7
6
|
/**
|
|
8
7
|
* Processes RSC components by replacing temporary UUIDs with content-based hashes.
|
|
9
8
|
*
|
|
10
9
|
* @param deferRegistryIterator - Iterator yielding components with { id, data }
|
|
11
10
|
* @param appRscStream - The main RSC stream
|
|
11
|
+
* @param rscPayloadDir - Directory name used as a prefix for RSC payload IDs (e.g. "fun:rsc-payload")
|
|
12
12
|
* @param context - Optional context for logging warnings
|
|
13
13
|
*/
|
|
14
|
-
async function processRscComponents(deferRegistryIterator, appRscStream, context) {
|
|
14
|
+
async function processRscComponents(deferRegistryIterator, appRscStream, rscPayloadDir, context) {
|
|
15
15
|
const components = /* @__PURE__ */ new Map();
|
|
16
16
|
const componentNames = /* @__PURE__ */ new Map();
|
|
17
17
|
for await (const { id, data, name } of deferRegistryIterator) {
|
|
@@ -42,7 +42,7 @@ async function processRscComponents(deferRegistryIterator, appRscStream, context
|
|
|
42
42
|
for (const tempId of sorted) {
|
|
43
43
|
let content = components.get(tempId);
|
|
44
44
|
for (const [oldId, newId] of idMapping) if (oldId !== newId) content = content.replaceAll(oldId, newId);
|
|
45
|
-
const finalId = getPayloadIDFor(await computeContentHash(content));
|
|
45
|
+
const finalId = getPayloadIDFor(await computeContentHash(content), rscPayloadDir);
|
|
46
46
|
idMapping.set(tempId, finalId);
|
|
47
47
|
processedComponents.push({
|
|
48
48
|
finalId,
|
|
@@ -66,7 +66,7 @@ async function processRscComponents(deferRegistryIterator, appRscStream, context
|
|
|
66
66
|
idMapping
|
|
67
67
|
};
|
|
68
68
|
}
|
|
69
|
-
|
|
70
69
|
//#endregion
|
|
71
70
|
export { processRscComponents };
|
|
71
|
+
|
|
72
72
|
//# sourceMappingURL=rscProcessor.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rscProcessor.mjs","names":[],"sources":["../../src/build/rscProcessor.ts"],"sourcesContent":["import { drainStream } from \"../util/drainStream\";\nimport { getPayloadIDFor } from \"../rsc/rscModule\";\nimport { computeContentHash } from \"./contentHash\";\nimport { findReferencedIds, topologicalSort } from \"./dependencyGraph\";\n\nexport interface ProcessedComponent {\n finalId: string;\n finalContent: string;\n name?: string;\n}\n\nexport interface ProcessResult {\n components: ProcessedComponent[];\n appRscContent: string;\n idMapping: Map<string, string>;\n}\n\ninterface RawComponent {\n id: string;\n data: string;\n name?: string;\n}\n\n/**\n * Processes RSC components by replacing temporary UUIDs with content-based hashes.\n *\n * @param deferRegistryIterator - Iterator yielding components with { id, data }\n * @param appRscStream - The main RSC stream\n * @param context - Optional context for logging warnings\n */\nexport async function processRscComponents(\n deferRegistryIterator: AsyncIterable<RawComponent>,\n appRscStream: ReadableStream,\n context?: { warn: (message: string) => void },\n): Promise<ProcessResult> {\n // Step 1: Collect all components from deferRegistry\n const components = new Map<string, string>();\n const componentNames = new Map<string, string | undefined>();\n for await (const { id, data, name } of deferRegistryIterator) {\n components.set(id, data);\n componentNames.set(id, name);\n }\n\n // Step 2: Drain appRsc stream to string\n let appRscContent = await drainStream(appRscStream);\n\n // If no components, return early\n if (components.size === 0) {\n return {\n components: [],\n appRscContent,\n idMapping: new Map(),\n };\n }\n\n const allIds = new Set(components.keys());\n\n // Step 3: Build dependency graph\n // For each component, find which other component IDs appear in its content\n const dependencies = new Map<string, Set<string>>();\n for (const [id, content] of components) {\n const otherIds = new Set(allIds);\n otherIds.delete(id); // Don't include self-references\n const refs = findReferencedIds(content, otherIds);\n dependencies.set(id, refs);\n }\n\n // Step 4: Topologically sort components\n const { sorted, inCycle } = topologicalSort(dependencies);\n\n // Step 5: Handle cycles - warn and keep original temp IDs\n const idMapping = new Map<string, string>();\n\n if (inCycle.length > 0) {\n context?.warn(\n `[funstack] Warning: ${inCycle.length} RSC component(s) are in dependency cycles and will keep unstable IDs: ${inCycle.join(\", \")}`,\n );\n for (const id of inCycle) {\n idMapping.set(id, id); // Map to itself (keep original ID)\n }\n }\n\n // Step 6: Process sorted components in order\n const processedComponents: ProcessedComponent[] = [];\n\n for (const tempId of sorted) {\n let content = components.get(tempId)!;\n\n // Replace all already-finalized temp IDs with their hash-based IDs\n for (const [oldId, newId] of idMapping) {\n if (oldId !== newId) {\n content = content.replaceAll(oldId, newId);\n }\n }\n\n // Compute content hash for this component\n const contentHash = await computeContentHash(content);\n const finalId = getPayloadIDFor(contentHash);\n\n // Create mapping\n idMapping.set(tempId, finalId);\n\n processedComponents.push({\n finalId,\n finalContent: content,\n name: componentNames.get(tempId),\n });\n }\n\n // Add cycle members to processed components (with original IDs)\n for (const tempId of inCycle) {\n let content = components.get(tempId)!;\n\n // Replace finalized IDs in cycle member content\n for (const [oldId, newId] of idMapping) {\n if (oldId !== newId) {\n content = content.replaceAll(oldId, newId);\n }\n }\n\n processedComponents.push({\n finalId: tempId, // Keep original temp ID\n finalContent: content,\n name: componentNames.get(tempId),\n });\n }\n\n // Step 7: Process appRsc - replace all temp IDs with final IDs\n for (const [oldId, newId] of idMapping) {\n if (oldId !== newId) {\n appRscContent = appRscContent.replaceAll(oldId, newId);\n }\n }\n\n return {\n components: processedComponents,\n appRscContent,\n idMapping,\n };\n}\n"],"mappings":";;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"rscProcessor.mjs","names":[],"sources":["../../src/build/rscProcessor.ts"],"sourcesContent":["import { drainStream } from \"../util/drainStream\";\nimport { getPayloadIDFor } from \"../rsc/rscModule\";\nimport { computeContentHash } from \"./contentHash\";\nimport { findReferencedIds, topologicalSort } from \"./dependencyGraph\";\n\nexport interface ProcessedComponent {\n finalId: string;\n finalContent: string;\n name?: string;\n}\n\nexport interface ProcessResult {\n components: ProcessedComponent[];\n appRscContent: string;\n idMapping: Map<string, string>;\n}\n\ninterface RawComponent {\n id: string;\n data: string;\n name?: string;\n}\n\n/**\n * Processes RSC components by replacing temporary UUIDs with content-based hashes.\n *\n * @param deferRegistryIterator - Iterator yielding components with { id, data }\n * @param appRscStream - The main RSC stream\n * @param rscPayloadDir - Directory name used as a prefix for RSC payload IDs (e.g. \"fun:rsc-payload\")\n * @param context - Optional context for logging warnings\n */\nexport async function processRscComponents(\n deferRegistryIterator: AsyncIterable<RawComponent>,\n appRscStream: ReadableStream,\n rscPayloadDir: string,\n context?: { warn: (message: string) => void },\n): Promise<ProcessResult> {\n // Step 1: Collect all components from deferRegistry\n const components = new Map<string, string>();\n const componentNames = new Map<string, string | undefined>();\n for await (const { id, data, name } of deferRegistryIterator) {\n components.set(id, data);\n componentNames.set(id, name);\n }\n\n // Step 2: Drain appRsc stream to string\n let appRscContent = await drainStream(appRscStream);\n\n // If no components, return early\n if (components.size === 0) {\n return {\n components: [],\n appRscContent,\n idMapping: new Map(),\n };\n }\n\n const allIds = new Set(components.keys());\n\n // Step 3: Build dependency graph\n // For each component, find which other component IDs appear in its content\n const dependencies = new Map<string, Set<string>>();\n for (const [id, content] of components) {\n const otherIds = new Set(allIds);\n otherIds.delete(id); // Don't include self-references\n const refs = findReferencedIds(content, otherIds);\n dependencies.set(id, refs);\n }\n\n // Step 4: Topologically sort components\n const { sorted, inCycle } = topologicalSort(dependencies);\n\n // Step 5: Handle cycles - warn and keep original temp IDs\n const idMapping = new Map<string, string>();\n\n if (inCycle.length > 0) {\n context?.warn(\n `[funstack] Warning: ${inCycle.length} RSC component(s) are in dependency cycles and will keep unstable IDs: ${inCycle.join(\", \")}`,\n );\n for (const id of inCycle) {\n idMapping.set(id, id); // Map to itself (keep original ID)\n }\n }\n\n // Step 6: Process sorted components in order\n const processedComponents: ProcessedComponent[] = [];\n\n for (const tempId of sorted) {\n let content = components.get(tempId)!;\n\n // Replace all already-finalized temp IDs with their hash-based IDs\n for (const [oldId, newId] of idMapping) {\n if (oldId !== newId) {\n content = content.replaceAll(oldId, newId);\n }\n }\n\n // Compute content hash for this component\n const contentHash = await computeContentHash(content);\n const finalId = getPayloadIDFor(contentHash, rscPayloadDir);\n\n // Create mapping\n idMapping.set(tempId, finalId);\n\n processedComponents.push({\n finalId,\n finalContent: content,\n name: componentNames.get(tempId),\n });\n }\n\n // Add cycle members to processed components (with original IDs)\n for (const tempId of inCycle) {\n let content = components.get(tempId)!;\n\n // Replace finalized IDs in cycle member content\n for (const [oldId, newId] of idMapping) {\n if (oldId !== newId) {\n content = content.replaceAll(oldId, newId);\n }\n }\n\n processedComponents.push({\n finalId: tempId, // Keep original temp ID\n finalContent: content,\n name: componentNames.get(tempId),\n });\n }\n\n // Step 7: Process appRsc - replace all temp IDs with final IDs\n for (const [oldId, newId] of idMapping) {\n if (oldId !== newId) {\n appRscContent = appRscContent.replaceAll(oldId, newId);\n }\n }\n\n return {\n components: processedComponents,\n appRscContent,\n idMapping,\n };\n}\n"],"mappings":";;;;;;;;;;;;;AA+BA,eAAsB,qBACpB,uBACA,cACA,eACA,SACwB;CAExB,MAAM,6BAAa,IAAI,KAAqB;CAC5C,MAAM,iCAAiB,IAAI,KAAiC;AAC5D,YAAW,MAAM,EAAE,IAAI,MAAM,UAAU,uBAAuB;AAC5D,aAAW,IAAI,IAAI,KAAK;AACxB,iBAAe,IAAI,IAAI,KAAK;;CAI9B,IAAI,gBAAgB,MAAM,YAAY,aAAa;AAGnD,KAAI,WAAW,SAAS,EACtB,QAAO;EACL,YAAY,EAAE;EACd;EACA,2BAAW,IAAI,KAAK;EACrB;CAGH,MAAM,SAAS,IAAI,IAAI,WAAW,MAAM,CAAC;CAIzC,MAAM,+BAAe,IAAI,KAA0B;AACnD,MAAK,MAAM,CAAC,IAAI,YAAY,YAAY;EACtC,MAAM,WAAW,IAAI,IAAI,OAAO;AAChC,WAAS,OAAO,GAAG;EACnB,MAAM,OAAO,kBAAkB,SAAS,SAAS;AACjD,eAAa,IAAI,IAAI,KAAK;;CAI5B,MAAM,EAAE,QAAQ,YAAY,gBAAgB,aAAa;CAGzD,MAAM,4BAAY,IAAI,KAAqB;AAE3C,KAAI,QAAQ,SAAS,GAAG;AACtB,WAAS,KACP,uBAAuB,QAAQ,OAAO,yEAAyE,QAAQ,KAAK,KAAK,GAClI;AACD,OAAK,MAAM,MAAM,QACf,WAAU,IAAI,IAAI,GAAG;;CAKzB,MAAM,sBAA4C,EAAE;AAEpD,MAAK,MAAM,UAAU,QAAQ;EAC3B,IAAI,UAAU,WAAW,IAAI,OAAO;AAGpC,OAAK,MAAM,CAAC,OAAO,UAAU,UAC3B,KAAI,UAAU,MACZ,WAAU,QAAQ,WAAW,OAAO,MAAM;EAM9C,MAAM,UAAU,gBADI,MAAM,mBAAmB,QAAQ,EACR,cAAc;AAG3D,YAAU,IAAI,QAAQ,QAAQ;AAE9B,sBAAoB,KAAK;GACvB;GACA,cAAc;GACd,MAAM,eAAe,IAAI,OAAO;GACjC,CAAC;;AAIJ,MAAK,MAAM,UAAU,SAAS;EAC5B,IAAI,UAAU,WAAW,IAAI,OAAO;AAGpC,OAAK,MAAM,CAAC,OAAO,UAAU,UAC3B,KAAI,UAAU,MACZ,WAAU,QAAQ,WAAW,OAAO,MAAM;AAI9C,sBAAoB,KAAK;GACvB,SAAS;GACT,cAAc;GACd,MAAM,eAAe,IAAI,OAAO;GACjC,CAAC;;AAIJ,MAAK,MAAM,CAAC,OAAO,UAAU,UAC3B,KAAI,UAAU,MACZ,iBAAgB,cAAc,WAAW,OAAO,MAAM;AAI1D,QAAO;EACL,YAAY;EACZ;EACA;EACD"}
|
package/dist/client/entry.d.mts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
export { };
|
package/dist/client/entry.mjs
CHANGED
|
@@ -9,7 +9,6 @@ import { createRoot, hydrateRoot } from "react-dom/client";
|
|
|
9
9
|
import { rscStream } from "rsc-html-stream/client";
|
|
10
10
|
import { jsx } from "react/jsx-runtime";
|
|
11
11
|
import { ssr } from "virtual:funstack/config";
|
|
12
|
-
|
|
13
12
|
//#region src/client/entry.tsx
|
|
14
13
|
async function devMain() {
|
|
15
14
|
let setPayload;
|
|
@@ -55,7 +54,7 @@ async function prodMain() {
|
|
|
55
54
|
}
|
|
56
55
|
if (import.meta.env.DEV) devMain();
|
|
57
56
|
else prodMain();
|
|
58
|
-
|
|
59
57
|
//#endregion
|
|
60
|
-
export {
|
|
58
|
+
export {};
|
|
59
|
+
|
|
61
60
|
//# sourceMappingURL=entry.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"entry.mjs","names":["ssrEnabled"],"sources":["../../src/client/entry.tsx"],"sourcesContent":["// Client initialization - runs before React (side effects only)\nimport \"virtual:funstack/client-init\";\n\nimport {\n createFromReadableStream,\n createFromFetch,\n} from \"@vitejs/plugin-rsc/browser\";\nimport React, { startTransition, useEffect, useState } from \"react\";\nimport { createRoot, hydrateRoot } from \"react-dom/client\";\nimport { rscStream } from \"rsc-html-stream/client\";\nimport { GlobalErrorBoundary } from \"./error-boundary\";\nimport type { RscPayload } from \"../rsc/entry\";\nimport { devMainRscPath } from \"../rsc/request\";\nimport { appClientManifestVar, type AppClientManifest } from \"./globals\";\nimport { withBasePath } from \"../util/basePath\";\n\nimport { ssr as ssrEnabled } from \"virtual:funstack/config\";\n\nasync function devMain() {\n let setPayload: (v: RscPayload) => void;\n\n const initialPayload = await createFromReadableStream<RscPayload>(rscStream);\n\n function BrowserRoot() {\n const [payload, setPayload_] = useState(initialPayload);\n\n useEffect(() => {\n setPayload = (v) => startTransition(() => setPayload_(v));\n }, [setPayload_]);\n\n return payload.root;\n }\n\n // re-fetch RSC and trigger re-rendering\n async function fetchRscPayload() {\n const payload = await createFromFetch<RscPayload>(\n fetch(withBasePath(devMainRscPath)),\n );\n setPayload(payload);\n }\n\n const browserRoot = (\n <React.StrictMode>\n <GlobalErrorBoundary>\n <BrowserRoot />\n </GlobalErrorBoundary>\n </React.StrictMode>\n );\n\n if (\n // @ts-expect-error\n globalThis.__NO_HYDRATE\n ) {\n // This happens when SSR failed on server\n createRoot(document).render(browserRoot);\n } else if (ssrEnabled) {\n hydrateRoot(document, browserRoot);\n } else {\n // SSR off: Root shell is static HTML, mount App client-side\n createRoot(document).render(browserRoot);\n }\n\n // implement server HMR by triggering re-fetch/render of RSC upon server code change\n if (import.meta.hot) {\n import.meta.hot.on(\"rsc:update\", () => {\n fetchRscPayload();\n });\n }\n}\n\nasync function prodMain() {\n const manifest: AppClientManifest =\n // @ts-expect-error\n globalThis[appClientManifestVar];\n\n const payload = await createFromFetch<RscPayload>(fetch(manifest.stream));\n\n function BrowserRoot() {\n return payload.root;\n }\n\n if (ssrEnabled) {\n // SSR on: full tree was SSR'd, hydrate from RSC payload\n const browserRoot = (\n <React.StrictMode>\n <GlobalErrorBoundary>\n <BrowserRoot />\n </GlobalErrorBoundary>\n </React.StrictMode>\n );\n\n hydrateRoot(document, browserRoot);\n } else {\n // SSR off: Root shell only, mount App client-side\n const browserRoot = <BrowserRoot />;\n const appRootId = manifest.marker!;\n\n const appMarker = document.getElementById(appRootId);\n if (!appMarker) {\n throw new Error(\n `Failed to find app root element by id \"${appRootId}\". This is likely a bug.`,\n );\n }\n const appRoot = appMarker.parentElement;\n if (!appRoot) {\n throw new Error(\n `App root element has no parent element. This is likely a bug.`,\n );\n }\n appMarker.remove();\n\n createRoot(appRoot).render(browserRoot);\n }\n}\n\nif (import.meta.env.DEV) {\n devMain();\n} else {\n prodMain();\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"entry.mjs","names":["ssrEnabled"],"sources":["../../src/client/entry.tsx"],"sourcesContent":["// Client initialization - runs before React (side effects only)\nimport \"virtual:funstack/client-init\";\n\nimport {\n createFromReadableStream,\n createFromFetch,\n} from \"@vitejs/plugin-rsc/browser\";\nimport React, { startTransition, useEffect, useState } from \"react\";\nimport { createRoot, hydrateRoot } from \"react-dom/client\";\nimport { rscStream } from \"rsc-html-stream/client\";\nimport { GlobalErrorBoundary } from \"./error-boundary\";\nimport type { RscPayload } from \"../rsc/entry\";\nimport { devMainRscPath } from \"../rsc/request\";\nimport { appClientManifestVar, type AppClientManifest } from \"./globals\";\nimport { withBasePath } from \"../util/basePath\";\n\nimport { ssr as ssrEnabled } from \"virtual:funstack/config\";\n\nasync function devMain() {\n let setPayload: (v: RscPayload) => void;\n\n const initialPayload = await createFromReadableStream<RscPayload>(rscStream);\n\n function BrowserRoot() {\n const [payload, setPayload_] = useState(initialPayload);\n\n useEffect(() => {\n setPayload = (v) => startTransition(() => setPayload_(v));\n }, [setPayload_]);\n\n return payload.root;\n }\n\n // re-fetch RSC and trigger re-rendering\n async function fetchRscPayload() {\n const payload = await createFromFetch<RscPayload>(\n fetch(withBasePath(devMainRscPath)),\n );\n setPayload(payload);\n }\n\n const browserRoot = (\n <React.StrictMode>\n <GlobalErrorBoundary>\n <BrowserRoot />\n </GlobalErrorBoundary>\n </React.StrictMode>\n );\n\n if (\n // @ts-expect-error\n globalThis.__NO_HYDRATE\n ) {\n // This happens when SSR failed on server\n createRoot(document).render(browserRoot);\n } else if (ssrEnabled) {\n hydrateRoot(document, browserRoot);\n } else {\n // SSR off: Root shell is static HTML, mount App client-side\n createRoot(document).render(browserRoot);\n }\n\n // implement server HMR by triggering re-fetch/render of RSC upon server code change\n if (import.meta.hot) {\n import.meta.hot.on(\"rsc:update\", () => {\n fetchRscPayload();\n });\n }\n}\n\nasync function prodMain() {\n const manifest: AppClientManifest =\n // @ts-expect-error\n globalThis[appClientManifestVar];\n\n const payload = await createFromFetch<RscPayload>(fetch(manifest.stream));\n\n function BrowserRoot() {\n return payload.root;\n }\n\n if (ssrEnabled) {\n // SSR on: full tree was SSR'd, hydrate from RSC payload\n const browserRoot = (\n <React.StrictMode>\n <GlobalErrorBoundary>\n <BrowserRoot />\n </GlobalErrorBoundary>\n </React.StrictMode>\n );\n\n hydrateRoot(document, browserRoot);\n } else {\n // SSR off: Root shell only, mount App client-side\n const browserRoot = <BrowserRoot />;\n const appRootId = manifest.marker!;\n\n const appMarker = document.getElementById(appRootId);\n if (!appMarker) {\n throw new Error(\n `Failed to find app root element by id \"${appRootId}\". This is likely a bug.`,\n );\n }\n const appRoot = appMarker.parentElement;\n if (!appRoot) {\n throw new Error(\n `App root element has no parent element. This is likely a bug.`,\n );\n }\n appMarker.remove();\n\n createRoot(appRoot).render(browserRoot);\n }\n}\n\nif (import.meta.env.DEV) {\n devMain();\n} else {\n prodMain();\n}\n"],"mappings":";;;;;;;;;;;;AAkBA,eAAe,UAAU;CACvB,IAAI;CAEJ,MAAM,iBAAiB,MAAM,yBAAqC,UAAU;CAE5E,SAAS,cAAc;EACrB,MAAM,CAAC,SAAS,eAAe,SAAS,eAAe;AAEvD,kBAAgB;AACd,iBAAc,MAAM,sBAAsB,YAAY,EAAE,CAAC;KACxD,CAAC,YAAY,CAAC;AAEjB,SAAO,QAAQ;;CAIjB,eAAe,kBAAkB;EAC/B,MAAM,UAAU,MAAM,gBACpB,MAAM,aAAa,eAAe,CAAC,CACpC;AACD,aAAW,QAAQ;;CAGrB,MAAM,cACJ,oBAAC,MAAM,YAAP,EAAA,UACE,oBAAC,qBAAD,EAAA,UACE,oBAAC,aAAD,EAAe,CAAA,EACK,CAAA,EACL,CAAA;AAGrB,KAEE,WAAW,aAGX,YAAW,SAAS,CAAC,OAAO,YAAY;UAC/BA,IACT,aAAY,UAAU,YAAY;KAGlC,YAAW,SAAS,CAAC,OAAO,YAAY;AAI1C,KAAI,OAAO,KAAK,IACd,QAAO,KAAK,IAAI,GAAG,oBAAoB;AACrC,mBAAiB;GACjB;;AAIN,eAAe,WAAW;CACxB,MAAM,WAEJ,WAAW;CAEb,MAAM,UAAU,MAAM,gBAA4B,MAAM,SAAS,OAAO,CAAC;CAEzE,SAAS,cAAc;AACrB,SAAO,QAAQ;;AAGjB,KAAIA,KAAY;EAEd,MAAM,cACJ,oBAAC,MAAM,YAAP,EAAA,UACE,oBAAC,qBAAD,EAAA,UACE,oBAAC,aAAD,EAAe,CAAA,EACK,CAAA,EACL,CAAA;AAGrB,cAAY,UAAU,YAAY;QAC7B;EAEL,MAAM,cAAc,oBAAC,aAAD,EAAe,CAAA;EACnC,MAAM,YAAY,SAAS;EAE3B,MAAM,YAAY,SAAS,eAAe,UAAU;AACpD,MAAI,CAAC,UACH,OAAM,IAAI,MACR,0CAA0C,UAAU,0BACrD;EAEH,MAAM,UAAU,UAAU;AAC1B,MAAI,CAAC,QACH,OAAM,IAAI,MACR,gEACD;AAEH,YAAU,QAAQ;AAElB,aAAW,QAAQ,CAAC,OAAO,YAAY;;;AAI3C,IAAI,OAAO,KAAK,IAAI,IAClB,UAAS;IAET,WAAU"}
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
|
|
3
|
-
import React, { startTransition } from "react";
|
|
2
|
+
import { startTransition } from "react";
|
|
4
3
|
import { ErrorBoundary } from "react-error-boundary";
|
|
5
4
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
-
|
|
7
5
|
//#region src/client/error-boundary.tsx
|
|
8
6
|
/**
|
|
9
7
|
* Whole-page error boundary for unexpected errors during development
|
|
@@ -42,7 +40,7 @@ const Fallback = ({ error, resetErrorBoundary }) => {
|
|
|
42
40
|
]
|
|
43
41
|
})] });
|
|
44
42
|
};
|
|
45
|
-
|
|
46
43
|
//#endregion
|
|
47
44
|
export { GlobalErrorBoundary };
|
|
45
|
+
|
|
48
46
|
//# sourceMappingURL=error-boundary.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"error-boundary.mjs","names":[],"sources":["../../src/client/error-boundary.tsx"],"sourcesContent":["\"use client\";\n\nimport React, { startTransition } from \"react\";\nimport { ErrorBoundary, type FallbackProps } from \"react-error-boundary\";\n\n/**\n * Whole-page error boundary for unexpected errors during development\n */\nexport const GlobalErrorBoundary: React.FC<React.PropsWithChildren> = (\n props,\n) => {\n return (\n <ErrorBoundary FallbackComponent={Fallback}>{props.children}</ErrorBoundary>\n );\n};\n\nconst Fallback: React.FC<FallbackProps> = ({ error, resetErrorBoundary }) => {\n const errorMessage = error instanceof Error ? error.message : String(error);\n return (\n <html>\n <head>\n <title>Unexpected Error</title>\n </head>\n <body\n style={{\n height: \"100vh\",\n display: \"flex\",\n flexDirection: \"column\",\n placeContent: \"center\",\n placeItems: \"center\",\n fontSize: \"24px\",\n fontWeight: 400,\n lineHeight: \"1.5em\",\n }}\n >\n <h1>Caught an unexpected error</h1>\n <p>See the console for details.</p>\n <pre>Error: {errorMessage}</pre>\n <button\n onClick={() => {\n startTransition(() => {\n resetErrorBoundary();\n });\n }}\n >\n Reset\n </button>\n </body>\n </html>\n );\n};\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"error-boundary.mjs","names":[],"sources":["../../src/client/error-boundary.tsx"],"sourcesContent":["\"use client\";\n\nimport React, { startTransition } from \"react\";\nimport { ErrorBoundary, type FallbackProps } from \"react-error-boundary\";\n\n/**\n * Whole-page error boundary for unexpected errors during development\n */\nexport const GlobalErrorBoundary: React.FC<React.PropsWithChildren> = (\n props,\n) => {\n return (\n <ErrorBoundary FallbackComponent={Fallback}>{props.children}</ErrorBoundary>\n );\n};\n\nconst Fallback: React.FC<FallbackProps> = ({ error, resetErrorBoundary }) => {\n const errorMessage = error instanceof Error ? error.message : String(error);\n return (\n <html>\n <head>\n <title>Unexpected Error</title>\n </head>\n <body\n style={{\n height: \"100vh\",\n display: \"flex\",\n flexDirection: \"column\",\n placeContent: \"center\",\n placeItems: \"center\",\n fontSize: \"24px\",\n fontWeight: 400,\n lineHeight: \"1.5em\",\n }}\n >\n <h1>Caught an unexpected error</h1>\n <p>See the console for details.</p>\n <pre>Error: {errorMessage}</pre>\n <button\n onClick={() => {\n startTransition(() => {\n resetErrorBoundary();\n });\n }}\n >\n Reset\n </button>\n </body>\n </html>\n );\n};\n"],"mappings":";;;;;;;;AAQA,MAAa,uBACX,UACG;AACH,QACE,oBAAC,eAAD;EAAe,mBAAmB;YAAW,MAAM;EAAyB,CAAA;;AAIhF,MAAM,YAAqC,EAAE,OAAO,yBAAyB;CAC3E,MAAM,eAAe,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM;AAC3E,QACE,qBAAC,QAAD,EAAA,UAAA,CACE,oBAAC,QAAD,EAAA,UACE,oBAAC,SAAD,EAAA,UAAO,oBAAwB,CAAA,EAC1B,CAAA,EACP,qBAAC,QAAD;EACE,OAAO;GACL,QAAQ;GACR,SAAS;GACT,eAAe;GACf,cAAc;GACd,YAAY;GACZ,UAAU;GACV,YAAY;GACZ,YAAY;GACb;YAVH;GAYE,oBAAC,MAAD,EAAA,UAAI,8BAA+B,CAAA;GACnC,oBAAC,KAAD,EAAA,UAAG,gCAAgC,CAAA;GACnC,qBAAC,OAAD,EAAA,UAAA,CAAK,WAAQ,aAAmB,EAAA,CAAA;GAChC,oBAAC,UAAD;IACE,eAAe;AACb,2BAAsB;AACpB,0BAAoB;OACpB;;cAEL;IAEQ,CAAA;GACJ;IACF,EAAA,CAAA"}
|
package/dist/client/globals.mjs
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
//#region src/client/globals.ts
|
|
2
|
-
const globalPrefix = "FUNSTACK_STATIC_";
|
|
3
1
|
/**
|
|
4
2
|
* Variable name for the app client manifest
|
|
5
3
|
*/
|
|
6
|
-
const appClientManifestVar =
|
|
7
|
-
|
|
4
|
+
const appClientManifestVar = `FUNSTACK_STATIC_appClientManifest`;
|
|
8
5
|
//#endregion
|
|
9
6
|
export { appClientManifestVar };
|
|
7
|
+
|
|
10
8
|
//# sourceMappingURL=globals.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"globals.mjs","names":[],"sources":["../../src/client/globals.ts"],"sourcesContent":["// Prefix for global variables\nconst globalPrefix = \"FUNSTACK_STATIC_\";\n\n/**\n * Variable name for the app client manifest\n */\nexport const appClientManifestVar = `${globalPrefix}appClientManifest`;\n\nexport interface AppClientManifest {\n marker?: string;\n stream: string;\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"globals.mjs","names":[],"sources":["../../src/client/globals.ts"],"sourcesContent":["// Prefix for global variables\nconst globalPrefix = \"FUNSTACK_STATIC_\";\n\n/**\n * Variable name for the app client manifest\n */\nexport const appClientManifestVar = `${globalPrefix}appClientManifest`;\n\nexport interface AppClientManifest {\n marker?: string;\n stream: string;\n}\n"],"mappings":";;;AAMA,MAAa,uBAAuB"}
|
|
@@ -23,7 +23,7 @@ pnpm add @funstack/static react react-dom
|
|
|
23
23
|
Create or update your `vite.config.ts`:
|
|
24
24
|
|
|
25
25
|
```typescript
|
|
26
|
-
import
|
|
26
|
+
import funstackStatic from "@funstack/static";
|
|
27
27
|
import react from "@vitejs/plugin-react";
|
|
28
28
|
import { defineConfig } from "vite";
|
|
29
29
|
|
|
@@ -168,7 +168,11 @@ Only two files are required: `Root.tsx` and `App.tsx`. The paths to these files
|
|
|
168
168
|
If you use AI coding assistants like [Claude Code](https://docs.anthropic.com/en/docs/claude-code), you can install the FUNSTACK Static knowledge skill to help your AI assistant better understand the framework:
|
|
169
169
|
|
|
170
170
|
```bash
|
|
171
|
-
npx funstack-static-skill-installer
|
|
171
|
+
npx -p @funstack/static funstack-static-skill-installer
|
|
172
|
+
# or
|
|
173
|
+
yarn dlx -p @funstack/static funstack-static-skill-installer
|
|
174
|
+
# or
|
|
175
|
+
pnpm --package @funstack/static dlx funstack-static-skill-installer
|
|
172
176
|
# or, if you use skills CLI (https://skills.sh/)
|
|
173
177
|
npx skills add uhyo/funstack-static
|
|
174
178
|
```
|
|
@@ -177,7 +181,7 @@ This registers the `funstack-static-knowledge` skill, which provides your AI ass
|
|
|
177
181
|
|
|
178
182
|
## What's Next?
|
|
179
183
|
|
|
180
|
-
- Learn about the [funstackStatic() Plugin API](/
|
|
181
|
-
- Understand [defer()](/
|
|
182
|
-
- Build multi-page static sites with [Multiple Entrypoints](/
|
|
183
|
-
- Dive into [React Server Components](/
|
|
184
|
+
- Learn about the [funstackStatic() Plugin API](/api/funstack-static) for configuration options
|
|
185
|
+
- Understand [defer()](/api/defer) for Server Component chunk splitting
|
|
186
|
+
- Build multi-page static sites with [Multiple Entrypoints](/advanced/multiple-entrypoints)
|
|
187
|
+
- Dive into [React Server Components](/learn/rsc) concepts
|
|
@@ -27,14 +27,14 @@ Or with pnpm:
|
|
|
27
27
|
pnpm add @funstack/static
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
**Hint:** at this point,
|
|
30
|
+
**Hint:** at this point, you can add FUNSTACK Static knowledge to your AI agents. Run `npx -p @funstack/static funstack-static-skill-installer` (or `npx skills add uhyo/funstack-static` if you use [skills CLI](https://skills.sh/)) to add the skill. Then you can ask your AI assistant for help with the migration!
|
|
31
31
|
|
|
32
32
|
## Step 2: Update Vite Config
|
|
33
33
|
|
|
34
34
|
Modify your `vite.config.ts` to add the FUNSTACK Static plugin:
|
|
35
35
|
|
|
36
36
|
```typescript
|
|
37
|
-
import
|
|
37
|
+
import funstackStatic from "@funstack/static";
|
|
38
38
|
import react from "@vitejs/plugin-react";
|
|
39
39
|
import { defineConfig } from "vite";
|
|
40
40
|
|
|
@@ -316,6 +316,6 @@ Ensure CSS imports are in `App.tsx` or in client components that are actually re
|
|
|
316
316
|
|
|
317
317
|
## What's Next?
|
|
318
318
|
|
|
319
|
-
- Learn about [defer()](/
|
|
320
|
-
- Explore [Optimizing RSC Payloads](/
|
|
321
|
-
- Understand [How It Works](/
|
|
319
|
+
- Learn about [defer()](/api/defer) for code splitting Server Components
|
|
320
|
+
- Explore [Optimizing RSC Payloads](/learn/optimizing-payloads) for better performance
|
|
321
|
+
- Understand [How It Works](/learn/how-it-works) under the hood
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
# Multiple Entrypoints
|
|
1
|
+
# Multiple Entrypoints (SSG)
|
|
2
2
|
|
|
3
3
|
By default, FUNSTACK Static produces a single `index.html` from one `root` + `app` pair. The **multiple entries** feature lets you produce multiple HTML pages from a single project, targeting SSG (Static Site Generation) use cases where a site has distinct pages like `index.html`, `about.html`, and `blog/post-1.html`.
|
|
4
4
|
|
|
5
5
|
## When to Use Multiple Entries
|
|
6
6
|
|
|
7
|
-
Use the `entries` option when you want to build a **multi-page static site** where each page is a self-contained HTML document.
|
|
7
|
+
Use the `entries` option when you want to build a **multi-page static site** where each page is more like a self-contained HTML document.
|
|
8
8
|
|
|
9
|
-
- **Single-entry mode** (`root` + `app`): One HTML file, client-side routing between pages. Best for app-like experiences where
|
|
10
|
-
- **Multiple entries mode** (`entries`): Multiple HTML files, each independently pre-rendered. Best for content sites (blogs, docs, marketing pages) where
|
|
9
|
+
- **Single-entry mode** (`root` + `app`): One HTML file, client-side routing between pages. Best for app-like experiences where dynamic data loading and client-side interactivity are heavily used, and SEO is less of a concern (e.g., dashboards, web apps).
|
|
10
|
+
- **Multiple entries mode** (`entries`): Multiple HTML files, each independently pre-rendered. Best for content sites (blogs, docs, marketing pages) where SEO and fast initial load are priorities. Client-side routing is still possible by using a router library with SSR support.
|
|
11
11
|
|
|
12
12
|
## Basic Setup
|
|
13
13
|
|
|
@@ -33,7 +33,7 @@ export default defineConfig({
|
|
|
33
33
|
|
|
34
34
|
### 2. Create the Entries Module
|
|
35
35
|
|
|
36
|
-
The entries module default-
|
|
36
|
+
The entries module is run in **the server environment at build time**. It must default-export a function that returns an array of entry definitions (async functions are also supported):
|
|
37
37
|
|
|
38
38
|
```tsx
|
|
39
39
|
// src/entries.tsx
|
|
@@ -110,7 +110,7 @@ import type { EntryDefinition } from "@funstack/static/entries";
|
|
|
110
110
|
|
|
111
111
|
**Type:** `string`
|
|
112
112
|
|
|
113
|
-
The output file path relative to the build output directory. Must
|
|
113
|
+
The output file path relative to the build output directory. Must include file extensions and must not start with `/`.
|
|
114
114
|
|
|
115
115
|
```typescript
|
|
116
116
|
{
|
|
@@ -133,8 +133,8 @@ The root component module. Accepts either a lazy import or a synchronous module
|
|
|
133
133
|
root: () => import("./root"),
|
|
134
134
|
|
|
135
135
|
// Synchronous module object
|
|
136
|
-
import Root from "./root";
|
|
137
|
-
root:
|
|
136
|
+
import * as Root from "./root";
|
|
137
|
+
root: Root,
|
|
138
138
|
```
|
|
139
139
|
|
|
140
140
|
The module must have a `default` export of a component that accepts `children`.
|
|
@@ -150,8 +150,8 @@ The app content for this entry. Accepts a module (sync or lazy), or a React node
|
|
|
150
150
|
app: () => import("./pages/Home"),
|
|
151
151
|
|
|
152
152
|
// Synchronous module object
|
|
153
|
-
import Home from "./pages/Home";
|
|
154
|
-
app:
|
|
153
|
+
import * as Home from "./pages/Home";
|
|
154
|
+
app: Home,
|
|
155
155
|
|
|
156
156
|
// React node (server component JSX)
|
|
157
157
|
app: <BlogPost slug="hello-world" />,
|
|
@@ -166,14 +166,14 @@ For sites with many pages generated from external data, use an async generator t
|
|
|
166
166
|
```tsx
|
|
167
167
|
// src/entries.tsx
|
|
168
168
|
import type { EntryDefinition } from "@funstack/static/entries";
|
|
169
|
-
import Root from "./root";
|
|
169
|
+
import * as Root from "./root";
|
|
170
170
|
import { readdir } from "node:fs/promises";
|
|
171
171
|
|
|
172
172
|
export default async function* getEntries(): AsyncGenerator<EntryDefinition> {
|
|
173
173
|
// Static pages
|
|
174
174
|
yield {
|
|
175
175
|
path: "index.html",
|
|
176
|
-
root:
|
|
176
|
+
root: Root,
|
|
177
177
|
app: () => import("./pages/Home"),
|
|
178
178
|
};
|
|
179
179
|
|
|
@@ -182,7 +182,7 @@ export default async function* getEntries(): AsyncGenerator<EntryDefinition> {
|
|
|
182
182
|
const content = await loadMarkdown(`./content/blog/${slug}`);
|
|
183
183
|
yield {
|
|
184
184
|
path: `blog/${slug.replace(/\.md$/, ".html")}`,
|
|
185
|
-
root:
|
|
185
|
+
root: Root,
|
|
186
186
|
app: <BlogPost content={content} />,
|
|
187
187
|
};
|
|
188
188
|
}
|
|
@@ -213,34 +213,6 @@ dist/public/
|
|
|
213
213
|
|
|
214
214
|
All pages share the same client JavaScript bundle. Only the HTML and RSC payloads differ per entry.
|
|
215
215
|
|
|
216
|
-
## Navigation Between Entries
|
|
217
|
-
|
|
218
|
-
Each entry is a fully independent HTML page. Navigation between entries is a full page reload via standard `<a>` links. Client-side interactivity within each page works as usual.
|
|
219
|
-
|
|
220
|
-
If you need client-side navigation between pages (SPA-style transitions), use single-entry mode with a client-side router instead.
|
|
221
|
-
|
|
222
|
-
## Interaction with defer()
|
|
223
|
-
|
|
224
|
-
The `defer()` function works with multiple entries. Deferred components are shared across entries via content hashing -- if multiple entries defer the same component, it is rendered once and reused.
|
|
225
|
-
|
|
226
|
-
## Path Validation
|
|
227
|
-
|
|
228
|
-
The build enforces these rules for entry paths:
|
|
229
|
-
|
|
230
|
-
- Must end with `.html`
|
|
231
|
-
- Must not start with `/` (paths are relative to the output directory)
|
|
232
|
-
- Duplicate paths cause a build error
|
|
233
|
-
|
|
234
|
-
## Dev and Preview Server
|
|
235
|
-
|
|
236
|
-
Both the dev server (`vite dev`) and preview server (`vite preview`) handle URL-to-file mapping automatically:
|
|
237
|
-
|
|
238
|
-
- `/` serves `index.html`
|
|
239
|
-
- `/about` serves `about.html`, falling back to `about/index.html`
|
|
240
|
-
- `/blog/post-1` serves `blog/post-1.html`, falling back to `blog/post-1/index.html`
|
|
241
|
-
|
|
242
216
|
## See Also
|
|
243
217
|
|
|
244
|
-
- [
|
|
245
|
-
- [Getting Started](/funstack-static/getting-started) - Quick start guide
|
|
246
|
-
- [defer()](/funstack-static/api/defer) - Deferred rendering for streaming
|
|
218
|
+
- [Server-Side Rendering](/advanced/SSR) - Content-heavy sites may also benefit from SSR for faster initial paint
|
|
@@ -90,7 +90,7 @@ Common browser APIs to watch for:
|
|
|
90
90
|
|
|
91
91
|
- You want the fastest possible initial paint
|
|
92
92
|
- Your client components are already SSR-compatible
|
|
93
|
-
- You're building content-heavy pages
|
|
93
|
+
- You're building content-heavy pages with [multiple entrypoints](/advanced/multiple-entrypoints)
|
|
94
94
|
|
|
95
95
|
**Keep SSR disabled when:**
|
|
96
96
|
|
|
@@ -100,5 +100,5 @@ Common browser APIs to watch for:
|
|
|
100
100
|
|
|
101
101
|
## See Also
|
|
102
102
|
|
|
103
|
-
- [How It Works](/
|
|
104
|
-
- [funstackStatic()](/
|
|
103
|
+
- [How It Works](/learn/how-it-works) - Understanding the build process
|
|
104
|
+
- [funstackStatic()](/api/funstack-static) - Configuration reference
|
|
@@ -135,5 +135,5 @@ export default async function* getEntries() {
|
|
|
135
135
|
|
|
136
136
|
## See Also
|
|
137
137
|
|
|
138
|
-
- [Multiple Entrypoints](/
|
|
139
|
-
- [funstackStatic()](/
|
|
138
|
+
- [Multiple Entrypoints](/advanced/multiple-entrypoints) - Guide and examples
|
|
139
|
+
- [funstackStatic()](/api/funstack-static) - Plugin configuration
|