@hublo/sentinel 1.5.0-alpha.9 → 1.5.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/README.md +9 -7
- package/dist/bin/sentinel.js +61 -16
- package/dist/{chunk-KLEXJFJ6.js → chunk-43HQ6BVJ.js} +61 -15
- package/dist/{chunk-YQRDKWSH.js → chunk-IL5PPLXE.js} +11 -1
- package/dist/chunk-IL5PPLXE.js.map +1 -0
- package/dist/chunk-MOELAQPC.js +10365 -0
- package/dist/chunk-WBDBRGLV.js +214 -0
- package/dist/chunk-WBDBRGLV.js.map +1 -0
- package/dist/{chunk-AMCGMLPS.js → chunk-Y57KA4QS.js} +2 -2
- package/dist/index.d.ts +28 -1
- package/dist/index.js +4 -4
- package/dist/roles/build/nest/toolchain.js +68 -18
- package/dist/roles/build/nest/toolchain.js.map +1 -1
- package/dist/roles/static-analysis/config.d.ts +15 -1
- package/dist/roles/static-analysis/config.js +1 -1
- package/dist/roles/test/setup/jest-parity.js +2 -2
- package/dist/roles/test/setup/workspace-entry.js +2 -2
- package/dist/{validate-AWWMBSH5.js → validate-FRMENKHM.js} +2 -2
- package/docs/bundle-analysis-spec.md +272 -0
- package/docs/sql-verification-study.md +401 -0
- package/docs/static-analysis-adoption.md +285 -0
- package/docs/static-analysis-study.md +824 -18
- package/docs/typescript-adoption.md +1 -1
- package/docs/typescript-traces.md +27 -12
- package/docs/using-sentinel.md +22 -12
- package/docs/validating-a-change.md +185 -0
- package/lint/nest.json +98 -18
- package/lint/node.json +83 -3
- package/lint/react-lib.json +121 -31
- package/lint/react.json +101 -2
- package/package.json +27 -6
- package/plugins/hublo/no-palette-common.js +76 -0
- package/plugins/hublo/no-regex-lookbehind.js +70 -0
- package/plugins/hublo/require-reload-document.js +70 -0
- package/plugins/hublo.js +20 -145
- package/plugins/sonar-cognitive.js +108 -0
- package/dist/chunk-EDFIVLUA.js +0 -100
- package/dist/chunk-EDFIVLUA.js.map +0 -1
- package/dist/chunk-OZZICSSZ.js +0 -6469
- package/dist/chunk-YQRDKWSH.js.map +0 -1
- /package/dist/{chunk-AMCGMLPS.js.map → chunk-Y57KA4QS.js.map} +0 -0
package/README.md
CHANGED
|
@@ -137,7 +137,7 @@ The per-tool knowledge (eslint → `eslint.config.js`, tsc → `tsconfig`, …)
|
|
|
137
137
|
|
|
138
138
|
**Registry: public npm**, under `@hublo`. We started on **GitHub Packages (private)** and moved off it: it authenticates every consumer, including a one-off run, which is incompatible with the zero-setup adoption above. Public npm needs no consumer auth, so a module adopts sentinel without any `.npmrc` or token. Releases go out through the repo's `publish` workflow, never from a laptop; a prerelease is published under its prerelease dist-tag (`alpha`) and a stable one under `latest`.
|
|
139
139
|
|
|
140
|
-
**Node.** sentinel needs **Node >=
|
|
140
|
+
**Node.** sentinel needs **Node >= 24.15**, which is the version the monorepo it serves pins in its own `.node-version`. The floor follows the consumer deliberately: a lower one means testing a runtime nobody runs, and it twice let an API through that the types allowed and the runtime did not. It fails fast with a clear message on an older runtime rather than crashing. If a project runs on an older Node (e.g. a legacy app on Node 10), run sentinel with a modern Node via `fnm`/`nvm`; you do not need to change the project's own Node.
|
|
141
141
|
|
|
142
142
|
**Try it without installing.** A one-off run needs no auth and touches nothing:
|
|
143
143
|
|
|
@@ -171,19 +171,21 @@ throwaway run wants. Once a module has adopted, nothing is fetched any more: the
|
|
|
171
171
|
- [`docs/build-adoption.md`](docs/build-adoption.md) — moving a React app's Vite toolchain into sentinel: adoption changes the config's IMPORTS and nothing else, what sentinel owns versus what stays the app's, and why reading nothing is what makes losing nothing a guarantee.
|
|
172
172
|
- [`docs/typescript-adoption.md`](docs/typescript-adoption.md) — the adoption cheat sheet: the two adoption steps, the command model (verb x type x location), options, reading a report, and troubleshooting.
|
|
173
173
|
- [`docs/test-adoption.md`](docs/test-adoption.md) — migrating a module from jest to Vitest: the two steps, what the migration rewrites and what it refuses BY NAME, how the baseline gate proves nothing was lost, and the failures worth recognising.
|
|
174
|
+
- [`docs/static-analysis-adoption.md`](docs/static-analysis-adoption.md) — running the static analysis role on a module: the two steps, the four output shapes, how to read each row and where its threshold comes from, declaring an entry point, and the two limits worth knowing before trusting a number.
|
|
174
175
|
- [`docs/typescript-traces.md`](docs/typescript-traces.md) — a **generated, versioned** reference of live command + output traces (every verb, option, config result and edge case) against the mock monorepo. Regenerate after CLI changes with `pnpm docs:traces`.
|
|
175
176
|
|
|
176
177
|
## Architecture: `target → runner → preset`
|
|
177
178
|
|
|
178
179
|
Every check is described by three layers:
|
|
179
180
|
|
|
180
|
-
| Layer | Flag | What it is
|
|
181
|
-
| -------------------- | --------------------------- |
|
|
182
|
-
| **target** (role) | `--lint`, `--typescript`, … | the _kind_ of check, stable
|
|
183
|
-
| **runner** (adapter) | `--runner=<tool>` | the _tool_ behind the target, swappable
|
|
184
|
-
| **
|
|
181
|
+
| Layer | Flag | What it is | Examples |
|
|
182
|
+
| -------------------- | --------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
183
|
+
| **target** (role) | `--lint`, `--typescript`, … | the _kind_ of check, stable | `lint` `format` `typescript` `build` `dev` `test` `static-analysis` `runtime-analysis` |
|
|
184
|
+
| **runner** (adapter) | `--runner=<tool>` | the _tool_ behind the target, swappable | lint: `eslint`/`biome`/`oxlint` · types: `tsc`/`tsgo` · build: `vite` · test: `vitest` |
|
|
185
|
+
| **shape** (output) | `--output=<shape>` | what a reporting role WRITES, not what it measures | `console` (default, writes nothing) `html` `json` `markdown`; roles that do not report ignore it |
|
|
186
|
+
| **preset** (preset) | detected / `--preset` | the _variant_ per stack (strict by default) | `react` `nest` `node` `svelte` (svelte: lint + format; no tsconfig preset yet, so the TypeScript role stands aside on those modules and the other two adopt) |
|
|
185
187
|
|
|
186
|
-
A run is `target × runner × preset`, e.g. `sentinel --run --lint --runner=eslint` from the `host-admin` dir.
|
|
188
|
+
A run is `target × runner × preset`, e.g. `sentinel --run --lint --runner=eslint` from the `host-admin` dir. The shape is not part of that identity: it decides what the run WRITES, not what it does.
|
|
187
189
|
|
|
188
190
|
- `--runner` is an **optional override on a central default**. `sentinel --lint` uses the configured default runner, so swapping a tool globally is a one-place change; `--runner=biome` overrides for a single run (great for benchmarking eslint vs biome vs oxlint, and for gradual migration).
|
|
189
191
|
- The **preset is detected** from the module's dependencies (deterministic: a framework dep → its preset, else `node`), and **`--preset` overrides it**. Detection can be wrong where deps are hoisted at the repo root (it returns `node`), so pass `--preset` for `--init` (that is where the preset is chosen). `--run`/`--report` don't depend on it, they run the tool on the committed config.
|
package/dist/bin/sentinel.js
CHANGED
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
LONG_RUNNING_TARGETS,
|
|
4
|
-
PRESET_NAMES,
|
|
5
3
|
PresetUnsupportedError,
|
|
6
|
-
SWEEPABLE_TARGETS,
|
|
7
|
-
TARGETS,
|
|
8
|
-
VERBS,
|
|
9
4
|
availableTargets,
|
|
10
5
|
buildConfigFile,
|
|
11
6
|
correctingSignal,
|
|
@@ -15,18 +10,25 @@ import {
|
|
|
15
10
|
palette,
|
|
16
11
|
registerAdapters,
|
|
17
12
|
resolve
|
|
18
|
-
} from "../chunk-
|
|
13
|
+
} from "../chunk-MOELAQPC.js";
|
|
19
14
|
import {
|
|
20
15
|
resolveContext
|
|
21
|
-
} from "../chunk-
|
|
16
|
+
} from "../chunk-43HQ6BVJ.js";
|
|
22
17
|
import {
|
|
23
18
|
ensureWorkspacePrep,
|
|
24
19
|
findWorkspaceRoot,
|
|
25
20
|
inspectWorkspacePrep,
|
|
26
21
|
readOwnVersion,
|
|
27
22
|
readProjectPackageJson
|
|
28
|
-
} from "../chunk-
|
|
29
|
-
import
|
|
23
|
+
} from "../chunk-IL5PPLXE.js";
|
|
24
|
+
import {
|
|
25
|
+
LONG_RUNNING_TARGETS,
|
|
26
|
+
OUTPUT_SHAPES,
|
|
27
|
+
PRESET_NAMES,
|
|
28
|
+
SWEEPABLE_TARGETS,
|
|
29
|
+
TARGETS,
|
|
30
|
+
VERBS
|
|
31
|
+
} from "../chunk-WBDBRGLV.js";
|
|
30
32
|
import "../chunk-O7REVMOC.js";
|
|
31
33
|
import "../chunk-QXFCZON7.js";
|
|
32
34
|
|
|
@@ -79,6 +81,10 @@ async function runInit(ctx) {
|
|
|
79
81
|
}
|
|
80
82
|
}
|
|
81
83
|
const toRun = ctx.targets.filter((type) => available.includes(type)).sort((a, b) => Number(a === "format") - Number(b === "format"));
|
|
84
|
+
const outcomes = [];
|
|
85
|
+
const record = (outcome) => {
|
|
86
|
+
outcomes.push(outcome);
|
|
87
|
+
};
|
|
82
88
|
let worst = 0;
|
|
83
89
|
for (const type of toRun) {
|
|
84
90
|
try {
|
|
@@ -89,7 +95,8 @@ async function runInit(ctx) {
|
|
|
89
95
|
cwd: module.root,
|
|
90
96
|
preset: ctx.preset,
|
|
91
97
|
dryRun: ctx.dryRun,
|
|
92
|
-
json: ctx.json
|
|
98
|
+
json: ctx.json,
|
|
99
|
+
record
|
|
93
100
|
});
|
|
94
101
|
worst = Math.max(worst, code);
|
|
95
102
|
} catch (error) {
|
|
@@ -103,14 +110,21 @@ sentinel (${type}): this module's preset is "${error.preset}", and --${type} has
|
|
|
103
110
|
);
|
|
104
111
|
worst = Math.max(worst, 1);
|
|
105
112
|
}
|
|
113
|
+
record({
|
|
114
|
+
role: type,
|
|
115
|
+
status: "unsupported",
|
|
116
|
+
reason: `this module's preset is "${error.preset}", and --${type} has no adapter for it.`
|
|
117
|
+
});
|
|
106
118
|
continue;
|
|
107
119
|
}
|
|
108
120
|
process.stderr.write(`
|
|
109
121
|
sentinel (${type}): ${asMessage(error)}
|
|
110
122
|
`);
|
|
123
|
+
record({ role: type, status: "failed", reason: asMessage(error) });
|
|
111
124
|
worst = Math.max(worst, 1);
|
|
112
125
|
}
|
|
113
126
|
}
|
|
127
|
+
let workspace = [];
|
|
114
128
|
if (worst === 0) {
|
|
115
129
|
const root = findWorkspaceRoot(module.root);
|
|
116
130
|
if (root) {
|
|
@@ -123,6 +137,7 @@ sentinel (${type}): ${asMessage(error)}
|
|
|
123
137
|
// Same rule for the report directory: only when the STATIC ANALYSIS role adopted.
|
|
124
138
|
analysedModule: toRun.includes("static-analysis")
|
|
125
139
|
});
|
|
140
|
+
workspace = changes;
|
|
126
141
|
const prefix = ctx.dryRun ? " dry run: " : " ";
|
|
127
142
|
for (const change of changes) process.stderr.write(`${prefix}${change}
|
|
128
143
|
`);
|
|
@@ -133,6 +148,15 @@ sentinel (${type}): ${asMessage(error)}
|
|
|
133
148
|
}
|
|
134
149
|
}
|
|
135
150
|
}
|
|
151
|
+
if (ctx.json) {
|
|
152
|
+
process.stdout.write(
|
|
153
|
+
JSON.stringify(
|
|
154
|
+
{ dryRun: ctx.dryRun, module: module.name, roles: outcomes, workspace },
|
|
155
|
+
null,
|
|
156
|
+
2
|
|
157
|
+
) + "\n"
|
|
158
|
+
);
|
|
159
|
+
}
|
|
136
160
|
return worst;
|
|
137
161
|
}
|
|
138
162
|
|
|
@@ -148,7 +172,7 @@ async function runValidate(ctx) {
|
|
|
148
172
|
);
|
|
149
173
|
}
|
|
150
174
|
if (!ctx.targets.includes("test")) return 1;
|
|
151
|
-
const { validateModuleTests } = await import("../validate-
|
|
175
|
+
const { validateModuleTests } = await import("../validate-FRMENKHM.js");
|
|
152
176
|
const outcomes = modules.flatMap(
|
|
153
177
|
(module) => validateModuleTests(module.name, module.root)
|
|
154
178
|
);
|
|
@@ -549,7 +573,7 @@ async function runVerb(ctx) {
|
|
|
549
573
|
}
|
|
550
574
|
|
|
551
575
|
// src/shared/node-version.ts
|
|
552
|
-
var MIN_NODE = "
|
|
576
|
+
var MIN_NODE = "24.15.0";
|
|
553
577
|
function parts(version) {
|
|
554
578
|
const [major = 0, minor = 0, patch = 0] = version.replace(/^v/, "").split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
555
579
|
return [major, minor, patch];
|
|
@@ -583,8 +607,8 @@ var TARGET_HELP = {
|
|
|
583
607
|
build: "build",
|
|
584
608
|
dev: "dev server (long-running: never swept by an unqualified --run)",
|
|
585
609
|
test: "tests",
|
|
586
|
-
"static-analysis": "
|
|
587
|
-
"runtime-analysis": "
|
|
610
|
+
"static-analysis": "duplication, dead code, complexity, import graph, file size, bundle weight",
|
|
611
|
+
"runtime-analysis": "Lighthouse, web vitals",
|
|
588
612
|
arch: "architecture boundaries"
|
|
589
613
|
};
|
|
590
614
|
program.name("sentinel").description("One CLI that guards code health: presets, analysis, and arch checks.").version(readOwnVersion()).configureHelp({ sortOptions: false }).showSuggestionAfterError(true).showHelpAfterError('(run "sentinel --help" for usage)').addHelpText(
|
|
@@ -605,7 +629,7 @@ program.name("sentinel").description("One CLI that guards code health: presets,
|
|
|
605
629
|
"from the workspace root: scope to one module (omit = all; inside a module dir, drop this)"
|
|
606
630
|
).option("--preset <name>", `the stack preset to apply (${PRESET_NAMES.join(", ")})`).option("--flavour <name>", "deprecated: use --preset").option("--runner <tool>", "override the default runner (e.g. eslint, biome)").option("--ci", "CI mode: from the root, only the affected modules; non-zero exit on failure").option("--fix", "auto-fix where applicable").option("--dry-run", "preview the changes without writing (--init)").option("--json", "machine-readable output, for every verb").option(
|
|
607
631
|
"--output <shape>",
|
|
608
|
-
"output shape for a role that writes reports (console, json, html); console by default"
|
|
632
|
+
"output shape for a role that writes reports (console, json, html, markdown); console by default"
|
|
609
633
|
).option(
|
|
610
634
|
"--max-diagnostics <n>",
|
|
611
635
|
"cap the diagnostics embedded per module in --report (0 = no cap)",
|
|
@@ -665,6 +689,22 @@ function parseFlavour(value) {
|
|
|
665
689
|
}
|
|
666
690
|
return value;
|
|
667
691
|
}
|
|
692
|
+
function parseOutput(value, json) {
|
|
693
|
+
if (value === void 0) return void 0;
|
|
694
|
+
const asked = typeof value === "string" ? value.split(",").map((one) => one.trim()).filter((one) => one !== "") : [];
|
|
695
|
+
const unknown = asked.filter((one) => !OUTPUT_SHAPES.includes(one));
|
|
696
|
+
if (asked.length === 0 || unknown.length > 0) {
|
|
697
|
+
return program.error(
|
|
698
|
+
`sentinel: unknown --output ${unknown.map((one) => JSON.stringify(one)).join(", ") || JSON.stringify(value)}. Supported: ${OUTPUT_SHAPES.join(", ")}; several at once with a comma.`
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
if (json && asked.some((one) => one !== "json")) {
|
|
702
|
+
return program.error(
|
|
703
|
+
`sentinel: --json already selects the json shape, so it cannot be combined with --output ${JSON.stringify(value)}. Use one or the other.`
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
return asked.join(",");
|
|
707
|
+
}
|
|
668
708
|
function asMessage2(err) {
|
|
669
709
|
return err instanceof Error ? err.message : String(err);
|
|
670
710
|
}
|
|
@@ -688,6 +728,11 @@ if (opts.all && namedTargets.length > 0) {
|
|
|
688
728
|
}
|
|
689
729
|
var targets = opts.all || namedTargets.length === 0 ? [...SWEEPABLE_TARGETS] : namedTargets;
|
|
690
730
|
var targetsExplicit = !(opts.all || namedTargets.length === 0);
|
|
731
|
+
if (namedTargets.includes("static-analysis") && (verb === "report" || verb === "run" && opts.json)) {
|
|
732
|
+
program.error(
|
|
733
|
+
`sentinel: --static-analysis writes its own report, so ${verb === "report" ? "--report" : "--json"} has nothing to add to it. Run it bare for the console report, or with --output json|html|markdown for the files.`
|
|
734
|
+
);
|
|
735
|
+
}
|
|
691
736
|
var maxDiagnostics = Number(opts.maxDiagnostics);
|
|
692
737
|
if (!Number.isInteger(maxDiagnostics) || maxDiagnostics < 0) {
|
|
693
738
|
program.error(
|
|
@@ -734,7 +779,7 @@ async function main() {
|
|
|
734
779
|
json: Boolean(opts.json),
|
|
735
780
|
// `--json` stays the engine-wide machine-readable flag, so it also selects the json shape for
|
|
736
781
|
// a role that writes reports: two ways to ask for the same thing would be a trap.
|
|
737
|
-
output:
|
|
782
|
+
output: parseOutput(opts.output, Boolean(opts.json)) ?? (opts.json ? "json" : void 0),
|
|
738
783
|
dryRun: Boolean(opts.dryRun),
|
|
739
784
|
fail: (message) => program.error(message)
|
|
740
785
|
};
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
readNxProjectName,
|
|
7
7
|
readOwnPackage,
|
|
8
8
|
readProjectPackageJson
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-IL5PPLXE.js";
|
|
10
10
|
import {
|
|
11
11
|
matchesTsconfigPattern
|
|
12
12
|
} from "./chunk-O7REVMOC.js";
|
|
@@ -122,6 +122,21 @@ function testPresetSpecifier(preset) {
|
|
|
122
122
|
}
|
|
123
123
|
var ALL_TEST_PRESET_SPECIFIERS = Object.values(TEST_PRESET_SPECIFIERS);
|
|
124
124
|
var TEST_PRESET_SPECIFIER = TEST_PRESET_SPECIFIERS.react;
|
|
125
|
+
var TEST_SETUP_SPECIFIERS = {
|
|
126
|
+
a11y: "@hublo/sentinel/test/setup/a11y",
|
|
127
|
+
w3c: "@hublo/sentinel/test/setup/w3c",
|
|
128
|
+
"jest-parity": "@hublo/sentinel/test/setup/jest-parity",
|
|
129
|
+
"file-boundary": "@hublo/sentinel/test/setup/file-boundary",
|
|
130
|
+
"file-boundary-close": "@hublo/sentinel/test/setup/file-boundary-close",
|
|
131
|
+
"msw-lifecycle": "@hublo/sentinel/test/setup/msw-lifecycle",
|
|
132
|
+
workspace: "@hublo/sentinel/test/setup/workspace"
|
|
133
|
+
};
|
|
134
|
+
function declaredTestSetups(source) {
|
|
135
|
+
return Object.entries(TEST_SETUP_SPECIFIERS).filter(([, specifier]) => {
|
|
136
|
+
const escaped = specifier.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
137
|
+
return new RegExp(String.raw`[[,]\s*(['"])${escaped}\1`).test(source);
|
|
138
|
+
}).map(([name]) => name);
|
|
139
|
+
}
|
|
125
140
|
var TEST_CONFIG_FILES = [
|
|
126
141
|
"vitest.config.ts",
|
|
127
142
|
"vitest.config.mts",
|
|
@@ -262,7 +277,9 @@ function readTestAdoption(cwd) {
|
|
|
262
277
|
runner: hasTests ? "jest" : "none",
|
|
263
278
|
jestConfigs,
|
|
264
279
|
inheritsRootConfig,
|
|
265
|
-
ownDeclarations
|
|
280
|
+
ownDeclarations,
|
|
281
|
+
// No vitest config, so no `setupFiles` to name one. Empty, not null: this is a fact.
|
|
282
|
+
setups: []
|
|
266
283
|
};
|
|
267
284
|
}
|
|
268
285
|
const source = readTestConfigSource(cwd, configFile);
|
|
@@ -279,7 +296,9 @@ function readTestAdoption(cwd) {
|
|
|
279
296
|
jestConfigs,
|
|
280
297
|
inheritsRootConfig: false,
|
|
281
298
|
// it has a config of its own, readable or not
|
|
282
|
-
ownDeclarations
|
|
299
|
+
ownDeclarations,
|
|
300
|
+
// ⛔ null, not empty: the config exists and we could not read it, so we do not know.
|
|
301
|
+
setups: null
|
|
283
302
|
};
|
|
284
303
|
}
|
|
285
304
|
const preset = importedTestPreset(source);
|
|
@@ -297,9 +316,16 @@ function readTestAdoption(cwd) {
|
|
|
297
316
|
jestConfigs,
|
|
298
317
|
inheritsRootConfig: false,
|
|
299
318
|
// local always wins: this module is out of the root's reach
|
|
300
|
-
ownDeclarations
|
|
319
|
+
ownDeclarations,
|
|
320
|
+
setups: declaredTestSetups(source)
|
|
301
321
|
};
|
|
302
322
|
}
|
|
323
|
+
function hasLostItsSuite(cwd, adoption) {
|
|
324
|
+
if (adoption.adopted) return false;
|
|
325
|
+
if (adoption.jestConfigs.length > 0) return false;
|
|
326
|
+
if (!hasTestFiles(cwd)) return false;
|
|
327
|
+
return /\bsentinel\b[^&|;]*--test\b/.test(moduleScripts(cwd).test ?? "");
|
|
328
|
+
}
|
|
303
329
|
|
|
304
330
|
// src/roles/test/module-env.ts
|
|
305
331
|
import { existsSync as existsSync3, readFileSync as readFileSync4 } from "node:fs";
|
|
@@ -2134,13 +2160,26 @@ function namespaceBindings(program) {
|
|
|
2134
2160
|
import { readFileSync as readFileSync9, statSync } from "node:fs";
|
|
2135
2161
|
import { dirname as dirname2, join as join10, resolve as resolve4 } from "node:path";
|
|
2136
2162
|
import { parseSync as parseSync4 } from "oxc-parser";
|
|
2137
|
-
|
|
2163
|
+
|
|
2164
|
+
// src/shared/module-resolution.ts
|
|
2165
|
+
var RESOLVABLE_EXTENSIONS = [
|
|
2166
|
+
".ts",
|
|
2167
|
+
".tsx",
|
|
2168
|
+
".mts",
|
|
2169
|
+
".cts",
|
|
2170
|
+
".js",
|
|
2171
|
+
".jsx",
|
|
2172
|
+
".mjs",
|
|
2173
|
+
".cjs"
|
|
2174
|
+
];
|
|
2175
|
+
|
|
2176
|
+
// src/roles/test/codemod/static-import-for-require.ts
|
|
2138
2177
|
function resolveRelative(fromFile, specifier) {
|
|
2139
2178
|
const base = resolve4(dirname2(fromFile), specifier);
|
|
2140
2179
|
const candidates = [
|
|
2141
2180
|
base,
|
|
2142
|
-
...
|
|
2143
|
-
...
|
|
2181
|
+
...RESOLVABLE_EXTENSIONS.map((extension) => `${base}${extension}`),
|
|
2182
|
+
...RESOLVABLE_EXTENSIONS.map((extension) => join10(base, `index${extension}`))
|
|
2144
2183
|
];
|
|
2145
2184
|
for (const candidate of candidates) {
|
|
2146
2185
|
try {
|
|
@@ -3511,7 +3550,7 @@ function resolveContext(cwd, opts) {
|
|
|
3511
3550
|
// src/roles/test/codemod/workspace-specifier.ts
|
|
3512
3551
|
import { existsSync as existsSync13, readFileSync as readFileSync13 } from "node:fs";
|
|
3513
3552
|
import { join as join15, relative as relative2, resolve as resolve6 } from "node:path";
|
|
3514
|
-
var
|
|
3553
|
+
var EXTENSIONS = ["", ".ts", ".tsx", ".js", ".jsx", ".mts", ".cts", "/index.ts", "/index.js"];
|
|
3515
3554
|
function pathEntries(workspaceRoot) {
|
|
3516
3555
|
const file = join15(workspaceRoot, "tsconfig.base.json");
|
|
3517
3556
|
if (!existsSync13(file)) return [];
|
|
@@ -3541,7 +3580,7 @@ function workspaceSpecifier(workspaceRoot, file) {
|
|
|
3541
3580
|
const absolute = resolve6(file);
|
|
3542
3581
|
for (const entry of pathEntries(workspaceRoot)) {
|
|
3543
3582
|
if (!entry.wildcard) {
|
|
3544
|
-
if (
|
|
3583
|
+
if (EXTENSIONS.some((extension) => `${entry.target}${extension}` === absolute)) {
|
|
3545
3584
|
return entry.key;
|
|
3546
3585
|
}
|
|
3547
3586
|
continue;
|
|
@@ -3686,14 +3725,13 @@ function rewriteMockFactoryGenerics(source, program) {
|
|
|
3686
3725
|
import { readFileSync as readFileSync14, statSync as statSync3 } from "node:fs";
|
|
3687
3726
|
import { dirname as dirname7, join as join16, relative as relative3, resolve as resolve8 } from "node:path";
|
|
3688
3727
|
import { parseSync as parseSync7 } from "oxc-parser";
|
|
3689
|
-
var EXTENSIONS3 = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
|
|
3690
3728
|
function resolveRelative2(fromFile, specifier) {
|
|
3691
3729
|
if (!specifier.startsWith(".")) return void 0;
|
|
3692
3730
|
const base = resolve8(dirname7(fromFile), specifier);
|
|
3693
3731
|
const candidates = [
|
|
3694
3732
|
base,
|
|
3695
|
-
...
|
|
3696
|
-
...
|
|
3733
|
+
...RESOLVABLE_EXTENSIONS.map((extension) => `${base}${extension}`),
|
|
3734
|
+
...RESOLVABLE_EXTENSIONS.map((extension) => join16(base, `index${extension}`))
|
|
3697
3735
|
];
|
|
3698
3736
|
for (const candidate of candidates) {
|
|
3699
3737
|
try {
|
|
@@ -4283,7 +4321,7 @@ var THE_SHIM_CANNOT_ANSWER = [/\brequire\s*\(/, /\bjest\.(?:fn|Mock|SpyInstance)
|
|
|
4283
4321
|
var SKIPPED = /* @__PURE__ */ new Set(["node_modules", "dist", "coverage", ".git", ".vite"]);
|
|
4284
4322
|
var TEST_FILE2 = /[.-](spec|test)\.[cm]?[jt]sx?$/;
|
|
4285
4323
|
var SOURCE_FILE = /\.[cm]?[jt]sx?$/;
|
|
4286
|
-
var
|
|
4324
|
+
var EXTENSIONS2 = [".ts", ".tsx", ".js", ".jsx", ".mts", ".cts"];
|
|
4287
4325
|
function relativeSpecifiers2(source) {
|
|
4288
4326
|
const found = [];
|
|
4289
4327
|
const patterns = [
|
|
@@ -4300,10 +4338,10 @@ function relativeSpecifiers2(source) {
|
|
|
4300
4338
|
function resolveSpecifier(fromFile, specifier) {
|
|
4301
4339
|
const base = resolve9(dirname9(fromFile), specifier);
|
|
4302
4340
|
if (existsSync14(base) && statSync4(base).isFile()) return base;
|
|
4303
|
-
for (const extension of
|
|
4341
|
+
for (const extension of EXTENSIONS2) {
|
|
4304
4342
|
if (existsSync14(`${base}${extension}`)) return `${base}${extension}`;
|
|
4305
4343
|
}
|
|
4306
|
-
for (const extension of
|
|
4344
|
+
for (const extension of EXTENSIONS2) {
|
|
4307
4345
|
const index = join17(base, `index${extension}`);
|
|
4308
4346
|
if (existsSync14(index)) return index;
|
|
4309
4347
|
}
|
|
@@ -6417,6 +6455,12 @@ var VitestAdapter = class _VitestAdapter extends BaseAdapter {
|
|
|
6417
6455
|
async run(ctx) {
|
|
6418
6456
|
const adoption = readTestAdoption(ctx.cwd);
|
|
6419
6457
|
if (!adoption.adopted) {
|
|
6458
|
+
if (hasLostItsSuite(ctx.cwd, adoption)) {
|
|
6459
|
+
this.say(
|
|
6460
|
+
`this module's \`test\` script runs sentinel and it has test files, but no sentinel preset was found in its Vitest config. Running would report success without executing a single test, so this fails instead. Restore the config, or take sentinel out of the \`test\` script if the module is leaving it on purpose.`
|
|
6461
|
+
);
|
|
6462
|
+
return Promise.resolve({ ok: false, code: 1 });
|
|
6463
|
+
}
|
|
6420
6464
|
this.say(
|
|
6421
6465
|
adoption.state === "jest" ? `this module tests with jest (${adoption.jestConfigs.join(", ")}); run \`sentinel --init --test\` to migrate it.` : adoption.state === "no-tests" ? `this module has no tests.` : `no sentinel preset in this module's Vitest config; run \`sentinel --init --test\` to adopt.`
|
|
6422
6466
|
);
|
|
@@ -6541,6 +6585,8 @@ export {
|
|
|
6541
6585
|
oxlintPath,
|
|
6542
6586
|
oxlintSearchPath,
|
|
6543
6587
|
walk,
|
|
6588
|
+
writtenFileFormatter,
|
|
6589
|
+
RESOLVABLE_EXTENSIONS,
|
|
6544
6590
|
readTestAdoption,
|
|
6545
6591
|
resolveContext,
|
|
6546
6592
|
moduleEnvironment,
|
|
@@ -24,6 +24,15 @@ function readOwnPackage() {
|
|
|
24
24
|
function readOwnVersion() {
|
|
25
25
|
return readOwnPackage().version;
|
|
26
26
|
}
|
|
27
|
+
function ownPackageDir() {
|
|
28
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
for (; ; ) {
|
|
30
|
+
if (existsSync(join(dir, "package.json"))) return dir;
|
|
31
|
+
const parent = dirname(dir);
|
|
32
|
+
if (parent === dir) return dir;
|
|
33
|
+
dir = parent;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
27
36
|
function readProjectPackageJson(dir) {
|
|
28
37
|
const path = join(dir, "package.json");
|
|
29
38
|
if (!existsSync(path)) return {};
|
|
@@ -279,6 +288,7 @@ function inspectWorkspacePrep(root) {
|
|
|
279
288
|
export {
|
|
280
289
|
readOwnPackage,
|
|
281
290
|
readOwnVersion,
|
|
291
|
+
ownPackageDir,
|
|
282
292
|
readProjectPackageJson,
|
|
283
293
|
readNxProjectName,
|
|
284
294
|
moduleName,
|
|
@@ -288,4 +298,4 @@ export {
|
|
|
288
298
|
ensureWorkspacePrep,
|
|
289
299
|
inspectWorkspacePrep
|
|
290
300
|
};
|
|
291
|
-
//# sourceMappingURL=chunk-
|
|
301
|
+
//# sourceMappingURL=chunk-IL5PPLXE.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/shared/package-json.ts","../src/core/settings.ts","../src/core/workspace-prep.ts"],"sourcesContent":["/**\n * package.json helpers, shared by the CLI and the engine.\n */\nimport { existsSync, readFileSync } from 'node:fs'\nimport { basename, dirname, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\n/**\n * sentinel's OWN version, for `--version`. We must NOT use\n * `process.env.npm_package_version`, that is the version of whatever script\n * invoked us (an app's package.json, or nothing). Instead walk up from this\n * module to the nearest package.json, which is always sentinel's own, whether\n * running from source or the bundled dist.\n */\n/**\n * sentinel's OWN name + version, read by walking up from this module to the nearest\n * package.json. Used both for `--version` and to pin the exact dependency `--init`\n * writes into an adopted module, so a module always references the version that adopted it.\n */\nexport function readOwnPackage(): { name: string; version: string; bin?: string } {\n let dir = dirname(fileURLToPath(import.meta.url))\n for (;;) {\n const pkgPath = join(dir, 'package.json')\n if (existsSync(pkgPath)) {\n try {\n const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) as {\n name?: string\n version?: string\n bin?: string | Record<string, string>\n }\n if (typeof pkg.version === 'string') {\n // The bin PATH, not the command name: a package that adopts itself has to invoke\n // its own build output, because the `.bin` shim does not exist yet (see below).\n const bin = typeof pkg.bin === 'string' ? pkg.bin : Object.values(pkg.bin ?? {})[0]\n return { name: pkg.name ?? '@hublo/sentinel', version: pkg.version, bin }\n }\n } catch {\n // Malformed package.json; keep walking up.\n }\n }\n const parent = dirname(dir)\n if (parent === dir) return { name: '@hublo/sentinel', version: '0.0.0' } // filesystem root\n dir = parent\n }\n}\n\nexport function readOwnVersion(): string {\n return readOwnPackage().version\n}\n\n/**\n * The directory sentinel's own `package.json` sits in, for the files it SHIPS beside its code.\n *\n * ⛔ Walked up to, never computed from a count of `..`. The emitted layout is not ours to predict:\n * `tsup` bundles the source into chunks whose depth has nothing to do with the source tree's, and\n * it moves code between them from one build to the next. A relative path from `import.meta.url` is\n * therefore right in `src/` and wrong in `dist/`, which is exactly the failure\n * `roles/test/shared-test-config.ts` records at length after it cost a module 83 passing tests.\n *\n * Measured here: `plugins/sonar-cognitive.js` resolved through a four-segment relative path existed\n * when run from source and did not after the build, and the only symptom was the whole size pass\n * reporting \"oxlint could not be run\".\n */\nexport function ownPackageDir(): string {\n let dir = dirname(fileURLToPath(import.meta.url))\n for (;;) {\n if (existsSync(join(dir, 'package.json'))) return dir\n const parent = dirname(dir)\n if (parent === dir) return dir\n dir = parent\n }\n}\n\n/**\n * Read a module's package.json for framework detection; tolerant of a missing or\n * malformed file (returns `{}`, so detection falls through to the `node` default).\n */\nexport function readProjectPackageJson(dir: string): {\n /** The package name, which is also what nx calls the project when there is no `project.json`. */\n name?: string\n dependencies?: Record<string, string>\n devDependencies?: Record<string, string>\n /**\n * How the module's `.js`/`.ts` files are loaded. It decides whether a Vite config can import\n * an ESM-only package at all, which is why the build role reads it before rewriting one.\n */\n type?: string\n /**\n * What nx alone needs per target, which every role writes here and none could read back.\n *\n * Typed loosely on purpose: this reader is shared by every role and each one knows the shape of\n * its own target. Declared at all because a role that WRITES a block and then has to cast to\n * read it is one refactor away from reading a key it never wrote.\n */\n nx?: { targets?: Record<string, Record<string, unknown>> }\n /**\n * The npm scripts, which several roles read to know what a module can DO rather than what it is.\n *\n * ⚠️ Added when the bundle pass had to tell two silences apart: a module that declares a build\n * and produced no manifest is a defect to act on, a module that declares none has nothing to\n * measure. Without this the two shared one sentence, and it told a front team their application\n * ships no bundle.\n */\n scripts?: Record<string, string>\n} {\n const path = join(dir, 'package.json')\n if (!existsSync(path)) return {}\n try {\n return JSON.parse(readFileSync(path, 'utf8')) as Record<string, never>\n } catch {\n process.stderr.write(`sentinel: could not parse ${path}; ignoring for detection.\\n`)\n return {}\n }\n}\n\n/**\n * The nx project name for a module, read from its `project.json`, or undefined if\n * there is none/unreadable. Used when `--init` has to scaffold a `package.json`\n * for a module that only has a `project.json` (common for nx apps/services): the\n * scaffolded file borrows this name so it is a valid, uniquely-named workspace\n * package. Reusable by any tool that scaffolds, not just TypeScript.\n */\nexport function readNxProjectName(dir: string): string | undefined {\n const path = join(dir, 'project.json')\n if (!existsSync(path)) return undefined\n try {\n const parsed = JSON.parse(readFileSync(path, 'utf8')) as { name?: unknown }\n return typeof parsed.name === 'string' ? parsed.name : undefined\n } catch {\n return undefined\n }\n}\n\n/**\n * What nx calls this module, resolved the way nx itself resolves it.\n *\n * The precedence is `project.json`, then `package.json`, then the directory, and only the\n * first and last of those were being consulted. A module with no `project.json` therefore\n * answered with its DIRECTORY while nx answered with its package name, and the two disagree\n * in the real repo: `apps/front/hr-management` is `front-hr-management` to nx, as is\n * `apps/front/storybook-app`.\n *\n * That gap is not cosmetic, because this name is what `--module` is matched against. The\n * `serve` script `--init` writes carries `--module <name>`, so a module in that shape was\n * adopted with a script naming a project the workspace does not contain, and `pnpm run serve`\n * failed the day it was adopted with nothing having reported it.\n *\n * One function so the two ends cannot drift again: whoever WRITES the name and whoever\n * RESOLVES it now read the same source.\n */\nexport function moduleName(dir: string): string {\n const declared = readProjectPackageJson(dir).name\n return readNxProjectName(dir) ?? (declared === undefined ? basename(dir) : declared)\n}\n","/**\n * Tunable settings: the knobs you would actually change. The fixed vocabulary and\n * its types live in `domain.ts`.\n *\n * Note: the preset is DETECTED from a module's dependencies (see\n * `detect-framework.ts`), deterministically, `node` when no framework dep is present.\n * `--preset` overrides the detection (recommended for `--init`, since hoisted deps\n * can make detection return `node`). The detection table lives in `detect-framework.ts`.\n */\n\n/** A file that marks a workspace root; sentinel refuses to operate there. */\nexport const WORKSPACE_ROOT_MARKER = 'nx.json'\n\n/**\n * How many structured diagnostics a report embeds per module, when the caller does not say.\n *\n * A cap, not a limit on what is counted: the totals stay exact and `diagnosticsTruncated`\n * says when rows were dropped, so a bounded envelope never reads as a clean module. Shared\n * by every role, because a consumer reading `--json` across roles should not have to learn\n * a different cap for each one. `--max-diagnostics 0` means no cap.\n */\nexport const DEFAULT_MAX_DIAGNOSTICS = 100\n","/**\n * Workspace-level preparation that `--init` applies once, at the workspace root, so an\n * adopted module actually installs and resolves cleanly. `--init` writes a module's own\n * files; this is the small, deliberate exception where it also touches the root, because\n * \"set this module up\" is not honest if the workspace it lives in cannot then install or\n * resolve it.\n *\n * Kept intentionally narrow (YAGNI): it applies the specific fixes a workspace's OWN\n * config shows it needs, and is a no-op everywhere else. It is NOT a speculative dedupe\n * engine, when a second, different case appears we generalize from real cases instead of\n * predicting them. The two fixes today:\n *\n * - i18next singleton override. If the root declares a second TypeScript via the\n * `@typescript/native` alias (a repo mid-migration running two TypeScripts), adopting a\n * module re-resolves pnpm and i18next, which carries an optional type-only `typescript`\n * peer, forks into two peer-keyed variants. That forks react-i18next into two React\n * contexts and breaks translation-singleton tests. Pinning i18next's `typescript` peer\n * to the declared TS collapses it to one. Read from the root's own declaration, so it\n * tracks the version and disappears (no-op) once the migration removes the second TS.\n * Written to `pnpm-workspace.yaml`, the only place current pnpm reads overrides from.\n *\n * - release-age allow-list. If the root uses pnpm's `minimumReleaseAgeExclude` gate, a\n * freshly published sentinel is younger than the gate and will not install; allow-list\n * it. Only touched when the gate is actually configured.\n *\n * - root formatter exclusion. A module that has adopted the FORMAT role is formatted by\n * oxfmt, from its own config. The root Prettier does not know that and still owns every\n * file in the repo, so `nx format:write` or `prettier --write .` from the root reformats\n * the module back to the ROOT's style, and the next `pnpm run format` in the module\n * reverses it. Two formatters fighting over the same files, with whichever ran last\n * winning, is the exact state adoption removes INSIDE a module, and it has to be removed\n * at the root too. So the module is excluded in the root `.prettierignore` as it adopts,\n * and the root's own config is otherwise untouched: it still serves every module that has\n * not migrated.\n *\n * All three edits are idempotent and defensive: absent the trigger (no second TypeScript, no\n * release-age gate, no root Prettier) each is skipped, so the step is safe on any workspace.\n */\nimport { existsSync, readFileSync, writeFileSync } from 'node:fs'\nimport { dirname, join, relative } from 'node:path'\n\nimport { readOwnPackage } from '../shared/package-json.js'\n\nimport { WORKSPACE_ROOT_MARKER } from './settings.js'\n\nconst OVERRIDE_KEY = 'i18next>typescript'\nconst NATIVE_TS_ALIAS = '@typescript/native' // `npm:typescript@^7.x`: a workspace's second, real TS\nconst WORKSPACE_YAML = 'pnpm-workspace.yaml'\nconst RELEASE_AGE_KEY = 'minimumReleaseAgeExclude'\nconst LIST_ITEM = /^(\\s*)-\\s+(.*?)\\s*$/\nconst MAP_ENTRY = /^(\\s+)(.*?):\\s+(.*?)\\s*$/\n\ninterface RootPackageJson {\n dependencies?: Record<string, string>\n devDependencies?: Record<string, string>\n pnpm?: { overrides?: Record<string, string> }\n}\n\n/** Walk up from a module directory to the workspace root (the dir holding the marker). */\nexport function findWorkspaceRoot(startDir: string): string | undefined {\n let dir = startDir\n for (;;) {\n if (existsSync(join(dir, WORKSPACE_ROOT_MARKER))) return dir\n const parent = dirname(dir)\n if (parent === dir) return undefined\n dir = parent\n }\n}\n\n/**\n * The TypeScript version to pin i18next's peer to, read from the root's own\n * `@typescript/native` alias (`npm:typescript@^7.0.2` -> `7.0.2`). Returns undefined when\n * the alias is absent (no second TypeScript, so no fork to fix).\n */\nfunction declaredNativeTs(pkg: RootPackageJson): string | undefined {\n const spec = pkg.dependencies?.[NATIVE_TS_ALIAS] ?? pkg.devDependencies?.[NATIVE_TS_ALIAS]\n if (!spec) return undefined\n const version = spec.slice(spec.lastIndexOf('@') + 1).replace(/^[\\^~>=<\\s]+/, '')\n return version || undefined\n}\n\n/**\n * Ensure the i18next singleton override matches the declared second TypeScript. No-op when\n * the workspace runs a single TypeScript (no `@typescript/native`). Returns a message when\n * it changed (or would change, under dryRun), else undefined.\n *\n * The override is written to `pnpm-workspace.yaml`, never to `package.json#pnpm`: pnpm 11\n * stopped reading the `pnpm` field and does so SILENTLY, with no warning and no deprecation\n * (pnpm/pnpm#11536), so an override left in `package.json` is simply not applied. The yaml is\n * read by pnpm 10 and 11 alike (https://pnpm.io/settings), which is why there is one target\n * here rather than a version-dependent fallback.\n */\nfunction ensureI18nextSingleton(root: string, dryRun: boolean): string | undefined {\n const pkgPath = join(root, 'package.json')\n if (!existsSync(pkgPath)) return undefined\n const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) as RootPackageJson\n const want = declaredNativeTs(pkg)\n if (!want) return undefined // single TypeScript: nothing forks\n\n const yamlPath = join(root, WORKSPACE_YAML)\n if (!existsSync(yamlPath)) return undefined // no workspace file: nowhere pnpm would read it\n const lines = readFileSync(yamlPath, 'utf8').split('\\n')\n const entry = findOverrideEntry(lines)\n if (entry.valueIdx !== undefined && entry.value === want) return undefined // already pinned\n if (dryRun) return `would pin ${OVERRIDE_KEY} to ${want} in ${WORKSPACE_YAML} (i18next singleton)`\n\n applyOverride(lines, entry, want)\n writeFileSync(yamlPath, lines.join('\\n'))\n return `pinned ${OVERRIDE_KEY} to ${want} in ${WORKSPACE_YAML} (i18next singleton)`\n}\n\n/** Where the override sits in the yaml, if at all, plus where a new one would be inserted. */\ninterface OverrideEntry {\n /** Index of the `OVERRIDE_KEY` line, when the key is already declared. */\n valueIdx?: number\n /** Its current value, unquoted. */\n value?: string\n /** Index of the `overrides:` key line, when the block exists. */\n blockIdx?: number\n /** Index of the block's last entry, so an insertion keeps the block contiguous. */\n lastEntryIdx?: number\n /** Indentation the block's entries use. */\n indent: string\n}\n\n/**\n * Locate the `overrides:` block and the i18next key inside it. A targeted text read rather\n * than a yaml parse, for the same reason `ensureReleaseAgeAllowList` does it: this file is\n * hand-maintained and round-tripping it through a parser would drop its comments and ordering.\n */\nfunction findOverrideEntry(lines: string[]): OverrideEntry {\n const blockIdx = lines.findIndex((line) => line.replace(/\\s+$/, '') === 'overrides:')\n if (blockIdx === -1) return { indent: ' ' }\n\n let lastEntryIdx = blockIdx\n let indent = ' '\n for (let i = blockIdx + 1; i < lines.length; i++) {\n const match = lines[i]?.match(MAP_ENTRY)\n if (!match) break // dedent or blank line: the block ended\n indent = match[1] ?? indent\n lastEntryIdx = i\n if ((match[2] ?? '').replace(/^['\"]|['\"]$/g, '') === OVERRIDE_KEY) {\n return {\n valueIdx: i,\n value: (match[3] ?? '').replace(/^['\"]|['\"]$/g, ''),\n blockIdx,\n lastEntryIdx,\n indent,\n }\n }\n }\n return { blockIdx, lastEntryIdx, indent }\n}\n\n/** Update the key in place, append it to an existing block, or open a block at the end. */\nfunction applyOverride(lines: string[], entry: OverrideEntry, want: string): void {\n const line = `${entry.indent}'${OVERRIDE_KEY}': ${want}`\n if (entry.valueIdx !== undefined) {\n lines[entry.valueIdx] = line\n return\n }\n if (entry.lastEntryIdx !== undefined) {\n lines.splice(entry.lastEntryIdx + 1, 0, line)\n return\n }\n // No `overrides:` block at all: open one, keeping a single trailing newline.\n while (lines.length > 0 && lines[lines.length - 1] === '') lines.pop()\n lines.push('overrides:', line, '')\n}\n\n/**\n * Ensure sentinel is allow-listed under `minimumReleaseAgeExclude` so a fresh publish\n * installs. No-op when the workspace has no such gate. Targeted text edit so comments and\n * formatting are preserved. Returns a message when it changed (or would), else undefined.\n */\nfunction ensureReleaseAgeAllowList(root: string, dryRun: boolean): string | undefined {\n const yamlPath = join(root, WORKSPACE_YAML)\n if (!existsSync(yamlPath)) return undefined\n const own = readOwnPackage().name\n const lines = readFileSync(yamlPath, 'utf8').split('\\n')\n const keyIdx = lines.findIndex((line) => line.replace(/\\s+$/, '') === `${RELEASE_AGE_KEY}:`)\n if (keyIdx === -1) return undefined // no release-age gate in use\n\n let lastItemIdx = keyIdx\n let indent = ' '\n for (let i = keyIdx + 1; i < lines.length; i++) {\n const match = lines[i]?.match(LIST_ITEM)\n if (!match) break\n indent = match[1] ?? indent\n lastItemIdx = i\n if ((match[2] ?? '').replace(/^['\"]|['\"]$/g, '') === own) return undefined // already listed\n }\n if (dryRun) return `would allow-list ${own} under ${RELEASE_AGE_KEY}`\n\n lines.splice(lastItemIdx + 1, 0, `${indent}- '${own}'`)\n writeFileSync(yamlPath, lines.join('\\n'))\n return `allow-listed ${own} under ${RELEASE_AGE_KEY} in ${WORKSPACE_YAML}`\n}\n\n/** The root ignore file Prettier reads, and the section sentinel owns inside it. */\nconst GIT_IGNORE = '.gitignore'\nconst REPORT_DIR_NAME = 'static-analysis-report'\nconst REPORT_PATTERN = `${REPORT_DIR_NAME}/`\nconst REPORT_HEADER = '# @hublo/sentinel: static analysis reports, written inside each module'\nconst PRETTIER_IGNORE = '.prettierignore'\nconst EXCLUSION_HEADER = '# @hublo/sentinel: modules formatted by oxfmt, not by the root Prettier'\n\n/** Root Prettier configs. Their presence is what makes an exclusion meaningful. */\nconst ROOT_PRETTIER_CONFIGS = [\n '.prettierrc',\n '.prettierrc.json',\n '.prettierrc.json5',\n '.prettierrc.yml',\n '.prettierrc.yaml',\n '.prettierrc.js',\n '.prettierrc.cjs',\n '.prettierrc.mjs',\n 'prettier.config.js',\n 'prettier.config.cjs',\n 'prettier.config.mjs',\n]\n\n/**\n * Exclude a module that now formats itself from the ROOT Prettier.\n *\n * Written as a directory pattern anchored at the root (`/apps/front/x/`), which is what\n * gitignore syntax means by \"this exact directory\", so a module cannot accidentally exclude a\n * same-named directory somewhere else.\n *\n * Skipped entirely when the root has no Prettier at all: there is then nothing to conflict\n * with, and creating an ignore file for a tool the workspace does not use would be noise.\n */\nfunction ensureFormatterExclusion(\n root: string,\n moduleDir: string,\n dryRun: boolean,\n): string | undefined {\n const rel = relative(root, moduleDir).replaceAll('\\\\', '/')\n if (rel === '' || rel.startsWith('..')) return undefined // the root itself, or outside it\n\n const ignorePath = join(root, PRETTIER_IGNORE)\n const hasPrettier =\n existsSync(ignorePath) || ROOT_PRETTIER_CONFIGS.some((name) => existsSync(join(root, name)))\n if (!hasPrettier) return undefined\n\n const pattern = `/${rel}/`\n const existing = existsSync(ignorePath) ? readFileSync(ignorePath, 'utf8') : ''\n const lines = existing.split('\\n')\n // Already excluded, in whichever spelling a human may have used.\n if (lines.some((line) => line.trim().replace(/\\/$/, '') === pattern.replace(/\\/$/, ''))) {\n return undefined\n }\n if (dryRun) return `would exclude ${rel} from the root Prettier (${PRETTIER_IGNORE})`\n\n const headerIdx = lines.findIndex((line) => line.trim() === EXCLUSION_HEADER)\n if (headerIdx === -1) {\n const prefix = existing.trim() === '' ? '' : `${existing.replace(/\\n*$/, '')}\\n\\n`\n writeFileSync(ignorePath, `${prefix}${EXCLUSION_HEADER}\\n${pattern}\\n`)\n } else {\n // Keep the managed section together and sorted, so the list stays readable as it grows\n // to 150 entries and a diff shows one added line rather than a reshuffle.\n let end = headerIdx + 1\n while (end < lines.length && lines[end]?.trim().startsWith('/')) end += 1\n const managed = [...lines.slice(headerIdx + 1, end), pattern].sort()\n writeFileSync(\n ignorePath,\n [...lines.slice(0, headerIdx + 1), ...managed, ...lines.slice(end)].join('\\n'),\n )\n }\n return `excluded ${rel} from the root Prettier (${PRETTIER_IGNORE}): it formats itself now`\n}\n\n/**\n * Apply every workspace-level fix the given root needs. Returns the human-readable\n * messages for what changed (empty when nothing did). Under dryRun nothing is written and\n * the messages describe what would change.\n *\n * `formattedModule` is the module that just adopted the FORMAT role, when one did. Absent\n * for a lint-only or TypeScript-only `--init`, because a module that has not adopted the\n * formatter must keep being formatted by the root.\n */\n/**\n * Ignore the static analysis report directory, once, at the ROOT.\n *\n * ## Why the root and not each module\n *\n * The role writes its json and html into `<module>/static-analysis-report/`, so the obvious place\n * for the rule is the module. It was there first, and it was wrong twice over: it CREATES a\n * `.gitignore` in a module that had none, which is a structural change to someone else's module for\n * one line, and it repeats that line once per adopting module. The root already ignores output this\n * way, with `/dist` and `/coverage`.\n *\n * ⚠️ The pattern is deliberately NOT anchored. `/dist` matches only the root's `dist`; the report\n * directory lives inside a module, so `static-analysis-report/` has to match at any depth. Getting\n * that backwards would leave every adopting module with an untracked directory, which is the exact\n * defect this guards against.\n *\n * ## Why this root write is not the dangerous kind\n *\n * The comment on `inspectWorkspacePrep` is right that root writes are what nobody sees afterwards,\n * and the i18next override earned that warning: it re-resolved a module from a pull request that\n * touched none of its files. An ignore line changes no resolution, no build and no install. If it\n * ever goes stale the cost is one unused line, which is why it is reported rather than feared.\n */\nfunction ensureReportIgnored(root: string, dryRun: boolean): string | undefined {\n const path = join(root, GIT_IGNORE)\n const existing = existsSync(path) ? readFileSync(path, 'utf8') : ''\n // Whatever spelling a human may have used: with or without a leading or trailing slash.\n const already = existing\n .split('\\n')\n .some((line) => line.trim().replace(/^\\//, '').replace(/\\/$/, '') === REPORT_DIR_NAME)\n if (already) return undefined\n if (dryRun) return `would ignore ${REPORT_PATTERN} in the root ${GIT_IGNORE}`\n\n const prefix = existing.trim() === '' ? '' : `${existing.replace(/\\n*$/, '')}\\n\\n`\n writeFileSync(path, `${prefix}${REPORT_HEADER}\\n${REPORT_PATTERN}\\n`)\n return `ignored ${REPORT_PATTERN} in the root ${GIT_IGNORE}`\n}\n\nexport function ensureWorkspacePrep(opts: {\n root: string\n dryRun?: boolean\n formattedModule?: string\n /** True when the STATIC ANALYSIS role adopted: it is what earns the ignore line. */\n analysedModule?: boolean\n}): string[] {\n const dryRun = Boolean(opts.dryRun)\n return [\n ensureI18nextSingleton(opts.root, dryRun),\n ensureReleaseAgeAllowList(opts.root, dryRun),\n opts.formattedModule === undefined\n ? undefined\n : ensureFormatterExclusion(opts.root, opts.formattedModule, dryRun),\n opts.analysedModule === true ? ensureReportIgnored(opts.root, dryRun) : undefined,\n ].filter((message): message is string => message !== undefined)\n}\n\n/**\n * One standing change sentinel has made to the workspace ROOT, and whether it is still earned.\n *\n * This exists because the three root writes are the only thing sentinel does that nobody can\n * see afterwards. `--init` announces each one on the run that makes it, and then it is a line\n * in someone else's file forever. Two consequences already bit us.\n *\n * The `i18next>typescript` override re-resolved `apps/front/console` from a PR that touched no\n * file under it, so CODEOWNERS pulled nobody and its owners learned about it afterwards.\n *\n * And every one of them is TEMPORARY with its removal condition written in prose. That failed\n * three times in one week: `SENTINEL_TAG=alpha` in the migration-page workflow kept building\n * with a pre-release long after 1.1.0 went stable and reported `Format 0/439` for 15 adopted\n * modules; ADR 0042 and its RFC both sat mid-flight past their deadline; and this release-age\n * exclusion is still waiting on \"once sentinel's release cadence settles\". Nothing failed when\n * any of them expired, because a comment cannot fail.\n *\n * So each entry carries the condition that JUSTIFIES it, evaluated now rather than described.\n * A trigger that has gone away makes the entry dead config, and dead config is something a\n * command can say out loud instead of something a reader has to notice.\n *\n * Read-only, and offline: it inspects the same files `--init` writes and nothing else. What it\n * deliberately does NOT claim is anything needing the npm registry — whether a published\n * version is old enough to clear the release-age gate is not knowable from the repo, so it is\n * stated as the standing exemption it is rather than guessed at.\n */\nexport interface WorkspacePrepEntry {\n /** `rule` to match the shape the shared renderer already prints as a labelled block. */\n rule: string\n /** What it does, whether its trigger still holds, and what would retire it. */\n reason: string\n}\n\n/** The root-level changes sentinel maintains here, each with its justification checked now. */\nexport function inspectWorkspacePrep(root: string): WorkspacePrepEntry[] {\n const entries: WorkspacePrepEntry[] = []\n // Tolerant, unlike the write path above, and the difference is deliberate. `--init` should\n // fail loudly on a root it cannot parse, because it is about to edit it. `--inspect` is a\n // read verb that sweeps 441 modules, and one malformed root file must not take the whole\n // sweep down: the modules still have honest answers to give. Caught by its own test.\n let pkg: RootPackageJson = {}\n const pkgPath = join(root, 'package.json')\n if (existsSync(pkgPath)) {\n try {\n pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) as RootPackageJson\n } catch {\n pkg = {}\n }\n }\n\n const yamlPath = join(root, WORKSPACE_YAML)\n const yaml = existsSync(yamlPath) ? readFileSync(yamlPath, 'utf8') : undefined\n\n const pinned = yaml === undefined ? undefined : findOverrideEntry(yaml.split('\\n')).value\n if (pinned !== undefined) {\n const stillForks = declaredNativeTs(pkg)\n entries.push({\n rule: `${WORKSPACE_YAML} → overrides.${OVERRIDE_KEY}`,\n reason:\n stillForks === undefined\n ? `pinned to ${pinned}, but the root no longer declares ${NATIVE_TS_ALIAS}, so nothing ` +\n `forks any more. This override is now dead config and can be removed.`\n : `pinned to ${pinned}, matching the second TypeScript the root declares ` +\n `(${NATIVE_TS_ALIAS}). It re-resolves i18next for EVERY module that depends on it, ` +\n `not only adopted ones. It retires with the second TypeScript.`,\n })\n }\n\n // A copy left in `package.json#pnpm` by an older sentinel, or by hand. Reported because it\n // is INERT rather than redundant: pnpm 11 dropped the `pnpm` field with no warning\n // (pnpm/pnpm#11536), so it reads as protection that is not applied.\n const legacy = pkg.pnpm?.overrides?.[OVERRIDE_KEY]\n if (legacy !== undefined) {\n entries.push({\n rule: `package.json → pnpm.overrides.${OVERRIDE_KEY}`,\n reason:\n `pinned to ${legacy}, but pnpm 11 no longer reads the \\`pnpm\\` field of package.json ` +\n `and says nothing about it, so this override is not applied. Move it to ` +\n `${WORKSPACE_YAML} under \\`overrides:\\`, which pnpm 10 and 11 both read.`,\n })\n }\n\n const ignorePath = join(root, GIT_IGNORE)\n if (existsSync(ignorePath)) {\n const ignored = readFileSync(ignorePath, 'utf8')\n .split('\\n')\n .some((line) => line.trim().replace(/^\\//, '').replace(/\\/$/, '') === REPORT_DIR_NAME)\n if (ignored) {\n entries.push({\n rule: `${GIT_IGNORE} → ${REPORT_PATTERN}`,\n reason:\n `the static analysis role writes its json and html into ` +\n `\\`<module>/${REPORT_PATTERN}\\`, and one unanchored line at the root covers every module ` +\n `instead of creating a \\`${GIT_IGNORE}\\` in each one. Unlike the override above it changes ` +\n `no resolution and no build, so a stale one costs a single unused line. It retires when no ` +\n `module declares a \\`static-analysis\\` script any more, which the adoption report counts ` +\n `and this read deliberately does not: it would have to sweep every module.`,\n })\n }\n }\n\n const own = readOwnPackage().name\n if (yaml !== undefined) {\n const listed = new RegExp(`^\\\\s*-\\\\s*['\"]?${own.replace('/', '\\\\/')}['\"]?\\\\s*$`, 'm').test(yaml)\n if (listed) {\n const gated = new RegExp(`^\\\\s*minimumReleaseAge\\\\s*:`, 'm').test(yaml)\n entries.push({\n rule: `${WORKSPACE_YAML} → ${RELEASE_AGE_KEY}: ${own}`,\n reason: gated\n ? `${own} is exempt from this workspace's release-age gate, so a freshly published ` +\n `version installs instead of being held. That is a standing exemption from a ` +\n `supply-chain control: the exposure is one bump wide, since the version is pinned ` +\n `and the lockfile carries its integrity, so a re-publish of the same version does ` +\n `not pass. Retire it once a bump can simply wait out the window.`\n : `${own} is allow-listed, but this workspace no longer sets minimumReleaseAge, so ` +\n `there is no gate to be exempt from. This entry is now dead config and can be removed.`,\n })\n }\n }\n\n return entries\n}\n"],"mappings":"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","names":["existsSync","readFileSync","dirname","join","existsSync","join","dirname","readFileSync"]}
|