@hublo/sentinel 1.4.0-alpha.4 → 1.4.0-alpha.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/sentinel.js +0 -1
- package/dist/chunk-2XLX6PFR.js.map +1 -0
- package/dist/chunk-3TDUIKVQ.js.map +1 -0
- package/dist/{chunk-MT7VQXRA.js → chunk-7PUVK4YM.js} +64 -3
- package/dist/chunk-7PUVK4YM.js.map +1 -0
- package/dist/chunk-ASBZQAXV.js +0 -1
- package/dist/chunk-CPCUPK4J.js.map +1 -0
- package/dist/chunk-PWV3BMDA.js.map +1 -0
- package/dist/chunk-WLFE5RUU.js.map +1 -0
- package/dist/index.js +0 -1
- package/dist/roles/test/nest/toolchain.js +1 -1
- package/dist/roles/test/react/toolchain.js +1 -1
- package/dist/roles/test/shared-test-config.js +1 -1
- package/package.json +2 -4
package/dist/bin/sentinel.js
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/roles/test/setup/mock-extended.ts"],"sourcesContent":["/**\n * `jest-mock-extended`'s deep mocks, made answerable to the question Vitest's `spyOn` asks.\n *\n * A deep mock is a Proxy that invents a property the first time something READS it. Nothing exists\n * until then, so the object reports itself empty:\n *\n * const provider = mockDeep<InstitutionProvider>()\n * 'getAdminFirstAndLastNames' in provider // false\n * Object.getOwnPropertyDescriptor(provider, 'getAdmin...') // undefined\n * typeof provider.getAdminFirstAndLastNames // 'function', and now it exists\n *\n * jest's `spyOn` reads the property, so the Proxy created it and the spy worked. Vitest's asks the\n * object whether it HAS the property first, gets no for both questions, and throws\n * `The property \"getAdminFirstAndLastNames\" is not defined on the object.` The file then reports no\n * test at all, so its names simply go missing rather than failing.\n *\n * Measured on `apps/cloud/shift`: one such spy took 37 of its 108 tests away. The repo has 128 of\n * them across 29 files, mostly in `apps/nest/microservices` and\n * `apps/nest/backends-for-frontends`.\n *\n * ## Why it is fixed here and not in the 128 test files\n *\n * Because it is a difference between two runners, not something 29 suites each got wrong. A\n * codemod rewriting every site would put a migration artefact in front of every reader of those\n * files forever, and teams would carry it. One adapter keeps the test files exactly as they are.\n *\n * The two traps answer by doing what jest's `spyOn` did: read the property once, then answer. The\n * read is the Proxy's own documented way of materialising it, so nothing here reimplements the\n * mock, it only asks the question in the form the underlying object understands.\n *\n * `then` is never materialised. A deep mock that suddenly HAS a `then` is a thenable, and awaiting\n * it, or returning it from an async function, would hang on a promise nothing resolves.\n */\nimport * as mockExtended from 'vitest-mock-extended'\n\n/**\n * Keys that must stay absent however they are asked for.\n *\n * Not an arbitrary list: every one of them is a PROTOCOL PROBE, a property a framework reads to\n * decide what KIND of value it is holding. A deep mock invents whatever it is asked for, so it\n * answers yes to every probe, and each yes is a lie about what it is.\n *\n * then a thenable. Awaiting the mock, or returning it from an async function,\n * hangs on a promise nothing resolves.\n * asymmetricMatch an asymmetric matcher. Measured: `expect(p).rejects.toThrow(deepMock)`\n * answers `expected error to match asymmetric matcher` instead of comparing\n * against the class, so the assertion stops testing what it says.\n * $$typeof a React element. Serializers branch on it and read fields that are not\n * there.\n * nodeType a DOM node, which testing-library and serializers both branch on.\n *\n * Symbols never reach here: the materialiser returns early for a non-string key, so `Symbol.\n * iterator` and friends are already safe.\n */\nconst NEVER_MATERIALISED = new Set(['then', 'asymmetricMatch', '$$typeof', 'nodeType'])\n\nfunction materialise(target: object, key: PropertyKey): void {\n if (typeof key !== 'string' || NEVER_MATERIALISED.has(key)) return\n // The read IS the creation: that is how the deep mock's own Proxy works.\n void (target as Record<string, unknown>)[key]\n}\n\n/**\n * Wrap a deep mock so that `in` and `getOwnPropertyDescriptor` see what a read would create.\n *\n * Only the object handed back is wrapped, not what it returns: a nested spy would need its own\n * wrapper, and the corpus has none. Wrapping every read would also mean a new wrapper per access\n * unless they were cached, and an identity that changes between two reads is a worse bug than the\n * one being fixed.\n */\n/**\n * One proxy per underlying mock, so identity is stable.\n *\n * A suite holds on to `db.timeSlot` and compares it later, or passes it to `toHaveBeenCalledWith`.\n * Handing back a fresh proxy on every read would make the same mock unequal to itself.\n */\nconst wrappers = new WeakMap<object, object>()\n\nfunction answeringProxy<T extends object>(mock: T): T {\n return new Proxy(mock, {\n /*\n * A NESTED mock has to answer the same questions as the top-level one.\n *\n * The adapter used to wrap only what `mockDeep()` returned, and the level below it was a raw\n * mock again. Measured: `'deleteMany' in db.timeSlot` answered false and its descriptor was\n * absent, so `vi.spyOn(db.timeSlot, 'deleteMany')` failed with\n * `The property \"deleteMany\" is not defined on the function` — the exact error\n * `apps/nest/microservices/hublo-pool` reports, and the exact failure this adapter was written\n * to remove, one level down.\n *\n * Read from the TARGET, never with this proxy as the receiver: forwarding the receiver changes\n * what the deep mock hands back.\n */\n get(target, key) {\n if (typeof key === 'string' && NEVER_MATERIALISED.has(key)) return undefined\n\n const value = Reflect.get(target, key) as unknown\n if (value === null) return value\n if (typeof value !== 'object' && typeof value !== 'function') return value\n\n /*\n * A Proxy invariant, not a preference: for a property that is non-configurable AND\n * non-writable, `get` MUST hand back the target's own value. Wrapping one throws\n * `'get' on proxy: property 'mock' is a read-only and non-configurable data property on the\n * proxy target but the proxy did not return its actual value`, which is what a mock's own\n * `mock` record is.\n */\n const descriptor = Reflect.getOwnPropertyDescriptor(target, key)\n if (\n descriptor !== undefined &&\n descriptor.configurable === false &&\n descriptor.writable === false\n ) {\n return value\n }\n\n const existing = wrappers.get(value as object)\n if (existing !== undefined) return existing\n\n const wrapper = answeringProxy(value as object)\n wrappers.set(value as object, wrapper)\n return wrapper\n },\n has(target, key) {\n if (Reflect.has(target, key)) return true\n materialise(target, key)\n return Reflect.has(target, key)\n },\n getOwnPropertyDescriptor(target, key) {\n const existing = Reflect.getOwnPropertyDescriptor(target, key)\n if (existing !== undefined) return existing\n materialise(target, key)\n return Reflect.getOwnPropertyDescriptor(target, key)\n },\n })\n}\n\n/*\n * Everything the fork exports, so a module importing `jest-mock-extended` still gets `mockReset`,\n * `mockClear`, `anyString`, the matchers and the types. The two names below are declared after it:\n * an explicit export wins over a star export, which is what lets this file override exactly two\n * functions and pass the rest through untouched.\n */\nexport * from 'vitest-mock-extended'\n\n/*\n * And the same names again, one by one.\n *\n * `export *` from a CJS package forwards only what a static read of that package can SEE.\n * `vitest-mock-extended` ships CommonJS and assembles part of its exports at runtime, so the star\n * carries the types and loses several of the functions. A spec importing one of them then reads\n * `undefined` and dies at the call, naming the binding and nothing else. Measured on\n * `apps/nest/microservices/mission`:\n *\n * TypeError: (0 , __vite_ssr_import_3__.mockReset) is not a function\n *\n * It is the same shape as the automock namespace built from a static export list, which\n * `server.deps.inline` exists to answer; here the file doing the re-export is ours, so it can\n * simply say the names out loud. A name that ever disappears from the fork breaks the build\n * instead of a suite, which is the better end of that trade.\n */\nexport {\n any,\n anyArray,\n anyBoolean,\n anyFunction,\n anyMap,\n anyNumber,\n anyObject,\n anySet,\n anyString,\n anySymbol,\n arrayIncludes,\n calledWithFn,\n captor,\n isA,\n isMockObject,\n mapHas,\n matches,\n mockClear,\n mocked,\n mockedFn,\n mockFn,\n mockReset,\n notEmpty,\n notNull,\n notUndefined,\n objectContainsKey,\n} from 'vitest-mock-extended'\n\n/**\n * Every deep mock handed out, so `vi.resetAllMocks()` can reach them.\n *\n * ⚠️ Kept on `globalThis`, and that is not laziness. This file is reached by TWO specifiers: the\n * setup imports it by its path, and a spec imports `jest-mock-extended`, which the preset aliases\n * here. Vite resolves those to two ids and instantiates the module twice, so a registry held in\n * module scope would be filled by one instance and read by the other, and the reset would find\n * nothing. It is the same dual-identity trap the axios alias exists to close, one layer up.\n *\n * ⚠️ It does not reach them on its own, and that is a silent divergence. jest's\n * `jest.resetAllMocks()` reset these because `jest-mock-extended` built them with `jest.fn()`, so\n * they sat in jest's own registry. `vitest-mock-extended` builds them its own way, so\n * `vi.resetAllMocks()` walks past them and their call history survives into the next test.\n *\n * Measured on `apps/nest/microservices/activity`, whose suite does exactly 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 test before them.\n * Each one PASSES on its own and fails right after its neighbour, which is the signature of\n * leakage rather than of a broken assertion.\n *\n * A plain Set: a test file gets its own module instance, so the registry lives and dies with it.\n */\nconst REGISTRY = Symbol.for('@hublo/sentinel/deep-mocks')\nconst handedOut: Set<object> = ((globalThis as Record<symbol, unknown>)[REGISTRY] ??=\n new Set<object>()) as Set<object>\n\n/** Reset every deep mock this adapter created, the way jest's registry did. */\nexport function resetDeepMocks(): void {\n for (const created of handedOut) mockExtended.mockReset(created as never)\n}\n\n/** Clear their calls without touching their implementations. */\nexport function clearDeepMocks(): void {\n for (const created of handedOut) mockExtended.mockClear(created as never)\n}\n\nexport const mock = ((...args: unknown[]) => {\n const created = (mockExtended.mock as (...rest: unknown[]) => object)(...args)\n handedOut.add(created)\n return answeringProxy(created)\n}) as unknown as typeof mockExtended.mock\n\nexport const mockDeep = ((...args: unknown[]) => {\n const created = (mockExtended.mockDeep as (...rest: unknown[]) => object)(...args)\n handedOut.add(created)\n return answeringProxy(created)\n}) as unknown as typeof mockExtended.mockDeep\n"],"mappings":";AAiCA,YAAY,kBAAkB;AA8G9B,cAAc;AAkBd;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,aAAAA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,aAAAC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAtIP,IAAM,qBAAqB,oBAAI,IAAI,CAAC,QAAQ,mBAAmB,YAAY,UAAU,CAAC;AAEtF,SAAS,YAAY,QAAgB,KAAwB;AAC3D,MAAI,OAAO,QAAQ,YAAY,mBAAmB,IAAI,GAAG,EAAG;AAE5D,OAAM,OAAmC,GAAG;AAC9C;AAgBA,IAAM,WAAW,oBAAI,QAAwB;AAE7C,SAAS,eAAiCC,OAAY;AACpD,SAAO,IAAI,MAAMA,OAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAcrB,IAAI,QAAQ,KAAK;AACf,UAAI,OAAO,QAAQ,YAAY,mBAAmB,IAAI,GAAG,EAAG,QAAO;AAEnE,YAAM,QAAQ,QAAQ,IAAI,QAAQ,GAAG;AACrC,UAAI,UAAU,KAAM,QAAO;AAC3B,UAAI,OAAO,UAAU,YAAY,OAAO,UAAU,WAAY,QAAO;AASrE,YAAM,aAAa,QAAQ,yBAAyB,QAAQ,GAAG;AAC/D,UACE,eAAe,UACf,WAAW,iBAAiB,SAC5B,WAAW,aAAa,OACxB;AACA,eAAO;AAAA,MACT;AAEA,YAAM,WAAW,SAAS,IAAI,KAAe;AAC7C,UAAI,aAAa,OAAW,QAAO;AAEnC,YAAM,UAAU,eAAe,KAAe;AAC9C,eAAS,IAAI,OAAiB,OAAO;AACrC,aAAO;AAAA,IACT;AAAA,IACA,IAAI,QAAQ,KAAK;AACf,UAAI,QAAQ,IAAI,QAAQ,GAAG,EAAG,QAAO;AACrC,kBAAY,QAAQ,GAAG;AACvB,aAAO,QAAQ,IAAI,QAAQ,GAAG;AAAA,IAChC;AAAA,IACA,yBAAyB,QAAQ,KAAK;AACpC,YAAM,WAAW,QAAQ,yBAAyB,QAAQ,GAAG;AAC7D,UAAI,aAAa,OAAW,QAAO;AACnC,kBAAY,QAAQ,GAAG;AACvB,aAAO,QAAQ,yBAAyB,QAAQ,GAAG;AAAA,IACrD;AAAA,EACF,CAAC;AACH;AAgFA,IAAM,WAAW,uBAAO,IAAI,4BAA4B;AACxD,IAAM,YAA2B,WAAuC,QAAQ,MAC9E,oBAAI,IAAY;AAGX,SAAS,iBAAuB;AACrC,aAAW,WAAW,UAAW,CAAa,uBAAU,OAAgB;AAC1E;AAGO,SAAS,iBAAuB;AACrC,aAAW,WAAW,UAAW,CAAa,uBAAU,OAAgB;AAC1E;AAEO,IAAMA,SAAQ,IAAI,SAAoB;AAC3C,QAAM,UAAwB,kBAAwC,GAAG,IAAI;AAC7E,YAAU,IAAI,OAAO;AACrB,SAAO,eAAe,OAAO;AAC/B;AAEO,IAAMC,aAAY,IAAI,SAAoB;AAC/C,QAAM,UAAwB,sBAA4C,GAAG,IAAI;AACjF,YAAU,IAAI,OAAO;AACrB,SAAO,eAAe,OAAO;AAC/B;","names":["mockClear","mockReset","mock","mockDeep"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/roles/build/nest/decorator-metadata.ts","../src/roles/build/nest/tsconfig-aliases.ts"],"sourcesContent":["/**\n * Decorator metadata, which esbuild cannot emit and Nest cannot live without, plus the TypeScript\n * transformers a service declared on its webpack target.\n *\n * Vite's default transformer is esbuild, and esbuild does not implement `emitDecoratorMetadata`.\n * Nest resolves a constructor's parameters from exactly that metadata, so without it the DI\n * container comes up empty and the application fails on its first module. Measured in the\n * monorepo this ships to: `emitDecoratorMetadata: true` in `tsconfig.base.json`, and 7365 files\n * carrying `@Injectable`, `@Module` or `@Controller`.\n *\n * ## TWO modes, chosen exactly as nx chooses them\n *\n * This is the one decision in this file that is not ours, and trying to make it ours cost a day.\n *\n * `@nx/webpack` configures ts-loader with one line:\n *\n * transpileOnly: !hasPlugin // hasPlugin = this target declares `transformers`\n *\n * So webpack already had two behaviours. A service with no transformers compiled with\n * `transpileOnly`, which is `ts.transpileModule` semantics: no type checker, one file at a time.\n * A service WITH transformers compiled through a full `ts.Program`, because that is the only way\n * a TypeScript transformer can run at all.\n *\n * The difference is visible, and measured. On a property typed by an imported string enum:\n *\n * Program : __metadata(\"design:type\", String)\n * transpileModule : __metadata(\"design:type\", typeof (_a = ...) === \"function\" ? _a : Object)\n *\n * and on a `@Body()` whose DTO arrives through `import type`, a Program emits `Function` where\n * transpileOnly emits `Object`, which `@nestjs/swagger` turns into four `$ref: Function` request\n * bodies that the published contract does not have.\n *\n * This plugin used only the first mode and this file claimed it was \"the serializer ts-loader was\n * already using\". Half true, and the wrong half: it is what ts-loader uses for the 29 services\n * with no transformers, and those came back byte-identical. Then a Program was used for ALL of\n * them, which fixed the eight and moved the contract of `network`, which had been correct.\n *\n * Both were the same mistake: choosing one mode for everyone, when the tool being replaced chose\n * per service. So the condition is copied rather than invented, and there is no third case to\n * discover: nx has two, and both are reproduced here.\n *\n * The Program costs about six seconds more on the largest service (1419 files: ~1.9s to build it,\n * ~3.3ms per file against ~0.9ms). Only the services that declare a transformer pay it, which is\n * also what webpack did.\n *\n * ## The transformers, which are configuration and are read, never invented\n *\n * Eight services and BFFs declare `@nestjs/swagger/plugin` on their webpack target, with\n * `introspectComments` and `classValidatorShim`. It writes the `@ApiProperty` decorators a\n * developer would otherwise write by hand, turns JSDoc into descriptions, and projects\n * class-validator rules into the schema.\n *\n * Dropping it does not fail a build. Measured on `institution` by regenerating its committed\n * contract after a sentinel build: 385 insertions and 1378 deletions. Response schemas and every\n * `minItems`, `minimum`, `maximum`, `minLength` and `format` disappeared, silently.\n *\n * So `--init` reads the declaration off the webpack target and writes it into the generated Vite\n * config, and this plugin loads it. Nothing here decides which transformers a service runs.\n *\n * ## Why the MODULE's compiler now, and not sentinel's own\n *\n * This plugin used to pin sentinel's TypeScript, on the grounds that `typescript` in the consumer\n * could resolve to the 7.x native port, which exports a version and no compiler API. Measured\n * from a service today it resolves to `@typescript/typescript6` 6.0.2, with the full API.\n *\n * The pin cannot survive the transformers anyway: a plugin loaded from the module's\n * `node_modules` imports the module's TypeScript, and a transformer built against one compiler\n * cannot be handed a Program built by another. One compiler, the module's, is also what ts-loader\n * used, so it is the faithful answer rather than merely the workable one. The API is asserted at\n * `buildStart`, where a failure is reported.\n *\n * ## Why the module's own tsconfig, and not options chosen here\n *\n * Emit follows compiler options, so options invented here would be a second, silent source of\n * divergence from what ts-loader produced. `target` alone decides how classes and fields are\n * downlevelled: this workspace is on `es2021`, where `useDefineForClassFields` defaults to false\n * and a declared-but-unassigned DTO field is therefore NOT emitted. Defaulting to `es2022` here\n * would flip that, `Object.defineProperty` would write `undefined` over every field, and\n * `class-transformer` payloads would arrive empty at runtime with nothing in the build to explain\n * it.\n *\n * So the config is read, `extends` resolved by the compiler itself, and only what this plugin\n * genuinely owns is overridden.\n *\n * ## Still no type CHECKING, which is a different thing from having types\n *\n * The Program gives this plugin a type checker; it does not use it to report errors. Every Nest\n * project already runs `tsc -b` in its own `typecheck` target, and the build never was the thing\n * that type-checked. Emitting with type information and refusing to type-check are independent,\n * and only the first is needed to reproduce what ts-loader wrote.\n */\nimport { existsSync } from 'node:fs'\nimport { createRequire } from 'node:module'\nimport path from 'node:path'\n\nimport type TS from 'typescript'\nimport type { Plugin } from 'vite'\n\n/** Files this transform owns: TypeScript sources, never a dependency's compiled output. */\nconst TYPESCRIPT_SOURCE = /\\.ts$/\n\n/**\n * Where a Nest service keeps the config it builds with, most specific first.\n *\n * `tsconfig.app.json` is the one Nx generates for the application's own sources and the one\n * ts-loader was pointed at; `tsconfig.json` is the fallback for a service that never split them.\n * Measured on `main`: all 37 services still on webpack name `tsconfig.app.json`.\n */\nconst TSCONFIG_CANDIDATES = ['tsconfig.app.json', 'tsconfig.json']\n\n/** A TypeScript transformer a service runs at build time, as its build target declares it. */\nexport interface TransformerDeclaration {\n /** The package to load it from, e.g. `@nestjs/swagger/plugin`. */\n name: string\n /** Passed to the transformer's own factory, unread here. */\n options?: Record<string, unknown>\n}\n\nexport interface DecoratorMetadataOptions {\n /** The module being built. Its tsconfig is the one whose emit must be preserved. */\n root: string\n /**\n * An explicit tsconfig, when the service does not use either conventional name.\n *\n * Relative paths resolve against `root`.\n */\n tsconfig?: string\n /** The transformers this service declared, translated from its webpack target. */\n transformers?: readonly TransformerDeclaration[]\n}\n\n/**\n * The module's own TypeScript, with its emit API asserted.\n *\n * Resolved from `root` rather than from here, because a transformer loaded from the module binds\n * to the compiler the module resolves, and Programs are not interchangeable between compilers.\n */\nfunction loadCompiler(root: string): typeof TS {\n const require = createRequire(path.join(root, 'noop.js'))\n let compiler: typeof TS\n try {\n compiler = require('typescript') as typeof TS\n } catch {\n throw new Error(\n `sentinel build(nest): no \\`typescript\\` resolvable from ${root}. The build emits decorator ` +\n `metadata with the module's own compiler, so one has to be installed there.`,\n )\n }\n if (typeof compiler.createProgram !== 'function') {\n throw new Error(\n `sentinel build(nest): the typescript resolved from ${root} (${compiler.version ?? 'unknown'}) ` +\n `has no createProgram API. TypeScript 7 is the native port and exposes none; the emit API ` +\n `lives under @typescript/typescript6. Point this module's \\`typescript\\` at a compiler ` +\n `with an emit API, which is the one its \\`typecheck\\` target already uses.`,\n )\n }\n return compiler\n}\n\nfunction resolveTsconfig(options: DecoratorMetadataOptions): string {\n if (options.tsconfig) {\n const explicit = path.resolve(options.root, options.tsconfig)\n if (!existsSync(explicit)) {\n throw new Error(`sentinel build(nest): tsconfig not found at ${explicit}`)\n }\n return explicit\n }\n for (const candidate of TSCONFIG_CANDIDATES) {\n const found = path.join(options.root, candidate)\n if (existsSync(found)) return found\n }\n throw new Error(\n `sentinel build(nest): no ${TSCONFIG_CANDIDATES.join(' or ')} in ${options.root}. ` +\n `Pass \\`tsconfig\\` if this service keeps it elsewhere.`,\n )\n}\n\n/** The module's compiler options, with `extends` already resolved, plus what this plugin owns. */\nfunction readConfig(\n ts: typeof TS,\n options: DecoratorMetadataOptions,\n): { fileNames: string[]; compilerOptions: TS.CompilerOptions } {\n const configPath = resolveTsconfig(options)\n const parsed = ts.getParsedCommandLineOfConfigFile(configPath, {}, {\n ...ts.sys,\n onUnRecoverableConfigFileDiagnostic: (diagnostic) => {\n throw new Error(\n `sentinel build(nest): could not read ${configPath}: ` +\n ts.flattenDiagnosticMessageText(diagnostic.messageText, ' '),\n )\n },\n } as TS.ParseConfigFileHost)\n\n return {\n fileNames: parsed?.fileNames ?? [],\n compilerOptions: {\n ...parsed?.options,\n // ESM out, so Rollup sees imports and exports rather than an opaque `require` it cannot\n // follow. The service still SHIPS as CJS: that conversion is the bundler's, further down.\n module: ts.ModuleKind.ESNext,\n // Vite consumes the map; the tsconfig's own answer is about a different pipeline.\n sourceMap: true,\n inlineSourceMap: false,\n inlineSources: false,\n // Types are another target's job, and emitting them here would write into the source tree.\n declaration: false,\n declarationMap: false,\n emitDeclarationOnly: false,\n noEmit: false,\n // `composite` projects refuse to emit without a `tsBuildInfoFile`, and there is no\n // incremental build here to inform.\n composite: false,\n incremental: false,\n },\n }\n}\n\n/**\n * The declared transformers, loaded from the MODULE so each binds to the compiler it expects.\n *\n * A name that cannot be loaded, or a package that exports no `before`, fails the build here\n * rather than producing a bundle quietly missing what the transformer contributes. That failure\n * mode is the reason this exists: dropping `@nestjs/swagger/plugin` costs a service most of its\n * published contract and nothing in the build says so.\n */\nfunction loadTransformers(\n root: string,\n program: TS.Program,\n declared: readonly TransformerDeclaration[],\n): TS.TransformerFactory<TS.SourceFile>[] {\n const require = createRequire(path.join(root, 'noop.js'))\n return declared.map(({ name, options }) => {\n let loaded: { before?: unknown }\n try {\n loaded = require(name) as { before?: unknown }\n } catch (error) {\n throw new Error(\n `sentinel build(nest): the build target declares the transformer \\`${name}\\`, which ` +\n `cannot be loaded from ${root}: ${error instanceof Error ? error.message : String(error)}`,\n { cause: error },\n )\n }\n if (typeof loaded.before !== 'function') {\n throw new Error(\n `sentinel build(nest): \\`${name}\\` exports no \\`before\\` factory, so it cannot run as a ` +\n `TypeScript transformer. Building without it would drop whatever it contributes.`,\n )\n }\n const factory = loaded.before as (\n options: Record<string, unknown> | undefined,\n program: TS.Program,\n ) => TS.TransformerFactory<TS.SourceFile>\n return factory(options, program)\n })\n}\n\nexport const decoratorMetadata = (options: DecoratorMetadataOptions): Plugin => {\n /*\n * Everything is prepared in `buildStart`, and that placement is the whole point.\n *\n * It used to be resolved lazily inside `transform`, on the reasoning that an error belongs to\n * the build rather than to importing the preset. Half right: it belongs to the build, but\n * Rollup SWALLOWS a throwing `transform` hook. The wrong compiler, an unreadable tsconfig and a\n * missing one all produced the same symptom instead of their own message: \"1 modules\n * transformed\", nothing written, then an unrelated error from whichever later hook tripped over\n * the empty output directory. It cost two diagnoses to recognise.\n *\n * `buildStart` is reported. So the compiler is loaded, the config read, the Program built and\n * the transformers resolved exactly once, before a single file is transformed, where failing\n * says what failed.\n */\n /** `hasPlugin` in nx's own words: this service declares transformers, so it needs a Program. */\n const typeAware = (options.transformers ?? []).length > 0\n\n let ts: typeof TS | undefined\n let program: TS.Program | undefined\n let compilerOptions: TS.CompilerOptions | undefined\n let before: TS.TransformerFactory<TS.SourceFile>[] = []\n /** Files emitted without the Program, which is a fidelity gap and is reported, never silent. */\n const withoutTypes: string[] = []\n\n return {\n name: 'sentinel:decorator-metadata',\n // Before Vite's own transform, so esbuild never sees the decorators it cannot handle, and so\n // the source this plugin reads from the Program is still the source on disk.\n enforce: 'pre',\n buildStart() {\n ts = loadCompiler(options.root)\n const config = readConfig(ts, options)\n compilerOptions = config.compilerOptions\n // Built only in the type-aware mode. It costs ~1.9s on the largest service here and buys\n // nothing a service without transformers can use, which is why nx does not build one either.\n if (!typeAware) return\n program = ts.createProgram(config.fileNames, config.compilerOptions)\n before = loadTransformers(options.root, program, options.transformers ?? [])\n },\n transform(code, id) {\n if (!TYPESCRIPT_SOURCE.test(id) || id.includes('node_modules')) return null\n // Only reachable if `buildStart` did not run, which no Rollup build does. Named rather than\n // defaulted, so it cannot silently emit with TypeScript's defaults instead of the module's.\n if (ts === undefined || compilerOptions === undefined) {\n throw new Error(\n `sentinel build(nest): the compiler was never prepared, so ${id} would be emitted with ` +\n `defaults rather than this module's tsconfig.`,\n )\n }\n\n /*\n * The Program's own SourceFile, which is what carries the type information.\n *\n * Three cases emit without it. The service declares no transformer, which is `transpileOnly`\n * and is what ts-loader did for it. A file the Program never reached, because nothing\n * imports it from the entry. And a file another plugin has already rewritten, where the\n * Program's copy is no longer the truth: emitting that stale copy over someone else's\n * transform would be the worse failure.\n *\n * The last two are counted and reported at the end. The first is not a gap, it is the mode.\n *\n * Measured on the largest service here: neither of the last two fires, on any of its 5594\n * files. That is the point of reporting them rather than tolerating them quietly — a warning\n * from this build means something genuinely new, not a known rough edge.\n */\n const sourceFile = program?.getSourceFile(id)\n if (program === undefined || sourceFile === undefined || sourceFile.text !== code) {\n if (typeAware) withoutTypes.push(id)\n const output = ts.transpileModule(code, { fileName: id, compilerOptions })\n return { code: output.outputText, map: output.sourceMapText ?? null }\n }\n\n let emitted: string | undefined\n let map: string | undefined\n program.emit(\n sourceFile,\n (fileName, text) => {\n if (fileName.endsWith('.map')) map = text\n else emitted = text\n },\n undefined,\n false,\n { before },\n )\n if (emitted === undefined) {\n throw new Error(`sentinel build(nest): TypeScript emitted nothing for ${id}.`)\n }\n return { code: emitted, map: map ?? null }\n },\n closeBundle() {\n if (withoutTypes.length === 0) return\n const shown = withoutTypes.slice(0, 5).map((file) => path.relative(options.root, file))\n this.warn(\n `sentinel build(nest): ${withoutTypes.length} file(s) were emitted without type ` +\n `information, so their decorator metadata may differ from what ts-loader produced ` +\n `(${shown.join(', ')}${withoutTypes.length > shown.length ? ', …' : ''}). They are ` +\n `outside this module's tsconfig, or another plugin rewrote them first.`,\n )\n },\n }\n}\n","/**\n * The workspace's `paths` mappings, as Vite resolve aliases.\n *\n * They matter more than they look. The mappings point at SOURCE files, and that is what makes\n * a workspace library get BUNDLED rather than externalised: Vite treats anything outside\n * `node_modules` as source. Drop them and every `@hublo/nest/*` import becomes an external the\n * runtime cannot resolve, because those packages are never published.\n *\n * This reproduces what webpack did through `tsconfig-paths-webpack-plugin`, alongside\n * `webpack-node-externals` which only ever externalised real packages. Measured on the first\n * migrated service: 45 externals under Vite against 47 under webpack, and nothing externalised\n * by Vite that webpack did not.\n */\nimport { readFileSync } from 'node:fs'\nimport { join } from 'node:path'\n\nexport interface Alias {\n find: RegExp\n replacement: string\n}\n\n/** `tsconfig.base.json` carries `//` comments, which `JSON.parse` refuses. */\nconst stripLineComments = (json: string): string => json.replace(/^\\s*\\/\\/.*$/gm, '')\n\nconst toAlias = (workspaceRoot: string, pattern: string, target: string): Alias => {\n const escaped = pattern.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&').replace(/\\\\\\*/g, '(.*)')\n return {\n find: new RegExp(`^${escaped}$`),\n replacement: join(workspaceRoot, target.replace(/\\*/g, '$1')),\n }\n}\n\n/**\n * Read the mappings from the workspace's base tsconfig.\n *\n * Longest pattern first, because Vite takes the first alias that matches and the mappings\n * overlap by design: `@front/theme/node` must not be swallowed by `@front/theme`.\n */\nexport const tsconfigAliases = (workspaceRoot: string, file = 'tsconfig.base.json'): Alias[] => {\n const raw = readFileSync(join(workspaceRoot, file), 'utf8')\n const { compilerOptions } = JSON.parse(stripLineComments(raw)) as {\n compilerOptions?: { paths?: Record<string, string[]> }\n }\n return Object.entries(compilerOptions?.paths ?? {})\n .flatMap(([pattern, targets]) => {\n const [target] = targets\n return target === undefined ? [] : [toAlias(workspaceRoot, pattern, target)]\n })\n .sort((a, b) => b.find.source.length - a.find.source.length)\n}\n"],"mappings":"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","names":["require"]}
|
|
@@ -281,7 +281,36 @@ function baseConfig(options) {
|
|
|
281
281
|
* A module that declared its own concurrency overrides this, in both environments, exactly as
|
|
282
282
|
* it does today.
|
|
283
283
|
*/
|
|
284
|
-
fileParallelism: !process.env.CI
|
|
284
|
+
fileParallelism: !process.env.CI,
|
|
285
|
+
/*
|
|
286
|
+
* ⚠️ Sentinel's own setup files go through VITE, not round it, and that is load-bearing.
|
|
287
|
+
*
|
|
288
|
+
* Vitest externalizes anything under `node_modules`, so an installed `@hublo/sentinel` is
|
|
289
|
+
* loaded by Node while the module's own code is served by Vite. The two then hold DIFFERENT
|
|
290
|
+
* instances of the same package, and a setup whose whole job is to mutate per-copy state
|
|
291
|
+
* mutates a copy nobody reads.
|
|
292
|
+
*
|
|
293
|
+
* Measured on `apps/nest/microservices/agency`, with the published 1.4.0-alpha.4:
|
|
294
|
+
*
|
|
295
|
+
* zone of the copy sentinel pinned UTC
|
|
296
|
+
* zone of the copy the test imports Europe/Paris
|
|
297
|
+
* same object false
|
|
298
|
+
*
|
|
299
|
+
* Six tests that passed under jest failed, all comparing `DateTime.now().toISO()` against a
|
|
300
|
+
* date the provider had serialised: identical instants, one rendered `+02:00` and the other
|
|
301
|
+
* `Z`. The root jest setup pinned `Settings.defaultZone = 'utc'` and 97 modules named it, so
|
|
302
|
+
* this is not one module's edge case.
|
|
303
|
+
*
|
|
304
|
+
* ⚠️ It hid for a whole campaign because the harness SYMLINKS sentinel to its source tree.
|
|
305
|
+
* The resolved path then holds no `node_modules`, Vitest inlines it, and the pin lands on the
|
|
306
|
+
* right copy: 96 modules reported IDENTIQUE while the real install was red. An instrument
|
|
307
|
+
* that installs the package differently from a team does not measure what a team will see.
|
|
308
|
+
*
|
|
309
|
+
* The narrow form `/@hublo\/sentinel\/dist\/roles\//` was tried and does NOT work: the pin
|
|
310
|
+
* itself lives in a shared chunk at the dist root, which stays external. It has to be the
|
|
311
|
+
* whole package.
|
|
312
|
+
*/
|
|
313
|
+
server: { deps: { inline: [/@hublo\/sentinel/] } }
|
|
285
314
|
}
|
|
286
315
|
};
|
|
287
316
|
}
|
|
@@ -325,12 +354,44 @@ function sharedTestConfig(options) {
|
|
|
325
354
|
...asAliasArray(options.overrides.resolve?.alias)
|
|
326
355
|
]
|
|
327
356
|
},
|
|
328
|
-
test: {
|
|
357
|
+
test: {
|
|
358
|
+
...base.test,
|
|
359
|
+
...options.overrides.test,
|
|
360
|
+
/*
|
|
361
|
+
* ⚠️ `server.deps.inline` is the one key inside `test` that must not be REPLACED.
|
|
362
|
+
*
|
|
363
|
+
* The shallow spread above is right for everything else: a module overriding `environment`
|
|
364
|
+
* or `testTimeout` means to replace it. But the base uses this list to put sentinel's own
|
|
365
|
+
* setup inside Vite's graph, and a module declares its own entries there for its workspace
|
|
366
|
+
* packages. Dropping either half breaks something silently, and the halves have nothing to
|
|
367
|
+
* do with each other, so they are concatenated.
|
|
368
|
+
*
|
|
369
|
+
* Measured on `libs/front/api`, which declares `['@front/runtime-config', '@segment/snippet']`
|
|
370
|
+
* and would otherwise have discarded the base's entry and lost the timezone pin with it.
|
|
371
|
+
*/
|
|
372
|
+
server: mergeServerDeps(base.test?.server, options.overrides.test?.server)
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
function mergeServerDeps(base, override) {
|
|
377
|
+
if (base === void 0) return override;
|
|
378
|
+
if (override === void 0) return base;
|
|
379
|
+
const inline = [
|
|
380
|
+
...asInlineArray(base.deps?.inline) ?? [],
|
|
381
|
+
...asInlineArray(override.deps?.inline) ?? []
|
|
382
|
+
];
|
|
383
|
+
return {
|
|
384
|
+
...base,
|
|
385
|
+
...override,
|
|
386
|
+
deps: { ...base.deps, ...override.deps, ...inline.length > 0 ? { inline } : {} }
|
|
329
387
|
};
|
|
330
388
|
}
|
|
389
|
+
function asInlineArray(inline) {
|
|
390
|
+
return Array.isArray(inline) ? inline : void 0;
|
|
391
|
+
}
|
|
331
392
|
|
|
332
393
|
export {
|
|
333
394
|
jestExportConditions,
|
|
334
395
|
sharedTestConfig
|
|
335
396
|
};
|
|
336
|
-
//# sourceMappingURL=chunk-
|
|
397
|
+
//# sourceMappingURL=chunk-7PUVK4YM.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/roles/test/shared-test-config.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 { 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 /** 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 */\nfunction mswAliases(): Alias[] {\n const server = siblingFile('./setup/msw-server.js')\n const own = resolveFrom('msw')\n return [\n ...(own === undefined ? [] : [{ find: /^msw$/, replacement: own }]),\n ...(server === undefined ? [] : [{ find: /^@hublo\\/test\\/msw\\/server$/, replacement: server }]),\n ]\n}\n\n/**\n * One PHYSICAL axios, resolved from the MODULE and not from sentinel.\n *\n * pnpm installs axios twice here, same 1.17.0, differing only by a peer. The service client\n * resolves one copy and a provider's `import { AxiosError } from 'axios'` the other, so\n * `if (!(error instanceof AxiosError)) throw error` answers false on a genuine axios error and\n * rethrows it raw. The test then fails on the WRAPPER being absent, naming nothing near the cause.\n *\n * `resolve.dedupe` does NOT fix this, and neither does `deps.inline`: dedupe settles bare\n * specifiers, not two physical paths. The alias has to name the path.\n *\n * The module's OWN `node_modules`, checked directly rather than resolved.\n *\n * `createRequire(...).resolve()` was the first version and it is wrong twice. It walks UP the\n * tree, so a module without axios would be aliased to a PARENT's copy, which is the opposite of\n * pinning its own. And because it consults ambient resolution, the answer depends on where the\n * process runs: it correctly found nothing from a plain CLI run and found something under Vitest,\n * which is how a test of it became a test of its environment.\n *\n * pnpm links a module's direct dependencies into `<module>/node_modules`, so that path is both the\n * right answer and a deterministic one.\n *\n * Measured on `mission`: 378 passing suites to 381 of 387.\n *\n * ## ⚠️ And the workspace root when the module has none of its own\n *\n * The first version stopped there and returned nothing for a module without its own axios, on the\n * reasoning that pinning it to a PARENT's copy is the opposite of pinning its own. That reasoning\n * has an blind spot: a module with no copy of its own does not thereby have no axios PROBLEM. It\n * receives one through its dependencies, and it is still importing it two ways.\n *\n * Measured in `apps/nest/backends-for-frontends/backoffice`, which declares no axios, under this\n * very preset with no alias:\n *\n * the ESM AxiosError === the CJS AxiosError false\n * a CJS error instanceof the CJS class true\n * a CJS error instanceof the ESM class FALSE\n *\n * `@nestjs/axios` is externalised, so Node loads it and it throws the CJS class; the provider does\n * `import { AxiosError } from 'axios'` through Vite and gets the ESM one. Its\n * `if (error instanceof AxiosError) throw new RemoteBackendException(...)` therefore answers false\n * on a genuine axios error and rethrows it raw, and the test fails on the WRAPPER being absent,\n * naming nothing near the cause. Four modules reported it: `admin`, `backoffice`, `mission`,\n * `institution`.\n *\n * So: the module's own copy first, the workspace root second, nearest wins. That is the same shape\n * as `luxonAlias` above, and for the same reason: when a module has nothing of its own, the copy\n * it actually uses is the one to pin. Absent axios everywhere is still not an error.\n */\n/**\n * ONE physical luxon, for the same reason as axios: `Settings` is per-copy state.\n *\n * The setup pins `Settings.defaultZone = 'utc'`, which the root jest setup did for every suite in\n * the repo. It was reaching the copy `require` resolves, the CJS build, while the tests import the\n * one Vite resolves, the ESM build. Two instances, two `Settings`, and the pin landed on the one\n * nobody read.\n *\n * Measured on `libs/cloud/events-notifications`: the suite ran in `Europe/Paris`, so every date\n * came out two hours off and 12 assertions compared timestamps that differed by exactly that.\n *\n * luxon sits at the workspace root here rather than in each module, so both places are checked,\n * nearest first. Absent luxon is not an error: most modules have none.\n */\nfunction luxonAlias(root: string, workspaceRoot: string): Alias[] {\n for (const base of [root, workspaceRoot]) {\n const manifest = join(base, 'node_modules', 'luxon', 'package.json')\n if (existsSync(manifest)) return [{ find: /^luxon$/, replacement: dirname(manifest) }]\n }\n return []\n}\n\nfunction axiosAlias(root: string, workspaceRoot: string): Alias[] {\n for (const base of [root, workspaceRoot]) {\n const manifest = join(base, 'node_modules', 'axios', 'package.json')\n if (existsSync(manifest)) return [{ find: /^axios$/, replacement: dirname(manifest) }]\n }\n return []\n}\n\n/**\n * A generated Prisma client's runtime, resolved to the file the package actually ships.\n *\n * The generated client reaches its own runtime by package name, and the package's `exports` map\n * answers differently depending on which condition asks:\n *\n * \"./runtime/library\": { \"require\": \"./runtime/library.js\", // shipped\n * \"import\": \"./runtime/library.mjs\" } // NOT shipped\n *\n * jest asked as CJS and got the file. Vitest resolves the same specifier under `import`, is sent\n * to a `.mjs` that does not exist, and the spec file does not load at all, so its tests go missing\n * rather than failing. Node itself spells the answer out: \"Did you mean to import\n * .../runtime/library.js?\".\n *\n * Measured on `apps/nest/microservices/client-management` (1 file) and\n * `apps/nest/microservices/institution` (3 files), on two different generated clients. This repo\n * generates 31 of them, all with the same manifest, so this is a property of the generator rather\n * than of a module.\n *\n * Written as a pattern with a back-reference, not one entry per client: the module does not know\n * which clients its dependencies pull in, and enumerating 31 names would go stale the day a\n * thirty-second schema is added.\n *\n * ⚠️ Not a workaround for a mistake of ours. The package declares a target it does not ship, and\n * pointing at the shipped file is what `require` already did. Nothing else changes: the `.js` IS\n * the runtime, in the same package, at the version installed.\n */\nfunction prismaRuntimeAlias(workspaceRoot: string): Alias[] {\n const prisma = join(workspaceRoot, 'node_modules', '@prisma')\n if (!existsSync(prisma)) return []\n\n return [\n {\n find: /^@prisma\\/([^/]+)\\/runtime\\/library$/,\n replacement: join(prisma, '$1', 'runtime', 'library.js'),\n },\n ]\n}\n\n/**\n * The base every Nest module gets. Four decisions, each with its measurement.\n */\nfunction baseConfig(options: 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 ...(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 fileParallelism: !process.env.CI,\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 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 * 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":[]}
|
package/dist/chunk-ASBZQAXV.js
CHANGED
|
@@ -10018,4 +10018,3 @@ export {
|
|
|
10018
10018
|
detectFramework,
|
|
10019
10019
|
dispatch
|
|
10020
10020
|
};
|
|
10021
|
-
//# sourceMappingURL=chunk-ASBZQAXV.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/roles/test/setup/workspace.ts"],"sourcesContent":["/**\n * The rest of what a migrated suite needs before its tests, loaded DEFENSIVELY.\n *\n * ## Why this is in sentinel and not in the repo\n *\n * Because it is config, and an app must depend on its own config and nothing outside it. The first\n * version of the generated config referenced `'../../../../vitest.setup.mts'`, a relative path\n * climbing four levels to the workspace root — which is exactly the external link this whole\n * migration exists to remove. A module that reaches outside itself cannot move on its own.\n *\n * ## Why loading is conditional, and why that is the point rather than a precaution\n *\n * Unlike the msw lifecycle beside it, nothing here reacts to the RUNNER. `dotenv-flow` and luxon's\n * zone would behave identically under jest, Vitest or `node:test`; what they react to is the REPO,\n * which chose those two packages. So the conditional load is the statement of ownership: sentinel\n * offers the behaviour, and the repo that installed the package decides whether it happens.\n *\n * That is also what keeps sentinel installable elsewhere. Neither package can be a hard dependency,\n * so each is loaded only if present and skipped in silence if not. Elsewhere this file does nothing;\n * here both are present and the behaviour is the one the jest setup had.\n *\n * Silence is the right answer for an absent package and the wrong one for a broken package, so a\n * load that fails for any other reason is re-thrown rather than swallowed.\n */\nimport { createRequire } from 'node:module'\nimport { join } from 'node:path'\n\nimport { findWorkspaceRoot } from '../../../core/workspace-prep.js'\n\n/** Was this module absent, as opposed to present and broken? */\nfunction isMissingModule(error: unknown): boolean {\n return (error as { code?: string } | null)?.code === 'ERR_MODULE_NOT_FOUND'\n}\n\n/**\n * Optional by construction: `import()` inside a try, so a missing package is a no-op rather than a\n * failure at load time. A static import would make every one of them a hard dependency.\n */\nasync function optional(specifier: string, use: (module: unknown) => void): Promise<void> {\n try {\n use(await import(/* @vite-ignore */ specifier))\n } catch (error) {\n if (!isMissingModule(error)) throw error\n }\n}\n\n/**\n * The MODULE's copy of a package, not sentinel's.\n *\n * Under pnpm, an import written here resolves from sentinel's own directory, so it sees only\n * sentinel's dependencies. That is right for anything whose effect is GLOBAL, and wrong for\n * anything whose effect is per-copy: pinning `Settings.defaultZone` on sentinel's luxon leaves\n * the module's luxon in the machine's local zone, and the tests drift by an hour twice a year\n * with nothing saying why.\n *\n * Measured 2026-09-21: neither `luxon` nor `dotenv-flow` resolves from sentinel, while both\n * resolve from the module under test. The silent `optional` above therefore skipped BOTH, so a\n * migrated suite lost the `.env` cascade and the UTC pin at once, without a word.\n */\nfunction fromModule(specifier: string): unknown | undefined {\n try {\n const require = createRequire(join(process.cwd(), 'noop.cjs'))\n return require(specifier)\n } catch {\n return undefined\n }\n}\n\n/**\n * `dotenv-flow`, which the root jest setup loaded first.\n *\n * Vite's own `.env` handling does NOT replace it, which was checked before writing this. Measured on\n * Vitest 4 against a fixture holding `.env` and `.env.test`: the cascade is read and `MODE` is\n * `test`, but only `VITE_`-prefixed values are exposed, and only on `import.meta.env`. `process.env`\n * comes back untouched, and the code under test reads unprefixed `process.env`.\n *\n * `purge_dotenv` is carried across: it clears variables a previous load left behind, and without it\n * a value from one run leaks into the next. Not a detail in a suite that asserts on URLs.\n */\nasync function loadEnvironment(): Promise<void> {\n // A plain dependency of sentinel now: it mutates `process.env`, which is global, so WHICH copy\n // runs does not matter. It is the other half of replacing the root setup, and the root is going\n // away (Héla, 2026-09-21: \"mon but est de supprimer la config root\").\n await optional('dotenv-flow', (module) => {\n const flow = module as {\n config?: (options: Record<string, unknown>) => void\n default?: unknown\n }\n const config =\n flow.config ?? (flow.default as { config?: (o: Record<string, unknown>) => void })?.config\n /*\n * From the WORKSPACE ROOT, not from the module.\n *\n * dotenv-flow reads its cascade from the current directory, and the two runners do not share\n * one: jest ran from the repo root, where `.env`, `.env.test` and `.env.token` live, while\n * `sentinel --run --test` runs from the module, where there is nothing to read. Measured on\n * `libs/cloud/events-notifications`: from the module, a service call waits for an endpoint its\n * env never named and the test dies on a 5 s timeout; from the root, it answers in 200 ms.\n *\n * The root is found rather than configured: the setup cannot be handed a value, since a module\n * that overrides `test.env` would replace whatever the preset injected there.\n */\n const workspaceRoot = findWorkspaceRoot(process.cwd())\n config?.({\n silent: true,\n purge_dotenv: true,\n ...(workspaceRoot === undefined ? {} : { path: workspaceRoot }),\n })\n })\n}\n\n/**\n * Luxon's default zone, carried as luxon's own setting and NOT translated to `process.env.TZ`.\n *\n * They are different instructions: one configures luxon, the other the whole process, `Date` and\n * `Intl` included. Swapping them would change what a suite does while claiming to migrate it, and\n * timezone is not a detail here — a machine's zone accounted for a large part of 345 local failures\n * on `host-admin` that did not exist in CI.\n */\nasync function pinLuxonZone(): Promise<void> {\n // The module's copy, deliberately: `Settings` is per-copy state, so sentinel's own luxon is the\n // one package this setup must NOT configure.\n /*\n * Through the RUNNER's resolver first, which is what the tests use. `fromModule` goes through\n * `require`, and a package shipping both builds hands it the CJS one while Vite hands the tests\n * the ESM one. Two copies, two `Settings`, and the pin lands on the one nobody reads: measured on\n * `libs/cloud/events-notifications`, whose suite ran in `Europe/Paris` and compared timestamps\n * two hours apart. The preset aliases luxon to one path so both routes agree.\n */\n let pinned = false\n await optional('luxon', (module) => {\n const settings = (module as { Settings?: { defaultZone: string } }).Settings\n if (settings === undefined) return\n settings.defaultZone = 'utc'\n pinned = true\n })\n if (pinned) return\n\n const own = fromModule('luxon') as { Settings?: { defaultZone: string } } | undefined\n if (own?.Settings !== undefined) {\n own.Settings.defaultZone = 'utc'\n return\n }\n\n // Said out loud rather than skipped: a suite whose dates silently run in the machine's zone is\n // the kind of failure that gets blamed on the migration months later.\n process.stderr.write(\n 'sentinel (test): luxon was not found in this module, so the UTC default zone is NOT pinned. ' +\n 'Dates will follow the machine zone, as they did not under jest.\\n',\n )\n}\n\n/**\n * Everything the workspace needs beyond the msw lifecycle.\n *\n * NOT included: the `jest.mock('dynamoose')` and `jest.mock('@opentelemetry/exporter-metrics-otlp-grpc')`\n * calls the jest setup made. A module mock is a per-suite decision that `vi.mock` must make in the\n * file that needs it, and hoisting it into a shared setup is what makes a test pass for a reason\n * nobody can see. The migration reports them instead, so the module that relies on one declares it.\n */\nexport async function installWorkspaceSetup(): Promise<void> {\n await loadEnvironment()\n}\n\n/**\n * Luxon's default zone, split out because it is not in the same class as the rest.\n *\n * ⚠️ These two were one function until 23/09 and moving them together broke three modules.\n *\n * The `.env` cascade is needed by any suite whose code reads `process.env`, whatever setup its\n * jest config named: under jest those modules got it from a `globalSetup` running in the main\n * process, and Vitest's workers do not inherit it the same way. Removing it cost\n * `libs/front/logic` 23 tests and `libs/front/api` 44, all with `missing env var\n * NEXT_PUBLIC_MONOREPO_BASE_URL`.\n *\n * The UTC pin is a decision ONE file at the workspace root made, and only the modules naming that\n * file ever had it. So it is conditional and the cascade is not.\n */\nexport async function pinWorkspaceTimezone(): Promise<void> {\n await pinLuxonZone()\n}\n"],"mappings":";;;;;AAwBA,SAAS,qBAAqB;AAC9B,SAAS,YAAY;AAKrB,SAAS,gBAAgB,OAAyB;AAChD,SAAQ,OAAoC,SAAS;AACvD;AAMA,eAAe,SAAS,WAAmB,KAA+C;AACxF,MAAI;AACF,QAAI,MAAM;AAAA;AAAA,MAA0B;AAAA,KAAU;AAAA,EAChD,SAAS,OAAO;AACd,QAAI,CAAC,gBAAgB,KAAK,EAAG,OAAM;AAAA,EACrC;AACF;AAeA,SAAS,WAAW,WAAwC;AAC1D,MAAI;AACF,UAAMA,WAAU,cAAc,KAAK,QAAQ,IAAI,GAAG,UAAU,CAAC;AAC7D,WAAOA,SAAQ,SAAS;AAAA,EAC1B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAaA,eAAe,kBAAiC;AAI9C,QAAM,SAAS,eAAe,CAAC,WAAW;AACxC,UAAM,OAAO;AAIb,UAAM,SACJ,KAAK,UAAW,KAAK,SAA+D;AAatF,UAAM,gBAAgB,kBAAkB,QAAQ,IAAI,CAAC;AACrD,aAAS;AAAA,MACP,QAAQ;AAAA,MACR,cAAc;AAAA,MACd,GAAI,kBAAkB,SAAY,CAAC,IAAI,EAAE,MAAM,cAAc;AAAA,IAC/D,CAAC;AAAA,EACH,CAAC;AACH;AAUA,eAAe,eAA8B;AAU3C,MAAI,SAAS;AACb,QAAM,SAAS,SAAS,CAAC,WAAW;AAClC,UAAM,WAAY,OAAkD;AACpE,QAAI,aAAa,OAAW;AAC5B,aAAS,cAAc;AACvB,aAAS;AAAA,EACX,CAAC;AACD,MAAI,OAAQ;AAEZ,QAAM,MAAM,WAAW,OAAO;AAC9B,MAAI,KAAK,aAAa,QAAW;AAC/B,QAAI,SAAS,cAAc;AAC3B;AAAA,EACF;AAIA,UAAQ,OAAO;AAAA,IACb;AAAA,EAEF;AACF;AAUA,eAAsB,wBAAuC;AAC3D,QAAM,gBAAgB;AACxB;AAgBA,eAAsB,uBAAsC;AAC1D,QAAM,aAAa;AACrB;","names":["require"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/roles/test/setup/msw.ts"],"sourcesContent":["/**\n * The HTTP interception a test suite runs against, owned here rather than by each module.\n *\n * ## Why sentinel ships msw at all\n *\n * The goal is that a future migration is ONE version bump of sentinel. Today's migration is\n * painful for the opposite reason: the runner and everything around it are declared in 133 places,\n * so moving them means moving 133 modules. A tool that sits between the runner and the tests, as\n * msw does, has to travel with the runner or it becomes the next thing nobody can move.\n *\n * It also puts the compatibility question where someone can answer it. When Vitest moves, msw\n * either still works with it or does not, and that is a question for the tooling team with one\n * test suite, not for thirty teams discovering it separately.\n *\n * ## Why version 1.3.3, which is not the newest\n *\n * Measured across the repo: 523 test files use the msw 1 API (`rest.get`), and NOT ONE uses the\n * msw 2 API (`http.get`). Shipping msw 2 would break all 523 on the day they migrate, turning one\n * migration into two. Shipping 1.3.3 keeps them working, and the eventual 1 to 2 move becomes\n * OURS: studied once, then automated or documented, which is the whole point of owning it.\n *\n * The one module on msw 2 is `apps/front/hr-management`, which is Svelte, and the test role defers\n * Svelte for its own reasons.\n */\nimport { setupServer } from 'msw/node'\n\n/**\n * No default handlers, deliberately.\n *\n * The repo's own server had `defaultHandlers = []` too, so nothing is lost. A handler here would\n * be a Hublo endpoint inside a package meant to be usable by anyone, which is the line between\n * what sentinel owns (the config) and what an app owns (what it mocks).\n *\n * A suite adds its own with `server.use(...)`, which is how all 600 files that touch this already\n * work.\n */\nexport const server = setupServer()\n\n/** The options the repo's own setup passed, kept in one place since two calls now use them. */\nconst LISTEN_OPTIONS = { onUnhandledRequest: 'error' } as const\n\n/**\n * The lifecycle, as the root jest setup had it, plus ONE deviation the runners forced.\n *\n * `onUnhandledRequest: 'error'` is carried over rather than softened: a test that reaches a real\n * network is a test whose result depends on something outside the repo, and it should say so\n * loudly. The repo already ran this way, so nothing changes for a migrating suite.\n *\n * Registered through the globals Vitest exposes, because the preset sets `globals: true` and a\n * setup file that imported them would need them resolvable from wherever it is loaded.\n *\n * ## Why `listen()` also runs here, at module scope\n *\n * msw intercepts by REPLACING `request` and `get` on the `http` and `https` modules. Under jest,\n * `import * as https from 'https'` compiled to `require('https')`, so the module object was read\n * at call time and a patch applied later was seen. Under Vitest the same line is a real ESM\n * namespace import of a CJS builtin, and Node builds that namespace ONCE, from the exports as they\n * stand at first import. A patch applied afterwards never reaches it.\n *\n * `beforeAll` runs after the test file and everything it imports have been evaluated, so the\n * namespace is already frozen by then: the provider keeps calling the ORIGINAL `https.request` and\n * the request leaves the machine. Measured on `libs/cloud/hermes`: `send-push` reached the REAL\n * OneSignal API, which answered `400 Failed to parse app_id from request`, a message that exists\n * nowhere in this repo. msw never saw the call, so `onUnhandledRequest: 'error'` never fired\n * either, and the only visible symptom was `expected \"vi.fn()\" to be called 50 times, but got 0`.\n *\n * A setup file is evaluated BEFORE the test module graph, so patching here happens before the\n * namespace is built. Same file, same suite, 0 of 7 to 7 of 7.\n *\n * The `beforeAll` call is kept as well: it is what restores interception for a suite that closed\n * the server itself, and msw's own `listen()` is safe to call twice.\n */\nexport function installMswLifecycle(hooks: {\n beforeAll: (fn: () => void) => void\n afterEach: (fn: () => void) => void\n afterAll: (fn: () => void) => void\n}): void {\n server.listen(LISTEN_OPTIONS)\n hooks.beforeAll(() => server.listen(LISTEN_OPTIONS))\n hooks.afterAll(() => server.close())\n\n /*\n * `server.resetHandlers()` is NOT called here, and that is a transcription and not a judgement.\n *\n * The root jest setup has the line commented out. Enabling it would be an improvement in the\n * abstract — a handler one test adds leaks into the next, and the failure then appears in\n * whichever test happens to run after, which is the hardest kind to trace. But some suite may\n * depend on that persistence today, and a migration is the wrong moment to find out.\n *\n * So the behaviour is carried across exactly, and the question is reported rather than settled\n * here: it belongs to whoever owns those suites, as its own reviewed change.\n */\n}\n"],"mappings":";AAwBA,SAAS,mBAAmB;AAYrB,IAAM,SAAS,YAAY;AAGlC,IAAM,iBAAiB,EAAE,oBAAoB,QAAQ;AAiC9C,SAAS,oBAAoB,OAI3B;AACP,SAAO,OAAO,cAAc;AAC5B,QAAM,UAAU,MAAM,OAAO,OAAO,cAAc,CAAC;AACnD,QAAM,SAAS,MAAM,OAAO,MAAM,CAAC;AAarC;","names":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/shared/package-json.ts","../src/core/workspace-prep.ts","../src/core/settings.ts"],"sourcesContent":["/**\n * package.json helpers, shared by the CLI and the engine.\n */\nimport { existsSync, readFileSync } from 'node:fs'\nimport { basename, dirname, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\n/**\n * sentinel's OWN version, for `--version`. We must NOT use\n * `process.env.npm_package_version`, that is the version of whatever script\n * invoked us (an app's package.json, or nothing). Instead walk up from this\n * module to the nearest package.json, which is always sentinel's own, whether\n * running from source or the bundled dist.\n */\n/**\n * sentinel's OWN name + version, read by walking up from this module to the nearest\n * package.json. Used both for `--version` and to pin the exact dependency `--init`\n * writes into an adopted module, so a module always references the version that adopted it.\n */\nexport function readOwnPackage(): { name: string; version: string; bin?: string } {\n let dir = dirname(fileURLToPath(import.meta.url))\n for (;;) {\n const pkgPath = join(dir, 'package.json')\n if (existsSync(pkgPath)) {\n try {\n const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) as {\n name?: string\n version?: string\n bin?: string | Record<string, string>\n }\n if (typeof pkg.version === 'string') {\n // The bin PATH, not the command name: a package that adopts itself has to invoke\n // its own build output, because the `.bin` shim does not exist yet (see below).\n const bin = typeof pkg.bin === 'string' ? pkg.bin : Object.values(pkg.bin ?? {})[0]\n return { name: pkg.name ?? '@hublo/sentinel', version: pkg.version, bin }\n }\n } catch {\n // Malformed package.json; keep walking up.\n }\n }\n const parent = dirname(dir)\n if (parent === dir) return { name: '@hublo/sentinel', version: '0.0.0' } // filesystem root\n dir = parent\n }\n}\n\nexport function readOwnVersion(): string {\n return readOwnPackage().version\n}\n\n/**\n * Read a module's package.json for framework detection; tolerant of a missing or\n * malformed file (returns `{}`, so detection falls through to the `node` default).\n */\nexport function readProjectPackageJson(dir: string): {\n /** The package name, which is also what nx calls the project when there is no `project.json`. */\n name?: string\n dependencies?: Record<string, string>\n devDependencies?: Record<string, string>\n /**\n * How the module's `.js`/`.ts` files are loaded. It decides whether a Vite config can import\n * an ESM-only package at all, which is why the build role reads it before rewriting one.\n */\n type?: string\n /**\n * What nx alone needs per target, which every role writes here and none could read back.\n *\n * Typed loosely on purpose: this reader is shared by every role and each one knows the shape of\n * its own target. Declared at all because a role that WRITES a block and then has to cast to\n * read it is one refactor away from reading a key it never wrote.\n */\n nx?: { targets?: Record<string, Record<string, unknown>> }\n} {\n const path = join(dir, 'package.json')\n if (!existsSync(path)) return {}\n try {\n return JSON.parse(readFileSync(path, 'utf8')) as Record<string, never>\n } catch {\n process.stderr.write(`sentinel: could not parse ${path}; ignoring for detection.\\n`)\n return {}\n }\n}\n\n/**\n * The nx project name for a module, read from its `project.json`, or undefined if\n * there is none/unreadable. Used when `--init` has to scaffold a `package.json`\n * for a module that only has a `project.json` (common for nx apps/services): the\n * scaffolded file borrows this name so it is a valid, uniquely-named workspace\n * package. Reusable by any tool that scaffolds, not just TypeScript.\n */\nexport function readNxProjectName(dir: string): string | undefined {\n const path = join(dir, 'project.json')\n if (!existsSync(path)) return undefined\n try {\n const parsed = JSON.parse(readFileSync(path, 'utf8')) as { name?: unknown }\n return typeof parsed.name === 'string' ? parsed.name : undefined\n } catch {\n return undefined\n }\n}\n\n/**\n * What nx calls this module, resolved the way nx itself resolves it.\n *\n * The precedence is `project.json`, then `package.json`, then the directory, and only the\n * first and last of those were being consulted. A module with no `project.json` therefore\n * answered with its DIRECTORY while nx answered with its package name, and the two disagree\n * in the real repo: `apps/front/hr-management` is `front-hr-management` to nx, as is\n * `apps/front/storybook-app`.\n *\n * That gap is not cosmetic, because this name is what `--module` is matched against. The\n * `serve` script `--init` writes carries `--module <name>`, so a module in that shape was\n * adopted with a script naming a project the workspace does not contain, and `pnpm run serve`\n * failed the day it was adopted with nothing having reported it.\n *\n * One function so the two ends cannot drift again: whoever WRITES the name and whoever\n * RESOLVES it now read the same source.\n */\nexport function moduleName(dir: string): string {\n const declared = readProjectPackageJson(dir).name\n return readNxProjectName(dir) ?? (declared === undefined ? basename(dir) : declared)\n}\n","/**\n * Workspace-level preparation that `--init` applies once, at the workspace root, so an\n * adopted module actually installs and resolves cleanly. `--init` writes a module's own\n * files; this is the small, deliberate exception where it also touches the root, because\n * \"set this module up\" is not honest if the workspace it lives in cannot then install or\n * resolve it.\n *\n * Kept intentionally narrow (YAGNI): it applies the specific fixes a workspace's OWN\n * config shows it needs, and is a no-op everywhere else. It is NOT a speculative dedupe\n * engine, when a second, different case appears we generalize from real cases instead of\n * predicting them. The two fixes today:\n *\n * - i18next singleton override. If the root declares a second TypeScript via the\n * `@typescript/native` alias (a repo mid-migration running two TypeScripts), adopting a\n * module re-resolves pnpm and i18next, which carries an optional type-only `typescript`\n * peer, forks into two peer-keyed variants. That forks react-i18next into two React\n * contexts and breaks translation-singleton tests. Pinning i18next's `typescript` peer\n * to the declared TS collapses it to one. Read from the root's own declaration, so it\n * tracks the version and disappears (no-op) once the migration removes the second TS.\n * Written to `pnpm-workspace.yaml`, the only place current pnpm reads overrides from.\n *\n * - release-age allow-list. If the root uses pnpm's `minimumReleaseAgeExclude` gate, a\n * freshly published sentinel is younger than the gate and will not install; allow-list\n * it. Only touched when the gate is actually configured.\n *\n * - root formatter exclusion. A module that has adopted the FORMAT role is formatted by\n * oxfmt, from its own config. The root Prettier does not know that and still owns every\n * file in the repo, so `nx format:write` or `prettier --write .` from the root reformats\n * the module back to the ROOT's style, and the next `pnpm run format` in the module\n * reverses it. Two formatters fighting over the same files, with whichever ran last\n * winning, is the exact state adoption removes INSIDE a module, and it has to be removed\n * at the root too. So the module is excluded in the root `.prettierignore` as it adopts,\n * and the root's own config is otherwise untouched: it still serves every module that has\n * not migrated.\n *\n * All three edits are idempotent and defensive: absent the trigger (no second TypeScript, no\n * release-age gate, no root Prettier) each is skipped, so the step is safe on any workspace.\n */\nimport { existsSync, readFileSync, writeFileSync } from 'node:fs'\nimport { dirname, join, relative } from 'node:path'\n\nimport { readOwnPackage } from '../shared/package-json.js'\n\nimport { WORKSPACE_ROOT_MARKER } from './settings.js'\n\nconst OVERRIDE_KEY = 'i18next>typescript'\nconst NATIVE_TS_ALIAS = '@typescript/native' // `npm:typescript@^7.x`: a workspace's second, real TS\nconst WORKSPACE_YAML = 'pnpm-workspace.yaml'\nconst RELEASE_AGE_KEY = 'minimumReleaseAgeExclude'\nconst LIST_ITEM = /^(\\s*)-\\s+(.*?)\\s*$/\nconst MAP_ENTRY = /^(\\s+)(.*?):\\s+(.*?)\\s*$/\n\ninterface RootPackageJson {\n dependencies?: Record<string, string>\n devDependencies?: Record<string, string>\n pnpm?: { overrides?: Record<string, string> }\n}\n\n/** Walk up from a module directory to the workspace root (the dir holding the marker). */\nexport function findWorkspaceRoot(startDir: string): string | undefined {\n let dir = startDir\n for (;;) {\n if (existsSync(join(dir, WORKSPACE_ROOT_MARKER))) return dir\n const parent = dirname(dir)\n if (parent === dir) return undefined\n dir = parent\n }\n}\n\n/**\n * The TypeScript version to pin i18next's peer to, read from the root's own\n * `@typescript/native` alias (`npm:typescript@^7.0.2` -> `7.0.2`). Returns undefined when\n * the alias is absent (no second TypeScript, so no fork to fix).\n */\nfunction declaredNativeTs(pkg: RootPackageJson): string | undefined {\n const spec = pkg.dependencies?.[NATIVE_TS_ALIAS] ?? pkg.devDependencies?.[NATIVE_TS_ALIAS]\n if (!spec) return undefined\n const version = spec.slice(spec.lastIndexOf('@') + 1).replace(/^[\\^~>=<\\s]+/, '')\n return version || undefined\n}\n\n/**\n * Ensure the i18next singleton override matches the declared second TypeScript. No-op when\n * the workspace runs a single TypeScript (no `@typescript/native`). Returns a message when\n * it changed (or would change, under dryRun), else undefined.\n *\n * The override is written to `pnpm-workspace.yaml`, never to `package.json#pnpm`: pnpm 11\n * stopped reading the `pnpm` field and does so SILENTLY, with no warning and no deprecation\n * (pnpm/pnpm#11536), so an override left in `package.json` is simply not applied. The yaml is\n * read by pnpm 10 and 11 alike (https://pnpm.io/settings), which is why there is one target\n * here rather than a version-dependent fallback.\n */\nfunction ensureI18nextSingleton(root: string, dryRun: boolean): string | undefined {\n const pkgPath = join(root, 'package.json')\n if (!existsSync(pkgPath)) return undefined\n const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) as RootPackageJson\n const want = declaredNativeTs(pkg)\n if (!want) return undefined // single TypeScript: nothing forks\n\n const yamlPath = join(root, WORKSPACE_YAML)\n if (!existsSync(yamlPath)) return undefined // no workspace file: nowhere pnpm would read it\n const lines = readFileSync(yamlPath, 'utf8').split('\\n')\n const entry = findOverrideEntry(lines)\n if (entry.valueIdx !== undefined && entry.value === want) return undefined // already pinned\n if (dryRun) return `would pin ${OVERRIDE_KEY} to ${want} in ${WORKSPACE_YAML} (i18next singleton)`\n\n applyOverride(lines, entry, want)\n writeFileSync(yamlPath, lines.join('\\n'))\n return `pinned ${OVERRIDE_KEY} to ${want} in ${WORKSPACE_YAML} (i18next singleton)`\n}\n\n/** Where the override sits in the yaml, if at all, plus where a new one would be inserted. */\ninterface OverrideEntry {\n /** Index of the `OVERRIDE_KEY` line, when the key is already declared. */\n valueIdx?: number\n /** Its current value, unquoted. */\n value?: string\n /** Index of the `overrides:` key line, when the block exists. */\n blockIdx?: number\n /** Index of the block's last entry, so an insertion keeps the block contiguous. */\n lastEntryIdx?: number\n /** Indentation the block's entries use. */\n indent: string\n}\n\n/**\n * Locate the `overrides:` block and the i18next key inside it. A targeted text read rather\n * than a yaml parse, for the same reason `ensureReleaseAgeAllowList` does it: this file is\n * hand-maintained and round-tripping it through a parser would drop its comments and ordering.\n */\nfunction findOverrideEntry(lines: string[]): OverrideEntry {\n const blockIdx = lines.findIndex((line) => line.replace(/\\s+$/, '') === 'overrides:')\n if (blockIdx === -1) return { indent: ' ' }\n\n let lastEntryIdx = blockIdx\n let indent = ' '\n for (let i = blockIdx + 1; i < lines.length; i++) {\n const match = lines[i]?.match(MAP_ENTRY)\n if (!match) break // dedent or blank line: the block ended\n indent = match[1] ?? indent\n lastEntryIdx = i\n if ((match[2] ?? '').replace(/^['\"]|['\"]$/g, '') === OVERRIDE_KEY) {\n return {\n valueIdx: i,\n value: (match[3] ?? '').replace(/^['\"]|['\"]$/g, ''),\n blockIdx,\n lastEntryIdx,\n indent,\n }\n }\n }\n return { blockIdx, lastEntryIdx, indent }\n}\n\n/** Update the key in place, append it to an existing block, or open a block at the end. */\nfunction applyOverride(lines: string[], entry: OverrideEntry, want: string): void {\n const line = `${entry.indent}'${OVERRIDE_KEY}': ${want}`\n if (entry.valueIdx !== undefined) {\n lines[entry.valueIdx] = line\n return\n }\n if (entry.lastEntryIdx !== undefined) {\n lines.splice(entry.lastEntryIdx + 1, 0, line)\n return\n }\n // No `overrides:` block at all: open one, keeping a single trailing newline.\n while (lines.length > 0 && lines[lines.length - 1] === '') lines.pop()\n lines.push('overrides:', line, '')\n}\n\n/**\n * Ensure sentinel is allow-listed under `minimumReleaseAgeExclude` so a fresh publish\n * installs. No-op when the workspace has no such gate. Targeted text edit so comments and\n * formatting are preserved. Returns a message when it changed (or would), else undefined.\n */\nfunction ensureReleaseAgeAllowList(root: string, dryRun: boolean): string | undefined {\n const yamlPath = join(root, WORKSPACE_YAML)\n if (!existsSync(yamlPath)) return undefined\n const own = readOwnPackage().name\n const lines = readFileSync(yamlPath, 'utf8').split('\\n')\n const keyIdx = lines.findIndex((line) => line.replace(/\\s+$/, '') === `${RELEASE_AGE_KEY}:`)\n if (keyIdx === -1) return undefined // no release-age gate in use\n\n let lastItemIdx = keyIdx\n let indent = ' '\n for (let i = keyIdx + 1; i < lines.length; i++) {\n const match = lines[i]?.match(LIST_ITEM)\n if (!match) break\n indent = match[1] ?? indent\n lastItemIdx = i\n if ((match[2] ?? '').replace(/^['\"]|['\"]$/g, '') === own) return undefined // already listed\n }\n if (dryRun) return `would allow-list ${own} under ${RELEASE_AGE_KEY}`\n\n lines.splice(lastItemIdx + 1, 0, `${indent}- '${own}'`)\n writeFileSync(yamlPath, lines.join('\\n'))\n return `allow-listed ${own} under ${RELEASE_AGE_KEY} in ${WORKSPACE_YAML}`\n}\n\n/** The root ignore file Prettier reads, and the section sentinel owns inside it. */\nconst PRETTIER_IGNORE = '.prettierignore'\nconst EXCLUSION_HEADER = '# @hublo/sentinel: modules formatted by oxfmt, not by the root Prettier'\n\n/** Root Prettier configs. Their presence is what makes an exclusion meaningful. */\nconst ROOT_PRETTIER_CONFIGS = [\n '.prettierrc',\n '.prettierrc.json',\n '.prettierrc.json5',\n '.prettierrc.yml',\n '.prettierrc.yaml',\n '.prettierrc.js',\n '.prettierrc.cjs',\n '.prettierrc.mjs',\n 'prettier.config.js',\n 'prettier.config.cjs',\n 'prettier.config.mjs',\n]\n\n/**\n * Exclude a module that now formats itself from the ROOT Prettier.\n *\n * Written as a directory pattern anchored at the root (`/apps/front/x/`), which is what\n * gitignore syntax means by \"this exact directory\", so a module cannot accidentally exclude a\n * same-named directory somewhere else.\n *\n * Skipped entirely when the root has no Prettier at all: there is then nothing to conflict\n * with, and creating an ignore file for a tool the workspace does not use would be noise.\n */\nfunction ensureFormatterExclusion(\n root: string,\n moduleDir: string,\n dryRun: boolean,\n): string | undefined {\n const rel = relative(root, moduleDir).replaceAll('\\\\', '/')\n if (rel === '' || rel.startsWith('..')) return undefined // the root itself, or outside it\n\n const ignorePath = join(root, PRETTIER_IGNORE)\n const hasPrettier =\n existsSync(ignorePath) || ROOT_PRETTIER_CONFIGS.some((name) => existsSync(join(root, name)))\n if (!hasPrettier) return undefined\n\n const pattern = `/${rel}/`\n const existing = existsSync(ignorePath) ? readFileSync(ignorePath, 'utf8') : ''\n const lines = existing.split('\\n')\n // Already excluded, in whichever spelling a human may have used.\n if (lines.some((line) => line.trim().replace(/\\/$/, '') === pattern.replace(/\\/$/, ''))) {\n return undefined\n }\n if (dryRun) return `would exclude ${rel} from the root Prettier (${PRETTIER_IGNORE})`\n\n const headerIdx = lines.findIndex((line) => line.trim() === EXCLUSION_HEADER)\n if (headerIdx === -1) {\n const prefix = existing.trim() === '' ? '' : `${existing.replace(/\\n*$/, '')}\\n\\n`\n writeFileSync(ignorePath, `${prefix}${EXCLUSION_HEADER}\\n${pattern}\\n`)\n } else {\n // Keep the managed section together and sorted, so the list stays readable as it grows\n // to 150 entries and a diff shows one added line rather than a reshuffle.\n let end = headerIdx + 1\n while (end < lines.length && lines[end]?.trim().startsWith('/')) end += 1\n const managed = [...lines.slice(headerIdx + 1, end), pattern].sort()\n writeFileSync(\n ignorePath,\n [...lines.slice(0, headerIdx + 1), ...managed, ...lines.slice(end)].join('\\n'),\n )\n }\n return `excluded ${rel} from the root Prettier (${PRETTIER_IGNORE}): it formats itself now`\n}\n\n/**\n * Apply every workspace-level fix the given root needs. Returns the human-readable\n * messages for what changed (empty when nothing did). Under dryRun nothing is written and\n * the messages describe what would change.\n *\n * `formattedModule` is the module that just adopted the FORMAT role, when one did. Absent\n * for a lint-only or TypeScript-only `--init`, because a module that has not adopted the\n * formatter must keep being formatted by the root.\n */\nexport function ensureWorkspacePrep(opts: {\n root: string\n dryRun?: boolean\n formattedModule?: string\n}): string[] {\n const dryRun = Boolean(opts.dryRun)\n return [\n ensureI18nextSingleton(opts.root, dryRun),\n ensureReleaseAgeAllowList(opts.root, dryRun),\n opts.formattedModule === undefined\n ? undefined\n : ensureFormatterExclusion(opts.root, opts.formattedModule, dryRun),\n ].filter((message): message is string => message !== undefined)\n}\n\n/**\n * One standing change sentinel has made to the workspace ROOT, and whether it is still earned.\n *\n * This exists because the three root writes are the only thing sentinel does that nobody can\n * see afterwards. `--init` announces each one on the run that makes it, and then it is a line\n * in someone else's file forever. Two consequences already bit us.\n *\n * The `i18next>typescript` override re-resolved `apps/front/console` from a PR that touched no\n * file under it, so CODEOWNERS pulled nobody and its owners learned about it afterwards.\n *\n * And every one of them is TEMPORARY with its removal condition written in prose. That failed\n * three times in one week: `SENTINEL_TAG=alpha` in the migration-page workflow kept building\n * with a pre-release long after 1.1.0 went stable and reported `Format 0/439` for 15 adopted\n * modules; ADR 0042 and its RFC both sat mid-flight past their deadline; and this release-age\n * exclusion is still waiting on \"once sentinel's release cadence settles\". Nothing failed when\n * any of them expired, because a comment cannot fail.\n *\n * So each entry carries the condition that JUSTIFIES it, evaluated now rather than described.\n * A trigger that has gone away makes the entry dead config, and dead config is something a\n * command can say out loud instead of something a reader has to notice.\n *\n * Read-only, and offline: it inspects the same files `--init` writes and nothing else. What it\n * deliberately does NOT claim is anything needing the npm registry — whether a published\n * version is old enough to clear the release-age gate is not knowable from the repo, so it is\n * stated as the standing exemption it is rather than guessed at.\n */\nexport interface WorkspacePrepEntry {\n /** `rule` to match the shape the shared renderer already prints as a labelled block. */\n rule: string\n /** What it does, whether its trigger still holds, and what would retire it. */\n reason: string\n}\n\n/** The root-level changes sentinel maintains here, each with its justification checked now. */\nexport function inspectWorkspacePrep(root: string): WorkspacePrepEntry[] {\n const entries: WorkspacePrepEntry[] = []\n // Tolerant, unlike the write path above, and the difference is deliberate. `--init` should\n // fail loudly on a root it cannot parse, because it is about to edit it. `--inspect` is a\n // read verb that sweeps 441 modules, and one malformed root file must not take the whole\n // sweep down: the modules still have honest answers to give. Caught by its own test.\n let pkg: RootPackageJson = {}\n const pkgPath = join(root, 'package.json')\n if (existsSync(pkgPath)) {\n try {\n pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) as RootPackageJson\n } catch {\n pkg = {}\n }\n }\n\n const yamlPath = join(root, WORKSPACE_YAML)\n const yaml = existsSync(yamlPath) ? readFileSync(yamlPath, 'utf8') : undefined\n\n const pinned = yaml === undefined ? undefined : findOverrideEntry(yaml.split('\\n')).value\n if (pinned !== undefined) {\n const stillForks = declaredNativeTs(pkg)\n entries.push({\n rule: `${WORKSPACE_YAML} → overrides.${OVERRIDE_KEY}`,\n reason:\n stillForks === undefined\n ? `pinned to ${pinned}, but the root no longer declares ${NATIVE_TS_ALIAS}, so nothing ` +\n `forks any more. This override is now dead config and can be removed.`\n : `pinned to ${pinned}, matching the second TypeScript the root declares ` +\n `(${NATIVE_TS_ALIAS}). It re-resolves i18next for EVERY module that depends on it, ` +\n `not only adopted ones. It retires with the second TypeScript.`,\n })\n }\n\n // A copy left in `package.json#pnpm` by an older sentinel, or by hand. Reported because it\n // is INERT rather than redundant: pnpm 11 dropped the `pnpm` field with no warning\n // (pnpm/pnpm#11536), so it reads as protection that is not applied.\n const legacy = pkg.pnpm?.overrides?.[OVERRIDE_KEY]\n if (legacy !== undefined) {\n entries.push({\n rule: `package.json → pnpm.overrides.${OVERRIDE_KEY}`,\n reason:\n `pinned to ${legacy}, but pnpm 11 no longer reads the \\`pnpm\\` field of package.json ` +\n `and says nothing about it, so this override is not applied. Move it to ` +\n `${WORKSPACE_YAML} under \\`overrides:\\`, which pnpm 10 and 11 both read.`,\n })\n }\n\n const own = readOwnPackage().name\n if (yaml !== undefined) {\n const listed = new RegExp(`^\\\\s*-\\\\s*['\"]?${own.replace('/', '\\\\/')}['\"]?\\\\s*$`, 'm').test(yaml)\n if (listed) {\n const gated = new RegExp(`^\\\\s*minimumReleaseAge\\\\s*:`, 'm').test(yaml)\n entries.push({\n rule: `${WORKSPACE_YAML} → ${RELEASE_AGE_KEY}: ${own}`,\n reason: gated\n ? `${own} is exempt from this workspace's release-age gate, so a freshly published ` +\n `version installs instead of being held. That is a standing exemption from a ` +\n `supply-chain control: the exposure is one bump wide, since the version is pinned ` +\n `and the lockfile carries its integrity, so a re-publish of the same version does ` +\n `not pass. Retire it once a bump can simply wait out the window.`\n : `${own} is allow-listed, but this workspace no longer sets minimumReleaseAge, so ` +\n `there is no gate to be exempt from. This entry is now dead config and can be removed.`,\n })\n }\n }\n\n return entries\n}\n","/**\n * Tunable settings: the knobs you would actually change. The fixed vocabulary and\n * its types live in `domain.ts`.\n *\n * Note: the preset is DETECTED from a module's dependencies (see\n * `detect-framework.ts`), deterministically, `node` when no framework dep is present.\n * `--preset` overrides the detection (recommended for `--init`, since hoisted deps\n * can make detection return `node`). The detection table lives in `detect-framework.ts`.\n */\n\n/** A file that marks a workspace root; sentinel refuses to operate there. */\nexport const WORKSPACE_ROOT_MARKER = 'nx.json'\n\n/**\n * How many structured diagnostics a report embeds per module, when the caller does not say.\n *\n * A cap, not a limit on what is counted: the totals stay exact and `diagnosticsTruncated`\n * says when rows were dropped, so a bounded envelope never reads as a clean module. Shared\n * by every role, because a consumer reading `--json` across roles should not have to learn\n * a different cap for each one. `--max-diagnostics 0` means no cap.\n */\nexport const DEFAULT_MAX_DIAGNOSTICS = 100\n"],"mappings":"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","names":["existsSync","readFileSync","dirname","join","existsSync","join","dirname","readFileSync"]}
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hublo/sentinel",
|
|
3
|
-
"version": "1.4.0-alpha.
|
|
3
|
+
"version": "1.4.0-alpha.5",
|
|
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",
|
|
@@ -76,9 +76,7 @@
|
|
|
76
76
|
"docs",
|
|
77
77
|
"lint",
|
|
78
78
|
"oxlint",
|
|
79
|
-
"plugins"
|
|
80
|
-
"!dist/**/*.map",
|
|
81
|
-
"dist/roles/**/*.map"
|
|
79
|
+
"plugins"
|
|
82
80
|
],
|
|
83
81
|
"dependencies": {
|
|
84
82
|
"@nx/eslint-plugin": "23.0.1",
|