@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 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 one target (`--init --lint`) adopts just that role and leaves the rest alone.
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
 
@@ -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-MOELAQPC.js";
14
+ } from "../chunk-CL7HOHTU.js";
14
15
  import {
15
16
  resolveContext
16
- } from "../chunk-43HQ6BVJ.js";
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-IL5PPLXE.js";
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 = join(cwd2, "project.json");
44
- if (!existsSync(projectJson)) return false;
158
+ const projectJson = join2(cwd2, "project.json");
159
+ if (!existsSync2(projectJson)) return false;
45
160
  try {
46
- const project = JSON.parse(readFileSync(projectJson, "utf8"));
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 toRun = ctx.targets.filter((type) => available.includes(type)).sort((a, b) => Number(a === "format") - Number(b === "format"));
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-FRMENKHM.js");
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("--json", "machine-readable output, for every verb").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-43HQ6BVJ.js";
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-IL5PPLXE.js";
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 ? ". Run `sentinel --init --format` to move the formatting part too" : "")
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 = preparePlan(opts.cwd, plan3).filter((file) => file.before !== file.after);
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
- return previewPlan(opts, plan3);
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-IL5PPLXE.js";
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 existsSync7 } from "node:fs";
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, targets } = options;
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 (existsSync7(join7(cwd, "project.json"))) {
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(options));
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));
@@ -298,4 +298,4 @@ export {
298
298
  ensureWorkspacePrep,
299
299
  inspectWorkspacePrep
300
300
  };
301
- //# sourceMappingURL=chunk-IL5PPLXE.js.map
301
+ //# sourceMappingURL=chunk-JRZI5IIN.js.map