@bensandee/tooling 0.10.1 → 0.12.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 (2) hide show
  1. package/dist/bin.mjs +153 -55
  2. package/package.json +1 -1
package/dist/bin.mjs CHANGED
@@ -516,7 +516,8 @@ const STANDARD_SCRIPTS_SINGLE = {
516
516
  test: "vitest run",
517
517
  lint: "oxlint",
518
518
  knip: "knip",
519
- check: "pnpm typecheck && pnpm build && pnpm lint && pnpm knip"
519
+ check: "pnpm exec tooling repo:run-checks",
520
+ "tooling:check": "pnpm exec tooling repo:check"
520
521
  };
521
522
  const STANDARD_SCRIPTS_MONOREPO = {
522
523
  build: "pnpm -r build",
@@ -524,7 +525,13 @@ const STANDARD_SCRIPTS_MONOREPO = {
524
525
  typecheck: "pnpm -r --parallel run typecheck",
525
526
  lint: "oxlint",
526
527
  knip: "knip",
527
- check: "pnpm typecheck && pnpm build && pnpm lint && pnpm knip"
528
+ check: "pnpm exec tooling repo:run-checks",
529
+ "tooling:check": "pnpm exec tooling repo:check"
530
+ };
531
+ /** Scripts that tooling owns — map from script name to keyword that must appear in the value. */
532
+ const MANAGED_SCRIPTS = {
533
+ check: "repo:run-checks",
534
+ "tooling:check": "repo:check"
528
535
  };
529
536
  /** DevDeps that belong in every project (single repo) or per-package (monorepo). */
530
537
  const PER_PACKAGE_DEV_DEPS = {
@@ -581,7 +588,7 @@ function getAddedDevDepNames(config) {
581
588
  const deps = { ...ROOT_DEV_DEPS };
582
589
  if (config.structure !== "monorepo") Object.assign(deps, PER_PACKAGE_DEV_DEPS);
583
590
  deps["@bensandee/config"] = "0.7.1";
584
- deps["@bensandee/tooling"] = "0.10.1";
591
+ deps["@bensandee/tooling"] = "0.12.0";
585
592
  if (config.formatter === "oxfmt") deps["oxfmt"] = "0.35.0";
586
593
  if (config.formatter === "prettier") deps["prettier"] = "3.8.1";
587
594
  addReleaseDeps(deps, config);
@@ -598,11 +605,11 @@ async function generatePackageJson(ctx) {
598
605
  format: formatScript
599
606
  };
600
607
  if (ctx.config.releaseStrategy === "changesets") allScripts["changeset"] = "changeset";
601
- if (ctx.config.releaseStrategy !== "none") allScripts["trigger-release"] = "pnpm exec tooling release:trigger";
608
+ if (ctx.config.releaseStrategy !== "none" && ctx.config.releaseStrategy !== "changesets") allScripts["trigger-release"] = "pnpm exec tooling release:trigger";
602
609
  const devDeps = { ...ROOT_DEV_DEPS };
603
610
  if (!isMonorepo) Object.assign(devDeps, PER_PACKAGE_DEV_DEPS);
604
611
  devDeps["@bensandee/config"] = isWorkspacePackage(ctx, "@bensandee/config") ? "workspace:*" : "0.7.1";
605
- devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.10.1";
612
+ devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.12.0";
606
613
  if (ctx.config.useEslintPlugin) devDeps["@bensandee/eslint-plugin"] = isWorkspacePackage(ctx, "@bensandee/eslint-plugin") ? "workspace:*" : "0.9.0";
607
614
  if (ctx.config.formatter === "oxfmt") devDeps["oxfmt"] = "0.35.0";
608
615
  if (ctx.config.formatter === "prettier") devDeps["prettier"] = "3.8.1";
@@ -623,6 +630,9 @@ async function generatePackageJson(ctx) {
623
630
  for (const [key, value] of Object.entries(allScripts)) if (!(key in existingScripts)) {
624
631
  existingScripts[key] = value;
625
632
  changes.push(`added script: ${key}`);
633
+ } else if (key in MANAGED_SCRIPTS && !existingScripts[key]?.includes(MANAGED_SCRIPTS[key])) {
634
+ existingScripts[key] = value;
635
+ changes.push(`updated script: ${key}`);
626
636
  }
627
637
  pkg.scripts = existingScripts;
628
638
  const existingDevDeps = pkg.devDependencies ?? {};
@@ -1193,6 +1203,7 @@ async function generateTsdown(ctx) {
1193
1203
  /** Entries that every project should have — repo:check flags these as missing. */
1194
1204
  const REQUIRED_ENTRIES = [
1195
1205
  "node_modules/",
1206
+ ".pnpm-store/",
1196
1207
  "dist/",
1197
1208
  "*.tsbuildinfo",
1198
1209
  ".env",
@@ -1249,6 +1260,12 @@ const FORGEJO_SCHEMA_COMMENT = "# yaml-language-server: $schema=../../.vscode/fo
1249
1260
  function workflowSchemaComment(ci) {
1250
1261
  return ci === "forgejo" ? FORGEJO_SCHEMA_COMMENT : "";
1251
1262
  }
1263
+ /** Prepend the Forgejo schema comment if it's not already present. No-op for GitHub. */
1264
+ function ensureSchemaComment(content, ci) {
1265
+ if (ci !== "forgejo") return content;
1266
+ if (content.includes("yaml-language-server")) return content;
1267
+ return FORGEJO_SCHEMA_COMMENT + content;
1268
+ }
1252
1269
  /** Check if a YAML file has an opt-out comment in the first 10 lines. */
1253
1270
  function isToolingIgnored(content) {
1254
1271
  return content.split("\n", 10).some((line) => line.includes(IGNORE_PATTERN));
@@ -1348,10 +1365,7 @@ function hasEnginesNode$1(ctx) {
1348
1365
  if (!raw) return false;
1349
1366
  return typeof parsePackageJson(raw)?.engines?.["node"] === "string";
1350
1367
  }
1351
- function ciWorkflow(isMonorepo, nodeVersionYaml, isForgejo) {
1352
- const buildCmd = isMonorepo ? "pnpm -r build" : "pnpm build";
1353
- const testCmd = isMonorepo ? "pnpm -r test" : "pnpm test";
1354
- const typecheckCmd = isMonorepo ? "pnpm -r --parallel run typecheck" : "pnpm typecheck";
1368
+ function ciWorkflow(nodeVersionYaml, isForgejo) {
1355
1369
  const emailNotifications = isForgejo ? "\nenable-email-notifications: true\n" : "";
1356
1370
  return `${workflowSchemaComment(isForgejo ? "forgejo" : "github")}name: CI
1357
1371
  ${emailNotifications}on:
@@ -1371,19 +1385,11 @@ jobs:
1371
1385
  ${nodeVersionYaml}
1372
1386
  cache: pnpm
1373
1387
  - run: pnpm install --frozen-lockfile
1374
- - run: ${typecheckCmd}
1375
- - run: pnpm lint
1376
- - run: ${buildCmd}
1377
- - run: ${testCmd}
1378
- - run: pnpm format --check
1379
- - run: pnpm knip
1380
- - run: pnpm exec tooling repo:check
1388
+ - name: Run all checks
1389
+ run: pnpm check
1381
1390
  `;
1382
1391
  }
1383
- function requiredCheckSteps(isMonorepo, nodeVersionYaml) {
1384
- const buildCmd = isMonorepo ? "pnpm -r build" : "pnpm build";
1385
- const testCmd = isMonorepo ? "pnpm -r test" : "pnpm test";
1386
- const typecheckCmd = isMonorepo ? "pnpm -r --parallel run typecheck" : "pnpm typecheck";
1392
+ function requiredCheckSteps(nodeVersionYaml) {
1387
1393
  return [
1388
1394
  {
1389
1395
  match: { uses: "actions/checkout" },
@@ -1408,32 +1414,11 @@ function requiredCheckSteps(isMonorepo, nodeVersionYaml) {
1408
1414
  step: { run: "pnpm install --frozen-lockfile" }
1409
1415
  },
1410
1416
  {
1411
- match: { run: "typecheck" },
1412
- step: { run: typecheckCmd }
1413
- },
1414
- {
1415
- match: { run: "lint" },
1416
- step: { run: "pnpm lint" }
1417
- },
1418
- {
1419
- match: { run: "build" },
1420
- step: { run: buildCmd }
1421
- },
1422
- {
1423
- match: { run: "test" },
1424
- step: { run: testCmd }
1425
- },
1426
- {
1427
- match: { run: "format" },
1428
- step: { run: "pnpm format --check" }
1429
- },
1430
- {
1431
- match: { run: "knip" },
1432
- step: { run: "pnpm knip" }
1433
- },
1434
- {
1435
- match: { run: "repo:check" },
1436
- step: { run: "pnpm exec tooling repo:check" }
1417
+ match: { run: "check" },
1418
+ step: {
1419
+ name: "Run all checks",
1420
+ run: "pnpm check"
1421
+ }
1437
1422
  }
1438
1423
  ];
1439
1424
  }
@@ -1443,17 +1428,17 @@ async function generateCi(ctx) {
1443
1428
  action: "skipped",
1444
1429
  description: "CI workflow not requested"
1445
1430
  };
1446
- const isMonorepo = ctx.config.structure === "monorepo";
1447
1431
  const isGitHub = ctx.config.ci === "github";
1448
1432
  const nodeVersionYaml = hasEnginesNode$1(ctx) ? "node-version-file: package.json" : "node-version: \"24\"";
1449
1433
  const filePath = isGitHub ? ".github/workflows/check.yml" : ".forgejo/workflows/check.yml";
1450
- const content = ciWorkflow(isMonorepo, nodeVersionYaml, !isGitHub);
1434
+ const content = ciWorkflow(nodeVersionYaml, !isGitHub);
1451
1435
  if (ctx.exists(filePath)) {
1452
1436
  const existing = ctx.read(filePath);
1453
1437
  if (existing) {
1454
- const merged = mergeWorkflowSteps(existing, "check", requiredCheckSteps(isMonorepo, nodeVersionYaml));
1455
- if (merged.changed) {
1456
- ctx.write(filePath, merged.content);
1438
+ const merged = mergeWorkflowSteps(existing, "check", requiredCheckSteps(nodeVersionYaml));
1439
+ const withComment = ensureSchemaComment(merged.content, isGitHub ? "github" : "forgejo");
1440
+ if (merged.changed || withComment !== merged.content) {
1441
+ ctx.write(filePath, withComment);
1457
1442
  return {
1458
1443
  filePath,
1459
1444
  action: "updated",
@@ -2135,8 +2120,9 @@ async function generateReleaseCi(ctx) {
2135
2120
  const existing = ctx.read(workflowPath);
2136
2121
  if (existing) {
2137
2122
  const merged = mergeWorkflowSteps(existing, "release", requiredReleaseSteps(ctx.config.releaseStrategy, nodeVersionYaml));
2138
- if (merged.changed) {
2139
- ctx.write(workflowPath, merged.content);
2123
+ const withComment = ensureSchemaComment(merged.content, ctx.config.ci);
2124
+ if (merged.changed || withComment !== merged.content) {
2125
+ ctx.write(workflowPath, withComment);
2140
2126
  return {
2141
2127
  filePath: workflowPath,
2142
2128
  action: "updated",
@@ -3424,24 +3410,136 @@ function mergeGitHub(dryRun) {
3424
3410
  p.log.info(`Merged changesets PR and deleted branch ${HEAD_BRANCH}`);
3425
3411
  }
3426
3412
  //#endregion
3413
+ //#region src/commands/repo-run-checks.ts
3414
+ const CHECKS = [
3415
+ {
3416
+ name: "build",
3417
+ cmd: "pnpm run --if-present build"
3418
+ },
3419
+ {
3420
+ name: "typecheck",
3421
+ cmd: "pnpm run --if-present typecheck"
3422
+ },
3423
+ {
3424
+ name: "lint",
3425
+ cmd: "pnpm run --if-present lint"
3426
+ },
3427
+ {
3428
+ name: "test",
3429
+ cmd: "pnpm run --if-present test"
3430
+ },
3431
+ {
3432
+ name: "format",
3433
+ cmd: "pnpm run --if-present format -- --check"
3434
+ },
3435
+ {
3436
+ name: "knip",
3437
+ cmd: "pnpm run --if-present knip"
3438
+ },
3439
+ {
3440
+ name: "tooling:check",
3441
+ cmd: "pnpm run --if-present tooling:check"
3442
+ },
3443
+ {
3444
+ name: "image:check",
3445
+ cmd: "pnpm run --if-present image:check"
3446
+ }
3447
+ ];
3448
+ function defaultExecCommand(cmd, cwd) {
3449
+ try {
3450
+ execSync(cmd, {
3451
+ cwd,
3452
+ stdio: "inherit"
3453
+ });
3454
+ return 0;
3455
+ } catch (err) {
3456
+ if (isExecSyncError(err)) return err.status;
3457
+ return 1;
3458
+ }
3459
+ }
3460
+ const ciLog = (msg) => console.log(msg);
3461
+ function runRunChecks(targetDir, options = {}) {
3462
+ const exec = options.execCommand ?? defaultExecCommand;
3463
+ const skip = options.skip ?? /* @__PURE__ */ new Set();
3464
+ const add = options.add ?? [];
3465
+ const isCI = Boolean(process.env["CI"]);
3466
+ const allChecks = [...CHECKS, ...add.map((name) => ({
3467
+ name,
3468
+ cmd: `pnpm run --if-present ${name}`
3469
+ }))];
3470
+ const failures = [];
3471
+ for (const check of allChecks) {
3472
+ if (skip.has(check.name)) {
3473
+ p.log.info(`${check.name} (skipped)`);
3474
+ continue;
3475
+ }
3476
+ if (isCI) ciLog(`::group::${check.name}`);
3477
+ const exitCode = exec(check.cmd, targetDir);
3478
+ if (isCI) ciLog("::endgroup::");
3479
+ if (exitCode === 0) p.log.success(check.name);
3480
+ else {
3481
+ if (isCI) ciLog(`::error::${check.name} failed`);
3482
+ p.log.error(`${check.name} failed`);
3483
+ failures.push(check.name);
3484
+ }
3485
+ }
3486
+ if (failures.length > 0) {
3487
+ p.log.error(`Failed checks: ${failures.join(", ")}`);
3488
+ return 1;
3489
+ }
3490
+ p.log.success("All checks passed");
3491
+ return 0;
3492
+ }
3493
+ const runChecksCommand = defineCommand({
3494
+ meta: {
3495
+ name: "repo:run-checks",
3496
+ description: "Run all standard checks (build, typecheck, lint, test, format, knip, tooling:check, image:check)"
3497
+ },
3498
+ args: {
3499
+ dir: {
3500
+ type: "positional",
3501
+ description: "Target directory (default: current directory)",
3502
+ required: false
3503
+ },
3504
+ skip: {
3505
+ type: "string",
3506
+ description: "Comma-separated list of checks to skip (build, typecheck, lint, test, format, knip, tooling:check, image:check)",
3507
+ required: false
3508
+ },
3509
+ add: {
3510
+ type: "string",
3511
+ description: "Comma-separated list of additional check names to run (uses pnpm run --if-present <name>)",
3512
+ required: false
3513
+ }
3514
+ },
3515
+ run({ args }) {
3516
+ const exitCode = runRunChecks(path.resolve(args.dir ?? "."), {
3517
+ skip: args.skip ? new Set(args.skip.split(",").map((s) => s.trim())) : void 0,
3518
+ add: args.add ? args.add.split(",").map((s) => s.trim()) : void 0
3519
+ });
3520
+ process.exitCode = exitCode;
3521
+ }
3522
+ });
3523
+ //#endregion
3427
3524
  //#region src/bin.ts
3428
3525
  const main = defineCommand({
3429
3526
  meta: {
3430
3527
  name: "tooling",
3431
- version: "0.10.1",
3528
+ version: "0.12.0",
3432
3529
  description: "Bootstrap and maintain standardized TypeScript project tooling"
3433
3530
  },
3434
3531
  subCommands: {
3435
3532
  "repo:init": initCommand,
3436
3533
  "repo:update": updateCommand,
3437
3534
  "repo:check": checkCommand,
3535
+ "repo:run-checks": runChecksCommand,
3438
3536
  "release:changesets": releaseForgejoCommand,
3439
3537
  "release:trigger": releaseTriggerCommand,
3440
3538
  "release:create-forgejo-release": createForgejoReleaseCommand,
3441
3539
  "release:merge": releaseMergeCommand
3442
3540
  }
3443
3541
  });
3444
- console.log(`@bensandee/tooling v0.10.1`);
3542
+ console.log(`@bensandee/tooling v0.12.0`);
3445
3543
  runMain(main);
3446
3544
  //#endregion
3447
3545
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bensandee/tooling",
3
- "version": "0.10.1",
3
+ "version": "0.12.0",
4
4
  "description": "CLI tool to bootstrap and maintain standardized TypeScript project tooling",
5
5
  "bin": {
6
6
  "tooling": "./dist/bin.mjs"