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

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