@hublo/sentinel 1.2.0-alpha.11 → 1.2.0-alpha.13

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.
@@ -22,7 +22,7 @@ import {
22
22
  registerAdapters,
23
23
  resolve,
24
24
  resolveBin
25
- } from "../chunk-6VOQDDZD.js";
25
+ } from "../chunk-X675OYUC.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() {
337
+ return {
338
+ [BUILD_SCRIPT_NAME]: {
339
+ cache: true,
340
+ inputs: ["default", "^default", ...BUILD_CONFIG_FILES.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 {
@@ -318,14 +570,14 @@ function existingCommand(cwd, target) {
318
570
  }
319
571
 
320
572
  // src/core/config/nx-target.ts
321
- import { existsSync as existsSync5 } from "fs";
322
- import { join as join5 } from "path";
573
+ import { existsSync as existsSync7 } from "fs";
574
+ import { join as join7 } from "path";
323
575
  function nxTargetOperations(options) {
324
576
  const { cwd, targets } = options;
325
577
  const names = Object.keys(targets);
326
578
  if (names.length === 0) return [];
327
579
  const operations = [];
328
- if (existsSync5(join5(cwd, "project.json"))) {
580
+ if (existsSync7(join7(cwd, "project.json"))) {
329
581
  operations.push({
330
582
  kind: "remove-json-keys",
331
583
  path: "project.json",
@@ -382,112 +634,6 @@ function keepsOtherCommands(script, sentinelCommand) {
382
634
  return script.split(SEGMENT_SEPARATOR).filter((_, index) => !isSeparatorAt(index)).some((segment) => segment.trim() !== "" && segment.trim() !== sentinelCommand);
383
635
  }
384
636
 
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."
450
- }
451
- };
452
-
453
- // src/roles/build/preset-data.ts
454
- var REQUIREMENTS = requirements_default;
455
- var OWNED_PACKAGES = toolchain_default.owned;
456
-
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}`)]
484
- }
485
- };
486
- }
487
- var SENTINEL_OWNED_BUILD_PACKAGES = OWNED_PACKAGES.map(
488
- (entry) => entry.package
489
- );
490
-
491
637
  // src/roles/build/dev-script.ts
492
638
  var CHAIN = " -- ";
493
639
  var VITE_TOKEN = /(^|\s|\/)vite(\s|$)/;
@@ -504,84 +650,12 @@ function composeDevScript(existing, command) {
504
650
  if (at === -1) return existing;
505
651
  const rest = (chunks[at] ?? "").replace(/^.*?(^|\s|\/)vite(\s|$)/, "");
506
652
  const options = appOptions(rest);
507
- chunks[at] = options === "" ? command : `${command} -- ${options}`;
653
+ chunks[at] = options === "" ? `${command} --` : `${command} -- ${options}`;
508
654
  return chunks.join(CHAIN);
509
655
  }
510
656
 
511
657
  // src/roles/build/nest/adopt.ts
512
658
  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
659
  var NEST_CONFIG_FILE = "vite.config.mts";
586
660
  function nestConfigSource(cwd, target) {
587
661
  const hops = workspaceRootHops(cwd, target);
@@ -628,80 +702,6 @@ function nestAdoptionOperations(cwd, target) {
628
702
  ];
629
703
  }
630
704
 
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
705
  // src/roles/build/requirements.ts
706
706
  function atLeast(declared, needed) {
707
707
  const parse2 = (value) => value.replace(/^[\^~>=<\s]+/, "").split("-")[0].split(".").map((part) => Number.parseInt(part, 10) || 0);
@@ -858,9 +858,9 @@ function buildScripts(cwd) {
858
858
  const own2 = moduleName(cwd);
859
859
  scripts[DEV_SCRIPT_NAME] = composeDevScript(
860
860
  existingDev,
861
- // Trailing `--` for the same reason as the build script: whatever the caller appends
862
- // reaches Vite, not sentinel's parser. It goes after `--module`, which is sentinel's own.
863
- `${selfCommand(cwd, "--run --dev") ?? SENTINEL_DEV_COMMAND} --module ${own2} --`
861
+ // No trailing `--` here: `composeDevScript` owns the separator, and adding one on both
862
+ // sides produced `-- --`, handing Vite an argument nobody wrote.
863
+ `${selfCommand(cwd, "--run --dev") ?? SENTINEL_DEV_COMMAND} --module ${own2}`
864
864
  );
865
865
  }
866
866
  return scripts;
@@ -1112,6 +1112,12 @@ var ViteDevAdapter = class extends ViteRoleAdapter {
1112
1112
  * wrong correction, it added friction to a command that already produced the right result.
1113
1113
  */
1114
1114
  plan(context) {
1115
+ if (declaredBuildPreset(context.cwd) === "nest" || webpackBuildTarget(context.cwd)) {
1116
+ return {
1117
+ operations: [],
1118
+ 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.`
1119
+ };
1120
+ }
1115
1121
  const planned = super.plan(context);
1116
1122
  if (planned.operations.length === 0) return planned;
1117
1123
  return {
@@ -6412,12 +6418,12 @@ export {
6412
6418
  declaredPresetFor,
6413
6419
  availableTargets,
6414
6420
  resolve,
6415
- BaseAdapter,
6416
- resolveBin,
6417
6421
  readOwnVersion,
6418
6422
  readProjectPackageJson,
6419
6423
  moduleName,
6420
6424
  buildConfigFile,
6425
+ BaseAdapter,
6426
+ resolveBin,
6421
6427
  VERBS,
6422
6428
  TARGETS,
6423
6429
  LONG_RUNNING_TARGETS,
@@ -6433,4 +6439,4 @@ export {
6433
6439
  detectFramework,
6434
6440
  dispatch
6435
6441
  };
6436
- //# sourceMappingURL=chunk-6VOQDDZD.js.map
6442
+ //# sourceMappingURL=chunk-X675OYUC.js.map
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  registerAdapters,
8
8
  resolve,
9
9
  setDefaultRunner
10
- } from "./chunk-6VOQDDZD.js";
10
+ } from "./chunk-X675OYUC.js";
11
11
  export {
12
12
  BaseAdapter,
13
13
  all,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hublo/sentinel",
3
- "version": "1.2.0-alpha.11",
3
+ "version": "1.2.0-alpha.13",
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",