toiljs 0.0.55 → 0.0.57
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/README.md +72 -14
- package/build/backend/.tsbuildinfo +1 -1
- package/build/cli/.tsbuildinfo +1 -1
- package/build/cli/index.js +293 -142
- package/build/client/.tsbuildinfo +1 -1
- package/build/client/auth.js +1 -1
- package/build/client/components/Image.d.ts +1 -1
- package/build/client/dev/devtools.js +4 -2
- package/build/client/index.d.ts +2 -2
- package/build/client/index.js +2 -2
- package/build/client/routing/Router.js +1 -1
- package/build/client/routing/hooks.js +2 -2
- package/build/client/routing/mount.js +1 -1
- package/build/compiler/.tsbuildinfo +1 -1
- package/build/compiler/docs.js +1 -1
- package/build/compiler/seo.js +1 -3
- package/build/compiler/template-build.d.ts +5 -2
- package/build/compiler/template-build.js +19 -7
- package/build/devserver/.tsbuildinfo +1 -1
- package/build/devserver/cache.js +0 -0
- package/build/devserver/crypto.js +45 -17
- package/build/devserver/database.d.ts +1 -1
- package/build/devserver/database.js +84 -0
- package/build/devserver/email/caps.js +0 -0
- package/build/devserver/email/config.js +7 -2
- package/build/devserver/email/validate.js +1 -4
- package/build/devserver/host.js +18 -1
- package/build/devserver/index.d.ts +1 -1
- package/build/devserver/index.js +3 -2
- package/build/devserver/module.js +51 -12
- package/build/devserver/proxy.js +2 -1
- package/build/io/.tsbuildinfo +1 -1
- package/build/io/codec.d.ts +5 -5
- package/build/io/codec.js +193 -77
- package/examples/basic/client/components/HoneycombBackground.tsx +1 -1
- package/examples/basic/client/public/images/logo.svg +37 -34
- package/examples/basic/client/public/index.html +14 -14
- package/examples/basic/client/routes/auth.tsx +18 -10
- package/examples/basic/client/routes/cookies.tsx +15 -24
- package/examples/basic/client/routes/crypto.tsx +4 -5
- package/examples/basic/client/routes/features/template/template.tsx +1 -1
- package/examples/basic/client/routes/hello.tsx +1 -1
- package/examples/basic/client/routes/pq.tsx +14 -14
- package/examples/basic/client/routes/rest.tsx +1 -3
- package/examples/basic/client/styles/main.css +25 -22
- package/examples/basic/client/toil.tsx +1 -1
- package/examples/basic/server/README.md +8 -8
- package/examples/basic/server/core/AppHandler.ts +4 -7
- package/examples/basic/server/routes/Auth.ts +13 -10
- package/examples/basic/server/routes/EnvDemo.ts +9 -3
- package/examples/basic/server/routes/Guestbook.ts +2 -4
- package/package.json +26 -26
- package/src/backend/index.ts +4 -2
- package/src/cli/create.ts +19 -4
- package/src/cli/diagnostics.ts +48 -0
- package/src/cli/doctor.ts +155 -9
- package/src/cli/notify.ts +1 -6
- package/src/cli/ui.ts +3 -3
- package/src/cli/version-check.ts +5 -1
- package/src/client/auth.ts +33 -10
- package/src/client/components/Form.tsx +2 -2
- package/src/client/components/Image.tsx +1 -1
- package/src/client/components/Script.tsx +1 -1
- package/src/client/components/Slot.tsx +1 -1
- package/src/client/dev/devtools.tsx +126 -55
- package/src/client/dev/error-overlay.tsx +7 -1
- package/src/client/head/metadata.ts +1 -1
- package/src/client/index.ts +13 -2
- package/src/client/routing/Router.tsx +2 -2
- package/src/client/routing/error-boundary.tsx +1 -1
- package/src/client/routing/hooks.ts +5 -3
- package/src/client/routing/loader.ts +2 -2
- package/src/client/routing/mount.tsx +5 -6
- package/src/compiler/docs.ts +1 -1
- package/src/compiler/email-preview.ts +1 -1
- package/src/compiler/generate.ts +1 -1
- package/src/compiler/seo.ts +1 -3
- package/src/compiler/ssg.ts +10 -4
- package/src/compiler/template-build.ts +43 -11
- package/src/compiler/template.ts +1 -4
- package/src/compiler/vite.ts +1 -1
- package/src/devserver/cache.ts +0 -0
- package/src/devserver/crypto.ts +140 -51
- package/src/devserver/database.ts +168 -9
- package/src/devserver/dotenv.ts +10 -2
- package/src/devserver/email/caps.ts +0 -0
- package/src/devserver/email/config.ts +8 -2
- package/src/devserver/email/index.ts +3 -3
- package/src/devserver/email/validate.ts +1 -4
- package/src/devserver/envelope.ts +3 -3
- package/src/devserver/host.ts +46 -6
- package/src/devserver/index.ts +15 -6
- package/src/devserver/module.ts +56 -14
- package/src/devserver/proxy.ts +5 -7
- package/src/io/codec.ts +226 -83
- package/test/devserver-database.test.ts +60 -0
- package/test/devserver-secrets.test.ts +59 -0
- package/test/doctor.test.ts +30 -0
package/src/client/auth.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* JSON, byte-identical to the server's `AuthService.buildLoginMessage`.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import { argon2id,
|
|
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(
|
|
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(
|
|
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(
|
|
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,
|
|
303
|
-
|
|
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(
|
|
353
|
+
const sessionKey = await hmacSha256(
|
|
354
|
+
sharedSecret,
|
|
355
|
+
concatBytes(utf8(SESSION_KEY_LABEL), transcriptHash),
|
|
356
|
+
);
|
|
337
357
|
wipe(sharedSecret);
|
|
338
|
-
const expected = await hmacSha256(
|
|
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 {
|
|
1
|
+
import { type ReactNode, type SyntheticEvent, useRef } from 'react';
|
|
2
2
|
|
|
3
|
-
import {
|
|
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 {
|
|
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.
|
|
@@ -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
|
|
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
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
68
|
-
|
|
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
|
|
72
|
-
|
|
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"
|
|
@@ -143,7 +171,11 @@ function loadPrefs(): Prefs {
|
|
|
143
171
|
}
|
|
144
172
|
}
|
|
145
173
|
|
|
146
|
-
|
|
174
|
+
// Gate on `window`, not `localStorage`: the devtools are browser-only, and merely
|
|
175
|
+
// touching the bare `localStorage` global under SSR/Node trips its experimental-API
|
|
176
|
+
// warning ("localStorage is not available because --localstorage-file ..."). In the
|
|
177
|
+
// browser `loadPrefs()` reads it; under SSR we keep the defaults and never touch it.
|
|
178
|
+
let prefs: Prefs = typeof window !== 'undefined' ? loadPrefs() : defaultPrefs;
|
|
147
179
|
const prefListeners = new Set<() => void>();
|
|
148
180
|
function setPrefs(next: Partial<Prefs>): void {
|
|
149
181
|
prefs = { ...prefs, ...next };
|
|
@@ -194,7 +226,10 @@ function safeJson(value: unknown): string {
|
|
|
194
226
|
}
|
|
195
227
|
|
|
196
228
|
/** Reads the current document head's meta + link tags (live). */
|
|
197
|
-
function readHead(): {
|
|
229
|
+
function readHead(): {
|
|
230
|
+
metas: { name: string; content: string }[];
|
|
231
|
+
links: { rel: string; href: string }[];
|
|
232
|
+
} {
|
|
198
233
|
const metas: { name: string; content: string }[] = [];
|
|
199
234
|
const links: { rel: string; href: string }[] = [];
|
|
200
235
|
if (typeof document === 'undefined') return { metas, links };
|
|
@@ -346,7 +381,9 @@ function RouteTab({
|
|
|
346
381
|
<Row k="slots">{activeSlots.length ? activeSlots.join(', ') : 'none'}</Row>
|
|
347
382
|
<Row k="navigating">{pending ? 'yes' : 'no'}</Row>
|
|
348
383
|
|
|
349
|
-
<p
|
|
384
|
+
<p
|
|
385
|
+
className="toil-dt-sec"
|
|
386
|
+
style={{ marginTop: 12 }}>
|
|
350
387
|
Routes ({routes.length})
|
|
351
388
|
</p>
|
|
352
389
|
{routes.map((r) => {
|
|
@@ -474,7 +511,9 @@ function HeadTab(): ReactNode {
|
|
|
474
511
|
<div className="toil-dt-body">
|
|
475
512
|
<Row k="title">{title || '(none)'}</Row>
|
|
476
513
|
|
|
477
|
-
<p
|
|
514
|
+
<p
|
|
515
|
+
className="toil-dt-sec"
|
|
516
|
+
style={{ marginTop: 10 }}>
|
|
478
517
|
OpenGraph preview
|
|
479
518
|
</p>
|
|
480
519
|
<div className="toil-dt-og">
|
|
@@ -491,23 +530,41 @@ function HeadTab(): ReactNode {
|
|
|
491
530
|
</div>
|
|
492
531
|
</div>
|
|
493
532
|
|
|
494
|
-
<p
|
|
533
|
+
<p
|
|
534
|
+
className="toil-dt-sec"
|
|
535
|
+
style={{ marginTop: 10 }}>
|
|
495
536
|
SEO checklist
|
|
496
537
|
</p>
|
|
497
|
-
<Check
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
538
|
+
<Check
|
|
539
|
+
ok={Boolean(title)}
|
|
540
|
+
label="Has a title"
|
|
541
|
+
/>
|
|
542
|
+
<Check
|
|
543
|
+
ok={meta('description') !== undefined}
|
|
544
|
+
label="Has a meta description"
|
|
545
|
+
/>
|
|
546
|
+
<Check
|
|
547
|
+
ok={og.image !== undefined}
|
|
548
|
+
label="Has an og:image"
|
|
549
|
+
/>
|
|
550
|
+
<Check
|
|
551
|
+
ok={links.some((l) => l.rel === 'canonical')}
|
|
552
|
+
label="Has a canonical link"
|
|
553
|
+
/>
|
|
501
554
|
<Check
|
|
502
555
|
ok={pages.length === 0 || described === pages.length}
|
|
503
556
|
label={`Pages with a description: ${String(described)}/${String(pages.length)}`}
|
|
504
557
|
/>
|
|
505
558
|
|
|
506
|
-
<p
|
|
559
|
+
<p
|
|
560
|
+
className="toil-dt-sec"
|
|
561
|
+
style={{ marginTop: 10 }}>
|
|
507
562
|
Meta ({metas.length})
|
|
508
563
|
</p>
|
|
509
564
|
{metas.map((m, i) => (
|
|
510
|
-
<Row
|
|
565
|
+
<Row
|
|
566
|
+
k={m.name}
|
|
567
|
+
key={`${m.name}:${String(i)}`}>
|
|
511
568
|
{m.content}
|
|
512
569
|
</Row>
|
|
513
570
|
))}
|
|
@@ -546,7 +603,8 @@ function BuildTab({ info }: { info: DevInfo | null }): ReactNode {
|
|
|
546
603
|
|
|
547
604
|
function ErrorsTab(): ReactNode {
|
|
548
605
|
const errors = useErrors();
|
|
549
|
-
if (errors.length === 0)
|
|
606
|
+
if (errors.length === 0)
|
|
607
|
+
return <p className="toil-dt-empty toil-dt-body">No errors captured.</p>;
|
|
550
608
|
return (
|
|
551
609
|
<div className="toil-dt-body">
|
|
552
610
|
{[...errors].reverse().map((e, i) => (
|
|
@@ -974,45 +1032,58 @@ export function DevToolbar({
|
|
|
974
1032
|
|
|
975
1033
|
return (
|
|
976
1034
|
<>
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
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) => (
|
|
1035
|
+
<div className={`toil-dt ${p.side}`}>
|
|
1036
|
+
<div className="toil-dt-panel">
|
|
1037
|
+
<div className="toil-dt-head">
|
|
1038
|
+
<span style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
|
1039
|
+
<ToilLogo size={14} />
|
|
1040
|
+
<span className="toil-dt-logo">toiljs</span> devtools
|
|
1041
|
+
<span className={`toil-dt-dot ${dotClass}`} />
|
|
1042
|
+
</span>
|
|
995
1043
|
<button
|
|
996
|
-
|
|
997
|
-
className={`toil-dt-tab ${p.tab === t.id ? 'active' : ''}`}
|
|
1044
|
+
className="toil-dt-x"
|
|
998
1045
|
onClick={() => {
|
|
999
|
-
setPrefs({
|
|
1046
|
+
setPrefs({ open: false });
|
|
1000
1047
|
}}>
|
|
1001
|
-
|
|
1002
|
-
{t.id === 'errors' && errors.length > 0 ? ` (${String(errors.length)})` : ''}
|
|
1048
|
+
✕
|
|
1003
1049
|
</button>
|
|
1004
|
-
|
|
1050
|
+
</div>
|
|
1051
|
+
<div className="toil-dt-tabs">
|
|
1052
|
+
{TABS.map((t) => (
|
|
1053
|
+
<button
|
|
1054
|
+
key={t.id}
|
|
1055
|
+
className={`toil-dt-tab ${p.tab === t.id ? 'active' : ''}`}
|
|
1056
|
+
onClick={() => {
|
|
1057
|
+
setPrefs({ tab: t.id });
|
|
1058
|
+
}}>
|
|
1059
|
+
{t.label}
|
|
1060
|
+
{t.id === 'errors' && errors.length > 0
|
|
1061
|
+
? ` (${String(errors.length)})`
|
|
1062
|
+
: ''}
|
|
1063
|
+
</button>
|
|
1064
|
+
))}
|
|
1065
|
+
</div>
|
|
1066
|
+
{p.tab === 'route' && (
|
|
1067
|
+
<RouteTab
|
|
1068
|
+
routes={routes}
|
|
1069
|
+
slots={slots}
|
|
1070
|
+
info={info}
|
|
1071
|
+
/>
|
|
1072
|
+
)}
|
|
1073
|
+
{p.tab === 'data' && <DataTab />}
|
|
1074
|
+
{p.tab === 'head' && <HeadTab />}
|
|
1075
|
+
{p.tab === 'build' && <BuildTab info={info} />}
|
|
1076
|
+
{p.tab === 'errors' && <ErrorsTab />}
|
|
1077
|
+
{p.tab === 'ai' && (
|
|
1078
|
+
<AiTab
|
|
1079
|
+
info={info}
|
|
1080
|
+
routes={routes}
|
|
1081
|
+
/>
|
|
1082
|
+
)}
|
|
1083
|
+
{p.tab === 'prefs' && <PrefsTab />}
|
|
1005
1084
|
</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
1085
|
</div>
|
|
1014
|
-
|
|
1015
|
-
{pal}
|
|
1086
|
+
{pal}
|
|
1016
1087
|
</>
|
|
1017
1088
|
);
|
|
1018
1089
|
}
|
|
@@ -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 {
|
|
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 {
|
|
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. */
|
package/src/client/index.ts
CHANGED
|
@@ -10,7 +10,13 @@
|
|
|
10
10
|
|
|
11
11
|
export { mount } from './routing/mount.js';
|
|
12
12
|
export { Router } from './routing/Router.js';
|
|
13
|
-
export {
|
|
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 {
|
|
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
|
|
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 {
|
|
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';
|
|
@@ -101,10 +101,12 @@ function useLocationSubscription(): void {
|
|
|
101
101
|
);
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
/** Subscribes to and returns the current `location.pathname`.
|
|
104
|
+
/** Subscribes to and returns the current `location.pathname`. SSR-safe: during a
|
|
105
|
+
* server render (build-time template extraction / edge SSR) there is no `window`,
|
|
106
|
+
* so it reports `/`; the client recomputes on hydration. */
|
|
105
107
|
export function useLocation(): string {
|
|
106
108
|
useLocationSubscription();
|
|
107
|
-
return window.location.pathname;
|
|
109
|
+
return typeof window === 'undefined' ? '/' : window.location.pathname;
|
|
108
110
|
}
|
|
109
111
|
|
|
110
112
|
/** Alias of {@link useLocation}: the current `location.pathname`. */
|
|
@@ -115,7 +117,7 @@ export function usePathname(): string {
|
|
|
115
117
|
/** The current query string as a `URLSearchParams`, re-read on every navigation. */
|
|
116
118
|
export function useSearchParams(): URLSearchParams {
|
|
117
119
|
useLocationSubscription();
|
|
118
|
-
const search = window.location.search;
|
|
120
|
+
const search = typeof window === 'undefined' ? '' : window.location.search;
|
|
119
121
|
return useMemo(() => new URLSearchParams(search), [search]);
|
|
120
122
|
}
|
|
121
123
|
|
|
@@ -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
|
|
13
|
+
import { type ComponentType, createContext, useContext } from 'react';
|
|
14
14
|
|
|
15
15
|
import type { HeadSpec } from '../head/head.js';
|
|
16
|
-
import {
|
|
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
|
|
72
|
+
<DevToolbar
|
|
73
|
+
routes={routes}
|
|
74
|
+
slots={slots}
|
|
75
|
+
/>
|
|
77
76
|
</>,
|
|
78
77
|
);
|
|
79
78
|
} else if (isSsrDocument()) {
|
package/src/compiler/docs.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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 {
|
package/src/compiler/generate.ts
CHANGED
|
@@ -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 {
|
|
7
|
+
import { type ScannedRoute, scanRoutes } from './routes.js';
|
|
8
8
|
import { llmsTxt, robotsTxt, sitemapXml } from './seo.js';
|
|
9
9
|
|
|
10
10
|
/**
|
package/src/compiler/seo.ts
CHANGED
|
@@ -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
|
}
|
package/src/compiler/ssg.ts
CHANGED
|
@@ -16,7 +16,7 @@ import { createServer } from 'vite';
|
|
|
16
16
|
import { type ResolvedToilConfig } from './config.js';
|
|
17
17
|
import { extractStaticMetadata, loadTypeScript } from './prerender.js';
|
|
18
18
|
import { scanRoutes } from './routes.js';
|
|
19
|
-
import { injectSeoHtml, joinUrl, llmsTxt, routeSeo, sitemapXml
|
|
19
|
+
import { injectSeoHtml, joinUrl, type LlmsPage, llmsTxt, routeSeo, sitemapXml } from './seo.js';
|
|
20
20
|
import { createViteConfig } from './vite.js';
|
|
21
21
|
|
|
22
22
|
/** Reads a string field off a metadata record, or undefined. */
|
|
@@ -91,7 +91,9 @@ export async function prerenderStaticParams(cfg: ResolvedToilConfig): Promise<st
|
|
|
91
91
|
try {
|
|
92
92
|
mod = (await server.ssrLoadModule(route.file)) as RouteModule;
|
|
93
93
|
} catch (err) {
|
|
94
|
-
warn(
|
|
94
|
+
warn(
|
|
95
|
+
`skipped ${route.pattern} (${err instanceof Error ? err.message : String(err)})`,
|
|
96
|
+
);
|
|
95
97
|
continue;
|
|
96
98
|
}
|
|
97
99
|
if (typeof mod.generateStaticParams !== 'function') continue;
|
|
@@ -116,12 +118,16 @@ export async function prerenderStaticParams(cfg: ResolvedToilConfig): Promise<st
|
|
|
116
118
|
typeof mod.loader === 'function'
|
|
117
119
|
? await mod.loader({ params, searchParams })
|
|
118
120
|
: undefined;
|
|
119
|
-
metadata = asMetadata(
|
|
121
|
+
metadata = asMetadata(
|
|
122
|
+
await mod.generateMetadata({ params, searchParams, data }),
|
|
123
|
+
);
|
|
120
124
|
} else if (mod.metadata) {
|
|
121
125
|
metadata = asMetadata(mod.metadata);
|
|
122
126
|
}
|
|
123
127
|
} catch (err) {
|
|
124
|
-
warn(
|
|
128
|
+
warn(
|
|
129
|
+
`metadata failed for ${url} (${err instanceof Error ? err.message : String(err)})`,
|
|
130
|
+
);
|
|
125
131
|
}
|
|
126
132
|
const html = injectSeoHtml(shell, routeSeo(cfg.seo, metadata, url));
|
|
127
133
|
fs.mkdirSync(path.dirname(target), { recursive: true });
|