toiljs 0.0.59 → 0.0.61
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 +15 -0
- package/build/cli/.tsbuildinfo +1 -1
- package/build/cli/index.js +311 -118
- 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 +12 -1
- package/build/client/ssr/markers.d.ts +1 -0
- package/build/client/ssr/markers.js +3 -0
- 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 +21 -1
- package/build/compiler/template-build.js +110 -26
- 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 -0
- package/build/devserver/db/catalog.js +80 -0
- package/build/devserver/db/database.d.ts +80 -0
- package/build/devserver/db/database.js +1032 -0
- package/build/devserver/db/index.d.ts +3 -0
- package/build/devserver/db/index.js +3 -0
- package/build/devserver/db/routeKinds.d.ts +8 -0
- package/build/devserver/db/routeKinds.js +139 -0
- package/build/devserver/db/types.d.ts +121 -0
- package/build/devserver/db/types.js +52 -0
- package/build/devserver/email/index.js +1 -1
- package/build/devserver/index.d.ts +19 -24
- package/build/devserver/index.js +11 -165
- package/build/devserver/mstore/store.d.ts +18 -0
- package/build/devserver/mstore/store.js +82 -0
- package/build/devserver/{host.d.ts → runtime/host.d.ts} +7 -1
- package/build/devserver/{host.js → runtime/host.js} +51 -7
- package/build/devserver/{module.d.ts → runtime/module.d.ts} +2 -1
- package/build/devserver/{module.js → runtime/module.js} +34 -1
- package/build/devserver/server.d.ts +23 -0
- package/build/devserver/server.js +223 -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 +3 -3
- 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/migrations/GuestEntry.migration.ts +39 -0
- package/examples/basic/server/streams/Echo.ts +49 -0
- package/package.json +12 -10
- package/scripts/gen-toil-docs.mjs +96 -0
- package/server/runtime/time.ts +3 -3
- package/src/cli/create.ts +40 -3
- package/src/cli/db.ts +158 -0
- package/src/cli/diagnostics.ts +19 -0
- package/src/cli/doctor.ts +20 -0
- package/src/cli/index.ts +10 -0
- package/src/cli/update.ts +58 -0
- package/src/client/index.ts +1 -1
- package/src/client/routing/mount.tsx +18 -2
- package/src/client/ssr/markers.tsx +22 -0
- 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 +247 -46
- 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 +108 -0
- package/src/devserver/db/database.ts +1633 -0
- package/src/devserver/db/index.ts +18 -0
- package/src/devserver/db/routeKinds.ts +147 -0
- package/src/devserver/db/types.ts +139 -0
- package/src/devserver/email/index.ts +1 -1
- package/src/devserver/index.ts +31 -287
- package/src/devserver/mstore/store.ts +121 -0
- package/src/devserver/{host.ts → runtime/host.ts} +98 -7
- package/src/devserver/{module.ts → runtime/module.ts} +47 -1
- package/src/devserver/server.ts +393 -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/db.test.ts +0 -0
- package/test/devserver-database.test.ts +510 -14
- package/test/devserver-pqauth.test.ts +1 -1
- package/test/devserver-secrets.test.ts +5 -1
- package/test/doctor.test.ts +13 -0
- package/test/email-preview.test.ts +6 -1
- package/test/example-guestbook.test.ts +43 -1
- package/test/fixtures/daemon-app.ts +56 -0
- package/test/global-setup.ts +17 -0
- package/test/pqauth-e2e.test.ts +1 -1
- package/test/ssr-render.test.ts +94 -27
- package/test/ssr-template.test.tsx +44 -1
- package/vitest.config.ts +3 -0
- package/build/devserver/database.d.ts +0 -8
- package/build/devserver/database.js +0 -418
- package/src/devserver/database.ts +0 -618
- /package/build/devserver/{dotenv.d.ts → config/dotenv.d.ts} +0 -0
- /package/build/devserver/{dotenv.js → config/dotenv.js} +0 -0
- /package/build/devserver/{env.d.ts → config/env.d.ts} +0 -0
- /package/build/devserver/{env.js → config/env.js} +0 -0
- /package/build/devserver/{ratelimit.d.ts → config/ratelimit.d.ts} +0 -0
- /package/build/devserver/{ratelimit.js → config/ratelimit.js} +0 -0
- /package/build/devserver/{cache.d.ts → http/cache.d.ts} +0 -0
- /package/build/devserver/{cache.js → http/cache.js} +0 -0
- /package/build/devserver/{envelope.d.ts → http/envelope.d.ts} +0 -0
- /package/build/devserver/{envelope.js → http/envelope.js} +0 -0
- /package/build/devserver/{proxy.d.ts → http/proxy.d.ts} +0 -0
- /package/build/devserver/{proxy.js → http/proxy.js} +0 -0
- /package/build/devserver/{crypto.d.ts → runtime/crypto.d.ts} +0 -0
- /package/build/devserver/{crypto.js → runtime/crypto.js} +0 -0
- /package/src/devserver/{dotenv.ts → config/dotenv.ts} +0 -0
- /package/src/devserver/{env.ts → config/env.ts} +0 -0
- /package/src/devserver/{ratelimit.ts → config/ratelimit.ts} +0 -0
- /package/src/devserver/{cache.ts → http/cache.ts} +0 -0
- /package/src/devserver/{envelope.ts → http/envelope.ts} +0 -0
- /package/src/devserver/{proxy.ts → http/proxy.ts} +0 -0
- /package/src/devserver/{crypto.ts → runtime/crypto.ts} +0 -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';
|
|
@@ -146,46 +155,81 @@ export function writeTemplateArtifacts(ssrDir: string, art: TemplateArtifacts):
|
|
|
146
155
|
fs.writeFileSync(path.join(ssrDir, `${art.name}.slots.ts`), art.slotsModule);
|
|
147
156
|
}
|
|
148
157
|
|
|
149
|
-
/**
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
158
|
+
/**
|
|
159
|
+
* The server source dir (where toilscript-compiled modules live): the dir of the first toilconfig
|
|
160
|
+
* entry, else `<root>/server`. Mirrors the same resolution in `emails.ts` (`_emails.ts` lives here).
|
|
161
|
+
*/
|
|
162
|
+
function serverDir(root: string): string {
|
|
163
|
+
try {
|
|
164
|
+
const cfg = JSON.parse(fs.readFileSync(path.join(root, 'toilconfig.json'), 'utf8')) as {
|
|
165
|
+
entries?: unknown;
|
|
166
|
+
};
|
|
167
|
+
const first = Array.isArray(cfg.entries)
|
|
168
|
+
? cfg.entries.find((e): e is string => typeof e === 'string')
|
|
169
|
+
: undefined;
|
|
170
|
+
if (first) return path.dirname(path.resolve(root, first));
|
|
171
|
+
} catch {
|
|
172
|
+
// fall through to the default
|
|
173
|
+
}
|
|
174
|
+
return path.join(root, 'server');
|
|
153
175
|
}
|
|
154
176
|
|
|
155
|
-
/**
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
177
|
+
/**
|
|
178
|
+
* The build-owned dir that holds the GENERATED, server-importable `<name>.slots.ts` modules
|
|
179
|
+
* (the `Slot` enum + `HASH` the guest `render` imports). It sits inside the server source tree so
|
|
180
|
+
* toilscript compiles it, named `_ssr` to match the generated `_emails.ts` convention, and is
|
|
181
|
+
* gitignored + regenerated every build — never hand-edited. The server `render` imports
|
|
182
|
+
* `./_ssr/<name>.slots`.
|
|
183
|
+
*/
|
|
184
|
+
export function serverSlotsDir(root: string): string {
|
|
185
|
+
return path.join(serverDir(root), '_ssr');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* (Re)write the generated `<server>/_ssr/<name>.slots.ts` module(s) the server `render` imports.
|
|
190
|
+
* Returns the map of `name -> module source` actually on disk afterwards, so the caller can detect
|
|
191
|
+
* whether a later authoritative extraction changed the `HASH` and a server recompile is needed.
|
|
192
|
+
* Only rewrites a file whose content actually changed (keeps mtimes stable for the dev watcher).
|
|
193
|
+
*/
|
|
194
|
+
export function writeServerSlotsModules(
|
|
195
|
+
root: string,
|
|
196
|
+
modules: { name: string; slotsModule: string }[],
|
|
197
|
+
): Map<string, string> {
|
|
198
|
+
const dir = serverSlotsDir(root);
|
|
199
|
+
const written = new Map<string, string>();
|
|
200
|
+
if (modules.length === 0) return written;
|
|
201
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
202
|
+
for (const m of modules) {
|
|
203
|
+
const file = path.join(dir, `${m.name}.slots.ts`);
|
|
204
|
+
let prev: string | null = null;
|
|
205
|
+
try {
|
|
206
|
+
prev = fs.readFileSync(file, 'utf8');
|
|
207
|
+
} catch {
|
|
208
|
+
// first build for this route: no prior module
|
|
209
|
+
}
|
|
210
|
+
if (prev !== m.slotsModule) fs.writeFileSync(file, m.slotsModule);
|
|
211
|
+
written.set(m.name, m.slotsModule);
|
|
160
212
|
}
|
|
161
|
-
return
|
|
213
|
+
return written;
|
|
162
214
|
}
|
|
163
215
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
216
|
+
/** A rendered SSR route plus the artifacts the build emits for it. */
|
|
217
|
+
interface RenderedRoute {
|
|
218
|
+
pattern: string;
|
|
219
|
+
art: TemplateArtifacts;
|
|
168
220
|
}
|
|
169
221
|
|
|
170
222
|
/**
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
223
|
+
* Spin up a short-lived Vite SSR server, render every `export const ssr = true` route under its
|
|
224
|
+
* layout chain (with sample loader data, in sentinel mode) against `shell`, and return the
|
|
225
|
+
* per-route artifacts. Shared by the pre-pass (`extractServerSlots`, slots only) and the final
|
|
226
|
+
* `extractTemplates` (full artifacts). A route (or layout) that throws under static markup is
|
|
227
|
+
* skipped with a warning and omitted from the result, so it falls back to client rendering.
|
|
175
228
|
*/
|
|
176
|
-
|
|
177
|
-
cfg: ResolvedToilConfig,
|
|
178
|
-
hostName = 'edge',
|
|
179
|
-
): Promise<string[]> {
|
|
229
|
+
async function renderSsrRoutes(cfg: ResolvedToilConfig, shell: string): Promise<RenderedRoute[]> {
|
|
180
230
|
const routes = scanRoutes(cfg.routesAbsDir).filter((r) => r.slot === undefined && !r.intercept);
|
|
181
231
|
if (routes.length === 0) return [];
|
|
182
232
|
|
|
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
233
|
const warn = (msg: string): void => {
|
|
190
234
|
process.stderr.write(` toil: SSR ${msg}\n`);
|
|
191
235
|
};
|
|
@@ -223,11 +267,7 @@ export async function extractTemplates(
|
|
|
223
267
|
renderToStaticMarkup: typeof renderToStaticMarkup;
|
|
224
268
|
};
|
|
225
269
|
|
|
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
|
-
|
|
270
|
+
const rendered: RenderedRoute[] = [];
|
|
231
271
|
try {
|
|
232
272
|
for (const r of routes) {
|
|
233
273
|
let mod: RouteModule;
|
|
@@ -270,18 +310,7 @@ export async function extractTemplates(
|
|
|
270
310
|
createElement: react.createElement,
|
|
271
311
|
renderToStaticMarkup: reactDomServer.renderToStaticMarkup,
|
|
272
312
|
});
|
|
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);
|
|
313
|
+
rendered.push({ pattern: r.pattern, art });
|
|
285
314
|
} catch (err) {
|
|
286
315
|
warn(
|
|
287
316
|
`skipped ${r.pattern} (render failed: ${
|
|
@@ -293,6 +322,178 @@ export async function extractTemplates(
|
|
|
293
322
|
} finally {
|
|
294
323
|
await server.close();
|
|
295
324
|
}
|
|
325
|
+
return rendered;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Resolve the HTML shell to splice routes into. The authoritative shell is the BUILT (post-Vite)
|
|
330
|
+
* `index.html`, whose hashed `<script>`/`<link>` tags are part of the template and therefore the
|
|
331
|
+
* coherence `HASH`. `preferBuilt` (the final extraction) demands it; the slots PRE-PASS (which runs
|
|
332
|
+
* before Vite) falls back to the previous build's shell when present (so an unchanged rebuild's
|
|
333
|
+
* pre-pass `HASH` already matches the final one and no server recompile is needed), and finally to
|
|
334
|
+
* the un-built `.toil/index.html` template on a first clean build. Returns `null` when no shell
|
|
335
|
+
* exists at all (no client build yet and no template), so the caller no-ops.
|
|
336
|
+
*/
|
|
337
|
+
function resolveShell(cfg: ResolvedToilConfig, preferBuilt: boolean): string | null {
|
|
338
|
+
const outDir = path.resolve(cfg.root, cfg.outDir);
|
|
339
|
+
const builtIndex = path.join(outDir, 'index.html');
|
|
340
|
+
const stashed = path.join(cfg.toilDir, 'shell.html');
|
|
341
|
+
const templateIndex = path.join(cfg.toilDir, 'index.html');
|
|
342
|
+
const order = preferBuilt
|
|
343
|
+
? [stashed, builtIndex]
|
|
344
|
+
: // Pre-pass (before Vite): use a prior build's shell if it exists so the HASH is stable
|
|
345
|
+
// across rebuilds; otherwise the generated (un-built) template, which still yields the
|
|
346
|
+
// correct Slot ids (the final pass reconciles the HASH).
|
|
347
|
+
[builtIndex, stashed, templateIndex];
|
|
348
|
+
for (const p of order) {
|
|
349
|
+
if (fs.existsSync(p)) return fs.readFileSync(p, 'utf8');
|
|
350
|
+
}
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* SLOTS PRE-PASS (runs BEFORE the server build): render every `ssr = true` route to its `Slot`
|
|
356
|
+
* enum + `HASH` and write the server-importable `<server>/_ssr/<name>.slots.ts` module(s), so the
|
|
357
|
+
* server `render` can import them when toilscript compiles it. On a clean build no `.slots.ts`
|
|
358
|
+
* exists yet, so this is what bootstraps it; on a rebuild it refreshes them. The `Slot` ids are
|
|
359
|
+
* always correct (they depend only on the route's hole structure); the `HASH` is final only when a
|
|
360
|
+
* prior build's shell is reused, so the final `extractTemplates` reconciles it (and the caller
|
|
361
|
+
* recompiles the server if it rotated). Returns the modules written keyed by route name.
|
|
362
|
+
*/
|
|
363
|
+
export async function extractServerSlots(cfg: ResolvedToilConfig): Promise<Map<string, string>> {
|
|
364
|
+
const shell = resolveShell(cfg, false);
|
|
365
|
+
if (shell === null) return new Map();
|
|
366
|
+
const rendered = await renderSsrRoutes(cfg, shell);
|
|
367
|
+
return writeServerSlotsModules(
|
|
368
|
+
cfg.root,
|
|
369
|
+
rendered.map((r) => ({ name: r.art.name, slotsModule: r.art.slotsModule })),
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** One SSR route's in-memory dev template: the spliceable `.tmpl` plus its
|
|
374
|
+
* top-level slot insertion points (numeric id -> byte offset), parsed from the
|
|
375
|
+
* `.slots` manifest. Used by the dev server to serve SSR without a prod build. */
|
|
376
|
+
export interface DevSsrTemplate {
|
|
377
|
+
pattern: string;
|
|
378
|
+
name: string;
|
|
379
|
+
tmpl: Buffer;
|
|
380
|
+
entries: { id: number; offset: number }[];
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Render every `ssr = true` route against the given (live, Vite-transformed) dev
|
|
385
|
+
* `shell` into an in-memory template + slot offsets, for the dev server to splice
|
|
386
|
+
* the guest `render` values into. Unlike {@link extractTemplates} this has NO
|
|
387
|
+
* side effects (it writes nothing and does not touch `server/_ssr` or the host
|
|
388
|
+
* bundle); the dev server builds together with the guest, so there is no hash to
|
|
389
|
+
* reconcile. Returns `[]` when no route opts in.
|
|
390
|
+
*/
|
|
391
|
+
export async function extractDevSsrTemplates(
|
|
392
|
+
cfg: ResolvedToilConfig,
|
|
393
|
+
shell: string,
|
|
394
|
+
): Promise<DevSsrTemplate[]> {
|
|
395
|
+
const rendered = await renderSsrRoutes(cfg, shell);
|
|
396
|
+
return rendered.map(({ pattern, art }) => {
|
|
397
|
+
// Parse the `.slots` manifest: 46-byte header (n_slots at offset 44),
|
|
398
|
+
// then 8-byte entries (u32 offset, u16 id, u8 kind, u8 reserved).
|
|
399
|
+
const bin = art.slotsBin;
|
|
400
|
+
const n = bin.readUInt16LE(44);
|
|
401
|
+
const entries: { id: number; offset: number }[] = [];
|
|
402
|
+
let o = 46;
|
|
403
|
+
for (let i = 0; i < n; i++) {
|
|
404
|
+
entries.push({ offset: bin.readUInt32LE(o), id: bin.readUInt16LE(o + 4) });
|
|
405
|
+
o += 8;
|
|
406
|
+
}
|
|
407
|
+
return { pattern, name: art.name, tmpl: art.tmpl, entries };
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/** A file-safe, identifier-ish name for a route pattern (`/u/:name` -> `u_name`). */
|
|
412
|
+
export function routeTemplateName(pattern: string): string {
|
|
413
|
+
const n = pattern.replace(/[^A-Za-z0-9]+/g, '_').replace(/^_+|_+$/g, '');
|
|
414
|
+
return n.length > 0 ? n : 'index';
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/** Synthesize a sample param set for a pattern's dynamic segments. */
|
|
418
|
+
function sampleParams(pattern: string): Record<string, string> {
|
|
419
|
+
const params: Record<string, string> = {};
|
|
420
|
+
for (const m of pattern.matchAll(/[:*]+([A-Za-z0-9_]+)/g)) {
|
|
421
|
+
params[m[1]] = 'sample';
|
|
422
|
+
}
|
|
423
|
+
return params;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
interface RouteModule {
|
|
427
|
+
default: ComponentType;
|
|
428
|
+
ssr?: boolean;
|
|
429
|
+
loader?: (args: { params: Record<string, string>; searchParams: URLSearchParams }) => unknown;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/** The outcome of the final {@link extractTemplates} pass. */
|
|
433
|
+
export interface ExtractResult {
|
|
434
|
+
/** The route patterns that produced a template. */
|
|
435
|
+
readonly generated: string[];
|
|
436
|
+
/**
|
|
437
|
+
* Whether the AUTHORITATIVE `<server>/_ssr/<name>.slots.ts` module(s) just written differ from
|
|
438
|
+
* the ones the server was already compiled against (`priorServerSlots`). True means the `HASH`
|
|
439
|
+
* (or `Slot` ids) rotated after the server build, so the server must be recompiled to bake the
|
|
440
|
+
* correct `HASH` (otherwise the host rejects the guest's stale hash as a deploy skew).
|
|
441
|
+
*/
|
|
442
|
+
readonly serverSlotsChanged: boolean;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* FINAL pass (runs AFTER the client/Vite build): render every `export const ssr = true` route to
|
|
447
|
+
* `<outDir>/_ssr/<name>.{tmpl,slots,slots.ts}` + a `templates.json` index, copy the `.tmpl`/`.slots`
|
|
448
|
+
* into the edge host bundle at `hosts/<host>/_tmpl/`, AND (re)write the server-importable
|
|
449
|
+
* `<server>/_ssr/<name>.slots.ts` module(s) — now against the real built shell, so the `HASH` is
|
|
450
|
+
* authoritative. Skips (with a warning) any route that throws under static markup.
|
|
451
|
+
*
|
|
452
|
+
* `priorServerSlots` is the module map the slots PRE-PASS wrote (what the server was compiled
|
|
453
|
+
* against); compared against the authoritative modules here to report whether a server recompile is
|
|
454
|
+
* needed (see {@link ExtractResult.serverSlotsChanged}).
|
|
455
|
+
*/
|
|
456
|
+
export async function extractTemplates(
|
|
457
|
+
cfg: ResolvedToilConfig,
|
|
458
|
+
hostName = 'edge',
|
|
459
|
+
priorServerSlots: Map<string, string> = new Map(),
|
|
460
|
+
): Promise<ExtractResult> {
|
|
461
|
+
const shell = resolveShell(cfg, true);
|
|
462
|
+
if (shell === null) return { generated: [], serverSlotsChanged: false };
|
|
463
|
+
|
|
464
|
+
const rendered = await renderSsrRoutes(cfg, shell);
|
|
465
|
+
|
|
466
|
+
const outDir = path.resolve(cfg.root, cfg.outDir);
|
|
467
|
+
const ssrDir = path.join(outDir, '_ssr');
|
|
468
|
+
const hostsTmplDir = path.join(cfg.root, 'hosts', hostName, '_tmpl');
|
|
469
|
+
const generated: string[] = [];
|
|
470
|
+
const index: { route: string; name: string; hash: string }[] = [];
|
|
471
|
+
|
|
472
|
+
for (const { pattern, art } of rendered) {
|
|
473
|
+
writeTemplateArtifacts(ssrDir, art);
|
|
474
|
+
fs.mkdirSync(hostsTmplDir, { recursive: true });
|
|
475
|
+
fs.copyFileSync(
|
|
476
|
+
path.join(ssrDir, `${art.name}.tmpl`),
|
|
477
|
+
path.join(hostsTmplDir, `${art.name}.tmpl`),
|
|
478
|
+
);
|
|
479
|
+
fs.copyFileSync(
|
|
480
|
+
path.join(ssrDir, `${art.name}.slots`),
|
|
481
|
+
path.join(hostsTmplDir, `${art.name}.slots`),
|
|
482
|
+
);
|
|
483
|
+
index.push({ route: pattern, name: art.name, hash: art.hash.toString('hex') });
|
|
484
|
+
generated.push(pattern);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Write the AUTHORITATIVE server-importable slots module(s) (the real built-shell HASH) and
|
|
488
|
+
// detect whether they changed since the pre-pass the server was compiled against.
|
|
489
|
+
const authoritative = writeServerSlotsModules(
|
|
490
|
+
cfg.root,
|
|
491
|
+
rendered.map((r) => ({ name: r.art.name, slotsModule: r.art.slotsModule })),
|
|
492
|
+
);
|
|
493
|
+
let serverSlotsChanged = false;
|
|
494
|
+
for (const [name, mod] of authoritative) {
|
|
495
|
+
if (priorServerSlots.get(name) !== mod) serverSlotsChanged = true;
|
|
496
|
+
}
|
|
296
497
|
|
|
297
498
|
if (generated.length > 0) {
|
|
298
499
|
fs.mkdirSync(ssrDir, { recursive: true });
|
|
@@ -303,5 +504,5 @@ export async function extractTemplates(
|
|
|
303
504
|
}\n`,
|
|
304
505
|
);
|
|
305
506
|
}
|
|
306
|
-
return generated;
|
|
507
|
+
return { generated, serverSlotsChanged };
|
|
307
508
|
}
|