@hublo/sentinel 1.4.0-alpha.4 → 1.4.0-alpha.41

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.
Files changed (66) hide show
  1. package/README.md +1 -0
  2. package/dist/bin/sentinel.d.ts +1 -0
  3. package/dist/bin/sentinel.js +45 -102
  4. package/dist/{chunk-ASBZQAXV.js → chunk-2UU57Y2T.js} +221 -4339
  5. package/dist/chunk-2XLX6PFR.js.map +1 -0
  6. package/dist/{chunk-CPCUPK4J.js → chunk-DWVRVPMN.js} +5 -12
  7. package/dist/chunk-DWVRVPMN.js.map +1 -0
  8. package/dist/chunk-ELZHIN6E.js +16 -0
  9. package/dist/chunk-ELZHIN6E.js.map +1 -0
  10. package/dist/chunk-H3GSGAX2.js +11 -0
  11. package/dist/chunk-H3GSGAX2.js.map +1 -0
  12. package/dist/chunk-I3FX32E7.js +5644 -0
  13. package/dist/{chunk-PWV3BMDA.js → chunk-MMKBDO5V.js} +11 -2
  14. package/dist/chunk-MMKBDO5V.js.map +1 -0
  15. package/dist/chunk-NUOQXAYR.js +14 -0
  16. package/dist/chunk-NUOQXAYR.js.map +1 -0
  17. package/dist/{chunk-WLFE5RUU.js → chunk-NW6UHNYX.js} +6 -6
  18. package/dist/chunk-NW6UHNYX.js.map +1 -0
  19. package/dist/chunk-O7REVMOC.js +38 -0
  20. package/dist/chunk-O7REVMOC.js.map +1 -0
  21. package/dist/chunk-QXFCZON7.js +16 -0
  22. package/dist/chunk-QXFCZON7.js.map +1 -0
  23. package/dist/{chunk-MT7VQXRA.js → chunk-SOCLCHJK.js} +228 -13
  24. package/dist/chunk-SOCLCHJK.js.map +1 -0
  25. package/dist/{chunk-3TDUIKVQ.js → chunk-SWWQ7X7B.js} +11 -8
  26. package/dist/chunk-SWWQ7X7B.js.map +1 -0
  27. package/dist/index.d.ts +28 -5
  28. package/dist/index.js +7 -4
  29. package/dist/roles/build/nest/toolchain.js +11 -33
  30. package/dist/roles/build/nest/toolchain.js.map +1 -1
  31. package/dist/roles/test/nest/toolchain.js +3 -2
  32. package/dist/roles/test/nest/toolchain.js.map +1 -1
  33. package/dist/roles/test/react/toolchain.js +3 -2
  34. package/dist/roles/test/react/toolchain.js.map +1 -1
  35. package/dist/roles/test/setup/a11y.d.ts +45 -0
  36. package/dist/roles/test/setup/a11y.js +72 -0
  37. package/dist/roles/test/setup/a11y.js.map +1 -0
  38. package/dist/roles/test/setup/file-boundary-close.d.ts +2 -0
  39. package/dist/roles/test/setup/file-boundary-close.js +8 -0
  40. package/dist/roles/test/setup/file-boundary-close.js.map +1 -0
  41. package/dist/roles/test/setup/file-boundary.d.ts +2 -0
  42. package/dist/roles/test/setup/file-boundary.js +62 -0
  43. package/dist/roles/test/setup/file-boundary.js.map +1 -0
  44. package/dist/roles/test/setup/jest-parity.js +19 -2
  45. package/dist/roles/test/setup/jest-parity.js.map +1 -1
  46. package/dist/roles/test/setup/msw-lifecycle.js +2 -1
  47. package/dist/roles/test/setup/msw-lifecycle.js.map +1 -1
  48. package/dist/roles/test/setup/msw-server.js +2 -1
  49. package/dist/roles/test/setup/msw-server.js.map +1 -1
  50. package/dist/roles/test/setup/w3c.d.ts +75 -0
  51. package/dist/roles/test/setup/w3c.js +49 -0
  52. package/dist/roles/test/setup/w3c.js.map +1 -0
  53. package/dist/roles/test/setup/workspace-entry.js +3 -2
  54. package/dist/roles/test/setup/workspace-entry.js.map +1 -1
  55. package/dist/roles/test/shared-test-config.d.ts +13 -0
  56. package/dist/roles/test/shared-test-config.js +3 -2
  57. package/dist/roles/test/tools/msw.d.ts +1 -0
  58. package/dist/roles/test/tools/msw.js +3 -0
  59. package/dist/roles/test/tools/msw.js.map +1 -0
  60. package/dist/validate-ANM7RQ3C.js +169 -0
  61. package/docs/test-adoption.md +282 -5
  62. package/docs/using-sentinel.md +17 -1
  63. package/docs/validating-a-change.md +35 -2
  64. package/package.json +29 -4
  65. package/types/jest-global.d.ts +85 -0
  66. package/types/mock-extended.d.ts +52 -0
package/README.md CHANGED
@@ -24,6 +24,7 @@ A large monorepo accumulates:
24
24
  - **One source of truth for config** — every project just `extends @hublo/sentinel/...`; the actual rules live in one versioned place. Change a rule once, everyone gets it on the next version bump.
25
25
  - **One source of truth for tooling dependencies** — a project depends on `@hublo/sentinel`, not on a scattered pile of eslint / vitest / plugin devDeps. Bump one version and the whole toolchain moves, atomically, tested in isolation first.
26
26
  - **`--init` sets a module up** — the one command generates the stubs the first time (**adopt**), regenerates them after a change like a runner swap (**refresh**), and applies the workspace prep the module needs. Run it module by module to roll out gradually. (`--migrate`, for changing an already-initialized setup, is a reserved future verb.)
27
+ - **`--validate` proves the move did not change anything** — a verb of its own, because what separates it from `--init` is what SURVIVES the run: `--init` and `--migrate` write state that REMAINS, `--validate` may instrument the module it is proving provided nothing it writes stays. Run before a migration it records what the module does today; run after, it compares and refuses a run that lost a test or invented one. It composes with every type, and answers "not available yet" for the ones without a proof rather than calling a valid command wrong.
27
28
  - **Move one module at a time** — installed per module, so you adopt at your pace; a module can adopt sentinel while its neighbour keeps the old setup. No big-bang.
28
29
  - **Swap tools without touching projects** — change eslint → biome (or benchmark them) in one place; `--init` regenerates the stubs.
29
30
  - **No silent drift** — the guard keeps every project's config converged on the source of truth.
@@ -4,3 +4,4 @@ import '@vitejs/plugin-react';
4
4
  import 'nitro/vite';
5
5
  import 'vite-plugin-svgr';
6
6
  import 'vite';
7
+ import 'msw';
@@ -14,118 +14,33 @@ import {
14
14
  dispatch,
15
15
  palette,
16
16
  registerAdapters,
17
- resolve,
18
- resolveBin
19
- } from "../chunk-ASBZQAXV.js";
17
+ resolve
18
+ } from "../chunk-2UU57Y2T.js";
19
+ import {
20
+ resolveContext
21
+ } from "../chunk-I3FX32E7.js";
22
+ import "../chunk-O7REVMOC.js";
23
+ import "../chunk-QXFCZON7.js";
20
24
  import {
21
- WORKSPACE_ROOT_MARKER,
22
25
  ensureWorkspacePrep,
23
26
  findWorkspaceRoot,
24
27
  inspectWorkspacePrep,
25
- moduleName,
26
28
  readOwnVersion,
27
29
  readProjectPackageJson
28
- } from "../chunk-WLFE5RUU.js";
30
+ } from "../chunk-NW6UHNYX.js";
29
31
 
30
32
  // bin/sentinel.ts
31
33
  import { program } from "commander";
32
34
 
33
- // src/core/context.ts
34
- import { existsSync } from "fs";
35
- import { join as join2 } from "path";
36
-
37
- // src/core/discover-modules.ts
38
- import { execFileSync } from "child_process";
39
- import { mkdtempSync, readFileSync, rmSync } from "fs";
40
- import { tmpdir } from "os";
41
- import { join } from "path";
42
- function runNx(cwd2, args) {
43
- const nx = resolveBin(cwd2, "nx") ?? "nx";
44
- try {
45
- return execFileSync(nx, args, {
46
- cwd: cwd2,
47
- encoding: "utf8",
48
- env: { ...process.env, NX_DAEMON: "false" }
49
- });
50
- } catch (error) {
51
- const message = error instanceof Error ? error.message : String(error);
52
- throw new Error(
53
- `sentinel: could not run nx (${message}). Is nx installed in this workspace, and are you at its root?`,
54
- { cause: error }
55
- );
56
- }
57
- }
58
- function readGraph(cwd2) {
59
- const dir = mkdtempSync(join(tmpdir(), "sentinel-nx-"));
60
- const file = join(dir, "graph.json");
61
- try {
62
- runNx(cwd2, ["graph", "--file", file]);
63
- const parsed = JSON.parse(readFileSync(file, "utf8"));
64
- const nodes = parsed.graph?.nodes;
65
- if (!nodes || typeof nodes !== "object") {
66
- throw new Error(
67
- "sentinel: unexpected nx graph output (no graph.nodes); the installed nx version may be incompatible."
68
- );
69
- }
70
- return Object.entries(nodes).map(([name, node]) => ({
71
- name,
72
- root: join(cwd2, node.data.root)
73
- }));
74
- } finally {
75
- rmSync(dir, { recursive: true, force: true });
76
- }
77
- }
78
- function discoverModules(cwd2, options = {}) {
79
- const modules = readGraph(cwd2);
80
- if (!options.affected) return modules;
81
- const affected = new Set(
82
- JSON.parse(runNx(cwd2, ["show", "projects", "--affected", "--json"]))
83
- );
84
- return modules.filter((module) => affected.has(module.name));
85
- }
86
-
87
- // src/core/context.ts
88
- var MODULE_MARKERS = ["package.json", "project.json"];
89
- function isModuleDir(cwd2) {
90
- return MODULE_MARKERS.some((marker) => existsSync(join2(cwd2, marker)));
91
- }
92
- function resolveContext(cwd2, opts2) {
93
- const atRoot = existsSync(join2(cwd2, WORKSPACE_ROOT_MARKER));
94
- if (!atRoot && isModuleDir(cwd2)) {
95
- if (opts2.module) {
96
- throw new Error(
97
- "You are in a module directory: drop --module (the context is the current module)."
98
- );
99
- }
100
- if (opts2.ci) {
101
- throw new Error("--ci selects the affected set from the workspace root; run it there.");
102
- }
103
- const name = moduleName(cwd2);
104
- return { modules: [{ name, root: cwd2 }], scope: "cwd-module" };
105
- }
106
- if (atRoot) {
107
- if (opts2.module) {
108
- const found = discoverModules(cwd2).find((module) => module.name === opts2.module);
109
- if (!found) throw new Error(`module "${opts2.module}" not found in the workspace.`);
110
- return { modules: [found], scope: "named-module" };
111
- }
112
- if (opts2.ci) return { modules: discoverModules(cwd2, { affected: true }), scope: "affected" };
113
- return { modules: discoverModules(cwd2), scope: "all" };
114
- }
115
- throw new Error(
116
- `Run sentinel from a module directory or the workspace root (found neither ${MODULE_MARKERS.join("/")} nor ${WORKSPACE_ROOT_MARKER} here).`
117
- );
118
- }
119
-
120
35
  // src/roles/build/declined.ts
121
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
122
- import { join as join3 } from "path";
36
+ import { existsSync, readFileSync } from "node:fs";
37
+ import { join } from "node:path";
123
38
  var DEPRECATED_NX_BUILDER = "@nx/webpack:webpack";
124
39
  function buildsWithDeprecatedExecutor(cwd2) {
125
- const projectJson = join3(cwd2, "project.json");
126
- if (!existsSync2(projectJson)) return false;
40
+ const projectJson = join(cwd2, "project.json");
41
+ if (!existsSync(projectJson)) return false;
127
42
  try {
128
- const project = JSON.parse(readFileSync2(projectJson, "utf8"));
43
+ const project = JSON.parse(readFileSync(projectJson, "utf8"));
129
44
  return project.targets?.build?.executor === DEPRECATED_NX_BUILDER;
130
45
  } catch {
131
46
  return false;
@@ -218,6 +133,32 @@ sentinel (${type}): ${asMessage(error)}
218
133
  return worst;
219
134
  }
220
135
 
136
+ // src/cli/run-validate.ts
137
+ async function runValidate(ctx) {
138
+ const { modules } = resolveContext(ctx.cwd, { module: ctx.module, ci: ctx.ci });
139
+ const unsupported = ctx.targets.filter((target) => target !== "test");
140
+ if (unsupported.length > 0) {
141
+ const named = unsupported.map((target) => `--${target}`).join(", ");
142
+ process.stdout.write(
143
+ ` \xB7 ${named}: not available yet. \`--validate\` answers \`--test\` today, by recording a suite before a migration and comparing it after. Nothing was checked for ${named}.
144
+ `
145
+ );
146
+ }
147
+ if (!ctx.targets.includes("test")) return 1;
148
+ const { validateModuleTests } = await import("../validate-ANM7RQ3C.js");
149
+ const outcomes = modules.flatMap(
150
+ (module) => validateModuleTests(module.name, module.root)
151
+ );
152
+ for (const outcome of outcomes) {
153
+ const suite = outcome.suite === "main" ? "" : ` [${outcome.suite}]`;
154
+ process.stdout.write(
155
+ ` ${outcome.ok ? "\u2713" : "\u2717"} ${outcome.module}${suite} \u2014 ${outcome.message}
156
+ `
157
+ );
158
+ }
159
+ return outcomes.every((outcome) => outcome.ok) ? 0 : 1;
160
+ }
161
+
221
162
  // src/core/orchestrate.ts
222
163
  async function analyse(params) {
223
164
  const results = [];
@@ -645,13 +586,16 @@ program.name("sentinel").description("One CLI that guards code health: presets,
645
586
  "before",
646
587
  [
647
588
  "A check composes: verb + type + location.",
648
- " verb what to do: --run --inspect --report --status --init (--migrate: planned)",
589
+ " verb what to do: --run --inspect --report --status --init --validate (--migrate: planned)",
649
590
  " type which check: --lint --typescript ... (omit = all types; or --all)",
650
591
  " where run from a MODULE dir \u2192 that module; from the ROOT \u2192 --module <name>,",
651
592
  " --ci (affected), or all modules. --init targets one module only.",
652
593
  ""
653
594
  ].join("\n")
654
- ).option("--run", "execute the target tool").option("--inspect", "show the resolved configuration").option("--init", "set up a module: write its config stubs + the workspace prep it needs").option("--migrate", "planned: change an already-initialized setup (not available yet)").option("--report", "deprecated: use --run --json").option("--status", "deprecated: use --inspect").option(
595
+ ).option("--run", "execute the target tool").option("--inspect", "show the resolved configuration").option("--init", "set up a module: write its config stubs + the workspace prep it needs").option("--migrate", "planned: change an already-initialized setup (not available yet)").option(
596
+ "--validate",
597
+ "prove a migration kept the suite: record the reference, then compare against it"
598
+ ).option("--report", "deprecated: use --run --json").option("--status", "deprecated: use --inspect").option(
655
599
  "--module <name>",
656
600
  "from the workspace root: scope to one module (omit = all; inside a module dir, drop this)"
657
601
  ).option("--preset <name>", `the stack preset to apply (${PRESET_NAMES.join(", ")})`).option("--flavour <name>", "deprecated: use --preset").option("--runner <tool>", "override the default runner (e.g. eslint, biome)").option("--ci", "CI mode: from the root, only the affected modules; non-zero exit on failure").option("--fix", "auto-fix where applicable").option("--dry-run", "preview the changes without writing (--init)").option("--json", "machine-readable output, for every verb").option(
@@ -783,7 +727,7 @@ async function main() {
783
727
  dryRun: Boolean(opts.dryRun),
784
728
  fail: (message) => program.error(message)
785
729
  };
786
- const exitCode = verb === "init" ? await runInit(context) : await runVerb(context);
730
+ const exitCode = verb === "init" ? await runInit(context) : verb === "validate" ? await runValidate(context) : await runVerb(context);
787
731
  await exitWithoutTruncating(exitCode);
788
732
  }
789
733
  main().catch((error) => {
@@ -792,4 +736,3 @@ sentinel: ${asMessage2(error)}
792
736
  `);
793
737
  void exitWithoutTruncating(1);
794
738
  });
795
- //# sourceMappingURL=sentinel.js.map