@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.
package/dist/bin/sentinel.d.ts
CHANGED
package/dist/bin/sentinel.js
CHANGED
|
@@ -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
|
|
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 &&
|
|
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
|
|
442
|
+
import { existsSync as existsSync5 } from "fs";
|
|
191
443
|
import { createRequire } from "module";
|
|
192
|
-
import { delimiter, dirname as dirname2, join as
|
|
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 =
|
|
198
|
-
if (
|
|
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 =
|
|
211
|
-
return
|
|
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
|
|
228
|
-
import { join as
|
|
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
|
|
244
|
-
import { basename as basename2, join as
|
|
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(
|
|
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(
|
|
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 (
|
|
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
|
-
|
|
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/
|
|
454
|
-
|
|
455
|
-
|
|
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/
|
|
458
|
-
var
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
];
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
"
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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/
|
|
734
|
+
// src/core/config/rewrite-imports.ts
|
|
734
735
|
import { parseSync } from "oxc-parser";
|
|
735
|
-
var
|
|
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
|
|
742
|
-
|
|
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 '${
|
|
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 '${
|
|
752
|
+
lines.push(`import type { ${render(types)} } from '${presetSpecifier}'`);
|
|
755
753
|
}
|
|
756
754
|
return lines.join("\n");
|
|
757
755
|
}
|
|
758
|
-
function
|
|
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 ===
|
|
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) =>
|
|
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 =
|
|
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 (
|
|
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:
|
|
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
|
|
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-
|
|
6460
|
+
//# sourceMappingURL=chunk-RV34GQVH.js.map
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hublo/sentinel",
|
|
3
|
-
"version": "1.2.0-alpha.
|
|
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:.",
|