@funstack/static 0.0.10 → 1.0.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 +1 -2
- 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 +1 -2
- package/dist/build/rscPath.mjs.map +1 -1
- package/dist/build/rscProcessor.mjs +1 -2
- 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 +9 -5
- package/dist/docs/MigratingFromViteSPA.md +4 -4
- 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 +6 -6
- package/dist/docs/index.md +5 -2
- package/dist/docs/learn/DeferAndActivity.md +3 -3
- package/dist/docs/learn/HowItWorks.md +2 -2
- 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.mjs +1 -2
- 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.mjs +1 -2
- 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 +1 -1
- package/dist/rsc-client/clientWrapper.mjs +2 -3
- package/dist/rsc-client/clientWrapper.mjs.map +1 -1
- 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/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,7 +7,6 @@ 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
11
|
async function buildApp(builder, context) {
|
|
13
12
|
const { config } = builder;
|
|
@@ -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, idMapping, baseDir, base, context);\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 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)\n : base + getRscPayloadPath(mainPayloadHash);\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(baseDir, getRscPayloadPath(mainPayloadHash).replace(/^\\//, \"\")),\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":"
|
|
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, idMapping, baseDir, base, context);\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 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)\n : base + getRscPayloadPath(mainPayloadHash);\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(baseDir, getRscPayloadPath(mainPayloadHash).replace(/^\\//, \"\")),\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;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,QACD;AAGD,MAAK,MAAM,UAAU,QACnB,OAAM,iBAAiB,QAAQ,WAAW,SAAS,MAAM,QAAQ;AAInE,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,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,gBAAgB,GAClC,OAAO,kBAAkB,gBAAgB;CAG/C,MAAM,mBAAmB,YAAY,WACnC,uBACA,gBACD;AAGD,OAAM,gBACJ,KAAK,KAAK,SAAS,UAAU,EAC7B,kBACA,QACD;AAGD,OAAM,gBACJ,KAAK,KAAK,SAAS,kBAAkB,gBAAgB,CAAC,QAAQ,OAAO,GAAG,CAAC,EACzE,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)
|
|
@@ -11,7 +10,7 @@ const rscPayloadPlaceholder = "__FUNSTACK_RSC_PAYLOAD_PATH__";
|
|
|
11
10
|
function getRscPayloadPath(contentHash) {
|
|
12
11
|
return getModulePathFor(getPayloadIDFor(contentHash));
|
|
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(contentHash: string): string {\n return getModulePathFor(getPayloadIDFor(contentHash));\n}\n"],"mappings":";;;;;AAKA,MAAa,wBAAwB;;;;AAKrC,SAAgB,kBAAkB,aAA6B;AAC7D,QAAO,iBAAiB,gBAAgB,YAAY,CAAC"}
|
|
@@ -2,7 +2,6 @@ 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.
|
|
@@ -66,7 +65,7 @@ async function processRscComponents(deferRegistryIterator, appRscStream, context
|
|
|
66
65
|
idMapping
|
|
67
66
|
};
|
|
68
67
|
}
|
|
69
|
-
|
|
70
68
|
//#endregion
|
|
71
69
|
export { processRscComponents };
|
|
70
|
+
|
|
72
71
|
//# 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 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":";;;;;;;;;;;;AA8BA,eAAsB,qBACpB,uBACA,cACA,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,CACT;AAG5C,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"}
|
|
@@ -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,7 +27,7 @@ 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
|
|
|
@@ -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
|
|
@@ -51,7 +51,7 @@ export default defineConfig({
|
|
|
51
51
|
});
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
-
See [Multiple Entrypoints](/
|
|
54
|
+
See [Multiple Entrypoints](/advanced/multiple-entrypoints) for a full guide.
|
|
55
55
|
|
|
56
56
|
## Options
|
|
57
57
|
|
|
@@ -158,7 +158,7 @@ export default function getEntries(): EntryDefinition[] {
|
|
|
158
158
|
}
|
|
159
159
|
```
|
|
160
160
|
|
|
161
|
-
See [Multiple Entrypoints](/
|
|
161
|
+
See [Multiple Entrypoints](/advanced/multiple-entrypoints) for details on the `EntryDefinition` type and advanced usage patterns like async generators.
|
|
162
162
|
|
|
163
163
|
### publicOutDir (optional)
|
|
164
164
|
|
|
@@ -274,7 +274,7 @@ You can use the same Vite commands you would use in a normal Vite project:
|
|
|
274
274
|
|
|
275
275
|
## See Also
|
|
276
276
|
|
|
277
|
-
- [Getting Started](/
|
|
278
|
-
- [Multiple Entrypoints](/
|
|
279
|
-
- [defer()](/
|
|
280
|
-
- [React Server Components](/
|
|
277
|
+
- [Getting Started](/getting-started) - Quick start guide
|
|
278
|
+
- [Multiple Entrypoints](/advanced/multiple-entrypoints) - Multi-page static site generation
|
|
279
|
+
- [defer()](/api/defer) - Deferred rendering for streaming
|
|
280
|
+
- [React Server Components](/learn/rsc) - Understanding RSC
|
package/dist/docs/index.md
CHANGED
|
@@ -8,6 +8,11 @@ A Vite plugin for building static sites with React Server Components.
|
|
|
8
8
|
- [Getting Started](./GettingStarted.md) - Welcome to **FUNSTACK Static**! Build high-performance Single Page Applications powered by React Server Components - no server required at runtime.
|
|
9
9
|
- [Migrating from Vite SPA](./MigratingFromViteSPA.md) - Already have a Vite-powered React SPA? This guide walks you through migrating to FUNSTACK Static to unlock React Server Components and improved performance.
|
|
10
10
|
|
|
11
|
+
### Advanced
|
|
12
|
+
|
|
13
|
+
- [Multiple Entrypoints (SSG)](./advanced/MultipleEntrypoints.md) - 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`.
|
|
14
|
+
- [Server-Side Rendering](./advanced/SSR.md) - In FUNSTACK Static, **Server-Side Rendering (SSR)** means a build-time process that pre-renders your React components (including client components) to HTML. This can make the initial paint faster.
|
|
15
|
+
|
|
11
16
|
### API
|
|
12
17
|
|
|
13
18
|
- [defer()](./api/Defer.md) - The `defer()` function enables deferred rendering for React Server Components, reducing initial data load.
|
|
@@ -19,7 +24,5 @@ A Vite plugin for building static sites with React Server Components.
|
|
|
19
24
|
- [Prefetching with defer() and Activity](./learn/DeferAndActivity.md) - When using `defer()` to split RSC payloads, content is fetched on-demand as it renders. But what if you want to start fetching _before_ the user actually needs it? By combining `defer()` with React 19's `<Activity>` component, you can prefetch deferred payloads in the background so they're ready instantly when revealed.
|
|
20
25
|
- [How It Works](./learn/HowItWorks.md) - FUNSTACK Static is a React framework that leverages React Server Components (RSC) to build a fully static Single Page Application. The result is a set of files that can be deployed to **any static file hosting service** - no server required at runtime.
|
|
21
26
|
- [Using lazy() in Server Components](./learn/LazyServerComponents.md) - React's `lazy()` API is typically associated with client-side code splitting. However, it can also be used in server environments to reduce the initial response time of the development server by deferring the work needed to compute your application.
|
|
22
|
-
- [Multiple Entrypoints](./learn/MultipleEntrypoints.md) - 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`.
|
|
23
27
|
- [Optimizing RSC Payloads](./learn/OptimizingPayloads.md) - FUNSTACK Static uses React Server Components (RSC) to pre-render your application at build time. By default, all content is bundled into a single RSC payload. This page explains how to split that payload into smaller chunks for better loading performance.
|
|
24
28
|
- [React Server Components](./learn/RSC.md) - [React Server Components (RSC)](https://react.dev/reference/rsc/server-components) are a new paradigm for building React applications where components can run on the server (or at build time) rather than in the browser.
|
|
25
|
-
- [Server-Side Rendering](./learn/SSR.md) - In FUNSTACK Static, **Server-Side Rendering (SSR)** means a build-time process that pre-renders your React components (including client components) to HTML. This can make the initial paint faster.
|
|
@@ -171,6 +171,6 @@ Avoid this pattern when:
|
|
|
171
171
|
|
|
172
172
|
## See Also
|
|
173
173
|
|
|
174
|
-
- [Optimizing RSC Payloads](/
|
|
175
|
-
- [defer()](/
|
|
176
|
-
- [React Server Components](/
|
|
174
|
+
- [Optimizing RSC Payloads](/learn/optimizing-payloads) - Using `defer()` to split RSC payloads
|
|
175
|
+
- [defer()](/api/defer) - API reference with full signature and technical details
|
|
176
|
+
- [React Server Components](/learn/rsc) - Understanding RSC fundamentals
|
|
@@ -105,5 +105,5 @@ Note that in both modes, React Server Components are still used - the `ssr` opti
|
|
|
105
105
|
|
|
106
106
|
## See Also
|
|
107
107
|
|
|
108
|
-
- [React Server Components](/
|
|
109
|
-
- [Getting Started](/
|
|
108
|
+
- [React Server Components](/learn/rsc) - Understanding RSC in depth
|
|
109
|
+
- [Getting Started](/getting-started) - Set up your first project
|
|
@@ -115,6 +115,6 @@ For small applications with few routes, the overhead of `lazy()` may not be wort
|
|
|
115
115
|
|
|
116
116
|
## See Also
|
|
117
117
|
|
|
118
|
-
- [Optimizing RSC Payloads](/
|
|
119
|
-
- [How It Works](/
|
|
120
|
-
- [defer()](/
|
|
118
|
+
- [Optimizing RSC Payloads](/learn/optimizing-payloads) - Using `defer()` to split RSC payloads
|
|
119
|
+
- [How It Works](/learn/how-it-works) - Overall FUNSTACK Static architecture
|
|
120
|
+
- [defer()](/api/defer) - API reference for the defer function
|