@hublo/sentinel 1.4.0-alpha.2 → 1.4.0-alpha.4

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.
@@ -0,0 +1,336 @@
1
+ import {
2
+ decoratorMetadata,
3
+ tsconfigAliases
4
+ } from "./chunk-3TDUIKVQ.js";
5
+
6
+ // src/roles/test/shared-test-config.ts
7
+ import { existsSync } from "fs";
8
+ import { createRequire } from "module";
9
+ import { dirname, join } from "path";
10
+ import { fileURLToPath } from "url";
11
+
12
+ // src/roles/test/react/jest-export-conditions.ts
13
+ var ABSENT_UNDER_JEST = /* @__PURE__ */ new Set(["development", "development|production"]);
14
+ function stripInPlace(conditions) {
15
+ if (conditions === void 0) return;
16
+ for (let index = conditions.length - 1; index >= 0; index -= 1) {
17
+ const condition = conditions[index];
18
+ if (condition !== void 0 && ABSENT_UNDER_JEST.has(condition)) conditions.splice(index, 1);
19
+ }
20
+ }
21
+ function jestExportConditions() {
22
+ return {
23
+ name: "sentinel:jest-export-conditions",
24
+ configResolved(config) {
25
+ const environments = config;
26
+ stripInPlace(config.resolve?.conditions);
27
+ stripInPlace(config.ssr?.resolve?.conditions);
28
+ stripInPlace(environments.environments?.ssr?.resolve?.conditions);
29
+ }
30
+ };
31
+ }
32
+
33
+ // src/roles/test/shared-test-config.ts
34
+ function siblingFile(relative) {
35
+ const candidates = [
36
+ relative,
37
+ `roles/test/${relative.replace(/^\.\//, "")}`,
38
+ `../${relative.replace(/^\.\//, "")}`
39
+ ];
40
+ for (const candidate of candidates) {
41
+ try {
42
+ const sibling = fileURLToPath(new URL(candidate, import.meta.url));
43
+ if (existsSync(sibling)) return sibling;
44
+ } catch {
45
+ continue;
46
+ }
47
+ }
48
+ return void 0;
49
+ }
50
+ function resolveFrom(specifier) {
51
+ try {
52
+ return fileURLToPath(import.meta.resolve(specifier));
53
+ } catch {
54
+ try {
55
+ return createRequire(import.meta.url).resolve(specifier);
56
+ } catch {
57
+ return void 0;
58
+ }
59
+ }
60
+ }
61
+ function mockExtendedAdapterPath() {
62
+ return siblingFile("./setup/mock-extended.js");
63
+ }
64
+ function vitestMockExtendedPath() {
65
+ try {
66
+ return fileURLToPath(import.meta.resolve("vitest-mock-extended"));
67
+ } catch {
68
+ try {
69
+ return createRequire(import.meta.url).resolve("vitest-mock-extended");
70
+ } catch {
71
+ return "vitest-mock-extended";
72
+ }
73
+ }
74
+ }
75
+ function mswAliases() {
76
+ const server = siblingFile("./setup/msw-server.js");
77
+ const own = resolveFrom("msw");
78
+ return [
79
+ ...own === void 0 ? [] : [{ find: /^msw$/, replacement: own }],
80
+ ...server === void 0 ? [] : [{ find: /^@hublo\/test\/msw\/server$/, replacement: server }]
81
+ ];
82
+ }
83
+ function luxonAlias(root, workspaceRoot) {
84
+ for (const base of [root, workspaceRoot]) {
85
+ const manifest = join(base, "node_modules", "luxon", "package.json");
86
+ if (existsSync(manifest)) return [{ find: /^luxon$/, replacement: dirname(manifest) }];
87
+ }
88
+ return [];
89
+ }
90
+ function axiosAlias(root, workspaceRoot) {
91
+ for (const base of [root, workspaceRoot]) {
92
+ const manifest = join(base, "node_modules", "axios", "package.json");
93
+ if (existsSync(manifest)) return [{ find: /^axios$/, replacement: dirname(manifest) }];
94
+ }
95
+ return [];
96
+ }
97
+ function prismaRuntimeAlias(workspaceRoot) {
98
+ const prisma = join(workspaceRoot, "node_modules", "@prisma");
99
+ if (!existsSync(prisma)) return [];
100
+ return [
101
+ {
102
+ find: /^@prisma\/([^/]+)\/runtime\/library$/,
103
+ replacement: join(prisma, "$1", "runtime", "library.js")
104
+ }
105
+ ];
106
+ }
107
+ function baseConfig(options) {
108
+ const { root, workspaceRoot } = options;
109
+ return {
110
+ /*
111
+ * NO decorator-metadata plugin, and that is a measured removal rather than an omission.
112
+ *
113
+ * Nest reads constructor parameter types from `emitDecoratorMetadata`, so this preset used to
114
+ * run the BUILD role's SWC plugin to emit it, on the premise that the bundler does not. That
115
+ * premise was true of esbuild and is false of Vite 8, which transforms with oxc: oxc lowers the
116
+ * decorators itself and emits the metadata, provided a tsconfig with `experimentalDecorators`
117
+ * applies to the file.
118
+ *
119
+ * Measured five times before removing it. On `apps/nest/microservices/mission`, with and
120
+ * without the plugin, the output is strictly identical for `@Injectable()` constructors
121
+ * (including a service caught in a two-file import CYCLE, all 8 parameters resolved), for route
122
+ * handler parameters and return types, and for DTO properties; the suite then passes in 98s
123
+ * instead of 249s. Confirmed by a direct `Reflect.getMetadata` probe on
124
+ * `apps/nest/backends-for-frontends/admin` and on `libs/nest/starter`, and by two A/B runs
125
+ * through this very preset: `agency-notification` (100 tests) identical with and without, and
126
+ * `grid-leave` (3335 tests) identical without.
127
+ *
128
+ * One caveat that the same measurement produced, and which belongs to the CONSUMER rather than
129
+ * here: a property typed by an interface or by a type-alias imported with `import type` emits
130
+ * `Object`. That is `emitDecoratorMetadata`'s own behaviour, identical with and without the
131
+ * plugin, not a Vite regression.
132
+ *
133
+ * The BUILD role keeps its plugin. Its context differs, it feeds a shipped artefact, and
134
+ * removing it there needs its own proof.
135
+ *
136
+ * ⚠️ ONE construct escapes oxc, and the exception is why this line is a condition rather than
137
+ * an empty array: a decorator on an `abstract` class member. swc emitted it, oxc erases the
138
+ * member and the decorator with it, silently. There is exactly one such member in this repo,
139
+ * so `lowerDecoratorsWithTypeScript` buys the plugin back for that module alone.
140
+ */
141
+ /*
142
+ * `jestExportConditions` is UNCONDITIONAL, and it is the one plugin every migrated module gets.
143
+ *
144
+ * jest resolved with `['node', 'require', 'default']`, read off the installed `@nx/jest/preset`
145
+ * rather than off its documentation. `development` was never in that list, so a package
146
+ * shipping a separate development build loaded its PRODUCTION file. Vite's own conditions carry
147
+ * `development|production`, so `@emotion/cache` loads `emotion-cache.development.cjs.js`, whose
148
+ * extra stylis plugin calls `console.error(':first-child is potentially unsafe...')`, and with
149
+ * `jest-fail-on-console` in the setup that console call IS a failure.
150
+ *
151
+ * Measured on `libs/front/components`: 2 of its 3 remaining failures, both green again with
152
+ * this. `apps/front/front-legacy` has 100 over 26 files from the same cause.
153
+ *
154
+ * Unconditional rather than reserved for a jsdom module, because it is not a statement about
155
+ * the DOM: it is what this migration is for, running the suite the way the runner it was
156
+ * written for ran it. It changes nothing for a module with no dual-published dependency, which
157
+ * is why nest and cloud were measured identical without it.
158
+ */
159
+ plugins: [
160
+ jestExportConditions(),
161
+ ...options.lowerDecoratorsWithTypeScript ? [decoratorMetadata({ root })] : []
162
+ ],
163
+ /*
164
+ * The proviso in the paragraph above, made unconditional.
165
+ *
166
+ * oxc lowers decorators from the tsconfig that applies to the file, so a file belonging to NO
167
+ * tsconfig `include` is lowered as if it used the STANDARD decorators, and comes out of Vite as
168
+ * invalid JavaScript. Not merely without metadata: the file fails to parse, and every file that
169
+ * imports it disappears with it.
170
+ *
171
+ * Measured on `apps/nest/microservices/mission`, where ONE uncovered helper,
172
+ * `src/app/test/mission.test-wrapper.ts`, took down 158 of 416 test files. Reproduced on a
173
+ * three-file case: covered file fine, uncovered file `SyntaxError: Invalid or unexpected
174
+ * token`, and this option alone turns it into the same output the covered file gets, metadata
175
+ * included.
176
+ *
177
+ * Declared here rather than left to each module's tsconfig `include`, because the alternative
178
+ * is asking 107 teams to find which of their files no tsconfig covers, which is the work this
179
+ * role exists to do for them.
180
+ *
181
+ * Two things the same measurement established, both deliberate:
182
+ *
183
+ * - it OVERRIDES the tsconfig, it is not a default the tsconfig refines. A file under a
184
+ * tsconfig saying `emitDecoratorMetadata: false` gets metadata anyway. Acceptable because
185
+ * this is the NEST preset and a Nest module is legacy decorators by definition: no tsconfig
186
+ * under `apps/nest`, `libs/nest`, `apps/cloud` or `libs/cloud` sets either option to false.
187
+ * - it needs no polyfill. Without `reflect-metadata` loaded, nothing throws, the metadata is
188
+ * simply unreadable, exactly as before.
189
+ */
190
+ oxc: { decorator: { legacy: true, emitDecoratorMetadata: true } },
191
+ resolve: {
192
+ alias: [
193
+ ...axiosAlias(root, workspaceRoot),
194
+ ...luxonAlias(root, workspaceRoot),
195
+ /*
196
+ * Nest only, and measured: 1186 files under `apps/nest` and `libs/nest` mention `@prisma/`,
197
+ * and ZERO under `apps/front` and `libs/front`. A React module paying for an alias to a
198
+ * client it never generates is noise in a file someone has to read.
199
+ */
200
+ ...options.flavour === "nest" ? prismaRuntimeAlias(workspaceRoot) : [],
201
+ ...mswAliases(),
202
+ /*
203
+ * `jest-mock-extended` loads `@jest/globals`, which refuses to run outside jest. 2491
204
+ * files import it, 104 of them under `libs/` as SHARED helpers, so migrating those helpers
205
+ * breaks every module still on jest and leaving them breaks every module moved to Vitest.
206
+ * Old and new therefore do not cohabit on shared helpers, which would have killed the
207
+ * per-module plan.
208
+ *
209
+ * This one line removes the constraint: an unmigrated helper resolves to the Vitest fork
210
+ * inside an adopted module and keeps resolving to the jest one everywhere else.
211
+ * `vitest-mock-extended@5.1.1` is a fork of the same package and exports the same names.
212
+ *
213
+ * Measured on `mission`: failing suites went from 159 to 10.
214
+ */
215
+ {
216
+ find: /^jest-mock-extended$/,
217
+ replacement: mockExtendedAdapterPath() ?? vitestMockExtendedPath()
218
+ },
219
+ ...tsconfigAliases(workspaceRoot)
220
+ ]
221
+ },
222
+ test: {
223
+ globals: true,
224
+ environment: "node",
225
+ root,
226
+ /*
227
+ * What the repo's jest preset actually matched, copied rather than approximated:
228
+ * `**\/?(*.)+(spec|test).[jt]s?(x)`.
229
+ *
230
+ * Both NAMES, because jest ran both: `**\/*.spec.ts` alone read green while missing three
231
+ * `.test.ts` files and 23 tests, with nothing saying so. And all four EXTENSIONS, for the
232
+ * same reason one notch further out. Measured over the repo's 7063 test files:
233
+ *
234
+ * .ts 5844 .tsx 1108 .js 99 .mjs/.cjs 12
235
+ *
236
+ * The `.ts`-only form cost nothing on nest and cloud, which have none of the others, and it
237
+ * cost `libs/front/api` three files and 13 tests on the first front module it met. Half the
238
+ * front's test files are `.tsx`.
239
+ *
240
+ * `.mjs` and `.cjs` are deliberately OUT: jest's `[jt]s?(x)` does not match them either, and
241
+ * Vitest's own default include does. Running a file the reference never ran is as wrong as
242
+ * skipping one it did.
243
+ */
244
+ include: ["**/*.spec.[jt]s?(x)", "**/*.test.[jt]s?(x)"],
245
+ /*
246
+ * Kept, and it is not a performance knob. Nest registers metadata as an import SIDE EFFECT:
247
+ * a decorator writes into a catalog when its module loads. Sharing a module registry across
248
+ * files lets one suite see what another registered, and the failure appears in whichever
249
+ * file happens to run second.
250
+ */
251
+ /*
252
+ * Nest only. A Nest module registers metadata as an import SIDE EFFECT, so sharing a module
253
+ * registry across files lets one suite see what another registered. A React module has no
254
+ * such catalog, and isolation is not free.
255
+ */
256
+ ...options.flavour === "nest" ? { isolate: true } : {},
257
+ /*
258
+ * Concurrency, transposed from what this repo does today rather than chosen.
259
+ *
260
+ * Every jest target inherits `configurations.ci = { ci: true, runInBand: true }` from the
261
+ * `@nx/jest:jest` key in `nx.json`, and CI invokes every test target with
262
+ * `--configuration=ci`. So on CI every suite in this repo runs ONE FILE AT A TIME today. That
263
+ * key belongs to the jest executor and cannot be touched, because the workspace is mixed: it
264
+ * still serves the modules that have not moved.
265
+ *
266
+ * ⚠️ And it is CI-ONLY. A local run omits `--configuration=ci`, so jest runs files in
267
+ * PARALLEL on a developer's machine. A flat `fileParallelism: false` here would make local
268
+ * runs slower than jest, which loses something the module had. Hence the condition rather
269
+ * than the constant: parallel locally, one file at a time on CI, which is jest on both sides.
270
+ *
271
+ * Measured on three files that each hold the clock for 400ms and record their interval:
272
+ *
273
+ * default files overlap, 402ms
274
+ * fileParallelism: false no overlap, 1442ms
275
+ * maxWorkers: 1 no overlap, 1423ms
276
+ *
277
+ * Both candidates give the property that matters. `fileParallelism` is the one that says what
278
+ * the module MEANS ("do not run my files at the same time"); `maxWorkers` is a pool size, and
279
+ * it is what a module asking for a CAP gets instead (three BFFs ask for 4).
280
+ *
281
+ * A module that declared its own concurrency overrides this, in both environments, exactly as
282
+ * it does today.
283
+ */
284
+ fileParallelism: !process.env.CI
285
+ }
286
+ };
287
+ }
288
+ function asAliasArray(alias) {
289
+ if (alias === void 0) return [];
290
+ if (Array.isArray(alias)) return alias;
291
+ return Object.entries(alias).map(([find, replacement]) => ({
292
+ find,
293
+ replacement
294
+ }));
295
+ }
296
+ function sharedTestConfig(options) {
297
+ const base = baseConfig(options);
298
+ if (options.overrides === void 0) return base;
299
+ return {
300
+ ...base,
301
+ ...options.overrides,
302
+ plugins: [...base.plugins ?? [], ...options.overrides.plugins ?? []],
303
+ resolve: {
304
+ ...base.resolve,
305
+ ...options.overrides.resolve,
306
+ /*
307
+ * The module's own aliases come after sentinel's, and in a Vite alias ARRAY the FIRST match
308
+ * wins. So sentinel's win, which is the opposite of what this comment used to claim.
309
+ *
310
+ * ⚠️ The order is corrected here rather than in the code, because changing it would flip a
311
+ * behaviour that nothing exercises. Every alias sentinel adds is ANCHORED to one exact
312
+ * specifier: `^axios$`, `^luxon$`, `^msw$`, `^jest-mock-extended$` and
313
+ * `^@prisma/<x>/runtime/library$`. A module's own entry collides only by naming the identical
314
+ * string, and measured across every `jest.config*` in the repo, none does: the closest are
315
+ * `@front/type/axios` and `@front/api/msw-handlers` in `front-legacy`, different specifiers
316
+ * in the one module this role refuses anyway.
317
+ *
318
+ * So today the order decides nothing, and all 97 migrations were measured with it this way
319
+ * round. Reversing it on a hypothesis would be a silent behaviour change bought with nothing.
320
+ * What was actually wrong was a comment promising a module it could win, which a reader would
321
+ * have relied on.
322
+ */
323
+ alias: [
324
+ ...asAliasArray(base.resolve?.alias),
325
+ ...asAliasArray(options.overrides.resolve?.alias)
326
+ ]
327
+ },
328
+ test: { ...base.test, ...options.overrides.test }
329
+ };
330
+ }
331
+
332
+ export {
333
+ jestExportConditions,
334
+ sharedTestConfig
335
+ };
336
+ //# sourceMappingURL=chunk-MT7VQXRA.js.map
package/dist/index.d.ts CHANGED
@@ -275,6 +275,19 @@ interface Adapter {
275
275
  * preview.
276
276
  */
277
277
  afterInit?(ctx: RunContext): Promise<void>;
278
+ /**
279
+ * Run BEFORE `--init` writes anything, so a role can capture what it is about to replace.
280
+ *
281
+ * The test role is why this exists. A migration can only be judged against what the module did
282
+ * BEFORE it, and the jest config is deleted by the very plan that is about to be applied: after
283
+ * that moment the reference is unobtainable. So the reference is recorded here, at the last
284
+ * instant it exists.
285
+ *
286
+ * Never called under `--dry-run`, which must touch nothing, and a failure here does NOT stop the
287
+ * init: a module whose jest run is already broken can still be migrated, it simply cannot be
288
+ * proved, and saying so is better than refusing.
289
+ */
290
+ beforeInit?(ctx: RunContext): Promise<void>;
278
291
  /**
279
292
  * The module's resolved configuration, for `--inspect`: what preset it uses and
280
293
  * how it was resolved. Takes the run context so it can report the config as
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  registerAdapters,
8
8
  resolve,
9
9
  setDefaultRunner
10
- } from "./chunk-4UIZJ3TR.js";
10
+ } from "./chunk-ASBZQAXV.js";
11
11
  import "./chunk-WLFE5RUU.js";
12
12
  export {
13
13
  BaseAdapter,
@@ -1,29 +1,9 @@
1
1
  import { ViteUserConfig } from 'vitest/config';
2
2
  export { ConfigEnv, TestUserConfig, ViteUserConfig, defineConfig, mergeConfig } from 'vitest/config';
3
3
  export { loadEnv } from 'vite';
4
+ import { SharedTestOptions } from '../shared-test-config.js';
4
5
  export { A as Alias, D as DecoratorMetadataOptions, d as decoratorMetadata, t as tsconfigAliases } from '../../../tsconfig-aliases-Ce6axdJ4.js';
5
6
 
6
- interface NestTestOptions {
7
- /** The module's own directory: where its specs live and what its config is relative to. */
8
- root: string;
9
- /** The workspace root, which is where `tsconfig.base.json` and its path aliases are. */
10
- workspaceRoot: string;
11
- /**
12
- * Lower decorators with TypeScript before oxc sees them, for a module that needs it.
13
- *
14
- * ⚠️ Written by `--init --test` from the module's own sources, never by hand, because the
15
- * condition is not a preference: it is whether a decorator sits on an `abstract` class member,
16
- * the one construct oxc does not reproduce. See `generate-config.ts` for the measurement.
17
- *
18
- * Off by default, and that default is the measured one: on
19
- * `apps/nest/microservices/mission`, 715 of 716 decorated files need nothing, and running the
20
- * plugin for all of them took the suite from 98s to 249s.
21
- */
22
- lowerDecoratorsWithTypeScript?: boolean;
23
- /** Merged over the base. For what a module genuinely needs to differ on, nothing else. */
24
- overrides?: ViteUserConfig;
25
- }
26
- /** The config, with the module's own overrides merged over it. */
27
- declare function nestTestConfig(options: NestTestOptions): ViteUserConfig;
7
+ declare function nestTestConfig(options: SharedTestOptions): ViteUserConfig;
28
8
 
29
- export { type NestTestOptions, nestTestConfig };
9
+ export { SharedTestOptions as NestTestOptions, nestTestConfig };