@hublo/sentinel 1.2.0-alpha.12 → 1.2.0-alpha.14

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.
@@ -4,3 +4,4 @@ import '@vitejs/plugin-react';
4
4
  import 'nitro/vite';
5
5
  import 'vite-plugin-svgr';
6
6
  import 'vite';
7
+ import 'vitest/config';
@@ -22,7 +22,7 @@ import {
22
22
  registerAdapters,
23
23
  resolve,
24
24
  resolveBin
25
- } from "../chunk-I4ATYZKK.js";
25
+ } from "../chunk-RV34GQVH.js";
26
26
 
27
27
  // bin/sentinel.ts
28
28
  import { program } from "commander";
@@ -67,19 +67,6 @@ function resolve(target, preset, runner) {
67
67
  return matching[0];
68
68
  }
69
69
 
70
- // src/core/base-adapter.ts
71
- var BaseAdapter = class {
72
- inspect(_ctx) {
73
- throw new Error(`${this.runner}: --inspect not implemented yet`);
74
- }
75
- report(_ctx) {
76
- throw new Error(`${this.runner}: --report not implemented yet`);
77
- }
78
- status(_ctx) {
79
- throw new Error(`${this.runner}: --status not implemented yet`);
80
- }
81
- };
82
-
83
70
  // src/shared/package-json.ts
84
71
  import { existsSync, readFileSync } from "fs";
85
72
  import { basename, dirname, join } from "path";
@@ -132,6 +119,19 @@ function moduleName(dir) {
132
119
  return readNxProjectName(dir) ?? (declared === void 0 ? basename(dir) : declared);
133
120
  }
134
121
 
122
+ // src/core/base-adapter.ts
123
+ var BaseAdapter = class {
124
+ inspect(_ctx) {
125
+ throw new Error(`${this.runner}: --inspect not implemented yet`);
126
+ }
127
+ report(_ctx) {
128
+ throw new Error(`${this.runner}: --report not implemented yet`);
129
+ }
130
+ status(_ctx) {
131
+ throw new Error(`${this.runner}: --status not implemented yet`);
132
+ }
133
+ };
134
+
135
135
  // src/core/domain.ts
136
136
  var VERBS = ["run", "inspect", "init", "migrate", "report", "status"];
137
137
  var TARGETS = [
@@ -165,11 +165,263 @@ function palette(stream) {
165
165
  };
166
166
  }
167
167
 
168
+ // src/roles/build/nest/nx-target.ts
169
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
170
+ import { join as join2, relative } from "path";
171
+ var DEPRECATED_NX_BUILDER = "@nx/webpack:webpack";
172
+ var TRANSLATABLE_WEBPACK_CONFIG = "tools/webpack-configs/update.webpack.config.js";
173
+ var readProjectJson = (cwd) => {
174
+ const path = join2(cwd, "project.json");
175
+ if (!existsSync2(path)) return void 0;
176
+ try {
177
+ return JSON.parse(readFileSync2(path, "utf8"));
178
+ } catch {
179
+ return void 0;
180
+ }
181
+ };
182
+ function webpackBuildTarget(cwd) {
183
+ const project = readProjectJson(cwd);
184
+ const target = project?.targets?.build;
185
+ if (target?.executor !== DEPRECATED_NX_BUILDER) return void 0;
186
+ const { outputPath, main, webpackConfig } = target.options ?? {};
187
+ if (typeof project?.name !== "string" || typeof outputPath !== "string" || typeof main !== "string" || typeof webpackConfig !== "string") {
188
+ return void 0;
189
+ }
190
+ return {
191
+ project: project.name,
192
+ outputPath,
193
+ main,
194
+ webpackConfig,
195
+ configurations: target.configurations
196
+ };
197
+ }
198
+ function refusalFor(cwd, target) {
199
+ if (workspaceRootFor(cwd, target) === void 0) {
200
+ return `this service's build target declares \`main: ${target.main}\`, which does not place this directory inside a workspace. Every value the config needs is derived from that path, so a wrong one would resolve \`workspaceRoot\` to the service itself and leave the alias table resolving nothing. Nothing was written.`;
201
+ }
202
+ if (target.webpackConfig !== TRANSLATABLE_WEBPACK_CONFIG) {
203
+ return `this service builds with ${target.webpackConfig}, not the shared ${TRANSLATABLE_WEBPACK_CONFIG} this preset reproduces. Its own config adds loaders or rules the preset has no equivalent for, and translating it anyway would produce a build that succeeds and a service that fails later. Read that file, decide what the Vite config needs, and write it by hand. Nothing was written.`;
204
+ }
205
+ const live = liveFileReplacements(cwd, target);
206
+ if (live.length > 0) {
207
+ return `this service's production configuration replaces ${live.join(", ")}, and those files exist. The preset has no equivalent, so translating the target would drop a real substitution. Nothing was written.`;
208
+ }
209
+ return void 0;
210
+ }
211
+ function liveFileReplacements(cwd, target) {
212
+ const workspaceRoot = workspaceRootFor(cwd, target);
213
+ if (workspaceRoot === void 0) return [];
214
+ const configurations = target.configurations ?? {};
215
+ const live = [];
216
+ for (const configuration of Object.values(configurations)) {
217
+ for (const replacement of configuration.fileReplacements ?? []) {
218
+ const exists = existsSync2(join2(workspaceRoot, replacement.replace)) || existsSync2(join2(workspaceRoot, replacement.with));
219
+ if (exists) live.push(replacement.replace);
220
+ }
221
+ }
222
+ return live;
223
+ }
224
+ function workspaceRootFor(cwd, target) {
225
+ const moduleRoot = target.main.replace(/\/src\/.*$/, "");
226
+ if (moduleRoot === target.main || !cwd.endsWith(moduleRoot)) return void 0;
227
+ return cwd.slice(0, cwd.length - moduleRoot.length - 1);
228
+ }
229
+ function workspaceRootHops(cwd, target) {
230
+ const root = workspaceRootFor(cwd, target);
231
+ if (root === void 0) {
232
+ throw new Error(`sentinel build(nest): cannot place ${cwd} in a workspace from ${target.main}`);
233
+ }
234
+ const rel = relative(root, cwd);
235
+ return rel === "" ? 0 : rel.split(/[\\/]/).length;
236
+ }
237
+
238
+ // src/roles/build/read-adoption.ts
239
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
240
+ import { join as join3 } from "path";
241
+
242
+ // src/roles/build/presets/requirements.json
243
+ var requirements_default = {
244
+ svelte: {
245
+ why: "sentinel ships Vite 8, and @sveltejs/vite-plugin-svelte caps at Vite 6 until its major 7",
246
+ requires: {
247
+ "@sveltejs/vite-plugin-svelte": "7.0.0",
248
+ svelte: "5.46.4"
249
+ }
250
+ }
251
+ };
252
+
253
+ // src/roles/build/presets/toolchain.json
254
+ var toolchain_default = {
255
+ $comment: "The build toolchain sentinel owns. One table, because three places used to hold parts of it: the list to remove from a module's package.json, the list of import specifiers to rewrite, and the map from a default import to the name sentinel re-exports it under. A package added to one and not the others gives a removal with no rewrite, or an import pointing at a package the module no longer declares.",
256
+ owned: [
257
+ {
258
+ package: "vite",
259
+ specifier: "vite",
260
+ named: ["defineConfig", "loadEnv", "mergeConfig"],
261
+ types: [
262
+ "Plugin",
263
+ "PluginOption",
264
+ "SassPreprocessorOptions",
265
+ "UserConfig",
266
+ "UserConfigExport"
267
+ ],
268
+ why: "the bundler itself. `defineConfig`, `loadEnv` and `mergeConfig` are what every config in this repo imports from it, and the types travel with them: a config importing PluginOption from a package it no longer declares builds but does not typecheck."
269
+ },
270
+ {
271
+ package: "@vitejs/plugin-react",
272
+ specifier: "@vitejs/plugin-react",
273
+ default: "react",
274
+ why: "the React transform. Bound to the Vite that runs it, so it has to come from the same install."
275
+ },
276
+ {
277
+ package: "@tailwindcss/vite",
278
+ specifier: "@tailwindcss/vite",
279
+ default: "tailwindcss",
280
+ why: "the Tailwind pipeline, identical in all three front apps."
281
+ },
282
+ {
283
+ package: "vite-plugin-svgr",
284
+ specifier: "vite-plugin-svgr",
285
+ default: "svgr",
286
+ why: "SVG as components, identical in all three front apps."
287
+ },
288
+ {
289
+ package: "nitro",
290
+ specifier: "nitro/vite",
291
+ named: ["nitro"],
292
+ why: "the SSR server. Note the package and the import specifier differ, which is exactly the kind of split that made three separate lists disagree."
293
+ }
294
+ ],
295
+ add: [
296
+ {
297
+ package: "@hublo/sentinel",
298
+ why: "the module now gets its toolchain from here, so it declares it. Pinned by the installer rather than by us: `--init` writes the dependency, `pnpm install` resolves it."
299
+ }
300
+ ],
301
+ $notOwned: {
302
+ "@rolldown/plugin-babel": "declared by career and host-admin themselves, not at the root. It supports LocatorJS, a development convenience gated on LOCATORJS=true, not shared build infrastructure. Removing it would remove a feature from two teams, which is their call and must not ride along inside an adoption.",
303
+ "@locator/babel-jsx": "same, and same owners.",
304
+ "@tanstack/react-start": "a framework the apps code against: 48 source files import it directly.",
305
+ "@tanstack/router-plugin": "generates the app's own route tree.",
306
+ "vite-bundle-analyzer": "at the root at 1.3.7 and imported nowhere, the only mention being a commented-out line in host-admin. Not a candidate to re-export, a candidate to delete from the root."
307
+ }
308
+ };
309
+
310
+ // src/roles/build/preset-data.ts
311
+ var REQUIREMENTS = requirements_default;
312
+ var OWNED_PACKAGES = toolchain_default.owned;
313
+
314
+ // src/roles/build/config-policy.ts
315
+ var COMPANION_CONFIG_FILES = [
316
+ "vitest.config.ts",
317
+ "vitest.config.mts",
318
+ "vitest.config.js",
319
+ "vitest.config.mjs"
320
+ ];
321
+ var BUILD_CONFIG_FILES = [
322
+ "vite.config.ts",
323
+ "vite.config.mts",
324
+ "vite.config.js",
325
+ "vite.config.mjs"
326
+ ];
327
+ var BUILD_PRESET_SPECIFIERS = {
328
+ react: "@hublo/sentinel/build/react",
329
+ nest: "@hublo/sentinel/build/nest"
330
+ };
331
+ var BUILD_PRESET_SPECIFIER = BUILD_PRESET_SPECIFIERS.react;
332
+ var BUILD_SCRIPT_NAME = "build";
333
+ var SENTINEL_BUILD_COMMAND = "sentinel --run --build --";
334
+ var DEV_SCRIPT_NAME = "serve";
335
+ var SENTINEL_DEV_COMMAND = "sentinel --run --dev";
336
+ function buildTarget(configFiles = BUILD_CONFIG_FILES) {
337
+ return {
338
+ [BUILD_SCRIPT_NAME]: {
339
+ cache: true,
340
+ inputs: ["default", "^default", ...configFiles.map((name) => `{projectRoot}/${name}`)]
341
+ }
342
+ };
343
+ }
344
+ var SENTINEL_OWNED_BUILD_PACKAGES = OWNED_PACKAGES.map(
345
+ (entry) => entry.package
346
+ );
347
+
348
+ // src/roles/build/read-adoption.ts
349
+ var NOT_ADOPTED = (configFile, unreadable = null) => ({
350
+ configFile,
351
+ preset: null,
352
+ adopted: false,
353
+ conformant: false,
354
+ drift: [],
355
+ ownDeclarations: [],
356
+ unreadable
357
+ });
358
+ function buildConfigFile(cwd) {
359
+ return BUILD_CONFIG_FILES.find((name) => existsSync3(join3(cwd, name)));
360
+ }
361
+ function importsSpecifier(source, specifier) {
362
+ const escaped = specifier.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
363
+ return new RegExp(`(?:from|import)\\s*\\(?\\s*['"\`]${escaped}(?:/[^'"\`]*)?['"\`]`).test(source);
364
+ }
365
+ function importsPreset(source) {
366
+ return Object.values(BUILD_PRESET_SPECIFIERS).some(
367
+ (specifier) => importsSpecifier(source, specifier)
368
+ );
369
+ }
370
+ var REACT_PLUGIN_SPECIFIERS = ["@vitejs/plugin-react", "@tanstack/react-start"];
371
+ function declaredBuildPreset(cwd) {
372
+ const source = readBuildConfigSource(cwd);
373
+ if (source === void 0) {
374
+ if (buildConfigFile(cwd) !== void 0) return void 0;
375
+ return webpackBuildTarget(cwd) === void 0 ? void 0 : "nest";
376
+ }
377
+ if (importsSpecifier(source, BUILD_PRESET_SPECIFIERS.nest)) return "nest";
378
+ if (importsPreset(source)) return "react";
379
+ return REACT_PLUGIN_SPECIFIERS.some((specifier) => importsSpecifier(source, specifier)) ? "react" : void 0;
380
+ }
381
+ function readBuildConfigSource(cwd) {
382
+ const configFile = buildConfigFile(cwd);
383
+ if (!configFile) return void 0;
384
+ try {
385
+ return readFileSync3(join3(cwd, configFile), "utf8");
386
+ } catch {
387
+ return void 0;
388
+ }
389
+ }
390
+ function readBuildAdoption(cwd) {
391
+ const configFile = buildConfigFile(cwd);
392
+ if (!configFile) return NOT_ADOPTED(null);
393
+ let source;
394
+ try {
395
+ source = readFileSync3(join3(cwd, configFile), "utf8");
396
+ } catch (error) {
397
+ return NOT_ADOPTED(configFile, error instanceof Error ? error.message : String(error));
398
+ }
399
+ if (!importsPreset(source)) return NOT_ADOPTED(configFile);
400
+ const manifest = readProjectPackageJson(cwd);
401
+ const declared = { ...manifest.dependencies, ...manifest.devDependencies };
402
+ const ownDeclarations = SENTINEL_OWNED_BUILD_PACKAGES.filter((name) => name in declared).map(
403
+ (name) => ({ name, version: declared[name] })
404
+ );
405
+ return {
406
+ configFile,
407
+ // Read from the config rather than assumed: the two presets are adopted the same way and
408
+ // reporting the wrong one would make `--inspect` lie about what a module builds.
409
+ preset: declaredBuildPreset(cwd) ?? "react",
410
+ adopted: true,
411
+ conformant: ownDeclarations.length === 0,
412
+ drift: ownDeclarations.map(
413
+ ({ name, version }) => `declares ${name}@${version} of its own; sentinel owns it, and two copies in one build give the plugins a different Vite than the one running them`
414
+ ),
415
+ ownDeclarations,
416
+ unreadable: null
417
+ };
418
+ }
419
+
168
420
  // src/roles/build/adapters/vite/vite-role.adapter.ts
169
421
  import { spawnSync } from "child_process";
170
422
 
171
423
  // src/core/config/tool-args.ts
172
- import { existsSync as existsSync2 } from "fs";
424
+ import { existsSync as existsSync4 } from "fs";
173
425
  import { isAbsolute, resolve as resolve2 } from "path";
174
426
  function splitToolArgs(cwd, toolArgs = [], { valueFlags = [] } = {}) {
175
427
  const takesValue = new Set(valueFlags);
@@ -179,7 +431,7 @@ function splitToolArgs(cwd, toolArgs = [], { valueFlags = [] } = {}) {
179
431
  for (const arg of toolArgs) {
180
432
  const looksLikeOption = arg.startsWith("-");
181
433
  const target = isAbsolute(arg) ? arg : resolve2(cwd, arg);
182
- if (!previousTakesValue && !looksLikeOption && existsSync2(target)) paths.push(arg);
434
+ if (!previousTakesValue && !looksLikeOption && existsSync4(target)) paths.push(arg);
183
435
  else options.push(arg);
184
436
  previousTakesValue = !arg.includes("=") && takesValue.has(arg);
185
437
  }
@@ -187,15 +439,15 @@ function splitToolArgs(cwd, toolArgs = [], { valueFlags = [] } = {}) {
187
439
  }
188
440
 
189
441
  // src/shared/resolve-bin.ts
190
- import { existsSync as existsSync3 } from "fs";
442
+ import { existsSync as existsSync5 } from "fs";
191
443
  import { createRequire } from "module";
192
- import { delimiter, dirname as dirname2, join as join2 } from "path";
444
+ import { delimiter, dirname as dirname2, join as join4 } from "path";
193
445
  var require2 = createRequire(import.meta.url);
194
446
  function resolveBin(fromDir, name) {
195
447
  let dir = fromDir;
196
448
  for (; ; ) {
197
- const candidate = join2(dir, "node_modules", ".bin", name);
198
- if (existsSync3(candidate)) return candidate;
449
+ const candidate = join4(dir, "node_modules", ".bin", name);
450
+ if (existsSync5(candidate)) return candidate;
199
451
  const parent = dirname2(dir);
200
452
  if (parent === dir) return void 0;
201
453
  dir = parent;
@@ -207,8 +459,8 @@ function binFromOwnInstall(packageName, binName) {
207
459
  const bin = require2(manifest).bin;
208
460
  const relative6 = typeof bin === "string" ? bin : bin?.[binName];
209
461
  if (!relative6) return void 0;
210
- const executable = join2(dirname2(manifest), relative6);
211
- return existsSync3(executable) ? executable : void 0;
462
+ const executable = join4(dirname2(manifest), relative6);
463
+ return existsSync5(executable) ? executable : void 0;
212
464
  } catch {
213
465
  return void 0;
214
466
  }
@@ -224,8 +476,8 @@ import { existsSync as existsSync8, readFileSync as readFileSync6 } from "fs";
224
476
  import { join as join8 } from "path";
225
477
 
226
478
  // src/core/config/existing-command.ts
227
- import { readFileSync as readFileSync3 } from "fs";
228
- import { join as join4 } from "path";
479
+ import { readFileSync as readFileSync5 } from "fs";
480
+ import { join as join6 } from "path";
229
481
 
230
482
  // src/shared/jsonc.ts
231
483
  import { parse, printParseErrorCode } from "jsonc-parser";
@@ -240,11 +492,11 @@ function parseJsonc(text, source = "config") {
240
492
  }
241
493
 
242
494
  // src/core/config/manifest.ts
243
- import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
244
- import { basename as basename2, join as join3 } from "path";
495
+ import { existsSync as existsSync6, readFileSync as readFileSync4 } from "fs";
496
+ import { basename as basename2, join as join5 } from "path";
245
497
  function moduleScripts(cwd) {
246
498
  try {
247
- const pkg = JSON.parse(readFileSync2(join3(cwd, "package.json"), "utf8"));
499
+ const pkg = JSON.parse(readFileSync4(join5(cwd, "package.json"), "utf8"));
248
500
  return pkg.scripts ?? {};
249
501
  } catch {
250
502
  return {};
@@ -252,7 +504,7 @@ function moduleScripts(cwd) {
252
504
  }
253
505
  function moduleName2(cwd) {
254
506
  try {
255
- const pkg = JSON.parse(readFileSync2(join3(cwd, "package.json"), "utf8"));
507
+ const pkg = JSON.parse(readFileSync4(join5(cwd, "package.json"), "utf8"));
256
508
  return pkg.name;
257
509
  } catch {
258
510
  return void 0;
@@ -272,7 +524,7 @@ function manifestOperation(cwd, scripts) {
272
524
  scripts,
273
525
  devDependencies: { [own.name]: isSelfAdoption(cwd) ? "link:." : own.version }
274
526
  };
275
- if (existsSync4(join3(cwd, "package.json"))) {
527
+ if (existsSync6(join5(cwd, "package.json"))) {
276
528
  return { kind: "merge-json", path: "package.json", value };
277
529
  }
278
530
  return {
@@ -287,7 +539,7 @@ function nxTargetCommand(cwd, target) {
287
539
  let project;
288
540
  try {
289
541
  project = parseJsonc(
290
- readFileSync3(join4(cwd, "project.json"), "utf8"),
542
+ readFileSync5(join6(cwd, "project.json"), "utf8"),
291
543
  "project.json"
292
544
  );
293
545
  } catch {
@@ -300,193 +552,88 @@ function nxTargetCommand(cwd, target) {
300
552
  if (typeof command === "string" && command.trim() !== "") {
301
553
  return { command: command.trim(), ranInModule };
302
554
  }
303
- if (Array.isArray(commands)) {
304
- const parts = commands.filter((c) => typeof c === "string" && c.trim() !== "");
305
- if (parts.length > 0) return { command: parts.map((c) => c.trim()).join(" && "), ranInModule };
306
- }
307
- return void 0;
308
- }
309
- function rootedForScript(command) {
310
- return command.replace(/(^|&&\s*)pnpm exec /g, "$1pnpm --workspace-root exec ");
311
- }
312
- function existingCommand(cwd, target) {
313
- const script = moduleScripts(cwd)[target];
314
- if (script !== void 0 && script.trim() !== "") return script;
315
- const fromTarget = nxTargetCommand(cwd, target);
316
- if (fromTarget === void 0) return void 0;
317
- return fromTarget.ranInModule ? fromTarget.command : rootedForScript(fromTarget.command);
318
- }
319
-
320
- // src/core/config/nx-target.ts
321
- import { existsSync as existsSync5 } from "fs";
322
- import { join as join5 } from "path";
323
- function nxTargetOperations(options) {
324
- const { cwd, targets } = options;
325
- const names = Object.keys(targets);
326
- if (names.length === 0) return [];
327
- const operations = [];
328
- if (existsSync5(join5(cwd, "project.json"))) {
329
- operations.push({
330
- kind: "remove-json-keys",
331
- path: "project.json",
332
- keys: names.map((name) => ["targets", name])
333
- });
334
- }
335
- operations.push({
336
- kind: "merge-json",
337
- path: "package.json",
338
- value: { nx: { targets } }
339
- });
340
- return operations;
341
- }
342
-
343
- // src/core/config/tool-script.ts
344
- var SEGMENT_SEPARATOR = /(\s*(?:&&|\|\||;|&|\|)\s*)/;
345
- function isSeparatorAt(index) {
346
- return index % 2 === 1;
347
- }
348
- function invokes(segment, binary) {
349
- return new RegExp(String.raw`(^|[\s/])${binary}(\s|$)`).test(segment.trim());
350
- }
351
- function isSentinelSegment(segment, roleFlag) {
352
- const runsSentinel = invokes(segment, "sentinel") || segment.includes("bin/sentinel.js");
353
- return runsSentinel && new RegExp(String.raw`(^|\s)${roleFlag}(\s|$)`).test(segment);
354
- }
355
- function composeToolScript(existing, options) {
356
- const { command } = options;
357
- if (!existing || existing.trim() === "") return command;
358
- const { roleFlag } = options;
359
- const isReplaceable = (segment) => options.replaces(segment) || roleFlag !== void 0 && isSentinelSegment(segment, roleFlag);
360
- const parts = existing.split(SEGMENT_SEPARATOR);
361
- const commands = parts.filter((_, index) => !isSeparatorAt(index));
362
- if (!commands.some(isReplaceable)) return existing;
363
- let replacedOnce = false;
364
- const rebuilt = parts.map((part, index) => {
365
- if (isSeparatorAt(index) || !isReplaceable(part)) return part;
366
- if (replacedOnce) return null;
367
- replacedOnce = true;
368
- return options.keepFix && /(^|\s)--fix(\s|$)/.test(part) ? `${command} --fix` : command;
369
- });
370
- const kept = [];
371
- for (let index = 0; index < rebuilt.length; index += 1) {
372
- const part = rebuilt[index];
373
- if (part === null) {
374
- if (kept.length > 0) kept.pop();
375
- continue;
376
- }
377
- kept.push(part);
378
- }
379
- return kept.join("").trim();
380
- }
381
- function keepsOtherCommands(script, sentinelCommand) {
382
- return script.split(SEGMENT_SEPARATOR).filter((_, index) => !isSeparatorAt(index)).some((segment) => segment.trim() !== "" && segment.trim() !== sentinelCommand);
383
- }
384
-
385
- // src/roles/build/presets/requirements.json
386
- var requirements_default = {
387
- svelte: {
388
- why: "sentinel ships Vite 8, and @sveltejs/vite-plugin-svelte caps at Vite 6 until its major 7",
389
- requires: {
390
- "@sveltejs/vite-plugin-svelte": "7.0.0",
391
- svelte: "5.46.4"
392
- }
393
- }
394
- };
395
-
396
- // src/roles/build/presets/toolchain.json
397
- var toolchain_default = {
398
- $comment: "The build toolchain sentinel owns. One table, because three places used to hold parts of it: the list to remove from a module's package.json, the list of import specifiers to rewrite, and the map from a default import to the name sentinel re-exports it under. A package added to one and not the others gives a removal with no rewrite, or an import pointing at a package the module no longer declares.",
399
- owned: [
400
- {
401
- package: "vite",
402
- specifier: "vite",
403
- named: ["defineConfig", "loadEnv", "mergeConfig"],
404
- types: [
405
- "Plugin",
406
- "PluginOption",
407
- "SassPreprocessorOptions",
408
- "UserConfig",
409
- "UserConfigExport"
410
- ],
411
- why: "the bundler itself. `defineConfig`, `loadEnv` and `mergeConfig` are what every config in this repo imports from it, and the types travel with them: a config importing PluginOption from a package it no longer declares builds but does not typecheck."
412
- },
413
- {
414
- package: "@vitejs/plugin-react",
415
- specifier: "@vitejs/plugin-react",
416
- default: "react",
417
- why: "the React transform. Bound to the Vite that runs it, so it has to come from the same install."
418
- },
419
- {
420
- package: "@tailwindcss/vite",
421
- specifier: "@tailwindcss/vite",
422
- default: "tailwindcss",
423
- why: "the Tailwind pipeline, identical in all three front apps."
424
- },
425
- {
426
- package: "vite-plugin-svgr",
427
- specifier: "vite-plugin-svgr",
428
- default: "svgr",
429
- why: "SVG as components, identical in all three front apps."
430
- },
431
- {
432
- package: "nitro",
433
- specifier: "nitro/vite",
434
- named: ["nitro"],
435
- why: "the SSR server. Note the package and the import specifier differ, which is exactly the kind of split that made three separate lists disagree."
436
- }
437
- ],
438
- add: [
439
- {
440
- package: "@hublo/sentinel",
441
- why: "the module now gets its toolchain from here, so it declares it. Pinned by the installer rather than by us: `--init` writes the dependency, `pnpm install` resolves it."
442
- }
443
- ],
444
- $notOwned: {
445
- "@rolldown/plugin-babel": "declared by career and host-admin themselves, not at the root. It supports LocatorJS, a development convenience gated on LOCATORJS=true, not shared build infrastructure. Removing it would remove a feature from two teams, which is their call and must not ride along inside an adoption.",
446
- "@locator/babel-jsx": "same, and same owners.",
447
- "@tanstack/react-start": "a framework the apps code against: 48 source files import it directly.",
448
- "@tanstack/router-plugin": "generates the app's own route tree.",
449
- "vite-bundle-analyzer": "at the root at 1.3.7 and imported nowhere, the only mention being a commented-out line in host-admin. Not a candidate to re-export, a candidate to delete from the root."
555
+ if (Array.isArray(commands)) {
556
+ const parts = commands.filter((c) => typeof c === "string" && c.trim() !== "");
557
+ if (parts.length > 0) return { command: parts.map((c) => c.trim()).join(" && "), ranInModule };
450
558
  }
451
- };
559
+ return void 0;
560
+ }
561
+ function rootedForScript(command) {
562
+ return command.replace(/(^|&&\s*)pnpm exec /g, "$1pnpm --workspace-root exec ");
563
+ }
564
+ function existingCommand(cwd, target) {
565
+ const script = moduleScripts(cwd)[target];
566
+ if (script !== void 0 && script.trim() !== "") return script;
567
+ const fromTarget = nxTargetCommand(cwd, target);
568
+ if (fromTarget === void 0) return void 0;
569
+ return fromTarget.ranInModule ? fromTarget.command : rootedForScript(fromTarget.command);
570
+ }
452
571
 
453
- // src/roles/build/preset-data.ts
454
- var REQUIREMENTS = requirements_default;
455
- var OWNED_PACKAGES = toolchain_default.owned;
572
+ // src/core/config/nx-target.ts
573
+ import { existsSync as existsSync7 } from "fs";
574
+ import { join as join7 } from "path";
575
+ function nxTargetMetadataOperations(options) {
576
+ const { targets } = options;
577
+ if (Object.keys(targets).length === 0) return [];
578
+ return [{ kind: "merge-json", path: "package.json", value: { nx: { targets } } }];
579
+ }
580
+ function nxTargetOperations(options) {
581
+ const { cwd, targets } = options;
582
+ const names = Object.keys(targets);
583
+ if (names.length === 0) return [];
584
+ const operations = [];
585
+ if (existsSync7(join7(cwd, "project.json"))) {
586
+ operations.push({
587
+ kind: "remove-json-keys",
588
+ path: "project.json",
589
+ keys: names.map((name) => ["targets", name])
590
+ });
591
+ }
592
+ operations.push(...nxTargetMetadataOperations(options));
593
+ return operations;
594
+ }
456
595
 
457
- // src/roles/build/config-policy.ts
458
- var COMPANION_CONFIG_FILES = [
459
- "vitest.config.ts",
460
- "vitest.config.mts",
461
- "vitest.config.js",
462
- "vitest.config.mjs"
463
- ];
464
- var BUILD_CONFIG_FILES = [
465
- "vite.config.ts",
466
- "vite.config.mts",
467
- "vite.config.js",
468
- "vite.config.mjs"
469
- ];
470
- var BUILD_PRESET_SPECIFIERS = {
471
- react: "@hublo/sentinel/build/react",
472
- nest: "@hublo/sentinel/build/nest"
473
- };
474
- var BUILD_PRESET_SPECIFIER = BUILD_PRESET_SPECIFIERS.react;
475
- var BUILD_SCRIPT_NAME = "build";
476
- var SENTINEL_BUILD_COMMAND = "sentinel --run --build --";
477
- var DEV_SCRIPT_NAME = "serve";
478
- var SENTINEL_DEV_COMMAND = "sentinel --run --dev";
479
- function buildTarget() {
480
- return {
481
- [BUILD_SCRIPT_NAME]: {
482
- cache: true,
483
- inputs: ["default", "^default", ...BUILD_CONFIG_FILES.map((name) => `{projectRoot}/${name}`)]
596
+ // src/core/config/tool-script.ts
597
+ var SEGMENT_SEPARATOR = /(\s*(?:&&|\|\||;|&|\|)\s*)/;
598
+ function isSeparatorAt(index) {
599
+ return index % 2 === 1;
600
+ }
601
+ function invokes(segment, binary) {
602
+ return new RegExp(String.raw`(^|[\s/])${binary}(\s|$)`).test(segment.trim());
603
+ }
604
+ function isSentinelSegment(segment, roleFlag) {
605
+ const runsSentinel = invokes(segment, "sentinel") || segment.includes("bin/sentinel.js");
606
+ return runsSentinel && new RegExp(String.raw`(^|\s)${roleFlag}(\s|$)`).test(segment);
607
+ }
608
+ function composeToolScript(existing, options) {
609
+ const { command } = options;
610
+ if (!existing || existing.trim() === "") return command;
611
+ const { roleFlag } = options;
612
+ const isReplaceable = (segment) => options.replaces(segment) || roleFlag !== void 0 && isSentinelSegment(segment, roleFlag);
613
+ const parts = existing.split(SEGMENT_SEPARATOR);
614
+ const commands = parts.filter((_, index) => !isSeparatorAt(index));
615
+ if (!commands.some(isReplaceable)) return existing;
616
+ let replacedOnce = false;
617
+ const rebuilt = parts.map((part, index) => {
618
+ if (isSeparatorAt(index) || !isReplaceable(part)) return part;
619
+ if (replacedOnce) return null;
620
+ replacedOnce = true;
621
+ return options.keepFix && /(^|\s)--fix(\s|$)/.test(part) ? `${command} --fix` : command;
622
+ });
623
+ const kept = [];
624
+ for (let index = 0; index < rebuilt.length; index += 1) {
625
+ const part = rebuilt[index];
626
+ if (part === null) {
627
+ if (kept.length > 0) kept.pop();
628
+ continue;
484
629
  }
485
- };
630
+ kept.push(part);
631
+ }
632
+ return kept.join("").trim();
633
+ }
634
+ function keepsOtherCommands(script, sentinelCommand) {
635
+ return script.split(SEGMENT_SEPARATOR).filter((_, index) => !isSeparatorAt(index)).some((segment) => segment.trim() !== "" && segment.trim() !== sentinelCommand);
486
636
  }
487
- var SENTINEL_OWNED_BUILD_PACKAGES = OWNED_PACKAGES.map(
488
- (entry) => entry.package
489
- );
490
637
 
491
638
  // src/roles/build/dev-script.ts
492
639
  var CHAIN = " -- ";
@@ -510,78 +657,6 @@ function composeDevScript(existing, command) {
510
657
 
511
658
  // src/roles/build/nest/adopt.ts
512
659
  import { relative as relative2 } from "path";
513
-
514
- // src/roles/build/nest/nx-target.ts
515
- import { existsSync as existsSync6, readFileSync as readFileSync4 } from "fs";
516
- import { join as join6, relative } from "path";
517
- var DEPRECATED_NX_BUILDER = "@nx/webpack:webpack";
518
- var TRANSLATABLE_WEBPACK_CONFIG = "tools/webpack-configs/update.webpack.config.js";
519
- var readProjectJson = (cwd) => {
520
- const path = join6(cwd, "project.json");
521
- if (!existsSync6(path)) return void 0;
522
- try {
523
- return JSON.parse(readFileSync4(path, "utf8"));
524
- } catch {
525
- return void 0;
526
- }
527
- };
528
- function webpackBuildTarget(cwd) {
529
- const project = readProjectJson(cwd);
530
- const target = project?.targets?.build;
531
- if (target?.executor !== DEPRECATED_NX_BUILDER) return void 0;
532
- const { outputPath, main, webpackConfig } = target.options ?? {};
533
- if (typeof project?.name !== "string" || typeof outputPath !== "string" || typeof main !== "string" || typeof webpackConfig !== "string") {
534
- return void 0;
535
- }
536
- return {
537
- project: project.name,
538
- outputPath,
539
- main,
540
- webpackConfig,
541
- configurations: target.configurations
542
- };
543
- }
544
- function refusalFor(cwd, target) {
545
- if (workspaceRootFor(cwd, target) === void 0) {
546
- return `this service's build target declares \`main: ${target.main}\`, which does not place this directory inside a workspace. Every value the config needs is derived from that path, so a wrong one would resolve \`workspaceRoot\` to the service itself and leave the alias table resolving nothing. Nothing was written.`;
547
- }
548
- if (target.webpackConfig !== TRANSLATABLE_WEBPACK_CONFIG) {
549
- return `this service builds with ${target.webpackConfig}, not the shared ${TRANSLATABLE_WEBPACK_CONFIG} this preset reproduces. Its own config adds loaders or rules the preset has no equivalent for, and translating it anyway would produce a build that succeeds and a service that fails later. Read that file, decide what the Vite config needs, and write it by hand. Nothing was written.`;
550
- }
551
- const live = liveFileReplacements(cwd, target);
552
- if (live.length > 0) {
553
- return `this service's production configuration replaces ${live.join(", ")}, and those files exist. The preset has no equivalent, so translating the target would drop a real substitution. Nothing was written.`;
554
- }
555
- return void 0;
556
- }
557
- function liveFileReplacements(cwd, target) {
558
- const workspaceRoot = workspaceRootFor(cwd, target);
559
- if (workspaceRoot === void 0) return [];
560
- const configurations = target.configurations ?? {};
561
- const live = [];
562
- for (const configuration of Object.values(configurations)) {
563
- for (const replacement of configuration.fileReplacements ?? []) {
564
- const exists = existsSync6(join6(workspaceRoot, replacement.replace)) || existsSync6(join6(workspaceRoot, replacement.with));
565
- if (exists) live.push(replacement.replace);
566
- }
567
- }
568
- return live;
569
- }
570
- function workspaceRootFor(cwd, target) {
571
- const moduleRoot = target.main.replace(/\/src\/.*$/, "");
572
- if (moduleRoot === target.main || !cwd.endsWith(moduleRoot)) return void 0;
573
- return cwd.slice(0, cwd.length - moduleRoot.length - 1);
574
- }
575
- function workspaceRootHops(cwd, target) {
576
- const root = workspaceRootFor(cwd, target);
577
- if (root === void 0) {
578
- throw new Error(`sentinel build(nest): cannot place ${cwd} in a workspace from ${target.main}`);
579
- }
580
- const rel = relative(root, cwd);
581
- return rel === "" ? 0 : rel.split(/[\\/]/).length;
582
- }
583
-
584
- // src/roles/build/nest/adopt.ts
585
660
  var NEST_CONFIG_FILE = "vite.config.mts";
586
661
  function nestConfigSource(cwd, target) {
587
662
  const hops = workspaceRootHops(cwd, target);
@@ -628,80 +703,6 @@ function nestAdoptionOperations(cwd, target) {
628
703
  ];
629
704
  }
630
705
 
631
- // src/roles/build/read-adoption.ts
632
- import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
633
- import { join as join7 } from "path";
634
- var NOT_ADOPTED = (configFile, unreadable = null) => ({
635
- configFile,
636
- preset: null,
637
- adopted: false,
638
- conformant: false,
639
- drift: [],
640
- ownDeclarations: [],
641
- unreadable
642
- });
643
- function buildConfigFile(cwd) {
644
- return BUILD_CONFIG_FILES.find((name) => existsSync7(join7(cwd, name)));
645
- }
646
- function importsSpecifier(source, specifier) {
647
- const escaped = specifier.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
648
- return new RegExp(`(?:from|import)\\s*\\(?\\s*['"\`]${escaped}(?:/[^'"\`]*)?['"\`]`).test(source);
649
- }
650
- function importsPreset(source) {
651
- return Object.values(BUILD_PRESET_SPECIFIERS).some(
652
- (specifier) => importsSpecifier(source, specifier)
653
- );
654
- }
655
- var REACT_PLUGIN_SPECIFIERS = ["@vitejs/plugin-react", "@tanstack/react-start"];
656
- function declaredBuildPreset(cwd) {
657
- const source = readBuildConfigSource(cwd);
658
- if (source === void 0) {
659
- if (buildConfigFile(cwd) !== void 0) return void 0;
660
- return webpackBuildTarget(cwd) === void 0 ? void 0 : "nest";
661
- }
662
- if (importsSpecifier(source, BUILD_PRESET_SPECIFIERS.nest)) return "nest";
663
- if (importsPreset(source)) return "react";
664
- return REACT_PLUGIN_SPECIFIERS.some((specifier) => importsSpecifier(source, specifier)) ? "react" : void 0;
665
- }
666
- function readBuildConfigSource(cwd) {
667
- const configFile = buildConfigFile(cwd);
668
- if (!configFile) return void 0;
669
- try {
670
- return readFileSync5(join7(cwd, configFile), "utf8");
671
- } catch {
672
- return void 0;
673
- }
674
- }
675
- function readBuildAdoption(cwd) {
676
- const configFile = buildConfigFile(cwd);
677
- if (!configFile) return NOT_ADOPTED(null);
678
- let source;
679
- try {
680
- source = readFileSync5(join7(cwd, configFile), "utf8");
681
- } catch (error) {
682
- return NOT_ADOPTED(configFile, error instanceof Error ? error.message : String(error));
683
- }
684
- if (!importsPreset(source)) return NOT_ADOPTED(configFile);
685
- const manifest = readProjectPackageJson(cwd);
686
- const declared = { ...manifest.dependencies, ...manifest.devDependencies };
687
- const ownDeclarations = SENTINEL_OWNED_BUILD_PACKAGES.filter((name) => name in declared).map(
688
- (name) => ({ name, version: declared[name] })
689
- );
690
- return {
691
- configFile,
692
- // Read from the config rather than assumed: the two presets are adopted the same way and
693
- // reporting the wrong one would make `--inspect` lie about what a module builds.
694
- preset: declaredBuildPreset(cwd) ?? "react",
695
- adopted: true,
696
- conformant: ownDeclarations.length === 0,
697
- drift: ownDeclarations.map(
698
- ({ name, version }) => `declares ${name}@${version} of its own; sentinel owns it, and two copies in one build give the plugins a different Vite than the one running them`
699
- ),
700
- ownDeclarations,
701
- unreadable: null
702
- };
703
- }
704
-
705
706
  // src/roles/build/requirements.ts
706
707
  function atLeast(declared, needed) {
707
708
  const parse2 = (value) => value.replace(/^[\^~>=<\s]+/, "").split("-")[0].split(".").map((part) => Number.parseInt(part, 10) || 0);
@@ -730,32 +731,31 @@ function describeUnmet(preset, unmet) {
730
731
  return `the build role does not apply to this module yet: ${list}. ${entry?.why ?? ""}. Nothing was written. Raise those versions, check the module still builds and its tests still pass, then re-run \`sentinel --init --build\`.`;
731
732
  }
732
733
 
733
- // src/roles/build/rewrite-imports.ts
734
+ // src/core/config/rewrite-imports.ts
734
735
  import { parseSync } from "oxc-parser";
735
- var MOVED_SPECIFIERS = new Set(OWNED_PACKAGES.map((entry) => entry.specifier));
736
+ var movedSpecifiers = (toolchain) => new Set(toolchain.owned.map((entry) => entry.specifier));
736
737
  function esmBlocker(fileName, packageType) {
737
738
  if (/\.(mts|mjs)$/.test(fileName)) return void 0;
738
739
  if (packageType === "module") return void 0;
739
740
  return `sentinel is ESM only, and this module does not declare \`"type": "module"\`, so Vite would load ${fileName} as CommonJS and fail to require it. Add \`"type": "module"\` to this module's package.json, or rename the config to \`${fileName.replace(/\.(ts|js)$/, ".m$1")}\`, then run this again.`;
740
741
  }
741
- var DEFAULT_EXPORT_NAMES = Object.fromEntries(
742
- OWNED_PACKAGES.filter((entry) => entry.default !== void 0).map((entry) => [
743
- entry.specifier,
744
- entry.default
745
- ])
742
+ var defaultExportNames = (toolchain) => Object.fromEntries(
743
+ toolchain.owned.filter((entry) => entry.default !== void 0).map((entry) => [entry.specifier, entry.default])
746
744
  );
747
- function renderImport(bindings) {
745
+ function renderImport(bindings, presetSpecifier) {
748
746
  const render = (list) => list.map((b) => b.exported === b.local ? b.exported : `${b.exported} as ${b.local}`).sort((left, right) => left.localeCompare(right)).join(", ");
749
747
  const values = bindings.filter((b) => !b.typeOnly);
750
748
  const types = bindings.filter((b) => b.typeOnly);
751
749
  const lines = [];
752
- if (values.length > 0) lines.push(`import { ${render(values)} } from '${BUILD_PRESET_SPECIFIER}'`);
750
+ if (values.length > 0) lines.push(`import { ${render(values)} } from '${presetSpecifier}'`);
753
751
  if (types.length > 0) {
754
- lines.push(`import type { ${render(types)} } from '${BUILD_PRESET_SPECIFIER}'`);
752
+ lines.push(`import type { ${render(types)} } from '${presetSpecifier}'`);
755
753
  }
756
754
  return lines.join("\n");
757
755
  }
758
- function rewriteBuildImports(source, fileName = "vite.config.ts") {
756
+ function rewriteToolchainImports(toolchain, source, fileName) {
757
+ const moved = movedSpecifiers(toolchain);
758
+ const defaults = defaultExportNames(toolchain);
759
759
  const { program, errors } = parseSync(fileName, source, { sourceType: "module" });
760
760
  if (errors.length > 0) {
761
761
  return {
@@ -766,10 +766,10 @@ function rewriteBuildImports(source, fileName = "vite.config.ts") {
766
766
  const imports = program.body.filter(
767
767
  (node) => node.type === "ImportDeclaration"
768
768
  );
769
- if (imports.some((node) => node.source.value === BUILD_PRESET_SPECIFIER)) {
769
+ if (imports.some((node) => node.source.value === toolchain.presetSpecifier)) {
770
770
  return { kind: "unchanged", why: "this config already imports its toolchain from sentinel" };
771
771
  }
772
- const moving = imports.filter((node) => MOVED_SPECIFIERS.has(node.source.value));
772
+ const moving = imports.filter((node) => moved.has(node.source.value));
773
773
  if (moving.length === 0) {
774
774
  return {
775
775
  kind: "unchanged",
@@ -786,7 +786,7 @@ function rewriteBuildImports(source, fileName = "vite.config.ts") {
786
786
  for (const spec of specifiers) {
787
787
  const typeOnly = node.importKind === "type" || spec.importKind === "type";
788
788
  if (spec.type === "ImportDefaultSpecifier") {
789
- const exported = DEFAULT_EXPORT_NAMES[from];
789
+ const exported = defaults[from];
790
790
  if (!exported) {
791
791
  return {
792
792
  kind: "blocked",
@@ -818,7 +818,7 @@ function rewriteBuildImports(source, fileName = "vite.config.ts") {
818
818
  const after = out.slice(span.end).match(/^\r?\n/)?.[0].length ?? 0;
819
819
  out = out.slice(0, span.start) + out.slice(span.end + after);
820
820
  }
821
- out = out.slice(0, first.start) + renderImport(bindings) + out.slice(first.end);
821
+ out = out.slice(0, first.start) + renderImport(bindings, toolchain.presetSpecifier) + out.slice(first.end);
822
822
  return {
823
823
  kind: "rewritten",
824
824
  source: out,
@@ -826,6 +826,15 @@ function rewriteBuildImports(source, fileName = "vite.config.ts") {
826
826
  };
827
827
  }
828
828
 
829
+ // src/roles/build/rewrite-imports.ts
830
+ var BUILD_TOOLCHAIN = {
831
+ presetSpecifier: BUILD_PRESET_SPECIFIER,
832
+ owned: OWNED_PACKAGES
833
+ };
834
+ function rewriteBuildImports(source, fileName = "vite.config.ts") {
835
+ return rewriteToolchainImports(BUILD_TOOLCHAIN, source, fileName);
836
+ }
837
+
829
838
  // src/roles/build/plan.ts
830
839
  function ownedBuildDependencies(cwd) {
831
840
  const manifest = readProjectPackageJson(cwd);
@@ -889,7 +898,7 @@ function plan(context) {
889
898
  if (configFile === void 0) {
890
899
  return {
891
900
  operations: [],
892
- skipped: `no Vite config in this module, so there is nothing to point at sentinel. This role moves an existing config's imports; it does not introduce a bundler. If this module builds another way (36 services here build through \`@nx/webpack\`), the build role does not apply to it.`
901
+ skipped: `no Vite config in this module, so there is nothing to point at sentinel. This role moves an existing config's imports; it does not introduce a bundler. If this module builds another way (38 services here build through \`@nx/webpack\`), the build role does not apply to it.`
893
902
  };
894
903
  }
895
904
  return planReactApp(context, configFile);
@@ -949,7 +958,16 @@ function planNestService(context, configFile, webpackTarget) {
949
958
  if (refusal !== void 0) return { operations: [], blocked: refusal };
950
959
  const dropped = Object.keys(webpackTarget.configurations ?? {});
951
960
  return {
952
- operations: nestAdoptionOperations(context.cwd, webpackTarget),
961
+ operations: [
962
+ ...nestAdoptionOperations(context.cwd, webpackTarget),
963
+ // The same nx metadata the React path writes, and for the same reason: declaring `inputs`
964
+ // at all REPLACES nx's default `['default','^default']`, so a target that declares none
965
+ // runs on defaults while every other target this module has declares them. The Nest path
966
+ // wrote none, which left its build the one target that could replay a stale bundle after a
967
+ // dependency changed. One name, because a Nest service's config can only be `.mts`.
968
+ ...nxTargetMetadataOperations({ cwd: context.cwd, targets: buildTarget([NEST_CONFIG_FILE]) }),
969
+ manifestOperation(context.cwd, { [BUILD_SCRIPT_NAME]: SENTINEL_BUILD_COMMAND })
970
+ ],
953
971
  notes: [
954
972
  `wrote ${NEST_CONFIG_FILE} and pointed the nx build target at sentinel. The target stays \`nx:run-commands\` with \`forwardAllArgs: false\`, so the image's \`--generatePackageJson\` still works and the Dockerfile needs no change.`,
955
973
  ...dropped.length > 0 ? [
@@ -1003,7 +1021,7 @@ var ViteRoleAdapter = class extends BaseAdapter {
1003
1021
  * which tells nobody what to do about it.
1004
1022
  *
1005
1023
  * Nest it now serves too, through `@hublo/sentinel/build/nest`. It used to be declined on the
1006
- * grounds that "there is nothing to bundle", which was never true: those 36 services bundle
1024
+ * grounds that "there is nothing to bundle", which was never true: those 38 services bundle
1007
1025
  * with webpack. What changed is that Nx v24 removes the `@nx/webpack:webpack` executor and
1008
1026
  * the `composePlugins` / `withNx` helpers their shared config is built on, and nx's own
1009
1027
  * migration generator refuses every one of them because they use `@nx/js:node`
@@ -1112,6 +1130,12 @@ var ViteDevAdapter = class extends ViteRoleAdapter {
1112
1130
  * wrong correction, it added friction to a command that already produced the right result.
1113
1131
  */
1114
1132
  plan(context) {
1133
+ if (declaredBuildPreset(context.cwd) === "nest" || webpackBuildTarget(context.cwd)) {
1134
+ return {
1135
+ operations: [],
1136
+ skipped: `a Nest service has no dev server, so --dev has nothing to adopt here. Its build is \`sentinel --init --build\`, and it runs as a Node process afterwards.`
1137
+ };
1138
+ }
1115
1139
  const planned = super.plan(context);
1116
1140
  if (planned.operations.length === 0) return planned;
1117
1141
  return {
@@ -6412,12 +6436,12 @@ export {
6412
6436
  declaredPresetFor,
6413
6437
  availableTargets,
6414
6438
  resolve,
6415
- BaseAdapter,
6416
- resolveBin,
6417
6439
  readOwnVersion,
6418
6440
  readProjectPackageJson,
6419
6441
  moduleName,
6420
6442
  buildConfigFile,
6443
+ BaseAdapter,
6444
+ resolveBin,
6421
6445
  VERBS,
6422
6446
  TARGETS,
6423
6447
  LONG_RUNNING_TARGETS,
@@ -6433,4 +6457,4 @@ export {
6433
6457
  detectFramework,
6434
6458
  dispatch
6435
6459
  };
6436
- //# sourceMappingURL=chunk-I4ATYZKK.js.map
6460
+ //# sourceMappingURL=chunk-RV34GQVH.js.map
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  registerAdapters,
8
8
  resolve,
9
9
  setDefaultRunner
10
- } from "./chunk-I4ATYZKK.js";
10
+ } from "./chunk-RV34GQVH.js";
11
11
  export {
12
12
  BaseAdapter,
13
13
  all,
@@ -0,0 +1,2 @@
1
+ export { ConfigEnv, TestUserConfig, ViteUserConfig, defineConfig, mergeConfig } from 'vitest/config';
2
+ export { loadEnv } from 'vite';
@@ -0,0 +1,9 @@
1
+ // src/roles/test/react/toolchain.ts
2
+ import { defineConfig, mergeConfig } from "vitest/config";
3
+ import { loadEnv } from "vite";
4
+ export {
5
+ defineConfig,
6
+ loadEnv,
7
+ mergeConfig
8
+ };
9
+ //# sourceMappingURL=toolchain.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hublo/sentinel",
3
- "version": "1.2.0-alpha.12",
3
+ "version": "1.2.0-alpha.14",
4
4
  "description": "One CLI that guards code health across Hublo repos: shared lint/typescript/build/test presets, static & dynamic analysis, and architecture checks.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -45,6 +45,10 @@
45
45
  "./build/nest": {
46
46
  "types": "./dist/roles/build/nest/toolchain.d.ts",
47
47
  "import": "./dist/roles/build/nest/toolchain.js"
48
+ },
49
+ "./test/react": {
50
+ "types": "./dist/roles/test/react/toolchain.d.ts",
51
+ "import": "./dist/roles/test/react/toolchain.js"
48
52
  }
49
53
  },
50
54
  "files": [
@@ -72,7 +76,8 @@
72
76
  "oxlint-tsgolint": "7.0.2001",
73
77
  "typescript": "5.9.3",
74
78
  "vite": "8.0.8",
75
- "vite-plugin-svgr": "5.2.0"
79
+ "vite-plugin-svgr": "5.2.0",
80
+ "vitest": "4.1.4"
76
81
  },
77
82
  "devDependencies": {
78
83
  "@hublo/sentinel": "link:.",