@hublo/sentinel 1.4.0-alpha.24 → 1.4.0-alpha.25
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
|
@@ -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
|
|
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 (
|
|
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
|
|
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 || !
|
|
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
|
|
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
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.25",
|
|
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",
|
package/types/jest-global.d.ts
CHANGED
|
@@ -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>
|