toiljs 0.0.122 → 0.0.123

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.
@@ -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.",
@@ -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");
@@ -6970,7 +7072,8 @@ function buildRows(upgraded, currentDeps) {
6970
7072
  function detectPackageManager2(root) {
6971
7073
  if (fs7.existsSync(path8.join(root, "pnpm-lock.yaml"))) return { name: "pnpm", ncuName: "pnpm" };
6972
7074
  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" };
7075
+ if (fs7.existsSync(path8.join(root, "bun.lock")) || fs7.existsSync(path8.join(root, "bun.lockb")))
7076
+ return { name: "bun", ncuName: "bun" };
6974
7077
  return { name: "npm", ncuName: "npm" };
6975
7078
  }
6976
7079
  function readDependencies(pkgPath) {
@@ -7331,6 +7434,115 @@ async function notifyIfOutdated(rootArg) {
7331
7434
  }
7332
7435
  }
7333
7436
 
7437
+ // src/cli/test.ts
7438
+ import fs9 from "node:fs/promises";
7439
+ import path10 from "node:path";
7440
+ import { createRequire as createRequire2 } from "node:module";
7441
+ import { spawn as spawn2 } from "node:child_process";
7442
+ async function writeMissing(root, file, content) {
7443
+ const target = path10.join(root, file);
7444
+ await fs9.mkdir(path10.dirname(target), { recursive: true });
7445
+ try {
7446
+ await fs9.writeFile(target, content, { flag: "wx" });
7447
+ } catch (error) {
7448
+ if (!(error instanceof Error) || !("code" in error) || error.code !== "EEXIST") throw error;
7449
+ }
7450
+ }
7451
+ async function ensureTesting(root) {
7452
+ const pkgPath = path10.join(root, "package.json");
7453
+ const raw = JSON.parse(await fs9.readFile(pkgPath, "utf8"));
7454
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw))
7455
+ throw new Error("toiljs test: package.json must contain an object.");
7456
+ const pkg = raw;
7457
+ for (const field of [pkg.scripts, pkg.dependencies, pkg.devDependencies]) {
7458
+ if (field !== void 0 && (field === null || typeof field !== "object" || Array.isArray(field) || Object.values(field).some((value) => typeof value !== "string")))
7459
+ throw new Error("toiljs test: package scripts and dependencies must be string maps.");
7460
+ }
7461
+ const files = await fs9.readdir(root);
7462
+ const custom = files.find((file) => /^vitest\.config\.[cm]?[jt]s$/.test(file)) ?? files.find((file) => /^vite\.config\.[cm]?[jt]s$/.test(file));
7463
+ const source = custom === void 0 ? "" : await fs9.readFile(path10.join(root, custom), "utf8");
7464
+ const native = custom === void 0 || source.includes("from 'toiljs/vitest'") || source.includes('from "toiljs/vitest"');
7465
+ let changed = false;
7466
+ let install = false;
7467
+ const require2 = createRequire2(pkgPath);
7468
+ for (const [name, range] of Object.entries(TEST_DEPENDENCIES)) {
7469
+ if (pkg.dependencies?.[name] === void 0 && pkg.devDependencies?.[name] === void 0) {
7470
+ pkg.devDependencies ??= {};
7471
+ pkg.devDependencies[name] = range;
7472
+ changed = true;
7473
+ install = true;
7474
+ }
7475
+ try {
7476
+ require2.resolve(name);
7477
+ } catch {
7478
+ install = true;
7479
+ }
7480
+ }
7481
+ pkg.scripts ??= {};
7482
+ for (const [name, script] of Object.entries(TEST_SCRIPTS)) {
7483
+ if (pkg.scripts[name] !== void 0) continue;
7484
+ pkg.scripts[name] = script;
7485
+ changed = true;
7486
+ }
7487
+ if (changed) await fs9.writeFile(pkgPath, JSON.stringify(raw, null, 4) + "\n");
7488
+ if (custom === void 0) {
7489
+ for (const [file, content] of Object.entries(testingFiles()))
7490
+ await writeMissing(root, file, content);
7491
+ }
7492
+ return { install, native };
7493
+ }
7494
+ function parseTestArgs(args) {
7495
+ let root;
7496
+ let browser = false;
7497
+ let all = false;
7498
+ let watch = false;
7499
+ const forwarded = [];
7500
+ for (let index = 0; index < args.length; index++) {
7501
+ const arg = args[index];
7502
+ if (arg === "--root") {
7503
+ const value = args[++index];
7504
+ if (!value) throw new Error("toiljs test: --root requires a directory.");
7505
+ root = value;
7506
+ } else if (arg === "--browser") browser = true;
7507
+ else if (arg === "--all") all = true;
7508
+ else if (arg === "--watch" || arg === "-w") watch = true;
7509
+ else if (arg !== "--") forwarded.push(arg);
7510
+ }
7511
+ if (browser && all) throw new Error("toiljs test: use either --browser or --all.");
7512
+ return { root, browser, all, watch, arguments: forwarded };
7513
+ }
7514
+ async function runTest(options) {
7515
+ const root = path10.resolve(options.root ?? process.cwd());
7516
+ const setup = await ensureTesting(root);
7517
+ if (setup.install) {
7518
+ const pm = detectPackageManager2(root).name;
7519
+ process.stdout.write(
7520
+ `toiljs test: installing Vitest, browser testing, and V8 coverage with ${pm}.
7521
+ `
7522
+ );
7523
+ await run(pm, ["install"], root, { stdio: "inherit" });
7524
+ }
7525
+ const require2 = createRequire2(path10.join(root, "package.json"));
7526
+ const cli = path10.join(
7527
+ path10.dirname(require2.resolve("vitest/package.json")),
7528
+ "vitest.mjs"
7529
+ );
7530
+ const args = [cli, ...options.watch ? [] : ["run"]];
7531
+ const projectFlag = options.arguments.some(
7532
+ (arg) => arg === "--project" || arg.startsWith("--project=")
7533
+ );
7534
+ if (setup.native && !options.all && !projectFlag)
7535
+ args.push("--project", options.browser ? "browser" : "unit");
7536
+ if (!setup.native && options.browser) args.push("--browser");
7537
+ args.push(...options.arguments);
7538
+ const code = await new Promise((resolve, reject) => {
7539
+ const child = spawn2(process.execPath, args, { cwd: root, stdio: "inherit" });
7540
+ child.once("error", reject);
7541
+ child.once("exit", (status) => resolve(status ?? 1));
7542
+ });
7543
+ process.exitCode = code;
7544
+ }
7545
+
7334
7546
  // src/cli/index.ts
7335
7547
  var __origEmitWarning = process.emitWarning.bind(process);
7336
7548
  process.emitWarning = ((warning, ...rest) => {
@@ -7437,6 +7649,7 @@ function printHelp() {
7437
7649
  cmd("create [name]", "scaffold a new toiljs app"),
7438
7650
  cmd("configure", "toggle styling features (Sass/Less/Stylus, Tailwind)"),
7439
7651
  cmd("generate auth", "generate private auth keys and the matching client public key"),
7652
+ cmd("test", "run Vitest (auto-installs missing test tooling)"),
7440
7653
  cmd("dev", "start the dev server with HMR"),
7441
7654
  cmd("build", "build the optimized production bundle"),
7442
7655
  cmd("start", "self-host the built app (hyper-express / uWS)"),
@@ -7447,7 +7660,10 @@ function printHelp() {
7447
7660
  bold("Options"),
7448
7661
  cmd("--root <dir>", "project root (default: current directory)"),
7449
7662
  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)"),
7663
+ cmd(
7664
+ "--host <h>",
7665
+ "dev/start: bind host, e.g. 0.0.0.0 (or HOST env / client.host; default 127.0.0.1)"
7666
+ ),
7451
7667
  cmd("--threads <n>", "start: production HTTP worker count"),
7452
7668
  cmd("-t, --template", "create: app | minimal | agent"),
7453
7669
  cmd("--style <name>", "create/configure: css | sass | less | stylus"),
@@ -7455,6 +7671,10 @@ function printHelp() {
7455
7671
  cmd("--no-ai", "create: skip AI assistant files (CLAUDE.md, etc.)"),
7456
7672
  cmd("-y, --yes", "create: accept defaults (non-interactive)"),
7457
7673
  cmd("--no-install", "create: don't install dependencies"),
7674
+ cmd("--browser", "test: run real Chrome browser tests"),
7675
+ cmd("--all", "test: run unit and browser projects"),
7676
+ cmd("--coverage", "test: collect V8 coverage reports"),
7677
+ cmd("--watch", "test: watch for changes"),
7458
7678
  cmd("--server", "build: build only the server (regenerate shared/server.ts + wasm)"),
7459
7679
  cmd("--json", "doctor: machine-readable output"),
7460
7680
  cmd("--fix", "doctor: auto-fix what it can (typed-RPC wiring)"),
@@ -7476,6 +7696,9 @@ async function main() {
7476
7696
  const flags = parseArgs(rest);
7477
7697
  await notifyIfOutdated(flags.root);
7478
7698
  switch (command) {
7699
+ case "test":
7700
+ await runTest(parseTestArgs(rest));
7701
+ break;
7479
7702
  case "create":
7480
7703
  banner();
7481
7704
  await runCreate({
@@ -7534,7 +7757,9 @@ async function main() {
7534
7757
  const shownHost = wildcard || server.host === "127.0.0.1" ? "localhost" : server.host;
7535
7758
  const exposed = server.host !== "127.0.0.1" && server.host !== "localhost" && server.host !== "::1";
7536
7759
  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"
7760
+ accent(" \u279C ") + bold(`http://${shownHost}:${String(server.port)}`) + dim(` ws channel: ${server.wsPath}`) + (exposed ? "\n" + dim(
7761
+ ` bound to ${server.host}:${String(server.port)} (exposed beyond loopback)`
7762
+ ) : "") + "\n\n"
7538
7763
  );
7539
7764
  break;
7540
7765
  }