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

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;
@@ -5654,9 +5661,17 @@ function testTarget(options) {
5654
5661
  };
5655
5662
  }
5656
5663
 
5664
+ // src/roles/test/jest-migration.ts
5665
+ import { existsSync as existsSync27, readFileSync as readFileSync26 } from "fs";
5666
+ import { join as join32 } from "path";
5667
+
5668
+ // src/roles/test/codemod/migrate-module.ts
5669
+ import { readFileSync as readFileSync24 } from "fs";
5670
+ import { join as join30 } from "path";
5671
+
5657
5672
  // src/roles/test/read-adoption.ts
5658
5673
  import { existsSync as existsSync25, readFileSync as readFileSync23, readdirSync as readdirSync3 } from "fs";
5659
- import { join as join29 } from "path";
5674
+ import { join as join29, relative as relative5 } from "path";
5660
5675
  var JEST_CONFIG_FILES = [
5661
5676
  "jest.config.ts",
5662
5677
  "jest.config.js",
@@ -5678,6 +5693,13 @@ function rootJestConfigFiles(cwd) {
5678
5693
  var TEST_FILE = /[.-](spec|test)\.[cm]?[jt]sx?$/;
5679
5694
  var NOT_WALKED = /* @__PURE__ */ new Set(["node_modules", "dist", "coverage", ".git", ".nx", ".turbo"]);
5680
5695
  function hasTestFiles(cwd) {
5696
+ return walkTestFiles(cwd, true).length > 0;
5697
+ }
5698
+ function listTestFiles(cwd) {
5699
+ return walkTestFiles(cwd, false).sort();
5700
+ }
5701
+ function walkTestFiles(cwd, stopAtFirst) {
5702
+ const found = [];
5681
5703
  const stack = [cwd];
5682
5704
  while (stack.length > 0) {
5683
5705
  const dir = stack.pop();
@@ -5691,11 +5713,12 @@ function hasTestFiles(cwd) {
5691
5713
  if (entry.isDirectory()) {
5692
5714
  if (!NOT_WALKED.has(entry.name)) stack.push(join29(dir, entry.name));
5693
5715
  } else if (TEST_FILE.test(entry.name)) {
5694
- return true;
5716
+ found.push(relative5(cwd, join29(dir, entry.name)).replaceAll("\\", "/"));
5717
+ if (stopAtFirst) return found;
5695
5718
  }
5696
5719
  }
5697
5720
  }
5698
- return false;
5721
+ return found;
5699
5722
  }
5700
5723
  function readTestConfigSource(cwd, fileName) {
5701
5724
  try {
@@ -5766,6 +5789,769 @@ function readTestAdoption(cwd) {
5766
5789
  };
5767
5790
  }
5768
5791
 
5792
+ // src/roles/test/codemod/migrate-file.ts
5793
+ import { parseSync as parseSync5 } from "oxc-parser";
5794
+
5795
+ // src/roles/test/codemod/scope.ts
5796
+ function readIdentifiers(node) {
5797
+ const names = [];
5798
+ walk(node, (current) => {
5799
+ if (current.type === "Property" || current.type === "ObjectProperty") {
5800
+ if (current.shorthand === true) {
5801
+ names.push(...readIdentifiers(current.value));
5802
+ return SKIP;
5803
+ }
5804
+ if (current.computed === true) return;
5805
+ const key = current.key;
5806
+ if (key?.type !== "Identifier") return;
5807
+ names.push(...readIdentifiers(current.value));
5808
+ return SKIP;
5809
+ }
5810
+ if (current.type === "MemberExpression" || current.type === "StaticMemberExpression") {
5811
+ if (current.computed === true) return;
5812
+ names.push(...readIdentifiers(current.object));
5813
+ return SKIP;
5814
+ }
5815
+ if (current.type === "Identifier") {
5816
+ names.push(current.name);
5817
+ return SKIP;
5818
+ }
5819
+ return void 0;
5820
+ });
5821
+ return names;
5822
+ }
5823
+ function patternNames(pattern) {
5824
+ const names = [];
5825
+ const visit = (node) => {
5826
+ if (node === null || typeof node !== "object") return;
5827
+ const current = node;
5828
+ switch (current.type) {
5829
+ case "Identifier":
5830
+ names.push(current.name);
5831
+ return;
5832
+ case "ObjectPattern":
5833
+ for (const property of current.properties ?? []) {
5834
+ visit(property.type === "RestElement" ? property.argument : property.value);
5835
+ }
5836
+ return;
5837
+ case "ArrayPattern":
5838
+ for (const element of current.elements ?? []) visit(element);
5839
+ return;
5840
+ case "AssignmentPattern":
5841
+ visit(current.left);
5842
+ return;
5843
+ case "RestElement":
5844
+ visit(current.argument);
5845
+ return;
5846
+ default:
5847
+ return;
5848
+ }
5849
+ };
5850
+ visit(pattern);
5851
+ return names;
5852
+ }
5853
+ function moduleBindings(program) {
5854
+ const bindings = /* @__PURE__ */ new Map();
5855
+ for (const statement of program.body ?? []) {
5856
+ if (statement.type !== "VariableDeclaration") continue;
5857
+ for (const declarator of statement.declarations ?? []) {
5858
+ const init = declarator.init ?? void 0;
5859
+ for (const name of patternNames(declarator.id)) bindings.set(name, init);
5860
+ }
5861
+ }
5862
+ return bindings;
5863
+ }
5864
+ function importedNames(program) {
5865
+ const names = /* @__PURE__ */ new Set();
5866
+ for (const statement of program.body ?? []) {
5867
+ if (statement.type !== "ImportDeclaration") continue;
5868
+ for (const specifier of statement.specifiers ?? []) {
5869
+ const local = specifier.local;
5870
+ if (local?.type === "Identifier") names.add(local.name);
5871
+ }
5872
+ }
5873
+ return names;
5874
+ }
5875
+ function declaredInside(factory) {
5876
+ const names = /* @__PURE__ */ new Set();
5877
+ walk(factory, (node) => {
5878
+ if (node.type === "VariableDeclarator" || node.type === "FunctionDeclaration") {
5879
+ for (const name of patternNames(node.id)) names.add(name);
5880
+ }
5881
+ if (node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression") {
5882
+ for (const parameter of node.params ?? []) {
5883
+ for (const name of patternNames(parameter)) names.add(name);
5884
+ }
5885
+ }
5886
+ });
5887
+ return names;
5888
+ }
5889
+ function capturedBindings(factory, bindings) {
5890
+ const own = declaredInside(factory);
5891
+ const captured = /* @__PURE__ */ new Set();
5892
+ for (const name of readIdentifiers(factory)) {
5893
+ if (bindings.has(name) && !own.has(name)) captured.add(name);
5894
+ }
5895
+ return [...captured];
5896
+ }
5897
+ function hoistable(name, bindings, imported, seen = /* @__PURE__ */ new Set()) {
5898
+ if (seen.has(name)) return true;
5899
+ seen.add(name);
5900
+ const init = bindings.get(name);
5901
+ if (init === void 0) return false;
5902
+ for (const read2 of readIdentifiers(init)) {
5903
+ if (imported.has(read2)) return false;
5904
+ if (bindings.has(read2) && !hoistable(read2, bindings, imported, seen)) return false;
5905
+ }
5906
+ return true;
5907
+ }
5908
+
5909
+ // src/roles/test/codemod/hoist.ts
5910
+ function simpleDeclarations(program) {
5911
+ const found = /* @__PURE__ */ new Map();
5912
+ for (const statement of program.body ?? []) {
5913
+ if (statement.type !== "VariableDeclaration") continue;
5914
+ const declarations = statement.declarations ?? [];
5915
+ if (declarations.length !== 1) continue;
5916
+ const declarator = declarations[0];
5917
+ const id = declarator.id;
5918
+ const init = declarator.init;
5919
+ if (id?.type !== "Identifier" || init === void 0 || init === null) continue;
5920
+ found.set(id.name, { statement, init });
5921
+ }
5922
+ return found;
5923
+ }
5924
+ function interdependent(names, bindings) {
5925
+ for (const name of names) {
5926
+ for (const read2 of readIdentifiers(bindings.get(name))) {
5927
+ if (read2 !== name && names.includes(read2)) return `${name} reads ${read2}`;
5928
+ }
5929
+ }
5930
+ return void 0;
5931
+ }
5932
+ function statementSpan(source, statement) {
5933
+ let end = statement.end;
5934
+ if (source[end] === "\r") end++;
5935
+ if (source[end] === "\n") end++;
5936
+ return { start: statement.start, end };
5937
+ }
5938
+ function hoistCapturedBindings(source, program, factories, bindings, imported) {
5939
+ const captured = [...new Set(factories.flatMap((factory) => capturedBindings(factory, bindings)))];
5940
+ if (captured.length === 0) return { source, hoisted: [], refusals: [] };
5941
+ const refusals = [];
5942
+ for (const name of captured) {
5943
+ if (hoistable(name, bindings, imported)) continue;
5944
+ refusals.push({
5945
+ binding: name,
5946
+ 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`
5947
+ });
5948
+ }
5949
+ const declarations = simpleDeclarations(program);
5950
+ for (const name of captured) {
5951
+ if (!declarations.has(name) && !refusals.some((refusal) => refusal.binding === name)) {
5952
+ refusals.push({
5953
+ binding: name,
5954
+ reason: `it is not declared as a plain \`const ${name} = ...\` at module scope`
5955
+ });
5956
+ }
5957
+ }
5958
+ const cycle = interdependent(captured, bindings);
5959
+ if (cycle !== void 0) {
5960
+ refusals.push({
5961
+ binding: captured.join(", "),
5962
+ reason: `${cycle}, and an object literal cannot refer to its own keys`
5963
+ });
5964
+ }
5965
+ if (refusals.length > 0) return { source, hoisted: [], refusals };
5966
+ const ordered = captured.map((name) => ({ name, ...declarations.get(name) })).sort((left, right) => left.statement.start - right.statement.start);
5967
+ const entries = ordered.map(({ name, init }) => `${name}: ${source.slice(init.start, init.end)}`).join(", ");
5968
+ const first = ordered[0];
5969
+ const semi = source.slice(first.statement.start, first.statement.end).trimEnd().endsWith(";");
5970
+ const hoistedLine = `const { ${ordered.map(({ name }) => name).join(", ")} } = vi.hoisted(() => ({ ${entries} }))${semi ? ";" : ""}
5971
+ `;
5972
+ const edits = ordered.map(({ statement }, index) => {
5973
+ const span = statementSpan(source, statement);
5974
+ return { ...span, text: index === 0 ? hoistedLine : "" };
5975
+ });
5976
+ let rewritten = source;
5977
+ for (const edit of edits.sort((left, right) => right.start - left.start)) {
5978
+ rewritten = rewritten.slice(0, edit.start) + edit.text + rewritten.slice(edit.end);
5979
+ }
5980
+ return { source: rewritten, hoisted: ordered.map(({ name }) => name), refusals: [] };
5981
+ }
5982
+
5983
+ // src/roles/test/codemod/inject-type-import.ts
5984
+ import { parseSync as parseSync4 } from "oxc-parser";
5985
+
5986
+ // src/roles/test/codemod/rename.ts
5987
+ import { parseSync as parseSync3 } from "oxc-parser";
5988
+ var VITEST_TYPE_SOURCE = "vitest";
5989
+ var RUNTIME_RENAMES = /* @__PURE__ */ new Set([
5990
+ // Mocks
5991
+ "clearAllMocks",
5992
+ "doMock",
5993
+ "fn",
5994
+ "mock",
5995
+ "mocked",
5996
+ "resetAllMocks",
5997
+ "resetModules",
5998
+ "restoreAllMocks",
5999
+ "spyOn",
6000
+ "unmock",
6001
+ /*
6002
+ * Timers, as a family rather than as the two the corpus happens to use today. Running this over
6003
+ * the repo reported `jest.runAllTimers` as unknown, which was a hole in the table rather than a
6004
+ * finding about the code. Each name here was then checked against a running Vitest, so adding the
6005
+ * siblings is not speculation: a file gaining one tomorrow gets the same answer as one using it
6006
+ * today, instead of a report about a member that was always going to be a rename.
6007
+ */
6008
+ "advanceTimersByTime",
6009
+ "advanceTimersByTimeAsync",
6010
+ "advanceTimersToNextTimer",
6011
+ "clearAllTimers",
6012
+ "getRealSystemTime",
6013
+ "getTimerCount",
6014
+ "runAllTimers",
6015
+ "runAllTimersAsync",
6016
+ "runOnlyPendingTimers",
6017
+ "setSystemTime",
6018
+ "useFakeTimers",
6019
+ "useRealTimers"
6020
+ ]);
6021
+ var TYPE_RENAMES = {
6022
+ Mock: "Mock",
6023
+ MockedClass: "MockedClass",
6024
+ MockedFunction: "MockedFunction",
6025
+ MockedObject: "MockedObject",
6026
+ Mocked: "Mocked",
6027
+ SpiedFunction: "MockInstance",
6028
+ SpyInstance: "MockInstance"
6029
+ };
6030
+ var NAMED_RULE = {
6031
+ isolateModules: `vi.resetModules() followed by a dynamic import, since vi.resetModules() with a require() returns the CACHED module`,
6032
+ requireActual: `await vi.importActual(...), which makes the surrounding factory async`,
6033
+ requireMock: `await vi.importMock(...), which makes the surrounding factory async`,
6034
+ setTimeout: `vi.setConfig({ testTimeout: ... }), which is configuration rather than a call`
6035
+ };
6036
+ function lineOf(source, offset) {
6037
+ let line = 1;
6038
+ for (let index = 0; index < offset && index < source.length; index++) {
6039
+ if (source[index] === "\n") line++;
6040
+ }
6041
+ return line;
6042
+ }
6043
+ function valueEdit(node, source, outcome) {
6044
+ const object = node.object;
6045
+ const property = node.property;
6046
+ if (object?.type !== "Identifier" || object.name !== "jest") return void 0;
6047
+ if (property?.type !== "Identifier") return void 0;
6048
+ const member = property.name;
6049
+ if (RUNTIME_RENAMES.has(member)) {
6050
+ outcome.rewritten++;
6051
+ return { start: object.start, end: object.end, text: "vi" };
6052
+ }
6053
+ outcome.unhandled.push({
6054
+ member: `jest.${member}`,
6055
+ line: lineOf(source, object.start),
6056
+ ...NAMED_RULE[member] === void 0 ? {} : { rule: NAMED_RULE[member] }
6057
+ });
6058
+ return void 0;
6059
+ }
6060
+ function typeEdit(node, source, outcome, needed) {
6061
+ const left = node.left;
6062
+ const right = node.right;
6063
+ if (left?.type !== "Identifier" || left.name !== "jest") return void 0;
6064
+ if (right?.type !== "Identifier") return void 0;
6065
+ const member = right.name;
6066
+ const replacement = TYPE_RENAMES[member];
6067
+ if (replacement === void 0) {
6068
+ outcome.unhandled.push({ member: `jest.${member}`, line: lineOf(source, node.start) });
6069
+ return void 0;
6070
+ }
6071
+ needed.add(replacement);
6072
+ outcome.rewritten++;
6073
+ return { start: node.start, end: node.end, text: replacement };
6074
+ }
6075
+ function renameJestToVitest(fileName, source) {
6076
+ const outcome = { source, typeImports: [], rewritten: 0, unhandled: [] };
6077
+ const { program, errors } = parseSync3(fileName, source, { sourceType: "module" });
6078
+ if (errors.length > 0) return outcome;
6079
+ const edits = [];
6080
+ const needed = /* @__PURE__ */ new Set();
6081
+ walk(program, (node) => {
6082
+ if (node.type === "MemberExpression" || node.type === "StaticMemberExpression") {
6083
+ const edit = valueEdit(node, source, outcome);
6084
+ if (edit !== void 0) edits.push(edit);
6085
+ return;
6086
+ }
6087
+ if (node.type === "TSQualifiedName") {
6088
+ const edit = typeEdit(node, source, outcome, needed);
6089
+ if (edit !== void 0) edits.push(edit);
6090
+ }
6091
+ });
6092
+ outcome.typeImports = [...needed].sort();
6093
+ let rewritten = source;
6094
+ for (const edit of edits.sort((a, b) => b.start - a.start)) {
6095
+ rewritten = rewritten.slice(0, edit.start) + edit.text + rewritten.slice(edit.end);
6096
+ }
6097
+ outcome.source = rewritten;
6098
+ return outcome;
6099
+ }
6100
+
6101
+ // src/roles/test/codemod/inject-type-import.ts
6102
+ function renderTypeImport(names, semi) {
6103
+ return `import type { ${names.join(", ")} } from '${VITEST_TYPE_SOURCE}'${semi ? ";" : ""}`;
6104
+ }
6105
+ function importDeclarations(program) {
6106
+ return (program.body ?? []).filter((node) => node.type === "ImportDeclaration");
6107
+ }
6108
+ function usesSemicolons(source, imports) {
6109
+ const first = imports[0];
6110
+ if (first === void 0) return false;
6111
+ return source.slice(first.start, first.end).trimEnd().endsWith(";");
6112
+ }
6113
+ function existingVitestImport(imports) {
6114
+ return imports.find(
6115
+ (node) => node.source?.value === VITEST_TYPE_SOURCE
6116
+ );
6117
+ }
6118
+ function alreadyImported(declaration) {
6119
+ return (declaration.specifiers ?? []).filter((specifier) => specifier.type === "ImportSpecifier").map((specifier) => specifier.imported?.name).filter((name) => name !== void 0);
6120
+ }
6121
+ function injectTypeImport(fileName, source, typeNames) {
6122
+ if (typeNames.length === 0) return source;
6123
+ const { program, errors } = parseSync4(fileName, source, { sourceType: "module" });
6124
+ if (errors.length > 0) return source;
6125
+ const imports = importDeclarations(program);
6126
+ const semi = usesSemicolons(source, imports);
6127
+ const existing = existingVitestImport(imports);
6128
+ if (existing !== void 0) {
6129
+ const merged = [.../* @__PURE__ */ new Set([...alreadyImported(existing), ...typeNames])].sort();
6130
+ if (merged.length === alreadyImported(existing).length) return source;
6131
+ return source.slice(0, existing.start) + renderTypeImport(merged, semi) + source.slice(existing.end);
6132
+ }
6133
+ const line = renderTypeImport([...typeNames].sort(), semi);
6134
+ const first = imports[0];
6135
+ if (first === void 0) return `${line}
6136
+
6137
+ ${source}`;
6138
+ return source.slice(0, first.start) + line + "\n" + source.slice(first.start);
6139
+ }
6140
+
6141
+ // src/roles/test/codemod/named-rules.ts
6142
+ var ASYNC_REPLACEMENTS = {
6143
+ requireActual: "importActual",
6144
+ requireMock: "importMock"
6145
+ };
6146
+ var FUNCTIONS = /* @__PURE__ */ new Set(["ArrowFunctionExpression", "FunctionExpression", "FunctionDeclaration"]);
6147
+ function walkWithEnclosingFunction(node, enclosing, visit) {
6148
+ if (node === null || typeof node !== "object") return;
6149
+ if (Array.isArray(node)) {
6150
+ for (const entry of node) walkWithEnclosingFunction(entry, enclosing, visit);
6151
+ return;
6152
+ }
6153
+ const current = node;
6154
+ const next = FUNCTIONS.has(current.type) ? current : enclosing;
6155
+ if (typeof current.type === "string") visit(current, enclosing);
6156
+ for (const value of Object.values(current)) {
6157
+ if (value !== null && typeof value === "object") walkWithEnclosingFunction(value, next, visit);
6158
+ }
6159
+ }
6160
+ function ownedCall(node) {
6161
+ if (node.type !== "CallExpression") return void 0;
6162
+ const callee = node.callee;
6163
+ if (callee?.type !== "MemberExpression" && callee?.type !== "StaticMemberExpression") {
6164
+ return void 0;
6165
+ }
6166
+ const object = callee.object;
6167
+ const property = callee.property;
6168
+ if (object?.type !== "Identifier" || object.name !== "jest") return void 0;
6169
+ return property?.type === "Identifier" ? property.name : void 0;
6170
+ }
6171
+ function argumentText(source, call) {
6172
+ const args = call.arguments ?? [];
6173
+ if (args.length === 0) return "";
6174
+ const first = args[0];
6175
+ const last = args[args.length - 1];
6176
+ return source.slice(first.start, last.end);
6177
+ }
6178
+ function applyEdits(source, edits) {
6179
+ let result = source;
6180
+ for (const edit of [...edits].sort((left, right) => right.start - left.start)) {
6181
+ result = result.slice(0, edit.start) + edit.text + result.slice(edit.end);
6182
+ }
6183
+ return result;
6184
+ }
6185
+ function applyNamedRules(source, program) {
6186
+ const edits = [];
6187
+ const applied = /* @__PURE__ */ new Set();
6188
+ const asyncify = /* @__PURE__ */ new Set();
6189
+ walkWithEnclosingFunction(program, void 0, (node, enclosing) => {
6190
+ const member = ownedCall(node);
6191
+ if (member === void 0) return;
6192
+ const asyncReplacement = ASYNC_REPLACEMENTS[member];
6193
+ if (asyncReplacement !== void 0) {
6194
+ edits.push({
6195
+ start: node.start,
6196
+ end: node.end,
6197
+ text: `(await vi.${asyncReplacement}(${argumentText(source, node)}))`
6198
+ });
6199
+ applied.add(`jest.${member}`);
6200
+ if (enclosing !== void 0 && enclosing.async !== true) asyncify.add(enclosing);
6201
+ return;
6202
+ }
6203
+ if (member === "setTimeout") {
6204
+ edits.push({
6205
+ start: node.start,
6206
+ end: node.end,
6207
+ text: `vi.setConfig({ testTimeout: ${argumentText(source, node)} })`
6208
+ });
6209
+ applied.add("jest.setTimeout");
6210
+ }
6211
+ });
6212
+ for (const fn of asyncify) {
6213
+ edits.push({ start: fn.start, end: fn.start, text: "async " });
6214
+ }
6215
+ return { source: applyEdits(source, edits), applied: [...applied].sort() };
6216
+ }
6217
+
6218
+ // src/roles/test/codemod/migrate-file.ts
6219
+ function mockFactories(program) {
6220
+ const factories = [];
6221
+ walk(program, (node) => {
6222
+ if (node.type !== "CallExpression") return;
6223
+ const callee = node.callee;
6224
+ if (callee?.type !== "MemberExpression" && callee?.type !== "StaticMemberExpression") return;
6225
+ const object = callee.object;
6226
+ const property = callee.property;
6227
+ if (object?.type !== "Identifier") return;
6228
+ if (object.name !== "jest" && object.name !== "vi") return;
6229
+ if (property?.name !== "mock") return;
6230
+ const args = node.arguments ?? [];
6231
+ if (args.length > 1) factories.push(args[1]);
6232
+ });
6233
+ return factories;
6234
+ }
6235
+ function parse2(fileName, source) {
6236
+ const { program, errors } = parseSync5(fileName, source, { sourceType: "module" });
6237
+ return errors.length > 0 ? void 0 : program;
6238
+ }
6239
+ function migrateTestFile(fileName, source) {
6240
+ const untouched = {
6241
+ source,
6242
+ changed: false,
6243
+ renamed: 0,
6244
+ hoisted: [],
6245
+ unhandled: [],
6246
+ refusals: [],
6247
+ rulesApplied: []
6248
+ };
6249
+ const original = parse2(fileName, source);
6250
+ if (original === void 0) return untouched;
6251
+ const ruled = applyNamedRules(source, original);
6252
+ const renamed = renameJestToVitest(fileName, ruled.source);
6253
+ const program = parse2(fileName, renamed.source);
6254
+ if (program === void 0) return untouched;
6255
+ const hoist = hoistCapturedBindings(
6256
+ renamed.source,
6257
+ program,
6258
+ mockFactories(program),
6259
+ moduleBindings(program),
6260
+ importedNames(program)
6261
+ );
6262
+ const withImport = injectTypeImport(fileName, hoist.source, renamed.typeImports);
6263
+ return {
6264
+ source: withImport,
6265
+ changed: withImport !== source,
6266
+ renamed: renamed.rewritten,
6267
+ hoisted: hoist.hoisted,
6268
+ unhandled: renamed.unhandled,
6269
+ refusals: hoist.refusals,
6270
+ rulesApplied: ruled.applied
6271
+ };
6272
+ }
6273
+
6274
+ // src/roles/test/codemod/migrate-module.ts
6275
+ function read(path) {
6276
+ try {
6277
+ return readFileSync24(path, "utf8");
6278
+ } catch {
6279
+ return void 0;
6280
+ }
6281
+ }
6282
+ function migrateModuleTests(cwd) {
6283
+ const operations = [];
6284
+ const blockers = [];
6285
+ let files = 0;
6286
+ let renamed = 0;
6287
+ let hoisted = 0;
6288
+ for (const relative7 of listTestFiles(cwd)) {
6289
+ const source = read(join30(cwd, relative7));
6290
+ if (source === void 0) {
6291
+ blockers.push({ file: relative7, reason: "this file could not be read" });
6292
+ continue;
6293
+ }
6294
+ const migrated = migrateTestFile(join30(cwd, relative7), source);
6295
+ for (const member of migrated.unhandled) {
6296
+ blockers.push({
6297
+ file: `${relative7}:${member.line}`,
6298
+ 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}`
6299
+ });
6300
+ }
6301
+ for (const refusal of migrated.refusals) {
6302
+ blockers.push({
6303
+ file: relative7,
6304
+ reason: `\`${refusal.binding}\` cannot move into vi.hoisted: ${refusal.reason}`
6305
+ });
6306
+ }
6307
+ if (!migrated.changed) continue;
6308
+ files++;
6309
+ renamed += migrated.renamed;
6310
+ if (migrated.hoisted.length > 0) hoisted++;
6311
+ operations.push({ kind: "write", path: relative7, contents: migrated.source });
6312
+ }
6313
+ return blockers.length > 0 ? { operations: [], files, renamed, hoisted, blockers } : { operations, files, renamed, hoisted, blockers: [] };
6314
+ }
6315
+
6316
+ // src/roles/test/nest/generate-config.ts
6317
+ import { relative as relative6 } from "path";
6318
+ var NEST_TEST_PRESET_SPECIFIER = "@hublo/sentinel/test/nest";
6319
+ var SENTINEL_TEST_SETUP = "@hublo/sentinel/test/setup/nest";
6320
+ var NEST_TEST_CONFIG_FILE = "vitest.config.mts";
6321
+ function hopsToRoot(cwd, workspaceRoot) {
6322
+ const up = relative6(cwd, workspaceRoot).replaceAll("\\", "/");
6323
+ return up === "" ? "." : up;
6324
+ }
6325
+ function coverageBlock(reading) {
6326
+ return reading.coverageDirectory === void 0 ? "" : ` coverage: { reportsDirectory: '${reading.coverageDirectory}' },`;
6327
+ }
6328
+ function environmentBlock(reading) {
6329
+ return reading.testEnvironment === void 0 ? "" : ` environment: '${reading.testEnvironment}',`;
6330
+ }
6331
+ function generateNestTestConfig(cwd, reading) {
6332
+ if (reading.refusals.length > 0) return { refusals: reading.refusals };
6333
+ const workspaceRoot = findWorkspaceRoot(cwd);
6334
+ if (workspaceRoot === void 0) {
6335
+ return { refusals: ["the workspace root could not be found from this module"] };
6336
+ }
6337
+ const up = hopsToRoot(cwd, workspaceRoot);
6338
+ const test = [
6339
+ environmentBlock(reading),
6340
+ ` setupFiles: ['${SENTINEL_TEST_SETUP}'],`,
6341
+ coverageBlock(reading)
6342
+ ].filter((line) => line !== "");
6343
+ return {
6344
+ refusals: [],
6345
+ source: `import path from 'node:path'
6346
+
6347
+ import { defineConfig, nestTestConfig } from '${NEST_TEST_PRESET_SPECIFIER}'
6348
+
6349
+ export default defineConfig(
6350
+ nestTestConfig({
6351
+ root: import.meta.dirname,
6352
+ workspaceRoot: path.resolve(import.meta.dirname, '${up}'),
6353
+ overrides: {
6354
+ test: {
6355
+ ${test.join("\n")}
6356
+ },
6357
+ },
6358
+ }),
6359
+ )
6360
+ `
6361
+ };
6362
+ }
6363
+
6364
+ // src/roles/test/nest/read-jest-config.ts
6365
+ import { existsSync as existsSync26, readFileSync as readFileSync25 } from "fs";
6366
+ import { join as join31 } from "path";
6367
+ var CARRIED = ["testEnvironment", "coverageDirectory", "setupFilesAfterEnv"];
6368
+ var DISCARDED = ["preset", "displayName", "moduleFileExtensions"];
6369
+ var STRING = String.raw`['"]([^'"]*)['"]`;
6370
+ function readString(body, key) {
6371
+ return new RegExp(String.raw`\b${key}\s*:\s*${STRING}`).exec(body)?.[1];
6372
+ }
6373
+ function readStringArray(body, key) {
6374
+ const raw = new RegExp(String.raw`\b${key}\s*:\s*\[([^\]]*)\]`).exec(body)?.[1];
6375
+ if (raw === void 0) return void 0;
6376
+ return [...raw.matchAll(new RegExp(STRING, "g"))].map((match) => match[1]);
6377
+ }
6378
+ function configBody(source) {
6379
+ const start = /(?:const\s+\w+(?::\s*\w+)?\s*=|export\s+default)\s*\{/.exec(source);
6380
+ if (start === null) return void 0;
6381
+ let depth = 0;
6382
+ const from = start.index + start[0].length - 1;
6383
+ for (let i = from; i < source.length; i += 1) {
6384
+ const char = source[i];
6385
+ if (char === "{") depth += 1;
6386
+ else if (char === "}") {
6387
+ depth -= 1;
6388
+ if (depth === 0) return source.slice(from + 1, i);
6389
+ }
6390
+ }
6391
+ return void 0;
6392
+ }
6393
+ function declaredKeys(body) {
6394
+ const keys = [];
6395
+ let depth = 0;
6396
+ for (const line of body.split("\n")) {
6397
+ if (depth === 0) {
6398
+ const key = /^\s*([A-Za-z_$][\w$]*)\s*:/.exec(line);
6399
+ if (key !== null) keys.push(key[1]);
6400
+ }
6401
+ for (const char of line) {
6402
+ if (char === "{" || char === "[") depth += 1;
6403
+ else if (char === "}" || char === "]") depth -= 1;
6404
+ }
6405
+ }
6406
+ return keys;
6407
+ }
6408
+ function sideEffects(source) {
6409
+ const found = [];
6410
+ if (/^\s*process\.env\s*[.=]/m.test(source)) {
6411
+ found.push("it sets `process.env`, which the generated config would not carry");
6412
+ }
6413
+ const valueImport = /^\s*import\s+(?!type\b)[^\n]*$/m.exec(source);
6414
+ if (valueImport !== null && !/from\s+['"]jest['"]/.test(valueImport[0])) {
6415
+ found.push(`it imports a value (\`${valueImport[0].trim()}\`)`);
6416
+ }
6417
+ return found;
6418
+ }
6419
+ function readJestConfig(cwd) {
6420
+ const file = JEST_CONFIG_FILES.find((name) => existsSync26(join31(cwd, name)));
6421
+ if (file === void 0) return void 0;
6422
+ let source;
6423
+ try {
6424
+ source = readFileSync25(join31(cwd, file), "utf8");
6425
+ } catch {
6426
+ return { file, refusals: [`${file} could not be read`] };
6427
+ }
6428
+ const refusals = sideEffects(source);
6429
+ const body = configBody(source);
6430
+ if (body === void 0) {
6431
+ return { file, refusals: [...refusals, "its config object could not be located"] };
6432
+ }
6433
+ const known = /* @__PURE__ */ new Set([...CARRIED, ...DISCARDED]);
6434
+ for (const key of declaredKeys(body)) {
6435
+ if (!known.has(key)) refusals.push(`it declares \`${key}\`, which is not transposed`);
6436
+ }
6437
+ const setup = readStringArray(source, "setupFilesAfterEnv");
6438
+ return {
6439
+ file,
6440
+ ...readString(source, "testEnvironment") !== void 0 ? { testEnvironment: readString(source, "testEnvironment") } : {},
6441
+ ...readString(source, "coverageDirectory") !== void 0 ? { coverageDirectory: readString(source, "coverageDirectory") } : {},
6442
+ ...setup !== void 0 ? { setupFilesAfterEnv: setup } : {},
6443
+ refusals
6444
+ };
6445
+ }
6446
+
6447
+ // src/roles/test/jest-migration.ts
6448
+ var SPEC_TSCONFIG = "tsconfig.spec.json";
6449
+ var VITEST_GLOBAL_TYPES = "vitest/globals";
6450
+ function specTypesOperation(cwd) {
6451
+ const path = join32(cwd, SPEC_TSCONFIG);
6452
+ if (!existsSync27(path)) return void 0;
6453
+ let config;
6454
+ try {
6455
+ config = parseJsonc(
6456
+ readFileSync26(path, "utf8"),
6457
+ SPEC_TSCONFIG
6458
+ );
6459
+ } catch {
6460
+ return void 0;
6461
+ }
6462
+ const types = config?.compilerOptions?.types;
6463
+ if (!Array.isArray(types) || !types.includes("jest")) return void 0;
6464
+ return {
6465
+ kind: "merge-json",
6466
+ path: SPEC_TSCONFIG,
6467
+ value: {
6468
+ compilerOptions: {
6469
+ types: types.map((entry) => entry === "jest" ? VITEST_GLOBAL_TYPES : entry)
6470
+ }
6471
+ }
6472
+ };
6473
+ }
6474
+ function siblingScriptRewrites(cwd) {
6475
+ const rewrites = {};
6476
+ for (const [name, command] of Object.entries(moduleScripts(cwd))) {
6477
+ if (name === TEST_SCRIPT_NAME || typeof command !== "string") continue;
6478
+ const rewritten = rewriteVitestScript(command);
6479
+ if (rewritten !== command) rewrites[name] = rewritten;
6480
+ }
6481
+ return rewrites;
6482
+ }
6483
+ function blocked(reason) {
6484
+ return { operations: [], blocked: reason };
6485
+ }
6486
+ function planJestMigration(context, adoption) {
6487
+ if (adoption.jestConfigs.length > 1) {
6488
+ const [primary, ...rest] = adoption.jestConfigs;
6489
+ return blocked(
6490
+ `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.`
6491
+ );
6492
+ }
6493
+ const reading = readJestConfig(context.cwd);
6494
+ if (reading === void 0) {
6495
+ return blocked(`${adoption.jestConfigs.join(", ")} could not be read, so nothing was written.`);
6496
+ }
6497
+ const generated = generateNestTestConfig(context.cwd, reading);
6498
+ if (generated.source === void 0) {
6499
+ return blocked(
6500
+ `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.`
6501
+ );
6502
+ }
6503
+ const migration = migrateModuleTests(context.cwd);
6504
+ if (migration.blockers.length > 0) {
6505
+ const listed = migration.blockers.map((blocker) => `
6506
+ ${blocker.file}: ${blocker.reason}`).join("");
6507
+ return blocked(
6508
+ `${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}`
6509
+ );
6510
+ }
6511
+ const existing = projectTargets(context.cwd)?.[TEST_SCRIPT_NAME];
6512
+ const siblings = siblingScriptRewrites(context.cwd);
6513
+ const specTypes = specTypesOperation(context.cwd);
6514
+ const operations = [
6515
+ { kind: "write", path: NEST_TEST_CONFIG_FILE, contents: generated.source },
6516
+ ...migration.operations,
6517
+ ...specTypes === void 0 ? [] : [specTypes],
6518
+ manifestOperation(context.cwd, { [TEST_SCRIPT_NAME]: SENTINEL_TEST_COMMAND, ...siblings }),
6519
+ ...nxTargetOperations({
6520
+ cwd: context.cwd,
6521
+ targets: testTarget({
6522
+ configFiles: [NEST_TEST_CONFIG_FILE],
6523
+ outputs: existing?.outputs ?? [],
6524
+ dependsOn: existing?.dependsOn ?? [],
6525
+ configurations: existing?.configurations ?? {}
6526
+ })
6527
+ }),
6528
+ ...adoption.jestConfigs.map((name) => ({ kind: "delete", path: name }))
6529
+ ];
6530
+ if (existing !== void 0) {
6531
+ operations.push({
6532
+ kind: "remove-json-keys",
6533
+ path: "project.json",
6534
+ keys: [["targets", TEST_SCRIPT_NAME]]
6535
+ });
6536
+ }
6537
+ const notes = [
6538
+ `${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` : ""}.`,
6539
+ `${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.`
6540
+ ];
6541
+ if (specTypes !== void 0) {
6542
+ notes.push(
6543
+ `${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.`
6544
+ );
6545
+ }
6546
+ notes.push(
6547
+ `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.`
6548
+ );
6549
+ if (Object.keys(siblings).length > 0) {
6550
+ notes.push(`rewrote ${Object.keys(siblings).join(", ")} to run through sentinel.`);
6551
+ }
6552
+ return { operations, notes };
6553
+ }
6554
+
5769
6555
  // src/roles/test/plan.ts
5770
6556
  function ownedTestDependencies(cwd, declared) {
5771
6557
  return declared.filter((entry) => SENTINEL_OWNED_TEST_PACKAGES.includes(entry.name)).flatMap((entry) => [
@@ -5786,10 +6572,44 @@ function plan3(context) {
5786
6572
  }
5787
6573
  const configFile = testConfigFile(context.cwd);
5788
6574
  const adoption = readTestAdoption(context.cwd);
6575
+ if (adoption.state === "no-tests") {
6576
+ const operations2 = adoption.jestConfigs.map((name) => ({
6577
+ kind: "delete",
6578
+ path: name
6579
+ }));
6580
+ const owned2 = ownedTestDependencies(context.cwd, adoption.ownDeclarations);
6581
+ if (owned2.length > 0) {
6582
+ operations2.push({ kind: "remove-json-keys", path: "package.json", keys: owned2 });
6583
+ }
6584
+ const hasTarget = projectTargets(context.cwd)?.[TEST_SCRIPT_NAME] !== void 0;
6585
+ if (hasTarget) {
6586
+ operations2.push({
6587
+ kind: "remove-json-keys",
6588
+ path: "project.json",
6589
+ keys: [["targets", TEST_SCRIPT_NAME]]
6590
+ });
6591
+ }
6592
+ if (operations2.length === 0) {
6593
+ return {
6594
+ operations: [],
6595
+ skipped: `this module has no test and no jest leftovers, so there is nothing to do here.`
6596
+ };
6597
+ }
6598
+ return {
6599
+ operations: operations2,
6600
+ notes: [
6601
+ // Names what is actually removed, never a shape this module does not have: half of these
6602
+ // modules carry a config with no target behind it, and claiming otherwise would describe
6603
+ // an operation the plan never performs.
6604
+ `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.`
6605
+ ]
6606
+ };
6607
+ }
5789
6608
  if (configFile === void 0) {
6609
+ if (adoption.state === "jest") return planJestMigration(context, adoption);
5790
6610
  return {
5791
6611
  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.`
6612
+ 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
6613
  };
5794
6614
  }
5795
6615
  const operations = [];
@@ -5948,9 +6768,9 @@ function registerTest() {
5948
6768
 
5949
6769
  // src/roles/typescript/adapters/tsc/tsc.adapter.ts
5950
6770
  import { spawnSync as spawnSync6 } from "child_process";
5951
- import { existsSync as existsSync26, readFileSync as readFileSync25 } from "fs";
6771
+ import { existsSync as existsSync28, readFileSync as readFileSync28 } from "fs";
5952
6772
  import { createRequire as createRequire4 } from "module";
5953
- import { join as join31 } from "path";
6773
+ import { join as join34 } from "path";
5954
6774
 
5955
6775
  // src/roles/typescript/presets/nest.json
5956
6776
  var nest_default2 = {
@@ -6131,8 +6951,8 @@ function readTsconfigAdoption(cwd) {
6131
6951
  }
6132
6952
 
6133
6953
  // src/roles/typescript/adapters/tsc/plan.ts
6134
- import { readFileSync as readFileSync24 } from "fs";
6135
- import { join as join30 } from "path";
6954
+ import { readFileSync as readFileSync27 } from "fs";
6955
+ import { join as join33 } from "path";
6136
6956
 
6137
6957
  // src/roles/typescript/typecheck-script.ts
6138
6958
  var SENTINEL_TYPECHECK_COMMAND = "sentinel --run --typescript";
@@ -6225,7 +7045,7 @@ function planAdoption(context) {
6225
7045
  };
6226
7046
  }
6227
7047
  const existing = parseJsonc(
6228
- readFileSync24(join30(context.cwd, target.path), "utf8"),
7048
+ readFileSync27(join33(context.cwd, target.path), "utf8"),
6229
7049
  target.path
6230
7050
  );
6231
7051
  const extendsChain = composeExtends(existing.extends, preset);
@@ -6363,7 +7183,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
6363
7183
  let chain;
6364
7184
  try {
6365
7185
  const parsed = parseJsonc(
6366
- readFileSync25(join31(cwd, target.path), "utf8"),
7186
+ readFileSync28(join34(cwd, target.path), "utf8"),
6367
7187
  target.path
6368
7188
  );
6369
7189
  chain = parsed.extends;
@@ -6376,7 +7196,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
6376
7196
  );
6377
7197
  if (preset === void 0) return void 0;
6378
7198
  try {
6379
- createRequire4(join31(cwd, "noop.js")).resolve(preset);
7199
+ createRequire4(join34(cwd, "noop.js")).resolve(preset);
6380
7200
  return void 0;
6381
7201
  } catch {
6382
7202
  return preset;
@@ -6472,7 +7292,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
6472
7292
  referencedProjects(cwd, config) {
6473
7293
  try {
6474
7294
  const parsed = parseJsonc(
6475
- readFileSync25(join31(cwd, config), "utf8"),
7295
+ readFileSync28(join34(cwd, config), "utf8"),
6476
7296
  config
6477
7297
  );
6478
7298
  const referenced = (parsed.references ?? []).map((reference) => reference.path).filter((path) => typeof path === "string" && path.length > 0);
@@ -6488,7 +7308,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
6488
7308
  * check.
6489
7309
  */
6490
7310
  typecheckTarget(cwd) {
6491
- if (existsSync26(join31(cwd, "tsconfig.json"))) return "tsconfig.json";
7311
+ if (existsSync28(join34(cwd, "tsconfig.json"))) return "tsconfig.json";
6492
7312
  const target = resolveTsconfigTarget(cwd);
6493
7313
  return target.reason === "none" ? null : target.path;
6494
7314
  }
@@ -6693,9 +7513,9 @@ function replaceLines(current, replacements) {
6693
7513
  }
6694
7514
 
6695
7515
  // src/core/apply-plan.ts
6696
- import { existsSync as existsSync27, readFileSync as readFileSync26, renameSync, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "fs";
7516
+ import { existsSync as existsSync29, readFileSync as readFileSync29, renameSync, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "fs";
6697
7517
  import { resolve as resolve4, sep } from "path";
6698
- import { applyEdits, findNodeAtLocation, modify, parseTree } from "jsonc-parser";
7518
+ import { applyEdits as applyEdits2, findNodeAtLocation, modify, parseTree } from "jsonc-parser";
6699
7519
 
6700
7520
  // src/shared/deep-merge.ts
6701
7521
  function isPlainObject(value) {
@@ -6712,7 +7532,7 @@ function resolveWithinRoot(cwd, relativePath) {
6712
7532
  return absolutePath;
6713
7533
  }
6714
7534
  function readIfExists(absolutePath) {
6715
- return existsSync27(absolutePath) ? readFileSync26(absolutePath, "utf8") : void 0;
7535
+ return existsSync29(absolutePath) ? readFileSync29(absolutePath, "utf8") : void 0;
6716
7536
  }
6717
7537
  function* leaves(value, prefix = []) {
6718
7538
  for (const [key, keyValue] of Object.entries(value)) {
@@ -6727,7 +7547,7 @@ function mergeJsonc(current, value) {
6727
7547
  const edits = modify(text, path, leaf, {
6728
7548
  formattingOptions: { insertSpaces: true, tabSize: 2 }
6729
7549
  });
6730
- text = applyEdits(text, edits);
7550
+ text = applyEdits2(text, edits);
6731
7551
  }
6732
7552
  return text.endsWith("\n") ? text : text + "\n";
6733
7553
  }
@@ -6739,7 +7559,7 @@ function removeJsoncKeys(current, keys) {
6739
7559
  const edits = modify(text, path, void 0, {
6740
7560
  formattingOptions: { insertSpaces: true, tabSize: 2 }
6741
7561
  });
6742
- text = applyEdits(text, edits);
7562
+ text = applyEdits2(text, edits);
6743
7563
  }
6744
7564
  return text.endsWith("\n") ? text : text + "\n";
6745
7565
  }
@@ -6830,8 +7650,8 @@ function applyPlan(cwd, plan4) {
6830
7650
  }
6831
7651
 
6832
7652
  // 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";
7653
+ import { existsSync as existsSync30, readdirSync as readdirSync4, readFileSync as readFileSync30 } from "fs";
7654
+ import { join as join35 } from "path";
6835
7655
  var PATH_SIGNALS = [
6836
7656
  {
6837
7657
  preset: "nest",
@@ -6844,12 +7664,42 @@ var DEPENDENCY_SIGNALS = [
6844
7664
  { preset: "svelte", pattern: /^svelte$|^@sveltejs\// },
6845
7665
  { preset: "nest", pattern: /^@nestjs\// }
6846
7666
  ];
7667
+ var IMPORT_SIGNALS = [
7668
+ { preset: "nest", pattern: /@nestjs\//, evidence: 'its code imports "@nestjs/"' }
7669
+ ];
7670
+ var SOURCE_FILE = /\.[cm]?[jt]sx?$/;
7671
+ var NOT_SCANNED = /* @__PURE__ */ new Set(["node_modules", "dist", "coverage", ".git", ".nx", ".turbo", "build"]);
7672
+ function sourceImports(cwd, pattern) {
7673
+ const stack = [cwd];
7674
+ while (stack.length > 0) {
7675
+ const dir = stack.pop();
7676
+ let entries;
7677
+ try {
7678
+ entries = readdirSync4(dir, { withFileTypes: true });
7679
+ } catch {
7680
+ continue;
7681
+ }
7682
+ for (const entry of entries) {
7683
+ if (entry.isDirectory()) {
7684
+ if (!NOT_SCANNED.has(entry.name)) stack.push(join35(dir, entry.name));
7685
+ continue;
7686
+ }
7687
+ if (!SOURCE_FILE.test(entry.name)) continue;
7688
+ try {
7689
+ if (pattern.test(readFileSync30(join35(dir, entry.name), "utf8"))) return true;
7690
+ } catch {
7691
+ continue;
7692
+ }
7693
+ }
7694
+ }
7695
+ return false;
7696
+ }
6847
7697
  function dependencyNames(cwd) {
6848
- const path = join32(cwd, "package.json");
6849
- if (!existsSync28(path)) return [];
7698
+ const path = join35(cwd, "package.json");
7699
+ if (!existsSync30(path)) return [];
6850
7700
  try {
6851
7701
  const manifest = parseJsonc(
6852
- readFileSync27(path, "utf8"),
7702
+ readFileSync30(path, "utf8"),
6853
7703
  path
6854
7704
  );
6855
7705
  return [
@@ -6872,7 +7722,7 @@ function declaresJsx(cwd) {
6872
7722
  for (const name of entries) {
6873
7723
  try {
6874
7724
  const config = parseJsonc(
6875
- readFileSync27(join32(cwd, name), "utf8"),
7725
+ readFileSync30(join35(cwd, name), "utf8"),
6876
7726
  name
6877
7727
  );
6878
7728
  if (config.compilerOptions?.jsx !== void 0) return true;
@@ -6897,6 +7747,10 @@ function presetSignals(cwd) {
6897
7747
  signals.push({ preset, evidence });
6898
7748
  }
6899
7749
  }
7750
+ for (const { preset, pattern, evidence } of IMPORT_SIGNALS) {
7751
+ if (signals.some((signal) => signal.preset === preset)) continue;
7752
+ if (sourceImports(cwd, pattern)) signals.push({ preset, evidence });
7753
+ }
6900
7754
  return signals;
6901
7755
  }
6902
7756
  function presetContradiction(declared, cwd) {
@@ -7058,4 +7912,4 @@ export {
7058
7912
  detectFramework,
7059
7913
  dispatch
7060
7914
  };
7061
- //# sourceMappingURL=chunk-XZLF7FBK.js.map
7915
+ //# sourceMappingURL=chunk-GQID2U34.js.map