@hublo/sentinel 1.5.0 → 1.5.1-alpha.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 +3 -1
- package/dist/bin/sentinel.js +190 -13
- package/dist/{chunk-MOELAQPC.js → chunk-CL7HOHTU.js} +83 -6
- package/dist/{chunk-43HQ6BVJ.js → chunk-I2WDZNPR.js} +55 -19
- package/dist/{chunk-IL5PPLXE.js → chunk-JRZI5IIN.js} +1 -1
- package/dist/{chunk-IL5PPLXE.js.map → chunk-JRZI5IIN.js.map} +1 -1
- package/dist/{chunk-Y57KA4QS.js → chunk-LFNOBQZU.js} +2 -2
- package/dist/index.d.ts +48 -0
- package/dist/index.js +3 -3
- package/dist/roles/test/setup/jest-parity.js +2 -2
- package/dist/roles/test/setup/workspace-entry.js +2 -2
- package/dist/{validate-FRMENKHM.js → validate-S6PIOVAH.js} +2 -2
- package/docs/format-adoption.md +13 -9
- package/docs/lint-adoption.md +21 -9
- package/docs/typescript-adoption.md +1 -0
- package/docs/typescript-traces.md +249 -0
- package/docs/using-sentinel.md +9 -1
- package/package.json +1 -1
- /package/dist/{chunk-Y57KA4QS.js.map → chunk-LFNOBQZU.js.map} +0 -0
package/README.md
CHANGED
|
@@ -57,7 +57,8 @@ pnpm install # fetch what --init declared, then commit
|
|
|
57
57
|
|
|
58
58
|
With no target named, `--init` adopts every role, in the order that makes the result correct:
|
|
59
59
|
the linter's autofix runs, then the formatter runs **last** and formats everything every role
|
|
60
|
-
wrote. Naming
|
|
60
|
+
wrote. Naming a target (`--init --build`) adopts just that role and leaves the rest alone, with
|
|
61
|
+
one pairing: lint and format are always adopted together, so `--init --lint` adopts oxfmt too.
|
|
61
62
|
|
|
62
63
|
`--init` writes the config stubs, the `typecheck`/`lint`/... scripts, and pins the `@hublo/sentinel` devDependency into the module (no manual `pnpm add`); it scaffolds a `package.json` for a `project.json`-only module. It also applies, once, the workspace prep that module needs at the root, only when the root's own config shows it is needed (e.g. an i18next singleton override when the repo runs a second TypeScript, a release-age allow-list when the repo uses that pnpm gate). See the [adoption cheat sheet](docs/typescript-adoption.md) for the full list.
|
|
63
64
|
|
|
@@ -412,6 +413,7 @@ OPTIONS --module <name> from the root: scope to one module
|
|
|
412
413
|
--ci from the root: affected only; non-zero exit on failure
|
|
413
414
|
--fix auto-fix where applicable
|
|
414
415
|
--dry-run preview a --init without writing
|
|
416
|
+
--rehearse with --dry-run: run the autofixes in a copy, name the source they rewrite
|
|
415
417
|
--json machine-readable output (report / inspect / status / --dry-run)
|
|
416
418
|
-- <tool options> --run only: pass the rest to the tool itself (one type)
|
|
417
419
|
|
package/dist/bin/sentinel.js
CHANGED
|
@@ -8,19 +8,20 @@ import {
|
|
|
8
8
|
describeFramework,
|
|
9
9
|
dispatch,
|
|
10
10
|
palette,
|
|
11
|
+
presetNotInstalledYet,
|
|
11
12
|
registerAdapters,
|
|
12
13
|
resolve
|
|
13
|
-
} from "../chunk-
|
|
14
|
+
} from "../chunk-CL7HOHTU.js";
|
|
14
15
|
import {
|
|
15
16
|
resolveContext
|
|
16
|
-
} from "../chunk-
|
|
17
|
+
} from "../chunk-I2WDZNPR.js";
|
|
17
18
|
import {
|
|
18
19
|
ensureWorkspacePrep,
|
|
19
20
|
findWorkspaceRoot,
|
|
20
21
|
inspectWorkspacePrep,
|
|
21
22
|
readOwnVersion,
|
|
22
23
|
readProjectPackageJson
|
|
23
|
-
} from "../chunk-
|
|
24
|
+
} from "../chunk-JRZI5IIN.js";
|
|
24
25
|
import {
|
|
25
26
|
LONG_RUNNING_TARGETS,
|
|
26
27
|
OUTPUT_SHAPES,
|
|
@@ -35,15 +36,129 @@ import "../chunk-QXFCZON7.js";
|
|
|
35
36
|
// bin/sentinel.ts
|
|
36
37
|
import { program } from "commander";
|
|
37
38
|
|
|
39
|
+
// src/core/rehearse.ts
|
|
40
|
+
import { createHash } from "node:crypto";
|
|
41
|
+
import {
|
|
42
|
+
cpSync,
|
|
43
|
+
existsSync,
|
|
44
|
+
lstatSync,
|
|
45
|
+
mkdirSync,
|
|
46
|
+
mkdtempSync,
|
|
47
|
+
readdirSync,
|
|
48
|
+
readFileSync,
|
|
49
|
+
realpathSync,
|
|
50
|
+
rmSync,
|
|
51
|
+
symlinkSync,
|
|
52
|
+
writeFileSync
|
|
53
|
+
} from "node:fs";
|
|
54
|
+
import { tmpdir } from "node:os";
|
|
55
|
+
import { basename, dirname, join, relative, sep } from "node:path";
|
|
56
|
+
var NOT_COPIED = /* @__PURE__ */ new Set(["node_modules", ".git"]);
|
|
57
|
+
function contents(root) {
|
|
58
|
+
const found = /* @__PURE__ */ new Map();
|
|
59
|
+
const queue = [root];
|
|
60
|
+
while (queue.length > 0) {
|
|
61
|
+
const dir = queue.pop();
|
|
62
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
63
|
+
const path = join(dir, entry.name);
|
|
64
|
+
if (entry.isDirectory()) {
|
|
65
|
+
if (!NOT_COPIED.has(entry.name)) queue.push(path);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (!entry.isFile()) continue;
|
|
69
|
+
found.set(relative(root, path), createHash("sha1").update(readFileSync(path)).digest("hex"));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return found;
|
|
73
|
+
}
|
|
74
|
+
function stage(moduleDir, scratch) {
|
|
75
|
+
const root = findWorkspaceRoot(moduleDir);
|
|
76
|
+
const segments = root === void 0 ? [basename(moduleDir)] : relative(root, moduleDir).split(sep).filter((segment) => segment !== "");
|
|
77
|
+
let real = root ?? dirname(moduleDir);
|
|
78
|
+
let shadow = scratch;
|
|
79
|
+
for (const segment of segments) {
|
|
80
|
+
for (const entry of root === void 0 ? [] : readdirSync(real)) {
|
|
81
|
+
if (entry === segment || entry === ".git") continue;
|
|
82
|
+
symlinkSync(join(real, entry), join(shadow, entry));
|
|
83
|
+
}
|
|
84
|
+
real = join(real, segment);
|
|
85
|
+
shadow = join(shadow, segment);
|
|
86
|
+
mkdirSync(shadow);
|
|
87
|
+
}
|
|
88
|
+
rmSync(shadow, { recursive: true, force: true });
|
|
89
|
+
cpSync(moduleDir, shadow, {
|
|
90
|
+
recursive: true,
|
|
91
|
+
filter: (source) => source === moduleDir || !NOT_COPIED.has(basename(source)) && !lstatSync(source).isSymbolicLink()
|
|
92
|
+
});
|
|
93
|
+
if (existsSync(join(moduleDir, "node_modules"))) {
|
|
94
|
+
symlinkSync(join(moduleDir, "node_modules"), join(shadow, "node_modules"), "dir");
|
|
95
|
+
}
|
|
96
|
+
return shadow;
|
|
97
|
+
}
|
|
98
|
+
async function rehearseAfterInit(moduleDir, roles) {
|
|
99
|
+
const rehearsing = roles.filter((role) => role.adapter.rehearseAfterInit !== void 0);
|
|
100
|
+
if (rehearsing.length === 0) return [];
|
|
101
|
+
const scratch = realpathSync(mkdtempSync(join(tmpdir(), "sentinel-rehearsal-")));
|
|
102
|
+
try {
|
|
103
|
+
let copy;
|
|
104
|
+
try {
|
|
105
|
+
copy = stage(moduleDir, scratch);
|
|
106
|
+
} catch (error) {
|
|
107
|
+
const failure = error instanceof Error ? error.message : String(error);
|
|
108
|
+
return rehearsing.map((role) => ({ target: role.target, rewrites: [], failure }));
|
|
109
|
+
}
|
|
110
|
+
const planned = /* @__PURE__ */ new Set();
|
|
111
|
+
for (const role of roles) {
|
|
112
|
+
for (const file of role.files) {
|
|
113
|
+
planned.add(file.path);
|
|
114
|
+
const target = join(copy, file.path);
|
|
115
|
+
if (file.deleted) {
|
|
116
|
+
rmSync(target, { force: true });
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
120
|
+
writeFileSync(target, file.after);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const results = [];
|
|
124
|
+
for (const role of rehearsing) {
|
|
125
|
+
const before = contents(copy);
|
|
126
|
+
const failure = await role.adapter.rehearseAfterInit?.(
|
|
127
|
+
{
|
|
128
|
+
module: basename(moduleDir),
|
|
129
|
+
cwd: copy,
|
|
130
|
+
preset: role.preset,
|
|
131
|
+
ci: false,
|
|
132
|
+
fix: true,
|
|
133
|
+
written: role.files.filter((file) => !file.deleted).map((file) => file.path)
|
|
134
|
+
},
|
|
135
|
+
moduleDir
|
|
136
|
+
);
|
|
137
|
+
const after = contents(copy);
|
|
138
|
+
const rewrites = [...after].filter(([path, hash]) => before.get(path) !== hash && !planned.has(path)).map(([path]) => path).sort();
|
|
139
|
+
results.push({
|
|
140
|
+
target: role.target,
|
|
141
|
+
rewrites,
|
|
142
|
+
// The tool names files by the path it read them from, which is the copy. Said as the
|
|
143
|
+
// module's own paths: the copy is gone by the time anyone reads this.
|
|
144
|
+
...failure === void 0 ? {} : { failure: failure.replaceAll(`${copy}/`, "").replaceAll(copy, ".") }
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
return results;
|
|
148
|
+
} finally {
|
|
149
|
+
rmSync(scratch, { recursive: true, force: true });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
38
153
|
// src/roles/build/declined.ts
|
|
39
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
40
|
-
import { join } from "node:path";
|
|
154
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
|
|
155
|
+
import { join as join2 } from "node:path";
|
|
41
156
|
var DEPRECATED_NX_BUILDER = "@nx/webpack:webpack";
|
|
42
157
|
function buildsWithDeprecatedExecutor(cwd2) {
|
|
43
|
-
const projectJson =
|
|
44
|
-
if (!
|
|
158
|
+
const projectJson = join2(cwd2, "project.json");
|
|
159
|
+
if (!existsSync2(projectJson)) return false;
|
|
45
160
|
try {
|
|
46
|
-
const project = JSON.parse(
|
|
161
|
+
const project = JSON.parse(readFileSync2(projectJson, "utf8"));
|
|
47
162
|
return project.targets?.build?.executor === DEPRECATED_NX_BUILDER;
|
|
48
163
|
} catch {
|
|
49
164
|
return false;
|
|
@@ -63,6 +178,30 @@ function explainDecline(cwd2) {
|
|
|
63
178
|
function asMessage(error) {
|
|
64
179
|
return error instanceof Error ? error.message : String(error);
|
|
65
180
|
}
|
|
181
|
+
var ADOPTED_TOGETHER = ["lint", "format"];
|
|
182
|
+
function pairedTargets(named, available) {
|
|
183
|
+
if (!named.some((type) => ADOPTED_TOGETHER.includes(type))) return [];
|
|
184
|
+
return ADOPTED_TOGETHER.filter((type) => available.includes(type) && !named.includes(type));
|
|
185
|
+
}
|
|
186
|
+
var NAMED_REWRITES = 10;
|
|
187
|
+
var AUTOFIX_TOOL = { lint: "linter", format: "formatter" };
|
|
188
|
+
function describeUnrehearsed(target) {
|
|
189
|
+
return `the ${target} autofix runs on adoption and can rewrite source, as moving to another ${AUTOFIX_TOOL[target] ?? "tool"} does; add --rehearse to name the files (it runs the pass in a throwaway copy, as long as the real one)`;
|
|
190
|
+
}
|
|
191
|
+
function describeRehearsal(target, rewrites, failure) {
|
|
192
|
+
const pass = `the ${target} autofix`;
|
|
193
|
+
const said = failure === void 0 ? "" : ` (it also reported: ${failure})`;
|
|
194
|
+
if (rewrites.length === 0) {
|
|
195
|
+
if (failure === void 0) return `${pass} would rewrite nothing beyond the files above`;
|
|
196
|
+
if (presetNotInstalledYet(failure)) {
|
|
197
|
+
return `${pass} cannot be rehearsed yet: its preset is not installed in this module, which is expected before the first \`pnpm install\`. After it, this dry-run names what it rewrites`;
|
|
198
|
+
}
|
|
199
|
+
return `could not rehearse ${pass}, so what it rewrites is not known: ${failure}`;
|
|
200
|
+
}
|
|
201
|
+
const named = rewrites.slice(0, NAMED_REWRITES).join(", ");
|
|
202
|
+
const rest = rewrites.length - NAMED_REWRITES;
|
|
203
|
+
return `${pass} would rewrite ${rewrites.length} file${rewrites.length === 1 ? "" : "s"} beyond the ones above: ${rest > 0 ? `${named} and ${rest} more` : named}${said}`;
|
|
204
|
+
}
|
|
66
205
|
async function runInit(ctx) {
|
|
67
206
|
const { modules, scope } = resolveContext(ctx.cwd, { module: ctx.module, ci: false });
|
|
68
207
|
const [module] = modules;
|
|
@@ -80,11 +219,24 @@ async function runInit(ctx) {
|
|
|
80
219
|
);
|
|
81
220
|
}
|
|
82
221
|
}
|
|
83
|
-
const
|
|
222
|
+
const named = ctx.targets.filter((type) => available.includes(type));
|
|
223
|
+
const paired = pairedTargets(named, available);
|
|
224
|
+
const toRun = [...named, ...paired].sort(
|
|
225
|
+
(a, b) => Number(a === "format") - Number(b === "format")
|
|
226
|
+
);
|
|
227
|
+
for (const type of paired) {
|
|
228
|
+
process.stderr.write(
|
|
229
|
+
palette(process.stderr).dim(
|
|
230
|
+
` adopting --${type} too: lint and format are adopted together, see the lint adoption guide
|
|
231
|
+
`
|
|
232
|
+
)
|
|
233
|
+
);
|
|
234
|
+
}
|
|
84
235
|
const outcomes = [];
|
|
85
236
|
const record = (outcome) => {
|
|
86
237
|
outcomes.push(outcome);
|
|
87
238
|
};
|
|
239
|
+
const planned = [];
|
|
88
240
|
let worst = 0;
|
|
89
241
|
for (const type of toRun) {
|
|
90
242
|
try {
|
|
@@ -96,12 +248,13 @@ async function runInit(ctx) {
|
|
|
96
248
|
preset: ctx.preset,
|
|
97
249
|
dryRun: ctx.dryRun,
|
|
98
250
|
json: ctx.json,
|
|
99
|
-
record
|
|
251
|
+
record,
|
|
252
|
+
planned: (role) => planned.push(role)
|
|
100
253
|
});
|
|
101
254
|
worst = Math.max(worst, code);
|
|
102
255
|
} catch (error) {
|
|
103
256
|
if (error instanceof PresetUnsupportedError) {
|
|
104
|
-
if (ctx.targetsExplicit) {
|
|
257
|
+
if (ctx.targetsExplicit && !paired.includes(type)) {
|
|
105
258
|
const hint = type === "build" ? explainDecline(module.root) : ` Pass --preset if it was detected wrongly (hoisted dependencies make detection fall back to "node").`;
|
|
106
259
|
process.stderr.write(
|
|
107
260
|
`
|
|
@@ -124,6 +277,21 @@ sentinel (${type}): ${asMessage(error)}
|
|
|
124
277
|
worst = Math.max(worst, 1);
|
|
125
278
|
}
|
|
126
279
|
}
|
|
280
|
+
if (ctx.dryRun && worst === 0 && !ctx.rehearse && !ctx.json) {
|
|
281
|
+
for (const { target } of planned.filter((role) => role.adapter.rehearseAfterInit)) {
|
|
282
|
+
process.stderr.write(` dry run: ${describeUnrehearsed(target)}
|
|
283
|
+
`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (ctx.dryRun && worst === 0 && ctx.rehearse) {
|
|
287
|
+
for (const { target, rewrites, failure } of await rehearseAfterInit(module.root, planned)) {
|
|
288
|
+
const outcome = outcomes.find((entry) => entry.role === target);
|
|
289
|
+
if (outcome !== void 0) outcome.rewrites = rewrites;
|
|
290
|
+
if (ctx.json) continue;
|
|
291
|
+
process.stderr.write(` dry run: ${describeRehearsal(target, rewrites, failure)}
|
|
292
|
+
`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
127
295
|
let workspace = [];
|
|
128
296
|
if (worst === 0) {
|
|
129
297
|
const root = findWorkspaceRoot(module.root);
|
|
@@ -172,7 +340,7 @@ async function runValidate(ctx) {
|
|
|
172
340
|
);
|
|
173
341
|
}
|
|
174
342
|
if (!ctx.targets.includes("test")) return 1;
|
|
175
|
-
const { validateModuleTests } = await import("../validate-
|
|
343
|
+
const { validateModuleTests } = await import("../validate-S6PIOVAH.js");
|
|
176
344
|
const outcomes = modules.flatMap(
|
|
177
345
|
(module) => validateModuleTests(module.name, module.root)
|
|
178
346
|
);
|
|
@@ -627,7 +795,10 @@ program.name("sentinel").description("One CLI that guards code health: presets,
|
|
|
627
795
|
).option("--report", "deprecated: use --run --json").option("--status", "deprecated: use --inspect").option(
|
|
628
796
|
"--module <name>",
|
|
629
797
|
"from the workspace root: scope to one module (omit = all; inside a module dir, drop this)"
|
|
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(
|
|
798
|
+
).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(
|
|
799
|
+
"--rehearse",
|
|
800
|
+
"with --init --dry-run: run the lint and format autofixes in a throwaway copy and name the source they rewrite (as slow as the real pass)"
|
|
801
|
+
).option("--json", "machine-readable output, for every verb").option(
|
|
631
802
|
"--output <shape>",
|
|
632
803
|
"output shape for a role that writes reports (console, json, html, markdown); console by default"
|
|
633
804
|
).option(
|
|
@@ -742,6 +913,11 @@ if (!Number.isInteger(maxDiagnostics) || maxDiagnostics < 0) {
|
|
|
742
913
|
if (opts.dryRun && verb !== "init") {
|
|
743
914
|
program.error("--dry-run only applies to --init (the read verbs never write).");
|
|
744
915
|
}
|
|
916
|
+
if (opts.rehearse && !opts.dryRun) {
|
|
917
|
+
program.error(
|
|
918
|
+
"--rehearse only applies to --init --dry-run: the real --init runs the autofix passes themselves."
|
|
919
|
+
);
|
|
920
|
+
}
|
|
745
921
|
if (toolArgs.length > 0) {
|
|
746
922
|
if (verb !== "run") {
|
|
747
923
|
program.error(
|
|
@@ -781,6 +957,7 @@ async function main() {
|
|
|
781
957
|
// a role that writes reports: two ways to ask for the same thing would be a trap.
|
|
782
958
|
output: parseOutput(opts.output, Boolean(opts.json)) ?? (opts.json ? "json" : void 0),
|
|
783
959
|
dryRun: Boolean(opts.dryRun),
|
|
960
|
+
rehearse: Boolean(opts.rehearse),
|
|
784
961
|
fail: (message) => program.error(message)
|
|
785
962
|
};
|
|
786
963
|
const exitCode = verb === "init" ? await runInit(context) : verb === "validate" ? await runValidate(context) : await runVerb(context);
|
|
@@ -55,7 +55,7 @@ import {
|
|
|
55
55
|
splitToolArgs,
|
|
56
56
|
walk,
|
|
57
57
|
writtenFileFormatter
|
|
58
|
-
} from "./chunk-
|
|
58
|
+
} from "./chunk-I2WDZNPR.js";
|
|
59
59
|
import {
|
|
60
60
|
DEFAULT_MAX_DIAGNOSTICS,
|
|
61
61
|
WORKSPACE_ROOT_MARKER,
|
|
@@ -64,7 +64,7 @@ import {
|
|
|
64
64
|
ownPackageDir,
|
|
65
65
|
readOwnPackage,
|
|
66
66
|
readProjectPackageJson
|
|
67
|
-
} from "./chunk-
|
|
67
|
+
} from "./chunk-JRZI5IIN.js";
|
|
68
68
|
import {
|
|
69
69
|
PRESET_NAMES,
|
|
70
70
|
REPORT_DIR,
|
|
@@ -1655,6 +1655,22 @@ var OxfmtAdapter = class extends BaseAdapter {
|
|
|
1655
1655
|
* walks targets in order), so what the linter rewrote gets formatted rather than the
|
|
1656
1656
|
* other way round.
|
|
1657
1657
|
*/
|
|
1658
|
+
/**
|
|
1659
|
+
* The same `oxfmt --write .`, in the copy `--init --dry-run` hands it, and silent. See
|
|
1660
|
+
* `Adapter.rehearseAfterInit`. It runs after the lint rehearsal for the reason `afterInit` runs
|
|
1661
|
+
* after the lint fix, so what the linter rewrote is what gets formatted.
|
|
1662
|
+
*
|
|
1663
|
+
* The copy sits where the module sits in a stand-in workspace, and that matters to this tool in
|
|
1664
|
+
* particular: oxfmt reads the nearest `.editorconfig` upwards (verified: an `indent_style = tab`
|
|
1665
|
+
* two levels up reindented a file), and the root of this repo has one.
|
|
1666
|
+
*/
|
|
1667
|
+
async rehearseAfterInit(ctx, origin) {
|
|
1668
|
+
const oxfmt = resolveOxfmt(origin);
|
|
1669
|
+
if (!oxfmt) return "oxfmt is not installed in this module yet";
|
|
1670
|
+
const pass = spawnSync3(oxfmt, ["--write", "."], { cwd: ctx.cwd, encoding: "utf8" });
|
|
1671
|
+
if (pass.error) return pass.error.message;
|
|
1672
|
+
return pass.status === 0 ? void 0 : oxfmtSaid(`${pass.stdout ?? ""}${pass.stderr ?? ""}`);
|
|
1673
|
+
}
|
|
1658
1674
|
async afterInit(ctx) {
|
|
1659
1675
|
const oxfmt = resolveOxfmt(ctx.cwd);
|
|
1660
1676
|
if (!oxfmt) return;
|
|
@@ -1967,6 +1983,9 @@ function changedSince(cwd, before, extensions) {
|
|
|
1967
1983
|
}
|
|
1968
1984
|
var NAMED_IN_REPORT = 5;
|
|
1969
1985
|
var NOT_INSTALLED_YET = /invalid config file[\s\S]*node_modules/i;
|
|
1986
|
+
function presetNotInstalledYet(failure) {
|
|
1987
|
+
return NOT_INSTALLED_YET.test(failure);
|
|
1988
|
+
}
|
|
1970
1989
|
function describeFixOutcome(outcome, toolLabel) {
|
|
1971
1990
|
if (outcome.failure !== void 0) {
|
|
1972
1991
|
if (NOT_INSTALLED_YET.test(outcome.failure)) {
|
|
@@ -4788,8 +4807,13 @@ function plan2(context2) {
|
|
|
4788
4807
|
...rootIgnores.patterns
|
|
4789
4808
|
].filter((pattern) => !defaults2.has(bare(pattern)));
|
|
4790
4809
|
const carried = [.../* @__PURE__ */ new Set([...DEFAULT_IGNORE_PATTERNS, ...moduleOwn])];
|
|
4810
|
+
const { kept: teamRules, governed: droppedRules } = moduleOwnRules(
|
|
4811
|
+
committedRules(context2.cwd),
|
|
4812
|
+
lintPresetFor(variant)
|
|
4813
|
+
);
|
|
4791
4814
|
const stub = {
|
|
4792
4815
|
extends: extendsWithPreset(committedExtends(context2.cwd), variant),
|
|
4816
|
+
...Object.keys(teamRules).length > 0 ? { rules: teamRules } : {},
|
|
4793
4817
|
...carried.length > 0 ? { ignorePatterns: carried } : {}
|
|
4794
4818
|
};
|
|
4795
4819
|
const operations = [
|
|
@@ -4838,6 +4862,16 @@ function plan2(context2) {
|
|
|
4838
4862
|
if (eslintConfigs.length > 0) {
|
|
4839
4863
|
notes.push(`removed ${eslintConfigs.join(", ")} and pointed the nx lint target at oxlint`);
|
|
4840
4864
|
}
|
|
4865
|
+
if (Object.keys(teamRules).length > 0) {
|
|
4866
|
+
notes.push(
|
|
4867
|
+
`kept ${Object.keys(teamRules).length} rule(s) this module set itself (${Object.keys(teamRules).join(", ")}): the preset does not configure them`
|
|
4868
|
+
);
|
|
4869
|
+
}
|
|
4870
|
+
if (droppedRules.length > 0) {
|
|
4871
|
+
notes.push(
|
|
4872
|
+
`dropped ${droppedRules.length} rule(s) this module set over the preset (${droppedRules.join(", ")}): the preset governs them`
|
|
4873
|
+
);
|
|
4874
|
+
}
|
|
4841
4875
|
if (rootIgnores.patterns.length > 0) {
|
|
4842
4876
|
notes.push(
|
|
4843
4877
|
`carried ${rootIgnores.patterns.length} ignore pattern(s) from the workspace eslint config (${rootIgnores.patterns.join(", ")}): this module is linted by that config today, so without them adoption would start linting what the repo has always excluded`
|
|
@@ -4867,7 +4901,7 @@ function plan2(context2) {
|
|
|
4867
4901
|
if (keepsOtherCommands3(script)) {
|
|
4868
4902
|
const stillPrettier = /(^|[\s/])prettier(\s|$)/.test(script);
|
|
4869
4903
|
notes.push(
|
|
4870
|
-
`kept the rest of the lint script (\`${script}\`): only the eslint part is sentinel's` + (stillPrettier ? "
|
|
4904
|
+
`kept the rest of the lint script (\`${script}\`): only the eslint part is sentinel's` + (stillPrettier ? "; the format role, adopted with it, moves the prettier part" : "")
|
|
4871
4905
|
);
|
|
4872
4906
|
}
|
|
4873
4907
|
notes.push(
|
|
@@ -4910,6 +4944,31 @@ function committedExtends(cwd) {
|
|
|
4910
4944
|
return [];
|
|
4911
4945
|
}
|
|
4912
4946
|
}
|
|
4947
|
+
function committedRules(cwd) {
|
|
4948
|
+
try {
|
|
4949
|
+
const parsed = parseJsonc(
|
|
4950
|
+
readFileSync12(join15(cwd, LINT_CONFIG_FILE), "utf8"),
|
|
4951
|
+
LINT_CONFIG_FILE
|
|
4952
|
+
);
|
|
4953
|
+
const rules = parsed.rules;
|
|
4954
|
+
return typeof rules === "object" && rules !== null && !Array.isArray(rules) ? rules : {};
|
|
4955
|
+
} catch {
|
|
4956
|
+
return {};
|
|
4957
|
+
}
|
|
4958
|
+
}
|
|
4959
|
+
function moduleOwnRules(committed, preset) {
|
|
4960
|
+
const governedNames = /* @__PURE__ */ new Set([
|
|
4961
|
+
...Object.keys(preset.rules),
|
|
4962
|
+
...(preset.overrides ?? []).flatMap((override) => Object.keys(override.rules))
|
|
4963
|
+
]);
|
|
4964
|
+
const kept = {};
|
|
4965
|
+
const governed = [];
|
|
4966
|
+
for (const [rule, value] of Object.entries(committed)) {
|
|
4967
|
+
if (governedNames.has(rule)) governed.push(rule);
|
|
4968
|
+
else kept[rule] = value;
|
|
4969
|
+
}
|
|
4970
|
+
return { kept, governed };
|
|
4971
|
+
}
|
|
4913
4972
|
function committedIgnorePatterns(cwd) {
|
|
4914
4973
|
try {
|
|
4915
4974
|
const parsed = parseJsonc(
|
|
@@ -5041,6 +5100,16 @@ ${result.stderr ?? ""}`);
|
|
|
5041
5100
|
`);
|
|
5042
5101
|
this.writeModuleBaseline(ctx, oxlint, env);
|
|
5043
5102
|
}
|
|
5103
|
+
/**
|
|
5104
|
+
* The fix pass alone, in the copy `--init --dry-run` hands it, without the baseline measurement
|
|
5105
|
+
* that follows it for real. See `Adapter.rehearseAfterInit`.
|
|
5106
|
+
*/
|
|
5107
|
+
async rehearseAfterInit(ctx, origin) {
|
|
5108
|
+
const oxlint = resolveOxlint(origin);
|
|
5109
|
+
if (!oxlint) return "oxlint is not installed in this module yet";
|
|
5110
|
+
const env = { ...process.env, PATH: oxlintPath(origin, process.env) };
|
|
5111
|
+
return this.fixPass(ctx, oxlint, env).failure;
|
|
5112
|
+
}
|
|
5044
5113
|
/**
|
|
5045
5114
|
* Measure what THIS module already violates, and hold exactly those rules at `warn`.
|
|
5046
5115
|
*
|
|
@@ -10221,8 +10290,8 @@ function resolveFlavour(opts) {
|
|
|
10221
10290
|
}
|
|
10222
10291
|
return detection.preset;
|
|
10223
10292
|
}
|
|
10224
|
-
function previewPlan(opts, plan3) {
|
|
10225
|
-
const changed =
|
|
10293
|
+
function previewPlan(opts, plan3, prepared) {
|
|
10294
|
+
const changed = prepared.filter((file) => file.before !== file.after);
|
|
10226
10295
|
if (opts.json) {
|
|
10227
10296
|
opts.record?.({
|
|
10228
10297
|
role: opts.target,
|
|
@@ -10304,7 +10373,14 @@ async function dispatch(opts) {
|
|
|
10304
10373
|
return 0;
|
|
10305
10374
|
}
|
|
10306
10375
|
if (opts.dryRun) {
|
|
10307
|
-
|
|
10376
|
+
const prepared = preparePlan(opts.cwd, plan3);
|
|
10377
|
+
opts.planned?.({
|
|
10378
|
+
target: opts.target,
|
|
10379
|
+
adapter,
|
|
10380
|
+
preset,
|
|
10381
|
+
files: prepared.filter((file) => file.before !== file.after)
|
|
10382
|
+
});
|
|
10383
|
+
return previewPlan(opts, plan3, prepared);
|
|
10308
10384
|
}
|
|
10309
10385
|
if (adapter.beforeInit) {
|
|
10310
10386
|
await adapter.beforeInit({
|
|
@@ -10357,6 +10433,7 @@ export {
|
|
|
10357
10433
|
resolve,
|
|
10358
10434
|
buildConfigFile,
|
|
10359
10435
|
palette,
|
|
10436
|
+
presetNotInstalledYet,
|
|
10360
10437
|
registerAdapters,
|
|
10361
10438
|
correctingSignal,
|
|
10362
10439
|
describeFramework,
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
readNxProjectName,
|
|
7
7
|
readOwnPackage,
|
|
8
8
|
readProjectPackageJson
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-JRZI5IIN.js";
|
|
10
10
|
import {
|
|
11
11
|
matchesTsconfigPattern
|
|
12
12
|
} from "./chunk-O7REVMOC.js";
|
|
@@ -819,42 +819,78 @@ function lineLostByTheFixer(before, after) {
|
|
|
819
819
|
}
|
|
820
820
|
|
|
821
821
|
// src/core/config/nx-target.ts
|
|
822
|
-
import { existsSync as
|
|
822
|
+
import { existsSync as existsSync8 } from "node:fs";
|
|
823
|
+
import { join as join8 } from "node:path";
|
|
824
|
+
|
|
825
|
+
// src/core/config/project-json.ts
|
|
826
|
+
import { existsSync as existsSync7, readFileSync as readFileSync6 } from "node:fs";
|
|
823
827
|
import { join as join7 } from "node:path";
|
|
828
|
+
function projectTargets(cwd) {
|
|
829
|
+
const path = join7(cwd, "project.json");
|
|
830
|
+
if (!existsSync7(path)) return void 0;
|
|
831
|
+
try {
|
|
832
|
+
return JSON.parse(readFileSync6(path, "utf8")).targets;
|
|
833
|
+
} catch {
|
|
834
|
+
return void 0;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// src/core/config/nx-target.ts
|
|
824
839
|
function nxTargetMetadataOperations(options) {
|
|
825
840
|
const { targets } = options;
|
|
826
841
|
if (Object.keys(targets).length === 0) return [];
|
|
827
842
|
return [{ kind: "merge-json", path: "package.json", value: { nx: { targets } } }];
|
|
828
843
|
}
|
|
844
|
+
function sameInput(left, right) {
|
|
845
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
846
|
+
}
|
|
847
|
+
function inputsOf(target) {
|
|
848
|
+
const inputs = target?.inputs;
|
|
849
|
+
return Array.isArray(inputs) ? inputs : [];
|
|
850
|
+
}
|
|
851
|
+
function annotationOf(target) {
|
|
852
|
+
if (typeof target !== "object" || target === null) return void 0;
|
|
853
|
+
const { executor, command } = target;
|
|
854
|
+
return executor === void 0 && command === void 0 ? target : void 0;
|
|
855
|
+
}
|
|
856
|
+
function withTeamInputs(cwd, targets) {
|
|
857
|
+
const committed = manifestTargets(cwd);
|
|
858
|
+
const annotated = projectTargets(cwd) ?? {};
|
|
859
|
+
const merged = {};
|
|
860
|
+
for (const [name, metadata] of Object.entries(targets)) {
|
|
861
|
+
const ours = metadata.inputs;
|
|
862
|
+
if (!Array.isArray(ours)) {
|
|
863
|
+
merged[name] = metadata;
|
|
864
|
+
continue;
|
|
865
|
+
}
|
|
866
|
+
const inputs = [...ours];
|
|
867
|
+
for (const entry of [
|
|
868
|
+
...inputsOf(committed[name]),
|
|
869
|
+
...inputsOf(annotationOf(annotated[name]))
|
|
870
|
+
]) {
|
|
871
|
+
if (!inputs.some((kept) => sameInput(kept, entry))) inputs.push(entry);
|
|
872
|
+
}
|
|
873
|
+
merged[name] = { ...metadata, inputs };
|
|
874
|
+
}
|
|
875
|
+
return merged;
|
|
876
|
+
}
|
|
829
877
|
function nxTargetOperations(options) {
|
|
830
|
-
const { cwd
|
|
831
|
-
const names = Object.keys(targets);
|
|
878
|
+
const { cwd } = options;
|
|
879
|
+
const names = Object.keys(options.targets);
|
|
832
880
|
if (names.length === 0) return [];
|
|
881
|
+
const targets = withTeamInputs(cwd, options.targets);
|
|
833
882
|
const operations = [];
|
|
834
|
-
if (
|
|
883
|
+
if (existsSync8(join8(cwd, "project.json"))) {
|
|
835
884
|
operations.push({
|
|
836
885
|
kind: "remove-json-keys",
|
|
837
886
|
path: "project.json",
|
|
838
887
|
keys: names.map((name) => ["targets", name])
|
|
839
888
|
});
|
|
840
889
|
}
|
|
841
|
-
operations.push(...nxTargetMetadataOperations(
|
|
890
|
+
operations.push(...nxTargetMetadataOperations({ cwd, targets }));
|
|
842
891
|
return operations;
|
|
843
892
|
}
|
|
844
893
|
|
|
845
|
-
// src/core/config/project-json.ts
|
|
846
|
-
import { existsSync as existsSync8, readFileSync as readFileSync6 } from "node:fs";
|
|
847
|
-
import { join as join8 } from "node:path";
|
|
848
|
-
function projectTargets(cwd) {
|
|
849
|
-
const path = join8(cwd, "project.json");
|
|
850
|
-
if (!existsSync8(path)) return void 0;
|
|
851
|
-
try {
|
|
852
|
-
return JSON.parse(readFileSync6(path, "utf8")).targets;
|
|
853
|
-
} catch {
|
|
854
|
-
return void 0;
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
|
|
858
894
|
// src/core/config/rewrite-imports.ts
|
|
859
895
|
import { parseSync } from "oxc-parser";
|
|
860
896
|
var movedSpecifiers = (toolchain) => new Set(toolchain.owned.map((entry) => entry.specifier));
|