@bensandee/tooling 0.63.0 → 0.64.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.
- package/dist/bin.mjs +203 -21
- package/package.json +1 -1
package/dist/bin.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import path from "node:path";
|
|
|
7
7
|
import { appendFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
8
8
|
import { execFileSync, execSync } from "node:child_process";
|
|
9
9
|
import JSON5 from "json5";
|
|
10
|
-
import { parse } from "jsonc-parser";
|
|
10
|
+
import { applyEdits, modify, parse } from "jsonc-parser";
|
|
11
11
|
import { z } from "zod";
|
|
12
12
|
import { FatalError, TransientError, UnexpectedError } from "@bensandee/common";
|
|
13
13
|
import { isMap, isScalar, isSeq, parse as parse$1, parseDocument, stringify, visit } from "yaml";
|
|
@@ -2006,23 +2006,149 @@ function diagnoseCheckScriptForwardingAt(targetDir) {
|
|
|
2006
2006
|
//#endregion
|
|
2007
2007
|
//#region src/generators/package-json.ts
|
|
2008
2008
|
/**
|
|
2009
|
-
*
|
|
2009
|
+
* Single source of truth for the supported Node floor. `engines.node` is the
|
|
2010
|
+
* honest minimum (`>=<floor>`); `devEngines.runtime.version` is the caret form
|
|
2011
|
+
* (`^<floor>` = `>=floor <nextmajor>`), which serves two masters at once:
|
|
2010
2012
|
*
|
|
2011
|
-
* `actions/setup-node` reads `devEngines.runtime` (for `name: "node"`) before
|
|
2012
|
-
*
|
|
2013
|
-
*
|
|
2014
|
-
*
|
|
2015
|
-
* `
|
|
2016
|
-
*
|
|
2013
|
+
* - `actions/setup-node` reads `devEngines.runtime` (for `name: "node"`) before
|
|
2014
|
+
* `engines.node`. The `<nextmajor>` upper bound keeps it on the pinned LTS
|
|
2015
|
+
* major (resolving to the latest patch within it) instead of floating to the
|
|
2016
|
+
* newest released major (e.g. Node 26) the way a bare `>=` range would.
|
|
2017
|
+
* - pnpm resolves `devEngines.runtime.version` to its floor and checks it
|
|
2018
|
+
* against `engines.node`. The caret's floor equals the engines floor, so the
|
|
2019
|
+
* two never contradict — a `<major>.x` floor of `<major>.0.0` sits below
|
|
2020
|
+
* `>=<floor>` and makes `pnpm install` emit a spurious "Unsupported engine"
|
|
2021
|
+
* warning. The floor MUST track `engines.node`, so it is derived from it
|
|
2022
|
+
* rather than hard-coded.
|
|
2023
|
+
*
|
|
2024
|
+
* `onFail: "warn"` keeps a local Node mismatch non-fatal.
|
|
2017
2025
|
*/
|
|
2018
|
-
const
|
|
2026
|
+
const NODE_ENGINES_FLOOR = "24.13.0";
|
|
2027
|
+
const ENGINES_NODE = `>=${NODE_ENGINES_FLOOR}`;
|
|
2028
|
+
/** The `^<floor>` runtime range whose floor satisfies a `>=<floor>` engines range. */
|
|
2029
|
+
function caretRange(floor) {
|
|
2030
|
+
return `^${floor}`;
|
|
2031
|
+
}
|
|
2019
2032
|
function devEnginesRuntime() {
|
|
2020
2033
|
return { runtime: {
|
|
2021
2034
|
name: "node",
|
|
2022
|
-
version:
|
|
2035
|
+
version: caretRange(NODE_ENGINES_FLOOR),
|
|
2023
2036
|
onFail: "warn"
|
|
2024
2037
|
} };
|
|
2025
2038
|
}
|
|
2039
|
+
function versionPart(s) {
|
|
2040
|
+
return s && s !== "x" && s !== "*" ? Number(s) : 0;
|
|
2041
|
+
}
|
|
2042
|
+
function compareTuples(a, b) {
|
|
2043
|
+
for (let i = 0; i < 3; i++) if (a[i] !== b[i]) return a[i] - b[i];
|
|
2044
|
+
return 0;
|
|
2045
|
+
}
|
|
2046
|
+
/**
|
|
2047
|
+
* Effective lower bound of a single conjunction (space-separated comparators like
|
|
2048
|
+
* `>=24.14.0 <25.0.0`). Upper bounds (`<`, `<=`) don't raise the floor and are
|
|
2049
|
+
* ignored; the floor is the **max** of the remaining lower-bound tokens (`>=`,
|
|
2050
|
+
* `>`, `^`, `~`, `=`, bare, `<major>.x`). Missing/wildcard positions read as 0.
|
|
2051
|
+
* `undefined` when no token carries a numeric lower bound.
|
|
2052
|
+
*/
|
|
2053
|
+
function conjunctionFloor(conjunction) {
|
|
2054
|
+
let floor;
|
|
2055
|
+
for (const token of conjunction.trim().split(/\s+/)) {
|
|
2056
|
+
if (!token || token.startsWith("<")) continue;
|
|
2057
|
+
const match = /(\d+)(?:\.(\d+|x|\*))?(?:\.(\d+|x|\*))?/.exec(token);
|
|
2058
|
+
if (!match) continue;
|
|
2059
|
+
const candidate = [
|
|
2060
|
+
Number(match[1]),
|
|
2061
|
+
versionPart(match[2]),
|
|
2062
|
+
versionPart(match[3])
|
|
2063
|
+
];
|
|
2064
|
+
if (!floor || compareTuples(candidate, floor) > 0) floor = candidate;
|
|
2065
|
+
}
|
|
2066
|
+
return floor;
|
|
2067
|
+
}
|
|
2068
|
+
/**
|
|
2069
|
+
* Lowest version a semver range permits, as a `[major, minor, patch]` tuple —
|
|
2070
|
+
* what pnpm resolves a `devEngines.runtime.version` to when cross-checking
|
|
2071
|
+
* `engines.node`. Handles floor-first forms (`>=24.14.0`, `^24.13.0`, `24.x`,
|
|
2072
|
+
* `24`), compound ranges in any order (`>=24.14.0 <25.0.0`, `<25 >=24.14.0` →
|
|
2073
|
+
* `24.14.0`), and `||` alternation (the minimum across alternatives).
|
|
2074
|
+
*
|
|
2075
|
+
* Returns `undefined` when no floor can be determined (e.g. `*`, `latest`, an
|
|
2076
|
+
* alternative with only an upper bound) — callers must warn rather than guess.
|
|
2077
|
+
*/
|
|
2078
|
+
function minVersionTuple(range) {
|
|
2079
|
+
let min;
|
|
2080
|
+
for (const alternative of range.split("||")) {
|
|
2081
|
+
const floor = conjunctionFloor(alternative);
|
|
2082
|
+
if (!floor) return void 0;
|
|
2083
|
+
if (!min || compareTuples(floor, min) < 0) min = floor;
|
|
2084
|
+
}
|
|
2085
|
+
return min;
|
|
2086
|
+
}
|
|
2087
|
+
/** True when `range`'s floor is >= `enginesNode`'s floor (i.e. pnpm won't warn). */
|
|
2088
|
+
function runtimeFloorSatisfies(range, enginesNode) {
|
|
2089
|
+
const dev = minVersionTuple(range);
|
|
2090
|
+
const eng = minVersionTuple(enginesNode);
|
|
2091
|
+
if (!dev || !eng) return true;
|
|
2092
|
+
return compareTuples(dev, eng) >= 0;
|
|
2093
|
+
}
|
|
2094
|
+
/**
|
|
2095
|
+
* Decide how to reconcile a package's `devEngines.runtime` (node entries) with
|
|
2096
|
+
* its `engines.node` floor, without mutating anything. The caret form `^<floor>`
|
|
2097
|
+
* keeps pnpm quiet (its floor equals the engines floor) while staying bounded to
|
|
2098
|
+
* the same major for `setup-node`.
|
|
2099
|
+
*
|
|
2100
|
+
* - `noop` — no node runtime declared, or every node runtime already satisfies
|
|
2101
|
+
* the engines floor (a deliberate higher pin is left alone). Never adds a
|
|
2102
|
+
* `devEngines.runtime` that isn't already there.
|
|
2103
|
+
* - `warn` — a node runtime is declared but `engines.node` has no determinable
|
|
2104
|
+
* floor; leave it unchanged rather than guess.
|
|
2105
|
+
* - `fix` — `staleIndices` index the node runtimes (in array order, or `[0]`
|
|
2106
|
+
* for the single-object form) whose `version` must become `desired`.
|
|
2107
|
+
*/
|
|
2108
|
+
function planDevEnginesFix(pkg, enginesNode) {
|
|
2109
|
+
const runtimeField = pkg["devEngines"]?.runtime;
|
|
2110
|
+
if (!runtimeField) return { kind: "noop" };
|
|
2111
|
+
const nodeEntries = (Array.isArray(runtimeField) ? runtimeField : [runtimeField]).map((runtime, index) => ({
|
|
2112
|
+
runtime,
|
|
2113
|
+
index
|
|
2114
|
+
})).filter(({ runtime }) => runtime.name === "node" && typeof runtime.version === "string");
|
|
2115
|
+
if (nodeEntries.length === 0) return { kind: "noop" };
|
|
2116
|
+
const floor = minVersionTuple(enginesNode);
|
|
2117
|
+
if (!floor) return {
|
|
2118
|
+
kind: "warn",
|
|
2119
|
+
message: `could not determine a floor from engines.node "${enginesNode}"`
|
|
2120
|
+
};
|
|
2121
|
+
const desired = caretRange(`${floor[0]}.${floor[1]}.${floor[2]}`);
|
|
2122
|
+
const staleIndices = nodeEntries.filter(({ runtime }) => !runtimeFloorSatisfies(runtime.version ?? "", enginesNode)).map(({ index }) => index);
|
|
2123
|
+
if (staleIndices.length === 0) return { kind: "noop" };
|
|
2124
|
+
return {
|
|
2125
|
+
kind: "fix",
|
|
2126
|
+
desired,
|
|
2127
|
+
staleIndices
|
|
2128
|
+
};
|
|
2129
|
+
}
|
|
2130
|
+
/**
|
|
2131
|
+
* Correct an existing `devEngines.runtime` (node) in place so its floor matches
|
|
2132
|
+
* `engines.node` — only the `version` changes; `name` and `onFail` are
|
|
2133
|
+
* preserved. Never adds `devEngines.runtime` to a package that lacks it.
|
|
2134
|
+
*/
|
|
2135
|
+
function ensureDevEnginesConsistent(pkg, enginesNode, changes) {
|
|
2136
|
+
const plan = planDevEnginesFix(pkg, enginesNode);
|
|
2137
|
+
if (plan.kind === "warn") {
|
|
2138
|
+
log.warn(`package.json: ${plan.message}; leaving devEngines.runtime unchanged`);
|
|
2139
|
+
return;
|
|
2140
|
+
}
|
|
2141
|
+
if (plan.kind !== "fix") return;
|
|
2142
|
+
const runtimeField = pkg["devEngines"]?.runtime;
|
|
2143
|
+
const runtimes = Array.isArray(runtimeField) ? runtimeField : runtimeField ? [runtimeField] : [];
|
|
2144
|
+
for (const index of plan.staleIndices) {
|
|
2145
|
+
const runtime = runtimes[index];
|
|
2146
|
+
if (!runtime) continue;
|
|
2147
|
+
const previous = runtime.version;
|
|
2148
|
+
runtime.version = plan.desired;
|
|
2149
|
+
changes.push(`bumped devEngines.runtime node ${previous} → ${plan.desired}`);
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2026
2152
|
const STANDARD_SCRIPTS_SINGLE = {
|
|
2027
2153
|
build: "tsdown",
|
|
2028
2154
|
dev: "tsdown --watch",
|
|
@@ -2332,13 +2458,11 @@ function applyMetadataDefaults(pkg, ctx, changes) {
|
|
|
2332
2458
|
}
|
|
2333
2459
|
}
|
|
2334
2460
|
if (!pkg["engines"]) {
|
|
2335
|
-
pkg["engines"] = { node:
|
|
2336
|
-
changes.push(
|
|
2337
|
-
}
|
|
2338
|
-
if (!pkg["devEngines"]) {
|
|
2339
|
-
pkg["devEngines"] = devEnginesRuntime();
|
|
2340
|
-
changes.push(`set devEngines.runtime node ${DEV_NODE_VERSION}`);
|
|
2461
|
+
pkg["engines"] = { node: ENGINES_NODE };
|
|
2462
|
+
changes.push(`set engines.node ${ENGINES_NODE}`);
|
|
2341
2463
|
}
|
|
2464
|
+
const enginesNode = pkg["engines"]?.["node"];
|
|
2465
|
+
if (typeof enginesNode === "string") ensureDevEnginesConsistent(pkg, enginesNode, changes);
|
|
2342
2466
|
const pinnedPnpm = {
|
|
2343
2467
|
"@changesets/cli": "2.31.0",
|
|
2344
2468
|
"@release-it/bumper": "7.0.6",
|
|
@@ -2392,7 +2516,7 @@ function buildManagedDevDeps(ctx) {
|
|
|
2392
2516
|
const devDeps = { ...ROOT_DEV_DEPS };
|
|
2393
2517
|
if (!isMonorepo) Object.assign(devDeps, PER_PACKAGE_DEV_DEPS);
|
|
2394
2518
|
devDeps["@bensandee/config"] = isWorkspacePackage(ctx, "@bensandee/config") ? "workspace:*" : "0.12.0";
|
|
2395
|
-
if (!isWorkspacePackage(ctx, "@bensandee/tooling")) devDeps["@bensandee/tooling"] = "0.
|
|
2519
|
+
if (!isWorkspacePackage(ctx, "@bensandee/tooling")) devDeps["@bensandee/tooling"] = "0.64.0";
|
|
2396
2520
|
if (ctx.config.useEslintPlugin) devDeps["@bensandee/eslint-plugin"] = isWorkspacePackage(ctx, "@bensandee/eslint-plugin") ? "workspace:*" : "0.13.1";
|
|
2397
2521
|
if (ctx.config.formatter === "oxfmt") devDeps["oxfmt"] = {
|
|
2398
2522
|
"@changesets/cli": "2.31.0",
|
|
@@ -2472,7 +2596,7 @@ async function generatePackageJson(ctx) {
|
|
|
2472
2596
|
type: "module",
|
|
2473
2597
|
scripts: allScripts,
|
|
2474
2598
|
devDependencies: devDeps,
|
|
2475
|
-
engines: { node:
|
|
2599
|
+
engines: { node: ENGINES_NODE },
|
|
2476
2600
|
devEngines: devEnginesRuntime(),
|
|
2477
2601
|
packageManager: `pnpm@${{
|
|
2478
2602
|
"@changesets/cli": "2.31.0",
|
|
@@ -2498,6 +2622,63 @@ async function generatePackageJson(ctx) {
|
|
|
2498
2622
|
description: "Generated package.json"
|
|
2499
2623
|
};
|
|
2500
2624
|
}
|
|
2625
|
+
/**
|
|
2626
|
+
* Reconcile `devEngines.runtime` against `engines.node` in nested workspace
|
|
2627
|
+
* packages — pnpm's "Unsupported engine" warning fires per-package, so a
|
|
2628
|
+
* subpackage that declares both fields can warn even when the root is clean.
|
|
2629
|
+
*
|
|
2630
|
+
* Unlike the root `package.json` (a fully managed file), a subpackage's
|
|
2631
|
+
* `package.json` is otherwise owned by the user, so this edits *only* the stale
|
|
2632
|
+
* `version` string via `jsonc-parser` rather than reserialising the whole file
|
|
2633
|
+
* (which would reorder keys and produce a large surprising diff). It never adds
|
|
2634
|
+
* `engines.node` or `devEngines.runtime` — it touches a package only when both
|
|
2635
|
+
* already exist and a node runtime's floor is below the engines floor.
|
|
2636
|
+
*
|
|
2637
|
+
* Scans `packages/*` (the same convention as the rest of the tool); other
|
|
2638
|
+
* workspace layouts (`apps/*`, custom globs) are out of scope.
|
|
2639
|
+
*/
|
|
2640
|
+
async function generateWorkspaceEngines(ctx) {
|
|
2641
|
+
if (ctx.config.structure !== "monorepo") return [];
|
|
2642
|
+
const results = [];
|
|
2643
|
+
for (const member of getMonorepoPackages(ctx.targetDir)) {
|
|
2644
|
+
const result = reconcileWorkspaceDevEngines(ctx, path.join(path.relative(ctx.targetDir, member.dir), "package.json"));
|
|
2645
|
+
if (result) results.push(result);
|
|
2646
|
+
}
|
|
2647
|
+
return results;
|
|
2648
|
+
}
|
|
2649
|
+
/** Surgically fix a single nested package.json's devEngines, preserving its formatting. */
|
|
2650
|
+
function reconcileWorkspaceDevEngines(ctx, relPath) {
|
|
2651
|
+
const raw = ctx.read(relPath);
|
|
2652
|
+
if (!raw) return void 0;
|
|
2653
|
+
const pkg = parsePackageJson(raw);
|
|
2654
|
+
if (!pkg) return void 0;
|
|
2655
|
+
const enginesNode = pkg["engines"]?.["node"];
|
|
2656
|
+
if (typeof enginesNode !== "string") return void 0;
|
|
2657
|
+
const plan = planDevEnginesFix(pkg, enginesNode);
|
|
2658
|
+
if (plan.kind === "warn") {
|
|
2659
|
+
log.warn(`${relPath}: ${plan.message}; leaving devEngines.runtime unchanged`);
|
|
2660
|
+
return;
|
|
2661
|
+
}
|
|
2662
|
+
if (plan.kind !== "fix") return void 0;
|
|
2663
|
+
const isArray = Array.isArray(pkg["devEngines"]?.runtime);
|
|
2664
|
+
let text = raw;
|
|
2665
|
+
for (const index of plan.staleIndices) text = applyEdits(text, modify(text, isArray ? [
|
|
2666
|
+
"devEngines",
|
|
2667
|
+
"runtime",
|
|
2668
|
+
index,
|
|
2669
|
+
"version"
|
|
2670
|
+
] : [
|
|
2671
|
+
"devEngines",
|
|
2672
|
+
"runtime",
|
|
2673
|
+
"version"
|
|
2674
|
+
], plan.desired, {}));
|
|
2675
|
+
ctx.write(relPath, text);
|
|
2676
|
+
return {
|
|
2677
|
+
filePath: relPath,
|
|
2678
|
+
action: "updated",
|
|
2679
|
+
description: `bumped devEngines.runtime node → ${plan.desired}`
|
|
2680
|
+
};
|
|
2681
|
+
}
|
|
2501
2682
|
//#endregion
|
|
2502
2683
|
//#region src/generators/tsconfig.ts
|
|
2503
2684
|
async function generateTsconfig(ctx) {
|
|
@@ -5701,6 +5882,7 @@ async function generateVersionrc(ctx) {
|
|
|
5701
5882
|
async function runGenerators(ctx) {
|
|
5702
5883
|
const results = [];
|
|
5703
5884
|
results.push(await generatePackageJson(ctx));
|
|
5885
|
+
results.push(...await generateWorkspaceEngines(ctx));
|
|
5704
5886
|
results.push(await generatePnpmWorkspace(ctx));
|
|
5705
5887
|
results.push(...await generateTsconfig(ctx));
|
|
5706
5888
|
results.push(await generateTsdown(ctx));
|
|
@@ -5734,7 +5916,7 @@ async function runGenerators(ctx) {
|
|
|
5734
5916
|
function generateMigratePrompt(results, config, detected) {
|
|
5735
5917
|
const sections = [];
|
|
5736
5918
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
5737
|
-
sections.push(`# Migration Prompt\n\n_Generated by \`@bensandee/tooling@0.
|
|
5919
|
+
sections.push(`# Migration Prompt\n\n_Generated by \`@bensandee/tooling@0.64.0 repo:sync\` on ${timestamp}_\n`);
|
|
5738
5920
|
const changed = results.filter((r) => r.action === "created" || r.action === "updated" || r.action === "archived");
|
|
5739
5921
|
if (changed.length > 0) {
|
|
5740
5922
|
const items = changed.map((r) => {
|
|
@@ -10214,7 +10396,7 @@ const out = (msg) => console.log(msg);
|
|
|
10214
10396
|
const main = defineCommand({
|
|
10215
10397
|
meta: {
|
|
10216
10398
|
name: "bst",
|
|
10217
|
-
version: "0.
|
|
10399
|
+
version: "0.64.0",
|
|
10218
10400
|
description: "Bootstrap and maintain standardized TypeScript project tooling"
|
|
10219
10401
|
},
|
|
10220
10402
|
subCommands: {
|
|
@@ -10286,7 +10468,7 @@ const main = defineCommand({
|
|
|
10286
10468
|
})
|
|
10287
10469
|
}
|
|
10288
10470
|
});
|
|
10289
|
-
console.log(`@bensandee/tooling v0.
|
|
10471
|
+
console.log(`@bensandee/tooling v0.64.0`);
|
|
10290
10472
|
async function run() {
|
|
10291
10473
|
await runMain(main);
|
|
10292
10474
|
process.exit(process.exitCode ?? 0);
|