toiljs 0.0.60 → 0.0.62
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/.github/workflows/ci.yml +31 -0
- package/CHANGELOG.md +17 -0
- package/build/cli/.tsbuildinfo +1 -1
- package/build/cli/index.js +2 -2
- package/build/client/.tsbuildinfo +1 -1
- package/build/client/index.d.ts +1 -1
- package/build/client/index.js +1 -1
- package/build/client/routing/mount.js +11 -26
- package/build/client/ssr/markers.d.ts +1 -0
- package/build/client/ssr/markers.js +9 -2
- package/build/compiler/.tsbuildinfo +1 -1
- package/build/compiler/config.d.ts +21 -0
- package/build/compiler/config.js +35 -0
- package/build/compiler/docs.d.ts +2 -1
- package/build/compiler/docs.js +33 -304
- package/build/compiler/index.d.ts +13 -0
- package/build/compiler/index.js +113 -21
- package/build/compiler/template-build.d.ts +23 -3
- package/build/compiler/template-build.js +120 -30
- package/build/compiler/toil-docs.generated.d.ts +1 -0
- package/build/compiler/toil-docs.generated.js +20 -0
- package/build/devserver/.tsbuildinfo +1 -1
- package/build/devserver/daemon/catalog.d.ts +26 -0
- package/build/devserver/daemon/catalog.js +48 -0
- package/build/devserver/daemon/cron.d.ts +4 -0
- package/build/devserver/daemon/cron.js +50 -0
- package/build/devserver/daemon/host.d.ts +37 -0
- package/build/devserver/daemon/host.js +94 -0
- package/build/devserver/daemon/index.d.ts +34 -0
- package/build/devserver/daemon/index.js +241 -0
- package/build/devserver/db/catalog.d.ts +2 -1
- package/build/devserver/db/catalog.js +44 -44
- package/build/devserver/db/database.d.ts +27 -11
- package/build/devserver/db/database.js +539 -169
- package/build/devserver/db/index.d.ts +1 -1
- package/build/devserver/db/index.js +1 -1
- package/build/devserver/db/routeKinds.d.ts +8 -0
- package/build/devserver/db/routeKinds.js +139 -0
- package/build/devserver/db/types.d.ts +64 -1
- package/build/devserver/db/types.js +33 -1
- package/build/devserver/index.d.ts +10 -0
- package/build/devserver/index.js +7 -0
- package/build/devserver/mstore/store.d.ts +18 -0
- package/build/devserver/mstore/store.js +82 -0
- package/build/devserver/runtime/host.d.ts +6 -0
- package/build/devserver/runtime/host.js +45 -1
- package/build/devserver/runtime/module.d.ts +1 -0
- package/build/devserver/runtime/module.js +27 -1
- package/build/devserver/server.d.ts +6 -0
- package/build/devserver/server.js +59 -0
- package/build/devserver/ssr.d.ts +25 -0
- package/build/devserver/ssr.js +114 -0
- package/build/devserver/wasm/sections.d.ts +2 -0
- package/build/devserver/wasm/sections.js +42 -0
- package/build/devserver/wasm/surface.d.ts +18 -0
- package/build/devserver/wasm/surface.js +41 -0
- package/docs/README.md +4 -4
- package/docs/auth-todo.md +6 -6
- package/docs/caching.md +5 -5
- package/docs/cli.md +15 -0
- package/docs/client.md +40 -0
- package/docs/crypto.md +4 -4
- package/docs/data.md +6 -6
- package/docs/email.md +28 -28
- package/docs/environment.md +10 -10
- package/docs/index.md +26 -0
- package/docs/ratelimit.md +10 -10
- package/docs/routing.md +2 -2
- package/docs/server.md +61 -0
- package/docs/ssr.md +561 -113
- package/docs/styling.md +22 -0
- package/docs/time.md +1 -1
- package/eslint.config.js +10 -1
- package/examples/basic/client/components/Header.tsx +3 -0
- package/examples/basic/client/routes/features/actions.tsx +0 -2
- package/examples/basic/client/routes/hello.tsx +89 -19
- package/examples/basic/client/styles/main.css +48 -0
- package/examples/basic/server/SsrHelloRender.ts +97 -0
- package/examples/basic/server/main.ts +5 -0
- package/examples/basic/server/streams/Echo.ts +49 -0
- package/package.json +12 -10
- package/scripts/gen-toil-docs.mjs +96 -0
- package/src/cli/create.ts +2 -2
- package/src/client/index.ts +1 -1
- package/src/client/routing/mount.tsx +19 -31
- package/src/client/ssr/markers.tsx +33 -4
- package/src/compiler/config.ts +88 -2
- package/src/compiler/docs.ts +47 -308
- package/src/compiler/index.ts +236 -32
- package/src/compiler/ssr-codegen.ts +1 -1
- package/src/compiler/template-build.ts +271 -53
- package/src/compiler/toil-docs.generated.ts +26 -0
- package/src/devserver/daemon/catalog.ts +120 -0
- package/src/devserver/daemon/cron.ts +87 -0
- package/src/devserver/daemon/host.ts +224 -0
- package/src/devserver/daemon/index.ts +349 -0
- package/src/devserver/db/catalog.ts +61 -53
- package/src/devserver/db/database.ts +613 -149
- package/src/devserver/db/index.ts +1 -1
- package/src/devserver/db/routeKinds.ts +147 -0
- package/src/devserver/db/types.ts +65 -2
- package/src/devserver/index.ts +12 -0
- package/src/devserver/mstore/store.ts +121 -0
- package/src/devserver/runtime/host.ts +92 -1
- package/src/devserver/runtime/module.ts +35 -1
- package/src/devserver/server.ts +101 -0
- package/src/devserver/ssr.ts +166 -0
- package/src/devserver/wasm/sections.ts +59 -0
- package/src/devserver/wasm/surface.ts +88 -0
- package/test/daemon-build.test.ts +198 -0
- package/test/daemon-catalog.test.ts +265 -0
- package/test/daemon-emulation.test.ts +216 -0
- package/test/devserver-database.test.ts +396 -5
- package/test/email-preview.test.ts +6 -1
- package/test/fixtures/daemon-app.ts +56 -0
- package/test/global-setup.ts +17 -0
- package/test/ssr-hydration.test.tsx +107 -0
- package/test/ssr-render.test.ts +96 -27
- package/test/ssr-template.test.tsx +47 -2
- package/vitest.config.ts +3 -0
|
@@ -14,6 +14,15 @@
|
|
|
14
14
|
* `Slot` enum + `HASH`. A route (or layout) that throws under static markup
|
|
15
15
|
* (e.g. it uses router hooks outside the supported subset) is skipped with a
|
|
16
16
|
* warning and falls back to pure client rendering.
|
|
17
|
+
*
|
|
18
|
+
* The `Slot` enum + `HASH` is also what the SERVER `render` imports, so it must
|
|
19
|
+
* exist BEFORE the server compiles. The build therefore runs the render in two
|
|
20
|
+
* passes: a slots PRE-PASS (`extractServerSlots`, before the server build) emits
|
|
21
|
+
* the server-importable `<server>/_ssr/<name>.slots.ts` so toilscript can compile
|
|
22
|
+
* the `render`; the FINAL pass (`extractTemplates`, after the Vite client build)
|
|
23
|
+
* rewrites it against the real built shell so the `HASH` is authoritative, and
|
|
24
|
+
* reports whether it rotated so the caller can recompile the server once. This is
|
|
25
|
+
* what makes a clean build need ZERO hand-maintained slots.
|
|
17
26
|
*/
|
|
18
27
|
|
|
19
28
|
import fs from 'node:fs';
|
|
@@ -21,7 +30,7 @@ import { createRequire } from 'node:module';
|
|
|
21
30
|
import path from 'node:path';
|
|
22
31
|
|
|
23
32
|
import { type ComponentType, type Context, createElement, type ReactNode } from 'react';
|
|
24
|
-
import {
|
|
33
|
+
import { renderToString } from 'react-dom/server';
|
|
25
34
|
import { createServer } from 'vite';
|
|
26
35
|
|
|
27
36
|
import { type ResolvedToilConfig } from './config.js';
|
|
@@ -62,8 +71,10 @@ export interface RouteRenderInput {
|
|
|
62
71
|
* React copies leaves the hook dispatcher null ("Cannot read properties of
|
|
63
72
|
* null (reading 'useRef')"). */
|
|
64
73
|
createElement?: typeof createElement;
|
|
65
|
-
/** `
|
|
66
|
-
|
|
74
|
+
/** `renderToString` paired with {@link createElement}'s React. We use it (NOT
|
|
75
|
+
* `renderToStaticMarkup`) because hydration needs the `<!-- -->` text-node
|
|
76
|
+
* boundary markers it emits, so `hydrateRoot` can align "text + hole" runs. */
|
|
77
|
+
renderToString?: typeof renderToString;
|
|
67
78
|
}
|
|
68
79
|
|
|
69
80
|
export interface TemplateArtifacts {
|
|
@@ -106,10 +117,25 @@ export function injectIntoShell(shell: string, routeHtml: string): string {
|
|
|
106
117
|
return shell.replace(ROOT_DIV, `<div id="root">${routeHtml}</div>${SSR_MARKER}`);
|
|
107
118
|
}
|
|
108
119
|
|
|
120
|
+
/**
|
|
121
|
+
* React 19 auto-emits hoistable resource tags into `<head>` on the client (it
|
|
122
|
+
* adds a `<link rel="preload">` for an `<img>`, and hoists `<title>` / `<meta>`),
|
|
123
|
+
* but the string renderer emits them INLINE in the route fragment. Left in the
|
|
124
|
+
* spliced `#root` template they would not appear in the client's hydrated `#root`
|
|
125
|
+
* (the client puts them in `<head>`), so `hydrateRoot` reports a mismatch. Strip
|
|
126
|
+
* them from the route fragment; the shell already carries the document head, and
|
|
127
|
+
* the client re-adds its own resource hints. Only the fragment is stripped. */
|
|
128
|
+
function stripHoistedResourceTags(html: string): string {
|
|
129
|
+
return html
|
|
130
|
+
.replace(/<link\b[^>]*>/gi, '')
|
|
131
|
+
.replace(/<meta\b[^>]*>/gi, '')
|
|
132
|
+
.replace(/<title\b[^>]*>[\s\S]*?<\/title>/gi, '');
|
|
133
|
+
}
|
|
134
|
+
|
|
109
135
|
/** Render one route to its template artifacts (pure given its inputs). */
|
|
110
136
|
export function extractRouteTemplate(input: RouteRenderInput): TemplateArtifacts {
|
|
111
137
|
const h = input.createElement ?? createElement;
|
|
112
|
-
const render = input.
|
|
138
|
+
const render = input.renderToString ?? renderToString;
|
|
113
139
|
const element = assembleRouteElement(
|
|
114
140
|
input.Page,
|
|
115
141
|
input.layouts,
|
|
@@ -124,7 +150,7 @@ export function extractRouteTemplate(input: RouteRenderInput): TemplateArtifacts
|
|
|
124
150
|
} finally {
|
|
125
151
|
input.setSsrBuild(false);
|
|
126
152
|
}
|
|
127
|
-
const full = injectIntoShell(input.shell, routeHtml);
|
|
153
|
+
const full = injectIntoShell(input.shell, stripHoistedResourceTags(routeHtml));
|
|
128
154
|
const extracted: Extracted = extractFromHtml(full);
|
|
129
155
|
const ids = assignSlotIds(extracted.slots);
|
|
130
156
|
const hash = coherenceHash(extracted.tmpl, extracted.slots);
|
|
@@ -146,46 +172,81 @@ export function writeTemplateArtifacts(ssrDir: string, art: TemplateArtifacts):
|
|
|
146
172
|
fs.writeFileSync(path.join(ssrDir, `${art.name}.slots.ts`), art.slotsModule);
|
|
147
173
|
}
|
|
148
174
|
|
|
149
|
-
/**
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
175
|
+
/**
|
|
176
|
+
* The server source dir (where toilscript-compiled modules live): the dir of the first toilconfig
|
|
177
|
+
* entry, else `<root>/server`. Mirrors the same resolution in `emails.ts` (`_emails.ts` lives here).
|
|
178
|
+
*/
|
|
179
|
+
function serverDir(root: string): string {
|
|
180
|
+
try {
|
|
181
|
+
const cfg = JSON.parse(fs.readFileSync(path.join(root, 'toilconfig.json'), 'utf8')) as {
|
|
182
|
+
entries?: unknown;
|
|
183
|
+
};
|
|
184
|
+
const first = Array.isArray(cfg.entries)
|
|
185
|
+
? cfg.entries.find((e): e is string => typeof e === 'string')
|
|
186
|
+
: undefined;
|
|
187
|
+
if (first) return path.dirname(path.resolve(root, first));
|
|
188
|
+
} catch {
|
|
189
|
+
// fall through to the default
|
|
190
|
+
}
|
|
191
|
+
return path.join(root, 'server');
|
|
153
192
|
}
|
|
154
193
|
|
|
155
|
-
/**
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
194
|
+
/**
|
|
195
|
+
* The build-owned dir that holds the GENERATED, server-importable `<name>.slots.ts` modules
|
|
196
|
+
* (the `Slot` enum + `HASH` the guest `render` imports). It sits inside the server source tree so
|
|
197
|
+
* toilscript compiles it, named `_ssr` to match the generated `_emails.ts` convention, and is
|
|
198
|
+
* gitignored + regenerated every build — never hand-edited. The server `render` imports
|
|
199
|
+
* `./_ssr/<name>.slots`.
|
|
200
|
+
*/
|
|
201
|
+
export function serverSlotsDir(root: string): string {
|
|
202
|
+
return path.join(serverDir(root), '_ssr');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* (Re)write the generated `<server>/_ssr/<name>.slots.ts` module(s) the server `render` imports.
|
|
207
|
+
* Returns the map of `name -> module source` actually on disk afterwards, so the caller can detect
|
|
208
|
+
* whether a later authoritative extraction changed the `HASH` and a server recompile is needed.
|
|
209
|
+
* Only rewrites a file whose content actually changed (keeps mtimes stable for the dev watcher).
|
|
210
|
+
*/
|
|
211
|
+
export function writeServerSlotsModules(
|
|
212
|
+
root: string,
|
|
213
|
+
modules: { name: string; slotsModule: string }[],
|
|
214
|
+
): Map<string, string> {
|
|
215
|
+
const dir = serverSlotsDir(root);
|
|
216
|
+
const written = new Map<string, string>();
|
|
217
|
+
if (modules.length === 0) return written;
|
|
218
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
219
|
+
for (const m of modules) {
|
|
220
|
+
const file = path.join(dir, `${m.name}.slots.ts`);
|
|
221
|
+
let prev: string | null = null;
|
|
222
|
+
try {
|
|
223
|
+
prev = fs.readFileSync(file, 'utf8');
|
|
224
|
+
} catch {
|
|
225
|
+
// first build for this route: no prior module
|
|
226
|
+
}
|
|
227
|
+
if (prev !== m.slotsModule) fs.writeFileSync(file, m.slotsModule);
|
|
228
|
+
written.set(m.name, m.slotsModule);
|
|
160
229
|
}
|
|
161
|
-
return
|
|
230
|
+
return written;
|
|
162
231
|
}
|
|
163
232
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
233
|
+
/** A rendered SSR route plus the artifacts the build emits for it. */
|
|
234
|
+
interface RenderedRoute {
|
|
235
|
+
pattern: string;
|
|
236
|
+
art: TemplateArtifacts;
|
|
168
237
|
}
|
|
169
238
|
|
|
170
239
|
/**
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
240
|
+
* Spin up a short-lived Vite SSR server, render every `export const ssr = true` route under its
|
|
241
|
+
* layout chain (with sample loader data, in sentinel mode) against `shell`, and return the
|
|
242
|
+
* per-route artifacts. Shared by the pre-pass (`extractServerSlots`, slots only) and the final
|
|
243
|
+
* `extractTemplates` (full artifacts). A route (or layout) that throws under static markup is
|
|
244
|
+
* skipped with a warning and omitted from the result, so it falls back to client rendering.
|
|
175
245
|
*/
|
|
176
|
-
|
|
177
|
-
cfg: ResolvedToilConfig,
|
|
178
|
-
hostName = 'edge',
|
|
179
|
-
): Promise<string[]> {
|
|
246
|
+
async function renderSsrRoutes(cfg: ResolvedToilConfig, shell: string): Promise<RenderedRoute[]> {
|
|
180
247
|
const routes = scanRoutes(cfg.routesAbsDir).filter((r) => r.slot === undefined && !r.intercept);
|
|
181
248
|
if (routes.length === 0) return [];
|
|
182
249
|
|
|
183
|
-
const outDir = path.resolve(cfg.root, cfg.outDir);
|
|
184
|
-
const stashed = path.join(cfg.toilDir, 'shell.html');
|
|
185
|
-
const shellPath = fs.existsSync(stashed) ? stashed : path.join(outDir, 'index.html');
|
|
186
|
-
if (!fs.existsSync(shellPath)) return [];
|
|
187
|
-
const shell = fs.readFileSync(shellPath, 'utf8');
|
|
188
|
-
|
|
189
250
|
const warn = (msg: string): void => {
|
|
190
251
|
process.stderr.write(` toil: SSR ${msg}\n`);
|
|
191
252
|
};
|
|
@@ -220,14 +281,10 @@ export async function extractTemplates(
|
|
|
220
281
|
const appRequire = createRequire(path.join(cfg.root, 'package.json'));
|
|
221
282
|
const react = appRequire('react') as { createElement: typeof createElement };
|
|
222
283
|
const reactDomServer = appRequire('react-dom/server') as {
|
|
223
|
-
|
|
284
|
+
renderToString: typeof renderToString;
|
|
224
285
|
};
|
|
225
286
|
|
|
226
|
-
const
|
|
227
|
-
const hostsTmplDir = path.join(cfg.root, 'hosts', hostName, '_tmpl');
|
|
228
|
-
const generated: string[] = [];
|
|
229
|
-
const index: { route: string; name: string; hash: string }[] = [];
|
|
230
|
-
|
|
287
|
+
const rendered: RenderedRoute[] = [];
|
|
231
288
|
try {
|
|
232
289
|
for (const r of routes) {
|
|
233
290
|
let mod: RouteModule;
|
|
@@ -268,20 +325,9 @@ export async function extractTemplates(
|
|
|
268
325
|
setSsrBuild: client.__setSsrBuild,
|
|
269
326
|
shell,
|
|
270
327
|
createElement: react.createElement,
|
|
271
|
-
|
|
328
|
+
renderToString: reactDomServer.renderToString,
|
|
272
329
|
});
|
|
273
|
-
|
|
274
|
-
fs.mkdirSync(hostsTmplDir, { recursive: true });
|
|
275
|
-
fs.copyFileSync(
|
|
276
|
-
path.join(ssrDir, `${name}.tmpl`),
|
|
277
|
-
path.join(hostsTmplDir, `${name}.tmpl`),
|
|
278
|
-
);
|
|
279
|
-
fs.copyFileSync(
|
|
280
|
-
path.join(ssrDir, `${name}.slots`),
|
|
281
|
-
path.join(hostsTmplDir, `${name}.slots`),
|
|
282
|
-
);
|
|
283
|
-
index.push({ route: r.pattern, name, hash: art.hash.toString('hex') });
|
|
284
|
-
generated.push(r.pattern);
|
|
330
|
+
rendered.push({ pattern: r.pattern, art });
|
|
285
331
|
} catch (err) {
|
|
286
332
|
warn(
|
|
287
333
|
`skipped ${r.pattern} (render failed: ${
|
|
@@ -293,6 +339,178 @@ export async function extractTemplates(
|
|
|
293
339
|
} finally {
|
|
294
340
|
await server.close();
|
|
295
341
|
}
|
|
342
|
+
return rendered;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Resolve the HTML shell to splice routes into. The authoritative shell is the BUILT (post-Vite)
|
|
347
|
+
* `index.html`, whose hashed `<script>`/`<link>` tags are part of the template and therefore the
|
|
348
|
+
* coherence `HASH`. `preferBuilt` (the final extraction) demands it; the slots PRE-PASS (which runs
|
|
349
|
+
* before Vite) falls back to the previous build's shell when present (so an unchanged rebuild's
|
|
350
|
+
* pre-pass `HASH` already matches the final one and no server recompile is needed), and finally to
|
|
351
|
+
* the un-built `.toil/index.html` template on a first clean build. Returns `null` when no shell
|
|
352
|
+
* exists at all (no client build yet and no template), so the caller no-ops.
|
|
353
|
+
*/
|
|
354
|
+
function resolveShell(cfg: ResolvedToilConfig, preferBuilt: boolean): string | null {
|
|
355
|
+
const outDir = path.resolve(cfg.root, cfg.outDir);
|
|
356
|
+
const builtIndex = path.join(outDir, 'index.html');
|
|
357
|
+
const stashed = path.join(cfg.toilDir, 'shell.html');
|
|
358
|
+
const templateIndex = path.join(cfg.toilDir, 'index.html');
|
|
359
|
+
const order = preferBuilt
|
|
360
|
+
? [stashed, builtIndex]
|
|
361
|
+
: // Pre-pass (before Vite): use a prior build's shell if it exists so the HASH is stable
|
|
362
|
+
// across rebuilds; otherwise the generated (un-built) template, which still yields the
|
|
363
|
+
// correct Slot ids (the final pass reconciles the HASH).
|
|
364
|
+
[builtIndex, stashed, templateIndex];
|
|
365
|
+
for (const p of order) {
|
|
366
|
+
if (fs.existsSync(p)) return fs.readFileSync(p, 'utf8');
|
|
367
|
+
}
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* SLOTS PRE-PASS (runs BEFORE the server build): render every `ssr = true` route to its `Slot`
|
|
373
|
+
* enum + `HASH` and write the server-importable `<server>/_ssr/<name>.slots.ts` module(s), so the
|
|
374
|
+
* server `render` can import them when toilscript compiles it. On a clean build no `.slots.ts`
|
|
375
|
+
* exists yet, so this is what bootstraps it; on a rebuild it refreshes them. The `Slot` ids are
|
|
376
|
+
* always correct (they depend only on the route's hole structure); the `HASH` is final only when a
|
|
377
|
+
* prior build's shell is reused, so the final `extractTemplates` reconciles it (and the caller
|
|
378
|
+
* recompiles the server if it rotated). Returns the modules written keyed by route name.
|
|
379
|
+
*/
|
|
380
|
+
export async function extractServerSlots(cfg: ResolvedToilConfig): Promise<Map<string, string>> {
|
|
381
|
+
const shell = resolveShell(cfg, false);
|
|
382
|
+
if (shell === null) return new Map();
|
|
383
|
+
const rendered = await renderSsrRoutes(cfg, shell);
|
|
384
|
+
return writeServerSlotsModules(
|
|
385
|
+
cfg.root,
|
|
386
|
+
rendered.map((r) => ({ name: r.art.name, slotsModule: r.art.slotsModule })),
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** One SSR route's in-memory dev template: the spliceable `.tmpl` plus its
|
|
391
|
+
* top-level slot insertion points (numeric id -> byte offset), parsed from the
|
|
392
|
+
* `.slots` manifest. Used by the dev server to serve SSR without a prod build. */
|
|
393
|
+
export interface DevSsrTemplate {
|
|
394
|
+
pattern: string;
|
|
395
|
+
name: string;
|
|
396
|
+
tmpl: Buffer;
|
|
397
|
+
entries: { id: number; offset: number }[];
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Render every `ssr = true` route against the given (live, Vite-transformed) dev
|
|
402
|
+
* `shell` into an in-memory template + slot offsets, for the dev server to splice
|
|
403
|
+
* the guest `render` values into. Unlike {@link extractTemplates} this has NO
|
|
404
|
+
* side effects (it writes nothing and does not touch `server/_ssr` or the host
|
|
405
|
+
* bundle); the dev server builds together with the guest, so there is no hash to
|
|
406
|
+
* reconcile. Returns `[]` when no route opts in.
|
|
407
|
+
*/
|
|
408
|
+
export async function extractDevSsrTemplates(
|
|
409
|
+
cfg: ResolvedToilConfig,
|
|
410
|
+
shell: string,
|
|
411
|
+
): Promise<DevSsrTemplate[]> {
|
|
412
|
+
const rendered = await renderSsrRoutes(cfg, shell);
|
|
413
|
+
return rendered.map(({ pattern, art }) => {
|
|
414
|
+
// Parse the `.slots` manifest: 46-byte header (n_slots at offset 44),
|
|
415
|
+
// then 8-byte entries (u32 offset, u16 id, u8 kind, u8 reserved).
|
|
416
|
+
const bin = art.slotsBin;
|
|
417
|
+
const n = bin.readUInt16LE(44);
|
|
418
|
+
const entries: { id: number; offset: number }[] = [];
|
|
419
|
+
let o = 46;
|
|
420
|
+
for (let i = 0; i < n; i++) {
|
|
421
|
+
entries.push({ offset: bin.readUInt32LE(o), id: bin.readUInt16LE(o + 4) });
|
|
422
|
+
o += 8;
|
|
423
|
+
}
|
|
424
|
+
return { pattern, name: art.name, tmpl: art.tmpl, entries };
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/** A file-safe, identifier-ish name for a route pattern (`/u/:name` -> `u_name`). */
|
|
429
|
+
export function routeTemplateName(pattern: string): string {
|
|
430
|
+
const n = pattern.replace(/[^A-Za-z0-9]+/g, '_').replace(/^_+|_+$/g, '');
|
|
431
|
+
return n.length > 0 ? n : 'index';
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/** Synthesize a sample param set for a pattern's dynamic segments. */
|
|
435
|
+
function sampleParams(pattern: string): Record<string, string> {
|
|
436
|
+
const params: Record<string, string> = {};
|
|
437
|
+
for (const m of pattern.matchAll(/[:*]+([A-Za-z0-9_]+)/g)) {
|
|
438
|
+
params[m[1]] = 'sample';
|
|
439
|
+
}
|
|
440
|
+
return params;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
interface RouteModule {
|
|
444
|
+
default: ComponentType;
|
|
445
|
+
ssr?: boolean;
|
|
446
|
+
loader?: (args: { params: Record<string, string>; searchParams: URLSearchParams }) => unknown;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/** The outcome of the final {@link extractTemplates} pass. */
|
|
450
|
+
export interface ExtractResult {
|
|
451
|
+
/** The route patterns that produced a template. */
|
|
452
|
+
readonly generated: string[];
|
|
453
|
+
/**
|
|
454
|
+
* Whether the AUTHORITATIVE `<server>/_ssr/<name>.slots.ts` module(s) just written differ from
|
|
455
|
+
* the ones the server was already compiled against (`priorServerSlots`). True means the `HASH`
|
|
456
|
+
* (or `Slot` ids) rotated after the server build, so the server must be recompiled to bake the
|
|
457
|
+
* correct `HASH` (otherwise the host rejects the guest's stale hash as a deploy skew).
|
|
458
|
+
*/
|
|
459
|
+
readonly serverSlotsChanged: boolean;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* FINAL pass (runs AFTER the client/Vite build): render every `export const ssr = true` route to
|
|
464
|
+
* `<outDir>/_ssr/<name>.{tmpl,slots,slots.ts}` + a `templates.json` index, copy the `.tmpl`/`.slots`
|
|
465
|
+
* into the edge host bundle at `hosts/<host>/_tmpl/`, AND (re)write the server-importable
|
|
466
|
+
* `<server>/_ssr/<name>.slots.ts` module(s) — now against the real built shell, so the `HASH` is
|
|
467
|
+
* authoritative. Skips (with a warning) any route that throws under static markup.
|
|
468
|
+
*
|
|
469
|
+
* `priorServerSlots` is the module map the slots PRE-PASS wrote (what the server was compiled
|
|
470
|
+
* against); compared against the authoritative modules here to report whether a server recompile is
|
|
471
|
+
* needed (see {@link ExtractResult.serverSlotsChanged}).
|
|
472
|
+
*/
|
|
473
|
+
export async function extractTemplates(
|
|
474
|
+
cfg: ResolvedToilConfig,
|
|
475
|
+
hostName = 'edge',
|
|
476
|
+
priorServerSlots: Map<string, string> = new Map(),
|
|
477
|
+
): Promise<ExtractResult> {
|
|
478
|
+
const shell = resolveShell(cfg, true);
|
|
479
|
+
if (shell === null) return { generated: [], serverSlotsChanged: false };
|
|
480
|
+
|
|
481
|
+
const rendered = await renderSsrRoutes(cfg, shell);
|
|
482
|
+
|
|
483
|
+
const outDir = path.resolve(cfg.root, cfg.outDir);
|
|
484
|
+
const ssrDir = path.join(outDir, '_ssr');
|
|
485
|
+
const hostsTmplDir = path.join(cfg.root, 'hosts', hostName, '_tmpl');
|
|
486
|
+
const generated: string[] = [];
|
|
487
|
+
const index: { route: string; name: string; hash: string }[] = [];
|
|
488
|
+
|
|
489
|
+
for (const { pattern, art } of rendered) {
|
|
490
|
+
writeTemplateArtifacts(ssrDir, art);
|
|
491
|
+
fs.mkdirSync(hostsTmplDir, { recursive: true });
|
|
492
|
+
fs.copyFileSync(
|
|
493
|
+
path.join(ssrDir, `${art.name}.tmpl`),
|
|
494
|
+
path.join(hostsTmplDir, `${art.name}.tmpl`),
|
|
495
|
+
);
|
|
496
|
+
fs.copyFileSync(
|
|
497
|
+
path.join(ssrDir, `${art.name}.slots`),
|
|
498
|
+
path.join(hostsTmplDir, `${art.name}.slots`),
|
|
499
|
+
);
|
|
500
|
+
index.push({ route: pattern, name: art.name, hash: art.hash.toString('hex') });
|
|
501
|
+
generated.push(pattern);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Write the AUTHORITATIVE server-importable slots module(s) (the real built-shell HASH) and
|
|
505
|
+
// detect whether they changed since the pre-pass the server was compiled against.
|
|
506
|
+
const authoritative = writeServerSlotsModules(
|
|
507
|
+
cfg.root,
|
|
508
|
+
rendered.map((r) => ({ name: r.art.name, slotsModule: r.art.slotsModule })),
|
|
509
|
+
);
|
|
510
|
+
let serverSlotsChanged = false;
|
|
511
|
+
for (const [name, mod] of authoritative) {
|
|
512
|
+
if (priorServerSlots.get(name) !== mod) serverSlotsChanged = true;
|
|
513
|
+
}
|
|
296
514
|
|
|
297
515
|
if (generated.length > 0) {
|
|
298
516
|
fs.mkdirSync(ssrDir, { recursive: true });
|
|
@@ -303,5 +521,5 @@ export async function extractTemplates(
|
|
|
303
521
|
}\n`,
|
|
304
522
|
);
|
|
305
523
|
}
|
|
306
|
-
return generated;
|
|
524
|
+
return { generated, serverSlotsChanged };
|
|
307
525
|
}
|