@hublo/sentinel 1.4.0-alpha.15 → 1.4.0-alpha.17
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 +1 -1
- package/dist/{chunk-BIDHDUUZ.js → chunk-I64KRL6O.js} +116 -10
- package/dist/index.d.ts +10 -0
- package/dist/index.js +1 -1
- package/package.json +1 -1
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"]);
|
|
@@ -7381,7 +7453,10 @@ function migrateTestFile(fileName, source, defaultImported = /* @__PURE__ */ new
|
|
|
7381
7453
|
const afterEmpty = parse2(fileName, emptyImplementations.source);
|
|
7382
7454
|
if (afterEmpty === void 0) return unparsable("empty mock implementation");
|
|
7383
7455
|
const hooks = rewriteHookReturnValue(emptyImplementations.source, afterEmpty);
|
|
7384
|
-
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);
|
|
7385
7460
|
const program = parse2(fileName, awaited.source);
|
|
7386
7461
|
if (program === void 0) return unparsable("hook body");
|
|
7387
7462
|
const hoist = hoistCapturedBindings(
|
|
@@ -8847,6 +8922,7 @@ function resolveVitest(cwd) {
|
|
|
8847
8922
|
const fromModule = resolveBin(cwd, "vitest");
|
|
8848
8923
|
return fromModule !== void 0 ? { bin: fromModule, origin: "module" } : { bin: void 0, origin: "module" };
|
|
8849
8924
|
}
|
|
8925
|
+
var FORMATTABLE = /\.(m|c)?(j|t)sx?$|\.json$/;
|
|
8850
8926
|
var VitestAdapter = class extends BaseAdapter {
|
|
8851
8927
|
target = "test";
|
|
8852
8928
|
runner = "vitest";
|
|
@@ -8885,21 +8961,48 @@ var VitestAdapter = class extends BaseAdapter {
|
|
|
8885
8961
|
* MODULE's decision: `printWidth` lives in its own config, and 12 modules already set their
|
|
8886
8962
|
* own. Guessing it is how a tool starts owning a setting it does not own.
|
|
8887
8963
|
*
|
|
8888
|
-
* The same delegation the build role asks a human to do by hand, done.
|
|
8889
|
-
*
|
|
8890
|
-
*
|
|
8964
|
+
* The same delegation the build role asks a human to do by hand, done.
|
|
8965
|
+
*
|
|
8966
|
+
* ## ⚠️ Two boundaries, both learned by crossing them
|
|
8967
|
+
*
|
|
8968
|
+
* ### Only a module that has a formatter CONFIG
|
|
8969
|
+
*
|
|
8970
|
+
* The question is not "can a formatter be found". It always can: sentinel depends on oxfmt and
|
|
8971
|
+
* ships the binary, so `resolveOxfmt` falls back to its own install and never returns undefined
|
|
8972
|
+
* once sentinel is installed. Asking it that way meant a module with no oxfmt config was
|
|
8973
|
+
* formatted with oxfmt's DEFAULTS, which is a style nobody here chose.
|
|
8974
|
+
*
|
|
8975
|
+
* So the CONFIG is read. A module that wrote its own `.oxfmtrc.json` without adopting the role
|
|
8976
|
+
* has still stated a style, and running it is delegation; running oxfmt with no config is us
|
|
8977
|
+
* choosing one.
|
|
8978
|
+
*
|
|
8979
|
+
* Measured on `apps/nest/microservices/institution`, which has not adopted format and whose lint
|
|
8980
|
+
* forbids semicolons: `institution:lint` went from green to failing, thousands of `semi`, on a
|
|
8981
|
+
* module whose 2958 tests pass. So the adoption is read, not the binary.
|
|
8982
|
+
*
|
|
8983
|
+
* ### Only the files this just WROTE
|
|
8984
|
+
*
|
|
8985
|
+
* `--write .` is a whole-directory pass, and that is not a tidy-up. Same measurement: 1813 files
|
|
8986
|
+
* changed, 47333 insertions, 55506 deletions, in a module whose migration touched 268. A
|
|
8987
|
+
* migration diff is read by a person.
|
|
8988
|
+
*
|
|
8989
|
+
* A module that has not adopted format keeps what was written exactly as written, and is told so.
|
|
8990
|
+
* The codemod rewrites tokens in place, so those files keep the shape they already had.
|
|
8891
8991
|
*/
|
|
8892
8992
|
async afterInit(ctx) {
|
|
8893
|
-
const
|
|
8894
|
-
if (
|
|
8993
|
+
const written = (ctx.written ?? []).filter((file) => FORMATTABLE.test(file));
|
|
8994
|
+
if (written.length === 0) return Promise.resolve();
|
|
8995
|
+
if (readFormatAdoption(ctx.cwd).configFile === null) {
|
|
8895
8996
|
this.say(
|
|
8896
|
-
`no formatter
|
|
8997
|
+
`no formatter configured here, so the ${written.length} file(s) this wrote keep the shape they were written in. Run your own formatter over them before committing.`
|
|
8897
8998
|
);
|
|
8898
8999
|
return Promise.resolve();
|
|
8899
9000
|
}
|
|
8900
|
-
const
|
|
9001
|
+
const oxfmt = resolveOxfmt(ctx.cwd);
|
|
9002
|
+
if (oxfmt === void 0) return Promise.resolve();
|
|
9003
|
+
const pass = spawnSync5(oxfmt, ["--write", ...written], { cwd: ctx.cwd, encoding: "utf8" });
|
|
8901
9004
|
this.say(
|
|
8902
|
-
pass.status === 0 ? `formatted
|
|
9005
|
+
pass.status === 0 ? `formatted the ${written.length} file(s) this wrote with the module's own oxfmt config, so its \`--run --format\` passes on files it did not write itself.` : (
|
|
8903
9006
|
// Adoption SUCCEEDED; only the tidy-up did not. Failing here would revert a migration
|
|
8904
9007
|
// over a formatting pass, which is the wrong trade.
|
|
8905
9008
|
`the module's formatter could not run over what this wrote (${pass.status ?? "no exit code"}). Adoption itself is applied; run \`sentinel --run --format --fix\` before committing.`
|
|
@@ -10134,9 +10237,11 @@ async function dispatch(opts) {
|
|
|
10134
10237
|
fix: false
|
|
10135
10238
|
});
|
|
10136
10239
|
}
|
|
10240
|
+
const written = [];
|
|
10137
10241
|
for (const change of applyPlan(opts.cwd, plan4)) {
|
|
10138
10242
|
process.stderr.write(` ${change.deleted ? "removed" : "wrote"} ${change.path}
|
|
10139
10243
|
`);
|
|
10244
|
+
if (!change.deleted) written.push(change.path);
|
|
10140
10245
|
}
|
|
10141
10246
|
for (const note of plan4.notes ?? []) process.stderr.write(` ${note}
|
|
10142
10247
|
`);
|
|
@@ -10148,7 +10253,8 @@ async function dispatch(opts) {
|
|
|
10148
10253
|
cwd: opts.cwd,
|
|
10149
10254
|
preset,
|
|
10150
10255
|
ci: false,
|
|
10151
|
-
fix: true
|
|
10256
|
+
fix: true,
|
|
10257
|
+
written
|
|
10152
10258
|
});
|
|
10153
10259
|
}
|
|
10154
10260
|
return 0;
|
package/dist/index.d.ts
CHANGED
|
@@ -66,6 +66,16 @@ interface RunContext {
|
|
|
66
66
|
ci: boolean;
|
|
67
67
|
/** Apply auto-fixes where the tool supports it. */
|
|
68
68
|
fix: boolean;
|
|
69
|
+
/**
|
|
70
|
+
* The files `--init` just wrote, relative to `cwd`, for an `afterInit` that tidies up after
|
|
71
|
+
* itself.
|
|
72
|
+
*
|
|
73
|
+
* ⚠️ It exists because the alternative is a whole-directory pass, and that is not a tidy-up.
|
|
74
|
+
* Measured on `apps/nest/microservices/institution`: an `afterInit` running its formatter over
|
|
75
|
+
* `.` reformatted 1813 files, 47333 insertions and 55506 deletions, in a module whose migration
|
|
76
|
+
* touched 268. Only set by the `--init` path, so an adapter must treat it as optional.
|
|
77
|
+
*/
|
|
78
|
+
written?: string[];
|
|
69
79
|
/**
|
|
70
80
|
* Cap on the number of structured diagnostics a `--report` embeds per module (the
|
|
71
81
|
* full count still lives in `errors`, so nothing is hidden silently). `0` means no
|
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.17",
|
|
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",
|