@hublo/sentinel 1.3.0 → 1.4.0-alpha.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +8 -4
  2. package/dist/bin/sentinel.d.ts +1 -1
  3. package/dist/bin/sentinel.js +22 -9
  4. package/dist/chunk-2XLX6PFR.js +132 -0
  5. package/dist/chunk-2XLX6PFR.js.map +1 -0
  6. package/dist/chunk-3TDUIKVQ.js +178 -0
  7. package/dist/chunk-3TDUIKVQ.js.map +1 -0
  8. package/dist/chunk-CPCUPK4J.js +70 -0
  9. package/dist/chunk-CPCUPK4J.js.map +1 -0
  10. package/dist/chunk-CRKUEP4J.js +427 -0
  11. package/dist/chunk-CRKUEP4J.js.map +1 -0
  12. package/dist/{chunk-676GBPMS.js → chunk-L7WS36XV.js} +3921 -556
  13. package/dist/chunk-PWV3BMDA.js +15 -0
  14. package/dist/chunk-PWV3BMDA.js.map +1 -0
  15. package/dist/chunk-WLFE5RUU.js +264 -0
  16. package/dist/chunk-WLFE5RUU.js.map +1 -0
  17. package/dist/index.d.ts +13 -0
  18. package/dist/index.js +2 -2
  19. package/dist/roles/build/nest/toolchain.d.ts +4 -36
  20. package/dist/roles/build/nest/toolchain.js +10 -178
  21. package/dist/roles/build/nest/toolchain.js.map +1 -0
  22. package/dist/roles/build/toolchain.js.map +1 -0
  23. package/dist/roles/test/nest/toolchain.d.ts +9 -0
  24. package/dist/roles/test/nest/toolchain.js +25 -0
  25. package/dist/roles/test/nest/toolchain.js.map +1 -0
  26. package/dist/roles/test/react/toolchain.d.ts +66 -0
  27. package/dist/roles/test/react/toolchain.js +14 -1
  28. package/dist/roles/test/react/toolchain.js.map +1 -0
  29. package/dist/roles/test/setup/jest-parity.d.ts +2 -0
  30. package/dist/roles/test/setup/jest-parity.js +159 -0
  31. package/dist/roles/test/setup/jest-parity.js.map +1 -0
  32. package/dist/roles/test/setup/mock-extended.d.ts +46 -0
  33. package/dist/roles/test/setup/mock-extended.js +65 -0
  34. package/dist/roles/test/setup/mock-extended.js.map +1 -0
  35. package/dist/roles/test/setup/msw-lifecycle.d.ts +16 -0
  36. package/dist/roles/test/setup/msw-lifecycle.js +12 -0
  37. package/dist/roles/test/setup/msw-lifecycle.js.map +1 -0
  38. package/dist/roles/test/setup/msw-server.d.ts +3 -0
  39. package/dist/roles/test/setup/msw-server.js +10 -0
  40. package/dist/roles/test/setup/msw-server.js.map +1 -0
  41. package/dist/roles/test/setup/workspace-entry.d.ts +2 -0
  42. package/dist/roles/test/setup/workspace-entry.js +8 -0
  43. package/dist/roles/test/setup/workspace-entry.js.map +1 -0
  44. package/dist/roles/test/shared-test-config.d.ts +32 -0
  45. package/dist/roles/test/shared-test-config.js +8 -0
  46. package/dist/roles/test/shared-test-config.js.map +1 -0
  47. package/dist/roles/test/tools/msw.d.ts +1 -0
  48. package/dist/roles/test/tools/msw.js +3 -0
  49. package/dist/roles/test/tools/msw.js.map +1 -0
  50. package/dist/tsconfig-aliases-Ce6axdJ4.d.ts +36 -0
  51. package/docs/.gitkeep +0 -0
  52. package/docs/build-adoption.md +521 -0
  53. package/docs/format-adoption.md +321 -0
  54. package/docs/lint-adoption.md +290 -0
  55. package/docs/performance.md +49 -0
  56. package/docs/test-adoption.md +219 -0
  57. package/docs/typescript-adoption.md +184 -0
  58. package/docs/typescript-traces.md +798 -0
  59. package/docs/using-sentinel.md +195 -0
  60. package/docs/validating-a-change.md +101 -0
  61. package/package.json +35 -6
@@ -0,0 +1,15 @@
1
+ // src/roles/test/setup/msw.ts
2
+ import { setupServer } from "msw/node";
3
+ var server = setupServer();
4
+ var LISTEN_OPTIONS = { onUnhandledRequest: "error" };
5
+ function installMswLifecycle(hooks) {
6
+ server.listen(LISTEN_OPTIONS);
7
+ hooks.beforeAll(() => server.listen(LISTEN_OPTIONS));
8
+ hooks.afterAll(() => server.close());
9
+ }
10
+
11
+ export {
12
+ server,
13
+ installMswLifecycle
14
+ };
15
+ //# sourceMappingURL=chunk-PWV3BMDA.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/roles/test/setup/msw.ts"],"sourcesContent":["/**\n * The HTTP interception a test suite runs against, owned here rather than by each module.\n *\n * ## Why sentinel ships msw at all\n *\n * The goal is that a future migration is ONE version bump of sentinel. Today's migration is\n * painful for the opposite reason: the runner and everything around it are declared in 133 places,\n * so moving them means moving 133 modules. A tool that sits between the runner and the tests, as\n * msw does, has to travel with the runner or it becomes the next thing nobody can move.\n *\n * It also puts the compatibility question where someone can answer it. When Vitest moves, msw\n * either still works with it or does not, and that is a question for the tooling team with one\n * test suite, not for thirty teams discovering it separately.\n *\n * ## Why version 1.3.3, which is not the newest\n *\n * Measured across the repo: 523 test files use the msw 1 API (`rest.get`), and NOT ONE uses the\n * msw 2 API (`http.get`). Shipping msw 2 would break all 523 on the day they migrate, turning one\n * migration into two. Shipping 1.3.3 keeps them working, and the eventual 1 to 2 move becomes\n * OURS: studied once, then automated or documented, which is the whole point of owning it.\n *\n * The one module on msw 2 is `apps/front/hr-management`, which is Svelte, and the test role defers\n * Svelte for its own reasons.\n */\nimport { setupServer } from 'msw/node'\n\n/**\n * No default handlers, deliberately.\n *\n * The repo's own server had `defaultHandlers = []` too, so nothing is lost. A handler here would\n * be a Hublo endpoint inside a package meant to be usable by anyone, which is the line between\n * what sentinel owns (the config) and what an app owns (what it mocks).\n *\n * A suite adds its own with `server.use(...)`, which is how all 600 files that touch this already\n * work.\n */\nexport const server = setupServer()\n\n/** The options the repo's own setup passed, kept in one place since two calls now use them. */\nconst LISTEN_OPTIONS = { onUnhandledRequest: 'error' } as const\n\n/**\n * The lifecycle, as the root jest setup had it, plus ONE deviation the runners forced.\n *\n * `onUnhandledRequest: 'error'` is carried over rather than softened: a test that reaches a real\n * network is a test whose result depends on something outside the repo, and it should say so\n * loudly. The repo already ran this way, so nothing changes for a migrating suite.\n *\n * Registered through the globals Vitest exposes, because the preset sets `globals: true` and a\n * setup file that imported them would need them resolvable from wherever it is loaded.\n *\n * ## Why `listen()` also runs here, at module scope\n *\n * msw intercepts by REPLACING `request` and `get` on the `http` and `https` modules. Under jest,\n * `import * as https from 'https'` compiled to `require('https')`, so the module object was read\n * at call time and a patch applied later was seen. Under Vitest the same line is a real ESM\n * namespace import of a CJS builtin, and Node builds that namespace ONCE, from the exports as they\n * stand at first import. A patch applied afterwards never reaches it.\n *\n * `beforeAll` runs after the test file and everything it imports have been evaluated, so the\n * namespace is already frozen by then: the provider keeps calling the ORIGINAL `https.request` and\n * the request leaves the machine. Measured on `libs/cloud/hermes`: `send-push` reached the REAL\n * OneSignal API, which answered `400 Failed to parse app_id from request`, a message that exists\n * nowhere in this repo. msw never saw the call, so `onUnhandledRequest: 'error'` never fired\n * either, and the only visible symptom was `expected \"vi.fn()\" to be called 50 times, but got 0`.\n *\n * A setup file is evaluated BEFORE the test module graph, so patching here happens before the\n * namespace is built. Same file, same suite, 0 of 7 to 7 of 7.\n *\n * The `beforeAll` call is kept as well: it is what restores interception for a suite that closed\n * the server itself, and msw's own `listen()` is safe to call twice.\n */\nexport function installMswLifecycle(hooks: {\n beforeAll: (fn: () => void) => void\n afterEach: (fn: () => void) => void\n afterAll: (fn: () => void) => void\n}): void {\n server.listen(LISTEN_OPTIONS)\n hooks.beforeAll(() => server.listen(LISTEN_OPTIONS))\n hooks.afterAll(() => server.close())\n\n /*\n * `server.resetHandlers()` is NOT called here, and that is a transcription and not a judgement.\n *\n * The root jest setup has the line commented out. Enabling it would be an improvement in the\n * abstract — a handler one test adds leaks into the next, and the failure then appears in\n * whichever test happens to run after, which is the hardest kind to trace. But some suite may\n * depend on that persistence today, and a migration is the wrong moment to find out.\n *\n * So the behaviour is carried across exactly, and the question is reported rather than settled\n * here: it belongs to whoever owns those suites, as its own reviewed change.\n */\n}\n"],"mappings":";AAwBA,SAAS,mBAAmB;AAYrB,IAAM,SAAS,YAAY;AAGlC,IAAM,iBAAiB,EAAE,oBAAoB,QAAQ;AAiC9C,SAAS,oBAAoB,OAI3B;AACP,SAAO,OAAO,cAAc;AAC5B,QAAM,UAAU,MAAM,OAAO,OAAO,cAAc,CAAC;AACnD,QAAM,SAAS,MAAM,OAAO,MAAM,CAAC;AAarC;","names":[]}
@@ -0,0 +1,264 @@
1
+ // src/shared/package-json.ts
2
+ import { existsSync, readFileSync } from "fs";
3
+ import { basename, dirname, join } from "path";
4
+ import { fileURLToPath } from "url";
5
+ function readOwnPackage() {
6
+ let dir = dirname(fileURLToPath(import.meta.url));
7
+ for (; ; ) {
8
+ const pkgPath = join(dir, "package.json");
9
+ if (existsSync(pkgPath)) {
10
+ try {
11
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
12
+ if (typeof pkg.version === "string") {
13
+ const bin = typeof pkg.bin === "string" ? pkg.bin : Object.values(pkg.bin ?? {})[0];
14
+ return { name: pkg.name ?? "@hublo/sentinel", version: pkg.version, bin };
15
+ }
16
+ } catch {
17
+ }
18
+ }
19
+ const parent = dirname(dir);
20
+ if (parent === dir) return { name: "@hublo/sentinel", version: "0.0.0" };
21
+ dir = parent;
22
+ }
23
+ }
24
+ function readOwnVersion() {
25
+ return readOwnPackage().version;
26
+ }
27
+ function readProjectPackageJson(dir) {
28
+ const path = join(dir, "package.json");
29
+ if (!existsSync(path)) return {};
30
+ try {
31
+ return JSON.parse(readFileSync(path, "utf8"));
32
+ } catch {
33
+ process.stderr.write(`sentinel: could not parse ${path}; ignoring for detection.
34
+ `);
35
+ return {};
36
+ }
37
+ }
38
+ function readNxProjectName(dir) {
39
+ const path = join(dir, "project.json");
40
+ if (!existsSync(path)) return void 0;
41
+ try {
42
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
43
+ return typeof parsed.name === "string" ? parsed.name : void 0;
44
+ } catch {
45
+ return void 0;
46
+ }
47
+ }
48
+ function moduleName(dir) {
49
+ const declared = readProjectPackageJson(dir).name;
50
+ return readNxProjectName(dir) ?? (declared === void 0 ? basename(dir) : declared);
51
+ }
52
+
53
+ // src/core/workspace-prep.ts
54
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
55
+ import { dirname as dirname2, join as join2, relative } from "path";
56
+
57
+ // src/core/settings.ts
58
+ var WORKSPACE_ROOT_MARKER = "nx.json";
59
+ var DEFAULT_MAX_DIAGNOSTICS = 100;
60
+
61
+ // src/core/workspace-prep.ts
62
+ var OVERRIDE_KEY = "i18next>typescript";
63
+ var NATIVE_TS_ALIAS = "@typescript/native";
64
+ var WORKSPACE_YAML = "pnpm-workspace.yaml";
65
+ var RELEASE_AGE_KEY = "minimumReleaseAgeExclude";
66
+ var LIST_ITEM = /^(\s*)-\s+(.*?)\s*$/;
67
+ var MAP_ENTRY = /^(\s+)(.*?):\s+(.*?)\s*$/;
68
+ function findWorkspaceRoot(startDir) {
69
+ let dir = startDir;
70
+ for (; ; ) {
71
+ if (existsSync2(join2(dir, WORKSPACE_ROOT_MARKER))) return dir;
72
+ const parent = dirname2(dir);
73
+ if (parent === dir) return void 0;
74
+ dir = parent;
75
+ }
76
+ }
77
+ function declaredNativeTs(pkg) {
78
+ const spec = pkg.dependencies?.[NATIVE_TS_ALIAS] ?? pkg.devDependencies?.[NATIVE_TS_ALIAS];
79
+ if (!spec) return void 0;
80
+ const version = spec.slice(spec.lastIndexOf("@") + 1).replace(/^[\^~>=<\s]+/, "");
81
+ return version || void 0;
82
+ }
83
+ function ensureI18nextSingleton(root, dryRun) {
84
+ const pkgPath = join2(root, "package.json");
85
+ if (!existsSync2(pkgPath)) return void 0;
86
+ const pkg = JSON.parse(readFileSync2(pkgPath, "utf8"));
87
+ const want = declaredNativeTs(pkg);
88
+ if (!want) return void 0;
89
+ const yamlPath = join2(root, WORKSPACE_YAML);
90
+ if (!existsSync2(yamlPath)) return void 0;
91
+ const lines = readFileSync2(yamlPath, "utf8").split("\n");
92
+ const entry = findOverrideEntry(lines);
93
+ if (entry.valueIdx !== void 0 && entry.value === want) return void 0;
94
+ if (dryRun) return `would pin ${OVERRIDE_KEY} to ${want} in ${WORKSPACE_YAML} (i18next singleton)`;
95
+ applyOverride(lines, entry, want);
96
+ writeFileSync(yamlPath, lines.join("\n"));
97
+ return `pinned ${OVERRIDE_KEY} to ${want} in ${WORKSPACE_YAML} (i18next singleton)`;
98
+ }
99
+ function findOverrideEntry(lines) {
100
+ const blockIdx = lines.findIndex((line) => line.replace(/\s+$/, "") === "overrides:");
101
+ if (blockIdx === -1) return { indent: " " };
102
+ let lastEntryIdx = blockIdx;
103
+ let indent = " ";
104
+ for (let i = blockIdx + 1; i < lines.length; i++) {
105
+ const match = lines[i]?.match(MAP_ENTRY);
106
+ if (!match) break;
107
+ indent = match[1] ?? indent;
108
+ lastEntryIdx = i;
109
+ if ((match[2] ?? "").replace(/^['"]|['"]$/g, "") === OVERRIDE_KEY) {
110
+ return {
111
+ valueIdx: i,
112
+ value: (match[3] ?? "").replace(/^['"]|['"]$/g, ""),
113
+ blockIdx,
114
+ lastEntryIdx,
115
+ indent
116
+ };
117
+ }
118
+ }
119
+ return { blockIdx, lastEntryIdx, indent };
120
+ }
121
+ function applyOverride(lines, entry, want) {
122
+ const line = `${entry.indent}'${OVERRIDE_KEY}': ${want}`;
123
+ if (entry.valueIdx !== void 0) {
124
+ lines[entry.valueIdx] = line;
125
+ return;
126
+ }
127
+ if (entry.lastEntryIdx !== void 0) {
128
+ lines.splice(entry.lastEntryIdx + 1, 0, line);
129
+ return;
130
+ }
131
+ while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
132
+ lines.push("overrides:", line, "");
133
+ }
134
+ function ensureReleaseAgeAllowList(root, dryRun) {
135
+ const yamlPath = join2(root, WORKSPACE_YAML);
136
+ if (!existsSync2(yamlPath)) return void 0;
137
+ const own = readOwnPackage().name;
138
+ const lines = readFileSync2(yamlPath, "utf8").split("\n");
139
+ const keyIdx = lines.findIndex((line) => line.replace(/\s+$/, "") === `${RELEASE_AGE_KEY}:`);
140
+ if (keyIdx === -1) return void 0;
141
+ let lastItemIdx = keyIdx;
142
+ let indent = " ";
143
+ for (let i = keyIdx + 1; i < lines.length; i++) {
144
+ const match = lines[i]?.match(LIST_ITEM);
145
+ if (!match) break;
146
+ indent = match[1] ?? indent;
147
+ lastItemIdx = i;
148
+ if ((match[2] ?? "").replace(/^['"]|['"]$/g, "") === own) return void 0;
149
+ }
150
+ if (dryRun) return `would allow-list ${own} under ${RELEASE_AGE_KEY}`;
151
+ lines.splice(lastItemIdx + 1, 0, `${indent}- '${own}'`);
152
+ writeFileSync(yamlPath, lines.join("\n"));
153
+ return `allow-listed ${own} under ${RELEASE_AGE_KEY} in ${WORKSPACE_YAML}`;
154
+ }
155
+ var PRETTIER_IGNORE = ".prettierignore";
156
+ var EXCLUSION_HEADER = "# @hublo/sentinel: modules formatted by oxfmt, not by the root Prettier";
157
+ var ROOT_PRETTIER_CONFIGS = [
158
+ ".prettierrc",
159
+ ".prettierrc.json",
160
+ ".prettierrc.json5",
161
+ ".prettierrc.yml",
162
+ ".prettierrc.yaml",
163
+ ".prettierrc.js",
164
+ ".prettierrc.cjs",
165
+ ".prettierrc.mjs",
166
+ "prettier.config.js",
167
+ "prettier.config.cjs",
168
+ "prettier.config.mjs"
169
+ ];
170
+ function ensureFormatterExclusion(root, moduleDir, dryRun) {
171
+ const rel = relative(root, moduleDir).replaceAll("\\", "/");
172
+ if (rel === "" || rel.startsWith("..")) return void 0;
173
+ const ignorePath = join2(root, PRETTIER_IGNORE);
174
+ const hasPrettier = existsSync2(ignorePath) || ROOT_PRETTIER_CONFIGS.some((name) => existsSync2(join2(root, name)));
175
+ if (!hasPrettier) return void 0;
176
+ const pattern = `/${rel}/`;
177
+ const existing = existsSync2(ignorePath) ? readFileSync2(ignorePath, "utf8") : "";
178
+ const lines = existing.split("\n");
179
+ if (lines.some((line) => line.trim().replace(/\/$/, "") === pattern.replace(/\/$/, ""))) {
180
+ return void 0;
181
+ }
182
+ if (dryRun) return `would exclude ${rel} from the root Prettier (${PRETTIER_IGNORE})`;
183
+ const headerIdx = lines.findIndex((line) => line.trim() === EXCLUSION_HEADER);
184
+ if (headerIdx === -1) {
185
+ const prefix = existing.trim() === "" ? "" : `${existing.replace(/\n*$/, "")}
186
+
187
+ `;
188
+ writeFileSync(ignorePath, `${prefix}${EXCLUSION_HEADER}
189
+ ${pattern}
190
+ `);
191
+ } else {
192
+ let end = headerIdx + 1;
193
+ while (end < lines.length && lines[end]?.trim().startsWith("/")) end += 1;
194
+ const managed = [...lines.slice(headerIdx + 1, end), pattern].sort();
195
+ writeFileSync(
196
+ ignorePath,
197
+ [...lines.slice(0, headerIdx + 1), ...managed, ...lines.slice(end)].join("\n")
198
+ );
199
+ }
200
+ return `excluded ${rel} from the root Prettier (${PRETTIER_IGNORE}): it formats itself now`;
201
+ }
202
+ function ensureWorkspacePrep(opts) {
203
+ const dryRun = Boolean(opts.dryRun);
204
+ return [
205
+ ensureI18nextSingleton(opts.root, dryRun),
206
+ ensureReleaseAgeAllowList(opts.root, dryRun),
207
+ opts.formattedModule === void 0 ? void 0 : ensureFormatterExclusion(opts.root, opts.formattedModule, dryRun)
208
+ ].filter((message) => message !== void 0);
209
+ }
210
+ function inspectWorkspacePrep(root) {
211
+ const entries = [];
212
+ let pkg = {};
213
+ const pkgPath = join2(root, "package.json");
214
+ if (existsSync2(pkgPath)) {
215
+ try {
216
+ pkg = JSON.parse(readFileSync2(pkgPath, "utf8"));
217
+ } catch {
218
+ pkg = {};
219
+ }
220
+ }
221
+ const yamlPath = join2(root, WORKSPACE_YAML);
222
+ const yaml = existsSync2(yamlPath) ? readFileSync2(yamlPath, "utf8") : void 0;
223
+ const pinned = yaml === void 0 ? void 0 : findOverrideEntry(yaml.split("\n")).value;
224
+ if (pinned !== void 0) {
225
+ const stillForks = declaredNativeTs(pkg);
226
+ entries.push({
227
+ rule: `${WORKSPACE_YAML} \u2192 overrides.${OVERRIDE_KEY}`,
228
+ reason: stillForks === void 0 ? `pinned to ${pinned}, but the root no longer declares ${NATIVE_TS_ALIAS}, so nothing forks any more. This override is now dead config and can be removed.` : `pinned to ${pinned}, matching the second TypeScript the root declares (${NATIVE_TS_ALIAS}). It re-resolves i18next for EVERY module that depends on it, not only adopted ones. It retires with the second TypeScript.`
229
+ });
230
+ }
231
+ const legacy = pkg.pnpm?.overrides?.[OVERRIDE_KEY];
232
+ if (legacy !== void 0) {
233
+ entries.push({
234
+ rule: `package.json \u2192 pnpm.overrides.${OVERRIDE_KEY}`,
235
+ reason: `pinned to ${legacy}, but pnpm 11 no longer reads the \`pnpm\` field of package.json and says nothing about it, so this override is not applied. Move it to ${WORKSPACE_YAML} under \`overrides:\`, which pnpm 10 and 11 both read.`
236
+ });
237
+ }
238
+ const own = readOwnPackage().name;
239
+ if (yaml !== void 0) {
240
+ const listed = new RegExp(`^\\s*-\\s*['"]?${own.replace("/", "\\/")}['"]?\\s*$`, "m").test(yaml);
241
+ if (listed) {
242
+ const gated = new RegExp(`^\\s*minimumReleaseAge\\s*:`, "m").test(yaml);
243
+ entries.push({
244
+ rule: `${WORKSPACE_YAML} \u2192 ${RELEASE_AGE_KEY}: ${own}`,
245
+ reason: gated ? `${own} is exempt from this workspace's release-age gate, so a freshly published version installs instead of being held. That is a standing exemption from a supply-chain control: the exposure is one bump wide, since the version is pinned and the lockfile carries its integrity, so a re-publish of the same version does not pass. Retire it once a bump can simply wait out the window.` : `${own} is allow-listed, but this workspace no longer sets minimumReleaseAge, so there is no gate to be exempt from. This entry is now dead config and can be removed.`
246
+ });
247
+ }
248
+ }
249
+ return entries;
250
+ }
251
+
252
+ export {
253
+ readOwnPackage,
254
+ readOwnVersion,
255
+ readProjectPackageJson,
256
+ readNxProjectName,
257
+ moduleName,
258
+ WORKSPACE_ROOT_MARKER,
259
+ DEFAULT_MAX_DIAGNOSTICS,
260
+ findWorkspaceRoot,
261
+ ensureWorkspacePrep,
262
+ inspectWorkspacePrep
263
+ };
264
+ //# sourceMappingURL=chunk-WLFE5RUU.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/shared/package-json.ts","../src/core/workspace-prep.ts","../src/core/settings.ts"],"sourcesContent":["/**\n * package.json helpers, shared by the CLI and the engine.\n */\nimport { existsSync, readFileSync } from 'node:fs'\nimport { basename, dirname, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\n/**\n * sentinel's OWN version, for `--version`. We must NOT use\n * `process.env.npm_package_version`, that is the version of whatever script\n * invoked us (an app's package.json, or nothing). Instead walk up from this\n * module to the nearest package.json, which is always sentinel's own, whether\n * running from source or the bundled dist.\n */\n/**\n * sentinel's OWN name + version, read by walking up from this module to the nearest\n * package.json. Used both for `--version` and to pin the exact dependency `--init`\n * writes into an adopted module, so a module always references the version that adopted it.\n */\nexport function readOwnPackage(): { name: string; version: string; bin?: string } {\n let dir = dirname(fileURLToPath(import.meta.url))\n for (;;) {\n const pkgPath = join(dir, 'package.json')\n if (existsSync(pkgPath)) {\n try {\n const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) as {\n name?: string\n version?: string\n bin?: string | Record<string, string>\n }\n if (typeof pkg.version === 'string') {\n // The bin PATH, not the command name: a package that adopts itself has to invoke\n // its own build output, because the `.bin` shim does not exist yet (see below).\n const bin = typeof pkg.bin === 'string' ? pkg.bin : Object.values(pkg.bin ?? {})[0]\n return { name: pkg.name ?? '@hublo/sentinel', version: pkg.version, bin }\n }\n } catch {\n // Malformed package.json; keep walking up.\n }\n }\n const parent = dirname(dir)\n if (parent === dir) return { name: '@hublo/sentinel', version: '0.0.0' } // filesystem root\n dir = parent\n }\n}\n\nexport function readOwnVersion(): string {\n return readOwnPackage().version\n}\n\n/**\n * Read a module's package.json for framework detection; tolerant of a missing or\n * malformed file (returns `{}`, so detection falls through to the `node` default).\n */\nexport function readProjectPackageJson(dir: string): {\n /** The package name, which is also what nx calls the project when there is no `project.json`. */\n name?: string\n dependencies?: Record<string, string>\n devDependencies?: Record<string, string>\n /**\n * How the module's `.js`/`.ts` files are loaded. It decides whether a Vite config can import\n * an ESM-only package at all, which is why the build role reads it before rewriting one.\n */\n type?: string\n /**\n * What nx alone needs per target, which every role writes here and none could read back.\n *\n * Typed loosely on purpose: this reader is shared by every role and each one knows the shape of\n * its own target. Declared at all because a role that WRITES a block and then has to cast to\n * read it is one refactor away from reading a key it never wrote.\n */\n nx?: { targets?: Record<string, Record<string, unknown>> }\n} {\n const path = join(dir, 'package.json')\n if (!existsSync(path)) return {}\n try {\n return JSON.parse(readFileSync(path, 'utf8')) as Record<string, never>\n } catch {\n process.stderr.write(`sentinel: could not parse ${path}; ignoring for detection.\\n`)\n return {}\n }\n}\n\n/**\n * The nx project name for a module, read from its `project.json`, or undefined if\n * there is none/unreadable. Used when `--init` has to scaffold a `package.json`\n * for a module that only has a `project.json` (common for nx apps/services): the\n * scaffolded file borrows this name so it is a valid, uniquely-named workspace\n * package. Reusable by any tool that scaffolds, not just TypeScript.\n */\nexport function readNxProjectName(dir: string): string | undefined {\n const path = join(dir, 'project.json')\n if (!existsSync(path)) return undefined\n try {\n const parsed = JSON.parse(readFileSync(path, 'utf8')) as { name?: unknown }\n return typeof parsed.name === 'string' ? parsed.name : undefined\n } catch {\n return undefined\n }\n}\n\n/**\n * What nx calls this module, resolved the way nx itself resolves it.\n *\n * The precedence is `project.json`, then `package.json`, then the directory, and only the\n * first and last of those were being consulted. A module with no `project.json` therefore\n * answered with its DIRECTORY while nx answered with its package name, and the two disagree\n * in the real repo: `apps/front/hr-management` is `front-hr-management` to nx, as is\n * `apps/front/storybook-app`.\n *\n * That gap is not cosmetic, because this name is what `--module` is matched against. The\n * `serve` script `--init` writes carries `--module <name>`, so a module in that shape was\n * adopted with a script naming a project the workspace does not contain, and `pnpm run serve`\n * failed the day it was adopted with nothing having reported it.\n *\n * One function so the two ends cannot drift again: whoever WRITES the name and whoever\n * RESOLVES it now read the same source.\n */\nexport function moduleName(dir: string): string {\n const declared = readProjectPackageJson(dir).name\n return readNxProjectName(dir) ?? (declared === undefined ? basename(dir) : declared)\n}\n","/**\n * Workspace-level preparation that `--init` applies once, at the workspace root, so an\n * adopted module actually installs and resolves cleanly. `--init` writes a module's own\n * files; this is the small, deliberate exception where it also touches the root, because\n * \"set this module up\" is not honest if the workspace it lives in cannot then install or\n * resolve it.\n *\n * Kept intentionally narrow (YAGNI): it applies the specific fixes a workspace's OWN\n * config shows it needs, and is a no-op everywhere else. It is NOT a speculative dedupe\n * engine, when a second, different case appears we generalize from real cases instead of\n * predicting them. The two fixes today:\n *\n * - i18next singleton override. If the root declares a second TypeScript via the\n * `@typescript/native` alias (a repo mid-migration running two TypeScripts), adopting a\n * module re-resolves pnpm and i18next, which carries an optional type-only `typescript`\n * peer, forks into two peer-keyed variants. That forks react-i18next into two React\n * contexts and breaks translation-singleton tests. Pinning i18next's `typescript` peer\n * to the declared TS collapses it to one. Read from the root's own declaration, so it\n * tracks the version and disappears (no-op) once the migration removes the second TS.\n * Written to `pnpm-workspace.yaml`, the only place current pnpm reads overrides from.\n *\n * - release-age allow-list. If the root uses pnpm's `minimumReleaseAgeExclude` gate, a\n * freshly published sentinel is younger than the gate and will not install; allow-list\n * it. Only touched when the gate is actually configured.\n *\n * - root formatter exclusion. A module that has adopted the FORMAT role is formatted by\n * oxfmt, from its own config. The root Prettier does not know that and still owns every\n * file in the repo, so `nx format:write` or `prettier --write .` from the root reformats\n * the module back to the ROOT's style, and the next `pnpm run format` in the module\n * reverses it. Two formatters fighting over the same files, with whichever ran last\n * winning, is the exact state adoption removes INSIDE a module, and it has to be removed\n * at the root too. So the module is excluded in the root `.prettierignore` as it adopts,\n * and the root's own config is otherwise untouched: it still serves every module that has\n * not migrated.\n *\n * All three edits are idempotent and defensive: absent the trigger (no second TypeScript, no\n * release-age gate, no root Prettier) each is skipped, so the step is safe on any workspace.\n */\nimport { existsSync, readFileSync, writeFileSync } from 'node:fs'\nimport { dirname, join, relative } from 'node:path'\n\nimport { readOwnPackage } from '../shared/package-json.js'\n\nimport { WORKSPACE_ROOT_MARKER } from './settings.js'\n\nconst OVERRIDE_KEY = 'i18next>typescript'\nconst NATIVE_TS_ALIAS = '@typescript/native' // `npm:typescript@^7.x`: a workspace's second, real TS\nconst WORKSPACE_YAML = 'pnpm-workspace.yaml'\nconst RELEASE_AGE_KEY = 'minimumReleaseAgeExclude'\nconst LIST_ITEM = /^(\\s*)-\\s+(.*?)\\s*$/\nconst MAP_ENTRY = /^(\\s+)(.*?):\\s+(.*?)\\s*$/\n\ninterface RootPackageJson {\n dependencies?: Record<string, string>\n devDependencies?: Record<string, string>\n pnpm?: { overrides?: Record<string, string> }\n}\n\n/** Walk up from a module directory to the workspace root (the dir holding the marker). */\nexport function findWorkspaceRoot(startDir: string): string | undefined {\n let dir = startDir\n for (;;) {\n if (existsSync(join(dir, WORKSPACE_ROOT_MARKER))) return dir\n const parent = dirname(dir)\n if (parent === dir) return undefined\n dir = parent\n }\n}\n\n/**\n * The TypeScript version to pin i18next's peer to, read from the root's own\n * `@typescript/native` alias (`npm:typescript@^7.0.2` -> `7.0.2`). Returns undefined when\n * the alias is absent (no second TypeScript, so no fork to fix).\n */\nfunction declaredNativeTs(pkg: RootPackageJson): string | undefined {\n const spec = pkg.dependencies?.[NATIVE_TS_ALIAS] ?? pkg.devDependencies?.[NATIVE_TS_ALIAS]\n if (!spec) return undefined\n const version = spec.slice(spec.lastIndexOf('@') + 1).replace(/^[\\^~>=<\\s]+/, '')\n return version || undefined\n}\n\n/**\n * Ensure the i18next singleton override matches the declared second TypeScript. No-op when\n * the workspace runs a single TypeScript (no `@typescript/native`). Returns a message when\n * it changed (or would change, under dryRun), else undefined.\n *\n * The override is written to `pnpm-workspace.yaml`, never to `package.json#pnpm`: pnpm 11\n * stopped reading the `pnpm` field and does so SILENTLY, with no warning and no deprecation\n * (pnpm/pnpm#11536), so an override left in `package.json` is simply not applied. The yaml is\n * read by pnpm 10 and 11 alike (https://pnpm.io/settings), which is why there is one target\n * here rather than a version-dependent fallback.\n */\nfunction ensureI18nextSingleton(root: string, dryRun: boolean): string | undefined {\n const pkgPath = join(root, 'package.json')\n if (!existsSync(pkgPath)) return undefined\n const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) as RootPackageJson\n const want = declaredNativeTs(pkg)\n if (!want) return undefined // single TypeScript: nothing forks\n\n const yamlPath = join(root, WORKSPACE_YAML)\n if (!existsSync(yamlPath)) return undefined // no workspace file: nowhere pnpm would read it\n const lines = readFileSync(yamlPath, 'utf8').split('\\n')\n const entry = findOverrideEntry(lines)\n if (entry.valueIdx !== undefined && entry.value === want) return undefined // already pinned\n if (dryRun) return `would pin ${OVERRIDE_KEY} to ${want} in ${WORKSPACE_YAML} (i18next singleton)`\n\n applyOverride(lines, entry, want)\n writeFileSync(yamlPath, lines.join('\\n'))\n return `pinned ${OVERRIDE_KEY} to ${want} in ${WORKSPACE_YAML} (i18next singleton)`\n}\n\n/** Where the override sits in the yaml, if at all, plus where a new one would be inserted. */\ninterface OverrideEntry {\n /** Index of the `OVERRIDE_KEY` line, when the key is already declared. */\n valueIdx?: number\n /** Its current value, unquoted. */\n value?: string\n /** Index of the `overrides:` key line, when the block exists. */\n blockIdx?: number\n /** Index of the block's last entry, so an insertion keeps the block contiguous. */\n lastEntryIdx?: number\n /** Indentation the block's entries use. */\n indent: string\n}\n\n/**\n * Locate the `overrides:` block and the i18next key inside it. A targeted text read rather\n * than a yaml parse, for the same reason `ensureReleaseAgeAllowList` does it: this file is\n * hand-maintained and round-tripping it through a parser would drop its comments and ordering.\n */\nfunction findOverrideEntry(lines: string[]): OverrideEntry {\n const blockIdx = lines.findIndex((line) => line.replace(/\\s+$/, '') === 'overrides:')\n if (blockIdx === -1) return { indent: ' ' }\n\n let lastEntryIdx = blockIdx\n let indent = ' '\n for (let i = blockIdx + 1; i < lines.length; i++) {\n const match = lines[i]?.match(MAP_ENTRY)\n if (!match) break // dedent or blank line: the block ended\n indent = match[1] ?? indent\n lastEntryIdx = i\n if ((match[2] ?? '').replace(/^['\"]|['\"]$/g, '') === OVERRIDE_KEY) {\n return {\n valueIdx: i,\n value: (match[3] ?? '').replace(/^['\"]|['\"]$/g, ''),\n blockIdx,\n lastEntryIdx,\n indent,\n }\n }\n }\n return { blockIdx, lastEntryIdx, indent }\n}\n\n/** Update the key in place, append it to an existing block, or open a block at the end. */\nfunction applyOverride(lines: string[], entry: OverrideEntry, want: string): void {\n const line = `${entry.indent}'${OVERRIDE_KEY}': ${want}`\n if (entry.valueIdx !== undefined) {\n lines[entry.valueIdx] = line\n return\n }\n if (entry.lastEntryIdx !== undefined) {\n lines.splice(entry.lastEntryIdx + 1, 0, line)\n return\n }\n // No `overrides:` block at all: open one, keeping a single trailing newline.\n while (lines.length > 0 && lines[lines.length - 1] === '') lines.pop()\n lines.push('overrides:', line, '')\n}\n\n/**\n * Ensure sentinel is allow-listed under `minimumReleaseAgeExclude` so a fresh publish\n * installs. No-op when the workspace has no such gate. Targeted text edit so comments and\n * formatting are preserved. Returns a message when it changed (or would), else undefined.\n */\nfunction ensureReleaseAgeAllowList(root: string, dryRun: boolean): string | undefined {\n const yamlPath = join(root, WORKSPACE_YAML)\n if (!existsSync(yamlPath)) return undefined\n const own = readOwnPackage().name\n const lines = readFileSync(yamlPath, 'utf8').split('\\n')\n const keyIdx = lines.findIndex((line) => line.replace(/\\s+$/, '') === `${RELEASE_AGE_KEY}:`)\n if (keyIdx === -1) return undefined // no release-age gate in use\n\n let lastItemIdx = keyIdx\n let indent = ' '\n for (let i = keyIdx + 1; i < lines.length; i++) {\n const match = lines[i]?.match(LIST_ITEM)\n if (!match) break\n indent = match[1] ?? indent\n lastItemIdx = i\n if ((match[2] ?? '').replace(/^['\"]|['\"]$/g, '') === own) return undefined // already listed\n }\n if (dryRun) return `would allow-list ${own} under ${RELEASE_AGE_KEY}`\n\n lines.splice(lastItemIdx + 1, 0, `${indent}- '${own}'`)\n writeFileSync(yamlPath, lines.join('\\n'))\n return `allow-listed ${own} under ${RELEASE_AGE_KEY} in ${WORKSPACE_YAML}`\n}\n\n/** The root ignore file Prettier reads, and the section sentinel owns inside it. */\nconst PRETTIER_IGNORE = '.prettierignore'\nconst EXCLUSION_HEADER = '# @hublo/sentinel: modules formatted by oxfmt, not by the root Prettier'\n\n/** Root Prettier configs. Their presence is what makes an exclusion meaningful. */\nconst ROOT_PRETTIER_CONFIGS = [\n '.prettierrc',\n '.prettierrc.json',\n '.prettierrc.json5',\n '.prettierrc.yml',\n '.prettierrc.yaml',\n '.prettierrc.js',\n '.prettierrc.cjs',\n '.prettierrc.mjs',\n 'prettier.config.js',\n 'prettier.config.cjs',\n 'prettier.config.mjs',\n]\n\n/**\n * Exclude a module that now formats itself from the ROOT Prettier.\n *\n * Written as a directory pattern anchored at the root (`/apps/front/x/`), which is what\n * gitignore syntax means by \"this exact directory\", so a module cannot accidentally exclude a\n * same-named directory somewhere else.\n *\n * Skipped entirely when the root has no Prettier at all: there is then nothing to conflict\n * with, and creating an ignore file for a tool the workspace does not use would be noise.\n */\nfunction ensureFormatterExclusion(\n root: string,\n moduleDir: string,\n dryRun: boolean,\n): string | undefined {\n const rel = relative(root, moduleDir).replaceAll('\\\\', '/')\n if (rel === '' || rel.startsWith('..')) return undefined // the root itself, or outside it\n\n const ignorePath = join(root, PRETTIER_IGNORE)\n const hasPrettier =\n existsSync(ignorePath) || ROOT_PRETTIER_CONFIGS.some((name) => existsSync(join(root, name)))\n if (!hasPrettier) return undefined\n\n const pattern = `/${rel}/`\n const existing = existsSync(ignorePath) ? readFileSync(ignorePath, 'utf8') : ''\n const lines = existing.split('\\n')\n // Already excluded, in whichever spelling a human may have used.\n if (lines.some((line) => line.trim().replace(/\\/$/, '') === pattern.replace(/\\/$/, ''))) {\n return undefined\n }\n if (dryRun) return `would exclude ${rel} from the root Prettier (${PRETTIER_IGNORE})`\n\n const headerIdx = lines.findIndex((line) => line.trim() === EXCLUSION_HEADER)\n if (headerIdx === -1) {\n const prefix = existing.trim() === '' ? '' : `${existing.replace(/\\n*$/, '')}\\n\\n`\n writeFileSync(ignorePath, `${prefix}${EXCLUSION_HEADER}\\n${pattern}\\n`)\n } else {\n // Keep the managed section together and sorted, so the list stays readable as it grows\n // to 150 entries and a diff shows one added line rather than a reshuffle.\n let end = headerIdx + 1\n while (end < lines.length && lines[end]?.trim().startsWith('/')) end += 1\n const managed = [...lines.slice(headerIdx + 1, end), pattern].sort()\n writeFileSync(\n ignorePath,\n [...lines.slice(0, headerIdx + 1), ...managed, ...lines.slice(end)].join('\\n'),\n )\n }\n return `excluded ${rel} from the root Prettier (${PRETTIER_IGNORE}): it formats itself now`\n}\n\n/**\n * Apply every workspace-level fix the given root needs. Returns the human-readable\n * messages for what changed (empty when nothing did). Under dryRun nothing is written and\n * the messages describe what would change.\n *\n * `formattedModule` is the module that just adopted the FORMAT role, when one did. Absent\n * for a lint-only or TypeScript-only `--init`, because a module that has not adopted the\n * formatter must keep being formatted by the root.\n */\nexport function ensureWorkspacePrep(opts: {\n root: string\n dryRun?: boolean\n formattedModule?: string\n}): string[] {\n const dryRun = Boolean(opts.dryRun)\n return [\n ensureI18nextSingleton(opts.root, dryRun),\n ensureReleaseAgeAllowList(opts.root, dryRun),\n opts.formattedModule === undefined\n ? undefined\n : ensureFormatterExclusion(opts.root, opts.formattedModule, dryRun),\n ].filter((message): message is string => message !== undefined)\n}\n\n/**\n * One standing change sentinel has made to the workspace ROOT, and whether it is still earned.\n *\n * This exists because the three root writes are the only thing sentinel does that nobody can\n * see afterwards. `--init` announces each one on the run that makes it, and then it is a line\n * in someone else's file forever. Two consequences already bit us.\n *\n * The `i18next>typescript` override re-resolved `apps/front/console` from a PR that touched no\n * file under it, so CODEOWNERS pulled nobody and its owners learned about it afterwards.\n *\n * And every one of them is TEMPORARY with its removal condition written in prose. That failed\n * three times in one week: `SENTINEL_TAG=alpha` in the migration-page workflow kept building\n * with a pre-release long after 1.1.0 went stable and reported `Format 0/439` for 15 adopted\n * modules; ADR 0042 and its RFC both sat mid-flight past their deadline; and this release-age\n * exclusion is still waiting on \"once sentinel's release cadence settles\". Nothing failed when\n * any of them expired, because a comment cannot fail.\n *\n * So each entry carries the condition that JUSTIFIES it, evaluated now rather than described.\n * A trigger that has gone away makes the entry dead config, and dead config is something a\n * command can say out loud instead of something a reader has to notice.\n *\n * Read-only, and offline: it inspects the same files `--init` writes and nothing else. What it\n * deliberately does NOT claim is anything needing the npm registry — whether a published\n * version is old enough to clear the release-age gate is not knowable from the repo, so it is\n * stated as the standing exemption it is rather than guessed at.\n */\nexport interface WorkspacePrepEntry {\n /** `rule` to match the shape the shared renderer already prints as a labelled block. */\n rule: string\n /** What it does, whether its trigger still holds, and what would retire it. */\n reason: string\n}\n\n/** The root-level changes sentinel maintains here, each with its justification checked now. */\nexport function inspectWorkspacePrep(root: string): WorkspacePrepEntry[] {\n const entries: WorkspacePrepEntry[] = []\n // Tolerant, unlike the write path above, and the difference is deliberate. `--init` should\n // fail loudly on a root it cannot parse, because it is about to edit it. `--inspect` is a\n // read verb that sweeps 441 modules, and one malformed root file must not take the whole\n // sweep down: the modules still have honest answers to give. Caught by its own test.\n let pkg: RootPackageJson = {}\n const pkgPath = join(root, 'package.json')\n if (existsSync(pkgPath)) {\n try {\n pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) as RootPackageJson\n } catch {\n pkg = {}\n }\n }\n\n const yamlPath = join(root, WORKSPACE_YAML)\n const yaml = existsSync(yamlPath) ? readFileSync(yamlPath, 'utf8') : undefined\n\n const pinned = yaml === undefined ? undefined : findOverrideEntry(yaml.split('\\n')).value\n if (pinned !== undefined) {\n const stillForks = declaredNativeTs(pkg)\n entries.push({\n rule: `${WORKSPACE_YAML} → overrides.${OVERRIDE_KEY}`,\n reason:\n stillForks === undefined\n ? `pinned to ${pinned}, but the root no longer declares ${NATIVE_TS_ALIAS}, so nothing ` +\n `forks any more. This override is now dead config and can be removed.`\n : `pinned to ${pinned}, matching the second TypeScript the root declares ` +\n `(${NATIVE_TS_ALIAS}). It re-resolves i18next for EVERY module that depends on it, ` +\n `not only adopted ones. It retires with the second TypeScript.`,\n })\n }\n\n // A copy left in `package.json#pnpm` by an older sentinel, or by hand. Reported because it\n // is INERT rather than redundant: pnpm 11 dropped the `pnpm` field with no warning\n // (pnpm/pnpm#11536), so it reads as protection that is not applied.\n const legacy = pkg.pnpm?.overrides?.[OVERRIDE_KEY]\n if (legacy !== undefined) {\n entries.push({\n rule: `package.json → pnpm.overrides.${OVERRIDE_KEY}`,\n reason:\n `pinned to ${legacy}, but pnpm 11 no longer reads the \\`pnpm\\` field of package.json ` +\n `and says nothing about it, so this override is not applied. Move it to ` +\n `${WORKSPACE_YAML} under \\`overrides:\\`, which pnpm 10 and 11 both read.`,\n })\n }\n\n const own = readOwnPackage().name\n if (yaml !== undefined) {\n const listed = new RegExp(`^\\\\s*-\\\\s*['\"]?${own.replace('/', '\\\\/')}['\"]?\\\\s*$`, 'm').test(yaml)\n if (listed) {\n const gated = new RegExp(`^\\\\s*minimumReleaseAge\\\\s*:`, 'm').test(yaml)\n entries.push({\n rule: `${WORKSPACE_YAML} → ${RELEASE_AGE_KEY}: ${own}`,\n reason: gated\n ? `${own} is exempt from this workspace's release-age gate, so a freshly published ` +\n `version installs instead of being held. That is a standing exemption from a ` +\n `supply-chain control: the exposure is one bump wide, since the version is pinned ` +\n `and the lockfile carries its integrity, so a re-publish of the same version does ` +\n `not pass. Retire it once a bump can simply wait out the window.`\n : `${own} is allow-listed, but this workspace no longer sets minimumReleaseAge, so ` +\n `there is no gate to be exempt from. This entry is now dead config and can be removed.`,\n })\n }\n }\n\n return entries\n}\n","/**\n * Tunable settings: the knobs you would actually change. The fixed vocabulary and\n * its types live in `domain.ts`.\n *\n * Note: the preset is DETECTED from a module's dependencies (see\n * `detect-framework.ts`), deterministically, `node` when no framework dep is present.\n * `--preset` overrides the detection (recommended for `--init`, since hoisted deps\n * can make detection return `node`). The detection table lives in `detect-framework.ts`.\n */\n\n/** A file that marks a workspace root; sentinel refuses to operate there. */\nexport const WORKSPACE_ROOT_MARKER = 'nx.json'\n\n/**\n * How many structured diagnostics a report embeds per module, when the caller does not say.\n *\n * A cap, not a limit on what is counted: the totals stay exact and `diagnosticsTruncated`\n * says when rows were dropped, so a bounded envelope never reads as a clean module. Shared\n * by every role, because a consumer reading `--json` across roles should not have to learn\n * a different cap for each one. `--max-diagnostics 0` means no cap.\n */\nexport const DEFAULT_MAX_DIAGNOSTICS = 100\n"],"mappings":"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","names":["existsSync","readFileSync","dirname","join","existsSync","join","dirname","readFileSync"]}
package/dist/index.d.ts CHANGED
@@ -275,6 +275,19 @@ interface Adapter {
275
275
  * preview.
276
276
  */
277
277
  afterInit?(ctx: RunContext): Promise<void>;
278
+ /**
279
+ * Run BEFORE `--init` writes anything, so a role can capture what it is about to replace.
280
+ *
281
+ * The test role is why this exists. A migration can only be judged against what the module did
282
+ * BEFORE it, and the jest config is deleted by the very plan that is about to be applied: after
283
+ * that moment the reference is unobtainable. So the reference is recorded here, at the last
284
+ * instant it exists.
285
+ *
286
+ * Never called under `--dry-run`, which must touch nothing, and a failure here does NOT stop the
287
+ * init: a module whose jest run is already broken can still be migrated, it simply cannot be
288
+ * proved, and saying so is better than refusing.
289
+ */
290
+ beforeInit?(ctx: RunContext): Promise<void>;
278
291
  /**
279
292
  * The module's resolved configuration, for `--inspect`: what preset it uses and
280
293
  * how it was resolved. Takes the run context so it can report the config as
package/dist/index.js CHANGED
@@ -7,7 +7,8 @@ import {
7
7
  registerAdapters,
8
8
  resolve,
9
9
  setDefaultRunner
10
- } from "./chunk-676GBPMS.js";
10
+ } from "./chunk-L7WS36XV.js";
11
+ import "./chunk-WLFE5RUU.js";
11
12
  export {
12
13
  BaseAdapter,
13
14
  all,
@@ -18,4 +19,3 @@ export {
18
19
  resolve,
19
20
  setDefaultRunner
20
21
  };
21
- //# sourceMappingURL=index.js.map
@@ -1,8 +1,9 @@
1
1
  import { UserConfig, Plugin } from 'vite';
2
2
  export { Plugin, PluginOption, UserConfig, UserConfigExport, defineConfig, loadEnv, mergeConfig } from 'vite';
3
+ export { A as Alias, D as DecoratorMetadataOptions, d as decoratorMetadata, t as tsconfigAliases } from '../../../tsconfig-aliases-Ce6axdJ4.js';
3
4
 
4
5
  /** A transformer as the target declares it, passed through to the config unchanged. */
5
- interface TransformerDeclaration$1 {
6
+ interface TransformerDeclaration {
6
7
  name: string;
7
8
  options?: Record<string, unknown>;
8
9
  }
@@ -41,7 +42,7 @@ interface NestServiceOptions {
41
42
  * without it succeeds and silently costs them most of their published contract (measured on
42
43
  * `institution`: 385 insertions, 1378 deletions in the committed OpenAPI document).
43
44
  */
44
- transformers?: readonly TransformerDeclaration$1[];
45
+ transformers?: readonly TransformerDeclaration[];
45
46
  /**
46
47
  * Files copied into the output beside the bundle, in the shape `@nx/webpack` declared them.
47
48
  *
@@ -60,27 +61,6 @@ interface NestServiceOptions {
60
61
  }
61
62
  declare const nestService: (options: NestServiceOptions) => UserConfig;
62
63
 
63
- /** A TypeScript transformer a service runs at build time, as its build target declares it. */
64
- interface TransformerDeclaration {
65
- /** The package to load it from, e.g. `@nestjs/swagger/plugin`. */
66
- name: string;
67
- /** Passed to the transformer's own factory, unread here. */
68
- options?: Record<string, unknown>;
69
- }
70
- interface DecoratorMetadataOptions {
71
- /** The module being built. Its tsconfig is the one whose emit must be preserved. */
72
- root: string;
73
- /**
74
- * An explicit tsconfig, when the service does not use either conventional name.
75
- *
76
- * Relative paths resolve against `root`.
77
- */
78
- tsconfig?: string;
79
- /** The transformers this service declared, translated from its webpack target. */
80
- transformers?: readonly TransformerDeclaration[];
81
- }
82
- declare const decoratorMetadata: (options: DecoratorMetadataOptions) => Plugin;
83
-
84
64
  interface NodeManifestOptions {
85
65
  /** The nx project name, which is how the graph is keyed. */
86
66
  project: string;
@@ -115,16 +95,4 @@ interface NodeManifestOptions {
115
95
  */
116
96
  declare const nodeManifest: (options: NodeManifestOptions) => Plugin;
117
97
 
118
- interface Alias {
119
- find: RegExp;
120
- replacement: string;
121
- }
122
- /**
123
- * Read the mappings from the workspace's base tsconfig.
124
- *
125
- * Longest pattern first, because Vite takes the first alias that matches and the mappings
126
- * overlap by design: `@front/theme/node` must not be swallowed by `@front/theme`.
127
- */
128
- declare const tsconfigAliases: (workspaceRoot: string, file?: string) => Alias[];
129
-
130
- export { type Alias, type DecoratorMetadataOptions, type NestServiceOptions, type NodeManifestOptions, decoratorMetadata, nestService, nodeManifest, tsconfigAliases };
98
+ export { type NestServiceOptions, type NodeManifestOptions, nestService, nodeManifest };