@hublo/sentinel 1.3.0 → 1.4.0-alpha.2

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 (48) hide show
  1. package/README.md +8 -4
  2. package/dist/bin/sentinel.d.ts +0 -1
  3. package/dist/bin/sentinel.js +22 -8
  4. package/dist/chunk-2XLX6PFR.js +132 -0
  5. package/dist/chunk-3TDUIKVQ.js +178 -0
  6. package/dist/{chunk-676GBPMS.js → chunk-4UIZJ3TR.js} +3399 -549
  7. package/dist/chunk-CPCUPK4J.js +70 -0
  8. package/dist/chunk-NX4GHIHF.js +25 -0
  9. package/dist/chunk-PWV3BMDA.js +15 -0
  10. package/dist/chunk-WLFE5RUU.js +264 -0
  11. package/dist/index.js +2 -1
  12. package/dist/roles/build/nest/toolchain.d.ts +4 -36
  13. package/dist/roles/build/nest/toolchain.js +10 -178
  14. package/dist/roles/build/nest/toolchain.js.map +1 -0
  15. package/dist/roles/build/toolchain.js.map +1 -0
  16. package/dist/roles/test/nest/toolchain.d.ts +29 -0
  17. package/dist/roles/test/nest/toolchain.js +289 -0
  18. package/dist/roles/test/nest/toolchain.js.map +1 -0
  19. package/dist/roles/test/react/toolchain.d.ts +62 -0
  20. package/dist/roles/test/react/toolchain.js +5 -0
  21. package/dist/roles/test/react/toolchain.js.map +1 -0
  22. package/dist/roles/test/setup/mock-extended.d.ts +46 -0
  23. package/dist/roles/test/setup/mock-extended.js +65 -0
  24. package/dist/roles/test/setup/mock-extended.js.map +1 -0
  25. package/dist/roles/test/setup/msw-lifecycle.d.ts +16 -0
  26. package/dist/roles/test/setup/msw-lifecycle.js +12 -0
  27. package/dist/roles/test/setup/msw-lifecycle.js.map +1 -0
  28. package/dist/roles/test/setup/msw-server.d.ts +3 -0
  29. package/dist/roles/test/setup/msw-server.js +10 -0
  30. package/dist/roles/test/setup/msw-server.js.map +1 -0
  31. package/dist/roles/test/setup/nest.d.ts +2 -0
  32. package/dist/roles/test/setup/nest.js +159 -0
  33. package/dist/roles/test/setup/nest.js.map +1 -0
  34. package/dist/roles/test/setup/workspace-entry.d.ts +2 -0
  35. package/dist/roles/test/setup/workspace-entry.js +8 -0
  36. package/dist/roles/test/setup/workspace-entry.js.map +1 -0
  37. package/dist/tsconfig-aliases-Ce6axdJ4.d.ts +36 -0
  38. package/docs/.gitkeep +0 -0
  39. package/docs/build-adoption.md +521 -0
  40. package/docs/format-adoption.md +321 -0
  41. package/docs/lint-adoption.md +290 -0
  42. package/docs/performance.md +49 -0
  43. package/docs/test-adoption.md +175 -0
  44. package/docs/typescript-adoption.md +184 -0
  45. package/docs/typescript-traces.md +798 -0
  46. package/docs/using-sentinel.md +180 -0
  47. package/docs/validating-a-change.md +101 -0
  48. package/package.json +32 -5
@@ -0,0 +1,180 @@
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) and
7
+ [`build-adoption.md`](build-adoption.md).
8
+
9
+ ## The grid
10
+
11
+ A command is **verb + target + location**.
12
+
13
+ ```bash
14
+ sentinel --run --lint # in a module dir -> that module
15
+ sentinel --run --lint --module bff-admin # from the root -> one module
16
+ sentinel --run --ci # from the root -> affected only
17
+ sentinel --run # no target -> every wired target
18
+ ```
19
+
20
+ | Verb | `--lint` | `--format` | `--typescript` |
21
+ | ----------- | --------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- |
22
+ | `--init` | stub + scripts + nx cache metadata, removes the ESLint config and the module's ESLint deps, then runs oxlint's autofix once | materializes the preset + scripts + nx cache metadata, removes the Prettier config and deps, then formats the module once | writes/extends the tsconfig + `typecheck` script |
23
+ | `--run` | lints; `--fix` autofixes in the same pass | **checks**; `--fix` writes | typechecks |
24
+ | `--inspect` | rule count, what is disabled or downgraded and **why**, adoption, drift | effective options, every option that departs from the standard and why, adoption, drift | resolved options, what is deferred, adoption, drift |
25
+ | `--report` | error and warning counts with a per-rule breakdown | how many files are unformatted, and which | error counts |
26
+ | `--status` | deprecated, `--inspect` answers this and more | same | same |
27
+ | `--migrate` | planned, refused for now | same | same |
28
+
29
+ `--json` works for every verb, and stdout carries **only** the envelope, so `| jq` always
30
+ parses. `--dry-run` applies to `--init` and writes nothing.
31
+
32
+ `--init` is the one verb that targets exactly **one** module. Adopting everything at once
33
+ would be a big-bang; migration is meant to be gradual and per-team.
34
+
35
+ ## When a role has nothing to do here
36
+
37
+ Adoption is per role, not per module, and a role declines when **its own tool** has nothing to
38
+ read. Run in `apps/front/maintenance`, which is a static `index.html`, a stylesheet and an SVG:
39
+
40
+ ```console
41
+ $ sentinel --init
42
+ lint: no file oxlint can read (looked for .js, .mjs, .cjs, .jsx, .ts, ...), so there is
43
+ nothing to lint here. Other roles may still apply: the formatter reads markdown,
44
+ YAML, HTML and CSS too.
45
+ typescript: no TypeScript here (looked for .ts, .mts, .cts, .tsx), so there is nothing
46
+ for tsc to check.
47
+ wrote .oxfmtrc.json
48
+ ...
49
+ ```
50
+
51
+ Two roles decline, the formatter adopts, and the command **exits 0**. Nothing failed: the
52
+ module simply is what it is. "Not a TypeScript project" would have been the wrong question,
53
+ since oxfmt has real work in exactly that module.
54
+
55
+ Declining matters for the numbers as much as the tidiness. A lint config written for a module
56
+ with nothing to lint reports as covered on the migration page, forever, while checking an
57
+ empty set.
58
+
59
+ The check names what it looked for, so you can disagree with it. It searches the whole module
60
+ rather than assuming `src/` (code lives in `lib/`, `scripts/` and at the root here too), stops
61
+ at the first match, and never descends into `node_modules` or build output: 0.1ms on a
62
+ 4210-file module, 47ms in the pathological case where nothing matches anywhere.
63
+
64
+ ## Asking the tool your own question
65
+
66
+ Everything after `--` goes to the underlying tool, on `--run` only, with one target named:
67
+
68
+ ```bash
69
+ sentinel --run --typescript -- --noImplicitAny # what would this deferred rule cost?
70
+ sentinel --run --lint -- --deny-warnings # what would zero warnings take?
71
+ sentinel --run --lint --fix -- src/a.ts src/b.ts # only these files (what a commit hook wants)
72
+ ```
73
+
74
+ A trailing argument is a **file** when it exists on disk, and a tool **option** otherwise.
75
+ That is why `-- -D no-console` still works: `no-console` is not a file.
76
+
77
+ A run carrying your own options says so on stderr and marks itself in the `--json` envelope.
78
+ These options can weaken a check as easily as strengthen it, and `--run` is what CI calls.
79
+
80
+ ## What a fully adopted module looks like
81
+
82
+ Four files, and none of them holds a rule.
83
+
84
+ ```jsonc
85
+ // .oxlintrc.json — the rules come from the preset; the ignores must be here, because oxlint
86
+ // does not inherit them through `extends`
87
+ {
88
+ "extends": ["./node_modules/@hublo/sentinel/oxlint/react.json"],
89
+ "ignorePatterns": ["**/dist/**", "**/node_modules/**", "..."],
90
+ }
91
+ ```
92
+
93
+ ```jsonc
94
+ // .oxfmtrc.json — MATERIALIZED, because oxfmt has no `extends` and ignores the key silently.
95
+ // `$sentinel.local` names the options this module deliberately owns; a re-init keeps exactly
96
+ // those and refreshes the rest.
97
+ {
98
+ "$sentinel": { "preset": "base", "version": "1.1.0", "local": ["printWidth"] },
99
+ "printWidth": 100,
100
+ "semi": false,
101
+ }
102
+ ```
103
+
104
+ ```jsonc
105
+ // tsconfig.json — composes the repo base with the sentinel preset
106
+ {
107
+ "extends": ["../../tsconfig.base.json", "@hublo/sentinel/tsconfig/react"],
108
+ "include": ["src"],
109
+ }
110
+ ```
111
+
112
+ ```jsonc
113
+ // package.json
114
+ {
115
+ "scripts": {
116
+ // `lint` checks BOTH, the way `prettier --check . && eslint .` used to. In the :fix pair
117
+ // the formatter runs LAST, because the linter's autofix rewrites code.
118
+ "lint": "sentinel --run --lint && sentinel --run --format",
119
+ "lint:fix": "sentinel --run --lint --fix && sentinel --run --format --fix",
120
+ "format": "sentinel --run --format",
121
+ "format:fix": "sentinel --run --format --fix",
122
+ "typecheck": "sentinel --run --typescript",
123
+ },
124
+ "devDependencies": { "@hublo/sentinel": "1.1.0" },
125
+ }
126
+ ```
127
+
128
+ The `nx` block carries what only nx needs: which inputs invalidate the cache. **Not the
129
+ command.** `pnpm run lint` has to work whatever orchestrates the module, so nx must not be the
130
+ only thing that knows how to check it; nx infers the target from the script, which it does
131
+ even when the module also has a `project.json`.
132
+
133
+ ```jsonc
134
+ // package.json, continued
135
+ "nx": {
136
+ "targets": {
137
+ "lint": { "cache": true, "inputs": ["default", "{projectRoot}/.oxlintrc.json"] },
138
+ "format": { "cache": true, "inputs": ["default", "{projectRoot}/.oxfmtrc.json"] },
139
+ // the writer is never cached: a cache hit would change nothing and leave the files
140
+ // unformatted while nx reports success
141
+ "format:fix": { "cache": false },
142
+ },
143
+ }
144
+ ```
145
+
146
+ And **`--init` removes that role's target from `project.json`**, leaving every other target
147
+ alone. Each adoption decouples one more thing from nx: the command moves to the script, the
148
+ caching moves to the manifest, and `project.json` shrinks. Declaring the inputs is not
149
+ tidiness, it is the point: without them the inferred target inherits `targetDefaults`, and in
150
+ this repo the lint default pins the cache to the ROOT eslint config, a file an adopted module
151
+ no longer uses.
152
+
153
+ And one line, added by adoption, in the workspace root's `.prettierignore`, so the root
154
+ formatter stops competing for a module that now formats itself.
155
+
156
+ ## Versions, and moving between them
157
+
158
+ An adopting module pins an exact version:
159
+
160
+ ```json
161
+ "devDependencies": { "@hublo/sentinel": "1.1.0-alpha.5" }
162
+ ```
163
+
164
+ That pin is what makes a preset change **reviewable**: it lands in one module's PR, run by
165
+ that module's team, rather than arriving everywhere at once on an unrelated install.
166
+
167
+ How a change reaches modules depends on the role, and the difference is not cosmetic:
168
+
169
+ - **lint and typescript** commit a stub that `extends` a file inside the package, so a preset
170
+ change arrives with the new version: bump, install, done.
171
+ - **format** materializes the values, because oxfmt has no `extends`. A preset change reaches
172
+ nobody until someone re-runs `sentinel --init --format` in that module.
173
+
174
+ `sentinel --inspect --format` from the root reports which modules are behind, so the gap is
175
+ visible. Closing it in one action is [issue #17](https://github.com/hublo/sentinel/issues/17);
176
+ today it is one command per module.
177
+
178
+ **Upgrading:** bump the pin, `pnpm install`, then `sentinel --init` in the module and commit
179
+ what changes. `--init` is idempotent, so on a version that changed nothing relevant it reports
180
+ 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.2",
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,52 @@
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/nest": {
58
+ "types": "./dist/roles/test/setup/nest.d.ts",
59
+ "import": "./dist/roles/test/setup/nest.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/setup/msw-lifecycle": {
66
+ "types": "./dist/roles/test/setup/msw-lifecycle.d.ts",
67
+ "import": "./dist/roles/test/setup/msw-lifecycle.js"
68
+ },
69
+ "./test/setup/workspace": {
70
+ "types": "./dist/roles/test/setup/workspace-entry.d.ts",
71
+ "import": "./dist/roles/test/setup/workspace-entry.js"
52
72
  }
53
73
  },
54
74
  "files": [
55
75
  "dist",
76
+ "docs",
56
77
  "lint",
57
78
  "oxlint",
58
79
  "plugins",
59
- "!dist/**/*.map"
80
+ "!dist/**/*.map",
81
+ "dist/roles/**/*.map"
60
82
  ],
61
83
  "dependencies": {
62
84
  "@nx/eslint-plugin": "23.0.1",
63
85
  "@tailwindcss/vite": "4.1.18",
64
86
  "@vitejs/plugin-react": "6.0.1",
87
+ "@vitest/coverage-v8": "4.1.4",
65
88
  "commander": "^13.0.0",
89
+ "dotenv-flow": "4.1.0",
66
90
  "eslint-plugin-jest-dom": "5.5.0",
67
91
  "eslint-plugin-no-barrel-files": "1.2.1",
68
92
  "eslint-plugin-react-refresh": "0.4.19",
69
93
  "eslint-plugin-storybook": "10.3.5",
70
94
  "eslint-plugin-styled-components-a11y": "2.2.0",
95
+ "jsdom": "^24.1.3",
71
96
  "jsonc-parser": "^3.3.1",
97
+ "msw": "1.3.3",
72
98
  "nitro": "3.0.260415-beta",
73
99
  "oxc-parser": "0.147.0",
74
100
  "oxfmt": "0.63.0",
@@ -77,7 +103,8 @@
77
103
  "typescript": "5.9.3",
78
104
  "vite": "8.2.2",
79
105
  "vite-plugin-svgr": "5.2.0",
80
- "vitest": "4.1.4"
106
+ "vitest": "4.1.4",
107
+ "vitest-mock-extended": "5.1.1"
81
108
  },
82
109
  "devDependencies": {
83
110
  "@hublo/sentinel": "link:.",
@@ -110,10 +137,10 @@
110
137
  "nx": ">= 21"
111
138
  },
112
139
  "peerDependenciesMeta": {
113
- "nx": {
140
+ "@nx/js": {
114
141
  "optional": true
115
142
  },
116
- "@nx/js": {
143
+ "nx": {
117
144
  "optional": true
118
145
  }
119
146
  },