toiljs 0.0.59 → 0.0.61

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/.github/workflows/ci.yml +31 -0
  2. package/CHANGELOG.md +15 -0
  3. package/build/cli/.tsbuildinfo +1 -1
  4. package/build/cli/index.js +311 -118
  5. package/build/client/.tsbuildinfo +1 -1
  6. package/build/client/index.d.ts +1 -1
  7. package/build/client/index.js +1 -1
  8. package/build/client/routing/mount.js +12 -1
  9. package/build/client/ssr/markers.d.ts +1 -0
  10. package/build/client/ssr/markers.js +3 -0
  11. package/build/compiler/.tsbuildinfo +1 -1
  12. package/build/compiler/config.d.ts +21 -0
  13. package/build/compiler/config.js +35 -0
  14. package/build/compiler/docs.d.ts +2 -1
  15. package/build/compiler/docs.js +33 -304
  16. package/build/compiler/index.d.ts +13 -0
  17. package/build/compiler/index.js +113 -21
  18. package/build/compiler/template-build.d.ts +21 -1
  19. package/build/compiler/template-build.js +110 -26
  20. package/build/compiler/toil-docs.generated.d.ts +1 -0
  21. package/build/compiler/toil-docs.generated.js +20 -0
  22. package/build/devserver/.tsbuildinfo +1 -1
  23. package/build/devserver/daemon/catalog.d.ts +26 -0
  24. package/build/devserver/daemon/catalog.js +48 -0
  25. package/build/devserver/daemon/cron.d.ts +4 -0
  26. package/build/devserver/daemon/cron.js +50 -0
  27. package/build/devserver/daemon/host.d.ts +37 -0
  28. package/build/devserver/daemon/host.js +94 -0
  29. package/build/devserver/daemon/index.d.ts +34 -0
  30. package/build/devserver/daemon/index.js +241 -0
  31. package/build/devserver/db/catalog.d.ts +2 -0
  32. package/build/devserver/db/catalog.js +80 -0
  33. package/build/devserver/db/database.d.ts +80 -0
  34. package/build/devserver/db/database.js +1032 -0
  35. package/build/devserver/db/index.d.ts +3 -0
  36. package/build/devserver/db/index.js +3 -0
  37. package/build/devserver/db/routeKinds.d.ts +8 -0
  38. package/build/devserver/db/routeKinds.js +139 -0
  39. package/build/devserver/db/types.d.ts +121 -0
  40. package/build/devserver/db/types.js +52 -0
  41. package/build/devserver/email/index.js +1 -1
  42. package/build/devserver/index.d.ts +19 -24
  43. package/build/devserver/index.js +11 -165
  44. package/build/devserver/mstore/store.d.ts +18 -0
  45. package/build/devserver/mstore/store.js +82 -0
  46. package/build/devserver/{host.d.ts → runtime/host.d.ts} +7 -1
  47. package/build/devserver/{host.js → runtime/host.js} +51 -7
  48. package/build/devserver/{module.d.ts → runtime/module.d.ts} +2 -1
  49. package/build/devserver/{module.js → runtime/module.js} +34 -1
  50. package/build/devserver/server.d.ts +23 -0
  51. package/build/devserver/server.js +223 -0
  52. package/build/devserver/ssr.d.ts +25 -0
  53. package/build/devserver/ssr.js +114 -0
  54. package/build/devserver/wasm/sections.d.ts +2 -0
  55. package/build/devserver/wasm/sections.js +42 -0
  56. package/build/devserver/wasm/surface.d.ts +18 -0
  57. package/build/devserver/wasm/surface.js +41 -0
  58. package/docs/README.md +4 -4
  59. package/docs/auth-todo.md +6 -6
  60. package/docs/caching.md +5 -5
  61. package/docs/cli.md +15 -0
  62. package/docs/client.md +40 -0
  63. package/docs/crypto.md +4 -4
  64. package/docs/data.md +6 -6
  65. package/docs/email.md +28 -28
  66. package/docs/environment.md +10 -10
  67. package/docs/index.md +26 -0
  68. package/docs/ratelimit.md +10 -10
  69. package/docs/routing.md +2 -2
  70. package/docs/server.md +61 -0
  71. package/docs/ssr.md +561 -113
  72. package/docs/styling.md +22 -0
  73. package/docs/time.md +3 -3
  74. package/eslint.config.js +10 -1
  75. package/examples/basic/client/components/Header.tsx +3 -0
  76. package/examples/basic/client/routes/features/actions.tsx +0 -2
  77. package/examples/basic/client/routes/hello.tsx +89 -19
  78. package/examples/basic/client/styles/main.css +48 -0
  79. package/examples/basic/server/SsrHelloRender.ts +97 -0
  80. package/examples/basic/server/main.ts +5 -0
  81. package/examples/basic/server/migrations/GuestEntry.migration.ts +39 -0
  82. package/examples/basic/server/streams/Echo.ts +49 -0
  83. package/package.json +12 -10
  84. package/scripts/gen-toil-docs.mjs +96 -0
  85. package/server/runtime/time.ts +3 -3
  86. package/src/cli/create.ts +40 -3
  87. package/src/cli/db.ts +158 -0
  88. package/src/cli/diagnostics.ts +19 -0
  89. package/src/cli/doctor.ts +20 -0
  90. package/src/cli/index.ts +10 -0
  91. package/src/cli/update.ts +58 -0
  92. package/src/client/index.ts +1 -1
  93. package/src/client/routing/mount.tsx +18 -2
  94. package/src/client/ssr/markers.tsx +22 -0
  95. package/src/compiler/config.ts +88 -2
  96. package/src/compiler/docs.ts +47 -308
  97. package/src/compiler/index.ts +236 -32
  98. package/src/compiler/ssr-codegen.ts +1 -1
  99. package/src/compiler/template-build.ts +247 -46
  100. package/src/compiler/toil-docs.generated.ts +26 -0
  101. package/src/devserver/daemon/catalog.ts +120 -0
  102. package/src/devserver/daemon/cron.ts +87 -0
  103. package/src/devserver/daemon/host.ts +224 -0
  104. package/src/devserver/daemon/index.ts +349 -0
  105. package/src/devserver/db/catalog.ts +108 -0
  106. package/src/devserver/db/database.ts +1633 -0
  107. package/src/devserver/db/index.ts +18 -0
  108. package/src/devserver/db/routeKinds.ts +147 -0
  109. package/src/devserver/db/types.ts +139 -0
  110. package/src/devserver/email/index.ts +1 -1
  111. package/src/devserver/index.ts +31 -287
  112. package/src/devserver/mstore/store.ts +121 -0
  113. package/src/devserver/{host.ts → runtime/host.ts} +98 -7
  114. package/src/devserver/{module.ts → runtime/module.ts} +47 -1
  115. package/src/devserver/server.ts +393 -0
  116. package/src/devserver/ssr.ts +166 -0
  117. package/src/devserver/wasm/sections.ts +59 -0
  118. package/src/devserver/wasm/surface.ts +88 -0
  119. package/test/daemon-build.test.ts +198 -0
  120. package/test/daemon-catalog.test.ts +265 -0
  121. package/test/daemon-emulation.test.ts +216 -0
  122. package/test/db.test.ts +0 -0
  123. package/test/devserver-database.test.ts +510 -14
  124. package/test/devserver-pqauth.test.ts +1 -1
  125. package/test/devserver-secrets.test.ts +5 -1
  126. package/test/doctor.test.ts +13 -0
  127. package/test/email-preview.test.ts +6 -1
  128. package/test/example-guestbook.test.ts +43 -1
  129. package/test/fixtures/daemon-app.ts +56 -0
  130. package/test/global-setup.ts +17 -0
  131. package/test/pqauth-e2e.test.ts +1 -1
  132. package/test/ssr-render.test.ts +94 -27
  133. package/test/ssr-template.test.tsx +44 -1
  134. package/vitest.config.ts +3 -0
  135. package/build/devserver/database.d.ts +0 -8
  136. package/build/devserver/database.js +0 -418
  137. package/src/devserver/database.ts +0 -618
  138. /package/build/devserver/{dotenv.d.ts → config/dotenv.d.ts} +0 -0
  139. /package/build/devserver/{dotenv.js → config/dotenv.js} +0 -0
  140. /package/build/devserver/{env.d.ts → config/env.d.ts} +0 -0
  141. /package/build/devserver/{env.js → config/env.js} +0 -0
  142. /package/build/devserver/{ratelimit.d.ts → config/ratelimit.d.ts} +0 -0
  143. /package/build/devserver/{ratelimit.js → config/ratelimit.js} +0 -0
  144. /package/build/devserver/{cache.d.ts → http/cache.d.ts} +0 -0
  145. /package/build/devserver/{cache.js → http/cache.js} +0 -0
  146. /package/build/devserver/{envelope.d.ts → http/envelope.d.ts} +0 -0
  147. /package/build/devserver/{envelope.js → http/envelope.js} +0 -0
  148. /package/build/devserver/{proxy.d.ts → http/proxy.d.ts} +0 -0
  149. /package/build/devserver/{proxy.js → http/proxy.js} +0 -0
  150. /package/build/devserver/{crypto.d.ts → runtime/crypto.d.ts} +0 -0
  151. /package/build/devserver/{crypto.js → runtime/crypto.js} +0 -0
  152. /package/src/devserver/{dotenv.ts → config/dotenv.ts} +0 -0
  153. /package/src/devserver/{env.ts → config/env.ts} +0 -0
  154. /package/src/devserver/{ratelimit.ts → config/ratelimit.ts} +0 -0
  155. /package/src/devserver/{cache.ts → http/cache.ts} +0 -0
  156. /package/src/devserver/{envelope.ts → http/envelope.ts} +0 -0
  157. /package/src/devserver/{proxy.ts → http/proxy.ts} +0 -0
  158. /package/src/devserver/{crypto.ts → runtime/crypto.ts} +0 -0
@@ -0,0 +1,22 @@
1
+ # Styling
2
+
3
+ The app imports one stylesheet from `client/toil.tsx` (e.g. `./styles/main.css`).
4
+
5
+ ## Preprocessors & Tailwind
6
+
7
+ Pick a CSS preprocessor (none / Sass / Less / Stylus) and optionally Tailwind at
8
+ `toiljs create`, or change it later on an existing project:
9
+
10
+ ```sh
11
+ toiljs configure # interactive
12
+ toiljs configure --tailwind # add Tailwind
13
+ toiljs configure --style sass # switch preprocessor
14
+ ```
15
+
16
+ `configure` installs/removes the right packages and rewrites the imports. Tailwind lives
17
+ in its own `styles/tailwind.css` (`@import "tailwindcss";`).
18
+
19
+ ## Imports
20
+
21
+ `.css` / `.scss` / `.sass` / `.less` / `.styl` and image imports (`.svg`, `.png`, …) are
22
+ typed via `toil-env.d.ts`.
package/docs/time.md CHANGED
@@ -11,7 +11,7 @@ from `toiljs/server/runtime`.
11
11
  ```ts
12
12
  import { Time } from 'toiljs/server/runtime'; // optional; Time is also a global
13
13
 
14
- const ms = Time.nowMillis(); // i64 milliseconds since the Unix epoch
14
+ const ms = Time.nowMillis(); // u64 milliseconds since the Unix epoch
15
15
  const s = Time.nowSeconds(); // u64 whole seconds since the Unix epoch
16
16
  ```
17
17
 
@@ -19,12 +19,12 @@ const s = Time.nowSeconds(); // u64 whole seconds since the Unix epoch
19
19
 
20
20
  | Member | Signature | Description |
21
21
  | --- | --- | --- |
22
- | `Time.nowMillis()` | `static nowMillis(): i64` | Milliseconds since the Unix epoch (the raw host `Date.now()` value). |
22
+ | `Time.nowMillis()` | `static nowMillis(): u64` | Milliseconds since the Unix epoch (the raw host `Date.now()` value). |
23
23
  | `Time.nowSeconds()` | `static nowSeconds(): u64` | Whole seconds since the epoch (`nowMillis() / 1000`). The unit used by sessions and login challenges. |
24
24
 
25
25
  ## Semantics
26
26
 
27
- `Time` is **wall-clock, not monotonic** exactly like browser `Date.now()`. It
27
+ `Time` is **wall-clock, not monotonic**, exactly like browser `Date.now()`. It
28
28
  tracks the system clock and can step backward across an NTP correction.
29
29
 
30
30
  - Use it to stamp and compare absolute instants: session `iat`/`exp`, login
package/eslint.config.js CHANGED
@@ -5,7 +5,16 @@ import tseslint from 'typescript-eslint';
5
5
 
6
6
  export default tseslint.config(
7
7
  // toilscript server (WASM) + its ambient std + build output are not part of the TS project.
8
- { ignores: ['build/**', 'server/**', 'std/server/**', 'toil-env.d.ts'] },
8
+ // toil-docs.generated.ts is a build artifact (gitignored, emitted by gen:docs) — not linted.
9
+ {
10
+ ignores: [
11
+ 'build/**',
12
+ 'server/**',
13
+ 'std/server/**',
14
+ 'toil-env.d.ts',
15
+ 'src/compiler/toil-docs.generated.ts',
16
+ ],
17
+ },
9
18
  eslint.configs.recommended,
10
19
  ...tseslint.configs.strictTypeChecked,
11
20
  {
@@ -22,6 +22,9 @@ export default function Header() {
22
22
  <Toil.NavLink href="/features" className="nav-center-link">
23
23
  Features
24
24
  </Toil.NavLink>
25
+ <Toil.NavLink href="/hello" className="nav-center-link">
26
+ Edge SSR
27
+ </Toil.NavLink>
25
28
  <Toil.NavLink href="/search" className="nav-center-link">
26
29
  Search
27
30
  </Toil.NavLink>
@@ -1,5 +1,3 @@
1
- // Mutations: a `loader` reads, an action writes, then revalidation refetches the loader so the UI
2
- // reflects the new state with no manual refetch. Here a module-level counter stands in for a server.
3
1
  let serverCount = 0;
4
2
  async function wait(ms: number): Promise<void> {
5
3
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -1,43 +1,113 @@
1
1
  /**
2
- * An edge-SSR route. `export const ssr = true` opts it into the template
3
- * extractor: at build time toil renders it once into a template-with-holes
4
- * (`_ssr/hello.{tmpl,slots}` + a guest `Slot` module); at request time the edge
5
- * splices the guest's hole values into that template (no per-request render).
2
+ * An edge-SSR route, server-rendered at the edge with no per-request React.
6
3
  *
7
- * SSR routes must render under static markup: use the hole markers (`Hole`,
8
- * `Repeat`, `RawHtml`) and `useLoaderData`, and keep router-hook-dependent or
9
- * client-only bits inside an `<Island>`.
4
+ * `export const ssr = true` opts it into the template extractor: at build time
5
+ * toil renders this page (under the real layout chain) ONCE into a
6
+ * template-with-holes (`build/client/_ssr/hello.{tmpl,slots}` + the guest
7
+ * `Slot` module), and the matching server `render` (see
8
+ * `server/SsrHelloRender.ts`) fills only the holes per request. The edge
9
+ * splices the values into the template, so this page is served about as fast as
10
+ * a static file while still delivering real first-paint HTML and SEO, and the
11
+ * browser hydrates it in place.
12
+ *
13
+ * The dynamic bits are wrapped in the hole markers from `toiljs/client`, which
14
+ * are transparent in the browser (they just render their children) but are what
15
+ * the build extractor and the server `render` key off:
16
+ *
17
+ * - `<Hole>` a text hole (React-escaped for you)
18
+ * - `<RawHtml>` a raw-HTML block (you own sanitisation)
19
+ * - `<Repeat>` a repeated region, the row markup stamped per item
20
+ * - `<Island>` a client-only escape hatch (empty server-side; appears after
21
+ * hydration). Router-hook / browser-only bits live here so the
22
+ * page renders under static markup.
10
23
  */
11
- import { Hole, Repeat, useLoaderData } from 'toiljs/client';
24
+ import { Hole, Island, RawHtml, Repeat, useLoaderData } from 'toiljs/client';
12
25
 
13
26
  export const ssr = true;
14
27
 
15
- interface HelloData {
28
+ export const metadata: Toil.Metadata = {
29
+ title: 'Edge SSR',
30
+ description: 'A server-rendered greeting, filled at the edge from a tiny values envelope.',
31
+ openGraph: { title: 'Edge SSR, ToilJS', type: 'website' }
32
+ };
33
+
34
+ interface Service {
35
+ name: string;
36
+ region: string;
37
+ }
38
+
39
+ interface GreetingData {
40
+ /** Who we are greeting (a text hole). */
16
41
  name: string;
17
- items: string[];
42
+ /** A short, pre-sanitised HTML blurb (a raw-HTML hole). */
43
+ blurbHtml: string;
44
+ /** A live status snapshot, stamped row by row (a repeat region). */
45
+ services: Service[];
18
46
  }
19
47
 
20
- export const loader = ({ params }: { params: Record<string, string> }): HelloData => ({
21
- name: params.name ?? 'world',
22
- items: ['alpha', 'beta', 'gamma']
48
+ /**
49
+ * This loader plays two roles:
50
+ *
51
+ * 1. BUILD: it is rendered once (with empty search params) so the extractor
52
+ * captures the holes; only the SHAPE matters there.
53
+ * 2. CLIENT HYDRATION: after the edge serves the server-rendered HTML, the
54
+ * browser hydrates by re-rendering this component with THIS loader's data.
55
+ * For a byte-clean hydrate, that data must reproduce the values the SERVER
56
+ * `render` stamped (see `server/SsrHelloRender.ts`). So the greeting reads
57
+ * `?name=` here exactly as the server `render` does; if it didn't,
58
+ * `/hello?Bob` would hydrate the server's "Bob" back to the loader default
59
+ * and flash. (Holes that the client cannot reproduce belong in an
60
+ * `<Island>`.)
61
+ */
62
+ export const loader = ({ searchParams }: { searchParams: URLSearchParams }): GreetingData => ({
63
+ name: searchParams.get('name') || 'world',
64
+ blurbHtml: 'Rendered at the <strong>edge</strong> from a tiny values envelope.',
65
+ services: [
66
+ { name: 'record', region: 'us-east' },
67
+ { name: 'unique', region: 'eu-west' },
68
+ { name: 'counter', region: 'ap-south' }
69
+ ]
23
70
  });
24
71
 
25
72
  export default function Hello(): React.JSX.Element {
26
73
  const d = useLoaderData<typeof loader>();
27
74
  return (
28
- <section>
75
+ <section className="hello">
29
76
  <h1>
30
- Hello <Hole id="name">{d.name}</Hole>
77
+ Hello, <Hole id="name">{d.name}</Hole>!
31
78
  </h1>
32
- <ul>
33
- <Repeat id="items" each={d.items}>
34
- {(s: string) => (
79
+
80
+ {/* A raw-HTML hole: the server is responsible for sanitising it. */}
81
+ <p className="hello-blurb">
82
+ <RawHtml id="blurb" html={d.blurbHtml} as="span" />
83
+ </p>
84
+
85
+ {/* A repeat region: the row markup is captured once and stamped per
86
+ item. The nested <Hole>s are filled inside each stamped row. */}
87
+ <h2>Service snapshot</h2>
88
+ <ul className="hello-services">
89
+ <Repeat id="services" each={d.services}>
90
+ {(s: Service) => (
35
91
  <li>
36
- <Hole id="item">{s}</Hole>
92
+ <strong>
93
+ <Hole id="svcName">{s.name}</Hole>
94
+ </strong>
95
+ <span className="hello-region">
96
+ <Hole id="svcRegion">{s.region}</Hole>
97
+ </span>
37
98
  </li>
38
99
  )}
39
100
  </Repeat>
40
101
  </ul>
102
+
103
+ {/* A client-only island: empty in the server HTML, rendered after
104
+ hydration. Anything router-hook / browser-only lives here so the
105
+ page above stays server-renderable. */}
106
+ <Island>
107
+ <p className="hello-island">
108
+ Hydrated in your browser at {new Date().toLocaleTimeString()}.
109
+ </p>
110
+ </Island>
41
111
  </section>
42
112
  );
43
113
  }
@@ -625,3 +625,51 @@ code {
625
625
  pointer-events: none;
626
626
  z-index: 0;
627
627
  }
628
+
629
+ /* ── Edge SSR demo (/hello) ── */
630
+ .hello {
631
+ max-width: 640px;
632
+ margin: 0 auto;
633
+ padding: 2rem 1rem;
634
+ }
635
+
636
+ .hello h1 {
637
+ background: linear-gradient(90deg, var(--accent), var(--accent3));
638
+ -webkit-background-clip: text;
639
+ background-clip: text;
640
+ color: transparent;
641
+ }
642
+
643
+ .hello-blurb {
644
+ color: var(--muted);
645
+ }
646
+
647
+ .hello-services {
648
+ list-style: none;
649
+ margin: 0;
650
+ padding: 0;
651
+ display: grid;
652
+ gap: 0.5rem;
653
+ }
654
+
655
+ .hello-services li {
656
+ display: flex;
657
+ align-items: center;
658
+ justify-content: space-between;
659
+ padding: 0.6rem 0.9rem;
660
+ background: var(--surface);
661
+ border: 1px solid var(--border);
662
+ border-radius: 8px;
663
+ }
664
+
665
+ .hello-region {
666
+ color: var(--accent3);
667
+ font-family: ui-monospace, monospace;
668
+ font-size: 0.85rem;
669
+ }
670
+
671
+ .hello-island {
672
+ margin-top: 1.5rem;
673
+ color: var(--muted);
674
+ font-size: 0.85rem;
675
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * The edge-SSR `render` for the `/hello` route (`client/routes/hello.tsx`).
3
+ *
4
+ * In the single-wasm build, SSR is part of the normal server build: the
5
+ * compiler renders the opted-in route into a template-with-holes and emits the
6
+ * typed `Slot` enum + `HASH` as an AUTO-GENERATED module at `server/_ssr/hello.slots.ts`
7
+ * (gitignored, regenerated every build — never hand-edited); this function
8
+ * fills only the holes per request, and the edge splices the values into the
9
+ * template. The host never re-runs React — it just stamps the values envelope
10
+ * this returns into the precompiled template, so the route serves about as fast
11
+ * as a static file.
12
+ *
13
+ * It derives its data from the request, fills the typed `Slot`s on a
14
+ * `SlotValues`, and self-registers with the `Ssr` router. `main.ts` imports
15
+ * this module so the server build discovers it (the side-effect `Ssr.register`
16
+ * call is what wires it in). A render returns `SlotValues` for a path it owns,
17
+ * or `null` to let the next registered renderer try.
18
+ *
19
+ * Hole escaping mirrors React exactly (`setText`/`HtmlBuilder.text` React-escape
20
+ * for you; `setRaw` is verbatim, so YOU own its sanitisation), which is what
21
+ * lets the browser hydrate the spliced markup with no re-render.
22
+ */
23
+ import { HtmlBuilder, Request, SlotValues, Ssr } from 'toiljs/server/runtime';
24
+ // AUTO-GENERATED by the build (the SSR slots pre-pass renders the route's holes
25
+ // into the `Slot` enum + coherence `HASH`). Lives in `server/_ssr/` next to the
26
+ // generated `_emails.ts`, gitignored and regenerated every build — never edited.
27
+ import { HASH, Slot } from './_ssr/hello.slots';
28
+
29
+ class Service {
30
+ constructor(
31
+ public name: string,
32
+ public region: string,
33
+ ) {}
34
+ }
35
+
36
+ /** Pull the greeting target from `?name=...`, defaulting to `world` (matches the
37
+ * route loader's default). Kept tiny — the point is to show a real per-request
38
+ * derivation, not a query parser. */
39
+ function greetingName(req: Request): string {
40
+ const q = req.path.indexOf('?');
41
+ if (q < 0) return 'world';
42
+ const query = req.path.substring(q + 1);
43
+ const parts = query.split('&');
44
+ for (let i = 0; i < parts.length; i++) {
45
+ const kv = parts[i];
46
+ if (kv.startsWith('name=')) {
47
+ const v = kv.substring(5);
48
+ return v.length > 0 ? v : 'world';
49
+ }
50
+ }
51
+ return 'world';
52
+ }
53
+
54
+ function renderHello(req: Request): SlotValues | null {
55
+ // The guest re-derives WHICH route this is from the path (the template name
56
+ // is not in the request envelope), exactly as a @rest controller matches its
57
+ // own prefix. Match `/hello` with or without a query string.
58
+ if (req.path != '/hello' && !req.path.startsWith('/hello?')) return null;
59
+
60
+ const v = new SlotValues(HASH);
61
+
62
+ // A text hole: React-escaped (so e.g. `?name=<a>&b` is safe).
63
+ v.setText(Slot.name, greetingName(req));
64
+
65
+ // A raw-HTML hole: inserted verbatim, so the author owns sanitisation. This
66
+ // is a fixed, trusted blurb (no request data) — matches the route's sample.
67
+ v.setRaw(Slot.blurb, 'Rendered at the <strong>edge</strong> from a tiny values envelope.');
68
+
69
+ // A repeat region: stamp the captured row markup once per item. The row
70
+ // sub-template is `<li><strong>{svcName}</strong>` +
71
+ // `<span class="hello-region">{svcRegion}</span></li>`; `text(...)` escapes
72
+ // each nested hole exactly as React does, so the stamped rows are byte-
73
+ // identical to a client render.
74
+ const services: Service[] = [
75
+ new Service('record', 'us-east'),
76
+ new Service('unique', 'eu-west'),
77
+ new Service('counter', 'ap-south'),
78
+ ];
79
+ const rows = new HtmlBuilder();
80
+ for (let i = 0; i < services.length; i++) {
81
+ const s = services[i];
82
+ rows.raw('<li><strong>')
83
+ .text(s.name)
84
+ .raw('</strong><span class="hello-region">')
85
+ .text(s.region)
86
+ .raw('</span></li>');
87
+ }
88
+ v.setRepeat(Slot.services, rows);
89
+
90
+ return v;
91
+ }
92
+
93
+ // Side-effect registration: `main.ts` imports this module so the build compiles
94
+ // it in and this renderer joins the SSR router. (In a fully-generated build the
95
+ // compiler injects this registration for auto-discovered routes; the demo wires
96
+ // it explicitly, the documented escape hatch.)
97
+ Ssr.register(renderHello);
@@ -15,6 +15,11 @@ import './routes/EnvDemo';
15
15
  import './services/Stats';
16
16
  import './services/remotes';
17
17
 
18
+ // Edge SSR: importing the render module compiles it in and self-registers its
19
+ // `/hello` renderer with the `Ssr` router (the route opts in via `export const
20
+ // ssr = true` in client/routes/hello.tsx). See server/SsrHelloRender.ts.
21
+ import './SsrHelloRender';
22
+
18
23
  // DO NOT TOUCH THIS.
19
24
  Server.handler = () => {
20
25
  // ONLY CHANGE THE HANDLER CLASS NAME.
@@ -0,0 +1,39 @@
1
+ /**
2
+ * A ToilDB data MIGRATION for the guestbook's `GuestEntry` (see ../models/GuestEntry).
3
+ *
4
+ * THE CONVENTION: every `@migrate` lives in a `*.migration.ts` file under a
5
+ * `migrations/` folder (enforced at compile time). The build auto-discovers this
6
+ * file - nothing imports it - and weaves its transform into `GuestEntry`'s decoder.
7
+ *
8
+ * THE STORY: `GuestEntry` started life with just `author` + `message`. Later we
9
+ * added an `at` timestamp. Without a migration, every entry already written would
10
+ * fail to decode (its bytes have no `at`). With this file, an OLD entry is decoded
11
+ * as its original shape and upgraded on READ - lazily, per row, only when touched.
12
+ * No backfill, no downtime: rows written under the old layout keep working, and a
13
+ * read rewrites the converged value back so it is paid for at most once.
14
+ *
15
+ * Try it under `toiljs dev`: sign the guestbook, then add a field to `GuestEntry`
16
+ * + extend this transform + rebuild. The entries already on disk (in `.toil/`)
17
+ * surface their OLD schema_version, so this `@migrate` runs when you `list()` them.
18
+ */
19
+
20
+ import { GuestEntry } from '../models/GuestEntry';
21
+
22
+ /** The ORIGINAL `GuestEntry` layout (v1): no `at` timestamp. Kept so entries on
23
+ * disk written under it still decode. One kept shape per past layout. */
24
+ @data
25
+ export class GuestEntryV1 {
26
+ author: string = '';
27
+ message: string = '';
28
+ }
29
+
30
+ /**
31
+ * Upgrade a v1 entry to the current `GuestEntry`. The DELTA form `(old, into)`
32
+ * auto-copies the fields the two layouts SHARE (`author`, `message`); we only fill
33
+ * the field that is new. A migration is a PURE transform - it may not touch the
34
+ * database (that is a compile error).
35
+ */
36
+ @migrate
37
+ export function up(old: GuestEntryV1, into: GuestEntry): void {
38
+ into.at = 0; // unknown for pre-timestamp entries
39
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * A `@stream` protocol handler mounted at `/echo`, running as a RESIDENT wasm box
3
+ * per WebTransport connection on the Toil edge - distributed across the eligible
4
+ * L2/L3 nodes and pinned to ONE worker for the connection's lifetime via QUIC
5
+ * connection-id steering.
6
+ *
7
+ * The defining property of a `@stream` (vs a `@rest` handler): the box is
8
+ * RESIDENT, so instance state PERSISTS across events on the same connection. Here
9
+ * `count` survives every `@message` because the box is never reset between events
10
+ * - unlike a `@rest` handler, which is fresh per request. On the client:
11
+ *
12
+ * const stream = await Server.STREAM.echo.connect();
13
+ * stream.send(new TextEncoder().encode('hi'));
14
+ *
15
+ * Lifecycle hooks: `@connect` (open), `@message` (an inbound frame), `@close`
16
+ * (graceful close), `@disconnect` (abrupt transport loss).
17
+ *
18
+ * NOTE: reading the inbound frame and replying is the NEXT increment (the
19
+ * `StreamPacket` / `StreamOutbound` message bridge). The intended shape is:
20
+ *
21
+ * @message reply(packet: StreamPacket): StreamOutbound {
22
+ * return StreamOutbound.reply(packet.bytes()); // echo the bytes back
23
+ * }
24
+ *
25
+ * Until that lands, the hooks run on the connection lifecycle; this example counts
26
+ * frames to demonstrate that the resident box keeps state across them.
27
+ */
28
+ @stream('echo')
29
+ class Echo {
30
+ // Resident per-connection state: survives across events (ResetMode::None).
31
+ private count: i32 = 0;
32
+
33
+ @connect
34
+ onConnect(): void {
35
+ // A fresh connection: its dedicated box starts the counter at 0.
36
+ this.count = 0;
37
+ }
38
+
39
+ @message
40
+ onMessage(): void {
41
+ // Persists across frames because the box is resident, not reset per event.
42
+ this.count = this.count + 1;
43
+ }
44
+
45
+ @close
46
+ onClose(): void {
47
+ // Graceful close: the per-connection box is torn down after this hook.
48
+ }
49
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "toiljs",
3
3
  "type": "module",
4
- "version": "0.0.59",
4
+ "version": "0.0.61",
5
5
  "author": "Dacely",
6
6
  "description": "The modern React framework: a file-based React frontend and a ToilScript-compiled WebAssembly backend.",
7
7
  "repository": {
@@ -95,6 +95,8 @@
95
95
  },
96
96
  "scripts": {
97
97
  "watch": "tsc -p tsconfig.json --watch",
98
+ "prepublishOnly": "npm run build",
99
+ "gen:docs": "node scripts/gen-toil-docs.mjs",
98
100
  "build": "npm run build:shared && npm run build:logger && npm run build:io && npm run build:client && npm run build:backend && npm run build:devserver && npm run build:compiler && npm run build:cli",
99
101
  "build:shared": "tsc -p tsconfig.shared.json",
100
102
  "build:logger": "tsc -p tsconfig.logger.json",
@@ -102,7 +104,7 @@
102
104
  "build:io": "tsc -p tsconfig.io.json",
103
105
  "build:backend": "tsc -p tsconfig.backend.json",
104
106
  "build:devserver": "tsc -p tsconfig.devserver.json",
105
- "build:compiler": "tsc -p tsconfig.compiler.json",
107
+ "build:compiler": "npm run gen:docs && tsc -p tsconfig.compiler.json",
106
108
  "build:cli": "tsc -p tsconfig.cli.json --noEmit && esbuild src/cli/index.ts --bundle --platform=node --format=esm --outfile=build/cli/index.js --external:toiljs/*",
107
109
  "test": "vitest run --coverage",
108
110
  "test:watch": "vitest",
@@ -111,6 +113,7 @@
111
113
  "test:server": "asp --config as-pect.config.js --verbose --no-logo",
112
114
  "test:server:ci": "asp --config as-pect.config.js --summary --no-logo",
113
115
  "test:all": "npm run test && npm run test:server",
116
+ "prelint": "npm run gen:docs",
114
117
  "lint": "eslint src",
115
118
  "docs": "typedoc --out docs --tsconfig tsconfig.json --readme README.md --name TOILJS --plugin typedoc-material-theme --themeColor '#cb9820' src",
116
119
  "setup": "npm i && npm run build"
@@ -118,12 +121,12 @@
118
121
  "dependencies": {
119
122
  "@dacely/hyper-express": "6.17.4",
120
123
  "@dacely/noble-post-quantum": "^0.6.1",
121
- "@noble/curves": "^2.2.0",
122
124
  "@dacely/toilscript-loader": "^0.1.0",
123
- "@eslint-react/eslint-plugin": "^5.9.1",
125
+ "@eslint-react/eslint-plugin": "^5.9.2",
124
126
  "@eslint/js": "^10.0.1",
125
- "@typescript-eslint/utils": "^8.61.1",
126
- "@vitejs/plugin-react": "^6.0.2",
127
+ "@noble/curves": "^2.2.0",
128
+ "@typescript-eslint/utils": "^8.62.0",
129
+ "@vitejs/plugin-react": "^6.0.3",
127
130
  "eslint-plugin-react-hooks": "^7.1.1",
128
131
  "eslint-plugin-react-refresh": "^0.5.3",
129
132
  "hash-wasm": "^4.12.0",
@@ -131,9 +134,9 @@
131
134
  "nodemailer": "^9.0.1",
132
135
  "picocolors": "^1.1.1",
133
136
  "sharp": "^0.35.2",
134
- "toilscript": "^0.1.36",
135
- "typescript-eslint": "^8.61.1",
136
- "vite": "^8.0.16",
137
+ "toilscript": "^0.1.39",
138
+ "typescript-eslint": "^8.62.0",
139
+ "vite": "^8.1.0",
137
140
  "vite-imagetools": "^10.0.1",
138
141
  "vite-plugin-node-polyfills": "^0.28.0"
139
142
  },
@@ -170,7 +173,6 @@
170
173
  "eslint": "^10.5.0",
171
174
  "jsdom": "^29.1.1",
172
175
  "micromatch": "^4.0.8",
173
- "playwright": "^1.61.0",
174
176
  "prettier": "^3.8.4",
175
177
  "react": "^19.2.7",
176
178
  "react-dom": "^19.2.7",
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Build-time generator: turns the human Markdown guides in `docs/` into the agent
4
+ * documentation map (`TOIL_DOCS`) consumed by the compiler.
5
+ *
6
+ * `docs/*.md` is the SINGLE SOURCE for both the human docs (the site) and the agent
7
+ * docs written into a project's `.toil/docs/` on `toiljs dev` / `build` / `create`.
8
+ * This script reads every published guide, builds `Record<filename, content>`, and
9
+ * writes a generated TS module that `src/compiler/docs.ts` imports. The generated
10
+ * file is gitignored and (re)created before `build:compiler`, so the shipped
11
+ * compiler never reads the filesystem at a user's build time, and the npm package
12
+ * does not need to carry `docs/`.
13
+ *
14
+ * Run: `node scripts/gen-toil-docs.mjs` (wired into `npm run build`).
15
+ */
16
+ import fs from 'node:fs';
17
+ import path from 'node:path';
18
+ import { fileURLToPath } from 'node:url';
19
+
20
+ const here = path.dirname(fileURLToPath(import.meta.url));
21
+ const repoRoot = path.resolve(here, '..');
22
+ const docsDir = path.join(repoRoot, 'docs');
23
+ const outFile = path.join(repoRoot, 'src', 'compiler', 'toil-docs.generated.ts');
24
+
25
+ /**
26
+ * Guides excluded from the AGENT doc set (still published as human docs):
27
+ * - `auth-todo.md`: an internal tracking/roadmap doc, not project guidance.
28
+ * - `README.md`: the human-site docs index; the agent gets `index.md` instead.
29
+ */
30
+ const EXCLUDE = new Set(['auth-todo.md', 'README.md']);
31
+
32
+ /**
33
+ * Stable order for the agent docs. Files not listed here are appended
34
+ * alphabetically, so adding a new `docs/*.md` is picked up automatically.
35
+ */
36
+ const ORDER = [
37
+ 'index.md',
38
+ 'getting-started.md',
39
+ 'routing.md',
40
+ 'client.md',
41
+ 'styling.md',
42
+ 'server.md',
43
+ 'ssr.md',
44
+ 'rpc.md',
45
+ 'data.md',
46
+ 'caching.md',
47
+ 'ratelimit.md',
48
+ 'auth.md',
49
+ 'environment.md',
50
+ 'email.md',
51
+ 'cookies.md',
52
+ 'crypto.md',
53
+ 'time.md',
54
+ 'cli.md',
55
+ ];
56
+
57
+ function collect() {
58
+ const all = fs
59
+ .readdirSync(docsDir)
60
+ .filter((f) => f.endsWith('.md') && !EXCLUDE.has(f))
61
+ .sort();
62
+ const ordered = [
63
+ ...ORDER.filter((f) => all.includes(f)),
64
+ ...all.filter((f) => !ORDER.includes(f)),
65
+ ];
66
+ const out = {};
67
+ for (const name of ordered) {
68
+ out[name] = fs.readFileSync(path.join(docsDir, name), 'utf8');
69
+ }
70
+ return out;
71
+ }
72
+
73
+ function emit(map) {
74
+ const entries = Object.entries(map)
75
+ .map(([name, content]) => ` ${JSON.stringify(name)}: ${JSON.stringify(content)},`)
76
+ .join('\n');
77
+ return (
78
+ '/* eslint-disable */\n' +
79
+ '// AUTO-GENERATED by scripts/gen-toil-docs.mjs from docs/*.md, do not edit.\n' +
80
+ '// `docs/*.md` is the single source for both the human docs and these agent docs.\n' +
81
+ '// Regenerate with `node scripts/gen-toil-docs.mjs` (runs before `build:compiler`).\n' +
82
+ '\n' +
83
+ '/** The framework guides written into `.toil/docs/`, keyed by filename, generated from `docs/`. */\n' +
84
+ 'export const TOIL_DOCS: Record<string, string> = {\n' +
85
+ entries +
86
+ '\n};\n'
87
+ );
88
+ }
89
+
90
+ const map = collect();
91
+ fs.mkdirSync(path.dirname(outFile), { recursive: true });
92
+ fs.writeFileSync(outFile, emit(map));
93
+ console.log(
94
+ `gen-toil-docs: wrote ${Object.keys(map).length} doc(s) to ` +
95
+ `${path.relative(repoRoot, outFile)}`,
96
+ );
@@ -17,13 +17,13 @@
17
17
  @global
18
18
  export class Time {
19
19
  /** Milliseconds since the Unix epoch (the host `Date.now()` value). */
20
- static nowMillis(): i64 {
21
- return <i64>Date.now();
20
+ static nowMillis(): u64 {
21
+ return Date.now();
22
22
  }
23
23
 
24
24
  /** Whole seconds since the Unix epoch (`nowMillis() / 1000`), the unit used
25
25
  * for session and challenge timestamps. */
26
26
  static nowSeconds(): u64 {
27
- return <u64>(Date.now() / 1000);
27
+ return Date.now() / 1000;
28
28
  }
29
29
  }