@hublo/sentinel 1.4.0-alpha.42 → 1.4.0-alpha.44

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.
@@ -15,10 +15,10 @@ import {
15
15
  palette,
16
16
  registerAdapters,
17
17
  resolve
18
- } from "../chunk-BF5SF6HQ.js";
18
+ } from "../chunk-ZSPDXVQL.js";
19
19
  import {
20
20
  resolveContext
21
- } from "../chunk-YHNQONE2.js";
21
+ } from "../chunk-RO626QTL.js";
22
22
  import "../chunk-O7REVMOC.js";
23
23
  import "../chunk-QXFCZON7.js";
24
24
  import {
@@ -145,7 +145,7 @@ async function runValidate(ctx) {
145
145
  );
146
146
  }
147
147
  if (!ctx.targets.includes("test")) return 1;
148
- const { validateModuleTests } = await import("../validate-AUKAA6EG.js");
148
+ const { validateModuleTests } = await import("../validate-UIOW5VEV.js");
149
149
  const outcomes = modules.flatMap(
150
150
  (module) => validateModuleTests(module.name, module.root)
151
151
  );
@@ -430,8 +430,25 @@ function baseConfig(options) {
430
430
  * The narrow form `/@hublo\/sentinel\/dist\/roles\//` was tried and does NOT work: the pin
431
431
  * itself lives in a shared chunk at the dist root, which stays external. It has to be the
432
432
  * whole package.
433
+ *
434
+ * ## ⚠️ And `vitest` itself, for the same reason one level further out
435
+ *
436
+ * Inlining sentinel is not enough: its own `import { expect } from 'vitest'` still leaves
437
+ * through Node, which loads a SECOND instance of the runner. The snapshot client is
438
+ * per-instance state, so the instance the test file uses was never set up for that file:
439
+ *
440
+ * Error: The snapshot state for '…/asyncapi.module.spec.ts' is not found.
441
+ * Did you call 'SnapshotClient.setup()'?
442
+ *
443
+ * Isolated to the smallest possible case: an EMPTY setup file under `node_modules` passes,
444
+ * and the same file containing nothing but `import { expect } from 'vitest'` fails, while
445
+ * the identical import from a setup file INSIDE the module passes. So it is the location,
446
+ * not the import, and the remedy has to reach the import too.
447
+ *
448
+ * Measured on `libs/nest/asyncapi`: sentinel alone 1 failed of 13, sentinel and `vitest`
449
+ * 13 passed, 525 ms against 570 ms. Not a trade.
433
450
  */
434
- server: { deps: { inline: [/@hublo\/sentinel/] } }
451
+ server: { deps: { inline: [/@hublo\/sentinel/, "vitest"] } }
435
452
  }
436
453
  };
437
454
  }
@@ -548,4 +565,4 @@ export {
548
565
  jestExportConditions,
549
566
  sharedTestConfig
550
567
  };
551
- //# sourceMappingURL=chunk-SOCLCHJK.js.map
568
+ //# sourceMappingURL=chunk-F53J6U2W.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/roles/test/shared-test-config.ts","../src/roles/test/isolation-split.ts","../src/roles/test/msw-server-redirect.ts","../src/roles/test/react/jest-export-conditions.ts"],"sourcesContent":["/**\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\nimport { asIsolationSplit } from './isolation-split.js'\nimport { mswServerRedirect } from './msw-server-redirect.js'\nimport { jestExportConditions } from './react/jest-export-conditions.js'\n\n/** Which family this config is for: it decides the three things that are not shared. */\nexport type TestFlavour = 'nest' | 'react'\n\nexport interface SharedTestOptions {\n /** Which family this config is for. It decides the three things that are not shared. */\n flavour?: TestFlavour\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 /**\n * Let the files of this suite share a worker instead of each getting its own registry.\n *\n * Off by default, which is Vitest's own default and jest's semantics. Turning it off buys the\n * whole of `./setup/file-boundary.js` and `./isolation-split.js`: measured on\n * `front-components`, 44.6s isolated against 11.0s split, with all 1418 tests green and the\n * same result across ten shuffled file orders.\n *\n * A module takes it deliberately, and proves it with `--validate`, because the switch is the one\n * thing in this preset that changes what a suite MEANS: a file can see what an earlier file left\n * behind, and only running the suite can say whether any of them does.\n */\n isolate?: 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 /*\n * ⚠️ Several candidates, because the EMITTED layout is not ours to predict. This file is imported\n * by two toolchains, so the bundler extracts it into a shared chunk at the package root, and it\n * does so even when it is declared as its own entry (the entry then only re-exports the chunk).\n * A single `import.meta.url`-relative path therefore resolves differently depending on where the\n * bundler decided to put the code.\n *\n * That is not theoretical: it silently broke the jest-mock-extended and msw-server adapters, and\n * `apps/cloud/shift-offer` went from 83/83 to 56/83 with \"The property X is not defined on the\n * object\" — a mockDeep shape error that names nothing near the cause. The alias had quietly\n * fallen back to the raw fork.\n *\n * So the file is LOOKED FOR rather than computed, and the first one that exists wins.\n */\n const candidates = [\n relative,\n `roles/test/${relative.replace(/^\\.\\//, '')}`,\n `../${relative.replace(/^\\.\\//, '')}`,\n ]\n for (const candidate of candidates) {\n try {\n const sibling = fileURLToPath(new URL(candidate, import.meta.url))\n if (existsSync(sibling)) return sibling\n } catch {\n continue\n }\n }\n return undefined\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 */\n/** sentinel's own server file, the one its setup starts listening on. */\nfunction mswServerFile(): string | undefined {\n return siblingFile('./setup/msw-server.js')\n}\n\nfunction mswAliases(): Alias[] {\n const server = mswServerFile()\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: SharedTestOptions): 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 /*\n * `jestExportConditions` is UNCONDITIONAL, and it is the one plugin every migrated module gets.\n *\n * jest resolved with `['node', 'require', 'default']`, read off the installed `@nx/jest/preset`\n * rather than off its documentation. `development` was never in that list, so a package\n * shipping a separate development build loaded its PRODUCTION file. Vite's own conditions carry\n * `development|production`, so `@emotion/cache` loads `emotion-cache.development.cjs.js`, whose\n * extra stylis plugin calls `console.error(':first-child is potentially unsafe...')`, and with\n * `jest-fail-on-console` in the setup that console call IS a failure.\n *\n * Measured on `libs/front/components`: 2 of its 3 remaining failures, both green again with\n * this. `apps/front/front-legacy` has 100 over 26 files from the same cause.\n *\n * Unconditional rather than reserved for a jsdom module, because it is not a statement about\n * the DOM: it is what this migration is for, running the suite the way the runner it was\n * written for ran it. It changes nothing for a module with no dual-published dependency, which\n * is why nest and cloud were measured identical without it.\n */\n plugins: [\n jestExportConditions(),\n /*\n * The alias beside it covers a file naming the workspace's msw server by its package\n * specifier. This covers the one that reaches the same module RELATIVELY, which an alias\n * cannot: `./server` is a spelling thousands of unrelated files use, so the redirect is\n * decided on the resolved path instead. See `msw-server-redirect.ts` for the measurement.\n */\n ...(mswServerFile() === undefined\n ? []\n : [mswServerRedirect(mswServerFile() as string, workspaceRoot)]),\n ...(options.lowerDecoratorsWithTypeScript ? [decoratorMetadata({ root })] : []),\n ],\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 /*\n * Nest only, and measured: 1186 files under `apps/nest` and `libs/nest` mention `@prisma/`,\n * and ZERO under `apps/front` and `libs/front`. A React module paying for an alias to a\n * client it never generates is noise in a file someone has to read.\n */\n ...(options.flavour === 'nest' ? 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 * What the repo's jest preset actually matched, copied rather than approximated:\n * `**\\/?(*.)+(spec|test).[jt]s?(x)`.\n *\n * Both NAMES, because jest ran both: `**\\/*.spec.ts` alone read green while missing three\n * `.test.ts` files and 23 tests, with nothing saying so. And all four EXTENSIONS, for the\n * same reason one notch further out. Measured over the repo's 7063 test files:\n *\n * .ts 5844 .tsx 1108 .js 99 .mjs/.cjs 12\n *\n * The `.ts`-only form cost nothing on nest and cloud, which have none of the others, and it\n * cost `libs/front/api` three files and 13 tests on the first front module it met. Half the\n * front's test files are `.tsx`.\n *\n * `.mjs` and `.cjs` are deliberately OUT: jest's `[jt]s?(x)` does not match them either, and\n * Vitest's own default include does. Running a file the reference never ran is as wrong as\n * skipping one it did.\n */\n include: ['**/*.spec.[jt]s?(x)', '**/*.test.[jt]s?(x)'],\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 /*\n * Nest only. A Nest module registers metadata as an import SIDE EFFECT, so sharing a module\n * registry across files lets one suite see what another registered. A React module has no\n * such catalog, and isolation is not free.\n */\n ...(options.flavour === 'nest' ? { isolate: true } : {}),\n /*\n * Concurrency, transposed from what this repo does today rather than chosen.\n *\n * Every jest target inherits `configurations.ci = { ci: true, runInBand: true }` from the\n * `@nx/jest:jest` key in `nx.json`, and CI invokes every test target with\n * `--configuration=ci`. So on CI every suite in this repo runs ONE FILE AT A TIME today. That\n * key belongs to the jest executor and cannot be touched, because the workspace is mixed: it\n * still serves the modules that have not moved.\n *\n * ⚠️ And it is CI-ONLY. A local run omits `--configuration=ci`, so jest runs files in\n * PARALLEL on a developer's machine. A flat `fileParallelism: false` here would make local\n * runs slower than jest, which loses something the module had. Hence the condition rather\n * than the constant: parallel locally, one file at a time on CI, which is jest on both sides.\n *\n * Measured on three files that each hold the clock for 400ms and record their interval:\n *\n * default files overlap, 402ms\n * fileParallelism: false no overlap, 1442ms\n * maxWorkers: 1 no overlap, 1423ms\n *\n * Both candidates give the property that matters. `fileParallelism` is the one that says what\n * the module MEANS (\"do not run my files at the same time\"); `maxWorkers` is a pool size, and\n * it is what a module asking for a CAP gets instead (three BFFs ask for 4).\n *\n * A module that declared its own concurrency overrides this, in both environments, exactly as\n * it does today.\n */\n /*\n * ⚠️ PARALLEL on CI too, since 26/09, and that reverses what the note above decided.\n *\n * The transposition was faithful and expensive. Measured on `libs/front/components`, 1418\n * tests green on every line, on the repo's own 4-core agents:\n *\n * jest, as CI runs it 158.7 s\n * vitest serial, the transposition 644.5 s 4.1x slower\n * vitest, 3 workers 324.7 s 2.05x slower\n *\n * Serialising costs vitest far more than it costs jest, because vitest re-evaluates the\n * module graph per file through Vite's SSR runner: 555 ms of `import` and 478 ms of\n * `environment` per file against 139 ms of actual tests. Parallelism does not remove that\n * cost, it pays it on several cores at once.\n *\n * Every other documented lever was measured and does nothing or breaks the suite:\n * `deps.optimizer.ssr`, `NODE_COMPILE_CACHE` and `fsModuleCache` all cache TRANSFORMATION\n * where the cost is EVALUATION; `pool: threads` is noise; `vmThreads` collapses into\n * `no-isolate` when serial and fails 473 tests; `--no-isolate` fails 351.\n *\n * ⚠️ And NO worker cap, which is deliberate after reading vitest's own sizing:\n *\n * getDefaultThreadsCount: config.watch ? max(numCpus / 2, 1) : max(numCpus - 1, 1)\n *\n * `cores - 1` is 3 on this repo's 4-core agents, which is exactly the pool the 324.7 s above\n * was measured with, and 9 on a 10-core laptop. Pinning a number would slow every developer\n * to serve the agent, and a percentage would only re-describe a default that already adapts.\n * So one variable changes here and the tool keeps sizing its own pool.\n *\n * Memory was the reason to hesitate and it was measured rather than assumed: a full jsdom\n * run holds 1.1 GB at three workers and 1.9 GB at six, on agents with 32 GB. The OOM this\n * repo suffered was three nx tasks near 7.45 GB each, which is not what a test worker costs.\n *\n * A module that declares its own concurrency still overrides this, as before.\n */\n fileParallelism: true,\n\n /*\n * ⚠️ Sentinel's own setup files go through VITE, not round it, and that is load-bearing.\n *\n * Vitest externalizes anything under `node_modules`, so an installed `@hublo/sentinel` is\n * loaded by Node while the module's own code is served by Vite. The two then hold DIFFERENT\n * instances of the same package, and a setup whose whole job is to mutate per-copy state\n * mutates a copy nobody reads.\n *\n * Measured on `apps/nest/microservices/agency`, with the published 1.4.0-alpha.4:\n *\n * zone of the copy sentinel pinned UTC\n * zone of the copy the test imports Europe/Paris\n * same object false\n *\n * Six tests that passed under jest failed, all comparing `DateTime.now().toISO()` against a\n * date the provider had serialised: identical instants, one rendered `+02:00` and the other\n * `Z`. The root jest setup pinned `Settings.defaultZone = 'utc'` and 97 modules named it, so\n * this is not one module's edge case.\n *\n * ⚠️ It hid for a whole campaign because the harness SYMLINKS sentinel to its source tree.\n * The resolved path then holds no `node_modules`, Vitest inlines it, and the pin lands on the\n * right copy: 96 modules reported IDENTIQUE while the real install was red. An instrument\n * that installs the package differently from a team does not measure what a team will see.\n *\n * The narrow form `/@hublo\\/sentinel\\/dist\\/roles\\//` was tried and does NOT work: the pin\n * itself lives in a shared chunk at the dist root, which stays external. It has to be the\n * whole package.\n */\n server: { deps: { inline: [/@hublo\\/sentinel/] } },\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/** A `test.server` block, named off Vitest's own type so the merge cannot drift from it. */\ntype TestServer = NonNullable<ViteUserConfig['test']>['server']\n\n/** The config, with the module's own overrides merged over it. */\nexport function sharedTestConfig(\n options: SharedTestOptions & { flavour: TestFlavour },\n): ViteUserConfig {\n return withIsolationChoice(mergedConfig(options), options)\n}\n\n/**\n * The isolation the module asked for, and everything that has to come with it.\n *\n * Two things travel together and neither works alone: the file boundary, which gives back per\n * file what a shared worker leaks, and the split, which keeps isolation for the files that mock a\n * module. See `./setup/file-boundary.js` and `./isolation-split.js` for what each measured.\n */\nfunction withIsolationChoice(\n config: ViteUserConfig,\n options: SharedTestOptions & { flavour: TestFlavour },\n): ViteUserConfig {\n if (options.isolate !== false) return config\n\n /*\n * Nest does not get this, and the refusal is measured rather than cautious.\n *\n * What it would buy: `agency`, 120 files and 539 tests, goes from 43s to 40s and uses the same\n * peak memory. Seven percent, where the same change takes a jsdom module from 44.6s to 13s,\n * because a Nest suite's cost is its test bodies and a React suite's cost is re-evaluating its\n * import graph once per file.\n *\n * What it would cost is written a few lines up, where `isolate: true` is set for this flavour:\n * Nest registers metadata as an import SIDE EFFECT, so a decorator writes into a catalog when\n * its module loads and a shared registry lets one suite see what another registered. Refused\n * rather than ignored, because a module that asked for it and silently did not get it would\n * read its own config as a lie.\n */\n if (options.flavour === 'nest') {\n throw new Error(\n 'sentinel test: `isolate: false` is not available for a Nest suite. Nest registers its ' +\n 'metadata as an import side effect, so sharing a module registry lets one suite see what ' +\n 'another registered. Measured on `agency`, it would save 7% (43s to 40s) where a React ' +\n 'module saves 71%, so the trade is not worth making. Remove the option.',\n )\n }\n\n const bracketed: ViteUserConfig = {\n ...config,\n test: {\n ...config.test,\n /*\n * The boundary brackets the module's own setup files rather than joining them. The closing\n * entry has to run after the module's, because what it records as \"infrastructure\" is\n * whatever the setup files built, and `setupFiles` order is the only way to say \"last\".\n */\n setupFiles: [\n '@hublo/sentinel/test/setup/file-boundary',\n ...asSetupList(config.test?.setupFiles),\n '@hublo/sentinel/test/setup/file-boundary-close',\n ],\n },\n }\n\n return asIsolationSplit(bracketed, options.root)\n}\n\n/** `setupFiles` accepts a string or a list; the boundary needs a list either way. */\nfunction asSetupList(setupFiles: unknown): string[] {\n if (typeof setupFiles === 'string') return [setupFiles]\n if (Array.isArray(setupFiles)) return setupFiles as string[]\n return []\n}\n\n/** The base with the module's own overrides merged over it. */\nfunction mergedConfig(options: SharedTestOptions & { flavour: TestFlavour }): 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 /*\n * The module's own aliases come after sentinel's, and in a Vite alias ARRAY the FIRST match\n * wins. So sentinel's win, which is the opposite of what this comment used to claim.\n *\n * ⚠️ The order is corrected here rather than in the code, because changing it would flip a\n * behaviour that nothing exercises. Every alias sentinel adds is ANCHORED to one exact\n * specifier: `^axios$`, `^luxon$`, `^msw$`, `^jest-mock-extended$` and\n * `^@prisma/<x>/runtime/library$`. A module's own entry collides only by naming the identical\n * string, and measured across every `jest.config*` in the repo, none does: the closest are\n * `@front/type/axios` and `@front/api/msw-handlers` in `front-legacy`, different specifiers\n * in the one module this role refuses anyway.\n *\n * So today the order decides nothing, and all 97 migrations were measured with it this way\n * round. Reversing it on a hypothesis would be a silent behaviour change bought with nothing.\n * What was actually wrong was a comment promising a module it could win, which a reader would\n * have relied on.\n */\n alias: [\n ...asAliasArray(base.resolve?.alias),\n ...asAliasArray(options.overrides.resolve?.alias),\n ],\n },\n test: {\n ...base.test,\n ...options.overrides.test,\n /*\n * ⚠️ `server.deps.inline` is the one key inside `test` that must not be REPLACED.\n *\n * The shallow spread above is right for everything else: a module overriding `environment`\n * or `testTimeout` means to replace it. But the base uses this list to put sentinel's own\n * setup inside Vite's graph, and a module declares its own entries there for its workspace\n * packages. Dropping either half breaks something silently, and the halves have nothing to\n * do with each other, so they are concatenated.\n *\n * Measured on `libs/front/api`, which declares `['@front/runtime-config', '@segment/snippet']`\n * and would otherwise have discarded the base's entry and lost the timezone pin with it.\n */\n server: mergeServerDeps(base.test?.server, options.overrides.test?.server),\n },\n }\n}\n\n/** Two `server` blocks, with their `deps.inline` lists joined rather than one winning. */\nfunction mergeServerDeps(base: TestServer, override: TestServer): TestServer {\n if (base === undefined) return override\n if (override === undefined) return base\n\n const inline = [\n ...(asInlineArray(base.deps?.inline) ?? []),\n ...(asInlineArray(override.deps?.inline) ?? []),\n ]\n return {\n ...base,\n ...override,\n deps: { ...base.deps, ...override.deps, ...(inline.length > 0 ? { inline } : {}) },\n }\n}\n\n/** `inline` is a list or the literal `true`, and `true` cannot be concatenated with anything. */\nfunction asInlineArray(inline: unknown): (string | RegExp)[] | undefined {\n return Array.isArray(inline) ? (inline as (string | RegExp)[]) : undefined\n}\n","/**\n * Turning one config into two projects, so that most files can share a worker and the few that\n * cannot keep their isolation.\n *\n * ## Why a split rather than one setting\n *\n * Isolation is what makes a Vitest run expensive: every test file re-evaluates the whole import\n * graph. Measured on `front-components`, 230 files and 1418 tests, same machine, same session:\n *\n * jest, files in parallel 15.9 s\n * Vitest, isolated 44.6 s\n * Vitest, this split 11.0 s\n *\n * `./setup/file-boundary.js` gives back, by hand, everything a shared worker would otherwise leak.\n * One thing it cannot give back is a module that has already been evaluated. `vi.mock` has to be\n * in place BEFORE the module it replaces is imported, so a file declaring a mock cannot be handed\n * a registry another file already filled, and no hook can undo that after the fact.\n *\n * So those files, and only those, keep isolation. Measured on `front-components`, 13 of 230, which\n * is 6%. The two projects run in one command and report as one run.\n *\n * ## Why the split is read, not declared\n *\n * A list a module maintains by hand goes stale the first time someone adds a test, and it goes\n * stale silently: the file still runs, it just runs in the wrong project, and what it then breaks\n * lands on an unrelated file. Reading the sources costs one `readFileSync` per test file, measured\n * at a few milliseconds for 230, and it cannot drift.\n *\n * `jest.mock` counts as well as `vi.mock`: a module can be adopted before its codemod has run, and\n * `./setup/jest-global.js` makes the `jest` global work, so the call is real. `doMock` is spelled\n * out rather than folded into an optional `do` prefix, which is how the first version missed it:\n * the M is capital, and a case-insensitive flag would have matched a variable called `domock`.\n */\nimport { globSync, readFileSync } from 'node:fs'\nimport { join } from 'node:path'\n\nimport type { ViteUserConfig } from 'vitest/config'\n\n/**\n * The call that forces a file to keep its isolation.\n *\n * Deliberately not anchored to the start of a line: `vi.mock` is hoisted by Vitest wherever it\n * sits, including inside a `describe`. A match inside a comment or a string costs that one file\n * its share of the speed-up and nothing else, which is the right way round for a heuristic that\n * decides isolation.\n */\nconst DECLARES_A_MODULE_MOCK = /\\b(?:vi|jest)\\.(?:mock|doMock)\\s*\\(/\n\n/** The test files under `root` that `include` selects, relative to `root`, in a stable order. */\nexport function testFilesUnder(root: string, include: readonly string[]): string[] {\n return globSync([...include], { cwd: root, exclude: ['**/node_modules/**'] })\n .map(String)\n .sort()\n}\n\n/** Those of `files` that declare a module mock, and those that do not. */\nexport function splitByModuleMock(\n root: string,\n files: readonly string[],\n): { mocked: string[]; shared: string[] } {\n const mocked: string[] = []\n const shared: string[] = []\n\n for (const file of files) {\n const source = readSource(join(root, file))\n if (source !== undefined && DECLARES_A_MODULE_MOCK.test(source)) mocked.push(file)\n else shared.push(file)\n }\n\n return { mocked, shared }\n}\n\n/**\n * `config`, expressed as two projects: everything in a shared worker, the module-mocking files\n * isolated.\n *\n * The base is SPREAD into each project rather than reached through Vitest's `extends`. Measured:\n * `extends` merges with `mergeConfig`, which concatenates arrays, so each project's `include` came\n * back as the base's patterns plus its own and both projects ran all 230 files, reporting 460\n * files and 2836 tests for a module that has 230 and 1418. The count is what caught it; the run\n * was green and nearly the right duration.\n */\nexport function asIsolationSplit(config: ViteUserConfig, root: string): ViteUserConfig {\n const include = config.test?.include ?? []\n const files = testFilesUnder(root, include)\n\n // Nothing to split, and an empty `include` would make a project run the default patterns\n // instead of nothing at all.\n if (files.length === 0) return { ...config, test: { ...config.test, isolate: false } }\n\n const { mocked, shared } = splitByModuleMock(root, files)\n\n return {\n ...config,\n test: {\n // Kept at the root, where a project cannot answer for the whole run.\n coverage: config.test?.coverage,\n projects: [project(config, 'shared', shared, false), project(config, 'mocked', mocked, true)],\n },\n }\n}\n\nfunction project(\n config: ViteUserConfig,\n name: string,\n include: string[],\n isolate: boolean,\n): ViteUserConfig {\n return {\n ...config,\n test: {\n ...config.test,\n name,\n include,\n isolate,\n /*\n * A project with nothing to run is still cheaper than a conditional here: Vitest starts it,\n * finds no file and reports none. Leaving both projects in place keeps the shape of the\n * config the same for every module, which is what makes a generated config reviewable.\n */\n coverage: undefined,\n },\n }\n}\n\nfunction readSource(path: string): string | undefined {\n try {\n return readFileSync(path, 'utf8')\n } catch {\n // A file that vanished between the glob and the read is not this function's problem to report:\n // Vitest will say so itself, by name, when it fails to run it.\n return undefined\n }\n}\n","/**\n * Send every importer of the workspace's msw server to the one that actually listens.\n *\n * ## The gap an alias cannot close\n *\n * The generated config already aliases `@hublo/test/msw/server`, which covers a file naming the\n * server by its package specifier. It cannot cover a file that reaches the same module by a\n * RELATIVE path, because an alias matches the spelling of the import and `./server` is a spelling\n * thousands of unrelated files use.\n *\n * That gap is not hypothetical. `libs/nest/tests/src/msw/create-handlers.ts` exports\n * `createMswHandler`, whose body calls `server.use(...)` on `./server`. So a migrated module gets\n * its handlers registered on the WORKSPACE's server while sentinel's is the one listening, and the\n * request arrives unmatched.\n *\n * Measured on `apps/nest/microservices/institution`, its only consumer:\n *\n * jest 2958 passed\n * vitest, without this 2957 passed, 1 failed\n * the failure Worker Webservice transformTemporaryPersonToWorker\n * onUnhandledRequest, raised by sentinel's copy\n *\n * ## Why here rather than in that file\n *\n * One line in `create-handlers.ts` would also fix it. Héla, 2026-09-24: the fix belongs in what we\n * OWN. A config sentinel generates reaches every module that adopts, today and later, and asks\n * nothing of a team whose shared lib happens to sit in the path. Editing their file fixes one\n * import and leaves the next one to be discovered by whoever writes it.\n *\n * ## Why the RESOLVED path, which is narrower than it looks\n *\n * This matches what the import turned out to BE, not how it was written, so `./server`,\n * `../msw/server` and `@hublo/test/msw/server` are one case rather than three. The pattern is the\n * same convention the codemod already uses to repoint a server import: a file whose path ends in\n * `msw/server`. It is checked against the resolved id only, so a module's own `./server` that has\n * nothing to do with msw is never touched.\n *\n * ## What it does NOT do\n *\n * Redirect anything outside the workspace. A `msw/server` inside `node_modules` is a package's own\n * business, and sentinel's own server file is skipped so the redirect cannot point at itself.\n */\nimport type { Plugin } from 'vite'\n\n/** The convention: a module whose path ends in `msw/server`, whatever extension it carries. */\nconst WORKSPACE_MSW_SERVER = /(^|\\/)msw\\/server\\.[cm]?[jt]sx?$/\n\n/**\n * @param serverFile sentinel's own server, the one its setup starts listening on.\n * @param workspaceRoot the boundary: nothing outside it is redirected.\n */\nexport function mswServerRedirect(serverFile: string, workspaceRoot: string): Plugin {\n return {\n name: 'sentinel:msw-server-redirect',\n // Before the resolver settles on the workspace file, since the point is to replace it.\n enforce: 'pre',\n async resolveId(source, importer, options) {\n // Our own file, and anything already pointing at it: nothing to do, and no self-recursion.\n if (source === serverFile) return undefined\n\n const resolved = await this.resolve(source, importer, { ...options, skipSelf: true })\n if (resolved === null || resolved.external) return undefined\n\n const id = resolved.id\n if (!id.startsWith(workspaceRoot)) return undefined\n if (id.includes('/node_modules/')) return undefined\n if (!WORKSPACE_MSW_SERVER.test(id)) return undefined\n\n return serverFile\n },\n }\n}\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":"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","names":["join","join"]}
1
+ {"version":3,"sources":["../src/roles/test/shared-test-config.ts","../src/roles/test/isolation-split.ts","../src/roles/test/msw-server-redirect.ts","../src/roles/test/react/jest-export-conditions.ts"],"sourcesContent":["/**\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\nimport { asIsolationSplit } from './isolation-split.js'\nimport { mswServerRedirect } from './msw-server-redirect.js'\nimport { jestExportConditions } from './react/jest-export-conditions.js'\n\n/** Which family this config is for: it decides the three things that are not shared. */\nexport type TestFlavour = 'nest' | 'react'\n\nexport interface SharedTestOptions {\n /** Which family this config is for. It decides the three things that are not shared. */\n flavour?: TestFlavour\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 /**\n * Let the files of this suite share a worker instead of each getting its own registry.\n *\n * Off by default, which is Vitest's own default and jest's semantics. Turning it off buys the\n * whole of `./setup/file-boundary.js` and `./isolation-split.js`: measured on\n * `front-components`, 44.6s isolated against 11.0s split, with all 1418 tests green and the\n * same result across ten shuffled file orders.\n *\n * A module takes it deliberately, and proves it with `--validate`, because the switch is the one\n * thing in this preset that changes what a suite MEANS: a file can see what an earlier file left\n * behind, and only running the suite can say whether any of them does.\n */\n isolate?: 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 /*\n * ⚠️ Several candidates, because the EMITTED layout is not ours to predict. This file is imported\n * by two toolchains, so the bundler extracts it into a shared chunk at the package root, and it\n * does so even when it is declared as its own entry (the entry then only re-exports the chunk).\n * A single `import.meta.url`-relative path therefore resolves differently depending on where the\n * bundler decided to put the code.\n *\n * That is not theoretical: it silently broke the jest-mock-extended and msw-server adapters, and\n * `apps/cloud/shift-offer` went from 83/83 to 56/83 with \"The property X is not defined on the\n * object\" — a mockDeep shape error that names nothing near the cause. The alias had quietly\n * fallen back to the raw fork.\n *\n * So the file is LOOKED FOR rather than computed, and the first one that exists wins.\n */\n const candidates = [\n relative,\n `roles/test/${relative.replace(/^\\.\\//, '')}`,\n `../${relative.replace(/^\\.\\//, '')}`,\n ]\n for (const candidate of candidates) {\n try {\n const sibling = fileURLToPath(new URL(candidate, import.meta.url))\n if (existsSync(sibling)) return sibling\n } catch {\n continue\n }\n }\n return undefined\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 */\n/** sentinel's own server file, the one its setup starts listening on. */\nfunction mswServerFile(): string | undefined {\n return siblingFile('./setup/msw-server.js')\n}\n\nfunction mswAliases(): Alias[] {\n const server = mswServerFile()\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: SharedTestOptions): 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 /*\n * `jestExportConditions` is UNCONDITIONAL, and it is the one plugin every migrated module gets.\n *\n * jest resolved with `['node', 'require', 'default']`, read off the installed `@nx/jest/preset`\n * rather than off its documentation. `development` was never in that list, so a package\n * shipping a separate development build loaded its PRODUCTION file. Vite's own conditions carry\n * `development|production`, so `@emotion/cache` loads `emotion-cache.development.cjs.js`, whose\n * extra stylis plugin calls `console.error(':first-child is potentially unsafe...')`, and with\n * `jest-fail-on-console` in the setup that console call IS a failure.\n *\n * Measured on `libs/front/components`: 2 of its 3 remaining failures, both green again with\n * this. `apps/front/front-legacy` has 100 over 26 files from the same cause.\n *\n * Unconditional rather than reserved for a jsdom module, because it is not a statement about\n * the DOM: it is what this migration is for, running the suite the way the runner it was\n * written for ran it. It changes nothing for a module with no dual-published dependency, which\n * is why nest and cloud were measured identical without it.\n */\n plugins: [\n jestExportConditions(),\n /*\n * The alias beside it covers a file naming the workspace's msw server by its package\n * specifier. This covers the one that reaches the same module RELATIVELY, which an alias\n * cannot: `./server` is a spelling thousands of unrelated files use, so the redirect is\n * decided on the resolved path instead. See `msw-server-redirect.ts` for the measurement.\n */\n ...(mswServerFile() === undefined\n ? []\n : [mswServerRedirect(mswServerFile() as string, workspaceRoot)]),\n ...(options.lowerDecoratorsWithTypeScript ? [decoratorMetadata({ root })] : []),\n ],\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 /*\n * Nest only, and measured: 1186 files under `apps/nest` and `libs/nest` mention `@prisma/`,\n * and ZERO under `apps/front` and `libs/front`. A React module paying for an alias to a\n * client it never generates is noise in a file someone has to read.\n */\n ...(options.flavour === 'nest' ? 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 * What the repo's jest preset actually matched, copied rather than approximated:\n * `**\\/?(*.)+(spec|test).[jt]s?(x)`.\n *\n * Both NAMES, because jest ran both: `**\\/*.spec.ts` alone read green while missing three\n * `.test.ts` files and 23 tests, with nothing saying so. And all four EXTENSIONS, for the\n * same reason one notch further out. Measured over the repo's 7063 test files:\n *\n * .ts 5844 .tsx 1108 .js 99 .mjs/.cjs 12\n *\n * The `.ts`-only form cost nothing on nest and cloud, which have none of the others, and it\n * cost `libs/front/api` three files and 13 tests on the first front module it met. Half the\n * front's test files are `.tsx`.\n *\n * `.mjs` and `.cjs` are deliberately OUT: jest's `[jt]s?(x)` does not match them either, and\n * Vitest's own default include does. Running a file the reference never ran is as wrong as\n * skipping one it did.\n */\n include: ['**/*.spec.[jt]s?(x)', '**/*.test.[jt]s?(x)'],\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 /*\n * Nest only. A Nest module registers metadata as an import SIDE EFFECT, so sharing a module\n * registry across files lets one suite see what another registered. A React module has no\n * such catalog, and isolation is not free.\n */\n ...(options.flavour === 'nest' ? { isolate: true } : {}),\n /*\n * Concurrency, transposed from what this repo does today rather than chosen.\n *\n * Every jest target inherits `configurations.ci = { ci: true, runInBand: true }` from the\n * `@nx/jest:jest` key in `nx.json`, and CI invokes every test target with\n * `--configuration=ci`. So on CI every suite in this repo runs ONE FILE AT A TIME today. That\n * key belongs to the jest executor and cannot be touched, because the workspace is mixed: it\n * still serves the modules that have not moved.\n *\n * ⚠️ And it is CI-ONLY. A local run omits `--configuration=ci`, so jest runs files in\n * PARALLEL on a developer's machine. A flat `fileParallelism: false` here would make local\n * runs slower than jest, which loses something the module had. Hence the condition rather\n * than the constant: parallel locally, one file at a time on CI, which is jest on both sides.\n *\n * Measured on three files that each hold the clock for 400ms and record their interval:\n *\n * default files overlap, 402ms\n * fileParallelism: false no overlap, 1442ms\n * maxWorkers: 1 no overlap, 1423ms\n *\n * Both candidates give the property that matters. `fileParallelism` is the one that says what\n * the module MEANS (\"do not run my files at the same time\"); `maxWorkers` is a pool size, and\n * it is what a module asking for a CAP gets instead (three BFFs ask for 4).\n *\n * A module that declared its own concurrency overrides this, in both environments, exactly as\n * it does today.\n */\n /*\n * ⚠️ PARALLEL on CI too, since 26/09, and that reverses what the note above decided.\n *\n * The transposition was faithful and expensive. Measured on `libs/front/components`, 1418\n * tests green on every line, on the repo's own 4-core agents:\n *\n * jest, as CI runs it 158.7 s\n * vitest serial, the transposition 644.5 s 4.1x slower\n * vitest, 3 workers 324.7 s 2.05x slower\n *\n * Serialising costs vitest far more than it costs jest, because vitest re-evaluates the\n * module graph per file through Vite's SSR runner: 555 ms of `import` and 478 ms of\n * `environment` per file against 139 ms of actual tests. Parallelism does not remove that\n * cost, it pays it on several cores at once.\n *\n * Every other documented lever was measured and does nothing or breaks the suite:\n * `deps.optimizer.ssr`, `NODE_COMPILE_CACHE` and `fsModuleCache` all cache TRANSFORMATION\n * where the cost is EVALUATION; `pool: threads` is noise; `vmThreads` collapses into\n * `no-isolate` when serial and fails 473 tests; `--no-isolate` fails 351.\n *\n * ⚠️ And NO worker cap, which is deliberate after reading vitest's own sizing:\n *\n * getDefaultThreadsCount: config.watch ? max(numCpus / 2, 1) : max(numCpus - 1, 1)\n *\n * `cores - 1` is 3 on this repo's 4-core agents, which is exactly the pool the 324.7 s above\n * was measured with, and 9 on a 10-core laptop. Pinning a number would slow every developer\n * to serve the agent, and a percentage would only re-describe a default that already adapts.\n * So one variable changes here and the tool keeps sizing its own pool.\n *\n * Memory was the reason to hesitate and it was measured rather than assumed: a full jsdom\n * run holds 1.1 GB at three workers and 1.9 GB at six, on agents with 32 GB. The OOM this\n * repo suffered was three nx tasks near 7.45 GB each, which is not what a test worker costs.\n *\n * A module that declares its own concurrency still overrides this, as before.\n */\n fileParallelism: true,\n\n /*\n * ⚠️ Sentinel's own setup files go through VITE, not round it, and that is load-bearing.\n *\n * Vitest externalizes anything under `node_modules`, so an installed `@hublo/sentinel` is\n * loaded by Node while the module's own code is served by Vite. The two then hold DIFFERENT\n * instances of the same package, and a setup whose whole job is to mutate per-copy state\n * mutates a copy nobody reads.\n *\n * Measured on `apps/nest/microservices/agency`, with the published 1.4.0-alpha.4:\n *\n * zone of the copy sentinel pinned UTC\n * zone of the copy the test imports Europe/Paris\n * same object false\n *\n * Six tests that passed under jest failed, all comparing `DateTime.now().toISO()` against a\n * date the provider had serialised: identical instants, one rendered `+02:00` and the other\n * `Z`. The root jest setup pinned `Settings.defaultZone = 'utc'` and 97 modules named it, so\n * this is not one module's edge case.\n *\n * ⚠️ It hid for a whole campaign because the harness SYMLINKS sentinel to its source tree.\n * The resolved path then holds no `node_modules`, Vitest inlines it, and the pin lands on the\n * right copy: 96 modules reported IDENTIQUE while the real install was red. An instrument\n * that installs the package differently from a team does not measure what a team will see.\n *\n * The narrow form `/@hublo\\/sentinel\\/dist\\/roles\\//` was tried and does NOT work: the pin\n * itself lives in a shared chunk at the dist root, which stays external. It has to be the\n * whole package.\n *\n * ## ⚠️ And `vitest` itself, for the same reason one level further out\n *\n * Inlining sentinel is not enough: its own `import { expect } from 'vitest'` still leaves\n * through Node, which loads a SECOND instance of the runner. The snapshot client is\n * per-instance state, so the instance the test file uses was never set up for that file:\n *\n * Error: The snapshot state for '…/asyncapi.module.spec.ts' is not found.\n * Did you call 'SnapshotClient.setup()'?\n *\n * Isolated to the smallest possible case: an EMPTY setup file under `node_modules` passes,\n * and the same file containing nothing but `import { expect } from 'vitest'` fails, while\n * the identical import from a setup file INSIDE the module passes. So it is the location,\n * not the import, and the remedy has to reach the import too.\n *\n * Measured on `libs/nest/asyncapi`: sentinel alone 1 failed of 13, sentinel and `vitest`\n * 13 passed, 525 ms against 570 ms. Not a trade.\n */\n server: { deps: { inline: [/@hublo\\/sentinel/, 'vitest'] } },\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/** A `test.server` block, named off Vitest's own type so the merge cannot drift from it. */\ntype TestServer = NonNullable<ViteUserConfig['test']>['server']\n\n/** The config, with the module's own overrides merged over it. */\nexport function sharedTestConfig(\n options: SharedTestOptions & { flavour: TestFlavour },\n): ViteUserConfig {\n return withIsolationChoice(mergedConfig(options), options)\n}\n\n/**\n * The isolation the module asked for, and everything that has to come with it.\n *\n * Two things travel together and neither works alone: the file boundary, which gives back per\n * file what a shared worker leaks, and the split, which keeps isolation for the files that mock a\n * module. See `./setup/file-boundary.js` and `./isolation-split.js` for what each measured.\n */\nfunction withIsolationChoice(\n config: ViteUserConfig,\n options: SharedTestOptions & { flavour: TestFlavour },\n): ViteUserConfig {\n if (options.isolate !== false) return config\n\n /*\n * Nest does not get this, and the refusal is measured rather than cautious.\n *\n * What it would buy: `agency`, 120 files and 539 tests, goes from 43s to 40s and uses the same\n * peak memory. Seven percent, where the same change takes a jsdom module from 44.6s to 13s,\n * because a Nest suite's cost is its test bodies and a React suite's cost is re-evaluating its\n * import graph once per file.\n *\n * What it would cost is written a few lines up, where `isolate: true` is set for this flavour:\n * Nest registers metadata as an import SIDE EFFECT, so a decorator writes into a catalog when\n * its module loads and a shared registry lets one suite see what another registered. Refused\n * rather than ignored, because a module that asked for it and silently did not get it would\n * read its own config as a lie.\n */\n if (options.flavour === 'nest') {\n throw new Error(\n 'sentinel test: `isolate: false` is not available for a Nest suite. Nest registers its ' +\n 'metadata as an import side effect, so sharing a module registry lets one suite see what ' +\n 'another registered. Measured on `agency`, it would save 7% (43s to 40s) where a React ' +\n 'module saves 71%, so the trade is not worth making. Remove the option.',\n )\n }\n\n const bracketed: ViteUserConfig = {\n ...config,\n test: {\n ...config.test,\n /*\n * The boundary brackets the module's own setup files rather than joining them. The closing\n * entry has to run after the module's, because what it records as \"infrastructure\" is\n * whatever the setup files built, and `setupFiles` order is the only way to say \"last\".\n */\n setupFiles: [\n '@hublo/sentinel/test/setup/file-boundary',\n ...asSetupList(config.test?.setupFiles),\n '@hublo/sentinel/test/setup/file-boundary-close',\n ],\n },\n }\n\n return asIsolationSplit(bracketed, options.root)\n}\n\n/** `setupFiles` accepts a string or a list; the boundary needs a list either way. */\nfunction asSetupList(setupFiles: unknown): string[] {\n if (typeof setupFiles === 'string') return [setupFiles]\n if (Array.isArray(setupFiles)) return setupFiles as string[]\n return []\n}\n\n/** The base with the module's own overrides merged over it. */\nfunction mergedConfig(options: SharedTestOptions & { flavour: TestFlavour }): 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 /*\n * The module's own aliases come after sentinel's, and in a Vite alias ARRAY the FIRST match\n * wins. So sentinel's win, which is the opposite of what this comment used to claim.\n *\n * ⚠️ The order is corrected here rather than in the code, because changing it would flip a\n * behaviour that nothing exercises. Every alias sentinel adds is ANCHORED to one exact\n * specifier: `^axios$`, `^luxon$`, `^msw$`, `^jest-mock-extended$` and\n * `^@prisma/<x>/runtime/library$`. A module's own entry collides only by naming the identical\n * string, and measured across every `jest.config*` in the repo, none does: the closest are\n * `@front/type/axios` and `@front/api/msw-handlers` in `front-legacy`, different specifiers\n * in the one module this role refuses anyway.\n *\n * So today the order decides nothing, and all 97 migrations were measured with it this way\n * round. Reversing it on a hypothesis would be a silent behaviour change bought with nothing.\n * What was actually wrong was a comment promising a module it could win, which a reader would\n * have relied on.\n */\n alias: [\n ...asAliasArray(base.resolve?.alias),\n ...asAliasArray(options.overrides.resolve?.alias),\n ],\n },\n test: {\n ...base.test,\n ...options.overrides.test,\n /*\n * ⚠️ `server.deps.inline` is the one key inside `test` that must not be REPLACED.\n *\n * The shallow spread above is right for everything else: a module overriding `environment`\n * or `testTimeout` means to replace it. But the base uses this list to put sentinel's own\n * setup inside Vite's graph, and a module declares its own entries there for its workspace\n * packages. Dropping either half breaks something silently, and the halves have nothing to\n * do with each other, so they are concatenated.\n *\n * Measured on `libs/front/api`, which declares `['@front/runtime-config', '@segment/snippet']`\n * and would otherwise have discarded the base's entry and lost the timezone pin with it.\n */\n server: mergeServerDeps(base.test?.server, options.overrides.test?.server),\n },\n }\n}\n\n/** Two `server` blocks, with their `deps.inline` lists joined rather than one winning. */\nfunction mergeServerDeps(base: TestServer, override: TestServer): TestServer {\n if (base === undefined) return override\n if (override === undefined) return base\n\n const inline = [\n ...(asInlineArray(base.deps?.inline) ?? []),\n ...(asInlineArray(override.deps?.inline) ?? []),\n ]\n return {\n ...base,\n ...override,\n deps: { ...base.deps, ...override.deps, ...(inline.length > 0 ? { inline } : {}) },\n }\n}\n\n/** `inline` is a list or the literal `true`, and `true` cannot be concatenated with anything. */\nfunction asInlineArray(inline: unknown): (string | RegExp)[] | undefined {\n return Array.isArray(inline) ? (inline as (string | RegExp)[]) : undefined\n}\n","/**\n * Turning one config into two projects, so that most files can share a worker and the few that\n * cannot keep their isolation.\n *\n * ## Why a split rather than one setting\n *\n * Isolation is what makes a Vitest run expensive: every test file re-evaluates the whole import\n * graph. Measured on `front-components`, 230 files and 1418 tests, same machine, same session:\n *\n * jest, files in parallel 15.9 s\n * Vitest, isolated 44.6 s\n * Vitest, this split 11.0 s\n *\n * `./setup/file-boundary.js` gives back, by hand, everything a shared worker would otherwise leak.\n * One thing it cannot give back is a module that has already been evaluated. `vi.mock` has to be\n * in place BEFORE the module it replaces is imported, so a file declaring a mock cannot be handed\n * a registry another file already filled, and no hook can undo that after the fact.\n *\n * So those files, and only those, keep isolation. Measured on `front-components`, 13 of 230, which\n * is 6%. The two projects run in one command and report as one run.\n *\n * ## Why the split is read, not declared\n *\n * A list a module maintains by hand goes stale the first time someone adds a test, and it goes\n * stale silently: the file still runs, it just runs in the wrong project, and what it then breaks\n * lands on an unrelated file. Reading the sources costs one `readFileSync` per test file, measured\n * at a few milliseconds for 230, and it cannot drift.\n *\n * `jest.mock` counts as well as `vi.mock`: a module can be adopted before its codemod has run, and\n * `./setup/jest-global.js` makes the `jest` global work, so the call is real. `doMock` is spelled\n * out rather than folded into an optional `do` prefix, which is how the first version missed it:\n * the M is capital, and a case-insensitive flag would have matched a variable called `domock`.\n */\nimport { globSync, readFileSync } from 'node:fs'\nimport { join } from 'node:path'\n\nimport type { ViteUserConfig } from 'vitest/config'\n\n/**\n * The call that forces a file to keep its isolation.\n *\n * Deliberately not anchored to the start of a line: `vi.mock` is hoisted by Vitest wherever it\n * sits, including inside a `describe`. A match inside a comment or a string costs that one file\n * its share of the speed-up and nothing else, which is the right way round for a heuristic that\n * decides isolation.\n */\nconst DECLARES_A_MODULE_MOCK = /\\b(?:vi|jest)\\.(?:mock|doMock)\\s*\\(/\n\n/** The test files under `root` that `include` selects, relative to `root`, in a stable order. */\nexport function testFilesUnder(root: string, include: readonly string[]): string[] {\n return globSync([...include], { cwd: root, exclude: ['**/node_modules/**'] })\n .map(String)\n .sort()\n}\n\n/** Those of `files` that declare a module mock, and those that do not. */\nexport function splitByModuleMock(\n root: string,\n files: readonly string[],\n): { mocked: string[]; shared: string[] } {\n const mocked: string[] = []\n const shared: string[] = []\n\n for (const file of files) {\n const source = readSource(join(root, file))\n if (source !== undefined && DECLARES_A_MODULE_MOCK.test(source)) mocked.push(file)\n else shared.push(file)\n }\n\n return { mocked, shared }\n}\n\n/**\n * `config`, expressed as two projects: everything in a shared worker, the module-mocking files\n * isolated.\n *\n * The base is SPREAD into each project rather than reached through Vitest's `extends`. Measured:\n * `extends` merges with `mergeConfig`, which concatenates arrays, so each project's `include` came\n * back as the base's patterns plus its own and both projects ran all 230 files, reporting 460\n * files and 2836 tests for a module that has 230 and 1418. The count is what caught it; the run\n * was green and nearly the right duration.\n */\nexport function asIsolationSplit(config: ViteUserConfig, root: string): ViteUserConfig {\n const include = config.test?.include ?? []\n const files = testFilesUnder(root, include)\n\n // Nothing to split, and an empty `include` would make a project run the default patterns\n // instead of nothing at all.\n if (files.length === 0) return { ...config, test: { ...config.test, isolate: false } }\n\n const { mocked, shared } = splitByModuleMock(root, files)\n\n return {\n ...config,\n test: {\n // Kept at the root, where a project cannot answer for the whole run.\n coverage: config.test?.coverage,\n projects: [project(config, 'shared', shared, false), project(config, 'mocked', mocked, true)],\n },\n }\n}\n\nfunction project(\n config: ViteUserConfig,\n name: string,\n include: string[],\n isolate: boolean,\n): ViteUserConfig {\n return {\n ...config,\n test: {\n ...config.test,\n name,\n include,\n isolate,\n /*\n * A project with nothing to run is still cheaper than a conditional here: Vitest starts it,\n * finds no file and reports none. Leaving both projects in place keeps the shape of the\n * config the same for every module, which is what makes a generated config reviewable.\n */\n coverage: undefined,\n },\n }\n}\n\nfunction readSource(path: string): string | undefined {\n try {\n return readFileSync(path, 'utf8')\n } catch {\n // A file that vanished between the glob and the read is not this function's problem to report:\n // Vitest will say so itself, by name, when it fails to run it.\n return undefined\n }\n}\n","/**\n * Send every importer of the workspace's msw server to the one that actually listens.\n *\n * ## The gap an alias cannot close\n *\n * The generated config already aliases `@hublo/test/msw/server`, which covers a file naming the\n * server by its package specifier. It cannot cover a file that reaches the same module by a\n * RELATIVE path, because an alias matches the spelling of the import and `./server` is a spelling\n * thousands of unrelated files use.\n *\n * That gap is not hypothetical. `libs/nest/tests/src/msw/create-handlers.ts` exports\n * `createMswHandler`, whose body calls `server.use(...)` on `./server`. So a migrated module gets\n * its handlers registered on the WORKSPACE's server while sentinel's is the one listening, and the\n * request arrives unmatched.\n *\n * Measured on `apps/nest/microservices/institution`, its only consumer:\n *\n * jest 2958 passed\n * vitest, without this 2957 passed, 1 failed\n * the failure Worker Webservice transformTemporaryPersonToWorker\n * onUnhandledRequest, raised by sentinel's copy\n *\n * ## Why here rather than in that file\n *\n * One line in `create-handlers.ts` would also fix it. Héla, 2026-09-24: the fix belongs in what we\n * OWN. A config sentinel generates reaches every module that adopts, today and later, and asks\n * nothing of a team whose shared lib happens to sit in the path. Editing their file fixes one\n * import and leaves the next one to be discovered by whoever writes it.\n *\n * ## Why the RESOLVED path, which is narrower than it looks\n *\n * This matches what the import turned out to BE, not how it was written, so `./server`,\n * `../msw/server` and `@hublo/test/msw/server` are one case rather than three. The pattern is the\n * same convention the codemod already uses to repoint a server import: a file whose path ends in\n * `msw/server`. It is checked against the resolved id only, so a module's own `./server` that has\n * nothing to do with msw is never touched.\n *\n * ## What it does NOT do\n *\n * Redirect anything outside the workspace. A `msw/server` inside `node_modules` is a package's own\n * business, and sentinel's own server file is skipped so the redirect cannot point at itself.\n */\nimport type { Plugin } from 'vite'\n\n/** The convention: a module whose path ends in `msw/server`, whatever extension it carries. */\nconst WORKSPACE_MSW_SERVER = /(^|\\/)msw\\/server\\.[cm]?[jt]sx?$/\n\n/**\n * @param serverFile sentinel's own server, the one its setup starts listening on.\n * @param workspaceRoot the boundary: nothing outside it is redirected.\n */\nexport function mswServerRedirect(serverFile: string, workspaceRoot: string): Plugin {\n return {\n name: 'sentinel:msw-server-redirect',\n // Before the resolver settles on the workspace file, since the point is to replace it.\n enforce: 'pre',\n async resolveId(source, importer, options) {\n // Our own file, and anything already pointing at it: nothing to do, and no self-recursion.\n if (source === serverFile) return undefined\n\n const resolved = await this.resolve(source, importer, { ...options, skipSelf: true })\n if (resolved === null || resolved.external) return undefined\n\n const id = resolved.id\n if (!id.startsWith(workspaceRoot)) return undefined\n if (id.includes('/node_modules/')) return undefined\n if (!WORKSPACE_MSW_SERVER.test(id)) return undefined\n\n return serverFile\n },\n }\n}\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":"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","names":["join","join"]}
@@ -367,9 +367,9 @@ function binFromOwnInstall(packageName, binName) {
367
367
  try {
368
368
  const manifest = require2.resolve(`${packageName}/package.json`);
369
369
  const bin = require2(manifest).bin;
370
- const relative6 = typeof bin === "string" ? bin : bin?.[binName];
371
- if (!relative6) return void 0;
372
- const executable = join5(dirname(manifest), relative6);
370
+ const relative7 = typeof bin === "string" ? bin : bin?.[binName];
371
+ if (!relative7) return void 0;
372
+ const executable = join5(dirname(manifest), relative7);
373
373
  return existsSync4(executable) ? executable : void 0;
374
374
  } catch {
375
375
  return void 0;
@@ -748,7 +748,7 @@ function resolveVitest(cwd) {
748
748
  // src/roles/test/adapters/vitest/vitest.adapter.ts
749
749
  import { spawnSync } from "node:child_process";
750
750
  import { readFileSync as readFileSync24, writeFileSync as writeFileSync2 } from "node:fs";
751
- import { isAbsolute as isAbsolute2, join as join30, resolve as resolve9 } from "node:path";
751
+ import { isAbsolute as isAbsolute2, join as join30, relative as relative6, resolve as resolve9 } from "node:path";
752
752
 
753
753
  // src/core/config/tool-args.ts
754
754
  import { existsSync as existsSync6 } from "node:fs";
@@ -769,11 +769,14 @@ function splitToolArgs(cwd, toolArgs = [], { valueFlags = [] } = {}) {
769
769
  }
770
770
 
771
771
  // src/roles/test/ordering-is-a-permutation.ts
772
+ function isAnImport(line) {
773
+ return /^import\b/.test(line) || /^export\s+(?:\*|\{)[^;]*\bfrom\b/.test(line);
774
+ }
772
775
  function lineCounts(source) {
773
776
  const counts = /* @__PURE__ */ new Map();
774
777
  for (const line of source.split("\n")) {
775
778
  const trimmed = line.trim();
776
- if (trimmed === "") continue;
779
+ if (trimmed === "" || isAnImport(trimmed)) continue;
777
780
  counts.set(trimmed, (counts.get(trimmed) ?? 0) + 1);
778
781
  }
779
782
  return counts;
@@ -947,11 +950,11 @@ function ownsMswLifecycle(cwd, setupFiles) {
947
950
  // src/roles/test/support-file-name.ts
948
951
  var JEST_PREFIX = "jest.";
949
952
  var VITEST_PREFIX = "vitest.";
950
- function vitestSupportName(relative6) {
951
- const cut = relative6.lastIndexOf("/");
952
- const directory = cut === -1 ? "" : relative6.slice(0, cut + 1);
953
- const base = relative6.slice(cut + 1);
954
- if (!base.startsWith(JEST_PREFIX)) return relative6;
953
+ function vitestSupportName(relative7) {
954
+ const cut = relative7.lastIndexOf("/");
955
+ const directory = cut === -1 ? "" : relative7.slice(0, cut + 1);
956
+ const base = relative7.slice(cut + 1);
957
+ if (!base.startsWith(JEST_PREFIX)) return relative7;
955
958
  return `${directory}${VITEST_PREFIX}${base.slice(JEST_PREFIX.length)}`;
956
959
  }
957
960
 
@@ -1406,7 +1409,9 @@ function assertedMockedClass(source, node) {
1406
1409
  if (callee === void 0 || !MEMBER_TYPES.has(callee.type)) return false;
1407
1410
  const object = callee.object;
1408
1411
  if (object === void 0) return false;
1409
- return /\bMockedClass\b/.test(source.slice(object.start, object.end));
1412
+ const receiver = source.slice(object.start, object.end);
1413
+ if (/\bMockedClass\b/.test(receiver)) return true;
1414
+ return /^(?:vi|jest)\s*\.\s*mocked\s*\(\s*[A-Z]/.test(receiver);
1410
1415
  }
1411
1416
  function classValuedProperties(factories) {
1412
1417
  const found = [];
@@ -1434,6 +1439,7 @@ function rewriteConstructibleMocks(source, program, factories) {
1434
1439
  const refusals = [];
1435
1440
  let rewritten = 0;
1436
1441
  const seen = /* @__PURE__ */ new Set();
1442
+ const suppressed = /* @__PURE__ */ new Set();
1437
1443
  const subtrees = [...classValuedProperties(factories)];
1438
1444
  walk(program, (node) => {
1439
1445
  if (assertedMockedClass(source, node)) subtrees.push(node);
@@ -1458,6 +1464,17 @@ function rewriteConstructibleMocks(source, program, factories) {
1458
1464
  return ${bodyText}
1459
1465
  }`;
1460
1466
  edits.push({ start: arrow.start, end: arrow.end, text });
1467
+ const lineStart = source.lastIndexOf("\n", arrow.start - 1) + 1;
1468
+ if (!suppressed.has(lineStart)) {
1469
+ suppressed.add(lineStart);
1470
+ const indent = /^[ \t]*/.exec(source.slice(lineStart, arrow.start))?.[0] ?? "";
1471
+ edits.push({
1472
+ start: lineStart,
1473
+ end: lineStart,
1474
+ text: `${indent}// eslint-disable-next-line prefer-arrow-callback -- Vitest CONSTRUCTS a mock implementation and an arrow is not a constructor
1475
+ `
1476
+ });
1477
+ }
1461
1478
  rewritten += 1;
1462
1479
  });
1463
1480
  }
@@ -2024,17 +2041,30 @@ function readableName(specifier) {
2024
2041
  function namespaceBindings(program) {
2025
2042
  const taken = namesInUse(program);
2026
2043
  const bySpecifier = /* @__PURE__ */ new Map();
2044
+ const typesBySpecifier = /* @__PURE__ */ new Map();
2045
+ const bind = (into, specifier, kind) => {
2046
+ const existing = into.get(specifier);
2047
+ if (existing !== void 0) return existing;
2048
+ const base = `${readableName(specifier)}${kind}`;
2049
+ let name = base;
2050
+ for (let suffix = 2; taken.has(name); suffix += 1) name = `${base}${suffix}`;
2051
+ taken.add(name);
2052
+ into.set(specifier, name);
2053
+ return name;
2054
+ };
2027
2055
  return {
2028
- bindingFor(specifier) {
2029
- const existing = bySpecifier.get(specifier);
2030
- if (existing !== void 0) return existing;
2031
- const base = `${readableName(specifier)}Mock`;
2032
- let name = base;
2033
- for (let suffix = 2; taken.has(name); suffix += 1) name = `${base}${suffix}`;
2034
- taken.add(name);
2035
- bySpecifier.set(specifier, name);
2036
- return name;
2037
- },
2056
+ bindingFor: (specifier) => bind(bySpecifier, specifier, "Mock"),
2057
+ /*
2058
+ * A TYPE-only namespace, for naming a module's type without writing `typeof import('x')`.
2059
+ *
2060
+ * ⚠️ The inline form is what `@typescript-eslint/consistent-type-imports` calls
2061
+ * "`import()` type annotations are forbidden", and it took `front-api:lint` red. 44 sites in
2062
+ * 39 files gain one of these, so the form has to be the one every module's lint accepts, and
2063
+ * `import type * as X from 'x'` is what that rule asks for. It is erased at runtime, so it
2064
+ * costs a module nothing, not even on a specifier the file also mocks.
2065
+ */
2066
+ typeBindingFor: (specifier) => bind(typesBySpecifier, specifier, "Types"),
2067
+ plannedTypes: () => [...typesBySpecifier].map(([specifier, binding]) => ({ specifier, binding })),
2038
2068
  planned() {
2039
2069
  return [...bySpecifier].map(([specifier, binding]) => ({
2040
2070
  specifier,
@@ -2094,8 +2124,8 @@ function reachesBack(fromFile, specifier) {
2094
2124
  } catch {
2095
2125
  continue;
2096
2126
  }
2097
- for (const relative6 of relativeSpecifiers(source)) {
2098
- const next = resolveRelative(file, relative6);
2127
+ for (const relative7 of relativeSpecifiers(source)) {
2128
+ const next = resolveRelative(file, relative7);
2099
2129
  if (next !== void 0) stack.push(next);
2100
2130
  }
2101
2131
  }
@@ -2198,6 +2228,59 @@ function endOfLine(source, end) {
2198
2228
  return at;
2199
2229
  }
2200
2230
 
2231
+ // src/roles/test/codemod/test-only-exports.ts
2232
+ function isMockFunction(node) {
2233
+ if (node === void 0) return false;
2234
+ if (node.type === "CallExpression") {
2235
+ const callee = node.callee;
2236
+ if (callee !== void 0 && MEMBER_TYPES.has(callee.type)) {
2237
+ const object = callee.object;
2238
+ const property = callee.property;
2239
+ if (object?.type === "Identifier" && (object.name === "jest" || object.name === "vi") && property?.type === "Identifier" && property.name === "fn") {
2240
+ return true;
2241
+ }
2242
+ return isMockFunction(object);
2243
+ }
2244
+ }
2245
+ return false;
2246
+ }
2247
+ function testOnlyExports(program) {
2248
+ const found = /* @__PURE__ */ new Map();
2249
+ walk(program, (node) => {
2250
+ if (node.type !== "CallExpression") return;
2251
+ const callee = node.callee;
2252
+ if (callee === void 0 || !MEMBER_TYPES.has(callee.type)) return;
2253
+ const object = callee.object;
2254
+ const property = callee.property;
2255
+ if (object?.type !== "Identifier" || object.name !== "jest" && object.name !== "vi") return;
2256
+ if (property?.type !== "Identifier" || property.name !== "mock") return;
2257
+ const [specifier, factory] = node.arguments ?? [];
2258
+ if (specifier?.type !== "StringLiteral" && specifier?.type !== "Literal") return;
2259
+ if (factory === void 0) return;
2260
+ const locals = /* @__PURE__ */ new Map();
2261
+ walk(factory, (inner) => {
2262
+ if (inner.type !== "VariableDeclarator") return;
2263
+ const id = inner.id;
2264
+ const init = inner.init;
2265
+ if (id?.type === "Identifier" && init !== void 0) locals.set(id.name, init);
2266
+ });
2267
+ const keys = [];
2268
+ walk(factory, (inner) => {
2269
+ if (inner.type !== "Property" && inner.type !== "ObjectProperty") return;
2270
+ const key = inner.key;
2271
+ if (key?.type !== "Identifier" || !key.name.startsWith("__")) return;
2272
+ const value = inner.value;
2273
+ const resolved = value?.type === "Identifier" ? locals.get(value.name) : value ?? void 0;
2274
+ if (isMockFunction(resolved)) keys.push(key.name);
2275
+ });
2276
+ if (keys.length > 0) found.set(String(specifier.value), keys);
2277
+ });
2278
+ return found;
2279
+ }
2280
+ function testOnlyExportCast(keys) {
2281
+ return ` as unknown as { ${keys.map((key) => `${key}: jest.Mock`).join("; ")} }`;
2282
+ }
2283
+
2201
2284
  // src/roles/test/codemod/named-rules.ts
2202
2285
  function isCommonJsPackage(fromFile, specifier) {
2203
2286
  if (specifier.startsWith(".") || specifier.startsWith("/")) return false;
@@ -2333,12 +2416,12 @@ function typeArgumentText(source, call) {
2333
2416
  if (node?.start === void 0 || node.end === void 0) return "";
2334
2417
  return source.slice(node.start, node.end);
2335
2418
  }
2336
- function typeArgumentOrTheModulesOwn(source, call) {
2419
+ function typeArgumentOrTheModulesOwn(source, call, typeNamespace) {
2337
2420
  const own = typeArgumentText(source, call);
2338
2421
  if (own !== "") return own;
2339
2422
  const [specifier] = call.arguments ?? [];
2340
2423
  if (specifier?.type !== "StringLiteral" && specifier?.type !== "Literal") return "";
2341
- return `<typeof import(${source.slice(specifier.start, specifier.end)})>`;
2424
+ return `<typeof ${typeNamespace(String(specifier.value))}>`;
2342
2425
  }
2343
2426
  function applyNamedRules(source, program, fileName = "") {
2344
2427
  const edits = [];
@@ -2347,7 +2430,12 @@ function applyNamedRules(source, program, fileName = "") {
2347
2430
  const refusals = [];
2348
2431
  const staticImports = [];
2349
2432
  const namespaces = namespaceBindings(program);
2433
+ const castIfTestOnly = (specifier, read3) => {
2434
+ const keys = testOnly.get(specifier);
2435
+ return keys === void 0 ? read3 : `(${read3}${testOnlyExportCast(keys)})`;
2436
+ };
2350
2437
  const mocked = mockedSpecifiers(program);
2438
+ const testOnly = testOnlyExports(program);
2351
2439
  const methods = methodValues(program);
2352
2440
  const cannotAwaitHere = (enclosing, what, call) => {
2353
2441
  if (enclosing === void 0 || !methods.has(enclosing)) return false;
@@ -2457,7 +2545,7 @@ function applyNamedRules(source, program, fileName = "") {
2457
2545
  * A call expression needs no parentheses: `vi.mocked(x).y` and `vi.mocked(x)()` read
2458
2546
  * exactly where the original call read.
2459
2547
  */
2460
- text: `vi.mocked(${namespaces.bindingFor(literal)})`
2548
+ text: castIfTestOnly(literal, `vi.mocked(${namespaces.bindingFor(literal)})`)
2461
2549
  });
2462
2550
  applied.add("jest.requireMock");
2463
2551
  return;
@@ -2471,7 +2559,7 @@ function applyNamedRules(source, program, fileName = "") {
2471
2559
  end: node.end,
2472
2560
  text: placed(
2473
2561
  node,
2474
- `await vi.${asyncReplacement}${typeArgumentOrTheModulesOwn(source, node)}(${argumentText(source, node)})`
2562
+ `await vi.${asyncReplacement}${typeArgumentOrTheModulesOwn(source, node, namespaces.typeBindingFor)}(${argumentText(source, node)})`
2475
2563
  )
2476
2564
  });
2477
2565
  applied.add(`jest.${member}`);
@@ -2479,9 +2567,18 @@ function applyNamedRules(source, program, fileName = "") {
2479
2567
  return;
2480
2568
  }
2481
2569
  if (bareRequire(node)) {
2482
- if (cannotAwaitHere(enclosing, "require", node)) return;
2483
2570
  const [specifier] = node.arguments ?? [];
2484
2571
  const literal = specifier?.type === "StringLiteral" || specifier?.type === "Literal" ? String(specifier.value) : void 0;
2572
+ if (literal !== void 0 && mocked.has(literal)) {
2573
+ edits.push({
2574
+ start: node.start,
2575
+ end: node.end,
2576
+ text: castIfTestOnly(literal, namespaces.bindingFor(literal))
2577
+ });
2578
+ applied.add("require of a specifier this file mocks");
2579
+ return;
2580
+ }
2581
+ if (cannotAwaitHere(enclosing, "require", node)) return;
2485
2582
  const interop = literal !== void 0 && !readsANameOffIt.has(node) && !handedToTheRunnerAsAModule.has(node) && !wholeStatement.has(node) && !spiedOn.has(node) && isCommonJsPackage(fileName, literal) ? ".default" : "";
2486
2583
  edits.push({
2487
2584
  start: node.start,
@@ -2515,9 +2612,13 @@ function applyNamedRules(source, program, fileName = "") {
2515
2612
  for (const fn of asyncify) {
2516
2613
  edits.push(...asyncifyEdits(source, fn));
2517
2614
  }
2518
- const namespaceLines = namespaces.planned().map(
2519
- ({ specifier, binding }) => `import * as ${binding} from '${specifier}'${fileUsesSemicolons(source) ? ";" : ""}`
2520
- );
2615
+ const semicolon = fileUsesSemicolons(source) ? ";" : "";
2616
+ const namespaceLines = [
2617
+ ...namespaces.planned().map(({ specifier, binding }) => `import * as ${binding} from '${specifier}'${semicolon}`),
2618
+ ...namespaces.plannedTypes().map(
2619
+ ({ specifier, binding }) => `import type * as ${binding} from '${specifier}'${semicolon}`
2620
+ )
2621
+ ];
2521
2622
  if (staticImports.length > 0 || namespaceLines.length > 0) {
2522
2623
  const first = (program.body ?? []).find(
2523
2624
  (node) => node.type === "ImportDeclaration"
@@ -3193,6 +3294,24 @@ function repointMswServer(fileName, source) {
3193
3294
  return { source: rewritten, repointed: true };
3194
3295
  }
3195
3296
 
3297
+ // src/roles/test/codemod/runner-artefact-names.ts
3298
+ var JEST_MOCK_FUNCTION_NAME = "mockConstructor";
3299
+ var VITEST_MOCK_FUNCTION_NAME = "Mock";
3300
+ function rewriteRunnerArtefactNames(source, program) {
3301
+ const edits = [];
3302
+ walk(program, (node) => {
3303
+ if (node.type !== "StringLiteral" && node.type !== "Literal") return;
3304
+ if (node.value !== JEST_MOCK_FUNCTION_NAME) return;
3305
+ if (node.start === void 0 || node.end === void 0) return;
3306
+ edits.push({
3307
+ start: node.start + 1,
3308
+ end: node.end - 1,
3309
+ text: VITEST_MOCK_FUNCTION_NAME
3310
+ });
3311
+ });
3312
+ return edits.length === 0 ? source : applyEdits(source, edits);
3313
+ }
3314
+
3196
3315
  // src/roles/test/codemod/setup-static-imports.ts
3197
3316
  import { dirname as dirname6, resolve as resolve7 } from "node:path";
3198
3317
 
@@ -3490,6 +3609,54 @@ function rewriteMockFactoryGenerics(source, program) {
3490
3609
  return { source: result, rewritten: edits.length, typeImports: [] };
3491
3610
  }
3492
3611
 
3612
+ // src/roles/test/codemod/typed-matcher.ts
3613
+ var SINGLE_VALUE_MATCHERS = /* @__PURE__ */ new Set([
3614
+ "toBe",
3615
+ "toContainEqual",
3616
+ "toEqual",
3617
+ "toHaveReturnedWith",
3618
+ "toMatchObject",
3619
+ "toReturnWith",
3620
+ "toStrictEqual"
3621
+ ]);
3622
+ function rewriteTypedMatchers(source, program) {
3623
+ const edits = [];
3624
+ const refusals = [];
3625
+ let rewritten = 0;
3626
+ walk(program, (node) => {
3627
+ if (node.type !== "CallExpression") return;
3628
+ const list = node.typeArguments ?? node.typeParameters;
3629
+ const params = list?.params ?? [];
3630
+ if (list?.start === void 0 || params.length !== 1) return;
3631
+ const callee = node.callee;
3632
+ if (callee === void 0 || !MEMBER_TYPES.has(callee.type)) return;
3633
+ const property = callee.property;
3634
+ if (property?.type !== "Identifier") return;
3635
+ const name = property.name;
3636
+ if (!name.startsWith("to")) return;
3637
+ const args = node.arguments ?? [];
3638
+ if (!SINGLE_VALUE_MATCHERS.has(name)) {
3639
+ refusals.push({
3640
+ binding: `expect(...).${name}<\u2026>`,
3641
+ reason: `\`${name}\` carries a type argument and Vitest's matchers take none. This pass only moves the argument onto the value for matchers whose type parameter constrains ONE value; \`${name}\` is not one of those, and guessing what its parameter meant is how an assertion is quietly weakened. This one needs a human.`
3642
+ });
3643
+ return;
3644
+ }
3645
+ if (args.length !== 1) return;
3646
+ const argument = args[0];
3647
+ const type = source.slice(params[0]?.start, params[0]?.end);
3648
+ const value = source.slice(argument.start, argument.end);
3649
+ edits.push({ start: list.start, end: list.end, text: "" });
3650
+ edits.push({
3651
+ start: argument.start,
3652
+ end: argument.end,
3653
+ text: `${value} satisfies ${type}`
3654
+ });
3655
+ rewritten += 1;
3656
+ });
3657
+ return { source: edits.length === 0 ? source : applyEdits(source, edits), rewritten, refusals };
3658
+ }
3659
+
3493
3660
  // src/roles/test/codemod/migrate-file.ts
3494
3661
  function mockFactories(program) {
3495
3662
  const factories = [];
@@ -3553,7 +3720,13 @@ function migrateTestFile(fileName, source, defaultImported = /* @__PURE__ */ new
3553
3720
  const factoryGenerics = rewriteMockFactoryGenerics(spyTypes.source, afterSpyTypes);
3554
3721
  const afterFactoryGenerics = parse2(fileName, factoryGenerics.source);
3555
3722
  if (afterFactoryGenerics === void 0) return unparsable("mock factory generics");
3556
- const ruled = applyNamedRules(factoryGenerics.source, afterFactoryGenerics, fileName);
3723
+ const artefacts = rewriteRunnerArtefactNames(factoryGenerics.source, afterFactoryGenerics);
3724
+ const afterArtefacts = parse2(fileName, artefacts);
3725
+ if (afterArtefacts === void 0) return unparsable("runner artefact names");
3726
+ const matchers = rewriteTypedMatchers(artefacts, afterArtefacts);
3727
+ const afterMatchers = parse2(fileName, matchers.source);
3728
+ if (afterMatchers === void 0) return unparsable("matcher type arguments");
3729
+ const ruled = applyNamedRules(matchers.source, afterMatchers, fileName);
3557
3730
  const renamed = renameJestToVitest(fileName, ruled.source);
3558
3731
  const afterRename = parse2(fileName, renamed.source);
3559
3732
  if (afterRename === void 0) return unparsable("jest to vi rename");
@@ -3654,6 +3827,7 @@ function migrateTestFile(fileName, source, defaultImported = /* @__PURE__ */ new
3654
3827
  ...mocks.refusals.map((refusal) => ({ binding: refusal.binding, reason: refusal.reason })),
3655
3828
  ...constructible.refusals,
3656
3829
  ...isolated.refusals,
3830
+ ...matchers.refusals,
3657
3831
  ...ruled.refusals
3658
3832
  ],
3659
3833
  warnings: fakeTimers.warnings,
@@ -3829,14 +4003,14 @@ function migrateModuleTests(cwd, setupFiles = []) {
3829
4003
  let hoisted = 0;
3830
4004
  const defaultImported = defaultImportedSpecifiers(cwd);
3831
4005
  const asyncHelpers = /* @__PURE__ */ new Set();
3832
- for (const relative6 of listTestSupportFiles(cwd, setupFiles)) {
3833
- const source = read2(join17(cwd, relative6));
4006
+ for (const relative7 of listTestSupportFiles(cwd, setupFiles)) {
4007
+ const source = read2(join17(cwd, relative7));
3834
4008
  if (source === void 0) {
3835
- blockers.push({ file: relative6, reason: "this file could not be read" });
4009
+ blockers.push({ file: relative7, reason: "this file could not be read" });
3836
4010
  continue;
3837
4011
  }
3838
4012
  const migrated = migrateTestFile(
3839
- join17(cwd, relative6),
4013
+ join17(cwd, relative7),
3840
4014
  source,
3841
4015
  defaultImported,
3842
4016
  /* @__PURE__ */ new Set(),
@@ -3844,19 +4018,19 @@ function migrateModuleTests(cwd, setupFiles = []) {
3844
4018
  );
3845
4019
  for (const member of migrated.unhandled) {
3846
4020
  blockers.push({
3847
- file: `${relative6}:${member.line}`,
4021
+ file: `${relative7}:${member.line}`,
3848
4022
  reason: member.rule === void 0 ? `${member.member} has no rule, and guessing one is how a suite goes green for the wrong reason` : `${member.member} needs ${member.rule}`
3849
4023
  });
3850
4024
  }
3851
4025
  for (const refusal of migrated.refusals) {
3852
- blockers.push({ file: relative6, reason: refusal.reason });
4026
+ blockers.push({ file: relative7, reason: refusal.reason });
3853
4027
  }
3854
4028
  collected.push(...migrated.warnings);
3855
- const renamedTo = vitestSupportName(relative6);
3856
- if (!migrated.changed && renamedTo === relative6) continue;
3857
- if (renamedTo !== relative6) operations.push({ kind: "delete", path: relative6 });
3858
- const before = parse3(join17(cwd, relative6), source);
3859
- const after = parse3(join17(cwd, relative6), migrated.source);
4029
+ const renamedTo = vitestSupportName(relative7);
4030
+ if (!migrated.changed && renamedTo === relative7) continue;
4031
+ if (renamedTo !== relative7) operations.push({ kind: "delete", path: relative7 });
4032
+ const before = parse3(join17(cwd, relative7), source);
4033
+ const after = parse3(join17(cwd, relative7), migrated.source);
3860
4034
  if (before !== void 0 && after !== void 0) {
3861
4035
  const wasAsync = asyncFunctionNames(before);
3862
4036
  for (const name of asyncFunctionNames(after)) {
@@ -3868,14 +4042,14 @@ function migrateModuleTests(cwd, setupFiles = []) {
3868
4042
  const contents = /\bvi\./.test(migrated.source) ? injectViImport(join17(cwd, renamedTo), migrated.source) : migrated.source;
3869
4043
  operations.push({ kind: "write", path: renamedTo, contents });
3870
4044
  }
3871
- for (const relative6 of listTestFiles(cwd)) {
3872
- const source = read2(join17(cwd, relative6));
4045
+ for (const relative7 of listTestFiles(cwd)) {
4046
+ const source = read2(join17(cwd, relative7));
3873
4047
  if (source === void 0) {
3874
- blockers.push({ file: relative6, reason: "this file could not be read" });
4048
+ blockers.push({ file: relative7, reason: "this file could not be read" });
3875
4049
  continue;
3876
4050
  }
3877
4051
  const migrated = migrateTestFile(
3878
- join17(cwd, relative6),
4052
+ join17(cwd, relative7),
3879
4053
  source,
3880
4054
  defaultImported,
3881
4055
  asyncHelpers,
@@ -3883,24 +4057,24 @@ function migrateModuleTests(cwd, setupFiles = []) {
3883
4057
  );
3884
4058
  for (const member of migrated.unhandled) {
3885
4059
  blockers.push({
3886
- file: `${relative6}:${member.line}`,
4060
+ file: `${relative7}:${member.line}`,
3887
4061
  reason: member.rule === void 0 ? `${member.member} has no rule, and guessing one is how a suite goes green for the wrong reason` : `${member.member} needs ${member.rule}`
3888
4062
  });
3889
4063
  }
3890
4064
  collected.push(...migrated.warnings);
3891
4065
  for (const refusal of migrated.refusals) {
3892
- blockers.push({ file: relative6, reason: refusal.reason });
4066
+ blockers.push({ file: relative7, reason: refusal.reason });
3893
4067
  }
3894
4068
  if (!migrated.changed) continue;
3895
4069
  files++;
3896
4070
  renamed += migrated.renamed;
3897
4071
  if (migrated.hoisted.length > 0) hoisted++;
3898
4072
  const withInheritedMocks = declaredInheritedMocks(
3899
- join17(cwd, relative6),
4073
+ join17(cwd, relative7),
3900
4074
  migrated.source,
3901
4075
  inherited
3902
4076
  );
3903
- operations.push({ kind: "write", path: relative6, contents: withInheritedMocks });
4077
+ operations.push({ kind: "write", path: relative7, contents: withInheritedMocks });
3904
4078
  }
3905
4079
  const warnings = [...new Set(collected)];
3906
4080
  return blockers.length > 0 ? { operations: [], files, renamed, hoisted, blockers, warnings } : { operations, files, renamed, hoisted, blockers: [], warnings };
@@ -5502,13 +5676,15 @@ var VitestAdapter = class extends BaseAdapter {
5502
5676
  fixWithTheModulesLinter(ctx, written) {
5503
5677
  const linter = writtenFileLinter(ctx.cwd);
5504
5678
  if (linter === void 0) return;
5679
+ const workspaceRoot = findWorkspaceRoot(ctx.cwd) ?? ctx.cwd;
5680
+ const fromRoot = (file) => relative6(workspaceRoot, join30(ctx.cwd, file));
5505
5681
  let unsettled = 0;
5506
5682
  const reverted = [];
5507
5683
  for (let at = 0; at < written.length; at += FORMAT_BATCH) {
5508
5684
  const batch = written.slice(at, at + FORMAT_BATCH);
5509
5685
  const before = new Map(batch.map((file) => [file, readIfPossible(join30(ctx.cwd, file))]));
5510
- const pass = spawnSync(linter.bin, [...linter.args, ...batch], {
5511
- cwd: ctx.cwd,
5686
+ const pass = spawnSync(linter.bin, [...linter.args, ...batch.map(fromRoot)], {
5687
+ cwd: workspaceRoot,
5512
5688
  encoding: "utf8"
5513
5689
  });
5514
5690
  if (pass.status !== 0) unsettled += 1;
@@ -53,7 +53,7 @@ import {
53
53
  selfCommand,
54
54
  splitToolArgs,
55
55
  walk
56
- } from "./chunk-YHNQONE2.js";
56
+ } from "./chunk-RO626QTL.js";
57
57
  import {
58
58
  parseJsonc
59
59
  } from "./chunk-QXFCZON7.js";
package/dist/index.js CHANGED
@@ -6,10 +6,10 @@ import {
6
6
  registerAdapters,
7
7
  resolve,
8
8
  setDefaultRunner
9
- } from "./chunk-BF5SF6HQ.js";
9
+ } from "./chunk-ZSPDXVQL.js";
10
10
  import {
11
11
  BaseAdapter
12
- } from "./chunk-YHNQONE2.js";
12
+ } from "./chunk-RO626QTL.js";
13
13
  import "./chunk-O7REVMOC.js";
14
14
  import "./chunk-QXFCZON7.js";
15
15
  import "./chunk-NW6UHNYX.js";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  sharedTestConfig
3
- } from "../../../chunk-SOCLCHJK.js";
3
+ } from "../../../chunk-F53J6U2W.js";
4
4
  import {
5
5
  decoratorMetadata,
6
6
  tsconfigAliases
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  jestExportConditions,
3
3
  sharedTestConfig
4
- } from "../../../chunk-SOCLCHJK.js";
4
+ } from "../../../chunk-F53J6U2W.js";
5
5
  import "../../../chunk-SWWQ7X7B.js";
6
6
  import "../../../chunk-QXFCZON7.js";
7
7
 
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  sharedTestConfig
3
- } from "../../chunk-SOCLCHJK.js";
3
+ } from "../../chunk-F53J6U2W.js";
4
4
  import "../../chunk-SWWQ7X7B.js";
5
5
  import "../../chunk-QXFCZON7.js";
6
6
  export {
@@ -7,7 +7,7 @@ import {
7
7
  readTestAdoption,
8
8
  resolveVitest,
9
9
  suiteOf
10
- } from "./chunk-YHNQONE2.js";
10
+ } from "./chunk-RO626QTL.js";
11
11
  import "./chunk-O7REVMOC.js";
12
12
  import "./chunk-QXFCZON7.js";
13
13
  import "./chunk-NW6UHNYX.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hublo/sentinel",
3
- "version": "1.4.0-alpha.42",
3
+ "version": "1.4.0-alpha.44",
4
4
  "description": "One CLI that guards code health across Hublo repos: shared lint/typescript/build/test presets, static & dynamic analysis, and architecture checks.",
5
5
  "type": "module",
6
6
  "license": "MIT",