@hublo/sentinel 1.4.0-alpha.24 → 1.4.0-alpha.26

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
  resolveContext
19
- } from "../chunk-U744PK2U.js";
19
+ } from "../chunk-ATDETQX5.js";
20
20
  import {
21
21
  ensureWorkspacePrep,
22
22
  findWorkspaceRoot,
@@ -6515,7 +6515,7 @@ function injectTypeImport(fileName, source, typeNames) {
6515
6515
  if (merged.length === alreadyImported(existing).length) return source;
6516
6516
  return source.slice(0, existing.start) + renderTypeImport(merged, semi) + source.slice(existing.end);
6517
6517
  }
6518
- const line = renderTypeImport([...typeNames].sort(), semi);
6518
+ const line = renderTypeImport([...new Set(typeNames)].sort(), semi);
6519
6519
  const first = imports[0];
6520
6520
  if (first === void 0) return `${line}
6521
6521
 
@@ -7113,10 +7113,70 @@ function rewriteMockPromiseResolve(source, program) {
7113
7113
  return { source: result, rewritten: edits.length };
7114
7114
  }
7115
7115
 
7116
+ // src/roles/test/codemod/mocked-module-binding.ts
7117
+ var MEMBER_TYPES12 = /* @__PURE__ */ new Set(["MemberExpression", "StaticMemberExpression"]);
7118
+ function mockedSpecifier(source, node) {
7119
+ if (node.type !== "CallExpression") return void 0;
7120
+ const callee = node.callee;
7121
+ if (callee === void 0 || !MEMBER_TYPES12.has(callee.type)) return void 0;
7122
+ const object = callee.object;
7123
+ const property = callee.property;
7124
+ if (object?.type !== "Identifier") return void 0;
7125
+ if (object.name !== "vi" && object.name !== "jest") return void 0;
7126
+ if (property?.type !== "Identifier" || property.name !== "mock") return void 0;
7127
+ const [first] = node.arguments ?? [];
7128
+ if (first?.type !== "Literal") return void 0;
7129
+ return source.slice(first.start, first.end).slice(1, -1);
7130
+ }
7131
+ function awaitedImport(source, node) {
7132
+ if (node?.type !== "AwaitExpression") return void 0;
7133
+ const call = node.argument;
7134
+ if (call?.type !== "ImportExpression") return void 0;
7135
+ const specifier = call.source ?? call.arguments?.[0];
7136
+ if (specifier?.type !== "Literal") return void 0;
7137
+ return source.slice(specifier.start, specifier.end).slice(1, -1);
7138
+ }
7139
+ function annotateMockedBindings(source, program) {
7140
+ const mocked = /* @__PURE__ */ new Set();
7141
+ walk(program, (node) => {
7142
+ const specifier = mockedSpecifier(source, node);
7143
+ if (specifier !== void 0) mocked.add(specifier);
7144
+ });
7145
+ if (mocked.size === 0) return { source, rewritten: 0, typeImports: [] };
7146
+ const edits = [];
7147
+ for (const statement2 of program.body ?? []) {
7148
+ if (statement2.type !== "VariableDeclaration") continue;
7149
+ const declarations = statement2.declarations ?? [];
7150
+ if (declarations.length !== 1) continue;
7151
+ const [only] = declarations;
7152
+ if (only === void 0) continue;
7153
+ if (only.id?.type !== "Identifier") continue;
7154
+ const init = only.init;
7155
+ const specifier = awaitedImport(source, init);
7156
+ if (specifier === void 0 || !mocked.has(specifier)) continue;
7157
+ const start = init?.start;
7158
+ const end = init?.end;
7159
+ const expression = source.slice(start, end);
7160
+ if (source.slice(end, end + 4).trimStart().startsWith("as"))
7161
+ continue;
7162
+ edits.push({
7163
+ start,
7164
+ end,
7165
+ text: `(${expression}) as unknown as Record<string, Mock>`
7166
+ });
7167
+ }
7168
+ if (edits.length === 0) return { source, rewritten: 0, typeImports: [] };
7169
+ let result = source;
7170
+ for (const edit of [...edits].sort((left, right) => right.start - left.start)) {
7171
+ result = `${result.slice(0, edit.start)}${edit.text}${result.slice(edit.end)}`;
7172
+ }
7173
+ return { source: result, rewritten: edits.length, typeImports: ["Mock"] };
7174
+ }
7175
+
7116
7176
  // src/roles/test/codemod/propagate-await.ts
7117
7177
  import { parseSync as parseSync5 } from "oxc-parser";
7118
7178
  var FUNCTIONS3 = /* @__PURE__ */ new Set(["ArrowFunctionExpression", "FunctionExpression", "FunctionDeclaration"]);
7119
- var MEMBER_TYPES12 = /* @__PURE__ */ new Set(["MemberExpression", "StaticMemberExpression"]);
7179
+ var MEMBER_TYPES13 = /* @__PURE__ */ new Set(["MemberExpression", "StaticMemberExpression"]);
7120
7180
  var ALREADY_A_PROMISE = /* @__PURE__ */ new Set(["then", "catch", "finally"]);
7121
7181
  function asyncFunctionNames(program) {
7122
7182
  const names = /* @__PURE__ */ new Set();
@@ -7190,7 +7250,7 @@ function alreadyHandled(call, parent) {
7190
7250
  if (parent === void 0) return false;
7191
7251
  if (parent.type === "AwaitExpression") return true;
7192
7252
  if (parent.type === "ReturnStatement" || parent.type === "ArrowFunctionExpression") return true;
7193
- if (MEMBER_TYPES12.has(parent.type) && parent.object === call) {
7253
+ if (MEMBER_TYPES13.has(parent.type) && parent.object === call) {
7194
7254
  const property = parent.property;
7195
7255
  return property?.type === "Identifier" && ALREADY_A_PROMISE.has(property.name);
7196
7256
  }
@@ -7243,7 +7303,7 @@ function propagateAwait(fileName, source, original, elsewhere = /* @__PURE__ */
7243
7303
  import { readFileSync as readFileSync24, readdirSync as readdirSync4, statSync as statSync4 } from "node:fs";
7244
7304
  import { createRequire as createRequire5 } from "node:module";
7245
7305
  import { dirname as dirname5, join as join30 } from "node:path";
7246
- var MEMBER_TYPES13 = /* @__PURE__ */ new Set(["MemberExpression", "StaticMemberExpression"]);
7306
+ var MEMBER_TYPES14 = /* @__PURE__ */ new Set(["MemberExpression", "StaticMemberExpression"]);
7247
7307
  var PACKAGE_SUBPATH = /^(@[^/]+\/[^/]+|[^@.][^/]*)\/(.+)$/;
7248
7308
  var PURE_REEXPORT = /^module\.exports\s*=\s*require\(\s*(['"])([^'"]+)\1\s*\)\s*;?$/;
7249
7309
  function mockCalls(program) {
@@ -7251,7 +7311,7 @@ function mockCalls(program) {
7251
7311
  walk(program, (node) => {
7252
7312
  if (node.type !== "CallExpression") return;
7253
7313
  const callee = node.callee;
7254
- if (callee === void 0 || !MEMBER_TYPES13.has(callee.type)) return;
7314
+ if (callee === void 0 || !MEMBER_TYPES14.has(callee.type)) return;
7255
7315
  const object = callee.object;
7256
7316
  const property = callee.property;
7257
7317
  if (object?.type !== "Identifier") return;
@@ -7741,9 +7801,13 @@ function migrateTestFile(fileName, source, defaultImported = /* @__PURE__ */ new
7741
7801
  moduleBindings(program),
7742
7802
  importedNames(program)
7743
7803
  );
7744
- const withImport = injectTypeImport(fileName, hoist.source, [
7804
+ const afterHoist = parse2(fileName, hoist.source);
7805
+ if (afterHoist === void 0) return unparsable("hoisting captured bindings");
7806
+ const mockedBindings = annotateMockedBindings(hoist.source, afterHoist);
7807
+ const withImport = injectTypeImport(fileName, mockedBindings.source, [
7745
7808
  ...renamed.typeImports,
7746
- ...spyTypes.typeImports
7809
+ ...spyTypes.typeImports,
7810
+ ...mockedBindings.typeImports
7747
7811
  ]);
7748
7812
  const deep = normaliseDeepImports(withImport);
7749
7813
  const repointed = modulesOwnMsw ? { source: deep.source, repointed: false } : repointMswServer(fileName, deep.source);
@@ -7766,6 +7830,7 @@ function migrateTestFile(fileName, source, defaultImported = /* @__PURE__ */ new
7766
7830
  ...emptyImplementations.rewritten > 0 ? ["empty mockImplementation"] : [],
7767
7831
  ...hooks.rewritten > 0 ? ["hook body, so it does not return its mock"] : [],
7768
7832
  ...spyTypes.rewritten > 0 ? ["SpyInstance<T> as a signature, not a return type"] : [],
7833
+ ...mockedBindings.rewritten > 0 ? ["a mocked module binding, typed as mocked"] : [],
7769
7834
  ...awaited.rewritten > 0 ? ["await propagated to a helper that became async"] : [],
7770
7835
  ...barrels.added.map((alias) => `mock carried to ${alias}, which re-exports the mocked file`),
7771
7836
  ...fakeTimers.translated
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  registerAdapters,
8
8
  resolve,
9
9
  setDefaultRunner
10
- } from "./chunk-U744PK2U.js";
10
+ } from "./chunk-ATDETQX5.js";
11
11
  import "./chunk-NW6UHNYX.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.24",
3
+ "version": "1.4.0-alpha.26",
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",
@@ -61,6 +61,14 @@ declare const jest: (typeof import('vitest'))['vi']
61
61
  *
62
62
  * A type-only namespace declares no value, which is why it merges with the const above instead of
63
63
  * colliding with it.
64
+ *
65
+ * ## Alongside a program that still carries `@types/jest`
66
+ *
67
+ * No collision either, and it was measured rather than assumed: a fixture with
68
+ * `types: ["jest", "vitest/globals"]` plus this file compiles clean, and the same fixture WITHOUT
69
+ * this file also compiles, which is what proves `@types/jest` was really loaded. Declaration
70
+ * merging applies to the namespace the way it already applies to the const, so a module part-way
71
+ * through its migration is not broken by carrying both.
64
72
  */
65
73
  declare namespace jest {
66
74
  type Mock<T = any, Y extends any[] = any[]> = import('vitest').Mock<(...args: Y) => T>