@hublo/sentinel 1.2.0-alpha.34 → 1.2.0-alpha.36

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.
@@ -596,9 +596,9 @@ function binFromOwnInstall(packageName, binName) {
596
596
  try {
597
597
  const manifest = require2.resolve(`${packageName}/package.json`);
598
598
  const bin = require2(manifest).bin;
599
- const relative5 = typeof bin === "string" ? bin : bin?.[binName];
600
- if (!relative5) return void 0;
601
- const executable = join6(dirname2(manifest), relative5);
599
+ const relative7 = typeof bin === "string" ? bin : bin?.[binName];
600
+ if (!relative7) return void 0;
601
+ const executable = join6(dirname2(manifest), relative7);
602
602
  return existsSync7(executable) ? executable : void 0;
603
603
  } catch {
604
604
  return void 0;
@@ -1005,9 +1005,9 @@ function nestAdoptionOperations(cwd, target) {
1005
1005
 
1006
1006
  // src/roles/build/requirements.ts
1007
1007
  function atLeast(declared, needed) {
1008
- const parse2 = (value) => value.replace(/^[\^~>=<\s]+/, "").split("-")[0].split(".").map((part) => Number.parseInt(part, 10) || 0);
1009
- const left = parse2(declared);
1010
- const right = parse2(needed);
1008
+ const parse3 = (value) => value.replace(/^[\^~>=<\s]+/, "").split("-")[0].split(".").map((part) => Number.parseInt(part, 10) || 0);
1009
+ const left = parse3(declared);
1010
+ const right = parse3(needed);
1011
1011
  for (let at = 0; at < Math.max(left.length, right.length); at++) {
1012
1012
  const a = left[at] ?? 0;
1013
1013
  const b = right[at] ?? 0;
@@ -4692,6 +4692,23 @@ import { join as join27 } from "path";
4692
4692
  import { existsSync as existsSync22, readFileSync as readFileSync19 } from "fs";
4693
4693
  import { join as join25 } from "path";
4694
4694
  import { parseSync as parseSync2 } from "oxc-parser";
4695
+
4696
+ // src/shared/ast.ts
4697
+ var SKIP = /* @__PURE__ */ Symbol("skip-subtree");
4698
+ function walk(node, visit) {
4699
+ if (node === null || typeof node !== "object") return;
4700
+ if (Array.isArray(node)) {
4701
+ for (const entry of node) walk(entry, visit);
4702
+ return;
4703
+ }
4704
+ const current = node;
4705
+ if (typeof current.type === "string" && visit(current) === SKIP) return;
4706
+ for (const value of Object.values(current)) {
4707
+ if (value !== null && typeof value === "object") walk(value, visit);
4708
+ }
4709
+ }
4710
+
4711
+ // src/roles/lint/eslint-ignores.ts
4695
4712
  var OPAQUE_SOURCE = /\b(includeIgnoreFile)\s*\([^)]*\)/g;
4696
4713
  function toOxlintPattern(pattern) {
4697
4714
  return pattern.replace(/^\.\//, "");
@@ -4737,16 +4754,6 @@ function readEslintIgnores(cwd) {
4737
4754
  });
4738
4755
  return { patterns: [...patterns], unresolved: [...unresolved] };
4739
4756
  }
4740
- function walk(node, visit) {
4741
- if (!node || typeof node !== "object") return;
4742
- const current = node;
4743
- if (typeof current.type === "string") visit(current);
4744
- for (const key of Object.keys(current)) {
4745
- const value = current[key];
4746
- if (Array.isArray(value)) for (const entry of value) walk(entry, visit);
4747
- else if (value && typeof value === "object") walk(value, visit);
4748
- }
4749
- }
4750
4757
  function calleeName(node) {
4751
4758
  const callee = node.callee;
4752
4759
  return callee?.type === "Identifier" ? callee.name : void 0;
@@ -5611,7 +5618,15 @@ var toolchain_default2 = {
5611
5618
  var OWNED_TEST_PACKAGES = toolchain_default2.owned;
5612
5619
 
5613
5620
  // src/roles/test/config-policy.ts
5614
- var TEST_PRESET_SPECIFIER = "@hublo/sentinel/test/react";
5621
+ var TEST_PRESET_SPECIFIERS = {
5622
+ react: "@hublo/sentinel/test/react",
5623
+ nest: "@hublo/sentinel/test/nest"
5624
+ };
5625
+ function testPresetSpecifier(preset) {
5626
+ return TEST_PRESET_SPECIFIERS[preset] ?? TEST_PRESET_SPECIFIERS.react;
5627
+ }
5628
+ var ALL_TEST_PRESET_SPECIFIERS = Object.values(TEST_PRESET_SPECIFIERS);
5629
+ var TEST_PRESET_SPECIFIER = TEST_PRESET_SPECIFIERS.react;
5615
5630
  var TEST_CONFIG_FILES = [
5616
5631
  "vitest.config.ts",
5617
5632
  "vitest.config.mts",
@@ -5622,10 +5637,9 @@ var TEST_OWNED_PACKAGES = OWNED_TEST_PACKAGES;
5622
5637
  var SENTINEL_OWNED_TEST_PACKAGES = TEST_OWNED_PACKAGES.filter(
5623
5638
  (entry) => entry.package !== "vite"
5624
5639
  ).map((entry) => entry.package);
5625
- var TEST_TOOLCHAIN = {
5626
- presetSpecifier: TEST_PRESET_SPECIFIER,
5627
- owned: TEST_OWNED_PACKAGES
5628
- };
5640
+ function testToolchain(preset) {
5641
+ return { presetSpecifier: testPresetSpecifier(preset), owned: TEST_OWNED_PACKAGES };
5642
+ }
5629
5643
  var TEST_SCRIPT_NAME = "test";
5630
5644
  var SENTINEL_TEST_COMMAND = "sentinel --run --test --";
5631
5645
  var VITEST_INVOCATION = /(^|&&\s*|\|\|\s*|;\s*)(?:npx\s+|pnpm\s+(?:exec\s+|dlx\s+)?)?vitest\b([^&|;]*)/g;
@@ -5654,9 +5668,17 @@ function testTarget(options) {
5654
5668
  };
5655
5669
  }
5656
5670
 
5671
+ // src/roles/test/jest-migration.ts
5672
+ import { existsSync as existsSync27, readFileSync as readFileSync26 } from "fs";
5673
+ import { join as join32 } from "path";
5674
+
5675
+ // src/roles/test/codemod/migrate-module.ts
5676
+ import { readFileSync as readFileSync24 } from "fs";
5677
+ import { join as join30 } from "path";
5678
+
5657
5679
  // src/roles/test/read-adoption.ts
5658
5680
  import { existsSync as existsSync25, readFileSync as readFileSync23, readdirSync as readdirSync3 } from "fs";
5659
- import { join as join29 } from "path";
5681
+ import { join as join29, relative as relative5 } from "path";
5660
5682
  var JEST_CONFIG_FILES = [
5661
5683
  "jest.config.ts",
5662
5684
  "jest.config.js",
@@ -5678,6 +5700,13 @@ function rootJestConfigFiles(cwd) {
5678
5700
  var TEST_FILE = /[.-](spec|test)\.[cm]?[jt]sx?$/;
5679
5701
  var NOT_WALKED = /* @__PURE__ */ new Set(["node_modules", "dist", "coverage", ".git", ".nx", ".turbo"]);
5680
5702
  function hasTestFiles(cwd) {
5703
+ return walkTestFiles(cwd, true).length > 0;
5704
+ }
5705
+ function listTestFiles(cwd) {
5706
+ return walkTestFiles(cwd, false).sort();
5707
+ }
5708
+ function walkTestFiles(cwd, stopAtFirst) {
5709
+ const found = [];
5681
5710
  const stack = [cwd];
5682
5711
  while (stack.length > 0) {
5683
5712
  const dir = stack.pop();
@@ -5691,11 +5720,12 @@ function hasTestFiles(cwd) {
5691
5720
  if (entry.isDirectory()) {
5692
5721
  if (!NOT_WALKED.has(entry.name)) stack.push(join29(dir, entry.name));
5693
5722
  } else if (TEST_FILE.test(entry.name)) {
5694
- return true;
5723
+ found.push(relative5(cwd, join29(dir, entry.name)).replaceAll("\\", "/"));
5724
+ if (stopAtFirst) return found;
5695
5725
  }
5696
5726
  }
5697
5727
  }
5698
- return false;
5728
+ return found;
5699
5729
  }
5700
5730
  function readTestConfigSource(cwd, fileName) {
5701
5731
  try {
@@ -5705,8 +5735,10 @@ function readTestConfigSource(cwd, fileName) {
5705
5735
  }
5706
5736
  }
5707
5737
  function importsPreset2(source) {
5708
- const escaped = TEST_PRESET_SPECIFIER.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
5709
- return new RegExp(String.raw`(?:from|import)\s*\(?\s*['"]${escaped}['"]`).test(source);
5738
+ return ALL_TEST_PRESET_SPECIFIERS.some((specifier) => {
5739
+ const escaped = specifier.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
5740
+ return new RegExp(String.raw`(?:from|import)\s*\(?\s*['"]${escaped}['"]`).test(source);
5741
+ });
5710
5742
  }
5711
5743
  function readTestAdoption(cwd) {
5712
5744
  const manifest = readProjectPackageJson(cwd);
@@ -5766,6 +5798,769 @@ function readTestAdoption(cwd) {
5766
5798
  };
5767
5799
  }
5768
5800
 
5801
+ // src/roles/test/codemod/migrate-file.ts
5802
+ import { parseSync as parseSync5 } from "oxc-parser";
5803
+
5804
+ // src/roles/test/codemod/scope.ts
5805
+ function readIdentifiers(node) {
5806
+ const names = [];
5807
+ walk(node, (current) => {
5808
+ if (current.type === "Property" || current.type === "ObjectProperty") {
5809
+ if (current.shorthand === true) {
5810
+ names.push(...readIdentifiers(current.value));
5811
+ return SKIP;
5812
+ }
5813
+ if (current.computed === true) return;
5814
+ const key = current.key;
5815
+ if (key?.type !== "Identifier") return;
5816
+ names.push(...readIdentifiers(current.value));
5817
+ return SKIP;
5818
+ }
5819
+ if (current.type === "MemberExpression" || current.type === "StaticMemberExpression") {
5820
+ if (current.computed === true) return;
5821
+ names.push(...readIdentifiers(current.object));
5822
+ return SKIP;
5823
+ }
5824
+ if (current.type === "Identifier") {
5825
+ names.push(current.name);
5826
+ return SKIP;
5827
+ }
5828
+ return void 0;
5829
+ });
5830
+ return names;
5831
+ }
5832
+ function patternNames(pattern) {
5833
+ const names = [];
5834
+ const visit = (node) => {
5835
+ if (node === null || typeof node !== "object") return;
5836
+ const current = node;
5837
+ switch (current.type) {
5838
+ case "Identifier":
5839
+ names.push(current.name);
5840
+ return;
5841
+ case "ObjectPattern":
5842
+ for (const property of current.properties ?? []) {
5843
+ visit(property.type === "RestElement" ? property.argument : property.value);
5844
+ }
5845
+ return;
5846
+ case "ArrayPattern":
5847
+ for (const element of current.elements ?? []) visit(element);
5848
+ return;
5849
+ case "AssignmentPattern":
5850
+ visit(current.left);
5851
+ return;
5852
+ case "RestElement":
5853
+ visit(current.argument);
5854
+ return;
5855
+ default:
5856
+ return;
5857
+ }
5858
+ };
5859
+ visit(pattern);
5860
+ return names;
5861
+ }
5862
+ function moduleBindings(program) {
5863
+ const bindings = /* @__PURE__ */ new Map();
5864
+ for (const statement of program.body ?? []) {
5865
+ if (statement.type !== "VariableDeclaration") continue;
5866
+ for (const declarator of statement.declarations ?? []) {
5867
+ const init = declarator.init ?? void 0;
5868
+ for (const name of patternNames(declarator.id)) bindings.set(name, init);
5869
+ }
5870
+ }
5871
+ return bindings;
5872
+ }
5873
+ function importedNames(program) {
5874
+ const names = /* @__PURE__ */ new Set();
5875
+ for (const statement of program.body ?? []) {
5876
+ if (statement.type !== "ImportDeclaration") continue;
5877
+ for (const specifier of statement.specifiers ?? []) {
5878
+ const local = specifier.local;
5879
+ if (local?.type === "Identifier") names.add(local.name);
5880
+ }
5881
+ }
5882
+ return names;
5883
+ }
5884
+ function declaredInside(factory) {
5885
+ const names = /* @__PURE__ */ new Set();
5886
+ walk(factory, (node) => {
5887
+ if (node.type === "VariableDeclarator" || node.type === "FunctionDeclaration") {
5888
+ for (const name of patternNames(node.id)) names.add(name);
5889
+ }
5890
+ if (node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression") {
5891
+ for (const parameter of node.params ?? []) {
5892
+ for (const name of patternNames(parameter)) names.add(name);
5893
+ }
5894
+ }
5895
+ });
5896
+ return names;
5897
+ }
5898
+ function capturedBindings(factory, bindings) {
5899
+ const own = declaredInside(factory);
5900
+ const captured = /* @__PURE__ */ new Set();
5901
+ for (const name of readIdentifiers(factory)) {
5902
+ if (bindings.has(name) && !own.has(name)) captured.add(name);
5903
+ }
5904
+ return [...captured];
5905
+ }
5906
+ function hoistable(name, bindings, imported, seen = /* @__PURE__ */ new Set()) {
5907
+ if (seen.has(name)) return true;
5908
+ seen.add(name);
5909
+ const init = bindings.get(name);
5910
+ if (init === void 0) return false;
5911
+ for (const read2 of readIdentifiers(init)) {
5912
+ if (imported.has(read2)) return false;
5913
+ if (bindings.has(read2) && !hoistable(read2, bindings, imported, seen)) return false;
5914
+ }
5915
+ return true;
5916
+ }
5917
+
5918
+ // src/roles/test/codemod/hoist.ts
5919
+ function simpleDeclarations(program) {
5920
+ const found = /* @__PURE__ */ new Map();
5921
+ for (const statement of program.body ?? []) {
5922
+ if (statement.type !== "VariableDeclaration") continue;
5923
+ const declarations = statement.declarations ?? [];
5924
+ if (declarations.length !== 1) continue;
5925
+ const declarator = declarations[0];
5926
+ const id = declarator.id;
5927
+ const init = declarator.init;
5928
+ if (id?.type !== "Identifier" || init === void 0 || init === null) continue;
5929
+ found.set(id.name, { statement, init });
5930
+ }
5931
+ return found;
5932
+ }
5933
+ function interdependent(names, bindings) {
5934
+ for (const name of names) {
5935
+ for (const read2 of readIdentifiers(bindings.get(name))) {
5936
+ if (read2 !== name && names.includes(read2)) return `${name} reads ${read2}`;
5937
+ }
5938
+ }
5939
+ return void 0;
5940
+ }
5941
+ function statementSpan(source, statement) {
5942
+ let end = statement.end;
5943
+ if (source[end] === "\r") end++;
5944
+ if (source[end] === "\n") end++;
5945
+ return { start: statement.start, end };
5946
+ }
5947
+ function hoistCapturedBindings(source, program, factories, bindings, imported) {
5948
+ const captured = [...new Set(factories.flatMap((factory) => capturedBindings(factory, bindings)))];
5949
+ if (captured.length === 0) return { source, hoisted: [], refusals: [] };
5950
+ const refusals = [];
5951
+ for (const name of captured) {
5952
+ if (hoistable(name, bindings, imported)) continue;
5953
+ refusals.push({
5954
+ binding: name,
5955
+ reason: bindings.get(name) === void 0 ? `it is declared without an initializer, so there is nothing for vi.hoisted to carry` : `its initializer reads something imported, so it cannot live above the import graph`
5956
+ });
5957
+ }
5958
+ const declarations = simpleDeclarations(program);
5959
+ for (const name of captured) {
5960
+ if (!declarations.has(name) && !refusals.some((refusal) => refusal.binding === name)) {
5961
+ refusals.push({
5962
+ binding: name,
5963
+ reason: `it is not declared as a plain \`const ${name} = ...\` at module scope`
5964
+ });
5965
+ }
5966
+ }
5967
+ const cycle = interdependent(captured, bindings);
5968
+ if (cycle !== void 0) {
5969
+ refusals.push({
5970
+ binding: captured.join(", "),
5971
+ reason: `${cycle}, and an object literal cannot refer to its own keys`
5972
+ });
5973
+ }
5974
+ if (refusals.length > 0) return { source, hoisted: [], refusals };
5975
+ const ordered = captured.map((name) => ({ name, ...declarations.get(name) })).sort((left, right) => left.statement.start - right.statement.start);
5976
+ const entries = ordered.map(({ name, init }) => `${name}: ${source.slice(init.start, init.end)}`).join(", ");
5977
+ const first = ordered[0];
5978
+ const semi = source.slice(first.statement.start, first.statement.end).trimEnd().endsWith(";");
5979
+ const hoistedLine = `const { ${ordered.map(({ name }) => name).join(", ")} } = vi.hoisted(() => ({ ${entries} }))${semi ? ";" : ""}
5980
+ `;
5981
+ const edits = ordered.map(({ statement }, index) => {
5982
+ const span = statementSpan(source, statement);
5983
+ return { ...span, text: index === 0 ? hoistedLine : "" };
5984
+ });
5985
+ let rewritten = source;
5986
+ for (const edit of edits.sort((left, right) => right.start - left.start)) {
5987
+ rewritten = rewritten.slice(0, edit.start) + edit.text + rewritten.slice(edit.end);
5988
+ }
5989
+ return { source: rewritten, hoisted: ordered.map(({ name }) => name), refusals: [] };
5990
+ }
5991
+
5992
+ // src/roles/test/codemod/inject-type-import.ts
5993
+ import { parseSync as parseSync4 } from "oxc-parser";
5994
+
5995
+ // src/roles/test/codemod/rename.ts
5996
+ import { parseSync as parseSync3 } from "oxc-parser";
5997
+ var VITEST_TYPE_SOURCE = "vitest";
5998
+ var RUNTIME_RENAMES = /* @__PURE__ */ new Set([
5999
+ // Mocks
6000
+ "clearAllMocks",
6001
+ "doMock",
6002
+ "fn",
6003
+ "mock",
6004
+ "mocked",
6005
+ "resetAllMocks",
6006
+ "resetModules",
6007
+ "restoreAllMocks",
6008
+ "spyOn",
6009
+ "unmock",
6010
+ /*
6011
+ * Timers, as a family rather than as the two the corpus happens to use today. Running this over
6012
+ * the repo reported `jest.runAllTimers` as unknown, which was a hole in the table rather than a
6013
+ * finding about the code. Each name here was then checked against a running Vitest, so adding the
6014
+ * siblings is not speculation: a file gaining one tomorrow gets the same answer as one using it
6015
+ * today, instead of a report about a member that was always going to be a rename.
6016
+ */
6017
+ "advanceTimersByTime",
6018
+ "advanceTimersByTimeAsync",
6019
+ "advanceTimersToNextTimer",
6020
+ "clearAllTimers",
6021
+ "getRealSystemTime",
6022
+ "getTimerCount",
6023
+ "runAllTimers",
6024
+ "runAllTimersAsync",
6025
+ "runOnlyPendingTimers",
6026
+ "setSystemTime",
6027
+ "useFakeTimers",
6028
+ "useRealTimers"
6029
+ ]);
6030
+ var TYPE_RENAMES = {
6031
+ Mock: "Mock",
6032
+ MockedClass: "MockedClass",
6033
+ MockedFunction: "MockedFunction",
6034
+ MockedObject: "MockedObject",
6035
+ Mocked: "Mocked",
6036
+ SpiedFunction: "MockInstance",
6037
+ SpyInstance: "MockInstance"
6038
+ };
6039
+ var NAMED_RULE = {
6040
+ isolateModules: `vi.resetModules() followed by a dynamic import, since vi.resetModules() with a require() returns the CACHED module`,
6041
+ requireActual: `await vi.importActual(...), which makes the surrounding factory async`,
6042
+ requireMock: `await vi.importMock(...), which makes the surrounding factory async`,
6043
+ setTimeout: `vi.setConfig({ testTimeout: ... }), which is configuration rather than a call`
6044
+ };
6045
+ function lineOf(source, offset) {
6046
+ let line = 1;
6047
+ for (let index = 0; index < offset && index < source.length; index++) {
6048
+ if (source[index] === "\n") line++;
6049
+ }
6050
+ return line;
6051
+ }
6052
+ function valueEdit(node, source, outcome) {
6053
+ const object = node.object;
6054
+ const property = node.property;
6055
+ if (object?.type !== "Identifier" || object.name !== "jest") return void 0;
6056
+ if (property?.type !== "Identifier") return void 0;
6057
+ const member = property.name;
6058
+ if (RUNTIME_RENAMES.has(member)) {
6059
+ outcome.rewritten++;
6060
+ return { start: object.start, end: object.end, text: "vi" };
6061
+ }
6062
+ outcome.unhandled.push({
6063
+ member: `jest.${member}`,
6064
+ line: lineOf(source, object.start),
6065
+ ...NAMED_RULE[member] === void 0 ? {} : { rule: NAMED_RULE[member] }
6066
+ });
6067
+ return void 0;
6068
+ }
6069
+ function typeEdit(node, source, outcome, needed) {
6070
+ const left = node.left;
6071
+ const right = node.right;
6072
+ if (left?.type !== "Identifier" || left.name !== "jest") return void 0;
6073
+ if (right?.type !== "Identifier") return void 0;
6074
+ const member = right.name;
6075
+ const replacement = TYPE_RENAMES[member];
6076
+ if (replacement === void 0) {
6077
+ outcome.unhandled.push({ member: `jest.${member}`, line: lineOf(source, node.start) });
6078
+ return void 0;
6079
+ }
6080
+ needed.add(replacement);
6081
+ outcome.rewritten++;
6082
+ return { start: node.start, end: node.end, text: replacement };
6083
+ }
6084
+ function renameJestToVitest(fileName, source) {
6085
+ const outcome = { source, typeImports: [], rewritten: 0, unhandled: [] };
6086
+ const { program, errors } = parseSync3(fileName, source, { sourceType: "module" });
6087
+ if (errors.length > 0) return outcome;
6088
+ const edits = [];
6089
+ const needed = /* @__PURE__ */ new Set();
6090
+ walk(program, (node) => {
6091
+ if (node.type === "MemberExpression" || node.type === "StaticMemberExpression") {
6092
+ const edit = valueEdit(node, source, outcome);
6093
+ if (edit !== void 0) edits.push(edit);
6094
+ return;
6095
+ }
6096
+ if (node.type === "TSQualifiedName") {
6097
+ const edit = typeEdit(node, source, outcome, needed);
6098
+ if (edit !== void 0) edits.push(edit);
6099
+ }
6100
+ });
6101
+ outcome.typeImports = [...needed].sort();
6102
+ let rewritten = source;
6103
+ for (const edit of edits.sort((a, b) => b.start - a.start)) {
6104
+ rewritten = rewritten.slice(0, edit.start) + edit.text + rewritten.slice(edit.end);
6105
+ }
6106
+ outcome.source = rewritten;
6107
+ return outcome;
6108
+ }
6109
+
6110
+ // src/roles/test/codemod/inject-type-import.ts
6111
+ function renderTypeImport(names, semi) {
6112
+ return `import type { ${names.join(", ")} } from '${VITEST_TYPE_SOURCE}'${semi ? ";" : ""}`;
6113
+ }
6114
+ function importDeclarations(program) {
6115
+ return (program.body ?? []).filter((node) => node.type === "ImportDeclaration");
6116
+ }
6117
+ function usesSemicolons(source, imports) {
6118
+ const first = imports[0];
6119
+ if (first === void 0) return false;
6120
+ return source.slice(first.start, first.end).trimEnd().endsWith(";");
6121
+ }
6122
+ function existingVitestImport(imports) {
6123
+ return imports.find(
6124
+ (node) => node.source?.value === VITEST_TYPE_SOURCE
6125
+ );
6126
+ }
6127
+ function alreadyImported(declaration) {
6128
+ return (declaration.specifiers ?? []).filter((specifier) => specifier.type === "ImportSpecifier").map((specifier) => specifier.imported?.name).filter((name) => name !== void 0);
6129
+ }
6130
+ function injectTypeImport(fileName, source, typeNames) {
6131
+ if (typeNames.length === 0) return source;
6132
+ const { program, errors } = parseSync4(fileName, source, { sourceType: "module" });
6133
+ if (errors.length > 0) return source;
6134
+ const imports = importDeclarations(program);
6135
+ const semi = usesSemicolons(source, imports);
6136
+ const existing = existingVitestImport(imports);
6137
+ if (existing !== void 0) {
6138
+ const merged = [.../* @__PURE__ */ new Set([...alreadyImported(existing), ...typeNames])].sort();
6139
+ if (merged.length === alreadyImported(existing).length) return source;
6140
+ return source.slice(0, existing.start) + renderTypeImport(merged, semi) + source.slice(existing.end);
6141
+ }
6142
+ const line = renderTypeImport([...typeNames].sort(), semi);
6143
+ const first = imports[0];
6144
+ if (first === void 0) return `${line}
6145
+
6146
+ ${source}`;
6147
+ return source.slice(0, first.start) + line + "\n" + source.slice(first.start);
6148
+ }
6149
+
6150
+ // src/roles/test/codemod/named-rules.ts
6151
+ var ASYNC_REPLACEMENTS = {
6152
+ requireActual: "importActual",
6153
+ requireMock: "importMock"
6154
+ };
6155
+ var FUNCTIONS = /* @__PURE__ */ new Set(["ArrowFunctionExpression", "FunctionExpression", "FunctionDeclaration"]);
6156
+ function walkWithEnclosingFunction(node, enclosing, visit) {
6157
+ if (node === null || typeof node !== "object") return;
6158
+ if (Array.isArray(node)) {
6159
+ for (const entry of node) walkWithEnclosingFunction(entry, enclosing, visit);
6160
+ return;
6161
+ }
6162
+ const current = node;
6163
+ const next = FUNCTIONS.has(current.type) ? current : enclosing;
6164
+ if (typeof current.type === "string") visit(current, enclosing);
6165
+ for (const value of Object.values(current)) {
6166
+ if (value !== null && typeof value === "object") walkWithEnclosingFunction(value, next, visit);
6167
+ }
6168
+ }
6169
+ function ownedCall(node) {
6170
+ if (node.type !== "CallExpression") return void 0;
6171
+ const callee = node.callee;
6172
+ if (callee?.type !== "MemberExpression" && callee?.type !== "StaticMemberExpression") {
6173
+ return void 0;
6174
+ }
6175
+ const object = callee.object;
6176
+ const property = callee.property;
6177
+ if (object?.type !== "Identifier" || object.name !== "jest") return void 0;
6178
+ return property?.type === "Identifier" ? property.name : void 0;
6179
+ }
6180
+ function argumentText(source, call) {
6181
+ const args = call.arguments ?? [];
6182
+ if (args.length === 0) return "";
6183
+ const first = args[0];
6184
+ const last = args[args.length - 1];
6185
+ return source.slice(first.start, last.end);
6186
+ }
6187
+ function applyEdits(source, edits) {
6188
+ let result = source;
6189
+ for (const edit of [...edits].sort((left, right) => right.start - left.start)) {
6190
+ result = result.slice(0, edit.start) + edit.text + result.slice(edit.end);
6191
+ }
6192
+ return result;
6193
+ }
6194
+ function applyNamedRules(source, program) {
6195
+ const edits = [];
6196
+ const applied = /* @__PURE__ */ new Set();
6197
+ const asyncify = /* @__PURE__ */ new Set();
6198
+ walkWithEnclosingFunction(program, void 0, (node, enclosing) => {
6199
+ const member = ownedCall(node);
6200
+ if (member === void 0) return;
6201
+ const asyncReplacement = ASYNC_REPLACEMENTS[member];
6202
+ if (asyncReplacement !== void 0) {
6203
+ edits.push({
6204
+ start: node.start,
6205
+ end: node.end,
6206
+ text: `(await vi.${asyncReplacement}(${argumentText(source, node)}))`
6207
+ });
6208
+ applied.add(`jest.${member}`);
6209
+ if (enclosing !== void 0 && enclosing.async !== true) asyncify.add(enclosing);
6210
+ return;
6211
+ }
6212
+ if (member === "setTimeout") {
6213
+ edits.push({
6214
+ start: node.start,
6215
+ end: node.end,
6216
+ text: `vi.setConfig({ testTimeout: ${argumentText(source, node)} })`
6217
+ });
6218
+ applied.add("jest.setTimeout");
6219
+ }
6220
+ });
6221
+ for (const fn of asyncify) {
6222
+ edits.push({ start: fn.start, end: fn.start, text: "async " });
6223
+ }
6224
+ return { source: applyEdits(source, edits), applied: [...applied].sort() };
6225
+ }
6226
+
6227
+ // src/roles/test/codemod/migrate-file.ts
6228
+ function mockFactories(program) {
6229
+ const factories = [];
6230
+ walk(program, (node) => {
6231
+ if (node.type !== "CallExpression") return;
6232
+ const callee = node.callee;
6233
+ if (callee?.type !== "MemberExpression" && callee?.type !== "StaticMemberExpression") return;
6234
+ const object = callee.object;
6235
+ const property = callee.property;
6236
+ if (object?.type !== "Identifier") return;
6237
+ if (object.name !== "jest" && object.name !== "vi") return;
6238
+ if (property?.name !== "mock") return;
6239
+ const args = node.arguments ?? [];
6240
+ if (args.length > 1) factories.push(args[1]);
6241
+ });
6242
+ return factories;
6243
+ }
6244
+ function parse2(fileName, source) {
6245
+ const { program, errors } = parseSync5(fileName, source, { sourceType: "module" });
6246
+ return errors.length > 0 ? void 0 : program;
6247
+ }
6248
+ function migrateTestFile(fileName, source) {
6249
+ const untouched = {
6250
+ source,
6251
+ changed: false,
6252
+ renamed: 0,
6253
+ hoisted: [],
6254
+ unhandled: [],
6255
+ refusals: [],
6256
+ rulesApplied: []
6257
+ };
6258
+ const original = parse2(fileName, source);
6259
+ if (original === void 0) return untouched;
6260
+ const ruled = applyNamedRules(source, original);
6261
+ const renamed = renameJestToVitest(fileName, ruled.source);
6262
+ const program = parse2(fileName, renamed.source);
6263
+ if (program === void 0) return untouched;
6264
+ const hoist = hoistCapturedBindings(
6265
+ renamed.source,
6266
+ program,
6267
+ mockFactories(program),
6268
+ moduleBindings(program),
6269
+ importedNames(program)
6270
+ );
6271
+ const withImport = injectTypeImport(fileName, hoist.source, renamed.typeImports);
6272
+ return {
6273
+ source: withImport,
6274
+ changed: withImport !== source,
6275
+ renamed: renamed.rewritten,
6276
+ hoisted: hoist.hoisted,
6277
+ unhandled: renamed.unhandled,
6278
+ refusals: hoist.refusals,
6279
+ rulesApplied: ruled.applied
6280
+ };
6281
+ }
6282
+
6283
+ // src/roles/test/codemod/migrate-module.ts
6284
+ function read(path) {
6285
+ try {
6286
+ return readFileSync24(path, "utf8");
6287
+ } catch {
6288
+ return void 0;
6289
+ }
6290
+ }
6291
+ function migrateModuleTests(cwd) {
6292
+ const operations = [];
6293
+ const blockers = [];
6294
+ let files = 0;
6295
+ let renamed = 0;
6296
+ let hoisted = 0;
6297
+ for (const relative7 of listTestFiles(cwd)) {
6298
+ const source = read(join30(cwd, relative7));
6299
+ if (source === void 0) {
6300
+ blockers.push({ file: relative7, reason: "this file could not be read" });
6301
+ continue;
6302
+ }
6303
+ const migrated = migrateTestFile(join30(cwd, relative7), source);
6304
+ for (const member of migrated.unhandled) {
6305
+ blockers.push({
6306
+ file: `${relative7}:${member.line}`,
6307
+ reason: member.rule === void 0 ? `${member.member} has no rule, and guessing one is how a suite goes green for the wrong reason` : `${member.member} needs ${member.rule}`
6308
+ });
6309
+ }
6310
+ for (const refusal of migrated.refusals) {
6311
+ blockers.push({
6312
+ file: relative7,
6313
+ reason: `\`${refusal.binding}\` cannot move into vi.hoisted: ${refusal.reason}`
6314
+ });
6315
+ }
6316
+ if (!migrated.changed) continue;
6317
+ files++;
6318
+ renamed += migrated.renamed;
6319
+ if (migrated.hoisted.length > 0) hoisted++;
6320
+ operations.push({ kind: "write", path: relative7, contents: migrated.source });
6321
+ }
6322
+ return blockers.length > 0 ? { operations: [], files, renamed, hoisted, blockers } : { operations, files, renamed, hoisted, blockers: [] };
6323
+ }
6324
+
6325
+ // src/roles/test/nest/generate-config.ts
6326
+ import { relative as relative6 } from "path";
6327
+ var NEST_TEST_PRESET_SPECIFIER = "@hublo/sentinel/test/nest";
6328
+ var SENTINEL_TEST_SETUP = "@hublo/sentinel/test/setup/nest";
6329
+ var NEST_TEST_CONFIG_FILE = "vitest.config.mts";
6330
+ function hopsToRoot(cwd, workspaceRoot) {
6331
+ const up = relative6(cwd, workspaceRoot).replaceAll("\\", "/");
6332
+ return up === "" ? "." : up;
6333
+ }
6334
+ function coverageBlock(reading) {
6335
+ return reading.coverageDirectory === void 0 ? "" : ` coverage: { reportsDirectory: '${reading.coverageDirectory}' },`;
6336
+ }
6337
+ function environmentBlock(reading) {
6338
+ return reading.testEnvironment === void 0 ? "" : ` environment: '${reading.testEnvironment}',`;
6339
+ }
6340
+ function generateNestTestConfig(cwd, reading) {
6341
+ if (reading.refusals.length > 0) return { refusals: reading.refusals };
6342
+ const workspaceRoot = findWorkspaceRoot(cwd);
6343
+ if (workspaceRoot === void 0) {
6344
+ return { refusals: ["the workspace root could not be found from this module"] };
6345
+ }
6346
+ const up = hopsToRoot(cwd, workspaceRoot);
6347
+ const test = [
6348
+ environmentBlock(reading),
6349
+ ` setupFiles: ['${SENTINEL_TEST_SETUP}'],`,
6350
+ coverageBlock(reading)
6351
+ ].filter((line) => line !== "");
6352
+ return {
6353
+ refusals: [],
6354
+ source: `import path from 'node:path'
6355
+
6356
+ import { defineConfig, nestTestConfig } from '${NEST_TEST_PRESET_SPECIFIER}'
6357
+
6358
+ export default defineConfig(
6359
+ nestTestConfig({
6360
+ root: import.meta.dirname,
6361
+ workspaceRoot: path.resolve(import.meta.dirname, '${up}'),
6362
+ overrides: {
6363
+ test: {
6364
+ ${test.join("\n")}
6365
+ },
6366
+ },
6367
+ }),
6368
+ )
6369
+ `
6370
+ };
6371
+ }
6372
+
6373
+ // src/roles/test/nest/read-jest-config.ts
6374
+ import { existsSync as existsSync26, readFileSync as readFileSync25 } from "fs";
6375
+ import { join as join31 } from "path";
6376
+ var CARRIED = ["testEnvironment", "coverageDirectory", "setupFilesAfterEnv"];
6377
+ var DISCARDED = ["preset", "displayName", "moduleFileExtensions"];
6378
+ var STRING = String.raw`['"]([^'"]*)['"]`;
6379
+ function readString(body, key) {
6380
+ return new RegExp(String.raw`\b${key}\s*:\s*${STRING}`).exec(body)?.[1];
6381
+ }
6382
+ function readStringArray(body, key) {
6383
+ const raw = new RegExp(String.raw`\b${key}\s*:\s*\[([^\]]*)\]`).exec(body)?.[1];
6384
+ if (raw === void 0) return void 0;
6385
+ return [...raw.matchAll(new RegExp(STRING, "g"))].map((match) => match[1]);
6386
+ }
6387
+ function configBody(source) {
6388
+ const start = /(?:const\s+\w+(?::\s*\w+)?\s*=|export\s+default)\s*\{/.exec(source);
6389
+ if (start === null) return void 0;
6390
+ let depth = 0;
6391
+ const from = start.index + start[0].length - 1;
6392
+ for (let i = from; i < source.length; i += 1) {
6393
+ const char = source[i];
6394
+ if (char === "{") depth += 1;
6395
+ else if (char === "}") {
6396
+ depth -= 1;
6397
+ if (depth === 0) return source.slice(from + 1, i);
6398
+ }
6399
+ }
6400
+ return void 0;
6401
+ }
6402
+ function declaredKeys(body) {
6403
+ const keys = [];
6404
+ let depth = 0;
6405
+ for (const line of body.split("\n")) {
6406
+ if (depth === 0) {
6407
+ const key = /^\s*([A-Za-z_$][\w$]*)\s*:/.exec(line);
6408
+ if (key !== null) keys.push(key[1]);
6409
+ }
6410
+ for (const char of line) {
6411
+ if (char === "{" || char === "[") depth += 1;
6412
+ else if (char === "}" || char === "]") depth -= 1;
6413
+ }
6414
+ }
6415
+ return keys;
6416
+ }
6417
+ function sideEffects(source) {
6418
+ const found = [];
6419
+ if (/^\s*process\.env\s*[.=]/m.test(source)) {
6420
+ found.push("it sets `process.env`, which the generated config would not carry");
6421
+ }
6422
+ const valueImport = /^\s*import\s+(?!type\b)[^\n]*$/m.exec(source);
6423
+ if (valueImport !== null && !/from\s+['"]jest['"]/.test(valueImport[0])) {
6424
+ found.push(`it imports a value (\`${valueImport[0].trim()}\`)`);
6425
+ }
6426
+ return found;
6427
+ }
6428
+ function readJestConfig(cwd) {
6429
+ const file = JEST_CONFIG_FILES.find((name) => existsSync26(join31(cwd, name)));
6430
+ if (file === void 0) return void 0;
6431
+ let source;
6432
+ try {
6433
+ source = readFileSync25(join31(cwd, file), "utf8");
6434
+ } catch {
6435
+ return { file, refusals: [`${file} could not be read`] };
6436
+ }
6437
+ const refusals = sideEffects(source);
6438
+ const body = configBody(source);
6439
+ if (body === void 0) {
6440
+ return { file, refusals: [...refusals, "its config object could not be located"] };
6441
+ }
6442
+ const known = /* @__PURE__ */ new Set([...CARRIED, ...DISCARDED]);
6443
+ for (const key of declaredKeys(body)) {
6444
+ if (!known.has(key)) refusals.push(`it declares \`${key}\`, which is not transposed`);
6445
+ }
6446
+ const setup = readStringArray(source, "setupFilesAfterEnv");
6447
+ return {
6448
+ file,
6449
+ ...readString(source, "testEnvironment") !== void 0 ? { testEnvironment: readString(source, "testEnvironment") } : {},
6450
+ ...readString(source, "coverageDirectory") !== void 0 ? { coverageDirectory: readString(source, "coverageDirectory") } : {},
6451
+ ...setup !== void 0 ? { setupFilesAfterEnv: setup } : {},
6452
+ refusals
6453
+ };
6454
+ }
6455
+
6456
+ // src/roles/test/jest-migration.ts
6457
+ var SPEC_TSCONFIG = "tsconfig.spec.json";
6458
+ var VITEST_GLOBAL_TYPES = "vitest/globals";
6459
+ function specTypesOperation(cwd) {
6460
+ const path = join32(cwd, SPEC_TSCONFIG);
6461
+ if (!existsSync27(path)) return void 0;
6462
+ let config;
6463
+ try {
6464
+ config = parseJsonc(
6465
+ readFileSync26(path, "utf8"),
6466
+ SPEC_TSCONFIG
6467
+ );
6468
+ } catch {
6469
+ return void 0;
6470
+ }
6471
+ const types = config?.compilerOptions?.types;
6472
+ if (!Array.isArray(types) || !types.includes("jest")) return void 0;
6473
+ return {
6474
+ kind: "merge-json",
6475
+ path: SPEC_TSCONFIG,
6476
+ value: {
6477
+ compilerOptions: {
6478
+ types: types.map((entry) => entry === "jest" ? VITEST_GLOBAL_TYPES : entry)
6479
+ }
6480
+ }
6481
+ };
6482
+ }
6483
+ function siblingScriptRewrites(cwd) {
6484
+ const rewrites = {};
6485
+ for (const [name, command] of Object.entries(moduleScripts(cwd))) {
6486
+ if (name === TEST_SCRIPT_NAME || typeof command !== "string") continue;
6487
+ const rewritten = rewriteVitestScript(command);
6488
+ if (rewritten !== command) rewrites[name] = rewritten;
6489
+ }
6490
+ return rewrites;
6491
+ }
6492
+ function blocked(reason) {
6493
+ return { operations: [], blocked: reason };
6494
+ }
6495
+ function planJestMigration(context, adoption) {
6496
+ if (adoption.jestConfigs.length > 1) {
6497
+ const [primary, ...rest] = adoption.jestConfigs;
6498
+ return blocked(
6499
+ `this module has a second run mode (${rest.join(", ")}) beside ${primary}, with its own target. One config file cannot stand for both, and how run modes are expressed under Vitest is an open question here, so this module is not migrated yet.`
6500
+ );
6501
+ }
6502
+ const reading = readJestConfig(context.cwd);
6503
+ if (reading === void 0) {
6504
+ return blocked(`${adoption.jestConfigs.join(", ")} could not be read, so nothing was written.`);
6505
+ }
6506
+ const generated = generateNestTestConfig(context.cwd, reading);
6507
+ if (generated.source === void 0) {
6508
+ return blocked(
6509
+ `this module's jest config does not transpose as it stands: ${generated.refusals.join("; ")}. Nothing was written; this needs a look before the migration runs.`
6510
+ );
6511
+ }
6512
+ const migration = migrateModuleTests(context.cwd);
6513
+ if (migration.blockers.length > 0) {
6514
+ const listed = migration.blockers.map((blocker) => `
6515
+ ${blocker.file}: ${blocker.reason}`).join("");
6516
+ return blocked(
6517
+ `${migration.blockers.length} thing(s) in this module's test files need a human before it can move. Nothing was written, because a config without its files is a suite calling jest.* under Vitest.${listed}`
6518
+ );
6519
+ }
6520
+ const existing = projectTargets(context.cwd)?.[TEST_SCRIPT_NAME];
6521
+ const siblings = siblingScriptRewrites(context.cwd);
6522
+ const specTypes = specTypesOperation(context.cwd);
6523
+ const operations = [
6524
+ { kind: "write", path: NEST_TEST_CONFIG_FILE, contents: generated.source },
6525
+ ...migration.operations,
6526
+ ...specTypes === void 0 ? [] : [specTypes],
6527
+ manifestOperation(context.cwd, { [TEST_SCRIPT_NAME]: SENTINEL_TEST_COMMAND, ...siblings }),
6528
+ ...nxTargetOperations({
6529
+ cwd: context.cwd,
6530
+ targets: testTarget({
6531
+ configFiles: [NEST_TEST_CONFIG_FILE],
6532
+ outputs: existing?.outputs ?? [],
6533
+ dependsOn: existing?.dependsOn ?? [],
6534
+ configurations: existing?.configurations ?? {}
6535
+ })
6536
+ }),
6537
+ ...adoption.jestConfigs.map((name) => ({ kind: "delete", path: name }))
6538
+ ];
6539
+ if (existing !== void 0) {
6540
+ operations.push({
6541
+ kind: "remove-json-keys",
6542
+ path: "project.json",
6543
+ keys: [["targets", TEST_SCRIPT_NAME]]
6544
+ });
6545
+ }
6546
+ const notes = [
6547
+ `${migration.files} test file(s) migrated from jest to Vitest, ${migration.renamed} site(s) rewritten${migration.hoisted > 0 ? `, ${migration.hoisted} of them moving a mock factory's captures into vi.hoisted` : ""}.`,
6548
+ `${NEST_TEST_CONFIG_FILE} written from what ${adoption.jestConfigs.join(", ")} declared, and that config deleted. Every specifier in the new one is a package name: nothing in it reaches outside this module.`
6549
+ ];
6550
+ if (specTypes !== void 0) {
6551
+ notes.push(
6552
+ `${SPEC_TSCONFIG} now declares \`${VITEST_GLOBAL_TYPES}\` instead of \`jest\`. Without it the module keeps depending on @types/jest and \`jest.Mock\` still resolves, so the migration would look finished while the dependency it cuts is still declared.`
6553
+ );
6554
+ }
6555
+ notes.push(
6556
+ `run this module's own lint --fix (or its formatter) after installing: the Vitest type imports are written beside the existing ones, and ordering belongs to the tool that enforces it.`
6557
+ );
6558
+ if (Object.keys(siblings).length > 0) {
6559
+ notes.push(`rewrote ${Object.keys(siblings).join(", ")} to run through sentinel.`);
6560
+ }
6561
+ return { operations, notes };
6562
+ }
6563
+
5769
6564
  // src/roles/test/plan.ts
5770
6565
  function ownedTestDependencies(cwd, declared) {
5771
6566
  return declared.filter((entry) => SENTINEL_OWNED_TEST_PACKAGES.includes(entry.name)).flatMap((entry) => [
@@ -5786,16 +6581,50 @@ function plan3(context) {
5786
6581
  }
5787
6582
  const configFile = testConfigFile(context.cwd);
5788
6583
  const adoption = readTestAdoption(context.cwd);
6584
+ if (adoption.state === "no-tests") {
6585
+ const operations2 = adoption.jestConfigs.map((name) => ({
6586
+ kind: "delete",
6587
+ path: name
6588
+ }));
6589
+ const owned2 = ownedTestDependencies(context.cwd, adoption.ownDeclarations);
6590
+ if (owned2.length > 0) {
6591
+ operations2.push({ kind: "remove-json-keys", path: "package.json", keys: owned2 });
6592
+ }
6593
+ const hasTarget = projectTargets(context.cwd)?.[TEST_SCRIPT_NAME] !== void 0;
6594
+ if (hasTarget) {
6595
+ operations2.push({
6596
+ kind: "remove-json-keys",
6597
+ path: "project.json",
6598
+ keys: [["targets", TEST_SCRIPT_NAME]]
6599
+ });
6600
+ }
6601
+ if (operations2.length === 0) {
6602
+ return {
6603
+ operations: [],
6604
+ skipped: `this module has no test and no jest leftovers, so there is nothing to do here.`
6605
+ };
6606
+ }
6607
+ return {
6608
+ operations: operations2,
6609
+ notes: [
6610
+ // Names what is actually removed, never a shape this module does not have: half of these
6611
+ // modules carry a config with no target behind it, and claiming otherwise would describe
6612
+ // an operation the plan never performs.
6613
+ `this module has no test file, so this is a cleanup rather than an adoption: ${[...adoption.jestConfigs, ...hasTarget ? ["the jest target it feeds"] : []].join(" and ")} removed, so nothing here breaks the day jest leaves the workspace. No test config is written in exchange, because there is nothing to configure.`
6614
+ ]
6615
+ };
6616
+ }
5789
6617
  if (configFile === void 0) {
6618
+ if (adoption.state === "jest") return planJestMigration(context, adoption);
5790
6619
  return {
5791
6620
  operations: [],
5792
- skipped: adoption.state === "jest" ? `this module tests with jest (${adoption.jestConfigs.join(", ")}). Migrating it moves its test FILES as well as its config, which is the next step of this role and not this one.` : `this module has no test config and no test files, so there is nothing to point at sentinel. What it needs is its jest leftovers removed, which is a cleanup rather than an adoption.`
6621
+ skipped: `this module has no test config and no test files, so there is nothing to point at sentinel. What it needs is its jest leftovers removed, which is a cleanup rather than an adoption.`
5793
6622
  };
5794
6623
  }
5795
6624
  const operations = [];
5796
6625
  const notes = [];
5797
6626
  const rewrite = rewriteToolchainImports(
5798
- TEST_TOOLCHAIN,
6627
+ testToolchain(context.preset),
5799
6628
  readTestConfigSource(context.cwd, configFile),
5800
6629
  configFile
5801
6630
  );
@@ -5948,9 +6777,9 @@ function registerTest() {
5948
6777
 
5949
6778
  // src/roles/typescript/adapters/tsc/tsc.adapter.ts
5950
6779
  import { spawnSync as spawnSync6 } from "child_process";
5951
- import { existsSync as existsSync26, readFileSync as readFileSync25 } from "fs";
6780
+ import { existsSync as existsSync28, readFileSync as readFileSync28 } from "fs";
5952
6781
  import { createRequire as createRequire4 } from "module";
5953
- import { join as join31 } from "path";
6782
+ import { join as join34 } from "path";
5954
6783
 
5955
6784
  // src/roles/typescript/presets/nest.json
5956
6785
  var nest_default2 = {
@@ -6131,8 +6960,8 @@ function readTsconfigAdoption(cwd) {
6131
6960
  }
6132
6961
 
6133
6962
  // src/roles/typescript/adapters/tsc/plan.ts
6134
- import { readFileSync as readFileSync24 } from "fs";
6135
- import { join as join30 } from "path";
6963
+ import { readFileSync as readFileSync27 } from "fs";
6964
+ import { join as join33 } from "path";
6136
6965
 
6137
6966
  // src/roles/typescript/typecheck-script.ts
6138
6967
  var SENTINEL_TYPECHECK_COMMAND = "sentinel --run --typescript";
@@ -6225,7 +7054,7 @@ function planAdoption(context) {
6225
7054
  };
6226
7055
  }
6227
7056
  const existing = parseJsonc(
6228
- readFileSync24(join30(context.cwd, target.path), "utf8"),
7057
+ readFileSync27(join33(context.cwd, target.path), "utf8"),
6229
7058
  target.path
6230
7059
  );
6231
7060
  const extendsChain = composeExtends(existing.extends, preset);
@@ -6363,7 +7192,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
6363
7192
  let chain;
6364
7193
  try {
6365
7194
  const parsed = parseJsonc(
6366
- readFileSync25(join31(cwd, target.path), "utf8"),
7195
+ readFileSync28(join34(cwd, target.path), "utf8"),
6367
7196
  target.path
6368
7197
  );
6369
7198
  chain = parsed.extends;
@@ -6376,7 +7205,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
6376
7205
  );
6377
7206
  if (preset === void 0) return void 0;
6378
7207
  try {
6379
- createRequire4(join31(cwd, "noop.js")).resolve(preset);
7208
+ createRequire4(join34(cwd, "noop.js")).resolve(preset);
6380
7209
  return void 0;
6381
7210
  } catch {
6382
7211
  return preset;
@@ -6472,7 +7301,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
6472
7301
  referencedProjects(cwd, config) {
6473
7302
  try {
6474
7303
  const parsed = parseJsonc(
6475
- readFileSync25(join31(cwd, config), "utf8"),
7304
+ readFileSync28(join34(cwd, config), "utf8"),
6476
7305
  config
6477
7306
  );
6478
7307
  const referenced = (parsed.references ?? []).map((reference) => reference.path).filter((path) => typeof path === "string" && path.length > 0);
@@ -6488,7 +7317,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
6488
7317
  * check.
6489
7318
  */
6490
7319
  typecheckTarget(cwd) {
6491
- if (existsSync26(join31(cwd, "tsconfig.json"))) return "tsconfig.json";
7320
+ if (existsSync28(join34(cwd, "tsconfig.json"))) return "tsconfig.json";
6492
7321
  const target = resolveTsconfigTarget(cwd);
6493
7322
  return target.reason === "none" ? null : target.path;
6494
7323
  }
@@ -6693,9 +7522,9 @@ function replaceLines(current, replacements) {
6693
7522
  }
6694
7523
 
6695
7524
  // src/core/apply-plan.ts
6696
- import { existsSync as existsSync27, readFileSync as readFileSync26, renameSync, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "fs";
7525
+ import { existsSync as existsSync29, readFileSync as readFileSync29, renameSync, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "fs";
6697
7526
  import { resolve as resolve4, sep } from "path";
6698
- import { applyEdits, findNodeAtLocation, modify, parseTree } from "jsonc-parser";
7527
+ import { applyEdits as applyEdits2, findNodeAtLocation, modify, parseTree } from "jsonc-parser";
6699
7528
 
6700
7529
  // src/shared/deep-merge.ts
6701
7530
  function isPlainObject(value) {
@@ -6712,7 +7541,7 @@ function resolveWithinRoot(cwd, relativePath) {
6712
7541
  return absolutePath;
6713
7542
  }
6714
7543
  function readIfExists(absolutePath) {
6715
- return existsSync27(absolutePath) ? readFileSync26(absolutePath, "utf8") : void 0;
7544
+ return existsSync29(absolutePath) ? readFileSync29(absolutePath, "utf8") : void 0;
6716
7545
  }
6717
7546
  function* leaves(value, prefix = []) {
6718
7547
  for (const [key, keyValue] of Object.entries(value)) {
@@ -6727,7 +7556,7 @@ function mergeJsonc(current, value) {
6727
7556
  const edits = modify(text, path, leaf, {
6728
7557
  formattingOptions: { insertSpaces: true, tabSize: 2 }
6729
7558
  });
6730
- text = applyEdits(text, edits);
7559
+ text = applyEdits2(text, edits);
6731
7560
  }
6732
7561
  return text.endsWith("\n") ? text : text + "\n";
6733
7562
  }
@@ -6739,7 +7568,7 @@ function removeJsoncKeys(current, keys) {
6739
7568
  const edits = modify(text, path, void 0, {
6740
7569
  formattingOptions: { insertSpaces: true, tabSize: 2 }
6741
7570
  });
6742
- text = applyEdits(text, edits);
7571
+ text = applyEdits2(text, edits);
6743
7572
  }
6744
7573
  return text.endsWith("\n") ? text : text + "\n";
6745
7574
  }
@@ -6830,8 +7659,8 @@ function applyPlan(cwd, plan4) {
6830
7659
  }
6831
7660
 
6832
7661
  // src/core/config/preset-evidence.ts
6833
- import { existsSync as existsSync28, readdirSync as readdirSync4, readFileSync as readFileSync27 } from "fs";
6834
- import { join as join32 } from "path";
7662
+ import { existsSync as existsSync30, readdirSync as readdirSync4, readFileSync as readFileSync30 } from "fs";
7663
+ import { join as join35 } from "path";
6835
7664
  var PATH_SIGNALS = [
6836
7665
  {
6837
7666
  preset: "nest",
@@ -6844,12 +7673,42 @@ var DEPENDENCY_SIGNALS = [
6844
7673
  { preset: "svelte", pattern: /^svelte$|^@sveltejs\// },
6845
7674
  { preset: "nest", pattern: /^@nestjs\// }
6846
7675
  ];
7676
+ var IMPORT_SIGNALS = [
7677
+ { preset: "nest", pattern: /@nestjs\//, evidence: 'its code imports "@nestjs/"' }
7678
+ ];
7679
+ var SOURCE_FILE = /\.[cm]?[jt]sx?$/;
7680
+ var NOT_SCANNED = /* @__PURE__ */ new Set(["node_modules", "dist", "coverage", ".git", ".nx", ".turbo", "build"]);
7681
+ function sourceImports(cwd, pattern) {
7682
+ const stack = [cwd];
7683
+ while (stack.length > 0) {
7684
+ const dir = stack.pop();
7685
+ let entries;
7686
+ try {
7687
+ entries = readdirSync4(dir, { withFileTypes: true });
7688
+ } catch {
7689
+ continue;
7690
+ }
7691
+ for (const entry of entries) {
7692
+ if (entry.isDirectory()) {
7693
+ if (!NOT_SCANNED.has(entry.name)) stack.push(join35(dir, entry.name));
7694
+ continue;
7695
+ }
7696
+ if (!SOURCE_FILE.test(entry.name)) continue;
7697
+ try {
7698
+ if (pattern.test(readFileSync30(join35(dir, entry.name), "utf8"))) return true;
7699
+ } catch {
7700
+ continue;
7701
+ }
7702
+ }
7703
+ }
7704
+ return false;
7705
+ }
6847
7706
  function dependencyNames(cwd) {
6848
- const path = join32(cwd, "package.json");
6849
- if (!existsSync28(path)) return [];
7707
+ const path = join35(cwd, "package.json");
7708
+ if (!existsSync30(path)) return [];
6850
7709
  try {
6851
7710
  const manifest = parseJsonc(
6852
- readFileSync27(path, "utf8"),
7711
+ readFileSync30(path, "utf8"),
6853
7712
  path
6854
7713
  );
6855
7714
  return [
@@ -6872,7 +7731,7 @@ function declaresJsx(cwd) {
6872
7731
  for (const name of entries) {
6873
7732
  try {
6874
7733
  const config = parseJsonc(
6875
- readFileSync27(join32(cwd, name), "utf8"),
7734
+ readFileSync30(join35(cwd, name), "utf8"),
6876
7735
  name
6877
7736
  );
6878
7737
  if (config.compilerOptions?.jsx !== void 0) return true;
@@ -6897,6 +7756,10 @@ function presetSignals(cwd) {
6897
7756
  signals.push({ preset, evidence });
6898
7757
  }
6899
7758
  }
7759
+ for (const { preset, pattern, evidence } of IMPORT_SIGNALS) {
7760
+ if (signals.some((signal) => signal.preset === preset)) continue;
7761
+ if (sourceImports(cwd, pattern)) signals.push({ preset, evidence });
7762
+ }
6900
7763
  return signals;
6901
7764
  }
6902
7765
  function presetContradiction(declared, cwd) {
@@ -7058,4 +7921,4 @@ export {
7058
7921
  detectFramework,
7059
7922
  dispatch
7060
7923
  };
7061
- //# sourceMappingURL=chunk-XZLF7FBK.js.map
7924
+ //# sourceMappingURL=chunk-U2KUKV4C.js.map