ts-builds 4.2.0 → 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +22 -4
  2. package/dist/cli.js +80 -29
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -221,6 +221,23 @@ npx ts-builds validate:core
221
221
  npx ts-builds validate:landing
222
222
  ```
223
223
 
224
+ A `commands` entry named after a built-in step (`format`, `format:check`, `lint`, `lint:check`, `typecheck`,
225
+ `ts-types`, `test`, `test:watch`, `test:coverage`, `test:ui`, `build`, `build:watch`, `dev`, `preview`) replaces
226
+ that step everywhere: inside a chain, and when you run it directly (`ts-builds lint:check`, or
227
+ `ts-builds lint check`). A chain with one of those names takes precedence over both, as it does inside a chain.
228
+ `compile` can be overridden too, but only runs inside a chain. An override runs your command instead of
229
+ ts-builds' lint, so it sends no lint telemetry, and an override of `lint` or `lint:check` removes any old
230
+ `.ts-builds/lint-report.json`, so `lint:summary` can't read stale counts.
231
+
232
+ ```json
233
+ {
234
+ "commands": {
235
+ "lint": "eslint ./src ./test --fix --max-warnings=0",
236
+ "lint:check": "eslint ./src ./test --max-warnings=0"
237
+ }
238
+ }
239
+ ```
240
+
224
241
  ### Aggregating lint results across a monorepo
225
242
 
226
243
  Under a task runner, `ts-builds` runs per-package, so each package's ESLint output
@@ -299,8 +316,9 @@ Each run sends one JSON object:
299
316
 
300
317
  - **Never sent:** file paths, source code or message text.
301
318
  - **Sent as-is:** the `package.json` name, left out when there isn't one, and rule ids as your config
302
- names them. Plugin names and versions are sent only when they look like an npm package name and a
303
- semver version.
319
+ names them. A plugin's declared name is used only when it looks like an npm package name, and
320
+ otherwise is replaced by the name derived from its rule prefix, such as `eslint-plugin-<prefix>`. Its
321
+ declared version is used only when it's semver.
304
322
  - **`project`:** a salted hash of the git remote, so one repo's packages group together. It's a
305
323
  stable pseudonym, not anonymity: anyone who knows the remote URL can compute it. It's left out
306
324
  when there's no remote.
@@ -311,8 +329,8 @@ Each run sends one JSON object:
311
329
  - **`enabled`:** the plugin rules that are on, with their severity, read from the first linted file's
312
330
  config. It tells a rule with no findings apart from a rule that's off. Core rules are left out.
313
331
 
314
- It never changes a run's result. It sends after lint finishes and its output is printed, waits at
315
- most 2 seconds, and ignores every error, so the exit code and output stay the same. A run that
332
+ It never changes a run's result. It sends after lint finishes and its output is printed, the send
333
+ waits at most 2 seconds, and it ignores every error, so the exit code and output stay the same. A run that
316
334
  fails before linting, such as one with a broken config, sends nothing.
317
335
 
318
336
  It doesn't cover `lint.useProjectEslint`, which spawns your own ESLint so ts-builds never sees the
package/dist/cli.js CHANGED
@@ -343,6 +343,13 @@ const LINT_REPORT_VERSION = 1;
343
343
  function lintReportPath(dir = targetDir) {
344
344
  return join(dir, ".ts-builds", "lint-report.json");
345
345
  }
346
+ /**
347
+ * Remove the sidecar. When a `commands` override replaces `lint` or `lint:check`, ts-builds' own lint
348
+ * doesn't run and writes no report, so an old one would leave `lint:summary` reading stale counts.
349
+ */
350
+ function forgetLintReport(dir = targetDir) {
351
+ rmSync(lintReportPath(dir), { force: true });
352
+ }
346
353
  /** The `name` in `<dir>/package.json`, when the file exists, parses and has a non-empty name. */
347
354
  function packageJsonName(dir = targetDir) {
348
355
  const pkgPath = join(dir, "package.json");
@@ -426,9 +433,10 @@ function resolveEnv(env) {
426
433
  ...env
427
434
  } : process.env;
428
435
  }
429
- function runCommand(command, args, options = {}) {
436
+ /** Spawn one shell command line; resolves with its exit code, or 1 if it can't start */
437
+ function spawnShell(commandLine, label, options) {
430
438
  return new Promise((resolve) => {
431
- const child = spawn(command, args, {
439
+ const child = spawn(commandLine, {
432
440
  cwd: resolveCwd(options.cwd),
433
441
  env: resolveEnv(options.env),
434
442
  stdio: "inherit",
@@ -438,27 +446,24 @@ function runCommand(command, args, options = {}) {
438
446
  resolve(code ?? 1);
439
447
  });
440
448
  child.on("error", (err) => {
441
- console.error(`Failed to run ${command}: ${err.message}`);
449
+ console.error(`Failed to run${label}: ${err.message}`);
442
450
  resolve(1);
443
451
  });
444
452
  });
445
453
  }
454
+ /**
455
+ * Run `command` with `args` through the shell, so `.bin` shims (including
456
+ * Windows `.cmd` files) and commands such as `npx eslint` resolve. `args` are
457
+ * joined into the command line as given, not escaped: quote any argument that
458
+ * holds spaces or shell characters, as `prettierFormatArgs` does. Joining here
459
+ * rather than passing `args` to `spawn` gives the same command line Node built
460
+ * before, without its DEP0190 warning, which flags exactly that unescaped join.
461
+ */
462
+ function runCommand(command, args, options = {}) {
463
+ return spawnShell([command, ...args].join(" "), ` ${command}`, options);
464
+ }
446
465
  function runShellCommand(shellCmd, options = {}) {
447
- return new Promise((resolve) => {
448
- const child = spawn(shellCmd, {
449
- cwd: resolveCwd(options.cwd),
450
- env: resolveEnv(options.env),
451
- stdio: "inherit",
452
- shell: true
453
- });
454
- child.on("close", (code) => {
455
- resolve(code ?? 1);
456
- });
457
- child.on("error", (err) => {
458
- console.error(`Failed to run: ${err.message}`);
459
- resolve(1);
460
- });
461
- });
466
+ return spawnShell(shellCmd, "", options);
462
467
  }
463
468
 
464
469
  // src/cli/commands/build.ts
@@ -606,7 +611,7 @@ function prettierConfigArg() {
606
611
  * no prettier config of their own (see `prettierConfigArg`), so we fill the gap without ever
607
612
  * overriding a config they wrote.
608
613
  *
609
- * Bundled paths are quoted because `runCommand` spawns with `shell: true` (the shell strips the
614
+ * Bundled paths are quoted because `runCommand` runs one shell command line (the shell strips the
610
615
  * quotes), keeping them correct when the install path contains spaces.
611
616
  */
612
617
  function prettierFormatArgs(check, bundledIgnore = bundledPrettierIgnorePath(), bundledConfig = prettierConfigArg()) {
@@ -2405,6 +2410,53 @@ function runSize(args) {
2405
2410
  function isRunFnCommand(cmd) {
2406
2411
  return "runFn" in cmd;
2407
2412
  }
2413
+ /** The built-in chain steps. A `commands` entry with one of these names replaces it, in a chain or run directly. */
2414
+ const BUILTIN_STEP_NAMES = [
2415
+ "format",
2416
+ "format:check",
2417
+ "lint",
2418
+ "lint:check",
2419
+ "typecheck",
2420
+ "ts-types",
2421
+ "test",
2422
+ "test:watch",
2423
+ "test:coverage",
2424
+ "test:ui",
2425
+ "build",
2426
+ "build:watch",
2427
+ "dev",
2428
+ "preview",
2429
+ "compile"
2430
+ ];
2431
+ const isBuiltinStepName = (name) => BUILTIN_STEP_NAMES.includes(name);
2432
+ /** The step a top-level command runs: `lint check` is `lint:check`, `test watch` is `test:watch`, `test run` is `test` */
2433
+ function stepName(command, subCommand) {
2434
+ if ((command === "lint" || command === "format") && subCommand === "check") return `${command}:check`;
2435
+ if (command === "build" && subCommand === "watch") return "build:watch";
2436
+ if (command === "test" && (subCommand === "watch" || subCommand === "coverage" || subCommand === "ui")) return `test:${subCommand}`;
2437
+ return command;
2438
+ }
2439
+ /**
2440
+ * The user's replacement for a built-in step, if their config has one. Run directly, a built-in
2441
+ * resolves the same way it does inside a chain (`runStep`): `config.commands[step] ?? builtin`.
2442
+ */
2443
+ function builtinOverride(step, config) {
2444
+ return Option(config.commands[step]).filter(() => isBuiltinStepName(step));
2445
+ }
2446
+ /** An override of `lint` or `lint:check` replaces ts-builds' lint, so its old report must go */
2447
+ function forgetLintReportIfLint(step) {
2448
+ if (step === "lint" || step === "lint:check") forgetLintReport();
2449
+ }
2450
+ /** Run a `commands` entry on its own: check its cwd, then run it through the shell */
2451
+ async function runConfiguredCommand(name, cmdDef) {
2452
+ const cwdError = escapingValidateCwdError(name, cmdDef);
2453
+ if (cwdError) {
2454
+ console.error(cwdError);
2455
+ return 1;
2456
+ }
2457
+ forgetLintReportIfLint(name);
2458
+ return runShellCommand(cmdDef.run, { cwd: cmdDef.cwd });
2459
+ }
2408
2460
  function getBuiltinCommands(config) {
2409
2461
  return {
2410
2462
  format: { runFn: () => runFormat(false) },
@@ -2455,6 +2507,7 @@ async function runStep(step, config, builtins, path) {
2455
2507
  console.error(cwdError);
2456
2508
  return 1;
2457
2509
  }
2510
+ if (Object.hasOwn(config.commands, step)) forgetLintReportIfLint(step);
2458
2511
  const cwdLabel = "cwd" in cmdDef && cmdDef.cwd ? ` (in ${cmdDef.cwd})` : "";
2459
2512
  console.log(`\n▶ Running ${step}...${cwdLabel}`);
2460
2513
  const code = isRunFnCommand(cmdDef) ? await cmdDef.runFn() : await runShellCommand(cmdDef.run, { cwd: cmdDef.cwd });
@@ -2472,6 +2525,12 @@ async function runValidate(chainName = "validate") {
2472
2525
  // src/cli.ts
2473
2526
  const command = process.argv[2] || "init";
2474
2527
  const subCommand = process.argv[3];
2528
+ const step = stepName(command, subCommand);
2529
+ if (isBuiltinStepName(step)) {
2530
+ const config = loadConfig();
2531
+ if (Object.hasOwn(config.chains, step)) process.exit(await runValidate(step));
2532
+ await builtinOverride(step, config).fold(() => Promise.resolve(), async (cmd) => process.exit(await runConfiguredCommand(step, cmd)));
2533
+ }
2475
2534
  switch (command) {
2476
2535
  case "help":
2477
2536
  case "--help":
@@ -2550,16 +2609,8 @@ switch (command) {
2550
2609
  default: {
2551
2610
  const config = loadConfig();
2552
2611
  if (config.chains[command]) process.exit(await runValidate(command));
2553
- else if (config.commands[command]) {
2554
- const cmdDef = config.commands[command];
2555
- const cwdError = escapingValidateCwdError(command, cmdDef);
2556
- if (cwdError) {
2557
- console.error(cwdError);
2558
- process.exit(1);
2559
- }
2560
- const code = await runShellCommand(cmdDef.run, { cwd: cmdDef.cwd });
2561
- process.exit(code);
2562
- } else {
2612
+ else if (config.commands[command]) process.exit(await runConfiguredCommand(command, config.commands[command]));
2613
+ else {
2563
2614
  console.error(`Unknown command: ${command}`);
2564
2615
  console.log("Run 'ts-builds help' for usage information.");
2565
2616
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ts-builds",
3
- "version": "4.2.0",
3
+ "version": "4.3.0",
4
4
  "description": "Shared TypeScript configuration files for library templates. Provides standardized ESLint, Prettier, Vitest, TypeScript, and build configs.",
5
5
  "keywords": [
6
6
  "typescript",