@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.
@@ -16,7 +16,7 @@ import {
16
16
  registerAdapters,
17
17
  resolve,
18
18
  resolveBin
19
- } from "../chunk-BIDHDUUZ.js";
19
+ } from "../chunk-I64KRL6O.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"]);
@@ -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 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);
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. A module that has not
8889
- * adopted the format role has no formatter to invoke, and this says nothing rather than
8890
- * choosing one for it.
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 oxfmt = resolveOxfmt(ctx.cwd);
8894
- if (oxfmt === void 0) {
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 adopted here, so what was written keeps the shape it was written in. Run your own formatter over it before committing.`
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 pass = spawnSync5(oxfmt, ["--write", "."], { cwd: ctx.cwd, encoding: "utf8" });
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 what this wrote with the module's own oxfmt config, so its \`--run --format\` passes on files it did not write itself.` : (
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
@@ -7,7 +7,7 @@ import {
7
7
  registerAdapters,
8
8
  resolve,
9
9
  setDefaultRunner
10
- } from "./chunk-BIDHDUUZ.js";
10
+ } from "./chunk-I64KRL6O.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.15",
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",