toiljs 0.0.121 → 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,23 +4177,22 @@ 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
- "toil.config.ts": `import { defineConfig } from 'toiljs/compiler';
4098
-
4099
- export default defineConfig({
4100
- client: {
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.
4183
+ (template === "app" ? " server: { auth: true },\n" : "") + ` client: {
4101
4184
  // Optimize images at build time (resize/compress imported images).
4102
4185
  images: ${String(images)},
4103
4186
  },
4104
4187
  });
4105
4188
  `,
4106
- "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',
4107
4190
  "oxlint.config.ts": "import toiljs from 'toiljs/oxlint';\n\nexport default toiljs;\n",
4108
4191
  ".prettierrc": '"toiljs/prettier"\n',
4109
4192
  // Generated files don't need formatting. (toilscript server decorators like @main /
4110
4193
  // @remote-on-functions are handled by the toiljs/prettier-plugin, so server/ is not ignored.)
4111
- ".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",
4112
- ".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",
4113
4196
  // Use the native TypeScript 7 language server.
4114
4197
  ".vscode/settings.json": JSON.stringify(
4115
4198
  {
@@ -4193,6 +4276,21 @@ export default defineConfig({
4193
4276
  "",
4194
4277
  " npm run build",
4195
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
+ "",
4196
4294
  "## Editor setup",
4197
4295
  "",
4198
4296
  "Run `npm install` first and use Node 26 for editor tooling.",
@@ -4547,6 +4645,8 @@ async function runCreate(opts2) {
4547
4645
  await fs3.cp(appClientDir(), path4.join(targetDir, "client"), { recursive: true });
4548
4646
  const serverAllow = /* @__PURE__ */ new Set([
4549
4647
  "main.ts",
4648
+ "main.stream.ts",
4649
+ "SsrHelloRender.ts",
4550
4650
  "README.md",
4551
4651
  "tsconfig.json",
4552
4652
  "toil-server-env.d.ts",
@@ -4554,6 +4654,7 @@ async function runCreate(opts2) {
4554
4654
  "models",
4555
4655
  "routes",
4556
4656
  "services",
4657
+ "streams",
4557
4658
  "scheduled"
4558
4659
  ]);
4559
4660
  const serverSrc = appServerDir();
@@ -4601,6 +4702,8 @@ async function runCreate(opts2) {
4601
4702
  if (!install) steps.push("npm install");
4602
4703
  steps.push(`${accent("npm run dev")} ${dim("start the dev server")}`);
4603
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")}`);
4604
4707
  steps.push("Install the Oxc plugin for VS Code or WebStorm (README.md \u2192 Editor setup)");
4605
4708
  steps.push("WebStorm: Settings \u2192 Tools \u2192 Oxlint \u2192 Enable type aware rules");
4606
4709
  note(steps.map((l2) => dim(" ") + l2).join("\n"), "Next steps");
@@ -6969,7 +7072,8 @@ function buildRows(upgraded, currentDeps) {
6969
7072
  function detectPackageManager2(root) {
6970
7073
  if (fs7.existsSync(path8.join(root, "pnpm-lock.yaml"))) return { name: "pnpm", ncuName: "pnpm" };
6971
7074
  if (fs7.existsSync(path8.join(root, "yarn.lock"))) return { name: "yarn", ncuName: "yarn" };
6972
- 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" };
6973
7077
  return { name: "npm", ncuName: "npm" };
6974
7078
  }
6975
7079
  function readDependencies(pkgPath) {
@@ -7330,6 +7434,115 @@ async function notifyIfOutdated(rootArg) {
7330
7434
  }
7331
7435
  }
7332
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
+
7333
7546
  // src/cli/index.ts
7334
7547
  var __origEmitWarning = process.emitWarning.bind(process);
7335
7548
  process.emitWarning = ((warning, ...rest) => {
@@ -7436,6 +7649,7 @@ function printHelp() {
7436
7649
  cmd("create [name]", "scaffold a new toiljs app"),
7437
7650
  cmd("configure", "toggle styling features (Sass/Less/Stylus, Tailwind)"),
7438
7651
  cmd("generate auth", "generate private auth keys and the matching client public key"),
7652
+ cmd("test", "run Vitest (auto-installs missing test tooling)"),
7439
7653
  cmd("dev", "start the dev server with HMR"),
7440
7654
  cmd("build", "build the optimized production bundle"),
7441
7655
  cmd("start", "self-host the built app (hyper-express / uWS)"),
@@ -7446,7 +7660,10 @@ function printHelp() {
7446
7660
  bold("Options"),
7447
7661
  cmd("--root <dir>", "project root (default: current directory)"),
7448
7662
  cmd("--port <n>", "dev/start: listen port (or PORT env / client.port; default 3000)"),
7449
- 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
+ ),
7450
7667
  cmd("--threads <n>", "start: production HTTP worker count"),
7451
7668
  cmd("-t, --template", "create: app | minimal | agent"),
7452
7669
  cmd("--style <name>", "create/configure: css | sass | less | stylus"),
@@ -7454,6 +7671,10 @@ function printHelp() {
7454
7671
  cmd("--no-ai", "create: skip AI assistant files (CLAUDE.md, etc.)"),
7455
7672
  cmd("-y, --yes", "create: accept defaults (non-interactive)"),
7456
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"),
7457
7678
  cmd("--server", "build: build only the server (regenerate shared/server.ts + wasm)"),
7458
7679
  cmd("--json", "doctor: machine-readable output"),
7459
7680
  cmd("--fix", "doctor: auto-fix what it can (typed-RPC wiring)"),
@@ -7475,6 +7696,9 @@ async function main() {
7475
7696
  const flags = parseArgs(rest);
7476
7697
  await notifyIfOutdated(flags.root);
7477
7698
  switch (command) {
7699
+ case "test":
7700
+ await runTest(parseTestArgs(rest));
7701
+ break;
7478
7702
  case "create":
7479
7703
  banner();
7480
7704
  await runCreate({
@@ -7533,7 +7757,9 @@ async function main() {
7533
7757
  const shownHost = wildcard || server.host === "127.0.0.1" ? "localhost" : server.host;
7534
7758
  const exposed = server.host !== "127.0.0.1" && server.host !== "localhost" && server.host !== "::1";
7535
7759
  process.stdout.write(
7536
- 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"
7537
7763
  );
7538
7764
  break;
7539
7765
  }