toiljs 0.0.55 → 0.0.56

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 (87) hide show
  1. package/build/backend/.tsbuildinfo +1 -1
  2. package/build/cli/.tsbuildinfo +1 -1
  3. package/build/cli/index.js +9 -5
  4. package/build/client/.tsbuildinfo +1 -1
  5. package/build/client/auth.js +1 -1
  6. package/build/client/components/Image.d.ts +1 -1
  7. package/build/client/dev/devtools.js +3 -1
  8. package/build/client/index.d.ts +2 -2
  9. package/build/client/index.js +2 -2
  10. package/build/client/routing/Router.js +1 -1
  11. package/build/client/routing/mount.js +1 -1
  12. package/build/compiler/.tsbuildinfo +1 -1
  13. package/build/compiler/docs.js +1 -1
  14. package/build/compiler/seo.js +1 -3
  15. package/build/compiler/template-build.js +1 -1
  16. package/build/devserver/.tsbuildinfo +1 -1
  17. package/build/devserver/cache.js +0 -0
  18. package/build/devserver/crypto.js +45 -17
  19. package/build/devserver/database.js +82 -0
  20. package/build/devserver/email/caps.js +0 -0
  21. package/build/devserver/email/config.js +7 -2
  22. package/build/devserver/email/validate.js +1 -4
  23. package/build/devserver/index.d.ts +1 -1
  24. package/build/devserver/index.js +3 -2
  25. package/build/devserver/module.js +51 -12
  26. package/build/devserver/proxy.js +2 -1
  27. package/build/io/.tsbuildinfo +1 -1
  28. package/build/io/codec.d.ts +5 -5
  29. package/build/io/codec.js +193 -77
  30. package/examples/basic/client/components/HoneycombBackground.tsx +1 -1
  31. package/examples/basic/client/public/images/logo.svg +37 -34
  32. package/examples/basic/client/public/index.html +14 -14
  33. package/examples/basic/client/routes/auth.tsx +18 -10
  34. package/examples/basic/client/routes/cookies.tsx +15 -24
  35. package/examples/basic/client/routes/crypto.tsx +4 -5
  36. package/examples/basic/client/routes/features/template/template.tsx +1 -1
  37. package/examples/basic/client/routes/hello.tsx +1 -1
  38. package/examples/basic/client/routes/pq.tsx +14 -14
  39. package/examples/basic/client/routes/rest.tsx +1 -3
  40. package/examples/basic/client/styles/main.css +25 -22
  41. package/examples/basic/client/toil.tsx +1 -1
  42. package/examples/basic/server/README.md +8 -8
  43. package/examples/basic/server/core/AppHandler.ts +4 -7
  44. package/examples/basic/server/routes/Auth.ts +11 -3
  45. package/examples/basic/server/routes/EnvDemo.ts +9 -3
  46. package/package.json +1 -1
  47. package/src/backend/index.ts +4 -2
  48. package/src/cli/doctor.ts +10 -3
  49. package/src/cli/notify.ts +1 -6
  50. package/src/cli/ui.ts +3 -3
  51. package/src/cli/version-check.ts +5 -1
  52. package/src/client/auth.ts +33 -10
  53. package/src/client/components/Form.tsx +2 -2
  54. package/src/client/components/Image.tsx +1 -1
  55. package/src/client/components/Script.tsx +1 -1
  56. package/src/client/components/Slot.tsx +1 -1
  57. package/src/client/dev/devtools.tsx +121 -54
  58. package/src/client/dev/error-overlay.tsx +7 -1
  59. package/src/client/head/metadata.ts +1 -1
  60. package/src/client/index.ts +13 -2
  61. package/src/client/routing/Router.tsx +2 -2
  62. package/src/client/routing/error-boundary.tsx +1 -1
  63. package/src/client/routing/loader.ts +2 -2
  64. package/src/client/routing/mount.tsx +5 -6
  65. package/src/compiler/docs.ts +1 -1
  66. package/src/compiler/email-preview.ts +1 -1
  67. package/src/compiler/generate.ts +1 -1
  68. package/src/compiler/seo.ts +1 -3
  69. package/src/compiler/ssg.ts +10 -4
  70. package/src/compiler/template-build.ts +2 -7
  71. package/src/compiler/template.ts +1 -4
  72. package/src/compiler/vite.ts +1 -1
  73. package/src/devserver/cache.ts +0 -0
  74. package/src/devserver/crypto.ts +140 -51
  75. package/src/devserver/database.ts +149 -8
  76. package/src/devserver/dotenv.ts +10 -2
  77. package/src/devserver/email/caps.ts +0 -0
  78. package/src/devserver/email/config.ts +8 -2
  79. package/src/devserver/email/index.ts +3 -3
  80. package/src/devserver/email/validate.ts +1 -4
  81. package/src/devserver/envelope.ts +3 -3
  82. package/src/devserver/host.ts +14 -5
  83. package/src/devserver/index.ts +15 -6
  84. package/src/devserver/module.ts +56 -14
  85. package/src/devserver/proxy.ts +5 -7
  86. package/src/io/codec.ts +226 -83
  87. package/test/devserver-database.test.ts +60 -0
package/src/cli/doctor.ts CHANGED
@@ -10,7 +10,12 @@ import { createRequire } from 'node:module';
10
10
  import path from 'node:path';
11
11
  import { fileURLToPath } from 'node:url';
12
12
 
13
- import { loadConfig, type ResolvedToilConfig, scanRoutes, TOIL_SERVER_ENV_DTS } from 'toiljs/compiler';
13
+ import {
14
+ loadConfig,
15
+ type ResolvedToilConfig,
16
+ scanRoutes,
17
+ TOIL_SERVER_ENV_DTS,
18
+ } from 'toiljs/compiler';
14
19
 
15
20
  import {
16
21
  type Check,
@@ -41,8 +46,8 @@ import {
41
46
  findRelativeAssets,
42
47
  hasFailures,
43
48
  type RestFacts,
44
- type RpcFacts,
45
49
  RPC_TOILSCRIPT_MIN,
50
+ type RpcFacts,
46
51
  satisfiesMin,
47
52
  type SourceFile,
48
53
  summarize,
@@ -353,7 +358,9 @@ function applyRpcFix(root: string): RpcFixResult {
353
358
  const serverToilconfig = readJsonObject(path.join(root, 'toilconfig.json'));
354
359
  if (serverToilconfig !== null) {
355
360
  const entries = Array.isArray(serverToilconfig.entries)
356
- ? (serverToilconfig.entries as unknown[]).filter((e): e is string => typeof e === 'string')
361
+ ? (serverToilconfig.entries as unknown[]).filter(
362
+ (e): e is string => typeof e === 'string',
363
+ )
357
364
  : [];
358
365
  const dirs = new Set<string>();
359
366
  for (const e of entries) dirs.add(path.dirname(path.resolve(root, e)));
package/src/cli/notify.ts CHANGED
@@ -13,12 +13,7 @@ import { fileURLToPath } from 'node:url';
13
13
 
14
14
  import { detectPackageManager } from './update.js';
15
15
  import { accent, bold, box, dim, version as cliVersion, warn } from './ui.js';
16
- import {
17
- findOutdated,
18
- isCacheFresh,
19
- type OutdatedRow,
20
- parseCheckCache,
21
- } from './version-check.js';
16
+ import { findOutdated, isCacheFresh, type OutdatedRow, parseCheckCache } from './version-check.js';
22
17
 
23
18
  const REGISTRY_URL = 'https://registry.npmjs.org/toiljs/latest';
24
19
  const FETCH_TIMEOUT_MS = 2000;
package/src/cli/ui.ts CHANGED
@@ -118,9 +118,7 @@ function visibleWidth(s: string): number {
118
118
  export function box(lines: readonly string[], paint: (s: string) => string = (s) => s): string {
119
119
  const width = lines.reduce((w, l) => Math.max(w, visibleWidth(l)), 0);
120
120
  const side = paint('│');
121
- const body = lines.map(
122
- (l) => ` ${side} ${l}${' '.repeat(width - visibleWidth(l))} ${side}`,
123
- );
121
+ const body = lines.map((l) => ` ${side} ${l}${' '.repeat(width - visibleWidth(l))} ${side}`);
124
122
  return [
125
123
  ' ' + paint(`╭${'─'.repeat(width + 4)}╮`),
126
124
  ...body,
@@ -167,3 +165,5 @@ export function banner(): void {
167
165
  const ver = `${dim(' v')}${brand(version())}`;
168
166
  process.stdout.write('\n' + lines.join('\n') + '\n\n ' + tagline() + ' ' + ver + '\n\n');
169
167
  }
168
+
169
+
@@ -29,7 +29,11 @@ export function parseCheckCache(raw: string): CheckCache | null {
29
29
  }
30
30
 
31
31
  /** True when the cached answer is still trustworthy (also stale if the clock went backwards). */
32
- export function isCacheFresh(cache: CheckCache, now: number, ttlMs: number = CHECK_TTL_MS): boolean {
32
+ export function isCacheFresh(
33
+ cache: CheckCache,
34
+ now: number,
35
+ ttlMs: number = CHECK_TTL_MS,
36
+ ): boolean {
33
37
  return cache.checkedAt <= now && now - cache.checkedAt < ttlMs;
34
38
  }
35
39
 
@@ -11,7 +11,7 @@
11
11
  * JSON, byte-identical to the server's `AuthService.buildLoginMessage`.
12
12
  */
13
13
 
14
- import { argon2id, createSHA256, createHMAC } from 'hash-wasm';
14
+ import { argon2id, createHMAC, createSHA256 } from 'hash-wasm';
15
15
  import { ml_dsa44 } from '@dacely/noble-post-quantum/ml-dsa.js';
16
16
  import { ml_kem768 } from '@dacely/noble-post-quantum/ml-kem.js';
17
17
  import { ristretto255_oprf } from '@noble/curves/ed25519.js';
@@ -45,7 +45,9 @@ function fromHex(hex: string): Uint8Array {
45
45
  * decapsulate, so a valid confirmation tag authenticates the server. This is
46
46
  * the demo dev key; a real deployment pins its own (and rotates it).
47
47
  */
48
- export const SERVER_KEM_PUBLIC_KEY = fromHex('29d765e8083182891302569b3712a856e564fdd484b0706b0c68568d5ab7edc742cf74459d64595455a60f267973aa55e43c5be61925a3822eafcca445e36dc4655636e31e6fc9bec338b253f94290008ef7f40dbddb49c15c690f6755a23a1b3c85cfd5207e71a607086a6fc6d74a05080f43276901a19cafdb8de7771d58ea07f0f1056b905127b22223d08e75173199f13ab13c5dcd3b51ac784f84e520484a262b845a897c41cf27324ab6ba545c78c9ccab361051e0bba53498af26240fa0d566d1572684f4b42e253e6d052c848650915063c35641e1121ef8d9cfd17b667b351103c56d195007c9376d0c08aa268396814490eab4c364175a94533267a1933862cc4c33bcf0a13d1fa2b9d6c5082eeca1480672f2526cbe013beff14dc908a386e0b633c8761023cbed760deac6709bc328d865ac82e12307b673d96711dbb27a4d939230d25b53d594169a318be0200fa33550e9418e2a3b30e9719edc09d5fc4306f1abfd021eab14637a8a72c5931d25dc9b56db0e6ab677522b10f25307dbb804a6774ce05b87b0976a4b227bfe6caf20a79e64004fbd27b1eea018b3ab8ffa629f2dc87f19278f95168e94e44660a3370c537795678eb2f056260609769740583b51b291862927a1938737c6a37f40b78f00671cccbcb88ac3427b37915ed58782998f84051647707d48995472baad3f64a7cca54e1c0734db08751c614a34f28b84f2c1b5a6817355ab61957c486b7acffbc092bc8a7b46387f33b53ed372f7168d31a71cd008539928b0cdf91e835aa97f6a2be6d327b87a6ae478701d75a59a25179cb14997bb2552853014724170a1c49b82c2bcebc3279024e1fa44c53c7afdc43f0bd22116490f3b74c90e7296be58b9a91168f2fa0c3d378a3bcac959f357825c9976a8c9ee944f29b45e96d7345d9b478431a20cf1c5d3a3227c717fd204619777636c0cb140db5c50d2a3302334461030bee34e4eb1a6f02b733f9ccda4290fa168bc039568373241542728d00030d1f251e83737cb215adbdc1de75978675a0cd0d75b12748abdda7a9852629c63697d145af2c69854b06e03f37c4b064e4c9a4c03f2ad4d081e70180e9547247921918118086b62b4f7727f46b24e3e79ba3f28209f32b5102035bf935856232f83642268c0292ec6bf8e9462382163d30a20b4bcb7b4439310ec9d0a148193907fc07697342967cf1a16c6b3c71558951fa915400736cf699262b54b723abb2ecc27b74b68ee494287595ef818388adb49e883c67bfa5c226c0eef037a0851a29d34675912c1ea1068310b6dfcd017c809c8fbfc2c3ae78dfef07299960eeefba182662a90fa422c1790f356a2ea909012b15623a9b9e450a282cb530589a68368b3583159d9010ac3e52cc974753c342e58279516339dfb691df94b13a223ad97eb6a09c21dafe6304a3642d6d2067b5238497661fe88ad1227ca3557be2a576b6e17c5a7f997ea07929e76407e376aba74c44cd8504804776f39bbb8327624188a63501e83b404d9438cade0b11dc3ac61856447fb072b91761c228878f01b2eb6b4b21ba664c2c75882431603b25a449ffeb8410b910558581777562aa9b2181fd9c04713ad9326462d3e842121c4997f9aa932417c67851625816de66e0d65637434629f39');
48
+ export const SERVER_KEM_PUBLIC_KEY = fromHex(
49
+ '29d765e8083182891302569b3712a856e564fdd484b0706b0c68568d5ab7edc742cf74459d64595455a60f267973aa55e43c5be61925a3822eafcca445e36dc4655636e31e6fc9bec338b253f94290008ef7f40dbddb49c15c690f6755a23a1b3c85cfd5207e71a607086a6fc6d74a05080f43276901a19cafdb8de7771d58ea07f0f1056b905127b22223d08e75173199f13ab13c5dcd3b51ac784f84e520484a262b845a897c41cf27324ab6ba545c78c9ccab361051e0bba53498af26240fa0d566d1572684f4b42e253e6d052c848650915063c35641e1121ef8d9cfd17b667b351103c56d195007c9376d0c08aa268396814490eab4c364175a94533267a1933862cc4c33bcf0a13d1fa2b9d6c5082eeca1480672f2526cbe013beff14dc908a386e0b633c8761023cbed760deac6709bc328d865ac82e12307b673d96711dbb27a4d939230d25b53d594169a318be0200fa33550e9418e2a3b30e9719edc09d5fc4306f1abfd021eab14637a8a72c5931d25dc9b56db0e6ab677522b10f25307dbb804a6774ce05b87b0976a4b227bfe6caf20a79e64004fbd27b1eea018b3ab8ffa629f2dc87f19278f95168e94e44660a3370c537795678eb2f056260609769740583b51b291862927a1938737c6a37f40b78f00671cccbcb88ac3427b37915ed58782998f84051647707d48995472baad3f64a7cca54e1c0734db08751c614a34f28b84f2c1b5a6817355ab61957c486b7acffbc092bc8a7b46387f33b53ed372f7168d31a71cd008539928b0cdf91e835aa97f6a2be6d327b87a6ae478701d75a59a25179cb14997bb2552853014724170a1c49b82c2bcebc3279024e1fa44c53c7afdc43f0bd22116490f3b74c90e7296be58b9a91168f2fa0c3d378a3bcac959f357825c9976a8c9ee944f29b45e96d7345d9b478431a20cf1c5d3a3227c717fd204619777636c0cb140db5c50d2a3302334461030bee34e4eb1a6f02b733f9ccda4290fa168bc039568373241542728d00030d1f251e83737cb215adbdc1de75978675a0cd0d75b12748abdda7a9852629c63697d145af2c69854b06e03f37c4b064e4c9a4c03f2ad4d081e70180e9547247921918118086b62b4f7727f46b24e3e79ba3f28209f32b5102035bf935856232f83642268c0292ec6bf8e9462382163d30a20b4bcb7b4439310ec9d0a148193907fc07697342967cf1a16c6b3c71558951fa915400736cf699262b54b723abb2ecc27b74b68ee494287595ef818388adb49e883c67bfa5c226c0eef037a0851a29d34675912c1ea1068310b6dfcd017c809c8fbfc2c3ae78dfef07299960eeefba182662a90fa422c1790f356a2ea909012b15623a9b9e450a282cb530589a68368b3583159d9010ac3e52cc974753c342e58279516339dfb691df94b13a223ad97eb6a09c21dafe6304a3642d6d2067b5238497661fe88ad1227ca3557be2a576b6e17c5a7f997ea07929e76407e376aba74c44cd8504804776f39bbb8327624188a63501e83b404d9438cade0b11dc3ac61856447fb072b91761c228878f01b2eb6b4b21ba664c2c75882431603b25a449ffeb8410b910558581777562aa9b2181fd9c04713ad9326462d3e842121c4997f9aa932417c67851625816de66e0d65637434629f39',
50
+ );
49
51
 
50
52
  export const PUBLIC_KEY_LEN = 1312;
51
53
  export const SECRET_KEY_LEN = 2560;
@@ -171,7 +173,6 @@ export function buildRegisterMessage(username: string, publicKey: Uint8Array): U
171
173
  return new DataWriter().writeU8(1).writeString(username).writeBytes(publicKey).toBytes();
172
174
  }
173
175
 
174
-
175
176
  function decodeKdf(r: DataReader): KdfParams {
176
177
  return {
177
178
  memKiB: r.readU32(),
@@ -181,7 +182,6 @@ function decodeKdf(r: DataReader): KdfParams {
181
182
  };
182
183
  }
183
184
 
184
-
185
185
  async function postBinary(baseUrl: string, path: string, body: Uint8Array): Promise<DataReader> {
186
186
  const res = await fetch(baseUrl + path, {
187
187
  method: 'POST',
@@ -204,7 +204,11 @@ export interface AuthOptions {
204
204
  * stretched with Argon2id into an ML-DSA-44 keypair, and ONLY the public key
205
205
  * (plus a proof-of-possession signature) is submitted. Throws on failure.
206
206
  */
207
- export async function register(username: string, password: string, opts: AuthOptions = {}): Promise<void> {
207
+ export async function register(
208
+ username: string,
209
+ password: string,
210
+ opts: AuthOptions = {},
211
+ ): Promise<void> {
208
212
  const baseUrl = opts.baseUrl ?? '/auth';
209
213
  const oprf = ristretto255_oprf.oprf;
210
214
  const pw = utf8(password.normalize('NFKC'));
@@ -268,7 +272,11 @@ export async function register(username: string, password: string, opts: AuthOpt
268
272
  * The secret key, seed, and shared secret are wiped as soon as they are used.
269
273
  * Returns the opaque session token. Throws (one generic message) on any failure.
270
274
  */
271
- export async function login(username: string, password: string, opts: AuthOptions = {}): Promise<Uint8Array> {
275
+ export async function login(
276
+ username: string,
277
+ password: string,
278
+ opts: AuthOptions = {},
279
+ ): Promise<Uint8Array> {
272
280
  const baseUrl = opts.baseUrl ?? '/auth';
273
281
  const oprf = ristretto255_oprf.oprf;
274
282
  const pw = utf8(password.normalize('NFKC'));
@@ -299,8 +307,17 @@ export async function login(username: string, password: string, opts: AuthOption
299
307
  const { cipherText, sharedSecret } = ml_kem768.encapsulate(SERVER_KEM_PUBLIC_KEY);
300
308
  const serverKemKeyId = await sha256Bytes(SERVER_KEM_PUBLIC_KEY);
301
309
  const message = buildLoginMessage(
302
- username, aud, cid, nonce, iat, exp,
303
- cipherText, kdf.memKiB, kdf.iterations, kdf.parallelism, serverKemKeyId,
310
+ username,
311
+ aud,
312
+ cid,
313
+ nonce,
314
+ iat,
315
+ exp,
316
+ cipherText,
317
+ kdf.memKiB,
318
+ kdf.iterations,
319
+ kdf.parallelism,
320
+ serverKemKeyId,
304
321
  );
305
322
  let signature: Uint8Array;
306
323
  try {
@@ -333,9 +350,15 @@ export async function login(username: string, password: string, opts: AuthOption
333
350
  // decapsulated correctly derives the same K, so a valid tag proves its
334
351
  // identity. Verify before returning the session.
335
352
  const transcriptHash = await sha256Bytes(message);
336
- const sessionKey = await hmacSha256(sharedSecret, concatBytes(utf8(SESSION_KEY_LABEL), transcriptHash));
353
+ const sessionKey = await hmacSha256(
354
+ sharedSecret,
355
+ concatBytes(utf8(SESSION_KEY_LABEL), transcriptHash),
356
+ );
337
357
  wipe(sharedSecret);
338
- const expected = await hmacSha256(sessionKey, concatBytes(utf8(SERVER_CONFIRM_LABEL), transcriptHash));
358
+ const expected = await hmacSha256(
359
+ sessionKey,
360
+ concatBytes(utf8(SERVER_CONFIRM_LABEL), transcriptHash),
361
+ );
339
362
  if (!bytesEqual(expected, serverConfirm)) throw new Error('auth: server authentication failed');
340
363
 
341
364
  return session; // session token
@@ -1,6 +1,6 @@
1
- import { useRef, type ReactNode, type SyntheticEvent } from 'react';
1
+ import { type ReactNode, type SyntheticEvent, useRef } from 'react';
2
2
 
3
- import { useAction, type ActionState, type RevalidateTarget } from '../routing/action.js';
3
+ import { type ActionState, type RevalidateTarget, useAction } from '../routing/action.js';
4
4
 
5
5
  /** Props for {@link Form}. */
6
6
  export interface FormProps {
@@ -1,4 +1,4 @@
1
- import { useState, type CSSProperties, type ComponentPropsWithRef, type ReactNode } from 'react';
1
+ import { type ComponentPropsWithRef, type CSSProperties, type ReactNode, useState } from 'react';
2
2
 
3
3
  /**
4
4
  * Props for {@link Image}: every standard `<img>` attribute, plus toil's layout/loading controls.
@@ -1,4 +1,4 @@
1
- import { useEffect, type ReactNode } from 'react';
1
+ import { type ReactNode, useEffect } from 'react';
2
2
 
3
3
  /**
4
4
  * When a {@link Script} is injected, relative to the app becoming interactive:
@@ -1,4 +1,4 @@
1
- import { useContext, type ReactNode } from 'react';
1
+ import { type ReactNode, useContext } from 'react';
2
2
 
3
3
  import { SlotContext } from '../routing/slot-context.js';
4
4
 
@@ -7,7 +7,7 @@
7
7
  * It stays decoupled from the Router (it computes the current match itself via `matchRoute`) so it
8
8
  * renders even when the app tree has crashed.
9
9
  */
10
- import { useEffect, useState, useSyncExternalStore, type ReactNode } from 'react';
10
+ import { type ReactNode, useEffect, useState, useSyncExternalStore } from 'react';
11
11
 
12
12
  import { type DevError, getErrorLog, subscribeErrors } from './error-overlay.js';
13
13
  import {
@@ -21,10 +21,10 @@ import {
21
21
  import {
22
22
  clearLoaderData,
23
23
  inspectLoaderCache,
24
+ type LoaderCacheSnapshot,
24
25
  loaderKey,
25
26
  revalidate,
26
27
  subscribeLoaderCache,
27
- type LoaderCacheSnapshot,
28
28
  } from '../routing/loader.js';
29
29
  import { matchRoute } from '../routing/match.js';
30
30
  import { getPages } from '../search/search.js';
@@ -52,10 +52,22 @@ function ToilLogo({ size = 16 }: { size?: number }): ReactNode {
52
52
  x2="467.12"
53
53
  y2="467.12"
54
54
  gradientUnits="userSpaceOnUse">
55
- <stop offset="0" stopColor="#6990ff" />
56
- <stop offset=".28" stopColor="#521be0" />
57
- <stop offset=".66" stopColor="#6900f4" />
58
- <stop offset="1" stopColor="#7f00f6" />
55
+ <stop
56
+ offset="0"
57
+ stopColor="#6990ff"
58
+ />
59
+ <stop
60
+ offset=".28"
61
+ stopColor="#521be0"
62
+ />
63
+ <stop
64
+ offset=".66"
65
+ stopColor="#6900f4"
66
+ />
67
+ <stop
68
+ offset="1"
69
+ stopColor="#7f00f6"
70
+ />
59
71
  </linearGradient>
60
72
  <linearGradient
61
73
  id="toilDtB"
@@ -64,12 +76,28 @@ function ToilLogo({ size = 16 }: { size?: number }): ReactNode {
64
76
  x2="149.99"
65
77
  y2="0"
66
78
  gradientUnits="userSpaceOnUse">
67
- <stop offset=".15" stopColor="#6990ff" stopOpacity=".6" />
68
- <stop offset=".55" stopColor="#531ae1" />
79
+ <stop
80
+ offset=".15"
81
+ stopColor="#6990ff"
82
+ stopOpacity=".6"
83
+ />
84
+ <stop
85
+ offset=".55"
86
+ stopColor="#531ae1"
87
+ />
69
88
  </linearGradient>
70
89
  </defs>
71
- <rect width="500" height="500" rx="130" ry="130" fill="url(#toilDtA)" />
72
- <path d="M299.98,0L0,355.49v-225.49C0,58.2,58.2,0,130,0h169.98Z" fill="url(#toilDtB)" />
90
+ <rect
91
+ width="500"
92
+ height="500"
93
+ rx="130"
94
+ ry="130"
95
+ fill="url(#toilDtA)"
96
+ />
97
+ <path
98
+ d="M299.98,0L0,355.49v-225.49C0,58.2,58.2,0,130,0h169.98Z"
99
+ fill="url(#toilDtB)"
100
+ />
73
101
  <path
74
102
  d="M106.17,111.11h285.24c9.9,0,16.7,9.96,13.09,19.18l-17.98,45.96c-2.11,5.39-7.31,8.94-13.09,8.94h-74.65c-7.76,0-14.06,6.29-14.06,14.06v214.94c0,7.76-6.29,14.06-14.06,14.06h-45.96c-7.76,0-14.06-6.29-14.06-14.06v-217.25c0-7.76-6.29-14.06-14.06-14.06h-73.66c-5.82,0-11.04-3.59-13.12-9.02l-16.76-43.64c-3.54-9.21,3.26-19.1,13.12-19.1Z"
75
103
  fill="#fff"
@@ -194,7 +222,10 @@ function safeJson(value: unknown): string {
194
222
  }
195
223
 
196
224
  /** Reads the current document head's meta + link tags (live). */
197
- function readHead(): { metas: { name: string; content: string }[]; links: { rel: string; href: string }[] } {
225
+ function readHead(): {
226
+ metas: { name: string; content: string }[];
227
+ links: { rel: string; href: string }[];
228
+ } {
198
229
  const metas: { name: string; content: string }[] = [];
199
230
  const links: { rel: string; href: string }[] = [];
200
231
  if (typeof document === 'undefined') return { metas, links };
@@ -346,7 +377,9 @@ function RouteTab({
346
377
  <Row k="slots">{activeSlots.length ? activeSlots.join(', ') : 'none'}</Row>
347
378
  <Row k="navigating">{pending ? 'yes' : 'no'}</Row>
348
379
 
349
- <p className="toil-dt-sec" style={{ marginTop: 12 }}>
380
+ <p
381
+ className="toil-dt-sec"
382
+ style={{ marginTop: 12 }}>
350
383
  Routes ({routes.length})
351
384
  </p>
352
385
  {routes.map((r) => {
@@ -474,7 +507,9 @@ function HeadTab(): ReactNode {
474
507
  <div className="toil-dt-body">
475
508
  <Row k="title">{title || '(none)'}</Row>
476
509
 
477
- <p className="toil-dt-sec" style={{ marginTop: 10 }}>
510
+ <p
511
+ className="toil-dt-sec"
512
+ style={{ marginTop: 10 }}>
478
513
  OpenGraph preview
479
514
  </p>
480
515
  <div className="toil-dt-og">
@@ -491,23 +526,41 @@ function HeadTab(): ReactNode {
491
526
  </div>
492
527
  </div>
493
528
 
494
- <p className="toil-dt-sec" style={{ marginTop: 10 }}>
529
+ <p
530
+ className="toil-dt-sec"
531
+ style={{ marginTop: 10 }}>
495
532
  SEO checklist
496
533
  </p>
497
- <Check ok={Boolean(title)} label="Has a title" />
498
- <Check ok={meta('description') !== undefined} label="Has a meta description" />
499
- <Check ok={og.image !== undefined} label="Has an og:image" />
500
- <Check ok={links.some((l) => l.rel === 'canonical')} label="Has a canonical link" />
534
+ <Check
535
+ ok={Boolean(title)}
536
+ label="Has a title"
537
+ />
538
+ <Check
539
+ ok={meta('description') !== undefined}
540
+ label="Has a meta description"
541
+ />
542
+ <Check
543
+ ok={og.image !== undefined}
544
+ label="Has an og:image"
545
+ />
546
+ <Check
547
+ ok={links.some((l) => l.rel === 'canonical')}
548
+ label="Has a canonical link"
549
+ />
501
550
  <Check
502
551
  ok={pages.length === 0 || described === pages.length}
503
552
  label={`Pages with a description: ${String(described)}/${String(pages.length)}`}
504
553
  />
505
554
 
506
- <p className="toil-dt-sec" style={{ marginTop: 10 }}>
555
+ <p
556
+ className="toil-dt-sec"
557
+ style={{ marginTop: 10 }}>
507
558
  Meta ({metas.length})
508
559
  </p>
509
560
  {metas.map((m, i) => (
510
- <Row k={m.name} key={`${m.name}:${String(i)}`}>
561
+ <Row
562
+ k={m.name}
563
+ key={`${m.name}:${String(i)}`}>
511
564
  {m.content}
512
565
  </Row>
513
566
  ))}
@@ -546,7 +599,8 @@ function BuildTab({ info }: { info: DevInfo | null }): ReactNode {
546
599
 
547
600
  function ErrorsTab(): ReactNode {
548
601
  const errors = useErrors();
549
- if (errors.length === 0) return <p className="toil-dt-empty toil-dt-body">No errors captured.</p>;
602
+ if (errors.length === 0)
603
+ return <p className="toil-dt-empty toil-dt-body">No errors captured.</p>;
550
604
  return (
551
605
  <div className="toil-dt-body">
552
606
  {[...errors].reverse().map((e, i) => (
@@ -974,45 +1028,58 @@ export function DevToolbar({
974
1028
 
975
1029
  return (
976
1030
  <>
977
- <div className={`toil-dt ${p.side}`}>
978
- <div className="toil-dt-panel">
979
- <div className="toil-dt-head">
980
- <span style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
981
- <ToilLogo size={14} />
982
- <span className="toil-dt-logo">toiljs</span> devtools
983
- <span className={`toil-dt-dot ${dotClass}`} />
984
- </span>
985
- <button
986
- className="toil-dt-x"
987
- onClick={() => {
988
- setPrefs({ open: false });
989
- }}>
990
-
991
- </button>
992
- </div>
993
- <div className="toil-dt-tabs">
994
- {TABS.map((t) => (
1031
+ <div className={`toil-dt ${p.side}`}>
1032
+ <div className="toil-dt-panel">
1033
+ <div className="toil-dt-head">
1034
+ <span style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
1035
+ <ToilLogo size={14} />
1036
+ <span className="toil-dt-logo">toiljs</span> devtools
1037
+ <span className={`toil-dt-dot ${dotClass}`} />
1038
+ </span>
995
1039
  <button
996
- key={t.id}
997
- className={`toil-dt-tab ${p.tab === t.id ? 'active' : ''}`}
1040
+ className="toil-dt-x"
998
1041
  onClick={() => {
999
- setPrefs({ tab: t.id });
1042
+ setPrefs({ open: false });
1000
1043
  }}>
1001
- {t.label}
1002
- {t.id === 'errors' && errors.length > 0 ? ` (${String(errors.length)})` : ''}
1044
+
1003
1045
  </button>
1004
- ))}
1046
+ </div>
1047
+ <div className="toil-dt-tabs">
1048
+ {TABS.map((t) => (
1049
+ <button
1050
+ key={t.id}
1051
+ className={`toil-dt-tab ${p.tab === t.id ? 'active' : ''}`}
1052
+ onClick={() => {
1053
+ setPrefs({ tab: t.id });
1054
+ }}>
1055
+ {t.label}
1056
+ {t.id === 'errors' && errors.length > 0
1057
+ ? ` (${String(errors.length)})`
1058
+ : ''}
1059
+ </button>
1060
+ ))}
1061
+ </div>
1062
+ {p.tab === 'route' && (
1063
+ <RouteTab
1064
+ routes={routes}
1065
+ slots={slots}
1066
+ info={info}
1067
+ />
1068
+ )}
1069
+ {p.tab === 'data' && <DataTab />}
1070
+ {p.tab === 'head' && <HeadTab />}
1071
+ {p.tab === 'build' && <BuildTab info={info} />}
1072
+ {p.tab === 'errors' && <ErrorsTab />}
1073
+ {p.tab === 'ai' && (
1074
+ <AiTab
1075
+ info={info}
1076
+ routes={routes}
1077
+ />
1078
+ )}
1079
+ {p.tab === 'prefs' && <PrefsTab />}
1005
1080
  </div>
1006
- {p.tab === 'route' && <RouteTab routes={routes} slots={slots} info={info} />}
1007
- {p.tab === 'data' && <DataTab />}
1008
- {p.tab === 'head' && <HeadTab />}
1009
- {p.tab === 'build' && <BuildTab info={info} />}
1010
- {p.tab === 'errors' && <ErrorsTab />}
1011
- {p.tab === 'ai' && <AiTab info={info} routes={routes} />}
1012
- {p.tab === 'prefs' && <PrefsTab />}
1013
1081
  </div>
1014
- </div>
1015
- {pal}
1082
+ {pal}
1016
1083
  </>
1017
1084
  );
1018
1085
  }
@@ -4,7 +4,13 @@
4
4
  * plus `window` `error` / `unhandledrejection` events. Shows the message, stack, and (for render
5
5
  * errors) the React component stack, with Dismiss / Reload. Inert in production builds.
6
6
  */
7
- import { Component, type CSSProperties, type ErrorInfo, type ReactNode, useSyncExternalStore, } from 'react';
7
+ import {
8
+ Component,
9
+ type CSSProperties,
10
+ type ErrorInfo,
11
+ type ReactNode,
12
+ useSyncExternalStore,
13
+ } from 'react';
8
14
 
9
15
  /** A captured dev error. */
10
16
  export interface DevError {
@@ -4,7 +4,7 @@
4
4
  * data); the compiler-driven loader resolves it to a {@link HeadSpec} that the router applies as the
5
5
  * route's baseline head (component-level `useHead`/`<Head>` still compose on top and can override).
6
6
  */
7
- import { useHead, type HeadSpec, type LinkTag, type MetaTag } from './head.js';
7
+ import { type HeadSpec, type LinkTag, type MetaTag, useHead } from './head.js';
8
8
  import type { RouteParams } from '../routing/match.js';
9
9
 
10
10
  /** OpenGraph fields, expanded to `og:*` meta tags. */
@@ -10,7 +10,13 @@
10
10
 
11
11
  export { mount } from './routing/mount.js';
12
12
  export { Router } from './routing/Router.js';
13
- export { Auth, register as authRegister, login as authLogin, buildLoginMessage, LOGIN_CONTEXT } from './auth.js';
13
+ export {
14
+ Auth,
15
+ register as authRegister,
16
+ login as authLogin,
17
+ buildLoginMessage,
18
+ LOGIN_CONTEXT,
19
+ } from './auth.js';
14
20
  export type { KdfParams, AuthOptions } from './auth.js';
15
21
  export { Link } from './navigation/Link.js';
16
22
  export type { LinkProps } from './navigation/Link.js';
@@ -36,7 +42,12 @@ export {
36
42
  useNavigationPending,
37
43
  } from './routing/hooks.js';
38
44
  export type { RouterInstance } from './routing/hooks.js';
39
- export { useLoaderData, revalidate, invalidateLoaderData, LoaderDataContext } from './routing/loader.js';
45
+ export {
46
+ useLoaderData,
47
+ revalidate,
48
+ invalidateLoaderData,
49
+ LoaderDataContext,
50
+ } from './routing/loader.js';
40
51
  export type {
41
52
  LoaderArgs,
42
53
  LoaderFunction,
@@ -1,4 +1,4 @@
1
- import { createElement, Suspense, useLayoutEffect, type ReactNode } from 'react';
1
+ import { createElement, type ReactNode, Suspense, useLayoutEffect } from 'react';
2
2
 
3
3
  import { ErrorBoundary } from './error-boundary.js';
4
4
  import { useLocation } from './hooks.js';
@@ -9,7 +9,7 @@ import {
9
9
  resolveLayout,
10
10
  resolveNotFound,
11
11
  } from './lazy.js';
12
- import { loaderKey, LoaderDataContext, readRouteData } from './loader.js';
12
+ import { LoaderDataContext, loaderKey, readRouteData } from './loader.js';
13
13
  import { matchRoute, type RouteParams } from './match.js';
14
14
  import { useRouteHead } from '../head/head.js';
15
15
  import { ParamsContext } from './params-context.js';
@@ -1,4 +1,4 @@
1
- import { Component, Suspense, type ComponentType, type ReactNode } from 'react';
1
+ import { Component, type ComponentType, type ReactNode, Suspense } from 'react';
2
2
 
3
3
  import type { RouteErrorProps } from '../types.js';
4
4
 
@@ -10,10 +10,10 @@
10
10
  * a number keeps data fresh for that many seconds; `false` caches until manual invalidation.
11
11
  * `revalidate()` / `router.refresh()` bust the cache to force a refetch.
12
12
  */
13
- import { createContext, useContext, type ComponentType } from 'react';
13
+ import { type ComponentType, createContext, useContext } from 'react';
14
14
 
15
15
  import type { HeadSpec } from '../head/head.js';
16
- import { resolveMetadata, type GenerateMetadata, type Metadata } from '../head/metadata.js';
16
+ import { type GenerateMetadata, type Metadata, resolveMetadata } from '../head/metadata.js';
17
17
  import { navigationEpoch, refresh as rerender } from '../navigation/navigation.js';
18
18
  import type { RouteDef } from '../types.js';
19
19
  import type { RouteParams } from './match.js';
@@ -1,11 +1,7 @@
1
1
  import { createRoot, hydrateRoot } from 'react-dom/client';
2
2
 
3
3
  import { DevToolbar } from '../dev/devtools.js';
4
- import {
5
- DevErrorBoundary,
6
- DevErrorOverlay,
7
- initDevErrorOverlay,
8
- } from '../dev/error-overlay.js';
4
+ import { DevErrorBoundary, DevErrorOverlay, initDevErrorOverlay } from '../dev/error-overlay.js';
9
5
  import { initNavigation } from '../navigation/navigation.js';
10
6
  import { startPrefetcher } from '../navigation/prefetch.js';
11
7
  import { hydrateLoaderData } from './loader.js';
@@ -73,7 +69,10 @@ export function mount(
73
69
  <>
74
70
  <DevErrorBoundary>{app}</DevErrorBoundary>
75
71
  <DevErrorOverlay />
76
- <DevToolbar routes={routes} slots={slots} />
72
+ <DevToolbar
73
+ routes={routes}
74
+ slots={slots}
75
+ />
77
76
  </>,
78
77
  );
79
78
  } else if (isSsrDocument()) {
@@ -346,7 +346,7 @@ export const TOIL_DOCS: Record<string, string> = {
346
346
  ' an `<Island>`. A route that cannot render this way is skipped at build (with a warning) and',
347
347
  ' simply falls back to normal client rendering.',
348
348
  '- Hole values are HTML-escaped exactly as React escapes them, so hydration is byte-for-byte',
349
- ' clean. Keep a repeat row\'s structure the same across items (only the leaf hole values vary).',
349
+ " clean. Keep a repeat row's structure the same across items (only the leaf hole values vary).",
350
350
  '- Build output for an SSR route lands in `build/client/_ssr/` (the template + its manifest)',
351
351
  ' alongside the generated `Slot` module; routes without `ssr = true` are unaffected.',
352
352
  ]),
@@ -13,7 +13,7 @@ import path from 'node:path';
13
13
  import type { ViteDevServer } from 'vite';
14
14
 
15
15
  import type { ResolvedToilConfig } from './config.js';
16
- import { renderEmailFile, toPascal, type RenderedEmail } from './emails.js';
16
+ import { type RenderedEmail, renderEmailFile, toPascal } from './emails.js';
17
17
 
18
18
  /** One discoverable email: its generated `Emails.<name>` and its absolute file. */
19
19
  export interface EmailListItem {
@@ -4,7 +4,7 @@ import path from 'node:path';
4
4
  import { type ResolvedToilConfig } from './config.js';
5
5
  import { writeDocs } from './docs.js';
6
6
  import { buildPageIndex, pagesModuleSource } from './pages.js';
7
- import { scanRoutes, type ScannedRoute } from './routes.js';
7
+ import { type ScannedRoute, scanRoutes } from './routes.js';
8
8
  import { llmsTxt, robotsTxt, sitemapXml } from './seo.js';
9
9
 
10
10
  /**
@@ -407,9 +407,7 @@ export function llmsTxt(
407
407
  const title = escapeMarkdownInline(page.title);
408
408
  const url = escapeMarkdownUrl(page.url);
409
409
  const desc =
410
- page.description !== undefined
411
- ? `: ${escapeMarkdownInline(page.description)}`
412
- : '';
410
+ page.description !== undefined ? `: ${escapeMarkdownInline(page.description)}` : '';
413
411
  out.push(`- [${title}](${url})${desc}`);
414
412
  }
415
413
  }