toiljs 0.0.122 → 0.0.124

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.
@@ -650,11 +650,11 @@ var require_valid = __commonJS({
650
650
  "node_modules/semver/functions/valid.js"(exports, module) {
651
651
  "use strict";
652
652
  var parse = require_parse();
653
- var valid = (version2, options) => {
653
+ var valid2 = (version2, options) => {
654
654
  const v = parse(version2, options);
655
655
  return v ? v.version : null;
656
656
  };
657
- module.exports = valid;
657
+ module.exports = valid2;
658
658
  }
659
659
  });
660
660
 
@@ -871,8 +871,8 @@ var require_eq = __commonJS({
871
871
  "node_modules/semver/functions/eq.js"(exports, module) {
872
872
  "use strict";
873
873
  var compare = require_compare();
874
- var eq = (a2, b, loose) => compare(a2, b, loose) === 0;
875
- module.exports = eq;
874
+ var eq2 = (a2, b, loose) => compare(a2, b, loose) === 0;
875
+ module.exports = eq2;
876
876
  }
877
877
  });
878
878
 
@@ -910,7 +910,7 @@ var require_lte = __commonJS({
910
910
  var require_cmp = __commonJS({
911
911
  "node_modules/semver/functions/cmp.js"(exports, module) {
912
912
  "use strict";
913
- var eq = require_eq();
913
+ var eq2 = require_eq();
914
914
  var neq = require_neq();
915
915
  var gt = require_gt();
916
916
  var gte = require_gte();
@@ -937,7 +937,7 @@ var require_cmp = __commonJS({
937
937
  case "":
938
938
  case "=":
939
939
  case "==":
940
- return eq(a2, b, loose);
940
+ return eq2(a2, b, loose);
941
941
  case "!=":
942
942
  return neq(a2, b, loose);
943
943
  case ">":
@@ -1584,7 +1584,7 @@ var require_satisfies = __commonJS({
1584
1584
  "node_modules/semver/functions/satisfies.js"(exports, module) {
1585
1585
  "use strict";
1586
1586
  var Range = require_range();
1587
- var satisfies2 = (version2, range, options) => {
1587
+ var satisfies4 = (version2, range, options) => {
1588
1588
  try {
1589
1589
  range = new Range(range, options);
1590
1590
  } catch (er) {
@@ -1592,7 +1592,7 @@ var require_satisfies = __commonJS({
1592
1592
  }
1593
1593
  return range.test(version2);
1594
1594
  };
1595
- module.exports = satisfies2;
1595
+ module.exports = satisfies4;
1596
1596
  }
1597
1597
  });
1598
1598
 
@@ -1728,14 +1728,14 @@ var require_valid2 = __commonJS({
1728
1728
  "node_modules/semver/ranges/valid.js"(exports, module) {
1729
1729
  "use strict";
1730
1730
  var Range = require_range();
1731
- var validRange2 = (range, options) => {
1731
+ var validRange4 = (range, options) => {
1732
1732
  try {
1733
1733
  return new Range(range, options).range || "*";
1734
1734
  } catch (er) {
1735
1735
  return null;
1736
1736
  }
1737
1737
  };
1738
- module.exports = validRange2;
1738
+ module.exports = validRange4;
1739
1739
  }
1740
1740
  });
1741
1741
 
@@ -1747,7 +1747,7 @@ var require_outside = __commonJS({
1747
1747
  var Comparator = require_comparator();
1748
1748
  var { ANY } = Comparator;
1749
1749
  var Range = require_range();
1750
- var satisfies2 = require_satisfies();
1750
+ var satisfies4 = require_satisfies();
1751
1751
  var gt = require_gt();
1752
1752
  var lt = require_lt();
1753
1753
  var lte = require_lte();
@@ -1774,7 +1774,7 @@ var require_outside = __commonJS({
1774
1774
  default:
1775
1775
  throw new TypeError('Must provide a hilo val of "<" or ">"');
1776
1776
  }
1777
- if (satisfies2(version2, range, options)) {
1777
+ if (satisfies4(version2, range, options)) {
1778
1778
  return false;
1779
1779
  }
1780
1780
  for (let i2 = 0; i2 < range.set.length; ++i2) {
@@ -1833,12 +1833,12 @@ var require_intersects = __commonJS({
1833
1833
  "node_modules/semver/ranges/intersects.js"(exports, module) {
1834
1834
  "use strict";
1835
1835
  var Range = require_range();
1836
- var intersects = (r1, r2, options) => {
1836
+ var intersects2 = (r1, r2, options) => {
1837
1837
  r1 = new Range(r1, options);
1838
1838
  r2 = new Range(r2, options);
1839
1839
  return r1.intersects(r2, options);
1840
1840
  };
1841
- module.exports = intersects;
1841
+ module.exports = intersects2;
1842
1842
  }
1843
1843
  });
1844
1844
 
@@ -1846,7 +1846,7 @@ var require_intersects = __commonJS({
1846
1846
  var require_simplify = __commonJS({
1847
1847
  "node_modules/semver/ranges/simplify.js"(exports, module) {
1848
1848
  "use strict";
1849
- var satisfies2 = require_satisfies();
1849
+ var satisfies4 = require_satisfies();
1850
1850
  var compare = require_compare();
1851
1851
  module.exports = (versions, range, options) => {
1852
1852
  const set = [];
@@ -1854,7 +1854,7 @@ var require_simplify = __commonJS({
1854
1854
  let prev = null;
1855
1855
  const v = versions.sort((a2, b) => compare(a2, b, options));
1856
1856
  for (const version2 of v) {
1857
- const included = satisfies2(version2, range, options);
1857
+ const included = satisfies4(version2, range, options);
1858
1858
  if (included) {
1859
1859
  prev = version2;
1860
1860
  if (!first) {
@@ -1899,9 +1899,9 @@ var require_subset = __commonJS({
1899
1899
  var Range = require_range();
1900
1900
  var Comparator = require_comparator();
1901
1901
  var { ANY } = Comparator;
1902
- var satisfies2 = require_satisfies();
1902
+ var satisfies4 = require_satisfies();
1903
1903
  var compare = require_compare();
1904
- var subset2 = (sub, dom, options = {}) => {
1904
+ var subset3 = (sub, dom, options = {}) => {
1905
1905
  if (sub === dom) {
1906
1906
  return true;
1907
1907
  }
@@ -1967,15 +1967,15 @@ var require_subset = __commonJS({
1967
1967
  return null;
1968
1968
  }
1969
1969
  }
1970
- for (const eq of eqSet) {
1971
- if (gt && !satisfies2(eq, String(gt), options)) {
1970
+ for (const eq2 of eqSet) {
1971
+ if (gt && !satisfies4(eq2, String(gt), options)) {
1972
1972
  return null;
1973
1973
  }
1974
- if (lt && !satisfies2(eq, String(lt), options)) {
1974
+ if (lt && !satisfies4(eq2, String(lt), options)) {
1975
1975
  return null;
1976
1976
  }
1977
1977
  for (const c2 of dom) {
1978
- if (!satisfies2(eq, String(c2), options)) {
1978
+ if (!satisfies4(eq2, String(c2), options)) {
1979
1979
  return false;
1980
1980
  }
1981
1981
  }
@@ -2050,7 +2050,7 @@ var require_subset = __commonJS({
2050
2050
  const comp = compare(a2.semver, b.semver, options);
2051
2051
  return comp < 0 ? a2 : comp > 0 ? b : b.operator === "<" && a2.operator === "<=" ? b : a2;
2052
2052
  };
2053
- module.exports = subset2;
2053
+ module.exports = subset3;
2054
2054
  }
2055
2055
  });
2056
2056
 
@@ -2063,7 +2063,7 @@ var require_semver2 = __commonJS({
2063
2063
  var SemVer = require_semver();
2064
2064
  var identifiers = require_identifiers();
2065
2065
  var parse = require_parse();
2066
- var valid = require_valid();
2066
+ var valid2 = require_valid();
2067
2067
  var clean2 = require_clean();
2068
2068
  var inc = require_inc();
2069
2069
  var diff = require_diff();
@@ -2079,7 +2079,7 @@ var require_semver2 = __commonJS({
2079
2079
  var rsort = require_rsort();
2080
2080
  var gt = require_gt();
2081
2081
  var lt = require_lt();
2082
- var eq = require_eq();
2082
+ var eq2 = require_eq();
2083
2083
  var neq = require_neq();
2084
2084
  var gte = require_gte();
2085
2085
  var lte = require_lte();
@@ -2088,21 +2088,21 @@ var require_semver2 = __commonJS({
2088
2088
  var truncate = require_truncate();
2089
2089
  var Comparator = require_comparator();
2090
2090
  var Range = require_range();
2091
- var satisfies2 = require_satisfies();
2091
+ var satisfies4 = require_satisfies();
2092
2092
  var toComparators = require_to_comparators();
2093
2093
  var maxSatisfying = require_max_satisfying();
2094
2094
  var minSatisfying = require_min_satisfying();
2095
2095
  var minVersion2 = require_min_version();
2096
- var validRange2 = require_valid2();
2096
+ var validRange4 = require_valid2();
2097
2097
  var outside = require_outside();
2098
2098
  var gtr = require_gtr();
2099
2099
  var ltr = require_ltr();
2100
- var intersects = require_intersects();
2100
+ var intersects2 = require_intersects();
2101
2101
  var simplifyRange = require_simplify();
2102
- var subset2 = require_subset();
2102
+ var subset3 = require_subset();
2103
2103
  module.exports = {
2104
2104
  parse,
2105
- valid,
2105
+ valid: valid2,
2106
2106
  clean: clean2,
2107
2107
  inc,
2108
2108
  diff,
@@ -2118,7 +2118,7 @@ var require_semver2 = __commonJS({
2118
2118
  rsort,
2119
2119
  gt,
2120
2120
  lt,
2121
- eq,
2121
+ eq: eq2,
2122
2122
  neq,
2123
2123
  gte,
2124
2124
  lte,
@@ -2127,18 +2127,18 @@ var require_semver2 = __commonJS({
2127
2127
  truncate,
2128
2128
  Comparator,
2129
2129
  Range,
2130
- satisfies: satisfies2,
2130
+ satisfies: satisfies4,
2131
2131
  toComparators,
2132
2132
  maxSatisfying,
2133
2133
  minSatisfying,
2134
2134
  minVersion: minVersion2,
2135
- validRange: validRange2,
2135
+ validRange: validRange4,
2136
2136
  outside,
2137
2137
  gtr,
2138
2138
  ltr,
2139
- intersects,
2139
+ intersects: intersects2,
2140
2140
  simplifyRange,
2141
- subset: subset2,
2141
+ subset: subset3,
2142
2142
  SemVer,
2143
2143
  re: internalRe.re,
2144
2144
  src: internalRe.src,
@@ -3510,7 +3510,7 @@ var PREPROCESSOR_PKG = {
3510
3510
  };
3511
3511
  var TAILWIND_PKGS = ["tailwindcss", "@tailwindcss/vite"];
3512
3512
  var PKG_VERSION = {
3513
- sass: "^1.104.1",
3513
+ sass: "^1.105.1",
3514
3514
  less: "^4.9.1",
3515
3515
  stylus: "^0.64.0",
3516
3516
  tailwindcss: "^4.3.3",
@@ -4017,6 +4017,88 @@ import { fileURLToPath as fileURLToPath2 } from "node:url";
4017
4017
  var import_picocolors3 = __toESM(require_picocolors(), 1);
4018
4018
  import { AI_HELPER_IDS, AI_HELPERS, aiHelperFiles, TOIL_DOCS, TOIL_ENV_DTS } from "toiljs/compiler";
4019
4019
 
4020
+ // src/cli/testing.ts
4021
+ var TEST_DEPENDENCIES = {
4022
+ vitest: "^5.0.3",
4023
+ "@vitest/browser-webdriverio": "^5.0.0",
4024
+ "@vitest/coverage-v8": "^5.0.3",
4025
+ webdriverio: "^10.0.2"
4026
+ };
4027
+ var TEST_SCRIPTS = {
4028
+ test: "toiljs test",
4029
+ "test:watch": "toiljs test --watch",
4030
+ "test:browser": "toiljs test --browser",
4031
+ "test:all": "toiljs test --all",
4032
+ "test:coverage": "toiljs test --all --coverage"
4033
+ };
4034
+ function testingFiles() {
4035
+ return {
4036
+ "vitest.config.ts": "import { defineTestConfig } from 'toiljs/vitest';\n\nexport default defineTestConfig();\n",
4037
+ "client/lib/Greeting.ts": [
4038
+ "/** Formats a greeting, treating blank input as an unnamed guest. */",
4039
+ "export class Greeting {",
4040
+ " public static message(name: string): string {",
4041
+ " const recipient: string = name.trim() || 'guest';",
4042
+ " return `Hello, ${recipient}!`;",
4043
+ " }",
4044
+ "}",
4045
+ ""
4046
+ ].join("\n"),
4047
+ "tests/Greeting.test.ts": [
4048
+ "import { expect, test } from 'vitest';",
4049
+ "import { Greeting } from '../client/lib/Greeting';",
4050
+ "",
4051
+ "test('trims the supplied name', (): void => {",
4052
+ " expect(Greeting.message(' Ada ')).toBe('Hello, Ada!');",
4053
+ "});",
4054
+ "",
4055
+ "test('greets an unnamed guest', (): void => {",
4056
+ " expect(Greeting.message(' ')).toBe('Hello, guest!');",
4057
+ "});",
4058
+ ""
4059
+ ].join("\n"),
4060
+ "client/components/TestCounter.tsx": [
4061
+ "import { useState, type ReactElement } from 'react';",
4062
+ "",
4063
+ "/** A small component used by the starter browser test. */",
4064
+ "export function TestCounter(): ReactElement {",
4065
+ " const [count, setCount] = useState<number>(0);",
4066
+ " return (",
4067
+ " <section>",
4068
+ ' <Toil.Link href="/">Home</Toil.Link>',
4069
+ " <button onClick={(): void => setCount(count + 1)}>Count: {count}</button>",
4070
+ " </section>",
4071
+ " );",
4072
+ "}",
4073
+ ""
4074
+ ].join("\n"),
4075
+ "tests/TestCounter.browser.test.tsx": [
4076
+ "import { afterEach, expect, test } from 'vitest';",
4077
+ "import { page } from 'vitest/browser';",
4078
+ "import { createRoot, type Root } from 'react-dom/client';",
4079
+ "import { TestCounter } from '../client/components/TestCounter';",
4080
+ "",
4081
+ "let root: Root | null = null;",
4082
+ "afterEach((): void => {",
4083
+ " root?.unmount();",
4084
+ " root = null;",
4085
+ " document.body.replaceChildren();",
4086
+ "});",
4087
+ "",
4088
+ "test('increments when clicked in a real browser', async (): Promise<void> => {",
4089
+ " const host: HTMLDivElement = document.createElement('div');",
4090
+ " document.body.append(host);",
4091
+ " root = createRoot(host);",
4092
+ " root.render(<TestCounter />);",
4093
+ " await expect.element(page.getByRole('link', { name: 'Home' })).toHaveAttribute('href', '/');",
4094
+ " await page.getByRole('button', { name: 'Count: 0' }).click();",
4095
+ " await expect.element(page.getByRole('button', { name: 'Count: 1' })).toBeVisible();",
4096
+ "});",
4097
+ ""
4098
+ ].join("\n")
4099
+ };
4100
+ }
4101
+
4020
4102
  // src/cli/validate.ts
4021
4103
  import path3 from "node:path";
4022
4104
  var PACKAGE_MANAGERS = ["npm", "pnpm", "yarn", "bun"];
@@ -4061,12 +4143,13 @@ async function isEmptyDir(dir) {
4061
4143
  function scaffold(name, template, features, aiTools, images) {
4062
4144
  const toilVersion = version();
4063
4145
  const devDependencies = {
4146
+ ...TEST_DEPENDENCIES,
4064
4147
  "@types/react": "^19.3.0",
4065
4148
  "@types/react-dom": "^19.3.0",
4066
- oxlint: "^1.83.0",
4067
- "oxlint-tsgolint": "^7.0.2001",
4068
- prettier: "^3.9.6",
4069
- toilscript: "^0.1.63",
4149
+ oxlint: "^1.87.0",
4150
+ "oxlint-tsgolint": "^7.0.2003",
4151
+ prettier: "^3.9.9",
4152
+ toilscript: ">=0.1.63",
4070
4153
  typescript: "^7.0.2"
4071
4154
  };
4072
4155
  for (const dep of requiredPackages(features).sort()) {
@@ -4077,6 +4160,7 @@ function scaffold(name, template, features, aiTools, images) {
4077
4160
  private: true,
4078
4161
  type: "module",
4079
4162
  scripts: {
4163
+ ...TEST_SCRIPTS,
4080
4164
  start: "toiljs start",
4081
4165
  dev: "toiljs dev",
4082
4166
  build: "toiljs build",
@@ -4093,6 +4177,7 @@ function scaffold(name, template, features, aiTools, images) {
4093
4177
  devDependencies
4094
4178
  };
4095
4179
  const files = {
4180
+ ...testingFiles(),
4096
4181
  "package.json": JSON.stringify(pkg, null, 4) + "\n",
4097
4182
  "toil.config.ts": "import { defineConfig } from 'toiljs/compiler';\n\nexport default defineConfig({\n" + // The app starter's session routes use the identity fields injected by built-in auth.
4098
4183
  (template === "app" ? " server: { auth: true },\n" : "") + ` client: {
@@ -4101,13 +4186,13 @@ function scaffold(name, template, features, aiTools, images) {
4101
4186
  },
4102
4187
  });
4103
4188
  `,
4104
- "tsconfig.json": '{\n "extends": "toiljs/tsconfig",\n "compilerOptions": {\n "paths": { "shared/*": ["./shared/*"] }\n },\n "include": ["client", "shared", "emails", "toil.config.ts", "toil-env.d.ts", "toil-routes.d.ts"]\n}\n',
4189
+ "tsconfig.json": '{\n "extends": "toiljs/tsconfig",\n "compilerOptions": {\n "paths": { "shared/*": ["./shared/*"] }\n },\n "include": ["client", "shared", "emails", "tests", "vitest.config.ts", "toil.config.ts", "toil-env.d.ts", "toil-routes.d.ts"]\n}\n',
4105
4190
  "oxlint.config.ts": "import toiljs from 'toiljs/oxlint';\n\nexport default toiljs;\n",
4106
4191
  ".prettierrc": '"toiljs/prettier"\n',
4107
4192
  // Generated files don't need formatting. (toilscript server decorators like @main /
4108
4193
  // @remote-on-functions are handled by the toiljs/prettier-plugin, so server/ is not ignored.)
4109
- ".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",
4110
- ".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",
4194
+ ".prettierignore": "node_modules\nbuild\n.toil\ncoverage\n.vitest\nshared/server.ts\ntoil-env.d.ts\ntoil-routes.d.ts\nserver/_emails.ts\nserver/_ssr/\nserver/toil-server-env.d.ts\n",
4195
+ ".gitignore": "node_modules\nbuild\n.toil\ncoverage\n.vitest\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",
4111
4196
  // Use the native TypeScript 7 language server.
4112
4197
  ".vscode/settings.json": JSON.stringify(
4113
4198
  {
@@ -4191,6 +4276,21 @@ function scaffold(name, template, features, aiTools, images) {
4191
4276
  "",
4192
4277
  " npm run build",
4193
4278
  "",
4279
+ "## Test",
4280
+ "",
4281
+ "Vitest, real-browser testing, and V8 coverage are installed with the project.",
4282
+ "Start with `tests/Greeting.test.ts` and `tests/TestCounter.browser.test.tsx`.",
4283
+ "",
4284
+ " npm test # unit tests",
4285
+ " npm run test:watch # unit tests in watch mode",
4286
+ " npm run test:browser # real Chrome tests",
4287
+ " npm run test:all # unit and browser tests",
4288
+ " npm run test:coverage # both suites with V8 coverage",
4289
+ "",
4290
+ "Coverage reports are in `coverage/` (HTML, JSON, and LCOV).",
4291
+ "Chrome and its driver are managed by WebdriverIO on the first browser run.",
4292
+ "See `.toil/docs/frontend/testing.md` for configuration, mocks, and CI.",
4293
+ "",
4194
4294
  "## Editor setup",
4195
4295
  "",
4196
4296
  "Run `npm install` first and use Node 26 for editor tooling.",
@@ -4425,9 +4525,9 @@ async function runCreate(opts2) {
4425
4525
  name = answer.trim() || "my-toil-app";
4426
4526
  }
4427
4527
  }
4428
- const valid = isValidName(name);
4429
- if (valid !== true) {
4430
- cancel(valid);
4528
+ const valid2 = isValidName(name);
4529
+ if (valid2 !== true) {
4530
+ cancel(valid2);
4431
4531
  process.exit(1);
4432
4532
  }
4433
4533
  const targetDir = resolveProjectDir(opts2.cwd, name);
@@ -4602,6 +4702,8 @@ async function runCreate(opts2) {
4602
4702
  if (!install) steps.push("npm install");
4603
4703
  steps.push(`${accent("npm run dev")} ${dim("start the dev server")}`);
4604
4704
  steps.push(`${accent("npm run build")} ${dim("build for production")}`);
4705
+ steps.push(`${accent("npm test")} ${dim("run the starter unit tests")}`);
4706
+ steps.push(`${accent("npm run test:browser")} ${dim("run the starter browser test")}`);
4605
4707
  steps.push("Install the Oxc plugin for VS Code or WebStorm (README.md \u2192 Editor setup)");
4606
4708
  steps.push("WebStorm: Settings \u2192 Tools \u2192 Oxlint \u2192 Enable type aware rules");
4607
4709
  note(steps.map((l2) => dim(" ") + l2).join("\n"), "Next steps");
@@ -4736,9 +4838,9 @@ function runDb(action, fileArg, opts2) {
4736
4838
  }
4737
4839
 
4738
4840
  // src/cli/doctor.ts
4739
- import fs5 from "node:fs";
4740
- import { createRequire } from "node:module";
4741
- import path6 from "node:path";
4841
+ import fs6 from "node:fs";
4842
+ import { createRequire as createRequire2 } from "node:module";
4843
+ import path7 from "node:path";
4742
4844
  import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
4743
4845
  import {
4744
4846
  loadConfig as loadConfig2,
@@ -5148,10 +5250,240 @@ function hasFailures(summary) {
5148
5250
  return summary.fail > 0;
5149
5251
  }
5150
5252
 
5253
+ // src/cli/test-setup.ts
5254
+ import fs5 from "node:fs/promises";
5255
+ import path6 from "node:path";
5256
+ import { createRequire } from "node:module";
5257
+ var import_semver3 = __toESM(require_semver2(), 1);
5258
+
5259
+ // src/cli/testing-versions.ts
5260
+ var import_semver2 = __toESM(require_semver2(), 1);
5261
+ function isSupportedTestingRange(name, range) {
5262
+ const supported = TEST_DEPENDENCIES[name];
5263
+ return supported === void 0 || (0, import_semver2.validRange)(range) !== null && (0, import_semver2.subset)(range, supported);
5264
+ }
5265
+ function isTestingSource(range) {
5266
+ return /^(file:|workspace:|link:|git[+:]|https?:|github:|npm:)/.test(range);
5267
+ }
5268
+
5269
+ // src/cli/test-setup.ts
5270
+ async function findTestConfiguration(root) {
5271
+ const files = await fs5.readdir(root);
5272
+ const custom = files.find((file) => /^vitest\.config\.[cm]?[jt]s$/.test(file)) ?? files.find((file) => /^vite\.config\.[cm]?[jt]s$/.test(file));
5273
+ const source = custom === void 0 ? "" : await fs5.readFile(path6.join(root, custom), "utf8");
5274
+ const native = custom === void 0 || source.includes("from 'toiljs/vitest'") || source.includes('from "toiljs/vitest"');
5275
+ return { file: custom ?? null, native };
5276
+ }
5277
+ async function writeMissing(root, file, content) {
5278
+ const target = path6.join(root, file);
5279
+ await fs5.mkdir(path6.dirname(target), { recursive: true });
5280
+ try {
5281
+ await fs5.writeFile(target, content, { flag: "wx" });
5282
+ return true;
5283
+ } catch (error) {
5284
+ if (!(error instanceof Error) || !("code" in error) || error.code !== "EEXIST") throw error;
5285
+ return false;
5286
+ }
5287
+ }
5288
+ async function ensureTesting(root, repairVersions = false) {
5289
+ const pkgPath = path6.join(root, "package.json");
5290
+ const raw = JSON.parse(await fs5.readFile(pkgPath, "utf8"));
5291
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw))
5292
+ throw new Error("toiljs test: package.json must contain an object.");
5293
+ const pkg = raw;
5294
+ for (const field of [pkg.scripts, pkg.dependencies, pkg.devDependencies]) {
5295
+ if (field !== void 0 && (field === null || typeof field !== "object" || Array.isArray(field) || Object.values(field).some((value) => typeof value !== "string")))
5296
+ throw new Error("toiljs test: package scripts and dependencies must be string maps.");
5297
+ }
5298
+ const config = await findTestConfiguration(root);
5299
+ const { native } = config;
5300
+ const changes = [];
5301
+ const skipped = [];
5302
+ let changed = false;
5303
+ let install = false;
5304
+ const require2 = createRequire(pkgPath);
5305
+ for (const [name, range] of Object.entries(TEST_DEPENDENCIES)) {
5306
+ for (const dependencies of [pkg.dependencies, pkg.devDependencies]) {
5307
+ const declared = dependencies?.[name];
5308
+ if (repairVersions && dependencies && declared && !isSupportedTestingRange(name, declared) && !isTestingSource(declared)) {
5309
+ dependencies[name] = range;
5310
+ changes.push(`package.json (${name} ${declared} -> ${range})`);
5311
+ changed = true;
5312
+ install = true;
5313
+ }
5314
+ }
5315
+ if (pkg.dependencies?.[name] === void 0 && pkg.devDependencies?.[name] === void 0) {
5316
+ pkg.devDependencies ??= {};
5317
+ pkg.devDependencies[name] = range;
5318
+ changes.push(`package.json (add ${name})`);
5319
+ changed = true;
5320
+ install = true;
5321
+ }
5322
+ try {
5323
+ require2.resolve(name);
5324
+ } catch {
5325
+ install = true;
5326
+ }
5327
+ }
5328
+ if (repairVersions) {
5329
+ const runnerRange = pkg.dependencies?.vitest ?? pkg.devDependencies?.vitest;
5330
+ const coverageDependencies = pkg.dependencies?.["@vitest/coverage-v8"] !== void 0 ? pkg.dependencies : pkg.devDependencies;
5331
+ if (runnerRange && isSupportedTestingRange("vitest", runnerRange) && coverageDependencies && (0, import_semver3.validRange)(coverageDependencies["@vitest/coverage-v8"] ?? "") !== null) {
5332
+ const installedRunner = await testVersion(require2, "vitest");
5333
+ const installedCoverage = await testVersion(
5334
+ require2,
5335
+ "@vitest/coverage-v8"
5336
+ );
5337
+ const target = installedRunner && installedCoverage && installedRunner !== installedCoverage && (0, import_semver3.satisfies)(installedRunner, runnerRange) ? installedRunner : (0, import_semver3.intersects)(coverageDependencies["@vitest/coverage-v8"], runnerRange) ? coverageDependencies["@vitest/coverage-v8"] : runnerRange;
5338
+ if (coverageDependencies["@vitest/coverage-v8"] !== target) {
5339
+ coverageDependencies["@vitest/coverage-v8"] = target;
5340
+ changes.push("package.json (align V8 coverage with Vitest)");
5341
+ changed = true;
5342
+ install = true;
5343
+ }
5344
+ if (installedRunner && installedCoverage && installedRunner !== installedCoverage)
5345
+ install = true;
5346
+ }
5347
+ }
5348
+ pkg.scripts ??= {};
5349
+ for (const [name, script] of Object.entries(TEST_SCRIPTS)) {
5350
+ if (pkg.scripts[name]?.trim()) continue;
5351
+ pkg.scripts[name] = script;
5352
+ changes.push(`package.json (add script ${name})`);
5353
+ changed = true;
5354
+ }
5355
+ if (changed) await fs5.writeFile(pkgPath, JSON.stringify(raw, null, 4) + "\n");
5356
+ if (config.file === null) {
5357
+ for (const [file, content] of Object.entries(testingFiles()))
5358
+ if (await writeMissing(root, file, content)) changes.push(file);
5359
+ }
5360
+ if (native) await includeTestTypes(root, config.file ?? "vitest.config.ts", changes, skipped);
5361
+ await ignoreTestReports(root, changes);
5362
+ return { install, native, changed: changes, skipped };
5363
+ }
5364
+ async function includeTestTypes(root, config, changes, skipped) {
5365
+ const file = path6.join(root, "tsconfig.json");
5366
+ let source;
5367
+ try {
5368
+ source = await fs5.readFile(file, "utf8");
5369
+ } catch (error) {
5370
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return;
5371
+ throw error;
5372
+ }
5373
+ try {
5374
+ const raw = JSON.parse(source);
5375
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw))
5376
+ throw new Error("Expected an object");
5377
+ const parsed = raw;
5378
+ if (!isStringArray(parsed.include)) {
5379
+ skipped.push(
5380
+ "tsconfig.json: include tests and the Vitest config in your existing file selection."
5381
+ );
5382
+ return;
5383
+ }
5384
+ const include = parsed.include;
5385
+ const missing = ["tests", config].filter(
5386
+ (entry) => !include.includes(entry)
5387
+ );
5388
+ if (!missing.length) return;
5389
+ include.push(...missing);
5390
+ await fs5.writeFile(file, JSON.stringify(parsed, null, 4) + "\n");
5391
+ changes.push("tsconfig.json (include tests and Vitest config)");
5392
+ } catch {
5393
+ skipped.push(
5394
+ "tsconfig.json: include tests and the Vitest config manually; custom JSON was preserved."
5395
+ );
5396
+ }
5397
+ }
5398
+ async function ignoreTestReports(root, changes) {
5399
+ const file = path6.join(root, ".gitignore");
5400
+ let source = "";
5401
+ try {
5402
+ source = await fs5.readFile(file, "utf8");
5403
+ } catch (error) {
5404
+ if (!(error instanceof Error) || !("code" in error) || error.code !== "ENOENT") throw error;
5405
+ }
5406
+ const lines = source.split(/\r?\n/).map((line) => line.trim().replace(/\/$/, ""));
5407
+ const missing = ["coverage", ".vitest"].filter(
5408
+ (entry) => !lines.includes(entry) && !lines.includes("/" + entry)
5409
+ );
5410
+ if (!missing.length) return;
5411
+ await fs5.writeFile(
5412
+ file,
5413
+ source + (source && !source.endsWith("\n") ? "\n" : "") + missing.join("\n") + "\n"
5414
+ );
5415
+ changes.push(".gitignore (test reports)");
5416
+ }
5417
+ async function testVersion(require2, name) {
5418
+ try {
5419
+ const raw = JSON.parse(
5420
+ await fs5.readFile(require2.resolve(`${name}/package.json`), "utf8")
5421
+ );
5422
+ return raw !== null && typeof raw === "object" && "version" in raw && typeof raw.version === "string" ? raw.version : null;
5423
+ } catch {
5424
+ return null;
5425
+ }
5426
+ }
5427
+ function isStringArray(value) {
5428
+ return Array.isArray(value) && value.every((entry) => typeof entry === "string");
5429
+ }
5430
+
5431
+ // src/cli/testing-checks.ts
5432
+ var import_semver4 = __toESM(require_semver2(), 1);
5433
+ function checkTesting(facts) {
5434
+ const configured = Object.keys(TEST_DEPENDENCIES).some(
5435
+ (name) => facts.dependencies[name] !== void 0
5436
+ ) || facts.configuration.file?.startsWith("vitest.config.") === true;
5437
+ const repair = "Run `toiljs doctor --fix`, then your package manager install (or `toiljs test` to install missing tooling).";
5438
+ const checks = Object.entries(TEST_DEPENDENCIES).map(([name, range]) => {
5439
+ const installed = facts.installed[name] ?? null;
5440
+ const declared = facts.dependencies[name];
5441
+ const ok = installed !== null && declared !== void 0 && (0, import_semver4.satisfies)(installed, range) && (isSupportedTestingRange(name, declared) || isTestingSource(declared));
5442
+ return {
5443
+ id: `testing:${name}`,
5444
+ label: name,
5445
+ status: ok ? "pass" : configured ? "fail" : "warn",
5446
+ detail: `${installed ?? "not installed"}; declared ${declared ?? "missing"} (requires ${range})`,
5447
+ fix: ok ? void 0 : repair
5448
+ };
5449
+ });
5450
+ const runner = facts.installed.vitest ?? null;
5451
+ const coverage = facts.installed["@vitest/coverage-v8"] ?? null;
5452
+ if (runner !== null && coverage !== null) {
5453
+ const match = (0, import_semver4.valid)(runner) !== null && (0, import_semver4.valid)(coverage) !== null && (0, import_semver4.eq)(runner, coverage);
5454
+ checks.push({
5455
+ id: "testing:coverage-match",
5456
+ label: "Vitest / V8 coverage versions",
5457
+ status: match ? "pass" : "fail",
5458
+ detail: `vitest ${runner}, @vitest/coverage-v8 ${coverage}`,
5459
+ fix: match ? void 0 : "Install matching versions of vitest and @vitest/coverage-v8; run `toiljs update --yes` to refresh both."
5460
+ });
5461
+ }
5462
+ const configPresent = facts.configuration.file !== null;
5463
+ checks.push({
5464
+ id: "testing:config",
5465
+ label: "Vitest configuration",
5466
+ status: configPresent ? "pass" : "warn",
5467
+ detail: configPresent ? `${facts.configuration.file} (${facts.configuration.native ? "toiljs preset" : "custom, preserved"})` : "missing",
5468
+ fix: configPresent ? void 0 : repair
5469
+ });
5470
+ const missing = Object.keys(TEST_SCRIPTS).filter(
5471
+ (name) => !facts.scripts[name]?.trim()
5472
+ );
5473
+ checks.push({
5474
+ id: "testing:scripts",
5475
+ label: "Unit, browser, watch, and coverage scripts",
5476
+ status: missing.length ? "warn" : "pass",
5477
+ detail: missing.length ? `missing ${missing.join(", ")}` : "configured",
5478
+ fix: missing.length ? repair : void 0
5479
+ });
5480
+ return checks;
5481
+ }
5482
+
5151
5483
  // src/cli/doctor.ts
5152
5484
  function readJsonObject(file) {
5153
5485
  try {
5154
- const parsed = JSON.parse(fs5.readFileSync(file, "utf8"));
5486
+ const parsed = JSON.parse(fs6.readFileSync(file, "utf8"));
5155
5487
  return typeof parsed === "object" && parsed !== null ? parsed : null;
5156
5488
  } catch {
5157
5489
  return null;
@@ -5165,16 +5497,16 @@ function stringRecord(value) {
5165
5497
  }
5166
5498
  function readFile(file) {
5167
5499
  try {
5168
- return fs5.readFileSync(file, "utf8");
5500
+ return fs6.readFileSync(file, "utf8");
5169
5501
  } catch {
5170
5502
  return null;
5171
5503
  }
5172
5504
  }
5173
5505
  function writeFile(file, content) {
5174
- fs5.writeFileSync(file, content);
5506
+ fs6.writeFileSync(file, content);
5175
5507
  }
5176
5508
  function isPackageInstalled(root, name) {
5177
- const require2 = createRequire(path6.join(root, "package.json"));
5509
+ const require2 = createRequire2(path7.join(root, "package.json"));
5178
5510
  for (const id of [`${name}/package.json`, name]) {
5179
5511
  try {
5180
5512
  require2.resolve(id);
@@ -5183,22 +5515,22 @@ function isPackageInstalled(root, name) {
5183
5515
  }
5184
5516
  }
5185
5517
  for (let dir = root; ; ) {
5186
- if (fs5.existsSync(path6.join(dir, "node_modules", name, "package.json"))) return true;
5187
- const parent = path6.dirname(dir);
5518
+ if (fs6.existsSync(path7.join(dir, "node_modules", name, "package.json"))) return true;
5519
+ const parent = path7.dirname(dir);
5188
5520
  if (parent === dir) return false;
5189
5521
  dir = parent;
5190
5522
  }
5191
5523
  }
5192
5524
  function installedVersion(root, name) {
5193
- const require2 = createRequire(path6.join(root, "package.json"));
5525
+ const require2 = createRequire2(path7.join(root, "package.json"));
5194
5526
  let pkgPath = null;
5195
5527
  try {
5196
5528
  pkgPath = require2.resolve(`${name}/package.json`);
5197
5529
  } catch {
5198
5530
  for (let dir = root; pkgPath === null; ) {
5199
- const candidate = path6.join(dir, "node_modules", name, "package.json");
5200
- if (fs5.existsSync(candidate)) pkgPath = candidate;
5201
- const parent = path6.dirname(dir);
5531
+ const candidate = path7.join(dir, "node_modules", name, "package.json");
5532
+ if (fs6.existsSync(candidate)) pkgPath = candidate;
5533
+ const parent = path7.dirname(dir);
5202
5534
  if (parent === dir) break;
5203
5535
  dir = parent;
5204
5536
  }
@@ -5210,7 +5542,7 @@ function installedVersion(root, name) {
5210
5542
  function applyTypeScriptFix(root) {
5211
5543
  const changed = [];
5212
5544
  const skipped = [];
5213
- const pkgPath = path6.join(root, "package.json");
5545
+ const pkgPath = path7.join(root, "package.json");
5214
5546
  const pkg = readJsonObject(pkgPath);
5215
5547
  if (pkg === null) return { changed, skipped };
5216
5548
  let declared = false;
@@ -5248,14 +5580,14 @@ function looksLikeSemverRange(range) {
5248
5580
  return /^\s*[v^~>=<]*\s*\d+\.\d+/.test(range);
5249
5581
  }
5250
5582
  function gatherRpcFacts(root) {
5251
- const pkg = readJsonObject(path6.join(root, "package.json"));
5583
+ const pkg = readJsonObject(path7.join(root, "package.json"));
5252
5584
  const scripts = pkg ? stringRecord(pkg.scripts) : {};
5253
5585
  const deps = {
5254
5586
  ...pkg ? stringRecord(pkg.dependencies) : {},
5255
5587
  ...pkg ? stringRecord(pkg.devDependencies) : {}
5256
5588
  };
5257
- const tsconfig = readJsonObject(path6.join(root, "tsconfig.json"));
5258
- const gitignore = readFile(path6.join(root, ".gitignore"));
5589
+ const tsconfig = readJsonObject(path7.join(root, "tsconfig.json"));
5590
+ const gitignore = readFile(path7.join(root, ".gitignore"));
5259
5591
  const buildServerWired = [scripts["build:server"], scripts["build"]].some(
5260
5592
  (s) => typeof s === "string" && (s.includes("--rpcModule") || /\btoiljs\s+build\b/.test(s))
5261
5593
  );
@@ -5274,7 +5606,7 @@ function gatherRpcFacts(root) {
5274
5606
  function serverSources(root, toilconfig) {
5275
5607
  const dirs = /* @__PURE__ */ new Set();
5276
5608
  const entries = Array.isArray(toilconfig?.entries) ? toilconfig.entries.filter((e) => typeof e === "string") : [];
5277
- for (const e of entries) dirs.add(path6.dirname(path6.resolve(root, e)));
5609
+ for (const e of entries) dirs.add(path7.dirname(path7.resolve(root, e)));
5278
5610
  const out2 = [];
5279
5611
  const cap = 200;
5280
5612
  const maxDepth = 16;
@@ -5282,13 +5614,13 @@ function serverSources(root, toilconfig) {
5282
5614
  if (out2.length >= cap || depth > maxDepth) return;
5283
5615
  let listing;
5284
5616
  try {
5285
- listing = fs5.readdirSync(current, { withFileTypes: true });
5617
+ listing = fs6.readdirSync(current, { withFileTypes: true });
5286
5618
  } catch {
5287
5619
  return;
5288
5620
  }
5289
5621
  for (const entry of listing) {
5290
5622
  if (out2.length >= cap) break;
5291
- const full = path6.join(current, entry.name);
5623
+ const full = path7.join(current, entry.name);
5292
5624
  if (entry.isDirectory()) {
5293
5625
  if (entry.name !== "node_modules") visit(full, depth + 1);
5294
5626
  } else if (entry.name.endsWith(".ts") && !entry.name.endsWith(".d.ts")) {
@@ -5312,7 +5644,7 @@ function gatherRestFacts(root, toilconfig) {
5312
5644
  return { hasControllers, dispatched };
5313
5645
  }
5314
5646
  function secretDefined(root, key) {
5315
- const raw = readFile(path6.join(root, ".env.secrets"));
5647
+ const raw = readFile(path7.join(root, ".env.secrets"));
5316
5648
  if (raw === null) return false;
5317
5649
  return new RegExp(`^\\s*(?:export\\s+)?${key}\\s*=\\s*\\S`, "m").test(raw);
5318
5650
  }
@@ -5329,21 +5661,21 @@ function gatherAuthFacts(root, toilconfig) {
5329
5661
  function serverTsconfigPath(root, toilconfig) {
5330
5662
  const dirs = /* @__PURE__ */ new Set();
5331
5663
  const entries = Array.isArray(toilconfig?.entries) ? toilconfig.entries.filter((e) => typeof e === "string") : [];
5332
- for (const e of entries) dirs.add(path6.dirname(path6.resolve(root, e)));
5333
- if (dirs.size === 0) dirs.add(path6.join(root, "server"));
5664
+ for (const e of entries) dirs.add(path7.dirname(path7.resolve(root, e)));
5665
+ if (dirs.size === 0) dirs.add(path7.join(root, "server"));
5334
5666
  for (const dir of dirs) {
5335
- const p = path6.join(dir, "tsconfig.json");
5336
- if (fs5.existsSync(p)) return p;
5667
+ const p = path7.join(dir, "tsconfig.json");
5668
+ if (fs6.existsSync(p)) return p;
5337
5669
  }
5338
5670
  return null;
5339
5671
  }
5340
5672
  function serverMigrationsExist(root, toilconfig) {
5341
5673
  const dirs = /* @__PURE__ */ new Set();
5342
5674
  const entries = Array.isArray(toilconfig?.entries) ? toilconfig.entries.filter((e) => typeof e === "string") : [];
5343
- for (const e of entries) dirs.add(path6.dirname(path6.resolve(root, e)));
5344
- if (dirs.size === 0) dirs.add(path6.join(root, "server"));
5675
+ for (const e of entries) dirs.add(path7.dirname(path7.resolve(root, e)));
5676
+ if (dirs.size === 0) dirs.add(path7.join(root, "server"));
5345
5677
  for (const dir of dirs) {
5346
- if (fs5.existsSync(path6.join(dir, "migrations"))) return true;
5678
+ if (fs6.existsSync(path7.join(dir, "migrations"))) return true;
5347
5679
  }
5348
5680
  return false;
5349
5681
  }
@@ -5358,7 +5690,7 @@ function tsconfigHasToilPlugin(tsconfig) {
5358
5690
  function applyRpcFix(root) {
5359
5691
  const changed = [];
5360
5692
  const skipped = [];
5361
- const pkgPath = path6.join(root, "package.json");
5693
+ const pkgPath = path7.join(root, "package.json");
5362
5694
  const pkgRaw = readFile(pkgPath);
5363
5695
  const pkg = pkgRaw !== null ? readJsonObject(pkgPath) : null;
5364
5696
  if (pkg !== null) {
@@ -5397,7 +5729,7 @@ function applyRpcFix(root) {
5397
5729
  } else if (pkgRaw !== null) {
5398
5730
  skipped.push("package.json (unparseable)");
5399
5731
  }
5400
- const tsPath = path6.join(root, "tsconfig.json");
5732
+ const tsPath = path7.join(root, "tsconfig.json");
5401
5733
  const tsRaw = readFile(tsPath);
5402
5734
  const tsconfig = tsRaw !== null ? readJsonObject(tsPath) : null;
5403
5735
  if (tsconfig !== null) {
@@ -5436,7 +5768,7 @@ function applyRpcFix(root) {
5436
5768
  } else if (tsRaw !== null) {
5437
5769
  skipped.push('tsconfig.json (JSON with comments, add "shared" + paths by hand)');
5438
5770
  }
5439
- const giPath = path6.join(root, ".gitignore");
5771
+ const giPath = path7.join(root, ".gitignore");
5440
5772
  const giRaw = readFile(giPath);
5441
5773
  if (giRaw === null) {
5442
5774
  writeFile(giPath, `${RPC_GITIGNORE_LINE}
@@ -5448,21 +5780,21 @@ function applyRpcFix(root) {
5448
5780
  `);
5449
5781
  changed.push(".gitignore");
5450
5782
  }
5451
- const serverToilconfig = readJsonObject(path6.join(root, "toilconfig.json"));
5783
+ const serverToilconfig = readJsonObject(path7.join(root, "toilconfig.json"));
5452
5784
  if (serverToilconfig !== null) {
5453
5785
  const entries = Array.isArray(serverToilconfig.entries) ? serverToilconfig.entries.filter(
5454
5786
  (e) => typeof e === "string"
5455
5787
  ) : [];
5456
5788
  const dirs = /* @__PURE__ */ new Set();
5457
- for (const e of entries) dirs.add(path6.dirname(path6.resolve(root, e)));
5458
- if (dirs.size === 0) dirs.add(path6.join(root, "server"));
5789
+ for (const e of entries) dirs.add(path7.dirname(path7.resolve(root, e)));
5790
+ if (dirs.size === 0) dirs.add(path7.join(root, "server"));
5459
5791
  for (const dir of dirs) {
5460
- const envPath = path6.join(dir, "toil-server-env.d.ts");
5792
+ const envPath = path7.join(dir, "toil-server-env.d.ts");
5461
5793
  if (readFile(envPath) !== TOIL_SERVER_ENV_DTS2) {
5462
5794
  try {
5463
- fs5.mkdirSync(dir, { recursive: true });
5795
+ fs6.mkdirSync(dir, { recursive: true });
5464
5796
  writeFile(envPath, TOIL_SERVER_ENV_DTS2);
5465
- changed.push(path6.relative(root, envPath));
5797
+ changed.push(path7.relative(root, envPath));
5466
5798
  } catch {
5467
5799
  }
5468
5800
  }
@@ -5492,7 +5824,7 @@ function prettierPluginPresent(root, pkg) {
5492
5824
  return true;
5493
5825
  }
5494
5826
  for (const name of PRETTIER_CONFIG_FILES) {
5495
- const raw = readFile(path6.join(root, name));
5827
+ const raw = readFile(path7.join(root, name));
5496
5828
  if (raw !== null && PRETTIER_MENTION.test(raw)) return true;
5497
5829
  }
5498
5830
  return false;
@@ -5501,7 +5833,7 @@ function applyPrettierFix(root, pkg) {
5501
5833
  const changed = [];
5502
5834
  const skipped = [];
5503
5835
  if (prettierPluginPresent(root, pkg)) return { changed, skipped };
5504
- const pkgPath = path6.join(root, "package.json");
5836
+ const pkgPath = path7.join(root, "package.json");
5505
5837
  const pkgConfig = pkg ? asRecord(pkg.prettier) : null;
5506
5838
  if (pkgConfig !== null) {
5507
5839
  const full = readJsonObject(pkgPath);
@@ -5517,7 +5849,7 @@ function applyPrettierFix(root, pkg) {
5517
5849
  }
5518
5850
  }
5519
5851
  for (const name of [".prettierrc", ".prettierrc.json"]) {
5520
- const filePath = path6.join(root, name);
5852
+ const filePath = path7.join(root, name);
5521
5853
  const raw = readFile(filePath);
5522
5854
  if (raw === null) continue;
5523
5855
  const obj = readJsonObject(filePath);
@@ -5533,13 +5865,13 @@ function applyPrettierFix(root, pkg) {
5533
5865
  changed.push(name);
5534
5866
  return { changed, skipped };
5535
5867
  }
5536
- const jsConfig = PRETTIER_CONFIG_FILES.find((name) => readFile(path6.join(root, name)) !== null);
5868
+ const jsConfig = PRETTIER_CONFIG_FILES.find((name) => readFile(path7.join(root, name)) !== null);
5537
5869
  if (jsConfig) {
5538
5870
  skipped.push(`${jsConfig} (add "${PRETTIER_PLUGIN}" to its plugins by hand)`);
5539
5871
  return { changed, skipped };
5540
5872
  }
5541
5873
  writeFile(
5542
- path6.join(root, ".prettierrc.json"),
5874
+ path7.join(root, ".prettierrc.json"),
5543
5875
  JSON.stringify({ plugins: [PRETTIER_PLUGIN] }, null, 4) + "\n"
5544
5876
  );
5545
5877
  changed.push(".prettierrc.json");
@@ -5553,7 +5885,7 @@ function applyServerEditorFix(root, toilconfig) {
5553
5885
  const parsed = readJsonObject(tsPath);
5554
5886
  if (parsed === null)
5555
5887
  skipped.push(
5556
- `${path6.relative(root, tsPath)} (remove legacy ts-plugin from JSONC manually)`
5888
+ `${path7.relative(root, tsPath)} (remove legacy ts-plugin from JSONC manually)`
5557
5889
  );
5558
5890
  else if (tsconfigHasToilPlugin(parsed)) {
5559
5891
  const co = asRecord(parsed.compilerOptions);
@@ -5565,12 +5897,12 @@ function applyServerEditorFix(root, toilconfig) {
5565
5897
  if (plugins.length === 0) delete co.plugins;
5566
5898
  else co.plugins = plugins;
5567
5899
  writeFile(tsPath, JSON.stringify(parsed, null, 4) + "\n");
5568
- changed.push(path6.relative(root, tsPath));
5900
+ changed.push(path7.relative(root, tsPath));
5569
5901
  }
5570
5902
  }
5571
5903
  }
5572
- const vsPath = path6.join(root, ".vscode/settings.json");
5573
- const vs = fs5.existsSync(vsPath) ? readJsonObject(vsPath) : {};
5904
+ const vsPath = path7.join(root, ".vscode/settings.json");
5905
+ const vs = fs6.existsSync(vsPath) ? readJsonObject(vsPath) : {};
5574
5906
  if (vs === null)
5575
5907
  skipped.push(".vscode/settings.json (enable js/ts.experimental.useTsgo manually)");
5576
5908
  else {
@@ -5579,7 +5911,7 @@ function applyServerEditorFix(root, toilconfig) {
5579
5911
  delete vs["typescript.enablePromptUseWorkspaceTsdk"];
5580
5912
  vs["js/ts.experimental.useTsgo"] = true;
5581
5913
  if (JSON.stringify(vs) !== before) {
5582
- fs5.mkdirSync(path6.dirname(vsPath), { recursive: true });
5914
+ fs6.mkdirSync(path7.dirname(vsPath), { recursive: true });
5583
5915
  writeFile(vsPath, JSON.stringify(vs, null, 4) + "\n");
5584
5916
  changed.push(".vscode/settings.json");
5585
5917
  }
@@ -5595,8 +5927,8 @@ async function checkNativeTypeScriptConfig(root) {
5595
5927
  status: "fail",
5596
5928
  detail: "Install TypeScript 7 before validating compiler configuration."
5597
5929
  };
5598
- const configPath = path6.join(root, "tsconfig.json");
5599
- if (!fs5.existsSync(configPath))
5930
+ const configPath = path7.join(root, "tsconfig.json");
5931
+ if (!fs6.existsSync(configPath))
5600
5932
  return {
5601
5933
  ...check,
5602
5934
  status: "fail",
@@ -5604,7 +5936,7 @@ async function checkNativeTypeScriptConfig(root) {
5604
5936
  fix: "Create a tsconfig.json extending toiljs/tsconfig."
5605
5937
  };
5606
5938
  try {
5607
- const apiPath = createRequire(path6.join(root, "package.json")).resolve(
5939
+ const apiPath = createRequire2(path7.join(root, "package.json")).resolve(
5608
5940
  "typescript/unstable/sync"
5609
5941
  );
5610
5942
  const native = await import(pathToFileURL(apiPath).href);
@@ -5637,9 +5969,9 @@ async function checkNativeTypeScriptConfig(root) {
5637
5969
  }
5638
5970
  }
5639
5971
  function checkLintMigration(root) {
5640
- const pkg = readJsonObject(path6.join(root, "package.json"));
5972
+ const pkg = readJsonObject(path7.join(root, "package.json"));
5641
5973
  const script = asRecord(pkg?.scripts)?.lint;
5642
- const config = readFile(path6.join(root, "oxlint.config.ts")) ?? readFile(path6.join(root, ".oxlintrc.json")) ?? "";
5974
+ const config = readFile(path7.join(root, "oxlint.config.ts")) ?? readFile(path7.join(root, ".oxlintrc.json")) ?? "";
5643
5975
  const configured = config.includes("toiljs/oxlint") || /["']?typeAware["']?\s*:\s*true/.test(config);
5644
5976
  const scriptOk = typeof script === "string" && /\boxlint\b/.test(script) && !/\beslint\b/.test(script) && (script.includes("--type-aware") || configured);
5645
5977
  const installed = installedVersion(root, "oxlint") !== null && installedVersion(root, "oxlint-tsgolint") !== null;
@@ -5652,8 +5984,8 @@ function checkLintMigration(root) {
5652
5984
  };
5653
5985
  }
5654
5986
  function frameworkMeta() {
5655
- const pkgPath = path6.resolve(
5656
- path6.dirname(fileURLToPath3(import.meta.url)),
5987
+ const pkgPath = path7.resolve(
5988
+ path7.dirname(fileURLToPath3(import.meta.url)),
5657
5989
  "..",
5658
5990
  "..",
5659
5991
  "package.json"
@@ -5663,10 +5995,10 @@ function frameworkMeta() {
5663
5995
  const peers = pkg ? stringRecord(pkg.peerDependencies) : {};
5664
5996
  return { node: engines.node ?? ">=24.0.0", peers };
5665
5997
  }
5666
- var LOCKFILES = ["package-lock.json", "pnpm-lock.yaml", "yarn.lock", "bun.lockb"];
5998
+ var LOCKFILES = ["package-lock.json", "pnpm-lock.yaml", "yarn.lock", "bun.lock", "bun.lockb"];
5667
5999
  function readEntry(clientAbsDir) {
5668
6000
  for (const name of ["toil.tsx", "toil.jsx", "main.tsx", "main.jsx"]) {
5669
- const source = readFile(path6.join(clientAbsDir, name));
6001
+ const source = readFile(path7.join(clientAbsDir, name));
5670
6002
  if (source !== null && /toiljs\/routes|\bmount\s*\(/.test(source)) return source;
5671
6003
  }
5672
6004
  return null;
@@ -5677,18 +6009,18 @@ function collectSources(root, dir, cap) {
5677
6009
  if (out2.length >= cap) return;
5678
6010
  let entries;
5679
6011
  try {
5680
- entries = fs5.readdirSync(current, { withFileTypes: true });
6012
+ entries = fs6.readdirSync(current, { withFileTypes: true });
5681
6013
  } catch {
5682
6014
  return;
5683
6015
  }
5684
6016
  for (const entry of entries) {
5685
6017
  if (out2.length >= cap) break;
5686
- const full = path6.join(current, entry.name);
6018
+ const full = path7.join(current, entry.name);
5687
6019
  if (entry.isDirectory()) {
5688
6020
  if (entry.name !== "node_modules") visit(full);
5689
6021
  } else if (/\.(tsx|jsx)$/.test(entry.name)) {
5690
6022
  const source = readFile(full);
5691
- if (source !== null) out2.push({ path: path6.relative(root, full), source });
6023
+ if (source !== null) out2.push({ path: path7.relative(root, full), source });
5692
6024
  }
5693
6025
  }
5694
6026
  };
@@ -5724,9 +6056,9 @@ function renderHuman(groups) {
5724
6056
  process.stdout.write(out2.join("\n") + "\n");
5725
6057
  }
5726
6058
  async function runDoctor(opts2) {
5727
- const root = path6.resolve(opts2.root ?? opts2.cwd);
6059
+ const root = path7.resolve(opts2.root ?? opts2.cwd);
5728
6060
  const meta = frameworkMeta();
5729
- const projectPkg = readJsonObject(path6.join(root, "package.json"));
6061
+ const projectPkg = readJsonObject(path7.join(root, "package.json"));
5730
6062
  const deps = {
5731
6063
  ...projectPkg ? stringRecord(projectPkg.dependencies) : {},
5732
6064
  ...projectPkg ? stringRecord(projectPkg.devDependencies) : {}
@@ -5738,11 +6070,11 @@ async function runDoctor(opts2) {
5738
6070
  } catch (err) {
5739
6071
  configError = err instanceof Error ? err.message : String(err);
5740
6072
  }
5741
- const clientAbsDir = cfg ? cfg.clientAbsDir : path6.join(root, "client");
5742
- const routesAbsDir = cfg ? cfg.routesAbsDir : path6.join(clientAbsDir, "routes");
5743
- const publicDir = cfg ? cfg.publicDir : path6.join(clientAbsDir, "public");
6073
+ const clientAbsDir = cfg ? cfg.clientAbsDir : path7.join(root, "client");
6074
+ const routesAbsDir = cfg ? cfg.routesAbsDir : path7.join(clientAbsDir, "routes");
6075
+ const publicDir = cfg ? cfg.publicDir : path7.join(clientAbsDir, "public");
5744
6076
  const entrySource = readEntry(clientAbsDir);
5745
- const indexHtml = readFile(path6.join(publicDir, "index.html"));
6077
+ const indexHtml = readFile(path7.join(publicDir, "index.html"));
5746
6078
  const routes = scanRoutes(routesAbsDir);
5747
6079
  const mainPatterns = routes.filter((r2) => r2.slot === void 0).map((r2) => r2.pattern);
5748
6080
  const assetIssues = findRelativeAssets(collectSources(root, clientAbsDir, 200));
@@ -5755,14 +6087,14 @@ async function runDoctor(opts2) {
5755
6087
  }
5756
6088
  const ppPkg = preprocessorImported ? PREPROCESSOR_PKG[preprocessorImported] : null;
5757
6089
  const preprocessorInstalled = ppPkg === null || ppPkg in deps;
5758
- const toilconfig = readJsonObject(path6.join(root, "toilconfig.json"));
6090
+ const toilconfig = readJsonObject(path7.join(root, "toilconfig.json"));
5759
6091
  const serverPresent = toilconfig !== null;
5760
6092
  let missingEntries = [];
5761
6093
  let toilscriptInstalled = false;
5762
6094
  let wasmExists = false;
5763
6095
  if (toilconfig) {
5764
6096
  const entries = Array.isArray(toilconfig.entries) ? toilconfig.entries.filter((e) => typeof e === "string") : [];
5765
- missingEntries = entries.filter((e) => !fs5.existsSync(path6.join(root, e)));
6097
+ missingEntries = entries.filter((e) => !fs6.existsSync(path7.join(root, e)));
5766
6098
  toilscriptInstalled = isPackageInstalled(root, "toilscript");
5767
6099
  const targets = typeof toilconfig.targets === "object" && toilconfig.targets !== null ? toilconfig.targets : {};
5768
6100
  const outFiles = [];
@@ -5772,20 +6104,20 @@ async function runDoctor(opts2) {
5772
6104
  if (typeof outFile === "string") outFiles.push(outFile);
5773
6105
  }
5774
6106
  }
5775
- wasmExists = outFiles.some((f) => fs5.existsSync(path6.join(root, f)));
6107
+ wasmExists = outFiles.some((f) => fs6.existsSync(path7.join(root, f)));
5776
6108
  if (!wasmExists && outFiles.length === 0) {
5777
6109
  try {
5778
- wasmExists = fs5.readdirSync(path6.join(root, "build", "server")).some((f) => f.endsWith(".wasm"));
6110
+ wasmExists = fs6.readdirSync(path7.join(root, "build", "server")).some((f) => f.endsWith(".wasm"));
5779
6111
  } catch {
5780
6112
  wasmExists = false;
5781
6113
  }
5782
6114
  }
5783
6115
  }
5784
6116
  const peerName = (n3) => checkPeer(n3, deps[n3] ?? null, meta.peers[n3] ?? "*");
5785
- const peerChecks = Object.keys(meta.peers).filter((n3) => n3 !== "typescript").map(peerName);
6117
+ const peerChecks = Object.keys(meta.peers).filter((n3) => n3 !== "typescript" && !Object.hasOwn(TEST_DEPENDENCIES, n3)).map(peerName);
5786
6118
  const typeScriptFix = opts2.fix ? applyTypeScriptFix(root) : null;
5787
6119
  const declaredTypeScript = (() => {
5788
- const pkg = readJsonObject(path6.join(root, "package.json"));
6120
+ const pkg = readJsonObject(path7.join(root, "package.json"));
5789
6121
  if (pkg === null) return deps.typescript ?? null;
5790
6122
  const ranges = [
5791
6123
  "dependencies",
@@ -5807,12 +6139,39 @@ async function runDoctor(opts2) {
5807
6139
  const authFacts = gatherAuthFacts(root, toilconfig);
5808
6140
  const prettierPresent = prettierPluginPresent(
5809
6141
  root,
5810
- readJsonObject(path6.join(root, "package.json"))
6142
+ readJsonObject(path7.join(root, "package.json"))
5811
6143
  );
5812
6144
  const serverTsPath = serverPresent ? serverTsconfigPath(root, toilconfig) : null;
5813
6145
  const serverTsParsed = serverTsPath ? readJsonObject(serverTsPath) : null;
5814
6146
  const serverTsPluginPresent = serverTsPath === null || serverTsParsed === null ? false : tsconfigHasToilPlugin(serverTsParsed);
5815
- const applied = [typeScriptFix, rpcFix, prettierFix, editorFix].filter(
6147
+ let testingFix = null;
6148
+ let testingChecks;
6149
+ try {
6150
+ if (opts2.fix && projectPkg !== null) testingFix = await ensureTesting(root, true);
6151
+ const testingPkg = readJsonObject(path7.join(root, "package.json"));
6152
+ testingChecks = checkTesting({
6153
+ dependencies: {
6154
+ ...stringRecord(testingPkg?.dependencies),
6155
+ ...stringRecord(testingPkg?.devDependencies)
6156
+ },
6157
+ scripts: stringRecord(testingPkg?.scripts),
6158
+ installed: Object.fromEntries(
6159
+ Object.keys(TEST_DEPENDENCIES).map((name) => [name, installedVersion(root, name)])
6160
+ ),
6161
+ configuration: await findTestConfiguration(root)
6162
+ });
6163
+ } catch (error) {
6164
+ testingChecks = [
6165
+ {
6166
+ id: "testing:setup",
6167
+ label: "Testing setup",
6168
+ status: "fail",
6169
+ detail: error instanceof Error ? error.message : String(error),
6170
+ fix: "Repair package.json or unreadable test configuration, then run `toiljs doctor --fix`."
6171
+ }
6172
+ ];
6173
+ }
6174
+ const applied = [typeScriptFix, rpcFix, prettierFix, editorFix, testingFix].filter(
5816
6175
  (fix) => fix !== null
5817
6176
  );
5818
6177
  const serverFix = applied.length > 0 ? {
@@ -5829,7 +6188,7 @@ async function runDoctor(opts2) {
5829
6188
  typeScriptCheck,
5830
6189
  checkLintMigration(root),
5831
6190
  await checkNativeTypeScriptConfig(root),
5832
- checkPackageManager(LOCKFILES.filter((f) => fs5.existsSync(path6.join(root, f))))
6191
+ checkPackageManager(LOCKFILES.filter((f) => fs6.existsSync(path7.join(root, f))))
5833
6192
  ]
5834
6193
  },
5835
6194
  {
@@ -5838,13 +6197,13 @@ async function runDoctor(opts2) {
5838
6197
  checkDir(
5839
6198
  "client-dir",
5840
6199
  "client/ directory",
5841
- fs5.existsSync(clientAbsDir),
6200
+ fs6.existsSync(clientAbsDir),
5842
6201
  "Create a client/ directory for your app."
5843
6202
  ),
5844
6203
  checkDir(
5845
6204
  "routes-dir",
5846
6205
  "routes/ directory",
5847
- fs5.existsSync(routesAbsDir),
6206
+ fs6.existsSync(routesAbsDir),
5848
6207
  "Create client/routes/ and add an index.tsx."
5849
6208
  ),
5850
6209
  checkRootElement(indexHtml),
@@ -5855,6 +6214,10 @@ async function runDoctor(opts2) {
5855
6214
  checkDevScripts(projectPkg ? stringRecord(projectPkg.scripts) : {})
5856
6215
  ]
5857
6216
  },
6217
+ {
6218
+ title: "Testing",
6219
+ checks: testingChecks
6220
+ },
5858
6221
  {
5859
6222
  title: "Config + assets",
5860
6223
  checks: [
@@ -5915,8 +6278,8 @@ function renderFix(result) {
5915
6278
 
5916
6279
  // src/cli/generate.ts
5917
6280
  import { randomBytes as randomBytes3, timingSafeEqual } from "node:crypto";
5918
- import fs6 from "node:fs";
5919
- import path7 from "node:path";
6281
+ import fs7 from "node:fs";
6282
+ import path8 from "node:path";
5920
6283
  import { parseEnv } from "node:util";
5921
6284
 
5922
6285
  // node_modules/@noble/hashes/_u64.js
@@ -6829,10 +7192,10 @@ var ml_kem768 = /* @__PURE__ */ (() => mk(PARAMS[768]))();
6829
7192
  var PRIVATE_KEYS = ["AUTH_SESSION_SECRET", "AUTH_OPRF_SEED", "AUTH_KEM_SK"];
6830
7193
  var PUBLIC_KEY = "VITE_AUTH_KEM_PUBLIC_KEY";
6831
7194
  function read(file) {
6832
- const stat = fs6.lstatSync(file, { throwIfNoEntry: false });
7195
+ const stat = fs7.lstatSync(file, { throwIfNoEntry: false });
6833
7196
  if (!stat) return "";
6834
7197
  if (!stat.isFile()) throw new Error(`Expected a regular file: ${file}`);
6835
- return fs6.readFileSync(file, "utf8");
7198
+ return fs7.readFileSync(file, "utf8");
6836
7199
  }
6837
7200
  function append(text2, lines) {
6838
7201
  if (!lines.length) return text2;
@@ -6855,10 +7218,10 @@ function publicKeyFor(secret) {
6855
7218
  }
6856
7219
  function runGenerate(target, opts2 = {}) {
6857
7220
  if (target !== "auth") throw new Error("Usage: toiljs generate auth [--root <dir>]");
6858
- const root = path7.resolve(opts2.root ?? process.cwd());
6859
- const envFile = path7.join(root, ".env");
6860
- const secretsFile = path7.join(root, ".env.secrets");
6861
- const ignoreFile = path7.join(root, ".gitignore");
7221
+ const root = path8.resolve(opts2.root ?? process.cwd());
7222
+ const envFile = path8.join(root, ".env");
7223
+ const secretsFile = path8.join(root, ".env.secrets");
7224
+ const ignoreFile = path8.join(root, ".gitignore");
6862
7225
  const envText = read(envFile);
6863
7226
  const secretsText = read(secretsFile);
6864
7227
  const ignoreText = read(ignoreFile);
@@ -6896,12 +7259,12 @@ function runGenerate(target, opts2 = {}) {
6896
7259
  const publicAdditions = Object.hasOwn(env, PUBLIC_KEY) ? [] : [`${PUBLIC_KEY}=${publicKey}`];
6897
7260
  const ignoreLines = new Set(ignoreText.split(/\r?\n/));
6898
7261
  const ignoreAdditions = ["/.env", "/.env.secrets"].filter((line) => !ignoreLines.has(line));
6899
- if (ignoreAdditions.length) fs6.writeFileSync(ignoreFile, append(ignoreText, ignoreAdditions));
6900
- if (fs6.existsSync(secretsFile)) fs6.chmodSync(secretsFile, 384);
7262
+ if (ignoreAdditions.length) fs7.writeFileSync(ignoreFile, append(ignoreText, ignoreAdditions));
7263
+ if (fs7.existsSync(secretsFile)) fs7.chmodSync(secretsFile, 384);
6901
7264
  if (additions.length)
6902
- fs6.writeFileSync(secretsFile, append(secretsText, additions), { mode: 384 });
7265
+ fs7.writeFileSync(secretsFile, append(secretsText, additions), { mode: 384 });
6903
7266
  if (publicAdditions.length)
6904
- fs6.writeFileSync(envFile, append(envText, publicAdditions), { mode: 384 });
7267
+ fs7.writeFileSync(envFile, append(envText, publicAdditions), { mode: 384 });
6905
7268
  process.stdout.write(
6906
7269
  [
6907
7270
  `Authentication keys ready in ${root}. Existing credentials were preserved.`,
@@ -6918,14 +7281,14 @@ function runGenerate(target, opts2 = {}) {
6918
7281
  }
6919
7282
 
6920
7283
  // src/cli/notify.ts
6921
- import fs8 from "node:fs";
7284
+ import fs9 from "node:fs";
6922
7285
  import os from "node:os";
6923
- import path9 from "node:path";
7286
+ import path10 from "node:path";
6924
7287
  import { fileURLToPath as fileURLToPath4 } from "node:url";
6925
7288
 
6926
7289
  // src/cli/update.ts
6927
- import fs7 from "node:fs";
6928
- import path8 from "node:path";
7290
+ import fs8 from "node:fs";
7291
+ import path9 from "node:path";
6929
7292
 
6930
7293
  // src/cli/updates.ts
6931
7294
  function parseVersion2(v) {
@@ -6968,13 +7331,14 @@ function buildRows(upgraded, currentDeps) {
6968
7331
 
6969
7332
  // src/cli/update.ts
6970
7333
  function detectPackageManager2(root) {
6971
- if (fs7.existsSync(path8.join(root, "pnpm-lock.yaml"))) return { name: "pnpm", ncuName: "pnpm" };
6972
- if (fs7.existsSync(path8.join(root, "yarn.lock"))) return { name: "yarn", ncuName: "yarn" };
6973
- if (fs7.existsSync(path8.join(root, "bun.lockb"))) return { name: "bun", ncuName: "bun" };
7334
+ if (fs8.existsSync(path9.join(root, "pnpm-lock.yaml"))) return { name: "pnpm", ncuName: "pnpm" };
7335
+ if (fs8.existsSync(path9.join(root, "yarn.lock"))) return { name: "yarn", ncuName: "yarn" };
7336
+ if (fs8.existsSync(path9.join(root, "bun.lock")) || fs8.existsSync(path9.join(root, "bun.lockb")))
7337
+ return { name: "bun", ncuName: "bun" };
6974
7338
  return { name: "npm", ncuName: "npm" };
6975
7339
  }
6976
7340
  function readDependencies(pkgPath) {
6977
- const parsed = JSON.parse(fs7.readFileSync(pkgPath, "utf8"));
7341
+ const parsed = JSON.parse(fs8.readFileSync(pkgPath, "utf8"));
6978
7342
  if (typeof parsed !== "object" || parsed === null) return {};
6979
7343
  const pkg = parsed;
6980
7344
  const merge = (v) => {
@@ -7002,26 +7366,26 @@ function serverDirs(root) {
7002
7366
  const dirs = /* @__PURE__ */ new Set();
7003
7367
  try {
7004
7368
  const parsed = JSON.parse(
7005
- fs7.readFileSync(path8.join(root, "toilconfig.json"), "utf8")
7369
+ fs8.readFileSync(path9.join(root, "toilconfig.json"), "utf8")
7006
7370
  );
7007
7371
  const entries = typeof parsed === "object" && parsed !== null && Array.isArray(parsed.entries) ? parsed.entries.filter(
7008
7372
  (e) => typeof e === "string"
7009
7373
  ) : [];
7010
- for (const e of entries) dirs.add(path8.dirname(path8.resolve(root, e)));
7374
+ for (const e of entries) dirs.add(path9.dirname(path9.resolve(root, e)));
7011
7375
  } catch {
7012
7376
  }
7013
- if (dirs.size === 0) dirs.add(path8.join(root, "server"));
7377
+ if (dirs.size === 0) dirs.add(path9.join(root, "server"));
7014
7378
  return [...dirs];
7015
7379
  }
7016
7380
  function ensureMigrationsDirs(root) {
7017
7381
  const created = [];
7018
7382
  for (const dir of serverDirs(root)) {
7019
- if (!fs7.existsSync(dir)) continue;
7020
- const migrations = path8.join(dir, "migrations");
7021
- if (fs7.existsSync(migrations)) continue;
7022
- fs7.mkdirSync(migrations, { recursive: true });
7023
- fs7.writeFileSync(path8.join(migrations, "README.md"), MIGRATIONS_README);
7024
- created.push(path8.relative(root, migrations) || "migrations");
7383
+ if (!fs8.existsSync(dir)) continue;
7384
+ const migrations = path9.join(dir, "migrations");
7385
+ if (fs8.existsSync(migrations)) continue;
7386
+ fs8.mkdirSync(migrations, { recursive: true });
7387
+ fs8.writeFileSync(path9.join(migrations, "README.md"), MIGRATIONS_README);
7388
+ created.push(path9.relative(root, migrations) || "migrations");
7025
7389
  }
7026
7390
  return created;
7027
7391
  }
@@ -7037,16 +7401,16 @@ function rowLine(row) {
7037
7401
  function noteWithheld(names) {
7038
7402
  note(
7039
7403
  names.map((n3) => `${dim("-")} ${n3}`).join("\n") + "\n\n" + dim(
7040
- "Held back: toiljs supports TypeScript 7 only. Compiler updates must stay within 7.x."
7404
+ "Held back: compiler updates must stay within TypeScript 7; native testing updates must stay within the supported Vitest and WebdriverIO versions."
7041
7405
  ),
7042
7406
  warn("Not upgraded")
7043
7407
  );
7044
7408
  }
7045
7409
  var TARGETS = /* @__PURE__ */ new Set(["latest", "minor", "patch", "newest", "greatest"]);
7046
7410
  async function runUpdate(opts2) {
7047
- const root = path8.resolve(opts2.root ?? opts2.cwd);
7048
- const pkgPath = path8.join(root, "package.json");
7049
- if (!fs7.existsSync(pkgPath)) {
7411
+ const root = path9.resolve(opts2.root ?? opts2.cwd);
7412
+ const pkgPath = path9.join(root, "package.json");
7413
+ if (!fs8.existsSync(pkgPath)) {
7050
7414
  throw new Error("No package.json here. Run from your project root or pass --root <dir>.");
7051
7415
  }
7052
7416
  const currentDeps = readDependencies(pkgPath);
@@ -7079,7 +7443,13 @@ async function runUpdate(opts2) {
7079
7443
  return;
7080
7444
  }
7081
7445
  const upgraded = parseNcuJson(res.stdout);
7082
- const withheld = withheldUpgrades(upgraded);
7446
+ const testConfig = await findTestConfiguration(root);
7447
+ const withheld = [
7448
+ ...withheldUpgrades(upgraded),
7449
+ ...Object.keys(upgraded).filter(
7450
+ (name) => testConfig.native && !isSupportedTestingRange(name, upgraded[name])
7451
+ )
7452
+ ];
7083
7453
  for (const name of withheld) delete upgraded[name];
7084
7454
  if (!isSupportedTypeScriptRange(currentDeps.typescript ?? "") && !upgraded.typescript) {
7085
7455
  const versions = await capture("npm", ["view", "typescript@7", "version", "--json"], root);
@@ -7096,7 +7466,20 @@ async function runUpdate(opts2) {
7096
7466
  if (rows.length === 0) {
7097
7467
  s.stop("Everything is up to date");
7098
7468
  if (withheld.length > 0) noteWithheld(withheld);
7099
- outro(success("Nothing to update."));
7469
+ if (opts2.yes) {
7470
+ const testing2 = await ensureTesting(root, true);
7471
+ if (testing2.changed.length) note(testing2.changed.join("\n"), "Testing setup");
7472
+ if (testing2.skipped.length) note(testing2.skipped.join("\n"), "Manual changes");
7473
+ if (testing2.install && !await installUpdates(pm.name, root)) return;
7474
+ outro(
7475
+ success(testing2.changed.length ? "Testing setup updated." : "Nothing to update.")
7476
+ );
7477
+ } else
7478
+ outro(
7479
+ success(
7480
+ "Nothing to update. Use `toiljs doctor --fix` or `toiljs update --yes` to add missing testing setup."
7481
+ )
7482
+ );
7100
7483
  return;
7101
7484
  }
7102
7485
  s.stop(`${String(rows.length)} update${rows.length === 1 ? "" : "s"} available`);
@@ -7131,7 +7514,17 @@ async function runUpdate(opts2) {
7131
7514
  outro(dim("No packages selected."));
7132
7515
  return;
7133
7516
  }
7134
- const pkg = JSON.parse(fs7.readFileSync(pkgPath, "utf8"));
7517
+ if (testConfig.native) {
7518
+ const paired = selected.find(
7519
+ (name) => name === "vitest" || name === "@vitest/coverage-v8"
7520
+ );
7521
+ if (paired) {
7522
+ const counterpart = paired === "vitest" ? "@vitest/coverage-v8" : "vitest";
7523
+ upgraded[counterpart] = upgraded[paired];
7524
+ if (!selected.includes(counterpart)) selected.push(counterpart);
7525
+ }
7526
+ }
7527
+ const pkg = JSON.parse(fs8.readFileSync(pkgPath, "utf8"));
7135
7528
  const targetTypeScript = selected.includes("typescript") ? upgraded.typescript : currentDeps.typescript;
7136
7529
  if (!targetTypeScript || !isSupportedTypeScriptRange(targetTypeScript)) {
7137
7530
  throw new Error("Select the TypeScript 7 migration before installing updates.");
@@ -7157,23 +7550,30 @@ async function runUpdate(opts2) {
7157
7550
  pkg.devDependencies = deps;
7158
7551
  }
7159
7552
  }
7160
- fs7.writeFileSync(pkgPath, JSON.stringify(pkg, null, 4) + "\n");
7553
+ fs8.writeFileSync(pkgPath, JSON.stringify(pkg, null, 4) + "\n");
7161
7554
  s.stop("package.json updated");
7162
- note(dim(`Running ${pm.name} install\u2026`), "Install");
7555
+ const testing = await ensureTesting(root, true);
7556
+ if (testing.changed.length) note(testing.changed.join("\n"), "Testing setup");
7557
+ if (testing.skipped.length) note(testing.skipped.join("\n"), "Manual changes");
7558
+ if (!await installUpdates(pm.name, root)) return;
7559
+ outro(
7560
+ success(`Updated ${String(selected.length)} package${selected.length === 1 ? "" : "s"}.`)
7561
+ );
7562
+ }
7563
+ async function installUpdates(pm, root) {
7564
+ note(dim(`Running ${pm} install\u2026`), "Install");
7163
7565
  try {
7164
- await run(pm.name, ["install"], root, { stdio: "inherit" });
7566
+ await run(pm, ["install"], root, { stdio: "inherit" });
7165
7567
  } catch {
7166
7568
  outro(
7167
7569
  danger(
7168
- `${pm.name} install failed \u2014 package.json was updated to the new versions, but the install did not finish. Fix the error printed above, then run \`${pm.name} install\`.`
7570
+ `${pm} install failed \u2014 package.json was updated to the new versions, but the install did not finish. Fix the error printed above, then run \`${pm} install\`.`
7169
7571
  )
7170
7572
  );
7171
7573
  process.exitCode = 1;
7172
- return;
7574
+ return false;
7173
7575
  }
7174
- outro(
7175
- success(`Updated ${String(selected.length)} package${selected.length === 1 ? "" : "s"}.`)
7176
- );
7576
+ return true;
7177
7577
  }
7178
7578
 
7179
7579
  // src/cli/version-check.ts
@@ -7248,8 +7648,8 @@ function findOutdated(latest, projectVersion, cliVersion, cliIsLocal, pm) {
7248
7648
  var REGISTRY_URL = "https://registry.npmjs.org/toiljs/latest";
7249
7649
  var FETCH_TIMEOUT_MS = 2e3;
7250
7650
  function cacheFile() {
7251
- const base = process.env.XDG_CACHE_HOME ?? path9.join(os.homedir(), ".cache");
7252
- return path9.join(base, "toiljs", "update-check.json");
7651
+ const base = process.env.XDG_CACHE_HOME ?? path10.join(os.homedir(), ".cache");
7652
+ return path10.join(base, "toiljs", "update-check.json");
7253
7653
  }
7254
7654
  async function fetchLatest() {
7255
7655
  const ctrl = new AbortController();
@@ -7275,22 +7675,22 @@ async function fetchLatest() {
7275
7675
  async function resolveLatest() {
7276
7676
  const file = cacheFile();
7277
7677
  try {
7278
- const cached = parseCheckCache(fs8.readFileSync(file, "utf8"));
7678
+ const cached = parseCheckCache(fs9.readFileSync(file, "utf8"));
7279
7679
  if (cached && isCacheFresh(cached, Date.now())) return cached.latest;
7280
7680
  } catch {
7281
7681
  }
7282
7682
  const latest = await fetchLatest();
7283
7683
  try {
7284
- fs8.mkdirSync(path9.dirname(file), { recursive: true });
7285
- fs8.writeFileSync(file, JSON.stringify({ latest, checkedAt: Date.now() }));
7684
+ fs9.mkdirSync(path10.dirname(file), { recursive: true });
7685
+ fs9.writeFileSync(file, JSON.stringify({ latest, checkedAt: Date.now() }));
7286
7686
  } catch {
7287
7687
  }
7288
7688
  return latest;
7289
7689
  }
7290
7690
  function projectToiljsVersion(root) {
7291
7691
  try {
7292
- const raw = fs8.readFileSync(
7293
- path9.join(root, "node_modules", "toiljs", "package.json"),
7692
+ const raw = fs9.readFileSync(
7693
+ path10.join(root, "node_modules", "toiljs", "package.json"),
7294
7694
  "utf8"
7295
7695
  );
7296
7696
  const parsed = JSON.parse(raw);
@@ -7315,9 +7715,9 @@ async function notifyIfOutdated(rootArg) {
7315
7715
  if (env.TOILJS_NO_UPDATE_CHECK || env.NO_UPDATE_NOTIFIER || env.CI) return;
7316
7716
  const latest = await resolveLatest();
7317
7717
  if (!latest) return;
7318
- const root = path9.resolve(rootArg ?? process.cwd());
7319
- const cliDir = path9.dirname(fileURLToPath4(import.meta.url));
7320
- const cliIsLocal = cliDir.startsWith(path9.join(root, "node_modules") + path9.sep);
7718
+ const root = path10.resolve(rootArg ?? process.cwd());
7719
+ const cliDir = path10.dirname(fileURLToPath4(import.meta.url));
7720
+ const cliIsLocal = cliDir.startsWith(path10.join(root, "node_modules") + path10.sep);
7321
7721
  const rows = findOutdated(
7322
7722
  latest,
7323
7723
  projectToiljsVersion(root),
@@ -7331,6 +7731,62 @@ async function notifyIfOutdated(rootArg) {
7331
7731
  }
7332
7732
  }
7333
7733
 
7734
+ // src/cli/test.ts
7735
+ import path11 from "node:path";
7736
+ import { createRequire as createRequire3 } from "node:module";
7737
+ import { spawn as spawn2 } from "node:child_process";
7738
+ function parseTestArgs(args) {
7739
+ let root;
7740
+ let browser = false;
7741
+ let all = false;
7742
+ let watch = false;
7743
+ const forwarded = [];
7744
+ for (let index = 0; index < args.length; index++) {
7745
+ const arg = args[index];
7746
+ if (arg === "--root") {
7747
+ const value = args[++index];
7748
+ if (!value) throw new Error("toiljs test: --root requires a directory.");
7749
+ root = value;
7750
+ } else if (arg === "--browser") browser = true;
7751
+ else if (arg === "--all") all = true;
7752
+ else if (arg === "--watch" || arg === "-w") watch = true;
7753
+ else if (arg !== "--") forwarded.push(arg);
7754
+ }
7755
+ if (browser && all) throw new Error("toiljs test: use either --browser or --all.");
7756
+ return { root, browser, all, watch, arguments: forwarded };
7757
+ }
7758
+ async function runTest(options) {
7759
+ const root = path11.resolve(options.root ?? process.cwd());
7760
+ const setup = await ensureTesting(root);
7761
+ if (setup.install) {
7762
+ const pm = detectPackageManager2(root).name;
7763
+ process.stdout.write(
7764
+ `toiljs test: installing Vitest, browser testing, and V8 coverage with ${pm}.
7765
+ `
7766
+ );
7767
+ await run(pm, ["install"], root, { stdio: "inherit" });
7768
+ }
7769
+ const require2 = createRequire3(path11.join(root, "package.json"));
7770
+ const cli = path11.join(
7771
+ path11.dirname(require2.resolve("vitest/package.json")),
7772
+ "vitest.mjs"
7773
+ );
7774
+ const args = [cli, ...options.watch ? [] : ["run"]];
7775
+ const projectFlag = options.arguments.some(
7776
+ (arg) => arg === "--project" || arg.startsWith("--project=")
7777
+ );
7778
+ if (setup.native && !options.all && !projectFlag)
7779
+ args.push("--project", options.browser ? "browser" : "unit");
7780
+ if (!setup.native && options.browser) args.push("--browser");
7781
+ args.push(...options.arguments);
7782
+ const code = await new Promise((resolve, reject) => {
7783
+ const child = spawn2(process.execPath, args, { cwd: root, stdio: "inherit" });
7784
+ child.once("error", reject);
7785
+ child.once("exit", (status) => resolve(status ?? 1));
7786
+ });
7787
+ process.exitCode = code;
7788
+ }
7789
+
7334
7790
  // src/cli/index.ts
7335
7791
  var __origEmitWarning = process.emitWarning.bind(process);
7336
7792
  process.emitWarning = ((warning, ...rest) => {
@@ -7437,6 +7893,7 @@ function printHelp() {
7437
7893
  cmd("create [name]", "scaffold a new toiljs app"),
7438
7894
  cmd("configure", "toggle styling features (Sass/Less/Stylus, Tailwind)"),
7439
7895
  cmd("generate auth", "generate private auth keys and the matching client public key"),
7896
+ cmd("test", "run Vitest (auto-installs missing test tooling)"),
7440
7897
  cmd("dev", "start the dev server with HMR"),
7441
7898
  cmd("build", "build the optimized production bundle"),
7442
7899
  cmd("start", "self-host the built app (hyper-express / uWS)"),
@@ -7447,7 +7904,10 @@ function printHelp() {
7447
7904
  bold("Options"),
7448
7905
  cmd("--root <dir>", "project root (default: current directory)"),
7449
7906
  cmd("--port <n>", "dev/start: listen port (or PORT env / client.port; default 3000)"),
7450
- cmd("--host <h>", "dev/start: bind host, e.g. 0.0.0.0 (or HOST env / client.host; default 127.0.0.1)"),
7907
+ cmd(
7908
+ "--host <h>",
7909
+ "dev/start: bind host, e.g. 0.0.0.0 (or HOST env / client.host; default 127.0.0.1)"
7910
+ ),
7451
7911
  cmd("--threads <n>", "start: production HTTP worker count"),
7452
7912
  cmd("-t, --template", "create: app | minimal | agent"),
7453
7913
  cmd("--style <name>", "create/configure: css | sass | less | stylus"),
@@ -7455,9 +7915,13 @@ function printHelp() {
7455
7915
  cmd("--no-ai", "create: skip AI assistant files (CLAUDE.md, etc.)"),
7456
7916
  cmd("-y, --yes", "create: accept defaults (non-interactive)"),
7457
7917
  cmd("--no-install", "create: don't install dependencies"),
7918
+ cmd("--browser", "test: run real Chrome browser tests"),
7919
+ cmd("--all", "test: run unit and browser projects"),
7920
+ cmd("--coverage", "test: collect V8 coverage reports"),
7921
+ cmd("--watch", "test: watch for changes"),
7458
7922
  cmd("--server", "build: build only the server (regenerate shared/server.ts + wasm)"),
7459
7923
  cmd("--json", "doctor: machine-readable output"),
7460
- cmd("--fix", "doctor: auto-fix what it can (typed-RPC wiring)"),
7924
+ cmd("--fix", "doctor: repair testing, compiler, and server wiring"),
7461
7925
  cmd("--target <t>", "update: latest | minor | patch | newest | greatest"),
7462
7926
  cmd("-v, --version", "print the toiljs version"),
7463
7927
  "",
@@ -7476,6 +7940,9 @@ async function main() {
7476
7940
  const flags = parseArgs(rest);
7477
7941
  await notifyIfOutdated(flags.root);
7478
7942
  switch (command) {
7943
+ case "test":
7944
+ await runTest(parseTestArgs(rest));
7945
+ break;
7479
7946
  case "create":
7480
7947
  banner();
7481
7948
  await runCreate({
@@ -7534,7 +8001,9 @@ async function main() {
7534
8001
  const shownHost = wildcard || server.host === "127.0.0.1" ? "localhost" : server.host;
7535
8002
  const exposed = server.host !== "127.0.0.1" && server.host !== "localhost" && server.host !== "::1";
7536
8003
  process.stdout.write(
7537
- accent(" \u279C ") + bold(`http://${shownHost}:${String(server.port)}`) + dim(` ws channel: ${server.wsPath}`) + (exposed ? "\n" + dim(` bound to ${server.host}:${String(server.port)} (exposed beyond loopback)`) : "") + "\n\n"
8004
+ accent(" \u279C ") + bold(`http://${shownHost}:${String(server.port)}`) + dim(` ws channel: ${server.wsPath}`) + (exposed ? "\n" + dim(
8005
+ ` bound to ${server.host}:${String(server.port)} (exposed beyond loopback)`
8006
+ ) : "") + "\n\n"
7538
8007
  );
7539
8008
  break;
7540
8009
  }