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.
Files changed (326) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +16 -7
  3. package/TYPESCRIPT_LAW.md +1946 -2073
  4. package/build/backend/.tsbuildinfo +1 -1
  5. package/build/cli/.tsbuildinfo +1 -1
  6. package/build/cli/index.js +2375 -258
  7. package/build/client/.tsbuildinfo +1 -1
  8. package/build/client/dev/devtools.js +1 -1
  9. package/build/client/dev/error-overlay.js +1 -1
  10. package/build/client/routing/mount.js +1 -1
  11. package/build/compiler/.tsbuildinfo +1 -1
  12. package/build/compiler/auth-emails.js +7 -3
  13. package/build/compiler/emails.js +1 -1
  14. package/build/compiler/index.d.ts +1 -0
  15. package/build/compiler/index.js +25 -4
  16. package/build/compiler/pages.js +2 -3
  17. package/build/compiler/prerender.d.ts +3 -8
  18. package/build/compiler/prerender.js +88 -127
  19. package/build/compiler/ssg.js +3 -4
  20. package/build/compiler/template-build.js +3 -4
  21. package/build/compiler/toil-docs.generated.js +6 -5
  22. package/build/compiler/vite.js +3 -6
  23. package/build/devserver/.tsbuildinfo +1 -1
  24. package/build/devserver/db/database.js +7 -3
  25. package/build/devserver/db/types.js +1 -1
  26. package/build/devserver/production.js +4 -3
  27. package/build/devserver/runtime/crypto.d.ts +1 -1
  28. package/build/io/.tsbuildinfo +1 -1
  29. package/build/logger/.tsbuildinfo +1 -1
  30. package/build/shared/.tsbuildinfo +1 -1
  31. package/examples/basic/client/routes/analytics.tsx +7 -5
  32. package/examples/basic/server/tsconfig.json +1 -3
  33. package/package.json +45 -44
  34. package/presets/no-uint8array-tostring.js +120 -153
  35. package/presets/oxlint.d.ts +5 -0
  36. package/presets/oxlint.js +224 -0
  37. package/server/runtime/tsconfig.json +1 -3
  38. package/tsconfig.json +2 -11
  39. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -38
  40. package/.github/ISSUE_TEMPLATE/bug_report.yml +0 -90
  41. package/.github/ISSUE_TEMPLATE/config.yml +0 -8
  42. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -20
  43. package/.github/PULL_REQUEST_TEMPLATE.md +0 -43
  44. package/.github/changelog-config.json +0 -45
  45. package/.github/dependabot.yml +0 -27
  46. package/.github/workflows/ci.yml +0 -222
  47. package/.vscode/settings.json +0 -10
  48. package/docs/README.md +0 -107
  49. package/docs/auth/README.md +0 -104
  50. package/docs/auth/configuration.md +0 -105
  51. package/docs/auth/emails.md +0 -92
  52. package/docs/auth/extending.md +0 -202
  53. package/docs/auth/how-it-works.md +0 -138
  54. package/docs/auth/usage.md +0 -188
  55. package/docs/backend/README.md +0 -143
  56. package/docs/backend/data.md +0 -351
  57. package/docs/backend/rest.md +0 -402
  58. package/docs/backend/rpc.md +0 -226
  59. package/docs/background/README.md +0 -114
  60. package/docs/background/daemons.md +0 -276
  61. package/docs/background/derive.md +0 -179
  62. package/docs/cli/README.md +0 -379
  63. package/docs/concepts/ai-guide.md +0 -132
  64. package/docs/concepts/config.md +0 -416
  65. package/docs/concepts/decorators.md +0 -127
  66. package/docs/concepts/security.md +0 -108
  67. package/docs/concepts/tiers.md +0 -166
  68. package/docs/concepts/types.md +0 -216
  69. package/docs/database/README.md +0 -143
  70. package/docs/database/capacity.md +0 -350
  71. package/docs/database/counters.md +0 -174
  72. package/docs/database/documents.md +0 -277
  73. package/docs/database/events.md +0 -307
  74. package/docs/database/membership.md +0 -246
  75. package/docs/database/setup.md +0 -155
  76. package/docs/database/unique.md +0 -216
  77. package/docs/database/views.md +0 -246
  78. package/docs/frontend/README.md +0 -104
  79. package/docs/frontend/components.md +0 -286
  80. package/docs/frontend/data-fetching.md +0 -260
  81. package/docs/frontend/images.md +0 -148
  82. package/docs/frontend/metadata.md +0 -344
  83. package/docs/frontend/navigation.md +0 -296
  84. package/docs/frontend/rendering.md +0 -236
  85. package/docs/frontend/routing.md +0 -346
  86. package/docs/frontend/scripts.md +0 -118
  87. package/docs/frontend/search.md +0 -191
  88. package/docs/frontend/styling.md +0 -147
  89. package/docs/frontend/toil-global.md +0 -216
  90. package/docs/getting-started/README.md +0 -81
  91. package/docs/getting-started/create-project.md +0 -131
  92. package/docs/getting-started/deploy.md +0 -101
  93. package/docs/getting-started/first-app.md +0 -215
  94. package/docs/getting-started/installation.md +0 -124
  95. package/docs/getting-started/migrating.md +0 -125
  96. package/docs/getting-started/project-structure.md +0 -163
  97. package/docs/introduction/README.md +0 -66
  98. package/docs/introduction/design-principles.md +0 -81
  99. package/docs/introduction/distributed.md +0 -91
  100. package/docs/introduction/how-it-works.md +0 -121
  101. package/docs/introduction/hyperscale.md +0 -70
  102. package/docs/introduction/modern-stack.md +0 -85
  103. package/docs/introduction/vs-other-frameworks.md +0 -64
  104. package/docs/introduction/why-toil.md +0 -99
  105. package/docs/llms.txt +0 -95
  106. package/docs/realtime/README.md +0 -102
  107. package/docs/realtime/channels.md +0 -211
  108. package/docs/realtime/streams.md +0 -369
  109. package/docs/services/README.md +0 -60
  110. package/docs/services/analytics.md +0 -279
  111. package/docs/services/caching.md +0 -175
  112. package/docs/services/cookies.md +0 -235
  113. package/docs/services/crypto.md +0 -209
  114. package/docs/services/email.md +0 -289
  115. package/docs/services/environment.md +0 -141
  116. package/docs/services/ratelimit.md +0 -174
  117. package/docs/services/time.md +0 -85
  118. package/eslint.config.js +0 -57
  119. package/examples/basic/client/routes/docs/[...slug].tsx +0 -12
  120. package/presets/eslint.js +0 -88
  121. package/src/backend/index.ts +0 -204
  122. package/src/cli/configure.ts +0 -363
  123. package/src/cli/create.ts +0 -924
  124. package/src/cli/db.ts +0 -158
  125. package/src/cli/diagnostics.ts +0 -665
  126. package/src/cli/doctor.ts +0 -982
  127. package/src/cli/features.ts +0 -170
  128. package/src/cli/index.ts +0 -326
  129. package/src/cli/notify.ts +0 -120
  130. package/src/cli/proc.ts +0 -56
  131. package/src/cli/ui.ts +0 -169
  132. package/src/cli/update.ts +0 -248
  133. package/src/cli/updates.ts +0 -92
  134. package/src/cli/validate.ts +0 -31
  135. package/src/cli/version-check.ts +0 -125
  136. package/src/client/auth.ts +0 -869
  137. package/src/client/channel/channel.ts +0 -146
  138. package/src/client/components/Form.tsx +0 -65
  139. package/src/client/components/Image.tsx +0 -160
  140. package/src/client/components/Script.tsx +0 -113
  141. package/src/client/components/Slot.tsx +0 -21
  142. package/src/client/dev/devtools.tsx +0 -1089
  143. package/src/client/dev/error-overlay.tsx +0 -225
  144. package/src/client/errors.ts +0 -11
  145. package/src/client/head/head.ts +0 -182
  146. package/src/client/head/metadata.ts +0 -110
  147. package/src/client/index.ts +0 -130
  148. package/src/client/navigation/Link.tsx +0 -108
  149. package/src/client/navigation/NavLink.tsx +0 -86
  150. package/src/client/navigation/navigation.ts +0 -235
  151. package/src/client/navigation/prefetch.ts +0 -169
  152. package/src/client/navigation/scroll.ts +0 -53
  153. package/src/client/routing/Router.tsx +0 -216
  154. package/src/client/routing/action.ts +0 -122
  155. package/src/client/routing/error-boundary.tsx +0 -43
  156. package/src/client/routing/hooks.ts +0 -140
  157. package/src/client/routing/lazy.ts +0 -77
  158. package/src/client/routing/loader.ts +0 -381
  159. package/src/client/routing/match.ts +0 -47
  160. package/src/client/routing/mount.tsx +0 -90
  161. package/src/client/routing/params-context.ts +0 -10
  162. package/src/client/routing/slot-context.ts +0 -7
  163. package/src/client/rpc.ts +0 -97
  164. package/src/client/search/search.ts +0 -189
  165. package/src/client/search/use-page-search.ts +0 -73
  166. package/src/client/ssr/markers.tsx +0 -172
  167. package/src/client/stream/client.ts +0 -237
  168. package/src/client/types.ts +0 -73
  169. package/src/compiler/auth-emails.ts +0 -312
  170. package/src/compiler/config.ts +0 -369
  171. package/src/compiler/docs.ts +0 -127
  172. package/src/compiler/email-preview.ts +0 -314
  173. package/src/compiler/emails.ts +0 -404
  174. package/src/compiler/fonts.ts +0 -89
  175. package/src/compiler/generate.ts +0 -471
  176. package/src/compiler/image-blur.ts +0 -42
  177. package/src/compiler/image-report.ts +0 -88
  178. package/src/compiler/index.ts +0 -1362
  179. package/src/compiler/pages.ts +0 -49
  180. package/src/compiler/plugin.ts +0 -391
  181. package/src/compiler/prerender.ts +0 -263
  182. package/src/compiler/routes.ts +0 -170
  183. package/src/compiler/seo.ts +0 -415
  184. package/src/compiler/sri.ts +0 -144
  185. package/src/compiler/ssg.ts +0 -177
  186. package/src/compiler/ssr-codegen.ts +0 -85
  187. package/src/compiler/template-build.ts +0 -676
  188. package/src/compiler/template.ts +0 -262
  189. package/src/compiler/toil-docs.generated.ts +0 -77
  190. package/src/compiler/vite.ts +0 -245
  191. package/src/devserver/analytics/index.ts +0 -317
  192. package/src/devserver/config/dotenv.ts +0 -102
  193. package/src/devserver/config/env.ts +0 -30
  194. package/src/devserver/config/ratelimit.ts +0 -123
  195. package/src/devserver/daemon/catalog.ts +0 -120
  196. package/src/devserver/daemon/cron.ts +0 -87
  197. package/src/devserver/daemon/host.ts +0 -134
  198. package/src/devserver/daemon/index.ts +0 -369
  199. package/src/devserver/daemon/runtime.ts +0 -48
  200. package/src/devserver/db/catalog.ts +0 -109
  201. package/src/devserver/db/database.ts +0 -1834
  202. package/src/devserver/db/derives.ts +0 -121
  203. package/src/devserver/db/index.ts +0 -20
  204. package/src/devserver/db/routeKinds.ts +0 -191
  205. package/src/devserver/db/types.ts +0 -168
  206. package/src/devserver/email/caps.ts +0 -0
  207. package/src/devserver/email/config.ts +0 -141
  208. package/src/devserver/email/console.ts +0 -348
  209. package/src/devserver/email/index.ts +0 -155
  210. package/src/devserver/email/providers.ts +0 -131
  211. package/src/devserver/email/status.ts +0 -23
  212. package/src/devserver/email/validate.ts +0 -37
  213. package/src/devserver/email/wire.ts +0 -55
  214. package/src/devserver/http/cache.ts +0 -0
  215. package/src/devserver/http/envelope.ts +0 -157
  216. package/src/devserver/http/proxy.ts +0 -172
  217. package/src/devserver/http/runtime.ts +0 -387
  218. package/src/devserver/index.ts +0 -43
  219. package/src/devserver/production-ipc.ts +0 -63
  220. package/src/devserver/production-worker.ts +0 -83
  221. package/src/devserver/production.ts +0 -718
  222. package/src/devserver/runtime/crypto.ts +0 -587
  223. package/src/devserver/runtime/host.ts +0 -427
  224. package/src/devserver/runtime/module.ts +0 -442
  225. package/src/devserver/server.ts +0 -251
  226. package/src/devserver/ssr.ts +0 -208
  227. package/src/devserver/stream/catalog.ts +0 -106
  228. package/src/devserver/stream/host.ts +0 -42
  229. package/src/devserver/stream/index.ts +0 -301
  230. package/src/devserver/stream/manager.ts +0 -167
  231. package/src/devserver/stream/router.ts +0 -101
  232. package/src/devserver/stream/wire.ts +0 -58
  233. package/src/devserver/stream/ws.ts +0 -78
  234. package/src/devserver/wasm/sections.ts +0 -59
  235. package/src/devserver/wasm/surface.ts +0 -115
  236. package/src/io/FastMap.ts +0 -151
  237. package/src/io/FastSet.ts +0 -113
  238. package/src/io/codec.ts +0 -362
  239. package/src/io/index.ts +0 -10
  240. package/src/io/lengths.ts +0 -14
  241. package/src/io/types.ts +0 -19
  242. package/src/logger/index.ts +0 -22
  243. package/src/shared/index.ts +0 -46
  244. package/test/analytics-dev.test.ts +0 -113
  245. package/test/assembly/aspect-shim.ts +0 -33
  246. package/test/assembly/cookie.spec.ts +0 -303
  247. package/test/assembly/example.spec.ts +0 -22
  248. package/test/assembly/ssr.spec.ts +0 -95
  249. package/test/assembly.test.ts +0 -59
  250. package/test/auth-emails.test.ts +0 -118
  251. package/test/built-ssr.test.ts +0 -148
  252. package/test/channel.test.ts +0 -21
  253. package/test/config-host-port.test.ts +0 -59
  254. package/test/configure.test.ts +0 -94
  255. package/test/daemon-build.test.ts +0 -278
  256. package/test/daemon-catalog.test.ts +0 -349
  257. package/test/daemon-emulation.test.ts +0 -256
  258. package/test/db.test.ts +0 -0
  259. package/test/devserver-database.test.ts +0 -1102
  260. package/test/devserver-email.test.ts +0 -306
  261. package/test/devserver-pqauth.test.ts +0 -93
  262. package/test/devserver-secrets.test.ts +0 -63
  263. package/test/devserver.test.ts +0 -259
  264. package/test/doctor.test.ts +0 -298
  265. package/test/dom/Image.test.tsx +0 -144
  266. package/test/dom/Link.test.tsx +0 -67
  267. package/test/dom/NavLink.test.tsx +0 -37
  268. package/test/dom/Script.test.tsx +0 -48
  269. package/test/dom/action.test.tsx +0 -146
  270. package/test/dom/error-overlay.test.tsx +0 -44
  271. package/test/dom/loader.test.tsx +0 -121
  272. package/test/dom/navigation.test.ts +0 -59
  273. package/test/dom/revalidate.test.tsx +0 -38
  274. package/test/dom/route-head.test.tsx +0 -56
  275. package/test/dom/router-loading.test.tsx +0 -44
  276. package/test/dom/scroll.test.ts +0 -56
  277. package/test/dom/slot.test.tsx +0 -131
  278. package/test/dom/use-metadata.test.tsx +0 -58
  279. package/test/dom/view-transitions.test.tsx +0 -53
  280. package/test/dynamic-routing.test.ts +0 -132
  281. package/test/email-console.test.ts +0 -86
  282. package/test/email-preview.test.ts +0 -73
  283. package/test/emails.test.ts +0 -58
  284. package/test/errors.test.ts +0 -21
  285. package/test/example-guestbook.test.ts +0 -123
  286. package/test/features.test.ts +0 -149
  287. package/test/fixtures/bignum-wire/spec.ts +0 -25
  288. package/test/fixtures/daemon-app.ts +0 -65
  289. package/test/fixtures/stream-echo.ts +0 -26
  290. package/test/fixtures/stream-gate.ts +0 -28
  291. package/test/fixtures/stream-trap.ts +0 -18
  292. package/test/fixtures/stream-typed.ts +0 -41
  293. package/test/fonts.test.ts +0 -28
  294. package/test/global-setup.ts +0 -17
  295. package/test/head.test.ts +0 -42
  296. package/test/image-blur.test.ts +0 -50
  297. package/test/io.test.ts +0 -117
  298. package/test/metadata.test.ts +0 -40
  299. package/test/navlink.test.ts +0 -28
  300. package/test/pages.test.ts +0 -105
  301. package/test/placeholder.test.ts +0 -9
  302. package/test/pqauth-e2e.test.ts +0 -216
  303. package/test/pqauth-email.test.ts +0 -801
  304. package/test/prerender.test.ts +0 -116
  305. package/test/prettier-plugin.test.ts +0 -46
  306. package/test/routes.test.ts +0 -76
  307. package/test/rpc-bignum-wire.test.ts +0 -164
  308. package/test/rpc-dispatch.test.ts +0 -132
  309. package/test/rpc-kinds.test.ts +0 -18
  310. package/test/rpc.test.ts +0 -66
  311. package/test/search.test.ts +0 -114
  312. package/test/seo.test.ts +0 -175
  313. package/test/slot-layouts.test.ts +0 -69
  314. package/test/sri.test.ts +0 -129
  315. package/test/ssg.test.ts +0 -36
  316. package/test/ssr-hydration.test.tsx +0 -122
  317. package/test/ssr-render.test.ts +0 -197
  318. package/test/ssr-template.test.tsx +0 -482
  319. package/test/stream-emulation.test.ts +0 -479
  320. package/test/ui.test.ts +0 -40
  321. package/test/update.test.ts +0 -58
  322. package/test/validate.test.ts +0 -42
  323. package/test/version-check.test.ts +0 -130
  324. package/tests/data-parity/generated-parity.ts +0 -99
  325. package/tests/data-parity/parity.ts +0 -80
  326. 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;