@hublo/sentinel 1.2.0 → 1.4.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/README.md +2 -0
  2. package/dist/bin/sentinel.d.ts +0 -1
  3. package/dist/bin/sentinel.js +28 -9
  4. package/dist/chunk-2XLX6PFR.js +132 -0
  5. package/dist/chunk-3TDUIKVQ.js +178 -0
  6. package/dist/{chunk-Z5I6SEKF.js → chunk-O3XCQGTJ.js} +2880 -520
  7. package/dist/chunk-PWV3BMDA.js +15 -0
  8. package/dist/chunk-WLFE5RUU.js +264 -0
  9. package/dist/index.js +2 -1
  10. package/dist/roles/build/nest/toolchain.d.ts +4 -36
  11. package/dist/roles/build/nest/toolchain.js +10 -178
  12. package/dist/roles/build/nest/toolchain.js.map +1 -0
  13. package/dist/roles/build/toolchain.js.map +1 -0
  14. package/dist/roles/test/nest/toolchain.d.ts +29 -0
  15. package/dist/roles/test/nest/toolchain.js +224 -0
  16. package/dist/roles/test/nest/toolchain.js.map +1 -0
  17. package/dist/roles/test/react/toolchain.d.ts +62 -0
  18. package/dist/roles/test/react/toolchain.js +22 -0
  19. package/dist/roles/test/react/toolchain.js.map +1 -0
  20. package/dist/roles/test/setup/mock-extended.d.ts +46 -0
  21. package/dist/roles/test/setup/mock-extended.js +65 -0
  22. package/dist/roles/test/setup/mock-extended.js.map +1 -0
  23. package/dist/roles/test/setup/msw-lifecycle.d.ts +16 -0
  24. package/dist/roles/test/setup/msw-lifecycle.js +12 -0
  25. package/dist/roles/test/setup/msw-lifecycle.js.map +1 -0
  26. package/dist/roles/test/setup/msw-server.d.ts +3 -0
  27. package/dist/roles/test/setup/msw-server.js +10 -0
  28. package/dist/roles/test/setup/msw-server.js.map +1 -0
  29. package/dist/roles/test/setup/nest.d.ts +2 -0
  30. package/dist/roles/test/setup/nest.js +211 -0
  31. package/dist/roles/test/setup/nest.js.map +1 -0
  32. package/dist/tsconfig-aliases-Ce6axdJ4.d.ts +36 -0
  33. package/package.json +24 -3
@@ -0,0 +1,29 @@
1
+ import { ViteUserConfig } from 'vitest/config';
2
+ export { ConfigEnv, TestUserConfig, ViteUserConfig, defineConfig, mergeConfig } from 'vitest/config';
3
+ export { loadEnv } from 'vite';
4
+ export { A as Alias, D as DecoratorMetadataOptions, d as decoratorMetadata, t as tsconfigAliases } from '../../../tsconfig-aliases-Ce6axdJ4.js';
5
+
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;
28
+
29
+ export { type NestTestOptions, nestTestConfig };
@@ -0,0 +1,224 @@
1
+ import {
2
+ decoratorMetadata,
3
+ tsconfigAliases
4
+ } from "../../../chunk-3TDUIKVQ.js";
5
+
6
+ // src/roles/test/nest/toolchain.ts
7
+ import { defineConfig, mergeConfig } from "vitest/config";
8
+ import { loadEnv } from "vite";
9
+
10
+ // src/roles/test/nest/nest-test-config.ts
11
+ import { existsSync } from "fs";
12
+ import { createRequire } from "module";
13
+ import { dirname, join } from "path";
14
+ import { fileURLToPath } from "url";
15
+ function siblingFile(relative) {
16
+ try {
17
+ const sibling = fileURLToPath(new URL(relative, import.meta.url));
18
+ return existsSync(sibling) ? sibling : void 0;
19
+ } catch {
20
+ return void 0;
21
+ }
22
+ }
23
+ function resolveFrom(specifier) {
24
+ try {
25
+ return fileURLToPath(import.meta.resolve(specifier));
26
+ } catch {
27
+ try {
28
+ return createRequire(import.meta.url).resolve(specifier);
29
+ } catch {
30
+ return void 0;
31
+ }
32
+ }
33
+ }
34
+ function mockExtendedAdapterPath() {
35
+ return siblingFile("../setup/mock-extended.js");
36
+ }
37
+ function vitestMockExtendedPath() {
38
+ try {
39
+ return fileURLToPath(import.meta.resolve("vitest-mock-extended"));
40
+ } catch {
41
+ try {
42
+ return createRequire(import.meta.url).resolve("vitest-mock-extended");
43
+ } catch {
44
+ return "vitest-mock-extended";
45
+ }
46
+ }
47
+ }
48
+ function mswAliases() {
49
+ const server = siblingFile("../setup/msw-server.js");
50
+ const own = resolveFrom("msw");
51
+ return [
52
+ ...own === void 0 ? [] : [{ find: /^msw$/, replacement: own }],
53
+ ...server === void 0 ? [] : [{ find: /^@hublo\/test\/msw\/server$/, replacement: server }]
54
+ ];
55
+ }
56
+ function luxonAlias(root, workspaceRoot) {
57
+ for (const base of [root, workspaceRoot]) {
58
+ const manifest = join(base, "node_modules", "luxon", "package.json");
59
+ if (existsSync(manifest)) return [{ find: /^luxon$/, replacement: dirname(manifest) }];
60
+ }
61
+ return [];
62
+ }
63
+ function axiosAlias(root, workspaceRoot) {
64
+ for (const base of [root, workspaceRoot]) {
65
+ const manifest = join(base, "node_modules", "axios", "package.json");
66
+ if (existsSync(manifest)) return [{ find: /^axios$/, replacement: dirname(manifest) }];
67
+ }
68
+ return [];
69
+ }
70
+ function prismaRuntimeAlias(workspaceRoot) {
71
+ const prisma = join(workspaceRoot, "node_modules", "@prisma");
72
+ if (!existsSync(prisma)) return [];
73
+ return [
74
+ {
75
+ find: /^@prisma\/([^/]+)\/runtime\/library$/,
76
+ replacement: join(prisma, "$1", "runtime", "library.js")
77
+ }
78
+ ];
79
+ }
80
+ function baseConfig(options) {
81
+ const { root, workspaceRoot } = options;
82
+ return {
83
+ /*
84
+ * NO decorator-metadata plugin, and that is a measured removal rather than an omission.
85
+ *
86
+ * Nest reads constructor parameter types from `emitDecoratorMetadata`, so this preset used to
87
+ * run the BUILD role's SWC plugin to emit it, on the premise that the bundler does not. That
88
+ * premise was true of esbuild and is false of Vite 8, which transforms with oxc: oxc lowers the
89
+ * decorators itself and emits the metadata, provided a tsconfig with `experimentalDecorators`
90
+ * applies to the file.
91
+ *
92
+ * Measured five times before removing it. On `apps/nest/microservices/mission`, with and
93
+ * without the plugin, the output is strictly identical for `@Injectable()` constructors
94
+ * (including a service caught in a two-file import CYCLE, all 8 parameters resolved), for route
95
+ * handler parameters and return types, and for DTO properties; the suite then passes in 98s
96
+ * instead of 249s. Confirmed by a direct `Reflect.getMetadata` probe on
97
+ * `apps/nest/backends-for-frontends/admin` and on `libs/nest/starter`, and by two A/B runs
98
+ * through this very preset: `agency-notification` (100 tests) identical with and without, and
99
+ * `grid-leave` (3335 tests) identical without.
100
+ *
101
+ * One caveat that the same measurement produced, and which belongs to the CONSUMER rather than
102
+ * here: a property typed by an interface or by a type-alias imported with `import type` emits
103
+ * `Object`. That is `emitDecoratorMetadata`'s own behaviour, identical with and without the
104
+ * plugin, not a Vite regression.
105
+ *
106
+ * The BUILD role keeps its plugin. Its context differs, it feeds a shipped artefact, and
107
+ * removing it there needs its own proof.
108
+ *
109
+ * ⚠️ ONE construct escapes oxc, and the exception is why this line is a condition rather than
110
+ * an empty array: a decorator on an `abstract` class member. swc emitted it, oxc erases the
111
+ * member and the decorator with it, silently. There is exactly one such member in this repo,
112
+ * so `lowerDecoratorsWithTypeScript` buys the plugin back for that module alone.
113
+ */
114
+ plugins: options.lowerDecoratorsWithTypeScript ? [decoratorMetadata({ root })] : [],
115
+ /*
116
+ * The proviso in the paragraph above, made unconditional.
117
+ *
118
+ * oxc lowers decorators from the tsconfig that applies to the file, so a file belonging to NO
119
+ * tsconfig `include` is lowered as if it used the STANDARD decorators, and comes out of Vite as
120
+ * invalid JavaScript. Not merely without metadata: the file fails to parse, and every file that
121
+ * imports it disappears with it.
122
+ *
123
+ * Measured on `apps/nest/microservices/mission`, where ONE uncovered helper,
124
+ * `src/app/test/mission.test-wrapper.ts`, took down 158 of 416 test files. Reproduced on a
125
+ * three-file case: covered file fine, uncovered file `SyntaxError: Invalid or unexpected
126
+ * token`, and this option alone turns it into the same output the covered file gets, metadata
127
+ * included.
128
+ *
129
+ * Declared here rather than left to each module's tsconfig `include`, because the alternative
130
+ * is asking 107 teams to find which of their files no tsconfig covers, which is the work this
131
+ * role exists to do for them.
132
+ *
133
+ * Two things the same measurement established, both deliberate:
134
+ *
135
+ * - it OVERRIDES the tsconfig, it is not a default the tsconfig refines. A file under a
136
+ * tsconfig saying `emitDecoratorMetadata: false` gets metadata anyway. Acceptable because
137
+ * this is the NEST preset and a Nest module is legacy decorators by definition: no tsconfig
138
+ * under `apps/nest`, `libs/nest`, `apps/cloud` or `libs/cloud` sets either option to false.
139
+ * - it needs no polyfill. Without `reflect-metadata` loaded, nothing throws, the metadata is
140
+ * simply unreadable, exactly as before.
141
+ */
142
+ oxc: { decorator: { legacy: true, emitDecoratorMetadata: true } },
143
+ resolve: {
144
+ alias: [
145
+ ...axiosAlias(root, workspaceRoot),
146
+ ...luxonAlias(root, workspaceRoot),
147
+ ...prismaRuntimeAlias(workspaceRoot),
148
+ ...mswAliases(),
149
+ /*
150
+ * `jest-mock-extended` loads `@jest/globals`, which refuses to run outside jest. 2491
151
+ * files import it, 104 of them under `libs/` as SHARED helpers, so migrating those helpers
152
+ * breaks every module still on jest and leaving them breaks every module moved to Vitest.
153
+ * Old and new therefore do not cohabit on shared helpers, which would have killed the
154
+ * per-module plan.
155
+ *
156
+ * This one line removes the constraint: an unmigrated helper resolves to the Vitest fork
157
+ * inside an adopted module and keeps resolving to the jest one everywhere else.
158
+ * `vitest-mock-extended@5.1.1` is a fork of the same package and exports the same names.
159
+ *
160
+ * Measured on `mission`: failing suites went from 159 to 10.
161
+ */
162
+ {
163
+ find: /^jest-mock-extended$/,
164
+ replacement: mockExtendedAdapterPath() ?? vitestMockExtendedPath()
165
+ },
166
+ ...tsconfigAliases(workspaceRoot)
167
+ ]
168
+ },
169
+ test: {
170
+ globals: true,
171
+ environment: "node",
172
+ root,
173
+ /*
174
+ * Both extensions, because jest ran both. `**\/*.spec.ts` alone read green while missing
175
+ * three `.test.ts` files and 23 tests, with nothing saying so: a counter does not compare to
176
+ * itself, it compares to the reference.
177
+ */
178
+ include: ["**/*.spec.ts", "**/*.test.ts"],
179
+ /*
180
+ * Kept, and it is not a performance knob. Nest registers metadata as an import SIDE EFFECT:
181
+ * a decorator writes into a catalog when its module loads. Sharing a module registry across
182
+ * files lets one suite see what another registered, and the failure appears in whichever
183
+ * file happens to run second.
184
+ */
185
+ isolate: true
186
+ }
187
+ };
188
+ }
189
+ function asAliasArray(alias) {
190
+ if (alias === void 0) return [];
191
+ if (Array.isArray(alias)) return alias;
192
+ return Object.entries(alias).map(([find, replacement]) => ({
193
+ find,
194
+ replacement
195
+ }));
196
+ }
197
+ function nestTestConfig(options) {
198
+ const base = baseConfig(options);
199
+ if (options.overrides === void 0) return base;
200
+ return {
201
+ ...base,
202
+ ...options.overrides,
203
+ plugins: [...base.plugins ?? [], ...options.overrides.plugins ?? []],
204
+ resolve: {
205
+ ...base.resolve,
206
+ ...options.overrides.resolve,
207
+ // The module's own aliases come AFTER sentinel's, so a module that needs to win can.
208
+ alias: [
209
+ ...asAliasArray(base.resolve?.alias),
210
+ ...asAliasArray(options.overrides.resolve?.alias)
211
+ ]
212
+ },
213
+ test: { ...base.test, ...options.overrides.test }
214
+ };
215
+ }
216
+ export {
217
+ decoratorMetadata,
218
+ defineConfig,
219
+ loadEnv,
220
+ mergeConfig,
221
+ nestTestConfig,
222
+ tsconfigAliases
223
+ };
224
+ //# sourceMappingURL=toolchain.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../src/roles/test/nest/toolchain.ts","../../../../src/roles/test/nest/nest-test-config.ts"],"sourcesContent":["/**\n * What `@hublo/sentinel/test/nest` gives an adopted module.\n *\n * Its own entry point, like the build ones, so importing it from a config never drags the CLI and\n * its adapters into a test run.\n *\n * It differs from `test/react` in one way that matters, and the difference is not decoration.\n * React modules already had a Vitest config carrying decisions their owners made and justified, so\n * that preset is a pure re-export surface and moves nothing. Nest modules have NO Vitest config:\n * one has to be written, and four of its lines are load-bearing in ways nobody would guess from a\n * migration guide. Those four live in `nestTestConfig` rather than in 133 copies.\n */\n\n/**\n * From `vitest/config`, and NOT from `vite`, which also exports a `defineConfig`.\n *\n * They are different functions: `vitest/config`'s knows the `test` key, vite's does not. A config\n * built with vite's would load and quietly drop its entire test block, which is the kind of\n * failure that reads as \"vitest found no tests\" and sends someone hunting through globs.\n *\n * The BUILD role's nest entry point re-exports vite's, correctly for its purpose. Importing that\n * one here would be the exact mistake this comment exists to prevent.\n */\nexport { defineConfig, mergeConfig } from 'vitest/config'\n\n/**\n * From `vite`, because `vitest/config` does not export it. Two of the six already-Vitest configs\n * build their test env with it, and Nest services read their env the same way.\n *\n * Re-exported rather than claimed: the `vite` package belongs to the build role, and a module\n * adopting only this one keeps its own.\n */\nexport { loadEnv } from 'vite'\n\n/** The whole config, as one call. See `nest-test-config.ts` for why each line is there. */\nexport { nestTestConfig, type NestTestOptions } from './nest-test-config.js'\n\n/**\n * Re-exported so a module that needs to compose rather than override can reach them without\n * importing the BUILD role's entry point from a test config, which would pull vite's\n * `defineConfig` into scope beside vitest's and invite the mistake above.\n */\nexport {\n decoratorMetadata,\n type DecoratorMetadataOptions,\n} from '../../build/nest/decorator-metadata.js'\nexport { tsconfigAliases, type Alias } from '../../build/nest/tsconfig-aliases.js'\n\nexport type { ConfigEnv, TestUserConfig, ViteUserConfig } from 'vitest/config'\n","/**\n * The Vitest config a Nest module gets, as one call instead of forty lines copied 133 times.\n *\n * The React family needed none of this: those modules already had a Vitest config, so adoption\n * moved its imports and changed nothing else. The Jest family has no config at all, so one has to\n * be WRITTEN, and writing the same four decisions into 133 files is how they drift. A module that\n * genuinely differs still can: `overrides` is merged over the base, the same shape `nestService`\n * uses on the build side.\n *\n * Every line below was established by running a real suite, not by reading a guide. `mission` is\n * the reference: 387 specs, the module the build role refused for having its own webpack config,\n * and the one with the only `isolateModules` in the repo.\n */\nimport { existsSync } from 'node:fs'\nimport { createRequire } from 'node:module'\nimport { dirname, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\nimport type { Alias as ViteAlias, AliasOptions } from 'vite'\nimport type { ViteUserConfig } from 'vitest/config'\n\nimport { decoratorMetadata } from '../../build/nest/decorator-metadata.js'\nimport { tsconfigAliases } from '../../build/nest/tsconfig-aliases.js'\nimport type { Alias } from '../../build/nest/tsconfig-aliases.js'\n\nexport interface NestTestOptions {\n /** The module's own directory: where its specs live and what its config is relative to. */\n root: string\n /** The workspace root, which is where `tsconfig.base.json` and its path aliases are. */\n workspaceRoot: string\n /**\n * Lower decorators with TypeScript before oxc sees them, for a module that needs it.\n *\n * ⚠️ Written by `--init --test` from the module's own sources, never by hand, because the\n * condition is not a preference: it is whether a decorator sits on an `abstract` class member,\n * the one construct oxc does not reproduce. See `generate-config.ts` for the measurement.\n *\n * Off by default, and that default is the measured one: on\n * `apps/nest/microservices/mission`, 715 of 716 decorated files need nothing, and running the\n * plugin for all of them took the suite from 98s to 249s.\n */\n lowerDecoratorsWithTypeScript?: boolean\n /** Merged over the base. For what a module genuinely needs to differ on, nothing else. */\n overrides?: ViteUserConfig\n}\n\n/** A file shipped beside this module in the build, or nothing when running from source. */\nfunction siblingFile(relative: string): string | undefined {\n try {\n const sibling = fileURLToPath(new URL(relative, import.meta.url))\n return existsSync(sibling) ? sibling : undefined\n } catch {\n return undefined\n }\n}\n\n/** A package resolved from SENTINEL, so the module gets the copy the setup uses. */\nfunction resolveFrom(specifier: string): string | undefined {\n try {\n return fileURLToPath(import.meta.resolve(specifier))\n } catch {\n try {\n return createRequire(import.meta.url).resolve(specifier)\n } catch {\n return undefined\n }\n }\n}\n\n/**\n * sentinel's own adapter around the fork, which the alias points at when it has been built.\n *\n * The adapter only makes a deep mock answer `in` and `getOwnPropertyDescriptor`, which Vitest's\n * `spyOn` asks and jest's did not; `mock-extended.ts` carries the measurement. Resolved as a\n * sibling file rather than by specifier, because it is not a package.\n *\n * Falls back to the fork itself when there is no build beside this module, which is the case while\n * sentinel's own suite runs from `src`. The alias then behaves exactly as it did before the\n * adapter existed, so a missing build degrades instead of throwing.\n */\nfunction mockExtendedAdapterPath(): string | undefined {\n return siblingFile('../setup/mock-extended.js')\n}\n\n/**\n * The ABSOLUTE path of the Vitest fork, resolved from sentinel rather than named.\n *\n * A bare specifier in an alias is resolved by Vite against the IMPORTER, which here is a spec file\n * inside the adopted module. `vitest-mock-extended` is sentinel's dependency, not the module's, so\n * under pnpm's isolated layout the module cannot see it and the alias points at nothing.\n *\n * Measured on `apps/cloud/agency`: three suites died with \"Cannot find package\n * 'jest-mock-extended'\" while the fork sat in the store, installed and unreachable. Same class of\n * defect as the axios alias below, and the same fix: resolve it where it actually is.\n *\n * Falls back to the bare name, which keeps the previous behaviour rather than throwing while a\n * config is being built.\n */\nfunction vitestMockExtendedPath(): string {\n /*\n * `import.meta.resolve` and not `createRequire().resolve`: the latter follows the `require`\n * condition and hands back the package's CJS entry, whose first line requires `vitest`. Vitest\n * refuses that outright (\"Vitest cannot be imported in a CommonJS module using require()\"), so\n * the alias resolved correctly and then failed one step later. Measured on `apps/cloud/agency`.\n */\n try {\n return fileURLToPath(import.meta.resolve('vitest-mock-extended'))\n } catch {\n try {\n return createRequire(import.meta.url).resolve('vitest-mock-extended')\n } catch {\n return 'vitest-mock-extended'\n }\n }\n}\n\n/**\n * The ONE msw copy a module's tests share with the server that listens.\n *\n * sentinel ships msw so that a module stands alone, and the codemod repoints the SPEC files to it.\n * Helpers are not spec files, so they keep `import { rest } from 'msw'` and build their handlers\n * with the WORKSPACE's copy while the server that listens was built with sentinel's. Same version,\n * two physical instances, and a handler registered on one is invisible to the other.\n *\n * Measured on `apps/cloud/shift-offer`, whose `setup-mocks.ts` registers every handler its suites\n * rely on: 19 tests failed on `captured a request without a matching request handler` for a URL the\n * helper had a handler for. The repo has 159 non-spec files importing msw, and 2 importing the\n * workspace's server directly, so this is not one module's habit.\n *\n * Aliased rather than codemodded, for the same reason as `jest-mock-extended` above: it lets an\n * UNMIGRATED shared helper keep working inside a migrated module, which is what makes the\n * per-module plan possible at all. `^msw$` only: `msw/node` stays the package's own, and it is\n * sentinel's setup that imports it.\n */\nfunction mswAliases(): Alias[] {\n const server = siblingFile('../setup/msw-server.js')\n const own = resolveFrom('msw')\n return [\n ...(own === undefined ? [] : [{ find: /^msw$/, replacement: own }]),\n ...(server === undefined ? [] : [{ find: /^@hublo\\/test\\/msw\\/server$/, replacement: server }]),\n ]\n}\n\n/**\n * One PHYSICAL axios, resolved from the MODULE and not from sentinel.\n *\n * pnpm installs axios twice here, same 1.17.0, differing only by a peer. The service client\n * resolves one copy and a provider's `import { AxiosError } from 'axios'` the other, so\n * `if (!(error instanceof AxiosError)) throw error` answers false on a genuine axios error and\n * rethrows it raw. The test then fails on the WRAPPER being absent, naming nothing near the cause.\n *\n * `resolve.dedupe` does NOT fix this, and neither does `deps.inline`: dedupe settles bare\n * specifiers, not two physical paths. The alias has to name the path.\n *\n * The module's OWN `node_modules`, checked directly rather than resolved.\n *\n * `createRequire(...).resolve()` was the first version and it is wrong twice. It walks UP the\n * tree, so a module without axios would be aliased to a PARENT's copy, which is the opposite of\n * pinning its own. And because it consults ambient resolution, the answer depends on where the\n * process runs: it correctly found nothing from a plain CLI run and found something under Vitest,\n * which is how a test of it became a test of its environment.\n *\n * pnpm links a module's direct dependencies into `<module>/node_modules`, so that path is both the\n * right answer and a deterministic one.\n *\n * Measured on `mission`: 378 passing suites to 381 of 387.\n *\n * ## ⚠️ And the workspace root when the module has none of its own\n *\n * The first version stopped there and returned nothing for a module without its own axios, on the\n * reasoning that pinning it to a PARENT's copy is the opposite of pinning its own. That reasoning\n * has an blind spot: a module with no copy of its own does not thereby have no axios PROBLEM. It\n * receives one through its dependencies, and it is still importing it two ways.\n *\n * Measured in `apps/nest/backends-for-frontends/backoffice`, which declares no axios, under this\n * very preset with no alias:\n *\n * the ESM AxiosError === the CJS AxiosError false\n * a CJS error instanceof the CJS class true\n * a CJS error instanceof the ESM class FALSE\n *\n * `@nestjs/axios` is externalised, so Node loads it and it throws the CJS class; the provider does\n * `import { AxiosError } from 'axios'` through Vite and gets the ESM one. Its\n * `if (error instanceof AxiosError) throw new RemoteBackendException(...)` therefore answers false\n * on a genuine axios error and rethrows it raw, and the test fails on the WRAPPER being absent,\n * naming nothing near the cause. Four modules reported it: `admin`, `backoffice`, `mission`,\n * `institution`.\n *\n * So: the module's own copy first, the workspace root second, nearest wins. That is the same shape\n * as `luxonAlias` above, and for the same reason: when a module has nothing of its own, the copy\n * it actually uses is the one to pin. Absent axios everywhere is still not an error.\n */\n/**\n * ONE physical luxon, for the same reason as axios: `Settings` is per-copy state.\n *\n * The setup pins `Settings.defaultZone = 'utc'`, which the root jest setup did for every suite in\n * the repo. It was reaching the copy `require` resolves, the CJS build, while the tests import the\n * one Vite resolves, the ESM build. Two instances, two `Settings`, and the pin landed on the one\n * nobody read.\n *\n * Measured on `libs/cloud/events-notifications`: the suite ran in `Europe/Paris`, so every date\n * came out two hours off and 12 assertions compared timestamps that differed by exactly that.\n *\n * luxon sits at the workspace root here rather than in each module, so both places are checked,\n * nearest first. Absent luxon is not an error: most modules have none.\n */\nfunction luxonAlias(root: string, workspaceRoot: string): Alias[] {\n for (const base of [root, workspaceRoot]) {\n const manifest = join(base, 'node_modules', 'luxon', 'package.json')\n if (existsSync(manifest)) return [{ find: /^luxon$/, replacement: dirname(manifest) }]\n }\n return []\n}\n\nfunction axiosAlias(root: string, workspaceRoot: string): Alias[] {\n for (const base of [root, workspaceRoot]) {\n const manifest = join(base, 'node_modules', 'axios', 'package.json')\n if (existsSync(manifest)) return [{ find: /^axios$/, replacement: dirname(manifest) }]\n }\n return []\n}\n\n/**\n * A generated Prisma client's runtime, resolved to the file the package actually ships.\n *\n * The generated client reaches its own runtime by package name, and the package's `exports` map\n * answers differently depending on which condition asks:\n *\n * \"./runtime/library\": { \"require\": \"./runtime/library.js\", // shipped\n * \"import\": \"./runtime/library.mjs\" } // NOT shipped\n *\n * jest asked as CJS and got the file. Vitest resolves the same specifier under `import`, is sent\n * to a `.mjs` that does not exist, and the spec file does not load at all, so its tests go missing\n * rather than failing. Node itself spells the answer out: \"Did you mean to import\n * .../runtime/library.js?\".\n *\n * Measured on `apps/nest/microservices/client-management` (1 file) and\n * `apps/nest/microservices/institution` (3 files), on two different generated clients. This repo\n * generates 31 of them, all with the same manifest, so this is a property of the generator rather\n * than of a module.\n *\n * Written as a pattern with a back-reference, not one entry per client: the module does not know\n * which clients its dependencies pull in, and enumerating 31 names would go stale the day a\n * thirty-second schema is added.\n *\n * ⚠️ Not a workaround for a mistake of ours. The package declares a target it does not ship, and\n * pointing at the shipped file is what `require` already did. Nothing else changes: the `.js` IS\n * the runtime, in the same package, at the version installed.\n */\nfunction prismaRuntimeAlias(workspaceRoot: string): Alias[] {\n const prisma = join(workspaceRoot, 'node_modules', '@prisma')\n if (!existsSync(prisma)) return []\n\n return [\n {\n find: /^@prisma\\/([^/]+)\\/runtime\\/library$/,\n replacement: join(prisma, '$1', 'runtime', 'library.js'),\n },\n ]\n}\n\n/**\n * The base every Nest module gets. Four decisions, each with its measurement.\n */\nfunction baseConfig(options: NestTestOptions): ViteUserConfig {\n const { root, workspaceRoot } = options\n\n return {\n /*\n * NO decorator-metadata plugin, and that is a measured removal rather than an omission.\n *\n * Nest reads constructor parameter types from `emitDecoratorMetadata`, so this preset used to\n * run the BUILD role's SWC plugin to emit it, on the premise that the bundler does not. That\n * premise was true of esbuild and is false of Vite 8, which transforms with oxc: oxc lowers the\n * decorators itself and emits the metadata, provided a tsconfig with `experimentalDecorators`\n * applies to the file.\n *\n * Measured five times before removing it. On `apps/nest/microservices/mission`, with and\n * without the plugin, the output is strictly identical for `@Injectable()` constructors\n * (including a service caught in a two-file import CYCLE, all 8 parameters resolved), for route\n * handler parameters and return types, and for DTO properties; the suite then passes in 98s\n * instead of 249s. Confirmed by a direct `Reflect.getMetadata` probe on\n * `apps/nest/backends-for-frontends/admin` and on `libs/nest/starter`, and by two A/B runs\n * through this very preset: `agency-notification` (100 tests) identical with and without, and\n * `grid-leave` (3335 tests) identical without.\n *\n * One caveat that the same measurement produced, and which belongs to the CONSUMER rather than\n * here: a property typed by an interface or by a type-alias imported with `import type` emits\n * `Object`. That is `emitDecoratorMetadata`'s own behaviour, identical with and without the\n * plugin, not a Vite regression.\n *\n * The BUILD role keeps its plugin. Its context differs, it feeds a shipped artefact, and\n * removing it there needs its own proof.\n *\n * ⚠️ ONE construct escapes oxc, and the exception is why this line is a condition rather than\n * an empty array: a decorator on an `abstract` class member. swc emitted it, oxc erases the\n * member and the decorator with it, silently. There is exactly one such member in this repo,\n * so `lowerDecoratorsWithTypeScript` buys the plugin back for that module alone.\n */\n plugins: options.lowerDecoratorsWithTypeScript ? [decoratorMetadata({ root })] : [],\n\n /*\n * The proviso in the paragraph above, made unconditional.\n *\n * oxc lowers decorators from the tsconfig that applies to the file, so a file belonging to NO\n * tsconfig `include` is lowered as if it used the STANDARD decorators, and comes out of Vite as\n * invalid JavaScript. Not merely without metadata: the file fails to parse, and every file that\n * imports it disappears with it.\n *\n * Measured on `apps/nest/microservices/mission`, where ONE uncovered helper,\n * `src/app/test/mission.test-wrapper.ts`, took down 158 of 416 test files. Reproduced on a\n * three-file case: covered file fine, uncovered file `SyntaxError: Invalid or unexpected\n * token`, and this option alone turns it into the same output the covered file gets, metadata\n * included.\n *\n * Declared here rather than left to each module's tsconfig `include`, because the alternative\n * is asking 107 teams to find which of their files no tsconfig covers, which is the work this\n * role exists to do for them.\n *\n * Two things the same measurement established, both deliberate:\n *\n * - it OVERRIDES the tsconfig, it is not a default the tsconfig refines. A file under a\n * tsconfig saying `emitDecoratorMetadata: false` gets metadata anyway. Acceptable because\n * this is the NEST preset and a Nest module is legacy decorators by definition: no tsconfig\n * under `apps/nest`, `libs/nest`, `apps/cloud` or `libs/cloud` sets either option to false.\n * - it needs no polyfill. Without `reflect-metadata` loaded, nothing throws, the metadata is\n * simply unreadable, exactly as before.\n */\n oxc: { decorator: { legacy: true, emitDecoratorMetadata: true } },\n\n resolve: {\n alias: [\n ...axiosAlias(root, workspaceRoot),\n ...luxonAlias(root, workspaceRoot),\n ...prismaRuntimeAlias(workspaceRoot),\n ...mswAliases(),\n /*\n * `jest-mock-extended` loads `@jest/globals`, which refuses to run outside jest. 2491\n * files import it, 104 of them under `libs/` as SHARED helpers, so migrating those helpers\n * breaks every module still on jest and leaving them breaks every module moved to Vitest.\n * Old and new therefore do not cohabit on shared helpers, which would have killed the\n * per-module plan.\n *\n * This one line removes the constraint: an unmigrated helper resolves to the Vitest fork\n * inside an adopted module and keeps resolving to the jest one everywhere else.\n * `vitest-mock-extended@5.1.1` is a fork of the same package and exports the same names.\n *\n * Measured on `mission`: failing suites went from 159 to 10.\n */\n {\n find: /^jest-mock-extended$/,\n replacement: mockExtendedAdapterPath() ?? vitestMockExtendedPath(),\n },\n ...tsconfigAliases(workspaceRoot),\n ],\n },\n\n test: {\n globals: true,\n environment: 'node',\n root,\n /*\n * Both extensions, because jest ran both. `**\\/*.spec.ts` alone read green while missing\n * three `.test.ts` files and 23 tests, with nothing saying so: a counter does not compare to\n * itself, it compares to the reference.\n */\n include: ['**/*.spec.ts', '**/*.test.ts'],\n /*\n * Kept, and it is not a performance knob. Nest registers metadata as an import SIDE EFFECT:\n * a decorator writes into a catalog when its module loads. Sharing a module registry across\n * files lets one suite see what another registered, and the failure appears in whichever\n * file happens to run second.\n */\n isolate: true,\n },\n }\n}\n\n/**\n * Vite accepts `alias` as an ARRAY or as an object map, and the two do not combine.\n *\n * The array form is the one that matters here: only it takes a RegExp `find`, which both the axios\n * and the `jest-mock-extended` aliases need. A module overriding with the object form gets its\n * entries converted rather than dropped, because silently losing an override is worse than a shape\n * the caller did not expect.\n */\nfunction asAliasArray(alias: AliasOptions | undefined): ViteAlias[] {\n if (alias === undefined) return []\n if (Array.isArray(alias)) return alias as ViteAlias[]\n return Object.entries(alias as Record<string, string>).map(([find, replacement]) => ({\n find,\n replacement,\n }))\n}\n\n/** The config, with the module's own overrides merged over it. */\nexport function nestTestConfig(options: NestTestOptions): ViteUserConfig {\n const base = baseConfig(options)\n if (options.overrides === undefined) return base\n // Shallow by section rather than a deep merge helper: the three sections a module overrides in\n // practice are `test`, `resolve` and `plugins`, and a deep merge would silently concatenate\n // arrays a module meant to replace.\n return {\n ...base,\n ...options.overrides,\n plugins: [...(base.plugins ?? []), ...(options.overrides.plugins ?? [])],\n resolve: {\n ...base.resolve,\n ...options.overrides.resolve,\n // The module's own aliases come AFTER sentinel's, so a module that needs to win can.\n alias: [\n ...asAliasArray(base.resolve?.alias),\n ...asAliasArray(options.overrides.resolve?.alias),\n ],\n },\n test: { ...base.test, ...options.overrides.test },\n }\n}\n"],"mappings":"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","names":[]}
@@ -1,2 +1,64 @@
1
1
  export { ConfigEnv, TestUserConfig, ViteUserConfig, defineConfig, mergeConfig } from 'vitest/config';
2
+ import { Plugin } from 'vite';
2
3
  export { loadEnv } from 'vite';
4
+
5
+ /**
6
+ * Resolve packages the way jest did, for a suite that was written against jest's resolution.
7
+ *
8
+ * ## What jest resolved, measured rather than remembered
9
+ *
10
+ * Every suite in this repo ran through `@nx/jest/preset`, whose
11
+ * `testEnvironmentOptions.customExportConditions` is exactly `['node', 'require', 'default']`
12
+ * (read off the installed preset, not off its documentation). `development` was never in that
13
+ * list, so a package shipping a separate development build behind that condition loaded its
14
+ * PRODUCTION file under jest.
15
+ *
16
+ * Vitest resolves with Vite's conditions instead, and its default carries the
17
+ * `development|production` token. Measured on a bare config, the resolved list is
18
+ * `['node', 'development|production']`. `@emotion/cache` then loads
19
+ * `emotion-cache.development.cjs.js`, whose extra stylis plugin calls
20
+ * `console.error(':first-child is potentially unsafe...')`.
21
+ *
22
+ * With `jest-fail-on-console` in the setup, that console call IS a failure: **100 tests over 26
23
+ * files on `apps/front/front-legacy`**, every one of them green under jest. `libs/front/components`
24
+ * hits the same root cause on one test.
25
+ *
26
+ * ## Why a plugin and not a config line
27
+ *
28
+ * Declaring `ssr.resolve.conditions: ['node']` does nothing: Vite merges config arrays by
29
+ * CONCATENATION, so Vitest's default is appended straight back. The token has to come off the
30
+ * RESOLVED config, which is what `configResolved` is for.
31
+ *
32
+ * This is the same shape as the other thing that cannot be declared: a module cannot reproduce
33
+ * `customExportConditions` either, because the list it asks for is PREFIXED to Vite's defaults
34
+ * rather than substituted for them.
35
+ *
36
+ * ## The three lists are one array
37
+ *
38
+ * Measured on Vite 8: `config.resolve.conditions`, `config.ssr.resolve.conditions` and
39
+ * `config.environments.ssr.resolve.conditions` are the SAME array object, so stripping one strips
40
+ * all three. All three are stripped anyway. Relying on an aliasing that nothing promises is how a
41
+ * silent regression arrives on a Vite upgrade, and the cost of being explicit is two lines.
42
+ *
43
+ * ## ⚠️ What this deliberately hides, said out loud
44
+ *
45
+ * emotion's warning is REAL: `libs/front/components` is consumed by a Next app, and a
46
+ * `:first-child` selector is genuinely unsafe when the markup is rendered server-side. Restoring
47
+ * jest's resolution puts that warning back out of sight.
48
+ *
49
+ * It is hidden here on purpose all the same, because a migration that also turns 100 green tests
50
+ * red cannot be told apart from a migration that broke something. The baseline gate compares test
51
+ * names, and it has no way to know which reds are progress. The selector belongs to the module's
52
+ * owners as its own piece of work, with its own ticket, not as a side effect of changing runner.
53
+ */
54
+
55
+ /**
56
+ * Add to `plugins` in a module whose suite was written against jest's resolution.
57
+ *
58
+ * Not applied by the preset for everyone: it is a MIGRATION aid, and a module that was always on
59
+ * Vitest never had jest's resolution to go back to. A module adopts it the day it migrates, and
60
+ * can drop it the day its suite no longer depends on the production build.
61
+ */
62
+ declare function jestExportConditions(): Plugin;
63
+
64
+ export { jestExportConditions };
@@ -1,8 +1,30 @@
1
1
  // src/roles/test/react/toolchain.ts
2
2
  import { defineConfig, mergeConfig } from "vitest/config";
3
3
  import { loadEnv } from "vite";
4
+
5
+ // src/roles/test/react/jest-export-conditions.ts
6
+ var ABSENT_UNDER_JEST = /* @__PURE__ */ new Set(["development", "development|production"]);
7
+ function stripInPlace(conditions) {
8
+ if (conditions === void 0) return;
9
+ for (let index = conditions.length - 1; index >= 0; index -= 1) {
10
+ const condition = conditions[index];
11
+ if (condition !== void 0 && ABSENT_UNDER_JEST.has(condition)) conditions.splice(index, 1);
12
+ }
13
+ }
14
+ function jestExportConditions() {
15
+ return {
16
+ name: "sentinel:jest-export-conditions",
17
+ configResolved(config) {
18
+ const environments = config;
19
+ stripInPlace(config.resolve?.conditions);
20
+ stripInPlace(config.ssr?.resolve?.conditions);
21
+ stripInPlace(environments.environments?.ssr?.resolve?.conditions);
22
+ }
23
+ };
24
+ }
4
25
  export {
5
26
  defineConfig,
27
+ jestExportConditions,
6
28
  loadEnv,
7
29
  mergeConfig
8
30
  };
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../src/roles/test/react/toolchain.ts","../../../../src/roles/test/react/jest-export-conditions.ts"],"sourcesContent":["/**\n * What `@hublo/sentinel/test/react` gives an adopted module.\n *\n * A re-export surface, not a shape, and that is the measured answer rather than a preference.\n * The six configs already on Vitest fall into two groups: three React apps that converge on one\n * skeleton, and three modules of twelve to twenty lines that share almost nothing. A shared form\n * would serve half of them.\n *\n * More decisive: those configs carry decisions their owners made and justified in comments, a\n * `pool: 'threads'` measured on 1100 test files, an `experimental.fsModuleCache` measured against\n * transform and import time. Moving them is not a tooling migration, so the role moves the\n * imports and leaves every line of judgement where it is.\n *\n * Its own entry point, like the build ones, so importing it from a config never drags the CLI\n * and its adapters into a test run.\n */\n\n/**\n * From `vitest/config`, and NOT from `vite`, which also exports a `defineConfig`.\n *\n * They are different functions: measured, `vitest/config`'s `defineConfig` is not the same object\n * as vite's, and only it knows the `test` key. A config built with vite's would load and quietly\n * drop its entire test block. `mergeConfig` IS the same function in both, so its source does not\n * matter and it is taken from here for consistency.\n */\nexport { defineConfig, mergeConfig } from 'vitest/config'\n\n/**\n * From `vite`, because `vitest/config` does not export it. Measured, not assumed.\n *\n * Two of the six configs build their test env with it. Re-exported rather than claimed: the\n * `vite` package belongs to the build role, and a module adopting only this one keeps its own.\n */\nexport { loadEnv } from 'vite'\n\n/**\n * The names `vitest/config` actually exports, checked against its own declarations rather than\n * guessed: vite's `UserConfig` is re-exported there AS `ViteUserConfig`, and the test-side one is\n * `TestUserConfig`. Importing `UserConfig` from it compiles nowhere.\n */\nexport type { ConfigEnv, TestUserConfig, ViteUserConfig } from 'vitest/config'\n\n/**\n * The one thing this surface ADDS rather than re-exports, and only because a module cannot write\n * it for itself: Vite appends its own export conditions back over anything a config declares, so\n * restoring jest's resolution takes a plugin. Its own file carries the measurement.\n */\nexport { jestExportConditions } from './jest-export-conditions.js'\n","/**\n * Resolve packages the way jest did, for a suite that was written against jest's resolution.\n *\n * ## What jest resolved, measured rather than remembered\n *\n * Every suite in this repo ran through `@nx/jest/preset`, whose\n * `testEnvironmentOptions.customExportConditions` is exactly `['node', 'require', 'default']`\n * (read off the installed preset, not off its documentation). `development` was never in that\n * list, so a package shipping a separate development build behind that condition loaded its\n * PRODUCTION file under jest.\n *\n * Vitest resolves with Vite's conditions instead, and its default carries the\n * `development|production` token. Measured on a bare config, the resolved list is\n * `['node', 'development|production']`. `@emotion/cache` then loads\n * `emotion-cache.development.cjs.js`, whose extra stylis plugin calls\n * `console.error(':first-child is potentially unsafe...')`.\n *\n * With `jest-fail-on-console` in the setup, that console call IS a failure: **100 tests over 26\n * files on `apps/front/front-legacy`**, every one of them green under jest. `libs/front/components`\n * hits the same root cause on one test.\n *\n * ## Why a plugin and not a config line\n *\n * Declaring `ssr.resolve.conditions: ['node']` does nothing: Vite merges config arrays by\n * CONCATENATION, so Vitest's default is appended straight back. The token has to come off the\n * RESOLVED config, which is what `configResolved` is for.\n *\n * This is the same shape as the other thing that cannot be declared: a module cannot reproduce\n * `customExportConditions` either, because the list it asks for is PREFIXED to Vite's defaults\n * rather than substituted for them.\n *\n * ## The three lists are one array\n *\n * Measured on Vite 8: `config.resolve.conditions`, `config.ssr.resolve.conditions` and\n * `config.environments.ssr.resolve.conditions` are the SAME array object, so stripping one strips\n * all three. All three are stripped anyway. Relying on an aliasing that nothing promises is how a\n * silent regression arrives on a Vite upgrade, and the cost of being explicit is two lines.\n *\n * ## ⚠️ What this deliberately hides, said out loud\n *\n * emotion's warning is REAL: `libs/front/components` is consumed by a Next app, and a\n * `:first-child` selector is genuinely unsafe when the markup is rendered server-side. Restoring\n * jest's resolution puts that warning back out of sight.\n *\n * It is hidden here on purpose all the same, because a migration that also turns 100 green tests\n * red cannot be told apart from a migration that broke something. The baseline gate compares test\n * names, and it has no way to know which reds are progress. The selector belongs to the module's\n * owners as its own piece of work, with its own ticket, not as a side effect of changing runner.\n */\nimport type { Plugin } from 'vite'\n\n/**\n * The tokens jest never had.\n *\n * Both spellings, because the resolved config carries `development|production` (Vite's own\n * placeholder, replaced per environment) while a config written by hand may carry `development`.\n */\nconst ABSENT_UNDER_JEST = new Set(['development', 'development|production'])\n\n/** Remove them in place, since the resolved config is what the resolver will read. */\nfunction stripInPlace(conditions: string[] | undefined): void {\n if (conditions === undefined) return\n\n for (let index = conditions.length - 1; index >= 0; index -= 1) {\n const condition = conditions[index]\n if (condition !== undefined && ABSENT_UNDER_JEST.has(condition)) conditions.splice(index, 1)\n }\n}\n\n/**\n * Add to `plugins` in a module whose suite was written against jest's resolution.\n *\n * Not applied by the preset for everyone: it is a MIGRATION aid, and a module that was always on\n * Vitest never had jest's resolution to go back to. A module adopts it the day it migrates, and\n * can drop it the day its suite no longer depends on the production build.\n */\nexport function jestExportConditions(): Plugin {\n return {\n name: 'sentinel:jest-export-conditions',\n configResolved(config) {\n const environments = config as unknown as {\n environments?: { ssr?: { resolve?: { conditions?: string[] } } }\n }\n\n stripInPlace(config.resolve?.conditions as string[] | undefined)\n stripInPlace(config.ssr?.resolve?.conditions as string[] | undefined)\n stripInPlace(environments.environments?.ssr?.resolve?.conditions)\n },\n }\n}\n"],"mappings":";AAyBA,SAAS,cAAc,mBAAmB;AAQ1C,SAAS,eAAe;;;ACwBxB,IAAM,oBAAoB,oBAAI,IAAI,CAAC,eAAe,wBAAwB,CAAC;AAG3E,SAAS,aAAa,YAAwC;AAC5D,MAAI,eAAe,OAAW;AAE9B,WAAS,QAAQ,WAAW,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG;AAC9D,UAAM,YAAY,WAAW,KAAK;AAClC,QAAI,cAAc,UAAa,kBAAkB,IAAI,SAAS,EAAG,YAAW,OAAO,OAAO,CAAC;AAAA,EAC7F;AACF;AASO,SAAS,uBAA+B;AAC7C,SAAO;AAAA,IACL,MAAM;AAAA,IACN,eAAe,QAAQ;AACrB,YAAM,eAAe;AAIrB,mBAAa,OAAO,SAAS,UAAkC;AAC/D,mBAAa,OAAO,KAAK,SAAS,UAAkC;AACpE,mBAAa,aAAa,cAAc,KAAK,SAAS,UAAU;AAAA,IAClE;AAAA,EACF;AACF;","names":[]}
@@ -0,0 +1,46 @@
1
+ import * as mockExtended from 'vitest-mock-extended';
2
+ export * from 'vitest-mock-extended';
3
+ export { any, anyArray, anyBoolean, anyFunction, anyMap, anyNumber, anyObject, anySet, anyString, anySymbol, arrayIncludes, calledWithFn, captor, isA, isMockObject, mapHas, matches, mockClear, mockFn, mockReset, mocked, mockedFn, notEmpty, notNull, notUndefined, objectContainsKey } from 'vitest-mock-extended';
4
+
5
+ /**
6
+ * `jest-mock-extended`'s deep mocks, made answerable to the question Vitest's `spyOn` asks.
7
+ *
8
+ * A deep mock is a Proxy that invents a property the first time something READS it. Nothing exists
9
+ * until then, so the object reports itself empty:
10
+ *
11
+ * const provider = mockDeep<InstitutionProvider>()
12
+ * 'getAdminFirstAndLastNames' in provider // false
13
+ * Object.getOwnPropertyDescriptor(provider, 'getAdmin...') // undefined
14
+ * typeof provider.getAdminFirstAndLastNames // 'function', and now it exists
15
+ *
16
+ * jest's `spyOn` reads the property, so the Proxy created it and the spy worked. Vitest's asks the
17
+ * object whether it HAS the property first, gets no for both questions, and throws
18
+ * `The property "getAdminFirstAndLastNames" is not defined on the object.` The file then reports no
19
+ * test at all, so its names simply go missing rather than failing.
20
+ *
21
+ * Measured on `apps/cloud/shift`: one such spy took 37 of its 108 tests away. The repo has 128 of
22
+ * them across 29 files, mostly in `apps/nest/microservices` and
23
+ * `apps/nest/backends-for-frontends`.
24
+ *
25
+ * ## Why it is fixed here and not in the 128 test files
26
+ *
27
+ * Because it is a difference between two runners, not something 29 suites each got wrong. A
28
+ * codemod rewriting every site would put a migration artefact in front of every reader of those
29
+ * files forever, and teams would carry it. One adapter keeps the test files exactly as they are.
30
+ *
31
+ * The two traps answer by doing what jest's `spyOn` did: read the property once, then answer. The
32
+ * read is the Proxy's own documented way of materialising it, so nothing here reimplements the
33
+ * mock, it only asks the question in the form the underlying object understands.
34
+ *
35
+ * `then` is never materialised. A deep mock that suddenly HAS a `then` is a thenable, and awaiting
36
+ * it, or returning it from an async function, would hang on a promise nothing resolves.
37
+ */
38
+
39
+ /** Reset every deep mock this adapter created, the way jest's registry did. */
40
+ declare function resetDeepMocks(): void;
41
+ /** Clear their calls without touching their implementations. */
42
+ declare function clearDeepMocks(): void;
43
+ declare const mock: typeof mockExtended.mock;
44
+ declare const mockDeep: typeof mockExtended.mockDeep;
45
+
46
+ export { clearDeepMocks, mock, mockDeep, resetDeepMocks };
@@ -0,0 +1,65 @@
1
+ import {
2
+ any,
3
+ anyArray,
4
+ anyBoolean,
5
+ anyFunction,
6
+ anyMap,
7
+ anyNumber,
8
+ anyObject,
9
+ anySet,
10
+ anyString,
11
+ anySymbol,
12
+ arrayIncludes,
13
+ calledWithFn,
14
+ captor,
15
+ clearDeepMocks,
16
+ isA,
17
+ isMockObject,
18
+ mapHas,
19
+ matches,
20
+ mock,
21
+ mockClear,
22
+ mockDeep,
23
+ mockFn,
24
+ mockReset,
25
+ mocked,
26
+ mockedFn,
27
+ notEmpty,
28
+ notNull,
29
+ notUndefined,
30
+ objectContainsKey,
31
+ resetDeepMocks
32
+ } from "../../../chunk-2XLX6PFR.js";
33
+ export {
34
+ any,
35
+ anyArray,
36
+ anyBoolean,
37
+ anyFunction,
38
+ anyMap,
39
+ anyNumber,
40
+ anyObject,
41
+ anySet,
42
+ anyString,
43
+ anySymbol,
44
+ arrayIncludes,
45
+ calledWithFn,
46
+ captor,
47
+ clearDeepMocks,
48
+ isA,
49
+ isMockObject,
50
+ mapHas,
51
+ matches,
52
+ mock,
53
+ mockClear,
54
+ mockDeep,
55
+ mockFn,
56
+ mockReset,
57
+ mocked,
58
+ mockedFn,
59
+ notEmpty,
60
+ notNull,
61
+ notUndefined,
62
+ objectContainsKey,
63
+ resetDeepMocks
64
+ };
65
+ //# sourceMappingURL=mock-extended.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,16 @@
1
+ import * as msw_node from 'msw/node';
2
+ import 'msw';
3
+
4
+ /**
5
+ * No default handlers, deliberately.
6
+ *
7
+ * The repo's own server had `defaultHandlers = []` too, so nothing is lost. A handler here would
8
+ * be a Hublo endpoint inside a package meant to be usable by anyone, which is the line between
9
+ * what sentinel owns (the config) and what an app owns (what it mocks).
10
+ *
11
+ * A suite adds its own with `server.use(...)`, which is how all 600 files that touch this already
12
+ * work.
13
+ */
14
+ declare const server: msw_node.SetupServer;
15
+
16
+ export { server };
@@ -0,0 +1,12 @@
1
+ import {
2
+ installMswLifecycle,
3
+ server
4
+ } from "../../../chunk-PWV3BMDA.js";
5
+
6
+ // src/roles/test/setup/msw-lifecycle.ts
7
+ import { afterAll, afterEach, beforeAll } from "vitest";
8
+ installMswLifecycle({ beforeAll, afterEach, afterAll });
9
+ export {
10
+ server
11
+ };
12
+ //# sourceMappingURL=msw-lifecycle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../src/roles/test/setup/msw-lifecycle.ts"],"sourcesContent":["/**\n * The msw lifecycle, as its OWN setup file, loaded only by a module that uses msw.\n *\n * ## Why it moved out of the shared setup\n *\n * It used to run for every adopted module, and that was wrong in a way only a real module could\n * show. Installing msw means patching `http` and `https` and refusing any request that matches no\n * handler. A module that never asked for msw gets that refusal anyway, and its own traffic is what\n * pays.\n *\n * Measured on `libs/nest/starter`, whose suite talks to a Fastify server it starts itself:\n *\n * its own config 11 of 11\n * the same config plus the shared setup 10 of 11, `TypeError: Invalid URL`\n * inside @mswjs/interceptors' fetch interceptor\n *\n * That module has no msw handler anywhere. The failing test does\n * `new URL(`${prefix}/events`, await app.getUrl())` and fetches its own server, and the\n * interceptor cannot read that URL.\n *\n * So the rule is the one asked for at the start: intelligent per module, decided from what the\n * module's files actually contain, not applied to everybody because it is convenient.\n *\n * ## What it does NOT change\n *\n * The lifecycle itself is untouched: `listen` at evaluation time AND in `beforeAll`,\n * `onUnhandledRequest: 'error'` carried across, no `resetHandlers`. `./msw.js` carries the\n * measurement for each of those.\n */\nimport { afterAll, afterEach, beforeAll } from 'vitest'\n\nimport { installMswLifecycle, server } from './msw.js'\n\ninstallMswLifecycle({ beforeAll, afterEach, afterAll })\n\n/**\n * Re-exported so a suite can add its own handlers, which is how all 600 files that touch msw here\n * already work: `server.use(...)` inside a test.\n */\nexport { server }\n"],"mappings":";;;;;;AA6BA,SAAS,UAAU,WAAW,iBAAiB;AAI/C,oBAAoB,EAAE,WAAW,WAAW,SAAS,CAAC;","names":[]}
@@ -0,0 +1,3 @@
1
+ export { server } from './msw-lifecycle.js';
2
+ export * from 'msw';
3
+ import 'msw/node';
@@ -0,0 +1,10 @@
1
+ import {
2
+ server
3
+ } from "../../../chunk-PWV3BMDA.js";
4
+
5
+ // src/roles/test/setup/msw-server.ts
6
+ export * from "msw";
7
+ export {
8
+ server
9
+ };
10
+ //# sourceMappingURL=msw-server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../src/roles/test/setup/msw-server.ts"],"sourcesContent":["/**\n * The msw server a migrated suite shares with its setup.\n *\n * ## Why this entry exists at all\n *\n * The setup beside it starts a server and the tests add handlers to one. If those are two different\n * instances, the one that listens has no handlers and every request is unhandled, which under\n * `onUnhandledRequest: 'error'` fails every test that touches HTTP.\n *\n * That is not hypothetical. Migrating `apps/cloud/agency` and running its suite produced exactly\n * it: 22 green tests under jest became 10 failures, with `[MSW] Cannot bypass a request when using\n * the \"error\" strategy`. The setup had been absorbed into sentinel while the 599 files that import\n * the server still pointed at the workspace's own instance.\n *\n * So the server is exported under its own name, and the codemod repoints those imports here. One\n * instance, listened to by the setup and used by the tests.\n */\nexport { server } from './msw.js'\n\n/**\n * msw's own API, re-exported, so a module gets its handlers from the SAME physical copy that\n * intercepts.\n *\n * sentinel ships msw as a dependency on purpose: a module must stand alone, and the workspace root\n * that used to provide it is going away. But a test that keeps `import { rest } from 'msw'` builds\n * its handlers with the WORKSPACE's copy while the server that listens is built with sentinel's.\n * Same version, two physical instances, one interceptor: the handler is never matched.\n *\n * Measured on `libs/cloud/shared`: the msw handler is never called and the test fails on\n * `expected \"vi.fn()\" to be called 1 times, but got 0 times`. Same family as the axios duplicate,\n * where `resolve.dedupe` changed nothing and only pointing at one physical path did.\n *\n * `export *` rather than a list: `rest` covers 669 of the 671 importing files here, but the\n * surface a test may need (`graphql`, `ctx`, the handler types) belongs to msw, not to a list\n * sentinel would have to keep in step. `setupServer` is not part of it: it lives in `msw/node`,\n * and the server is sentinel's to create.\n */\nexport * from 'msw'\n"],"mappings":";;;;;AAqCA,cAAc;","names":[]}
@@ -0,0 +1,2 @@
1
+
2
+ export { }