@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.
- package/README.md +8 -4
- package/dist/bin/sentinel.d.ts +1 -1
- package/dist/bin/sentinel.js +22 -9
- package/dist/chunk-2XLX6PFR.js +132 -0
- package/dist/chunk-2XLX6PFR.js.map +1 -0
- package/dist/chunk-3TDUIKVQ.js +178 -0
- package/dist/chunk-3TDUIKVQ.js.map +1 -0
- package/dist/chunk-CPCUPK4J.js +70 -0
- package/dist/chunk-CPCUPK4J.js.map +1 -0
- package/dist/chunk-CRKUEP4J.js +427 -0
- package/dist/chunk-CRKUEP4J.js.map +1 -0
- package/dist/{chunk-676GBPMS.js → chunk-L7WS36XV.js} +3921 -556
- package/dist/chunk-PWV3BMDA.js +15 -0
- package/dist/chunk-PWV3BMDA.js.map +1 -0
- package/dist/chunk-WLFE5RUU.js +264 -0
- package/dist/chunk-WLFE5RUU.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +2 -2
- 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 +9 -0
- package/dist/roles/test/nest/toolchain.js +25 -0
- package/dist/roles/test/nest/toolchain.js.map +1 -0
- package/dist/roles/test/react/toolchain.d.ts +66 -0
- package/dist/roles/test/react/toolchain.js +14 -1
- package/dist/roles/test/react/toolchain.js.map +1 -0
- package/dist/roles/test/setup/jest-parity.d.ts +2 -0
- package/dist/roles/test/setup/jest-parity.js +159 -0
- package/dist/roles/test/setup/jest-parity.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/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/roles/test/shared-test-config.d.ts +32 -0
- package/dist/roles/test/shared-test-config.js +8 -0
- package/dist/roles/test/shared-test-config.js.map +1 -0
- package/dist/roles/test/tools/msw.d.ts +1 -0
- package/dist/roles/test/tools/msw.js +3 -0
- package/dist/roles/test/tools/msw.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 +219 -0
- package/docs/typescript-adoption.md +184 -0
- package/docs/typescript-traces.md +798 -0
- package/docs/using-sentinel.md +195 -0
- package/docs/validating-a-change.md +101 -0
- package/package.json +35 -6
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
# Build adoption cheat sheet
|
|
2
|
+
|
|
3
|
+
Sentinel owns the Vite toolchain so a module can be built, moved and reasoned about on its own.
|
|
4
|
+
|
|
5
|
+
There are two families, and they get two different answers, from one rule: **sentinel provides
|
|
6
|
+
the FORM where the form is shared, and the PIECES where it is not.**
|
|
7
|
+
|
|
8
|
+
| | React apps | Nest services |
|
|
9
|
+
| ---------------------- | ------------------------------------- | ---------------------------------------------- |
|
|
10
|
+
| what sentinel gives | the packages, re-exported | the whole config, as `nestService()` |
|
|
11
|
+
| what your config keeps | everything below the imports | four lines |
|
|
12
|
+
| why | 4 apps, 4 genuinely different configs | 38 services, one shared 25-line webpack config |
|
|
13
|
+
|
|
14
|
+
Read [React](#react-apps) or [Nest](#nest-services). A SvelteKit module is declined with the
|
|
15
|
+
versions that would change that, not with a flat refusal: see
|
|
16
|
+
[below](#svelte-modules-and-what-they-need-first).
|
|
17
|
+
|
|
18
|
+
# React apps
|
|
19
|
+
|
|
20
|
+
Today `vite`, `@vitejs/plugin-react`, `@tailwindcss/vite`, `vite-plugin-svgr` and `nitro` sit in
|
|
21
|
+
the monorepo's **root** `package.json`, which is what makes a self-contained front app
|
|
22
|
+
impossible.
|
|
23
|
+
|
|
24
|
+
## Adopt a module
|
|
25
|
+
|
|
26
|
+
```console
|
|
27
|
+
$ pnpm add -D @hublo/sentinel
|
|
28
|
+
$ sentinel --init --build
|
|
29
|
+
$ pnpm install
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`--init` does the whole thing: the `build` script, the `serve` script, the nx target, the
|
|
33
|
+
removal of the build packages sentinel now owns, and **it points your `vite.config.ts` at
|
|
34
|
+
sentinel**.
|
|
35
|
+
|
|
36
|
+
## What changes in your config: the imports, and nothing else
|
|
37
|
+
|
|
38
|
+
```diff
|
|
39
|
+
- import tailwindcss from '@tailwindcss/vite'
|
|
40
|
+
- import react from '@vitejs/plugin-react'
|
|
41
|
+
- import { nitro } from 'nitro/vite'
|
|
42
|
+
- import { defineConfig, loadEnv } from 'vite'
|
|
43
|
+
- import svgr from 'vite-plugin-svgr'
|
|
44
|
+
+ import { defineConfig, loadEnv, nitro, react, svgr, tailwindcss } from '@hublo/sentinel/build/react'
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
That is the entire diff. Every attribute, every alias table, every path, every comment stays
|
|
48
|
+
exactly where it was, so your build produces what it produced before **by construction**: only
|
|
49
|
+
where the tools come from changed. Measured on `console`, `career` and `host-admin`, the file
|
|
50
|
+
is byte-identical below the import block.
|
|
51
|
+
|
|
52
|
+
Your own imports are untouched: TanStack, `@rolldown/plugin-babel`, `@front/theme/node`,
|
|
53
|
+
`node:path`. Sentinel takes only the five packages that sit in the workspace root, which is
|
|
54
|
+
what stops your app being buildable on its own.
|
|
55
|
+
|
|
56
|
+
If any import is in a shape sentinel cannot map — `import * as vite from 'vite'`, a
|
|
57
|
+
side-effect import, a file that does not parse — **nothing is written at all** and it tells you
|
|
58
|
+
why. A half-adopted build config is the one outcome worse than no adoption.
|
|
59
|
+
|
|
60
|
+
**Your module needs `"type": "module"`.** Sentinel is ESM only, and Vite decides how to load a
|
|
61
|
+
`.ts` config from the nearest `package.json`: without it, Vite loads the config as CommonJS,
|
|
62
|
+
`require`s sentinel, and fails with a message naming neither your module nor the fix. Sentinel
|
|
63
|
+
checks this before writing anything and tells you to add the field or rename the config to
|
|
64
|
+
`.mts`. Every front app in this repo already declares it, so this is about the next one.
|
|
65
|
+
|
|
66
|
+
## What sentinel owns, and what stays yours
|
|
67
|
+
|
|
68
|
+
Sentinel owns the five packages that sit in the workspace root today: `vite`,
|
|
69
|
+
`@vitejs/plugin-react`, `@tailwindcss/vite`, `vite-plugin-svgr` and `nitro`. It re-exports
|
|
70
|
+
them, so your imports resolve from it instead of from the root.
|
|
71
|
+
|
|
72
|
+
**Everything else stays yours**, and that is the whole design: your plugin order, your ports,
|
|
73
|
+
your `base`, your sourcemap decision, your alias tables, your `optimizeDeps`, your conditional
|
|
74
|
+
plugins. Sentinel never reads them, so nothing in them can be lost.
|
|
75
|
+
|
|
76
|
+
Your own packages are untouched too. Verified: `@rolldown/plugin-babel`, `@locator/babel-jsx`,
|
|
77
|
+
`@tanstack/react-start` and `@tanstack/router-plugin` are declared by the apps themselves and
|
|
78
|
+
not at the root, so none of them is sentinel's to take.
|
|
79
|
+
`src/roles/build/presets/toolchain.json` records what is taken and what deliberately is not,
|
|
80
|
+
each with its reason.
|
|
81
|
+
|
|
82
|
+
A shared base of common VALUES was designed and dropped. The measurement is why: parsing the
|
|
83
|
+
three React apps leaf by leaf, what they truly share is six values, and the things that LOOK
|
|
84
|
+
shared are not (`root: __dirname` is the same text with a different value in each,
|
|
85
|
+
`build.sourcemap` is the same variable name computed three ways, `optimizeDeps` is not even
|
|
86
|
+
the same key). Every hard case in those configs is precisely what is not common, so a shared
|
|
87
|
+
shape absorbing them would stop being shared. It may come back as an opt-in later; it is not
|
|
88
|
+
part of adopting.
|
|
89
|
+
|
|
90
|
+
## Commands
|
|
91
|
+
|
|
92
|
+
```console
|
|
93
|
+
$ sentinel --run --build # build this module
|
|
94
|
+
$ sentinel --run --build --ci # non-zero on failure
|
|
95
|
+
$ sentinel --run --dev # start the dev server
|
|
96
|
+
$ sentinel --inspect --build # what it resolves to, and where it departs
|
|
97
|
+
$ sentinel --init --build --dry-run # show what would change, write nothing
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## The `--` your `build` script ends with
|
|
101
|
+
|
|
102
|
+
`--init` writes `"build": "sentinel --run --build --"`, and that trailing `--` is doing work
|
|
103
|
+
rather than being a typo.
|
|
104
|
+
|
|
105
|
+
`pnpm run build --mode production` appends the option to the script, so the separator is what
|
|
106
|
+
decides who reads it:
|
|
107
|
+
|
|
108
|
+
```console
|
|
109
|
+
# with it -> sentinel --run --build -- --mode production -> Vite gets --mode
|
|
110
|
+
# without it -> sentinel --run --build --mode production -> error: unknown option '--mode'
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Sentinel refuses flags it does not know rather than passing them on, which is what turns
|
|
114
|
+
`--dryrun` into an error instead of an option Vite silently ignores. The separator is how you say
|
|
115
|
+
"the rest is for the tool", and sentinel prints `NOT the standard check` when you use it, because
|
|
116
|
+
a build with extra flags is not the one CI runs.
|
|
117
|
+
|
|
118
|
+
`--inspect --build` reads the manifest and the config's TEXT. It never loads the config, so it
|
|
119
|
+
cannot fail on an app's own terms (career throws without `VITE_BRAND_ID`) and it stays cheap
|
|
120
|
+
across a sweep:
|
|
121
|
+
|
|
122
|
+
```console
|
|
123
|
+
$ sentinel --inspect --build
|
|
124
|
+
✓ console (react) build — runner=vite configFile=vite.config.ts vite=sentinel
|
|
125
|
+
overrides=0 prerequisite=pnpm run prepare:brand-runtime-artifacts adopted=true
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
`vite=sentinel` is the field nothing else reports and the one worth reading: an adopted config
|
|
129
|
+
takes its plugins from sentinel, so a module answering `module` here is one build away from
|
|
130
|
+
running two copies of Vite against each other.
|
|
131
|
+
|
|
132
|
+
## The dev server, and why `--init` migrates it too
|
|
133
|
+
|
|
134
|
+
Your app calls the toolchain **twice**: `build` and `serve`. `--init` migrates both, because
|
|
135
|
+
migrating one leaves the workspace root unable to drop the packages.
|
|
136
|
+
|
|
137
|
+
That is not a sentinel limitation, it is how pnpm links binaries. Measured with sentinel's exact
|
|
138
|
+
shape — `vite` as a dependency of a package your app depends on:
|
|
139
|
+
|
|
140
|
+
```console
|
|
141
|
+
$ pnpm exec vite --version
|
|
142
|
+
Command "vite" not found
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
pnpm links a **direct** dependency's binaries, not a transitive one's. Sentinel resolves its own
|
|
146
|
+
Vite and always did; it is your `serve` script calling `vite` directly that stops resolving.
|
|
147
|
+
|
|
148
|
+
`--init` rewrites only the tool call and keeps whatever wraps it:
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
before: … --touch-file …/App.tsx -- pnpm --dir apps/front/console exec vite --host 0.0.0.0
|
|
152
|
+
after : … --touch-file …/App.tsx -- sentinel --run --dev -- --host 0.0.0.0
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Your own options travel through `--`. Vite's `dev` / `serve` subcommand is dropped, since the
|
|
156
|
+
verb already says which one this is. It rewrites `serve`, not `dev`, because that is where the
|
|
157
|
+
call lives in all three apps — `dev` is a one-line alias to it.
|
|
158
|
+
|
|
159
|
+
**The rewritten `serve` names its module** (`sentinel --run --dev --module console`). It has
|
|
160
|
+
to: the script does not call sentinel from your app, it hands the call to the `@front/theme`
|
|
161
|
+
watcher, which spawns from the WORKSPACE ROOT. From there sentinel sees every module, and a
|
|
162
|
+
dev server is a one-module command, so it refuses rather than starting several. Measured:
|
|
163
|
+
without the flag, `pnpm run serve` answers "this resolved to 439 [all]" and exits 1.
|
|
164
|
+
|
|
165
|
+
**`--dev` is never swept**, and it is refused across more than one module whoever asks. An
|
|
166
|
+
unqualified `sentinel --run` skips it entirely, so a sweep across the workspace answers a
|
|
167
|
+
question rather than launching a server per module.
|
|
168
|
+
|
|
169
|
+
**The read verbs refuse it too.** `--inspect --dev`, `--report --dev` and `--status --dev` exit
|
|
170
|
+
1 and point at `--build`. A server is started, not read: it has no state of its own, since its
|
|
171
|
+
config, its resolution and its plugins are the build's, since one `--init` plan writes `build` and
|
|
172
|
+
`serve` together. An answer here could only repeat `--build`'s, and a second surface saying the
|
|
173
|
+
same thing is one more to keep true. The refusal names the way out in your own verb, and says
|
|
174
|
+
that `--init --dev` is how the target is adopted.
|
|
175
|
+
|
|
176
|
+
Unlike `--run --build`, it does **not** run `prebuild`: the artefact is produced by the watcher
|
|
177
|
+
that wraps this command, and running it again would race the watcher about to own the file.
|
|
178
|
+
|
|
179
|
+
**And it retires the `serve` target in your `project.json`**, when that target does nothing but
|
|
180
|
+
call the script. A dev server needs nothing from nx: it never terminates so it is never cached, it
|
|
181
|
+
writes no output to restore, and no caller hands it a flag the bundler would refuse. So the script
|
|
182
|
+
alone declares it and nx infers the target, which is not a theory — `host-admin` has no `serve`
|
|
183
|
+
target at all and its dev server runs.
|
|
184
|
+
|
|
185
|
+
This arrived late, and the reason is worth stating: `--init` rewrote the script and stopped there,
|
|
186
|
+
so an adopted module kept one dev server with two declarations, and `--inspect` called it
|
|
187
|
+
conformant. It is a drift check now.
|
|
188
|
+
|
|
189
|
+
A target that does **more** than delegate — a `dependsOn`, its own options, a different command —
|
|
190
|
+
is left exactly where it is, and named in `--inspect` instead. Removing it would drop behaviour
|
|
191
|
+
this role never declared, and that is a decision for whoever wrote it.
|
|
192
|
+
|
|
193
|
+
## Your `vitest.config` moves too
|
|
194
|
+
|
|
195
|
+
An app calls the toolchain from more than one file, and `--init` follows it everywhere.
|
|
196
|
+
Measured here: `career` and `host-admin` import `loadEnv` from `vite` in their
|
|
197
|
+
`vitest.config.ts`, and `libs/front/ui` imports `mergeConfig`. Left behind, those imports stop
|
|
198
|
+
resolving the day the root drops the packages, and they fail your TESTS rather than your build,
|
|
199
|
+
long after the adoption that caused it.
|
|
200
|
+
|
|
201
|
+
Note that `vitest/config` exports a `mergeConfig` of its own. That one is vitest's and stays
|
|
202
|
+
where it is: the rewrite matches on the import specifier, never on the name.
|
|
203
|
+
|
|
204
|
+
## Why the imports, and only the imports
|
|
205
|
+
|
|
206
|
+
An earlier design read your config and rebuilt it around `reactApp`. It was dropped, for a
|
|
207
|
+
reason worth stating: a Vite config is **code**, not configuration. `career`'s alias table
|
|
208
|
+
opens with `...frontThemeAliases`, a constant declared 130 lines earlier; `host-admin` has
|
|
209
|
+
three conditional plugin groups; one nitro block carries a comment explaining a rolldown SSR
|
|
210
|
+
interop bug. Moving expressions and hoping nothing referenced them fails as a build that
|
|
211
|
+
_succeeds_ and ships the wrong bundle, which is the one failure a migration tool must not have.
|
|
212
|
+
|
|
213
|
+
Leaving that edit to you was worse. It is real work on a file nobody enjoys touching, so it
|
|
214
|
+
would not get done, and the root would stay unblocked forever.
|
|
215
|
+
|
|
216
|
+
Changing only the imports removes the dilemma: sentinel **reads nothing**, so there is nothing
|
|
217
|
+
it can lose. The parsing is done with `oxc-parser` rather than by matching lines, because an
|
|
218
|
+
import can be renamed, wrapped, or sitting next to a commented-out import of the same package
|
|
219
|
+
(`host-admin` has one) — and a regex that gets any of those wrong edits a file nobody
|
|
220
|
+
re-reads.
|
|
221
|
+
|
|
222
|
+
## `resolve.alias` is passed through, never read
|
|
223
|
+
|
|
224
|
+
Sentinel does not read, rewrite or resolve your alias table. It goes in as an option and comes
|
|
225
|
+
out unchanged, so nothing in it can be lost by adopting.
|
|
226
|
+
|
|
227
|
+
Your config file stays in your app, so `__dirname` is still your app's directory. Verified on
|
|
228
|
+
`console`: the 17 resolved `replacement` paths are identical before and after, pointing at the
|
|
229
|
+
app's real files rather than anything under sentinel.
|
|
230
|
+
|
|
231
|
+
Deriving the table from `tsconfig.base.json` — which already holds all 401 path mappings — is
|
|
232
|
+
its own ticket, with a build-output comparison as its acceptance criterion.
|
|
233
|
+
|
|
234
|
+
## TanStack stays yours
|
|
235
|
+
|
|
236
|
+
`@tanstack/react-start` and `@tanstack/router-plugin` do **not** move to sentinel. 48 app source
|
|
237
|
+
files import the first directly: it is a framework your app codes against, not a tool that
|
|
238
|
+
builds it, and the router plugin generates your app's own route tree.
|
|
239
|
+
|
|
240
|
+
That is why you pass the factories in. Sentinel decides _when_ to call them — the router plugin
|
|
241
|
+
alone under test, `tanstackStart` and `nitro` otherwise — while the packages stay where they
|
|
242
|
+
belong. The day the bundler changes, `--migrate` removes that line.
|
|
243
|
+
|
|
244
|
+
## Sentinel's Vite runs, not yours
|
|
245
|
+
|
|
246
|
+
The binary is resolved from **sentinel's** install first, which is the opposite of every other
|
|
247
|
+
role. An adopted config imports its plugins from `@hublo/sentinel/build/react`, so they are
|
|
248
|
+
bound to sentinel's Vite; a binary from your module — or, in this monorepo, from the workspace
|
|
249
|
+
root — hands the plugins a different Vite than the one running them. Nothing reports a version
|
|
250
|
+
conflict: a hook simply never fires.
|
|
251
|
+
|
|
252
|
+
`--run --build` says so out loud if it happens, and `--inspect --build` reports which install
|
|
253
|
+
answered.
|
|
254
|
+
|
|
255
|
+
## `prebuild` runs, even in CI
|
|
256
|
+
|
|
257
|
+
If your module declares a `prebuild` script, `--run --build` runs it first.
|
|
258
|
+
|
|
259
|
+
npm's lifecycle fires `prebuild` before `build`, so `pnpm run build` was always safe — but
|
|
260
|
+
`sentinel --run --build` is what CI calls, and it is not. All three front apps here generate a
|
|
261
|
+
gitignored branding artefact in `prebuild`; skipping it produces an error naming a tool that is
|
|
262
|
+
not at fault.
|
|
263
|
+
|
|
264
|
+
It runs **once**: `npm_lifecycle_event` says whether the script runner already fired it.
|
|
265
|
+
|
|
266
|
+
## Checking an adoption is COMPLETE, not merely started
|
|
267
|
+
|
|
268
|
+
`sentinel --inspect --build` reports a module as conformant only when everything adoption writes
|
|
269
|
+
is there. Seven things, and each of them shipped missing at least once:
|
|
270
|
+
|
|
271
|
+
| What it checks | What its absence cost |
|
|
272
|
+
| --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
273
|
+
| the `build` npm script | `pnpm run build` did nothing; only nx could build the module |
|
|
274
|
+
| the script invokes sentinel | the config pointed at sentinel while the command ran something else |
|
|
275
|
+
| `nx.targets.build` exists | the build ran on nx's DEFAULT inputs while every sibling target declared its own |
|
|
276
|
+
| `outputs`, when `cache: true` | nx cached the build and restored **0 of 464 files**, with the target green |
|
|
277
|
+
| `^default` among `inputs` | declaring `inputs` REPLACES nx's default, so the build stopped hashing its dependencies |
|
|
278
|
+
| `forwardAllArgs: false` | the image's `--generatePackageJson` reached Vite, which refuses it |
|
|
279
|
+
| no `build` target left in `project.json` | two declarations, and which one nx runs depends on the caller |
|
|
280
|
+
| no `serve` target left there either, once its script is adopted | the same fault on the dev server, and this check missed it for the whole role: `console` read `conformant=true drift=[]` with the target still on disk |
|
|
281
|
+
|
|
282
|
+
None of these was found by a check. Each was found by a person reading a diff, which is why they
|
|
283
|
+
are checks now: `--status` counts them across every module, so the next one is visible everywhere
|
|
284
|
+
rather than on the two files somebody happens to open.
|
|
285
|
+
|
|
286
|
+
**What it cannot check:** whether the translation LOST something. Once a service is adopted its
|
|
287
|
+
webpack target is gone, so an `assets` list that was dropped and one that never existed look
|
|
288
|
+
identical. That property belongs to the moment of translation, and is held by the refusals (a
|
|
289
|
+
target sentinel cannot translate faithfully is refused, not approximated) and by the tests that
|
|
290
|
+
pin what the translation reads.
|
|
291
|
+
|
|
292
|
+
## Your config is CODE, and it runs before the build does
|
|
293
|
+
|
|
294
|
+
A Vite config is executed, not parsed. Whatever it reads from the environment, it reads at that
|
|
295
|
+
moment — before a single file is bundled, and before sentinel has done anything.
|
|
296
|
+
|
|
297
|
+
So a shell or a container missing one of those variables fails at config load, with an error from
|
|
298
|
+
your config rather than from the build. Measured here: `host-admin` and `career` both stop with
|
|
299
|
+
|
|
300
|
+
```
|
|
301
|
+
Error: Missing branding environment variable "VITE_BRAND_ID". Expected one of: hublo, hublo-nova
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
That variable is not sentinel's, and adoption neither adds nor removes the requirement: the same
|
|
305
|
+
config asked for it before. It is worth knowing because the message names your config and the
|
|
306
|
+
moment is early enough to look like the build role is broken.
|
|
307
|
+
|
|
308
|
+
The one thing to check when you move a build somewhere new — a fresh clone, another CI job, an
|
|
309
|
+
image — is that it carries the variables your config reads. `sentinel --init --build` says so at
|
|
310
|
+
the end of a React adoption, for the same reason.
|
|
311
|
+
|
|
312
|
+
# Nest services
|
|
313
|
+
|
|
314
|
+
## Why this is happening now
|
|
315
|
+
|
|
316
|
+
Not a preference. Nx v24 removes the `@nx/webpack:webpack` executor and the `composePlugins` /
|
|
317
|
+
`withNx` helpers `tools/webpack-configs/` is built on, and nx's own migration generator refuses
|
|
318
|
+
all 38 services because they use `@nx/js:node` ([nrwl/nx#36389]). There is no version of "stay on
|
|
319
|
+
webpack" available, so the way out runs through here.
|
|
320
|
+
|
|
321
|
+
[nrwl/nx#36389]: https://github.com/nrwl/nx/issues/36389
|
|
322
|
+
|
|
323
|
+
## Your build stops checking types, and gets faster
|
|
324
|
+
|
|
325
|
+
The webpack target ran `"compiler": "tsc"`, so it type-checked every file it compiled, as a side
|
|
326
|
+
effect of compiling them. The new build bundles and does not. That is deliberate: a build builds,
|
|
327
|
+
and `typecheck` is its own target, which CI already runs on every pull request.
|
|
328
|
+
|
|
329
|
+
You gain time, and it was measured on `main` rather than promised: median compile went from 36s to
|
|
330
|
+
21s on `contract` (20 webpack builds against 10 Vite ones) and from 33s to 24s on `network`
|
|
331
|
+
(8 Vite builds).
|
|
332
|
+
|
|
333
|
+
You lose nothing on a pull request, and you gain coverage. Webpack compiled `tsconfig.app.json`,
|
|
334
|
+
which excludes `*.spec.ts`, `*.mock.ts`, `*.fixture.ts`, `*.steps.ts` and `*.integration-spec.ts`.
|
|
335
|
+
The `typecheck` target builds the projects your `tsconfig.json` REFERENCES, so the app and the
|
|
336
|
+
specs. Your test files are type-checked for the first time.
|
|
337
|
+
|
|
338
|
+
What genuinely changes is the deploy path: `build-and-publish-orchestrator` runs on a push to
|
|
339
|
+
`main` and type-checks nothing, where the old build would have failed there. The residual risk is
|
|
340
|
+
a type error that passes its own pull request and only appears once two of them meet on `main`.
|
|
341
|
+
Measured over the 12 weeks before adoption on those two services, the build's type check stopped
|
|
342
|
+
zero real errors, so nothing was added to the deploy workflow to cover it. A service that wants
|
|
343
|
+
the check back at build time adds it to its own `build` script, which stays its decision.
|
|
344
|
+
|
|
345
|
+
## Adopt a service
|
|
346
|
+
|
|
347
|
+
```console
|
|
348
|
+
$ pnpm add -D @hublo/sentinel
|
|
349
|
+
$ sentinel --init --build
|
|
350
|
+
$ pnpm install
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
That writes both files below and needs no manual step. It is a TRANSLATION, not a scaffold:
|
|
354
|
+
every value the config needs is already declared in the webpack target your service builds with
|
|
355
|
+
today, and those are exactly the values a human miscounts. It is also safe to re-run, which is
|
|
356
|
+
how you verify your own adoption.
|
|
357
|
+
|
|
358
|
+
It **refuses** rather than guessing when your target is not one it reproduces: a service with
|
|
359
|
+
its own webpack config (one exists here, loading `.sql` files as raw text), or a `production`
|
|
360
|
+
configuration whose `fileReplacements` point at files that exist. It says which, and writes
|
|
361
|
+
nothing.
|
|
362
|
+
|
|
363
|
+
What it writes, so you can review it. The config, which must be **`vite.config.mts`**:
|
|
364
|
+
|
|
365
|
+
```ts
|
|
366
|
+
import path from 'node:path'
|
|
367
|
+
|
|
368
|
+
import { nestService } from '@hublo/sentinel/build/nest'
|
|
369
|
+
|
|
370
|
+
export default nestService({
|
|
371
|
+
project: 'poe',
|
|
372
|
+
root: __dirname,
|
|
373
|
+
workspaceRoot: path.resolve(__dirname, '../../../..'),
|
|
374
|
+
outDir: 'dist/apps/nest/microservices/poe',
|
|
375
|
+
})
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
`.mts` and not `.ts`: a Nest service is CommonJS, sentinel is ESM only, and Vite decides how to
|
|
379
|
+
load a `.ts` config from the nearest `package.json`. Without the extension it loads the config as
|
|
380
|
+
CommonJS, `require`s sentinel, and fails with a message naming neither your module nor the fix.
|
|
381
|
+
|
|
382
|
+
Then the npm script and the nx target, both in `package.json`, exactly as `--init` writes them on
|
|
383
|
+
`poe`:
|
|
384
|
+
|
|
385
|
+
```json
|
|
386
|
+
"scripts": { "build": "sentinel --run --build --" },
|
|
387
|
+
"nx": {
|
|
388
|
+
"targets": {
|
|
389
|
+
"build": {
|
|
390
|
+
"executor": "nx:run-commands",
|
|
391
|
+
"options": {
|
|
392
|
+
"command": "pnpm --filter poe run build",
|
|
393
|
+
"forwardAllArgs": false
|
|
394
|
+
},
|
|
395
|
+
"cache": true,
|
|
396
|
+
"inputs": ["default", "^default", "{projectRoot}/vite.config.mts"],
|
|
397
|
+
"outputs": ["{workspaceRoot}/dist/apps/nest/microservices/poe"]
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
**Why an explicit target here**, where lint, format and typescript let nx infer one from the script:
|
|
404
|
+
the image runs `nx run <project>:build --generatePackageJson`, and an inferred npm-script target
|
|
405
|
+
forwards unknown arguments, which sentinel and Vite would both refuse. `forwardAllArgs: false`
|
|
406
|
+
makes nx swallow the flag, which is what lets the Dockerfile stay identical for a migrated and a
|
|
407
|
+
non-migrated service alike. That is also why the two roles' blocks do not look alike: those three
|
|
408
|
+
declare `cache` and `inputs` and nothing more, because nothing else is needed for them.
|
|
409
|
+
|
|
410
|
+
**Why the command runs the SCRIPT and not sentinel.** It used to be `pnpm exec sentinel --run
|
|
411
|
+
--build` with a `cwd`, and that skipped the module's own `prebuild`: `console` prepares its brand
|
|
412
|
+
artefacts there, and npm fires `pre<script>` only when the script is what was invoked. Going
|
|
413
|
+
through `pnpm --filter <name> run build` keeps that hook, and it costs nothing on a service that
|
|
414
|
+
has none.
|
|
415
|
+
|
|
416
|
+
A Nest service has no dev server to migrate; that half is
|
|
417
|
+
[a React concern](#the-dev-server-and-why---init-migrates-it-too).
|
|
418
|
+
|
|
419
|
+
## The check that decides whether you are done
|
|
420
|
+
|
|
421
|
+
Not "it builds", and not "it starts". Every service already has a `generate-swagger-file` target
|
|
422
|
+
that **runs the built bundle** and writes its OpenAPI contract into a committed file:
|
|
423
|
+
|
|
424
|
+
```console
|
|
425
|
+
$ nx run <service>:build --generatePackageJson
|
|
426
|
+
$ nx run <service>:generate-swagger-file
|
|
427
|
+
$ git diff --exit-code libs/api-types/service-<service>/
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
That proves the service boots **and** that its public API did not move, in one step. It is not
|
|
431
|
+
optional advice: the first version of this preset built and started, and moved the contract of
|
|
432
|
+
the second service it was tried on.
|
|
433
|
+
|
|
434
|
+
## What the preset does for you, and why each part exists
|
|
435
|
+
|
|
436
|
+
**Decorator metadata, emitted by TypeScript.** esbuild cannot emit `emitDecoratorMetadata` and
|
|
437
|
+
Nest cannot live without it. The transform reads your module's own tsconfig, so the emit is
|
|
438
|
+
preserved rather than re-chosen. It is deliberately not swc, which is ten times faster and wrong:
|
|
439
|
+
swc drops the `=== "function"` guard tsc puts around a serialized type, so an enum imported into
|
|
440
|
+
a DTO reaches `@nestjs/swagger` instead of `Object` and the service throws
|
|
441
|
+
`A circular dependency has been detected` at boot.
|
|
442
|
+
|
|
443
|
+
**One output file per module.** Hoisting everything into one scope forces the bundler to rename
|
|
444
|
+
duplicate class names, and `@nestjs/swagger` keys its schemas on `class.name`, so a renamed class
|
|
445
|
+
silently becomes a renamed schema in your published API.
|
|
446
|
+
|
|
447
|
+
**The pruned `package.json` and lockfile** the image installs from, built from the nx project
|
|
448
|
+
graph exactly as `--generatePackageJson` did.
|
|
449
|
+
|
|
450
|
+
**A refusal to ship a manifest that would not install.** The build compares what the bundler left
|
|
451
|
+
external against what the manifest declares. This is the check that would have caught `tslib`
|
|
452
|
+
arriving through `importHelpers` without any module declaring it, which killed a pod while every
|
|
453
|
+
build was green.
|
|
454
|
+
|
|
455
|
+
## If you need something the shape does not give you
|
|
456
|
+
|
|
457
|
+
Pass `overrides`, merged with Vite's own `mergeConfig`:
|
|
458
|
+
|
|
459
|
+
```ts
|
|
460
|
+
export default nestService({
|
|
461
|
+
/* … */
|
|
462
|
+
overrides: { build: { sourcemap: false } },
|
|
463
|
+
})
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
Or drop to the pieces, which the same entry point exports individually: `decoratorMetadata`,
|
|
467
|
+
`nodeManifest`, `tsconfigAliases`, plus Vite's `defineConfig`, `loadEnv` and `mergeConfig`.
|
|
468
|
+
|
|
469
|
+
# Both families
|
|
470
|
+
|
|
471
|
+
## Svelte modules, and what they need first
|
|
472
|
+
|
|
473
|
+
Sentinel ships **Vite 8**. `@sveltejs/vite-plugin-svelte` caps at Vite 6 until its major 7, and
|
|
474
|
+
that major requires `svelte ^5.46.4`. So the build role cannot apply to a SvelteKit module still
|
|
475
|
+
on Svelte 5.19 or 5.25, and it says exactly that rather than "this is not a React app":
|
|
476
|
+
|
|
477
|
+
```console
|
|
478
|
+
$ sentinel --init --build
|
|
479
|
+
build: the build role does not apply to this module yet:
|
|
480
|
+
@sveltejs/vite-plugin-svelte is at 5.0.3, needs >= 7.0.0; svelte is at 5.25.6,
|
|
481
|
+
needs >= 5.46.4. sentinel ships Vite 8, and @sveltejs/vite-plugin-svelte caps at
|
|
482
|
+
Vite 6 until its major 7. Nothing was written. Raise those versions, check the
|
|
483
|
+
module still builds and its tests still pass, then re-run `sentinel --init --build`.
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
**Sentinel does not raise them for you**, and that is deliberate. Removing a tool it replaces and
|
|
487
|
+
raising the framework your source is written against are different acts: a reviewer sees the line
|
|
488
|
+
`5.19.9 -> 5.46.4`, nobody sees a behaviour change at runtime, and a green build does not prove
|
|
489
|
+
there is none. The decision belongs to whoever owns the module.
|
|
490
|
+
|
|
491
|
+
It is a **skip**, not a failure. `sentinel --init` on such a module still adopts lint, format and
|
|
492
|
+
typescript, and exits 0.
|
|
493
|
+
|
|
494
|
+
Adding sentinel for those roles does **not** disturb your Vite 6, verified under pnpm's isolated
|
|
495
|
+
layout: your plugin keeps resolving your own Vite, and sentinel resolves its own.
|
|
496
|
+
|
|
497
|
+
The requirements live in `presets/requirements.json`, one entry per preset, each carrying the
|
|
498
|
+
reason it exists.
|
|
499
|
+
|
|
500
|
+
## Troubleshooting
|
|
501
|
+
|
|
502
|
+
**`--init --build` says there is no Vite config.** Then this module has not been given one yet.
|
|
503
|
+
Sentinel does not scaffold it, on either side: a config it invented would give the module a
|
|
504
|
+
second build path nobody reviewed. On the Nest side, write the four lines above yourself, which
|
|
505
|
+
is also how the diff stays readable to whoever approves it.
|
|
506
|
+
|
|
507
|
+
**`--init --build` says the preset does not handle this module.** Detection answers `node` for
|
|
508
|
+
a front app, because the build toolchain is declared at the workspace root and your app declares
|
|
509
|
+
none of it. Sentinel reads your Vite config instead: `@vitejs/plugin-react` or
|
|
510
|
+
`@tanstack/react-start` makes it React, and `@hublo/sentinel/build/nest` makes it Nest. If your
|
|
511
|
+
config imports neither, it genuinely is not one of the two.
|
|
512
|
+
|
|
513
|
+
**`--inspect --build` reports a `configError`.** Your config threw while loading. `career` does
|
|
514
|
+
this without `VITE_BRAND_ID`: the config is loaded for real, so it needs the same environment a
|
|
515
|
+
build needs. The message names what is missing.
|
|
516
|
+
|
|
517
|
+
**`--init --build` wrote nothing and named an import.** Sentinel refuses rather than guesses:
|
|
518
|
+
a namespace import (`import * as vite from 'vite'`), a side-effect import, or a config that
|
|
519
|
+
does not parse cannot be mapped onto named exports. Nothing is written, including the
|
|
520
|
+
`package.json` side, because a `build` script running sentinel's Vite against a config whose
|
|
521
|
+
plugins still come from the root is the two-copies failure this role exists to prevent.
|