@hublo/sentinel 1.3.0 → 1.4.0-alpha.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +8 -4
  2. package/dist/bin/sentinel.d.ts +1 -1
  3. package/dist/bin/sentinel.js +22 -9
  4. package/dist/chunk-2XLX6PFR.js +132 -0
  5. package/dist/chunk-2XLX6PFR.js.map +1 -0
  6. package/dist/chunk-3TDUIKVQ.js +178 -0
  7. package/dist/chunk-3TDUIKVQ.js.map +1 -0
  8. package/dist/chunk-CPCUPK4J.js +70 -0
  9. package/dist/chunk-CPCUPK4J.js.map +1 -0
  10. package/dist/chunk-CRKUEP4J.js +427 -0
  11. package/dist/chunk-CRKUEP4J.js.map +1 -0
  12. package/dist/{chunk-676GBPMS.js → chunk-L7WS36XV.js} +3921 -556
  13. package/dist/chunk-PWV3BMDA.js +15 -0
  14. package/dist/chunk-PWV3BMDA.js.map +1 -0
  15. package/dist/chunk-WLFE5RUU.js +264 -0
  16. package/dist/chunk-WLFE5RUU.js.map +1 -0
  17. package/dist/index.d.ts +13 -0
  18. package/dist/index.js +2 -2
  19. package/dist/roles/build/nest/toolchain.d.ts +4 -36
  20. package/dist/roles/build/nest/toolchain.js +10 -178
  21. package/dist/roles/build/nest/toolchain.js.map +1 -0
  22. package/dist/roles/build/toolchain.js.map +1 -0
  23. package/dist/roles/test/nest/toolchain.d.ts +9 -0
  24. package/dist/roles/test/nest/toolchain.js +25 -0
  25. package/dist/roles/test/nest/toolchain.js.map +1 -0
  26. package/dist/roles/test/react/toolchain.d.ts +66 -0
  27. package/dist/roles/test/react/toolchain.js +14 -1
  28. package/dist/roles/test/react/toolchain.js.map +1 -0
  29. package/dist/roles/test/setup/jest-parity.d.ts +2 -0
  30. package/dist/roles/test/setup/jest-parity.js +159 -0
  31. package/dist/roles/test/setup/jest-parity.js.map +1 -0
  32. package/dist/roles/test/setup/mock-extended.d.ts +46 -0
  33. package/dist/roles/test/setup/mock-extended.js +65 -0
  34. package/dist/roles/test/setup/mock-extended.js.map +1 -0
  35. package/dist/roles/test/setup/msw-lifecycle.d.ts +16 -0
  36. package/dist/roles/test/setup/msw-lifecycle.js +12 -0
  37. package/dist/roles/test/setup/msw-lifecycle.js.map +1 -0
  38. package/dist/roles/test/setup/msw-server.d.ts +3 -0
  39. package/dist/roles/test/setup/msw-server.js +10 -0
  40. package/dist/roles/test/setup/msw-server.js.map +1 -0
  41. package/dist/roles/test/setup/workspace-entry.d.ts +2 -0
  42. package/dist/roles/test/setup/workspace-entry.js +8 -0
  43. package/dist/roles/test/setup/workspace-entry.js.map +1 -0
  44. package/dist/roles/test/shared-test-config.d.ts +32 -0
  45. package/dist/roles/test/shared-test-config.js +8 -0
  46. package/dist/roles/test/shared-test-config.js.map +1 -0
  47. package/dist/roles/test/tools/msw.d.ts +1 -0
  48. package/dist/roles/test/tools/msw.js +3 -0
  49. package/dist/roles/test/tools/msw.js.map +1 -0
  50. package/dist/tsconfig-aliases-Ce6axdJ4.d.ts +36 -0
  51. package/docs/.gitkeep +0 -0
  52. package/docs/build-adoption.md +521 -0
  53. package/docs/format-adoption.md +321 -0
  54. package/docs/lint-adoption.md +290 -0
  55. package/docs/performance.md +49 -0
  56. package/docs/test-adoption.md +219 -0
  57. package/docs/typescript-adoption.md +184 -0
  58. package/docs/typescript-traces.md +798 -0
  59. package/docs/using-sentinel.md +195 -0
  60. package/docs/validating-a-change.md +101 -0
  61. package/package.json +35 -6
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../src/roles/test/setup/jest-parity.ts","../../../../src/roles/test/setup/constructor-semantics.ts","../../../../src/roles/test/setup/deep-mock-reset.ts","../../../../src/roles/test/setup/error-equality.ts","../../../../src/roles/test/setup/fake-timers.ts","../../../../src/roles/test/setup/jest-global.ts","../../../../src/roles/test/setup/mock-reset.ts","../../../../src/roles/test/setup/rejected-function.ts"],"sourcesContent":["/**\n * The setup any migrated suite runs before its tests, as ONE entry point.\n *\n * Referenced by name from a generated config, never by a relative path:\n * `setupFiles: ['@hublo/sentinel/test/setup/jest-parity']`. Measured: Vitest resolves a bare\n * package specifier there, so nothing has to know where sentinel sits relative to the module.\n *\n * ⚠️ This was exported as `test/setup/nest` until 23/09, which was wrong in the one place it is\n * read. Nothing below is Nest-specific: every line restores a `jest.*` semantic under Vitest, plus\n * the `.env` cascade. But the name is COMMITTED into each adopting module's config, so a React\n * module ended up with `setupFiles: ['@hublo/sentinel/test/setup/nest', './jest.setup.js']` sitting\n * in its repository, which reads like the bug where a module was handed the wrong family's preset.\n * A reviewer cannot tell that apart from the real thing without opening our source. The name says\n * what the file does instead: parity with jest.\n *\n * ## What it replaces, line for line\n *\n * The repo's root `jest.setup.after.env.js`, loaded by 99 of the 111 jest configs:\n *\n * require('dotenv-flow').config({ silent: true, purge_dotenv: true })\n * const { Settings } = require('luxon')\n * const { server } = require('./libs/nest/tests/src/msw/server')\n * jest.mock('dynamoose')\n * jest.mock('@opentelemetry/exporter-metrics-otlp-grpc')\n * beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))\n * afterEach(() => { if (global.gc) global.gc() })\n * afterAll(() => server.close())\n * Settings.defaultZone = 'utc'\n *\n * Everything here is a transcription of that, not an improvement on it. Where the original made a\n * choice that looks questionable, the choice is carried across and reported, because a migration\n * that also changes behaviour cannot be verified against its own baseline.\n *\n * ONE line is not transcribed: `server.listen()` also runs when this file is evaluated, not only\n * inside `beforeAll`. The runners differ on when a module's exports can still be patched, so the\n * original placement silently disabled interception under Vitest and let tests reach the real\n * internet. `./msw.js` carries the measurement.\n *\n * ## Two halves, and why one of them is loaded defensively\n *\n * The msw lifecycle used to live here and now has its own file, `./msw-lifecycle.js`, added to a\n * module's `setupFiles` only when that module actually uses msw. Installing it everywhere patches\n * `http`/`https` and refuses unmatched requests for modules that never asked: measured on\n * `libs/nest/starter`, 11 of 11 became 10 of 11 with `TypeError: Invalid URL` in the interceptor,\n * on a test fetching the Fastify server the suite starts itself.\n *\n * `dotenv-flow` and luxon's zone react to the REPO, not to the runner: they would read the same\n * under jest, Vitest or `node:test`. So `./workspace.js` loads them only if they are installed and\n * skips them in silence otherwise. That defensiveness is not a precaution bolted on, it IS the\n * statement that these belong to whoever installed them. Elsewhere, sentinel installs and that half\n * does nothing.\n *\n * `dotenv-flow` cannot simply be dropped in favour of Vite's own `.env` handling, which was the\n * first thing checked. Measured on Vitest 4: it does read the cascade and sets `MODE=test`, but it\n * exposes only `VITE_`-prefixed values, and only on `import.meta.env`. `process.env` is left\n * untouched, and Nest code reads unprefixed `process.env`.\n *\n * ## What is NOT here\n *\n * The two `jest.mock` calls. A module mock is a per-suite decision that `vi.mock` must make in the\n * file that needs it; hoisting it into a shared setup is what makes a test pass for a reason nobody\n * can see. The migration reports them so the module relying on one declares it.\n *\n * And `Settings.defaultZone = 'utc'` is carried as luxon's own setting, never translated to\n * `process.env.TZ`. One configures luxon, the other the whole process, `Date` and `Intl` included.\n * Swapping them would change what the suite does while claiming to migrate it, and timezone is not\n * a detail: measured on `host-admin`, a machine's zone accounted for a large part of 345 local\n * failures that did not exist in CI.\n */\nimport { expect, vi } from 'vitest'\n\nimport { installJestConstructorSemantics } from './constructor-semantics.js'\nimport { installDeepMockReset } from './deep-mock-reset.js'\nimport { installJestErrorEquality } from './error-equality.js'\nimport { installJestFakeTimerOptions } from './fake-timers.js'\nimport { installJestGlobal } from './jest-global.js'\nimport { installJestMockReset } from './mock-reset.js'\nimport { installJestRejectedFunction } from './rejected-function.js'\nimport { installWorkspaceSetup } from './workspace.js'\n\ninstallJestErrorEquality(expect)\ninstallJestMockReset(vi)\ninstallJestConstructorSemantics(vi)\ninstallDeepMockReset(vi)\ninstallJestFakeTimerOptions(vi)\ninstallJestRejectedFunction()\ninstallJestGlobal(vi)\n\n/*\n * The `.env` cascade, unconditionally: a suite reading `process.env` needs it whatever setup its\n * jest config named, because Vitest's workers do not inherit what a `globalSetup` set in the main\n * process. Awaited at the top level, so it is loaded before the first test file is imported: a\n * module read at import time would already have captured an unset variable.\n *\n * ⚠️ What is NOT here any more is luxon's UTC zone. That was a decision ONE file at the workspace\n * root made, and only the 97 modules naming it ever had it; it lives in\n * `@hublo/sentinel/test/setup/workspace`, which the generated config adds only for those.\n */\nawait installWorkspaceSetup()\n","/**\n * `new` on a mock, the way jest answered it.\n *\n * ## The divergence, measured on the same source under both runners\n *\n * jest vitest\n * mockImplementation(() => obj), then new the obj TypeError: not a constructor\n * mockImplementation(function(){}), new works works\n * mockReturnValue(obj), then new the obj TypeError, with an explanation\n *\n * jest calls the implementation as a PLAIN function when the mock is constructed and hands back\n * what it returned. Vitest applies real `new` semantics, and an arrow function has no [[Construct]]\n * slot, so it throws.\n *\n * The pattern this breaks is the ordinary way to stand in for a class: automock the module, then\n * say what `new` should give back.\n *\n * jest.mock('@hublo/cloud/event-scheduler-sdk')\n * ;(EventSchedulerSDK as jest.MockedClass<typeof EventSchedulerSDK>)\n * .mockImplementation(() => mockEventSchedulerSDK)\n *\n * Measured across the nest and cloud campaigns: 2 modules, 15 tests.\n * `apps/nest/microservices/client-management` (9, an SDK) and `apps/nest/microservices/hublo-pool`\n * (6, a mocked `Date`).\n *\n * ## Why this is safe to do for everybody, which is the part that matters\n *\n * It only changes a case that THROWS today. An implementation without a `prototype` cannot be\n * constructed at all under Vitest, so no suite anywhere can be relying on what it does: the only\n * behaviours available are \"throws\" and \"answers like jest\". Nothing that works today changes\n * shape, and a constructable implementation is passed through untouched.\n *\n * That bound is asserted by the tests, not just claimed here.\n *\n * ## `mockReturnValue(obj)` then `new`, which this used to leave alone\n *\n * It was left out on the grounds that answering it would decide \"a return value and a constructed\n * instance are the same thing\", a claim about the suite. That reading was wrong on both halves.\n *\n * It is not a claim about the suite, because jest's answer is not ambiguous: it hands back the\n * value, exactly as it does for `mockImplementation(() => obj)`, which this file already restores.\n * Treating the two differently would be the arbitrary choice.\n *\n * And \"no module in this repo hit it\" stopped being true the moment the front family was measured.\n * `libs/front/components` mocks Google Maps the ordinary way and constructs it:\n *\n * AutocompleteService: jest.fn().mockReturnValue({ getPlacePredictions: jest.fn() })\n * // and the hook: new window.google.maps.places.AutocompleteService()\n *\n * 7 tests, all with the same TypeError. The safety bound is unchanged and it is the whole reason\n * this is allowed: Vitest THROWS on that call today, so no suite anywhere can depend on what it\n * does, and the only behaviours available are \"throws\" and \"answers like jest\".\n *\n * Expressed by routing the value through the implementation, rather than by a second mechanism:\n * `mockReturnValue(v)` IS `mockImplementation(() => v)`, so it goes through the same wrapper and\n * `new` works for the same reason.\n */\n\n/** The mocking surface this touches. Vitest's own type is not needed to say it. */\ninterface MockLike {\n mockImplementation(implementation: (...args: unknown[]) => unknown): unknown\n mockImplementationOnce(implementation: (...args: unknown[]) => unknown): unknown\n mockReturnValue(value: unknown): unknown\n mockReturnValueOnce(value: unknown): unknown\n}\n\ninterface ViLike {\n fn(implementation?: (...args: unknown[]) => unknown): MockLike\n /**\n * ⚠️ The REST, not `(target, key)`.\n *\n * Vitest's third argument is the access type, `'get'` or `'set'`, and the first version of the\n * wrapper below forwarded two arguments and dropped it. `vi.spyOn(el, 'scrollWidth', 'get')`\n * then became a spy on the VALUE of an accessor that only exists on a prototype, and jsdom\n * answered `'get scrollWidth' called on an object that is not a valid instance of Element`.\n *\n * Measured on `libs/front/components`, 2 tests, and invisible to every nest module because none\n * of them spies on a DOM accessor.\n */\n spyOn(target: object, ...rest: unknown[]): MockLike\n}\n\n/**\n * Can this function be used with `new`?\n *\n * Asked of the `prototype` property rather than of the source text: an arrow function, a shorthand\n * method and a bound function all lack it, and all three are exactly the cases that throw. A\n * class and a plain `function` have it.\n */\nfunction constructable(value: unknown): boolean {\n return (\n typeof value === 'function' && Object.getOwnPropertyDescriptor(value, 'prototype') !== undefined\n )\n}\n\n/**\n * The same implementation, reachable through `new`.\n *\n * A plain `function` that forwards the call and RETURNS the result. JavaScript's own `new` then\n * hands that object back, which is what jest did, so nothing here imitates jest by hand: it\n * restores the one property the arrow was missing and lets the language do the rest.\n */\nfunction asConstructable(\n implementation: (...args: unknown[]) => unknown,\n): (...args: unknown[]) => unknown {\n if (constructable(implementation)) return implementation\n\n return function forwarded(this: unknown, ...args: unknown[]): unknown {\n return implementation.apply(this, args)\n }\n}\n\n/**\n * Wrap `vi.fn` and `vi.spyOn` so every mock they produce accepts `new` the way jest's did.\n *\n * Wrapped at the factory, like `installJestMockReset`, because the behaviour belongs to every mock\n * a suite makes and a suite should not have to ask for it.\n */\nexport function installJestConstructorSemantics(vi: ViLike): void {\n const patch = (mock: MockLike): MockLike => {\n const { mockImplementation, mockImplementationOnce } = mock\n\n mock.mockImplementation = function (implementation) {\n return mockImplementation.call(this, asConstructable(implementation))\n }\n mock.mockImplementationOnce = function (implementation) {\n return mockImplementationOnce.call(this, asConstructable(implementation))\n }\n\n /*\n * Routed through the implementation rather than given a mechanism of its own: the two are the\n * same statement, and one of them already accepts `new`.\n */\n mock.mockReturnValue = function (value) {\n return this.mockImplementation(() => value)\n }\n mock.mockReturnValueOnce = function (value) {\n return this.mockImplementationOnce(() => value)\n }\n return mock\n }\n\n const { fn, spyOn } = vi\n\n vi.fn = function (implementation) {\n return patch(fn.call(this, implementation && asConstructable(implementation)))\n }\n vi.spyOn = function (target, ...rest) {\n return patch(spyOn.call(this, target, ...rest))\n }\n}\n","/**\n * `vi.resetAllMocks()` reaching the deep mocks, the way jest's registry did.\n *\n * ## The divergence, and why it is invisible\n *\n * jest built `jest-mock-extended`'s mocks with `jest.fn()`, so they sat in jest's own registry and\n * `jest.resetAllMocks()` cleared them with everything else. `vitest-mock-extended` builds them its\n * own way, so `vi.resetAllMocks()` walks past them and their call history survives into the next\n * test.\n *\n * Nothing announces it. The suite still runs, and an assertion fails several tests later with a\n * count that is off by exactly what its neighbour did.\n *\n * Measured on `apps/nest/microservices/activity`, whose suite does what jest expected:\n *\n * beforeEach(() => mocked.findEvents.mockResolvedValue([]))\n * afterEach(() => vi.resetAllMocks())\n *\n * Eleven tests asserting `toHaveBeenCalledTimes(0)` saw the call left by the one before them. Each\n * PASSES on its own and fails as soon as its neighbour runs first, which is the signature of\n * leakage rather than of a wrong assertion.\n *\n * ## What this installs, and what it leaves alone\n *\n * `resetAllMocks` and `clearAllMocks` do what they did, then extend to the deep mocks: `reset`\n * drops implementations as well as calls, `clear` drops only calls, which is the same distinction\n * the two names already carry.\n *\n * `restoreAllMocks` is NOT extended. It restores spies to their originals, and a deep mock has no\n * original to go back to: it was invented. Extending it would mean deciding what \"restore\" means\n * for something that never existed, which is a claim, not a translation.\n */\nimport { clearDeepMocks, resetDeepMocks } from './mock-extended.js'\n\n/** The part of `vi` this touches. Vitest's own type is not needed to say it. */\ninterface ViLike {\n resetAllMocks(): unknown\n clearAllMocks(): unknown\n}\n\nexport function installDeepMockReset(vi: ViLike): void {\n const { resetAllMocks, clearAllMocks } = vi\n\n vi.resetAllMocks = function extended(this: unknown): unknown {\n const answer = resetAllMocks.call(this)\n resetDeepMocks()\n return answer\n }\n\n vi.clearAllMocks = function extended(this: unknown): unknown {\n const answer = clearAllMocks.call(this)\n clearDeepMocks()\n return answer\n }\n}\n","/**\n * How two `Error` values compare, which the two runners disagree about.\n *\n * A suite that asserts on a thrown or captured error usually writes the error it expects by hand:\n *\n * expect(save).toHaveBeenCalledWith({ error: new AxiosError('Request failed with status code 500'), ... })\n *\n * Under jest that passes whatever else the real error carries. Under Vitest it fails, and the\n * report is 6600 lines of an axios error's `config`, `request` and `response`, which reads like a\n * broken test rather than a runner difference.\n *\n * ## What each runner actually does, measured on the same four cases\n *\n * | two errors | jest 29 | Vitest 4 |\n * | --------------------------------- | -------- | ----------- |\n * | same message, same type | equal | equal |\n * | same message, DIFFERENT types | equal | not equal |\n * | same message, extra properties | equal | not equal |\n * | different messages | not equal| not equal |\n *\n * jest compares errors by their MESSAGE and nothing else: a `TypeError` and a `RangeError` with the\n * same text are equal to it. Vitest compares the type and the own properties too.\n *\n * ## Why the looser rule is the one restored\n *\n * Because it is the one 3481 test files were written against. Tightening it here would turn green\n * tests red during a migration whose whole promise is that the suite means the same thing\n * afterwards, and a baseline gate cannot tell that kind of loss from a real one.\n *\n * The question is reported rather than settled: comparing the type as well would be a better rule,\n * and it may cost nothing on this corpus. That is a measurement to run and a change to make on its\n * own, once the suites no longer move. Measured need so far: `libs/cloud/shared`, whose last\n * missing test was exactly this.\n */\nimport type { expect as ExpectApi } from 'vitest'\n\n/**\n * Restore jest's rule: two errors are equal when their messages are.\n *\n * Returning `undefined` for anything else hands the pair back to the default comparison, which is\n * what an equality tester is expected to do for values it has no opinion about.\n */\nexport function errorsCompareByMessage(left: unknown, right: unknown): boolean | undefined {\n if (left instanceof Error && right instanceof Error) return left.message === right.message\n return undefined\n}\n\nexport function installJestErrorEquality(expect: typeof ExpectApi): void {\n expect.addEqualityTesters([errorsCompareByMessage])\n}\n","/**\n * `useFakeTimers({ doNotFake: [...] })`, which Vitest accepts and ignores.\n *\n * jest names what to LEAVE ALONE, Vitest names what to FAKE. The option Vitest does not know is\n * dropped in silence, so a suite that carefully kept `setTimeout` real gets it faked, and anything\n * awaiting a timer never resolves.\n *\n * Measured on both runners with the same source:\n *\n * useFakeTimers({ doNotFake: ['setTimeout'] }) jest: setTimeout real Vitest: setTimeout FAKED\n * useFakeTimers({ toFake: ['Date'] }) Vitest: setTimeout real\n *\n * Found on `libs/cloud/events-notifications`: 4 tests in one file died on `Test timed out in\n * 5000ms` with nothing else to show, because the code under test awaits a real timer. The repo has\n * 2 files using `doNotFake`, the other in `apps/nest/microservices/institution`.\n *\n * ## The translation, and what it inherits\n *\n * `doNotFake: [a, b]` becomes `toFake: <everything the runner fakes by default> minus [a, b]`. The\n * default set is Vitest's, measured rather than assumed, and NOT jest's, which is wider: jest also\n * fakes `nextTick`, `queueMicrotask` and the animation-frame pair. Subtracting from Vitest's own\n * default is what every other `useFakeTimers()` call in the corpus already gets, so this keeps one\n * behaviour for the whole migration instead of two.\n */\n\n/**\n * What `vi.useFakeTimers()` replaces when told nothing, measured on Vitest 4 by comparing each\n * global before and after the call.\n */\nconst FAKED_BY_DEFAULT = [\n 'setTimeout',\n 'clearTimeout',\n 'setInterval',\n 'clearInterval',\n 'setImmediate',\n 'clearImmediate',\n 'Date',\n 'performance',\n 'hrtime',\n] as const\n\n/** The options both runners take, plus the one only jest knows. */\ninterface TimerOptions {\n toFake?: string[]\n doNotFake?: string[]\n}\n\n/**\n * Turn \"leave these alone\" into \"fake those\", leaving anything else untouched.\n *\n * Exported for its own test: the translation is the whole rule, and asserting it directly says more\n * than asserting that a wrapper was installed.\n */\nexport function withoutJestOnlyOptions<T>(options: T): T {\n const given = options as TimerOptions | undefined\n if (given?.doNotFake === undefined) return options\n\n const { doNotFake, ...rest } = given\n const base = rest.toFake ?? [...FAKED_BY_DEFAULT]\n\n return { ...rest, toFake: base.filter((timer) => !doNotFake.includes(timer)) } as T\n}\n\n/**\n * The one function this touches, named by its shape rather than by Vitest's type.\n *\n * `Options` is the caller's own parameter type: the wrapper hands back exactly what it was given,\n * minus the option Vitest does not know, so it must not narrow what the runner accepts.\n */\ninterface FakeTimerApi<Options> {\n useFakeTimers: (options?: Options) => unknown\n}\n\n/** Wrap `vi.useFakeTimers` so a jest-shaped options object still means what it said. */\nexport function installJestFakeTimerOptions<Options>(vi: FakeTimerApi<Options>): void {\n const inherited = vi.useFakeTimers.bind(vi)\n vi.useFakeTimers = (options?: Options) => inherited(withoutJestOnlyOptions(options))\n}\n","/**\n * The `jest` global, kept alive for helpers that a migrating module is not allowed to edit.\n *\n * ## Why a module cannot solve this for itself\n *\n * The codemod rewrites a module's own test files. It does not rewrite files in OTHER projects, and\n * it must not: a shared helper is imported by modules still on jest, so migrating it would break\n * them, and leaving it breaks the migrated one. That is the constraint the whole per-module plan\n * rests on.\n *\n * But those helpers call the jest API at MODULE scope. `libs/front/tests/src/mocks/**` does\n * `jest.fn()` when it is imported, before any test runs, so a migrated module dies on\n * `ReferenceError: jest is not defined` the moment it imports one.\n *\n * Measured repo-wide, excluding documentation: **50 files use the jest API without being test\n * files**, in `jest.setup.js`, `*.mock.ts`, `*.test-helper.ts`, `*.test-wrapper.ts`. Three\n * independent hand migrations reached this same line without knowing about each other:\n * `libs/front/components` (8 shared helpers, 16 sites), `apps/nest/microservices/mission` and\n * `apps/nest/backends-for-frontends/admin`.\n *\n * ## It is `vi`, not a fake jest\n *\n * The global IS Vitest's `vi`, so anything Vitest does not have keeps failing loudly:\n * `jest.requireActual` and `jest.isolateModules` are still errors, and a module relying on them\n * still has to be migrated properly. Handing over a hand-written imitation would turn those into\n * silent wrong behaviour, which is the opposite of the point.\n *\n * ## ⚠️ What it does NOT cover, and this bound is measured\n *\n * `jest.mock()`. Vitest hoists mock registrations above the imports by scanning the source\n * STATICALLY, and that scan only recognises the receivers `vi` and `vitest` (`@vitest/mocker`,\n * `hoistMocksPlugin`). A `jest.mock()` left in place is therefore NOT hoisted: it runs after the\n * imports it was meant to intercept and does nothing at all, in silence. Measured on\n * `apps/front/front-legacy`, where 216 of 427 files call it.\n *\n * So this covers a helper that CALLS the jest API. It does not make an unmigrated test file work,\n * and the codemod's rename stays load-bearing rather than cosmetic.\n */\n\n/** The part of `vi` this installs. Vitest's own type is not needed to say it. */\ntype JestLike = object\n\n/**\n * Put `vi` on `globalThis` under the name `jest`.\n *\n * Assigned rather than defined with a getter: a helper may well write to it (`jest.fn = ...` in a\n * test double), and a getter-only property would throw where jest allowed it.\n */\nexport function installJestGlobal(vi: JestLike): void {\n ;(globalThis as Record<string, unknown>).jest = vi\n}\n","/**\n * What `mockReset()` leaves behind, which is where the two runners disagree most dangerously.\n *\n * jest REMOVES the implementation: a reset spy returns `undefined` and the real function is not\n * called. Vitest puts the ORIGINAL implementation back: a reset spy calls the real function again.\n *\n * Measured on both runners with the same source:\n *\n * after resetAllMocks() on a spy jest: undefined Vitest: the real function\n * after mockReset() on a spy jest: undefined Vitest: the real function\n * after mockReset() on fn(impl) jest: undefined Vitest: impl\n *\n * The shape this breaks is ordinary and common: a suite spies on a provider in `beforeAll` and\n * resets its mocks in `beforeEach`. Under jest the provider stayed neutralised for every test.\n * Under Vitest the first `beforeEach` hands the real provider back, and every test after it runs\n * the real code. Measured on `libs/cloud/events-notifications`, that meant real HTTP: 19 tests\n * failed on `captured a request without a matching request handler` for the hermes API and 23 more\n * timed out waiting on it. Nothing in any report named a reset.\n *\n * 240 files in this repo both spy and reset, in every family: 111 under `apps/nest`, 77 under\n * `libs/cloud`, 31 under `apps/front`.\n *\n * ## Why here and not in the 240 files\n *\n * Because a codemod would have to decide, per spy, whether the suite wanted the real function\n * back, and the answer is in the test's intent rather than in its text. The runner-level rule is\n * the one that was true for all 240 while they were written, so restoring it is the transcription\n * and rewriting them would be the guess.\n *\n * Vitest routes `vi.resetAllMocks()` through each mock's own `mockReset`, measured, so overriding\n * that method covers the bulk form as well as the direct one. `mockRestore` is untouched: it puts\n * the original back under both runners, which is what it is for.\n */\n\n/** The part of a mock this file touches. Vitest's own types are not needed to say it. */\ninterface ResettableMock {\n mockReset: () => unknown\n mockImplementation: (fn: (...args: unknown[]) => unknown) => unknown\n}\n\nfunction isResettable(value: unknown): value is ResettableMock {\n return (\n typeof value === 'function' &&\n typeof (value as Partial<ResettableMock>).mockReset === 'function' &&\n typeof (value as Partial<ResettableMock>).mockImplementation === 'function'\n )\n}\n\n/**\n * Make one mock forget its implementation on reset, as jest's did.\n *\n * The override is installed on the instance rather than on a prototype: mocks are functions with\n * their own properties, and there is no shared prototype to reach.\n */\nfunction resetLikeJest<T>(mock: T): T {\n if (!isResettable(mock)) return mock\n\n const inherited = mock.mockReset.bind(mock)\n mock.mockReset = () => {\n inherited()\n mock.mockImplementation(() => undefined)\n return mock\n }\n return mock\n}\n\n/** The two factories a suite gets its mocks from. */\ntype MockFactories = { fn: (...args: never[]) => unknown; spyOn: (...args: never[]) => unknown }\n\n/**\n * Wrap `vi.fn` and `vi.spyOn` so everything they hand out resets the way jest's did.\n *\n * Called with the `vi` a setup file imports, so nothing here reaches for a global.\n */\nexport function installJestMockReset(vi: MockFactories): void {\n for (const name of ['fn', 'spyOn'] as const) {\n const factory = vi[name].bind(vi) as (...args: never[]) => unknown\n vi[name] = ((...args: never[]) => resetLikeJest(factory(...args))) as MockFactories[typeof name]\n }\n}\n","/**\n * A rejected value that is a FUNCTION, which jest calls and Vitest does not.\n *\n * ## The divergence, measured rather than reasoned\n *\n * jest's `toThrow` decides what was thrown like this: under `.rejects` it uses the rejection\n * reason ONLY when that reason is an Error. Otherwise it falls through to the ordinary branch,\n * sees a function, and CALLS it, asserting on whatever that call throws.\n *\n * Probed on this repo under jest 29, with controls:\n *\n * reject(() => { throw new Error('some error') })\n * await expect(…).rejects.toThrow(new Error('some error')) -> passes\n * await expect(…).rejects.toThrow(new Error('other text')) -> fails\n * await expect(…).rejects.toThrow('other text') -> fails\n *\n * The two controls are what make the first line mean something: jest is not passing everything,\n * it really is comparing the message of the error the CALL produced.\n *\n * Vitest treats the rejection reason as the thrown value, so the assertion is made against a\n * function. A function has no `message`, and the failure reads\n * `Cannot read properties of undefined (reading 'indexOf')`, which names nothing near the cause.\n *\n * ## Where it shows, and what it is worth\n *\n * Measured across the nest campaign: 4 tests in 3 modules.\n * `apps/nest/microservices/worker` (1), `apps/nest/microservices/institution` (2) and\n * `apps/nest/microservices/mission` (1). All four write the same shape, a mock rejecting with a\n * thunk that throws, or a `throw <a function>`.\n *\n * ## What this changes, and what it cannot\n *\n * Only a case that CANNOT work today: under `.rejects`, a reason that is a function and not an\n * Error. Vitest has no useful behaviour there, so nothing that passes today changes shape. An\n * Error reason, a string, an object, a rejected value of any other kind, and every assertion\n * outside `.rejects` all reach Vitest's own matcher untouched.\n *\n * ⚠️ It is jest's behaviour, not a good one. A suite reaching it is asserting on a function it\n * never meant to hand over, and it passed by accident of the runner. Reproducing it is what keeps\n * the migration honest: the gate promises the suite means the same thing afterwards, and a test\n * that was green cannot be turned red by us and called a finding. The teams own the cleanup.\n */\nimport { chai } from 'vitest'\n\n/** The part of a chai assertion this touches. Chai's own types are not needed to say it. */\ninterface AssertionLike {\n _obj: unknown\n}\n\ntype Matcher = (this: AssertionLike, ...args: unknown[]) => unknown\n\n/**\n * What calling the function throws, or the function itself when it throws nothing.\n *\n * Returning it unchanged matters: a function that completes is not \"nothing was thrown\", and\n * handing Vitest the same value it had leaves the report exactly as it would have been.\n */\nfunction thrownByCalling(candidate: () => unknown): unknown {\n try {\n candidate()\n } catch (thrown) {\n return thrown\n }\n return candidate\n}\n\nexport function installJestRejectedFunction(): void {\n const { Assertion, util } = chai as unknown as {\n Assertion: { prototype: Record<string, unknown> }\n util: { flag(object: unknown, key: string): unknown }\n }\n\n for (const name of ['toThrow', 'toThrowError']) {\n const original = Assertion.prototype[name] as Matcher | undefined\n if (typeof original !== 'function') continue\n\n Assertion.prototype[name] = function patched(this: AssertionLike, ...args: unknown[]): unknown {\n const reason = this._obj\n const rejected = util.flag(this, 'promise') === 'rejects'\n\n if (rejected && typeof reason === 'function' && !(reason instanceof Error)) {\n this._obj = thrownByCalling(reason as () => unknown)\n }\n\n return original.apply(this, args)\n } as unknown as Matcher\n }\n}\n"],"mappings":"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","names":["vi","vi","expect","vi","vi","vi"]}
@@ -0,0 +1,46 @@
1
+ import * as mockExtended from 'vitest-mock-extended';
2
+ export * from 'vitest-mock-extended';
3
+ export { any, anyArray, anyBoolean, anyFunction, anyMap, anyNumber, anyObject, anySet, anyString, anySymbol, arrayIncludes, calledWithFn, captor, isA, isMockObject, mapHas, matches, mockClear, mockFn, mockReset, mocked, mockedFn, notEmpty, notNull, notUndefined, objectContainsKey } from 'vitest-mock-extended';
4
+
5
+ /**
6
+ * `jest-mock-extended`'s deep mocks, made answerable to the question Vitest's `spyOn` asks.
7
+ *
8
+ * A deep mock is a Proxy that invents a property the first time something READS it. Nothing exists
9
+ * until then, so the object reports itself empty:
10
+ *
11
+ * const provider = mockDeep<InstitutionProvider>()
12
+ * 'getAdminFirstAndLastNames' in provider // false
13
+ * Object.getOwnPropertyDescriptor(provider, 'getAdmin...') // undefined
14
+ * typeof provider.getAdminFirstAndLastNames // 'function', and now it exists
15
+ *
16
+ * jest's `spyOn` reads the property, so the Proxy created it and the spy worked. Vitest's asks the
17
+ * object whether it HAS the property first, gets no for both questions, and throws
18
+ * `The property "getAdminFirstAndLastNames" is not defined on the object.` The file then reports no
19
+ * test at all, so its names simply go missing rather than failing.
20
+ *
21
+ * Measured on `apps/cloud/shift`: one such spy took 37 of its 108 tests away. The repo has 128 of
22
+ * them across 29 files, mostly in `apps/nest/microservices` and
23
+ * `apps/nest/backends-for-frontends`.
24
+ *
25
+ * ## Why it is fixed here and not in the 128 test files
26
+ *
27
+ * Because it is a difference between two runners, not something 29 suites each got wrong. A
28
+ * codemod rewriting every site would put a migration artefact in front of every reader of those
29
+ * files forever, and teams would carry it. One adapter keeps the test files exactly as they are.
30
+ *
31
+ * The two traps answer by doing what jest's `spyOn` did: read the property once, then answer. The
32
+ * read is the Proxy's own documented way of materialising it, so nothing here reimplements the
33
+ * mock, it only asks the question in the form the underlying object understands.
34
+ *
35
+ * `then` is never materialised. A deep mock that suddenly HAS a `then` is a thenable, and awaiting
36
+ * it, or returning it from an async function, would hang on a promise nothing resolves.
37
+ */
38
+
39
+ /** Reset every deep mock this adapter created, the way jest's registry did. */
40
+ declare function resetDeepMocks(): void;
41
+ /** Clear their calls without touching their implementations. */
42
+ declare function clearDeepMocks(): void;
43
+ declare const mock: typeof mockExtended.mock;
44
+ declare const mockDeep: typeof mockExtended.mockDeep;
45
+
46
+ export { clearDeepMocks, mock, mockDeep, resetDeepMocks };
@@ -0,0 +1,65 @@
1
+ import {
2
+ any,
3
+ anyArray,
4
+ anyBoolean,
5
+ anyFunction,
6
+ anyMap,
7
+ anyNumber,
8
+ anyObject,
9
+ anySet,
10
+ anyString,
11
+ anySymbol,
12
+ arrayIncludes,
13
+ calledWithFn,
14
+ captor,
15
+ clearDeepMocks,
16
+ isA,
17
+ isMockObject,
18
+ mapHas,
19
+ matches,
20
+ mock,
21
+ mockClear,
22
+ mockDeep,
23
+ mockFn,
24
+ mockReset,
25
+ mocked,
26
+ mockedFn,
27
+ notEmpty,
28
+ notNull,
29
+ notUndefined,
30
+ objectContainsKey,
31
+ resetDeepMocks
32
+ } from "../../../chunk-2XLX6PFR.js";
33
+ export {
34
+ any,
35
+ anyArray,
36
+ anyBoolean,
37
+ anyFunction,
38
+ anyMap,
39
+ anyNumber,
40
+ anyObject,
41
+ anySet,
42
+ anyString,
43
+ anySymbol,
44
+ arrayIncludes,
45
+ calledWithFn,
46
+ captor,
47
+ clearDeepMocks,
48
+ isA,
49
+ isMockObject,
50
+ mapHas,
51
+ matches,
52
+ mock,
53
+ mockClear,
54
+ mockDeep,
55
+ mockFn,
56
+ mockReset,
57
+ mocked,
58
+ mockedFn,
59
+ notEmpty,
60
+ notNull,
61
+ notUndefined,
62
+ objectContainsKey,
63
+ resetDeepMocks
64
+ };
65
+ //# sourceMappingURL=mock-extended.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,16 @@
1
+ import * as msw_node from 'msw/node';
2
+ import 'msw';
3
+
4
+ /**
5
+ * No default handlers, deliberately.
6
+ *
7
+ * The repo's own server had `defaultHandlers = []` too, so nothing is lost. A handler here would
8
+ * be a Hublo endpoint inside a package meant to be usable by anyone, which is the line between
9
+ * what sentinel owns (the config) and what an app owns (what it mocks).
10
+ *
11
+ * A suite adds its own with `server.use(...)`, which is how all 600 files that touch this already
12
+ * work.
13
+ */
14
+ declare const server: msw_node.SetupServer;
15
+
16
+ export { server };
@@ -0,0 +1,12 @@
1
+ import {
2
+ installMswLifecycle,
3
+ server
4
+ } from "../../../chunk-PWV3BMDA.js";
5
+
6
+ // src/roles/test/setup/msw-lifecycle.ts
7
+ import { afterAll, afterEach, beforeAll } from "vitest";
8
+ installMswLifecycle({ beforeAll, afterEach, afterAll });
9
+ export {
10
+ server
11
+ };
12
+ //# sourceMappingURL=msw-lifecycle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../src/roles/test/setup/msw-lifecycle.ts"],"sourcesContent":["/**\n * The msw lifecycle, as its OWN setup file, loaded only by a module that uses msw.\n *\n * ## Why it moved out of the shared setup\n *\n * It used to run for every adopted module, and that was wrong in a way only a real module could\n * show. Installing msw means patching `http` and `https` and refusing any request that matches no\n * handler. A module that never asked for msw gets that refusal anyway, and its own traffic is what\n * pays.\n *\n * Measured on `libs/nest/starter`, whose suite talks to a Fastify server it starts itself:\n *\n * its own config 11 of 11\n * the same config plus the shared setup 10 of 11, `TypeError: Invalid URL`\n * inside @mswjs/interceptors' fetch interceptor\n *\n * That module has no msw handler anywhere. The failing test does\n * `new URL(`${prefix}/events`, await app.getUrl())` and fetches its own server, and the\n * interceptor cannot read that URL.\n *\n * So the rule is the one asked for at the start: intelligent per module, decided from what the\n * module's files actually contain, not applied to everybody because it is convenient.\n *\n * ## What it does NOT change\n *\n * The lifecycle itself is untouched: `listen` at evaluation time AND in `beforeAll`,\n * `onUnhandledRequest: 'error'` carried across, no `resetHandlers`. `./msw.js` carries the\n * measurement for each of those.\n */\nimport { afterAll, afterEach, beforeAll } from 'vitest'\n\nimport { installMswLifecycle, server } from './msw.js'\n\ninstallMswLifecycle({ beforeAll, afterEach, afterAll })\n\n/**\n * Re-exported so a suite can add its own handlers, which is how all 600 files that touch msw here\n * already work: `server.use(...)` inside a test.\n */\nexport { server }\n"],"mappings":";;;;;;AA6BA,SAAS,UAAU,WAAW,iBAAiB;AAI/C,oBAAoB,EAAE,WAAW,WAAW,SAAS,CAAC;","names":[]}
@@ -0,0 +1,3 @@
1
+ export { server } from './msw-lifecycle.js';
2
+ export * from 'msw';
3
+ import 'msw/node';
@@ -0,0 +1,10 @@
1
+ import {
2
+ server
3
+ } from "../../../chunk-PWV3BMDA.js";
4
+
5
+ // src/roles/test/setup/msw-server.ts
6
+ export * from "msw";
7
+ export {
8
+ server
9
+ };
10
+ //# sourceMappingURL=msw-server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../src/roles/test/setup/msw-server.ts"],"sourcesContent":["/**\n * The msw server a migrated suite shares with its setup.\n *\n * ## Why this entry exists at all\n *\n * The setup beside it starts a server and the tests add handlers to one. If those are two different\n * instances, the one that listens has no handlers and every request is unhandled, which under\n * `onUnhandledRequest: 'error'` fails every test that touches HTTP.\n *\n * That is not hypothetical. Migrating `apps/cloud/agency` and running its suite produced exactly\n * it: 22 green tests under jest became 10 failures, with `[MSW] Cannot bypass a request when using\n * the \"error\" strategy`. The setup had been absorbed into sentinel while the 599 files that import\n * the server still pointed at the workspace's own instance.\n *\n * So the server is exported under its own name, and the codemod repoints those imports here. One\n * instance, listened to by the setup and used by the tests.\n */\nexport { server } from './msw.js'\n\n/**\n * msw's own API, re-exported, so a module gets its handlers from the SAME physical copy that\n * intercepts.\n *\n * sentinel ships msw as a dependency on purpose: a module must stand alone, and the workspace root\n * that used to provide it is going away. But a test that keeps `import { rest } from 'msw'` builds\n * its handlers with the WORKSPACE's copy while the server that listens is built with sentinel's.\n * Same version, two physical instances, one interceptor: the handler is never matched.\n *\n * Measured on `libs/cloud/shared`: the msw handler is never called and the test fails on\n * `expected \"vi.fn()\" to be called 1 times, but got 0 times`. Same family as the axios duplicate,\n * where `resolve.dedupe` changed nothing and only pointing at one physical path did.\n *\n * `export *` rather than a list: `rest` covers 669 of the 671 importing files here, but the\n * surface a test may need (`graphql`, `ctx`, the handler types) belongs to msw, not to a list\n * sentinel would have to keep in step. `setupServer` is not part of it: it lives in `msw/node`,\n * and the server is sentinel's to create.\n */\nexport * from 'msw'\n"],"mappings":";;;;;AAqCA,cAAc;","names":[]}
@@ -0,0 +1,2 @@
1
+
2
+ export { }
@@ -0,0 +1,8 @@
1
+ import {
2
+ pinWorkspaceTimezone
3
+ } from "../../../chunk-CPCUPK4J.js";
4
+ import "../../../chunk-WLFE5RUU.js";
5
+
6
+ // src/roles/test/setup/workspace-entry.ts
7
+ await pinWorkspaceTimezone();
8
+ //# sourceMappingURL=workspace-entry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../src/roles/test/setup/workspace-entry.ts"],"sourcesContent":["/**\n * What the WORKSPACE's shared jest setup did, for the modules that actually loaded it.\n *\n * Separate from `setup/jest-parity` on purpose, and the split is the whole point. `setup/jest-parity` carries the\n * runner shims: `jest.*` semantics restored under Vitest, which every migrated module needs whatever\n * its config said. This entry carries the ZONE that `jest.setup.after.env.js` pinned at the workspace root, which\n * only a module whose jest config NAMED that file ever had:\n *\n * require('dotenv-flow').config(...) the .env cascade\n * Settings.defaultZone = 'utc' luxon's default zone\n *\n * Measured on this repo: 97 modules name the root setup, 7 name a setup of their own instead. For\n * those 7 the pin was never applied, and applying it in the migration changes what the suite does\n * while claiming to move it.\n *\n * `libs/front/logic` is the one that said so out loud. It has its own setup, uses luxon, and its\n * test is literally called \"formats them in the local zone\":\n *\n * expect(formatTimeOfDay(new Date(2024, 2, 1, 8, 5))).toBe('08:05')\n * // pinned to utc: '07:05'\n *\n * One test, and it would have been one silent hour of offset in any suite that did not assert it.\n */\nimport { pinWorkspaceTimezone } from './workspace.js'\n\n/*\n * Awaited at the top level, so the environment is loaded before the first test file is imported.\n * Deferring it to a `beforeAll` would be too late: a module read at import time would already have\n * captured an unset variable.\n */\nawait pinWorkspaceTimezone()\n"],"mappings":";;;;;;AA8BA,MAAM,qBAAqB;","names":[]}
@@ -0,0 +1,32 @@
1
+ import { ViteUserConfig } from 'vitest/config';
2
+
3
+ /** Which family this config is for: it decides the three things that are not shared. */
4
+ type TestFlavour = 'nest' | 'react';
5
+ interface SharedTestOptions {
6
+ /** Which family this config is for. It decides the three things that are not shared. */
7
+ flavour?: TestFlavour;
8
+ /** The module's own directory: where its specs live and what its config is relative to. */
9
+ root: string;
10
+ /** The workspace root, which is where `tsconfig.base.json` and its path aliases are. */
11
+ workspaceRoot: string;
12
+ /**
13
+ * Lower decorators with TypeScript before oxc sees them, for a module that needs it.
14
+ *
15
+ * ⚠️ Written by `--init --test` from the module's own sources, never by hand, because the
16
+ * condition is not a preference: it is whether a decorator sits on an `abstract` class member,
17
+ * the one construct oxc does not reproduce. See `generate-config.ts` for the measurement.
18
+ *
19
+ * Off by default, and that default is the measured one: on
20
+ * `apps/nest/microservices/mission`, 715 of 716 decorated files need nothing, and running the
21
+ * plugin for all of them took the suite from 98s to 249s.
22
+ */
23
+ lowerDecoratorsWithTypeScript?: boolean;
24
+ /** Merged over the base. For what a module genuinely needs to differ on, nothing else. */
25
+ overrides?: ViteUserConfig;
26
+ }
27
+ /** The config, with the module's own overrides merged over it. */
28
+ declare function sharedTestConfig(options: SharedTestOptions & {
29
+ flavour: TestFlavour;
30
+ }): ViteUserConfig;
31
+
32
+ export { type SharedTestOptions, type TestFlavour, sharedTestConfig };
@@ -0,0 +1,8 @@
1
+ import {
2
+ sharedTestConfig
3
+ } from "../../chunk-CRKUEP4J.js";
4
+ import "../../chunk-3TDUIKVQ.js";
5
+ export {
6
+ sharedTestConfig
7
+ };
8
+ //# sourceMappingURL=shared-test-config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1 @@
1
+ export * from 'msw';
@@ -0,0 +1,3 @@
1
+ // src/roles/test/tools/msw.ts
2
+ export * from "msw";
3
+ //# sourceMappingURL=msw.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../src/roles/test/tools/msw.ts"],"sourcesContent":["/**\n * msw, from the copy sentinel governs, with nothing else in the box.\n *\n * ## Why a second entry, beside `test/msw`\n *\n * `@hublo/sentinel/test/msw` hands back the SERVER, and evaluating it starts that server. That is\n * right for the one import the codemod repoints, and wrong for every other: a spec that only wants\n * `rest` would be made to listen. Measured on `apps/nest/microservices/agency`, pointing all five\n * of its msw imports at that entry took a green suite to 444 failures, requests reaching the real\n * network because the interception had moved.\n *\n * So this file is the other half: the msw API, no server, no side effect.\n *\n * ## What it buys\n *\n * A Vite alias already gives the module ONE copy at run time. TypeScript does not read Vite\n * aliases, so it keeps resolving `msw` the way Node would, and a migrated module compiles two\n * copies: the handlers a spec builds with `rest` carry the workspace's declarations while the\n * `server` they are handed to carries sentinel's.\n *\n * TS2345 RestHandler<MockedRequest<DefaultBodyType>> is not assignable to\n * RequestHandler<RequestHandlerDefaultInfo, MockedRequest<DefaultBodyType>, ...>\n *\n * 19 of those on `agency`, a module whose typecheck was clean before it migrated and whose 535\n * tests are green. The version is the same on both sides, 1.3.3; what differs is the pnpm peer\n * set, so they are two physical copies with two sets of declarations.\n *\n * An import that BOTH tools follow closes that, where an alias only one of them reads cannot.\n *\n * ## Why `tools/`, and not a file named after one package\n *\n * Héla, 2026-09-24: a place for the cases that have this shape, not a special case for msw. The\n * shape is \"a package sentinel SHIPS whose values cross between its code and the module's\". msw is\n * the first; `vitest-mock-extended` is the other one aliased today for the same reason.\n */\nexport * from 'msw'\n"],"mappings":";AAmCA,cAAc;","names":[]}
@@ -0,0 +1,36 @@
1
+ import { Plugin } from 'vite';
2
+
3
+ /** A TypeScript transformer a service runs at build time, as its build target declares it. */
4
+ interface TransformerDeclaration {
5
+ /** The package to load it from, e.g. `@nestjs/swagger/plugin`. */
6
+ name: string;
7
+ /** Passed to the transformer's own factory, unread here. */
8
+ options?: Record<string, unknown>;
9
+ }
10
+ interface DecoratorMetadataOptions {
11
+ /** The module being built. Its tsconfig is the one whose emit must be preserved. */
12
+ root: string;
13
+ /**
14
+ * An explicit tsconfig, when the service does not use either conventional name.
15
+ *
16
+ * Relative paths resolve against `root`.
17
+ */
18
+ tsconfig?: string;
19
+ /** The transformers this service declared, translated from its webpack target. */
20
+ transformers?: readonly TransformerDeclaration[];
21
+ }
22
+ declare const decoratorMetadata: (options: DecoratorMetadataOptions) => Plugin;
23
+
24
+ interface Alias {
25
+ find: RegExp;
26
+ replacement: string;
27
+ }
28
+ /**
29
+ * Read the mappings from the workspace's base tsconfig.
30
+ *
31
+ * Longest pattern first, because Vite takes the first alias that matches and the mappings
32
+ * overlap by design: `@front/theme/node` must not be swallowed by `@front/theme`.
33
+ */
34
+ declare const tsconfigAliases: (workspaceRoot: string, file?: string) => Alias[];
35
+
36
+ export { type Alias as A, type DecoratorMetadataOptions as D, decoratorMetadata as d, tsconfigAliases as t };
package/docs/.gitkeep ADDED
File without changes