toiljs 0.0.85 → 0.0.87

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 (132) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/README.md +2 -2
  3. package/build/cli/.tsbuildinfo +1 -1
  4. package/build/cli/index.js +303 -293
  5. package/build/compiler/.tsbuildinfo +1 -1
  6. package/build/compiler/config.d.ts +2 -0
  7. package/build/compiler/config.js +1 -0
  8. package/build/compiler/docs.js +8 -24
  9. package/build/compiler/generate.js +4 -2
  10. package/build/compiler/index.d.ts +1 -1
  11. package/build/compiler/index.js +69 -6
  12. package/build/compiler/toil-docs.generated.js +64 -22
  13. package/build/devserver/.tsbuildinfo +1 -1
  14. package/build/devserver/analytics/index.js +7 -3
  15. package/build/devserver/db/database.d.ts +4 -0
  16. package/build/devserver/db/database.js +43 -1
  17. package/build/devserver/db/index.d.ts +1 -1
  18. package/build/devserver/db/index.js +1 -1
  19. package/build/devserver/db/types.d.ts +3 -0
  20. package/build/devserver/db/types.js +2 -0
  21. package/build/devserver/runtime/module.js +4 -1
  22. package/docs/README.md +104 -65
  23. package/docs/auth/README.md +102 -0
  24. package/docs/auth/configuration.md +94 -0
  25. package/docs/auth/extending.md +202 -0
  26. package/docs/auth/how-it-works.md +138 -0
  27. package/docs/auth/usage.md +188 -0
  28. package/docs/backend/README.md +143 -0
  29. package/docs/backend/data.md +351 -0
  30. package/docs/backend/rest.md +402 -0
  31. package/docs/backend/rpc.md +226 -0
  32. package/docs/background/README.md +114 -0
  33. package/docs/background/daemons.md +230 -0
  34. package/docs/background/derive.md +179 -0
  35. package/docs/cli/README.md +377 -0
  36. package/docs/concepts/config.md +416 -0
  37. package/docs/concepts/decorators.md +127 -0
  38. package/docs/concepts/security.md +108 -0
  39. package/docs/concepts/tiers.md +166 -0
  40. package/docs/concepts/types.md +216 -0
  41. package/docs/database/README.md +143 -0
  42. package/docs/database/capacity.md +350 -0
  43. package/docs/database/counters.md +174 -0
  44. package/docs/database/documents.md +255 -0
  45. package/docs/database/events.md +307 -0
  46. package/docs/database/membership.md +246 -0
  47. package/docs/database/setup.md +155 -0
  48. package/docs/database/unique.md +216 -0
  49. package/docs/database/views.md +246 -0
  50. package/docs/frontend/README.md +101 -0
  51. package/docs/frontend/data-fetching.md +243 -0
  52. package/docs/frontend/images.md +148 -0
  53. package/docs/frontend/metadata.md +344 -0
  54. package/docs/frontend/rendering.md +236 -0
  55. package/docs/frontend/routing.md +344 -0
  56. package/docs/frontend/scripts.md +118 -0
  57. package/docs/frontend/search.md +191 -0
  58. package/docs/frontend/styling.md +147 -0
  59. package/docs/getting-started/README.md +81 -0
  60. package/docs/getting-started/create-project.md +131 -0
  61. package/docs/getting-started/deploy.md +101 -0
  62. package/docs/getting-started/first-app.md +215 -0
  63. package/docs/getting-started/installation.md +106 -0
  64. package/docs/getting-started/migrating.md +125 -0
  65. package/docs/getting-started/project-structure.md +163 -0
  66. package/docs/introduction/README.md +41 -0
  67. package/docs/introduction/design-principles.md +55 -0
  68. package/docs/introduction/distributed.md +74 -0
  69. package/docs/introduction/how-it-works.md +74 -0
  70. package/docs/introduction/hyperscale.md +51 -0
  71. package/docs/introduction/modern-stack.md +36 -0
  72. package/docs/introduction/vs-other-frameworks.md +48 -0
  73. package/docs/introduction/why-toil.md +93 -0
  74. package/docs/llms.txt +90 -0
  75. package/docs/realtime/README.md +102 -0
  76. package/docs/realtime/channels.md +211 -0
  77. package/docs/realtime/streams.md +369 -0
  78. package/docs/services/README.md +60 -0
  79. package/docs/services/analytics.md +268 -0
  80. package/docs/services/caching.md +175 -0
  81. package/docs/services/cookies.md +235 -0
  82. package/docs/services/crypto.md +209 -0
  83. package/docs/services/email.md +289 -0
  84. package/docs/services/environment.md +141 -0
  85. package/docs/services/ratelimit.md +174 -0
  86. package/docs/services/time.md +85 -0
  87. package/examples/basic/client/routes/analytics.tsx +13 -12
  88. package/examples/basic/server/models/SiteAnalytics.ts +29 -17
  89. package/examples/basic/server/routes/Analytics.ts +15 -17
  90. package/examples/basic/server/routes/UserId.ts +22 -0
  91. package/package.json +15 -11
  92. package/scripts/gen-toil-docs.mjs +24 -35
  93. package/server/auth/AuthController.ts +336 -0
  94. package/server/auth/AuthUser.ts +23 -0
  95. package/server/auth/index.ts +16 -0
  96. package/server/globals/auth.ts +31 -0
  97. package/server/globals/userid.ts +107 -0
  98. package/src/compiler/config.ts +13 -0
  99. package/src/compiler/docs.ts +16 -33
  100. package/src/compiler/generate.ts +6 -2
  101. package/src/compiler/index.ts +114 -6
  102. package/src/compiler/toil-docs.generated.ts +64 -22
  103. package/src/devserver/analytics/index.ts +10 -4
  104. package/src/devserver/db/database.ts +67 -1
  105. package/src/devserver/db/index.ts +1 -0
  106. package/src/devserver/db/types.ts +13 -0
  107. package/src/devserver/runtime/module.ts +7 -0
  108. package/test/analytics-dev.test.ts +2 -1
  109. package/test/devserver-database.test.ts +113 -0
  110. package/docs/auth-todo.md +0 -149
  111. package/docs/auth.md +0 -322
  112. package/docs/caching.md +0 -115
  113. package/docs/cli.md +0 -17
  114. package/docs/client.md +0 -39
  115. package/docs/cookies.md +0 -457
  116. package/docs/crypto.md +0 -130
  117. package/docs/daemon.md +0 -123
  118. package/docs/data.md +0 -131
  119. package/docs/derive.md +0 -159
  120. package/docs/email.md +0 -326
  121. package/docs/environment.md +0 -97
  122. package/docs/getting-started.md +0 -128
  123. package/docs/index.md +0 -30
  124. package/docs/ratelimit.md +0 -95
  125. package/docs/routing.md +0 -259
  126. package/docs/rpc.md +0 -149
  127. package/docs/server.md +0 -61
  128. package/docs/ssr.md +0 -632
  129. package/docs/streams.md +0 -178
  130. package/docs/styling.md +0 -22
  131. package/docs/tiers.md +0 -133
  132. package/docs/time.md +0 -43
@@ -0,0 +1,85 @@
1
+ # Time
2
+
3
+ **`Time` is the edge's clock: the blessed way to read the current time in your backend.** It gives you the number of milliseconds or seconds since a fixed reference point, so you can stamp events and compute expiries.
4
+
5
+ ## How to use it
6
+
7
+ `Time` is an ambient global (no import needed), and is also exported from `toiljs/server/runtime`:
8
+
9
+ ```ts
10
+ const ms = Time.nowMillis(); // milliseconds since the Unix epoch (a u64)
11
+ const s = Time.nowSeconds(); // whole seconds since the Unix epoch (a u64)
12
+ ```
13
+
14
+ | Member | Returns | Description |
15
+ | --- | --- | --- |
16
+ | `Time.nowMillis()` | `u64` | Milliseconds since the **Unix epoch** (midnight UTC on 1 January 1970). This is the same value as JavaScript's `Date.now()`. |
17
+ | `Time.nowSeconds()` | `u64` | Whole seconds since the epoch (`nowMillis() / 1000`). This is the unit sessions and login challenges use. |
18
+
19
+ The **Unix epoch** is just the agreed "time zero" that computers count from. "Milliseconds since the epoch" is a single number that unambiguously names an instant, with no time zone to worry about.
20
+
21
+ ## Why time is a "host import" (and what that means)
22
+
23
+ Your backend runs as a **WebAssembly** module: a small, sandboxed program. A sandbox is deliberately sealed off from the outside world. It has no clock of its own, no network, no files, nothing except the memory it was given and a short list of functions the host chooses to hand it. Each of those handed-in functions is called a **host import**.
24
+
25
+ This is on purpose. WebAssembly aims to be **deterministic**: given the same inputs, the same code produces the same outputs, every time. That makes it safe to run untrusted code, easy to cache, and easy to reason about. But the current time is not deterministic: it is different every time you ask. So a WebAssembly module cannot just "know" the time; it has to **ask the host** for it. Reading the clock is a call across the sandbox boundary.
26
+
27
+ `Time` is the thin, friendly wrapper over that call. Under the hood it uses the host's `Date.now()` binding (`env.Date.now`).
28
+
29
+ ```mermaid
30
+ flowchart LR
31
+ subgraph Sandbox["Your backend (WebAssembly sandbox)"]
32
+ C["Time.nowMillis()"]
33
+ end
34
+ subgraph Host["Dacely edge host"]
35
+ D["env.Date.now"]
36
+ K["real system clock"]
37
+ end
38
+ C -->|host import| D
39
+ D --> K
40
+ K -->|milliseconds| C
41
+ ```
42
+
43
+ Both the edge and the dev server provide this binding, so time behaves **identically** in `toiljs dev` and in production.
44
+
45
+ ## `Time` vs calling `Date.now()`
46
+
47
+ ToilScript's `Date.now()` lowers to the exact same host import, so you *can* call it directly. Prefer `Time`:
48
+
49
+ - It makes the host boundary (and the single millisecond unit) explicit and easy to find in a codebase.
50
+ - It gives you `nowSeconds()` without writing an open-coded `/ 1000` at every call site.
51
+
52
+ Both are the same clock, so timing you do in a handler lines up with timing the framework does. For example, the auth system uses `Time.nowSeconds()` for session issue and expiry times.
53
+
54
+ ## Correct usage: timestamps and TTLs
55
+
56
+ `Time` is **wall-clock time**, exactly like a browser's `Date.now()`. It follows the system clock, which means it can occasionally **step backward** (for instance when the machine corrects its clock against a time server).
57
+
58
+ Do:
59
+
60
+ - **Stamp an instant.** Record when something happened, so you can compare it later: an event time, a "last seen" marker.
61
+ - **Compute an expiry (a TTL).** A **TTL** ("time to live") is a deadline. You set it by adding a duration to *now*, and you check it by comparing *now* against the stored deadline:
62
+
63
+ ```ts
64
+ const ttlSeconds: u64 = 3600; // valid for one hour
65
+ const expiresAt = Time.nowSeconds() + ttlSeconds;
66
+
67
+ // ...later, on another request...
68
+ const expired = Time.nowSeconds() >= expiresAt;
69
+ ```
70
+
71
+ Do not:
72
+
73
+ - **Measure elapsed time with it.** Do not use it as a stopwatch to time how long an operation took. Because the wall clock can step backward, `end - start` could come out zero or negative. `Time` answers "what instant is it now?", not "how much time has passed?".
74
+
75
+ ## Gotchas
76
+
77
+ - **It is wall-clock, not monotonic.** It can jump backward across a clock correction. Fine for timestamps and expiries; wrong for measuring durations.
78
+ - **Milliseconds vs seconds.** `nowMillis()` and `nowSeconds()` differ by a factor of 1000. Mixing them (comparing a millisecond stamp to a second stamp) is a common bug. Sessions and login challenges use **seconds**.
79
+ - **UTC only.** These are epoch counts, with no time zone. Format for display on the client if you need a local time.
80
+
81
+ ## Related
82
+
83
+ - [Auth, sessions, and `@user`](../auth/usage.md): sessions use `Time.nowSeconds()` for their issue and expiry stamps.
84
+ - [Caching](./caching.md): TTLs are the same idea, a deadline after which a saved value is stale.
85
+ - [Compute tiers](../concepts/tiers.md): more on the WebAssembly sandbox your backend runs in.
@@ -25,8 +25,10 @@ const METRICS = [
25
25
  { id: 13, label: 'DB ops', unit: 'count' },
26
26
  { id: 26, label: 'Stream bytes out', unit: 'bytes' },
27
27
  { id: 39, label: 'Memory bandwidth', unit: 'bytes' },
28
- { id: 41, label: 'Connected streams', unit: 'count' },
29
- { id: 43, label: 'Committed memory', unit: 'bytes' }
28
+ { id: 41, label: 'Cache hits', unit: 'count' },
29
+ { id: 42, label: 'Cache misses', unit: 'count' },
30
+ { id: 43, label: 'Connected streams', unit: 'count' },
31
+ { id: 45, label: 'Committed memory', unit: 'bytes' }
30
32
  ] as const;
31
33
 
32
34
  // AnalyticsRange (mirrors the server enum).
@@ -157,20 +159,19 @@ export default function AnalyticsDemo() {
157
159
 
158
160
  <section style={{ marginTop: 24 }}>
159
161
  <h2>Snapshot</h2>
162
+ <p style={{ color: '#64748b', marginTop: 0 }}>
163
+ The <code>live</code> gauges are the current level right now, and the windows are the
164
+ current rate-limit usage vs the plan cap (cap ∞ = unlimited). To see a metric over time,
165
+ use the chart above (the time-series), not an ever-growing lifetime total.
166
+ </p>
160
167
  <button onClick={loadStats}>Load my site analytics</button>
161
168
  {err && <p style={{ color: '#f87171' }}>{err}</p>}
162
169
  {stats && (
163
170
  <ul>
164
- <li>requests: {String(stats.requests)}</li>
165
- <li>bytes out (L1): {String(stats.bytesOutL1)}</li>
166
- <li>bytes in (L1): {String(stats.bytesInL1)}</li>
167
- <li>gas used: {String(stats.gasUsed)}</li>
168
- <li>2xx responses: {String(stats.status2xx)}</li>
169
- <li>db ops: {String(stats.dbOps)}</li>
170
- <li>connected streams (live): {String(stats.connectedStreams)}</li>
171
- <li>committed memory (live): {fmt(Number(stats.committedMemory), 'bytes')}</li>
172
- <li>requests this minute: {cap(stats.reqMinuteUsed, stats.reqMinuteCap)}</li>
173
- <li>requests today: {cap(stats.reqDayUsed, stats.reqDayCap)}</li>
171
+ <li>connected streams (live): {String(stats.liveConnectedStreams)}</li>
172
+ <li>committed memory (live): {fmt(Number(stats.liveCommittedMemoryBytes), 'bytes')}</li>
173
+ <li>requests this minute: {cap(stats.requestsThisMinute, stats.requestsThisMinuteCap)}</li>
174
+ <li>requests today: {cap(stats.requestsToday, stats.requestsTodayCap)}</li>
174
175
  </ul>
175
176
  )}
176
177
  </section>
@@ -1,25 +1,37 @@
1
- /** This site's own analytics, mapped from the toilscript `Analytics.self()` snapshot for the
2
- * client. Counts + usage are i64; the rate-limit caps are i64 too (0 = unlimited). */
1
+ /**
2
+ * This site's own analytics snapshot, mapped from the toilscript `Analytics.self()` frame.
3
+ *
4
+ * This example deliberately returns only CURRENT state: the `live*` gauges (the instantaneous level
5
+ * right now) and the request-window counters (usage in the current 1-minute and 24-hour rate-limit
6
+ * windows, paired with the plan cap, where cap `0` = unlimited). It does NOT surface lifetime
7
+ * cumulative `total*` counters. For historical, time-windowed values use the separate `SeriesData`
8
+ * time-series (`/series`), which reads per-bucket history rather than an ever-growing running total.
9
+ *
10
+ * All fields are `u64` (the guest exposes `u64` getters, so the mapping needs zero casts).
11
+ */
3
12
  @data
4
13
  export class SiteAnalytics {
5
- requests: i64 = 0;
6
- bytesOutL1: i64 = 0;
7
- bytesInL1: i64 = 0;
8
- status2xx: i64 = 0;
9
- wasmDispatches: i64 = 0;
10
- dbOps: i64 = 0;
11
- gasUsed: i64 = 0;
12
- connectedStreams: i64 = 0;
13
- committedMemory: i64 = 0;
14
- reqMinuteUsed: i64 = 0;
15
- reqMinuteCap: i64 = 0;
16
- reqDayUsed: i64 = 0;
17
- reqDayCap: i64 = 0;
14
+ // --- Live gauges (current instantaneous level, right now, NOT a total) ---
15
+ /** Streams currently connected right now. */
16
+ liveConnectedStreams: u64 = 0;
17
+ /** Wasm linear memory currently committed right now, in bytes. */
18
+ liveCommittedMemoryBytes: u64 = 0;
19
+
20
+ // --- Request windows: current rate-limit usage vs plan cap (cap 0 = unlimited) ---
21
+ /** Requests used in the current 1-minute window. */
22
+ requestsThisMinute: u64 = 0;
23
+ /** Plan cap for the 1-minute window (0 = unlimited). */
24
+ requestsThisMinuteCap: u64 = 0;
25
+ /** Requests used in the current 24-hour window. */
26
+ requestsToday: u64 = 0;
27
+ /** Plan cap for the 24-hour window (0 = unlimited). */
28
+ requestsTodayCap: u64 = 0;
18
29
  }
19
30
 
20
- /** One metric's historical time-series for a range, from `Analytics.series(metric, range)`: the raw
31
+ /** One metric's historical TIME-SERIES for a range, from `Analytics.series(metric, range)`: the raw
21
32
  * per-bucket totals (oldest→newest) plus the bucket width + newest-bucket end, so the client can draw a
22
- * graph and derive per-second rates. Demonstrates the typed series API. */
33
+ * graph and derive per-second rates. This is per-bucket HISTORY: the right way to see a metric over time,
34
+ * instead of an ever-growing lifetime counter. Demonstrates the typed series API. */
23
35
  @data
24
36
  export class SeriesData {
25
37
  metric: i32 = 0;
@@ -16,23 +16,21 @@ import { SeriesData, SiteAnalytics } from '../models/SiteAnalytics';
16
16
  class AnalyticsRoutes {
17
17
  @get('/')
18
18
  public self(): SiteAnalytics {
19
- const s = Analytics.self();
20
- const r = new SiteAnalytics();
21
- // Typed getters: no magic strings, no `.has()/.get()` map dance.
22
- r.requests = s.requests;
23
- r.bytesOutL1 = s.bytesOutL1;
24
- r.bytesInL1 = s.bytesInL1;
25
- r.status2xx = s.status2xx;
26
- r.wasmDispatches = s.wasmDispatches;
27
- r.dbOps = s.dbOps;
28
- r.gasUsed = s.gasUsed;
29
- r.connectedStreams = s.connectedStreams;
30
- r.committedMemory = s.committedMemory;
31
- r.reqMinuteUsed = s.reqMinuteUsed;
32
- r.reqMinuteCap = <i64>s.reqMinuteCap;
33
- r.reqDayUsed = s.reqDayUsed;
34
- r.reqDayCap = <i64>s.reqDayCap;
35
- return r;
19
+ const stats = Analytics.self();
20
+ const out = new SiteAnalytics();
21
+
22
+ // Live gauges (current instantaneous level, not a cumulative total).
23
+ out.liveConnectedStreams = stats.connectedStreams;
24
+ out.liveCommittedMemoryBytes = stats.committedMemory;
25
+
26
+ // Request windows: current usage vs plan cap (cap 0 = unlimited).
27
+ out.requestsThisMinute = stats.reqMinuteUsed;
28
+ out.requestsThisMinuteCap = stats.reqMinuteCap;
29
+ out.requestsToday = stats.reqDayUsed;
30
+ out.requestsTodayCap = stats.reqDayCap;
31
+
32
+ // For historical, time-windowed values (per-bucket, not a lifetime total), see `/series` below.
33
+ return out;
36
34
  }
37
35
 
38
36
  /// Any metric's historical time-series for a range, for graphs:
@@ -0,0 +1,22 @@
1
+ import { Response } from 'toiljs/server/runtime';
2
+
3
+ /**
4
+ * Demo of `ToilUserId` — the stable 256-bit user identity derived from the ML-DSA login public key +
5
+ * identifier (email/username) + tenant domain. Deterministic, opaque, O(1) to compare with `==` / `!=`.
6
+ */
7
+ @rest('userid')
8
+ class UserIdDemo {
9
+ @get('/demo')
10
+ public demo(): Response {
11
+ const key = new Uint8Array(1312); // an ML-DSA-44 public key (zeros for the demo)
12
+ const a = ToilUserId.derive(key, 'alice@example.com', 'acme.dacely.com');
13
+ const b = ToilUserId.derive(key, 'alice@example.com', 'acme.dacely.com');
14
+ const c = ToilUserId.derive(key, 'bob@example.com', 'acme.dacely.com');
15
+ // Same inputs => same id (a == b); a different identifier => a different id (a != c).
16
+ const same = a == b;
17
+ const diff = a != c;
18
+ return Response.text(
19
+ a.toHex() + '\nsame=' + same.toString() + ' diff=' + diff.toString() + '\n',
20
+ );
21
+ }
22
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "toiljs",
3
3
  "type": "module",
4
- "version": "0.0.85",
4
+ "version": "0.0.87",
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": {
@@ -67,6 +67,10 @@
67
67
  "types": "./server/runtime/*.ts",
68
68
  "default": "./server/runtime/*.ts"
69
69
  },
70
+ "./server/auth": {
71
+ "types": "./server/auth/index.ts",
72
+ "default": "./server/auth/index.ts"
73
+ },
70
74
  "./tsconfig": "./presets/tsconfig.json",
71
75
  "./eslint": "./presets/eslint.js",
72
76
  "./prettier": "./presets/prettier.json",
@@ -120,21 +124,21 @@
120
124
  "@dacely/hyper-express": "6.17.4",
121
125
  "@dacely/noble-post-quantum": "^0.6.1",
122
126
  "@dacely/toilscript-loader": "^0.1.0",
123
- "@eslint-react/eslint-plugin": "^5.9.2",
127
+ "@eslint-react/eslint-plugin": "^5.10.1",
124
128
  "@eslint/js": "^10.0.1",
125
129
  "@noble/curves": "^2.2.0",
126
- "@typescript-eslint/utils": "^8.62.0",
130
+ "@typescript-eslint/utils": "^8.62.1",
127
131
  "@vitejs/plugin-react": "^6.0.3",
128
132
  "eslint-plugin-react-hooks": "^7.1.1",
129
133
  "eslint-plugin-react-refresh": "^0.5.3",
130
134
  "hash-wasm": "^4.12.0",
131
135
  "juice": "^12.1.1",
132
- "nodemailer": "^9.0.1",
136
+ "nodemailer": "^9.0.3",
133
137
  "picocolors": "^1.1.1",
134
- "sharp": "^0.35.2",
135
- "toilscript": "^0.1.48",
136
- "typescript-eslint": "^8.62.0",
137
- "vite": "^8.1.0",
138
+ "sharp": "^0.35.3",
139
+ "toilscript": "^0.1.52",
140
+ "typescript-eslint": "^8.62.1",
141
+ "vite": "^8.1.3",
138
142
  "vite-imagetools": "^10.0.1",
139
143
  "vite-plugin-node-polyfills": "^0.28.0"
140
144
  },
@@ -157,17 +161,17 @@
157
161
  "@microsoft/api-extractor": "7.58.9",
158
162
  "@testing-library/dom": "^10.4.1",
159
163
  "@testing-library/react": "^16.3.2",
160
- "@types/node": "^26.0.0",
164
+ "@types/node": "^26.1.0",
161
165
  "@types/nodemailer": "^8.0.1",
162
166
  "@types/react": "^19.2.17",
163
167
  "@types/react-dom": "^19.2.3",
164
168
  "@vitest/coverage-v8": "^4.1.9",
165
169
  "@vitest/ui": "^4.1.9",
166
170
  "esbuild": "^0.28.1",
167
- "eslint": "^10.5.0",
171
+ "eslint": "^10.6.0",
168
172
  "jsdom": "^29.1.1",
169
173
  "micromatch": "^4.0.8",
170
- "prettier": "^3.8.4",
174
+ "prettier": "^3.9.4",
171
175
  "react": "^19.2.7",
172
176
  "react-dom": "^19.2.7",
173
177
  "typedoc": "^0.28.19",
@@ -23,48 +23,37 @@ const docsDir = path.join(repoRoot, 'docs');
23
23
  const outFile = path.join(repoRoot, 'src', 'compiler', 'toil-docs.generated.ts');
24
24
 
25
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.
26
+ * Guides excluded from the AGENT doc set (still published as human docs).
27
+ * Nothing is excluded today: every folder's `README.md` landing page and its
28
+ * topic pages are bundled so the agent gets the full navigation.
29
29
  */
30
- const EXCLUDE = new Set(['auth-todo.md', 'README.md']);
30
+ const EXCLUDE = new Set();
31
31
 
32
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.
33
+ * Stable order for the agent docs: the root landing page first, then every
34
+ * other guide alphabetically by its repo-relative path (which naturally groups
35
+ * each folder together, led by its `README.md` landing). Adding a new doc file
36
+ * is picked up automatically.
35
37
  */
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
- 'tiers.md',
46
- 'streams.md',
47
- 'daemon.md',
48
- 'data.md',
49
- 'caching.md',
50
- 'ratelimit.md',
51
- 'auth.md',
52
- 'environment.md',
53
- 'email.md',
54
- 'cookies.md',
55
- 'crypto.md',
56
- 'time.md',
57
- 'cli.md',
58
- ];
38
+ const ORDER = ['README.md'];
59
39
 
60
40
  function collect() {
61
- const all = fs
62
- .readdirSync(docsDir)
63
- .filter((f) => f.endsWith('.md') && !EXCLUDE.has(f))
64
- .sort();
41
+ // Recurse one+ levels so a topic FOLDER (e.g. `docs/auth/*.md`) is bundled too, keyed by its
42
+ // repo-relative path (`auth/index.md`). Adding a `docs/**/*.md` is picked up automatically.
43
+ const files = [];
44
+ const walk = (dir, prefix) => {
45
+ for (const ent of fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
46
+ if (ent.isDirectory()) {
47
+ walk(path.join(dir, ent.name), prefix + ent.name + '/');
48
+ } else if (ent.name.endsWith('.md') && !EXCLUDE.has(prefix + ent.name) && !EXCLUDE.has(ent.name)) {
49
+ files.push(prefix + ent.name);
50
+ }
51
+ }
52
+ };
53
+ walk(docsDir, '');
65
54
  const ordered = [
66
- ...ORDER.filter((f) => all.includes(f)),
67
- ...all.filter((f) => !ORDER.includes(f)),
55
+ ...ORDER.filter((f) => files.includes(f)),
56
+ ...files.filter((f) => !ORDER.includes(f)),
68
57
  ];
69
58
  const out = {};
70
59
  for (const name of ordered) {