toiljs 0.0.59 → 0.0.61
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +31 -0
- package/CHANGELOG.md +15 -0
- package/build/cli/.tsbuildinfo +1 -1
- package/build/cli/index.js +311 -118
- package/build/client/.tsbuildinfo +1 -1
- package/build/client/index.d.ts +1 -1
- package/build/client/index.js +1 -1
- package/build/client/routing/mount.js +12 -1
- package/build/client/ssr/markers.d.ts +1 -0
- package/build/client/ssr/markers.js +3 -0
- package/build/compiler/.tsbuildinfo +1 -1
- package/build/compiler/config.d.ts +21 -0
- package/build/compiler/config.js +35 -0
- package/build/compiler/docs.d.ts +2 -1
- package/build/compiler/docs.js +33 -304
- package/build/compiler/index.d.ts +13 -0
- package/build/compiler/index.js +113 -21
- package/build/compiler/template-build.d.ts +21 -1
- package/build/compiler/template-build.js +110 -26
- package/build/compiler/toil-docs.generated.d.ts +1 -0
- package/build/compiler/toil-docs.generated.js +20 -0
- package/build/devserver/.tsbuildinfo +1 -1
- package/build/devserver/daemon/catalog.d.ts +26 -0
- package/build/devserver/daemon/catalog.js +48 -0
- package/build/devserver/daemon/cron.d.ts +4 -0
- package/build/devserver/daemon/cron.js +50 -0
- package/build/devserver/daemon/host.d.ts +37 -0
- package/build/devserver/daemon/host.js +94 -0
- package/build/devserver/daemon/index.d.ts +34 -0
- package/build/devserver/daemon/index.js +241 -0
- package/build/devserver/db/catalog.d.ts +2 -0
- package/build/devserver/db/catalog.js +80 -0
- package/build/devserver/db/database.d.ts +80 -0
- package/build/devserver/db/database.js +1032 -0
- package/build/devserver/db/index.d.ts +3 -0
- package/build/devserver/db/index.js +3 -0
- package/build/devserver/db/routeKinds.d.ts +8 -0
- package/build/devserver/db/routeKinds.js +139 -0
- package/build/devserver/db/types.d.ts +121 -0
- package/build/devserver/db/types.js +52 -0
- package/build/devserver/email/index.js +1 -1
- package/build/devserver/index.d.ts +19 -24
- package/build/devserver/index.js +11 -165
- package/build/devserver/mstore/store.d.ts +18 -0
- package/build/devserver/mstore/store.js +82 -0
- package/build/devserver/{host.d.ts → runtime/host.d.ts} +7 -1
- package/build/devserver/{host.js → runtime/host.js} +51 -7
- package/build/devserver/{module.d.ts → runtime/module.d.ts} +2 -1
- package/build/devserver/{module.js → runtime/module.js} +34 -1
- package/build/devserver/server.d.ts +23 -0
- package/build/devserver/server.js +223 -0
- package/build/devserver/ssr.d.ts +25 -0
- package/build/devserver/ssr.js +114 -0
- package/build/devserver/wasm/sections.d.ts +2 -0
- package/build/devserver/wasm/sections.js +42 -0
- package/build/devserver/wasm/surface.d.ts +18 -0
- package/build/devserver/wasm/surface.js +41 -0
- package/docs/README.md +4 -4
- package/docs/auth-todo.md +6 -6
- package/docs/caching.md +5 -5
- package/docs/cli.md +15 -0
- package/docs/client.md +40 -0
- package/docs/crypto.md +4 -4
- package/docs/data.md +6 -6
- package/docs/email.md +28 -28
- package/docs/environment.md +10 -10
- package/docs/index.md +26 -0
- package/docs/ratelimit.md +10 -10
- package/docs/routing.md +2 -2
- package/docs/server.md +61 -0
- package/docs/ssr.md +561 -113
- package/docs/styling.md +22 -0
- package/docs/time.md +3 -3
- package/eslint.config.js +10 -1
- package/examples/basic/client/components/Header.tsx +3 -0
- package/examples/basic/client/routes/features/actions.tsx +0 -2
- package/examples/basic/client/routes/hello.tsx +89 -19
- package/examples/basic/client/styles/main.css +48 -0
- package/examples/basic/server/SsrHelloRender.ts +97 -0
- package/examples/basic/server/main.ts +5 -0
- package/examples/basic/server/migrations/GuestEntry.migration.ts +39 -0
- package/examples/basic/server/streams/Echo.ts +49 -0
- package/package.json +12 -10
- package/scripts/gen-toil-docs.mjs +96 -0
- package/server/runtime/time.ts +3 -3
- package/src/cli/create.ts +40 -3
- package/src/cli/db.ts +158 -0
- package/src/cli/diagnostics.ts +19 -0
- package/src/cli/doctor.ts +20 -0
- package/src/cli/index.ts +10 -0
- package/src/cli/update.ts +58 -0
- package/src/client/index.ts +1 -1
- package/src/client/routing/mount.tsx +18 -2
- package/src/client/ssr/markers.tsx +22 -0
- package/src/compiler/config.ts +88 -2
- package/src/compiler/docs.ts +47 -308
- package/src/compiler/index.ts +236 -32
- package/src/compiler/ssr-codegen.ts +1 -1
- package/src/compiler/template-build.ts +247 -46
- package/src/compiler/toil-docs.generated.ts +26 -0
- package/src/devserver/daemon/catalog.ts +120 -0
- package/src/devserver/daemon/cron.ts +87 -0
- package/src/devserver/daemon/host.ts +224 -0
- package/src/devserver/daemon/index.ts +349 -0
- package/src/devserver/db/catalog.ts +108 -0
- package/src/devserver/db/database.ts +1633 -0
- package/src/devserver/db/index.ts +18 -0
- package/src/devserver/db/routeKinds.ts +147 -0
- package/src/devserver/db/types.ts +139 -0
- package/src/devserver/email/index.ts +1 -1
- package/src/devserver/index.ts +31 -287
- package/src/devserver/mstore/store.ts +121 -0
- package/src/devserver/{host.ts → runtime/host.ts} +98 -7
- package/src/devserver/{module.ts → runtime/module.ts} +47 -1
- package/src/devserver/server.ts +393 -0
- package/src/devserver/ssr.ts +166 -0
- package/src/devserver/wasm/sections.ts +59 -0
- package/src/devserver/wasm/surface.ts +88 -0
- package/test/daemon-build.test.ts +198 -0
- package/test/daemon-catalog.test.ts +265 -0
- package/test/daemon-emulation.test.ts +216 -0
- package/test/db.test.ts +0 -0
- package/test/devserver-database.test.ts +510 -14
- package/test/devserver-pqauth.test.ts +1 -1
- package/test/devserver-secrets.test.ts +5 -1
- package/test/doctor.test.ts +13 -0
- package/test/email-preview.test.ts +6 -1
- package/test/example-guestbook.test.ts +43 -1
- package/test/fixtures/daemon-app.ts +56 -0
- package/test/global-setup.ts +17 -0
- package/test/pqauth-e2e.test.ts +1 -1
- package/test/ssr-render.test.ts +94 -27
- package/test/ssr-template.test.tsx +44 -1
- package/vitest.config.ts +3 -0
- package/build/devserver/database.d.ts +0 -8
- package/build/devserver/database.js +0 -418
- package/src/devserver/database.ts +0 -618
- /package/build/devserver/{dotenv.d.ts → config/dotenv.d.ts} +0 -0
- /package/build/devserver/{dotenv.js → config/dotenv.js} +0 -0
- /package/build/devserver/{env.d.ts → config/env.d.ts} +0 -0
- /package/build/devserver/{env.js → config/env.js} +0 -0
- /package/build/devserver/{ratelimit.d.ts → config/ratelimit.d.ts} +0 -0
- /package/build/devserver/{ratelimit.js → config/ratelimit.js} +0 -0
- /package/build/devserver/{cache.d.ts → http/cache.d.ts} +0 -0
- /package/build/devserver/{cache.js → http/cache.js} +0 -0
- /package/build/devserver/{envelope.d.ts → http/envelope.d.ts} +0 -0
- /package/build/devserver/{envelope.js → http/envelope.js} +0 -0
- /package/build/devserver/{proxy.d.ts → http/proxy.d.ts} +0 -0
- /package/build/devserver/{proxy.js → http/proxy.js} +0 -0
- /package/build/devserver/{crypto.d.ts → runtime/crypto.d.ts} +0 -0
- /package/build/devserver/{crypto.js → runtime/crypto.js} +0 -0
- /package/src/devserver/{dotenv.ts → config/dotenv.ts} +0 -0
- /package/src/devserver/{env.ts → config/env.ts} +0 -0
- /package/src/devserver/{ratelimit.ts → config/ratelimit.ts} +0 -0
- /package/src/devserver/{cache.ts → http/cache.ts} +0 -0
- /package/src/devserver/{envelope.ts → http/envelope.ts} +0 -0
- /package/src/devserver/{proxy.ts → http/proxy.ts} +0 -0
- /package/src/devserver/{crypto.ts → runtime/crypto.ts} +0 -0
package/src/cli/create.ts
CHANGED
|
@@ -40,6 +40,38 @@ import { isPackageManager, isValidName, resolveProjectDir } from './validate.js'
|
|
|
40
40
|
|
|
41
41
|
export type Template = 'app' | 'minimal';
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* The `server/migrations/` README, scaffolded by `create` and ensured by `doctor`/`update`. ToilDB
|
|
45
|
+
* migrations are enforced by convention: a `@migrate` function MUST live in a `*.migration.ts` file
|
|
46
|
+
* under a `migrations/` folder (the toilscript compiler raises a compile error otherwise), and the
|
|
47
|
+
* build auto-discovers every such file. Keeping the convention documented next to the folder means a
|
|
48
|
+
* fresh project ships with a place to evolve `@data` value types from day one.
|
|
49
|
+
*/
|
|
50
|
+
export const MIGRATIONS_README =
|
|
51
|
+
'# migrations/\n\n' +
|
|
52
|
+
'ToilDB schema migrations. One file per evolving `@data` value type, named `<Type>.migration.ts`\n' +
|
|
53
|
+
'(e.g. `User.migration.ts`).\n\n' +
|
|
54
|
+
'Each file keeps the OLD `@data` shapes (e.g. `UserV1`) alongside the `@migrate` transform(s) that\n' +
|
|
55
|
+
'carry old records forward, and imports the CURRENT value type from the app. The build\n' +
|
|
56
|
+
'auto-discovers every `*.migration.ts` under the project.\n\n' +
|
|
57
|
+
'Do NOT put `@migrate` anywhere else: a `@migrate` outside a `*.migration.ts` file in a\n' +
|
|
58
|
+
'`migrations/` folder is a compile error.\n\n' +
|
|
59
|
+
'## Example\n\n' +
|
|
60
|
+
'`User.migration.ts`:\n\n' +
|
|
61
|
+
'```ts\n' +
|
|
62
|
+
"import { User } from '../models/User';\n\n" +
|
|
63
|
+
'// The previous on-disk shape of a User record.\n' +
|
|
64
|
+
'@data\n' +
|
|
65
|
+
'export class UserV1 {\n' +
|
|
66
|
+
" name: string = '';\n" +
|
|
67
|
+
'}\n\n' +
|
|
68
|
+
'// Carry a UserV1 forward into the current User. Runs once per record on read.\n' +
|
|
69
|
+
'@migrate\n' +
|
|
70
|
+
'export function up(old: UserV1, into: User): void {\n' +
|
|
71
|
+
' into.name = old.name;\n' +
|
|
72
|
+
'}\n' +
|
|
73
|
+
'```\n';
|
|
74
|
+
|
|
43
75
|
/** Human label for each preprocessor in the styling picker. */
|
|
44
76
|
const PREPROCESSOR_LABEL: Record<Preprocessor, string> = {
|
|
45
77
|
css: 'Plain CSS',
|
|
@@ -114,7 +146,7 @@ function scaffold(
|
|
|
114
146
|
'@types/react-dom': '^19.2.3',
|
|
115
147
|
eslint: '^10.2.0',
|
|
116
148
|
prettier: '^3.8.1',
|
|
117
|
-
toilscript: '^0.1.
|
|
149
|
+
toilscript: '^0.1.37',
|
|
118
150
|
typescript: '^6.0.3',
|
|
119
151
|
};
|
|
120
152
|
for (const dep of requiredPackages(features).sort()) {
|
|
@@ -163,9 +195,9 @@ function scaffold(
|
|
|
163
195
|
// Generated files don't need formatting. (toilscript server decorators like @main /
|
|
164
196
|
// @remote-on-functions are handled by the toiljs/prettier-plugin, so server/ is not ignored.)
|
|
165
197
|
'.prettierignore':
|
|
166
|
-
'node_modules\nbuild\n.toil\nshared/server.ts\ntoil-env.d.ts\ntoil-routes.d.ts\nserver/_emails.ts\nserver/toil-server-env.d.ts\n',
|
|
198
|
+
'node_modules\nbuild\n.toil\nshared/server.ts\ntoil-env.d.ts\ntoil-routes.d.ts\nserver/_emails.ts\nserver/_ssr/\nserver/toil-server-env.d.ts\n',
|
|
167
199
|
'.gitignore':
|
|
168
|
-
'node_modules\nbuild\n.toil\nshared/server.ts\ntoil-env.d.ts\ntoil-routes.d.ts\nhosts/*/_tmpl/\n# Local dev env vars/secrets (never commit)\n.env\n.env.secrets\n',
|
|
200
|
+
'node_modules\nbuild\n.toil\nshared/server.ts\ntoil-env.d.ts\ntoil-routes.d.ts\nserver/_ssr/\nhosts/*/_tmpl/\n# Local dev env vars/secrets (never commit)\n.env\n.env.secrets\n',
|
|
169
201
|
// Use the project's pinned TypeScript (node_modules) instead of VS Code's bundled
|
|
170
202
|
// version, and prompt to switch, so the editor loads the toilscript LS plugin wired
|
|
171
203
|
// in server/tsconfig.json (which clears the @database / @data editor false positives).
|
|
@@ -259,6 +291,10 @@ function scaffold(
|
|
|
259
291
|
' npm run build',
|
|
260
292
|
'',
|
|
261
293
|
].join('\n'),
|
|
294
|
+
// ToilDB migrations live under server/migrations/ (folder + `*.migration.ts` is enforced by
|
|
295
|
+
// the compiler; the build auto-discovers them). Ship the folder, documented, with no live
|
|
296
|
+
// *.migration.ts: a @migrate referencing types a fresh project lacks would break the build.
|
|
297
|
+
'server/migrations/README.md': MIGRATIONS_README,
|
|
262
298
|
};
|
|
263
299
|
|
|
264
300
|
// The `app` template's client UI + server are copied from examples/basic at runtime; `minimal`
|
|
@@ -377,6 +413,7 @@ function minimalServer(): Record<string, string> {
|
|
|
377
413
|
'| `main.ts` | The entry point: wires the handler and imports the surface modules. |\n' +
|
|
378
414
|
'| `core/` | The request handler and shared app logic (state, helpers). |\n' +
|
|
379
415
|
'| `models/` | `@data` classes, the typed wire model shared by HTTP and RPC. One type per file. |\n' +
|
|
416
|
+
'| `migrations/` | ToilDB schema migrations: a `<Type>.migration.ts` per evolving `@data` value type, holding the old shapes + the `@migrate` transform. |\n' +
|
|
380
417
|
'| `routes/` | `@rest` controllers (HTTP). One controller per file, named after its class. |\n' +
|
|
381
418
|
'| `services/` | `@service` classes and free `@remote` functions (typed RPC). |\n' +
|
|
382
419
|
'| `scheduled/` | Reserved for scheduled tasks (not shipped yet). |\n\n' +
|
package/src/cli/db.ts
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `toiljs db <action>` — manage the dev server's on-disk ToilDB data.
|
|
3
|
+
*
|
|
4
|
+
* Under `toiljs dev`, the in-process ToilDB emulator persists every family
|
|
5
|
+
* (records / views / unique / events / membership / counter / capacity) plus each
|
|
6
|
+
* row's schema_version to `<root>/.toil/devdata.json` (so data + migrations survive
|
|
7
|
+
* restarts). These subcommands let you inspect, reset, snapshot, and restore that
|
|
8
|
+
* store from outside a running server — handy for fixtures, sharing repro data, or
|
|
9
|
+
* wiping a corrupt dev state. The snapshot is the exact JSON the dev DB writes, so
|
|
10
|
+
* an exported file imports cleanly (and a hand-crafted one seeds the dev DB).
|
|
11
|
+
*/
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
|
|
15
|
+
import { accent, bold, danger, dim, success, warn } from './ui.js';
|
|
16
|
+
|
|
17
|
+
export interface DbOptions {
|
|
18
|
+
/** Project root (where `.toil/` lives); defaults to the current directory. */
|
|
19
|
+
root?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The top-level families a valid snapshot carries (mirrors DbSnapshot). */
|
|
23
|
+
const FAMILIES = ['store', 'views', 'members', 'counters', 'events', 'eventDedup', 'capacity'] as const;
|
|
24
|
+
|
|
25
|
+
function devdataPath(opts: DbOptions): string {
|
|
26
|
+
return path.join(path.resolve(opts.root ?? process.cwd()), '.toil', 'devdata.json');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function out(line: string): void {
|
|
30
|
+
process.stdout.write(line + '\n');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Per-family key counts for a snapshot file, or null if unreadable. */
|
|
34
|
+
function familyCounts(file: string): Record<string, number> | null {
|
|
35
|
+
let snap: Record<string, unknown>;
|
|
36
|
+
try {
|
|
37
|
+
snap = JSON.parse(fs.readFileSync(file, 'utf8')) as Record<string, unknown>;
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
const counts: Record<string, number> = {};
|
|
42
|
+
for (const f of FAMILIES) {
|
|
43
|
+
const fam = snap[f];
|
|
44
|
+
counts[f] = fam && typeof fam === 'object' ? Object.keys(fam).length : 0;
|
|
45
|
+
}
|
|
46
|
+
return counts;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function isSnapshot(v: unknown): boolean {
|
|
50
|
+
return typeof v === 'object' && v !== null && FAMILIES.every((f) => f in (v as object));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Run `toiljs db <action> [fileArg]`. */
|
|
54
|
+
export function runDb(action: string | undefined, fileArg: string | undefined, opts: DbOptions): void {
|
|
55
|
+
const file = devdataPath(opts);
|
|
56
|
+
const rel = path.relative(process.cwd(), file) || file;
|
|
57
|
+
|
|
58
|
+
switch (action) {
|
|
59
|
+
case 'reset':
|
|
60
|
+
case 'purge': {
|
|
61
|
+
if (!fs.existsSync(file)) {
|
|
62
|
+
out(dim(` dev database already empty (${rel} not found)`));
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
fs.rmSync(file);
|
|
66
|
+
out(success(' ✓ ') + 'dev database reset ' + dim(`(${rel} deleted)`));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
case 'export': {
|
|
71
|
+
if (!fs.existsSync(file)) {
|
|
72
|
+
out(warn(' ! ') + `nothing to export (${rel} not found)`);
|
|
73
|
+
process.exitCode = 1;
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
let pretty: string;
|
|
77
|
+
try {
|
|
78
|
+
pretty = JSON.stringify(JSON.parse(fs.readFileSync(file, 'utf8')), null, 2) + '\n';
|
|
79
|
+
} catch {
|
|
80
|
+
out(danger(' ✗ ') + `the dev database at ${rel} is unreadable`);
|
|
81
|
+
process.exitCode = 1;
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (fileArg === undefined) {
|
|
85
|
+
process.stdout.write(pretty); // no banner/prefix -> pipe-friendly
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
fs.writeFileSync(path.resolve(fileArg), pretty);
|
|
89
|
+
out(success(' ✓ ') + 'exported dev database to ' + dim(fileArg));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
case 'import': {
|
|
94
|
+
if (fileArg === undefined) {
|
|
95
|
+
out(danger(' ✗ ') + 'usage: ' + dim('toiljs db import <file>'));
|
|
96
|
+
process.exitCode = 1;
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
let parsed: unknown;
|
|
100
|
+
try {
|
|
101
|
+
parsed = JSON.parse(fs.readFileSync(path.resolve(fileArg), 'utf8'));
|
|
102
|
+
} catch (e) {
|
|
103
|
+
out(danger(' ✗ ') + `cannot read/parse ${fileArg}: ${(e as Error).message}`);
|
|
104
|
+
process.exitCode = 1;
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (!isSnapshot(parsed)) {
|
|
108
|
+
out(danger(' ✗ ') + `${fileArg} is not a toiljs dev database snapshot`);
|
|
109
|
+
process.exitCode = 1;
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
113
|
+
fs.writeFileSync(file, JSON.stringify(parsed)); // compact: the dev DB's on-disk form
|
|
114
|
+
out(success(' ✓ ') + `imported dev database from ${dim(fileArg)} ` + dim(`(${rel})`));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
case 'path':
|
|
119
|
+
out(file); // bare path, scriptable
|
|
120
|
+
return;
|
|
121
|
+
|
|
122
|
+
case 'status':
|
|
123
|
+
case 'info': {
|
|
124
|
+
out(bold(' dev database') + dim(` ${rel}`));
|
|
125
|
+
if (!fs.existsSync(file)) {
|
|
126
|
+
out(dim(' (empty — no data written yet; run the dev server to populate it)'));
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const counts = familyCounts(file);
|
|
130
|
+
if (counts === null) {
|
|
131
|
+
out(danger(' ✗ unreadable snapshot'));
|
|
132
|
+
process.exitCode = 1;
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
out(dim(` ${(fs.statSync(file).size / 1024).toFixed(1)} KiB`));
|
|
136
|
+
const nonEmpty = FAMILIES.filter((f) => counts[f] > 0);
|
|
137
|
+
if (nonEmpty.length === 0) out(dim(' (no rows)'));
|
|
138
|
+
else for (const f of nonEmpty) out(' ' + accent(f.padEnd(12)) + String(counts[f]));
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
default:
|
|
143
|
+
out(
|
|
144
|
+
[
|
|
145
|
+
bold('Usage') + ' ' + dim('toiljs db') + ' <action>',
|
|
146
|
+
'',
|
|
147
|
+
bold('Actions'),
|
|
148
|
+
' ' + accent('status'.padEnd(16)) + dim('show the dev DB path + per-family row counts'),
|
|
149
|
+
' ' + accent('reset'.padEnd(16)) + dim('delete all dev data (alias: purge)'),
|
|
150
|
+
' ' + accent('export [file]'.padEnd(16)) + dim('write a snapshot to <file> (or stdout)'),
|
|
151
|
+
' ' + accent('import <file>'.padEnd(16)) + dim('replace the dev DB with a snapshot'),
|
|
152
|
+
' ' + accent('path'.padEnd(16)) + dim('print the devdata.json path'),
|
|
153
|
+
].join('\n'),
|
|
154
|
+
);
|
|
155
|
+
if (action !== undefined) process.exitCode = 1; // unknown action
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
}
|
package/src/cli/diagnostics.ts
CHANGED
|
@@ -521,6 +521,25 @@ export function checkServerTsPlugin(present: boolean): Check {
|
|
|
521
521
|
};
|
|
522
522
|
}
|
|
523
523
|
|
|
524
|
+
/**
|
|
525
|
+
* Whether the server has a `migrations/` folder. ToilDB `@migrate` functions MUST live in a
|
|
526
|
+
* `*.migration.ts` file under `migrations/` (the toilscript compiler enforces folder + extension as
|
|
527
|
+
* a compile error), and the build auto-discovers them, so a server project should keep the folder
|
|
528
|
+
* ready. Older projects predate the convention, so this WARNS (does not fail) and points at the
|
|
529
|
+
* one-command fix (`toiljs update` creates it).
|
|
530
|
+
*/
|
|
531
|
+
export function checkMigrationsDir(exists: boolean): Check {
|
|
532
|
+
return exists
|
|
533
|
+
? { id: 'migrations-dir', label: 'server/migrations/ directory', status: 'pass' }
|
|
534
|
+
: {
|
|
535
|
+
id: 'migrations-dir',
|
|
536
|
+
label: 'server/migrations/ directory',
|
|
537
|
+
status: 'warn',
|
|
538
|
+
detail: 'no server/migrations/ folder; ToilDB @migrate functions must live in a *.migration.ts file under it',
|
|
539
|
+
fix: 'Create server/migrations/ (one <Type>.migration.ts per evolving @data value type), or run `toiljs update` to add it.',
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
|
|
524
543
|
// --- Security -------------------------------------------------------------------------------------
|
|
525
544
|
|
|
526
545
|
/** Whether the project uses the auth primitive, and whether its session secret is configured. */
|
package/src/cli/doctor.ts
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
checkDir,
|
|
28
28
|
checkDuplicatePatterns,
|
|
29
29
|
type CheckGroup,
|
|
30
|
+
checkMigrationsDir,
|
|
30
31
|
checkMountSlots,
|
|
31
32
|
checkNode,
|
|
32
33
|
checkPackageManager,
|
|
@@ -279,6 +280,24 @@ function serverTsconfigPath(root: string, toilconfig: Record<string, unknown> |
|
|
|
279
280
|
return null;
|
|
280
281
|
}
|
|
281
282
|
|
|
283
|
+
/**
|
|
284
|
+
* Whether a `migrations/` folder exists under any server dir (the dir of a toilconfig entry,
|
|
285
|
+
* conventionally `server/`). ToilDB `@migrate` functions live in `*.migration.ts` files there, and
|
|
286
|
+
* the build auto-discovers them; a missing folder is a warn, not a fail (older projects predate it).
|
|
287
|
+
*/
|
|
288
|
+
function serverMigrationsExist(root: string, toilconfig: Record<string, unknown> | null): boolean {
|
|
289
|
+
const dirs = new Set<string>();
|
|
290
|
+
const entries = Array.isArray(toilconfig?.entries)
|
|
291
|
+
? (toilconfig.entries as unknown[]).filter((e): e is string => typeof e === 'string')
|
|
292
|
+
: [];
|
|
293
|
+
for (const e of entries) dirs.add(path.dirname(path.resolve(root, e)));
|
|
294
|
+
if (dirs.size === 0) dirs.add(path.join(root, 'server'));
|
|
295
|
+
for (const dir of dirs) {
|
|
296
|
+
if (fs.existsSync(path.join(dir, 'migrations'))) return true;
|
|
297
|
+
}
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
|
|
282
301
|
/** Whether a parsed tsconfig's `compilerOptions.plugins` references the toilscript LS plugin. */
|
|
283
302
|
function tsconfigHasToilPlugin(tsconfig: Record<string, unknown> | null): boolean {
|
|
284
303
|
const plugins = asRecord(tsconfig?.compilerOptions)?.plugins;
|
|
@@ -840,6 +859,7 @@ export async function runDoctor(opts: DoctorOptions): Promise<void> {
|
|
|
840
859
|
checkRestDispatch(restFacts),
|
|
841
860
|
checkPrettierPlugin(prettierPresent),
|
|
842
861
|
checkServerTsPlugin(serverTsPluginPresent),
|
|
862
|
+
checkMigrationsDir(serverMigrationsExist(root, toilconfig)),
|
|
843
863
|
]
|
|
844
864
|
: [checkToilconfig(false)],
|
|
845
865
|
},
|
package/src/cli/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { build, dev, start } from 'toiljs/compiler';
|
|
|
8
8
|
|
|
9
9
|
import { runConfigure } from './configure.js';
|
|
10
10
|
import { runCreate, type Template } from './create.js';
|
|
11
|
+
import { runDb } from './db.js';
|
|
11
12
|
import { runDoctor } from './doctor.js';
|
|
12
13
|
import { notifyIfOutdated } from './notify.js';
|
|
13
14
|
import { runUpdate } from './update.js';
|
|
@@ -132,6 +133,7 @@ function printHelp(): void {
|
|
|
132
133
|
cmd('start', 'self-host the built app (hyper-express / uWS)'),
|
|
133
134
|
cmd('doctor', 'diagnose project setup and dependencies'),
|
|
134
135
|
cmd('update', 'check for and apply dependency updates'),
|
|
136
|
+
cmd('db <action>', 'manage dev DB data (status | reset | export | import)'),
|
|
135
137
|
'',
|
|
136
138
|
bold('Options'),
|
|
137
139
|
cmd('--root <dir>', 'project root (default: current directory)'),
|
|
@@ -255,6 +257,14 @@ async function main(): Promise<void> {
|
|
|
255
257
|
});
|
|
256
258
|
break;
|
|
257
259
|
|
|
260
|
+
case 'db': {
|
|
261
|
+
// `toiljs db <action> [file]` — no banner so `path`/`export` pipe cleanly.
|
|
262
|
+
const action = rest[0];
|
|
263
|
+
const fileArg = rest[1] !== undefined && !rest[1].startsWith('-') ? rest[1] : undefined;
|
|
264
|
+
runDb(action, fileArg, { root: flags.root });
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
|
|
258
268
|
case 'help':
|
|
259
269
|
case '--help':
|
|
260
270
|
case '-h':
|
package/src/cli/update.ts
CHANGED
|
@@ -9,6 +9,7 @@ import path from 'node:path';
|
|
|
9
9
|
|
|
10
10
|
import { cancel, intro, isCancel, multiselect, note, outro, spinner } from '@clack/prompts';
|
|
11
11
|
|
|
12
|
+
import { MIGRATIONS_README } from './create.js';
|
|
12
13
|
import { capture, run } from './proc.js';
|
|
13
14
|
import { buildRows, type Bump, parseNcuJson, type UpdateRow } from './updates.js';
|
|
14
15
|
import { accent, danger, dim, success, warn } from './ui.js';
|
|
@@ -49,6 +50,53 @@ function readDependencies(pkgPath: string): Record<string, string> {
|
|
|
49
50
|
return { ...merge(pkg.dependencies), ...merge(pkg.devDependencies) };
|
|
50
51
|
}
|
|
51
52
|
|
|
53
|
+
/**
|
|
54
|
+
* The server dir(s) of a project: the directories of the toilconfig.json `entries`, conventionally
|
|
55
|
+
* `server/`. Falls back to `<root>/server` when there is no toilconfig (or it has no string entries),
|
|
56
|
+
* matching how `doctor` locates the server tree.
|
|
57
|
+
*/
|
|
58
|
+
function serverDirs(root: string): string[] {
|
|
59
|
+
const dirs = new Set<string>();
|
|
60
|
+
try {
|
|
61
|
+
const parsed: unknown = JSON.parse(
|
|
62
|
+
fs.readFileSync(path.join(root, 'toilconfig.json'), 'utf8'),
|
|
63
|
+
);
|
|
64
|
+
const entries =
|
|
65
|
+
typeof parsed === 'object' &&
|
|
66
|
+
parsed !== null &&
|
|
67
|
+
Array.isArray((parsed as { entries?: unknown }).entries)
|
|
68
|
+
? (parsed as { entries: unknown[] }).entries.filter(
|
|
69
|
+
(e): e is string => typeof e === 'string',
|
|
70
|
+
)
|
|
71
|
+
: [];
|
|
72
|
+
for (const e of entries) dirs.add(path.dirname(path.resolve(root, e)));
|
|
73
|
+
} catch {
|
|
74
|
+
// no/unreadable toilconfig.json: fall back to the conventional server/ below
|
|
75
|
+
}
|
|
76
|
+
if (dirs.size === 0) dirs.add(path.join(root, 'server'));
|
|
77
|
+
return [...dirs];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Ensures the ToilDB `migrations/` folder exists under each server dir that exists. `@migrate`
|
|
82
|
+
* functions MUST live in a `*.migration.ts` file under `migrations/` (folder + extension is a
|
|
83
|
+
* compile error otherwise) and the build auto-discovers them, so an up-to-date project keeps the
|
|
84
|
+
* folder ready. Idempotent: only writes the folder + README where the server dir exists and the
|
|
85
|
+
* folder is absent. Returns the project-relative paths created (for the note), empty if nothing.
|
|
86
|
+
*/
|
|
87
|
+
function ensureMigrationsDirs(root: string): string[] {
|
|
88
|
+
const created: string[] = [];
|
|
89
|
+
for (const dir of serverDirs(root)) {
|
|
90
|
+
if (!fs.existsSync(dir)) continue;
|
|
91
|
+
const migrations = path.join(dir, 'migrations');
|
|
92
|
+
if (fs.existsSync(migrations)) continue;
|
|
93
|
+
fs.mkdirSync(migrations, { recursive: true });
|
|
94
|
+
fs.writeFileSync(path.join(migrations, 'README.md'), MIGRATIONS_README);
|
|
95
|
+
created.push(path.relative(root, migrations) || 'migrations');
|
|
96
|
+
}
|
|
97
|
+
return created;
|
|
98
|
+
}
|
|
99
|
+
|
|
52
100
|
function bumpColor(bump: Bump, text: string): string {
|
|
53
101
|
if (bump === 'major') return danger(text);
|
|
54
102
|
if (bump === 'minor') return warn(text);
|
|
@@ -85,6 +133,16 @@ export async function runUpdate(opts: UpdateOptions): Promise<void> {
|
|
|
85
133
|
|
|
86
134
|
intro(accent('toiljs update'));
|
|
87
135
|
|
|
136
|
+
// Bring the project's structure up to date: ensure the ToilDB migrations folder exists (it is
|
|
137
|
+
// newer than some projects, and the compiler requires @migrate functions to live under it).
|
|
138
|
+
const createdMigrations = ensureMigrationsDirs(root);
|
|
139
|
+
if (createdMigrations.length > 0) {
|
|
140
|
+
note(
|
|
141
|
+
createdMigrations.map((p) => `${dim('+')} ${p}/`).join('\n'),
|
|
142
|
+
'Created ToilDB migrations folder',
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
88
146
|
const s = spinner();
|
|
89
147
|
s.start('Checking the registry for updates');
|
|
90
148
|
const res = await capture('npx', ncuArgs(['--jsonUpgraded']), root);
|
package/src/client/index.ts
CHANGED
|
@@ -102,5 +102,5 @@ export { Slot } from './components/Slot.js';
|
|
|
102
102
|
export type { SlotProps } from './components/Slot.js';
|
|
103
103
|
export { Server } from './rpc.js';
|
|
104
104
|
export { parseError } from './errors.js';
|
|
105
|
-
export { Hole, RawHtml, Repeat, Island, __setSsrBuild, __isSsrBuild } from './ssr/markers.js';
|
|
105
|
+
export { Hole, RawHtml, Repeat, Island, attr, __setSsrBuild, __isSsrBuild } from './ssr/markers.js';
|
|
106
106
|
export type { HoleProps, RawHtmlProps, RepeatProps, IslandProps } from './ssr/markers.js';
|
|
@@ -65,9 +65,15 @@ export function mount(
|
|
|
65
65
|
// branch, and the dev-only imports, are dead-code-eliminated and tree-shaken from production.
|
|
66
66
|
if ((import.meta as unknown as { env: { DEV: boolean } }).env.DEV) {
|
|
67
67
|
initDevErrorOverlay();
|
|
68
|
-
|
|
68
|
+
// Dev tools (error overlay + toolbar) render into their OWN body-level
|
|
69
|
+
// container, never inside #root, so they don't perturb #root's markup.
|
|
70
|
+
// That lets a dev SSR document hydrate cleanly (the server only rendered
|
|
71
|
+
// the app into #root), and is harmless for a plain client-rendered page.
|
|
72
|
+
const devEl = document.createElement('div');
|
|
73
|
+
devEl.id = '__toil_dev';
|
|
74
|
+
document.body.appendChild(devEl);
|
|
75
|
+
createRoot(devEl).render(
|
|
69
76
|
<>
|
|
70
|
-
<DevErrorBoundary>{app}</DevErrorBoundary>
|
|
71
77
|
<DevErrorOverlay />
|
|
72
78
|
<DevToolbar
|
|
73
79
|
routes={routes}
|
|
@@ -75,6 +81,16 @@ export function mount(
|
|
|
75
81
|
/>
|
|
76
82
|
</>,
|
|
77
83
|
);
|
|
84
|
+
const tree = <DevErrorBoundary>{app}</DevErrorBoundary>;
|
|
85
|
+
if (isSsrDocument()) {
|
|
86
|
+
// Dev edge-SSR: the dev server served real server-rendered markup
|
|
87
|
+
// (guest `render` + splice); hydrate it in place, same as production,
|
|
88
|
+
// instead of client-rendering from scratch.
|
|
89
|
+
seedSsrHydration(routes);
|
|
90
|
+
hydrateRoot(el, tree);
|
|
91
|
+
} else {
|
|
92
|
+
createRoot(el).render(tree);
|
|
93
|
+
}
|
|
78
94
|
} else if (isSsrDocument()) {
|
|
79
95
|
// Edge-SSR: the document already holds server-rendered markup. Seed the
|
|
80
96
|
// loader cache from `#__toil_state` and hydrate in place (reuse the DOM)
|
|
@@ -76,6 +76,28 @@ export function Hole(props: HoleProps): ReactNode {
|
|
|
76
76
|
return createElement(Fragment, null, props.children);
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
/**
|
|
80
|
+
* An attribute-value hole. Unlike the child markers, an attribute is not a child
|
|
81
|
+
* node, so this is a *function* used in attribute position rather than a JSX
|
|
82
|
+
* element:
|
|
83
|
+
*
|
|
84
|
+
* ```tsx
|
|
85
|
+
* <a href={attr('profileUrl', d.url)} class={attr('cls', d.cls)}>…</a>
|
|
86
|
+
* ```
|
|
87
|
+
*
|
|
88
|
+
* Browser: returns `value` unchanged, so the attribute renders normally. Build:
|
|
89
|
+
* returns a PUA sentinel token, so the extractor records an `attr` slot at the
|
|
90
|
+
* attribute's byte offset (the token sits between the quotes and strips to zero
|
|
91
|
+
* bytes). The guest fills it per request with the React-escaped value (kind
|
|
92
|
+
* `attr`), which the host splices at that offset — exactly like a text hole, but
|
|
93
|
+
* inside an attribute. It composes with surrounding literal text
|
|
94
|
+
* (`` `btn ${attr('x', v)}` ``), and `setAttr`/`HtmlBuilder.attr` produce the
|
|
95
|
+
* value on the server side.
|
|
96
|
+
*/
|
|
97
|
+
export function attr(id: string, value: string): string {
|
|
98
|
+
return ssrBuild ? token(HoleKindChar.Attr, id) : value;
|
|
99
|
+
}
|
|
100
|
+
|
|
79
101
|
export interface RawHtmlProps {
|
|
80
102
|
id: string;
|
|
81
103
|
/** Raw HTML string. The author owns sanitisation (same as React
|
package/src/compiler/config.ts
CHANGED
|
@@ -97,8 +97,32 @@ export interface ClientConfig {
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
/**
|
|
100
|
-
*
|
|
101
|
-
*
|
|
100
|
+
* Dev node mode: which layer the single dev process emulates. `hot` runs only the
|
|
101
|
+
* request path (today's behavior); `regional`/`continental` would run streams off
|
|
102
|
+
* `release-hot.wasm` (Phase 4); `daemon` runs `release-cold.wasm`; `all` runs every
|
|
103
|
+
* surface in one process (the default for a full local run). DEV / self-host knob;
|
|
104
|
+
* the production edge reads the authoritative role from per-host TCF + the plan.
|
|
105
|
+
*/
|
|
106
|
+
export type DevNodeMode = 'hot' | 'regional' | 'continental' | 'daemon' | 'all';
|
|
107
|
+
|
|
108
|
+
/** Daemon (L4) config mirror (dev / self-host). All optional. */
|
|
109
|
+
export interface DaemonConfig {
|
|
110
|
+
/** Region the daemon is pinned to (informational in dev; the dev process is leader). */
|
|
111
|
+
readonly region?: string;
|
|
112
|
+
/** Warm standby region (informational in dev). */
|
|
113
|
+
readonly standbyRegion?: string;
|
|
114
|
+
/** Default `@scheduled` interval (ms) when a task declares none. Default 60000. */
|
|
115
|
+
readonly defaultIntervalMs?: number;
|
|
116
|
+
/** Per-tick wall-clock budget (ms) before the dev scheduler logs an overrun. Default 30000. */
|
|
117
|
+
readonly tickBudgetMs?: number;
|
|
118
|
+
/** Per-tick gas cap (dev stub; charged-then-ignored). Mirrors `plan.gas_scheduled`. */
|
|
119
|
+
readonly gasTick?: number;
|
|
120
|
+
/** Max number of `@scheduled` tasks (mirrors `max_scheduled_tasks`). Default 64. */
|
|
121
|
+
readonly maxTasks?: number;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Server-side (toilscript → WASM) configuration.
|
|
102
126
|
*/
|
|
103
127
|
export interface ServerConfig {
|
|
104
128
|
/** Server source directory, relative to root. Default `server`. */
|
|
@@ -114,6 +138,20 @@ export interface ServerConfig {
|
|
|
114
138
|
* the per-tenant env store); this drives `toiljs dev` / self-host.
|
|
115
139
|
*/
|
|
116
140
|
readonly email?: EmailBackendConfig;
|
|
141
|
+
/** Which layer the dev process emulates. Default `all`. */
|
|
142
|
+
readonly nodeMode?: DevNodeMode;
|
|
143
|
+
/** Daemon (L4) config mirror (dev / self-host). */
|
|
144
|
+
readonly daemon?: DaemonConfig;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Fully-resolved {@link DaemonConfig}; every field non-optional, defaults applied. */
|
|
148
|
+
export interface ResolvedDaemonConfig {
|
|
149
|
+
readonly region: string | null;
|
|
150
|
+
readonly standbyRegion: string | null;
|
|
151
|
+
readonly defaultIntervalMs: number;
|
|
152
|
+
readonly tickBudgetMs: number;
|
|
153
|
+
readonly gasTick: number;
|
|
154
|
+
readonly maxTasks: number;
|
|
117
155
|
}
|
|
118
156
|
|
|
119
157
|
/**
|
|
@@ -157,6 +195,10 @@ export interface ResolvedToilConfig {
|
|
|
157
195
|
readonly seo: SeoConfig | null;
|
|
158
196
|
/** The `server.email` backend config (dev / self-host), or `null` when unset. */
|
|
159
197
|
readonly email: EmailBackendConfig | null;
|
|
198
|
+
/** Which layer the dev process emulates (dev / self-host). Default `all`. */
|
|
199
|
+
readonly nodeMode: DevNodeMode;
|
|
200
|
+
/** Daemon (L4) config mirror (dev / self-host), every field resolved. */
|
|
201
|
+
readonly daemon: ResolvedDaemonConfig;
|
|
160
202
|
/** Absolute path to the framework client runtime (`toiljs/client`). */
|
|
161
203
|
readonly runtimePath: string;
|
|
162
204
|
readonly vite: InlineConfig;
|
|
@@ -229,7 +271,51 @@ export async function loadConfig(
|
|
|
229
271
|
: null,
|
|
230
272
|
seo: client.seo ?? null,
|
|
231
273
|
email: user.server?.email ?? null,
|
|
274
|
+
nodeMode: resolveNodeMode(user.server?.nodeMode),
|
|
275
|
+
daemon: resolveDaemonConfig(user.server?.daemon),
|
|
232
276
|
runtimePath: resolveRuntimePath(),
|
|
233
277
|
vite: client.vite ?? {},
|
|
234
278
|
};
|
|
235
279
|
}
|
|
280
|
+
|
|
281
|
+
const DEV_NODE_MODES: readonly DevNodeMode[] = [
|
|
282
|
+
'hot',
|
|
283
|
+
'regional',
|
|
284
|
+
'continental',
|
|
285
|
+
'daemon',
|
|
286
|
+
'all',
|
|
287
|
+
];
|
|
288
|
+
|
|
289
|
+
/** A `nodeMode` outside the enum falls back to `all` with a warning (fail-soft:
|
|
290
|
+
* the authoritative role is the edge's TCF + plan, so dev never bricks on it). */
|
|
291
|
+
function resolveNodeMode(mode: DevNodeMode | undefined): DevNodeMode {
|
|
292
|
+
if (mode === undefined) return 'all';
|
|
293
|
+
if (DEV_NODE_MODES.includes(mode)) return mode;
|
|
294
|
+
process.stdout.write(
|
|
295
|
+
` ! server.nodeMode '${mode}' is not a valid node mode; falling back to 'all'\n`,
|
|
296
|
+
);
|
|
297
|
+
return 'all';
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** Resolve the daemon config with defaults + light, fail-soft clamping (never
|
|
301
|
+
* throws; the authoritative caps are the edge's). */
|
|
302
|
+
function resolveDaemonConfig(d: DaemonConfig | undefined): ResolvedDaemonConfig {
|
|
303
|
+
// The dev scheduler uses setInterval; a sub-second loop floods the console.
|
|
304
|
+
let defaultIntervalMs = d?.defaultIntervalMs ?? 60000;
|
|
305
|
+
if (defaultIntervalMs < 1000) {
|
|
306
|
+
process.stdout.write(
|
|
307
|
+
` ! server.daemon.defaultIntervalMs ${String(defaultIntervalMs)} < 1000; clamping to 1000\n`,
|
|
308
|
+
);
|
|
309
|
+
defaultIntervalMs = 1000;
|
|
310
|
+
}
|
|
311
|
+
let maxTasks = d?.maxTasks ?? 64;
|
|
312
|
+
if (maxTasks <= 0 || maxTasks > 1024) maxTasks = Math.min(1024, Math.max(1, maxTasks || 64));
|
|
313
|
+
return {
|
|
314
|
+
region: d?.region ?? null,
|
|
315
|
+
standbyRegion: d?.standbyRegion ?? null,
|
|
316
|
+
defaultIntervalMs,
|
|
317
|
+
tickBudgetMs: d?.tickBudgetMs ?? 30000,
|
|
318
|
+
gasTick: d?.gasTick ?? 0,
|
|
319
|
+
maxTasks,
|
|
320
|
+
};
|
|
321
|
+
}
|