@funstack/static 1.0.0 → 1.1.1
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/dist/build/buildApp.mjs +6 -6
- package/dist/build/buildApp.mjs.map +1 -1
- package/dist/build/rscPath.mjs +2 -2
- package/dist/build/rscPath.mjs.map +1 -1
- package/dist/build/rscProcessor.mjs +3 -2
- package/dist/build/rscProcessor.mjs.map +1 -1
- package/dist/docs/GettingStarted.md +1 -1
- package/dist/docs/MigratingFromViteSPA.md +1 -1
- package/dist/docs/api/FunstackStatic.md +20 -1
- package/dist/docs/learn/HowItWorks.md +1 -1
- package/dist/plugin/getRSCEntryPoint.mjs +2 -1
- package/dist/plugin/getRSCEntryPoint.mjs.map +1 -1
- package/dist/plugin/index.d.mts +14 -0
- package/dist/plugin/index.d.mts.map +1 -1
- package/dist/plugin/index.mjs +5 -3
- package/dist/plugin/index.mjs.map +1 -1
- package/dist/rsc/defer.d.mts.map +1 -1
- package/dist/rsc/defer.mjs +2 -1
- package/dist/rsc/defer.mjs.map +1 -1
- package/dist/rsc/rscModule.mjs +7 -7
- package/dist/rsc/rscModule.mjs.map +1 -1
- package/package.json +8 -8
- package/skills/funstack-static-knowledge/SKILL.md +1 -1
package/dist/build/buildApp.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import path from "node:path";
|
|
|
8
8
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
9
9
|
import { pathToFileURL } from "node:url";
|
|
10
10
|
//#region src/build/buildApp.ts
|
|
11
|
-
async function buildApp(builder, context) {
|
|
11
|
+
async function buildApp(builder, context, options) {
|
|
12
12
|
const { config } = builder;
|
|
13
13
|
const entry = await import(pathToFileURL(path.join(config.environments.rsc.build.outDir, "index.js")).href);
|
|
14
14
|
const baseDir = config.environments.client.build.outDir;
|
|
@@ -25,8 +25,8 @@ async function buildApp(builder, context) {
|
|
|
25
25
|
const dummyStream = new ReadableStream({ start(controller) {
|
|
26
26
|
controller.close();
|
|
27
27
|
} });
|
|
28
|
-
const { components, idMapping } = await processRscComponents(deferRegistry.loadAll(), dummyStream, context);
|
|
29
|
-
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);
|
|
30
30
|
for (const { finalId, finalContent, name } of components) await writeFileNormal(path.join(baseDir, getModulePathFor(finalId).replace(/^\//, "")), finalContent, context, name);
|
|
31
31
|
}
|
|
32
32
|
function normalizeBase(base) {
|
|
@@ -41,15 +41,15 @@ function replaceIdsInContent(content, idMapping) {
|
|
|
41
41
|
for (const [oldId, newId] of idMapping) if (oldId !== newId) result = result.replaceAll(oldId, newId);
|
|
42
42
|
return result;
|
|
43
43
|
}
|
|
44
|
-
async function buildSingleEntry(result, idMapping, baseDir, base, context) {
|
|
44
|
+
async function buildSingleEntry(result, idMapping, baseDir, base, rscPayloadDir, context) {
|
|
45
45
|
const { path: entryPath, html, appRsc } = result;
|
|
46
46
|
const htmlContent = await drainStream(html);
|
|
47
47
|
const appRscContent = replaceIdsInContent(await drainStream(appRsc), idMapping);
|
|
48
48
|
const mainPayloadHash = await computeContentHash(appRscContent);
|
|
49
|
-
const mainPayloadPath = base === "" ? getRscPayloadPath(mainPayloadHash) : base + getRscPayloadPath(mainPayloadHash);
|
|
49
|
+
const mainPayloadPath = base === "" ? getRscPayloadPath(mainPayloadHash, rscPayloadDir) : base + getRscPayloadPath(mainPayloadHash, rscPayloadDir);
|
|
50
50
|
const finalHtmlContent = htmlContent.replaceAll(rscPayloadPlaceholder, mainPayloadPath);
|
|
51
51
|
await writeFileNormal(path.join(baseDir, entryPath), finalHtmlContent, context);
|
|
52
|
-
await writeFileNormal(path.join(baseDir, getRscPayloadPath(mainPayloadHash).replace(/^\//, "")), appRscContent, context);
|
|
52
|
+
await writeFileNormal(path.join(baseDir, getRscPayloadPath(mainPayloadHash, rscPayloadDir).replace(/^\//, "")), appRscContent, context);
|
|
53
53
|
}
|
|
54
54
|
async function writeFileNormal(filePath, data, context, name) {
|
|
55
55
|
await mkdir(path.dirname(filePath), { recursive: true });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"buildApp.mjs","names":[],"sources":["../../src/build/buildApp.ts"],"sourcesContent":["import { mkdir, writeFile } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { pathToFileURL } from \"node:url\";\nimport type { ViteBuilder, MinimalPluginContextWithoutEnvironment } from \"vite\";\nimport { rscPayloadPlaceholder, getRscPayloadPath } from \"./rscPath\";\nimport { getModulePathFor } from \"../rsc/rscModule\";\nimport { processRscComponents } from \"./rscProcessor\";\nimport { computeContentHash } from \"./contentHash\";\nimport { drainStream } from \"../util/drainStream\";\nimport { validateEntryPath, checkDuplicatePaths } from \"./validateEntryPath\";\nimport type { EntryBuildResult } from \"../rsc/entry\";\n\nexport async function buildApp(\n builder: ViteBuilder,\n context: MinimalPluginContextWithoutEnvironment,\n) {\n const { config } = builder;\n // import server entry\n const entryPath = path.join(config.environments.rsc.build.outDir, \"index.js\");\n const entry: typeof import(\"../rsc/entry\") = await import(\n pathToFileURL(entryPath).href\n );\n\n const baseDir = config.environments.client.build.outDir;\n const base = normalizeBase(config.base);\n\n const { entries, deferRegistry } = await entry.build();\n\n // Validate all entry paths\n const paths: string[] = [];\n for (const result of entries) {\n const error = validateEntryPath(result.path);\n if (error) {\n throw new Error(error);\n }\n paths.push(result.path);\n }\n const dupError = checkDuplicatePaths(paths);\n if (dupError) {\n throw new Error(dupError);\n }\n\n // Process all deferred components once across all entries.\n // We pass a dummy empty stream since we handle per-entry RSC payloads separately.\n const dummyStream = new ReadableStream<Uint8Array>({\n start(controller) {\n controller.close();\n },\n });\n const { components, idMapping } = await processRscComponents(\n deferRegistry.loadAll(),\n dummyStream,\n context,\n );\n\n // Write each entry's HTML and RSC payload\n for (const result of entries) {\n await buildSingleEntry(result
|
|
1
|
+
{"version":3,"file":"buildApp.mjs","names":[],"sources":["../../src/build/buildApp.ts"],"sourcesContent":["import { mkdir, writeFile } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { pathToFileURL } from \"node:url\";\nimport type { ViteBuilder, MinimalPluginContextWithoutEnvironment } from \"vite\";\nimport { rscPayloadPlaceholder, getRscPayloadPath } from \"./rscPath\";\nimport { getModulePathFor } from \"../rsc/rscModule\";\nimport { processRscComponents } from \"./rscProcessor\";\nimport { computeContentHash } from \"./contentHash\";\nimport { drainStream } from \"../util/drainStream\";\nimport { validateEntryPath, checkDuplicatePaths } from \"./validateEntryPath\";\nimport type { EntryBuildResult } from \"../rsc/entry\";\n\nexport async function buildApp(\n builder: ViteBuilder,\n context: MinimalPluginContextWithoutEnvironment,\n options: { rscPayloadDir: string },\n) {\n const { config } = builder;\n // import server entry\n const entryPath = path.join(config.environments.rsc.build.outDir, \"index.js\");\n const entry: typeof import(\"../rsc/entry\") = await import(\n pathToFileURL(entryPath).href\n );\n\n const baseDir = config.environments.client.build.outDir;\n const base = normalizeBase(config.base);\n\n const { entries, deferRegistry } = await entry.build();\n\n // Validate all entry paths\n const paths: string[] = [];\n for (const result of entries) {\n const error = validateEntryPath(result.path);\n if (error) {\n throw new Error(error);\n }\n paths.push(result.path);\n }\n const dupError = checkDuplicatePaths(paths);\n if (dupError) {\n throw new Error(dupError);\n }\n\n // Process all deferred components once across all entries.\n // We pass a dummy empty stream since we handle per-entry RSC payloads separately.\n const dummyStream = new ReadableStream<Uint8Array>({\n start(controller) {\n controller.close();\n },\n });\n const { components, idMapping } = await processRscComponents(\n deferRegistry.loadAll(),\n dummyStream,\n options.rscPayloadDir,\n context,\n );\n\n // Write each entry's HTML and RSC payload\n for (const result of entries) {\n await buildSingleEntry(\n result,\n idMapping,\n baseDir,\n base,\n options.rscPayloadDir,\n context,\n );\n }\n\n // Write all deferred component payloads\n for (const { finalId, finalContent, name } of components) {\n const filePath = path.join(\n baseDir,\n getModulePathFor(finalId).replace(/^\\//, \"\"),\n );\n await writeFileNormal(filePath, finalContent, context, name);\n }\n}\n\nfunction normalizeBase(base: string): string {\n const normalized = base.endsWith(\"/\") ? base.slice(0, -1) : base;\n return normalized === \"/\" ? \"\" : normalized;\n}\n\n/**\n * Replaces temporary IDs with final hashed IDs in content.\n */\nfunction replaceIdsInContent(\n content: string,\n idMapping: Map<string, string>,\n): string {\n let result = content;\n for (const [oldId, newId] of idMapping) {\n if (oldId !== newId) {\n result = result.replaceAll(oldId, newId);\n }\n }\n return result;\n}\n\nasync function buildSingleEntry(\n result: EntryBuildResult,\n idMapping: Map<string, string>,\n baseDir: string,\n base: string,\n rscPayloadDir: string,\n context: MinimalPluginContextWithoutEnvironment,\n) {\n const { path: entryPath, html, appRsc } = result;\n\n // Drain HTML stream to string\n const htmlContent = await drainStream(html);\n\n // Drain and process RSC payload: replace temp IDs with final hashed IDs\n const rawAppRscContent = await drainStream(appRsc);\n const appRscContent = replaceIdsInContent(rawAppRscContent, idMapping);\n\n // Compute hash for this entry's RSC payload\n const mainPayloadHash = await computeContentHash(appRscContent);\n const mainPayloadPath =\n base === \"\"\n ? getRscPayloadPath(mainPayloadHash, rscPayloadDir)\n : base + getRscPayloadPath(mainPayloadHash, rscPayloadDir);\n\n // Replace placeholder with final hashed path\n const finalHtmlContent = htmlContent.replaceAll(\n rscPayloadPlaceholder,\n mainPayloadPath,\n );\n\n // entryPath is already a file name (e.g. \"index.html\", \"about.html\")\n await writeFileNormal(\n path.join(baseDir, entryPath),\n finalHtmlContent,\n context,\n );\n\n // Write RSC payload with hashed filename\n await writeFileNormal(\n path.join(\n baseDir,\n getRscPayloadPath(mainPayloadHash, rscPayloadDir).replace(/^\\//, \"\"),\n ),\n appRscContent,\n context,\n );\n}\n\nasync function writeFileNormal(\n filePath: string,\n data: string,\n context: MinimalPluginContextWithoutEnvironment,\n name?: string,\n) {\n await mkdir(path.dirname(filePath), { recursive: true });\n const nameInfo = name ? ` (${name})` : \"\";\n context.info(`[funstack] Writing ${filePath}${nameInfo}`);\n await writeFile(filePath, data);\n}\n"],"mappings":";;;;;;;;;;AAYA,eAAsB,SACpB,SACA,SACA,SACA;CACA,MAAM,EAAE,WAAW;CAGnB,MAAM,QAAuC,MAAM,OACjD,cAFgB,KAAK,KAAK,OAAO,aAAa,IAAI,MAAM,QAAQ,WAAW,CAEnD,CAAC;CAG3B,MAAM,UAAU,OAAO,aAAa,OAAO,MAAM;CACjD,MAAM,OAAO,cAAc,OAAO,KAAK;CAEvC,MAAM,EAAE,SAAS,kBAAkB,MAAM,MAAM,OAAO;CAGtD,MAAM,QAAkB,EAAE;AAC1B,MAAK,MAAM,UAAU,SAAS;EAC5B,MAAM,QAAQ,kBAAkB,OAAO,KAAK;AAC5C,MAAI,MACF,OAAM,IAAI,MAAM,MAAM;AAExB,QAAM,KAAK,OAAO,KAAK;;CAEzB,MAAM,WAAW,oBAAoB,MAAM;AAC3C,KAAI,SACF,OAAM,IAAI,MAAM,SAAS;CAK3B,MAAM,cAAc,IAAI,eAA2B,EACjD,MAAM,YAAY;AAChB,aAAW,OAAO;IAErB,CAAC;CACF,MAAM,EAAE,YAAY,cAAc,MAAM,qBACtC,cAAc,SAAS,EACvB,aACA,QAAQ,eACR,QACD;AAGD,MAAK,MAAM,UAAU,QACnB,OAAM,iBACJ,QACA,WACA,SACA,MACA,QAAQ,eACR,QACD;AAIH,MAAK,MAAM,EAAE,SAAS,cAAc,UAAU,WAK5C,OAAM,gBAJW,KAAK,KACpB,SACA,iBAAiB,QAAQ,CAAC,QAAQ,OAAO,GAAG,CAC7C,EAC+B,cAAc,SAAS,KAAK;;AAIhE,SAAS,cAAc,MAAsB;CAC3C,MAAM,aAAa,KAAK,SAAS,IAAI,GAAG,KAAK,MAAM,GAAG,GAAG,GAAG;AAC5D,QAAO,eAAe,MAAM,KAAK;;;;;AAMnC,SAAS,oBACP,SACA,WACQ;CACR,IAAI,SAAS;AACb,MAAK,MAAM,CAAC,OAAO,UAAU,UAC3B,KAAI,UAAU,MACZ,UAAS,OAAO,WAAW,OAAO,MAAM;AAG5C,QAAO;;AAGT,eAAe,iBACb,QACA,WACA,SACA,MACA,eACA,SACA;CACA,MAAM,EAAE,MAAM,WAAW,MAAM,WAAW;CAG1C,MAAM,cAAc,MAAM,YAAY,KAAK;CAI3C,MAAM,gBAAgB,oBADG,MAAM,YAAY,OAAO,EACU,UAAU;CAGtE,MAAM,kBAAkB,MAAM,mBAAmB,cAAc;CAC/D,MAAM,kBACJ,SAAS,KACL,kBAAkB,iBAAiB,cAAc,GACjD,OAAO,kBAAkB,iBAAiB,cAAc;CAG9D,MAAM,mBAAmB,YAAY,WACnC,uBACA,gBACD;AAGD,OAAM,gBACJ,KAAK,KAAK,SAAS,UAAU,EAC7B,kBACA,QACD;AAGD,OAAM,gBACJ,KAAK,KACH,SACA,kBAAkB,iBAAiB,cAAc,CAAC,QAAQ,OAAO,GAAG,CACrE,EACD,eACA,QACD;;AAGH,eAAe,gBACb,UACA,MACA,SACA,MACA;AACA,OAAM,MAAM,KAAK,QAAQ,SAAS,EAAE,EAAE,WAAW,MAAM,CAAC;CACxD,MAAM,WAAW,OAAO,KAAK,KAAK,KAAK;AACvC,SAAQ,KAAK,sBAAsB,WAAW,WAAW;AACzD,OAAM,UAAU,UAAU,KAAK"}
|
package/dist/build/rscPath.mjs
CHANGED
|
@@ -7,8 +7,8 @@ const rscPayloadPlaceholder = "__FUNSTACK_RSC_PAYLOAD_PATH__";
|
|
|
7
7
|
/**
|
|
8
8
|
* Generate final path from content hash (reuses same folder as deferred payloads)
|
|
9
9
|
*/
|
|
10
|
-
function getRscPayloadPath(contentHash) {
|
|
11
|
-
return getModulePathFor(getPayloadIDFor(contentHash));
|
|
10
|
+
function getRscPayloadPath(contentHash, rscPayloadDir) {
|
|
11
|
+
return getModulePathFor(getPayloadIDFor(contentHash, rscPayloadDir));
|
|
12
12
|
}
|
|
13
13
|
//#endregion
|
|
14
14
|
export { getRscPayloadPath, rscPayloadPlaceholder };
|
|
@@ -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,
|
|
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"}
|
|
@@ -8,9 +8,10 @@ import { findReferencedIds, topologicalSort } from "./dependencyGraph.mjs";
|
|
|
8
8
|
*
|
|
9
9
|
* @param deferRegistryIterator - Iterator yielding components with { id, data }
|
|
10
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")
|
|
11
12
|
* @param context - Optional context for logging warnings
|
|
12
13
|
*/
|
|
13
|
-
async function processRscComponents(deferRegistryIterator, appRscStream, context) {
|
|
14
|
+
async function processRscComponents(deferRegistryIterator, appRscStream, rscPayloadDir, context) {
|
|
14
15
|
const components = /* @__PURE__ */ new Map();
|
|
15
16
|
const componentNames = /* @__PURE__ */ new Map();
|
|
16
17
|
for await (const { id, data, name } of deferRegistryIterator) {
|
|
@@ -41,7 +42,7 @@ async function processRscComponents(deferRegistryIterator, appRscStream, context
|
|
|
41
42
|
for (const tempId of sorted) {
|
|
42
43
|
let content = components.get(tempId);
|
|
43
44
|
for (const [oldId, newId] of idMapping) if (oldId !== newId) content = content.replaceAll(oldId, newId);
|
|
44
|
-
const finalId = getPayloadIDFor(await computeContentHash(content));
|
|
45
|
+
const finalId = getPayloadIDFor(await computeContentHash(content), rscPayloadDir);
|
|
45
46
|
idMapping.set(tempId, finalId);
|
|
46
47
|
processedComponents.push({
|
|
47
48
|
finalId,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rscProcessor.mjs","names":[],"sources":["../../src/build/rscProcessor.ts"],"sourcesContent":["import { drainStream } from \"../util/drainStream\";\nimport { getPayloadIDFor } from \"../rsc/rscModule\";\nimport { computeContentHash } from \"./contentHash\";\nimport { findReferencedIds, topologicalSort } from \"./dependencyGraph\";\n\nexport interface ProcessedComponent {\n finalId: string;\n finalContent: string;\n name?: string;\n}\n\nexport interface ProcessResult {\n components: ProcessedComponent[];\n appRscContent: string;\n idMapping: Map<string, string>;\n}\n\ninterface RawComponent {\n id: string;\n data: string;\n name?: string;\n}\n\n/**\n * Processes RSC components by replacing temporary UUIDs with content-based hashes.\n *\n * @param deferRegistryIterator - Iterator yielding components with { id, data }\n * @param appRscStream - The main RSC stream\n * @param context - Optional context for logging warnings\n */\nexport async function processRscComponents(\n deferRegistryIterator: AsyncIterable<RawComponent>,\n appRscStream: ReadableStream,\n context?: { warn: (message: string) => void },\n): Promise<ProcessResult> {\n // Step 1: Collect all components from deferRegistry\n const components = new Map<string, string>();\n const componentNames = new Map<string, string | undefined>();\n for await (const { id, data, name } of deferRegistryIterator) {\n components.set(id, data);\n componentNames.set(id, name);\n }\n\n // Step 2: Drain appRsc stream to string\n let appRscContent = await drainStream(appRscStream);\n\n // If no components, return early\n if (components.size === 0) {\n return {\n components: [],\n appRscContent,\n idMapping: new Map(),\n };\n }\n\n const allIds = new Set(components.keys());\n\n // Step 3: Build dependency graph\n // For each component, find which other component IDs appear in its content\n const dependencies = new Map<string, Set<string>>();\n for (const [id, content] of components) {\n const otherIds = new Set(allIds);\n otherIds.delete(id); // Don't include self-references\n const refs = findReferencedIds(content, otherIds);\n dependencies.set(id, refs);\n }\n\n // Step 4: Topologically sort components\n const { sorted, inCycle } = topologicalSort(dependencies);\n\n // Step 5: Handle cycles - warn and keep original temp IDs\n const idMapping = new Map<string, string>();\n\n if (inCycle.length > 0) {\n context?.warn(\n `[funstack] Warning: ${inCycle.length} RSC component(s) are in dependency cycles and will keep unstable IDs: ${inCycle.join(\", \")}`,\n );\n for (const id of inCycle) {\n idMapping.set(id, id); // Map to itself (keep original ID)\n }\n }\n\n // Step 6: Process sorted components in order\n const processedComponents: ProcessedComponent[] = [];\n\n for (const tempId of sorted) {\n let content = components.get(tempId)!;\n\n // Replace all already-finalized temp IDs with their hash-based IDs\n for (const [oldId, newId] of idMapping) {\n if (oldId !== newId) {\n content = content.replaceAll(oldId, newId);\n }\n }\n\n // Compute content hash for this component\n const contentHash = await computeContentHash(content);\n const finalId = getPayloadIDFor(contentHash);\n\n // Create mapping\n idMapping.set(tempId, finalId);\n\n processedComponents.push({\n finalId,\n finalContent: content,\n name: componentNames.get(tempId),\n });\n }\n\n // Add cycle members to processed components (with original IDs)\n for (const tempId of inCycle) {\n let content = components.get(tempId)!;\n\n // Replace finalized IDs in cycle member content\n for (const [oldId, newId] of idMapping) {\n if (oldId !== newId) {\n content = content.replaceAll(oldId, newId);\n }\n }\n\n processedComponents.push({\n finalId: tempId, // Keep original temp ID\n finalContent: content,\n name: componentNames.get(tempId),\n });\n }\n\n // Step 7: Process appRsc - replace all temp IDs with final IDs\n for (const [oldId, newId] of idMapping) {\n if (oldId !== newId) {\n appRscContent = appRscContent.replaceAll(oldId, newId);\n }\n }\n\n return {\n components: processedComponents,\n appRscContent,\n idMapping,\n };\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"rscProcessor.mjs","names":[],"sources":["../../src/build/rscProcessor.ts"],"sourcesContent":["import { drainStream } from \"../util/drainStream\";\nimport { getPayloadIDFor } from \"../rsc/rscModule\";\nimport { computeContentHash } from \"./contentHash\";\nimport { findReferencedIds, topologicalSort } from \"./dependencyGraph\";\n\nexport interface ProcessedComponent {\n finalId: string;\n finalContent: string;\n name?: string;\n}\n\nexport interface ProcessResult {\n components: ProcessedComponent[];\n appRscContent: string;\n idMapping: Map<string, string>;\n}\n\ninterface RawComponent {\n id: string;\n data: string;\n name?: string;\n}\n\n/**\n * Processes RSC components by replacing temporary UUIDs with content-based hashes.\n *\n * @param deferRegistryIterator - Iterator yielding components with { id, data }\n * @param appRscStream - The main RSC stream\n * @param rscPayloadDir - Directory name used as a prefix for RSC payload IDs (e.g. \"fun:rsc-payload\")\n * @param context - Optional context for logging warnings\n */\nexport async function processRscComponents(\n deferRegistryIterator: AsyncIterable<RawComponent>,\n appRscStream: ReadableStream,\n rscPayloadDir: string,\n context?: { warn: (message: string) => void },\n): Promise<ProcessResult> {\n // Step 1: Collect all components from deferRegistry\n const components = new Map<string, string>();\n const componentNames = new Map<string, string | undefined>();\n for await (const { id, data, name } of deferRegistryIterator) {\n components.set(id, data);\n componentNames.set(id, name);\n }\n\n // Step 2: Drain appRsc stream to string\n let appRscContent = await drainStream(appRscStream);\n\n // If no components, return early\n if (components.size === 0) {\n return {\n components: [],\n appRscContent,\n idMapping: new Map(),\n };\n }\n\n const allIds = new Set(components.keys());\n\n // Step 3: Build dependency graph\n // For each component, find which other component IDs appear in its content\n const dependencies = new Map<string, Set<string>>();\n for (const [id, content] of components) {\n const otherIds = new Set(allIds);\n otherIds.delete(id); // Don't include self-references\n const refs = findReferencedIds(content, otherIds);\n dependencies.set(id, refs);\n }\n\n // Step 4: Topologically sort components\n const { sorted, inCycle } = topologicalSort(dependencies);\n\n // Step 5: Handle cycles - warn and keep original temp IDs\n const idMapping = new Map<string, string>();\n\n if (inCycle.length > 0) {\n context?.warn(\n `[funstack] Warning: ${inCycle.length} RSC component(s) are in dependency cycles and will keep unstable IDs: ${inCycle.join(\", \")}`,\n );\n for (const id of inCycle) {\n idMapping.set(id, id); // Map to itself (keep original ID)\n }\n }\n\n // Step 6: Process sorted components in order\n const processedComponents: ProcessedComponent[] = [];\n\n for (const tempId of sorted) {\n let content = components.get(tempId)!;\n\n // Replace all already-finalized temp IDs with their hash-based IDs\n for (const [oldId, newId] of idMapping) {\n if (oldId !== newId) {\n content = content.replaceAll(oldId, newId);\n }\n }\n\n // Compute content hash for this component\n const contentHash = await computeContentHash(content);\n const finalId = getPayloadIDFor(contentHash, rscPayloadDir);\n\n // Create mapping\n idMapping.set(tempId, finalId);\n\n processedComponents.push({\n finalId,\n finalContent: content,\n name: componentNames.get(tempId),\n });\n }\n\n // Add cycle members to processed components (with original IDs)\n for (const tempId of inCycle) {\n let content = components.get(tempId)!;\n\n // Replace finalized IDs in cycle member content\n for (const [oldId, newId] of idMapping) {\n if (oldId !== newId) {\n content = content.replaceAll(oldId, newId);\n }\n }\n\n processedComponents.push({\n finalId: tempId, // Keep original temp ID\n finalContent: content,\n name: componentNames.get(tempId),\n });\n }\n\n // Step 7: Process appRsc - replace all temp IDs with final IDs\n for (const [oldId, newId] of idMapping) {\n if (oldId !== newId) {\n appRscContent = appRscContent.replaceAll(oldId, newId);\n }\n }\n\n return {\n components: processedComponents,\n appRscContent,\n idMapping,\n };\n}\n"],"mappings":";;;;;;;;;;;;;AA+BA,eAAsB,qBACpB,uBACA,cACA,eACA,SACwB;CAExB,MAAM,6BAAa,IAAI,KAAqB;CAC5C,MAAM,iCAAiB,IAAI,KAAiC;AAC5D,YAAW,MAAM,EAAE,IAAI,MAAM,UAAU,uBAAuB;AAC5D,aAAW,IAAI,IAAI,KAAK;AACxB,iBAAe,IAAI,IAAI,KAAK;;CAI9B,IAAI,gBAAgB,MAAM,YAAY,aAAa;AAGnD,KAAI,WAAW,SAAS,EACtB,QAAO;EACL,YAAY,EAAE;EACd;EACA,2BAAW,IAAI,KAAK;EACrB;CAGH,MAAM,SAAS,IAAI,IAAI,WAAW,MAAM,CAAC;CAIzC,MAAM,+BAAe,IAAI,KAA0B;AACnD,MAAK,MAAM,CAAC,IAAI,YAAY,YAAY;EACtC,MAAM,WAAW,IAAI,IAAI,OAAO;AAChC,WAAS,OAAO,GAAG;EACnB,MAAM,OAAO,kBAAkB,SAAS,SAAS;AACjD,eAAa,IAAI,IAAI,KAAK;;CAI5B,MAAM,EAAE,QAAQ,YAAY,gBAAgB,aAAa;CAGzD,MAAM,4BAAY,IAAI,KAAqB;AAE3C,KAAI,QAAQ,SAAS,GAAG;AACtB,WAAS,KACP,uBAAuB,QAAQ,OAAO,yEAAyE,QAAQ,KAAK,KAAK,GAClI;AACD,OAAK,MAAM,MAAM,QACf,WAAU,IAAI,IAAI,GAAG;;CAKzB,MAAM,sBAA4C,EAAE;AAEpD,MAAK,MAAM,UAAU,QAAQ;EAC3B,IAAI,UAAU,WAAW,IAAI,OAAO;AAGpC,OAAK,MAAM,CAAC,OAAO,UAAU,UAC3B,KAAI,UAAU,MACZ,WAAU,QAAQ,WAAW,OAAO,MAAM;EAM9C,MAAM,UAAU,gBADI,MAAM,mBAAmB,QAAQ,EACR,cAAc;AAG3D,YAAU,IAAI,QAAQ,QAAQ;AAE9B,sBAAoB,KAAK;GACvB;GACA,cAAc;GACd,MAAM,eAAe,IAAI,OAAO;GACjC,CAAC;;AAIJ,MAAK,MAAM,UAAU,SAAS;EAC5B,IAAI,UAAU,WAAW,IAAI,OAAO;AAGpC,OAAK,MAAM,CAAC,OAAO,UAAU,UAC3B,KAAI,UAAU,MACZ,WAAU,QAAQ,WAAW,OAAO,MAAM;AAI9C,sBAAoB,KAAK;GACvB,SAAS;GACT,cAAc;GACd,MAAM,eAAe,IAAI,OAAO;GACjC,CAAC;;AAIJ,MAAK,MAAM,CAAC,OAAO,UAAU,UAC3B,KAAI,UAAU,MACZ,iBAAgB,cAAc,WAAW,OAAO,MAAM;AAI1D,QAAO;EACL,YAAY;EACZ;EACA;EACD"}
|
|
@@ -23,7 +23,7 @@ pnpm add @funstack/static react react-dom
|
|
|
23
23
|
Create or update your `vite.config.ts`:
|
|
24
24
|
|
|
25
25
|
```typescript
|
|
26
|
-
import
|
|
26
|
+
import funstackStatic from "@funstack/static";
|
|
27
27
|
import react from "@vitejs/plugin-react";
|
|
28
28
|
import { defineConfig } from "vite";
|
|
29
29
|
|
|
@@ -34,7 +34,7 @@ pnpm add @funstack/static
|
|
|
34
34
|
Modify your `vite.config.ts` to add the FUNSTACK Static plugin:
|
|
35
35
|
|
|
36
36
|
```typescript
|
|
37
|
-
import
|
|
37
|
+
import funstackStatic from "@funstack/static";
|
|
38
38
|
import react from "@vitejs/plugin-react";
|
|
39
39
|
import { defineConfig } from "vite";
|
|
40
40
|
|
|
@@ -226,13 +226,32 @@ Sentry.init({
|
|
|
226
226
|
|
|
227
227
|
**Note:** Errors in the client init module will propagate normally and prevent the app from rendering.
|
|
228
228
|
|
|
229
|
+
### rscPayloadDir (optional)
|
|
230
|
+
|
|
231
|
+
**Type:** `string`
|
|
232
|
+
**Default:** `"fun:rsc-payload"`
|
|
233
|
+
|
|
234
|
+
Directory name used for RSC payload files in the build output. The final file paths follow the pattern `/funstack__/{rscPayloadDir}/{hash}.txt`.
|
|
235
|
+
|
|
236
|
+
Change this if your hosting platform has issues with the default directory name. For example, Cloudflare Workers redirects URLs containing colons to percent-encoded equivalents, adding an extra round trip.
|
|
237
|
+
|
|
238
|
+
**Important:** The value is used as a marker for string replacement during the build process. Choose a value that is unique enough that it does not appear in your application's source code. The default value `"fun:rsc-payload"` is designed to be unlikely to collide with user code.
|
|
239
|
+
|
|
240
|
+
```typescript
|
|
241
|
+
funstackStatic({
|
|
242
|
+
root: "./src/root.tsx",
|
|
243
|
+
app: "./src/App.tsx",
|
|
244
|
+
rscPayloadDir: "fun-rsc-payload", // Avoid colons for Cloudflare Workers
|
|
245
|
+
});
|
|
246
|
+
```
|
|
247
|
+
|
|
229
248
|
## Full Example
|
|
230
249
|
|
|
231
250
|
### Single-Entry
|
|
232
251
|
|
|
233
252
|
```typescript
|
|
234
253
|
// vite.config.ts
|
|
235
|
-
import
|
|
254
|
+
import funstackStatic from "@funstack/static";
|
|
236
255
|
import { defineConfig } from "vite";
|
|
237
256
|
|
|
238
257
|
export default defineConfig({
|
|
@@ -50,7 +50,7 @@ dist/public
|
|
|
50
50
|
└── index.html
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
-
The RSC payload files under `funstack__` are loaded by the client-side code to bootstrap the application with server-rendered content.
|
|
53
|
+
The RSC payload files under `funstack__` are loaded by the client-side code to bootstrap the application with server-rendered content. The `fun:rsc-payload` directory name is [configurable](/api/funstack-static#rscpayloaddir-optional) via the `rscPayloadDir` option.
|
|
54
54
|
|
|
55
55
|
This can been seen as an **optimized version of traditional client-only SPAs**, where the entire application is bundled into JavaScript files. By using RSC, some of the rendering work is offloaded to the build time, resulting in smaller JavaScript bundles combined with RSC payloads that require less client-side processing (parsing is easier, no JavaScript execution needed).
|
|
56
56
|
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* Get the entry point module of the RSC environment.
|
|
4
4
|
*/
|
|
5
5
|
async function getRSCEntryPoint(environment) {
|
|
6
|
-
const
|
|
6
|
+
const buildConfig = environment.config.build;
|
|
7
|
+
const rscInput = (buildConfig.rolldownOptions ?? buildConfig.rollupOptions)?.input;
|
|
7
8
|
const source = rscInput !== void 0 && typeof rscInput !== "string" && !Array.isArray(rscInput) ? rscInput.index : void 0;
|
|
8
9
|
if (source === void 0) throw new Error("Cannot determine RSC entry point");
|
|
9
10
|
const resolved = await environment.pluginContainer.resolveId(source);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"getRSCEntryPoint.mjs","names":[],"sources":["../../src/plugin/getRSCEntryPoint.ts"],"sourcesContent":["import type { RunnableDevEnvironment } from \"vite\";\n\n/**\n * Get the entry point module of the RSC environment.\n */\nexport async function getRSCEntryPoint(environment: RunnableDevEnvironment) {\n const
|
|
1
|
+
{"version":3,"file":"getRSCEntryPoint.mjs","names":[],"sources":["../../src/plugin/getRSCEntryPoint.ts"],"sourcesContent":["import type { RunnableDevEnvironment } from \"vite\";\n\n/**\n * Get the entry point module of the RSC environment.\n */\nexport async function getRSCEntryPoint(environment: RunnableDevEnvironment) {\n // Vite 8 renamed rollupOptions to rolldownOptions; support both for Vite 7 compat\n const buildConfig = environment.config.build;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Vite 7 compat\n const rscInput = (\n buildConfig.rolldownOptions ?? (buildConfig as any).rollupOptions\n )?.input;\n const source =\n rscInput !== undefined &&\n typeof rscInput !== \"string\" &&\n !Array.isArray(rscInput)\n ? rscInput.index\n : undefined;\n if (source === undefined) {\n throw new Error(\"Cannot determine RSC entry point\");\n }\n const resolved = await environment.pluginContainer.resolveId(source);\n if (!resolved) {\n throw new Error(`Cannot resolve RSC entry: ${source}`);\n }\n const rscEntry = await environment.runner.import<\n typeof import(\"../rsc/entry\")\n >(resolved.id);\n return rscEntry;\n}\n"],"mappings":";;;;AAKA,eAAsB,iBAAiB,aAAqC;CAE1E,MAAM,cAAc,YAAY,OAAO;CAEvC,MAAM,YACJ,YAAY,mBAAoB,YAAoB,gBACnD;CACH,MAAM,SACJ,aAAa,KAAA,KACb,OAAO,aAAa,YACpB,CAAC,MAAM,QAAQ,SAAS,GACpB,SAAS,QACT,KAAA;AACN,KAAI,WAAW,KAAA,EACb,OAAM,IAAI,MAAM,mCAAmC;CAErD,MAAM,WAAW,MAAM,YAAY,gBAAgB,UAAU,OAAO;AACpE,KAAI,CAAC,SACH,OAAM,IAAI,MAAM,6BAA6B,SAAS;AAKxD,QAHiB,MAAM,YAAY,OAAO,OAExC,SAAS,GAAG"}
|
package/dist/plugin/index.d.mts
CHANGED
|
@@ -22,6 +22,20 @@ interface FunstackStaticBaseOptions {
|
|
|
22
22
|
* The module is imported for its side effects only (no exports needed).
|
|
23
23
|
*/
|
|
24
24
|
clientInit?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Directory name used for RSC payload files in the build output.
|
|
27
|
+
* The final path will be `/funstack__/{rscPayloadDir}/{hash}.txt`.
|
|
28
|
+
*
|
|
29
|
+
* Change this if your hosting platform has issues with the default
|
|
30
|
+
* directory name (e.g. Cloudflare Workers redirects URLs containing colons).
|
|
31
|
+
*
|
|
32
|
+
* The value is used as a marker for string replacement during the build
|
|
33
|
+
* process, so it should be unique enough that it does not appear in your
|
|
34
|
+
* application's source code.
|
|
35
|
+
*
|
|
36
|
+
* @default "fun:rsc-payload"
|
|
37
|
+
*/
|
|
38
|
+
rscPayloadDir?: string;
|
|
25
39
|
}
|
|
26
40
|
interface SingleEntryOptions {
|
|
27
41
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/plugin/index.ts"],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/plugin/index.ts"],"mappings":";;;UAOU,yBAAA;;AANyB;;;;EAYjC,YAAA;EAQA;;;;;AAoBa;;EApBb,GAAA;EAuB0B;;;;;EAjB1B,UAAA;EAgCQ;;;;;;;;;AAUV;;;;EA5BE,aAAA;AAAA;AAAA,UAGQ,kBAAA;EA0BoC;;;;;EApB5C,IAAA;EAsBsB;;;;EAjBtB,GAAA;EACA,OAAA;AAAA;AAAA,UAGQ,sBAAA;EACR,IAAA;EACA,GAAA;EAaE;;;;EARF,OAAA;AAAA;AAAA,KAGU,qBAAA,GAAwB,yBAAA,IACjC,kBAAA,GAAqB,sBAAA;AAAA,iBAEA,cAAA,CACtB,OAAA,EAAS,qBAAA,IACP,MAAA,GAAS,MAAA"}
|
package/dist/plugin/index.mjs
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
import { defaultRscPayloadDir } from "../rsc/rscModule.mjs";
|
|
1
2
|
import { buildApp } from "../build/buildApp.mjs";
|
|
2
3
|
import { serverPlugin } from "./server.mjs";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import rsc from "@vitejs/plugin-rsc";
|
|
5
6
|
//#region src/plugin/index.ts
|
|
6
7
|
function funstackStatic(options) {
|
|
7
|
-
const { publicOutDir = "dist/public", ssr = false, clientInit } = options;
|
|
8
|
+
const { publicOutDir = "dist/public", ssr = false, clientInit, rscPayloadDir = defaultRscPayloadDir } = options;
|
|
9
|
+
if (!rscPayloadDir || rscPayloadDir.includes("/") || rscPayloadDir.includes("\\") || rscPayloadDir === ".." || rscPayloadDir === ".") throw new Error(`[funstack] Invalid rscPayloadDir: "${rscPayloadDir}". Must be a non-empty single path segment without slashes.`);
|
|
8
10
|
let resolvedEntriesModule = "__uninitialized__";
|
|
9
11
|
let resolvedClientInitEntry;
|
|
10
12
|
const isMultiEntry = "entries" in options && options.entries !== void 0;
|
|
@@ -67,7 +69,7 @@ function funstackStatic(options) {
|
|
|
67
69
|
`}`
|
|
68
70
|
].join("\n");
|
|
69
71
|
}
|
|
70
|
-
if (id === "\0virtual:funstack/config") return `export const ssr = ${JSON.stringify(ssr)}
|
|
72
|
+
if (id === "\0virtual:funstack/config") return [`export const ssr = ${JSON.stringify(ssr)};`, `export const rscPayloadDir = ${JSON.stringify(rscPayloadDir)};`].join("\n");
|
|
71
73
|
if (id === "\0virtual:funstack/client-init") {
|
|
72
74
|
if (resolvedClientInitEntry) return `import "${resolvedClientInitEntry}";`;
|
|
73
75
|
return "";
|
|
@@ -77,7 +79,7 @@ function funstackStatic(options) {
|
|
|
77
79
|
{
|
|
78
80
|
name: "@funstack/static:build",
|
|
79
81
|
async buildApp(builder) {
|
|
80
|
-
await buildApp(builder, this);
|
|
82
|
+
await buildApp(builder, this, { rscPayloadDir });
|
|
81
83
|
}
|
|
82
84
|
}
|
|
83
85
|
];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/plugin/index.ts"],"sourcesContent":["import path from \"node:path\";\nimport type { Plugin } from \"vite\";\nimport rsc from \"@vitejs/plugin-rsc\";\nimport { buildApp } from \"../build/buildApp\";\nimport { serverPlugin } from \"./server\";\n\ninterface FunstackStaticBaseOptions {\n /**\n * Output directory for build.\n *\n * @default dist/public\n */\n publicOutDir?: string;\n /**\n * Enable server-side rendering of the App component.\n * When false, only the Root shell is SSR'd and the App renders client-side.\n * When true, both Root and App are SSR'd and the client hydrates.\n *\n * @default false\n */\n ssr?: boolean;\n /**\n * Path to a module that runs on the client side before React hydration.\n * Use this for client-side instrumentation like Sentry, analytics, or feature flags.\n * The module is imported for its side effects only (no exports needed).\n */\n clientInit?: string;\n}\n\ninterface SingleEntryOptions {\n /**\n * Root component of the page.\n * The file should `export default` a React component that renders the whole page.\n * (`<html>...</html>`).\n */\n root: string;\n /**\n * Entry point of your application.\n * The file should `export default` a React component that renders the application content.\n */\n app: string;\n entries?: never;\n}\n\ninterface MultipleEntriesOptions {\n root?: never;\n app?: never;\n /**\n * Path to a module that exports a function returning entry definitions.\n * Mutually exclusive with `root`+`app`.\n */\n entries: string;\n}\n\nexport type FunstackStaticOptions = FunstackStaticBaseOptions &\n (SingleEntryOptions | MultipleEntriesOptions);\n\nexport default function funstackStatic(\n options: FunstackStaticOptions,\n): (Plugin | Plugin[])[] {\n const {
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/plugin/index.ts"],"sourcesContent":["import path from \"node:path\";\nimport type { Plugin } from \"vite\";\nimport rsc from \"@vitejs/plugin-rsc\";\nimport { buildApp } from \"../build/buildApp\";\nimport { serverPlugin } from \"./server\";\nimport { defaultRscPayloadDir } from \"../rsc/rscModule\";\n\ninterface FunstackStaticBaseOptions {\n /**\n * Output directory for build.\n *\n * @default dist/public\n */\n publicOutDir?: string;\n /**\n * Enable server-side rendering of the App component.\n * When false, only the Root shell is SSR'd and the App renders client-side.\n * When true, both Root and App are SSR'd and the client hydrates.\n *\n * @default false\n */\n ssr?: boolean;\n /**\n * Path to a module that runs on the client side before React hydration.\n * Use this for client-side instrumentation like Sentry, analytics, or feature flags.\n * The module is imported for its side effects only (no exports needed).\n */\n clientInit?: string;\n /**\n * Directory name used for RSC payload files in the build output.\n * The final path will be `/funstack__/{rscPayloadDir}/{hash}.txt`.\n *\n * Change this if your hosting platform has issues with the default\n * directory name (e.g. Cloudflare Workers redirects URLs containing colons).\n *\n * The value is used as a marker for string replacement during the build\n * process, so it should be unique enough that it does not appear in your\n * application's source code.\n *\n * @default \"fun:rsc-payload\"\n */\n rscPayloadDir?: string;\n}\n\ninterface SingleEntryOptions {\n /**\n * Root component of the page.\n * The file should `export default` a React component that renders the whole page.\n * (`<html>...</html>`).\n */\n root: string;\n /**\n * Entry point of your application.\n * The file should `export default` a React component that renders the application content.\n */\n app: string;\n entries?: never;\n}\n\ninterface MultipleEntriesOptions {\n root?: never;\n app?: never;\n /**\n * Path to a module that exports a function returning entry definitions.\n * Mutually exclusive with `root`+`app`.\n */\n entries: string;\n}\n\nexport type FunstackStaticOptions = FunstackStaticBaseOptions &\n (SingleEntryOptions | MultipleEntriesOptions);\n\nexport default function funstackStatic(\n options: FunstackStaticOptions,\n): (Plugin | Plugin[])[] {\n const {\n publicOutDir = \"dist/public\",\n ssr = false,\n clientInit,\n rscPayloadDir = defaultRscPayloadDir,\n } = options;\n\n // Validate rscPayloadDir to prevent path traversal or invalid segments\n if (\n !rscPayloadDir ||\n rscPayloadDir.includes(\"/\") ||\n rscPayloadDir.includes(\"\\\\\") ||\n rscPayloadDir === \"..\" ||\n rscPayloadDir === \".\"\n ) {\n throw new Error(\n `[funstack] Invalid rscPayloadDir: \"${rscPayloadDir}\". Must be a non-empty single path segment without slashes.`,\n );\n }\n\n let resolvedEntriesModule: string = \"__uninitialized__\";\n let resolvedClientInitEntry: string | undefined;\n\n // Determine whether user specified entries or root+app\n const isMultiEntry = \"entries\" in options && options.entries !== undefined;\n\n return [\n {\n name: \"@funstack/static:config-pre\",\n // Placed early because the rsc plugin sets the outDir to the default value\n config(config) {\n return {\n environments: {\n client: {\n build: {\n outDir:\n config.environments?.client?.build?.outDir ?? publicOutDir,\n },\n },\n },\n };\n },\n },\n serverPlugin(),\n rsc({\n entries: {\n rsc: \"@funstack/static/entries/rsc\",\n ssr: \"@funstack/static/entries/ssr\",\n client: \"@funstack/static/entries/client\",\n },\n serverHandler: false,\n }),\n {\n name: \"@funstack/static:config\",\n configResolved(config) {\n if (isMultiEntry) {\n resolvedEntriesModule = path.resolve(config.root, options.entries);\n } else {\n // For single-entry, we store both resolved paths to generate a\n // synthetic entries module in the virtual module loader.\n const resolvedRoot = path.resolve(config.root, options.root);\n const resolvedApp = path.resolve(config.root, options.app);\n // Encode as JSON for safe embedding in generated code\n resolvedEntriesModule = JSON.stringify({\n root: resolvedRoot,\n app: resolvedApp,\n });\n }\n if (clientInit) {\n resolvedClientInitEntry = path.resolve(config.root, clientInit);\n }\n },\n configEnvironment(_name, config) {\n if (!config.optimizeDeps) {\n config.optimizeDeps = {};\n }\n // Needed for properly bundling @vitejs/plugin-rsc for browser.\n // See: https://github.com/vitejs/vite-plugin-react/tree/79bf57cc8b9c77e33970ec2e876bd6d2f1568d5d/packages/plugin-rsc#using-vitejsplugin-rsc-as-a-framework-packages-dependencies\n if (config.optimizeDeps.include) {\n config.optimizeDeps.include = config.optimizeDeps.include.map(\n (entry) => {\n if (entry.startsWith(\"@vitejs/plugin-rsc\")) {\n entry = `@funstack/static > ${entry}`;\n }\n return entry;\n },\n );\n }\n if (!config.optimizeDeps.exclude) {\n config.optimizeDeps.exclude = [];\n }\n // Since code includes imports to virtual modules, we need to exclude\n // us from Optimize Deps.\n config.optimizeDeps.exclude.push(\"@funstack/static\");\n },\n },\n {\n name: \"@funstack/static:virtual-entry\",\n resolveId(id) {\n if (id === \"virtual:funstack/entries\") {\n return \"\\0virtual:funstack/entries\";\n }\n if (id === \"virtual:funstack/config\") {\n return \"\\0virtual:funstack/config\";\n }\n if (id === \"virtual:funstack/client-init\") {\n return \"\\0virtual:funstack/client-init\";\n }\n },\n load(id) {\n if (id === \"\\0virtual:funstack/entries\") {\n if (isMultiEntry) {\n // Re-export the user's entries module\n return `export { default } from \"${resolvedEntriesModule}\";`;\n }\n // Synthesize a single-entry array from root+app\n const { root, app } = JSON.parse(resolvedEntriesModule);\n return [\n `import Root from \"${root}\";`,\n `import App from \"${app}\";`,\n `export default function getEntries() {`,\n ` return [{ path: \"index.html\", root: { default: Root }, app: { default: App } }];`,\n `}`,\n ].join(\"\\n\");\n }\n if (id === \"\\0virtual:funstack/config\") {\n return [\n `export const ssr = ${JSON.stringify(ssr)};`,\n `export const rscPayloadDir = ${JSON.stringify(rscPayloadDir)};`,\n ].join(\"\\n\");\n }\n if (id === \"\\0virtual:funstack/client-init\") {\n if (resolvedClientInitEntry) {\n return `import \"${resolvedClientInitEntry}\";`;\n }\n return \"\";\n }\n },\n },\n {\n name: \"@funstack/static:build\",\n async buildApp(builder) {\n await buildApp(builder, this, { rscPayloadDir });\n },\n },\n ];\n}\n"],"mappings":";;;;;;AAwEA,SAAwB,eACtB,SACuB;CACvB,MAAM,EACJ,eAAe,eACf,MAAM,OACN,YACA,gBAAgB,yBACd;AAGJ,KACE,CAAC,iBACD,cAAc,SAAS,IAAI,IAC3B,cAAc,SAAS,KAAK,IAC5B,kBAAkB,QAClB,kBAAkB,IAElB,OAAM,IAAI,MACR,sCAAsC,cAAc,6DACrD;CAGH,IAAI,wBAAgC;CACpC,IAAI;CAGJ,MAAM,eAAe,aAAa,WAAW,QAAQ,YAAY,KAAA;AAEjE,QAAO;EACL;GACE,MAAM;GAEN,OAAO,QAAQ;AACb,WAAO,EACL,cAAc,EACZ,QAAQ,EACN,OAAO,EACL,QACE,OAAO,cAAc,QAAQ,OAAO,UAAU,cACjD,EACF,EACF,EACF;;GAEJ;EACD,cAAc;EACd,IAAI;GACF,SAAS;IACP,KAAK;IACL,KAAK;IACL,QAAQ;IACT;GACD,eAAe;GAChB,CAAC;EACF;GACE,MAAM;GACN,eAAe,QAAQ;AACrB,QAAI,aACF,yBAAwB,KAAK,QAAQ,OAAO,MAAM,QAAQ,QAAQ;SAC7D;KAGL,MAAM,eAAe,KAAK,QAAQ,OAAO,MAAM,QAAQ,KAAK;KAC5D,MAAM,cAAc,KAAK,QAAQ,OAAO,MAAM,QAAQ,IAAI;AAE1D,6BAAwB,KAAK,UAAU;MACrC,MAAM;MACN,KAAK;MACN,CAAC;;AAEJ,QAAI,WACF,2BAA0B,KAAK,QAAQ,OAAO,MAAM,WAAW;;GAGnE,kBAAkB,OAAO,QAAQ;AAC/B,QAAI,CAAC,OAAO,aACV,QAAO,eAAe,EAAE;AAI1B,QAAI,OAAO,aAAa,QACtB,QAAO,aAAa,UAAU,OAAO,aAAa,QAAQ,KACvD,UAAU;AACT,SAAI,MAAM,WAAW,qBAAqB,CACxC,SAAQ,sBAAsB;AAEhC,YAAO;MAEV;AAEH,QAAI,CAAC,OAAO,aAAa,QACvB,QAAO,aAAa,UAAU,EAAE;AAIlC,WAAO,aAAa,QAAQ,KAAK,mBAAmB;;GAEvD;EACD;GACE,MAAM;GACN,UAAU,IAAI;AACZ,QAAI,OAAO,2BACT,QAAO;AAET,QAAI,OAAO,0BACT,QAAO;AAET,QAAI,OAAO,+BACT,QAAO;;GAGX,KAAK,IAAI;AACP,QAAI,OAAO,8BAA8B;AACvC,SAAI,aAEF,QAAO,4BAA4B,sBAAsB;KAG3D,MAAM,EAAE,MAAM,QAAQ,KAAK,MAAM,sBAAsB;AACvD,YAAO;MACL,qBAAqB,KAAK;MAC1B,oBAAoB,IAAI;MACxB;MACA;MACA;MACD,CAAC,KAAK,KAAK;;AAEd,QAAI,OAAO,4BACT,QAAO,CACL,sBAAsB,KAAK,UAAU,IAAI,CAAC,IAC1C,gCAAgC,KAAK,UAAU,cAAc,CAAC,GAC/D,CAAC,KAAK,KAAK;AAEd,QAAI,OAAO,kCAAkC;AAC3C,SAAI,wBACF,QAAO,WAAW,wBAAwB;AAE5C,YAAO;;;GAGZ;EACD;GACE,MAAM;GACN,MAAM,SAAS,SAAS;AACtB,UAAM,SAAS,SAAS,MAAM,EAAE,eAAe,CAAC;;GAEnD;EACF"}
|
package/dist/rsc/defer.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"defer.d.mts","names":[],"sources":["../../src/rsc/defer.tsx"],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"defer.d.mts","names":[],"sources":["../../src/rsc/defer.tsx"],"mappings":";;;UAOiB,UAAA;EACf,KAAA,EAAO,eAAA;EACP,IAAA;EACA,YAAA,GAAe,OAAA;AAAA;;;;UAMA,YAAA;EANf;;;;AAMF;EAME,IAAA;AAAA;AAAA,UAGe,gBAAA,SAAyB,UAAA;EACxC,KAAA,EAAO,OAAA,CAAQ,eAAA;IAAmB,KAAA;EAAA;EAClC,YAAA,EAAc,OAAA;AAAA;AAAA,KAGX,eAAA;EAEC,KAAA;EACA,OAAA,EAAS,YAAA;AAAA;EAGT,KAAA;EACA,MAAA,EAAQ,cAAA,CAAe,UAAA;AAAA;EAGvB,KAAA;AAAA;EAGA,KAAA;EACA,KAAA;AAAA;AAAA,cAeO,aAAA;EAAA;EAGX,QAAA,CAAS,OAAA,EAAS,YAAA,EAAc,EAAA,UAAY,IAAA;EAI5C,IAAA,CAAK,EAAA,WAAa,gBAAA;EAkClB,GAAA,CAAI,EAAA;EAnES;;;;EA2EN,OAAA,CAAA,GAAO,cAAA;;;;;;;;;;;;AAjDhB;;;;iBAuHgB,KAAA,CACd,OAAA,EAAS,YAAA,EACT,OAAA,GAAU,YAAA,GACT,SAAA"}
|
package/dist/rsc/defer.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getPayloadIDFor } from "./rscModule.mjs";
|
|
2
2
|
import { drainStream } from "../util/drainStream.mjs";
|
|
3
3
|
import { jsx } from "react/jsx-runtime";
|
|
4
|
+
import { rscPayloadDir } from "virtual:funstack/config";
|
|
4
5
|
import { renderToReadableStream } from "@vitejs/plugin-rsc/react/rsc";
|
|
5
6
|
import { DeferredComponent } from "#rsc-client";
|
|
6
7
|
//#region src/rsc/defer.tsx
|
|
@@ -110,7 +111,7 @@ const deferRegistry = new DeferRegistry();
|
|
|
110
111
|
function defer(element, options) {
|
|
111
112
|
const name = options?.name;
|
|
112
113
|
const sanitizedName = name ? sanitizeName(name) : void 0;
|
|
113
|
-
const id = getPayloadIDFor(sanitizedName ? `${sanitizedName}-${crypto.randomUUID()}` : crypto.randomUUID());
|
|
114
|
+
const id = getPayloadIDFor(sanitizedName ? `${sanitizedName}-${crypto.randomUUID()}` : crypto.randomUUID(), rscPayloadDir);
|
|
114
115
|
deferRegistry.register(element, id, name);
|
|
115
116
|
return /* @__PURE__ */ jsx(DeferredComponent, { moduleID: id });
|
|
116
117
|
}
|
package/dist/rsc/defer.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"defer.mjs","names":["#registry","#loadEntry"],"sources":["../../src/rsc/defer.tsx"],"sourcesContent":["import type { ReactElement, ReactNode } from \"react\";\nimport { renderToReadableStream } from \"@vitejs/plugin-rsc/react/rsc\";\nimport { DeferredComponent } from \"#rsc-client\";\nimport { drainStream } from \"../util/drainStream\";\nimport { getPayloadIDFor } from \"./rscModule\";\n\nexport interface DeferEntry {\n state: DeferEntryState;\n name?: string;\n drainPromise?: Promise<string>;\n}\n\n/**\n * Options for the defer function.\n */\nexport interface DeferOptions {\n /**\n * Optional name for debugging purposes.\n * In development: included in the RSC payload file name.\n * In production: logged when the payload file is emitted.\n */\n name?: string;\n}\n\nexport interface LoadedDeferEntry extends DeferEntry {\n state: Exclude<DeferEntryState, { state: \"pending\" }>;\n drainPromise: Promise<string>;\n}\n\ntype DeferEntryState =\n | {\n state: \"pending\";\n element: ReactElement;\n }\n | {\n state: \"streaming\";\n stream: ReadableStream<Uint8Array>;\n }\n | {\n state: \"ready\";\n }\n | {\n state: \"error\";\n error: unknown;\n };\n\n/**\n * Sanitizes a name for use in file paths.\n * Replaces non-alphanumeric characters with underscores and limits length.\n */\nfunction sanitizeName(name: string): string {\n return name\n .replace(/[^a-zA-Z0-9_-]/g, \"_\")\n .replace(/_+/g, \"_\")\n .replace(/^_|_$/g, \"\")\n .slice(0, 50);\n}\n\nexport class DeferRegistry {\n #registry = new Map<string, DeferEntry>();\n\n register(element: ReactElement, id: string, name?: string) {\n this.#registry.set(id, { state: { element, state: \"pending\" }, name });\n }\n\n load(id: string): LoadedDeferEntry | undefined {\n const entry = this.#registry.get(id);\n if (!entry) {\n return undefined;\n }\n return this.#loadEntry(entry);\n }\n\n #loadEntry(entry: DeferEntry): LoadedDeferEntry {\n const { state } = entry;\n switch (state.state) {\n case \"pending\": {\n const stream = renderToReadableStream<ReactNode>(state.element);\n const [stream1, stream2] = stream.tee();\n entry.state = { state: \"streaming\", stream: stream1 };\n const drainPromise = drainStream(stream2);\n entry.drainPromise = drainPromise;\n drainPromise.then(\n () => {\n entry.state = { state: \"ready\" };\n },\n (error) => {\n entry.state = { state: \"error\", error };\n },\n );\n return entry as LoadedDeferEntry;\n }\n case \"streaming\":\n case \"ready\":\n case \"error\":\n return entry as LoadedDeferEntry;\n }\n }\n\n has(id: string): boolean {\n return this.#registry.has(id);\n }\n\n /**\n * Iterates over all entries in parallel.\n * Yields results as each stream completes.\n */\n async *loadAll() {\n const errors: unknown[] = [];\n\n // Phase 1: Start all entries loading and collect drain promises.\n // We use drain promises (which drain stream2 from tee) instead of\n // draining stream1 directly, because stream1 may have been locked\n // by createFromReadableStream during SSR.\n const loadedEntries = Array.from(this.#registry, ([id, entry]) => {\n const loaded = this.#loadEntry(entry);\n return [id, loaded.drainPromise, entry.name] as const;\n });\n\n if (loadedEntries.length === 0) return;\n\n type Result = { id: string; data: string; name?: string };\n\n // Completion queue\n const completed: Array<Result | { error: unknown }> = [];\n let waiting: (() => void) | undefined;\n let remainingCount = loadedEntries.length;\n\n const onComplete = (result: Result | { error: unknown }) => {\n completed.push(result);\n remainingCount--;\n waiting?.();\n };\n\n // Phase 2: Await drain promises\n for (const [id, drainPromise, name] of loadedEntries) {\n drainPromise.then(\n (data) => onComplete({ id, data, name }),\n (error) => onComplete({ error }),\n );\n }\n\n // Phase 3: Yield from queue as results arrive\n while (remainingCount > 0 || completed.length > 0) {\n if (completed.length === 0) {\n await new Promise<void>((r) => {\n waiting = r;\n });\n waiting = undefined;\n }\n for (const result of completed.splice(0)) {\n if (\"error\" in result) {\n errors.push(result.error);\n } else {\n yield result;\n }\n }\n }\n\n if (errors.length > 0) {\n throw new AggregateError(errors);\n }\n }\n}\n\nexport const deferRegistry = new DeferRegistry();\n\n/**\n * Renders given Server Component into a separate RSC payload.\n *\n * During the client side rendering, fetching of the payload will be\n * deferred until the returned ReactNode is actually rendered.\n *\n * @param element - The React element to defer.\n * @param options - Optional configuration for the deferred payload.\n * @returns A ReactNode that virtually contains the result of rendering the given component.\n */\nexport function defer(\n element: ReactElement,\n options?: DeferOptions,\n): ReactNode {\n const name = options?.name;\n const sanitizedName = name ? sanitizeName(name) : undefined;\n const rawId = sanitizedName\n ? `${sanitizedName}-${crypto.randomUUID()}`\n : crypto.randomUUID();\n const id = getPayloadIDFor(rawId);\n deferRegistry.register(element, id, name);\n\n return <DeferredComponent moduleID={id} />;\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"defer.mjs","names":["#registry","#loadEntry"],"sources":["../../src/rsc/defer.tsx"],"sourcesContent":["import type { ReactElement, ReactNode } from \"react\";\nimport { renderToReadableStream } from \"@vitejs/plugin-rsc/react/rsc\";\nimport { DeferredComponent } from \"#rsc-client\";\nimport { drainStream } from \"../util/drainStream\";\nimport { getPayloadIDFor } from \"./rscModule\";\nimport { rscPayloadDir } from \"virtual:funstack/config\";\n\nexport interface DeferEntry {\n state: DeferEntryState;\n name?: string;\n drainPromise?: Promise<string>;\n}\n\n/**\n * Options for the defer function.\n */\nexport interface DeferOptions {\n /**\n * Optional name for debugging purposes.\n * In development: included in the RSC payload file name.\n * In production: logged when the payload file is emitted.\n */\n name?: string;\n}\n\nexport interface LoadedDeferEntry extends DeferEntry {\n state: Exclude<DeferEntryState, { state: \"pending\" }>;\n drainPromise: Promise<string>;\n}\n\ntype DeferEntryState =\n | {\n state: \"pending\";\n element: ReactElement;\n }\n | {\n state: \"streaming\";\n stream: ReadableStream<Uint8Array>;\n }\n | {\n state: \"ready\";\n }\n | {\n state: \"error\";\n error: unknown;\n };\n\n/**\n * Sanitizes a name for use in file paths.\n * Replaces non-alphanumeric characters with underscores and limits length.\n */\nfunction sanitizeName(name: string): string {\n return name\n .replace(/[^a-zA-Z0-9_-]/g, \"_\")\n .replace(/_+/g, \"_\")\n .replace(/^_|_$/g, \"\")\n .slice(0, 50);\n}\n\nexport class DeferRegistry {\n #registry = new Map<string, DeferEntry>();\n\n register(element: ReactElement, id: string, name?: string) {\n this.#registry.set(id, { state: { element, state: \"pending\" }, name });\n }\n\n load(id: string): LoadedDeferEntry | undefined {\n const entry = this.#registry.get(id);\n if (!entry) {\n return undefined;\n }\n return this.#loadEntry(entry);\n }\n\n #loadEntry(entry: DeferEntry): LoadedDeferEntry {\n const { state } = entry;\n switch (state.state) {\n case \"pending\": {\n const stream = renderToReadableStream<ReactNode>(state.element);\n const [stream1, stream2] = stream.tee();\n entry.state = { state: \"streaming\", stream: stream1 };\n const drainPromise = drainStream(stream2);\n entry.drainPromise = drainPromise;\n drainPromise.then(\n () => {\n entry.state = { state: \"ready\" };\n },\n (error) => {\n entry.state = { state: \"error\", error };\n },\n );\n return entry as LoadedDeferEntry;\n }\n case \"streaming\":\n case \"ready\":\n case \"error\":\n return entry as LoadedDeferEntry;\n }\n }\n\n has(id: string): boolean {\n return this.#registry.has(id);\n }\n\n /**\n * Iterates over all entries in parallel.\n * Yields results as each stream completes.\n */\n async *loadAll() {\n const errors: unknown[] = [];\n\n // Phase 1: Start all entries loading and collect drain promises.\n // We use drain promises (which drain stream2 from tee) instead of\n // draining stream1 directly, because stream1 may have been locked\n // by createFromReadableStream during SSR.\n const loadedEntries = Array.from(this.#registry, ([id, entry]) => {\n const loaded = this.#loadEntry(entry);\n return [id, loaded.drainPromise, entry.name] as const;\n });\n\n if (loadedEntries.length === 0) return;\n\n type Result = { id: string; data: string; name?: string };\n\n // Completion queue\n const completed: Array<Result | { error: unknown }> = [];\n let waiting: (() => void) | undefined;\n let remainingCount = loadedEntries.length;\n\n const onComplete = (result: Result | { error: unknown }) => {\n completed.push(result);\n remainingCount--;\n waiting?.();\n };\n\n // Phase 2: Await drain promises\n for (const [id, drainPromise, name] of loadedEntries) {\n drainPromise.then(\n (data) => onComplete({ id, data, name }),\n (error) => onComplete({ error }),\n );\n }\n\n // Phase 3: Yield from queue as results arrive\n while (remainingCount > 0 || completed.length > 0) {\n if (completed.length === 0) {\n await new Promise<void>((r) => {\n waiting = r;\n });\n waiting = undefined;\n }\n for (const result of completed.splice(0)) {\n if (\"error\" in result) {\n errors.push(result.error);\n } else {\n yield result;\n }\n }\n }\n\n if (errors.length > 0) {\n throw new AggregateError(errors);\n }\n }\n}\n\nexport const deferRegistry = new DeferRegistry();\n\n/**\n * Renders given Server Component into a separate RSC payload.\n *\n * During the client side rendering, fetching of the payload will be\n * deferred until the returned ReactNode is actually rendered.\n *\n * @param element - The React element to defer.\n * @param options - Optional configuration for the deferred payload.\n * @returns A ReactNode that virtually contains the result of rendering the given component.\n */\nexport function defer(\n element: ReactElement,\n options?: DeferOptions,\n): ReactNode {\n const name = options?.name;\n const sanitizedName = name ? sanitizeName(name) : undefined;\n const rawId = sanitizedName\n ? `${sanitizedName}-${crypto.randomUUID()}`\n : crypto.randomUUID();\n const id = getPayloadIDFor(rawId, rscPayloadDir);\n deferRegistry.register(element, id, name);\n\n return <DeferredComponent moduleID={id} />;\n}\n"],"mappings":";;;;;;;;;;;AAmDA,SAAS,aAAa,MAAsB;AAC1C,QAAO,KACJ,QAAQ,mBAAmB,IAAI,CAC/B,QAAQ,OAAO,IAAI,CACnB,QAAQ,UAAU,GAAG,CACrB,MAAM,GAAG,GAAG;;AAGjB,IAAa,gBAAb,MAA2B;CACzB,4BAAY,IAAI,KAAyB;CAEzC,SAAS,SAAuB,IAAY,MAAe;AACzD,QAAA,SAAe,IAAI,IAAI;GAAE,OAAO;IAAE;IAAS,OAAO;IAAW;GAAE;GAAM,CAAC;;CAGxE,KAAK,IAA0C;EAC7C,MAAM,QAAQ,MAAA,SAAe,IAAI,GAAG;AACpC,MAAI,CAAC,MACH;AAEF,SAAO,MAAA,UAAgB,MAAM;;CAG/B,WAAW,OAAqC;EAC9C,MAAM,EAAE,UAAU;AAClB,UAAQ,MAAM,OAAd;GACE,KAAK,WAAW;IAEd,MAAM,CAAC,SAAS,WADD,uBAAkC,MAAM,QAAQ,CAC7B,KAAK;AACvC,UAAM,QAAQ;KAAE,OAAO;KAAa,QAAQ;KAAS;IACrD,MAAM,eAAe,YAAY,QAAQ;AACzC,UAAM,eAAe;AACrB,iBAAa,WACL;AACJ,WAAM,QAAQ,EAAE,OAAO,SAAS;QAEjC,UAAU;AACT,WAAM,QAAQ;MAAE,OAAO;MAAS;MAAO;MAE1C;AACD,WAAO;;GAET,KAAK;GACL,KAAK;GACL,KAAK,QACH,QAAO;;;CAIb,IAAI,IAAqB;AACvB,SAAO,MAAA,SAAe,IAAI,GAAG;;;;;;CAO/B,OAAO,UAAU;EACf,MAAM,SAAoB,EAAE;EAM5B,MAAM,gBAAgB,MAAM,KAAK,MAAA,WAAiB,CAAC,IAAI,WAAW;AAEhE,UAAO;IAAC;IADO,MAAA,UAAgB,MAAM,CAClB;IAAc,MAAM;IAAK;IAC5C;AAEF,MAAI,cAAc,WAAW,EAAG;EAKhC,MAAM,YAAgD,EAAE;EACxD,IAAI;EACJ,IAAI,iBAAiB,cAAc;EAEnC,MAAM,cAAc,WAAwC;AAC1D,aAAU,KAAK,OAAO;AACtB;AACA,cAAW;;AAIb,OAAK,MAAM,CAAC,IAAI,cAAc,SAAS,cACrC,cAAa,MACV,SAAS,WAAW;GAAE;GAAI;GAAM;GAAM,CAAC,GACvC,UAAU,WAAW,EAAE,OAAO,CAAC,CACjC;AAIH,SAAO,iBAAiB,KAAK,UAAU,SAAS,GAAG;AACjD,OAAI,UAAU,WAAW,GAAG;AAC1B,UAAM,IAAI,SAAe,MAAM;AAC7B,eAAU;MACV;AACF,cAAU,KAAA;;AAEZ,QAAK,MAAM,UAAU,UAAU,OAAO,EAAE,CACtC,KAAI,WAAW,OACb,QAAO,KAAK,OAAO,MAAM;OAEzB,OAAM;;AAKZ,MAAI,OAAO,SAAS,EAClB,OAAM,IAAI,eAAe,OAAO;;;AAKtC,MAAa,gBAAgB,IAAI,eAAe;;;;;;;;;;;AAYhD,SAAgB,MACd,SACA,SACW;CACX,MAAM,OAAO,SAAS;CACtB,MAAM,gBAAgB,OAAO,aAAa,KAAK,GAAG,KAAA;CAIlD,MAAM,KAAK,gBAHG,gBACV,GAAG,cAAc,GAAG,OAAO,YAAY,KACvC,OAAO,YAAY,EACW,cAAc;AAChD,eAAc,SAAS,SAAS,IAAI,KAAK;AAEzC,QAAO,oBAAC,mBAAD,EAAmB,UAAU,IAAM,CAAA"}
|
package/dist/rsc/rscModule.mjs
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
//#region src/rsc/rscModule.ts
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* Default directory name for RSC payload files.
|
|
4
4
|
*/
|
|
5
|
-
const
|
|
5
|
+
const defaultRscPayloadDir = "fun:rsc-payload";
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* Combines the RSC payload directory with a raw ID to form a
|
|
8
|
+
* namespaced payload ID (e.g. "fun:rsc-payload/abc123").
|
|
9
9
|
*/
|
|
10
|
-
function getPayloadIDFor(rawId) {
|
|
11
|
-
return `${
|
|
10
|
+
function getPayloadIDFor(rawId, rscPayloadDir = defaultRscPayloadDir) {
|
|
11
|
+
return `${rscPayloadDir}/${rawId}`;
|
|
12
12
|
}
|
|
13
13
|
const rscModulePathPrefix = "/funstack__/";
|
|
14
14
|
const rscModulePathSuffix = ".txt";
|
|
@@ -20,6 +20,6 @@ function extractIDFromModulePath(modulePath) {
|
|
|
20
20
|
return modulePath.slice(12, -4);
|
|
21
21
|
}
|
|
22
22
|
//#endregion
|
|
23
|
-
export { extractIDFromModulePath, getModulePathFor, getPayloadIDFor };
|
|
23
|
+
export { defaultRscPayloadDir, extractIDFromModulePath, getModulePathFor, getPayloadIDFor };
|
|
24
24
|
|
|
25
25
|
//# sourceMappingURL=rscModule.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rscModule.mjs","names":[],"sources":["../../src/rsc/rscModule.ts"],"sourcesContent":["/**\n *
|
|
1
|
+
{"version":3,"file":"rscModule.mjs","names":[],"sources":["../../src/rsc/rscModule.ts"],"sourcesContent":["/**\n * Default directory name for RSC payload files.\n */\nexport const defaultRscPayloadDir = \"fun:rsc-payload\";\n\n/**\n * Combines the RSC payload directory with a raw ID to form a\n * namespaced payload ID (e.g. \"fun:rsc-payload/abc123\").\n */\nexport function getPayloadIDFor(\n rawId: string,\n rscPayloadDir: string = defaultRscPayloadDir,\n): string {\n return `${rscPayloadDir}/${rawId}`;\n}\n\nconst rscModulePathPrefix = \"/funstack__/\";\nconst rscModulePathSuffix = \".txt\";\n\nexport function getModulePathFor(id: string): string {\n return `${rscModulePathPrefix}${id}${rscModulePathSuffix}`;\n}\n\nexport function extractIDFromModulePath(\n modulePath: string,\n): string | undefined {\n if (\n !modulePath.startsWith(rscModulePathPrefix) ||\n !modulePath.endsWith(rscModulePathSuffix)\n ) {\n return undefined;\n }\n return modulePath.slice(\n rscModulePathPrefix.length,\n -rscModulePathSuffix.length,\n );\n}\n"],"mappings":";;;;AAGA,MAAa,uBAAuB;;;;;AAMpC,SAAgB,gBACd,OACA,gBAAwB,sBAChB;AACR,QAAO,GAAG,cAAc,GAAG;;AAG7B,MAAM,sBAAsB;AAC5B,MAAM,sBAAsB;AAE5B,SAAgB,iBAAiB,IAAoB;AACnD,QAAO,GAAG,sBAAsB,KAAK;;AAGvC,SAAgB,wBACd,YACoB;AACpB,KACE,CAAC,WAAW,WAAW,oBAAoB,IAC3C,CAAC,WAAW,SAAS,oBAAoB,CAEzC;AAEF,QAAO,WAAW,MAChB,IACA,GACD"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@funstack/static",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "FUNSTACK static library",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -47,28 +47,28 @@
|
|
|
47
47
|
"license": "MIT",
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@playwright/test": "^1.58.2",
|
|
50
|
-
"@types/node": "^25.
|
|
50
|
+
"@types/node": "^25.5.0",
|
|
51
51
|
"@types/react": "^19.2.14",
|
|
52
52
|
"@types/react-dom": "^19.2.3",
|
|
53
|
-
"jsdom": "^
|
|
53
|
+
"jsdom": "^29.0.0",
|
|
54
54
|
"react": "^19.2.4",
|
|
55
55
|
"react-dom": "^19.2.4",
|
|
56
|
-
"tsdown": "^0.21.
|
|
56
|
+
"tsdown": "^0.21.4",
|
|
57
57
|
"typescript": "^5.9.3",
|
|
58
|
-
"vite": "^
|
|
59
|
-
"vitest": "^4.0
|
|
58
|
+
"vite": "^8.0.0",
|
|
59
|
+
"vitest": "^4.1.0"
|
|
60
60
|
},
|
|
61
61
|
"dependencies": {
|
|
62
62
|
"@funstack/skill-installer": "^1.0.0",
|
|
63
63
|
"@vitejs/plugin-rsc": "^0.5.21",
|
|
64
64
|
"react-error-boundary": "^6.1.1",
|
|
65
65
|
"rsc-html-stream": "^0.0.7",
|
|
66
|
-
"srvx": "^0.11.
|
|
66
|
+
"srvx": "^0.11.12"
|
|
67
67
|
},
|
|
68
68
|
"peerDependencies": {
|
|
69
69
|
"react": "^19.2.3",
|
|
70
70
|
"react-dom": "^19.2.3",
|
|
71
|
-
"vite": "^7.
|
|
71
|
+
"vite": "^7.0.0 || ^8.0.0"
|
|
72
72
|
},
|
|
73
73
|
"scripts": {
|
|
74
74
|
"build": "tsdown",
|
|
@@ -16,7 +16,7 @@ FUNSTACk Static is served as a Vite plugin. See your app's `vite.config.ts` file
|
|
|
16
16
|
```ts
|
|
17
17
|
import { defineConfig } from "vite";
|
|
18
18
|
import react from "@vitejs/plugin-react";
|
|
19
|
-
import
|
|
19
|
+
import funstackStatic from "@funstack/static";
|
|
20
20
|
|
|
21
21
|
export default defineConfig({
|
|
22
22
|
plugins: [
|