@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,175 @@
1
+ # Test adoption cheat sheet
2
+
3
+ Sentinel owns the test toolchain so a module can be tested on its own, and so the workspace can
4
+ drop Jest before Nx v24 removes `@nx/jest:jest`.
5
+
6
+ This page is written from measurement. Every number in it was taken on this monorepo, and the ones
7
+ that decided a design choice say which choice.
8
+
9
+ - [What the ground actually looks like](#what-the-ground-actually-looks-like)
10
+ - [The one line that makes a per-module migration possible](#the-one-line-that-makes-a-per-module-migration-possible)
11
+ - [What `--init --test` rewrites, and what it refuses](#what---init---test-rewrites-and-what-it-refuses)
12
+ - [Nest services](#nest-services)
13
+ - [The check that decides whether you are done](#the-check-that-decides-whether-you-are-done)
14
+
15
+ ## What the ground actually looks like
16
+
17
+ | | |
18
+ | -------------------------------------------- | ---------------------------------------------- |
19
+ | test targets on `@nx/jest:jest` | **107**, the executor Nx v24 removes |
20
+ | on `@nx/vitest:test` | 3, all front, target named `test` |
21
+ | target `vitest`, inferred from an npm script | 2, both Svelte |
22
+ | jest configs | 108, of which **107 extend one shared preset** |
23
+ | environment | 102 `node`, 5 `jsdom` |
24
+ | test files | 6397, of which **3481 call `jest.*`** |
25
+ | files importing `jest-mock-extended` | **2491** |
26
+
27
+ Two consequences worth stating before anything else.
28
+
29
+ **The target is named `test` everywhere.** 107 modules already call it that, and so do the three
30
+ fronts already on Vitest. The two Svelte modules call it `vitest` only because that is the name of
31
+ an npm script, which nx turned into a target. Adopting under `test` means **CI never changes during
32
+ the migration**: a module on Jest and a module on Vitest both answer `nx affected -t test`.
33
+
34
+ **Tests are barely declared in `package.json` today.** Zero test targets in an `nx` block, four
35
+ `test` scripts in the whole repo. Where the build role usually rewired a script that existed, this
36
+ one creates it.
37
+
38
+ ## Which targets are in scope, decided by one question
39
+
40
+ Not "which directory", because tests live wherever their authors put them. The question is whether
41
+ a target **breaks when Jest is removed**, and that is measurable:
42
+
43
+ | target | how many | what runs it | in scope |
44
+ | ------------------------------- | -------: | ------------------------------------------------ | -------- |
45
+ | `test` | 107 | `@nx/jest:jest` | **yes** |
46
+ | `test-integration` | 3 | jest, 14 files call the jest API | **yes** |
47
+ | `test-acceptance` and `-legacy` | 3 | `nx:run-commands`, 15 files call the jest API | **yes** |
48
+ | `test-prisma` | 1 | `@nx/jest:jest` with `jest.prisma.config.ts` | **yes** |
49
+ | `test-functional` | 1 | `@nx/jest:jest` with `jest.functional.config.ts` | **yes** |
50
+ | `stress-test` | 2 | `zx` scripts | no |
51
+ | `loadtests` | 2 | `k6` | no |
52
+
53
+ The last two never load Jest, so removing it changes nothing for them. Everything else does, which
54
+ also means the migration has more than one config per module to answer for: `jest.prisma.config.ts`
55
+ and `jest.functional.config.ts` are targets of their own.
56
+
57
+ ## A module with no tests is cleaned, not configured
58
+
59
+ Six Nest services have **zero test files** and a green `test` target, because `passWithNoTests: true`
60
+ makes an empty run a success. Writing them a Vitest config would be furniture for a room nobody
61
+ enters.
62
+
63
+ What they need is the opposite: their Jest leftovers removed, so nothing breaks the day Jest leaves
64
+ the workspace. The Jest config file, the Jest dependencies, and nothing else. No new config, no new
65
+ script, no change to what they build or ship.
66
+
67
+ ## The one line that makes a per-module migration possible
68
+
69
+ **104 files under `libs/` import `jest-mock-extended`.** They are shared test helpers, used by
70
+ modules on both runners. That package loads `@jest/globals`, which refuses to run outside Jest, so:
71
+
72
+ - migrating them breaks every module still on Jest
73
+ - leaving them breaks every module moved to Vitest
74
+
75
+ Old and new do **not** cohabit on shared helpers, and that alone would force a big-bang migration.
76
+
77
+ ```ts
78
+ { find: /^jest-mock-extended$/, replacement: 'vitest-mock-extended' }
79
+ ```
80
+
81
+ `vitest-mock-extended` is a fork of the Jest package and exports the same four names this repo uses
82
+ (`mockDeep`, `mock`, `DeepMockProxy`, `MockProxy`), so an unmigrated helper resolves to the Vitest
83
+ fork **inside an adopted module** and keeps resolving to the Jest one everywhere else. Measured on
84
+ `mission`: failing suites went from **159 to 10**.
85
+
86
+ A helper that uses the raw Jest API (`jest.fn()`) cannot be aliased, because that is a global rather
87
+ than an import. There is one such helper on the Nest side, consumed by 6 modules. Sentinel ships the
88
+ Vitest equivalent and an adopted module imports it from here, the same answer as the build role's
89
+ toolchain: the tool owns the shared piece, so the monorepo keeps one copy per runner and no
90
+ ordering constraint.
91
+
92
+ ## What `--init --test` rewrites, and what it refuses
93
+
94
+ This role rewrites **test files**, not only configuration, and says so before it writes anything. A
95
+ config-only adoption would leave the module red, so it would be worth nothing.
96
+
97
+ | rule | files in this repo |
98
+ | ------------------------------------------------------------------------------------------------------------------- | ------------------ |
99
+ | `jest-mock-extended` → `vitest-mock-extended` | 2491 |
100
+ | `jest.*` → `vi.*` | 3481 |
101
+ | type references (`jest.Mock`, `jest.Mocked`, `jest.SpyInstance` → `MockInstance`, …) become an import from `vitest` | 744 occurrences |
102
+ | `jest.requireActual` → `await vi.importActual`, factory becomes `async` | 26 |
103
+ | `jest.requireMock` → the mocked module imported directly, since `vi.mock` makes a static import the mock | 16 |
104
+ | `jest.setTimeout(n)` → `vi.setConfig({ testTimeout: n })` | 4 |
105
+ | a mock factory returning a non-object → `{ default: … }`, which Vitest requires | 2 |
106
+ | `jest.isolateModules` → `vi.resetModules()` + `await import(...)` | 1 |
107
+
108
+ **Selection is on API USE, never on file name.** Test helpers carry the same API as specs:
109
+ `*.mock.ts`, `__mocks__/`, wrappers, fixtures. Ninety files in this repo are outside any spec glob.
110
+
111
+ **What it refuses is fenced off before any global rewrite.** The first version of this codemod
112
+ reported leaving two files alone and rewrote them anyway, producing `vi.requireMock`, which does not
113
+ exist. Announcing a refusal and performing the change is worse than either.
114
+
115
+ ## Nest services
116
+
117
+ The wall is decorator metadata: Nest resolves a constructor's parameters from
118
+ `emitDecoratorMetadata`, and esbuild does not emit it. The build role's `decoratorMetadata` is a
119
+ **Vite plugin**, so Vitest loads it unchanged, with the two compile modes already copied from nx.
120
+
121
+ ```ts
122
+ plugins: [decoratorMetadata({ root: here })],
123
+ resolve: { alias: [{ find: /^jest-mock-extended$/, replacement: 'vitest-mock-extended' },
124
+ ...tsconfigAliases(workspaceRoot)] },
125
+ test: { globals: true, environment: 'node', isolate: true },
126
+ ```
127
+
128
+ **`isolate` stays `true`**, whatever the performance note in Vitest's docs suggests. Jest gives each
129
+ test file a fresh module registry and `isolate: true` reproduces that. Nest registers metadata as an
130
+ import SIDE EFFECT, a decorator writing into a catalog when its module loads, so a shared registry
131
+ would make results depend on the order files ran in. That is the same fact the build role records
132
+ next to `treeshake: { moduleSideEffects: true }`.
133
+
134
+ ## The check that decides whether you are done
135
+
136
+ Not "the suite is green". **The suite gives the same result as before**, compared against a baseline
137
+ taken BEFORE the migration:
138
+
139
+ ```console
140
+ $ nx run <module>:test # jest, and write the numbers down
141
+ $ sentinel --init --test
142
+ $ nx run <module>:test # vitest
143
+ ```
144
+
145
+ Take the baseline first, and compare the failing SETS rather than the counts. On `mission` the Jest
146
+ baseline was not green (10 suites failed on `setTimeout is not defined`), and all ten pass under
147
+ Vitest, so a count comparison would have read as a regression where there was an improvement.
148
+
149
+ A codemod touching thousands of files cannot be reviewed by hand. The suite against its baseline is
150
+ the review.
151
+
152
+ ### And the typecheck, which the suite cannot stand in for
153
+
154
+ ```console
155
+ $ nx run <module>:typecheck # BEFORE, and write that down too
156
+ $ sentinel --init --test
157
+ $ nx run <module>:typecheck # after
158
+ ```
159
+
160
+ ⚠️ **A green suite does not mean a green module.** The migration rewrites TYPES as well as calls,
161
+ and a wrong type is invisible to a run: the tests never evaluate it.
162
+
163
+ Measured on `apps/nest/microservices/agency`, by someone adopting the published package with no
164
+ knowledge of how it was built:
165
+
166
+ | | before | after |
167
+ | -------------------- | ------------ | ------------- |
168
+ | `--run --test` | 534 green | 534 green |
169
+ | `--run --typescript` | **0 errors** | **10 errors** |
170
+
171
+ All ten came from two wrong mappings, both since fixed. The point is not those ten. It is that a
172
+ module can be migrated, pass its whole suite, and carry a broken typecheck into main, where the
173
+ next CI run of a different target finds it.
174
+
175
+ Take both baselines. A migration is done when both come back the same.
@@ -0,0 +1,184 @@
1
+ # TypeScript adoption cheat sheet
2
+
3
+ A quick reference for adopting the sentinel TypeScript preset on a module, and for the everyday `--run` / `--inspect` commands. For the full, live command-and-output traces, see [`typescript-traces.md`](typescript-traces.md).
4
+
5
+ ## Adopt a module in two steps
6
+
7
+ Run from the **module's own directory**. You do not need to install sentinel to run it: use `npx --yes` for a one-off.
8
+
9
+ ```bash
10
+ # 1. write the module's config + the pinned devDependency, AND the workspace prep it needs
11
+ cd apps/.../<module>
12
+ npx --yes @hublo/sentinel@<version> --init --typescript --preset <react|nest|node>
13
+
14
+ # 2. fetch what step 1 declared
15
+ pnpm install
16
+ ```
17
+
18
+ `--init` writes the module's `tsconfig` stub, its `typecheck` script, and pins `@hublo/sentinel@<version>` into the module's own `devDependencies` (so there is no manual `pnpm add`). It composes `extends: [<your base>, <preset>]` (keeping the monorepo's `paths` and structure), strips the options the preset owns (see below), and scaffolds a minimal `package.json` for a `project.json`-only module. Svelte is skipped (no preset yet). Always pass `--preset` in a monorepo: dependencies are hoisted, so detection can fall back to `node`.
19
+
20
+ **`--init` also applies the workspace prep the module needs**, the one place it reaches the workspace root, because "set this module up" is not honest if the workspace then cannot install or resolve it. It is defensive: each fix is applied only when the root's own config shows it is needed, and is a no-op otherwise.
21
+
22
+ - **release-age allow-list**: if the root uses pnpm's `minimumReleaseAgeExclude`, a freshly published sentinel is allow-listed so it installs.
23
+ - **i18next singleton override**: if the root declares a second TypeScript (via `@typescript/native`), i18next is pinned to one instance so a re-resolve does not fork react-i18next and break translation-singleton tests. The pin tracks the declared version and disappears once the migration removes the second TypeScript.
24
+
25
+ > **Renamed:** the option is `--preset`. `--flavour` was the original spelling and still
26
+ > works, so nothing you have already written breaks, but it prints a deprecation notice and
27
+ > will not be documented beyond this line.
28
+
29
+ ## Verbs at a glance
30
+
31
+ | Verb | Who / when | Runs the tool? | What you get |
32
+ | ----------- | ------------------ | ----------------- | --------------------------------------------------------------------------- |
33
+ | `--run` | dev, everyday | **yes** (tsc) | executes the check, pass/fail. This is your `typecheck` script. |
34
+ | `--inspect` | dev, everyday | no (reads config) | the resolved config: preset, target, and which rules are deferred |
35
+ | `--init` | dev, once to adopt | writes files | the tsconfig stub, the `typecheck` script, the pinned dep, + workspace prep |
36
+ | `--migrate` | planned | not available yet | reserved for changing an already-initialized setup (later ticket) |
37
+ | `--report` | **deprecated** | — | use `--run --json`: same work, same envelope |
38
+ | `--status` | **deprecated** | — | use `--inspect`, which absorbed it |
39
+
40
+ **`--run` vs `--inspect`:** run _executes_ the check and gives you pass/fail plus the numbers (`--json` for error counts and structured diagnostics); inspect _reads_ the config and shows what applies, instantly and without running the tool, including adoption and conformity across modules.
41
+
42
+ > **Deprecated, still working:** `--report` and `--status` were separate verbs and folded into
43
+ > the two above, because they answered the same two questions in a different shape. Both still
44
+ > run and print a notice telling you what to use instead.
45
+
46
+ **Location** (any verb): from a module dir → that module; from the root → `--module <name>`, `--ci` (affected), or all modules. `--init` writes, so it targets one module only.
47
+
48
+ **Your tsconfig extends exactly one preset.** The options every stack shares are flattened into
49
+ each published preset at build time, so `@hublo/sentinel/tsconfig/react` is self-contained and
50
+ there is nothing to stack under it. The shared layer lives in the repo as
51
+ `src/roles/typescript/presets/shared.json`; it is ours and is never published on its own.
52
+
53
+ ## Everyday use (developers)
54
+
55
+ ```bash
56
+ sentinel --run --typescript # in a module dir: type-check it (your `typecheck` script)
57
+ sentinel --inspect --typescript # what config/preset applies here, and what's deferred
58
+ sentinel --init --typescript --preset nest # adopt this module (writes the stub)
59
+ ```
60
+
61
+ ## CI & reporting
62
+
63
+ ```bash
64
+ sentinel --run --typescript --ci # from the root: check affected modules, non-zero exit on failure
65
+ sentinel --run --typescript --json # machine-readable metrics + diagnostics for a dashboard
66
+ sentinel --inspect --typescript # adoption + conformity coverage across the workspace
67
+ ```
68
+
69
+ ## Options
70
+
71
+ | Option | What it does |
72
+ | ----------------------- | ----------------------------------------------------------------------------------- |
73
+ | `--preset <name>` | override detection (`react` / `nest` / `svelte` / `node`); recommended for `--init` |
74
+ | `--module <name>` | from the root, scope to one module |
75
+ | `--ci` | from the root, only affected modules; non-zero exit on failure |
76
+ | `--json` | machine-readable envelope on stdout, for every verb; progress goes to stderr |
77
+ | `--max-diagnostics <n>` | cap the diagnostics embedded per module in `--run --json` (`0` = no cap) |
78
+ | `--dry-run` | preview what `--init` would write, changing nothing |
79
+ | `--runner <tool>` | override the default runner |
80
+ | `-- <tool options>` | `--run` only, one type named: everything after `--` goes to tsc itself (see below) |
81
+
82
+ ## Measuring a deferred rule
83
+
84
+ `--inspect` tells you a rule is deferred. That is honest, and it is not the question you
85
+ actually have, which is how much debt is behind it. Everything after `--` is handed to tsc,
86
+ so you can ask directly:
87
+
88
+ ```console
89
+ $ sentinel --run --typescript -- --noImplicitAny
90
+ sentinel: NOT the standard check — passing --noImplicitAny to the typescript tool.
91
+ src/…/bulk-update-profile-exclusions-request.dto.ts(77,16): error TS7006: Parameter 'item' implicitly has an 'any' type.
92
+ …
93
+ ```
94
+
95
+ Three things worth knowing:
96
+
97
+ - **The result is not your normal check**, and it says so on every run, plus `toolArgs` in
98
+ the `--json` envelope. These options can weaken a check as easily as strengthen it, and
99
+ `--run` is what CI calls, so never put one in a committed `typecheck` script.
100
+ - **Emit is forced off.** This asks a question about the code, it does not build, so it will
101
+ not touch your build output or `.tsbuildinfo`.
102
+ - **It checks the projects your `tsconfig.json` references**, not the solution file itself,
103
+ which holds no files. Build mode cannot serve this at all: it refuses compiler options
104
+ outright (`error TS5094: Compiler option '--noImplicitAny' may not be used with '--build'`).
105
+
106
+ ## Reading the numbers
107
+
108
+ A plain `--run` streams tsc's own output, so you read the errors themselves, and the summary
109
+ row is just the pass/fail mark. The counts are in the machine envelope:
110
+
111
+ ```console
112
+ $ sentinel --run --typescript --json --max-diagnostics 1
113
+ {
114
+ "schemaVersion": 2,
115
+ "results": [
116
+ {
117
+ "project": "network",
118
+ "target": "typescript",
119
+ "flavour": "node",
120
+ "ok": false,
121
+ "status": "failed",
122
+ "errors": 9,
123
+ "implicitAny": "deferred",
124
+ "diagnostics": [
125
+ { "file": "src/…/contractual-information.pg.repository.ts", "line": 133, "col": 21,
126
+ "code": "TS2339", "message": "Property 'closedAt' does not exist on type …" }
127
+ ],
128
+ "diagnosticsTruncated": true
129
+ }
130
+ ]
131
+ }
132
+ ```
133
+
134
+ - `errors` is the authoritative type-error count; `diagnostics` carries each one
135
+ (`{file, line, col, code, message}`), capped by `--max-diagnostics`, with
136
+ `diagnosticsTruncated` saying when the cap dropped some, so nothing is hidden silently.
137
+ - `implicitAny: "deferred"` means the rule is off in phase 1 (non-breaking adoption),
138
+ reported honestly instead of a misleading `0`. To find out what it is hiding, see
139
+ [Measuring a deferred rule](#measuring-a-deferred-rule).
140
+ - `flavour` is the stack (`node` here). The key kept its old name on purpose: `preset`
141
+ already means the preset PATH in this shape, and the migration status page reads
142
+ `flavour`.
143
+
144
+ ## Troubleshooting
145
+
146
+ | Symptom | Cause and fix |
147
+ | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
148
+ | `target(s) not available yet: --test` | only wired types run; `--help` lists what is available now vs planned |
149
+ | `preset is ambiguous, detected "react" ... also found sails` | a mixed stack; pass `--preset` to be explicit |
150
+ | `sentinel requires Node >= 20.12` | run sentinel with a modern Node via `fnm`/`nvm`; the project's own Node is untouched |
151
+ | fresh version will not install (`minimumReleaseAge`) | `--init` allow-lists sentinel automatically when the root uses the gate; if you adopted by hand, add the package to `minimumReleaseAgeExclude` (or `--config.minimumReleaseAge=0`) until it ages |
152
+ | a translation-singleton test goes red after adopting a front module | `--init` handles this automatically when the root runs more than one TypeScript: adopting re-resolves pnpm and can duplicate a package with an optional, type-only `typescript` peer (e.g. i18next); `--init` pins that peer so it collapses to one instance |
153
+
154
+ ## Phase 1 is non-breaking
155
+
156
+ Adoption is a lateral move: `noImplicitAny` / `noUnusedLocals` / `noUnusedParameters` are deferred (a warning says so; `--inspect` lists them). Enabling them centrally in a later wave is the migration.
157
+
158
+ ## What `--init` strips, and what it leaves alone
159
+
160
+ Your tsconfig ends up extending **two** things: your workspace base, then the sentinel preset. That is what decides which of your `compilerOptions` survive.
161
+
162
+ | Your option | What happens | Why |
163
+ | ----------------------------------------------------------------------------- | ---------------------- | ------------------------------------------------------------- |
164
+ | the preset sets it (`strict`, `jsx`, `module`, …) | **stripped** | the preset's value governs, which is the point of adopting it |
165
+ | on the allowlist (`paths`, `baseUrl`, `rootDir`, `outDir`, `tsBuildInfoFile`) | kept, silently | genuinely project-specific, no preset can know them |
166
+ | the preset does **not** set it | **kept, and reported** | see below |
167
+
168
+ That last row exists because of a real breakage. `libs/nest/swagger-typescript-types` had:
169
+
170
+ ```jsonc
171
+ { "extends": "../../../tsconfig.base.json", "compilerOptions": { "verbatimModuleSyntax": false } } // deliberately opting OUT
172
+ ```
173
+
174
+ The workspace base sets `verbatimModuleSyntax: true`; the sentinel preset does not set it at all. Removing that line therefore did not hand the decision to the preset, it handed it back to the base: the option switched on and nine files stopped compiling, while `--init` reported success. Across the Hublo monorepo, **205 tsconfigs** set an option this way, mostly `importHelpers: false`.
175
+
176
+ So an option the preset has no opinion on is **yours**, not drift. `--init` keeps it and says so:
177
+
178
+ ```
179
+ KEPT this module's own compilerOptions (verbatimModuleSyntax): the preset does not set them,
180
+ so they are yours rather than drift. Removing them would not hand the decision to the preset,
181
+ it would hand it to the workspace tsconfig this module extends
182
+ ```
183
+
184
+ It is not reported as drift by `--inspect` either, so a module that legitimately overrides the base does not read as non-conformant forever.