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.
- package/build/backend/.tsbuildinfo +1 -1
- package/build/cli/.tsbuildinfo +1 -1
- package/build/cli/index.js +9 -5
- 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 +3 -1
- 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/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.js +1 -1
- package/build/devserver/.tsbuildinfo +1 -1
- package/build/devserver/cache.js +0 -0
- package/build/devserver/crypto.js +45 -17
- package/build/devserver/database.js +82 -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/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 +11 -3
- package/examples/basic/server/routes/EnvDemo.ts +9 -3
- package/package.json +1 -1
- package/src/backend/index.ts +4 -2
- package/src/cli/doctor.ts +10 -3
- 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 +121 -54
- 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/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 +2 -7
- 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 +149 -8
- 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 +14 -5
- 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/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 {
|
|
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(
|
|
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
|
+
|
package/src/cli/version-check.ts
CHANGED
|
@@ -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(
|
|
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
|
|
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"
|
|
@@ -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(): {
|
|
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
|
|
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
|
|
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
|
|
529
|
+
<p
|
|
530
|
+
className="toil-dt-sec"
|
|
531
|
+
style={{ marginTop: 10 }}>
|
|
495
532
|
SEO checklist
|
|
496
533
|
</p>
|
|
497
|
-
<Check
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
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
|
|
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
|
|
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)
|
|
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
|
-
|
|
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) => (
|
|
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
|
-
|
|
997
|
-
className={`toil-dt-tab ${p.tab === t.id ? 'active' : ''}`}
|
|
1040
|
+
className="toil-dt-x"
|
|
998
1041
|
onClick={() => {
|
|
999
|
-
setPrefs({
|
|
1042
|
+
setPrefs({ open: false });
|
|
1000
1043
|
}}>
|
|
1001
|
-
|
|
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
|
-
|
|
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 {
|
|
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';
|
|
@@ -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
|
}
|