@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.
package/dist/bin/sentinel.js
CHANGED
|
@@ -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
|
|
7271
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
|
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),
|
|
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/${
|
|
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 ?
|
|
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
|
|
8345
|
+
function withShippedTypes(cwd, files) {
|
|
8265
8346
|
const entries = Array.isArray(files) ? files : [];
|
|
8266
|
-
|
|
8267
|
-
|
|
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
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hublo/sentinel",
|
|
3
|
-
"version": "1.4.0-alpha.
|
|
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']
|