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,381 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Route data loaders. A route file may export a `loader` alongside its default component; the
|
|
3
|
-
* loader runs on navigation, in parallel with the route's chunk load, and the page suspends until
|
|
4
|
-
* it resolves (so `loading.tsx` shows and `useNavigationPending` is true). The page reads the
|
|
5
|
-
* result with `useLoaderData<typeof loader>()`.
|
|
6
|
-
*
|
|
7
|
-
* Caching: results are cached by URL (`pathname + search`). Re-renders reuse the cached result (the
|
|
8
|
-
* loader does not re-run). Across navigations, the cache policy comes from the route's optional
|
|
9
|
-
* `export const revalidate` ({@link Revalidate}): by default the loader re-runs on every navigation;
|
|
10
|
-
* a number keeps data fresh for that many seconds; `false` caches until manual invalidation.
|
|
11
|
-
* `revalidate()` / `router.refresh()` bust the cache to force a refetch.
|
|
12
|
-
*/
|
|
13
|
-
import { type ComponentType, createContext, useContext } from 'react';
|
|
14
|
-
|
|
15
|
-
import type { HeadSpec } from '../head/head.js';
|
|
16
|
-
import { type GenerateMetadata, type Metadata, resolveMetadata } from '../head/metadata.js';
|
|
17
|
-
import { navigationEpoch, refresh as rerender } from '../navigation/navigation.js';
|
|
18
|
-
import type { RouteDef } from '../types.js';
|
|
19
|
-
import type { RouteParams } from './match.js';
|
|
20
|
-
|
|
21
|
-
/** Argument passed to a route `loader`. */
|
|
22
|
-
export interface LoaderArgs {
|
|
23
|
-
readonly params: RouteParams;
|
|
24
|
-
readonly searchParams: URLSearchParams;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** A route `loader`: `export const loader = ({ params }) => …` (sync or async). */
|
|
28
|
-
export type LoaderFunction<T = unknown> = (args: LoaderArgs) => T | Promise<T>;
|
|
29
|
-
|
|
30
|
-
/** One concrete set of route params (a dynamic segment maps to a string, a catch-all to a string[]). */
|
|
31
|
-
export type StaticParams = Record<string, string | string[]>;
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* A route's `export const generateStaticParams`: returns the concrete param sets to pre-render at
|
|
35
|
-
* build time (SSG). toil enumerates them, runs the route's `generateMetadata` per set, and bakes a
|
|
36
|
-
* `<url>/index.html` + sitemap entry for each, so dynamic routes get build-time SEO. Build-only.
|
|
37
|
-
*/
|
|
38
|
-
export type GenerateStaticParams = () => StaticParams[] | Promise<StaticParams[]>;
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Per-route cache policy, set with `export const revalidate` in a route file:
|
|
42
|
-
* - `0` (default): re-run the loader on every navigation to the route.
|
|
43
|
-
* - a positive number: reuse cached data across navigations for that many **seconds**, then refetch.
|
|
44
|
-
* - `false`: cache indefinitely until manually invalidated (`revalidate()` / `router.refresh()`).
|
|
45
|
-
*/
|
|
46
|
-
export type Revalidate = number | false;
|
|
47
|
-
|
|
48
|
-
/** Resolves the data type for {@link useLoaderData}: `typeof loader` → its (awaited) return, else `T`. */
|
|
49
|
-
export type LoaderData<T> = T extends (...args: never[]) => infer R ? Awaited<R> : T;
|
|
50
|
-
|
|
51
|
-
interface RouteModule {
|
|
52
|
-
default: ComponentType;
|
|
53
|
-
loader?: LoaderFunction;
|
|
54
|
-
revalidate?: Revalidate;
|
|
55
|
-
metadata?: Metadata;
|
|
56
|
-
generateMetadata?: GenerateMetadata;
|
|
57
|
-
}
|
|
58
|
-
interface RouteData {
|
|
59
|
-
Component: ComponentType;
|
|
60
|
-
data: unknown;
|
|
61
|
-
/** Resolved baseline head from the route's `metadata` / `generateMetadata`, if any. */
|
|
62
|
-
head?: HeadSpec;
|
|
63
|
-
}
|
|
64
|
-
interface Entry {
|
|
65
|
-
status: 'pending' | 'done' | 'error';
|
|
66
|
-
promise: Promise<void>;
|
|
67
|
-
value?: RouteData;
|
|
68
|
-
error?: unknown;
|
|
69
|
-
/** `Date.now()` when the fetch settled (`0` while pending). */
|
|
70
|
-
loadedAt: number;
|
|
71
|
-
/** Cache policy captured from the route module once loaded. */
|
|
72
|
-
revalidate: Revalidate;
|
|
73
|
-
/** Navigation epoch at which this entry was (re)fetched. */
|
|
74
|
-
epoch: number;
|
|
75
|
-
/** Whether the route exports a `loader`, a route without one has no data that can change. */
|
|
76
|
-
hasLoader: boolean;
|
|
77
|
-
/**
|
|
78
|
-
* Fetched ahead of navigation (hover/visible prefetch), not yet consumed by a navigation. Such an
|
|
79
|
-
* entry is reused once by the next navigation even under `revalidate: 0`, then behaves normally.
|
|
80
|
-
*/
|
|
81
|
-
prefetched: boolean;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const cache = new Map<string, Entry>();
|
|
85
|
-
const MAX_ENTRIES = 32;
|
|
86
|
-
|
|
87
|
-
// Dev-cache observers (the dev toolbar's Data tab). The emit calls are no-ops in production builds,
|
|
88
|
-
// where the toolbar (the only subscriber) is dead-code-eliminated.
|
|
89
|
-
const cacheListeners = new Set<() => void>();
|
|
90
|
-
// Cached snapshot for useSyncExternalStore: getSnapshot must return a stable reference between
|
|
91
|
-
// changes, so we recompute only when the cache mutates (in emitCache), not on every read. Returning
|
|
92
|
-
// a fresh array per call makes React think the store changed every render -> infinite loop.
|
|
93
|
-
let cacheSnapshot: LoaderCacheSnapshot[] = [];
|
|
94
|
-
function emitCache(): void {
|
|
95
|
-
cacheSnapshot = [...cache.entries()].map(([key, e]) => ({
|
|
96
|
-
key,
|
|
97
|
-
status: e.status,
|
|
98
|
-
hasLoader: e.hasLoader,
|
|
99
|
-
revalidate: e.revalidate,
|
|
100
|
-
loadedAt: e.loadedAt,
|
|
101
|
-
epoch: e.epoch,
|
|
102
|
-
data: e.value?.data,
|
|
103
|
-
}));
|
|
104
|
-
for (const l of cacheListeners) l();
|
|
105
|
-
}
|
|
106
|
-
/** Subscribes to loader-cache changes (dev toolbar). Returns an unsubscribe. */
|
|
107
|
-
export function subscribeLoaderCache(listener: () => void): () => void {
|
|
108
|
-
cacheListeners.add(listener);
|
|
109
|
-
return () => {
|
|
110
|
-
cacheListeners.delete(listener);
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
/** A read-only snapshot of one cached loader entry (dev toolbar). */
|
|
114
|
-
export interface LoaderCacheSnapshot {
|
|
115
|
-
readonly key: string;
|
|
116
|
-
readonly status: Entry['status'];
|
|
117
|
-
readonly hasLoader: boolean;
|
|
118
|
-
readonly revalidate: Revalidate;
|
|
119
|
-
readonly loadedAt: number;
|
|
120
|
-
readonly epoch: number;
|
|
121
|
-
readonly data: unknown;
|
|
122
|
-
}
|
|
123
|
-
/** Snapshots the live loader cache (dev toolbar). Returns a stable reference between changes. */
|
|
124
|
-
export function inspectLoaderCache(): LoaderCacheSnapshot[] {
|
|
125
|
-
return cacheSnapshot;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/** Cache key for a URL: path + query (hash is ignored, it never changes loader data). */
|
|
129
|
-
export function loaderKey(pathname: string, search: string): string {
|
|
130
|
-
return `${pathname}${search}`;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Warms a route's chunk *and* loader data for a URL ahead of navigation (the prefetcher calls this on
|
|
135
|
-
* link hover/focus), so the eventual click commits synchronously instead of suspending. The entry is
|
|
136
|
-
* flagged `prefetched` so the next navigation reuses it once even under the default `revalidate: 0`.
|
|
137
|
-
* No-op when the URL is already cached or its fetch is already in flight. Errors are swallowed into
|
|
138
|
-
* the entry (the real navigation will retry and surface them via the route's `error.tsx`).
|
|
139
|
-
*/
|
|
140
|
-
export function prefetchRouteData(
|
|
141
|
-
route: RouteDef,
|
|
142
|
-
params: RouteParams,
|
|
143
|
-
pathname: string,
|
|
144
|
-
search: string,
|
|
145
|
-
): void {
|
|
146
|
-
const key = loaderKey(pathname, search);
|
|
147
|
-
const existing = cache.get(key);
|
|
148
|
-
if (existing && existing.status !== 'error') return;
|
|
149
|
-
startFetch(route, params, key, navigationEpoch(), search, true);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/** Loads the route module and runs its loader (if any), in parallel where possible. */
|
|
153
|
-
async function loadRoute(
|
|
154
|
-
route: RouteDef,
|
|
155
|
-
params: RouteParams,
|
|
156
|
-
search: string,
|
|
157
|
-
): Promise<{ data: RouteData; revalidate: Revalidate; hasLoader: boolean }> {
|
|
158
|
-
const mod: RouteModule = await route.load();
|
|
159
|
-
const searchParams = new URLSearchParams(search);
|
|
160
|
-
const data = mod.loader ? await mod.loader({ params, searchParams }) : undefined;
|
|
161
|
-
let head: HeadSpec | undefined;
|
|
162
|
-
if (mod.generateMetadata) {
|
|
163
|
-
head = resolveMetadata(await mod.generateMetadata({ params, searchParams, data }));
|
|
164
|
-
} else if (mod.metadata) {
|
|
165
|
-
head = resolveMetadata(mod.metadata);
|
|
166
|
-
}
|
|
167
|
-
return {
|
|
168
|
-
data: { Component: mod.default, data, head },
|
|
169
|
-
revalidate: mod.revalidate ?? 0,
|
|
170
|
-
hasLoader: mod.loader != null,
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/** Whether a settled entry must be refetched for the current navigation. */
|
|
175
|
-
function isStale(entry: Entry, epoch: number): boolean {
|
|
176
|
-
if (entry.status === 'error') return true; // always retry a failed load
|
|
177
|
-
// A route with no loader has no data that can change, keep it cached so repeat navigations
|
|
178
|
-
// render synchronously (instant) instead of re-suspending and remounting on every switch.
|
|
179
|
-
if (!entry.hasLoader) return false;
|
|
180
|
-
if (entry.revalidate === false) return false; // cache forever
|
|
181
|
-
if (entry.revalidate === 0) {
|
|
182
|
-
// A just-prefetched entry is the fetch for this very navigation, reuse it once (consumed in
|
|
183
|
-
// readRouteData, so the *next* navigation refetches as `revalidate: 0` normally requires).
|
|
184
|
-
if (entry.prefetched) return false;
|
|
185
|
-
return entry.epoch !== epoch; // otherwise refetch once per navigation
|
|
186
|
-
}
|
|
187
|
-
return Date.now() - entry.loadedAt >= entry.revalidate * 1000; // time-based staleness
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/** Starts (and caches) a fresh fetch for `key`. */
|
|
191
|
-
function startFetch(
|
|
192
|
-
route: RouteDef,
|
|
193
|
-
params: RouteParams,
|
|
194
|
-
key: string,
|
|
195
|
-
epoch: number,
|
|
196
|
-
search: string,
|
|
197
|
-
prefetched = false,
|
|
198
|
-
): Entry {
|
|
199
|
-
const created: Entry = {
|
|
200
|
-
status: 'pending',
|
|
201
|
-
promise: Promise.resolve(),
|
|
202
|
-
loadedAt: 0,
|
|
203
|
-
revalidate: 0,
|
|
204
|
-
epoch,
|
|
205
|
-
hasLoader: false,
|
|
206
|
-
prefetched,
|
|
207
|
-
};
|
|
208
|
-
created.promise = loadRoute(route, params, search).then(
|
|
209
|
-
(result) => {
|
|
210
|
-
created.value = result.data;
|
|
211
|
-
created.revalidate = result.revalidate;
|
|
212
|
-
created.hasLoader = result.hasLoader;
|
|
213
|
-
created.loadedAt = Date.now();
|
|
214
|
-
created.status = 'done';
|
|
215
|
-
emitCache();
|
|
216
|
-
},
|
|
217
|
-
(error: unknown) => {
|
|
218
|
-
created.error = error;
|
|
219
|
-
created.loadedAt = Date.now();
|
|
220
|
-
created.status = 'error';
|
|
221
|
-
emitCache();
|
|
222
|
-
},
|
|
223
|
-
);
|
|
224
|
-
cache.set(key, created);
|
|
225
|
-
while (cache.size > MAX_ENTRIES) {
|
|
226
|
-
const oldest = cache.keys().next().value;
|
|
227
|
-
if (oldest === undefined || oldest === key) break;
|
|
228
|
-
cache.delete(oldest);
|
|
229
|
-
}
|
|
230
|
-
emitCache();
|
|
231
|
-
return created;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* Reads (or starts) the route's module + loader data for the URL `key`, suspending until ready.
|
|
236
|
-
* Throws the loader's error so the route's `error.tsx` boundary can catch it. `epoch` is the current
|
|
237
|
-
* navigation epoch, used to refetch once-per-navigation under the default cache policy.
|
|
238
|
-
*/
|
|
239
|
-
export function readRouteData(
|
|
240
|
-
route: RouteDef,
|
|
241
|
-
params: RouteParams,
|
|
242
|
-
key: string,
|
|
243
|
-
epoch: number,
|
|
244
|
-
): RouteData {
|
|
245
|
-
const search = typeof window === 'undefined' ? '' : window.location.search;
|
|
246
|
-
let entry = cache.get(key);
|
|
247
|
-
if (entry && entry.status !== 'pending' && isStale(entry, epoch)) {
|
|
248
|
-
entry = undefined; // stale → drop and refetch below
|
|
249
|
-
}
|
|
250
|
-
entry ??= startFetch(route, params, key, epoch, search);
|
|
251
|
-
if (entry.status === 'pending') throw entry.promise;
|
|
252
|
-
if (entry.status === 'error') throw entry.error;
|
|
253
|
-
if (!entry.value) throw entry.promise;
|
|
254
|
-
// Claim a prefetched entry for this navigation: clear the flag and stamp the current epoch so the
|
|
255
|
-
// next navigation re-evaluates its revalidate policy (a `revalidate: 0` route refetches again).
|
|
256
|
-
if (entry.prefetched) {
|
|
257
|
-
entry.prefetched = false;
|
|
258
|
-
entry.epoch = epoch;
|
|
259
|
-
}
|
|
260
|
-
return entry.value;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/**
|
|
264
|
-
* Seed the loader cache from server-rendered hydration state, BEFORE the first
|
|
265
|
-
* client render (called by `mount` on an SSR document). The route's module
|
|
266
|
-
* still loads (the browser needs the component), but its `loader` does NOT
|
|
267
|
-
* re-run: the server's `data` is used, so the first client render reproduces
|
|
268
|
-
* the server HTML and `hydrateRoot` stays clean. The entry is `prefetched`, so
|
|
269
|
-
* the NEXT navigation re-evaluates the route's `revalidate` policy normally.
|
|
270
|
-
*
|
|
271
|
-
* For a flash-free hydrate the SSR document should modulepreload the route
|
|
272
|
-
* chunk, so `route.load()` resolves in a microtask and the root never suspends.
|
|
273
|
-
*/
|
|
274
|
-
export function hydrateLoaderData(
|
|
275
|
-
route: RouteDef,
|
|
276
|
-
params: RouteParams,
|
|
277
|
-
pathname: string,
|
|
278
|
-
search: string,
|
|
279
|
-
data: unknown,
|
|
280
|
-
): void {
|
|
281
|
-
const key = loaderKey(pathname, search);
|
|
282
|
-
if (cache.has(key)) return;
|
|
283
|
-
const epoch = navigationEpoch();
|
|
284
|
-
const entry: Entry = {
|
|
285
|
-
status: 'pending',
|
|
286
|
-
promise: Promise.resolve(),
|
|
287
|
-
loadedAt: 0,
|
|
288
|
-
revalidate: 0,
|
|
289
|
-
epoch,
|
|
290
|
-
hasLoader: true,
|
|
291
|
-
prefetched: true,
|
|
292
|
-
};
|
|
293
|
-
entry.promise = route.load().then(
|
|
294
|
-
async (mod: RouteModule) => {
|
|
295
|
-
const searchParams = new URLSearchParams(search);
|
|
296
|
-
let head: HeadSpec | undefined;
|
|
297
|
-
if (mod.generateMetadata) {
|
|
298
|
-
head = resolveMetadata(await mod.generateMetadata({ params, searchParams, data }));
|
|
299
|
-
} else if (mod.metadata) {
|
|
300
|
-
head = resolveMetadata(mod.metadata);
|
|
301
|
-
}
|
|
302
|
-
entry.value = { Component: mod.default, data, head };
|
|
303
|
-
entry.revalidate = mod.revalidate ?? 0;
|
|
304
|
-
entry.loadedAt = Date.now();
|
|
305
|
-
entry.status = 'done';
|
|
306
|
-
emitCache();
|
|
307
|
-
},
|
|
308
|
-
(error: unknown) => {
|
|
309
|
-
entry.error = error;
|
|
310
|
-
entry.loadedAt = Date.now();
|
|
311
|
-
entry.status = 'error';
|
|
312
|
-
emitCache();
|
|
313
|
-
},
|
|
314
|
-
);
|
|
315
|
-
cache.set(key, entry);
|
|
316
|
-
emitCache();
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
/** Clears all cached loader data, so the next render re-runs loaders (used by router.refresh). */
|
|
320
|
-
export function clearLoaderData(): void {
|
|
321
|
-
cache.clear();
|
|
322
|
-
emitCache();
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
/** Cache key for an href (relative or absolute), matching {@link loaderKey}. */
|
|
326
|
-
function keyForHref(href: string): string | undefined {
|
|
327
|
-
if (typeof window === 'undefined') return undefined;
|
|
328
|
-
try {
|
|
329
|
-
const url = new URL(href, window.location.href);
|
|
330
|
-
return loaderKey(url.pathname, url.search);
|
|
331
|
-
} catch {
|
|
332
|
-
return undefined;
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
/**
|
|
337
|
-
* Invalidates cached loader data so it refetches on the next render. With no argument, clears every
|
|
338
|
-
* route; with an `href`, clears just that route's entry. Pair with a re-render (the active route
|
|
339
|
-
* refetches and suspends), see {@link revalidate}.
|
|
340
|
-
*/
|
|
341
|
-
export function invalidateLoaderData(href?: string): void {
|
|
342
|
-
if (href === undefined) {
|
|
343
|
-
cache.clear();
|
|
344
|
-
emitCache();
|
|
345
|
-
return;
|
|
346
|
-
}
|
|
347
|
-
const key = keyForHref(href);
|
|
348
|
-
if (key !== undefined) cache.delete(key);
|
|
349
|
-
emitCache();
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
/**
|
|
353
|
-
* Invalidates loader data and re-renders so the active route refetches. Call after a mutation to
|
|
354
|
-
* refresh the current route (`revalidate()`), or target another route by href
|
|
355
|
-
* (`revalidate('/posts')`). Usable outside React (e.g. in an event handler after a `fetch`).
|
|
356
|
-
*/
|
|
357
|
-
export function revalidate(href?: string): void {
|
|
358
|
-
invalidateLoaderData(href);
|
|
359
|
-
rerender();
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
/** Holds the active route's loader data; provided by the Router, read by {@link useLoaderData}. */
|
|
363
|
-
export const LoaderDataContext = createContext<unknown>(undefined);
|
|
364
|
-
|
|
365
|
-
/**
|
|
366
|
-
* The data returned by the active route's `loader`. Three ways to type it, easiest first:
|
|
367
|
-
*
|
|
368
|
-
* 1. **Pass the loader**, zero generics, fully inferred from your loader's return:
|
|
369
|
-
* `const data = useLoaderData(loader);`
|
|
370
|
-
* 2. Pass `typeof loader` as a type argument: `useLoaderData<typeof loader>();`
|
|
371
|
-
* 3. Pass an explicit shape: `useLoaderData<Post>();`
|
|
372
|
-
*
|
|
373
|
-
* With no argument and no type, it returns `unknown` (never `any`), so the data is there at runtime,
|
|
374
|
-
* but you must annotate or narrow before using it. There's no way to infer the type from a bare call:
|
|
375
|
-
* TypeScript can't tell which file (and so which `loader`) the call belongs to, hence option 1.
|
|
376
|
-
*/
|
|
377
|
-
export function useLoaderData<L extends LoaderFunction>(loader: L): Awaited<ReturnType<L>>;
|
|
378
|
-
export function useLoaderData<T = unknown>(): LoaderData<T>;
|
|
379
|
-
export function useLoaderData(_loader?: LoaderFunction): unknown {
|
|
380
|
-
return useContext(LoaderDataContext);
|
|
381
|
-
}
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
/** Extracted dynamic route parameters, e.g. `{ id: "42" }` for `/blog/:id` matching `/blog/42`. */
|
|
2
|
-
export type RouteParams = Record<string, string>;
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Matches a route pattern against a pathname, returning extracted params or `null` if no match.
|
|
6
|
-
* Pure and runtime-agnostic (used by the router and unit-tested directly).
|
|
7
|
-
* matchRoute('/', '/') -> {}
|
|
8
|
-
* matchRoute('/blog/:id', '/blog/42') -> { id: '42' }
|
|
9
|
-
* matchRoute('/docs/*slug', '/docs/a/b') -> { slug: 'a/b' } (catch-all, 1+ segments)
|
|
10
|
-
* matchRoute('/docs/**slug', '/docs') -> { slug: '' } (optional catch-all, 0+ segments)
|
|
11
|
-
* matchRoute('/about', '/x') -> null
|
|
12
|
-
*/
|
|
13
|
-
export function matchRoute(pattern: string, pathname: string): RouteParams | null {
|
|
14
|
-
const patternSegs = pattern.split('/').filter(Boolean);
|
|
15
|
-
const pathSegs = pathname.split('/').filter(Boolean);
|
|
16
|
-
|
|
17
|
-
const params: RouteParams = {};
|
|
18
|
-
for (let i = 0; i < patternSegs.length; i++) {
|
|
19
|
-
const p = patternSegs[i];
|
|
20
|
-
|
|
21
|
-
// Optional catch-all (`**slug`): captures the rest of the path, matching zero or more segments.
|
|
22
|
-
if (p.startsWith('**')) {
|
|
23
|
-
params[p.slice(2)] = pathSegs
|
|
24
|
-
.slice(i)
|
|
25
|
-
.map((s) => decodeURIComponent(s))
|
|
26
|
-
.join('/');
|
|
27
|
-
return params;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
if (p.startsWith('*')) {
|
|
31
|
-
const rest = pathSegs.slice(i);
|
|
32
|
-
if (rest.length === 0) return null;
|
|
33
|
-
params[p.slice(1)] = rest.map((s) => decodeURIComponent(s)).join('/');
|
|
34
|
-
return params;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
if (i >= pathSegs.length) return null;
|
|
38
|
-
const value = pathSegs[i];
|
|
39
|
-
if (p.startsWith(':')) {
|
|
40
|
-
params[p.slice(1)] = decodeURIComponent(value);
|
|
41
|
-
} else if (p !== value) {
|
|
42
|
-
return null;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return patternSegs.length === pathSegs.length ? params : null;
|
|
47
|
-
}
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import { createRoot, hydrateRoot } from 'react-dom/client';
|
|
2
|
-
|
|
3
|
-
import { initNavigation } from '../navigation/navigation.js';
|
|
4
|
-
import { startPrefetcher } from '../navigation/prefetch.js';
|
|
5
|
-
import { Router } from './Router.js';
|
|
6
|
-
import type { ErrorComponentLoader, LayoutLoader, NotFoundLoader, RouteDef } from '../types.js';
|
|
7
|
-
|
|
8
|
-
/** An edge-SSR document carries a `<* id="__toil_ssr">` marker baked into the
|
|
9
|
-
* template; its presence means the server rendered real first-paint HTML into
|
|
10
|
-
* `#root`, so `mount` hydrates it in place rather than client-rendering. */
|
|
11
|
-
function isSsrDocument(): boolean {
|
|
12
|
-
return typeof document !== 'undefined' && document.getElementById('__toil_ssr') !== null;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Mounts the toil client app into `#root` and starts idle link prefetching. Called by the
|
|
17
|
-
* compiler-generated `.toil/entry.tsx`.
|
|
18
|
-
*/
|
|
19
|
-
export function mount(
|
|
20
|
-
routes: RouteDef[],
|
|
21
|
-
layout: LayoutLoader = null,
|
|
22
|
-
notFound: NotFoundLoader = null,
|
|
23
|
-
globalError: ErrorComponentLoader = null,
|
|
24
|
-
slots: Record<string, RouteDef[]> = {},
|
|
25
|
-
): void {
|
|
26
|
-
const el = document.getElementById('root');
|
|
27
|
-
if (!el) throw new Error('toil: #root element not found');
|
|
28
|
-
initNavigation();
|
|
29
|
-
const app = (
|
|
30
|
-
<Router
|
|
31
|
-
routes={routes}
|
|
32
|
-
layout={layout}
|
|
33
|
-
notFound={notFound}
|
|
34
|
-
globalError={globalError}
|
|
35
|
-
slots={slots}
|
|
36
|
-
/>
|
|
37
|
-
);
|
|
38
|
-
// In dev, wrap the app in the error overlay + dev toolbar so uncaught errors surface and dev info
|
|
39
|
-
// is available. The guard is the literal `import.meta.env.DEV`, and the dev modules are pulled in
|
|
40
|
-
// with DYNAMIC `import()` INSIDE this branch. A production build dead-code-eliminates the whole
|
|
41
|
-
// branch - the `import()` calls included - so the devtools/overlay never enter the bundle, not even
|
|
42
|
-
// as an unreferenced chunk. (A static top-level import would otherwise survive into production unless
|
|
43
|
-
// the package opts into tree-shaking via a `sideEffects` declaration.)
|
|
44
|
-
if ((import.meta as unknown as { env: { DEV: boolean } }).env.DEV) {
|
|
45
|
-
void (async () => {
|
|
46
|
-
const { DevToolbar } = await import('../dev/devtools.js');
|
|
47
|
-
const { DevErrorBoundary, DevErrorOverlay, initDevErrorOverlay } = await import(
|
|
48
|
-
'../dev/error-overlay.js'
|
|
49
|
-
);
|
|
50
|
-
initDevErrorOverlay();
|
|
51
|
-
// Dev tools (error overlay + toolbar) render into their OWN body-level container, never
|
|
52
|
-
// inside `#root`, so `#root` holds only the app markup. That lets an SSR document hydrate
|
|
53
|
-
// cleanly (the server only rendered the app into `#root`), and is harmless for a plain
|
|
54
|
-
// client-rendered page.
|
|
55
|
-
const devEl = document.createElement('div');
|
|
56
|
-
devEl.id = '__toil_dev';
|
|
57
|
-
document.body.appendChild(devEl);
|
|
58
|
-
createRoot(devEl).render(
|
|
59
|
-
<>
|
|
60
|
-
<DevErrorOverlay />
|
|
61
|
-
<DevToolbar
|
|
62
|
-
routes={routes}
|
|
63
|
-
slots={slots}
|
|
64
|
-
/>
|
|
65
|
-
</>,
|
|
66
|
-
);
|
|
67
|
-
const tree = <DevErrorBoundary>{app}</DevErrorBoundary>;
|
|
68
|
-
if (isSsrDocument()) {
|
|
69
|
-
// Edge-SSR: hydrate the server-rendered markup in place.
|
|
70
|
-
hydrateRoot(el, tree);
|
|
71
|
-
// The dev shell inlined `<style data-toil-dev-ssr>` so the server-rendered first paint
|
|
72
|
-
// was already styled (no FOUC). Vite has since injected the same CSS via the entry's
|
|
73
|
-
// imports (HMR-managed), so drop the static style to avoid a stale copy surviving a hot edit.
|
|
74
|
-
document.querySelectorAll('[data-toil-dev-ssr]').forEach((n) => {
|
|
75
|
-
n.remove();
|
|
76
|
-
});
|
|
77
|
-
} else {
|
|
78
|
-
createRoot(el).render(tree);
|
|
79
|
-
}
|
|
80
|
-
})();
|
|
81
|
-
} else if (isSsrDocument()) {
|
|
82
|
-
// Edge-SSR: the document already holds server-rendered markup; hydrate it
|
|
83
|
-
// (reuse the DOM) rather than client-rendering from scratch.
|
|
84
|
-
hydrateRoot(el, app);
|
|
85
|
-
} else {
|
|
86
|
-
createRoot(el).render(app);
|
|
87
|
-
}
|
|
88
|
-
// Prefetch across the main tree and every slot tree (one prefetcher owns the whole table).
|
|
89
|
-
startPrefetcher([...routes, ...Object.values(slots).flat()]);
|
|
90
|
-
}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* React context carrying the current route's dynamic params. The provider is set by {@link Router};
|
|
3
|
-
* read it with the {@link useParams} hook rather than consuming the context directly.
|
|
4
|
-
*/
|
|
5
|
-
import { createContext } from 'react';
|
|
6
|
-
|
|
7
|
-
import type { RouteParams } from './match.js';
|
|
8
|
-
|
|
9
|
-
/** Holds the params extracted from the active route (e.g. `{ id }` for `/blog/:id`). */
|
|
10
|
-
export const ParamsContext = createContext<RouteParams>({});
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Context carrying the rendered element for each parallel-route slot (`@slot`), keyed by name.
|
|
3
|
-
* Provided by the Router for the current URL, consumed by {@link Slot}.
|
|
4
|
-
*/
|
|
5
|
-
import { createContext, type ReactNode } from 'react';
|
|
6
|
-
|
|
7
|
-
export const SlotContext = createContext<Record<string, ReactNode>>({});
|
package/src/client/rpc.ts
DELETED
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The client-callable server surface (`Server`). Its TYPED shape is generated by
|
|
3
|
-
* the toilscript server build into the project's `shared/server.ts`
|
|
4
|
-
* (`declare global { const Server }`); this module is the runtime behind it.
|
|
5
|
-
*
|
|
6
|
-
* Three surfaces live under `Server`, all attached by the generated `shared/server.ts` on import:
|
|
7
|
-
* - `Server.REST.<controller>.<route>(args)` - a fetch client (`globalThis.__toilRest`).
|
|
8
|
-
* - `Server.Stream.<class>.connect()` - the stream client (`globalThis.__toilStream`).
|
|
9
|
-
* - `Server.<service>.<method>(args)` and a free `Server.<remote>(args)` - the RPC client
|
|
10
|
-
* (`globalThis.__toilRpc`): each POSTs to `/__toil_rpc` with a method id and @data-encoded args.
|
|
11
|
-
* Build the server (`npm run build:server`) to (re)generate the typed surface + attach the clients.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
/** A recursive proxy that throws on call, used when the REST client hasn't loaded. */
|
|
15
|
-
function restMissingStub(path: string): unknown {
|
|
16
|
-
const call = (): never => {
|
|
17
|
-
throw new Error(
|
|
18
|
-
`toiljs REST: ${path}() is unavailable. The generated REST client has not loaded - ` +
|
|
19
|
-
`run 'npm run build:server' to generate shared/server.ts; the client then attaches automatically.`,
|
|
20
|
-
);
|
|
21
|
-
};
|
|
22
|
-
return new Proxy(call, {
|
|
23
|
-
get(_target, prop) {
|
|
24
|
-
if (typeof prop === 'symbol' || prop === 'then') return undefined;
|
|
25
|
-
return restMissingStub(`${path}.${prop}`);
|
|
26
|
-
},
|
|
27
|
-
apply() {
|
|
28
|
-
return call();
|
|
29
|
-
},
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** A recursive proxy that throws on call, used when the stream client hasn't loaded. */
|
|
34
|
-
function streamMissingStub(path: string): unknown {
|
|
35
|
-
const call = (): never => {
|
|
36
|
-
throw new Error(
|
|
37
|
-
`toiljs Stream: ${path}() is unavailable. The generated stream client has not loaded - ` +
|
|
38
|
-
`run 'npm run build:server' to generate shared/server.ts; the client then attaches automatically.`,
|
|
39
|
-
);
|
|
40
|
-
};
|
|
41
|
-
return new Proxy(call, {
|
|
42
|
-
get(_target, prop) {
|
|
43
|
-
if (typeof prop === 'symbol' || prop === 'then') return undefined;
|
|
44
|
-
return streamMissingStub(`${path}.${prop}`);
|
|
45
|
-
},
|
|
46
|
-
apply() {
|
|
47
|
-
return call();
|
|
48
|
-
},
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** Builds a recursive proxy that throws on call, supporting `Server.svc.method()`. */
|
|
53
|
-
function rpcStub(path: string): unknown {
|
|
54
|
-
const call = (): never => {
|
|
55
|
-
throw new Error(
|
|
56
|
-
`toiljs RPC: ${path}() is unavailable. The generated RPC client has not loaded - ` +
|
|
57
|
-
`run 'npm run build:server' to generate shared/server.ts; the client then attaches automatically.`,
|
|
58
|
-
);
|
|
59
|
-
};
|
|
60
|
-
return new Proxy(call, {
|
|
61
|
-
get(_target, prop) {
|
|
62
|
-
// Not thenable, and ignore symbol probes (e.g. from awaiting/inspection).
|
|
63
|
-
if (typeof prop === 'symbol' || prop === 'then') return undefined;
|
|
64
|
-
// `Server.REST` surfaces the generated fetch client attached by shared/server.ts.
|
|
65
|
-
if (path === 'Server' && prop === 'REST') {
|
|
66
|
-
const rest = (globalThis as { __toilRest?: unknown }).__toilRest;
|
|
67
|
-
return rest !== undefined ? rest : restMissingStub('Server.REST');
|
|
68
|
-
}
|
|
69
|
-
// `Server.Stream` surfaces the generated stream client attached by shared/server.ts.
|
|
70
|
-
if (path === 'Server' && prop === 'Stream') {
|
|
71
|
-
const stream = (globalThis as { __toilStream?: unknown }).__toilStream;
|
|
72
|
-
return stream !== undefined ? stream : streamMissingStub('Server.Stream');
|
|
73
|
-
}
|
|
74
|
-
// `Server.<service>` / `Server.<remote>` surface the generated RPC client. Its top-level
|
|
75
|
-
// keys are the @service names (-> a `{ method }` object) and free @remote functions.
|
|
76
|
-
if (path === 'Server') {
|
|
77
|
-
const rpc = (globalThis as { __toilRpc?: Record<string, unknown> }).__toilRpc;
|
|
78
|
-
if (rpc !== undefined && prop in rpc) return rpc[prop];
|
|
79
|
-
}
|
|
80
|
-
return rpcStub(`${path}.${prop}`);
|
|
81
|
-
},
|
|
82
|
-
apply() {
|
|
83
|
-
return call();
|
|
84
|
-
},
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Runtime value for the global `Server`. Typed as `unknown` here; the real types
|
|
90
|
-
* come from the generated `shared/server.ts`. toiljs assigns this to `globalThis`.
|
|
91
|
-
*/
|
|
92
|
-
export const Server: unknown = rpcStub('Server');
|
|
93
|
-
|
|
94
|
-
// Back the generated `declare global { const Server }` (shared/server.ts) with a runtime value, so an
|
|
95
|
-
// app reaches `Server.REST` / `Server.Stream.<Name>` via the global without an import (the design note
|
|
96
|
-
// above). Idempotent - the proxy is a singleton; importing `toiljs/client` evaluates this once.
|
|
97
|
-
(globalThis as Record<string, unknown>).Server = Server;
|