@hublo/sentinel 1.3.0 → 1.4.0-alpha.10

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 (61) hide show
  1. package/README.md +8 -4
  2. package/dist/bin/sentinel.d.ts +1 -1
  3. package/dist/bin/sentinel.js +22 -9
  4. package/dist/chunk-2XLX6PFR.js +132 -0
  5. package/dist/chunk-2XLX6PFR.js.map +1 -0
  6. package/dist/chunk-3TDUIKVQ.js +178 -0
  7. package/dist/chunk-3TDUIKVQ.js.map +1 -0
  8. package/dist/chunk-CPCUPK4J.js +70 -0
  9. package/dist/chunk-CPCUPK4J.js.map +1 -0
  10. package/dist/chunk-CRKUEP4J.js +427 -0
  11. package/dist/chunk-CRKUEP4J.js.map +1 -0
  12. package/dist/{chunk-676GBPMS.js → chunk-L7WS36XV.js} +3921 -556
  13. package/dist/chunk-PWV3BMDA.js +15 -0
  14. package/dist/chunk-PWV3BMDA.js.map +1 -0
  15. package/dist/chunk-WLFE5RUU.js +264 -0
  16. package/dist/chunk-WLFE5RUU.js.map +1 -0
  17. package/dist/index.d.ts +13 -0
  18. package/dist/index.js +2 -2
  19. package/dist/roles/build/nest/toolchain.d.ts +4 -36
  20. package/dist/roles/build/nest/toolchain.js +10 -178
  21. package/dist/roles/build/nest/toolchain.js.map +1 -0
  22. package/dist/roles/build/toolchain.js.map +1 -0
  23. package/dist/roles/test/nest/toolchain.d.ts +9 -0
  24. package/dist/roles/test/nest/toolchain.js +25 -0
  25. package/dist/roles/test/nest/toolchain.js.map +1 -0
  26. package/dist/roles/test/react/toolchain.d.ts +66 -0
  27. package/dist/roles/test/react/toolchain.js +14 -1
  28. package/dist/roles/test/react/toolchain.js.map +1 -0
  29. package/dist/roles/test/setup/jest-parity.d.ts +2 -0
  30. package/dist/roles/test/setup/jest-parity.js +159 -0
  31. package/dist/roles/test/setup/jest-parity.js.map +1 -0
  32. package/dist/roles/test/setup/mock-extended.d.ts +46 -0
  33. package/dist/roles/test/setup/mock-extended.js +65 -0
  34. package/dist/roles/test/setup/mock-extended.js.map +1 -0
  35. package/dist/roles/test/setup/msw-lifecycle.d.ts +16 -0
  36. package/dist/roles/test/setup/msw-lifecycle.js +12 -0
  37. package/dist/roles/test/setup/msw-lifecycle.js.map +1 -0
  38. package/dist/roles/test/setup/msw-server.d.ts +3 -0
  39. package/dist/roles/test/setup/msw-server.js +10 -0
  40. package/dist/roles/test/setup/msw-server.js.map +1 -0
  41. package/dist/roles/test/setup/workspace-entry.d.ts +2 -0
  42. package/dist/roles/test/setup/workspace-entry.js +8 -0
  43. package/dist/roles/test/setup/workspace-entry.js.map +1 -0
  44. package/dist/roles/test/shared-test-config.d.ts +32 -0
  45. package/dist/roles/test/shared-test-config.js +8 -0
  46. package/dist/roles/test/shared-test-config.js.map +1 -0
  47. package/dist/roles/test/tools/msw.d.ts +1 -0
  48. package/dist/roles/test/tools/msw.js +3 -0
  49. package/dist/roles/test/tools/msw.js.map +1 -0
  50. package/dist/tsconfig-aliases-Ce6axdJ4.d.ts +36 -0
  51. package/docs/.gitkeep +0 -0
  52. package/docs/build-adoption.md +521 -0
  53. package/docs/format-adoption.md +321 -0
  54. package/docs/lint-adoption.md +290 -0
  55. package/docs/performance.md +49 -0
  56. package/docs/test-adoption.md +219 -0
  57. package/docs/typescript-adoption.md +184 -0
  58. package/docs/typescript-traces.md +798 -0
  59. package/docs/using-sentinel.md +195 -0
  60. package/docs/validating-a-change.md +101 -0
  61. package/package.json +35 -6
@@ -0,0 +1,195 @@
1
+ # Using sentinel
2
+
3
+ The commands, what an adopted module ends up looking like, and how versions move. For
4
+ migrating a module see [`lint-adoption.md`](lint-adoption.md),
5
+ [`format-adoption.md`](format-adoption.md),
6
+ [`typescript-adoption.md`](typescript-adoption.md),
7
+ [`build-adoption.md`](build-adoption.md) and
8
+ [`test-adoption.md`](test-adoption.md), which also carries the reference gate `--test` runs a
9
+ migration against.
10
+
11
+ ## The grid
12
+
13
+ A command is **verb + target + location**.
14
+
15
+ ```bash
16
+ sentinel --run --lint # in a module dir -> that module
17
+ sentinel --run --lint --module bff-admin # from the root -> one module
18
+ sentinel --run --ci # from the root -> affected only
19
+ sentinel --run # no target -> every wired target
20
+ ```
21
+
22
+ Read it by target, since that is what you pick first. `--report` and `--status` are deprecated
23
+ everywhere (`--run --json` and `--inspect` answer them), and `--migrate` is refused everywhere.
24
+
25
+ | Target | `--init` writes | `--run` does | `--inspect` shows |
26
+ | -------------- | --------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | ---------------------------------------------------------------- |
27
+ | `--lint` | stub + scripts + nx metadata, removes the ESLint config and the module's ESLint deps, then autofixes once | lints; `--fix` autofixes in the same pass | rule count, what is disabled or downgraded and **why**, drift |
28
+ | `--format` | materializes the preset + scripts + nx metadata, removes the Prettier config and deps, then formats once | **checks**; `--fix` writes | effective options, every departure from the standard and why |
29
+ | `--typescript` | writes/extends the tsconfig + `typecheck` script | typechecks | resolved options, what is deferred, drift |
30
+ | `--build` | points the module's Vite config at sentinel's toolchain + scripts + nx metadata | builds | runner, config file, whose Vite, how many overrides, adoption |
31
+ | `--dev` | adopts the BUILD too: one plan writes both `build` and `serve` | serves; long-running, so never swept by an unqualified `--run` | refuses, and sends you to `--inspect --build`: it is that config |
32
+ | `--test` | records the jest reference, THEN the Vitest config + scripts + nx metadata, and migrates every spec file | runs Vitest and compares against that reference | runner, which configs were found, adoption state, drift |
33
+
34
+ `--json` works for every verb, and stdout carries **only** the envelope, so `| jq` always
35
+ parses. `--dry-run` applies to `--init` and writes nothing.
36
+
37
+ `--init` is the one verb that targets exactly **one** module. Adopting everything at once
38
+ would be a big-bang; migration is meant to be gradual and per-team.
39
+
40
+ ## When a role has nothing to do here
41
+
42
+ Adoption is per role, not per module, and a role declines when **its own tool** has nothing to
43
+ read. Run in `apps/front/maintenance`, which is a static `index.html`, a stylesheet and an SVG:
44
+
45
+ ```console
46
+ $ sentinel --init
47
+ lint: no file oxlint can read (looked for .js, .mjs, .cjs, .jsx, .ts, ...), so there is
48
+ nothing to lint here. Other roles may still apply: the formatter reads markdown,
49
+ YAML, HTML and CSS too.
50
+ typescript: no TypeScript here (looked for .ts, .mts, .cts, .tsx), so there is nothing
51
+ for tsc to check.
52
+ wrote .oxfmtrc.json
53
+ ...
54
+ ```
55
+
56
+ Two roles decline, the formatter adopts, and the command **exits 0**. Nothing failed: the
57
+ module simply is what it is. "Not a TypeScript project" would have been the wrong question,
58
+ since oxfmt has real work in exactly that module.
59
+
60
+ Declining matters for the numbers as much as the tidiness. A lint config written for a module
61
+ with nothing to lint reports as covered on the migration page, forever, while checking an
62
+ empty set.
63
+
64
+ The check names what it looked for, so you can disagree with it. It searches the whole module
65
+ rather than assuming `src/` (code lives in `lib/`, `scripts/` and at the root here too), stops
66
+ at the first match, and never descends into `node_modules` or build output: 0.1ms on a
67
+ 4210-file module, 47ms in the pathological case where nothing matches anywhere.
68
+
69
+ ## Asking the tool your own question
70
+
71
+ Everything after `--` goes to the underlying tool, on `--run` only, with one target named:
72
+
73
+ ```bash
74
+ sentinel --run --typescript -- --noImplicitAny # what would this deferred rule cost?
75
+ sentinel --run --lint -- --deny-warnings # what would zero warnings take?
76
+ sentinel --run --lint --fix -- src/a.ts src/b.ts # only these files (what a commit hook wants)
77
+ ```
78
+
79
+ A trailing argument is a **file** when it exists on disk, and a tool **option** otherwise.
80
+ That is why `-- -D no-console` still works: `no-console` is not a file.
81
+
82
+ A run carrying your own options says so on stderr and marks itself in the `--json` envelope.
83
+ These options can weaken a check as easily as strengthen it, and `--run` is what CI calls.
84
+
85
+ ## What a fully adopted module looks like
86
+
87
+ Four files, and none of them holds a rule.
88
+
89
+ ```jsonc
90
+ // .oxlintrc.json — the rules come from the preset; the ignores must be here, because oxlint
91
+ // does not inherit them through `extends`
92
+ {
93
+ "extends": ["./node_modules/@hublo/sentinel/oxlint/react.json"],
94
+ "ignorePatterns": ["**/dist/**", "**/node_modules/**", "..."],
95
+ }
96
+ ```
97
+
98
+ ```jsonc
99
+ // .oxfmtrc.json — MATERIALIZED, because oxfmt has no `extends` and ignores the key silently.
100
+ // `$sentinel.local` names the options this module deliberately owns; a re-init keeps exactly
101
+ // those and refreshes the rest.
102
+ {
103
+ "$sentinel": { "preset": "base", "version": "1.1.0", "local": ["printWidth"] },
104
+ "printWidth": 100,
105
+ "semi": false,
106
+ }
107
+ ```
108
+
109
+ ```jsonc
110
+ // tsconfig.json — composes the repo base with the sentinel preset
111
+ {
112
+ "extends": ["../../tsconfig.base.json", "@hublo/sentinel/tsconfig/react"],
113
+ "include": ["src"],
114
+ }
115
+ ```
116
+
117
+ ```jsonc
118
+ // package.json
119
+ {
120
+ "scripts": {
121
+ // `lint` checks BOTH, the way `prettier --check . && eslint .` used to. In the :fix pair
122
+ // the formatter runs LAST, because the linter's autofix rewrites code.
123
+ "lint": "sentinel --run --lint && sentinel --run --format",
124
+ "lint:fix": "sentinel --run --lint --fix && sentinel --run --format --fix",
125
+ "format": "sentinel --run --format",
126
+ "format:fix": "sentinel --run --format --fix",
127
+ "typecheck": "sentinel --run --typescript",
128
+ // `--` so the runner can still be asked your own question: `pnpm test -- --coverage`
129
+ "test": "sentinel --run --test --",
130
+ },
131
+ "devDependencies": { "@hublo/sentinel": "1.1.0" },
132
+ }
133
+ ```
134
+
135
+ The `nx` block carries what only nx needs: which inputs invalidate the cache. **Not the
136
+ command.** `pnpm run lint` has to work whatever orchestrates the module, so nx must not be the
137
+ only thing that knows how to check it; nx infers the target from the script, which it does
138
+ even when the module also has a `project.json`.
139
+
140
+ ```jsonc
141
+ // package.json, continued
142
+ "nx": {
143
+ "targets": {
144
+ "lint": { "cache": true, "inputs": ["default", "{projectRoot}/.oxlintrc.json"] },
145
+ "format": { "cache": true, "inputs": ["default", "{projectRoot}/.oxfmtrc.json"] },
146
+ // the writer is never cached: a cache hit would change nothing and leave the files
147
+ // unformatted while nx reports success
148
+ "format:fix": { "cache": false },
149
+ // `outputs` is CARRIED from the target being replaced, never invented: without one, a
150
+ // cache hit restores nothing and the coverage directory is silently empty, so a target
151
+ // that declares none is left uncached on purpose
152
+ "test": {
153
+ "cache": true,
154
+ "inputs": ["default", "^default", "{projectRoot}/vitest.config.mts"],
155
+ "outputs": ["{workspaceRoot}/coverage/libs/front/api"],
156
+ },
157
+ },
158
+ }
159
+ ```
160
+
161
+ And **`--init` removes that role's target from `project.json`**, leaving every other target
162
+ alone. Each adoption decouples one more thing from nx: the command moves to the script, the
163
+ caching moves to the manifest, and `project.json` shrinks. Declaring the inputs is not
164
+ tidiness, it is the point: without them the inferred target inherits `targetDefaults`, and in
165
+ this repo the lint default pins the cache to the ROOT eslint config, a file an adopted module
166
+ no longer uses.
167
+
168
+ And one line, added by adoption, in the workspace root's `.prettierignore`, so the root
169
+ formatter stops competing for a module that now formats itself.
170
+
171
+ ## Versions, and moving between them
172
+
173
+ An adopting module pins an exact version:
174
+
175
+ ```json
176
+ "devDependencies": { "@hublo/sentinel": "1.1.0-alpha.5" }
177
+ ```
178
+
179
+ That pin is what makes a preset change **reviewable**: it lands in one module's PR, run by
180
+ that module's team, rather than arriving everywhere at once on an unrelated install.
181
+
182
+ How a change reaches modules depends on the role, and the difference is not cosmetic:
183
+
184
+ - **lint and typescript** commit a stub that `extends` a file inside the package, so a preset
185
+ change arrives with the new version: bump, install, done.
186
+ - **format** materializes the values, because oxfmt has no `extends`. A preset change reaches
187
+ nobody until someone re-runs `sentinel --init --format` in that module.
188
+
189
+ `sentinel --inspect --format` from the root reports which modules are behind, so the gap is
190
+ visible. Closing it in one action is [issue #17](https://github.com/hublo/sentinel/issues/17);
191
+ today it is one command per module.
192
+
193
+ **Upgrading:** bump the pin, `pnpm install`, then `sentinel --init` in the module and commit
194
+ what changes. `--init` is idempotent, so on a version that changed nothing relevant it reports
195
+ nothing to change.
@@ -0,0 +1,101 @@
1
+ # Validating a change
2
+
3
+ How a change to sentinel is proven before it ships, and the traps that made each step necessary.
4
+
5
+ Everything here was learned the expensive way. None of it is theory.
6
+
7
+ - [The rule everything follows](#the-rule-everything-follows)
8
+ - [Phase 0: prove the premise](#phase-0-prove-the-premise)
9
+ - [Phase 1: measure the output](#phase-1-measure-the-output)
10
+ - [Traps that report success](#traps-that-report-success)
11
+ - [What CI does not cover](#what-ci-does-not-cover)
12
+
13
+ ## The rule everything follows
14
+
15
+ > A check must not be satisfiable by **doing nothing**.
16
+
17
+ Ask what your check would report if the step it verifies never ran. If the answer is "success",
18
+ the check is decoration.
19
+
20
+ This is not abstract. A test asserting `plan().operations` is empty passed for a week on a code
21
+ path that returned early and wrote nothing, while the thing it was supposed to guarantee (that
22
+ re-running changes nothing on disk) was broken in the other direction.
23
+
24
+ ## Phase 0: prove the premise
25
+
26
+ **Before** any expensive measurement, prove you are measuring the right thing. A perfect method
27
+ pointed at the wrong object gives a perfectly wrong answer, and nothing in the result says so.
28
+
29
+ For the build role that is one command:
30
+
31
+ ```bash
32
+ sentinel --inspect --build # conformant, or the drift named
33
+ ```
34
+
35
+ It reports the seven things a half-adoption is missing. Every one of them shipped at least once.
36
+
37
+ The cost of skipping this: a sweep once reported "36 of 36 contracts identical" on a branch where
38
+ 34 of the services still built with **webpack**. The method was sound. The premise was not, and the
39
+ only reason it surfaced was an unrelated error message mentioning webpack.
40
+
41
+ ## Phase 1: measure the output
42
+
43
+ For a Nest service, the acceptance test is its committed OpenAPI contract:
44
+
45
+ ```bash
46
+ echo '{"corrompu":true}' > libs/api-types/service-<x>/src/service-<x>.swagger.json
47
+ NX_CACHE_DIRECTORY=$(mktemp -d) nx run <x>:generate-swagger-file --skip-nx-cache
48
+ git diff --exit-code -- libs/api-types/service-<x>/
49
+ ```
50
+
51
+ Three things in there are load-bearing:
52
+
53
+ - **overwrite the contract first.** An untouched file is byte-identical to the committed one, so
54
+ without this "identical" can mean "nothing ran".
55
+ - **an empty `NX_CACHE_DIRECTORY`** kills the local cache.
56
+ - **`--skip-nx-cache`** disconnects the remote one.
57
+
58
+ Neither cache flag alone is enough, and neither name says what it does: `--skip-nx-cache` only
59
+ disconnects Nx Cloud, and `--no-cache` only stops **writing**. Measured: with either alone, nx
60
+ restored a cached contract over the corrupted file and reported success without building.
61
+
62
+ For a front app the equivalent is the output count:
63
+
64
+ ```bash
65
+ rm -rf apps/front/<x>/.output && nx run <x>:build # note the file count
66
+ rm -rf apps/front/<x>/.output && nx run <x>:build # cache hit: same count restored?
67
+ ```
68
+
69
+ `console` once restored **0 of 464** files from a green cache hit. Its image runs `pnpm` rather
70
+ than nx, so the one caller that would have failed never used the cache.
71
+
72
+ ## Traps that report success
73
+
74
+ | Trap | What it looks like | What closes it |
75
+ | --------------------------- | ------------------------------- | -------------------------------------------------------------------------- |
76
+ | An untouched file | "identical", forever | overwrite it with garbage first |
77
+ | `--skip-nx-cache` | a cold run | it only disconnects Nx Cloud |
78
+ | `--no-cache` | a cold run | it only stops **writing** to the cache |
79
+ | A clock reading vs an mtime | passes locally, fails in CI | stamp from the same filesystem |
80
+ | A stale cache entry | a hit that restores nothing | `nx reset`; entries predating a module's `outputs` restore nothing forever |
81
+ | `"$var:generate-x"` in zsh | a task name that does not exist | brace it: `"${var}:generate-x"` — `:g` is a zsh modifier |
82
+
83
+ The last one is the only one that lies toward FAILURE, which makes it visible but sends you
84
+ hunting for causes elsewhere. It produced several "failures" that were only quoting.
85
+
86
+ ## What CI does not cover
87
+
88
+ **The build only runs at deploy time.** Neither PR CI nor a local `nx build` exercises the image,
89
+ so a regression there is invisible until a deploy. Two of five defects on one release were only
90
+ visible on env4, which is why env4 is in the definition of done.
91
+
92
+ **Prisma clients** need `pnpm prisma:gen:*` before a local Nest build, or resolution fails on a
93
+ package that looks missing.
94
+
95
+ **Front configs are executed**, so their environment variables must exist wherever you build:
96
+ `host-admin` and `career` stop on a missing `VITE_BRAND_ID` before a single file is bundled.
97
+
98
+ **Fixtures are not modules.** Every fixture in this repo writes a `package.json`, because that is
99
+ what a module looks like once you have thought about it. Measured on the real monorepo: 22 of 37
100
+ Nest services have none at all. A change that looked complete against 64 test files refused the
101
+ majority of the migration, and only a sweep over every real module found it.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hublo/sentinel",
3
- "version": "1.3.0",
3
+ "version": "1.4.0-alpha.10",
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",
@@ -49,26 +49,54 @@
49
49
  "./test/react": {
50
50
  "types": "./dist/roles/test/react/toolchain.d.ts",
51
51
  "import": "./dist/roles/test/react/toolchain.js"
52
+ },
53
+ "./test/nest": {
54
+ "types": "./dist/roles/test/nest/toolchain.d.ts",
55
+ "import": "./dist/roles/test/nest/toolchain.js"
56
+ },
57
+ "./test/setup/jest-parity": {
58
+ "types": "./dist/roles/test/setup/jest-parity.d.ts",
59
+ "import": "./dist/roles/test/setup/jest-parity.js"
60
+ },
61
+ "./test/msw": {
62
+ "types": "./dist/roles/test/setup/msw-server.d.ts",
63
+ "import": "./dist/roles/test/setup/msw-server.js"
64
+ },
65
+ "./test/tools/msw": {
66
+ "types": "./dist/roles/test/tools/msw.d.ts",
67
+ "import": "./dist/roles/test/tools/msw.js"
68
+ },
69
+ "./test/setup/msw-lifecycle": {
70
+ "types": "./dist/roles/test/setup/msw-lifecycle.d.ts",
71
+ "import": "./dist/roles/test/setup/msw-lifecycle.js"
72
+ },
73
+ "./test/setup/workspace": {
74
+ "types": "./dist/roles/test/setup/workspace-entry.d.ts",
75
+ "import": "./dist/roles/test/setup/workspace-entry.js"
52
76
  }
53
77
  },
54
78
  "files": [
55
79
  "dist",
80
+ "docs",
56
81
  "lint",
57
82
  "oxlint",
58
- "plugins",
59
- "!dist/**/*.map"
83
+ "plugins"
60
84
  ],
61
85
  "dependencies": {
62
86
  "@nx/eslint-plugin": "23.0.1",
63
87
  "@tailwindcss/vite": "4.1.18",
64
88
  "@vitejs/plugin-react": "6.0.1",
89
+ "@vitest/coverage-v8": "4.1.4",
65
90
  "commander": "^13.0.0",
91
+ "dotenv-flow": "4.1.0",
66
92
  "eslint-plugin-jest-dom": "5.5.0",
67
93
  "eslint-plugin-no-barrel-files": "1.2.1",
68
94
  "eslint-plugin-react-refresh": "0.4.19",
69
95
  "eslint-plugin-storybook": "10.3.5",
70
96
  "eslint-plugin-styled-components-a11y": "2.2.0",
97
+ "jsdom": "^24.1.3",
71
98
  "jsonc-parser": "^3.3.1",
99
+ "msw": "1.3.3",
72
100
  "nitro": "3.0.260415-beta",
73
101
  "oxc-parser": "0.147.0",
74
102
  "oxfmt": "0.63.0",
@@ -77,7 +105,8 @@
77
105
  "typescript": "5.9.3",
78
106
  "vite": "8.2.2",
79
107
  "vite-plugin-svgr": "5.2.0",
80
- "vitest": "4.1.4"
108
+ "vitest": "4.1.4",
109
+ "vitest-mock-extended": "5.1.1"
81
110
  },
82
111
  "devDependencies": {
83
112
  "@hublo/sentinel": "link:.",
@@ -110,10 +139,10 @@
110
139
  "nx": ">= 21"
111
140
  },
112
141
  "peerDependenciesMeta": {
113
- "nx": {
142
+ "@nx/js": {
114
143
  "optional": true
115
144
  },
116
- "@nx/js": {
145
+ "nx": {
117
146
  "optional": true
118
147
  }
119
148
  },