@hublo/sentinel 1.4.0-alpha.8 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +1 -0
  2. package/dist/bin/sentinel.d.ts +1 -0
  3. package/dist/bin/sentinel.js +45 -101
  4. package/dist/{chunk-7PUVK4YM.js → chunk-5VNYQIFD.js} +224 -13
  5. package/dist/chunk-5VNYQIFD.js.map +1 -0
  6. package/dist/chunk-BXXNV6NP.js +6411 -0
  7. package/dist/chunk-ELZHIN6E.js +16 -0
  8. package/dist/chunk-ELZHIN6E.js.map +1 -0
  9. package/dist/chunk-H3GSGAX2.js +11 -0
  10. package/dist/chunk-H3GSGAX2.js.map +1 -0
  11. package/dist/{chunk-WLFE5RUU.js → chunk-KMKQDGI6.js} +6 -8
  12. package/dist/chunk-KMKQDGI6.js.map +1 -0
  13. package/dist/{chunk-PWV3BMDA.js → chunk-MMKBDO5V.js} +11 -2
  14. package/dist/chunk-MMKBDO5V.js.map +1 -0
  15. package/dist/{chunk-QP37G2HO.js → chunk-MWNOFSYR.js} +221 -4419
  16. package/dist/chunk-NUOQXAYR.js +14 -0
  17. package/dist/chunk-NUOQXAYR.js.map +1 -0
  18. package/dist/chunk-O7REVMOC.js +38 -0
  19. package/dist/chunk-O7REVMOC.js.map +1 -0
  20. package/dist/chunk-QXFCZON7.js +16 -0
  21. package/dist/chunk-QXFCZON7.js.map +1 -0
  22. package/dist/{chunk-3TDUIKVQ.js → chunk-SWWQ7X7B.js} +11 -8
  23. package/dist/{chunk-3TDUIKVQ.js.map → chunk-SWWQ7X7B.js.map} +1 -1
  24. package/dist/{chunk-CPCUPK4J.js → chunk-Z7L4FGKP.js} +5 -12
  25. package/dist/chunk-Z7L4FGKP.js.map +1 -0
  26. package/dist/index.d.ts +28 -5
  27. package/dist/index.js +7 -3
  28. package/dist/roles/build/nest/toolchain.js +11 -33
  29. package/dist/roles/build/nest/toolchain.js.map +1 -1
  30. package/dist/roles/test/nest/toolchain.js +3 -2
  31. package/dist/roles/test/nest/toolchain.js.map +1 -1
  32. package/dist/roles/test/react/toolchain.js +3 -2
  33. package/dist/roles/test/react/toolchain.js.map +1 -1
  34. package/dist/roles/test/setup/a11y.d.ts +45 -0
  35. package/dist/roles/test/setup/a11y.js +72 -0
  36. package/dist/roles/test/setup/a11y.js.map +1 -0
  37. package/dist/roles/test/setup/file-boundary-close.d.ts +2 -0
  38. package/dist/roles/test/setup/file-boundary-close.js +8 -0
  39. package/dist/roles/test/setup/file-boundary-close.js.map +1 -0
  40. package/dist/roles/test/setup/file-boundary.d.ts +2 -0
  41. package/dist/roles/test/setup/file-boundary.js +62 -0
  42. package/dist/roles/test/setup/file-boundary.js.map +1 -0
  43. package/dist/roles/test/setup/jest-parity.js +19 -2
  44. package/dist/roles/test/setup/jest-parity.js.map +1 -1
  45. package/dist/roles/test/setup/msw-lifecycle.js +2 -1
  46. package/dist/roles/test/setup/msw-lifecycle.js.map +1 -1
  47. package/dist/roles/test/setup/msw-server.js +2 -1
  48. package/dist/roles/test/setup/msw-server.js.map +1 -1
  49. package/dist/roles/test/setup/w3c.d.ts +75 -0
  50. package/dist/roles/test/setup/w3c.js +49 -0
  51. package/dist/roles/test/setup/w3c.js.map +1 -0
  52. package/dist/roles/test/setup/workspace-entry.js +3 -2
  53. package/dist/roles/test/setup/workspace-entry.js.map +1 -1
  54. package/dist/roles/test/shared-test-config.d.ts +13 -0
  55. package/dist/roles/test/shared-test-config.js +3 -2
  56. package/dist/roles/test/tools/msw.d.ts +1 -0
  57. package/dist/roles/test/tools/msw.js +3 -0
  58. package/dist/roles/test/tools/msw.js.map +1 -0
  59. package/dist/validate-BKD2ICT7.js +170 -0
  60. package/docs/test-adoption.md +282 -5
  61. package/docs/using-sentinel.md +17 -1
  62. package/docs/validating-a-change.md +35 -2
  63. package/package.json +29 -1
  64. package/types/jest-global.d.ts +85 -0
  65. package/types/mock-extended.d.ts +52 -0
  66. package/dist/chunk-7PUVK4YM.js.map +0 -1
  67. package/dist/chunk-CPCUPK4J.js.map +0 -1
  68. package/dist/chunk-PWV3BMDA.js.map +0 -1
  69. package/dist/chunk-WLFE5RUU.js.map +0 -1
@@ -0,0 +1,14 @@
1
+ // src/roles/test/setup/install-once.ts
2
+ var INSTALLED = /* @__PURE__ */ Symbol.for("@hublo/sentinel/test/setup/installed");
3
+ function alreadyInstalled(target, name) {
4
+ const marked = target;
5
+ const installed = marked[INSTALLED] ??= /* @__PURE__ */ new Set();
6
+ if (installed.has(name)) return true;
7
+ installed.add(name);
8
+ return false;
9
+ }
10
+
11
+ export {
12
+ alreadyInstalled
13
+ };
14
+ //# sourceMappingURL=chunk-NUOQXAYR.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/roles/test/setup/install-once.ts"],"sourcesContent":["/**\n * The guard every setup installer needs, because a setup file runs ONCE PER TEST FILE.\n *\n * ## The defect this exists to close\n *\n * Vitest re-executes each entry of `setupFiles` for every test file. Under the isolation Vitest\n * gives by default that is harmless: the file also gets a fresh module registry, so the `vi` a\n * setup wraps is a fresh `vi`, and last file's wrapper went away with last file's registry.\n *\n * Without isolation the registry is the worker's, so `vi` is ONE object for every file, and an\n * installer that wraps `vi.fn` wraps last file's wrapper. Measured on `front-components`, 230 test\n * files: the chain reaches 230 deep and the suite dies with\n * `RangeError: Maximum call stack size exceeded`. What makes it worth a named guard rather than a\n * fix at each call site is how it fails. 26 files never ran, and the reporter said\n * `204 passed (204)` with no red: the loss was only visible by comparing the test COUNT against\n * the run before it.\n *\n * `installJestErrorEquality` fails the same way more quietly, adding one more equality tester to\n * `expect` per file rather than overflowing anything.\n *\n * ## Why the mark goes on the target and not on `globalThis`\n *\n * The mark has to answer \"is the object I am about to wrap already wrapped\", and only the object\n * itself can answer that. On `globalThis` the answer would be wrong in the isolated case: Vitest\n * builds a new environment per file, but a `node`-environment worker keeps one global object, so a\n * flag set there would survive into a file whose `vi` is brand new and unwrapped, and the\n * installer would skip the work it still had to do.\n */\n\n/** Stable across copies of this module, which pnpm's layout makes possible. */\nconst INSTALLED = Symbol.for('@hublo/sentinel/test/setup/installed')\n\ntype Marked = Record<symbol, Set<string> | undefined>\n\n/**\n * Records `name` against `target` and says whether it was already there.\n *\n * Written as a question an installer asks on its first line, `if (alreadyInstalled(vi, '...'))\n * return`, so the guarantee travels with the installer instead of depending on every caller\n * remembering it.\n */\nexport function alreadyInstalled(target: object, name: string): boolean {\n const marked = target as Marked\n const installed = (marked[INSTALLED] ??= new Set<string>())\n\n if (installed.has(name)) return true\n\n installed.add(name)\n return false\n}\n"],"mappings":";AA8BA,IAAM,YAAY,uBAAO,IAAI,sCAAsC;AAW5D,SAAS,iBAAiB,QAAgB,MAAuB;AACtE,QAAM,SAAS;AACf,QAAM,YAAa,OAAO,SAAS,MAAM,oBAAI,IAAY;AAEzD,MAAI,UAAU,IAAI,IAAI,EAAG,QAAO;AAEhC,YAAU,IAAI,IAAI;AAClB,SAAO;AACT;","names":[]}
@@ -0,0 +1,38 @@
1
+ // src/shared/glob.ts
2
+ function globToRegExp(glob) {
3
+ let pattern = "";
4
+ for (let index = 0; index < glob.length; index++) {
5
+ const char = glob[index] ?? "";
6
+ if (char === "*") {
7
+ if (glob[index + 1] === "*") {
8
+ pattern += glob[index + 2] === "/" ? "(?:.*/)?" : ".*";
9
+ index += glob[index + 2] === "/" ? 2 : 1;
10
+ } else {
11
+ pattern += "[^/]*";
12
+ }
13
+ continue;
14
+ }
15
+ if (char === "{") {
16
+ const close = glob.indexOf("}", index);
17
+ if (close === -1) throw new Error(`sentinel: unbalanced \`{\` in glob ${glob}`);
18
+ const alternatives = glob.slice(index + 1, close).split(",");
19
+ pattern += `(?:${alternatives.map((one) => one.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})`;
20
+ index = close;
21
+ continue;
22
+ }
23
+ pattern += char.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
24
+ }
25
+ return new RegExp(`^${pattern}$`);
26
+ }
27
+ function matchesTsconfigPattern(pattern, relative) {
28
+ const cleaned = pattern.replace(/^\.\//, "").replace(/\/+$/, "");
29
+ if (globToRegExp(cleaned).test(relative)) return true;
30
+ const asDirectory = cleaned.endsWith("/**") ? cleaned.slice(0, -3) : cleaned;
31
+ return globToRegExp(`${asDirectory}/**/*`).test(relative);
32
+ }
33
+
34
+ export {
35
+ globToRegExp,
36
+ matchesTsconfigPattern
37
+ };
38
+ //# sourceMappingURL=chunk-O7REVMOC.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/shared/glob.ts"],"sourcesContent":["/**\n * The glob dialect a config file uses for a FILE NAME, reduced to what that needs.\n *\n * Shared because two roles read globs out of configuration and neither needs a glob library: the\n * build role reads `@nx/webpack`'s asset declarations, and the test role reads a tsconfig's\n * `include` and `exclude`. A second copy of this would drift from the first, which is a mistake\n * this package has already made once with the binary resolver.\n *\n * Three forms, and anything else is taken literally: `**` for any depth, `*` within a segment, and\n * a `{a,b}` alternation. A pattern that uses something else matches nothing rather than matching\n * by accident, which is the behaviour a caller can check.\n */\nexport function globToRegExp(glob: string): RegExp {\n let pattern = ''\n for (let index = 0; index < glob.length; index++) {\n const char = glob[index] ?? ''\n if (char === '*') {\n if (glob[index + 1] === '*') {\n // `**/` matches any number of directories, including none.\n pattern += glob[index + 2] === '/' ? '(?:.*/)?' : '.*'\n index += glob[index + 2] === '/' ? 2 : 1\n } else {\n pattern += '[^/]*'\n }\n continue\n }\n if (char === '{') {\n const close = glob.indexOf('}', index)\n if (close === -1) throw new Error(`sentinel: unbalanced \\`{\\` in glob ${glob}`)\n const alternatives = glob.slice(index + 1, close).split(',')\n pattern += `(?:${alternatives.map((one) => one.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')).join('|')})`\n index = close\n continue\n }\n pattern += char.replace(/[.+?^${}()|[\\]\\\\]/g, '\\\\$&')\n }\n return new RegExp(`^${pattern}$`)\n}\n\n/**\n * A tsconfig's `include` / `exclude`, applied to a path relative to that config.\n *\n * ⚠️ A tsconfig pattern ending in a DIRECTORY matches everything under it, and one naming no\n * extension matches the TypeScript ones. `src/tests/functional/**` and `src/tests` both have to\n * claim `src/tests/functional/a.spec.ts`, which a plain file-name match does not do.\n */\nexport function matchesTsconfigPattern(pattern: string, relative: string): boolean {\n const cleaned = pattern.replace(/^\\.\\//, '').replace(/\\/+$/, '')\n if (globToRegExp(cleaned).test(relative)) return true\n // A directory, named with or without a trailing `/**`, claims everything beneath it.\n const asDirectory = cleaned.endsWith('/**') ? cleaned.slice(0, -3) : cleaned\n return globToRegExp(`${asDirectory}/**/*`).test(relative)\n}\n"],"mappings":";AAYO,SAAS,aAAa,MAAsB;AACjD,MAAI,UAAU;AACd,WAAS,QAAQ,GAAG,QAAQ,KAAK,QAAQ,SAAS;AAChD,UAAM,OAAO,KAAK,KAAK,KAAK;AAC5B,QAAI,SAAS,KAAK;AAChB,UAAI,KAAK,QAAQ,CAAC,MAAM,KAAK;AAE3B,mBAAW,KAAK,QAAQ,CAAC,MAAM,MAAM,aAAa;AAClD,iBAAS,KAAK,QAAQ,CAAC,MAAM,MAAM,IAAI;AAAA,MACzC,OAAO;AACL,mBAAW;AAAA,MACb;AACA;AAAA,IACF;AACA,QAAI,SAAS,KAAK;AAChB,YAAM,QAAQ,KAAK,QAAQ,KAAK,KAAK;AACrC,UAAI,UAAU,GAAI,OAAM,IAAI,MAAM,sCAAsC,IAAI,EAAE;AAC9E,YAAM,eAAe,KAAK,MAAM,QAAQ,GAAG,KAAK,EAAE,MAAM,GAAG;AAC3D,iBAAW,MAAM,aAAa,IAAI,CAAC,QAAQ,IAAI,QAAQ,uBAAuB,MAAM,CAAC,EAAE,KAAK,GAAG,CAAC;AAChG,cAAQ;AACR;AAAA,IACF;AACA,eAAW,KAAK,QAAQ,sBAAsB,MAAM;AAAA,EACtD;AACA,SAAO,IAAI,OAAO,IAAI,OAAO,GAAG;AAClC;AASO,SAAS,uBAAuB,SAAiB,UAA2B;AACjF,QAAM,UAAU,QAAQ,QAAQ,SAAS,EAAE,EAAE,QAAQ,QAAQ,EAAE;AAC/D,MAAI,aAAa,OAAO,EAAE,KAAK,QAAQ,EAAG,QAAO;AAEjD,QAAM,cAAc,QAAQ,SAAS,KAAK,IAAI,QAAQ,MAAM,GAAG,EAAE,IAAI;AACrE,SAAO,aAAa,GAAG,WAAW,OAAO,EAAE,KAAK,QAAQ;AAC1D;","names":[]}
@@ -0,0 +1,16 @@
1
+ // src/shared/jsonc.ts
2
+ import { parse, printParseErrorCode } from "jsonc-parser";
3
+ function parseJsonc(text, source = "config") {
4
+ const errors = [];
5
+ const value = parse(text, errors, { allowTrailingComma: true });
6
+ if (errors.length > 0) {
7
+ const details = errors.map((error) => printParseErrorCode(error.error)).join(", ");
8
+ throw new Error(`${source}: malformed JSONC (${details}).`);
9
+ }
10
+ return value;
11
+ }
12
+
13
+ export {
14
+ parseJsonc
15
+ };
16
+ //# sourceMappingURL=chunk-QXFCZON7.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/shared/jsonc.ts"],"sourcesContent":["/**\n * JSONC (JSON with comments + trailing commas) helpers. tsconfig files are JSONC,\n * so reading them with plain `JSON.parse` throws on real projects (e.g.\n * host-admin's tsconfig.app.json has comments). Backed by jsonc-parser (the VS\n * Code library), which also underpins content-preserving edits (added with the\n * `--init` merge).\n */\nimport { parse, printParseErrorCode, type ParseError } from 'jsonc-parser'\n\n/**\n * Parse JSONC text into a value. Throws with a clear message listing the parse\n * errors, so a malformed config fails loudly rather than silently mis-reading.\n */\nexport function parseJsonc<T = unknown>(text: string, source = 'config'): T {\n const errors: ParseError[] = []\n const value = parse(text, errors, { allowTrailingComma: true }) as T\n if (errors.length > 0) {\n const details = errors.map((error) => printParseErrorCode(error.error)).join(', ')\n throw new Error(`${source}: malformed JSONC (${details}).`)\n }\n return value\n}\n"],"mappings":";AAOA,SAAS,OAAO,2BAA4C;AAMrD,SAAS,WAAwB,MAAc,SAAS,UAAa;AAC1E,QAAM,SAAuB,CAAC;AAC9B,QAAM,QAAQ,MAAM,MAAM,QAAQ,EAAE,oBAAoB,KAAK,CAAC;AAC9D,MAAI,OAAO,SAAS,GAAG;AACrB,UAAM,UAAU,OAAO,IAAI,CAAC,UAAU,oBAAoB,MAAM,KAAK,CAAC,EAAE,KAAK,IAAI;AACjF,UAAM,IAAI,MAAM,GAAG,MAAM,sBAAsB,OAAO,IAAI;AAAA,EAC5D;AACA,SAAO;AACT;","names":[]}
@@ -1,7 +1,11 @@
1
+ import {
2
+ parseJsonc
3
+ } from "./chunk-QXFCZON7.js";
4
+
1
5
  // src/roles/build/nest/decorator-metadata.ts
2
- import { existsSync } from "fs";
3
- import { createRequire } from "module";
4
- import path from "path";
6
+ import { existsSync } from "node:fs";
7
+ import { createRequire } from "node:module";
8
+ import path from "node:path";
5
9
  var TYPESCRIPT_SOURCE = /\.ts$/;
6
10
  var TSCONFIG_CANDIDATES = ["tsconfig.app.json", "tsconfig.json"];
7
11
  function loadCompiler(root) {
@@ -152,9 +156,8 @@ var decoratorMetadata = (options) => {
152
156
  };
153
157
 
154
158
  // src/roles/build/nest/tsconfig-aliases.ts
155
- import { readFileSync } from "fs";
156
- import { join } from "path";
157
- var stripLineComments = (json) => json.replace(/^\s*\/\/.*$/gm, "");
159
+ import { readFileSync } from "node:fs";
160
+ import { join } from "node:path";
158
161
  var toAlias = (workspaceRoot, pattern, target) => {
159
162
  const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\\\*/g, "(.*)");
160
163
  return {
@@ -164,7 +167,7 @@ var toAlias = (workspaceRoot, pattern, target) => {
164
167
  };
165
168
  var tsconfigAliases = (workspaceRoot, file = "tsconfig.base.json") => {
166
169
  const raw = readFileSync(join(workspaceRoot, file), "utf8");
167
- const { compilerOptions } = JSON.parse(stripLineComments(raw));
170
+ const { compilerOptions } = parseJsonc(raw, file);
168
171
  return Object.entries(compilerOptions?.paths ?? {}).flatMap(([pattern, targets]) => {
169
172
  const [target] = targets;
170
173
  return target === void 0 ? [] : [toAlias(workspaceRoot, pattern, target)];
@@ -175,4 +178,4 @@ export {
175
178
  decoratorMetadata,
176
179
  tsconfigAliases
177
180
  };
178
- //# sourceMappingURL=chunk-3TDUIKVQ.js.map
181
+ //# sourceMappingURL=chunk-SWWQ7X7B.js.map
@@ -1 +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"]}
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\nimport { parseJsonc } from '../../../shared/jsonc.js'\n\nexport interface Alias {\n find: RegExp\n replacement: string\n}\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 /*\n * ⚠️ JSONC, through the shared reader. This stripped `//` comments by hand and then called\n * `JSON.parse`, which handled a comment on its OWN line and nothing else: a trailing comment\n * after a value, a block comment, or a trailing comma, all legal in a tsconfig, would throw a\n * raw SyntaxError naming a character offset. Latent on this repo's base config today, and\n * `shared/jsonc.ts` already existed for exactly this.\n */\n const raw = readFileSync(join(workspaceRoot, file), 'utf8')\n const { compilerOptions } = parseJsonc<{\n compilerOptions?: { paths?: Record<string, string[]> }\n }>(raw, file)\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"]}
@@ -1,10 +1,11 @@
1
+ import {
2
+ fromModule
3
+ } from "./chunk-ELZHIN6E.js";
1
4
  import {
2
5
  findWorkspaceRoot
3
- } from "./chunk-WLFE5RUU.js";
6
+ } from "./chunk-KMKQDGI6.js";
4
7
 
5
8
  // src/roles/test/setup/workspace.ts
6
- import { createRequire } from "module";
7
- import { join } from "path";
8
9
  function isMissingModule(error) {
9
10
  return error?.code === "ERR_MODULE_NOT_FOUND";
10
11
  }
@@ -18,14 +19,6 @@ async function optional(specifier, use) {
18
19
  if (!isMissingModule(error)) throw error;
19
20
  }
20
21
  }
21
- function fromModule(specifier) {
22
- try {
23
- const require2 = createRequire(join(process.cwd(), "noop.cjs"));
24
- return require2(specifier);
25
- } catch {
26
- return void 0;
27
- }
28
- }
29
22
  async function loadEnvironment() {
30
23
  await optional("dotenv-flow", (module) => {
31
24
  const flow = module;
@@ -67,4 +60,4 @@ export {
67
60
  installWorkspaceSetup,
68
61
  pinWorkspaceTimezone
69
62
  };
70
- //# sourceMappingURL=chunk-CPCUPK4J.js.map
63
+ //# sourceMappingURL=chunk-Z7L4FGKP.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 */\n\nimport { findWorkspaceRoot } from '../../../core/workspace-prep.js'\n\nimport { fromModule } from './module-copy.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 * `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":";;;;;;;;AA8BA,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;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":[]}
package/dist/index.d.ts CHANGED
@@ -11,12 +11,25 @@ declare function registerAdapters(): void;
11
11
  * `settings.ts`, not here.
12
12
  */
13
13
  /**
14
- * Verbs: what to do. `init` writes a module's config + the workspace prep it needs (the
15
- * one-time setup); the read verbs (run/inspect/report/status) execute or introspect.
16
- * `migrate` is reserved for changing an already-initialized setup and is not wired yet
17
- * (the CLI lists it as planned and refuses it).
14
+ * Verbs: what to do.
15
+ *
16
+ * The axis is not "who runs it" but **what survives the run**, which is the distinction that
17
+ * outlives any sharing of the work:
18
+ *
19
+ * | verb | writes | what remains |
20
+ * | --- | --- | --- |
21
+ * | `init` | a module's config + the workspace prep it needs | the module's new state, that IS the deliverable |
22
+ * | `migrate` | changes an already-initialized setup | same; reserved, not wired yet |
23
+ * | `validate` | instrumentation, if it needs any | **nothing**, the project returns to what it was |
24
+ * | `run` / `inspect` / `report` / `status` | nothing | they execute or introspect |
25
+ *
26
+ * `validate` is the one that judges. Héla, 2026-09-25: it does not carry the migration's
27
+ * strictness, so it MAY write into the project under validation, a `data-testid` to make something
28
+ * addressable for instance, provided it takes it back out. That freedom is what makes its
29
+ * dependability the thing to design: idempotent, reversible on error, recoverable after a kill,
30
+ * and never silent about instrumentation it failed to remove.
18
31
  */
19
- declare const VERBS: readonly ["run", "inspect", "init", "migrate", "report", "status"];
32
+ declare const VERBS: readonly ["run", "inspect", "init", "migrate", "validate", "report", "status"];
20
33
  type Verb = (typeof VERBS)[number];
21
34
  /** Targets: the kind of check. The CLI `--<target>` flags map 1:1 to these. */
22
35
  declare const TARGETS: readonly ["lint", "format", "typescript", "build", "dev", "test", "static-analysis", "runtime-analysis", "arch"];
@@ -66,6 +79,16 @@ interface RunContext {
66
79
  ci: boolean;
67
80
  /** Apply auto-fixes where the tool supports it. */
68
81
  fix: boolean;
82
+ /**
83
+ * The files `--init` just wrote, relative to `cwd`, for an `afterInit` that tidies up after
84
+ * itself.
85
+ *
86
+ * ⚠️ It exists because the alternative is a whole-directory pass, and that is not a tidy-up.
87
+ * Measured on `apps/nest/microservices/institution`: an `afterInit` running its formatter over
88
+ * `.` reformatted 1813 files, 47333 insertions and 55506 deletions, in a module whose migration
89
+ * touched 268. Only set by the `--init` path, so an adapter must treat it as optional.
90
+ */
91
+ written?: string[];
69
92
  /**
70
93
  * Cap on the number of structured diagnostics a `--report` embeds per module (the
71
94
  * full count still lives in `errors`, so nothing is hidden silently). `0` means no
package/dist/index.js CHANGED
@@ -1,5 +1,4 @@
1
1
  import {
2
- BaseAdapter,
3
2
  all,
4
3
  detectFramework,
5
4
  dispatch,
@@ -7,8 +6,13 @@ import {
7
6
  registerAdapters,
8
7
  resolve,
9
8
  setDefaultRunner
10
- } from "./chunk-QP37G2HO.js";
11
- import "./chunk-WLFE5RUU.js";
9
+ } from "./chunk-MWNOFSYR.js";
10
+ import {
11
+ BaseAdapter
12
+ } from "./chunk-BXXNV6NP.js";
13
+ import "./chunk-O7REVMOC.js";
14
+ import "./chunk-QXFCZON7.js";
15
+ import "./chunk-KMKQDGI6.js";
12
16
  export {
13
17
  BaseAdapter,
14
18
  all,
@@ -1,41 +1,19 @@
1
+ import {
2
+ globToRegExp
3
+ } from "../../../chunk-O7REVMOC.js";
1
4
  import {
2
5
  decoratorMetadata,
3
6
  tsconfigAliases
4
- } from "../../../chunk-3TDUIKVQ.js";
7
+ } from "../../../chunk-SWWQ7X7B.js";
8
+ import "../../../chunk-QXFCZON7.js";
5
9
 
6
10
  // src/roles/build/nest/nest-service.ts
7
- import { join as join3 } from "path";
11
+ import { join as join3 } from "node:path";
8
12
  import { mergeConfig } from "vite";
9
13
 
10
14
  // src/roles/build/nest/copy-assets.ts
11
- import { cpSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
12
- import { dirname, join, relative, sep } from "path";
13
- function globToRegExp(glob) {
14
- let pattern = "";
15
- for (let index = 0; index < glob.length; index++) {
16
- const char = glob[index] ?? "";
17
- if (char === "*") {
18
- if (glob[index + 1] === "*") {
19
- pattern += glob[index + 2] === "/" ? "(?:.*/)?" : ".*";
20
- index += glob[index + 2] === "/" ? 2 : 1;
21
- } else {
22
- pattern += "[^/]*";
23
- }
24
- continue;
25
- }
26
- if (char === "{") {
27
- const close = glob.indexOf("}", index);
28
- if (close === -1)
29
- throw new Error(`sentinel build(nest): unbalanced \`{\` in asset glob ${glob}`);
30
- const alternatives = glob.slice(index + 1, close).split(",");
31
- pattern += `(?:${alternatives.map((one) => one.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})`;
32
- index = close;
33
- continue;
34
- }
35
- pattern += char.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
36
- }
37
- return new RegExp(`^${pattern}$`);
38
- }
15
+ import { cpSync, existsSync, mkdirSync, readdirSync, statSync } from "node:fs";
16
+ import { dirname, join, relative, sep } from "node:path";
39
17
  function filesUnder(root) {
40
18
  const found = [];
41
19
  const walk = (dir) => {
@@ -82,9 +60,9 @@ var copyAssets = (options) => ({
82
60
  });
83
61
 
84
62
  // src/roles/build/nest/node-manifest.ts
85
- import { existsSync as existsSync2, writeFileSync } from "fs";
86
- import { isBuiltin } from "module";
87
- import { join as join2 } from "path";
63
+ import { existsSync as existsSync2, writeFileSync } from "node:fs";
64
+ import { isBuiltin } from "node:module";
65
+ import { join as join2 } from "node:path";
88
66
  var DEFAULT_PROVIDED_BY_IMAGE = ["@prisma"];
89
67
  var externalPackages = (bundle) => {
90
68
  const emitted = new Set(Object.keys(bundle));
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/roles/build/nest/nest-service.ts","../../../../src/roles/build/nest/copy-assets.ts","../../../../src/roles/build/nest/node-manifest.ts","../../../../src/roles/build/nest/toolchain.ts"],"sourcesContent":["/**\n * The whole Vite config for a Nest service, so an adopted module holds two lines.\n *\n * ## Why a shape here, and pieces on the React side\n *\n * It looks inconsistent until you measure the two families. The four React apps each carry a\n * heavy, genuinely different config: one has a plugin wedged between two shared ones, another\n * has two different route-tree footers on the branches of one ternary. Two attempts at a\n * shared shape for them were abandoned, and the rule that survived is that sentinel rewrites\n * their IMPORTS and touches nothing else.\n *\n * The Nest side is the opposite by measurement: 38 services, one shared 25-line webpack config\n * between them, and nothing per-module to preserve. Owning the shape there is not a different\n * philosophy, it is the same one applied to a different fact: sentinel provides the FORM where\n * the form is shared, and the PIECES where it is not.\n *\n * The consequences are the point. An adopted module is readable in review. A correction to the\n * form is one publish instead of 38 diffs. And the day Nest becomes ESM-native, `format`\n * changes here rather than in 38 configs.\n *\n * ## It is a composition, not a block\n *\n * `nestService` assembles the same plugins this module exports individually. A service that\n * needs something else can pass `overrides`, or drop to the pieces entirely. And if the React\n * configs ever converge, a `reactApp()` can be built the same way with no new mechanism.\n */\nimport { join } from 'node:path'\n\nimport { mergeConfig, type UserConfig } from 'vite'\n\nimport { copyAssets } from './copy-assets.js'\nimport { decoratorMetadata } from './decorator-metadata.js'\nimport { nodeManifest } from './node-manifest.js'\nimport type { AssetDeclaration, TransformerDeclaration } from './nx-target.js'\nimport { tsconfigAliases } from './tsconfig-aliases.js'\n\nexport interface NestServiceOptions {\n /** The nx project name. Keys the dependency graph, and names the module in messages. */\n project: string\n /** Absolute path to the module's directory, normally `__dirname`. */\n root: string\n /** Absolute path to the workspace root. */\n workspaceRoot: string\n /** Entry point, relative to `root`. */\n entry?: string\n /**\n * The tsconfig whose emit must be preserved, when the service uses neither conventional name.\n *\n * Relative to `root`. Defaults to `tsconfig.app.json`, then `tsconfig.json`.\n */\n tsconfig?: string\n /** Output directory, relative to `workspaceRoot`. */\n outDir?: string\n /**\n * TypeScript transformers this service compiles with, as its build target declared them.\n *\n * Read off the webpack target by `--init` and written here, never inferred: what a service\n * compiles with is the service's own. Eight services and BFFs run `@nestjs/swagger/plugin`,\n * which writes the `@ApiProperty` decorators their DTOs do not declare by hand. Building\n * without it succeeds and silently costs them most of their published contract (measured on\n * `institution`: 385 insertions, 1378 deletions in the committed OpenAPI document).\n */\n transformers?: readonly TransformerDeclaration[]\n /**\n * Files copied into the output beside the bundle, in the shape `@nx/webpack` declared them.\n *\n * One service here uses it: `planning-period` ships the pug, css and ttf templates it renders\n * printed documents from. Without them the service starts and fails on its first print.\n */\n assets?: readonly AssetDeclaration[]\n /**\n * Anything this service needs that the shape does not give it.\n *\n * Merged with Vite's own `mergeConfig`, never with a merge of our own: plugins concatenate\n * and aliases stack the way Vite does it everywhere else, so there is no second set of\n * semantics to learn or to document.\n */\n overrides?: UserConfig\n}\n\nconst baseConfig = (options: NestServiceOptions): UserConfig => {\n const outDir = join(options.workspaceRoot, options.outDir ?? `dist/${options.project}`)\n const entry = join(options.root, options.entry ?? 'src/main.ts')\n return {\n plugins: [\n decoratorMetadata({\n root: options.root,\n tsconfig: options.tsconfig,\n transformers: options.transformers,\n }),\n ...(options.assets?.length\n ? [copyAssets({ workspaceRoot: options.workspaceRoot, outDir, assets: options.assets })]\n : []),\n nodeManifest({\n project: options.project,\n workspaceRoot: options.workspaceRoot,\n outDir,\n }),\n ],\n resolve: { alias: tsconfigAliases(options.workspaceRoot) },\n ssr: {\n // `importHelpers: true` in the workspace tsconfig makes TypeScript emit `require(\"tslib\")`\n // for `__decorate` and `__metadata`, so every decorated file depends on it. Hoisted into\n // one scope Rollup inlined it and nobody noticed; preserving modules left it external, and\n // the manifest nx generates from the project graph does not list it, because no module\n // DECLARES tslib. The image therefore did not install it and the service died on its first\n // require, only in the image: locally and in CI the workspace root has tslib.\n noExternal: ['tslib'],\n },\n build: {\n // An SSR build targets Node and leaves real packages external, which is what\n // `webpack-node-externals` did: the image installs them from the generated manifest.\n ssr: entry,\n outDir,\n emptyOutDir: true,\n sourcemap: true,\n target: 'node20',\n // The bundle is read by humans when a stack trace points into it, and minifying a\n // server bundle buys nothing: it is never downloaded.\n minify: false,\n rollupOptions: {\n output: {\n format: 'cjs',\n // One output file per source module, instead of hoisting everything into one scope.\n //\n // This is not a preference, it is the only shape that keeps the OpenAPI contract.\n // Merging every module into one scope forces Rollup to rename duplicate class names,\n // and `@nestjs/swagger` keys its schemas on `class.name`, so a renamed class silently\n // becomes a renamed schema in the published API. Measured on `network`, which has two\n // such collisions: single bundle renames `InvalidPermissionError` and\n // `PermissionNotFoundError`; preserving modules renames nothing and reproduces the\n // committed contract byte for byte.\n //\n // The cost is 4.5 MB across 2003 files against 2.9 MB in one, still well under\n // webpack's 6.9 MB for the same service, so nothing regresses against what it replaces.\n preserveModules: true,\n // The image runs `./dist/main.js`, so the entry keeps that name at the root while\n // every other module keeps its own path. Under `preserveModules` a plain string here\n // would be applied to all of them, which numbers them (`main962.js`) and loses the\n // entry.\n entryFileNames: (chunk) => (chunk.facadeModuleId === entry ? 'main.js' : '[name].js'),\n },\n // Nest registers metadata as an IMPORT SIDE EFFECT: a decorator writes into a catalog\n // when its module loads, and nothing references that module afterwards. Rollup may\n // drop such a module; webpack never did.\n //\n // Measured on the first migrated service this changes nothing, so it is insurance\n // rather than a fix, and it is recorded as such rather than credited with the smaller\n // bundle (that comes from barrel re-exports the service does not use).\n treeshake: { moduleSideEffects: true },\n },\n },\n }\n}\n\nexport const nestService = (options: NestServiceOptions): UserConfig =>\n options.overrides === undefined\n ? baseConfig(options)\n : mergeConfig(baseConfig(options), options.overrides)\n","/**\n * The files a service ships beside its bundle, which webpack copied and a bundler does not.\n *\n * `@nx/webpack` takes an `assets` list and copies each match into the output. Nothing in a Vite\n * build does that for a Node target: `publicDir` is a browser concept and is disabled for SSR\n * builds, so an unmigrated `assets` entry is not a smaller output, it is a service that starts\n * and then fails the first time it reaches for a file that is not there.\n *\n * One service in this repo declares it. `planning-period` ships the pug templates, stylesheet and\n * font it renders printed documents from, under `templates/`. Dropping them leaves a build that\n * succeeds, an image that boots, and a printing feature that throws.\n *\n * ## Copied at `closeBundle`, and only what the target declared\n *\n * The declaration is carried through from the webpack target unchanged, globs and all, so this\n * plugin has nothing to decide: it resolves each `input` against the workspace root, matches the\n * glob, and writes under `output` inside the bundle's directory. `closeBundle` because that is\n * when the output directory exists and Vite has finished emptying it.\n *\n * An entry matching nothing FAILS the build. A glob that has quietly stopped matching is the same\n * silent loss as no glob at all, and the point of this file is that this class of loss is loud.\n */\nimport { cpSync, existsSync, mkdirSync, readdirSync, statSync } from 'node:fs'\nimport { dirname, join, relative, sep } from 'node:path'\n\nimport type { Plugin } from 'vite'\n\nimport type { AssetDeclaration } from './nx-target.js'\n\nexport interface CopyAssetsOptions {\n /** Absolute path to the workspace root, which `input` is relative to. */\n workspaceRoot: string\n /** Absolute path to the build's output directory. */\n outDir: string\n assets: readonly AssetDeclaration[]\n}\n\n/**\n * `@nx/webpack`'s glob dialect, reduced to what it means for a file name.\n *\n * Only the forms the declarations here use are supported, and anything else is rejected at build\n * time rather than silently matching nothing: `**` for any depth, `*` within a segment, and a\n * `{a,b}` alternation. Written out rather than pulled from a glob library because the whole\n * dependency would exist to read one line of configuration.\n */\nfunction globToRegExp(glob: string): RegExp {\n let pattern = ''\n for (let index = 0; index < glob.length; index++) {\n const char = glob[index] ?? ''\n if (char === '*') {\n if (glob[index + 1] === '*') {\n // `**/` matches any number of directories, including none.\n pattern += glob[index + 2] === '/' ? '(?:.*/)?' : '.*'\n index += glob[index + 2] === '/' ? 2 : 1\n } else {\n pattern += '[^/]*'\n }\n continue\n }\n if (char === '{') {\n const close = glob.indexOf('}', index)\n if (close === -1)\n throw new Error(`sentinel build(nest): unbalanced \\`{\\` in asset glob ${glob}`)\n const alternatives = glob.slice(index + 1, close).split(',')\n pattern += `(?:${alternatives.map((one) => one.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')).join('|')})`\n index = close\n continue\n }\n pattern += char.replace(/[.+?^${}()|[\\]\\\\]/g, '\\\\$&')\n }\n return new RegExp(`^${pattern}$`)\n}\n\n/** Every file under `root`, as paths relative to it and with forward slashes. */\nfunction filesUnder(root: string): string[] {\n const found: string[] = []\n const walk = (dir: string): void => {\n for (const entry of readdirSync(dir)) {\n const full = join(dir, entry)\n if (statSync(full).isDirectory()) walk(full)\n else found.push(relative(root, full).split(sep).join('/'))\n }\n }\n walk(root)\n return found\n}\n\n/** What each declaration copies, or the reason it cannot. */\nexport function resolveAssets(options: CopyAssetsOptions): { from: string; to: string }[] {\n const copies: { from: string; to: string }[] = []\n for (const asset of options.assets) {\n const input = join(options.workspaceRoot, asset.input)\n if (!existsSync(input)) {\n throw new Error(\n `sentinel build(nest): the build target declares an asset from \\`${asset.input}\\`, which ` +\n `does not exist. webpack copied it into the output, so the built service would be ` +\n `missing files it reads at runtime.`,\n )\n }\n const matcher = globToRegExp(asset.glob)\n const matched = filesUnder(input).filter((file) => matcher.test(file))\n if (matched.length === 0) {\n throw new Error(\n `sentinel build(nest): the asset glob \\`${asset.glob}\\` under \\`${asset.input}\\` matches ` +\n `no file. An entry that matches nothing is the same silent loss as no entry at all.`,\n )\n }\n for (const file of matched) {\n copies.push({ from: join(input, file), to: join(options.outDir, asset.output, file) })\n }\n }\n return copies\n}\n\nexport const copyAssets = (options: CopyAssetsOptions): Plugin => ({\n name: 'sentinel:copy-assets',\n // After the bundle is written and the output directory has stopped being emptied.\n closeBundle() {\n for (const { from, to } of resolveAssets(options)) {\n mkdirSync(dirname(to), { recursive: true })\n cpSync(from, to)\n }\n },\n})\n","/**\n * The pruned `package.json` and lockfile a Node image installs from.\n *\n * This is the half that made the nx executor look load-bearing. `--generatePackageJson` was an\n * option of `@nx/webpack:webpack`, and the backend image is built on its two outputs:\n *\n * COPY --from=build /app/dist/apps/nest/<service>/package.json /app/package.json\n * COPY --from=build /app/dist/apps/nest/<service>/pnpm-lock.yaml /app/pnpm-lock.yaml\n * RUN pnpm i --prod=true --frozen-lockfile\n *\n * It is not bound to the executor. The executor supplies one thing, the project graph, and\n * every function that does the work is exported by `@nx/js`. So the same two files can be\n * produced from a Vite plugin, and `--frozen-lockfile` accepting them afterwards is the proof\n * that they agree with each other: pnpm refuses the install otherwise.\n *\n * ## Why the graph, and not something simpler\n *\n * A Nest service here declares almost nothing of its own: measured, one dependency in its\n * manifest against 1463 installed in its image. Everything it uses is declared at the\n * workspace root and reaches it through source imports. So the dependency list can only be\n * computed from what the code imports, which is what the graph knows.\n *\n * Deriving it from the bundle's own externals is not equivalent either: measured on the first\n * service, the bundle statically requires 42 packages while the correct manifest holds 65. The\n * 23 missing ones are transitive or required dynamically at runtime (`fastify` behind\n * `@nestjs/platform-fastify`, `jsonwebtoken`, the LaunchDarkly SDK). Shipping the short list\n * fails in production, not in the build.\n */\nimport { existsSync, writeFileSync } from 'node:fs'\nimport { isBuiltin } from 'node:module'\nimport { join } from 'node:path'\n\nimport type { Plugin, Rollup } from 'vite'\n\nexport interface NodeManifestOptions {\n /** The nx project name, which is how the graph is keyed. */\n project: string\n /** Absolute path to the workspace root. */\n workspaceRoot: string\n /** Where the bundle is written; the two files land beside it. */\n outDir: string\n /** Entry file name, written as `main` so `node .` resolves inside the image. */\n entry?: string\n /**\n * Packages the IMAGE provides, which are therefore allowed to be required without being\n * declared in the manifest.\n *\n * The generated Prisma clients are the case this exists for: the Dockerfile copies\n * `node_modules/@prisma` from its own stage, so they resolve at runtime while no module\n * declares them. Everything else that is required and undeclared is a bug, and the check\n * below refuses the build.\n */\n providedByImage?: readonly string[]\n}\n\n/** What the Dockerfile copies in itself, so requiring it without declaring it is legitimate. */\nconst DEFAULT_PROVIDED_BY_IMAGE = ['@prisma'] as const\n\n/**\n * Every real package the emitted output requires, from Rollup's own record rather than a scan\n * of the text.\n *\n * `chunk.imports` mixes three things, and only the third is a dependency:\n *\n * - other EMITTED chunks. Under `preserveModules` these are relative output paths that do not\n * begin with `.`, so `apps/…`, `libs/…` and `_virtual/…` read exactly like package names.\n * They are recognised by being keys of the bundle itself.\n * - Node BUILTINS, which appear both bare (`crypto`) and prefixed (`node:crypto`).\n * - genuine external packages, which is what the manifest has to cover.\n */\nconst externalPackages = (bundle: Rollup.OutputBundle): Set<string> => {\n const emitted = new Set(Object.keys(bundle))\n const packages = new Set<string>()\n\n for (const emittedFile of Object.values(bundle)) {\n // Assets carry no imports, and the bundle holds both. Narrowing on the discriminant is what\n // replaced a cast through `unknown`, which asserted the same thing without checking it.\n if (emittedFile.type !== 'chunk') continue\n\n for (const imported of emittedFile.imports) {\n if (emitted.has(imported)) continue\n if (imported.startsWith('.') || imported.startsWith('/')) continue\n if (isBuiltin(imported)) continue\n\n const parts = imported.split('/')\n const name = imported.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0]\n if (name) packages.add(name)\n }\n }\n return packages\n}\n\n/**\n * Writes the manifest and lockfile once the bundle is on disk.\n *\n * `closeBundle` rather than `writeBundle`, so the files land after Vite has finished with the\n * directory and cannot be cleared by `emptyOutDir`.\n *\n * `closeBundle` also runs after a FAILED build, where there is no directory to write into. Left\n * alone this hook then threw `ENOENT` on the manifest, and since it is the last error raised it\n * became the one Vite printed, hiding the failure that actually stopped the build. It cost two\n * diagnoses before being recognised, so the missing directory is now read as what it is: the\n * bundle was never written, and this plugin has nothing to say about why.\n */\nexport const nodeManifest = (options: NodeManifestOptions): Plugin => {\n /*\n * Captured while the bundle still exists in memory, and read back once the manifest is built.\n *\n * `undefined` until `generateBundle` runs, deliberately: an empty SET would make the coverage\n * check below pass by having seen nothing, which is the worst way for a guard to succeed. The\n * two states have to be told apart.\n */\n let required: Set<string> | undefined\n\n return {\n name: 'sentinel:node-manifest',\n apply: 'build',\n\n generateBundle(_outputOptions, bundle) {\n required = externalPackages(bundle)\n },\n\n async closeBundle() {\n // `closeBundle` runs after a FAILED build too. The ENTRY is what says a bundle was really\n // written: `emptyOutDir` leaves the directory in place, so its existence alone would let a\n // failed build still produce a manifest describing nothing.\n const entry = options.entry ?? 'main.js'\n if (!existsSync(join(options.outDir, entry))) return\n\n // Imported here rather than at module scope: a front config importing this file must not\n // pay for loading the nx graph machinery it will never call.\n const { createProjectGraphAsync } = await import('nx/src/devkit-exports.js')\n const { createPackageJson, createLockFile, getLockFileName } = await import('@nx/js')\n\n const graph = await createProjectGraphAsync({ exitOnError: false })\n const manifest = createPackageJson(options.project, graph, {\n root: options.workspaceRoot,\n isProduction: true,\n })\n manifest.main = entry\n\n if (required === undefined) {\n throw new Error(\n `sentinel build(nest): the bundle was written but never inspected, so the manifest ` +\n `could not be checked against what it requires. Refusing to write one that might ` +\n `not install.`,\n )\n }\n assertManifestCovers(required, manifest, options)\n\n writeFileSync(join(options.outDir, 'package.json'), `${JSON.stringify(manifest, null, 2)}\\n`)\n writeFileSync(\n join(options.outDir, getLockFileName('pnpm')),\n createLockFile(manifest, graph, 'pnpm'),\n )\n },\n }\n}\n\n/**\n * Refuse a bundle that requires something the image will not install.\n *\n * The manifest is derived from the project GRAPH, so it lists what modules declare. The bundle\n * requires what the bundler left external. Those two sets agreeing is an assumption, and it was\n * wrong: `tslib` arrives through `importHelpers` in the workspace tsconfig without any module\n * declaring it, so the manifest omitted it and the service died on its first require.\n *\n * It died only in the image. Locally and in CI the workspace root has the package, so the gap is\n * invisible until a container starts, which is the most expensive place to discover it. Checking\n * here turns that into a build failure naming the package.\n */\nconst assertManifestCovers = (\n required: ReadonlySet<string>,\n manifest: { dependencies?: Record<string, string> },\n options: NodeManifestOptions,\n): void => {\n const declared = new Set(Object.keys(manifest.dependencies ?? {}))\n const provided = options.providedByImage ?? DEFAULT_PROVIDED_BY_IMAGE\n const missing = [...required]\n .filter((name) => !declared.has(name))\n .filter((name) => !provided.some((prefix) => name === prefix || name.startsWith(`${prefix}/`)))\n .sort()\n\n if (missing.length === 0) return\n\n throw new Error(\n `sentinel build(nest): the bundle requires ${missing.length} package(s) the generated ` +\n `manifest does not declare, so the image would not install them: ${missing.join(', ')}. ` +\n `Either declare them in the module's package.json, inline them with ` +\n `\\`ssr.noExternal\\`, or list them in \\`providedByImage\\` if the Dockerfile copies them in.`,\n )\n}\n","/**\n * What `@hublo/sentinel/build/nest` gives an adopted service.\n *\n * The sibling `build/react` entry is a re-export surface: the module keeps its own config and\n * only its import lines move. This one carries a shape as well, because 38 Nest services share\n * one config and have nothing of their own to preserve. See `nest-service.ts` for why the two\n * answers differ, and why that is one rule rather than two.\n *\n * Its own entry point, like the React one, so importing it at build time never drags the CLI\n * and its adapters into a Vite config.\n */\nexport { nestService, type NestServiceOptions } from './nest-service.js'\n\n// The pieces the shape is built from, exported individually so a service that needs something\n// the shape does not give it can compose its own without leaving sentinel.\nexport { decoratorMetadata, type DecoratorMetadataOptions } from './decorator-metadata.js'\nexport { nodeManifest, type NodeManifestOptions } from './node-manifest.js'\nexport { tsconfigAliases, type Alias } from './tsconfig-aliases.js'\n\n// Vite's own, so an adopted config imports everything from one place and the module can drop\n// `vite` from its dependencies.\nexport { defineConfig, loadEnv, mergeConfig } from 'vite'\nexport type { Plugin, PluginOption, UserConfig, UserConfigExport } from 'vite'\n"],"mappings":"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","names":["join","existsSync","join","join","mergeConfig"]}
1
+ {"version":3,"sources":["../../../../src/roles/build/nest/nest-service.ts","../../../../src/roles/build/nest/copy-assets.ts","../../../../src/roles/build/nest/node-manifest.ts","../../../../src/roles/build/nest/toolchain.ts"],"sourcesContent":["/**\n * The whole Vite config for a Nest service, so an adopted module holds two lines.\n *\n * ## Why a shape here, and pieces on the React side\n *\n * It looks inconsistent until you measure the two families. The four React apps each carry a\n * heavy, genuinely different config: one has a plugin wedged between two shared ones, another\n * has two different route-tree footers on the branches of one ternary. Two attempts at a\n * shared shape for them were abandoned, and the rule that survived is that sentinel rewrites\n * their IMPORTS and touches nothing else.\n *\n * The Nest side is the opposite by measurement: 38 services, one shared 25-line webpack config\n * between them, and nothing per-module to preserve. Owning the shape there is not a different\n * philosophy, it is the same one applied to a different fact: sentinel provides the FORM where\n * the form is shared, and the PIECES where it is not.\n *\n * The consequences are the point. An adopted module is readable in review. A correction to the\n * form is one publish instead of 38 diffs. And the day Nest becomes ESM-native, `format`\n * changes here rather than in 38 configs.\n *\n * ## It is a composition, not a block\n *\n * `nestService` assembles the same plugins this module exports individually. A service that\n * needs something else can pass `overrides`, or drop to the pieces entirely. And if the React\n * configs ever converge, a `reactApp()` can be built the same way with no new mechanism.\n */\nimport { join } from 'node:path'\n\nimport { mergeConfig, type UserConfig } from 'vite'\n\nimport { copyAssets } from './copy-assets.js'\nimport { decoratorMetadata } from './decorator-metadata.js'\nimport { nodeManifest } from './node-manifest.js'\nimport type { AssetDeclaration, TransformerDeclaration } from './nx-target.js'\nimport { tsconfigAliases } from './tsconfig-aliases.js'\n\nexport interface NestServiceOptions {\n /** The nx project name. Keys the dependency graph, and names the module in messages. */\n project: string\n /** Absolute path to the module's directory, normally `__dirname`. */\n root: string\n /** Absolute path to the workspace root. */\n workspaceRoot: string\n /** Entry point, relative to `root`. */\n entry?: string\n /**\n * The tsconfig whose emit must be preserved, when the service uses neither conventional name.\n *\n * Relative to `root`. Defaults to `tsconfig.app.json`, then `tsconfig.json`.\n */\n tsconfig?: string\n /** Output directory, relative to `workspaceRoot`. */\n outDir?: string\n /**\n * TypeScript transformers this service compiles with, as its build target declared them.\n *\n * Read off the webpack target by `--init` and written here, never inferred: what a service\n * compiles with is the service's own. Eight services and BFFs run `@nestjs/swagger/plugin`,\n * which writes the `@ApiProperty` decorators their DTOs do not declare by hand. Building\n * without it succeeds and silently costs them most of their published contract (measured on\n * `institution`: 385 insertions, 1378 deletions in the committed OpenAPI document).\n */\n transformers?: readonly TransformerDeclaration[]\n /**\n * Files copied into the output beside the bundle, in the shape `@nx/webpack` declared them.\n *\n * One service here uses it: `planning-period` ships the pug, css and ttf templates it renders\n * printed documents from. Without them the service starts and fails on its first print.\n */\n assets?: readonly AssetDeclaration[]\n /**\n * Anything this service needs that the shape does not give it.\n *\n * Merged with Vite's own `mergeConfig`, never with a merge of our own: plugins concatenate\n * and aliases stack the way Vite does it everywhere else, so there is no second set of\n * semantics to learn or to document.\n */\n overrides?: UserConfig\n}\n\nconst baseConfig = (options: NestServiceOptions): UserConfig => {\n const outDir = join(options.workspaceRoot, options.outDir ?? `dist/${options.project}`)\n const entry = join(options.root, options.entry ?? 'src/main.ts')\n return {\n plugins: [\n decoratorMetadata({\n root: options.root,\n tsconfig: options.tsconfig,\n transformers: options.transformers,\n }),\n ...(options.assets?.length\n ? [copyAssets({ workspaceRoot: options.workspaceRoot, outDir, assets: options.assets })]\n : []),\n nodeManifest({\n project: options.project,\n workspaceRoot: options.workspaceRoot,\n outDir,\n }),\n ],\n resolve: { alias: tsconfigAliases(options.workspaceRoot) },\n ssr: {\n // `importHelpers: true` in the workspace tsconfig makes TypeScript emit `require(\"tslib\")`\n // for `__decorate` and `__metadata`, so every decorated file depends on it. Hoisted into\n // one scope Rollup inlined it and nobody noticed; preserving modules left it external, and\n // the manifest nx generates from the project graph does not list it, because no module\n // DECLARES tslib. The image therefore did not install it and the service died on its first\n // require, only in the image: locally and in CI the workspace root has tslib.\n noExternal: ['tslib'],\n },\n build: {\n // An SSR build targets Node and leaves real packages external, which is what\n // `webpack-node-externals` did: the image installs them from the generated manifest.\n ssr: entry,\n outDir,\n emptyOutDir: true,\n sourcemap: true,\n target: 'node20',\n // The bundle is read by humans when a stack trace points into it, and minifying a\n // server bundle buys nothing: it is never downloaded.\n minify: false,\n rollupOptions: {\n output: {\n format: 'cjs',\n // One output file per source module, instead of hoisting everything into one scope.\n //\n // This is not a preference, it is the only shape that keeps the OpenAPI contract.\n // Merging every module into one scope forces Rollup to rename duplicate class names,\n // and `@nestjs/swagger` keys its schemas on `class.name`, so a renamed class silently\n // becomes a renamed schema in the published API. Measured on `network`, which has two\n // such collisions: single bundle renames `InvalidPermissionError` and\n // `PermissionNotFoundError`; preserving modules renames nothing and reproduces the\n // committed contract byte for byte.\n //\n // The cost is 4.5 MB across 2003 files against 2.9 MB in one, still well under\n // webpack's 6.9 MB for the same service, so nothing regresses against what it replaces.\n preserveModules: true,\n // The image runs `./dist/main.js`, so the entry keeps that name at the root while\n // every other module keeps its own path. Under `preserveModules` a plain string here\n // would be applied to all of them, which numbers them (`main962.js`) and loses the\n // entry.\n entryFileNames: (chunk) => (chunk.facadeModuleId === entry ? 'main.js' : '[name].js'),\n },\n // Nest registers metadata as an IMPORT SIDE EFFECT: a decorator writes into a catalog\n // when its module loads, and nothing references that module afterwards. Rollup may\n // drop such a module; webpack never did.\n //\n // Measured on the first migrated service this changes nothing, so it is insurance\n // rather than a fix, and it is recorded as such rather than credited with the smaller\n // bundle (that comes from barrel re-exports the service does not use).\n treeshake: { moduleSideEffects: true },\n },\n },\n }\n}\n\nexport const nestService = (options: NestServiceOptions): UserConfig =>\n options.overrides === undefined\n ? baseConfig(options)\n : mergeConfig(baseConfig(options), options.overrides)\n","/**\n * The files a service ships beside its bundle, which webpack copied and a bundler does not.\n *\n * `@nx/webpack` takes an `assets` list and copies each match into the output. Nothing in a Vite\n * build does that for a Node target: `publicDir` is a browser concept and is disabled for SSR\n * builds, so an unmigrated `assets` entry is not a smaller output, it is a service that starts\n * and then fails the first time it reaches for a file that is not there.\n *\n * One service in this repo declares it. `planning-period` ships the pug templates, stylesheet and\n * font it renders printed documents from, under `templates/`. Dropping them leaves a build that\n * succeeds, an image that boots, and a printing feature that throws.\n *\n * ## Copied at `closeBundle`, and only what the target declared\n *\n * The declaration is carried through from the webpack target unchanged, globs and all, so this\n * plugin has nothing to decide: it resolves each `input` against the workspace root, matches the\n * glob, and writes under `output` inside the bundle's directory. `closeBundle` because that is\n * when the output directory exists and Vite has finished emptying it.\n *\n * An entry matching nothing FAILS the build. A glob that has quietly stopped matching is the same\n * silent loss as no glob at all, and the point of this file is that this class of loss is loud.\n */\nimport { cpSync, existsSync, mkdirSync, readdirSync, statSync } from 'node:fs'\nimport { dirname, join, relative, sep } from 'node:path'\n\nimport type { Plugin } from 'vite'\n\nimport { globToRegExp } from '../../../shared/glob.js'\n\nimport type { AssetDeclaration } from './nx-target.js'\n\nexport interface CopyAssetsOptions {\n /** Absolute path to the workspace root, which `input` is relative to. */\n workspaceRoot: string\n /** Absolute path to the build's output directory. */\n outDir: string\n assets: readonly AssetDeclaration[]\n}\n\n/** Every file under `root`, as paths relative to it and with forward slashes. */\nfunction filesUnder(root: string): string[] {\n const found: string[] = []\n const walk = (dir: string): void => {\n for (const entry of readdirSync(dir)) {\n const full = join(dir, entry)\n if (statSync(full).isDirectory()) walk(full)\n else found.push(relative(root, full).split(sep).join('/'))\n }\n }\n walk(root)\n return found\n}\n\n/** What each declaration copies, or the reason it cannot. */\nexport function resolveAssets(options: CopyAssetsOptions): { from: string; to: string }[] {\n const copies: { from: string; to: string }[] = []\n for (const asset of options.assets) {\n const input = join(options.workspaceRoot, asset.input)\n if (!existsSync(input)) {\n throw new Error(\n `sentinel build(nest): the build target declares an asset from \\`${asset.input}\\`, which ` +\n `does not exist. webpack copied it into the output, so the built service would be ` +\n `missing files it reads at runtime.`,\n )\n }\n const matcher = globToRegExp(asset.glob)\n const matched = filesUnder(input).filter((file) => matcher.test(file))\n if (matched.length === 0) {\n throw new Error(\n `sentinel build(nest): the asset glob \\`${asset.glob}\\` under \\`${asset.input}\\` matches ` +\n `no file. An entry that matches nothing is the same silent loss as no entry at all.`,\n )\n }\n for (const file of matched) {\n copies.push({ from: join(input, file), to: join(options.outDir, asset.output, file) })\n }\n }\n return copies\n}\n\nexport const copyAssets = (options: CopyAssetsOptions): Plugin => ({\n name: 'sentinel:copy-assets',\n // After the bundle is written and the output directory has stopped being emptied.\n closeBundle() {\n for (const { from, to } of resolveAssets(options)) {\n mkdirSync(dirname(to), { recursive: true })\n cpSync(from, to)\n }\n },\n})\n","/**\n * The pruned `package.json` and lockfile a Node image installs from.\n *\n * This is the half that made the nx executor look load-bearing. `--generatePackageJson` was an\n * option of `@nx/webpack:webpack`, and the backend image is built on its two outputs:\n *\n * COPY --from=build /app/dist/apps/nest/<service>/package.json /app/package.json\n * COPY --from=build /app/dist/apps/nest/<service>/pnpm-lock.yaml /app/pnpm-lock.yaml\n * RUN pnpm i --prod=true --frozen-lockfile\n *\n * It is not bound to the executor. The executor supplies one thing, the project graph, and\n * every function that does the work is exported by `@nx/js`. So the same two files can be\n * produced from a Vite plugin, and `--frozen-lockfile` accepting them afterwards is the proof\n * that they agree with each other: pnpm refuses the install otherwise.\n *\n * ## Why the graph, and not something simpler\n *\n * A Nest service here declares almost nothing of its own: measured, one dependency in its\n * manifest against 1463 installed in its image. Everything it uses is declared at the\n * workspace root and reaches it through source imports. So the dependency list can only be\n * computed from what the code imports, which is what the graph knows.\n *\n * Deriving it from the bundle's own externals is not equivalent either: measured on the first\n * service, the bundle statically requires 42 packages while the correct manifest holds 65. The\n * 23 missing ones are transitive or required dynamically at runtime (`fastify` behind\n * `@nestjs/platform-fastify`, `jsonwebtoken`, the LaunchDarkly SDK). Shipping the short list\n * fails in production, not in the build.\n */\nimport { existsSync, writeFileSync } from 'node:fs'\nimport { isBuiltin } from 'node:module'\nimport { join } from 'node:path'\n\nimport type { Plugin, Rollup } from 'vite'\n\nexport interface NodeManifestOptions {\n /** The nx project name, which is how the graph is keyed. */\n project: string\n /** Absolute path to the workspace root. */\n workspaceRoot: string\n /** Where the bundle is written; the two files land beside it. */\n outDir: string\n /** Entry file name, written as `main` so `node .` resolves inside the image. */\n entry?: string\n /**\n * Packages the IMAGE provides, which are therefore allowed to be required without being\n * declared in the manifest.\n *\n * The generated Prisma clients are the case this exists for: the Dockerfile copies\n * `node_modules/@prisma` from its own stage, so they resolve at runtime while no module\n * declares them. Everything else that is required and undeclared is a bug, and the check\n * below refuses the build.\n */\n providedByImage?: readonly string[]\n}\n\n/** What the Dockerfile copies in itself, so requiring it without declaring it is legitimate. */\nconst DEFAULT_PROVIDED_BY_IMAGE = ['@prisma'] as const\n\n/**\n * Every real package the emitted output requires, from Rollup's own record rather than a scan\n * of the text.\n *\n * `chunk.imports` mixes three things, and only the third is a dependency:\n *\n * - other EMITTED chunks. Under `preserveModules` these are relative output paths that do not\n * begin with `.`, so `apps/…`, `libs/…` and `_virtual/…` read exactly like package names.\n * They are recognised by being keys of the bundle itself.\n * - Node BUILTINS, which appear both bare (`crypto`) and prefixed (`node:crypto`).\n * - genuine external packages, which is what the manifest has to cover.\n */\nconst externalPackages = (bundle: Rollup.OutputBundle): Set<string> => {\n const emitted = new Set(Object.keys(bundle))\n const packages = new Set<string>()\n\n for (const emittedFile of Object.values(bundle)) {\n // Assets carry no imports, and the bundle holds both. Narrowing on the discriminant is what\n // replaced a cast through `unknown`, which asserted the same thing without checking it.\n if (emittedFile.type !== 'chunk') continue\n\n for (const imported of emittedFile.imports) {\n if (emitted.has(imported)) continue\n if (imported.startsWith('.') || imported.startsWith('/')) continue\n if (isBuiltin(imported)) continue\n\n const parts = imported.split('/')\n const name = imported.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0]\n if (name) packages.add(name)\n }\n }\n return packages\n}\n\n/**\n * Writes the manifest and lockfile once the bundle is on disk.\n *\n * `closeBundle` rather than `writeBundle`, so the files land after Vite has finished with the\n * directory and cannot be cleared by `emptyOutDir`.\n *\n * `closeBundle` also runs after a FAILED build, where there is no directory to write into. Left\n * alone this hook then threw `ENOENT` on the manifest, and since it is the last error raised it\n * became the one Vite printed, hiding the failure that actually stopped the build. It cost two\n * diagnoses before being recognised, so the missing directory is now read as what it is: the\n * bundle was never written, and this plugin has nothing to say about why.\n */\nexport const nodeManifest = (options: NodeManifestOptions): Plugin => {\n /*\n * Captured while the bundle still exists in memory, and read back once the manifest is built.\n *\n * `undefined` until `generateBundle` runs, deliberately: an empty SET would make the coverage\n * check below pass by having seen nothing, which is the worst way for a guard to succeed. The\n * two states have to be told apart.\n */\n let required: Set<string> | undefined\n\n return {\n name: 'sentinel:node-manifest',\n apply: 'build',\n\n generateBundle(_outputOptions, bundle) {\n required = externalPackages(bundle)\n },\n\n async closeBundle() {\n // `closeBundle` runs after a FAILED build too. The ENTRY is what says a bundle was really\n // written: `emptyOutDir` leaves the directory in place, so its existence alone would let a\n // failed build still produce a manifest describing nothing.\n const entry = options.entry ?? 'main.js'\n if (!existsSync(join(options.outDir, entry))) return\n\n // Imported here rather than at module scope: a front config importing this file must not\n // pay for loading the nx graph machinery it will never call.\n const { createProjectGraphAsync } = await import('nx/src/devkit-exports.js')\n const { createPackageJson, createLockFile, getLockFileName } = await import('@nx/js')\n\n const graph = await createProjectGraphAsync({ exitOnError: false })\n const manifest = createPackageJson(options.project, graph, {\n root: options.workspaceRoot,\n isProduction: true,\n })\n manifest.main = entry\n\n if (required === undefined) {\n throw new Error(\n `sentinel build(nest): the bundle was written but never inspected, so the manifest ` +\n `could not be checked against what it requires. Refusing to write one that might ` +\n `not install.`,\n )\n }\n assertManifestCovers(required, manifest, options)\n\n writeFileSync(join(options.outDir, 'package.json'), `${JSON.stringify(manifest, null, 2)}\\n`)\n writeFileSync(\n join(options.outDir, getLockFileName('pnpm')),\n createLockFile(manifest, graph, 'pnpm'),\n )\n },\n }\n}\n\n/**\n * Refuse a bundle that requires something the image will not install.\n *\n * The manifest is derived from the project GRAPH, so it lists what modules declare. The bundle\n * requires what the bundler left external. Those two sets agreeing is an assumption, and it was\n * wrong: `tslib` arrives through `importHelpers` in the workspace tsconfig without any module\n * declaring it, so the manifest omitted it and the service died on its first require.\n *\n * It died only in the image. Locally and in CI the workspace root has the package, so the gap is\n * invisible until a container starts, which is the most expensive place to discover it. Checking\n * here turns that into a build failure naming the package.\n */\nconst assertManifestCovers = (\n required: ReadonlySet<string>,\n manifest: { dependencies?: Record<string, string> },\n options: NodeManifestOptions,\n): void => {\n const declared = new Set(Object.keys(manifest.dependencies ?? {}))\n const provided = options.providedByImage ?? DEFAULT_PROVIDED_BY_IMAGE\n const missing = [...required]\n .filter((name) => !declared.has(name))\n .filter((name) => !provided.some((prefix) => name === prefix || name.startsWith(`${prefix}/`)))\n .sort()\n\n if (missing.length === 0) return\n\n throw new Error(\n `sentinel build(nest): the bundle requires ${missing.length} package(s) the generated ` +\n `manifest does not declare, so the image would not install them: ${missing.join(', ')}. ` +\n `Either declare them in the module's package.json, inline them with ` +\n `\\`ssr.noExternal\\`, or list them in \\`providedByImage\\` if the Dockerfile copies them in.`,\n )\n}\n","/**\n * What `@hublo/sentinel/build/nest` gives an adopted service.\n *\n * The sibling `build/react` entry is a re-export surface: the module keeps its own config and\n * only its import lines move. This one carries a shape as well, because 38 Nest services share\n * one config and have nothing of their own to preserve. See `nest-service.ts` for why the two\n * answers differ, and why that is one rule rather than two.\n *\n * Its own entry point, like the React one, so importing it at build time never drags the CLI\n * and its adapters into a Vite config.\n */\nexport { nestService, type NestServiceOptions } from './nest-service.js'\n\n// The pieces the shape is built from, exported individually so a service that needs something\n// the shape does not give it can compose its own without leaving sentinel.\nexport { decoratorMetadata, type DecoratorMetadataOptions } from './decorator-metadata.js'\nexport { nodeManifest, type NodeManifestOptions } from './node-manifest.js'\nexport { tsconfigAliases, type Alias } from './tsconfig-aliases.js'\n\n// Vite's own, so an adopted config imports everything from one place and the module can drop\n// `vite` from its dependencies.\nexport { defineConfig, loadEnv, mergeConfig } from 'vite'\nexport type { Plugin, PluginOption, UserConfig, UserConfigExport } from 'vite'\n"],"mappings":"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","names":["join","existsSync","join","join","mergeConfig"]}
@@ -1,10 +1,11 @@
1
1
  import {
2
2
  sharedTestConfig
3
- } from "../../../chunk-7PUVK4YM.js";
3
+ } from "../../../chunk-5VNYQIFD.js";
4
4
  import {
5
5
  decoratorMetadata,
6
6
  tsconfigAliases
7
- } from "../../../chunk-3TDUIKVQ.js";
7
+ } from "../../../chunk-SWWQ7X7B.js";
8
+ import "../../../chunk-QXFCZON7.js";
8
9
 
9
10
  // src/roles/test/nest/toolchain.ts
10
11
  import { defineConfig, mergeConfig } from "vitest/config";