@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.
Files changed (70) hide show
  1. package/README.md +4 -4
  2. package/dist/bin/skill-installer.mjs +5 -4
  3. package/dist/bin/skill-installer.mjs.map +1 -1
  4. package/dist/build/buildApp.mjs +7 -8
  5. package/dist/build/buildApp.mjs.map +1 -1
  6. package/dist/build/contentHash.mjs +1 -1
  7. package/dist/build/dependencyGraph.mjs +1 -1
  8. package/dist/build/dependencyGraph.mjs.map +1 -1
  9. package/dist/build/rscPath.mjs +3 -4
  10. package/dist/build/rscPath.mjs.map +1 -1
  11. package/dist/build/rscProcessor.mjs +4 -4
  12. package/dist/build/rscProcessor.mjs.map +1 -1
  13. package/dist/build/validateEntryPath.mjs +1 -1
  14. package/dist/client/entry.d.mts +1 -1
  15. package/dist/client/entry.mjs +2 -3
  16. package/dist/client/entry.mjs.map +1 -1
  17. package/dist/client/error-boundary.mjs +2 -4
  18. package/dist/client/error-boundary.mjs.map +1 -1
  19. package/dist/client/globals.mjs +2 -4
  20. package/dist/client/globals.mjs.map +1 -1
  21. package/dist/docs/GettingStarted.md +10 -6
  22. package/dist/docs/MigratingFromViteSPA.md +5 -5
  23. package/dist/docs/{learn → advanced}/MultipleEntrypoints.md +14 -42
  24. package/dist/docs/{learn → advanced}/SSR.md +3 -3
  25. package/dist/docs/api/EntryDefinition.md +2 -2
  26. package/dist/docs/api/FunstackStatic.md +26 -7
  27. package/dist/docs/index.md +5 -2
  28. package/dist/docs/learn/DeferAndActivity.md +3 -3
  29. package/dist/docs/learn/HowItWorks.md +3 -3
  30. package/dist/docs/learn/LazyServerComponents.md +3 -3
  31. package/dist/docs/learn/OptimizingPayloads.md +4 -4
  32. package/dist/docs/learn/RSC.md +3 -3
  33. package/dist/entries/client.d.mts +1 -1
  34. package/dist/entries/client.mjs +1 -2
  35. package/dist/entries/rsc-client.mjs +1 -3
  36. package/dist/entries/rsc.mjs +1 -2
  37. package/dist/entries/server.mjs +1 -2
  38. package/dist/entries/ssr.mjs +1 -2
  39. package/dist/entryDefinition.mjs +1 -1
  40. package/dist/index.mjs +1 -2
  41. package/dist/plugin/getRSCEntryPoint.mjs +1 -1
  42. package/dist/plugin/getRSCEntryPoint.mjs.map +1 -1
  43. package/dist/plugin/index.d.mts +14 -0
  44. package/dist/plugin/index.d.mts.map +1 -1
  45. package/dist/plugin/index.mjs +6 -5
  46. package/dist/plugin/index.mjs.map +1 -1
  47. package/dist/plugin/server.mjs +1 -2
  48. package/dist/plugin/server.mjs.map +1 -1
  49. package/dist/rsc/defer.d.mts.map +1 -1
  50. package/dist/rsc/defer.mjs +3 -3
  51. package/dist/rsc/defer.mjs.map +1 -1
  52. package/dist/rsc/entry.mjs +4 -5
  53. package/dist/rsc/entry.mjs.map +1 -1
  54. package/dist/rsc/marker.mjs +1 -1
  55. package/dist/rsc/request.mjs +1 -1
  56. package/dist/rsc/resolveEntry.mjs +1 -2
  57. package/dist/rsc/resolveEntry.mjs.map +1 -1
  58. package/dist/rsc/rscModule.mjs +8 -8
  59. package/dist/rsc/rscModule.mjs.map +1 -1
  60. package/dist/rsc-client/clientWrapper.mjs +2 -3
  61. package/dist/rsc-client/clientWrapper.mjs.map +1 -1
  62. package/dist/rsc-client/entry.d.mts +1 -0
  63. package/dist/rsc-client/entry.mjs +2 -4
  64. package/dist/ssr/entry.mjs +2 -2
  65. package/dist/ssr/entry.mjs.map +1 -1
  66. package/dist/util/basePath.mjs +1 -1
  67. package/dist/util/drainStream.mjs +1 -1
  68. package/dist/util/urlPath.mjs +1 -1
  69. package/package.json +5 -5
  70. 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. After installing `@funstack/static`, run the following command to add the skill to the project:
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 resolved = path.resolve("./node_modules/@funstack/static/skills/funstack-static-knowledge");
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 skillDir =\n \"./node_modules/@funstack/static/skills/funstack-static-knowledge\";\n\nconst resolved = path.resolve(skillDir);\n\nconsole.log(\"Installing skill from:\", resolved);\n\nawait install(resolved);\n"],"mappings":";;;;;AAQA,MAAM,WAAW,KAAK,QAFpB,mEAEqC;AAEvC,QAAQ,IAAI,0BAA0B,SAAS;AAE/C,MAAM,QAAQ,SAAS"}
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"}
@@ -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, 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"}
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
@@ -40,7 +40,7 @@ function topologicalSort(dependencies) {
40
40
  inCycle
41
41
  };
42
42
  }
43
-
44
43
  //#endregion
45
44
  export { findReferencedIds, topologicalSort };
45
+
46
46
  //# sourceMappingURL=dependencyGraph.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,MACF;QAAK,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"}
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"}
@@ -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":";;;;;;AAKA,MAAa,wBAAwB;;;;AAKrC,SAAgB,kBAAkB,aAA6B;AAC7D,QAAO,iBAAiB,gBAAgB,YAAY,CAAC"}
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":";;;;;;;;;;;;;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"}
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"}
@@ -22,7 +22,7 @@ function checkDuplicatePaths(paths) {
22
22
  seen.add(p);
23
23
  }
24
24
  }
25
-
26
25
  //#endregion
27
26
  export { checkDuplicatePaths, validateEntryPath };
27
+
28
28
  //# sourceMappingURL=validateEntryPath.mjs.map
@@ -1 +1 @@
1
- import "virtual:funstack/client-init";
1
+ export { };
@@ -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":";;;;;;;;;;;;;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,wBACL,oBAAC,iCACC,oBAAC,gBAAc,GACK,GACL;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,wBACL,oBAAC,iCACC,oBAAC,gBAAc,GACK,GACL;AAGrB,cAAY,UAAU,YAAY;QAC7B;EAEL,MAAM,cAAc,oBAAC,gBAAc;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
+ {"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":";;;;;;;;;;AAQA,MAAa,uBACX,UACG;AACH,QACE,oBAAC;EAAc,mBAAmB;YAAW,MAAM;GAAyB;;AAIhF,MAAM,YAAqC,EAAE,OAAO,yBAAyB;CAC3E,MAAM,eAAe,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM;AAC3E,QACE,qBAAC,qBACC,oBAAC,oBACC,oBAAC,qBAAM,qBAAwB,GAC1B,EACP,qBAAC;EACC,OAAO;GACL,QAAQ;GACR,SAAS;GACT,eAAe;GACf,cAAc;GACd,YAAY;GACZ,UAAU;GACV,YAAY;GACZ,YAAY;GACb;;GAED,oBAAC,kBAAG,+BAA+B;GACnC,oBAAC,iBAAE,iCAAgC;GACnC,qBAAC,oBAAI,WAAQ,gBAAmB;GAChC,oBAAC;IACC,eAAe;AACb,2BAAsB;AACpB,0BAAoB;OACpB;;cAEL;KAEQ;;GACJ,IACF"}
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"}
@@ -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 = `${globalPrefix}appClientManifest`;
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":";AACA,MAAM,eAAe;;;;AAKrB,MAAa,uBAAuB,GAAG,aAAa"}
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 { funstackStatic } from "@funstack/static";
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](/funstack-static/api/funstack-static) for configuration options
181
- - Understand [defer()](/funstack-static/api/defer) for Server Component chunk splitting
182
- - Build multi-page static sites with [Multiple Entrypoints](/funstack-static/learn/multiple-entrypoints)
183
- - Dive into [React Server Components](/funstack-static/learn/rsc) concepts
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, a skill installer command is available to add FUNSTACK Static knowledge to your AI agents. Run `npx 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!
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 { funstackStatic } from "@funstack/static";
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()](/funstack-static/api/defer) for code splitting Server Components
320
- - Explore [Optimizing RSC Payloads](/funstack-static/learn/optimizing-payloads) for better performance
321
- - Understand [How It Works](/funstack-static/learn/how-it-works) under the hood
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. This is different from a single-page app with client-side routing:
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 navigation should not trigger full page reloads.
10
- - **Multiple entries mode** (`entries`): Multiple HTML files, each independently pre-rendered. Best for content sites (blogs, docs, marketing pages) where each page should be a standalone document.
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-exports a function that returns an array of entry definitions:
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 end with `.html` and must not start with `/`.
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: { default: 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: { default: Home },
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: { default: 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: { default: 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
- - [funstackStatic()](/funstack-static/api/funstack-static) - Configuration reference
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](/funstack-static/learn/how-it-works) - Understanding the build process
104
- - [funstackStatic()](/funstack-static/api/funstack-static) - Configuration reference
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](/funstack-static/learn/multiple-entrypoints) - Guide and examples
139
- - [funstackStatic()](/funstack-static/api/funstack-static) - Plugin configuration
138
+ - [Multiple Entrypoints](/advanced/multiple-entrypoints) - Guide and examples
139
+ - [funstackStatic()](/api/funstack-static) - Plugin configuration