@funstack/static 0.0.5 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/build/buildApp.mjs +35 -6
- package/dist/build/buildApp.mjs.map +1 -1
- package/dist/build/validateEntryPath.mjs +28 -0
- package/dist/build/validateEntryPath.mjs.map +1 -0
- package/dist/docs/GettingStarted.md +1 -0
- package/dist/docs/api/EntryDefinition.md +139 -0
- package/dist/docs/api/FunstackStatic.md +98 -2
- package/dist/docs/index.md +3 -0
- package/dist/docs/learn/DeferAndActivity.md +176 -0
- package/dist/docs/learn/MultipleEntrypoints.md +246 -0
- package/dist/docs/learn/OptimizingPayloads.md +1 -1
- package/dist/entries/rsc-client.d.mts +0 -1
- package/dist/entries/rsc.d.mts +2 -2
- package/dist/entryDefinition.d.mts +44 -0
- package/dist/entryDefinition.d.mts.map +1 -0
- package/dist/entryDefinition.mjs +1 -0
- package/dist/plugin/index.d.mts +26 -19
- package/dist/plugin/index.d.mts.map +1 -1
- package/dist/plugin/index.mjs +25 -9
- package/dist/plugin/index.mjs.map +1 -1
- package/dist/plugin/server.mjs +9 -2
- package/dist/plugin/server.mjs.map +1 -1
- package/dist/rsc/entry.d.mts +13 -7
- package/dist/rsc/entry.d.mts.map +1 -1
- package/dist/rsc/entry.mjs +79 -36
- package/dist/rsc/entry.mjs.map +1 -1
- package/dist/rsc/resolveEntry.mjs +29 -0
- package/dist/rsc/resolveEntry.mjs.map +1 -0
- package/dist/ssr/entry.mjs +1 -1
- package/dist/ssr/entry.mjs.map +1 -1
- package/dist/util/urlPath.mjs +17 -0
- package/dist/util/urlPath.mjs.map +1 -0
- package/package.json +14 -9
- package/dist/rsc-client/entry.d.mts +0 -1
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# Multiple Entrypoints
|
|
2
|
+
|
|
3
|
+
By default, FUNSTACK Static produces a single `index.html` from one `root` + `app` pair. The **multiple entries** feature lets you produce multiple HTML pages from a single project, targeting SSG (Static Site Generation) use cases where a site has distinct pages like `index.html`, `about.html`, and `blog/post-1.html`.
|
|
4
|
+
|
|
5
|
+
## When to Use Multiple Entries
|
|
6
|
+
|
|
7
|
+
Use the `entries` option when you want to build a **multi-page static site** where each page is a self-contained HTML document. This is different from a single-page app with client-side routing:
|
|
8
|
+
|
|
9
|
+
- **Single-entry mode** (`root` + `app`): One HTML file, client-side routing between pages. Best for app-like experiences where navigation should not trigger full page reloads.
|
|
10
|
+
- **Multiple entries mode** (`entries`): Multiple HTML files, each independently pre-rendered. Best for content sites (blogs, docs, marketing pages) where each page should be a standalone document.
|
|
11
|
+
|
|
12
|
+
## Basic Setup
|
|
13
|
+
|
|
14
|
+
### 1. Configure Vite
|
|
15
|
+
|
|
16
|
+
Instead of `root` and `app`, pass an `entries` path:
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
// vite.config.ts
|
|
20
|
+
import funstackStatic from "@funstack/static";
|
|
21
|
+
import react from "@vitejs/plugin-react";
|
|
22
|
+
import { defineConfig } from "vite";
|
|
23
|
+
|
|
24
|
+
export default defineConfig({
|
|
25
|
+
plugins: [
|
|
26
|
+
funstackStatic({
|
|
27
|
+
entries: "./src/entries.tsx",
|
|
28
|
+
}),
|
|
29
|
+
react(),
|
|
30
|
+
],
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### 2. Create the Entries Module
|
|
35
|
+
|
|
36
|
+
The entries module default-exports a function that returns an array of entry definitions:
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
// src/entries.tsx
|
|
40
|
+
import type { EntryDefinition } from "@funstack/static/entries";
|
|
41
|
+
|
|
42
|
+
export default function getEntries(): EntryDefinition[] {
|
|
43
|
+
return [
|
|
44
|
+
{
|
|
45
|
+
path: "index.html",
|
|
46
|
+
root: () => import("./root"),
|
|
47
|
+
app: () => import("./pages/Home"),
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
path: "about.html",
|
|
51
|
+
root: () => import("./root"),
|
|
52
|
+
app: () => import("./pages/About"),
|
|
53
|
+
},
|
|
54
|
+
];
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### 3. Create Root and Page Components
|
|
59
|
+
|
|
60
|
+
The root and page components work exactly the same as in single-entry mode:
|
|
61
|
+
|
|
62
|
+
```tsx
|
|
63
|
+
// src/root.tsx
|
|
64
|
+
export default function Root({ children }: { children: React.ReactNode }) {
|
|
65
|
+
return (
|
|
66
|
+
<html lang="en">
|
|
67
|
+
<head>
|
|
68
|
+
<meta charSet="UTF-8" />
|
|
69
|
+
<title>My Site</title>
|
|
70
|
+
</head>
|
|
71
|
+
<body>{children}</body>
|
|
72
|
+
</html>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
// src/pages/Home.tsx
|
|
79
|
+
export default function Home() {
|
|
80
|
+
return (
|
|
81
|
+
<main>
|
|
82
|
+
<h1>Home Page</h1>
|
|
83
|
+
<a href="/about">About</a>
|
|
84
|
+
</main>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
```tsx
|
|
90
|
+
// src/pages/About.tsx
|
|
91
|
+
export default function About() {
|
|
92
|
+
return (
|
|
93
|
+
<main>
|
|
94
|
+
<h1>About Page</h1>
|
|
95
|
+
<a href="/">Home</a>
|
|
96
|
+
</main>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## EntryDefinition
|
|
102
|
+
|
|
103
|
+
Each entry in the array is an `EntryDefinition` object imported from `@funstack/static/entries`:
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
import type { EntryDefinition } from "@funstack/static/entries";
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### path
|
|
110
|
+
|
|
111
|
+
**Type:** `string`
|
|
112
|
+
|
|
113
|
+
The output file path relative to the build output directory. Must end with `.html` and must not start with `/`.
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
{
|
|
117
|
+
path: "index.html", // -> /index.html
|
|
118
|
+
path: "about.html", // -> /about.html
|
|
119
|
+
path: "blog/post-1.html", // -> /blog/post-1.html
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The `path` specifies the exact output file name. The dev and preview servers handle mapping URL paths to these file names automatically (e.g., a request to `/about` finds `about.html`).
|
|
124
|
+
|
|
125
|
+
### root
|
|
126
|
+
|
|
127
|
+
**Type:** `MaybePromise<RootModule> | (() => MaybePromise<RootModule>)`
|
|
128
|
+
|
|
129
|
+
The root component module. Accepts either a lazy import or a synchronous module object:
|
|
130
|
+
|
|
131
|
+
```tsx
|
|
132
|
+
// Lazy import (recommended for memory efficiency)
|
|
133
|
+
root: () => import("./root"),
|
|
134
|
+
|
|
135
|
+
// Synchronous module object
|
|
136
|
+
import Root from "./root";
|
|
137
|
+
root: { default: Root },
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
The module must have a `default` export of a component that accepts `children`.
|
|
141
|
+
|
|
142
|
+
### app
|
|
143
|
+
|
|
144
|
+
**Type:** `ReactNode | MaybePromise<AppModule> | (() => MaybePromise<AppModule>)`
|
|
145
|
+
|
|
146
|
+
The app content for this entry. Accepts a module (sync or lazy), or a React node for direct rendering:
|
|
147
|
+
|
|
148
|
+
```tsx
|
|
149
|
+
// Lazy import
|
|
150
|
+
app: () => import("./pages/Home"),
|
|
151
|
+
|
|
152
|
+
// Synchronous module object
|
|
153
|
+
import Home from "./pages/Home";
|
|
154
|
+
app: { default: Home },
|
|
155
|
+
|
|
156
|
+
// React node (server component JSX)
|
|
157
|
+
app: <BlogPost slug="hello-world" />,
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
The React node form is especially useful for parameterized SSG, where each entry renders the same component with different data.
|
|
161
|
+
|
|
162
|
+
## Advanced: Async Generators
|
|
163
|
+
|
|
164
|
+
For sites with many pages generated from external data, use an async generator to stream entries without building the full array in memory:
|
|
165
|
+
|
|
166
|
+
```tsx
|
|
167
|
+
// src/entries.tsx
|
|
168
|
+
import type { EntryDefinition } from "@funstack/static/entries";
|
|
169
|
+
import Root from "./root";
|
|
170
|
+
import { readdir } from "node:fs/promises";
|
|
171
|
+
|
|
172
|
+
export default async function* getEntries(): AsyncGenerator<EntryDefinition> {
|
|
173
|
+
// Static pages
|
|
174
|
+
yield {
|
|
175
|
+
path: "index.html",
|
|
176
|
+
root: { default: Root },
|
|
177
|
+
app: () => import("./pages/Home"),
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// Dynamic pages generated from the filesystem
|
|
181
|
+
for (const slug of await readdir("./content/blog")) {
|
|
182
|
+
const content = await loadMarkdown(`./content/blog/${slug}`);
|
|
183
|
+
yield {
|
|
184
|
+
path: `blog/${slug.replace(/\.md$/, ".html")}`,
|
|
185
|
+
root: { default: Root },
|
|
186
|
+
app: <BlogPost content={content} />,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
The entries function runs in the RSC environment at build time, so it has access to Node.js APIs like `fs`, making it straightforward to generate pages from files, a CMS, or a database.
|
|
193
|
+
|
|
194
|
+
## Output Structure
|
|
195
|
+
|
|
196
|
+
Given entries with paths `index.html`, `about.html`, and `blog/post-1.html`, the build produces:
|
|
197
|
+
|
|
198
|
+
```
|
|
199
|
+
dist/public/
|
|
200
|
+
├── index.html
|
|
201
|
+
├── about.html
|
|
202
|
+
├── blog/
|
|
203
|
+
│ └── post-1.html
|
|
204
|
+
├── funstack__/
|
|
205
|
+
│ └── fun:rsc-payload/
|
|
206
|
+
│ ├── a1b2c3d4.txt # RSC payload for index.html
|
|
207
|
+
│ ├── e5f6g7h8.txt # RSC payload for about.html
|
|
208
|
+
│ ├── i9j0k1l2.txt # RSC payload for blog/post-1.html
|
|
209
|
+
│ └── ... # deferred component payloads
|
|
210
|
+
└── assets/
|
|
211
|
+
└── client.js # Client bundle (shared)
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
All pages share the same client JavaScript bundle. Only the HTML and RSC payloads differ per entry.
|
|
215
|
+
|
|
216
|
+
## Navigation Between Entries
|
|
217
|
+
|
|
218
|
+
Each entry is a fully independent HTML page. Navigation between entries is a full page reload via standard `<a>` links. Client-side interactivity within each page works as usual.
|
|
219
|
+
|
|
220
|
+
If you need client-side navigation between pages (SPA-style transitions), use single-entry mode with a client-side router instead.
|
|
221
|
+
|
|
222
|
+
## Interaction with defer()
|
|
223
|
+
|
|
224
|
+
The `defer()` function works with multiple entries. Deferred components are shared across entries via content hashing -- if multiple entries defer the same component, it is rendered once and reused.
|
|
225
|
+
|
|
226
|
+
## Path Validation
|
|
227
|
+
|
|
228
|
+
The build enforces these rules for entry paths:
|
|
229
|
+
|
|
230
|
+
- Must end with `.html`
|
|
231
|
+
- Must not start with `/` (paths are relative to the output directory)
|
|
232
|
+
- Duplicate paths cause a build error
|
|
233
|
+
|
|
234
|
+
## Dev and Preview Server
|
|
235
|
+
|
|
236
|
+
Both the dev server (`vite dev`) and preview server (`vite preview`) handle URL-to-file mapping automatically:
|
|
237
|
+
|
|
238
|
+
- `/` serves `index.html`
|
|
239
|
+
- `/about` serves `about.html`, falling back to `about/index.html`
|
|
240
|
+
- `/blog/post-1` serves `blog/post-1.html`, falling back to `blog/post-1/index.html`
|
|
241
|
+
|
|
242
|
+
## See Also
|
|
243
|
+
|
|
244
|
+
- [funstackStatic()](/funstack-static/api/funstack-static) - Configuration reference
|
|
245
|
+
- [Getting Started](/funstack-static/getting-started) - Quick start guide
|
|
246
|
+
- [defer()](/funstack-static/api/defer) - Deferred rendering for streaming
|
|
@@ -98,7 +98,7 @@ Each payload file has a **content-based hash** in its filename. This enables agg
|
|
|
98
98
|
|
|
99
99
|
**Consider below-the-fold content** - Content hidden in collapsed sections, tabs, or modals is a good candidate for `defer()` since users may never need it.
|
|
100
100
|
|
|
101
|
-
> **Tip:** You can combine `defer()` with React 19's `<Activity>` component to prefetch content in the background. When a `defer()`ed node is rendered under `<Activity mode="hidden">`, it won't be shown in the UI, but the fetch of the RSC payload will start immediately. This is useful when hidden content (like inactive tabs or collapsed sections) should be fetched ahead of time so it's ready when the user needs it.
|
|
101
|
+
> **Tip:** You can combine `defer()` with React 19's `<Activity>` component to prefetch content in the background. When a `defer()`ed node is rendered under `<Activity mode="hidden">`, it won't be shown in the UI, but the fetch of the RSC payload will start immediately. This is useful when hidden content (like inactive tabs or collapsed sections) should be fetched ahead of time so it's ready when the user needs it. See [Prefetching with defer() and Activity](/funstack-static/learn/defer-and-activity) for a detailed guide.
|
|
102
102
|
|
|
103
103
|
## See Also
|
|
104
104
|
|
package/dist/entries/rsc.d.mts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { defer } from "../rsc/defer.mjs";
|
|
2
|
-
import { RscPayload, build, isServeRSCError, serveHTML, serveRSC } from "../rsc/entry.mjs";
|
|
3
|
-
export { RscPayload, build, defer, isServeRSCError, serveHTML, serveRSC };
|
|
2
|
+
import { EntryBuildResult, RscPayload, build, isServeRSCError, serveHTML, serveRSC } from "../rsc/entry.mjs";
|
|
3
|
+
export { EntryBuildResult, RscPayload, build, defer, isServeRSCError, serveHTML, serveRSC };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
//#region src/entryDefinition.d.ts
|
|
4
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
5
|
+
type RootModule = {
|
|
6
|
+
default: React.ComponentType<{
|
|
7
|
+
children: React.ReactNode;
|
|
8
|
+
}>;
|
|
9
|
+
};
|
|
10
|
+
type AppModule = {
|
|
11
|
+
default: React.ComponentType;
|
|
12
|
+
};
|
|
13
|
+
interface EntryDefinition {
|
|
14
|
+
/**
|
|
15
|
+
* Output file path relative to the build output directory.
|
|
16
|
+
* Must end with ".html".
|
|
17
|
+
* Examples:
|
|
18
|
+
* "index.html"
|
|
19
|
+
* "about.html"
|
|
20
|
+
* "blog/post-1.html"
|
|
21
|
+
* "blog/post-1/index.html"
|
|
22
|
+
*/
|
|
23
|
+
path: string;
|
|
24
|
+
/**
|
|
25
|
+
* Root component module.
|
|
26
|
+
* Can be a lazy import or a synchronous module object.
|
|
27
|
+
* The module must have a `default` export of a React component.
|
|
28
|
+
*/
|
|
29
|
+
root: MaybePromise<RootModule> | (() => MaybePromise<RootModule>);
|
|
30
|
+
/**
|
|
31
|
+
* App content for this entry.
|
|
32
|
+
* Can be:
|
|
33
|
+
* - A module (sync or lazy) with a `default` export component.
|
|
34
|
+
* - A React node (JSX of a server component) for direct rendering.
|
|
35
|
+
*/
|
|
36
|
+
app: ReactNode | MaybePromise<AppModule> | (() => MaybePromise<AppModule>);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Return type of the getEntries function.
|
|
40
|
+
*/
|
|
41
|
+
type GetEntriesResult = Iterable<EntryDefinition> | AsyncIterable<EntryDefinition>;
|
|
42
|
+
//#endregion
|
|
43
|
+
export { AppModule, EntryDefinition, GetEntriesResult, MaybePromise, RootModule };
|
|
44
|
+
//# sourceMappingURL=entryDefinition.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"entryDefinition.d.mts","names":[],"sources":["../src/entryDefinition.ts"],"mappings":";;;KAEY,YAAA,MAAkB,CAAA,GAAI,OAAA,CAAQ,CAAA;AAAA,KAE9B,UAAA;EACV,OAAA,EAAS,KAAA,CAAM,aAAA;IAAgB,QAAA,EAAU,KAAA,CAAM,SAAA;EAAA;AAAA;AAAA,KAGrC,SAAA;EAAc,OAAA,EAAS,KAAA,CAAM,aAAA;AAAA;AAAA,UAExB,eAAA;EARQ;;;;;;AAEzB;;;EAgBE,IAAA;EAfA;;;;;EAqBA,IAAA,EAAM,YAAA,CAAa,UAAA,WAAqB,YAAA,CAAa,UAAA;EArBG;;AAG1D;;;;EAyBE,GAAA,EAAK,SAAA,GAAY,YAAA,CAAa,SAAA,WAAoB,YAAA,CAAa,SAAA;AAAA;;;;KAMrD,gBAAA,GACR,QAAA,CAAS,eAAA,IACT,aAAA,CAAc,eAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
package/dist/plugin/index.d.mts
CHANGED
|
@@ -1,18 +1,7 @@
|
|
|
1
1
|
import { Plugin } from "vite";
|
|
2
2
|
|
|
3
3
|
//#region src/plugin/index.d.ts
|
|
4
|
-
interface
|
|
5
|
-
/**
|
|
6
|
-
* Root component of the page.
|
|
7
|
-
* The file should `export default` a React component that renders the whole page.
|
|
8
|
-
* (`<html>...</html>`).
|
|
9
|
-
*/
|
|
10
|
-
root: string;
|
|
11
|
-
/**
|
|
12
|
-
* Entry point of your application.
|
|
13
|
-
* The file should `export default` a React component that renders the application content.
|
|
14
|
-
*/
|
|
15
|
-
app: string;
|
|
4
|
+
interface FunstackStaticBaseOptions {
|
|
16
5
|
/**
|
|
17
6
|
* Output directory for build.
|
|
18
7
|
*
|
|
@@ -34,13 +23,31 @@ interface FunstackStaticOptions {
|
|
|
34
23
|
*/
|
|
35
24
|
clientInit?: string;
|
|
36
25
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
26
|
+
interface SingleEntryOptions {
|
|
27
|
+
/**
|
|
28
|
+
* Root component of the page.
|
|
29
|
+
* The file should `export default` a React component that renders the whole page.
|
|
30
|
+
* (`<html>...</html>`).
|
|
31
|
+
*/
|
|
32
|
+
root: string;
|
|
33
|
+
/**
|
|
34
|
+
* Entry point of your application.
|
|
35
|
+
* The file should `export default` a React component that renders the application content.
|
|
36
|
+
*/
|
|
37
|
+
app: string;
|
|
38
|
+
entries?: never;
|
|
39
|
+
}
|
|
40
|
+
interface MultipleEntriesOptions {
|
|
41
|
+
root?: never;
|
|
42
|
+
app?: never;
|
|
43
|
+
/**
|
|
44
|
+
* Path to a module that exports a function returning entry definitions.
|
|
45
|
+
* Mutually exclusive with `root`+`app`.
|
|
46
|
+
*/
|
|
47
|
+
entries: string;
|
|
48
|
+
}
|
|
49
|
+
type FunstackStaticOptions = FunstackStaticBaseOptions & (SingleEntryOptions | MultipleEntriesOptions);
|
|
50
|
+
declare function funstackStatic(options: FunstackStaticOptions): (Plugin | Plugin[])[];
|
|
44
51
|
//#endregion
|
|
45
52
|
export { FunstackStaticOptions, funstackStatic };
|
|
46
53
|
//# sourceMappingURL=index.d.mts.map
|
|
@@ -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":";;;UAMU,yBAAA;;AALyB;;;;EAWjC,YAAA;EAQA;;;;AAMU;;;EANV,GAAA;EAeA;;;;;EATA,UAAA;AAAA;AAAA,UAGQ,kBAAA;EAesB;;;;;EAT9B,IAAA;EAmBU;;;;EAdV,GAAA;EACA,OAAA;AAAA;AAAA,UAGQ,sBAAA;EACR,IAAA;EACA,GAAA;EASsB;;;AAAwB;EAJ9C,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
|
@@ -4,10 +4,11 @@ import path from "node:path";
|
|
|
4
4
|
import rsc from "@vitejs/plugin-rsc";
|
|
5
5
|
|
|
6
6
|
//#region src/plugin/index.ts
|
|
7
|
-
function funstackStatic(
|
|
8
|
-
|
|
9
|
-
let
|
|
7
|
+
function funstackStatic(options) {
|
|
8
|
+
const { publicOutDir = "dist/public", ssr = false, clientInit } = options;
|
|
9
|
+
let resolvedEntriesModule = "__uninitialized__";
|
|
10
10
|
let resolvedClientInitEntry;
|
|
11
|
+
const isMultiEntry = "entries" in options && options.entries !== void 0;
|
|
11
12
|
return [
|
|
12
13
|
{
|
|
13
14
|
name: "@funstack/static:config-pre",
|
|
@@ -27,8 +28,15 @@ function funstackStatic({ root, app, publicOutDir = "dist/public", ssr = false,
|
|
|
27
28
|
{
|
|
28
29
|
name: "@funstack/static:config",
|
|
29
30
|
configResolved(config) {
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
if (isMultiEntry) resolvedEntriesModule = path.resolve(config.root, options.entries);
|
|
32
|
+
else {
|
|
33
|
+
const resolvedRoot = path.resolve(config.root, options.root);
|
|
34
|
+
const resolvedApp = path.resolve(config.root, options.app);
|
|
35
|
+
resolvedEntriesModule = JSON.stringify({
|
|
36
|
+
root: resolvedRoot,
|
|
37
|
+
app: resolvedApp
|
|
38
|
+
});
|
|
39
|
+
}
|
|
32
40
|
if (clientInit) resolvedClientInitEntry = path.resolve(config.root, clientInit);
|
|
33
41
|
},
|
|
34
42
|
configEnvironment(_name, config) {
|
|
@@ -44,14 +52,22 @@ function funstackStatic({ root, app, publicOutDir = "dist/public", ssr = false,
|
|
|
44
52
|
{
|
|
45
53
|
name: "@funstack/static:virtual-entry",
|
|
46
54
|
resolveId(id) {
|
|
47
|
-
if (id === "virtual:funstack/
|
|
48
|
-
if (id === "virtual:funstack/app") return "\0virtual:funstack/app";
|
|
55
|
+
if (id === "virtual:funstack/entries") return "\0virtual:funstack/entries";
|
|
49
56
|
if (id === "virtual:funstack/config") return "\0virtual:funstack/config";
|
|
50
57
|
if (id === "virtual:funstack/client-init") return "\0virtual:funstack/client-init";
|
|
51
58
|
},
|
|
52
59
|
load(id) {
|
|
53
|
-
if (id === "\0virtual:funstack/
|
|
54
|
-
|
|
60
|
+
if (id === "\0virtual:funstack/entries") {
|
|
61
|
+
if (isMultiEntry) return `export { default } from "${resolvedEntriesModule}";`;
|
|
62
|
+
const { root, app } = JSON.parse(resolvedEntriesModule);
|
|
63
|
+
return [
|
|
64
|
+
`import Root from "${root}";`,
|
|
65
|
+
`import App from "${app}";`,
|
|
66
|
+
`export default function getEntries() {`,
|
|
67
|
+
` return [{ path: "index.html", root: { default: Root }, app: { default: App } }];`,
|
|
68
|
+
`}`
|
|
69
|
+
].join("\n");
|
|
70
|
+
}
|
|
55
71
|
if (id === "\0virtual:funstack/config") return `export const ssr = ${JSON.stringify(ssr)};`;
|
|
56
72
|
if (id === "\0virtual:funstack/client-init") {
|
|
57
73
|
if (resolvedClientInitEntry) return `import "${resolvedClientInitEntry}";`;
|
|
@@ -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\
|
|
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 { publicOutDir = \"dist/public\", ssr = false, clientInit } = options;\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 `export const ssr = ${JSON.stringify(ssr)};`;\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);\n },\n },\n ];\n}\n"],"mappings":";;;;;;AAyDA,SAAwB,eACtB,SACuB;CACvB,MAAM,EAAE,eAAe,eAAe,MAAM,OAAO,eAAe;CAElE,IAAI,wBAAgC;CACpC,IAAI;CAGJ,MAAM,eAAe,aAAa,WAAW,QAAQ,YAAY;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,sBAAsB,KAAK,UAAU,IAAI,CAAC;AAEnD,QAAI,OAAO,kCAAkC;AAC3C,SAAI,wBACF,QAAO,WAAW,wBAAwB;AAE5C,YAAO;;;GAGZ;EACD;GACE,MAAM;GACN,MAAM,SAAS,SAAS;AACtB,UAAM,SAAS,SAAS,KAAK;;GAEhC;EACF"}
|
package/dist/plugin/server.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getRSCEntryPoint } from "./getRSCEntryPoint.mjs";
|
|
2
|
+
import { urlPathToFileCandidates } from "../util/urlPath.mjs";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { readFile } from "node:fs/promises";
|
|
4
5
|
import { isRunnableDevEnvironment } from "vite";
|
|
@@ -44,8 +45,14 @@ const serverPlugin = () => {
|
|
|
44
45
|
server.middlewares.use(async (req, res, next) => {
|
|
45
46
|
try {
|
|
46
47
|
if (req.headers.accept?.includes("text/html")) {
|
|
47
|
-
const
|
|
48
|
-
|
|
48
|
+
const urlPath = new URL(req.url, `http://${req.headers.host}`).pathname;
|
|
49
|
+
const candidates = urlPathToFileCandidates(urlPath);
|
|
50
|
+
for (const candidate of candidates) try {
|
|
51
|
+
const html = await readFile(path.join(resolvedOutDir, candidate), "utf-8");
|
|
52
|
+
res.end(html);
|
|
53
|
+
return;
|
|
54
|
+
} catch {}
|
|
55
|
+
next();
|
|
49
56
|
return;
|
|
50
57
|
}
|
|
51
58
|
} catch (error) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.mjs","names":[],"sources":["../../src/plugin/server.ts"],"sourcesContent":["import { isRunnableDevEnvironment, type Plugin } from \"vite\";\nimport { readFile } from \"node:fs/promises\";\nimport { toNodeHandler } from \"srvx/node\";\nimport path from \"node:path\";\nimport { getRSCEntryPoint } from \"./getRSCEntryPoint\";\n\nexport const serverPlugin = (): Plugin => {\n let resolvedOutDir = \"__uninitialized__\";\n return {\n name: \"@funstack/static:server\",\n configResolved(config) {\n resolvedOutDir = path.resolve(\n config.root,\n config.environments.client.build.outDir,\n );\n },\n configureServer(server) {\n const rscEnv = server.environments.rsc;\n if (!isRunnableDevEnvironment(rscEnv)) {\n throw new Error(\"The rsc environment is not runnable\");\n }\n\n return () => {\n server.middlewares.use(async (req, res, next) => {\n try {\n const rscEntry = await getRSCEntryPoint(rscEnv);\n if (req.headers.accept?.includes(\"text/html\")) {\n const fetchHandler = toNodeHandler(rscEntry.serveHTML);\n await fetchHandler(req as any, res as any);\n return;\n }\n const fetchHandler = toNodeHandler(rscEntry.serveRSC);\n try {\n await fetchHandler(req as any, res as any);\n } catch (error) {\n if (rscEntry.isServeRSCError(error) && error.status === 404) {\n next();\n return;\n }\n next(error);\n }\n } catch (error) {\n next(error);\n }\n });\n };\n },\n configurePreviewServer(server) {\n return () => {\n server.middlewares.use(async (req, res, next) => {\n try {\n if (req.headers.accept?.includes(\"text/html\")) {\n const html = await readFile(\n
|
|
1
|
+
{"version":3,"file":"server.mjs","names":[],"sources":["../../src/plugin/server.ts"],"sourcesContent":["import { isRunnableDevEnvironment, type Plugin } from \"vite\";\nimport { readFile } from \"node:fs/promises\";\nimport { toNodeHandler } from \"srvx/node\";\nimport path from \"node:path\";\nimport { getRSCEntryPoint } from \"./getRSCEntryPoint\";\nimport { urlPathToFileCandidates } from \"../util/urlPath\";\n\nexport const serverPlugin = (): Plugin => {\n let resolvedOutDir = \"__uninitialized__\";\n return {\n name: \"@funstack/static:server\",\n configResolved(config) {\n resolvedOutDir = path.resolve(\n config.root,\n config.environments.client.build.outDir,\n );\n },\n configureServer(server) {\n const rscEnv = server.environments.rsc;\n if (!isRunnableDevEnvironment(rscEnv)) {\n throw new Error(\"The rsc environment is not runnable\");\n }\n\n return () => {\n server.middlewares.use(async (req, res, next) => {\n try {\n const rscEntry = await getRSCEntryPoint(rscEnv);\n if (req.headers.accept?.includes(\"text/html\")) {\n // serveHTML now accepts a Request and routes by URL path\n const fetchHandler = toNodeHandler(rscEntry.serveHTML);\n await fetchHandler(req as any, res as any);\n return;\n }\n const fetchHandler = toNodeHandler(rscEntry.serveRSC);\n try {\n await fetchHandler(req as any, res as any);\n } catch (error) {\n if (rscEntry.isServeRSCError(error) && error.status === 404) {\n next();\n return;\n }\n next(error);\n }\n } catch (error) {\n next(error);\n }\n });\n };\n },\n configurePreviewServer(server) {\n return () => {\n server.middlewares.use(async (req, res, next) => {\n try {\n if (req.headers.accept?.includes(\"text/html\")) {\n const urlPath = new URL(req.url!, `http://${req.headers.host}`)\n .pathname;\n const candidates = urlPathToFileCandidates(urlPath);\n for (const candidate of candidates) {\n try {\n const html = await readFile(\n path.join(resolvedOutDir, candidate),\n \"utf-8\",\n );\n res.end(html);\n return;\n } catch {\n // Try next candidate\n }\n }\n // No matching file found — fall through to 404\n next();\n return;\n }\n } catch (error) {\n next(error);\n return;\n }\n next();\n });\n };\n },\n };\n};\n"],"mappings":";;;;;;;;AAOA,MAAa,qBAA6B;CACxC,IAAI,iBAAiB;AACrB,QAAO;EACL,MAAM;EACN,eAAe,QAAQ;AACrB,oBAAiB,KAAK,QACpB,OAAO,MACP,OAAO,aAAa,OAAO,MAAM,OAClC;;EAEH,gBAAgB,QAAQ;GACtB,MAAM,SAAS,OAAO,aAAa;AACnC,OAAI,CAAC,yBAAyB,OAAO,CACnC,OAAM,IAAI,MAAM,sCAAsC;AAGxD,gBAAa;AACX,WAAO,YAAY,IAAI,OAAO,KAAK,KAAK,SAAS;AAC/C,SAAI;MACF,MAAM,WAAW,MAAM,iBAAiB,OAAO;AAC/C,UAAI,IAAI,QAAQ,QAAQ,SAAS,YAAY,EAAE;AAG7C,aADqB,cAAc,SAAS,UAAU,CACnC,KAAY,IAAW;AAC1C;;MAEF,MAAM,eAAe,cAAc,SAAS,SAAS;AACrD,UAAI;AACF,aAAM,aAAa,KAAY,IAAW;eACnC,OAAO;AACd,WAAI,SAAS,gBAAgB,MAAM,IAAI,MAAM,WAAW,KAAK;AAC3D,cAAM;AACN;;AAEF,YAAK,MAAM;;cAEN,OAAO;AACd,WAAK,MAAM;;MAEb;;;EAGN,uBAAuB,QAAQ;AAC7B,gBAAa;AACX,WAAO,YAAY,IAAI,OAAO,KAAK,KAAK,SAAS;AAC/C,SAAI;AACF,UAAI,IAAI,QAAQ,QAAQ,SAAS,YAAY,EAAE;OAC7C,MAAM,UAAU,IAAI,IAAI,IAAI,KAAM,UAAU,IAAI,QAAQ,OAAO,CAC5D;OACH,MAAM,aAAa,wBAAwB,QAAQ;AACnD,YAAK,MAAM,aAAa,WACtB,KAAI;QACF,MAAM,OAAO,MAAM,SACjB,KAAK,KAAK,gBAAgB,UAAU,EACpC,QACD;AACD,YAAI,IAAI,KAAK;AACb;eACM;AAKV,aAAM;AACN;;cAEK,OAAO;AACd,WAAK,MAAM;AACX;;AAEF,WAAM;MACN;;;EAGP"}
|
package/dist/rsc/entry.d.mts
CHANGED
|
@@ -4,27 +4,33 @@ import { DeferRegistry, defer } from "./defer.mjs";
|
|
|
4
4
|
type RscPayload = {
|
|
5
5
|
root: React.ReactNode;
|
|
6
6
|
};
|
|
7
|
+
type EntryBuildResult = {
|
|
8
|
+
path: string;
|
|
9
|
+
html: ReadableStream<Uint8Array>;
|
|
10
|
+
appRsc: ReadableStream<Uint8Array>;
|
|
11
|
+
};
|
|
7
12
|
/**
|
|
8
|
-
* Entrypoint to serve HTML response in dev environment
|
|
13
|
+
* Entrypoint to serve HTML response in dev environment.
|
|
14
|
+
* Accepts a Request to determine which entry to render based on URL path.
|
|
9
15
|
*/
|
|
10
|
-
declare function serveHTML(): Promise<Response>;
|
|
16
|
+
declare function serveHTML(request: Request): Promise<Response>;
|
|
11
17
|
declare class ServeRSCError extends Error {
|
|
12
18
|
status: 404 | 500;
|
|
13
19
|
constructor(message: string, status: 404 | 500);
|
|
14
20
|
}
|
|
15
21
|
declare function isServeRSCError(error: unknown): error is ServeRSCError;
|
|
16
22
|
/**
|
|
17
|
-
*
|
|
23
|
+
* Serves an RSC stream response
|
|
18
24
|
*/
|
|
19
25
|
declare function serveRSC(request: Request): Promise<Response>;
|
|
20
26
|
/**
|
|
21
|
-
* Build handler
|
|
27
|
+
* Build handler — iterates over all entries and returns per-entry results
|
|
28
|
+
* along with the shared defer registry.
|
|
22
29
|
*/
|
|
23
30
|
declare function build(): Promise<{
|
|
24
|
-
|
|
25
|
-
appRsc: ReadableStream<Uint8Array<ArrayBufferLike>>;
|
|
31
|
+
entries: EntryBuildResult[];
|
|
26
32
|
deferRegistry: DeferRegistry;
|
|
27
33
|
}>;
|
|
28
34
|
//#endregion
|
|
29
|
-
export { RscPayload, build, isServeRSCError, serveHTML, serveRSC };
|
|
35
|
+
export { EntryBuildResult, RscPayload, build, isServeRSCError, serveHTML, serveRSC };
|
|
30
36
|
//# sourceMappingURL=entry.d.mts.map
|
package/dist/rsc/entry.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"entry.d.mts","names":[],"sources":["../../src/rsc/entry.tsx"],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"entry.d.mts","names":[],"sources":["../../src/rsc/entry.tsx"],"mappings":";;;KAWY,UAAA;EACV,IAAA,EAAM,KAAA,CAAM,SAAA;AAAA;AAAA,KAGF,gBAAA;EACV,IAAA;EACA,IAAA,EAAM,cAAA,CAAe,UAAA;EACrB,MAAA,EAAQ,cAAA,CAAe,UAAA;AAAA;;;;AAHzB;iBAoHsB,SAAA,CAAU,OAAA,EAAS,OAAA,GAAU,OAAA,CAAQ,QAAA;AAAA,cAqBrD,aAAA,SAAsB,KAAA;EAC1B,MAAA;cACY,OAAA,UAAiB,MAAA;AAAA;AAAA,iBAOf,eAAA,CAAgB,KAAA,YAAiB,KAAA,IAAS,aAAA;;;;iBAOpC,QAAA,CAAS,OAAA,EAAS,OAAA,GAAU,OAAA,CAAQ,QAAA;;;;;iBAkFpC,KAAA,CAAA,GAAK,OAAA"}
|