@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.
- package/README.md +8 -4
- package/dist/bin/sentinel.d.ts +0 -1
- package/dist/bin/sentinel.js +22 -8
- package/dist/chunk-2XLX6PFR.js +132 -0
- package/dist/chunk-3TDUIKVQ.js +178 -0
- package/dist/{chunk-676GBPMS.js → chunk-4UIZJ3TR.js} +3399 -549
- package/dist/chunk-CPCUPK4J.js +70 -0
- package/dist/chunk-NX4GHIHF.js +25 -0
- package/dist/chunk-PWV3BMDA.js +15 -0
- package/dist/chunk-WLFE5RUU.js +264 -0
- package/dist/index.js +2 -1
- package/dist/roles/build/nest/toolchain.d.ts +4 -36
- package/dist/roles/build/nest/toolchain.js +10 -178
- package/dist/roles/build/nest/toolchain.js.map +1 -0
- package/dist/roles/build/toolchain.js.map +1 -0
- package/dist/roles/test/nest/toolchain.d.ts +29 -0
- package/dist/roles/test/nest/toolchain.js +289 -0
- package/dist/roles/test/nest/toolchain.js.map +1 -0
- package/dist/roles/test/react/toolchain.d.ts +62 -0
- package/dist/roles/test/react/toolchain.js +5 -0
- package/dist/roles/test/react/toolchain.js.map +1 -0
- package/dist/roles/test/setup/mock-extended.d.ts +46 -0
- package/dist/roles/test/setup/mock-extended.js +65 -0
- package/dist/roles/test/setup/mock-extended.js.map +1 -0
- package/dist/roles/test/setup/msw-lifecycle.d.ts +16 -0
- package/dist/roles/test/setup/msw-lifecycle.js +12 -0
- package/dist/roles/test/setup/msw-lifecycle.js.map +1 -0
- package/dist/roles/test/setup/msw-server.d.ts +3 -0
- package/dist/roles/test/setup/msw-server.js +10 -0
- package/dist/roles/test/setup/msw-server.js.map +1 -0
- package/dist/roles/test/setup/nest.d.ts +2 -0
- package/dist/roles/test/setup/nest.js +159 -0
- package/dist/roles/test/setup/nest.js.map +1 -0
- package/dist/roles/test/setup/workspace-entry.d.ts +2 -0
- package/dist/roles/test/setup/workspace-entry.js +8 -0
- package/dist/roles/test/setup/workspace-entry.js.map +1 -0
- package/dist/tsconfig-aliases-Ce6axdJ4.d.ts +36 -0
- package/docs/.gitkeep +0 -0
- package/docs/build-adoption.md +521 -0
- package/docs/format-adoption.md +321 -0
- package/docs/lint-adoption.md +290 -0
- package/docs/performance.md +49 -0
- package/docs/test-adoption.md +175 -0
- package/docs/typescript-adoption.md +184 -0
- package/docs/typescript-traces.md +798 -0
- package/docs/using-sentinel.md +180 -0
- package/docs/validating-a-change.md +101 -0
- package/package.json +32 -5
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
# Format adoption cheat sheet
|
|
2
|
+
|
|
3
|
+
Migrating one module from Prettier to oxfmt, through sentinel. For linting see
|
|
4
|
+
[`lint-adoption.md`](lint-adoption.md), for TypeScript
|
|
5
|
+
[`typescript-adoption.md`](typescript-adoption.md).
|
|
6
|
+
|
|
7
|
+
## Adopt a module
|
|
8
|
+
|
|
9
|
+
Run from the **module's own directory**.
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
cd apps/.../<module>
|
|
13
|
+
npx --yes @hublo/sentinel@<version> --init --preset <react|nest|node|svelte>
|
|
14
|
+
pnpm install
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`--init` with no target adopts every role, and that is the intended order: lint fixes what it
|
|
18
|
+
can, then the formatter runs **last** and formats everything every role wrote. Adopting the
|
|
19
|
+
formatter alone is `--init --format`.
|
|
20
|
+
|
|
21
|
+
**`--init` reformats your module.** That is deliberate: adoption that leaves a module failing
|
|
22
|
+
its own new format check is not adoption, it is a chore handed to someone else. Commit it on
|
|
23
|
+
its own, so the reformat is one reviewable diff rather than noise on top of a real change.
|
|
24
|
+
|
|
25
|
+
> **Order matters, and `--init` with no target gets it right for you.** The linter's autofix
|
|
26
|
+
> rewrites code, and sentinel writes JSON in its own style, so **formatting has to come after
|
|
27
|
+
> linting**, never before. Run whole-module `--init` and it happens automatically: format runs
|
|
28
|
+
> LAST, over everything every role wrote. If you adopt roles one at a time, run
|
|
29
|
+
> `sentinel --init --format` after `sentinel --init --lint`, or your first `pnpm run lint`
|
|
30
|
+
> fails at a formatting step that has nothing to do with linting.
|
|
31
|
+
|
|
32
|
+
## What `--init` does
|
|
33
|
+
|
|
34
|
+
| It writes | Why |
|
|
35
|
+
| ------------------------------- | ---------------------------------------------------------------------------- |
|
|
36
|
+
| `.oxfmtrc.json` | the preset's options, **materialized**, plus anything your module differs on |
|
|
37
|
+
| `format` / `format:fix` scripts | `format` is the CHECK (what CI runs), `format:fix` writes |
|
|
38
|
+
| the nx `format` target | with the config as an input, so a preset change invalidates the cache |
|
|
39
|
+
| `@hublo/sentinel` devDependency | pinned, so the CLI resolves |
|
|
40
|
+
|
|
41
|
+
| It removes | Why |
|
|
42
|
+
| ----------------------------------- | -------------------------------------------------------------------------------------------------- |
|
|
43
|
+
| your `.prettierrc*` | adoption REPLACES Prettier; two configured formatters means whichever the runner picks wins |
|
|
44
|
+
| your module's Prettier dependencies | they served that config. The **root** keeps its tooling, which every unmigrated module still needs |
|
|
45
|
+
|
|
46
|
+
Any other script that called `prettier` is rewritten too (a `format:check` left pointing at a
|
|
47
|
+
removed binary fails the first time someone runs it, not at adoption when it would be obvious).
|
|
48
|
+
Only the prettier segment is replaced, so `svelte-kit sync && <format> && eslint .` keeps its
|
|
49
|
+
other commands.
|
|
50
|
+
|
|
51
|
+
## Commands
|
|
52
|
+
|
|
53
|
+
Every verb, for every role, plus scoping a run to some files and what a fully adopted module
|
|
54
|
+
looks like: [`using-sentinel.md`](using-sentinel.md).
|
|
55
|
+
|
|
56
|
+
## Why this config is not a one-line stub
|
|
57
|
+
|
|
58
|
+
Every other role commits a stub that `extends` a preset shipped inside the package. **oxfmt has
|
|
59
|
+
no `extends`, and it ignores unknown top-level keys silently.** A stub like
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
{ "extends": ["./node_modules/@hublo/sentinel/oxfmt/base.json"] }
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
parses fine, reports as adopted, and formats with oxfmt's **defaults** (double quotes,
|
|
66
|
+
semicolons). Verified: under that stub a file in this repo's style fails the check and a file
|
|
67
|
+
in oxfmt's default style passes. Shipping it would have rewritten the whole monorepo into a
|
|
68
|
+
style nobody chose.
|
|
69
|
+
|
|
70
|
+
Note that `base` here **is** a preset you name, unlike the other roles. Formatting does not
|
|
71
|
+
vary by stack: the repo has always formatted Nest, React, Svelte and tooling from one root
|
|
72
|
+
`.prettierrc`, so there is one style and `base` is it (`svelte` and `nest` are that same style
|
|
73
|
+
with one option flipped). In the lint, typescript and build roles the shared layer is called
|
|
74
|
+
`shared.json`, is never published, and is not something a module names.
|
|
75
|
+
|
|
76
|
+
So the preset's values are written INTO your config. Two things follow:
|
|
77
|
+
|
|
78
|
+
- Your editor's oxfmt extension reads this file, so format-on-save agrees with CI.
|
|
79
|
+
- A preset change does **not** reach you by reinstalling. It needs a re-`init`.
|
|
80
|
+
`sentinel --inspect --format` from the root reports which modules are behind.
|
|
81
|
+
|
|
82
|
+
## Keeping your module's own formatting
|
|
83
|
+
|
|
84
|
+
The preset carries the repo's formatting. If your module genuinely differs, that is recorded
|
|
85
|
+
rather than overwritten: adoption reads your Prettier config and carries what it finds. Three
|
|
86
|
+
modules in this repo already differ (tabs at 100 columns, semicolons), and reformatting them
|
|
87
|
+
on the way past would have destroyed the adoption diff.
|
|
88
|
+
|
|
89
|
+
An override lives in the config and is DECLARED in `$sentinel.local`:
|
|
90
|
+
|
|
91
|
+
```json
|
|
92
|
+
{
|
|
93
|
+
"$sentinel": { "preset": "base", "version": "1.1.0", "local": ["printWidth", "useTabs"] },
|
|
94
|
+
"printWidth": 100,
|
|
95
|
+
"useTabs": true,
|
|
96
|
+
"semi": false
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
A re-`init` keeps exactly the keys listed in `local` and refreshes the rest. Adding an override
|
|
101
|
+
by hand means adding the key **and** listing it there; a value that differs without being
|
|
102
|
+
declared is reported as drift, because nobody said this module should format differently.
|
|
103
|
+
|
|
104
|
+
`ignorePatterns` and `overrides` are always yours and never need declaring.
|
|
105
|
+
|
|
106
|
+
A setting is carried only if oxfmt accepts the **value**, not just the key: what it rejects is
|
|
107
|
+
dropped and named in the adoption output (see "What did not survive the move"). The check asks
|
|
108
|
+
oxfmt's own shipped schema, so it stays true across oxfmt releases.
|
|
109
|
+
|
|
110
|
+
### What sentinel adds to `ignorePatterns`, and why
|
|
111
|
+
|
|
112
|
+
Prettier ran from the workspace **root**, so the root `.prettierignore` governed every file in
|
|
113
|
+
the repo. oxfmt reads `.gitignore` and `.prettierignore` from its **current directory**, and
|
|
114
|
+
sentinel runs it in your module, so the root file is simply not seen.
|
|
115
|
+
|
|
116
|
+
Left alone that silently widens what gets formatted, and oxfmt's reach is wider than
|
|
117
|
+
Prettier's was here: it formats markdown, YAML, HTML, CSS **and Handlebars**. The root file
|
|
118
|
+
excludes `**/*.md` and `**/*.hbs` among others, so the first `format:fix` would have
|
|
119
|
+
reformatted every README and rewritten Handlebars templates as HTML.
|
|
120
|
+
|
|
121
|
+
So `--init` reads the root file and carries forward the patterns that were already reaching
|
|
122
|
+
into your module: those with no slash (`node_modules`), and those anchored at the root but
|
|
123
|
+
spanning any depth (`/**/*.md` becomes `**/*.md`). Root-only patterns like `/dist` are
|
|
124
|
+
**not** carried, because they never applied inside a module and carrying them would ignore
|
|
125
|
+
more than before.
|
|
126
|
+
|
|
127
|
+
Your module's own `.prettierignore` is left exactly as it is: it sits in the module
|
|
128
|
+
directory, so oxfmt already reads it.
|
|
129
|
+
|
|
130
|
+
### And your module is excluded from the ROOT Prettier
|
|
131
|
+
|
|
132
|
+
The mirror of the same problem. The root Prettier still owns every file in the repo, and it
|
|
133
|
+
does not know your module now formats itself, so `nx format:write` or `prettier --write .`
|
|
134
|
+
from the root would reformat your module back to the ROOT's style, and your next
|
|
135
|
+
`pnpm run format` would reverse it. Whichever ran last wins, which is precisely the state
|
|
136
|
+
adoption removes inside a module.
|
|
137
|
+
|
|
138
|
+
So `--init --format` adds one line to the root `.prettierignore`:
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
# @hublo/sentinel: modules formatted by oxfmt, not by the root Prettier
|
|
142
|
+
/apps/front/your-module/
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
This is the only file outside your module that adoption touches, and it is the same
|
|
146
|
+
deliberate exception the workspace prep already makes. The root Prettier config itself is
|
|
147
|
+
untouched: it still serves every module that has not migrated.
|
|
148
|
+
|
|
149
|
+
## Svelte modules
|
|
150
|
+
|
|
151
|
+
Svelte is the one preset with a formatting difference, and it is not a style. oxfmt reads
|
|
152
|
+
`.svelte` files only when its `svelte` option is on (its default is **disabled**), so on the
|
|
153
|
+
base preset a Svelte module adopts cleanly, exits 0, and quietly stops formatting the files it
|
|
154
|
+
is mostly made of. `--preset svelte` materializes the same style with `"svelte": true`:
|
|
155
|
+
|
|
156
|
+
```json
|
|
157
|
+
{
|
|
158
|
+
"$sentinel": { "preset": "svelte", "version": "1.1.0", "local": [] },
|
|
159
|
+
"printWidth": 80,
|
|
160
|
+
"svelte": true
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Prettier said the same thing through `prettier-plugin-svelte`, which adoption removes, so this
|
|
165
|
+
carries your formatting across rather than adding to it. oxfmt does not bundle the Svelte
|
|
166
|
+
compiler: it requires `svelte` from your module, which every Svelte module has.
|
|
167
|
+
|
|
168
|
+
## What did not survive the move
|
|
169
|
+
|
|
170
|
+
Adoption reads your Prettier config and reports, per module, what it could not carry. Across
|
|
171
|
+
this repo that is:
|
|
172
|
+
|
|
173
|
+
- **`endOfLine: "auto"`**: oxfmt accepts `lf`, `crlf` and `cr` only. It always writes LF, which
|
|
174
|
+
is what git stores anyway. Carried verbatim it produced a config oxfmt refuses to parse, so a
|
|
175
|
+
value oxfmt rejects is now dropped and reported rather than written.
|
|
176
|
+
- **`prettier-plugin-gherkin`**, a real loss: `.feature` files stop being formatted. They are
|
|
177
|
+
not source, no check enforces their formatting today, and oxfmt has no plugin API to carry
|
|
178
|
+
it. Accepted, and written down here rather than discovered later.
|
|
179
|
+
- **`prettier-plugin-svelte`**, which is NOT a loss: `--preset svelte` covers it (above), and
|
|
180
|
+
adoption says so rather than listing it as dropped.
|
|
181
|
+
|
|
182
|
+
Prettier `overrides` are translated, not copied: `files` is a string in Prettier and a list in
|
|
183
|
+
oxfmt, and an override whose only option was a `parser` (a plugin concept) is dropped whole
|
|
184
|
+
rather than written empty.
|
|
185
|
+
|
|
186
|
+
`sortPackageJson` is switched **off**. Left on, the first `format:fix` in every module reorders
|
|
187
|
+
its `package.json` into a diff nobody can review beside the adoption itself.
|
|
188
|
+
|
|
189
|
+
## Import ordering: on for nest, off for the front
|
|
190
|
+
|
|
191
|
+
Since 1.1.3, **adoption reformats your module; whether it also reorders your imports depends
|
|
192
|
+
on your stack**, because the two stacks were in genuinely different positions.
|
|
193
|
+
|
|
194
|
+
| Preset | Import ordering | Why |
|
|
195
|
+
| ----------------------------------------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
196
|
+
| `nest` | **on** | `import/order` is enforced in `eslint.config.back.js`. Turning it off would remove a check you have today |
|
|
197
|
+
| `react`, `react-lib`, `node`, `svelte`, `tools` | **off** | The front and root ESLint configs order nothing. You never had the check, and adoption should not charge you thousands of files to gain one |
|
|
198
|
+
|
|
199
|
+
### Where the number came from
|
|
200
|
+
|
|
201
|
+
Share of reformatted files whose diff touches **only** the import block, on 1.1.2:
|
|
202
|
+
|
|
203
|
+
| Module | Files reformatted | Import-block only |
|
|
204
|
+
| ------------------- | ----------------: | ----------------: |
|
|
205
|
+
| `host-admin` | 2,670 | **2,374 (89%)** |
|
|
206
|
+
| `bff-admin` | 1,740 | **1,590 (91%)** |
|
|
207
|
+
| `hr-management` | 303 | **290 (96%)** |
|
|
208
|
+
| `libs/front/shared` | 13 | 7 (54%) |
|
|
209
|
+
|
|
210
|
+
The two halves had different causes, which is why they get different answers.
|
|
211
|
+
|
|
212
|
+
**The front had no ordering at all**, so sorting moved almost every import. Off is also
|
|
213
|
+
oxfmt's own default, so `base` now simply stops overriding it. Written as
|
|
214
|
+
`"sortImports": false` rather than omitted, so the config states the decision.
|
|
215
|
+
|
|
216
|
+
**Nest was already ordered, and its churn was not sorting.** Of bff-admin's 1,740 changed
|
|
217
|
+
files, **1,687 differed by one blank line** and only 3 by a real reordering. ESLint grouped the
|
|
218
|
+
workspace scopes (`@hublo/**`, `@shared/**`, ...) as _internal_; oxfmt reads them as
|
|
219
|
+
_external_, so two groups collapsed into one and the line between them disappeared.
|
|
220
|
+
|
|
221
|
+
The `nest` preset restores the grouping with `customGroups`:
|
|
222
|
+
|
|
223
|
+
```json
|
|
224
|
+
"sortImports": {
|
|
225
|
+
"customGroups": [
|
|
226
|
+
{ "groupName": "workspace",
|
|
227
|
+
"elementNamePattern": ["@hublo/**", "@shared/**", "@front/**", "@network/**"] }
|
|
228
|
+
],
|
|
229
|
+
"groups": ["builtin", "external", "workspace", ["parent", "index"], "sibling"],
|
|
230
|
+
"newlinesBetween": true
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Measured on bff-admin: **1,740 changed files down to 91**, of which 43 are genuine ordering
|
|
235
|
+
fixes worth having and 48 are formatting.
|
|
236
|
+
|
|
237
|
+
`internalPattern` looks like the option for this and is not: in oxfmt 0.63.0 setting it
|
|
238
|
+
collapses the external and internal groups into one whatever the pattern (a pattern matching
|
|
239
|
+
only `zod` produced the same single block as one matching only `@hublo/`). `customGroups` is
|
|
240
|
+
the one that works.
|
|
241
|
+
|
|
242
|
+
### Turning ordering on for a front module
|
|
243
|
+
|
|
244
|
+
One line, and it is worth doing in a commit of its own:
|
|
245
|
+
|
|
246
|
+
```jsonc
|
|
247
|
+
// .oxfmtrc.json
|
|
248
|
+
{
|
|
249
|
+
"sortImports": true,
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Declare it in `$sentinel.local` so a re-`init` keeps it, then run `pnpm run format:fix` and
|
|
254
|
+
expect most of the module to be touched.
|
|
255
|
+
|
|
256
|
+
### Finding the modules that do not check it
|
|
257
|
+
|
|
258
|
+
`sentinel --inspect --format` reports it, per module, under **not checked at all**:
|
|
259
|
+
|
|
260
|
+
```console
|
|
261
|
+
✓ host-admin (react) format — adopted=true preset=base conformant=true
|
|
262
|
+
not checked at all:
|
|
263
|
+
• sortImports — import order is not checked here. Enable it with `"sortImports": true` …
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
The gap is reported rather than warned on every run: it is a state of the module, not an
|
|
267
|
+
event. A message that fires on every `pnpm run lint` is a message people stop reading.
|
|
268
|
+
|
|
269
|
+
### The one grouping difference, and why it stands
|
|
270
|
+
|
|
271
|
+
`import/order` explicitly listed this repo's 14 workspace scopes (`@hublo/**`, `@shared/**`,
|
|
272
|
+
`@front/**`, `@network/**`, and the rest) as **internal**. oxfmt classifies them as
|
|
273
|
+
**external**, because to a formatter they look like any other scoped package. `@/...` is
|
|
274
|
+
still recognised as internal, and builtins and relative imports are unaffected.
|
|
275
|
+
|
|
276
|
+
So on adoption, imports of workspace packages move from the internal group up into the
|
|
277
|
+
external one. It is cosmetic, it happens once, and it is part of the reformat commit.
|
|
278
|
+
|
|
279
|
+
It is not fixed by configuration, and that was checked rather than assumed. oxfmt's
|
|
280
|
+
`sortImports` accepts an `internalPattern` key, but in 0.63.0 setting it **collapses the
|
|
281
|
+
external and internal groups into one**, whatever the pattern: a pattern matching only `zod`
|
|
282
|
+
produced the same single block as a pattern matching only `@hublo/`. The default behaviour
|
|
283
|
+
keeps three distinct groups and is the better of the two, so sentinel does not set it. If a
|
|
284
|
+
later oxfmt implements it properly, one line in the preset restores the old grouping and
|
|
285
|
+
`--inspect` will report every module as behind until they re-`init`.
|
|
286
|
+
|
|
287
|
+
## Troubleshooting
|
|
288
|
+
|
|
289
|
+
| Symptom | Cause |
|
|
290
|
+
| -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
|
|
291
|
+
| `could not find the oxfmt binary` | oxfmt ships with sentinel; the module needs `pnpm install` |
|
|
292
|
+
| `no sentinel .oxfmtrc.json in this module` | not adopted yet (an `.oxfmtrc.json` you wrote yourself is not adoption, `--init` records provenance) |
|
|
293
|
+
| `--inspect` says the module has **drifted** | a preset-owned value was edited without declaring it in `$sentinel.local`, or the preset moved on. Re-`init` |
|
|
294
|
+
| `pnpm run format` fails on files you did not touch | the module was adopted but never formatted. Run `pnpm run format:fix` as its own commit |
|
|
295
|
+
| `.feature` files lost their formatting | expected, see above |
|
|
296
|
+
|
|
297
|
+
### `TS(1098): Type parameter list cannot be empty` (and friends)
|
|
298
|
+
|
|
299
|
+
oxfmt refuses to format a file it cannot parse, and its TypeScript parser is stricter than
|
|
300
|
+
Prettier's. **Two files in the whole monorepo** are affected, both `.d.ts`, both genuine
|
|
301
|
+
TypeScript errors that Prettier recovered from:
|
|
302
|
+
|
|
303
|
+
| File | Error | Fix |
|
|
304
|
+
| ----------------------------------------------- | -------------------------------------------- | ------------------------------------------ |
|
|
305
|
+
| `apps/front/hr-management/src/app.d.ts` | `TS(1098)` `interface HTMLAttributes<>` | delete the empty `<>` |
|
|
306
|
+
| `libs/front/theme/types/breakpoints/index.d.ts` | `TS(1039)` an initializer in an ambient file | it holds a value, so it belongs in a `.ts` |
|
|
307
|
+
|
|
308
|
+
This is debt the migration surfaces rather than causes, and it is yours to fix: a formatter
|
|
309
|
+
cannot invent the missing syntax, and silently adding the file to `ignorePatterns` would be the
|
|
310
|
+
tool deciding to stop checking your code without telling you.
|
|
311
|
+
|
|
312
|
+
You will not have to go looking. `--init` names each file, its line and the reason, and says
|
|
313
|
+
that `pnpm run format` fails until they are fixed. It still exits 0: adoption itself worked, and
|
|
314
|
+
a module reported as un-adopted gets reverted for no reason.
|
|
315
|
+
|
|
316
|
+
### `.svelte files are not formatted yet`
|
|
317
|
+
|
|
318
|
+
Expected on the first adoption of a Svelte module. oxfmt does not bundle the Svelte compiler, it
|
|
319
|
+
loads it from your module, and `npx --yes ... --init` writes the config before any install has
|
|
320
|
+
happened. Run `pnpm install`, then `pnpm run format:fix` — both of which adoption tells you to
|
|
321
|
+
run anyway.
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
# Lint adoption cheat sheet
|
|
2
|
+
|
|
3
|
+
Migrating one module from ESLint to Oxlint, through sentinel. For the formatter see
|
|
4
|
+
[`format-adoption.md`](format-adoption.md), for TypeScript
|
|
5
|
+
[`typescript-adoption.md`](typescript-adoption.md); each is a separate role and can adopt
|
|
6
|
+
independently.
|
|
7
|
+
|
|
8
|
+
## Adopt a module in two steps
|
|
9
|
+
|
|
10
|
+
Run from the **module's own directory**. You do not need sentinel installed to run it.
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
cd apps/.../<module>
|
|
14
|
+
npx --yes @hublo/sentinel@<version> --init --preset <react|nest|node|svelte>
|
|
15
|
+
pnpm install
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Then run `pnpm run lint`.
|
|
19
|
+
|
|
20
|
+
`--init` with no target adopts **every role sentinel ships**, and for lint that is the
|
|
21
|
+
intended way round: it runs the linter's autofix, then writes the format config and formats
|
|
22
|
+
the module, so the files it generated are already in your module's style. Adopting lint
|
|
23
|
+
ALONE (`--init --lint`) leaves you to format them yourself.
|
|
24
|
+
|
|
25
|
+
`--init` fixes what a machine can fix, once. It does not loop, and it does not edit your code
|
|
26
|
+
beyond what the tools' own fixers do. What is left afterwards is real debt: see
|
|
27
|
+
[Reading the numbers](#reading-the-numbers).
|
|
28
|
+
|
|
29
|
+
> **Order matters, and `--init` with no target gets it right for you.** The linter's autofix
|
|
30
|
+
> rewrites code, and sentinel writes JSON in its own style, so **formatting has to come after
|
|
31
|
+
> linting**, never before. Run whole-module `--init` and it happens automatically: format runs
|
|
32
|
+
> LAST, over everything every role wrote. If you adopt roles one at a time, run
|
|
33
|
+
> `sentinel --init --format` after `sentinel --init --lint`, or your first `pnpm run lint`
|
|
34
|
+
> fails at a formatting step that has nothing to do with linting.
|
|
35
|
+
|
|
36
|
+
## What `--init` does
|
|
37
|
+
|
|
38
|
+
| It writes | Why |
|
|
39
|
+
| ------------------------------- | -------------------------------------------------------------------------------------- |
|
|
40
|
+
| `.oxlintrc.json` | a stub that `extends` the shipped preset, plus the ignore patterns (see below) |
|
|
41
|
+
| `lint` script | so `pnpm run lint` and the nx target cannot disagree about which linter runs |
|
|
42
|
+
| the nx `lint` target | replaced, not merged: the ESLint executor's options mean nothing to a different runner |
|
|
43
|
+
| `@hublo/sentinel` devDependency | pinned, so the preset resolves |
|
|
44
|
+
|
|
45
|
+
| It removes | Why |
|
|
46
|
+
| --------------------------------- | -------------------------------------------------------------------------------------------------- |
|
|
47
|
+
| your `eslint.config.*` | adoption REPLACES ESLint; two configured linters means whichever the runner picks wins |
|
|
48
|
+
| your module's ESLint dependencies | they served that config. The **root** keeps its tooling, which every unmigrated module still needs |
|
|
49
|
+
|
|
50
|
+
**It then runs the autofix once, and tells you what that did.** Adoption enforces rules your
|
|
51
|
+
module may have had switched off, and the fixer acts on them immediately, so this is the step
|
|
52
|
+
that can change your source:
|
|
53
|
+
|
|
54
|
+
```console
|
|
55
|
+
fixing what lint can fix automatically...
|
|
56
|
+
the lint autofix rewrote 12 files: src/a.ts, src/b.ts and 10 more. Review the diff
|
|
57
|
+
before committing: rules this module used to have switched off are enforced from now on.
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
It says `changed nothing` when there was nothing to do, and says so explicitly when the pass
|
|
61
|
+
**could not run** at all, which used to be silent: neither the exit status nor the output
|
|
62
|
+
stream distinguishes "your config did not load" from "here are your remaining violations", so
|
|
63
|
+
a module could look adopted while nothing had been checked.
|
|
64
|
+
|
|
65
|
+
It also **rewrites suppression comments** that a plugin rename would otherwise void. A hosted
|
|
66
|
+
plugin can be registered under a different name than ESLint knows it by (`@nx/eslint-plugin`
|
|
67
|
+
is hosted as `nx`), and every rule id changes with it, so `// eslint-disable @nx/...` would
|
|
68
|
+
silently stop suppressing while the findings it held back reappear. One stale comment produced
|
|
69
|
+
80 phantom warnings during the proof of concept. Same for `@next/next/*`, which oxlint ships
|
|
70
|
+
natively as `nextjs/*`.
|
|
71
|
+
|
|
72
|
+
**Import ordering is not a lint rule here.** `import/order` moved to the format role: oxfmt
|
|
73
|
+
rewrites the whole import block, so it sorts a file completely in one pass, where a lint fixer
|
|
74
|
+
emits edits over overlapping ranges and needs several. `eslint-plugin-import` is no longer
|
|
75
|
+
hosted at all, since its only other rule (`no-duplicates`) is native in oxlint.
|
|
76
|
+
|
|
77
|
+
### Why `ignorePatterns` is in your config and the rules are not
|
|
78
|
+
|
|
79
|
+
The rules stay in the preset, so changing them reaches every module by reinstalling. Ignore
|
|
80
|
+
patterns cannot work that way: oxlint does **not** inherit `ignorePatterns` through `extends`
|
|
81
|
+
(verified), so a preset declaring them gives them to nobody.
|
|
82
|
+
|
|
83
|
+
Sentinel used to compensate by passing `--ignore-pattern` flags whenever it ran oxlint. That
|
|
84
|
+
made the committed config correct only while sentinel was the thing running it. Everything
|
|
85
|
+
else read the file and linted your build output:
|
|
86
|
+
|
|
87
|
+
```console
|
|
88
|
+
$ oxlint -c .oxlintrc.json . # what your IDE effectively does
|
|
89
|
+
dist/built.js:1:1 error eslint(no-var)
|
|
90
|
+
node_modules/junk/bad.js:1:1 error eslint(no-var)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
So they are written into your module's config. Your own patterns are carried across from the
|
|
94
|
+
eslint config on top of the defaults, minus any that merely restate one (`dist/**` beside
|
|
95
|
+
`**/dist/**`).
|
|
96
|
+
|
|
97
|
+
If `--run --lint` warns that your config declares no `ignorePatterns`, it was written by an
|
|
98
|
+
older sentinel: re-run `sentinel --init --lint`.
|
|
99
|
+
|
|
100
|
+
## Commands
|
|
101
|
+
|
|
102
|
+
Every verb, for every role, plus scoping a run to some files and what a fully adopted module
|
|
103
|
+
looks like: [`using-sentinel.md`](using-sentinel.md).
|
|
104
|
+
|
|
105
|
+
## Presets, and the one you don't choose
|
|
106
|
+
|
|
107
|
+
Declare the stack: `react`, `nest`, `svelte`, or `node` (the stack-less base, for a module
|
|
108
|
+
that belongs to no stack).
|
|
109
|
+
|
|
110
|
+
**React has two tiers, and you do not pick them.** `apps/**` gets the application set, `libs/**`
|
|
111
|
+
gets the library set, because that is a structural fact of the workspace rather than a
|
|
112
|
+
judgement call. They differ by 96 rules, 61 of which are accessibility: an app shell has a
|
|
113
|
+
user to be accessible to, a component library does not, and a library instead runs rules an
|
|
114
|
+
app has no use for (`nextjs/*`, `jest-dom/*`, and the `storybook/*` rules that apply only to
|
|
115
|
+
`*.stories.*`).
|
|
116
|
+
|
|
117
|
+
Always pass `--preset` in a monorepo. Dependencies are hoisted, so detection honestly answers
|
|
118
|
+
`node` for a React app that declares no `react` of its own.
|
|
119
|
+
|
|
120
|
+
### Your module extends exactly one of them
|
|
121
|
+
|
|
122
|
+
There is nothing to stack. Every preset already contains the shared rule layer, flattened in
|
|
123
|
+
at build time, so the file you extend is self-contained:
|
|
124
|
+
|
|
125
|
+
| preset | rules | of which shared by every stack |
|
|
126
|
+
| ----------- | ----- | ------------------------------ |
|
|
127
|
+
| `react` | 188 | 21 |
|
|
128
|
+
| `react-lib` | 134 | 21 |
|
|
129
|
+
| `nest` | 110 | 21 |
|
|
130
|
+
| `svelte` | 81 | 21 |
|
|
131
|
+
| `tools` | 67 | 21 |
|
|
132
|
+
| `node` | 21 | 21 (the shared layer alone) |
|
|
133
|
+
|
|
134
|
+
The layer lives in the repo as `src/roles/lint/presets/shared.json` and is never published on
|
|
135
|
+
its own. If you are reading the source, that file is ours, not something a module names.
|
|
136
|
+
|
|
137
|
+
### If a rule you need is missing
|
|
138
|
+
|
|
139
|
+
Tell us and it goes in the preset, where every module gets it. That is the whole point of
|
|
140
|
+
sentinel: the rules were the same in module after module, each copy free to drift, and they
|
|
141
|
+
now live in one place.
|
|
142
|
+
|
|
143
|
+
If your team genuinely needs something the preset should not impose on everyone, declare it as
|
|
144
|
+
a **layer above the preset**, the way the planning team did:
|
|
145
|
+
|
|
146
|
+
```json
|
|
147
|
+
{ "extends": [".../lint/nest.json", "./team-rules.json"] }
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Order matters, sentinel's preset first, or your layer cannot override anything. `--inspect`
|
|
151
|
+
reports the layer so it stays visible.
|
|
152
|
+
|
|
153
|
+
Think of it as a queue rather than a fork: a rule promoted into the preset is deleted from the
|
|
154
|
+
layer. It is the exact mirror of `.oxlintrc.baseline.json`, which holds rules **below** the
|
|
155
|
+
preset and empties as debt is fixed, while a layer holds rules **above** it and empties as they
|
|
156
|
+
are adopted.
|
|
157
|
+
|
|
158
|
+
**Wire your layer before your first `--init`, not after.** `--init` prefixes the preset onto
|
|
159
|
+
whatever `extends` it finds, so a layer already there survives and is respected by the autofix
|
|
160
|
+
that follows. Added afterwards, the autofix has already run, and nothing undoes what it
|
|
161
|
+
rewrote. One team met this as twelve rewritten files.
|
|
162
|
+
|
|
163
|
+
## What a preset does not enforce
|
|
164
|
+
|
|
165
|
+
Every unenforced rule carries a reason, readable with `sentinel --inspect --lint`:
|
|
166
|
+
|
|
167
|
+
| State | Meaning |
|
|
168
|
+
| ----------------------------- | ------------------------------------------------------------------------------ |
|
|
169
|
+
| **not enforced, by decision** | it argues with a deliberate architectural choice, or only false-positives here |
|
|
170
|
+
| **reported, not blocking** | it runs and reports; it just cannot fail a build, so the debt stays visible |
|
|
171
|
+
| **uncovered** | oxlint cannot run it at all, so it can be neither enforced nor switched off |
|
|
172
|
+
|
|
173
|
+
`uncovered` is the one to read before adopting, because it is the only state where a check you
|
|
174
|
+
had simply stops existing. The clearest case is `no-restricted-syntax`: oxlint has no such
|
|
175
|
+
rule, so AST-selector guards cannot be carried across as configuration. This repo's eight
|
|
176
|
+
frontend selectors were rewritten as real rules in sentinel's own `hublo` plugin, which the
|
|
177
|
+
React tiers host. A Nest service that had architectural guards of its own has nowhere for them
|
|
178
|
+
to land yet, and one adopter lost two that way before noticing.
|
|
179
|
+
|
|
180
|
+
If that is your module, tell us which guards you rely on and they ship in the `hublo` plugin
|
|
181
|
+
alongside the frontend ones. A house rule copied per module drifts, which is what sentinel
|
|
182
|
+
exists to stop.
|
|
183
|
+
|
|
184
|
+
The governing rule for both: **adoption never turns a green module red.** A rule that would
|
|
185
|
+
newly fail is held at `warn`, visibly and with its reason, even when the previous
|
|
186
|
+
configuration was wrong or was not really running. You were green on Tuesday; you do not
|
|
187
|
+
arrive to a red build on Wednesday for code that was already there.
|
|
188
|
+
|
|
189
|
+
### How that promise is kept, since 1.1.1
|
|
190
|
+
|
|
191
|
+
Two mechanisms, and the second exists because the first is not enough on its own.
|
|
192
|
+
|
|
193
|
+
The **preset** holds back rules measured as newly-failing across the repository. That is
|
|
194
|
+
correct on the day it is measured and decays from then on: the repository keeps moving, and a
|
|
195
|
+
commit landing afterwards can violate a rule the preset recorded as clean. The failure then
|
|
196
|
+
arrives for whoever adopts LAST, on code they did not write. It happened: `host-admin` adopted
|
|
197
|
+
green on 1.1.0 and red a week later, on `jsx-a11y/no-noninteractive-tabindex` and
|
|
198
|
+
`typescript/no-duplicate-type-constituents`, neither of which the repo's ESLint had ever
|
|
199
|
+
enforced.
|
|
200
|
+
|
|
201
|
+
So `--init` also measures **your module**, after the autofix pass, with type information, and
|
|
202
|
+
writes what it already violates to `.oxlintrc.baseline.json`:
|
|
203
|
+
|
|
204
|
+
```jsonc
|
|
205
|
+
{
|
|
206
|
+
"rules": {
|
|
207
|
+
// 1 violation when this module adopted
|
|
208
|
+
"jsx-a11y/no-noninteractive-tabindex": "warn",
|
|
209
|
+
},
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Your `.oxlintrc.json` extends it after the preset, which is what lowers the severity. Four
|
|
214
|
+
things follow, and they are the point:
|
|
215
|
+
|
|
216
|
+
- the rules still **run and still report**, they just cannot fail the build for code that
|
|
217
|
+
predates the migration. A hold is never `off`;
|
|
218
|
+
- the file is **regenerated, not accumulated**. Fix the last violation, re-run
|
|
219
|
+
`sentinel --init --lint`, and the rule drops out and returns to the preset severity;
|
|
220
|
+
- your own config stays `extends` + `ignorePatterns`, so a rule **you** change by hand is
|
|
221
|
+
still reported as drift and is never confused with a hold sentinel made;
|
|
222
|
+
- `--init` says what it held and how many violations each hold covers, so the debt is
|
|
223
|
+
announced rather than discovered.
|
|
224
|
+
|
|
225
|
+
Do not edit `.oxlintrc.baseline.json` by hand. Commit it: it is part of the adoption, and it
|
|
226
|
+
is what makes the promise above true on the day you adopt rather than on the day the preset
|
|
227
|
+
was built.
|
|
228
|
+
|
|
229
|
+
A small number of rules cannot be carried at any severity, because oxlint validates rule and
|
|
230
|
+
plugin names when it _parses_ a config: naming one it does not know makes the config fail to
|
|
231
|
+
parse and the module lints **nothing**. Those are documented in the README rather than
|
|
232
|
+
silently dropped.
|
|
233
|
+
|
|
234
|
+
## Ask the linter your own question
|
|
235
|
+
|
|
236
|
+
Everything after `--` goes to oxlint:
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
sentinel --run --lint -- --deny-warnings # what would zero warnings take?
|
|
240
|
+
sentinel --run --lint -- --max-warnings 100 # hold a ceiling while you pay debt down
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
A run with extra options says so on every line of output and carries `toolArgs` in the
|
|
244
|
+
`--json` envelope, because these options can weaken a check as easily as strengthen it, and
|
|
245
|
+
`--run` is what CI calls. Never put one in a committed `lint` script.
|
|
246
|
+
|
|
247
|
+
## Reading the numbers
|
|
248
|
+
|
|
249
|
+
```console
|
|
250
|
+
$ sentinel --run --lint --json --max-diagnostics 2
|
|
251
|
+
{
|
|
252
|
+
"results": [
|
|
253
|
+
{
|
|
254
|
+
"project": "hr-management",
|
|
255
|
+
"adopted": true,
|
|
256
|
+
"errors": 0,
|
|
257
|
+
"warnings": 72,
|
|
258
|
+
"rules": [
|
|
259
|
+
{ "rule": "no-explicit-any", "severity": "warning", "count": 54 },
|
|
260
|
+
{ "rule": "no-unused-vars", "severity": "warning", "count": 15 }
|
|
261
|
+
],
|
|
262
|
+
"diagnostics": [{ "file": "src/…/index.ts", "line": 91, "col": 37, "rule": "no-explicit-any", … }],
|
|
263
|
+
"diagnosticsTruncated": true
|
|
264
|
+
}
|
|
265
|
+
]
|
|
266
|
+
}
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
The `rules` breakdown is the useful part: a total tells you a module is dirty, a per-rule
|
|
270
|
+
count tells you which rule to fix first, and across the fleet, which rule to fix everywhere.
|
|
271
|
+
The numbers come from oxlint's own JSON, not from parsing what it prints for humans.
|
|
272
|
+
|
|
273
|
+
## Troubleshooting
|
|
274
|
+
|
|
275
|
+
| Symptom | Cause and fix |
|
|
276
|
+
| ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
277
|
+
| `pnpm run lint` fails on **Prettier** right after adopting | you adopted lint alone, so the files sentinel generated are not in your module's format. Run `sentinel --init --format` (or your formatter) on them |
|
|
278
|
+
| `Plugin 'x' not found` / `extends … does not exist` | the preset path assumes pnpm's isolated layout. Run `pnpm install` in the module; if the workspace switched to a hoisted linker, that breaks every adopted module at once |
|
|
279
|
+
| A file that was never linted now reports errors | your ESLint config ignored it. `--init` carries your module's `ignores` across, but it **cannot read a computed one** (e.g. `includeIgnoreFile(gitignorePath)`) and says so when it hits one. Add what you need to `ignorePatterns` |
|
|
280
|
+
| `error: Unexpected token` in a `.svelte` file | oxlint reads a `<script>` block as JavaScript unless it declares `lang="ts"`. Add it, or ignore the file |
|
|
281
|
+
| Type-aware rules seem not to run | they need `oxlint-tsgolint`, which ships with sentinel. `sentinel --inspect --lint` reports `typeAware` so you can check rather than assume |
|
|
282
|
+
| A `eslint-disable` comment stopped working | its rule was renamed by a plugin alias. `--init` rewrites the ones it knows; `--inspect` lists the renames a preset causes |
|
|
283
|
+
|
|
284
|
+
## Svelte, specifically
|
|
285
|
+
|
|
286
|
+
Oxlint parses a `.svelte` file's `<script>` block and **not its template**. Fourteen
|
|
287
|
+
`svelte/*` rules therefore cannot run at all, whatever the config says, including
|
|
288
|
+
`svelte/no-at-html-tags`, an XSS guard that fires on the modules today. Adding a Svelte
|
|
289
|
+
plugin does not help: the missing piece is the parser. This is documented rather than hidden,
|
|
290
|
+
and it is the honest cost of the migration for those two modules.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Performance baseline
|
|
2
|
+
|
|
3
|
+
Numbers so a regression is something you can see rather than argue about. Reproduce with:
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pnpm build && pnpm bench <workspace-root>
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Recorded against the Hublo monorepo (**441 nx projects**, ~2900 source files in the largest
|
|
10
|
+
module) on an Apple Silicon laptop, sentinel `1.1.0-alpha.4`. Machine, disk and cache state all
|
|
11
|
+
move these, so compare **ratios to this table**, not absolute milliseconds. A number that
|
|
12
|
+
doubles is a finding; a number that moves 30% is a different laptop.
|
|
13
|
+
|
|
14
|
+
| What | Baseline | Why it is measured |
|
|
15
|
+
| ------------------------------------------------- | ----------: | ----------------------------------------------------------------------------------------------------------- |
|
|
16
|
+
| `--version` | **33 ms** | the floor under every command. If this grows, everything grows |
|
|
17
|
+
| `--inspect --lint` in one module | **35 ms** | sentinel's own overhead: config reads, no tool spawned |
|
|
18
|
+
| `--inspect --lint --json` across the workspace | **1300 ms** | what the migration status page runs, and the only command whose cost grows with the repo. ~3 ms per project |
|
|
19
|
+
| `--run --format` (144 files, sentinel's own repo) | **291 ms** | oxfmt doing real work |
|
|
20
|
+
| `--run --lint` (one module) | **~1.3 s** | dominated by oxlint, including its type-aware pass |
|
|
21
|
+
|
|
22
|
+
## Measured separately
|
|
23
|
+
|
|
24
|
+
**The source probe** (`hasSourceFiles`, which decides whether a role has anything to do) is not
|
|
25
|
+
in `pnpm bench`: timing it would mean exporting it from the package's public entry point, and
|
|
26
|
+
widening the published surface to benchmark something is the wrong trade. Measured directly:
|
|
27
|
+
|
|
28
|
+
| | |
|
|
29
|
+
| --------------------------------------------------- | ----------: |
|
|
30
|
+
| `apps/front/host-admin` (4210 files), match found | **0.11 ms** |
|
|
31
|
+
| `libs/api-types` (2001 files), match found | **0.44 ms** |
|
|
32
|
+
| worst case: exhaust a 2001-file tree, match nothing | **47 ms** |
|
|
33
|
+
|
|
34
|
+
It stops at the first match and never descends into `node_modules` or build output, which is
|
|
35
|
+
where the difference comes from: a `**` glob over the same tree measured **515 ms** against
|
|
36
|
+
**2 ms** bounded.
|
|
37
|
+
|
|
38
|
+
## What is deliberately not optimised
|
|
39
|
+
|
|
40
|
+
`readOwnPackage()` walks up the filesystem and re-parses sentinel's own manifest at each of its
|
|
41
|
+
call sites, with no memo. At this scale it is invisible, and a module-level cache is a footgun
|
|
42
|
+
in tests. If the whole-workspace scan ever becomes a problem, measure before assuming this is
|
|
43
|
+
why.
|
|
44
|
+
|
|
45
|
+
## If a number regresses
|
|
46
|
+
|
|
47
|
+
Take the ratio, not the difference. The scan is the one to watch: it is per-project work, so a
|
|
48
|
+
change that adds one file read per module costs 441 of them. The probe is the other, because it
|
|
49
|
+
runs on every `--init` and its worst case is unbounded by anything except the tree it walks.
|