@hublo/sentinel 1.4.0-alpha.14 → 1.4.0-alpha.16

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.
@@ -16,7 +16,7 @@ import {
16
16
  registerAdapters,
17
17
  resolve,
18
18
  resolveBin
19
- } from "../chunk-YHGCBMKJ.js";
19
+ } from "../chunk-XGC2KK5J.js";
20
20
  import {
21
21
  WORKSPACE_ROOT_MARKER,
22
22
  ensureWorkspacePrep,
@@ -6990,6 +6990,78 @@ function rewriteMockFactoryDefault(source, program, defaultImported) {
6990
6990
  };
6991
6991
  }
6992
6992
 
6993
+ // src/roles/test/codemod/mock-promise-resolve.ts
6994
+ var MOCK_FACTORY = /* @__PURE__ */ new Set(["fn", "spyOn"]);
6995
+ function isMockFactory(source, node) {
6996
+ if (node.type !== "CallExpression") return false;
6997
+ const callee = node.callee;
6998
+ if (callee?.type !== "MemberExpression") return false;
6999
+ const object = callee.object;
7000
+ const property = callee.property;
7001
+ if (object?.type !== "Identifier" || property?.type !== "Identifier") return false;
7002
+ const runner = source.slice(object.start, object.end);
7003
+ const method = source.slice(property.start, property.end);
7004
+ return (runner === "jest" || runner === "vi") && MOCK_FACTORY.has(method);
7005
+ }
7006
+ function resolvedValue(source, executor) {
7007
+ if (executor.type !== "ArrowFunctionExpression" && executor.type !== "FunctionExpression") {
7008
+ return void 0;
7009
+ }
7010
+ const params = executor.params;
7011
+ if (params?.length !== 1) return void 0;
7012
+ const parameter = params[0];
7013
+ if (parameter.type !== "Identifier") return void 0;
7014
+ const resolveName = source.slice(parameter.start, parameter.end);
7015
+ const body = executor.body;
7016
+ if (body === void 0) return void 0;
7017
+ let call;
7018
+ if (body.type === "BlockStatement") {
7019
+ const statements = body.body;
7020
+ if (statements?.length !== 1) return void 0;
7021
+ const only = statements[0];
7022
+ if (only.type !== "ExpressionStatement") return void 0;
7023
+ call = only.expression;
7024
+ } else {
7025
+ call = body;
7026
+ }
7027
+ if (call?.type !== "CallExpression") return void 0;
7028
+ const callee = call.callee;
7029
+ if (callee?.type !== "Identifier") return void 0;
7030
+ if (source.slice(callee.start, callee.end) !== resolveName) return void 0;
7031
+ const args = call.arguments;
7032
+ if (args?.length !== 1) return void 0;
7033
+ return args[0];
7034
+ }
7035
+ function rewriteMockPromiseResolve(source, program) {
7036
+ const edits = [];
7037
+ walk(program, (node) => {
7038
+ if (!isMockFactory(source, node)) return;
7039
+ for (const argument of node.arguments ?? []) {
7040
+ walk(argument, (inner) => {
7041
+ if (inner.type !== "NewExpression") return;
7042
+ const callee = inner.callee;
7043
+ if (callee?.type !== "Identifier") return;
7044
+ if (source.slice(callee.start, callee.end) !== "Promise") return;
7045
+ if ((inner.typeArguments ?? inner.typeParameters) !== void 0) return;
7046
+ const executors = inner.arguments ?? [];
7047
+ if (executors.length !== 1) return;
7048
+ const value = resolvedValue(source, executors[0]);
7049
+ if (value === void 0) return;
7050
+ edits.push({
7051
+ start: inner.start,
7052
+ end: inner.end,
7053
+ text: `Promise.resolve(${source.slice(value.start, value.end)})`
7054
+ });
7055
+ });
7056
+ }
7057
+ });
7058
+ let result = source;
7059
+ for (const edit of [...edits].sort((left, right) => right.start - left.start)) {
7060
+ result = `${result.slice(0, edit.start)}${edit.text}${result.slice(edit.end)}`;
7061
+ }
7062
+ return { source: result, rewritten: edits.length };
7063
+ }
7064
+
6993
7065
  // src/roles/test/codemod/propagate-await.ts
6994
7066
  import { parseSync as parseSync5 } from "oxc-parser";
6995
7067
  var FUNCTIONS3 = /* @__PURE__ */ new Set(["ArrowFunctionExpression", "FunctionExpression", "FunctionDeclaration"]);
@@ -7256,6 +7328,8 @@ function repointMswServer(fileName, source) {
7256
7328
 
7257
7329
  // src/roles/test/codemod/spy-instance-type.ts
7258
7330
  var SPY_TYPES = /* @__PURE__ */ new Set(["SpyInstance"]);
7331
+ var QUALIFIED_ONLY = /* @__PURE__ */ new Set(["Mock"]);
7332
+ var VITEST_NAME = { SpyInstance: "MockInstance", Mock: "Mock" };
7259
7333
  function typeNameOf(source, node) {
7260
7334
  const name = node.typeName;
7261
7335
  if (name === void 0) return void 0;
@@ -7267,17 +7341,20 @@ function rewriteSpyInstanceType(source, program) {
7267
7341
  if (node.type !== "TSTypeReference") return;
7268
7342
  const written = typeNameOf(source, node);
7269
7343
  if (written === void 0) return;
7270
- const bare = written.startsWith("jest.") ? written.slice("jest.".length) : written;
7271
- if (!SPY_TYPES.has(bare)) return;
7344
+ const qualified = written.startsWith("jest.");
7345
+ const bare = qualified ? written.slice("jest.".length) : written;
7346
+ if (!SPY_TYPES.has(bare) && !(qualified && QUALIFIED_ONLY.has(bare))) return;
7272
7347
  const args = (node.typeArguments ?? node.typeParameters)?.params;
7273
7348
  if (args === void 0 || args.length === 0) return;
7274
7349
  const text = (argument) => source.slice(argument.start, argument.end);
7275
7350
  const returns = text(args[0]);
7276
7351
  const parameters = args.length > 1 ? `...args: ${text(args[1])}` : "...args: never[]";
7352
+ const target = VITEST_NAME[bare];
7277
7353
  edits.push({
7278
7354
  start: node.start,
7279
7355
  end: node.end,
7280
- text: `MockInstance<(${parameters}) => ${returns}>`
7356
+ text: `${target}<(${parameters}) => ${returns}>`,
7357
+ imports: target
7281
7358
  });
7282
7359
  });
7283
7360
  let result = source;
@@ -7287,7 +7364,8 @@ function rewriteSpyInstanceType(source, program) {
7287
7364
  return {
7288
7365
  source: result,
7289
7366
  rewritten: edits.length,
7290
- typeImports: edits.length > 0 ? ["MockInstance"] : []
7367
+ // Only the names actually written, so a file does not import a type it never uses.
7368
+ typeImports: [...new Set(edits.map((edit) => edit.imports))]
7291
7369
  };
7292
7370
  }
7293
7371
 
@@ -7375,7 +7453,10 @@ function migrateTestFile(fileName, source, defaultImported = /* @__PURE__ */ new
7375
7453
  const afterEmpty = parse2(fileName, emptyImplementations.source);
7376
7454
  if (afterEmpty === void 0) return unparsable("empty mock implementation");
7377
7455
  const hooks = rewriteHookReturnValue(emptyImplementations.source, afterEmpty);
7378
- const awaited = propagateAwait(fileName, hooks.source, original, asyncHelpers);
7456
+ const afterHooks = parse2(fileName, hooks.source);
7457
+ if (afterHooks === void 0) return unparsable("hook return value");
7458
+ const promises = rewriteMockPromiseResolve(hooks.source, afterHooks);
7459
+ const awaited = propagateAwait(fileName, promises.source, original, asyncHelpers);
7379
7460
  const program = parse2(fileName, awaited.source);
7380
7461
  if (program === void 0) return unparsable("hook body");
7381
7462
  const hoist = hoistCapturedBindings(
@@ -8208,17 +8289,17 @@ function specTypesOperation(cwd) {
8208
8289
  }
8209
8290
  };
8210
8291
  }
8211
- function mockExtendedTypesPath(cwd) {
8292
+ function shippedTypesPath(cwd, declaration) {
8212
8293
  try {
8213
8294
  const manifest = createRequire6(join35(cwd, "noop.cjs")).resolve("@hublo/sentinel/package.json");
8214
- const file = join35(dirname7(manifest), MOCK_EXTENDED_TYPES);
8295
+ const file = join35(dirname7(manifest), declaration);
8215
8296
  if (existsSync29(file)) {
8216
8297
  const path = relative7(cwd, file);
8217
8298
  return path.startsWith(".") ? path : `./${path}`;
8218
8299
  }
8219
8300
  } catch {
8220
8301
  }
8221
- return `./node_modules/@hublo/sentinel/${MOCK_EXTENDED_TYPES}`;
8302
+ return `./node_modules/@hublo/sentinel/${declaration}`;
8222
8303
  }
8223
8304
  function tsconfigFileListOperations(cwd) {
8224
8305
  const operations = [];
@@ -8241,7 +8322,7 @@ function tsconfigFileListOperations(cwd) {
8241
8322
  const include = Array.isArray(config?.include) ? rewriteFileList(config.include) : void 0;
8242
8323
  const exclude = Array.isArray(config?.exclude) ? rewriteFileList(config.exclude) : void 0;
8243
8324
  const files = Array.isArray(config?.files) ? rewriteFileList(config.files) : void 0;
8244
- const withTypes = name === SPEC_TSCONFIG ? withMockExtendedTypes(cwd, files ?? config?.files) : void 0;
8325
+ const withTypes = name === SPEC_TSCONFIG ? withShippedTypes(cwd, files ?? config?.files) : void 0;
8245
8326
  if (include === void 0 && exclude === void 0 && files === void 0 && withTypes === void 0)
8246
8327
  continue;
8247
8328
  operations.push({
@@ -8261,12 +8342,15 @@ function rewriteFileList(entries) {
8261
8342
  const unchanged = rewritten.length === entries.length && rewritten.every((entry, index) => entry === entries[index]);
8262
8343
  return unchanged ? void 0 : rewritten;
8263
8344
  }
8264
- function withMockExtendedTypes(cwd, files) {
8345
+ function withShippedTypes(cwd, files) {
8265
8346
  const entries = Array.isArray(files) ? files : [];
8266
- if (entries.some((entry) => entry.endsWith(MOCK_EXTENDED_TYPES))) return void 0;
8267
- return [...entries, mockExtendedTypesPath(cwd)];
8347
+ const missing = SHIPPED_TYPES.filter(
8348
+ (declaration) => !entries.some((entry) => entry.endsWith(declaration))
8349
+ );
8350
+ if (missing.length === 0) return void 0;
8351
+ return [...entries, ...missing.map((declaration) => shippedTypesPath(cwd, declaration))];
8268
8352
  }
8269
- var MOCK_EXTENDED_TYPES = "types/mock-extended.d.ts";
8353
+ var SHIPPED_TYPES = ["types/mock-extended.d.ts", "types/jest-global.d.ts"];
8270
8354
  var JEST_CONFIG_ENTRY = /(^|\/)jest\.[\w.-]*config\.[cm]?[jt]s$/;
8271
8355
  var TSCONFIG_FILE = /^tsconfig[\w.-]*\.json$/;
8272
8356
  function siblingScriptRewrites(cwd) {
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  registerAdapters,
8
8
  resolve,
9
9
  setDefaultRunner
10
- } from "./chunk-YHGCBMKJ.js";
10
+ } from "./chunk-XGC2KK5J.js";
11
11
  import "./chunk-WLFE5RUU.js";
12
12
  export {
13
13
  BaseAdapter,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hublo/sentinel",
3
- "version": "1.4.0-alpha.14",
3
+ "version": "1.4.0-alpha.16",
4
4
  "description": "One CLI that guards code health across Hublo repos: shared lint/typescript/build/test presets, static & dynamic analysis, and architecture checks.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -0,0 +1,41 @@
1
+ /**
2
+ * The type-level half of the `jest` global the setup installs.
3
+ *
4
+ * ## What it pairs with
5
+ *
6
+ * The migration rewrites SPEC files and deliberately leaves helpers alone, because the setup puts
7
+ * Vitest's `vi` on `globalThis` under the name `jest`:
8
+ *
9
+ * // setup/jest-global.ts
10
+ * ;(globalThis as Record<string, unknown>).jest = vi
11
+ *
12
+ * That is what lets a module migrate on its own while a shared helper still calls `jest.fn()`, and
13
+ * it is why the suite of a migrated module is green with untouched helpers.
14
+ *
15
+ * The shim answers the RUNTIME. It says nothing to TypeScript, and the migration also replaces
16
+ * `"jest"` with `"vitest/globals"` in the module's `tsconfig.spec.json`, so the name stops being a
17
+ * value at compile time while it still is one at run time.
18
+ *
19
+ * Measured on `apps/nest/microservices/institution` with alpha.14, a module whose typecheck was
20
+ * green before it migrated and whose 2958 tests all pass after:
21
+ *
22
+ * src/app/institution.test-wrapper.ts(83,22)
23
+ * error TS2708: Cannot use namespace 'jest' as a value.
24
+ * tests/support/institution-prisma.test-wrapper.ts(58,22)
25
+ * error TS2708: Cannot use namespace 'jest' as a value.
26
+ *
27
+ * Both are helpers, neither is a spec, and both run correctly. This file states, for the type
28
+ * checker, exactly what the setup states for the runtime.
29
+ *
30
+ * ## Why `typeof vi` and not a hand-written subset
31
+ *
32
+ * The shim assigns the whole `vi`, so anything narrower would describe something the module does
33
+ * not run. Writing the truth costs one line and cannot drift.
34
+ *
35
+ * ## In a program that still carries `@types/jest`
36
+ *
37
+ * No conflict, and this one wins: measured on a fixture holding both, `jest.hoisted(() => 1)`
38
+ * compiles, and `hoisted` exists only on `vi`. So a module part-way through its migration gets the
39
+ * type of what actually executes rather than the type of the runner it is leaving.
40
+ */
41
+ declare const jest: (typeof import('vitest'))['vi']