@funstack/static 1.1.0 → 1.1.2
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/rscProcessor.mjs +1 -1
- package/dist/build/rscProcessor.mjs.map +1 -1
- package/dist/docs/advanced/MultipleEntrypoints.md +1 -1
- package/dist/docs/api/Defer.md +3 -3
- package/dist/docs/api/FunstackStatic.md +4 -4
- package/dist/docs/index.md +1 -0
- package/dist/docs/learn/FileSystemRouting.md +95 -0
- package/dist/docs/learn/HowItWorks.md +2 -2
- package/dist/docs/learn/OptimizingPayloads.md +2 -2
- package/dist/plugin/getRSCEntryPoint.mjs +2 -1
- package/dist/plugin/getRSCEntryPoint.mjs.map +1 -1
- package/dist/plugin/index.d.mts +2 -2
- package/dist/plugin/index.d.mts.map +1 -1
- package/dist/plugin/index.mjs +5 -4
- package/dist/plugin/index.mjs.map +1 -1
- package/dist/plugin/server.mjs +1 -1
- package/dist/rsc/rscModule.mjs +2 -2
- package/dist/rsc/rscModule.mjs.map +1 -1
- package/package.json +8 -8
|
@@ -8,7 +8,7 @@ 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. "
|
|
11
|
+
* @param rscPayloadDir - Directory name used as a prefix for RSC payload IDs (e.g. "fun__rsc-payload")
|
|
12
12
|
* @param context - Optional context for logging warnings
|
|
13
13
|
*/
|
|
14
14
|
async function processRscComponents(deferRegistryIterator, appRscStream, rscPayloadDir, context) {
|
|
@@ -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 rscPayloadDir - Directory name used as a prefix for RSC payload IDs (e.g. \"
|
|
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"}
|
|
@@ -202,7 +202,7 @@ dist/public/
|
|
|
202
202
|
├── blog/
|
|
203
203
|
│ └── post-1.html
|
|
204
204
|
├── funstack__/
|
|
205
|
-
│ └──
|
|
205
|
+
│ └── fun__rsc-payload/
|
|
206
206
|
│ ├── a1b2c3d4.txt # RSC payload for index.html
|
|
207
207
|
│ ├── e5f6g7h8.txt # RSC payload for about.html
|
|
208
208
|
│ ├── i9j0k1l2.txt # RSC payload for blog/post-1.html
|
package/dist/docs/api/Defer.md
CHANGED
|
@@ -65,7 +65,7 @@ interface DeferOptions {
|
|
|
65
65
|
```
|
|
66
66
|
|
|
67
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__/
|
|
68
|
+
- In development mode, the name is included in the RSC payload file name (e.g., `/funstack__/fun__rsc-payload/HomePage-b5698be72eea3c37`)
|
|
69
69
|
- In production mode, the name is logged when the payload file is emitted
|
|
70
70
|
|
|
71
71
|
### Returns
|
|
@@ -105,6 +105,6 @@ Using the `name` option makes it easier to identify which deferred component cor
|
|
|
105
105
|
|
|
106
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
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__/
|
|
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
109
|
|
|
110
|
-
In the main RSC payload, the `defer` call is replaced with a client component `<DeferredComponent moduleId="
|
|
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.
|
|
@@ -229,19 +229,19 @@ Sentry.init({
|
|
|
229
229
|
### rscPayloadDir (optional)
|
|
230
230
|
|
|
231
231
|
**Type:** `string`
|
|
232
|
-
**Default:** `"
|
|
232
|
+
**Default:** `"fun__rsc-payload"`
|
|
233
233
|
|
|
234
234
|
Directory name used for RSC payload files in the build output. The final file paths follow the pattern `/funstack__/{rscPayloadDir}/{hash}.txt`.
|
|
235
235
|
|
|
236
|
-
Change this if your hosting platform has issues with the default directory name.
|
|
236
|
+
Change this if your hosting platform has issues with the default directory name.
|
|
237
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 `"
|
|
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
239
|
|
|
240
240
|
```typescript
|
|
241
241
|
funstackStatic({
|
|
242
242
|
root: "./src/root.tsx",
|
|
243
243
|
app: "./src/App.tsx",
|
|
244
|
-
rscPayloadDir: "
|
|
244
|
+
rscPayloadDir: "my-custom-rsc-payload",
|
|
245
245
|
});
|
|
246
246
|
```
|
|
247
247
|
|
package/dist/docs/index.md
CHANGED
|
@@ -22,6 +22,7 @@ A Vite plugin for building static sites with React Server Components.
|
|
|
22
22
|
### Learn
|
|
23
23
|
|
|
24
24
|
- [Prefetching with defer() and Activity](./learn/DeferAndActivity.md) - When using `defer()` to split RSC payloads, content is fetched on-demand as it renders. But what if you want to start fetching _before_ the user actually needs it? By combining `defer()` with React 19's `<Activity>` component, you can prefetch deferred payloads in the background so they're ready instantly when revealed.
|
|
25
|
+
- [File-System Routing](./learn/FileSystemRouting.md) - FUNSTACK Static does not include a built-in file-system router, but you can implement one in userland using Vite's `import.meta.glob` and a router library like [FUNSTACK Router](https://github.com/uhyo/funstack-router).
|
|
25
26
|
- [How It Works](./learn/HowItWorks.md) - FUNSTACK Static is a React framework that leverages React Server Components (RSC) to build a fully static Single Page Application. The result is a set of files that can be deployed to **any static file hosting service** - no server required at runtime.
|
|
26
27
|
- [Using lazy() in Server Components](./learn/LazyServerComponents.md) - React's `lazy()` API is typically associated with client-side code splitting. However, it can also be used in server environments to reduce the initial response time of the development server by deferring the work needed to compute your application.
|
|
27
28
|
- [Optimizing RSC Payloads](./learn/OptimizingPayloads.md) - FUNSTACK Static uses React Server Components (RSC) to pre-render your application at build time. By default, all content is bundled into a single RSC payload. This page explains how to split that payload into smaller chunks for better loading performance.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# File-System Routing
|
|
2
|
+
|
|
3
|
+
FUNSTACK Static does not include a built-in file-system router, but you can implement one in userland using Vite's `import.meta.glob` and a router library like [FUNSTACK Router](https://github.com/uhyo/funstack-router).
|
|
4
|
+
|
|
5
|
+
## How It Works
|
|
6
|
+
|
|
7
|
+
The idea is to use `import.meta.glob` to discover page components from a `pages/` directory at compile time, then convert the file paths into route definitions.
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
import { route, type RouteDefinition } from "@funstack/router/server";
|
|
11
|
+
|
|
12
|
+
const pageModules = import.meta.glob<{ default: React.ComponentType }>(
|
|
13
|
+
"./pages/**/*.tsx",
|
|
14
|
+
{ eager: true },
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
function filePathToUrlPath(filePath: string): string {
|
|
18
|
+
let urlPath = filePath.replace(/^\.\/pages/, "").replace(/\.tsx$/, "");
|
|
19
|
+
if (urlPath.endsWith("/index")) {
|
|
20
|
+
urlPath = urlPath.slice(0, -"/index".length);
|
|
21
|
+
}
|
|
22
|
+
return urlPath || "/";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const routes: RouteDefinition[] = Object.entries(pageModules).map(
|
|
26
|
+
([filePath, module]) => {
|
|
27
|
+
const Page = module.default;
|
|
28
|
+
return route({
|
|
29
|
+
path: filePathToUrlPath(filePath),
|
|
30
|
+
component: <Page />,
|
|
31
|
+
});
|
|
32
|
+
},
|
|
33
|
+
);
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
With this setup, files in the `pages/` directory are automatically mapped to routes:
|
|
37
|
+
|
|
38
|
+
| File | Route |
|
|
39
|
+
| ---------------------- | -------- |
|
|
40
|
+
| `pages/index.tsx` | `/` |
|
|
41
|
+
| `pages/about.tsx` | `/about` |
|
|
42
|
+
| `pages/blog/index.tsx` | `/blog` |
|
|
43
|
+
|
|
44
|
+
## Why import.meta.glob?
|
|
45
|
+
|
|
46
|
+
Using `import.meta.glob` has two key advantages:
|
|
47
|
+
|
|
48
|
+
- **Automatic discovery** — you don't need to manually register each page. Just add a new `.tsx` file and it becomes a route.
|
|
49
|
+
- **Hot module replacement** — Vite tracks the glob pattern, so adding or removing page files in development triggers an automatic update without a server restart.
|
|
50
|
+
|
|
51
|
+
## Static Generation
|
|
52
|
+
|
|
53
|
+
To generate static HTML for each route, derive [entry definitions](/api/entry-definition) from the route list:
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
import type { EntryDefinition } from "@funstack/static/entries";
|
|
57
|
+
import type { RouteDefinition } from "@funstack/router/server";
|
|
58
|
+
|
|
59
|
+
function collectPaths(routes: RouteDefinition[]): string[] {
|
|
60
|
+
const paths: string[] = [];
|
|
61
|
+
for (const route of routes) {
|
|
62
|
+
if (route.children) {
|
|
63
|
+
paths.push(...collectPaths(route.children));
|
|
64
|
+
} else if (route.path !== undefined && route.path !== "*") {
|
|
65
|
+
paths.push(route.path);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return paths;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function pathToEntryPath(path: string): string {
|
|
72
|
+
if (path === "/") return "index.html";
|
|
73
|
+
return `${path.slice(1)}.html`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export default function getEntries(): EntryDefinition[] {
|
|
77
|
+
return collectPaths(routes).map((pathname) => ({
|
|
78
|
+
path: pathToEntryPath(pathname),
|
|
79
|
+
root: () => import("./root"),
|
|
80
|
+
app: <App ssrPath={pathname} />,
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
This produces one HTML file per route at build time.
|
|
86
|
+
|
|
87
|
+
## Full Example
|
|
88
|
+
|
|
89
|
+
For a complete working example, see the [`example-fs-routing`](https://github.com/uhyo/funstack-static/tree/master/packages/example-fs-routing) package in the FUNSTACK Static repository.
|
|
90
|
+
|
|
91
|
+
## See Also
|
|
92
|
+
|
|
93
|
+
- [Multiple Entrypoints](/advanced/multiple-entrypoints) - Generating multiple HTML pages from a single project
|
|
94
|
+
- [EntryDefinition](/api/entry-definition) - API reference for entry definitions
|
|
95
|
+
- [How It Works](/learn/how-it-works) - Overall FUNSTACK Static architecture
|
|
@@ -45,12 +45,12 @@ dist/public
|
|
|
45
45
|
│ ├── root-DvE5ENz2.css
|
|
46
46
|
│ └── rsc-D0fjt5Ie.js
|
|
47
47
|
├── funstack__
|
|
48
|
-
│ └──
|
|
48
|
+
│ └── fun__rsc-payload
|
|
49
49
|
│ └── db1923b9b6507ab4.txt
|
|
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. The `
|
|
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
|
|
|
@@ -8,7 +8,7 @@ Without any optimization, your entire application is rendered into one RSC paylo
|
|
|
8
8
|
|
|
9
9
|
```
|
|
10
10
|
dist/public/funstack__/
|
|
11
|
-
└──
|
|
11
|
+
└── fun__rsc-payload/
|
|
12
12
|
└── b62ec6668fd49300.txt ← Contains everything
|
|
13
13
|
```
|
|
14
14
|
|
|
@@ -75,7 +75,7 @@ After building with route-level `defer()`, your output looks like this:
|
|
|
75
75
|
|
|
76
76
|
```
|
|
77
77
|
dist/public/funstack__/
|
|
78
|
-
└──
|
|
78
|
+
└── fun__rsc-payload/
|
|
79
79
|
├── a3f2b1c9d8e7f6a5.txt ← Home page
|
|
80
80
|
├── b5698be72eea3c37.txt ← About page
|
|
81
81
|
├── b62ec6668fd49300.txt ← Main app shell
|
|
@@ -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
|
@@ -27,13 +27,13 @@ interface FunstackStaticBaseOptions {
|
|
|
27
27
|
* The final path will be `/funstack__/{rscPayloadDir}/{hash}.txt`.
|
|
28
28
|
*
|
|
29
29
|
* Change this if your hosting platform has issues with the default
|
|
30
|
-
* directory name
|
|
30
|
+
* directory name.
|
|
31
31
|
*
|
|
32
32
|
* The value is used as a marker for string replacement during the build
|
|
33
33
|
* process, so it should be unique enough that it does not appear in your
|
|
34
34
|
* application's source code.
|
|
35
35
|
*
|
|
36
|
-
* @default "
|
|
36
|
+
* @default "fun__rsc-payload"
|
|
37
37
|
*/
|
|
38
38
|
rscPayloadDir?: string;
|
|
39
39
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/plugin/index.ts"],"mappings":";;;UAOU,yBAAA;;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/plugin/index.ts"],"mappings":";;;UAOU,yBAAA;;AANwC;;;;EAYhD,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
|
@@ -2,6 +2,7 @@ import { defaultRscPayloadDir } from "../rsc/rscModule.mjs";
|
|
|
2
2
|
import { buildApp } from "../build/buildApp.mjs";
|
|
3
3
|
import { serverPlugin } from "./server.mjs";
|
|
4
4
|
import path from "node:path";
|
|
5
|
+
import { normalizePath } from "vite";
|
|
5
6
|
import rsc from "@vitejs/plugin-rsc";
|
|
6
7
|
//#region src/plugin/index.ts
|
|
7
8
|
function funstackStatic(options) {
|
|
@@ -29,16 +30,16 @@ function funstackStatic(options) {
|
|
|
29
30
|
{
|
|
30
31
|
name: "@funstack/static:config",
|
|
31
32
|
configResolved(config) {
|
|
32
|
-
if (isMultiEntry) resolvedEntriesModule = path.resolve(config.root, options.entries);
|
|
33
|
+
if (isMultiEntry) resolvedEntriesModule = normalizePath(path.resolve(config.root, options.entries));
|
|
33
34
|
else {
|
|
34
|
-
const resolvedRoot = path.resolve(config.root, options.root);
|
|
35
|
-
const resolvedApp = path.resolve(config.root, options.app);
|
|
35
|
+
const resolvedRoot = normalizePath(path.resolve(config.root, options.root));
|
|
36
|
+
const resolvedApp = normalizePath(path.resolve(config.root, options.app));
|
|
36
37
|
resolvedEntriesModule = JSON.stringify({
|
|
37
38
|
root: resolvedRoot,
|
|
38
39
|
app: resolvedApp
|
|
39
40
|
});
|
|
40
41
|
}
|
|
41
|
-
if (clientInit) resolvedClientInitEntry = path.resolve(config.root, clientInit);
|
|
42
|
+
if (clientInit) resolvedClientInitEntry = normalizePath(path.resolve(config.root, clientInit));
|
|
42
43
|
},
|
|
43
44
|
configEnvironment(_name, config) {
|
|
44
45
|
if (!config.optimizeDeps) config.optimizeDeps = {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/plugin/index.ts"],"sourcesContent":["import path from \"node:path\";\nimport type
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/plugin/index.ts"],"sourcesContent":["import path from \"node:path\";\nimport { normalizePath, 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.\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 = normalizePath(\n path.resolve(config.root, options.entries),\n );\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 = normalizePath(\n path.resolve(config.root, options.root),\n );\n const resolvedApp = normalizePath(\n path.resolve(config.root, options.app),\n );\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 = normalizePath(\n path.resolve(config.root, clientInit),\n );\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,cACtB,KAAK,QAAQ,OAAO,MAAM,QAAQ,QAAQ,CAC3C;SACI;KAGL,MAAM,eAAe,cACnB,KAAK,QAAQ,OAAO,MAAM,QAAQ,KAAK,CACxC;KACD,MAAM,cAAc,cAClB,KAAK,QAAQ,OAAO,MAAM,QAAQ,IAAI,CACvC;AAED,6BAAwB,KAAK,UAAU;MACrC,MAAM;MACN,KAAK;MACN,CAAC;;AAEJ,QAAI,WACF,2BAA0B,cACxB,KAAK,QAAQ,OAAO,MAAM,WAAW,CACtC;;GAGL,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/plugin/server.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { getRSCEntryPoint } from "./getRSCEntryPoint.mjs";
|
|
2
2
|
import { urlPathToFileCandidates } from "../util/urlPath.mjs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { readFile } from "node:fs/promises";
|
|
5
4
|
import { isRunnableDevEnvironment } from "vite";
|
|
5
|
+
import { readFile } from "node:fs/promises";
|
|
6
6
|
import { toNodeHandler } from "srvx/node";
|
|
7
7
|
//#region src/plugin/server.ts
|
|
8
8
|
const serverPlugin = () => {
|
package/dist/rsc/rscModule.mjs
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Default directory name for RSC payload files.
|
|
4
4
|
*/
|
|
5
|
-
const defaultRscPayloadDir = "
|
|
5
|
+
const defaultRscPayloadDir = "fun__rsc-payload";
|
|
6
6
|
/**
|
|
7
7
|
* Combines the RSC payload directory with a raw ID to form a
|
|
8
|
-
* namespaced payload ID (e.g. "
|
|
8
|
+
* namespaced payload ID (e.g. "fun__rsc-payload/abc123").
|
|
9
9
|
*/
|
|
10
10
|
function getPayloadIDFor(rawId, rscPayloadDir = defaultRscPayloadDir) {
|
|
11
11
|
return `${rscPayloadDir}/${rawId}`;
|
|
@@ -1 +1 @@
|
|
|
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 = \"
|
|
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.1.
|
|
3
|
+
"version": "1.1.2",
|
|
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.1",
|
|
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",
|