@funstack/static 0.0.2 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -0
- package/dist/bin/skill-installer.d.mts +1 -0
- package/dist/bin/skill-installer.mjs +12 -0
- package/dist/bin/skill-installer.mjs.map +1 -0
- package/dist/build/buildApp.mjs +4 -3
- package/dist/build/buildApp.mjs.map +1 -1
- package/dist/build/rscProcessor.mjs +9 -3
- package/dist/build/rscProcessor.mjs.map +1 -1
- package/dist/client/entry.d.mts +1 -0
- package/dist/client/entry.mjs +17 -9
- package/dist/client/entry.mjs.map +1 -1
- package/dist/client/globals.mjs.map +1 -1
- package/dist/docs/FAQ.md +5 -0
- package/dist/docs/GettingStarted.md +180 -0
- package/dist/docs/api/Defer.md +110 -0
- package/dist/docs/api/FunstackStatic.md +184 -0
- package/dist/docs/index.md +20 -0
- package/dist/docs/learn/HowItWorks.md +109 -0
- package/dist/docs/learn/OptimizingPayloads.md +105 -0
- package/dist/docs/learn/RSC.md +179 -0
- package/dist/docs/learn/SSR.md +104 -0
- package/dist/entries/client.d.mts +1 -1
- package/dist/entries/rsc-client.d.mts +2 -2
- package/dist/entries/rsc-client.mjs +2 -2
- package/dist/entries/server.d.mts +2 -2
- package/dist/plugin/index.d.mts +17 -1
- package/dist/plugin/index.d.mts.map +1 -1
- package/dist/plugin/index.mjs +10 -1
- package/dist/plugin/index.mjs.map +1 -1
- package/dist/rsc/defer.d.mts +18 -3
- package/dist/rsc/defer.d.mts.map +1 -1
- package/dist/rsc/defer.mjs +34 -14
- package/dist/rsc/defer.mjs.map +1 -1
- package/dist/rsc/entry.d.mts.map +1 -1
- package/dist/rsc/entry.mjs +85 -20
- package/dist/rsc/entry.mjs.map +1 -1
- package/dist/rsc-client/clientWrapper.d.mts +3 -3
- package/dist/rsc-client/clientWrapper.d.mts.map +1 -1
- package/dist/rsc-client/clientWrapper.mjs +2 -2
- package/dist/rsc-client/clientWrapper.mjs.map +1 -1
- package/dist/rsc-client/entry.d.mts +1 -1
- package/dist/rsc-client/entry.mjs +1 -1
- package/dist/ssr/entry.d.mts +2 -0
- package/dist/ssr/entry.d.mts.map +1 -1
- package/dist/ssr/entry.mjs +6 -2
- package/dist/ssr/entry.mjs.map +1 -1
- package/package.json +26 -11
- package/skills/funstack-static-knowledge/SKILL.md +44 -0
package/README.md
CHANGED
|
@@ -38,6 +38,18 @@ export default defineConfig({
|
|
|
38
38
|
});
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
+
## CLI Commands
|
|
42
|
+
|
|
43
|
+
### `funstack-static-skill-installer`
|
|
44
|
+
|
|
45
|
+
Installs the `funstack-static-knowledge` skill for AI coding assistants (like [Claude Code](https://docs.anthropic.com/en/docs/claude-code)).
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
npx funstack-static-skill-installer
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
This command registers the skill that provides AI assistants with knowledge about the FUNSTACK Static framework, including API references, best practices, and architectural guidance. After installation, your AI assistant will be able to better understand and work with your FUNSTACK Static project.
|
|
52
|
+
|
|
41
53
|
## Documentation
|
|
42
54
|
|
|
43
55
|
For detailed API documentation and guides, visit the **[Documentation](https://uhyo.github.io/funstack-static/)**.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#! /usr/bin/env node
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { install } from "@funstack/skill-installer";
|
|
4
|
+
|
|
5
|
+
//#region src/bin/skill-installer.ts
|
|
6
|
+
const resolved = path.resolve("./node_modules/@funstack/static/skills/funstack-static-knowledge");
|
|
7
|
+
console.log("Installing skill from:", resolved);
|
|
8
|
+
await install(resolved);
|
|
9
|
+
|
|
10
|
+
//#endregion
|
|
11
|
+
export { };
|
|
12
|
+
//# sourceMappingURL=skill-installer.mjs.map
|
|
@@ -0,0 +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"}
|
package/dist/build/buildApp.mjs
CHANGED
|
@@ -21,11 +21,12 @@ async function buildApp(builder, context) {
|
|
|
21
21
|
const finalHtmlContent = htmlContent.replaceAll(rscPayloadPlaceholder, mainPayloadPath);
|
|
22
22
|
await writeFileNormal(path.join(baseDir, "index.html"), finalHtmlContent, context);
|
|
23
23
|
await writeFileNormal(path.join(baseDir, getRscPayloadPath(mainPayloadHash).replace(/^\//, "")), appRscContent, context);
|
|
24
|
-
for (const { finalId, finalContent } of components) await writeFileNormal(path.join(baseDir, getModulePathFor(finalId).replace(/^\//, "")), finalContent, context);
|
|
24
|
+
for (const { finalId, finalContent, name } of components) await writeFileNormal(path.join(baseDir, getModulePathFor(finalId).replace(/^\//, "")), finalContent, context, name);
|
|
25
25
|
}
|
|
26
|
-
async function writeFileNormal(filePath, data, context) {
|
|
26
|
+
async function writeFileNormal(filePath, data, context, name) {
|
|
27
27
|
await mkdir(path.dirname(filePath), { recursive: true });
|
|
28
|
-
|
|
28
|
+
const nameInfo = name ? ` (${name})` : "";
|
|
29
|
+
context.info(`[funstack] Writing ${filePath}${nameInfo}`);
|
|
29
30
|
await writeFile(filePath, data);
|
|
30
31
|
}
|
|
31
32
|
|
|
@@ -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\";\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 // render rsc and html\n const baseDir = config.environments.client.build.outDir;\n const { html, appRsc, deferRegistry } = await entry.build();\n\n // Drain HTML stream to string (needed for placeholder replacement later)\n const htmlContent = await drainStream(html);\n\n // Process RSC components with content-based hashes for deterministic file names\n const { components, appRscContent } = await processRscComponents(\n deferRegistry.loadAll(),\n appRsc,\n context,\n );\n\n // Compute hash for main RSC payload and apply base path\n const mainPayloadHash = await computeContentHash(appRscContent);\n const base = config.base.endsWith(\"/\")\n ? config.base.slice(0, -1)\n : config.base;\n const mainPayloadPath =\n base === \"/\"\n ? getRscPayloadPath(mainPayloadHash)\n : base + getRscPayloadPath(mainPayloadHash);\n\n // Replace placeholder with final hashed path (including base path)\n const finalHtmlContent = htmlContent.replaceAll(\n rscPayloadPlaceholder,\n mainPayloadPath,\n );\n\n // Write HTML with replaced path\n await writeFileNormal(\n path.join(baseDir, \"index.html\"),\n finalHtmlContent,\n context,\n );\n\n // Write main RSC payload with hashed filename\n await writeFileNormal(\n path.join(baseDir, getRscPayloadPath(mainPayloadHash).replace(/^\\//, \"\")),\n appRscContent,\n context,\n );\n\n // Write processed components with hash-based IDs\n for (const { finalId, finalContent } of components) {\n const filePath = path.join(\n baseDir,\n getModulePathFor(finalId).replace(/^\\//, \"\"),\n );\n await writeFileNormal(filePath, finalContent, context);\n }\n}\n\nasync function writeFileNormal(\n filePath: string,\n data: string,\n context: MinimalPluginContextWithoutEnvironment,\n) {\n await mkdir(path.dirname(filePath), { recursive: true });\n context.info(`[funstack] Writing ${filePath}`);\n await writeFile(filePath, data);\n}\n"],"mappings":";;;;;;;;;;AAUA,eAAsB,SACpB,SACA,SACA;CACA,MAAM,EAAE,WAAW;CAGnB,MAAM,QAAuC,MAAM,OACjD,cAFgB,KAAK,KAAK,OAAO,aAAa,IAAI,MAAM,QAAQ,WAAW,CAEnD,CAAC;CAI3B,MAAM,UAAU,OAAO,aAAa,OAAO,MAAM;CACjD,MAAM,EAAE,MAAM,QAAQ,kBAAkB,MAAM,MAAM,OAAO;CAG3D,MAAM,cAAc,MAAM,YAAY,KAAK;CAG3C,MAAM,EAAE,YAAY,kBAAkB,MAAM,qBAC1C,cAAc,SAAS,EACvB,QACA,QACD;CAGD,MAAM,kBAAkB,MAAM,mBAAmB,cAAc;CAC/D,MAAM,OAAO,OAAO,KAAK,SAAS,IAAI,GAClC,OAAO,KAAK,MAAM,GAAG,GAAG,GACxB,OAAO;CACX,MAAM,kBACJ,SAAS,MACL,kBAAkB,gBAAgB,GAClC,OAAO,kBAAkB,gBAAgB;CAG/C,MAAM,mBAAmB,YAAY,WACnC,uBACA,gBACD;AAGD,OAAM,gBACJ,KAAK,KAAK,SAAS,aAAa,EAChC,kBACA,QACD;AAGD,OAAM,gBACJ,KAAK,KAAK,SAAS,kBAAkB,gBAAgB,CAAC,QAAQ,OAAO,GAAG,CAAC,EACzE,eACA,QACD;AAGD,MAAK,MAAM,EAAE,SAAS,
|
|
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\";\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 // render rsc and html\n const baseDir = config.environments.client.build.outDir;\n const { html, appRsc, deferRegistry } = await entry.build();\n\n // Drain HTML stream to string (needed for placeholder replacement later)\n const htmlContent = await drainStream(html);\n\n // Process RSC components with content-based hashes for deterministic file names\n const { components, appRscContent } = await processRscComponents(\n deferRegistry.loadAll(),\n appRsc,\n context,\n );\n\n // Compute hash for main RSC payload and apply base path\n const mainPayloadHash = await computeContentHash(appRscContent);\n const base = config.base.endsWith(\"/\")\n ? config.base.slice(0, -1)\n : config.base;\n const mainPayloadPath =\n base === \"/\"\n ? getRscPayloadPath(mainPayloadHash)\n : base + getRscPayloadPath(mainPayloadHash);\n\n // Replace placeholder with final hashed path (including base path)\n const finalHtmlContent = htmlContent.replaceAll(\n rscPayloadPlaceholder,\n mainPayloadPath,\n );\n\n // Write HTML with replaced path\n await writeFileNormal(\n path.join(baseDir, \"index.html\"),\n finalHtmlContent,\n context,\n );\n\n // Write main RSC payload with hashed filename\n await writeFileNormal(\n path.join(baseDir, getRscPayloadPath(mainPayloadHash).replace(/^\\//, \"\")),\n appRscContent,\n context,\n );\n\n // Write processed components with hash-based IDs\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\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":";;;;;;;;;;AAUA,eAAsB,SACpB,SACA,SACA;CACA,MAAM,EAAE,WAAW;CAGnB,MAAM,QAAuC,MAAM,OACjD,cAFgB,KAAK,KAAK,OAAO,aAAa,IAAI,MAAM,QAAQ,WAAW,CAEnD,CAAC;CAI3B,MAAM,UAAU,OAAO,aAAa,OAAO,MAAM;CACjD,MAAM,EAAE,MAAM,QAAQ,kBAAkB,MAAM,MAAM,OAAO;CAG3D,MAAM,cAAc,MAAM,YAAY,KAAK;CAG3C,MAAM,EAAE,YAAY,kBAAkB,MAAM,qBAC1C,cAAc,SAAS,EACvB,QACA,QACD;CAGD,MAAM,kBAAkB,MAAM,mBAAmB,cAAc;CAC/D,MAAM,OAAO,OAAO,KAAK,SAAS,IAAI,GAClC,OAAO,KAAK,MAAM,GAAG,GAAG,GACxB,OAAO;CACX,MAAM,kBACJ,SAAS,MACL,kBAAkB,gBAAgB,GAClC,OAAO,kBAAkB,gBAAgB;CAG/C,MAAM,mBAAmB,YAAY,WACnC,uBACA,gBACD;AAGD,OAAM,gBACJ,KAAK,KAAK,SAAS,aAAa,EAChC,kBACA,QACD;AAGD,OAAM,gBACJ,KAAK,KAAK,SAAS,kBAAkB,gBAAgB,CAAC,QAAQ,OAAO,GAAG,CAAC,EACzE,eACA,QACD;AAGD,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,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"}
|
|
@@ -13,7 +13,11 @@ import { findReferencedIds, topologicalSort } from "./dependencyGraph.mjs";
|
|
|
13
13
|
*/
|
|
14
14
|
async function processRscComponents(deferRegistryIterator, appRscStream, context) {
|
|
15
15
|
const components = /* @__PURE__ */ new Map();
|
|
16
|
-
|
|
16
|
+
const componentNames = /* @__PURE__ */ new Map();
|
|
17
|
+
for await (const { id, data, name } of deferRegistryIterator) {
|
|
18
|
+
components.set(id, data);
|
|
19
|
+
componentNames.set(id, name);
|
|
20
|
+
}
|
|
17
21
|
let appRscContent = await drainStream(appRscStream);
|
|
18
22
|
if (components.size === 0) return {
|
|
19
23
|
components: [],
|
|
@@ -42,7 +46,8 @@ async function processRscComponents(deferRegistryIterator, appRscStream, context
|
|
|
42
46
|
idMapping.set(tempId, finalId);
|
|
43
47
|
processedComponents.push({
|
|
44
48
|
finalId,
|
|
45
|
-
finalContent: content
|
|
49
|
+
finalContent: content,
|
|
50
|
+
name: componentNames.get(tempId)
|
|
46
51
|
});
|
|
47
52
|
}
|
|
48
53
|
for (const tempId of inCycle) {
|
|
@@ -50,7 +55,8 @@ async function processRscComponents(deferRegistryIterator, appRscStream, context
|
|
|
50
55
|
for (const [oldId, newId] of idMapping) if (oldId !== newId) content = content.replaceAll(oldId, newId);
|
|
51
56
|
processedComponents.push({
|
|
52
57
|
finalId: tempId,
|
|
53
|
-
finalContent: content
|
|
58
|
+
finalContent: content,
|
|
59
|
+
name: componentNames.get(tempId)
|
|
54
60
|
});
|
|
55
61
|
}
|
|
56
62
|
for (const [oldId, newId] of idMapping) if (oldId !== newId) appRscContent = appRscContent.replaceAll(oldId, newId);
|
|
@@ -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}\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}\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 for await (const { id, data } of deferRegistryIterator) {\n components.set(id, data);\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 });\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 });\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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "virtual:funstack/client-init";
|
package/dist/client/entry.mjs
CHANGED
|
@@ -2,11 +2,13 @@ import { GlobalErrorBoundary } from "./error-boundary.mjs";
|
|
|
2
2
|
import { devMainRscPath } from "../rsc/request.mjs";
|
|
3
3
|
import { appClientManifestVar } from "./globals.mjs";
|
|
4
4
|
import { withBasePath } from "../util/basePath.mjs";
|
|
5
|
+
import "virtual:funstack/client-init";
|
|
5
6
|
import { createFromFetch, createFromReadableStream } from "@vitejs/plugin-rsc/browser";
|
|
6
7
|
import React, { startTransition, useEffect, useState } from "react";
|
|
7
8
|
import { createRoot, hydrateRoot } from "react-dom/client";
|
|
8
9
|
import { rscStream } from "rsc-html-stream/client";
|
|
9
10
|
import { jsx } from "react/jsx-runtime";
|
|
11
|
+
import { ssr } from "virtual:funstack/config";
|
|
10
12
|
|
|
11
13
|
//#region src/client/entry.tsx
|
|
12
14
|
async function devMain() {
|
|
@@ -25,7 +27,8 @@ async function devMain() {
|
|
|
25
27
|
}
|
|
26
28
|
const browserRoot = /* @__PURE__ */ jsx(React.StrictMode, { children: /* @__PURE__ */ jsx(GlobalErrorBoundary, { children: /* @__PURE__ */ jsx(BrowserRoot, {}) }) });
|
|
27
29
|
if (globalThis.__NO_HYDRATE) createRoot(document).render(browserRoot);
|
|
28
|
-
else hydrateRoot(document, browserRoot);
|
|
30
|
+
else if (ssr) hydrateRoot(document, browserRoot);
|
|
31
|
+
else createRoot(document).render(browserRoot);
|
|
29
32
|
if (import.meta.hot) import.meta.hot.on("rsc:update", () => {
|
|
30
33
|
fetchRscPayload();
|
|
31
34
|
});
|
|
@@ -36,14 +39,19 @@ async function prodMain() {
|
|
|
36
39
|
function BrowserRoot() {
|
|
37
40
|
return payload.root;
|
|
38
41
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
42
|
+
if (ssr) {
|
|
43
|
+
const browserRoot = /* @__PURE__ */ jsx(React.StrictMode, { children: /* @__PURE__ */ jsx(GlobalErrorBoundary, { children: /* @__PURE__ */ jsx(BrowserRoot, {}) }) });
|
|
44
|
+
hydrateRoot(document, browserRoot);
|
|
45
|
+
} else {
|
|
46
|
+
const browserRoot = /* @__PURE__ */ jsx(BrowserRoot, {});
|
|
47
|
+
const appRootId = manifest.marker;
|
|
48
|
+
const appMarker = document.getElementById(appRootId);
|
|
49
|
+
if (!appMarker) throw new Error(`Failed to find app root element by id "${appRootId}". This is likely a bug.`);
|
|
50
|
+
const appRoot = appMarker.parentElement;
|
|
51
|
+
if (!appRoot) throw new Error(`App root element has no parent element. This is likely a bug.`);
|
|
52
|
+
appMarker.remove();
|
|
53
|
+
createRoot(appRoot).render(browserRoot);
|
|
54
|
+
}
|
|
47
55
|
}
|
|
48
56
|
if (import.meta.env.DEV) devMain();
|
|
49
57
|
else prodMain();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"entry.mjs","names":[],"sources":["../../src/client/entry.tsx"],"sourcesContent":["
|
|
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 +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
|
|
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"}
|
package/dist/docs/FAQ.md
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# Getting Started
|
|
2
|
+
|
|
3
|
+
Welcome to **FUNSTACK Static**! Build high-performance Single Page Applications powered by React Server Components - no server required at runtime.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Install `@funstack/static` and its peer dependencies:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @funstack/static react react-dom
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or with pnpm:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pnpm add @funstack/static react react-dom
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
### 1. Configure Vite
|
|
22
|
+
|
|
23
|
+
Create or update your `vite.config.ts`:
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
import { funstackStatic } from "@funstack/static";
|
|
27
|
+
import react from "@vitejs/plugin-react";
|
|
28
|
+
import { defineConfig } from "vite";
|
|
29
|
+
|
|
30
|
+
export default defineConfig({
|
|
31
|
+
plugins: [
|
|
32
|
+
funstackStatic({
|
|
33
|
+
root: "./src/Root.tsx",
|
|
34
|
+
app: "./src/App.tsx",
|
|
35
|
+
}),
|
|
36
|
+
react(),
|
|
37
|
+
],
|
|
38
|
+
});
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### 2. Create Your Root Component
|
|
42
|
+
|
|
43
|
+
The Root component wraps your entire application and defines the HTML structure.
|
|
44
|
+
|
|
45
|
+
```tsx
|
|
46
|
+
// src/Root.tsx
|
|
47
|
+
import type React from "react";
|
|
48
|
+
|
|
49
|
+
export default function Root({ children }: { children: React.ReactNode }) {
|
|
50
|
+
return (
|
|
51
|
+
<html lang="en">
|
|
52
|
+
<head>
|
|
53
|
+
<meta charSet="UTF-8" />
|
|
54
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
55
|
+
<title>My App</title>
|
|
56
|
+
</head>
|
|
57
|
+
<body>{children}</body>
|
|
58
|
+
</html>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The Root component:
|
|
64
|
+
|
|
65
|
+
- is responsible for defining the shell HTML structure of your app
|
|
66
|
+
- is a server component
|
|
67
|
+
- **CANNOT** import client components; you could, but they are fully rendered into static HTML and never hydrated
|
|
68
|
+
|
|
69
|
+
### 3. Create Your App Component
|
|
70
|
+
|
|
71
|
+
The App component defines what comes into the `children` of the Root. This is the entrypoint of your SPA.
|
|
72
|
+
|
|
73
|
+
Since the App component is a server component, for interactivity you can import and use client components inside it.
|
|
74
|
+
|
|
75
|
+
```tsx
|
|
76
|
+
// src/App.tsx
|
|
77
|
+
import Counter from "./Counter";
|
|
78
|
+
|
|
79
|
+
export default function App() {
|
|
80
|
+
return (
|
|
81
|
+
<main>
|
|
82
|
+
<h1>Welcome to my site!</h1>
|
|
83
|
+
<Counter />
|
|
84
|
+
</main>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
```tsx
|
|
90
|
+
// src/Counter.tsx
|
|
91
|
+
"use client";
|
|
92
|
+
|
|
93
|
+
import { useState } from "react";
|
|
94
|
+
|
|
95
|
+
export default function Counter() {
|
|
96
|
+
const [count, setCount] = useState(0);
|
|
97
|
+
return <button onClick={() => setCount(count + 1)}>Count: {count}</button>;
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The App component:
|
|
102
|
+
|
|
103
|
+
- is a server component
|
|
104
|
+
- **CAN** import client components and use them within the app
|
|
105
|
+
|
|
106
|
+
### 4. Add Routing (Optional)
|
|
107
|
+
|
|
108
|
+
If you want routing, you can bring your favorite SPA router. Here, we use `@funstack/router` as an example:
|
|
109
|
+
|
|
110
|
+
```tsx
|
|
111
|
+
// src/App.tsx
|
|
112
|
+
import { Router } from "@funstack/router";
|
|
113
|
+
import { route } from "@funstack/router/server";
|
|
114
|
+
|
|
115
|
+
const routes = [
|
|
116
|
+
route({
|
|
117
|
+
path: "/",
|
|
118
|
+
component: <h1>Welcome to my site!</h1>,
|
|
119
|
+
}),
|
|
120
|
+
route({
|
|
121
|
+
path: "/about",
|
|
122
|
+
component: <h1>About Us</h1>,
|
|
123
|
+
}),
|
|
124
|
+
];
|
|
125
|
+
|
|
126
|
+
export default function App() {
|
|
127
|
+
return <Router routes={routes} />;
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### 5. Start Development Server
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
npm run dev
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Your site is now running at `http://localhost:5173`!
|
|
138
|
+
|
|
139
|
+
### 6. Build for Production
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
npm run build
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Your SPA is pre-rendered to static files in the `dist/public` directory, ready to deploy to any static hosting provider.
|
|
146
|
+
|
|
147
|
+
Since FUNSTACK Static is built on top of Vite, any Vite configurations and plugins should work seamlessly.
|
|
148
|
+
|
|
149
|
+
## Project Structure
|
|
150
|
+
|
|
151
|
+
A typical FUNSTACK Static project looks like this:
|
|
152
|
+
|
|
153
|
+
```
|
|
154
|
+
my-site/
|
|
155
|
+
├── src/
|
|
156
|
+
│ ├── ...
|
|
157
|
+
│ ├── Root.tsx # HTML wrapper component
|
|
158
|
+
│ └── App.tsx # Routes and main app
|
|
159
|
+
├── public/ # Static assets
|
|
160
|
+
├── vite.config.ts # Vite configuration
|
|
161
|
+
└── package.json
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Only two files are required: `Root.tsx` and `App.tsx`. The paths to these files can be customized in the `funstackStatic()` configuration.
|
|
165
|
+
|
|
166
|
+
## AI-Assisted Development (Optional)
|
|
167
|
+
|
|
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
|
+
|
|
170
|
+
```bash
|
|
171
|
+
npx funstack-static-skill-installer
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
This registers the `funstack-static-knowledge` skill, which provides your AI assistant with API references, best practices, and architectural guidance for FUNSTACK Static.
|
|
175
|
+
|
|
176
|
+
## What's Next?
|
|
177
|
+
|
|
178
|
+
- Learn about the [funstackStatic() Plugin API](/funstack-static/api/funstack-static) for configuration options
|
|
179
|
+
- Understand [defer()](/funstack-static/api/defer) for Server Component chunk splitting
|
|
180
|
+
- Dive into [React Server Components](/funstack-static/learn/rsc) concepts
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# defer()
|
|
2
|
+
|
|
3
|
+
The `defer()` function enables deferred rendering for React Server Components, reducing initial data load.
|
|
4
|
+
|
|
5
|
+
You can think of this as React's `lazy` API but for Server Components.
|
|
6
|
+
|
|
7
|
+
## Import
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
// @funstack/static/server is where utilities for server components live
|
|
11
|
+
import { defer } from "@funstack/static/server";
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
```tsx
|
|
17
|
+
import { defer } from "@funstack/static/server";
|
|
18
|
+
import { HeavyServerComponent } from "./HeavyServerComponent";
|
|
19
|
+
|
|
20
|
+
function Page() {
|
|
21
|
+
return (
|
|
22
|
+
<details>
|
|
23
|
+
<summary>Very long description</summary>
|
|
24
|
+
<Suspense fallback={<p>Loading...</p>}>
|
|
25
|
+
{defer(<HeavyServerComponent />)}
|
|
26
|
+
</Suspense>
|
|
27
|
+
</details>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
By using `defer()`, the `HeavyServerComponent` will still be rendered on the server (during build), but its data will be sent to the client as a separate RSC payload.
|
|
33
|
+
|
|
34
|
+
This means that:
|
|
35
|
+
|
|
36
|
+
- Client can start rendering the rest of the page without waiting for `HeavyServerComponent`'s data
|
|
37
|
+
- When the `defer(<HeavyServerComponent />)` part is rendered on the client, it will fetch the separate RSC payload and show the content.
|
|
38
|
+
|
|
39
|
+
The key point is that `HeavyServerComponent` is still a Server Component, so only the rendered HTML (and usage of Client Components inside it) is sent to the client, not the component code itself.
|
|
40
|
+
|
|
41
|
+
**Note:** `defer()` must be used inside a `Suspense` boundary since the content will be streamed in later.
|
|
42
|
+
|
|
43
|
+
## Signature
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
export function defer(element: ReactElement, options?: DeferOptions): ReactNode;
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Parameters
|
|
50
|
+
|
|
51
|
+
- **element:** A JSX element (Server Component) to render with deferred loading.
|
|
52
|
+
- **options:** (optional) Configuration options for the deferred payload.
|
|
53
|
+
|
|
54
|
+
### DeferOptions
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
interface DeferOptions {
|
|
58
|
+
/**
|
|
59
|
+
* Optional name for debugging purposes.
|
|
60
|
+
* In development: included in the RSC payload file name.
|
|
61
|
+
* In production: logged when the payload file is emitted.
|
|
62
|
+
*/
|
|
63
|
+
name?: string;
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
- **name:** An optional identifier to help with debugging. When provided:
|
|
68
|
+
- In development mode, the name is included in the RSC payload file name (e.g., `/funstack__/fun:rsc-payload/HomePage-b5698be72eea3c37`)
|
|
69
|
+
- In production mode, the name is logged when the payload file is emitted
|
|
70
|
+
|
|
71
|
+
### Returns
|
|
72
|
+
|
|
73
|
+
A React Node that will stream its content separately from the main entry point.
|
|
74
|
+
|
|
75
|
+
## When to Use defer()
|
|
76
|
+
|
|
77
|
+
Use `defer()` when you have components that:
|
|
78
|
+
|
|
79
|
+
- Renders large HTML content
|
|
80
|
+
- Is not immediately visible on page load (e.g., inside a collapsed section)
|
|
81
|
+
|
|
82
|
+
Typically, you will want to wrap route components with `defer()` to improve initial load performance. Otherwise, user needs to wait for contents for all pages to arrive before seeing anything.
|
|
83
|
+
|
|
84
|
+
```tsx
|
|
85
|
+
import { defer } from "@funstack/static/server";
|
|
86
|
+
import HomePage from "./HomePage";
|
|
87
|
+
import AboutPage from "./AboutPage";
|
|
88
|
+
|
|
89
|
+
const routes = [
|
|
90
|
+
route({
|
|
91
|
+
path: "/",
|
|
92
|
+
component: defer(<HomePage />, { name: "HomePage" }),
|
|
93
|
+
}),
|
|
94
|
+
route({
|
|
95
|
+
path: "/about",
|
|
96
|
+
component: defer(<AboutPage />, { name: "AboutPage" }),
|
|
97
|
+
}),
|
|
98
|
+
// ...
|
|
99
|
+
];
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Using the `name` option makes it easier to identify which deferred component corresponds to which payload file during development and in build logs.
|
|
103
|
+
|
|
104
|
+
## How It Works
|
|
105
|
+
|
|
106
|
+
By default, FUNSTACK Static puts the entire app (`<App />`) into one RSC payload (`/funstack__/index.txt`). The client fetches this payload to render your SPA.
|
|
107
|
+
|
|
108
|
+
When you use `defer(<Component />)`, FUNSTACK Static creates **additional RSC payloads** for the rendering result of the element. This results in an additional emit of RSC payload files like `/funstack__/fun:rsc-payload/b5698be72eea3c37`. If you provide a `name` option, the file name will include it (e.g., `/funstack__/fun:rsc-payload/HomePage-b5698be72eea3c37`).
|
|
109
|
+
|
|
110
|
+
In the main RSC payload, the `defer` call is replaced with a client component `<DeferredComponent moduleId="fun:rsc-payload/b5698be72eea3c37" />`. This component is responsible for fetching the additional RSC payload from client and renders it when it's ready.
|