toiljs 0.0.115 → 0.0.117
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/CHANGELOG.md +10 -0
- package/README.md +16 -7
- package/TYPESCRIPT_LAW.md +1946 -2073
- package/build/backend/.tsbuildinfo +1 -1
- package/build/cli/.tsbuildinfo +1 -1
- package/build/cli/index.js +2375 -258
- package/build/client/.tsbuildinfo +1 -1
- package/build/client/dev/devtools.js +1 -1
- package/build/client/dev/error-overlay.js +1 -1
- package/build/client/routing/mount.js +1 -1
- package/build/compiler/.tsbuildinfo +1 -1
- package/build/compiler/auth-emails.js +7 -3
- package/build/compiler/emails.js +1 -1
- package/build/compiler/index.d.ts +1 -0
- package/build/compiler/index.js +25 -4
- package/build/compiler/pages.js +2 -3
- package/build/compiler/prerender.d.ts +3 -8
- package/build/compiler/prerender.js +88 -127
- package/build/compiler/ssg.js +3 -4
- package/build/compiler/template-build.js +3 -4
- package/build/compiler/toil-docs.generated.js +6 -5
- package/build/compiler/vite.js +3 -6
- package/build/devserver/.tsbuildinfo +1 -1
- package/build/devserver/db/database.js +7 -3
- package/build/devserver/db/types.js +1 -1
- package/build/devserver/production.js +4 -3
- package/build/devserver/runtime/crypto.d.ts +1 -1
- package/build/io/.tsbuildinfo +1 -1
- package/build/logger/.tsbuildinfo +1 -1
- package/build/shared/.tsbuildinfo +1 -1
- package/examples/basic/client/routes/analytics.tsx +7 -5
- package/examples/basic/server/tsconfig.json +1 -3
- package/package.json +45 -44
- package/presets/no-uint8array-tostring.js +120 -153
- package/presets/oxlint.d.ts +5 -0
- package/presets/oxlint.js +224 -0
- package/server/runtime/tsconfig.json +1 -3
- package/tsconfig.json +2 -11
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -38
- package/.github/ISSUE_TEMPLATE/bug_report.yml +0 -90
- package/.github/ISSUE_TEMPLATE/config.yml +0 -8
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -20
- package/.github/PULL_REQUEST_TEMPLATE.md +0 -43
- package/.github/changelog-config.json +0 -45
- package/.github/dependabot.yml +0 -27
- package/.github/workflows/ci.yml +0 -222
- package/.vscode/settings.json +0 -10
- package/docs/README.md +0 -107
- package/docs/auth/README.md +0 -104
- package/docs/auth/configuration.md +0 -105
- package/docs/auth/emails.md +0 -92
- package/docs/auth/extending.md +0 -202
- package/docs/auth/how-it-works.md +0 -138
- package/docs/auth/usage.md +0 -188
- package/docs/backend/README.md +0 -143
- package/docs/backend/data.md +0 -351
- package/docs/backend/rest.md +0 -402
- package/docs/backend/rpc.md +0 -226
- package/docs/background/README.md +0 -114
- package/docs/background/daemons.md +0 -276
- package/docs/background/derive.md +0 -179
- package/docs/cli/README.md +0 -379
- package/docs/concepts/ai-guide.md +0 -132
- package/docs/concepts/config.md +0 -416
- package/docs/concepts/decorators.md +0 -127
- package/docs/concepts/security.md +0 -108
- package/docs/concepts/tiers.md +0 -166
- package/docs/concepts/types.md +0 -216
- package/docs/database/README.md +0 -143
- package/docs/database/capacity.md +0 -350
- package/docs/database/counters.md +0 -174
- package/docs/database/documents.md +0 -277
- package/docs/database/events.md +0 -307
- package/docs/database/membership.md +0 -246
- package/docs/database/setup.md +0 -155
- package/docs/database/unique.md +0 -216
- package/docs/database/views.md +0 -246
- package/docs/frontend/README.md +0 -104
- package/docs/frontend/components.md +0 -286
- package/docs/frontend/data-fetching.md +0 -260
- package/docs/frontend/images.md +0 -148
- package/docs/frontend/metadata.md +0 -344
- package/docs/frontend/navigation.md +0 -296
- package/docs/frontend/rendering.md +0 -236
- package/docs/frontend/routing.md +0 -346
- package/docs/frontend/scripts.md +0 -118
- package/docs/frontend/search.md +0 -191
- package/docs/frontend/styling.md +0 -147
- package/docs/frontend/toil-global.md +0 -216
- package/docs/getting-started/README.md +0 -81
- package/docs/getting-started/create-project.md +0 -131
- package/docs/getting-started/deploy.md +0 -101
- package/docs/getting-started/first-app.md +0 -215
- package/docs/getting-started/installation.md +0 -124
- package/docs/getting-started/migrating.md +0 -125
- package/docs/getting-started/project-structure.md +0 -163
- package/docs/introduction/README.md +0 -66
- package/docs/introduction/design-principles.md +0 -81
- package/docs/introduction/distributed.md +0 -91
- package/docs/introduction/how-it-works.md +0 -121
- package/docs/introduction/hyperscale.md +0 -70
- package/docs/introduction/modern-stack.md +0 -85
- package/docs/introduction/vs-other-frameworks.md +0 -64
- package/docs/introduction/why-toil.md +0 -99
- package/docs/llms.txt +0 -95
- package/docs/realtime/README.md +0 -102
- package/docs/realtime/channels.md +0 -211
- package/docs/realtime/streams.md +0 -369
- package/docs/services/README.md +0 -60
- package/docs/services/analytics.md +0 -279
- package/docs/services/caching.md +0 -175
- package/docs/services/cookies.md +0 -235
- package/docs/services/crypto.md +0 -209
- package/docs/services/email.md +0 -289
- package/docs/services/environment.md +0 -141
- package/docs/services/ratelimit.md +0 -174
- package/docs/services/time.md +0 -85
- package/eslint.config.js +0 -57
- package/examples/basic/client/routes/docs/[...slug].tsx +0 -12
- package/presets/eslint.js +0 -88
- package/src/backend/index.ts +0 -204
- package/src/cli/configure.ts +0 -363
- package/src/cli/create.ts +0 -924
- package/src/cli/db.ts +0 -158
- package/src/cli/diagnostics.ts +0 -665
- package/src/cli/doctor.ts +0 -982
- package/src/cli/features.ts +0 -170
- package/src/cli/index.ts +0 -326
- package/src/cli/notify.ts +0 -120
- package/src/cli/proc.ts +0 -56
- package/src/cli/ui.ts +0 -169
- package/src/cli/update.ts +0 -248
- package/src/cli/updates.ts +0 -92
- package/src/cli/validate.ts +0 -31
- package/src/cli/version-check.ts +0 -125
- package/src/client/auth.ts +0 -869
- package/src/client/channel/channel.ts +0 -146
- package/src/client/components/Form.tsx +0 -65
- package/src/client/components/Image.tsx +0 -160
- package/src/client/components/Script.tsx +0 -113
- package/src/client/components/Slot.tsx +0 -21
- package/src/client/dev/devtools.tsx +0 -1089
- package/src/client/dev/error-overlay.tsx +0 -225
- package/src/client/errors.ts +0 -11
- package/src/client/head/head.ts +0 -182
- package/src/client/head/metadata.ts +0 -110
- package/src/client/index.ts +0 -130
- package/src/client/navigation/Link.tsx +0 -108
- package/src/client/navigation/NavLink.tsx +0 -86
- package/src/client/navigation/navigation.ts +0 -235
- package/src/client/navigation/prefetch.ts +0 -169
- package/src/client/navigation/scroll.ts +0 -53
- package/src/client/routing/Router.tsx +0 -216
- package/src/client/routing/action.ts +0 -122
- package/src/client/routing/error-boundary.tsx +0 -43
- package/src/client/routing/hooks.ts +0 -140
- package/src/client/routing/lazy.ts +0 -77
- package/src/client/routing/loader.ts +0 -381
- package/src/client/routing/match.ts +0 -47
- package/src/client/routing/mount.tsx +0 -90
- package/src/client/routing/params-context.ts +0 -10
- package/src/client/routing/slot-context.ts +0 -7
- package/src/client/rpc.ts +0 -97
- package/src/client/search/search.ts +0 -189
- package/src/client/search/use-page-search.ts +0 -73
- package/src/client/ssr/markers.tsx +0 -172
- package/src/client/stream/client.ts +0 -237
- package/src/client/types.ts +0 -73
- package/src/compiler/auth-emails.ts +0 -312
- package/src/compiler/config.ts +0 -369
- package/src/compiler/docs.ts +0 -127
- package/src/compiler/email-preview.ts +0 -314
- package/src/compiler/emails.ts +0 -404
- package/src/compiler/fonts.ts +0 -89
- package/src/compiler/generate.ts +0 -471
- package/src/compiler/image-blur.ts +0 -42
- package/src/compiler/image-report.ts +0 -88
- package/src/compiler/index.ts +0 -1362
- package/src/compiler/pages.ts +0 -49
- package/src/compiler/plugin.ts +0 -391
- package/src/compiler/prerender.ts +0 -263
- package/src/compiler/routes.ts +0 -170
- package/src/compiler/seo.ts +0 -415
- package/src/compiler/sri.ts +0 -144
- package/src/compiler/ssg.ts +0 -177
- package/src/compiler/ssr-codegen.ts +0 -85
- package/src/compiler/template-build.ts +0 -676
- package/src/compiler/template.ts +0 -262
- package/src/compiler/toil-docs.generated.ts +0 -77
- package/src/compiler/vite.ts +0 -245
- package/src/devserver/analytics/index.ts +0 -317
- package/src/devserver/config/dotenv.ts +0 -102
- package/src/devserver/config/env.ts +0 -30
- package/src/devserver/config/ratelimit.ts +0 -123
- package/src/devserver/daemon/catalog.ts +0 -120
- package/src/devserver/daemon/cron.ts +0 -87
- package/src/devserver/daemon/host.ts +0 -134
- package/src/devserver/daemon/index.ts +0 -369
- package/src/devserver/daemon/runtime.ts +0 -48
- package/src/devserver/db/catalog.ts +0 -109
- package/src/devserver/db/database.ts +0 -1834
- package/src/devserver/db/derives.ts +0 -121
- package/src/devserver/db/index.ts +0 -20
- package/src/devserver/db/routeKinds.ts +0 -191
- package/src/devserver/db/types.ts +0 -168
- package/src/devserver/email/caps.ts +0 -0
- package/src/devserver/email/config.ts +0 -141
- package/src/devserver/email/console.ts +0 -348
- package/src/devserver/email/index.ts +0 -155
- package/src/devserver/email/providers.ts +0 -131
- package/src/devserver/email/status.ts +0 -23
- package/src/devserver/email/validate.ts +0 -37
- package/src/devserver/email/wire.ts +0 -55
- package/src/devserver/http/cache.ts +0 -0
- package/src/devserver/http/envelope.ts +0 -157
- package/src/devserver/http/proxy.ts +0 -172
- package/src/devserver/http/runtime.ts +0 -387
- package/src/devserver/index.ts +0 -43
- package/src/devserver/production-ipc.ts +0 -63
- package/src/devserver/production-worker.ts +0 -83
- package/src/devserver/production.ts +0 -718
- package/src/devserver/runtime/crypto.ts +0 -587
- package/src/devserver/runtime/host.ts +0 -427
- package/src/devserver/runtime/module.ts +0 -442
- package/src/devserver/server.ts +0 -251
- package/src/devserver/ssr.ts +0 -208
- package/src/devserver/stream/catalog.ts +0 -106
- package/src/devserver/stream/host.ts +0 -42
- package/src/devserver/stream/index.ts +0 -301
- package/src/devserver/stream/manager.ts +0 -167
- package/src/devserver/stream/router.ts +0 -101
- package/src/devserver/stream/wire.ts +0 -58
- package/src/devserver/stream/ws.ts +0 -78
- package/src/devserver/wasm/sections.ts +0 -59
- package/src/devserver/wasm/surface.ts +0 -115
- package/src/io/FastMap.ts +0 -151
- package/src/io/FastSet.ts +0 -113
- package/src/io/codec.ts +0 -362
- package/src/io/index.ts +0 -10
- package/src/io/lengths.ts +0 -14
- package/src/io/types.ts +0 -19
- package/src/logger/index.ts +0 -22
- package/src/shared/index.ts +0 -46
- package/test/analytics-dev.test.ts +0 -113
- package/test/assembly/aspect-shim.ts +0 -33
- package/test/assembly/cookie.spec.ts +0 -303
- package/test/assembly/example.spec.ts +0 -22
- package/test/assembly/ssr.spec.ts +0 -95
- package/test/assembly.test.ts +0 -59
- package/test/auth-emails.test.ts +0 -118
- package/test/built-ssr.test.ts +0 -148
- package/test/channel.test.ts +0 -21
- package/test/config-host-port.test.ts +0 -59
- package/test/configure.test.ts +0 -94
- package/test/daemon-build.test.ts +0 -278
- package/test/daemon-catalog.test.ts +0 -349
- package/test/daemon-emulation.test.ts +0 -256
- package/test/db.test.ts +0 -0
- package/test/devserver-database.test.ts +0 -1102
- package/test/devserver-email.test.ts +0 -306
- package/test/devserver-pqauth.test.ts +0 -93
- package/test/devserver-secrets.test.ts +0 -63
- package/test/devserver.test.ts +0 -259
- package/test/doctor.test.ts +0 -298
- package/test/dom/Image.test.tsx +0 -144
- package/test/dom/Link.test.tsx +0 -67
- package/test/dom/NavLink.test.tsx +0 -37
- package/test/dom/Script.test.tsx +0 -48
- package/test/dom/action.test.tsx +0 -146
- package/test/dom/error-overlay.test.tsx +0 -44
- package/test/dom/loader.test.tsx +0 -121
- package/test/dom/navigation.test.ts +0 -59
- package/test/dom/revalidate.test.tsx +0 -38
- package/test/dom/route-head.test.tsx +0 -56
- package/test/dom/router-loading.test.tsx +0 -44
- package/test/dom/scroll.test.ts +0 -56
- package/test/dom/slot.test.tsx +0 -131
- package/test/dom/use-metadata.test.tsx +0 -58
- package/test/dom/view-transitions.test.tsx +0 -53
- package/test/dynamic-routing.test.ts +0 -132
- package/test/email-console.test.ts +0 -86
- package/test/email-preview.test.ts +0 -73
- package/test/emails.test.ts +0 -58
- package/test/errors.test.ts +0 -21
- package/test/example-guestbook.test.ts +0 -123
- package/test/features.test.ts +0 -149
- package/test/fixtures/bignum-wire/spec.ts +0 -25
- package/test/fixtures/daemon-app.ts +0 -65
- package/test/fixtures/stream-echo.ts +0 -26
- package/test/fixtures/stream-gate.ts +0 -28
- package/test/fixtures/stream-trap.ts +0 -18
- package/test/fixtures/stream-typed.ts +0 -41
- package/test/fonts.test.ts +0 -28
- package/test/global-setup.ts +0 -17
- package/test/head.test.ts +0 -42
- package/test/image-blur.test.ts +0 -50
- package/test/io.test.ts +0 -117
- package/test/metadata.test.ts +0 -40
- package/test/navlink.test.ts +0 -28
- package/test/pages.test.ts +0 -105
- package/test/placeholder.test.ts +0 -9
- package/test/pqauth-e2e.test.ts +0 -216
- package/test/pqauth-email.test.ts +0 -801
- package/test/prerender.test.ts +0 -116
- package/test/prettier-plugin.test.ts +0 -46
- package/test/routes.test.ts +0 -76
- package/test/rpc-bignum-wire.test.ts +0 -164
- package/test/rpc-dispatch.test.ts +0 -132
- package/test/rpc-kinds.test.ts +0 -18
- package/test/rpc.test.ts +0 -66
- package/test/search.test.ts +0 -114
- package/test/seo.test.ts +0 -175
- package/test/slot-layouts.test.ts +0 -69
- package/test/sri.test.ts +0 -129
- package/test/ssg.test.ts +0 -36
- package/test/ssr-hydration.test.tsx +0 -122
- package/test/ssr-render.test.ts +0 -197
- package/test/ssr-template.test.tsx +0 -482
- package/test/stream-emulation.test.ts +0 -479
- package/test/ui.test.ts +0 -40
- package/test/update.test.ts +0 -58
- package/test/validate.test.ts +0 -42
- package/test/version-check.test.ts +0 -130
- package/tests/data-parity/generated-parity.ts +0 -99
- package/tests/data-parity/parity.ts +0 -80
- package/tests/data-parity/spec.ts +0 -46
|
@@ -1,676 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Build-time orchestration for edge SSR: render each opted-in route to a
|
|
3
|
-
* template-with-holes and emit the artifacts the edge + guest consume.
|
|
4
|
-
*
|
|
5
|
-
* The deterministic core (`extractRouteTemplate`, `injectIntoShell`,
|
|
6
|
-
* `writeTemplateArtifacts`) is unit-tested with controlled components; the
|
|
7
|
-
* `extractTemplates` driver loads real route + layout modules through a short-
|
|
8
|
-
* lived Vite SSR server (the same pattern as `ssg.ts`).
|
|
9
|
-
*
|
|
10
|
-
* A route opts in with `export const ssr = true`. Its page + layout chain are
|
|
11
|
-
* rendered under the loader-data provider with sample data, in sentinel mode;
|
|
12
|
-
* the result is spliced into the built shell's `#root`, stripped to a `.tmpl`,
|
|
13
|
-
* and written alongside the binary `.slots` manifest and the generated AS
|
|
14
|
-
* `Slot` enum + `HASH`. A route (or layout) that throws under static markup
|
|
15
|
-
* (e.g. it uses router hooks outside the supported subset) is skipped with a
|
|
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.
|
|
26
|
-
*/
|
|
27
|
-
|
|
28
|
-
import fs from 'node:fs';
|
|
29
|
-
import { createRequire } from 'node:module';
|
|
30
|
-
import path from 'node:path';
|
|
31
|
-
|
|
32
|
-
import { type ComponentType, type Context, createElement, type ReactNode, Suspense } from 'react';
|
|
33
|
-
import { renderToString } from 'react-dom/server';
|
|
34
|
-
import { createServer } from 'vite';
|
|
35
|
-
|
|
36
|
-
import { type ResolvedToilConfig } from './config.js';
|
|
37
|
-
import { findLayout, findSpecialChain } from './generate.js';
|
|
38
|
-
import { scanRoutes } from './routes.js';
|
|
39
|
-
import { generateSlotsModule } from './ssr-codegen.js';
|
|
40
|
-
import {
|
|
41
|
-
assignSlotIds,
|
|
42
|
-
coherenceHash,
|
|
43
|
-
encodeSlots,
|
|
44
|
-
type Extracted,
|
|
45
|
-
extractFromHtml,
|
|
46
|
-
} from './template.js';
|
|
47
|
-
import { createViteConfig } from './vite.js';
|
|
48
|
-
import { extractStaticMetadata, loadTypeScript } from './prerender.js';
|
|
49
|
-
import { escapeAttr, escapeHtml, injectSeoHtml, routeSeo, type SeoConfig } from './seo.js';
|
|
50
|
-
|
|
51
|
-
/** Marker element the client `mount` looks for to switch to `hydrateRoot`. */
|
|
52
|
-
const SSR_MARKER = '<template id="__toil_ssr"></template>';
|
|
53
|
-
const ROOT_DIV = '<div id="root"></div>';
|
|
54
|
-
|
|
55
|
-
export interface RouteRenderInput {
|
|
56
|
-
/** File-safe route name (the `<name>.tmpl` stem). */
|
|
57
|
-
name: string;
|
|
58
|
-
Page: ComponentType;
|
|
59
|
-
/** Layout chain, OUTERMOST first (root layout, then nested). */
|
|
60
|
-
layouts: ComponentType<{ children?: ReactNode }>[];
|
|
61
|
-
/** Sample loader data provided via the loader context during the render. */
|
|
62
|
-
loaderData: unknown;
|
|
63
|
-
/** The client's `LoaderDataContext` (loaded via the SSR module graph so it
|
|
64
|
-
* is the same instance the page's `useLoaderData` reads). */
|
|
65
|
-
loaderContext: Context<unknown> | null;
|
|
66
|
-
/** The markers' build switch (same module instance the page imports). */
|
|
67
|
-
setSsrBuild: (on: boolean) => void;
|
|
68
|
-
/** The built HTML shell (with hashed script tags) to splice into. */
|
|
69
|
-
shell: string;
|
|
70
|
-
/** React's `createElement` from the SAME instance the page imports (the Vite
|
|
71
|
-
* SSR graph), so element creation and the components' hooks share one React.
|
|
72
|
-
* Defaults to the compiler's own React when omitted (unit tests). Mixing two
|
|
73
|
-
* React copies leaves the hook dispatcher null ("Cannot read properties of
|
|
74
|
-
* null (reading 'useRef')"). */
|
|
75
|
-
createElement?: typeof createElement;
|
|
76
|
-
/** `renderToString` paired with {@link createElement}'s React. We use it (NOT
|
|
77
|
-
* `renderToStaticMarkup`) because hydration needs the `<!-- -->` text-node
|
|
78
|
-
* boundary markers it emits, so `hydrateRoot` can align "text + hole" runs. */
|
|
79
|
-
renderToString?: typeof renderToString;
|
|
80
|
-
/** React's `Suspense` from the SAME instance as {@link createElement}, so the
|
|
81
|
-
* Suspense dehydration markers (`<!--$-->`) emitted match the client's. */
|
|
82
|
-
Suspense?: typeof Suspense;
|
|
83
|
-
/** Site SEO config. When set, this route's resolved SEO (site defaults overlaid with the
|
|
84
|
-
* route's static `metadata`) is baked into the template `<head>`, so an SSR route serves the
|
|
85
|
-
* same per-page title/description/og (incl og:image) a crawler gets from `<route>/index.html`. */
|
|
86
|
-
seo?: SeoConfig | null;
|
|
87
|
-
/** The route's static `export const metadata` (extracted at build), overlaid on the site SEO. */
|
|
88
|
-
metadata?: Record<string, unknown> | null;
|
|
89
|
-
/** The route pattern, used for the canonical / `og:url`. */
|
|
90
|
-
pattern?: string;
|
|
91
|
-
/** Drains the component-level head (`useHead`/`useTitle`/`<Head>`) collected during this route's
|
|
92
|
-
* render, so it's baked into the SSR `<head>` too. From `toiljs/client`'s `__drainSsrHead`. */
|
|
93
|
-
drainSsrHead?: () => SsrHead;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export interface TemplateArtifacts {
|
|
97
|
-
name: string;
|
|
98
|
-
tmpl: Buffer;
|
|
99
|
-
/** Binary `.slots` manifest for the Rust host. */
|
|
100
|
-
slotsBin: Buffer;
|
|
101
|
-
/** Generated AssemblyScript `Slot` enum + `HASH` module source. */
|
|
102
|
-
slotsModule: string;
|
|
103
|
-
hash: Buffer;
|
|
104
|
-
slotCount: number;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/** Build the route element tree, mirroring the client Router so `renderToString`
|
|
108
|
-
* emits the SAME Suspense dehydration markers (`<!--$-->`) `hydrateRoot` expects.
|
|
109
|
-
* The page sits under the loader-data provider inside a route `Suspense`, and EACH
|
|
110
|
-
* layout (outermost first) gets its own `Suspense`, exactly as `renderMatched` +
|
|
111
|
-
* `Router` wrap them. Without these markers the client's Suspense boundaries have
|
|
112
|
-
* nothing to align to and hydration regenerates the whole tree. (The ErrorBoundary
|
|
113
|
-
* / context wrappers the client adds emit no DOM and no markers, so they are
|
|
114
|
-
* omitted here.) The `Suspense` component must come from the SAME React as `h`. */
|
|
115
|
-
export function assembleRouteElement(
|
|
116
|
-
Page: ComponentType,
|
|
117
|
-
layouts: ComponentType<{ children?: ReactNode }>[],
|
|
118
|
-
loaderData: unknown,
|
|
119
|
-
loaderContext: Context<unknown> | null,
|
|
120
|
-
h: typeof createElement = createElement,
|
|
121
|
-
SuspenseComp: typeof Suspense = Suspense,
|
|
122
|
-
): ReactNode {
|
|
123
|
-
let node: ReactNode = h(Page);
|
|
124
|
-
if (loaderContext) {
|
|
125
|
-
node = h(loaderContext.Provider, { value: loaderData }, node);
|
|
126
|
-
}
|
|
127
|
-
node = h(SuspenseComp, { fallback: null }, node); // route Suspense (mirrors RoutePage's boundary)
|
|
128
|
-
for (let i = layouts.length - 1; i >= 0; i--) {
|
|
129
|
-
node = h(SuspenseComp, { fallback: null }, h(layouts[i], null, node));
|
|
130
|
-
}
|
|
131
|
-
return node;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/** Splice the rendered route HTML into the shell's `#root` and add the SSR
|
|
135
|
-
* marker so the client hydrates rather than client-renders. */
|
|
136
|
-
export function injectIntoShell(shell: string, routeHtml: string): string {
|
|
137
|
-
if (!shell.includes(ROOT_DIV)) {
|
|
138
|
-
throw new Error('toil ssr: built shell has no empty <div id="root"></div> to splice into');
|
|
139
|
-
}
|
|
140
|
-
return shell.replace(ROOT_DIV, `<div id="root">${routeHtml}</div>${SSR_MARKER}`);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* React 19 auto-emits hoistable resource tags into `<head>` on the client (it
|
|
145
|
-
* adds a `<link rel="preload">` for an `<img>`, and hoists `<title>` / `<meta>`),
|
|
146
|
-
* but the string renderer emits them INLINE in the route fragment. Left in the
|
|
147
|
-
* spliced `#root` template they would not appear in the client's hydrated `#root`
|
|
148
|
-
* (the client puts them in `<head>`), so `hydrateRoot` reports a mismatch. Strip
|
|
149
|
-
* them from the route fragment; the shell already carries the document head, and
|
|
150
|
-
* the client re-adds its own resource hints. Only the fragment is stripped. */
|
|
151
|
-
function stripHoistedResourceTags(html: string): string {
|
|
152
|
-
return html
|
|
153
|
-
.replace(/<link\b[^>]*>/gi, '')
|
|
154
|
-
.replace(/<meta\b[^>]*>/gi, '')
|
|
155
|
-
.replace(/<title\b[^>]*>[\s\S]*?<\/title>/gi, '');
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/** The resolved component-level head drained from the client head manager during an SSR-build render
|
|
159
|
-
* (structurally `ResolvedHead` from `toiljs/client`). */
|
|
160
|
-
interface SsrHead {
|
|
161
|
-
title?: string;
|
|
162
|
-
meta: { name?: string; property?: string; content: string; [attr: string]: string | undefined }[];
|
|
163
|
-
link: { rel: string; href: string; [attr: string]: string | undefined }[];
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/** Render one head `<meta>`/`<link>` to an HTML tag, marked `data-toil-head` so the client head
|
|
167
|
-
* manager owns it on hydration (it removes + re-emits `[data-toil-head]` tags on every navigation,
|
|
168
|
-
* exactly as it does for the tags it adds at runtime — so there's no duplication or stale leak). */
|
|
169
|
-
function headTag(tag: 'meta' | 'link', attrs: Record<string, string | undefined>): string {
|
|
170
|
-
const pairs = Object.entries(attrs)
|
|
171
|
-
.filter((e): e is [string, string] => e[1] !== undefined)
|
|
172
|
-
.map(([k, v]) => `${k}="${escapeAttr(v)}"`);
|
|
173
|
-
return ` <${tag} data-toil-head ${pairs.join(' ')} />`;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Bake the route's component-level head (layout `<Head>` + a page's `useHead`/`useTitle`, collected
|
|
178
|
-
* during render) into the document head: the title when the route's static `metadata` set none (the
|
|
179
|
-
* component title outranks the site default), plus any `<meta>`/`<link>` the route SEO didn't already
|
|
180
|
-
* carry — so the server HTML reflects the SAME head the client computes, not just the static metadata.
|
|
181
|
-
*/
|
|
182
|
-
function injectComponentHead(
|
|
183
|
-
html: string,
|
|
184
|
-
head: SsrHead,
|
|
185
|
-
routeMetadata: Record<string, unknown> | null,
|
|
186
|
-
): string {
|
|
187
|
-
let out = html;
|
|
188
|
-
if (head.title !== undefined && (routeMetadata === null || routeMetadata.title === undefined)) {
|
|
189
|
-
const tag = `<title>${escapeHtml(head.title)}</title>`;
|
|
190
|
-
out = /<title>[\s\S]*?<\/title>/i.test(out)
|
|
191
|
-
? out.replace(/<title>[\s\S]*?<\/title>/i, tag)
|
|
192
|
-
: out.replace(/<\/head>/i, ` ${tag}\n </head>`);
|
|
193
|
-
}
|
|
194
|
-
const tags: string[] = [];
|
|
195
|
-
for (const m of head.meta) {
|
|
196
|
-
const key =
|
|
197
|
-
m.name !== undefined
|
|
198
|
-
? `name="${m.name}"`
|
|
199
|
-
: m.property !== undefined
|
|
200
|
-
? `property="${m.property}"`
|
|
201
|
-
: null;
|
|
202
|
-
if (key === null || out.includes(key)) continue; // already covered by the route SEO
|
|
203
|
-
tags.push(headTag('meta', m));
|
|
204
|
-
}
|
|
205
|
-
for (const l of head.link) {
|
|
206
|
-
if (out.includes(`href="${l.href}"`)) continue;
|
|
207
|
-
tags.push(headTag('link', l));
|
|
208
|
-
}
|
|
209
|
-
if (tags.length > 0) {
|
|
210
|
-
out = out.includes('</head>')
|
|
211
|
-
? out.replace(/<\/head>/i, `${tags.join('\n')}\n </head>`)
|
|
212
|
-
: `${tags.join('\n')}\n${out}`;
|
|
213
|
-
}
|
|
214
|
-
return out;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/** Render one route to its template artifacts (pure given its inputs). */
|
|
218
|
-
export function extractRouteTemplate(input: RouteRenderInput): TemplateArtifacts {
|
|
219
|
-
const h = input.createElement ?? createElement;
|
|
220
|
-
const render = input.renderToString ?? renderToString;
|
|
221
|
-
const SuspenseComp = input.Suspense ?? Suspense;
|
|
222
|
-
const element = assembleRouteElement(
|
|
223
|
-
input.Page,
|
|
224
|
-
input.layouts,
|
|
225
|
-
input.loaderData,
|
|
226
|
-
input.loaderContext,
|
|
227
|
-
h,
|
|
228
|
-
SuspenseComp,
|
|
229
|
-
);
|
|
230
|
-
// Clear any specs leaked from a previously-failed route render before collecting this route's.
|
|
231
|
-
input.drainSsrHead?.();
|
|
232
|
-
input.setSsrBuild(true);
|
|
233
|
-
let routeHtml: string;
|
|
234
|
-
try {
|
|
235
|
-
routeHtml = render(element);
|
|
236
|
-
} finally {
|
|
237
|
-
input.setSsrBuild(false);
|
|
238
|
-
}
|
|
239
|
-
// The component-level head (layout <Head> + page useHead/useTitle) was collected during the render
|
|
240
|
-
// above; drain it now, per route, so it can be baked alongside the static SEO.
|
|
241
|
-
const componentHead: SsrHead = input.drainSsrHead?.() ?? { meta: [], link: [] };
|
|
242
|
-
let full = injectIntoShell(input.shell, stripHoistedResourceTags(routeHtml));
|
|
243
|
-
// Bake the route's resolved SEO into the template <head>, mirroring the static prerender
|
|
244
|
-
// (prerender.ts / ssg.ts) so an `ssr=true` route serves the SAME per-page metadata (title,
|
|
245
|
-
// description, canonical, og:* incl og:image, twitter, jsonLd) a crawler gets from the static
|
|
246
|
-
// <route>/index.html — which the dynamic SSR template otherwise shadows at request time. Runs on
|
|
247
|
-
// the FULL spliced document (injectSeoHtml's <title>/</head> regexes need the shell head, not the
|
|
248
|
-
// stripped fragment) and BEFORE extractFromHtml so the coherence hash covers the baked head.
|
|
249
|
-
if (input.seo) {
|
|
250
|
-
full = injectSeoHtml(full, routeSeo(input.seo, input.metadata ?? null, input.pattern ?? '/'));
|
|
251
|
-
}
|
|
252
|
-
// Then add the component-level head (a layout's <Head>, a page's useHead/useTitle) that the static
|
|
253
|
-
// metadata export + site SEO didn't already cover, so the server HTML carries the same head the
|
|
254
|
-
// client computes.
|
|
255
|
-
full = injectComponentHead(full, componentHead, input.metadata ?? null);
|
|
256
|
-
const extracted: Extracted = extractFromHtml(full);
|
|
257
|
-
const ids = assignSlotIds(extracted.slots);
|
|
258
|
-
const hash = coherenceHash(extracted.tmpl, extracted.slots);
|
|
259
|
-
return {
|
|
260
|
-
name: input.name,
|
|
261
|
-
tmpl: extracted.tmpl,
|
|
262
|
-
slotsBin: encodeSlots(extracted.tmpl.length, hash, extracted.slots, ids),
|
|
263
|
-
slotsModule: generateSlotsModule(input.name, extracted.slots, hash),
|
|
264
|
-
hash,
|
|
265
|
-
slotCount: extracted.slots.length,
|
|
266
|
-
};
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/** Write a route's `.tmpl` / `.slots` / `.slots.ts` into `ssrDir`. */
|
|
270
|
-
export function writeTemplateArtifacts(ssrDir: string, art: TemplateArtifacts): void {
|
|
271
|
-
fs.mkdirSync(ssrDir, { recursive: true });
|
|
272
|
-
fs.writeFileSync(path.join(ssrDir, `${art.name}.tmpl`), art.tmpl);
|
|
273
|
-
fs.writeFileSync(path.join(ssrDir, `${art.name}.slots`), art.slotsBin);
|
|
274
|
-
fs.writeFileSync(path.join(ssrDir, `${art.name}.slots.ts`), art.slotsModule);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
/**
|
|
278
|
-
* The server source dir (where toilscript-compiled modules live): the dir of the first toilconfig
|
|
279
|
-
* entry, else `<root>/server`. Mirrors the same resolution in `emails.ts` (`_emails.ts` lives here).
|
|
280
|
-
*/
|
|
281
|
-
function serverDir(root: string): string {
|
|
282
|
-
try {
|
|
283
|
-
const cfg = JSON.parse(fs.readFileSync(path.join(root, 'toilconfig.json'), 'utf8')) as {
|
|
284
|
-
entries?: unknown;
|
|
285
|
-
};
|
|
286
|
-
const first = Array.isArray(cfg.entries)
|
|
287
|
-
? cfg.entries.find((e): e is string => typeof e === 'string')
|
|
288
|
-
: undefined;
|
|
289
|
-
if (first) return path.dirname(path.resolve(root, first));
|
|
290
|
-
} catch {
|
|
291
|
-
// fall through to the default
|
|
292
|
-
}
|
|
293
|
-
return path.join(root, 'server');
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
/**
|
|
297
|
-
* The build-owned dir that holds the GENERATED, server-importable `<name>.slots.ts` modules
|
|
298
|
-
* (the `Slot` enum + `HASH` the guest `render` imports). It sits inside the server source tree so
|
|
299
|
-
* toilscript compiles it, named `_ssr` to match the generated `_emails.ts` convention, and is
|
|
300
|
-
* gitignored + regenerated every build — never hand-edited. The server `render` imports
|
|
301
|
-
* `./_ssr/<name>.slots`.
|
|
302
|
-
*/
|
|
303
|
-
export function serverSlotsDir(root: string): string {
|
|
304
|
-
return path.join(serverDir(root), '_ssr');
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
/**
|
|
308
|
-
* (Re)write the generated `<server>/_ssr/<name>.slots.ts` module(s) the server `render` imports.
|
|
309
|
-
* Returns the map of `name -> module source` actually on disk afterwards, so the caller can detect
|
|
310
|
-
* whether a later authoritative extraction changed the `HASH` and a server recompile is needed.
|
|
311
|
-
* Only rewrites a file whose content actually changed (keeps mtimes stable for the dev watcher).
|
|
312
|
-
*/
|
|
313
|
-
export function writeServerSlotsModules(
|
|
314
|
-
root: string,
|
|
315
|
-
modules: { name: string; slotsModule: string }[],
|
|
316
|
-
): Map<string, string> {
|
|
317
|
-
const dir = serverSlotsDir(root);
|
|
318
|
-
const written = new Map<string, string>();
|
|
319
|
-
if (modules.length === 0) return written;
|
|
320
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
321
|
-
for (const m of modules) {
|
|
322
|
-
const file = path.join(dir, `${m.name}.slots.ts`);
|
|
323
|
-
let prev: string | null = null;
|
|
324
|
-
try {
|
|
325
|
-
prev = fs.readFileSync(file, 'utf8');
|
|
326
|
-
} catch {
|
|
327
|
-
// first build for this route: no prior module
|
|
328
|
-
}
|
|
329
|
-
if (prev !== m.slotsModule) fs.writeFileSync(file, m.slotsModule);
|
|
330
|
-
written.set(m.name, m.slotsModule);
|
|
331
|
-
}
|
|
332
|
-
return written;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
/** A rendered SSR route plus the artifacts the build emits for it. */
|
|
336
|
-
interface RenderedRoute {
|
|
337
|
-
pattern: string;
|
|
338
|
-
art: TemplateArtifacts;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
/**
|
|
342
|
-
* Spin up a short-lived Vite SSR server, render every `export const ssr = true` route under its
|
|
343
|
-
* layout chain (with sample loader data, in sentinel mode) against `shell`, and return the
|
|
344
|
-
* per-route artifacts. Shared by the pre-pass (`extractServerSlots`, slots only) and the final
|
|
345
|
-
* `extractTemplates` (full artifacts). A route (or layout) that throws under static markup is
|
|
346
|
-
* skipped with a warning and omitted from the result, so it falls back to client rendering.
|
|
347
|
-
*/
|
|
348
|
-
async function renderSsrRoutes(cfg: ResolvedToilConfig, shell: string): Promise<RenderedRoute[]> {
|
|
349
|
-
const routes = scanRoutes(cfg.routesAbsDir).filter((r) => r.slot === undefined && !r.intercept);
|
|
350
|
-
if (routes.length === 0) return [];
|
|
351
|
-
|
|
352
|
-
// Load TypeScript once (same as prerender.ts) to read each route's static `metadata` for the
|
|
353
|
-
// SSR <head>. Only needed when the project configures SEO.
|
|
354
|
-
const ts = cfg.seo ? await loadTypeScript(cfg.root) : null;
|
|
355
|
-
|
|
356
|
-
const warn = (msg: string): void => {
|
|
357
|
-
process.stderr.write(` toil: SSR ${msg}\n`);
|
|
358
|
-
};
|
|
359
|
-
const server = await createServer({
|
|
360
|
-
...(await createViteConfig(cfg)),
|
|
361
|
-
server: { middlewareMode: true, hmr: false },
|
|
362
|
-
appType: 'custom',
|
|
363
|
-
logLevel: 'silent',
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
const client = (await server.ssrLoadModule('toiljs/client')) as unknown as {
|
|
367
|
-
__setSsrBuild: (on: boolean) => void;
|
|
368
|
-
__setSsrLocation: (path: string | null) => void;
|
|
369
|
-
__drainSsrHead: () => SsrHead;
|
|
370
|
-
LoaderDataContext: Context<unknown>;
|
|
371
|
-
};
|
|
372
|
-
|
|
373
|
-
// Install the ambient `Toil` global (+ registered pages / transitions) exactly
|
|
374
|
-
// as the client entry does, by evaluating the generated globals module. Without
|
|
375
|
-
// it, any layout or component using `Toil` (e.g. `<Toil.Head>`, `<Toil.Link>`)
|
|
376
|
-
// throws "Toil is not defined" during extraction, so the route is silently
|
|
377
|
-
// skipped and falls back to client rendering.
|
|
378
|
-
const globalsModule = path.join(cfg.toilDir, 'globals.ts');
|
|
379
|
-
if (fs.existsSync(globalsModule)) {
|
|
380
|
-
await server.ssrLoadModule(globalsModule);
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
// Render with the SAME React the components import. Vite externalizes react
|
|
384
|
-
// (CommonJS) for SSR and resolves it from the app root, so resolve it the same
|
|
385
|
-
// way (from cfg.root) rather than using the compiler's own copy. Two React
|
|
386
|
-
// copies leave the hook dispatcher null, so a layout/component hook
|
|
387
|
-
// (`useLocation` -> `useRef`) throws. (`ssrLoadModule('react')` can't be used:
|
|
388
|
-
// Vite's SSR runner cannot evaluate the CJS module -> "module is not defined".)
|
|
389
|
-
const appRequire = createRequire(path.join(cfg.root, 'package.json'));
|
|
390
|
-
const react = appRequire('react') as {
|
|
391
|
-
createElement: typeof createElement;
|
|
392
|
-
Suspense: typeof Suspense;
|
|
393
|
-
};
|
|
394
|
-
const reactDomServer = appRequire('react-dom/server') as {
|
|
395
|
-
renderToString: typeof renderToString;
|
|
396
|
-
};
|
|
397
|
-
|
|
398
|
-
const rendered: RenderedRoute[] = [];
|
|
399
|
-
try {
|
|
400
|
-
for (const r of routes) {
|
|
401
|
-
let mod: RouteModule;
|
|
402
|
-
try {
|
|
403
|
-
mod = (await server.ssrLoadModule(r.file)) as unknown as RouteModule;
|
|
404
|
-
} catch (err) {
|
|
405
|
-
warn(`skipped ${r.pattern} (${err instanceof Error ? err.message : String(err)})`);
|
|
406
|
-
continue;
|
|
407
|
-
}
|
|
408
|
-
if (mod.ssr !== true) continue;
|
|
409
|
-
|
|
410
|
-
try {
|
|
411
|
-
const params = sampleParams(r.pattern);
|
|
412
|
-
const loaderData =
|
|
413
|
-
typeof mod.loader === 'function'
|
|
414
|
-
? await mod.loader({ params, searchParams: new URLSearchParams() })
|
|
415
|
-
: undefined;
|
|
416
|
-
|
|
417
|
-
const rootLayout = findLayout(cfg);
|
|
418
|
-
const layoutFiles = [
|
|
419
|
-
...(rootLayout ? [rootLayout] : []),
|
|
420
|
-
...findSpecialChain(cfg, r.file, 'layout', false),
|
|
421
|
-
];
|
|
422
|
-
const layouts: ComponentType<{ children?: ReactNode }>[] = [];
|
|
423
|
-
for (const lf of layoutFiles) {
|
|
424
|
-
const lm = (await server.ssrLoadModule(lf)) as unknown as {
|
|
425
|
-
default: ComponentType<{ children?: ReactNode }>;
|
|
426
|
-
};
|
|
427
|
-
layouts.push(lm.default);
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
// The route's static `metadata` export (same static-AST read prerender.ts uses, so
|
|
431
|
-
// the SSR head matches the static <route>/index.html exactly). generateMetadata is
|
|
432
|
-
// dynamic and skipped here, as in prerender/ssg.
|
|
433
|
-
const metadata = ts ? extractStaticMetadata(ts, r.file) : null;
|
|
434
|
-
|
|
435
|
-
const name = routeTemplateName(r.pattern);
|
|
436
|
-
// Tell location hooks which URL this template is for, so a NavLink's active
|
|
437
|
-
// class / aria-current render as they will on the client at this route (else
|
|
438
|
-
// the `/` default mismatches on hydration). Cleared in `finally`.
|
|
439
|
-
client.__setSsrLocation(samplePath(r.pattern));
|
|
440
|
-
const art = extractRouteTemplate({
|
|
441
|
-
name,
|
|
442
|
-
Page: mod.default,
|
|
443
|
-
layouts,
|
|
444
|
-
loaderData,
|
|
445
|
-
loaderContext: client.LoaderDataContext,
|
|
446
|
-
setSsrBuild: client.__setSsrBuild,
|
|
447
|
-
drainSsrHead: client.__drainSsrHead,
|
|
448
|
-
shell,
|
|
449
|
-
createElement: react.createElement,
|
|
450
|
-
renderToString: reactDomServer.renderToString,
|
|
451
|
-
Suspense: react.Suspense,
|
|
452
|
-
seo: cfg.seo,
|
|
453
|
-
metadata,
|
|
454
|
-
pattern: r.pattern,
|
|
455
|
-
});
|
|
456
|
-
// An imported image asset is resolved through the dev SSR server to a dev-only URL
|
|
457
|
-
// (/@imagetools/..., /@fs/..., /src/...) that has no production path, so baking it into
|
|
458
|
-
// a frozen .tmpl would 404 in the built app. Rather than ship a broken template, skip
|
|
459
|
-
// the route (it client-renders, where the asset resolves correctly) and tell the author
|
|
460
|
-
// to use a public/ string path for images on SSR routes.
|
|
461
|
-
const devAsset = /(?:src|href)="\/(?:@imagetools|@fs|@id|src)\//.exec(
|
|
462
|
-
art.tmpl.toString('utf8'),
|
|
463
|
-
);
|
|
464
|
-
if (devAsset) {
|
|
465
|
-
warn(
|
|
466
|
-
`skipped ${r.pattern}: an imported asset resolves to a dev-only URL ` +
|
|
467
|
-
`("${devAsset[0]}…") with no production path — reference images on SSR routes ` +
|
|
468
|
-
`via a public/ string path (e.g. src="/images/x.webp") — falling back to client rendering`,
|
|
469
|
-
);
|
|
470
|
-
continue;
|
|
471
|
-
}
|
|
472
|
-
rendered.push({ pattern: r.pattern, art });
|
|
473
|
-
} catch (err) {
|
|
474
|
-
warn(
|
|
475
|
-
`skipped ${r.pattern} (render failed: ${
|
|
476
|
-
err instanceof Error ? err.message : String(err)
|
|
477
|
-
}) — falls back to client rendering`,
|
|
478
|
-
);
|
|
479
|
-
} finally {
|
|
480
|
-
client.__setSsrLocation(null);
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
} finally {
|
|
484
|
-
await server.close();
|
|
485
|
-
}
|
|
486
|
-
return rendered;
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
/**
|
|
490
|
-
* Resolve the HTML shell to splice routes into. The authoritative shell is the BUILT (post-Vite)
|
|
491
|
-
* `index.html`, whose hashed `<script>`/`<link>` tags are part of the template and therefore the
|
|
492
|
-
* coherence `HASH`. `preferBuilt` (the final extraction) demands it; the slots PRE-PASS (which runs
|
|
493
|
-
* before Vite) falls back to the previous build's shell when present (so an unchanged rebuild's
|
|
494
|
-
* pre-pass `HASH` already matches the final one and no server recompile is needed), and finally to
|
|
495
|
-
* the un-built `.toil/index.html` template on a first clean build. Returns `null` when no shell
|
|
496
|
-
* exists at all (no client build yet and no template), so the caller no-ops.
|
|
497
|
-
*/
|
|
498
|
-
function resolveShell(cfg: ResolvedToilConfig, preferBuilt: boolean): string | null {
|
|
499
|
-
const outDir = path.resolve(cfg.root, cfg.outDir);
|
|
500
|
-
const builtIndex = path.join(outDir, 'index.html');
|
|
501
|
-
const stashed = path.join(cfg.toilDir, 'shell.html');
|
|
502
|
-
const templateIndex = path.join(cfg.toilDir, 'index.html');
|
|
503
|
-
const order = preferBuilt
|
|
504
|
-
? [stashed, builtIndex]
|
|
505
|
-
: // Pre-pass (before Vite): use a prior build's shell if it exists so the HASH is stable
|
|
506
|
-
// across rebuilds; otherwise the generated (un-built) template, which still yields the
|
|
507
|
-
// correct Slot ids (the final pass reconciles the HASH).
|
|
508
|
-
[builtIndex, stashed, templateIndex];
|
|
509
|
-
for (const p of order) {
|
|
510
|
-
if (fs.existsSync(p)) return fs.readFileSync(p, 'utf8');
|
|
511
|
-
}
|
|
512
|
-
return null;
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
/**
|
|
516
|
-
* SLOTS PRE-PASS (runs BEFORE the server build): render every `ssr = true` route to its `Slot`
|
|
517
|
-
* enum + `HASH` and write the server-importable `<server>/_ssr/<name>.slots.ts` module(s), so the
|
|
518
|
-
* server `render` can import them when toilscript compiles it. On a clean build no `.slots.ts`
|
|
519
|
-
* exists yet, so this is what bootstraps it; on a rebuild it refreshes them. The `Slot` ids are
|
|
520
|
-
* always correct (they depend only on the route's hole structure); the `HASH` is final only when a
|
|
521
|
-
* prior build's shell is reused, so the final `extractTemplates` reconciles it (and the caller
|
|
522
|
-
* recompiles the server if it rotated). Returns the modules written keyed by route name.
|
|
523
|
-
*/
|
|
524
|
-
export async function extractServerSlots(cfg: ResolvedToilConfig): Promise<Map<string, string>> {
|
|
525
|
-
const shell = resolveShell(cfg, false);
|
|
526
|
-
if (shell === null) return new Map();
|
|
527
|
-
const rendered = await renderSsrRoutes(cfg, shell);
|
|
528
|
-
return writeServerSlotsModules(
|
|
529
|
-
cfg.root,
|
|
530
|
-
rendered.map((r) => ({ name: r.art.name, slotsModule: r.art.slotsModule })),
|
|
531
|
-
);
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
/** One SSR route's in-memory dev template: the spliceable `.tmpl` plus its
|
|
535
|
-
* top-level slot insertion points (numeric id -> byte offset), parsed from the
|
|
536
|
-
* `.slots` manifest. Used by the dev server to serve SSR without a prod build. */
|
|
537
|
-
export interface DevSsrTemplate {
|
|
538
|
-
pattern: string;
|
|
539
|
-
name: string;
|
|
540
|
-
tmpl: Buffer;
|
|
541
|
-
entries: { id: number; offset: number }[];
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
/**
|
|
545
|
-
* Render every `ssr = true` route against the given (live, Vite-transformed) dev
|
|
546
|
-
* `shell` into an in-memory template + slot offsets, for the dev server to splice
|
|
547
|
-
* the guest `render` values into. Unlike {@link extractTemplates} this has NO
|
|
548
|
-
* side effects (it writes nothing and does not touch `server/_ssr` or the host
|
|
549
|
-
* bundle); the dev server builds together with the guest, so there is no hash to
|
|
550
|
-
* reconcile. Returns `[]` when no route opts in.
|
|
551
|
-
*/
|
|
552
|
-
export async function extractDevSsrTemplates(
|
|
553
|
-
cfg: ResolvedToilConfig,
|
|
554
|
-
shell: string,
|
|
555
|
-
): Promise<DevSsrTemplate[]> {
|
|
556
|
-
const rendered = await renderSsrRoutes(cfg, shell);
|
|
557
|
-
return rendered.map(({ pattern, art }) => {
|
|
558
|
-
// Parse the `.slots` manifest: 46-byte header (n_slots at offset 44),
|
|
559
|
-
// then 8-byte entries (u32 offset, u16 id, u8 kind, u8 reserved).
|
|
560
|
-
const bin = art.slotsBin;
|
|
561
|
-
const n = bin.readUInt16LE(44);
|
|
562
|
-
const entries: { id: number; offset: number }[] = [];
|
|
563
|
-
let o = 46;
|
|
564
|
-
for (let i = 0; i < n; i++) {
|
|
565
|
-
entries.push({ offset: bin.readUInt32LE(o), id: bin.readUInt16LE(o + 4) });
|
|
566
|
-
o += 8;
|
|
567
|
-
}
|
|
568
|
-
return { pattern, name: art.name, tmpl: art.tmpl, entries };
|
|
569
|
-
});
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
/** A file-safe, identifier-ish name for a route pattern (`/u/:name` -> `u_name`). */
|
|
573
|
-
export function routeTemplateName(pattern: string): string {
|
|
574
|
-
const n = pattern.replace(/[^A-Za-z0-9]+/g, '_').replace(/^_+|_+$/g, '');
|
|
575
|
-
return n.length > 0 ? n : 'index';
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
/** Synthesize a sample param set for a pattern's dynamic segments. */
|
|
579
|
-
function sampleParams(pattern: string): Record<string, string> {
|
|
580
|
-
const params: Record<string, string> = {};
|
|
581
|
-
for (const m of pattern.matchAll(/[:*]+([A-Za-z0-9_]+)/g)) {
|
|
582
|
-
params[m[1]] = 'sample';
|
|
583
|
-
}
|
|
584
|
-
return params;
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
/** The concrete pathname for a route pattern, dynamic segments filled with the same `sample`
|
|
588
|
-
* value {@link sampleParams} uses, so location-dependent markup renders consistently. Static
|
|
589
|
-
* routes return their pattern unchanged. */
|
|
590
|
-
function samplePath(pattern: string): string {
|
|
591
|
-
return pattern.replace(/[:*]+([A-Za-z0-9_]+)/g, 'sample');
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
interface RouteModule {
|
|
595
|
-
default: ComponentType;
|
|
596
|
-
ssr?: boolean;
|
|
597
|
-
loader?: (args: { params: Record<string, string>; searchParams: URLSearchParams }) => unknown;
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
/** The outcome of the final {@link extractTemplates} pass. */
|
|
601
|
-
export interface ExtractResult {
|
|
602
|
-
/** The route patterns that produced a template. */
|
|
603
|
-
readonly generated: string[];
|
|
604
|
-
/**
|
|
605
|
-
* Whether the AUTHORITATIVE `<server>/_ssr/<name>.slots.ts` module(s) just written differ from
|
|
606
|
-
* the ones the server was already compiled against (`priorServerSlots`). True means the `HASH`
|
|
607
|
-
* (or `Slot` ids) rotated after the server build, so the server must be recompiled to bake the
|
|
608
|
-
* correct `HASH` (otherwise the host rejects the guest's stale hash as a deploy skew).
|
|
609
|
-
*/
|
|
610
|
-
readonly serverSlotsChanged: boolean;
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
/**
|
|
614
|
-
* FINAL pass (runs AFTER the client/Vite build): render every `export const ssr = true` route to
|
|
615
|
-
* `<outDir>/_ssr/<name>.{tmpl,slots,slots.ts}` + a `templates.json` index, copy the `.tmpl`/`.slots`
|
|
616
|
-
* into the edge host bundle at `hosts/<host>/_tmpl/`, AND (re)write the server-importable
|
|
617
|
-
* `<server>/_ssr/<name>.slots.ts` module(s) — now against the real built shell, so the `HASH` is
|
|
618
|
-
* authoritative. Skips (with a warning) any route that throws under static markup.
|
|
619
|
-
*
|
|
620
|
-
* `priorServerSlots` is the module map the slots PRE-PASS wrote (what the server was compiled
|
|
621
|
-
* against); compared against the authoritative modules here to report whether a server recompile is
|
|
622
|
-
* needed (see {@link ExtractResult.serverSlotsChanged}).
|
|
623
|
-
*/
|
|
624
|
-
export async function extractTemplates(
|
|
625
|
-
cfg: ResolvedToilConfig,
|
|
626
|
-
hostName = 'edge',
|
|
627
|
-
priorServerSlots: Map<string, string> = new Map(),
|
|
628
|
-
): Promise<ExtractResult> {
|
|
629
|
-
const shell = resolveShell(cfg, true);
|
|
630
|
-
if (shell === null) return { generated: [], serverSlotsChanged: false };
|
|
631
|
-
|
|
632
|
-
const rendered = await renderSsrRoutes(cfg, shell);
|
|
633
|
-
|
|
634
|
-
const outDir = path.resolve(cfg.root, cfg.outDir);
|
|
635
|
-
const ssrDir = path.join(outDir, '_ssr');
|
|
636
|
-
const hostsTmplDir = path.join(cfg.root, 'hosts', hostName, '_tmpl');
|
|
637
|
-
const generated: string[] = [];
|
|
638
|
-
const index: { route: string; name: string; hash: string }[] = [];
|
|
639
|
-
|
|
640
|
-
for (const { pattern, art } of rendered) {
|
|
641
|
-
writeTemplateArtifacts(ssrDir, art);
|
|
642
|
-
fs.mkdirSync(hostsTmplDir, { recursive: true });
|
|
643
|
-
fs.copyFileSync(
|
|
644
|
-
path.join(ssrDir, `${art.name}.tmpl`),
|
|
645
|
-
path.join(hostsTmplDir, `${art.name}.tmpl`),
|
|
646
|
-
);
|
|
647
|
-
fs.copyFileSync(
|
|
648
|
-
path.join(ssrDir, `${art.name}.slots`),
|
|
649
|
-
path.join(hostsTmplDir, `${art.name}.slots`),
|
|
650
|
-
);
|
|
651
|
-
index.push({ route: pattern, name: art.name, hash: art.hash.toString('hex') });
|
|
652
|
-
generated.push(pattern);
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
// Write the AUTHORITATIVE server-importable slots module(s) (the real built-shell HASH) and
|
|
656
|
-
// detect whether they changed since the pre-pass the server was compiled against.
|
|
657
|
-
const authoritative = writeServerSlotsModules(
|
|
658
|
-
cfg.root,
|
|
659
|
-
rendered.map((r) => ({ name: r.art.name, slotsModule: r.art.slotsModule })),
|
|
660
|
-
);
|
|
661
|
-
let serverSlotsChanged = false;
|
|
662
|
-
for (const [name, mod] of authoritative) {
|
|
663
|
-
if (priorServerSlots.get(name) !== mod) serverSlotsChanged = true;
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
if (generated.length > 0) {
|
|
667
|
-
fs.mkdirSync(ssrDir, { recursive: true });
|
|
668
|
-
fs.writeFileSync(path.join(ssrDir, 'templates.json'), JSON.stringify(index, null, 2));
|
|
669
|
-
process.stdout.write(
|
|
670
|
-
` ✓ extracted ${String(generated.length)} SSR template${
|
|
671
|
-
generated.length === 1 ? '' : 's'
|
|
672
|
-
}\n`,
|
|
673
|
-
);
|
|
674
|
-
}
|
|
675
|
-
return { generated, serverSlotsChanged };
|
|
676
|
-
}
|