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.
Files changed (158) hide show
  1. package/.github/workflows/ci.yml +31 -0
  2. package/CHANGELOG.md +15 -0
  3. package/build/cli/.tsbuildinfo +1 -1
  4. package/build/cli/index.js +311 -118
  5. package/build/client/.tsbuildinfo +1 -1
  6. package/build/client/index.d.ts +1 -1
  7. package/build/client/index.js +1 -1
  8. package/build/client/routing/mount.js +12 -1
  9. package/build/client/ssr/markers.d.ts +1 -0
  10. package/build/client/ssr/markers.js +3 -0
  11. package/build/compiler/.tsbuildinfo +1 -1
  12. package/build/compiler/config.d.ts +21 -0
  13. package/build/compiler/config.js +35 -0
  14. package/build/compiler/docs.d.ts +2 -1
  15. package/build/compiler/docs.js +33 -304
  16. package/build/compiler/index.d.ts +13 -0
  17. package/build/compiler/index.js +113 -21
  18. package/build/compiler/template-build.d.ts +21 -1
  19. package/build/compiler/template-build.js +110 -26
  20. package/build/compiler/toil-docs.generated.d.ts +1 -0
  21. package/build/compiler/toil-docs.generated.js +20 -0
  22. package/build/devserver/.tsbuildinfo +1 -1
  23. package/build/devserver/daemon/catalog.d.ts +26 -0
  24. package/build/devserver/daemon/catalog.js +48 -0
  25. package/build/devserver/daemon/cron.d.ts +4 -0
  26. package/build/devserver/daemon/cron.js +50 -0
  27. package/build/devserver/daemon/host.d.ts +37 -0
  28. package/build/devserver/daemon/host.js +94 -0
  29. package/build/devserver/daemon/index.d.ts +34 -0
  30. package/build/devserver/daemon/index.js +241 -0
  31. package/build/devserver/db/catalog.d.ts +2 -0
  32. package/build/devserver/db/catalog.js +80 -0
  33. package/build/devserver/db/database.d.ts +80 -0
  34. package/build/devserver/db/database.js +1032 -0
  35. package/build/devserver/db/index.d.ts +3 -0
  36. package/build/devserver/db/index.js +3 -0
  37. package/build/devserver/db/routeKinds.d.ts +8 -0
  38. package/build/devserver/db/routeKinds.js +139 -0
  39. package/build/devserver/db/types.d.ts +121 -0
  40. package/build/devserver/db/types.js +52 -0
  41. package/build/devserver/email/index.js +1 -1
  42. package/build/devserver/index.d.ts +19 -24
  43. package/build/devserver/index.js +11 -165
  44. package/build/devserver/mstore/store.d.ts +18 -0
  45. package/build/devserver/mstore/store.js +82 -0
  46. package/build/devserver/{host.d.ts → runtime/host.d.ts} +7 -1
  47. package/build/devserver/{host.js → runtime/host.js} +51 -7
  48. package/build/devserver/{module.d.ts → runtime/module.d.ts} +2 -1
  49. package/build/devserver/{module.js → runtime/module.js} +34 -1
  50. package/build/devserver/server.d.ts +23 -0
  51. package/build/devserver/server.js +223 -0
  52. package/build/devserver/ssr.d.ts +25 -0
  53. package/build/devserver/ssr.js +114 -0
  54. package/build/devserver/wasm/sections.d.ts +2 -0
  55. package/build/devserver/wasm/sections.js +42 -0
  56. package/build/devserver/wasm/surface.d.ts +18 -0
  57. package/build/devserver/wasm/surface.js +41 -0
  58. package/docs/README.md +4 -4
  59. package/docs/auth-todo.md +6 -6
  60. package/docs/caching.md +5 -5
  61. package/docs/cli.md +15 -0
  62. package/docs/client.md +40 -0
  63. package/docs/crypto.md +4 -4
  64. package/docs/data.md +6 -6
  65. package/docs/email.md +28 -28
  66. package/docs/environment.md +10 -10
  67. package/docs/index.md +26 -0
  68. package/docs/ratelimit.md +10 -10
  69. package/docs/routing.md +2 -2
  70. package/docs/server.md +61 -0
  71. package/docs/ssr.md +561 -113
  72. package/docs/styling.md +22 -0
  73. package/docs/time.md +3 -3
  74. package/eslint.config.js +10 -1
  75. package/examples/basic/client/components/Header.tsx +3 -0
  76. package/examples/basic/client/routes/features/actions.tsx +0 -2
  77. package/examples/basic/client/routes/hello.tsx +89 -19
  78. package/examples/basic/client/styles/main.css +48 -0
  79. package/examples/basic/server/SsrHelloRender.ts +97 -0
  80. package/examples/basic/server/main.ts +5 -0
  81. package/examples/basic/server/migrations/GuestEntry.migration.ts +39 -0
  82. package/examples/basic/server/streams/Echo.ts +49 -0
  83. package/package.json +12 -10
  84. package/scripts/gen-toil-docs.mjs +96 -0
  85. package/server/runtime/time.ts +3 -3
  86. package/src/cli/create.ts +40 -3
  87. package/src/cli/db.ts +158 -0
  88. package/src/cli/diagnostics.ts +19 -0
  89. package/src/cli/doctor.ts +20 -0
  90. package/src/cli/index.ts +10 -0
  91. package/src/cli/update.ts +58 -0
  92. package/src/client/index.ts +1 -1
  93. package/src/client/routing/mount.tsx +18 -2
  94. package/src/client/ssr/markers.tsx +22 -0
  95. package/src/compiler/config.ts +88 -2
  96. package/src/compiler/docs.ts +47 -308
  97. package/src/compiler/index.ts +236 -32
  98. package/src/compiler/ssr-codegen.ts +1 -1
  99. package/src/compiler/template-build.ts +247 -46
  100. package/src/compiler/toil-docs.generated.ts +26 -0
  101. package/src/devserver/daemon/catalog.ts +120 -0
  102. package/src/devserver/daemon/cron.ts +87 -0
  103. package/src/devserver/daemon/host.ts +224 -0
  104. package/src/devserver/daemon/index.ts +349 -0
  105. package/src/devserver/db/catalog.ts +108 -0
  106. package/src/devserver/db/database.ts +1633 -0
  107. package/src/devserver/db/index.ts +18 -0
  108. package/src/devserver/db/routeKinds.ts +147 -0
  109. package/src/devserver/db/types.ts +139 -0
  110. package/src/devserver/email/index.ts +1 -1
  111. package/src/devserver/index.ts +31 -287
  112. package/src/devserver/mstore/store.ts +121 -0
  113. package/src/devserver/{host.ts → runtime/host.ts} +98 -7
  114. package/src/devserver/{module.ts → runtime/module.ts} +47 -1
  115. package/src/devserver/server.ts +393 -0
  116. package/src/devserver/ssr.ts +166 -0
  117. package/src/devserver/wasm/sections.ts +59 -0
  118. package/src/devserver/wasm/surface.ts +88 -0
  119. package/test/daemon-build.test.ts +198 -0
  120. package/test/daemon-catalog.test.ts +265 -0
  121. package/test/daemon-emulation.test.ts +216 -0
  122. package/test/db.test.ts +0 -0
  123. package/test/devserver-database.test.ts +510 -14
  124. package/test/devserver-pqauth.test.ts +1 -1
  125. package/test/devserver-secrets.test.ts +5 -1
  126. package/test/doctor.test.ts +13 -0
  127. package/test/email-preview.test.ts +6 -1
  128. package/test/example-guestbook.test.ts +43 -1
  129. package/test/fixtures/daemon-app.ts +56 -0
  130. package/test/global-setup.ts +17 -0
  131. package/test/pqauth-e2e.test.ts +1 -1
  132. package/test/ssr-render.test.ts +94 -27
  133. package/test/ssr-template.test.tsx +44 -1
  134. package/vitest.config.ts +3 -0
  135. package/build/devserver/database.d.ts +0 -8
  136. package/build/devserver/database.js +0 -418
  137. package/src/devserver/database.ts +0 -618
  138. /package/build/devserver/{dotenv.d.ts → config/dotenv.d.ts} +0 -0
  139. /package/build/devserver/{dotenv.js → config/dotenv.js} +0 -0
  140. /package/build/devserver/{env.d.ts → config/env.d.ts} +0 -0
  141. /package/build/devserver/{env.js → config/env.js} +0 -0
  142. /package/build/devserver/{ratelimit.d.ts → config/ratelimit.d.ts} +0 -0
  143. /package/build/devserver/{ratelimit.js → config/ratelimit.js} +0 -0
  144. /package/build/devserver/{cache.d.ts → http/cache.d.ts} +0 -0
  145. /package/build/devserver/{cache.js → http/cache.js} +0 -0
  146. /package/build/devserver/{envelope.d.ts → http/envelope.d.ts} +0 -0
  147. /package/build/devserver/{envelope.js → http/envelope.js} +0 -0
  148. /package/build/devserver/{proxy.d.ts → http/proxy.d.ts} +0 -0
  149. /package/build/devserver/{proxy.js → http/proxy.js} +0 -0
  150. /package/build/devserver/{crypto.d.ts → runtime/crypto.d.ts} +0 -0
  151. /package/build/devserver/{crypto.js → runtime/crypto.js} +0 -0
  152. /package/src/devserver/{dotenv.ts → config/dotenv.ts} +0 -0
  153. /package/src/devserver/{env.ts → config/env.ts} +0 -0
  154. /package/src/devserver/{ratelimit.ts → config/ratelimit.ts} +0 -0
  155. /package/src/devserver/{cache.ts → http/cache.ts} +0 -0
  156. /package/src/devserver/{envelope.ts → http/envelope.ts} +0 -0
  157. /package/src/devserver/{proxy.ts → http/proxy.ts} +0 -0
  158. /package/src/devserver/{crypto.ts → runtime/crypto.ts} +0 -0
@@ -1666,12 +1666,12 @@ function gradientAt(t2) {
1666
1666
  }
1667
1667
  function gradientLine(line) {
1668
1668
  const n2 = line.length;
1669
- let out = "";
1669
+ let out2 = "";
1670
1670
  for (let i2 = 0; i2 < n2; i2++) {
1671
1671
  const [r2, g, b] = gradientAt(n2 > 1 ? i2 / (n2 - 1) : 0);
1672
- out += `\x1B[38;2;${r2};${g};${b}m${line[i2]}`;
1672
+ out2 += `\x1B[38;2;${r2};${g};${b}m${line[i2]}`;
1673
1673
  }
1674
- return out + "\x1B[39m";
1674
+ return out2 + "\x1B[39m";
1675
1675
  }
1676
1676
  function version() {
1677
1677
  try {
@@ -2010,6 +2010,7 @@ function isPackageManager(pm) {
2010
2010
  }
2011
2011
 
2012
2012
  // src/cli/create.ts
2013
+ var MIGRATIONS_README = "# migrations/\n\nToilDB schema migrations. One file per evolving `@data` value type, named `<Type>.migration.ts`\n(e.g. `User.migration.ts`).\n\nEach file keeps the OLD `@data` shapes (e.g. `UserV1`) alongside the `@migrate` transform(s) that\ncarry old records forward, and imports the CURRENT value type from the app. The build\nauto-discovers every `*.migration.ts` under the project.\n\nDo NOT put `@migrate` anywhere else: a `@migrate` outside a `*.migration.ts` file in a\n`migrations/` folder is a compile error.\n\n## Example\n\n`User.migration.ts`:\n\n```ts\nimport { User } from '../models/User';\n\n// The previous on-disk shape of a User record.\n@data\nexport class UserV1 {\n name: string = '';\n}\n\n// Carry a UserV1 forward into the current User. Runs once per record on read.\n@migrate\nexport function up(old: UserV1, into: User): void {\n into.name = old.name;\n}\n```\n";
2013
2014
  var PREPROCESSOR_LABEL2 = {
2014
2015
  css: "Plain CSS",
2015
2016
  sass: "Sass (SCSS)",
@@ -2038,7 +2039,7 @@ function scaffold(name, template, features, aiTools, images) {
2038
2039
  "@types/react-dom": "^19.2.3",
2039
2040
  eslint: "^10.2.0",
2040
2041
  prettier: "^3.8.1",
2041
- toilscript: "^0.1.36",
2042
+ toilscript: "^0.1.37",
2042
2043
  typescript: "^6.0.3"
2043
2044
  };
2044
2045
  for (const dep of requiredPackages(features).sort()) {
@@ -2079,8 +2080,8 @@ export default defineConfig({
2079
2080
  ".prettierrc": '"toiljs/prettier"\n',
2080
2081
  // Generated files don't need formatting. (toilscript server decorators like @main /
2081
2082
  // @remote-on-functions are handled by the toiljs/prettier-plugin, so server/ is not ignored.)
2082
- ".prettierignore": "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",
2083
- ".gitignore": "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",
2083
+ ".prettierignore": "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",
2084
+ ".gitignore": "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",
2084
2085
  // Use the project's pinned TypeScript (node_modules) instead of VS Code's bundled
2085
2086
  // version, and prompt to switch, so the editor loads the toilscript LS plugin wired
2086
2087
  // in server/tsconfig.json (which clears the @database / @data editor false positives).
@@ -2170,7 +2171,11 @@ export default defineConfig({
2170
2171
  "",
2171
2172
  " npm run build",
2172
2173
  ""
2173
- ].join("\n")
2174
+ ].join("\n"),
2175
+ // ToilDB migrations live under server/migrations/ (folder + `*.migration.ts` is enforced by
2176
+ // the compiler; the build auto-discovers them). Ship the folder, documented, with no live
2177
+ // *.migration.ts: a @migrate referencing types a fresh project lacks would break the build.
2178
+ "server/migrations/README.md": MIGRATIONS_README
2174
2179
  };
2175
2180
  if (template === "minimal") {
2176
2181
  Object.assign(files, minimalClient(name, features));
@@ -2223,7 +2228,7 @@ function minimalServer() {
2223
2228
  "server/toil-server-env.d.ts": TOIL_SERVER_ENV_DTS,
2224
2229
  "server/core/AppHandler.ts": "import { ToilHandler, Request, Response, Method } from 'toiljs/server/runtime';\n\n/** Every request enters here. Add `@rest` controllers under routes/ as you grow. */\nexport class AppHandler extends ToilHandler {\n public handle(req: Request): Response {\n if (req.method != Method.GET && req.method != Method.HEAD) {\n return Response.empty(405).setHeader('allow', 'GET, HEAD');\n }\n if (req.path == '/api/hello') {\n return Response.text('hello from toiljs\\n');\n }\n if (req.path == '/api/hash') {\n // `crypto` is a global (no import), synchronous Web Crypto.\n return Response.text(crypto.toHex(crypto.sha256Text(req.path)) + '\\n');\n }\n // Yield page routes and assets to the client: under `toiljs dev`\n // this falls through to Vite so the app renders at /.\n return Response.unhandled();\n }\n}\n",
2225
2230
  "server/main.ts": "import { Server } from 'toiljs/server/runtime';\nimport { revertOnError } from 'toiljs/server/runtime/abort/abort';\n\nimport { AppHandler } from './core/AppHandler';\n\n// As you add surface modules (@rest routes, @service/@remote RPC), import them here\n// so a direct `toilscript` run builds the same server `toiljs build` does, e.g.:\n// import './routes/Players';\n\n// Wire your handler here.\nServer.handler = () => new AppHandler();\n\n// Required: re-export the WASM entry points and the abort hook.\nexport * from 'toiljs/server/runtime/exports';\nexport function abort(message: string, fileName: string, line: u32, column: u32): void {\n revertOnError(message, fileName, line, column);\n}\n",
2226
- "server/README.md": "# server/\n\nYour ToilScript backend, compiled to a single WebAssembly module. One folder per concern:\n\n| Folder | What lives here |\n| --- | --- |\n| `main.ts` | The entry point: wires the handler and imports the surface modules. |\n| `core/` | The request handler and shared app logic (state, helpers). |\n| `models/` | `@data` classes, the typed wire model shared by HTTP and RPC. One type per file. |\n| `routes/` | `@rest` controllers (HTTP). One controller per file, named after its class. |\n| `services/` | `@service` classes and free `@remote` functions (typed RPC). |\n| `scheduled/` | Reserved for scheduled tasks (not shipped yet). |\n\nNew decorated files are picked up automatically by `toiljs build`/`dev`; also add an import\nin `main.ts` so a direct `toilscript` run builds the same server.\n"
2231
+ "server/README.md": "# server/\n\nYour ToilScript backend, compiled to a single WebAssembly module. One folder per concern:\n\n| Folder | What lives here |\n| --- | --- |\n| `main.ts` | The entry point: wires the handler and imports the surface modules. |\n| `core/` | The request handler and shared app logic (state, helpers). |\n| `models/` | `@data` classes, the typed wire model shared by HTTP and RPC. One type per file. |\n| `migrations/` | ToilDB schema migrations: a `<Type>.migration.ts` per evolving `@data` value type, holding the old shapes + the `@migrate` transform. |\n| `routes/` | `@rest` controllers (HTTP). One controller per file, named after its class. |\n| `services/` | `@service` classes and free `@remote` functions (typed RPC). |\n| `scheduled/` | Reserved for scheduled tasks (not shipped yet). |\n\nNew decorated files are picked up automatically by `toiljs build`/`dev`; also add an import\nin `main.ts` so a direct `toilscript` run builds the same server.\n"
2227
2232
  };
2228
2233
  }
2229
2234
  function minimalClient(name, features) {
@@ -2528,10 +2533,137 @@ async function runCreate(opts) {
2528
2533
  outro(`Created ${accent(path4.basename(name))}, happy building! ${dim("(v" + version() + ")")}`);
2529
2534
  }
2530
2535
 
2531
- // src/cli/doctor.ts
2536
+ // src/cli/db.ts
2532
2537
  import fs4 from "node:fs";
2533
- import { createRequire } from "node:module";
2534
2538
  import path5 from "node:path";
2539
+ var FAMILIES = ["store", "views", "members", "counters", "events", "eventDedup", "capacity"];
2540
+ function devdataPath(opts) {
2541
+ return path5.join(path5.resolve(opts.root ?? process.cwd()), ".toil", "devdata.json");
2542
+ }
2543
+ function out(line) {
2544
+ process.stdout.write(line + "\n");
2545
+ }
2546
+ function familyCounts(file) {
2547
+ let snap;
2548
+ try {
2549
+ snap = JSON.parse(fs4.readFileSync(file, "utf8"));
2550
+ } catch {
2551
+ return null;
2552
+ }
2553
+ const counts = {};
2554
+ for (const f of FAMILIES) {
2555
+ const fam = snap[f];
2556
+ counts[f] = fam && typeof fam === "object" ? Object.keys(fam).length : 0;
2557
+ }
2558
+ return counts;
2559
+ }
2560
+ function isSnapshot(v) {
2561
+ return typeof v === "object" && v !== null && FAMILIES.every((f) => f in v);
2562
+ }
2563
+ function runDb(action, fileArg, opts) {
2564
+ const file = devdataPath(opts);
2565
+ const rel = path5.relative(process.cwd(), file) || file;
2566
+ switch (action) {
2567
+ case "reset":
2568
+ case "purge": {
2569
+ if (!fs4.existsSync(file)) {
2570
+ out(dim(` dev database already empty (${rel} not found)`));
2571
+ return;
2572
+ }
2573
+ fs4.rmSync(file);
2574
+ out(success(" \u2713 ") + "dev database reset " + dim(`(${rel} deleted)`));
2575
+ return;
2576
+ }
2577
+ case "export": {
2578
+ if (!fs4.existsSync(file)) {
2579
+ out(warn(" ! ") + `nothing to export (${rel} not found)`);
2580
+ process.exitCode = 1;
2581
+ return;
2582
+ }
2583
+ let pretty;
2584
+ try {
2585
+ pretty = JSON.stringify(JSON.parse(fs4.readFileSync(file, "utf8")), null, 2) + "\n";
2586
+ } catch {
2587
+ out(danger(" \u2717 ") + `the dev database at ${rel} is unreadable`);
2588
+ process.exitCode = 1;
2589
+ return;
2590
+ }
2591
+ if (fileArg === void 0) {
2592
+ process.stdout.write(pretty);
2593
+ return;
2594
+ }
2595
+ fs4.writeFileSync(path5.resolve(fileArg), pretty);
2596
+ out(success(" \u2713 ") + "exported dev database to " + dim(fileArg));
2597
+ return;
2598
+ }
2599
+ case "import": {
2600
+ if (fileArg === void 0) {
2601
+ out(danger(" \u2717 ") + "usage: " + dim("toiljs db import <file>"));
2602
+ process.exitCode = 1;
2603
+ return;
2604
+ }
2605
+ let parsed;
2606
+ try {
2607
+ parsed = JSON.parse(fs4.readFileSync(path5.resolve(fileArg), "utf8"));
2608
+ } catch (e) {
2609
+ out(danger(" \u2717 ") + `cannot read/parse ${fileArg}: ${e.message}`);
2610
+ process.exitCode = 1;
2611
+ return;
2612
+ }
2613
+ if (!isSnapshot(parsed)) {
2614
+ out(danger(" \u2717 ") + `${fileArg} is not a toiljs dev database snapshot`);
2615
+ process.exitCode = 1;
2616
+ return;
2617
+ }
2618
+ fs4.mkdirSync(path5.dirname(file), { recursive: true });
2619
+ fs4.writeFileSync(file, JSON.stringify(parsed));
2620
+ out(success(" \u2713 ") + `imported dev database from ${dim(fileArg)} ` + dim(`(${rel})`));
2621
+ return;
2622
+ }
2623
+ case "path":
2624
+ out(file);
2625
+ return;
2626
+ case "status":
2627
+ case "info": {
2628
+ out(bold(" dev database") + dim(` ${rel}`));
2629
+ if (!fs4.existsSync(file)) {
2630
+ out(dim(" (empty \u2014 no data written yet; run the dev server to populate it)"));
2631
+ return;
2632
+ }
2633
+ const counts = familyCounts(file);
2634
+ if (counts === null) {
2635
+ out(danger(" \u2717 unreadable snapshot"));
2636
+ process.exitCode = 1;
2637
+ return;
2638
+ }
2639
+ out(dim(` ${(fs4.statSync(file).size / 1024).toFixed(1)} KiB`));
2640
+ const nonEmpty = FAMILIES.filter((f) => counts[f] > 0);
2641
+ if (nonEmpty.length === 0) out(dim(" (no rows)"));
2642
+ else for (const f of nonEmpty) out(" " + accent(f.padEnd(12)) + String(counts[f]));
2643
+ return;
2644
+ }
2645
+ default:
2646
+ out(
2647
+ [
2648
+ bold("Usage") + " " + dim("toiljs db") + " <action>",
2649
+ "",
2650
+ bold("Actions"),
2651
+ " " + accent("status".padEnd(16)) + dim("show the dev DB path + per-family row counts"),
2652
+ " " + accent("reset".padEnd(16)) + dim("delete all dev data (alias: purge)"),
2653
+ " " + accent("export [file]".padEnd(16)) + dim("write a snapshot to <file> (or stdout)"),
2654
+ " " + accent("import <file>".padEnd(16)) + dim("replace the dev DB with a snapshot"),
2655
+ " " + accent("path".padEnd(16)) + dim("print the devdata.json path")
2656
+ ].join("\n")
2657
+ );
2658
+ if (action !== void 0) process.exitCode = 1;
2659
+ return;
2660
+ }
2661
+ }
2662
+
2663
+ // src/cli/doctor.ts
2664
+ import fs5 from "node:fs";
2665
+ import { createRequire } from "node:module";
2666
+ import path6 from "node:path";
2535
2667
  import { fileURLToPath as fileURLToPath3 } from "node:url";
2536
2668
  import {
2537
2669
  loadConfig as loadConfig2,
@@ -2879,6 +3011,15 @@ function checkServerTsPlugin(present) {
2879
3011
  fix: 'Run `toiljs doctor --fix` to add { "plugins": [{ "name": "toilscript/std/ts-plugin.cjs" }] } to your server tsconfig, then pick the workspace TypeScript version and restart the TS server.'
2880
3012
  };
2881
3013
  }
3014
+ function checkMigrationsDir(exists) {
3015
+ return exists ? { id: "migrations-dir", label: "server/migrations/ directory", status: "pass" } : {
3016
+ id: "migrations-dir",
3017
+ label: "server/migrations/ directory",
3018
+ status: "warn",
3019
+ detail: "no server/migrations/ folder; ToilDB @migrate functions must live in a *.migration.ts file under it",
3020
+ fix: "Create server/migrations/ (one <Type>.migration.ts per evolving @data value type), or run `toiljs update` to add it."
3021
+ };
3022
+ }
2882
3023
  function checkAuthSecrets(f) {
2883
3024
  if (!f.usesAuth || f.sessionSecretSet) {
2884
3025
  return { id: "auth-secrets", label: "Session secret", status: "pass" };
@@ -2911,7 +3052,7 @@ function hasFailures(summary) {
2911
3052
  // src/cli/doctor.ts
2912
3053
  function readJsonObject(file) {
2913
3054
  try {
2914
- const parsed = JSON.parse(fs4.readFileSync(file, "utf8"));
3055
+ const parsed = JSON.parse(fs5.readFileSync(file, "utf8"));
2915
3056
  return typeof parsed === "object" && parsed !== null ? parsed : null;
2916
3057
  } catch {
2917
3058
  return null;
@@ -2919,22 +3060,22 @@ function readJsonObject(file) {
2919
3060
  }
2920
3061
  function stringRecord(value) {
2921
3062
  if (typeof value !== "object" || value === null) return {};
2922
- const out = {};
2923
- for (const [k, v] of Object.entries(value)) if (typeof v === "string") out[k] = v;
2924
- return out;
3063
+ const out2 = {};
3064
+ for (const [k, v] of Object.entries(value)) if (typeof v === "string") out2[k] = v;
3065
+ return out2;
2925
3066
  }
2926
3067
  function readFile(file) {
2927
3068
  try {
2928
- return fs4.readFileSync(file, "utf8");
3069
+ return fs5.readFileSync(file, "utf8");
2929
3070
  } catch {
2930
3071
  return null;
2931
3072
  }
2932
3073
  }
2933
3074
  function writeFile(file, content) {
2934
- fs4.writeFileSync(file, content);
3075
+ fs5.writeFileSync(file, content);
2935
3076
  }
2936
3077
  function isPackageInstalled(root, name) {
2937
- const require2 = createRequire(path5.join(root, "package.json"));
3078
+ const require2 = createRequire(path6.join(root, "package.json"));
2938
3079
  for (const id of [`${name}/package.json`, name]) {
2939
3080
  try {
2940
3081
  require2.resolve(id);
@@ -2943,8 +3084,8 @@ function isPackageInstalled(root, name) {
2943
3084
  }
2944
3085
  }
2945
3086
  for (let dir = root; ; ) {
2946
- if (fs4.existsSync(path5.join(dir, "node_modules", name, "package.json"))) return true;
2947
- const parent = path5.dirname(dir);
3087
+ if (fs5.existsSync(path6.join(dir, "node_modules", name, "package.json"))) return true;
3088
+ const parent = path6.dirname(dir);
2948
3089
  if (parent === dir) return false;
2949
3090
  dir = parent;
2950
3091
  }
@@ -2959,14 +3100,14 @@ function looksLikeSemverRange(range) {
2959
3100
  return /^\s*[v^~>=<]*\s*\d+\.\d+/.test(range);
2960
3101
  }
2961
3102
  function gatherRpcFacts(root) {
2962
- const pkg = readJsonObject(path5.join(root, "package.json"));
3103
+ const pkg = readJsonObject(path6.join(root, "package.json"));
2963
3104
  const scripts = pkg ? stringRecord(pkg.scripts) : {};
2964
3105
  const deps = {
2965
3106
  ...pkg ? stringRecord(pkg.dependencies) : {},
2966
3107
  ...pkg ? stringRecord(pkg.devDependencies) : {}
2967
3108
  };
2968
- const tsconfig = readJsonObject(path5.join(root, "tsconfig.json"));
2969
- const gitignore = readFile(path5.join(root, ".gitignore"));
3109
+ const tsconfig = readJsonObject(path6.join(root, "tsconfig.json"));
3110
+ const gitignore = readFile(path6.join(root, ".gitignore"));
2970
3111
  const buildServerWired = [scripts["build:server"], scripts["build"]].some(
2971
3112
  (s) => typeof s === "string" && s.includes("--rpcModule")
2972
3113
  );
@@ -2985,31 +3126,31 @@ function gatherRpcFacts(root) {
2985
3126
  function serverSources(root, toilconfig) {
2986
3127
  const dirs = /* @__PURE__ */ new Set();
2987
3128
  const entries = Array.isArray(toilconfig?.entries) ? toilconfig.entries.filter((e) => typeof e === "string") : [];
2988
- for (const e of entries) dirs.add(path5.dirname(path5.resolve(root, e)));
2989
- const out = [];
3129
+ for (const e of entries) dirs.add(path6.dirname(path6.resolve(root, e)));
3130
+ const out2 = [];
2990
3131
  const cap = 200;
2991
3132
  const maxDepth = 16;
2992
3133
  const visit = (current, depth) => {
2993
- if (out.length >= cap || depth > maxDepth) return;
3134
+ if (out2.length >= cap || depth > maxDepth) return;
2994
3135
  let listing;
2995
3136
  try {
2996
- listing = fs4.readdirSync(current, { withFileTypes: true });
3137
+ listing = fs5.readdirSync(current, { withFileTypes: true });
2997
3138
  } catch {
2998
3139
  return;
2999
3140
  }
3000
3141
  for (const entry of listing) {
3001
- if (out.length >= cap) break;
3002
- const full = path5.join(current, entry.name);
3142
+ if (out2.length >= cap) break;
3143
+ const full = path6.join(current, entry.name);
3003
3144
  if (entry.isDirectory()) {
3004
3145
  if (entry.name !== "node_modules") visit(full, depth + 1);
3005
3146
  } else if (entry.name.endsWith(".ts") && !entry.name.endsWith(".d.ts")) {
3006
3147
  const src = readFile(full);
3007
- if (src !== null) out.push(src);
3148
+ if (src !== null) out2.push(src);
3008
3149
  }
3009
3150
  }
3010
3151
  };
3011
3152
  for (const dir of dirs) visit(dir, 0);
3012
- return out;
3153
+ return out2;
3013
3154
  }
3014
3155
  function gatherRestFacts(root, toilconfig) {
3015
3156
  let hasControllers = false;
@@ -3023,7 +3164,7 @@ function gatherRestFacts(root, toilconfig) {
3023
3164
  return { hasControllers, dispatched };
3024
3165
  }
3025
3166
  function secretDefined(root, key) {
3026
- const raw = readFile(path5.join(root, ".env.secrets"));
3167
+ const raw = readFile(path6.join(root, ".env.secrets"));
3027
3168
  if (raw === null) return false;
3028
3169
  return new RegExp(`^\\s*(?:export\\s+)?${key}\\s*=\\s*\\S`, "m").test(raw);
3029
3170
  }
@@ -3041,14 +3182,24 @@ var TS_PLUGIN_NAME = "toilscript/std/ts-plugin.cjs";
3041
3182
  function serverTsconfigPath(root, toilconfig) {
3042
3183
  const dirs = /* @__PURE__ */ new Set();
3043
3184
  const entries = Array.isArray(toilconfig?.entries) ? toilconfig.entries.filter((e) => typeof e === "string") : [];
3044
- for (const e of entries) dirs.add(path5.dirname(path5.resolve(root, e)));
3045
- if (dirs.size === 0) dirs.add(path5.join(root, "server"));
3185
+ for (const e of entries) dirs.add(path6.dirname(path6.resolve(root, e)));
3186
+ if (dirs.size === 0) dirs.add(path6.join(root, "server"));
3046
3187
  for (const dir of dirs) {
3047
- const p = path5.join(dir, "tsconfig.json");
3048
- if (fs4.existsSync(p)) return p;
3188
+ const p = path6.join(dir, "tsconfig.json");
3189
+ if (fs5.existsSync(p)) return p;
3049
3190
  }
3050
3191
  return null;
3051
3192
  }
3193
+ function serverMigrationsExist(root, toilconfig) {
3194
+ const dirs = /* @__PURE__ */ new Set();
3195
+ const entries = Array.isArray(toilconfig?.entries) ? toilconfig.entries.filter((e) => typeof e === "string") : [];
3196
+ for (const e of entries) dirs.add(path6.dirname(path6.resolve(root, e)));
3197
+ if (dirs.size === 0) dirs.add(path6.join(root, "server"));
3198
+ for (const dir of dirs) {
3199
+ if (fs5.existsSync(path6.join(dir, "migrations"))) return true;
3200
+ }
3201
+ return false;
3202
+ }
3052
3203
  function tsconfigHasToilPlugin(tsconfig) {
3053
3204
  const plugins = asRecord(tsconfig?.compilerOptions)?.plugins;
3054
3205
  if (!Array.isArray(plugins)) return false;
@@ -3060,7 +3211,7 @@ function tsconfigHasToilPlugin(tsconfig) {
3060
3211
  function applyRpcFix(root) {
3061
3212
  const changed = [];
3062
3213
  const skipped = [];
3063
- const pkgPath = path5.join(root, "package.json");
3214
+ const pkgPath = path6.join(root, "package.json");
3064
3215
  const pkgRaw = readFile(pkgPath);
3065
3216
  const pkg = pkgRaw !== null ? readJsonObject(pkgPath) : null;
3066
3217
  if (pkg !== null) {
@@ -3099,7 +3250,7 @@ function applyRpcFix(root) {
3099
3250
  } else if (pkgRaw !== null) {
3100
3251
  skipped.push("package.json (unparseable)");
3101
3252
  }
3102
- const tsPath = path5.join(root, "tsconfig.json");
3253
+ const tsPath = path6.join(root, "tsconfig.json");
3103
3254
  const tsRaw = readFile(tsPath);
3104
3255
  const tsconfig = tsRaw !== null ? readJsonObject(tsPath) : null;
3105
3256
  if (tsconfig !== null) {
@@ -3138,7 +3289,7 @@ function applyRpcFix(root) {
3138
3289
  } else if (tsRaw !== null) {
3139
3290
  skipped.push('tsconfig.json (JSON with comments, add "shared" + paths by hand)');
3140
3291
  }
3141
- const giPath = path5.join(root, ".gitignore");
3292
+ const giPath = path6.join(root, ".gitignore");
3142
3293
  const giRaw = readFile(giPath);
3143
3294
  if (giRaw === null) {
3144
3295
  writeFile(giPath, `${RPC_GITIGNORE_LINE}
@@ -3150,21 +3301,21 @@ function applyRpcFix(root) {
3150
3301
  `);
3151
3302
  changed.push(".gitignore");
3152
3303
  }
3153
- const serverToilconfig = readJsonObject(path5.join(root, "toilconfig.json"));
3304
+ const serverToilconfig = readJsonObject(path6.join(root, "toilconfig.json"));
3154
3305
  if (serverToilconfig !== null) {
3155
3306
  const entries = Array.isArray(serverToilconfig.entries) ? serverToilconfig.entries.filter(
3156
3307
  (e) => typeof e === "string"
3157
3308
  ) : [];
3158
3309
  const dirs = /* @__PURE__ */ new Set();
3159
- for (const e of entries) dirs.add(path5.dirname(path5.resolve(root, e)));
3160
- if (dirs.size === 0) dirs.add(path5.join(root, "server"));
3310
+ for (const e of entries) dirs.add(path6.dirname(path6.resolve(root, e)));
3311
+ if (dirs.size === 0) dirs.add(path6.join(root, "server"));
3161
3312
  for (const dir of dirs) {
3162
- const envPath = path5.join(dir, "toil-server-env.d.ts");
3313
+ const envPath = path6.join(dir, "toil-server-env.d.ts");
3163
3314
  if (readFile(envPath) !== TOIL_SERVER_ENV_DTS2) {
3164
3315
  try {
3165
- fs4.mkdirSync(dir, { recursive: true });
3316
+ fs5.mkdirSync(dir, { recursive: true });
3166
3317
  writeFile(envPath, TOIL_SERVER_ENV_DTS2);
3167
- changed.push(path5.relative(root, envPath));
3318
+ changed.push(path6.relative(root, envPath));
3168
3319
  } catch {
3169
3320
  }
3170
3321
  }
@@ -3194,7 +3345,7 @@ function prettierPluginPresent(root, pkg) {
3194
3345
  return true;
3195
3346
  }
3196
3347
  for (const name of PRETTIER_CONFIG_FILES) {
3197
- const raw = readFile(path5.join(root, name));
3348
+ const raw = readFile(path6.join(root, name));
3198
3349
  if (raw !== null && PRETTIER_MENTION.test(raw)) return true;
3199
3350
  }
3200
3351
  return false;
@@ -3203,7 +3354,7 @@ function applyPrettierFix(root, pkg) {
3203
3354
  const changed = [];
3204
3355
  const skipped = [];
3205
3356
  if (prettierPluginPresent(root, pkg)) return { changed, skipped };
3206
- const pkgPath = path5.join(root, "package.json");
3357
+ const pkgPath = path6.join(root, "package.json");
3207
3358
  const pkgConfig = pkg ? asRecord(pkg.prettier) : null;
3208
3359
  if (pkgConfig !== null) {
3209
3360
  const full = readJsonObject(pkgPath);
@@ -3219,7 +3370,7 @@ function applyPrettierFix(root, pkg) {
3219
3370
  }
3220
3371
  }
3221
3372
  for (const name of [".prettierrc", ".prettierrc.json"]) {
3222
- const filePath = path5.join(root, name);
3373
+ const filePath = path6.join(root, name);
3223
3374
  const raw = readFile(filePath);
3224
3375
  if (raw === null) continue;
3225
3376
  const obj = readJsonObject(filePath);
@@ -3232,13 +3383,13 @@ function applyPrettierFix(root, pkg) {
3232
3383
  changed.push(name);
3233
3384
  return { changed, skipped };
3234
3385
  }
3235
- const jsConfig = PRETTIER_CONFIG_FILES.find((name) => readFile(path5.join(root, name)) !== null);
3386
+ const jsConfig = PRETTIER_CONFIG_FILES.find((name) => readFile(path6.join(root, name)) !== null);
3236
3387
  if (jsConfig) {
3237
3388
  skipped.push(`${jsConfig} (add "${PRETTIER_PLUGIN}" to its plugins by hand)`);
3238
3389
  return { changed, skipped };
3239
3390
  }
3240
3391
  writeFile(
3241
- path5.join(root, ".prettierrc.json"),
3392
+ path6.join(root, ".prettierrc.json"),
3242
3393
  JSON.stringify({ plugins: [PRETTIER_PLUGIN] }, null, 4) + "\n"
3243
3394
  );
3244
3395
  changed.push(".prettierrc.json");
@@ -3251,7 +3402,7 @@ function applyServerEditorFix(root, toilconfig) {
3251
3402
  if (tsPath === null) {
3252
3403
  skipped.push("server/tsconfig.json (not found; add the toilscript ts-plugin by hand)");
3253
3404
  } else {
3254
- const rel = path5.relative(root, tsPath);
3405
+ const rel = path6.relative(root, tsPath);
3255
3406
  const raw = readFile(tsPath);
3256
3407
  const parsed = raw !== null ? readJsonObject(tsPath) : null;
3257
3408
  if (parsed === null) {
@@ -3265,7 +3416,7 @@ function applyServerEditorFix(root, toilconfig) {
3265
3416
  changed.push(rel);
3266
3417
  }
3267
3418
  }
3268
- const vsPath = path5.join(root, ".vscode", "settings.json");
3419
+ const vsPath = path6.join(root, ".vscode", "settings.json");
3269
3420
  const vsRaw = readFile(vsPath);
3270
3421
  const vs = vsRaw !== null ? readJsonObject(vsPath) : {};
3271
3422
  if (vs === null) {
@@ -3281,7 +3432,7 @@ function applyServerEditorFix(root, toilconfig) {
3281
3432
  touched = true;
3282
3433
  }
3283
3434
  if (touched) {
3284
- fs4.mkdirSync(path5.dirname(vsPath), { recursive: true });
3435
+ fs5.mkdirSync(path6.dirname(vsPath), { recursive: true });
3285
3436
  writeFile(vsPath, JSON.stringify(vs, null, 4) + "\n");
3286
3437
  changed.push(".vscode/settings.json");
3287
3438
  }
@@ -3289,8 +3440,8 @@ function applyServerEditorFix(root, toilconfig) {
3289
3440
  return { changed, skipped };
3290
3441
  }
3291
3442
  function frameworkMeta() {
3292
- const pkgPath = path5.resolve(
3293
- path5.dirname(fileURLToPath3(import.meta.url)),
3443
+ const pkgPath = path6.resolve(
3444
+ path6.dirname(fileURLToPath3(import.meta.url)),
3294
3445
  "..",
3295
3446
  "..",
3296
3447
  "package.json"
@@ -3303,34 +3454,34 @@ function frameworkMeta() {
3303
3454
  var LOCKFILES = ["package-lock.json", "pnpm-lock.yaml", "yarn.lock", "bun.lockb"];
3304
3455
  function readEntry(clientAbsDir) {
3305
3456
  for (const name of ["toil.tsx", "toil.jsx", "main.tsx", "main.jsx"]) {
3306
- const source = readFile(path5.join(clientAbsDir, name));
3457
+ const source = readFile(path6.join(clientAbsDir, name));
3307
3458
  if (source !== null && /toiljs\/routes|\bmount\s*\(/.test(source)) return source;
3308
3459
  }
3309
3460
  return null;
3310
3461
  }
3311
3462
  function collectSources(root, dir, cap) {
3312
- const out = [];
3463
+ const out2 = [];
3313
3464
  const visit = (current) => {
3314
- if (out.length >= cap) return;
3465
+ if (out2.length >= cap) return;
3315
3466
  let entries;
3316
3467
  try {
3317
- entries = fs4.readdirSync(current, { withFileTypes: true });
3468
+ entries = fs5.readdirSync(current, { withFileTypes: true });
3318
3469
  } catch {
3319
3470
  return;
3320
3471
  }
3321
3472
  for (const entry of entries) {
3322
- if (out.length >= cap) break;
3323
- const full = path5.join(current, entry.name);
3473
+ if (out2.length >= cap) break;
3474
+ const full = path6.join(current, entry.name);
3324
3475
  if (entry.isDirectory()) {
3325
3476
  if (entry.name !== "node_modules") visit(full);
3326
3477
  } else if (/\.(tsx|jsx)$/.test(entry.name)) {
3327
3478
  const source = readFile(full);
3328
- if (source !== null) out.push({ path: path5.relative(root, full), source });
3479
+ if (source !== null) out2.push({ path: path6.relative(root, full), source });
3329
3480
  }
3330
3481
  }
3331
3482
  };
3332
3483
  visit(dir);
3333
- return out;
3484
+ return out2;
3334
3485
  }
3335
3486
  function glyph(status) {
3336
3487
  if (status === "pass") return success("\u2713");
@@ -3339,31 +3490,31 @@ function glyph(status) {
3339
3490
  }
3340
3491
  function renderHuman(groups) {
3341
3492
  const summary = summarize(groups);
3342
- const out = [];
3493
+ const out2 = [];
3343
3494
  for (const group of groups) {
3344
- out.push(" " + bold(group.title));
3495
+ out2.push(" " + bold(group.title));
3345
3496
  for (const check of group.checks) {
3346
3497
  let line = ` ${glyph(check.status)} ${check.label}`;
3347
3498
  if (check.detail) line += dim(` ${check.detail}`);
3348
- out.push(line);
3499
+ out2.push(line);
3349
3500
  if (check.fix && check.status !== "pass")
3350
- out.push(" " + dim(`fix: ${check.fix}`));
3501
+ out2.push(" " + dim(`fix: ${check.fix}`));
3351
3502
  }
3352
- out.push("");
3503
+ out2.push("");
3353
3504
  }
3354
3505
  const parts = [success(`${String(summary.pass)} passed`)];
3355
3506
  if (summary.warn > 0) {
3356
3507
  parts.push(warn(`${String(summary.warn)} warning${summary.warn === 1 ? "" : "s"}`));
3357
3508
  }
3358
3509
  if (summary.fail > 0) parts.push(danger(`${String(summary.fail)} failed`));
3359
- out.push(" " + parts.join(dim(", ")));
3360
- out.push("");
3361
- process.stdout.write(out.join("\n") + "\n");
3510
+ out2.push(" " + parts.join(dim(", ")));
3511
+ out2.push("");
3512
+ process.stdout.write(out2.join("\n") + "\n");
3362
3513
  }
3363
3514
  async function runDoctor(opts) {
3364
- const root = path5.resolve(opts.root ?? opts.cwd);
3515
+ const root = path6.resolve(opts.root ?? opts.cwd);
3365
3516
  const meta = frameworkMeta();
3366
- const projectPkg = readJsonObject(path5.join(root, "package.json"));
3517
+ const projectPkg = readJsonObject(path6.join(root, "package.json"));
3367
3518
  const deps = {
3368
3519
  ...projectPkg ? stringRecord(projectPkg.dependencies) : {},
3369
3520
  ...projectPkg ? stringRecord(projectPkg.devDependencies) : {}
@@ -3375,11 +3526,11 @@ async function runDoctor(opts) {
3375
3526
  } catch (err) {
3376
3527
  configError = err instanceof Error ? err.message : String(err);
3377
3528
  }
3378
- const clientAbsDir = cfg ? cfg.clientAbsDir : path5.join(root, "client");
3379
- const routesAbsDir = cfg ? cfg.routesAbsDir : path5.join(clientAbsDir, "routes");
3380
- const publicDir = cfg ? cfg.publicDir : path5.join(clientAbsDir, "public");
3529
+ const clientAbsDir = cfg ? cfg.clientAbsDir : path6.join(root, "client");
3530
+ const routesAbsDir = cfg ? cfg.routesAbsDir : path6.join(clientAbsDir, "routes");
3531
+ const publicDir = cfg ? cfg.publicDir : path6.join(clientAbsDir, "public");
3381
3532
  const entrySource = readEntry(clientAbsDir);
3382
- const indexHtml = readFile(path5.join(publicDir, "index.html"));
3533
+ const indexHtml = readFile(path6.join(publicDir, "index.html"));
3383
3534
  const routes = scanRoutes(routesAbsDir);
3384
3535
  const mainPatterns = routes.filter((r2) => r2.slot === void 0).map((r2) => r2.pattern);
3385
3536
  const assetIssues = findRelativeAssets(collectSources(root, clientAbsDir, 200));
@@ -3392,14 +3543,14 @@ async function runDoctor(opts) {
3392
3543
  }
3393
3544
  const ppPkg = preprocessorImported ? PREPROCESSOR_PKG[preprocessorImported] : null;
3394
3545
  const preprocessorInstalled = ppPkg === null || ppPkg in deps;
3395
- const toilconfig = readJsonObject(path5.join(root, "toilconfig.json"));
3546
+ const toilconfig = readJsonObject(path6.join(root, "toilconfig.json"));
3396
3547
  const serverPresent = toilconfig !== null;
3397
3548
  let missingEntries = [];
3398
3549
  let toilscriptInstalled = false;
3399
3550
  let wasmExists = false;
3400
3551
  if (toilconfig) {
3401
3552
  const entries = Array.isArray(toilconfig.entries) ? toilconfig.entries.filter((e) => typeof e === "string") : [];
3402
- missingEntries = entries.filter((e) => !fs4.existsSync(path5.join(root, e)));
3553
+ missingEntries = entries.filter((e) => !fs5.existsSync(path6.join(root, e)));
3403
3554
  toilscriptInstalled = isPackageInstalled(root, "toilscript");
3404
3555
  const targets = typeof toilconfig.targets === "object" && toilconfig.targets !== null ? toilconfig.targets : {};
3405
3556
  const outFiles = [];
@@ -3409,10 +3560,10 @@ async function runDoctor(opts) {
3409
3560
  if (typeof outFile === "string") outFiles.push(outFile);
3410
3561
  }
3411
3562
  }
3412
- wasmExists = outFiles.some((f) => fs4.existsSync(path5.join(root, f)));
3563
+ wasmExists = outFiles.some((f) => fs5.existsSync(path6.join(root, f)));
3413
3564
  if (!wasmExists && outFiles.length === 0) {
3414
3565
  try {
3415
- wasmExists = fs4.readdirSync(path5.join(root, "build", "server")).some((f) => f.endsWith(".wasm"));
3566
+ wasmExists = fs5.readdirSync(path6.join(root, "build", "server")).some((f) => f.endsWith(".wasm"));
3416
3567
  } catch {
3417
3568
  wasmExists = false;
3418
3569
  }
@@ -3428,7 +3579,7 @@ async function runDoctor(opts) {
3428
3579
  const authFacts = gatherAuthFacts(root, toilconfig);
3429
3580
  const prettierPresent = prettierPluginPresent(
3430
3581
  root,
3431
- readJsonObject(path5.join(root, "package.json"))
3582
+ readJsonObject(path6.join(root, "package.json"))
3432
3583
  );
3433
3584
  const serverTsPath = serverPresent ? serverTsconfigPath(root, toilconfig) : null;
3434
3585
  const serverTsParsed = serverTsPath ? readJsonObject(serverTsPath) : null;
@@ -3452,7 +3603,7 @@ async function runDoctor(opts) {
3452
3603
  checkNode(process.versions.node, meta.node),
3453
3604
  checkToiljsInstalled("toiljs" in deps ? version() : null),
3454
3605
  ...peerChecks,
3455
- checkPackageManager(LOCKFILES.filter((f) => fs4.existsSync(path5.join(root, f))))
3606
+ checkPackageManager(LOCKFILES.filter((f) => fs5.existsSync(path6.join(root, f))))
3456
3607
  ]
3457
3608
  },
3458
3609
  {
@@ -3461,13 +3612,13 @@ async function runDoctor(opts) {
3461
3612
  checkDir(
3462
3613
  "client-dir",
3463
3614
  "client/ directory",
3464
- fs4.existsSync(clientAbsDir),
3615
+ fs5.existsSync(clientAbsDir),
3465
3616
  "Create a client/ directory for your app."
3466
3617
  ),
3467
3618
  checkDir(
3468
3619
  "routes-dir",
3469
3620
  "routes/ directory",
3470
- fs4.existsSync(routesAbsDir),
3621
+ fs5.existsSync(routesAbsDir),
3471
3622
  "Create client/routes/ and add an index.tsx."
3472
3623
  ),
3473
3624
  checkRootElement(indexHtml),
@@ -3502,7 +3653,8 @@ async function runDoctor(opts) {
3502
3653
  checkRpcWiring(rpcFacts),
3503
3654
  checkRestDispatch(restFacts),
3504
3655
  checkPrettierPlugin(prettierPresent),
3505
- checkServerTsPlugin(serverTsPluginPresent)
3656
+ checkServerTsPlugin(serverTsPluginPresent),
3657
+ checkMigrationsDir(serverMigrationsExist(root, toilconfig))
3506
3658
  ] : [checkToilconfig(false)]
3507
3659
  }
3508
3660
  ];
@@ -3525,30 +3677,30 @@ async function runDoctor(opts) {
3525
3677
  if (hasFailures(summary)) process.exitCode = 1;
3526
3678
  }
3527
3679
  function renderRpcFix(result) {
3528
- const out = [];
3680
+ const out2 = [];
3529
3681
  if (result.changed.length > 0) {
3530
- out.push(" " + success("fixed server wiring") + dim(` ${result.changed.join(", ")}`));
3682
+ out2.push(" " + success("fixed server wiring") + dim(` ${result.changed.join(", ")}`));
3531
3683
  if (result.changed.includes("package.json")) {
3532
- out.push(
3684
+ out2.push(
3533
3685
  " " + dim("run your installer (npm/pnpm/yarn) if the toilscript version changed.")
3534
3686
  );
3535
3687
  }
3536
3688
  } else {
3537
- out.push(" " + dim("server wiring already in place, nothing to fix."));
3689
+ out2.push(" " + dim("server wiring already in place, nothing to fix."));
3538
3690
  }
3539
- for (const item of result.skipped) out.push(" " + warn("skipped") + dim(` ${item}`));
3540
- process.stdout.write(out.join("\n") + "\n\n");
3691
+ for (const item of result.skipped) out2.push(" " + warn("skipped") + dim(` ${item}`));
3692
+ process.stdout.write(out2.join("\n") + "\n\n");
3541
3693
  }
3542
3694
 
3543
3695
  // src/cli/notify.ts
3544
- import fs6 from "node:fs";
3696
+ import fs7 from "node:fs";
3545
3697
  import os from "node:os";
3546
- import path7 from "node:path";
3698
+ import path8 from "node:path";
3547
3699
  import { fileURLToPath as fileURLToPath4 } from "node:url";
3548
3700
 
3549
3701
  // src/cli/update.ts
3550
- import fs5 from "node:fs";
3551
- import path6 from "node:path";
3702
+ import fs6 from "node:fs";
3703
+ import path7 from "node:path";
3552
3704
 
3553
3705
  // src/cli/updates.ts
3554
3706
  function parseVersion2(v) {
@@ -3571,9 +3723,9 @@ function parseNcuJson(stdout2) {
3571
3723
  try {
3572
3724
  const parsed = JSON.parse(stdout2.slice(start2, end + 1));
3573
3725
  if (typeof parsed !== "object" || parsed === null) return {};
3574
- const out = {};
3575
- for (const [k, v] of Object.entries(parsed)) if (typeof v === "string") out[k] = v;
3576
- return out;
3726
+ const out2 = {};
3727
+ for (const [k, v] of Object.entries(parsed)) if (typeof v === "string") out2[k] = v;
3728
+ return out2;
3577
3729
  } catch {
3578
3730
  return {};
3579
3731
  }
@@ -3588,23 +3740,50 @@ function buildRows(upgraded, currentDeps) {
3588
3740
 
3589
3741
  // src/cli/update.ts
3590
3742
  function detectPackageManager2(root) {
3591
- if (fs5.existsSync(path6.join(root, "pnpm-lock.yaml"))) return { name: "pnpm", ncuName: "pnpm" };
3592
- if (fs5.existsSync(path6.join(root, "yarn.lock"))) return { name: "yarn", ncuName: "yarn" };
3593
- if (fs5.existsSync(path6.join(root, "bun.lockb"))) return { name: "bun", ncuName: "bun" };
3743
+ if (fs6.existsSync(path7.join(root, "pnpm-lock.yaml"))) return { name: "pnpm", ncuName: "pnpm" };
3744
+ if (fs6.existsSync(path7.join(root, "yarn.lock"))) return { name: "yarn", ncuName: "yarn" };
3745
+ if (fs6.existsSync(path7.join(root, "bun.lockb"))) return { name: "bun", ncuName: "bun" };
3594
3746
  return { name: "npm", ncuName: "npm" };
3595
3747
  }
3596
3748
  function readDependencies(pkgPath) {
3597
- const parsed = JSON.parse(fs5.readFileSync(pkgPath, "utf8"));
3749
+ const parsed = JSON.parse(fs6.readFileSync(pkgPath, "utf8"));
3598
3750
  if (typeof parsed !== "object" || parsed === null) return {};
3599
3751
  const pkg = parsed;
3600
3752
  const merge = (v) => {
3601
3753
  if (typeof v !== "object" || v === null) return {};
3602
- const out = {};
3603
- for (const [k, val] of Object.entries(v)) if (typeof val === "string") out[k] = val;
3604
- return out;
3754
+ const out2 = {};
3755
+ for (const [k, val] of Object.entries(v)) if (typeof val === "string") out2[k] = val;
3756
+ return out2;
3605
3757
  };
3606
3758
  return { ...merge(pkg.dependencies), ...merge(pkg.devDependencies) };
3607
3759
  }
3760
+ function serverDirs(root) {
3761
+ const dirs = /* @__PURE__ */ new Set();
3762
+ try {
3763
+ const parsed = JSON.parse(
3764
+ fs6.readFileSync(path7.join(root, "toilconfig.json"), "utf8")
3765
+ );
3766
+ const entries = typeof parsed === "object" && parsed !== null && Array.isArray(parsed.entries) ? parsed.entries.filter(
3767
+ (e) => typeof e === "string"
3768
+ ) : [];
3769
+ for (const e of entries) dirs.add(path7.dirname(path7.resolve(root, e)));
3770
+ } catch {
3771
+ }
3772
+ if (dirs.size === 0) dirs.add(path7.join(root, "server"));
3773
+ return [...dirs];
3774
+ }
3775
+ function ensureMigrationsDirs(root) {
3776
+ const created = [];
3777
+ for (const dir of serverDirs(root)) {
3778
+ if (!fs6.existsSync(dir)) continue;
3779
+ const migrations = path7.join(dir, "migrations");
3780
+ if (fs6.existsSync(migrations)) continue;
3781
+ fs6.mkdirSync(migrations, { recursive: true });
3782
+ fs6.writeFileSync(path7.join(migrations, "README.md"), MIGRATIONS_README);
3783
+ created.push(path7.relative(root, migrations) || "migrations");
3784
+ }
3785
+ return created;
3786
+ }
3608
3787
  function bumpColor(bump, text2) {
3609
3788
  if (bump === "major") return danger(text2);
3610
3789
  if (bump === "minor") return warn(text2);
@@ -3616,9 +3795,9 @@ function rowLine(row) {
3616
3795
  }
3617
3796
  var TARGETS = /* @__PURE__ */ new Set(["latest", "minor", "patch", "newest", "greatest"]);
3618
3797
  async function runUpdate(opts) {
3619
- const root = path6.resolve(opts.root ?? opts.cwd);
3620
- const pkgPath = path6.join(root, "package.json");
3621
- if (!fs5.existsSync(pkgPath)) {
3798
+ const root = path7.resolve(opts.root ?? opts.cwd);
3799
+ const pkgPath = path7.join(root, "package.json");
3800
+ if (!fs6.existsSync(pkgPath)) {
3622
3801
  throw new Error("No package.json here. Run from your project root or pass --root <dir>.");
3623
3802
  }
3624
3803
  const currentDeps = readDependencies(pkgPath);
@@ -3634,6 +3813,13 @@ async function runUpdate(opts) {
3634
3813
  ...extra
3635
3814
  ];
3636
3815
  intro(accent("toiljs update"));
3816
+ const createdMigrations = ensureMigrationsDirs(root);
3817
+ if (createdMigrations.length > 0) {
3818
+ note(
3819
+ createdMigrations.map((p) => `${dim("+")} ${p}/`).join("\n"),
3820
+ "Created ToilDB migrations folder"
3821
+ );
3822
+ }
3637
3823
  const s = spinner();
3638
3824
  s.start("Checking the registry for updates");
3639
3825
  const res = await capture("npx", ncuArgs(["--jsonUpgraded"]), root);
@@ -3773,8 +3959,8 @@ function findOutdated(latest, projectVersion, cliVersion, cliIsLocal, pm) {
3773
3959
  var REGISTRY_URL = "https://registry.npmjs.org/toiljs/latest";
3774
3960
  var FETCH_TIMEOUT_MS = 2e3;
3775
3961
  function cacheFile() {
3776
- const base = process.env.XDG_CACHE_HOME ?? path7.join(os.homedir(), ".cache");
3777
- return path7.join(base, "toiljs", "update-check.json");
3962
+ const base = process.env.XDG_CACHE_HOME ?? path8.join(os.homedir(), ".cache");
3963
+ return path8.join(base, "toiljs", "update-check.json");
3778
3964
  }
3779
3965
  async function fetchLatest() {
3780
3966
  const ctrl = new AbortController();
@@ -3800,22 +3986,22 @@ async function fetchLatest() {
3800
3986
  async function resolveLatest() {
3801
3987
  const file = cacheFile();
3802
3988
  try {
3803
- const cached = parseCheckCache(fs6.readFileSync(file, "utf8"));
3989
+ const cached = parseCheckCache(fs7.readFileSync(file, "utf8"));
3804
3990
  if (cached && isCacheFresh(cached, Date.now())) return cached.latest;
3805
3991
  } catch {
3806
3992
  }
3807
3993
  const latest = await fetchLatest();
3808
3994
  try {
3809
- fs6.mkdirSync(path7.dirname(file), { recursive: true });
3810
- fs6.writeFileSync(file, JSON.stringify({ latest, checkedAt: Date.now() }));
3995
+ fs7.mkdirSync(path8.dirname(file), { recursive: true });
3996
+ fs7.writeFileSync(file, JSON.stringify({ latest, checkedAt: Date.now() }));
3811
3997
  } catch {
3812
3998
  }
3813
3999
  return latest;
3814
4000
  }
3815
4001
  function projectToiljsVersion(root) {
3816
4002
  try {
3817
- const raw = fs6.readFileSync(
3818
- path7.join(root, "node_modules", "toiljs", "package.json"),
4003
+ const raw = fs7.readFileSync(
4004
+ path8.join(root, "node_modules", "toiljs", "package.json"),
3819
4005
  "utf8"
3820
4006
  );
3821
4007
  const parsed = JSON.parse(raw);
@@ -3840,9 +4026,9 @@ async function notifyIfOutdated(rootArg) {
3840
4026
  if (env.TOILJS_NO_UPDATE_CHECK || env.NO_UPDATE_NOTIFIER || env.CI) return;
3841
4027
  const latest = await resolveLatest();
3842
4028
  if (!latest) return;
3843
- const root = path7.resolve(rootArg ?? process.cwd());
3844
- const cliDir = path7.dirname(fileURLToPath4(import.meta.url));
3845
- const cliIsLocal = cliDir.startsWith(path7.join(root, "node_modules") + path7.sep);
4029
+ const root = path8.resolve(rootArg ?? process.cwd());
4030
+ const cliDir = path8.dirname(fileURLToPath4(import.meta.url));
4031
+ const cliIsLocal = cliDir.startsWith(path8.join(root, "node_modules") + path8.sep);
3846
4032
  const rows = findOutdated(
3847
4033
  latest,
3848
4034
  projectToiljsVersion(root),
@@ -3954,6 +4140,7 @@ function printHelp() {
3954
4140
  cmd("start", "self-host the built app (hyper-express / uWS)"),
3955
4141
  cmd("doctor", "diagnose project setup and dependencies"),
3956
4142
  cmd("update", "check for and apply dependency updates"),
4143
+ cmd("db <action>", "manage dev DB data (status | reset | export | import)"),
3957
4144
  "",
3958
4145
  bold("Options"),
3959
4146
  cmd("--root <dir>", "project root (default: current directory)"),
@@ -4054,6 +4241,12 @@ async function main() {
4054
4241
  target: flags.target
4055
4242
  });
4056
4243
  break;
4244
+ case "db": {
4245
+ const action = rest[0];
4246
+ const fileArg = rest[1] !== void 0 && !rest[1].startsWith("-") ? rest[1] : void 0;
4247
+ runDb(action, fileArg, { root: flags.root });
4248
+ break;
4249
+ }
4057
4250
  case "help":
4058
4251
  case "--help":
4059
4252
  case "-h":