@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.
- package/dist/bin/sentinel.js +1 -1
- package/dist/{chunk-4UIZJ3TR.js → chunk-MUQWAZQN.js} +407 -37
- package/dist/chunk-SX5CBE3Z.js +310 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +1 -1
- package/dist/roles/test/nest/toolchain.d.ts +3 -23
- package/dist/roles/test/nest/toolchain.js +4 -268
- package/dist/roles/test/nest/toolchain.js.map +1 -1
- package/dist/roles/test/react/toolchain.d.ts +5 -1
- package/dist/roles/test/react/toolchain.js +11 -3
- package/dist/roles/test/react/toolchain.js.map +1 -1
- package/dist/shared-test-config-CGxvMlmk.d.ts +28 -0
- package/package.json +1 -1
- package/dist/chunk-NX4GHIHF.js +0 -25
|
@@ -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
|
@@ -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
|
-
|
|
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 {
|
|
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
|
-
|
|
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,
|