@hublo/sentinel 1.2.0-alpha.0 → 1.2.0-alpha.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/bin/sentinel.d.ts +5 -0
- package/dist/bin/sentinel.js +57 -8
- package/dist/{chunk-TWL6T237.js → chunk-7WB7MQHD.js} +1128 -581
- package/dist/index.js +1 -2
- package/dist/roles/build/nest/toolchain.d.ts +90 -0
- package/dist/roles/build/nest/toolchain.js +253 -0
- package/dist/roles/build/toolchain.d.ts +5 -0
- package/dist/roles/build/toolchain.js +16 -0
- package/package.json +21 -3
- package/dist/chunk-XSCDJZDY.js +0 -26
- package/dist/roles/build/react-app.d.ts +0 -116
- package/dist/roles/build/react-app.js +0 -111
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
REACT_APP_DEFAULTS,
|
|
3
|
-
isPlainObject
|
|
4
|
-
} from "./chunk-XSCDJZDY.js";
|
|
5
|
-
|
|
6
1
|
// src/core/registry.ts
|
|
7
2
|
var adapters = [];
|
|
8
3
|
var PresetUnsupportedError = class extends Error {
|
|
@@ -87,7 +82,7 @@ var BaseAdapter = class {
|
|
|
87
82
|
|
|
88
83
|
// src/shared/package-json.ts
|
|
89
84
|
import { existsSync, readFileSync } from "fs";
|
|
90
|
-
import { dirname, join } from "path";
|
|
85
|
+
import { basename, dirname, join } from "path";
|
|
91
86
|
import { fileURLToPath } from "url";
|
|
92
87
|
function readOwnPackage() {
|
|
93
88
|
let dir = dirname(fileURLToPath(import.meta.url));
|
|
@@ -132,19 +127,9 @@ function readNxProjectName(dir) {
|
|
|
132
127
|
return void 0;
|
|
133
128
|
}
|
|
134
129
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
function palette(stream) {
|
|
139
|
-
const paint = (format, text) => styleText(format, text, { stream });
|
|
140
|
-
return {
|
|
141
|
-
ok: (text) => paint(["green", "bold"], text),
|
|
142
|
-
fail: (text) => paint(["red", "bold"], text),
|
|
143
|
-
warn: (text) => paint("yellow", text),
|
|
144
|
-
strong: (text) => paint("bold", text),
|
|
145
|
-
dim: (text) => paint("dim", text),
|
|
146
|
-
accent: (text) => paint("cyan", text)
|
|
147
|
-
};
|
|
130
|
+
function moduleName(dir) {
|
|
131
|
+
const declared = readProjectPackageJson(dir).name;
|
|
132
|
+
return readNxProjectName(dir) ?? (declared === void 0 ? basename(dir) : declared);
|
|
148
133
|
}
|
|
149
134
|
|
|
150
135
|
// src/core/domain.ts
|
|
@@ -166,7 +151,21 @@ var SWEEPABLE_TARGETS = TARGETS.filter(
|
|
|
166
151
|
);
|
|
167
152
|
var PRESET_NAMES = ["react", "nest", "svelte", "node", "tools"];
|
|
168
153
|
|
|
169
|
-
// src/
|
|
154
|
+
// src/shared/color.ts
|
|
155
|
+
import { styleText } from "util";
|
|
156
|
+
function palette(stream) {
|
|
157
|
+
const paint = (format, text) => styleText(format, text, { stream });
|
|
158
|
+
return {
|
|
159
|
+
ok: (text) => paint(["green", "bold"], text),
|
|
160
|
+
fail: (text) => paint(["red", "bold"], text),
|
|
161
|
+
warn: (text) => paint("yellow", text),
|
|
162
|
+
strong: (text) => paint("bold", text),
|
|
163
|
+
dim: (text) => paint("dim", text),
|
|
164
|
+
accent: (text) => paint("cyan", text)
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// src/roles/build/adapters/vite/vite-role.adapter.ts
|
|
170
169
|
import { spawnSync } from "child_process";
|
|
171
170
|
|
|
172
171
|
// src/core/config/tool-args.ts
|
|
@@ -206,9 +205,9 @@ function binFromOwnInstall(packageName, binName) {
|
|
|
206
205
|
try {
|
|
207
206
|
const manifest = require2.resolve(`${packageName}/package.json`);
|
|
208
207
|
const bin = require2(manifest).bin;
|
|
209
|
-
const
|
|
210
|
-
if (!
|
|
211
|
-
const executable = join2(dirname2(manifest),
|
|
208
|
+
const relative6 = typeof bin === "string" ? bin : bin?.[binName];
|
|
209
|
+
if (!relative6) return void 0;
|
|
210
|
+
const executable = join2(dirname2(manifest), relative6);
|
|
212
211
|
return existsSync3(executable) ? executable : void 0;
|
|
213
212
|
} catch {
|
|
214
213
|
return void 0;
|
|
@@ -221,8 +220,8 @@ function binSearchPath(cwd) {
|
|
|
221
220
|
}
|
|
222
221
|
|
|
223
222
|
// src/roles/build/plan.ts
|
|
224
|
-
import { existsSync as
|
|
225
|
-
import { join as
|
|
223
|
+
import { existsSync as existsSync8, readFileSync as readFileSync6 } from "fs";
|
|
224
|
+
import { join as join8 } from "path";
|
|
226
225
|
|
|
227
226
|
// src/core/config/existing-command.ts
|
|
228
227
|
import { readFileSync as readFileSync3 } from "fs";
|
|
@@ -242,7 +241,7 @@ function parseJsonc(text, source = "config") {
|
|
|
242
241
|
|
|
243
242
|
// src/core/config/manifest.ts
|
|
244
243
|
import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
|
|
245
|
-
import { basename, join as join3 } from "path";
|
|
244
|
+
import { basename as basename2, join as join3 } from "path";
|
|
246
245
|
function moduleScripts(cwd) {
|
|
247
246
|
try {
|
|
248
247
|
const pkg = JSON.parse(readFileSync2(join3(cwd, "package.json"), "utf8"));
|
|
@@ -251,7 +250,7 @@ function moduleScripts(cwd) {
|
|
|
251
250
|
return {};
|
|
252
251
|
}
|
|
253
252
|
}
|
|
254
|
-
function
|
|
253
|
+
function moduleName2(cwd) {
|
|
255
254
|
try {
|
|
256
255
|
const pkg = JSON.parse(readFileSync2(join3(cwd, "package.json"), "utf8"));
|
|
257
256
|
return pkg.name;
|
|
@@ -260,7 +259,7 @@ function moduleName(cwd) {
|
|
|
260
259
|
}
|
|
261
260
|
}
|
|
262
261
|
function isSelfAdoption(cwd) {
|
|
263
|
-
return
|
|
262
|
+
return moduleName2(cwd) === readOwnPackage().name;
|
|
264
263
|
}
|
|
265
264
|
function selfCommand(cwd, flags) {
|
|
266
265
|
const own = readOwnPackage();
|
|
@@ -279,7 +278,7 @@ function manifestOperation(cwd, scripts) {
|
|
|
279
278
|
return {
|
|
280
279
|
kind: "merge-json",
|
|
281
280
|
path: "package.json",
|
|
282
|
-
value: { name: readNxProjectName(cwd) ??
|
|
281
|
+
value: { name: readNxProjectName(cwd) ?? basename2(cwd), private: true, ...value }
|
|
283
282
|
};
|
|
284
283
|
}
|
|
285
284
|
|
|
@@ -383,33 +382,111 @@ function keepsOtherCommands(script, sentinelCommand) {
|
|
|
383
382
|
return script.split(SEGMENT_SEPARATOR).filter((_, index) => !isSeparatorAt(index)).some((segment) => segment.trim() !== "" && segment.trim() !== sentinelCommand);
|
|
384
383
|
}
|
|
385
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."
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
// src/roles/build/preset-data.ts
|
|
454
|
+
var REQUIREMENTS = requirements_default;
|
|
455
|
+
var OWNED_PACKAGES = toolchain_default.owned;
|
|
456
|
+
|
|
386
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
|
+
];
|
|
387
464
|
var BUILD_CONFIG_FILES = [
|
|
388
465
|
"vite.config.ts",
|
|
389
466
|
"vite.config.mts",
|
|
390
467
|
"vite.config.js",
|
|
391
468
|
"vite.config.mjs"
|
|
392
469
|
];
|
|
393
|
-
var
|
|
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;
|
|
394
475
|
var BUILD_SCRIPT_NAME = "build";
|
|
395
|
-
var SENTINEL_BUILD_COMMAND = "sentinel --run --build";
|
|
476
|
+
var SENTINEL_BUILD_COMMAND = "sentinel --run --build --";
|
|
396
477
|
var DEV_SCRIPT_NAME = "serve";
|
|
397
478
|
var SENTINEL_DEV_COMMAND = "sentinel --run --dev";
|
|
398
479
|
function buildTarget() {
|
|
399
480
|
return {
|
|
400
481
|
[BUILD_SCRIPT_NAME]: {
|
|
401
482
|
cache: true,
|
|
402
|
-
inputs: ["default", "^default", `{projectRoot}/${
|
|
483
|
+
inputs: ["default", "^default", ...BUILD_CONFIG_FILES.map((name) => `{projectRoot}/${name}`)]
|
|
403
484
|
}
|
|
404
485
|
};
|
|
405
486
|
}
|
|
406
|
-
var SENTINEL_OWNED_BUILD_PACKAGES =
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
"@tailwindcss/vite",
|
|
410
|
-
"vite-plugin-svgr",
|
|
411
|
-
"nitro"
|
|
412
|
-
];
|
|
487
|
+
var SENTINEL_OWNED_BUILD_PACKAGES = OWNED_PACKAGES.map(
|
|
488
|
+
(entry) => entry.package
|
|
489
|
+
);
|
|
413
490
|
|
|
414
491
|
// src/roles/build/dev-script.ts
|
|
415
492
|
var CHAIN = " -- ";
|
|
@@ -431,9 +508,129 @@ function composeDevScript(existing, command) {
|
|
|
431
508
|
return chunks.join(CHAIN);
|
|
432
509
|
}
|
|
433
510
|
|
|
434
|
-
// src/roles/build/
|
|
511
|
+
// src/roles/build/nest/adopt.ts
|
|
512
|
+
import { relative as relative2 } from "path";
|
|
513
|
+
|
|
514
|
+
// src/roles/build/nest/nx-target.ts
|
|
435
515
|
import { existsSync as existsSync6, readFileSync as readFileSync4 } from "fs";
|
|
436
|
-
import { join as join6 } from "path";
|
|
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
|
+
var NEST_CONFIG_FILE = "vite.config.mts";
|
|
586
|
+
function nestConfigSource(cwd, target) {
|
|
587
|
+
const hops = workspaceRootHops(cwd, target);
|
|
588
|
+
const up = Array.from({ length: hops }, () => "..").join("/");
|
|
589
|
+
return `import path from 'node:path'
|
|
590
|
+
|
|
591
|
+
import { nestService } from '${BUILD_PRESET_SPECIFIERS.nest}'
|
|
592
|
+
|
|
593
|
+
export default nestService({
|
|
594
|
+
project: '${target.project}',
|
|
595
|
+
root: __dirname,
|
|
596
|
+
workspaceRoot: path.resolve(__dirname, '${up}'),
|
|
597
|
+
outDir: '${target.outputPath}',
|
|
598
|
+
})
|
|
599
|
+
`;
|
|
600
|
+
}
|
|
601
|
+
function nestBuildTarget(cwd, target) {
|
|
602
|
+
const workspaceRoot = workspaceRootFor(cwd, target);
|
|
603
|
+
if (workspaceRoot === void 0) {
|
|
604
|
+
throw new Error(
|
|
605
|
+
`sentinel build(nest): cannot place ${cwd} inside a workspace from ${target.main}`
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
const moduleRoot = relative2(workspaceRoot, cwd);
|
|
609
|
+
return {
|
|
610
|
+
executor: "nx:run-commands",
|
|
611
|
+
outputs: [`{workspaceRoot}/${target.outputPath}`],
|
|
612
|
+
options: {
|
|
613
|
+
cwd: moduleRoot,
|
|
614
|
+
command: "pnpm exec sentinel --run --build",
|
|
615
|
+
forwardAllArgs: false
|
|
616
|
+
}
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
function nestAdoptionOperations(cwd, target) {
|
|
620
|
+
return [
|
|
621
|
+
{ kind: "write", path: NEST_CONFIG_FILE, contents: nestConfigSource(cwd, target) },
|
|
622
|
+
{ kind: "remove-json-keys", path: "project.json", keys: [["targets", "build"]] },
|
|
623
|
+
{
|
|
624
|
+
kind: "merge-json",
|
|
625
|
+
path: "project.json",
|
|
626
|
+
value: { targets: { build: nestBuildTarget(cwd, target) } }
|
|
627
|
+
}
|
|
628
|
+
];
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// src/roles/build/read-adoption.ts
|
|
632
|
+
import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
|
|
633
|
+
import { join as join7 } from "path";
|
|
437
634
|
var NOT_ADOPTED = (configFile, unreadable = null) => ({
|
|
438
635
|
configFile,
|
|
439
636
|
preset: null,
|
|
@@ -444,19 +641,32 @@ var NOT_ADOPTED = (configFile, unreadable = null) => ({
|
|
|
444
641
|
unreadable
|
|
445
642
|
});
|
|
446
643
|
function buildConfigFile(cwd) {
|
|
447
|
-
return BUILD_CONFIG_FILES.find((name) =>
|
|
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);
|
|
448
649
|
}
|
|
449
650
|
function importsPreset(source) {
|
|
450
|
-
|
|
451
|
-
|
|
651
|
+
return Object.values(BUILD_PRESET_SPECIFIERS).some(
|
|
652
|
+
(specifier) => importsSpecifier(source, specifier)
|
|
653
|
+
);
|
|
452
654
|
}
|
|
453
655
|
var REACT_PLUGIN_SPECIFIERS = ["@vitejs/plugin-react", "@tanstack/react-start"];
|
|
454
656
|
function declaredBuildPreset(cwd) {
|
|
657
|
+
const source = readBuildConfigSource(cwd);
|
|
658
|
+
if (source === void 0) {
|
|
659
|
+
return webpackBuildTarget(cwd) === void 0 ? void 0 : "nest";
|
|
660
|
+
}
|
|
661
|
+
if (importsSpecifier(source, BUILD_PRESET_SPECIFIERS.nest)) return "nest";
|
|
662
|
+
if (importsPreset(source)) return "react";
|
|
663
|
+
return REACT_PLUGIN_SPECIFIERS.some((specifier) => importsSpecifier(source, specifier)) ? "react" : void 0;
|
|
664
|
+
}
|
|
665
|
+
function readBuildConfigSource(cwd) {
|
|
455
666
|
const configFile = buildConfigFile(cwd);
|
|
456
667
|
if (!configFile) return void 0;
|
|
457
668
|
try {
|
|
458
|
-
|
|
459
|
-
return REACT_PLUGIN_SPECIFIERS.some((specifier) => source.includes(specifier)) ? "react" : void 0;
|
|
669
|
+
return readFileSync5(join7(cwd, configFile), "utf8");
|
|
460
670
|
} catch {
|
|
461
671
|
return void 0;
|
|
462
672
|
}
|
|
@@ -466,7 +676,7 @@ function readBuildAdoption(cwd) {
|
|
|
466
676
|
if (!configFile) return NOT_ADOPTED(null);
|
|
467
677
|
let source;
|
|
468
678
|
try {
|
|
469
|
-
source =
|
|
679
|
+
source = readFileSync5(join7(cwd, configFile), "utf8");
|
|
470
680
|
} catch (error) {
|
|
471
681
|
return NOT_ADOPTED(configFile, error instanceof Error ? error.message : String(error));
|
|
472
682
|
}
|
|
@@ -478,7 +688,9 @@ function readBuildAdoption(cwd) {
|
|
|
478
688
|
);
|
|
479
689
|
return {
|
|
480
690
|
configFile,
|
|
481
|
-
|
|
691
|
+
// Read from the config rather than assumed: the two presets are adopted the same way and
|
|
692
|
+
// reporting the wrong one would make `--inspect` lie about what a module builds.
|
|
693
|
+
preset: declaredBuildPreset(cwd) ?? "react",
|
|
482
694
|
adopted: true,
|
|
483
695
|
conformant: ownDeclarations.length === 0,
|
|
484
696
|
drift: ownDeclarations.map(
|
|
@@ -489,39 +701,131 @@ function readBuildAdoption(cwd) {
|
|
|
489
701
|
};
|
|
490
702
|
}
|
|
491
703
|
|
|
492
|
-
// src/roles/build/
|
|
493
|
-
function
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
704
|
+
// src/roles/build/requirements.ts
|
|
705
|
+
function atLeast(declared, needed) {
|
|
706
|
+
const parse2 = (value) => value.replace(/^[\^~>=<\s]+/, "").split("-")[0].split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
707
|
+
const left = parse2(declared);
|
|
708
|
+
const right = parse2(needed);
|
|
709
|
+
for (let at = 0; at < Math.max(left.length, right.length); at++) {
|
|
710
|
+
const a = left[at] ?? 0;
|
|
711
|
+
const b = right[at] ?? 0;
|
|
712
|
+
if (a !== b) return a > b;
|
|
713
|
+
}
|
|
714
|
+
return true;
|
|
715
|
+
}
|
|
716
|
+
function unmetRequirements(preset, declared) {
|
|
717
|
+
const entry = REQUIREMENTS[preset];
|
|
718
|
+
if (!entry) return [];
|
|
719
|
+
return Object.entries(entry.requires).filter(([name, needed]) => {
|
|
720
|
+
const have = declared[name];
|
|
721
|
+
return have === void 0 || !atLeast(have, needed);
|
|
722
|
+
}).map(([name, needed]) => ({ name, declared: declared[name], needed }));
|
|
723
|
+
}
|
|
724
|
+
function describeUnmet(preset, unmet) {
|
|
725
|
+
const entry = REQUIREMENTS[preset];
|
|
726
|
+
const list = unmet.map(
|
|
727
|
+
({ name, declared, needed }) => `${name} ${declared === void 0 ? "is not declared here" : `is at ${declared}`}, needs >= ${needed}`
|
|
728
|
+
).join("; ");
|
|
729
|
+
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\`.`;
|
|
730
|
+
}
|
|
498
731
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
}
|
|
522
|
-
)
|
|
523
|
-
|
|
732
|
+
// src/roles/build/rewrite-imports.ts
|
|
733
|
+
import { parseSync } from "oxc-parser";
|
|
734
|
+
var MOVED_SPECIFIERS = new Set(OWNED_PACKAGES.map((entry) => entry.specifier));
|
|
735
|
+
function esmBlocker(fileName, packageType) {
|
|
736
|
+
if (/\.(mts|mjs)$/.test(fileName)) return void 0;
|
|
737
|
+
if (packageType === "module") return void 0;
|
|
738
|
+
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.`;
|
|
739
|
+
}
|
|
740
|
+
var DEFAULT_EXPORT_NAMES = Object.fromEntries(
|
|
741
|
+
OWNED_PACKAGES.filter((entry) => entry.default !== void 0).map((entry) => [
|
|
742
|
+
entry.specifier,
|
|
743
|
+
entry.default
|
|
744
|
+
])
|
|
745
|
+
);
|
|
746
|
+
function renderImport(bindings) {
|
|
747
|
+
const render = (list) => list.map((b) => b.exported === b.local ? b.exported : `${b.exported} as ${b.local}`).sort((left, right) => left.localeCompare(right)).join(", ");
|
|
748
|
+
const values = bindings.filter((b) => !b.typeOnly);
|
|
749
|
+
const types = bindings.filter((b) => b.typeOnly);
|
|
750
|
+
const lines = [];
|
|
751
|
+
if (values.length > 0) lines.push(`import { ${render(values)} } from '${BUILD_PRESET_SPECIFIER}'`);
|
|
752
|
+
if (types.length > 0) {
|
|
753
|
+
lines.push(`import type { ${render(types)} } from '${BUILD_PRESET_SPECIFIER}'`);
|
|
754
|
+
}
|
|
755
|
+
return lines.join("\n");
|
|
756
|
+
}
|
|
757
|
+
function rewriteBuildImports(source, fileName = "vite.config.ts") {
|
|
758
|
+
const { program, errors } = parseSync(fileName, source, { sourceType: "module" });
|
|
759
|
+
if (errors.length > 0) {
|
|
760
|
+
return {
|
|
761
|
+
kind: "blocked",
|
|
762
|
+
why: `${fileName} does not parse: ${errors[0]?.message ?? "unknown"}`
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
const imports = program.body.filter(
|
|
766
|
+
(node) => node.type === "ImportDeclaration"
|
|
767
|
+
);
|
|
768
|
+
if (imports.some((node) => node.source.value === BUILD_PRESET_SPECIFIER)) {
|
|
769
|
+
return { kind: "unchanged", why: "this config already imports its toolchain from sentinel" };
|
|
770
|
+
}
|
|
771
|
+
const moving = imports.filter((node) => MOVED_SPECIFIERS.has(node.source.value));
|
|
772
|
+
if (moving.length === 0) {
|
|
773
|
+
return {
|
|
774
|
+
kind: "unchanged",
|
|
775
|
+
why: "this config imports none of the packages sentinel owns, so there is nothing to move"
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
const bindings = [];
|
|
779
|
+
for (const node of moving) {
|
|
780
|
+
const from = node.source.value;
|
|
781
|
+
const specifiers = node.specifiers ?? [];
|
|
782
|
+
if (specifiers.length === 0) {
|
|
783
|
+
return { kind: "blocked", why: `\`import '${from}'\` has no bindings to move` };
|
|
784
|
+
}
|
|
785
|
+
for (const spec of specifiers) {
|
|
786
|
+
const typeOnly = node.importKind === "type" || spec.importKind === "type";
|
|
787
|
+
if (spec.type === "ImportDefaultSpecifier") {
|
|
788
|
+
const exported = DEFAULT_EXPORT_NAMES[from];
|
|
789
|
+
if (!exported) {
|
|
790
|
+
return {
|
|
791
|
+
kind: "blocked",
|
|
792
|
+
why: `no sentinel export for the default import of \`${from}\``
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
bindings.push({ exported, local: spec.local.name, typeOnly });
|
|
796
|
+
continue;
|
|
797
|
+
}
|
|
798
|
+
if (spec.type === "ImportSpecifier") {
|
|
799
|
+
bindings.push({
|
|
800
|
+
exported: spec.imported?.name ?? spec.imported?.value ?? spec.local.name,
|
|
801
|
+
local: spec.local.name,
|
|
802
|
+
typeOnly
|
|
803
|
+
});
|
|
804
|
+
continue;
|
|
805
|
+
}
|
|
806
|
+
return {
|
|
807
|
+
kind: "blocked",
|
|
808
|
+
why: `\`import * as ${spec.local.name} from '${from}'\` cannot be mapped onto sentinel's named exports`
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
const spans = moving.map((node) => ({ start: node.start, end: node.end })).sort((left, right) => left.start - right.start);
|
|
813
|
+
const first = spans[0];
|
|
814
|
+
const rest = spans.slice(1);
|
|
815
|
+
let out = source;
|
|
816
|
+
for (const span of rest.slice().reverse()) {
|
|
817
|
+
const after = out.slice(span.end).match(/^\r?\n/)?.[0].length ?? 0;
|
|
818
|
+
out = out.slice(0, span.start) + out.slice(span.end + after);
|
|
819
|
+
}
|
|
820
|
+
out = out.slice(0, first.start) + renderImport(bindings) + out.slice(first.end);
|
|
821
|
+
return {
|
|
822
|
+
kind: "rewritten",
|
|
823
|
+
source: out,
|
|
824
|
+
moved: [...new Set(moving.map((node) => node.source.value))].sort()
|
|
825
|
+
};
|
|
524
826
|
}
|
|
827
|
+
|
|
828
|
+
// src/roles/build/plan.ts
|
|
525
829
|
function ownedBuildDependencies(cwd) {
|
|
526
830
|
const manifest = readProjectPackageJson(cwd);
|
|
527
831
|
const keys = [];
|
|
@@ -550,32 +854,93 @@ function buildScripts(cwd) {
|
|
|
550
854
|
};
|
|
551
855
|
const existingDev = existingCommand(cwd, DEV_SCRIPT_NAME);
|
|
552
856
|
if (existingDev !== void 0) {
|
|
857
|
+
const own2 = moduleName(cwd);
|
|
553
858
|
scripts[DEV_SCRIPT_NAME] = composeDevScript(
|
|
554
859
|
existingDev,
|
|
555
|
-
|
|
860
|
+
// Trailing `--` for the same reason as the build script: whatever the caller appends
|
|
861
|
+
// reaches Vite, not sentinel's parser. It goes after `--module`, which is sentinel's own.
|
|
862
|
+
`${selfCommand(cwd, "--run --dev") ?? SENTINEL_DEV_COMMAND} --module ${own2} --`
|
|
556
863
|
);
|
|
557
864
|
}
|
|
558
865
|
return scripts;
|
|
559
866
|
}
|
|
560
867
|
function plan(context) {
|
|
868
|
+
const manifest = readProjectPackageJson(context.cwd);
|
|
869
|
+
const unmet = unmetRequirements(context.preset, {
|
|
870
|
+
...manifest.dependencies,
|
|
871
|
+
...manifest.devDependencies
|
|
872
|
+
});
|
|
873
|
+
if (unmet.length > 0) {
|
|
874
|
+
return { operations: [], skipped: describeUnmet(context.preset, unmet) };
|
|
875
|
+
}
|
|
561
876
|
if (declaredBuildPreset(context.cwd) === void 0 && buildConfigFile(context.cwd) !== void 0) {
|
|
562
877
|
return {
|
|
563
878
|
operations: [],
|
|
564
|
-
skipped: `this module has a Vite config, but it does not build a React app (no @vitejs/plugin-react or @tanstack/react-start in it). The build role ships a React preset only
|
|
879
|
+
skipped: `this module has a Vite config, but it does not build a React app (no @vitejs/plugin-react or @tanstack/react-start in it). The build role ships a React preset only.`
|
|
565
880
|
};
|
|
566
881
|
}
|
|
567
882
|
const notes = [];
|
|
568
883
|
const operations = [];
|
|
569
884
|
const configFile = buildConfigFile(context.cwd);
|
|
885
|
+
const webpackTarget = webpackBuildTarget(context.cwd);
|
|
886
|
+
const isNest = declaredBuildPreset(context.cwd) === "nest" || webpackTarget !== void 0;
|
|
887
|
+
if (isNest) {
|
|
888
|
+
if (webpackTarget === void 0) {
|
|
889
|
+
return {
|
|
890
|
+
operations: [],
|
|
891
|
+
skipped: `already building through sentinel: ${configFile ?? NEST_CONFIG_FILE} imports the nest preset and the nx target runs it. Nothing to change.`
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
const refusal = refusalFor(context.cwd, webpackTarget);
|
|
895
|
+
if (refusal !== void 0) return { operations: [], blocked: refusal };
|
|
896
|
+
const dropped = Object.keys(webpackTarget.configurations ?? {});
|
|
897
|
+
return {
|
|
898
|
+
operations: nestAdoptionOperations(context.cwd, webpackTarget),
|
|
899
|
+
notes: [
|
|
900
|
+
`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.`,
|
|
901
|
+
...dropped.length > 0 ? [
|
|
902
|
+
`dropped the \`${dropped.join("`, `")}\` configuration${dropped.length > 1 ? "s" : ""} from the build target: its fileReplacements point at files that do not exist, so it replaced nothing. A live one would have blocked this instead.`
|
|
903
|
+
] : [],
|
|
904
|
+
`verify with \`nx run ${webpackTarget.project}:generate-swagger-file\` and \`git diff --exit-code\`: it runs the built bundle, so it proves the service boots AND that its published contract did not move.`
|
|
905
|
+
]
|
|
906
|
+
};
|
|
907
|
+
}
|
|
570
908
|
if (configFile === void 0) {
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
`
|
|
574
|
-
|
|
575
|
-
} else
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
);
|
|
909
|
+
return {
|
|
910
|
+
operations: [],
|
|
911
|
+
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.`
|
|
912
|
+
};
|
|
913
|
+
} else {
|
|
914
|
+
const notEsm = esmBlocker(configFile, readProjectPackageJson(context.cwd).type);
|
|
915
|
+
if (notEsm !== void 0) return { operations: [], skipped: notEsm };
|
|
916
|
+
const rewrite = rewriteBuildImports(readConfigSource(context.cwd, configFile), configFile);
|
|
917
|
+
if (rewrite.kind === "blocked") {
|
|
918
|
+
return {
|
|
919
|
+
operations: [],
|
|
920
|
+
skipped: `${configFile} could not be pointed at sentinel, so nothing was written: ${rewrite.why}. Nothing here is broken; this needs a look before the role applies.`
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
if (rewrite.kind === "rewritten") {
|
|
924
|
+
operations.push({ kind: "write", path: configFile, contents: rewrite.source });
|
|
925
|
+
}
|
|
926
|
+
for (const name of COMPANION_CONFIG_FILES) {
|
|
927
|
+
const source = readConfigSource(context.cwd, name);
|
|
928
|
+
if (source === "") continue;
|
|
929
|
+
const companion = rewriteBuildImports(source, name);
|
|
930
|
+
if (companion.kind === "rewritten") {
|
|
931
|
+
operations.push({ kind: "write", path: name, contents: companion.source });
|
|
932
|
+
notes.push(`${name} now imports ${companion.moved.join(", ")} from sentinel too.`);
|
|
933
|
+
} else if (companion.kind === "blocked") {
|
|
934
|
+
notes.push(
|
|
935
|
+
`${name} imports the toolchain and could NOT be pointed at sentinel: ${companion.why}. It will stop resolving when the workspace root drops these packages, and it will fail your TESTS rather than your build.`
|
|
936
|
+
);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
if (rewrite.kind === "rewritten") {
|
|
940
|
+
notes.push(
|
|
941
|
+
`${configFile} now imports ${rewrite.moved.join(", ")} from \`${BUILD_PRESET_SPECIFIER}\`. Only the import lines changed: every attribute, alias and path is untouched, so this build produces what it produced before.`
|
|
942
|
+
);
|
|
943
|
+
}
|
|
579
944
|
}
|
|
580
945
|
operations.push(manifestOperation(context.cwd, buildScripts(context.cwd)));
|
|
581
946
|
operations.push(...nxTargetOperations({ cwd: context.cwd, targets: buildTarget() }));
|
|
@@ -588,134 +953,186 @@ function plan(context) {
|
|
|
588
953
|
}
|
|
589
954
|
return { operations, notes };
|
|
590
955
|
}
|
|
591
|
-
function
|
|
592
|
-
if (!
|
|
956
|
+
function readConfigSource(cwd, configFile) {
|
|
957
|
+
if (!existsSync8(join8(cwd, configFile))) return "";
|
|
593
958
|
try {
|
|
594
|
-
return
|
|
959
|
+
return readFileSync6(join8(cwd, configFile), "utf8");
|
|
595
960
|
} catch {
|
|
596
|
-
return
|
|
961
|
+
return "";
|
|
597
962
|
}
|
|
598
963
|
}
|
|
599
964
|
|
|
600
965
|
// src/roles/build/resolve-vite.ts
|
|
601
|
-
function
|
|
602
|
-
|
|
966
|
+
function resolveViteWith(cwd) {
|
|
967
|
+
const own = binFromOwnInstall("vite", "vite");
|
|
968
|
+
if (own) return { bin: own, origin: "sentinel" };
|
|
969
|
+
const module_ = resolveBin(cwd, "vite");
|
|
970
|
+
return module_ ? { bin: module_, origin: "module" } : { bin: void 0, origin: "none" };
|
|
603
971
|
}
|
|
604
972
|
function viteOrigin(cwd) {
|
|
605
|
-
|
|
606
|
-
return resolveBin(cwd, "vite") ? "module" : "none";
|
|
973
|
+
return resolveViteWith(cwd).origin;
|
|
607
974
|
}
|
|
608
975
|
|
|
609
|
-
// src/roles/build/adapters/vite/vite-
|
|
610
|
-
var
|
|
611
|
-
|
|
976
|
+
// src/roles/build/adapters/vite/vite-role.adapter.ts
|
|
977
|
+
var VITE_VALUE_FLAGS = [
|
|
978
|
+
"--config",
|
|
979
|
+
"-c",
|
|
980
|
+
"--mode",
|
|
981
|
+
"-m",
|
|
982
|
+
"--outDir",
|
|
983
|
+
"--logLevel",
|
|
984
|
+
"--base",
|
|
985
|
+
"--host",
|
|
986
|
+
"--port"
|
|
987
|
+
];
|
|
988
|
+
var ViteRoleAdapter = class extends BaseAdapter {
|
|
612
989
|
runner = "vite";
|
|
990
|
+
/**
|
|
991
|
+
* The presets this role has something to SAY about, which is wider than the ones it serves.
|
|
992
|
+
*
|
|
993
|
+
* React it serves. Svelte it does not, yet, and the difference matters to a developer: the
|
|
994
|
+
* plugin caps at Vite 6 until its major 7, which needs `svelte ^5.46.4`, so the role applies
|
|
995
|
+
* once the module moves. `plan` reports that chain and SKIPS, which it can only do if
|
|
996
|
+
* resolution let it run at all — filtering here would give "no adapter for preset svelte",
|
|
997
|
+
* which tells nobody what to do about it.
|
|
998
|
+
*
|
|
999
|
+
* Nest it now serves too, through `@hublo/sentinel/build/nest`. It used to be declined on the
|
|
1000
|
+
* grounds that "there is nothing to bundle", which was never true: those 36 services bundle
|
|
1001
|
+
* with webpack. What changed is that Nx v24 removes the `@nx/webpack:webpack` executor and
|
|
1002
|
+
* the `composePlugins` / `withNx` helpers their shared config is built on, and nx's own
|
|
1003
|
+
* migration generator refuses every one of them because they use `@nx/js:node`
|
|
1004
|
+
* (nrwl/nx#36389). So the way out runs through here.
|
|
1005
|
+
*
|
|
1006
|
+
* `node` and `tools` remain genuinely declined: nothing under them bundles at all.
|
|
1007
|
+
*/
|
|
613
1008
|
appliesTo(preset) {
|
|
614
|
-
return preset === "react";
|
|
1009
|
+
return preset === "react" || preset === "svelte" || preset === "nest";
|
|
615
1010
|
}
|
|
1011
|
+
/**
|
|
1012
|
+
* `react`, read from the module's Vite config rather than from its dependencies.
|
|
1013
|
+
*
|
|
1014
|
+
* Without this the role is unreachable in practice: the toolchain is declared at the ROOT,
|
|
1015
|
+
* so every front app detects as `node` and `appliesTo` filters the adapter out — including
|
|
1016
|
+
* during `sentinel --inspect` with no target, which never passes `--preset`.
|
|
1017
|
+
*/
|
|
616
1018
|
declaredPreset(cwd) {
|
|
617
1019
|
return declaredBuildPreset(cwd);
|
|
618
1020
|
}
|
|
619
1021
|
/**
|
|
620
|
-
* The same plan
|
|
1022
|
+
* The same plan for both targets, deliberately.
|
|
621
1023
|
*
|
|
622
|
-
* `build` and `serve` are one app's two calls into one toolchain, so adopting one without
|
|
623
|
-
*
|
|
624
|
-
* idempotent, so `--init --build` and `--init --dev` are interchangeable rather than
|
|
625
|
-
* additive.
|
|
1024
|
+
* `build` and `serve` are one app's two calls into one toolchain, so adopting one without the
|
|
1025
|
+
* other leaves the module half-migrated in a way nothing reports. The operations are
|
|
1026
|
+
* idempotent, so `--init --build` and `--init --dev` are interchangeable rather than additive.
|
|
626
1027
|
*/
|
|
627
1028
|
plan(context) {
|
|
628
1029
|
return plan(context);
|
|
629
1030
|
}
|
|
1031
|
+
async status(ctx) {
|
|
1032
|
+
const { adopted, preset, conformant, drift, unreadable } = readBuildAdoption(ctx.cwd);
|
|
1033
|
+
return { adopted, preset, conformant, drift, unreadable };
|
|
1034
|
+
}
|
|
630
1035
|
/**
|
|
631
|
-
*
|
|
632
|
-
* beyond the exit code the developer's own Ctrl-C produces.
|
|
1036
|
+
* Adoption, the binary, and the warning about which copy of Vite is running.
|
|
633
1037
|
*
|
|
634
|
-
*
|
|
635
|
-
*
|
|
636
|
-
*
|
|
1038
|
+
* A module that has not adopted is reported and PASSES, the same stance every other role
|
|
1039
|
+
* takes: `--run` with no named target sweeps every wired target across the workspace, so
|
|
1040
|
+
* failing here would exit non-zero on every module that has not migrated, which is most of
|
|
1041
|
+
* them.
|
|
637
1042
|
*/
|
|
638
|
-
|
|
1043
|
+
prepare(ctx) {
|
|
639
1044
|
if (!readBuildAdoption(ctx.cwd).adopted) {
|
|
640
|
-
|
|
641
|
-
`
|
|
642
|
-
`
|
|
1045
|
+
this.say(
|
|
1046
|
+
`no sentinel preset in this module's Vite config; run \`sentinel --init --build\` to adopt.`
|
|
643
1047
|
);
|
|
644
|
-
return { ok: true, code: 0 };
|
|
1048
|
+
return { kind: "stop", result: { ok: true, code: 0 } };
|
|
645
1049
|
}
|
|
646
|
-
const
|
|
647
|
-
if (!
|
|
648
|
-
|
|
649
|
-
`
|
|
650
|
-
`
|
|
1050
|
+
const { bin, origin } = resolveViteWith(ctx.cwd);
|
|
1051
|
+
if (!bin) {
|
|
1052
|
+
this.say(
|
|
1053
|
+
`could not find the vite binary (looked in ${binSearchPath(ctx.cwd)}). Run \`pnpm install\` in the module.`
|
|
651
1054
|
);
|
|
652
|
-
return { ok: false, code: 1 };
|
|
1055
|
+
return { kind: "stop", result: { ok: false, code: 1 } };
|
|
653
1056
|
}
|
|
654
|
-
if (
|
|
655
|
-
|
|
656
|
-
`
|
|
657
|
-
`
|
|
1057
|
+
if (origin === "module") {
|
|
1058
|
+
this.say(
|
|
1059
|
+
`using the MODULE's vite, not sentinel's. An adopted config gets its plugins from sentinel, and two copies of Vite fail in ways that never mention a version. Remove vite from this module's package.json.`
|
|
658
1060
|
);
|
|
659
1061
|
}
|
|
660
|
-
|
|
661
|
-
|
|
1062
|
+
return { kind: "ready", vite: bin };
|
|
1063
|
+
}
|
|
1064
|
+
/**
|
|
1065
|
+
* Run Vite with the caller's own arguments after `--`, and report what happened.
|
|
1066
|
+
*
|
|
1067
|
+
* `whenSignalled` is the only difference between the two targets here, and it is a real one:
|
|
1068
|
+
* a build killed by a signal did not produce a bundle, while a dev server killed by Ctrl-C
|
|
1069
|
+
* did exactly what the developer asked.
|
|
1070
|
+
*/
|
|
1071
|
+
spawnVite(ctx, vite, argv, whenSignalled) {
|
|
1072
|
+
const { options, paths } = splitToolArgs(ctx.cwd, ctx.toolArgs, {
|
|
1073
|
+
valueFlags: VITE_VALUE_FLAGS
|
|
1074
|
+
});
|
|
1075
|
+
const result = spawnSync(vite, [...argv, ...options, ...paths], {
|
|
1076
|
+
cwd: ctx.cwd,
|
|
1077
|
+
stdio: "inherit"
|
|
1078
|
+
});
|
|
662
1079
|
if (result.error) {
|
|
663
|
-
|
|
664
|
-
`);
|
|
1080
|
+
this.say(`could not run vite (${result.error.message})`);
|
|
665
1081
|
return { ok: false, code: 1 };
|
|
666
1082
|
}
|
|
667
|
-
const code = result.status ??
|
|
1083
|
+
const code = result.status ?? whenSignalled;
|
|
668
1084
|
return { ok: code === 0, code };
|
|
669
1085
|
}
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
1086
|
+
/** One prefix for every message this role emits, so they cannot drift apart. */
|
|
1087
|
+
say(message) {
|
|
1088
|
+
process.stderr.write(`sentinel ${this.label}(vite): ${message}
|
|
1089
|
+
`);
|
|
673
1090
|
}
|
|
674
1091
|
};
|
|
675
1092
|
|
|
676
|
-
// src/roles/build/adapters/vite/vite.adapter.ts
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
"base",
|
|
682
|
-
"root",
|
|
683
|
-
"define",
|
|
684
|
-
"server",
|
|
685
|
-
"build",
|
|
686
|
-
"resolve",
|
|
687
|
-
"plugins"
|
|
688
|
-
];
|
|
689
|
-
function describeDepartures(config, defaults) {
|
|
690
|
-
const departures = [];
|
|
691
|
-
const build = asRecord(config.build);
|
|
692
|
-
const target = build?.target;
|
|
693
|
-
if (typeof target === "string" && target !== defaults.target) {
|
|
694
|
-
departures.push({
|
|
695
|
-
rule: "build.target",
|
|
696
|
-
reason: `${target}, where the convention is ${defaults.target}`
|
|
697
|
-
});
|
|
1093
|
+
// src/roles/build/adapters/vite/vite-dev.adapter.ts
|
|
1094
|
+
var ViteDevAdapter = class extends ViteRoleAdapter {
|
|
1095
|
+
target = "dev";
|
|
1096
|
+
get label() {
|
|
1097
|
+
return "dev";
|
|
698
1098
|
}
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
1099
|
+
/**
|
|
1100
|
+
* `--init --dev` does the same thing as `--init --build`, and now says so.
|
|
1101
|
+
*
|
|
1102
|
+
* The shared plan is deliberate (see `ViteRoleAdapter.plan`): one plan writes both scripts,
|
|
1103
|
+
* because adopting one without the other leaves the module half-migrated. What was missing
|
|
1104
|
+
* was only the telling: the run printed a message about the build and nothing about `serve`,
|
|
1105
|
+
* so a developer could believe the dev server had been adopted on its own. Refusing was the
|
|
1106
|
+
* wrong correction, it added friction to a command that already produced the right result.
|
|
1107
|
+
*/
|
|
1108
|
+
plan(context) {
|
|
1109
|
+
const planned = super.plan(context);
|
|
1110
|
+
if (planned.operations.length === 0) return planned;
|
|
1111
|
+
return {
|
|
1112
|
+
...planned,
|
|
1113
|
+
notes: [
|
|
1114
|
+
...planned.notes ?? [],
|
|
1115
|
+
"--init --dev adopts the build too: one plan writes both `build` and `serve`."
|
|
1116
|
+
]
|
|
1117
|
+
};
|
|
706
1118
|
}
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
1119
|
+
/**
|
|
1120
|
+
* Start the dev server. It does not return until stopped, so there is no verdict to report
|
|
1121
|
+
* beyond the exit code the developer's own Ctrl-C produces.
|
|
1122
|
+
*
|
|
1123
|
+
* No `prebuild` here, unlike `--run --build`: the artefact this repo generates before a build
|
|
1124
|
+
* is produced by the watcher that WRAPS this command (`run-with-runtime-artifact-watch`), and
|
|
1125
|
+
* running it again would race the watcher that is about to own the file.
|
|
1126
|
+
*
|
|
1127
|
+
* No subcommand either. `vite`, `vite dev` and `vite serve` all start the server, and the
|
|
1128
|
+
* bare form is the one every version accepts.
|
|
1129
|
+
*/
|
|
1130
|
+
async run(ctx) {
|
|
1131
|
+
const ready = this.prepare(ctx);
|
|
1132
|
+
if (ready.kind === "stop") return ready.result;
|
|
1133
|
+
return this.spawnVite(ctx, ready.vite, [], 0);
|
|
1134
|
+
}
|
|
1135
|
+
};
|
|
719
1136
|
|
|
720
1137
|
// src/roles/build/prerequisite.ts
|
|
721
1138
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
@@ -743,122 +1160,55 @@ function runPrerequisite(cwd, env = process.env) {
|
|
|
743
1160
|
return { kind: "ran" };
|
|
744
1161
|
}
|
|
745
1162
|
|
|
746
|
-
// src/roles/build/resolve-config.ts
|
|
747
|
-
async function resolveBuildConfig(cwd) {
|
|
748
|
-
try {
|
|
749
|
-
const { loadConfigFromFile } = await import("vite");
|
|
750
|
-
const loaded = await loadConfigFromFile(
|
|
751
|
-
{ command: "build", mode: "production" },
|
|
752
|
-
void 0,
|
|
753
|
-
cwd,
|
|
754
|
-
"silent"
|
|
755
|
-
);
|
|
756
|
-
if (!loaded) return { kind: "failed", reason: "Vite found no config file in this module" };
|
|
757
|
-
return { kind: "loaded", config: loaded.config, from: loaded.path };
|
|
758
|
-
} catch (error) {
|
|
759
|
-
return { kind: "failed", reason: error instanceof Error ? error.message : String(error) };
|
|
760
|
-
}
|
|
761
|
-
}
|
|
762
|
-
|
|
763
1163
|
// src/roles/build/adapters/vite/vite.adapter.ts
|
|
764
|
-
var
|
|
765
|
-
var ViteAdapter = class extends BaseAdapter {
|
|
1164
|
+
var ViteAdapter = class extends ViteRoleAdapter {
|
|
766
1165
|
target = "build";
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
* React only. The two SvelteKit configs in this repo are 34 and 21 lines and share nothing
|
|
770
|
-
* with the React three; claiming them would mean a preset that fits neither.
|
|
771
|
-
*/
|
|
772
|
-
appliesTo(preset) {
|
|
773
|
-
return preset === "react";
|
|
774
|
-
}
|
|
775
|
-
/**
|
|
776
|
-
* `react`, read from the module's Vite config rather than from its dependencies.
|
|
777
|
-
*
|
|
778
|
-
* Without this the role is unreachable in practice: the toolchain is declared at the ROOT,
|
|
779
|
-
* so every front app detects as `node` and `appliesTo` filters the adapter out — including
|
|
780
|
-
* during `sentinel --inspect` with no target, which never passes `--preset`.
|
|
781
|
-
*/
|
|
782
|
-
declaredPreset(cwd) {
|
|
783
|
-
return declaredBuildPreset(cwd);
|
|
784
|
-
}
|
|
785
|
-
plan(context) {
|
|
786
|
-
return plan(context);
|
|
1166
|
+
get label() {
|
|
1167
|
+
return "build";
|
|
787
1168
|
}
|
|
788
1169
|
/**
|
|
789
1170
|
* Build the module.
|
|
790
1171
|
*
|
|
791
|
-
*
|
|
792
|
-
*
|
|
793
|
-
* failing here would exit non-zero on every module that has not migrated, which is most of
|
|
794
|
-
* them.
|
|
1172
|
+
* Adoption, the binary and the two-copies warning are `prepare`'s, shared with `--dev`. What
|
|
1173
|
+
* is this target's own is the `prebuild` step and the `build` subcommand.
|
|
795
1174
|
*/
|
|
796
1175
|
async run(ctx) {
|
|
797
|
-
const
|
|
798
|
-
if (
|
|
799
|
-
process.stderr.write(
|
|
800
|
-
`sentinel build(vite): no sentinel preset in this module's Vite config; run \`sentinel --init --build\` to adopt.
|
|
801
|
-
`
|
|
802
|
-
);
|
|
803
|
-
return { ok: true, code: 0 };
|
|
804
|
-
}
|
|
1176
|
+
const ready = this.prepare(ctx);
|
|
1177
|
+
if (ready.kind === "stop") return ready.result;
|
|
805
1178
|
const prerequisite = runPrerequisite(ctx.cwd);
|
|
806
1179
|
if (prerequisite.kind === "failed") {
|
|
807
|
-
|
|
808
|
-
`
|
|
809
|
-
`
|
|
1180
|
+
this.say(
|
|
1181
|
+
`the ${PREBUILD_SCRIPT_NAME} step failed, so the build was not started: ${prerequisite.reason}`
|
|
810
1182
|
);
|
|
811
1183
|
return { ok: false, code: prerequisite.code };
|
|
812
1184
|
}
|
|
813
|
-
|
|
814
|
-
if (!vite) {
|
|
815
|
-
process.stderr.write(
|
|
816
|
-
`sentinel build(vite): could not find the vite binary (looked in ${binSearchPath(ctx.cwd)}). Run \`pnpm install\` in the module.
|
|
817
|
-
`
|
|
818
|
-
);
|
|
819
|
-
return { ok: false, code: 1 };
|
|
820
|
-
}
|
|
821
|
-
if (viteOrigin(ctx.cwd) === "module") {
|
|
822
|
-
process.stderr.write(
|
|
823
|
-
`sentinel build(vite): building with the MODULE's vite, not sentinel's. An adopted config gets its plugins from sentinel, and two copies of Vite in one build fail in ways that never mention a version. Remove vite from this module's package.json.
|
|
824
|
-
`
|
|
825
|
-
);
|
|
826
|
-
}
|
|
827
|
-
const { options, paths } = splitToolArgs(ctx.cwd, ctx.toolArgs, {
|
|
828
|
-
valueFlags: VITE_VALUE_FLAGS
|
|
829
|
-
});
|
|
830
|
-
const result = spawnSync3(vite, ["build", ...options, ...paths], {
|
|
831
|
-
cwd: ctx.cwd,
|
|
832
|
-
stdio: "inherit"
|
|
833
|
-
});
|
|
834
|
-
if (result.error) {
|
|
835
|
-
process.stderr.write(`sentinel build(vite): could not run vite (${result.error.message})
|
|
836
|
-
`);
|
|
837
|
-
return { ok: false, code: 1 };
|
|
838
|
-
}
|
|
839
|
-
const code = result.status ?? 1;
|
|
840
|
-
return { ok: code === 0, code };
|
|
1185
|
+
return this.spawnVite(ctx, ready.vite, ["build"], 1);
|
|
841
1186
|
}
|
|
842
1187
|
/**
|
|
843
|
-
* What this module builds with,
|
|
1188
|
+
* What this module builds with, WITHOUT loading its config.
|
|
844
1189
|
*
|
|
845
|
-
*
|
|
846
|
-
*
|
|
847
|
-
*
|
|
1190
|
+
* Everything reported here is read from the manifest and from the config's text: whether the
|
|
1191
|
+
* module imports its toolchain from sentinel, which Vite install would answer, what it still
|
|
1192
|
+
* declares of its own, and whether it has a `prebuild` step.
|
|
848
1193
|
*
|
|
849
|
-
*
|
|
850
|
-
*
|
|
851
|
-
*
|
|
1194
|
+
* It used to also load the config through Vite and report where it departed from a preset
|
|
1195
|
+
* and what it added on top. Both were dropped with the preset itself. Adoption changes a
|
|
1196
|
+
* config's IMPORTS and nothing else, so there is no preset the app signed up to and no
|
|
1197
|
+
* distance from it to measure; and computing it meant EXECUTING an app's config, which is
|
|
1198
|
+
* expensive and can fail on its own terms (career throws without `VITE_BRAND_ID`). Reading
|
|
1199
|
+
* a file to report on it must not be able to break.
|
|
852
1200
|
*
|
|
853
|
-
*
|
|
854
|
-
*
|
|
1201
|
+
* They come back with the shared base, when there is again something an app has chosen to
|
|
1202
|
+
* adopt and can therefore depart from.
|
|
855
1203
|
*/
|
|
856
1204
|
async inspect(ctx) {
|
|
857
1205
|
const adoption = readBuildAdoption(ctx.cwd);
|
|
858
|
-
|
|
1206
|
+
return {
|
|
859
1207
|
runner: "vite",
|
|
860
1208
|
configFile: adoption.configFile,
|
|
861
1209
|
...adoption.unreadable ? { unreadable: adoption.unreadable } : {},
|
|
1210
|
+
// The field nothing else reports: an adopted config takes its plugins from sentinel, so
|
|
1211
|
+
// a module answering `module` here is one build away from the two-copies failure.
|
|
862
1212
|
vite: viteOrigin(ctx.cwd),
|
|
863
1213
|
// Rendered as a labelled block by the shared renderer, the same shape the lint role's
|
|
864
1214
|
// parked rules and the format role's overrides already use.
|
|
@@ -868,34 +1218,6 @@ var ViteAdapter = class extends BaseAdapter {
|
|
|
868
1218
|
})),
|
|
869
1219
|
prerequisite: moduleScripts(ctx.cwd)[PREBUILD_SCRIPT_NAME] ?? null
|
|
870
1220
|
};
|
|
871
|
-
if (adoption.configFile === null) return base;
|
|
872
|
-
const resolved = await resolveBuildConfig(ctx.cwd);
|
|
873
|
-
if (resolved.kind === "failed") {
|
|
874
|
-
return { ...base, configError: resolved.reason };
|
|
875
|
-
}
|
|
876
|
-
const config = resolved.config;
|
|
877
|
-
return {
|
|
878
|
-
...base,
|
|
879
|
-
// What the app departs from, and what it adds, are different facts. A DEPARTURE is a
|
|
880
|
-
// disagreement with an opinion the preset holds; an ADDITION is the app needing
|
|
881
|
-
// something the preset never claimed, which is the preset working rather than being
|
|
882
|
-
// worked around.
|
|
883
|
-
departures: describeDepartures(config, REACT_APP_DEFAULTS),
|
|
884
|
-
additions: appAdditions(config),
|
|
885
|
-
resolved: {
|
|
886
|
-
base: config.base ?? null,
|
|
887
|
-
target: config.build?.target ?? null,
|
|
888
|
-
sourcemap: config.build?.sourcemap ?? null,
|
|
889
|
-
port: config.server?.port ?? null,
|
|
890
|
-
// The one number nothing else reports and that no formula predicts: career goes DOWN
|
|
891
|
-
// to 9998 where the others go up, which is why it is data rather than `port + 1`.
|
|
892
|
-
hmrPort: config.server?.hmr?.port ?? null,
|
|
893
|
-
// Counted, not listed. It is 56-67% of every config today, and printing 296 entries
|
|
894
|
-
// would bury everything above it. The count is what tells you whether it moved.
|
|
895
|
-
aliases: Array.isArray(config.resolve?.alias) ? config.resolve.alias.length : 0,
|
|
896
|
-
plugins: Array.isArray(config.plugins) ? config.plugins.flat(9).length : 0
|
|
897
|
-
}
|
|
898
|
-
};
|
|
899
1221
|
}
|
|
900
1222
|
/**
|
|
901
1223
|
* Adoption as data, WITHOUT building.
|
|
@@ -920,10 +1242,6 @@ var ViteAdapter = class extends BaseAdapter {
|
|
|
920
1242
|
}
|
|
921
1243
|
};
|
|
922
1244
|
}
|
|
923
|
-
async status(ctx) {
|
|
924
|
-
const { adopted, preset, conformant, drift, unreadable } = readBuildAdoption(ctx.cwd);
|
|
925
|
-
return { adopted, preset, conformant, drift, unreadable };
|
|
926
|
-
}
|
|
927
1245
|
};
|
|
928
1246
|
|
|
929
1247
|
// src/roles/build/register.ts
|
|
@@ -935,13 +1253,13 @@ function registerBuild() {
|
|
|
935
1253
|
}
|
|
936
1254
|
|
|
937
1255
|
// src/roles/format/adapters/oxfmt/oxfmt.adapter.ts
|
|
938
|
-
import { spawnSync as
|
|
939
|
-
import { existsSync as
|
|
940
|
-
import { join as
|
|
1256
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
1257
|
+
import { existsSync as existsSync13, readFileSync as readFileSync12 } from "fs";
|
|
1258
|
+
import { join as join15 } from "path";
|
|
941
1259
|
|
|
942
1260
|
// src/core/config/has-source.ts
|
|
943
1261
|
import { readdirSync } from "fs";
|
|
944
|
-
import { extname, join as
|
|
1262
|
+
import { extname, join as join9, relative as relative3 } from "path";
|
|
945
1263
|
var SKIP_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
946
1264
|
"node_modules",
|
|
947
1265
|
"dist",
|
|
@@ -992,7 +1310,7 @@ function hasSourceFiles(cwd, extensions) {
|
|
|
992
1310
|
}
|
|
993
1311
|
for (const entry of entries) {
|
|
994
1312
|
if (entry.isDirectory()) {
|
|
995
|
-
if (!SKIP_DIRECTORIES.has(entry.name)) queue.push(
|
|
1313
|
+
if (!SKIP_DIRECTORIES.has(entry.name)) queue.push(join9(dir, entry.name));
|
|
996
1314
|
continue;
|
|
997
1315
|
}
|
|
998
1316
|
if (wanted.has(extname(entry.name))) return true;
|
|
@@ -1000,14 +1318,44 @@ function hasSourceFiles(cwd, extensions) {
|
|
|
1000
1318
|
}
|
|
1001
1319
|
return false;
|
|
1002
1320
|
}
|
|
1321
|
+
function listSourceFiles(cwd, extensions) {
|
|
1322
|
+
const wanted = new Set(extensions);
|
|
1323
|
+
const queue = [cwd];
|
|
1324
|
+
const found = [];
|
|
1325
|
+
while (queue.length > 0) {
|
|
1326
|
+
const dir = queue.pop();
|
|
1327
|
+
let entries;
|
|
1328
|
+
try {
|
|
1329
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
1330
|
+
} catch {
|
|
1331
|
+
continue;
|
|
1332
|
+
}
|
|
1333
|
+
for (const entry of entries) {
|
|
1334
|
+
const full = join9(dir, entry.name);
|
|
1335
|
+
if (entry.isDirectory()) {
|
|
1336
|
+
if (!SKIP_DIRECTORIES.has(entry.name)) queue.push(full);
|
|
1337
|
+
continue;
|
|
1338
|
+
}
|
|
1339
|
+
if (wanted.has(extname(entry.name))) found.push(relative3(cwd, full));
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
return found;
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
// src/core/named-scope.ts
|
|
1346
|
+
var TOLERATE_EMPTY_FLAG = "--no-error-on-unmatched-pattern";
|
|
1347
|
+
function scopeFlags(paths, options = []) {
|
|
1348
|
+
if (paths.length === 0 || options.includes(TOLERATE_EMPTY_FLAG)) return [];
|
|
1349
|
+
return [TOLERATE_EMPTY_FLAG];
|
|
1350
|
+
}
|
|
1003
1351
|
|
|
1004
1352
|
// src/core/settings.ts
|
|
1005
1353
|
var WORKSPACE_ROOT_MARKER = "nx.json";
|
|
1006
1354
|
var DEFAULT_MAX_DIAGNOSTICS = 100;
|
|
1007
1355
|
|
|
1008
1356
|
// src/core/workspace-prep.ts
|
|
1009
|
-
import { existsSync as
|
|
1010
|
-
import { dirname as dirname3, join as
|
|
1357
|
+
import { existsSync as existsSync9, readFileSync as readFileSync7, writeFileSync } from "fs";
|
|
1358
|
+
import { dirname as dirname3, join as join10, relative as relative4 } from "path";
|
|
1011
1359
|
var OVERRIDE_KEY = "i18next>typescript";
|
|
1012
1360
|
var NATIVE_TS_ALIAS = "@typescript/native";
|
|
1013
1361
|
var WORKSPACE_YAML = "pnpm-workspace.yaml";
|
|
@@ -1016,7 +1364,7 @@ var LIST_ITEM = /^(\s*)-\s+(.*?)\s*$/;
|
|
|
1016
1364
|
function findWorkspaceRoot(startDir) {
|
|
1017
1365
|
let dir = startDir;
|
|
1018
1366
|
for (; ; ) {
|
|
1019
|
-
if (
|
|
1367
|
+
if (existsSync9(join10(dir, WORKSPACE_ROOT_MARKER))) return dir;
|
|
1020
1368
|
const parent = dirname3(dir);
|
|
1021
1369
|
if (parent === dir) return void 0;
|
|
1022
1370
|
dir = parent;
|
|
@@ -1029,9 +1377,9 @@ function declaredNativeTs(pkg) {
|
|
|
1029
1377
|
return version || void 0;
|
|
1030
1378
|
}
|
|
1031
1379
|
function ensureI18nextSingleton(root, dryRun) {
|
|
1032
|
-
const pkgPath =
|
|
1033
|
-
if (!
|
|
1034
|
-
const pkg = JSON.parse(
|
|
1380
|
+
const pkgPath = join10(root, "package.json");
|
|
1381
|
+
if (!existsSync9(pkgPath)) return void 0;
|
|
1382
|
+
const pkg = JSON.parse(readFileSync7(pkgPath, "utf8"));
|
|
1035
1383
|
const want = declaredNativeTs(pkg);
|
|
1036
1384
|
if (!want) return void 0;
|
|
1037
1385
|
const have = pkg.pnpm?.overrides?.[OVERRIDE_KEY];
|
|
@@ -1044,10 +1392,10 @@ function ensureI18nextSingleton(root, dryRun) {
|
|
|
1044
1392
|
return `pinned ${OVERRIDE_KEY} to ${want} in package.json (i18next singleton)`;
|
|
1045
1393
|
}
|
|
1046
1394
|
function ensureReleaseAgeAllowList(root, dryRun) {
|
|
1047
|
-
const yamlPath =
|
|
1048
|
-
if (!
|
|
1395
|
+
const yamlPath = join10(root, WORKSPACE_YAML);
|
|
1396
|
+
if (!existsSync9(yamlPath)) return void 0;
|
|
1049
1397
|
const own = readOwnPackage().name;
|
|
1050
|
-
const lines =
|
|
1398
|
+
const lines = readFileSync7(yamlPath, "utf8").split("\n");
|
|
1051
1399
|
const keyIdx = lines.findIndex((line) => line.replace(/\s+$/, "") === `${RELEASE_AGE_KEY}:`);
|
|
1052
1400
|
if (keyIdx === -1) return void 0;
|
|
1053
1401
|
let lastItemIdx = keyIdx;
|
|
@@ -1080,13 +1428,13 @@ var ROOT_PRETTIER_CONFIGS = [
|
|
|
1080
1428
|
"prettier.config.mjs"
|
|
1081
1429
|
];
|
|
1082
1430
|
function ensureFormatterExclusion(root, moduleDir, dryRun) {
|
|
1083
|
-
const rel =
|
|
1431
|
+
const rel = relative4(root, moduleDir).replaceAll("\\", "/");
|
|
1084
1432
|
if (rel === "" || rel.startsWith("..")) return void 0;
|
|
1085
|
-
const ignorePath =
|
|
1086
|
-
const hasPrettier =
|
|
1433
|
+
const ignorePath = join10(root, PRETTIER_IGNORE);
|
|
1434
|
+
const hasPrettier = existsSync9(ignorePath) || ROOT_PRETTIER_CONFIGS.some((name) => existsSync9(join10(root, name)));
|
|
1087
1435
|
if (!hasPrettier) return void 0;
|
|
1088
1436
|
const pattern = `/${rel}/`;
|
|
1089
|
-
const existing =
|
|
1437
|
+
const existing = existsSync9(ignorePath) ? readFileSync7(ignorePath, "utf8") : "";
|
|
1090
1438
|
const lines = existing.split("\n");
|
|
1091
1439
|
if (lines.some((line) => line.trim().replace(/\/$/, "") === pattern.replace(/\/$/, ""))) {
|
|
1092
1440
|
return void 0;
|
|
@@ -1122,10 +1470,10 @@ function ensureWorkspacePrep(opts) {
|
|
|
1122
1470
|
function inspectWorkspacePrep(root) {
|
|
1123
1471
|
const entries = [];
|
|
1124
1472
|
let pkg = {};
|
|
1125
|
-
const pkgPath =
|
|
1126
|
-
if (
|
|
1473
|
+
const pkgPath = join10(root, "package.json");
|
|
1474
|
+
if (existsSync9(pkgPath)) {
|
|
1127
1475
|
try {
|
|
1128
|
-
pkg = JSON.parse(
|
|
1476
|
+
pkg = JSON.parse(readFileSync7(pkgPath, "utf8"));
|
|
1129
1477
|
} catch {
|
|
1130
1478
|
pkg = {};
|
|
1131
1479
|
}
|
|
@@ -1139,9 +1487,9 @@ function inspectWorkspacePrep(root) {
|
|
|
1139
1487
|
});
|
|
1140
1488
|
}
|
|
1141
1489
|
const own = readOwnPackage().name;
|
|
1142
|
-
const yamlPath =
|
|
1143
|
-
if (
|
|
1144
|
-
const yaml =
|
|
1490
|
+
const yamlPath = join10(root, WORKSPACE_YAML);
|
|
1491
|
+
if (existsSync9(yamlPath)) {
|
|
1492
|
+
const yaml = readFileSync7(yamlPath, "utf8");
|
|
1145
1493
|
const listed = new RegExp(`^\\s*-\\s*['"]?${own.replace("/", "\\/")}['"]?\\s*$`, "m").test(yaml);
|
|
1146
1494
|
if (listed) {
|
|
1147
1495
|
const gated = new RegExp(`^\\s*minimumReleaseAge\\s*:`, "m").test(yaml);
|
|
@@ -1228,8 +1576,8 @@ function writesWhenRewritten(name, command) {
|
|
|
1228
1576
|
var CHECK_SCRIPT_NAMES = /* @__PURE__ */ new Set(["lint", "format", "check", "test", "typecheck", "verify"]);
|
|
1229
1577
|
|
|
1230
1578
|
// src/roles/format/inherited-ignores.ts
|
|
1231
|
-
import { existsSync as
|
|
1232
|
-
import { join as
|
|
1579
|
+
import { existsSync as existsSync10, readFileSync as readFileSync8 } from "fs";
|
|
1580
|
+
import { join as join11 } from "path";
|
|
1233
1581
|
var ROOT_IGNORE_FILE = ".prettierignore";
|
|
1234
1582
|
function isPattern(line) {
|
|
1235
1583
|
const trimmed = line.trim();
|
|
@@ -1244,11 +1592,11 @@ function toModulePattern(pattern) {
|
|
|
1244
1592
|
}
|
|
1245
1593
|
function inheritedIgnorePatterns(workspaceRoot) {
|
|
1246
1594
|
if (!workspaceRoot) return [];
|
|
1247
|
-
const path =
|
|
1248
|
-
if (!
|
|
1595
|
+
const path = join11(workspaceRoot, ROOT_IGNORE_FILE);
|
|
1596
|
+
if (!existsSync10(path)) return [];
|
|
1249
1597
|
let contents;
|
|
1250
1598
|
try {
|
|
1251
|
-
contents =
|
|
1599
|
+
contents = readFileSync8(path, "utf8");
|
|
1252
1600
|
} catch {
|
|
1253
1601
|
return [];
|
|
1254
1602
|
}
|
|
@@ -1335,13 +1683,13 @@ function formatPresetFor(preset) {
|
|
|
1335
1683
|
}
|
|
1336
1684
|
|
|
1337
1685
|
// src/roles/format/prettier-config.ts
|
|
1338
|
-
import { existsSync as
|
|
1339
|
-
import { join as
|
|
1686
|
+
import { existsSync as existsSync11, readFileSync as readFileSync10 } from "fs";
|
|
1687
|
+
import { join as join13 } from "path";
|
|
1340
1688
|
|
|
1341
1689
|
// src/roles/format/resolve-oxfmt.ts
|
|
1342
|
-
import { readFileSync as
|
|
1690
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
1343
1691
|
import { createRequire as createRequire2 } from "module";
|
|
1344
|
-
import { dirname as dirname4, join as
|
|
1692
|
+
import { dirname as dirname4, join as join12 } from "path";
|
|
1345
1693
|
function resolveOxfmt(cwd) {
|
|
1346
1694
|
return resolveBin(cwd, "oxfmt") ?? binFromOwnInstall("oxfmt", "oxfmt");
|
|
1347
1695
|
}
|
|
@@ -1370,8 +1718,8 @@ function configSchema() {
|
|
|
1370
1718
|
function readConfigSchema() {
|
|
1371
1719
|
try {
|
|
1372
1720
|
const manifest = createRequire2(import.meta.url).resolve("oxfmt/package.json");
|
|
1373
|
-
const schemaPath =
|
|
1374
|
-
return JSON.parse(
|
|
1721
|
+
const schemaPath = join12(dirname4(manifest), "configuration_schema.json");
|
|
1722
|
+
return JSON.parse(readFileSync9(schemaPath, "utf8"));
|
|
1375
1723
|
} catch {
|
|
1376
1724
|
return void 0;
|
|
1377
1725
|
}
|
|
@@ -1402,14 +1750,14 @@ function toOxfmtOverrides(value) {
|
|
|
1402
1750
|
return { overrides, unresolved };
|
|
1403
1751
|
}
|
|
1404
1752
|
function readPrettierSettings(cwd) {
|
|
1405
|
-
const file = PRETTIER_CONFIG_FILES.find((name) =>
|
|
1753
|
+
const file = PRETTIER_CONFIG_FILES.find((name) => existsSync11(join13(cwd, name)));
|
|
1406
1754
|
if (!file) return { options: {}, unresolved: [] };
|
|
1407
1755
|
if (/\.(js|cjs|mjs)$/.test(file)) {
|
|
1408
1756
|
return { options: {}, file, unresolved: [`${file} is JavaScript, so it was not executed`] };
|
|
1409
1757
|
}
|
|
1410
1758
|
let parsed;
|
|
1411
1759
|
try {
|
|
1412
|
-
parsed = parseJsonc(
|
|
1760
|
+
parsed = parseJsonc(readFileSync10(join13(cwd, file), "utf8"), file);
|
|
1413
1761
|
} catch {
|
|
1414
1762
|
return { options: {}, file, unresolved: [`${file} could not be parsed`] };
|
|
1415
1763
|
}
|
|
@@ -1439,8 +1787,8 @@ function readPrettierSettings(cwd) {
|
|
|
1439
1787
|
}
|
|
1440
1788
|
|
|
1441
1789
|
// src/roles/format/read-adoption.ts
|
|
1442
|
-
import { existsSync as
|
|
1443
|
-
import { join as
|
|
1790
|
+
import { existsSync as existsSync12, readFileSync as readFileSync11 } from "fs";
|
|
1791
|
+
import { join as join14 } from "path";
|
|
1444
1792
|
var NOT_ADOPTED2 = (configFile, unreadable = null) => ({
|
|
1445
1793
|
configFile,
|
|
1446
1794
|
preset: null,
|
|
@@ -1456,11 +1804,11 @@ function sameValue(a, b) {
|
|
|
1456
1804
|
return JSON.stringify(a) === JSON.stringify(b);
|
|
1457
1805
|
}
|
|
1458
1806
|
function readFormatAdoption(cwd) {
|
|
1459
|
-
const path =
|
|
1460
|
-
if (!
|
|
1807
|
+
const path = join14(cwd, FORMAT_CONFIG_FILE);
|
|
1808
|
+
if (!existsSync12(path)) return NOT_ADOPTED2(null);
|
|
1461
1809
|
let parsed;
|
|
1462
1810
|
try {
|
|
1463
|
-
parsed = parseJsonc(
|
|
1811
|
+
parsed = parseJsonc(readFileSync11(path, "utf8"), FORMAT_CONFIG_FILE);
|
|
1464
1812
|
} catch (error) {
|
|
1465
1813
|
const reason = error instanceof Error ? error.message : String(error);
|
|
1466
1814
|
return NOT_ADOPTED2(FORMAT_CONFIG_FILE, `${FORMAT_CONFIG_FILE} could not be parsed (${reason})`);
|
|
@@ -1527,6 +1875,22 @@ var OxfmtAdapter = class extends BaseAdapter {
|
|
|
1527
1875
|
appliesTo(_preset) {
|
|
1528
1876
|
return true;
|
|
1529
1877
|
}
|
|
1878
|
+
/**
|
|
1879
|
+
* The preset this module COMMITTED to, read from the `$sentinel` block it writes.
|
|
1880
|
+
*
|
|
1881
|
+
* Every other role implements this and format did not, so it was the only one answering from
|
|
1882
|
+
* dependency detection alone. On an adopted Nest module whose own `.oxfmtrc.json` records
|
|
1883
|
+
* `"preset": "nest"`, `--inspect` reported `flavour=node` — the module knew, and the role did
|
|
1884
|
+
* not pass it on. Same command, four rows, two different answers about what the module is.
|
|
1885
|
+
*
|
|
1886
|
+
* It reads the RECORD rather than re-deriving: the block is what `--init` wrote, so it is the
|
|
1887
|
+
* module's declaration, and a re-derivation from hoisted dependencies is exactly the wrong
|
|
1888
|
+
* answer this exists to avoid.
|
|
1889
|
+
*/
|
|
1890
|
+
declaredPreset(cwd) {
|
|
1891
|
+
const declared = readFormatAdoption(cwd).preset;
|
|
1892
|
+
return declared !== null && PRESET_NAMES.includes(declared) ? declared : void 0;
|
|
1893
|
+
}
|
|
1530
1894
|
plan(context) {
|
|
1531
1895
|
if (!hasSourceFiles(context.cwd, FORMATTABLE_EXTENSIONS)) {
|
|
1532
1896
|
return {
|
|
@@ -1569,7 +1933,7 @@ var OxfmtAdapter = class extends BaseAdapter {
|
|
|
1569
1933
|
manifestOperation(context.cwd, this.formatScripts(context.cwd))
|
|
1570
1934
|
];
|
|
1571
1935
|
const prettierConfigs = PRETTIER_CONFIG_FILES.filter(
|
|
1572
|
-
(name) =>
|
|
1936
|
+
(name) => existsSync13(join15(context.cwd, name))
|
|
1573
1937
|
);
|
|
1574
1938
|
for (const name of prettierConfigs) operations.push({ kind: "delete", path: name });
|
|
1575
1939
|
const removableDeps = this.modulePrettierDependencies(context.cwd);
|
|
@@ -1628,7 +1992,7 @@ var OxfmtAdapter = class extends BaseAdapter {
|
|
|
1628
1992
|
async afterInit(ctx) {
|
|
1629
1993
|
const oxfmt = resolveOxfmt(ctx.cwd);
|
|
1630
1994
|
if (!oxfmt) return;
|
|
1631
|
-
const pass =
|
|
1995
|
+
const pass = spawnSync3(oxfmt, ["--write", "."], { cwd: ctx.cwd, encoding: "utf8" });
|
|
1632
1996
|
process.stderr.write(` ${palette(process.stderr).dim(FORMATTER_DIFFERENCES)}
|
|
1633
1997
|
`);
|
|
1634
1998
|
if (pass.status === 0) return;
|
|
@@ -1691,10 +2055,14 @@ var OxfmtAdapter = class extends BaseAdapter {
|
|
|
1691
2055
|
const { options, paths } = splitToolArgs(ctx.cwd, ctx.toolArgs, {
|
|
1692
2056
|
valueFlags: OXFMT_VALUE_FLAGS
|
|
1693
2057
|
});
|
|
1694
|
-
const result =
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
2058
|
+
const result = spawnSync3(
|
|
2059
|
+
oxfmt,
|
|
2060
|
+
[mode, ...scopeFlags(paths, options), ...options, ...paths.length > 0 ? paths : ["."]],
|
|
2061
|
+
{
|
|
2062
|
+
cwd: ctx.cwd,
|
|
2063
|
+
stdio: "inherit"
|
|
2064
|
+
}
|
|
2065
|
+
);
|
|
1698
2066
|
if (result.error) {
|
|
1699
2067
|
process.stderr.write(
|
|
1700
2068
|
`sentinel format(oxfmt): could not run oxfmt (${result.error.message})
|
|
@@ -1769,7 +2137,7 @@ var OxfmtAdapter = class extends BaseAdapter {
|
|
|
1769
2137
|
);
|
|
1770
2138
|
return { ok: false, code: 1, metrics: { ...base, unformatted: null } };
|
|
1771
2139
|
}
|
|
1772
|
-
const result =
|
|
2140
|
+
const result = spawnSync3(oxfmt, ["--list-different", "."], { cwd: ctx.cwd, encoding: "utf8" });
|
|
1773
2141
|
if (result.error) {
|
|
1774
2142
|
process.stderr.write(
|
|
1775
2143
|
`sentinel format(oxfmt): could not run oxfmt (${result.error.message})
|
|
@@ -1805,7 +2173,7 @@ var OxfmtAdapter = class extends BaseAdapter {
|
|
|
1805
2173
|
modulePrettierDependencies(cwd) {
|
|
1806
2174
|
const isPrettierPackage = (name) => name === "prettier" || name.startsWith("prettier-plugin-") || name.startsWith("@prettier/");
|
|
1807
2175
|
try {
|
|
1808
|
-
const manifest = JSON.parse(
|
|
2176
|
+
const manifest = JSON.parse(readFileSync12(join15(cwd, "package.json"), "utf8"));
|
|
1809
2177
|
return Object.keys(manifest.devDependencies ?? {}).filter(isPrettierPackage).map((name) => ["devDependencies", name]);
|
|
1810
2178
|
} catch {
|
|
1811
2179
|
return [];
|
|
@@ -1884,15 +2252,122 @@ function registerFormat() {
|
|
|
1884
2252
|
register(new OxfmtAdapter());
|
|
1885
2253
|
setDefaultRunner("format", "oxfmt");
|
|
1886
2254
|
}
|
|
1887
|
-
|
|
1888
|
-
// src/roles/lint/adapters/oxlint/oxlint.adapter.ts
|
|
1889
|
-
import { spawnSync as
|
|
1890
|
-
import { existsSync as
|
|
1891
|
-
import { join as
|
|
1892
|
-
|
|
1893
|
-
// src/core/config/deferred-rules.ts
|
|
1894
|
-
function deferredRuleNames(rules) {
|
|
1895
|
-
return rules.map((entry) => entry.rule);
|
|
2255
|
+
|
|
2256
|
+
// src/roles/lint/adapters/oxlint/oxlint.adapter.ts
|
|
2257
|
+
import { spawnSync as spawnSync4 } from "child_process";
|
|
2258
|
+
import { existsSync as existsSync21, readFileSync as readFileSync20, rmSync, writeFileSync as writeFileSync2 } from "fs";
|
|
2259
|
+
import { join as join25 } from "path";
|
|
2260
|
+
|
|
2261
|
+
// src/core/config/deferred-rules.ts
|
|
2262
|
+
function deferredRuleNames(rules) {
|
|
2263
|
+
return rules.map((entry) => entry.rule);
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
// src/core/fix-report.ts
|
|
2267
|
+
import { statSync } from "fs";
|
|
2268
|
+
import { join as join16 } from "path";
|
|
2269
|
+
var CONFIG_REFUSED = /failed to parse .*config|invalid config file/i;
|
|
2270
|
+
function readFailure(output) {
|
|
2271
|
+
const lines = output.split("\n").map((line2) => line2.trim());
|
|
2272
|
+
if (!lines.some((line2) => CONFIG_REFUSED.test(line2))) return void 0;
|
|
2273
|
+
const detail = lines.find((line2) => /invalid config file/i.test(line2));
|
|
2274
|
+
const line = (detail ?? lines.find((entry) => entry !== "") ?? "").replace(/^x\s*/, "");
|
|
2275
|
+
return line.split(/:\s*Failed to parse config/i)[0] ?? line;
|
|
2276
|
+
}
|
|
2277
|
+
function fingerprint(path) {
|
|
2278
|
+
try {
|
|
2279
|
+
const stat = statSync(path);
|
|
2280
|
+
return `${stat.size}:${stat.mtimeMs}`;
|
|
2281
|
+
} catch {
|
|
2282
|
+
return void 0;
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
function snapshotFiles(cwd, extensions) {
|
|
2286
|
+
const snapshot = /* @__PURE__ */ new Map();
|
|
2287
|
+
for (const file of listSourceFiles(cwd, extensions)) {
|
|
2288
|
+
const mark = fingerprint(join16(cwd, file));
|
|
2289
|
+
if (mark !== void 0) snapshot.set(file, mark);
|
|
2290
|
+
}
|
|
2291
|
+
return snapshot;
|
|
2292
|
+
}
|
|
2293
|
+
function changedSince(cwd, before, extensions) {
|
|
2294
|
+
const changed = [];
|
|
2295
|
+
for (const file of listSourceFiles(cwd, extensions)) {
|
|
2296
|
+
const now = fingerprint(join16(cwd, file));
|
|
2297
|
+
if (now === void 0) continue;
|
|
2298
|
+
if (before.get(file) !== now) changed.push(file);
|
|
2299
|
+
}
|
|
2300
|
+
return changed.sort();
|
|
2301
|
+
}
|
|
2302
|
+
var NAMED_IN_REPORT = 5;
|
|
2303
|
+
function describeFixOutcome(outcome, toolLabel) {
|
|
2304
|
+
if (outcome.failure !== void 0) {
|
|
2305
|
+
return `the ${toolLabel} autofix could not run, so nothing was fixed and this module has not been checked: ${outcome.failure}`;
|
|
2306
|
+
}
|
|
2307
|
+
if (outcome.changed.length === 0) return `the ${toolLabel} autofix changed nothing`;
|
|
2308
|
+
const named = outcome.changed.slice(0, NAMED_IN_REPORT).join(", ");
|
|
2309
|
+
const rest = outcome.changed.length - NAMED_IN_REPORT;
|
|
2310
|
+
const listed = rest > 0 ? `${named} and ${rest} more` : named;
|
|
2311
|
+
return `the ${toolLabel} autofix rewrote ${outcome.changed.length} file${outcome.changed.length === 1 ? "" : "s"}: ${listed}. Review the diff before committing: rules this module used to have switched off are enforced from now on.`;
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
// src/roles/lint/module-baseline.ts
|
|
2315
|
+
import { existsSync as existsSync14, readFileSync as readFileSync13 } from "fs";
|
|
2316
|
+
import { join as join17 } from "path";
|
|
2317
|
+
var LINT_BASELINE_FILE = ".oxlintrc.baseline.json";
|
|
2318
|
+
var LINT_MEASURE_FILE = ".oxlintrc.measure.json";
|
|
2319
|
+
var LINT_BASELINE_SPECIFIER = `./${LINT_BASELINE_FILE}`;
|
|
2320
|
+
function holdsFrom(errorCounts) {
|
|
2321
|
+
return [...errorCounts].map(([rule, count]) => ({ rule, count })).sort((left, right) => right.count - left.count || left.rule.localeCompare(right.rule));
|
|
2322
|
+
}
|
|
2323
|
+
function renderBaseline(holds, version) {
|
|
2324
|
+
const entries = holds.map(({ rule }) => ` ${JSON.stringify(rule)}: "warn"`).join(",\n");
|
|
2325
|
+
return [
|
|
2326
|
+
"{",
|
|
2327
|
+
` // Written by @hublo/sentinel@${version}. Do not edit by hand: \`sentinel --init --lint\``,
|
|
2328
|
+
" // regenerates it from a fresh measurement, and a rule you have since fixed drops out",
|
|
2329
|
+
" // and returns to `error` on its own.",
|
|
2330
|
+
" //",
|
|
2331
|
+
" // These rules are held at `warn` because this module ALREADY violated them when it",
|
|
2332
|
+
" // adopted. They still run and still report, so the build cannot fail on them.",
|
|
2333
|
+
" //",
|
|
2334
|
+
" // The hold is per RULE, not per occurrence: oxlint cannot freeze a specific list of",
|
|
2335
|
+
" // violations, so a NEW violation of one of these rules is covered too, and will only",
|
|
2336
|
+
" // warn. That is the cost of adopting without a red build, and the reason to clear this",
|
|
2337
|
+
" // file rather than live with it.",
|
|
2338
|
+
" //",
|
|
2339
|
+
" // `sentinel --inspect --lint` lists what is held; `sentinel --run --lint --json` counts",
|
|
2340
|
+
" // what is left, per rule. Fix them and re-run `--init --lint` to get the severity back.",
|
|
2341
|
+
' "rules": {',
|
|
2342
|
+
entries,
|
|
2343
|
+
" }",
|
|
2344
|
+
"}",
|
|
2345
|
+
""
|
|
2346
|
+
].join("\n");
|
|
2347
|
+
}
|
|
2348
|
+
function extendsWithBaseline(current, hasHolds) {
|
|
2349
|
+
const withoutBaseline = current.filter((entry) => entry !== LINT_BASELINE_SPECIFIER);
|
|
2350
|
+
return hasHolds ? [...withoutBaseline, LINT_BASELINE_SPECIFIER] : withoutBaseline;
|
|
2351
|
+
}
|
|
2352
|
+
function describeHolds(holds) {
|
|
2353
|
+
if (holds.length === 0) return "";
|
|
2354
|
+
const total = holds.reduce((sum, hold) => sum + hold.count, 0);
|
|
2355
|
+
const listed = holds.slice(0, 5).map(({ rule, count }) => `${rule} (${count})`).join(", ");
|
|
2356
|
+
const rest = holds.length > 5 ? `, and ${holds.length - 5} more` : "";
|
|
2357
|
+
return `held ${holds.length} rule(s) at \`warn\` for this module, covering ${total} violation(s) that were already there: ${listed}${rest}. They are written to ${LINT_BASELINE_FILE}, they still report, and they return to \`error\` once fixed and re-initialized`;
|
|
2358
|
+
}
|
|
2359
|
+
function heldRules(cwd) {
|
|
2360
|
+
const path = join17(cwd, LINT_BASELINE_FILE);
|
|
2361
|
+
if (!existsSync14(path)) return [];
|
|
2362
|
+
try {
|
|
2363
|
+
const parsed = parseJsonc(
|
|
2364
|
+
readFileSync13(path, "utf8"),
|
|
2365
|
+
LINT_BASELINE_FILE
|
|
2366
|
+
);
|
|
2367
|
+
return Object.keys(parsed.rules ?? {});
|
|
2368
|
+
} catch {
|
|
2369
|
+
return [];
|
|
2370
|
+
}
|
|
1896
2371
|
}
|
|
1897
2372
|
|
|
1898
2373
|
// src/roles/lint/config-policy.ts
|
|
@@ -1907,8 +2382,10 @@ function isSentinelPreset(entry) {
|
|
|
1907
2382
|
}
|
|
1908
2383
|
function extendsWithPreset(current, variant) {
|
|
1909
2384
|
const own = presetPath(variant);
|
|
1910
|
-
const others = current.filter((entry) => entry !== own && !isSentinelPreset(entry));
|
|
1911
|
-
|
|
2385
|
+
const others = [...new Set(current.filter((entry) => entry !== own && !isSentinelPreset(entry)))];
|
|
2386
|
+
const baseline = others.filter((entry) => entry === LINT_BASELINE_SPECIFIER);
|
|
2387
|
+
const rest = others.filter((entry) => entry !== LINT_BASELINE_SPECIFIER);
|
|
2388
|
+
return [own, ...rest, ...baseline];
|
|
1912
2389
|
}
|
|
1913
2390
|
function presetVariant(preset, cwd) {
|
|
1914
2391
|
const normalised = cwd.replaceAll("\\", "/");
|
|
@@ -1932,46 +2409,6 @@ function lintTarget() {
|
|
|
1932
2409
|
return { cache: true, inputs: ["default", "^default", `{projectRoot}/${LINT_CONFIG_FILE}`] };
|
|
1933
2410
|
}
|
|
1934
2411
|
|
|
1935
|
-
// src/roles/lint/presets/base.json
|
|
1936
|
-
var base_default2 = {
|
|
1937
|
-
rules: {
|
|
1938
|
-
"no-var": "error",
|
|
1939
|
-
"prefer-const": [
|
|
1940
|
-
"error",
|
|
1941
|
-
{
|
|
1942
|
-
destructuring: "any",
|
|
1943
|
-
ignoreReadBeforeAssign: false
|
|
1944
|
-
}
|
|
1945
|
-
],
|
|
1946
|
-
"prefer-rest-params": "error",
|
|
1947
|
-
"prefer-spread": "error",
|
|
1948
|
-
"typescript/ban-ts-comment": "error",
|
|
1949
|
-
"typescript/no-array-constructor": "error",
|
|
1950
|
-
"typescript/no-duplicate-enum-values": "error",
|
|
1951
|
-
"typescript/no-empty-object-type": "error",
|
|
1952
|
-
"typescript/no-extra-non-null-assertion": "error",
|
|
1953
|
-
"typescript/no-misused-new": "error",
|
|
1954
|
-
"typescript/no-namespace": "error",
|
|
1955
|
-
"typescript/no-non-null-asserted-optional-chain": "error",
|
|
1956
|
-
"typescript/no-this-alias": "error",
|
|
1957
|
-
"typescript/no-unnecessary-type-constraint": "error",
|
|
1958
|
-
"typescript/no-unsafe-declaration-merging": "error",
|
|
1959
|
-
"typescript/no-unsafe-function-type": "error",
|
|
1960
|
-
"typescript/no-unused-expressions": [
|
|
1961
|
-
"error",
|
|
1962
|
-
{
|
|
1963
|
-
allowShortCircuit: false,
|
|
1964
|
-
allowTaggedTemplates: false,
|
|
1965
|
-
allowTernary: false
|
|
1966
|
-
}
|
|
1967
|
-
],
|
|
1968
|
-
"typescript/no-wrapper-object-types": "error",
|
|
1969
|
-
"typescript/prefer-as-const": "error",
|
|
1970
|
-
"typescript/prefer-namespace-keyword": "error",
|
|
1971
|
-
"typescript/triple-slash-reference": "error"
|
|
1972
|
-
}
|
|
1973
|
-
};
|
|
1974
|
-
|
|
1975
2412
|
// src/roles/lint/presets/nest.json
|
|
1976
2413
|
var nest_default = {
|
|
1977
2414
|
plugins: ["typescript", "jest", "import", "unicorn", "oxc", "node"],
|
|
@@ -2311,6 +2748,7 @@ var nest_default = {
|
|
|
2311
2748
|
"no-invalid-this": "no oxlint equivalent, and hosting does not help: the rule crashes on every file under oxlint's plugin API (sourceCode.getJSDocComment is not implemented). Its coverage moves to TypeScript's noImplicitThis, which the sentinel TypeScript role manages.",
|
|
2312
2749
|
"no-multi-spaces": "formatting, and the formatter owns it. It moves to oxfmt, which is deliberately the LAST step of the migration, so nothing enforces it in between.",
|
|
2313
2750
|
"no-octal": "no oxlint equivalent",
|
|
2751
|
+
"no-restricted-syntax": "no oxlint equivalent, and unlike the react tier this preset hosts no house-rule plugin, so architectural guards written as AST selectors are not enforced at all. They belong in sentinel's `hublo` plugin, next to the frontend ones: tell us which guards your service relies on. A per-module copy of a house rule drifts, which is the problem sentinel exists to remove.",
|
|
2314
2752
|
"no-trailing-spaces": "formatting, and the formatter owns it. It moves to oxfmt, which is deliberately the LAST step of the migration, so nothing enforces it in between.",
|
|
2315
2753
|
semi: "formatting, and the formatter owns it. It moves to oxfmt, which is deliberately the LAST step of the migration, so nothing enforces it in between.",
|
|
2316
2754
|
"template-curly-spacing": "formatting, and the formatter owns it. It moves to oxfmt, which is deliberately the LAST step of the migration, so nothing enforces it in between.",
|
|
@@ -3264,10 +3702,51 @@ var react_default = {
|
|
|
3264
3702
|
uncovered: {
|
|
3265
3703
|
"import/order": "import ordering is FORMATTING, and the format role owns it: oxfmt's `sortImports` reproduces exactly these groups (builtin, external, internal, parent/index, sibling, alphabetical, blank line between) and sorts a file completely in ONE pass, because it rewrites the whole import block. A lint fixer cannot: it emits text edits over overlapping ranges, so this rule needed several runs to finish (five imports took four passes, measured) and sentinel deliberately runs autofix once. Enforced by `pnpm run format`, not by the linter, which is also why `eslint-plugin-import` is no longer hosted at all: its only other rule, no-duplicates, is native in oxlint.",
|
|
3266
3704
|
"no-octal": "no oxlint equivalent",
|
|
3705
|
+
"no-restricted-syntax": "no oxlint equivalent as configuration, so the eight AST selectors this repo used are shipped as real rules in the local `hublo` plugin instead, which this tier hosts (`../plugins/hublo.js`). Any selector NOT in that plugin is not enforced here; propose it and it joins the plugin rather than a per-module copy.",
|
|
3267
3706
|
semi: "formatting, and the formatter owns it. It moves to oxfmt, which is deliberately the LAST step of the migration, so nothing enforces it in between."
|
|
3268
3707
|
}
|
|
3269
3708
|
};
|
|
3270
3709
|
|
|
3710
|
+
// src/roles/lint/presets/shared.json
|
|
3711
|
+
var shared_default = {
|
|
3712
|
+
rules: {
|
|
3713
|
+
"no-var": "error",
|
|
3714
|
+
"prefer-const": [
|
|
3715
|
+
"error",
|
|
3716
|
+
{
|
|
3717
|
+
destructuring: "any",
|
|
3718
|
+
ignoreReadBeforeAssign: false
|
|
3719
|
+
}
|
|
3720
|
+
],
|
|
3721
|
+
"prefer-rest-params": "error",
|
|
3722
|
+
"prefer-spread": "error",
|
|
3723
|
+
"typescript/ban-ts-comment": "error",
|
|
3724
|
+
"typescript/no-array-constructor": "error",
|
|
3725
|
+
"typescript/no-duplicate-enum-values": "error",
|
|
3726
|
+
"typescript/no-empty-object-type": "error",
|
|
3727
|
+
"typescript/no-extra-non-null-assertion": "error",
|
|
3728
|
+
"typescript/no-misused-new": "error",
|
|
3729
|
+
"typescript/no-namespace": "error",
|
|
3730
|
+
"typescript/no-non-null-asserted-optional-chain": "error",
|
|
3731
|
+
"typescript/no-this-alias": "error",
|
|
3732
|
+
"typescript/no-unnecessary-type-constraint": "error",
|
|
3733
|
+
"typescript/no-unsafe-declaration-merging": "error",
|
|
3734
|
+
"typescript/no-unsafe-function-type": "error",
|
|
3735
|
+
"typescript/no-unused-expressions": [
|
|
3736
|
+
"error",
|
|
3737
|
+
{
|
|
3738
|
+
allowShortCircuit: false,
|
|
3739
|
+
allowTaggedTemplates: false,
|
|
3740
|
+
allowTernary: false
|
|
3741
|
+
}
|
|
3742
|
+
],
|
|
3743
|
+
"typescript/no-wrapper-object-types": "error",
|
|
3744
|
+
"typescript/prefer-as-const": "error",
|
|
3745
|
+
"typescript/prefer-namespace-keyword": "error",
|
|
3746
|
+
"typescript/triple-slash-reference": "error"
|
|
3747
|
+
}
|
|
3748
|
+
};
|
|
3749
|
+
|
|
3271
3750
|
// src/roles/lint/presets/svelte.json
|
|
3272
3751
|
var svelte_default = {
|
|
3273
3752
|
plugins: ["typescript", "oxc"],
|
|
@@ -3606,7 +4085,7 @@ var tools_default = {
|
|
|
3606
4085
|
};
|
|
3607
4086
|
|
|
3608
4087
|
// src/roles/lint/preset-data.ts
|
|
3609
|
-
var
|
|
4088
|
+
var SHARED_RULES = shared_default.rules;
|
|
3610
4089
|
var PRESET_FILES = {
|
|
3611
4090
|
nest: nest_default,
|
|
3612
4091
|
node: node_default,
|
|
@@ -3628,15 +4107,18 @@ function hasLintPreset(preset) {
|
|
|
3628
4107
|
function presetFile(preset) {
|
|
3629
4108
|
return VARIANT_FILES[preset] ?? PRESET_FILES[preset] ?? node_default;
|
|
3630
4109
|
}
|
|
3631
|
-
function
|
|
3632
|
-
return
|
|
4110
|
+
function sharedRules() {
|
|
4111
|
+
return SHARED_RULES;
|
|
3633
4112
|
}
|
|
3634
4113
|
function presetRules(preset) {
|
|
3635
4114
|
return presetFile(preset).rules;
|
|
3636
4115
|
}
|
|
4116
|
+
function uncoveredFor(preset) {
|
|
4117
|
+
return Object.entries(presetFile(preset).uncovered ?? {}).map(([rule, reason]) => ({ rule, reason })).sort((left, right) => left.rule.localeCompare(right.rule));
|
|
4118
|
+
}
|
|
3637
4119
|
|
|
3638
4120
|
// src/roles/lint/rule-data.ts
|
|
3639
|
-
var
|
|
4121
|
+
var sharedRules2 = sharedRules();
|
|
3640
4122
|
function isDecision(value) {
|
|
3641
4123
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3642
4124
|
}
|
|
@@ -3663,7 +4145,7 @@ function validate(preset, rules) {
|
|
|
3663
4145
|
}
|
|
3664
4146
|
}
|
|
3665
4147
|
function layered(preset) {
|
|
3666
|
-
const base =
|
|
4148
|
+
const base = sharedRules();
|
|
3667
4149
|
const layer = presetRules(preset);
|
|
3668
4150
|
validate("base", base);
|
|
3669
4151
|
validate(preset, layer);
|
|
@@ -3702,60 +4184,14 @@ function downgradedRulesFor(preset) {
|
|
|
3702
4184
|
}
|
|
3703
4185
|
|
|
3704
4186
|
// src/roles/lint/extra-layers.ts
|
|
3705
|
-
import { existsSync as
|
|
3706
|
-
import { isAbsolute as isAbsolute2, join as
|
|
3707
|
-
|
|
3708
|
-
// src/roles/lint/module-baseline.ts
|
|
3709
|
-
var LINT_BASELINE_FILE = ".oxlintrc.baseline.json";
|
|
3710
|
-
var LINT_MEASURE_FILE = ".oxlintrc.measure.json";
|
|
3711
|
-
var LINT_BASELINE_SPECIFIER = `./${LINT_BASELINE_FILE}`;
|
|
3712
|
-
function holdsFrom(errorCounts) {
|
|
3713
|
-
return [...errorCounts].map(([rule, count]) => ({ rule, count })).sort((left, right) => right.count - left.count || left.rule.localeCompare(right.rule));
|
|
3714
|
-
}
|
|
3715
|
-
function renderBaseline(holds, version) {
|
|
3716
|
-
const entries = holds.map(({ rule }) => ` ${JSON.stringify(rule)}: "warn"`).join(",\n");
|
|
3717
|
-
return [
|
|
3718
|
-
"{",
|
|
3719
|
-
` // Written by @hublo/sentinel@${version}. Do not edit by hand: \`sentinel --init --lint\``,
|
|
3720
|
-
" // regenerates it from a fresh measurement, and a rule you have since fixed drops out",
|
|
3721
|
-
" // and returns to `error` on its own.",
|
|
3722
|
-
" //",
|
|
3723
|
-
" // These rules are held at `warn` because this module ALREADY violated them when it",
|
|
3724
|
-
" // adopted. They still run and still report, so the build cannot fail on them.",
|
|
3725
|
-
" //",
|
|
3726
|
-
" // The hold is per RULE, not per occurrence: oxlint cannot freeze a specific list of",
|
|
3727
|
-
" // violations, so a NEW violation of one of these rules is covered too, and will only",
|
|
3728
|
-
" // warn. That is the cost of adopting without a red build, and the reason to clear this",
|
|
3729
|
-
" // file rather than live with it.",
|
|
3730
|
-
" //",
|
|
3731
|
-
" // `sentinel --inspect --lint` counts what is left. Fix them and re-run `--init --lint`",
|
|
3732
|
-
" // to get the preset severity back.",
|
|
3733
|
-
' "rules": {',
|
|
3734
|
-
entries,
|
|
3735
|
-
" }",
|
|
3736
|
-
"}",
|
|
3737
|
-
""
|
|
3738
|
-
].join("\n");
|
|
3739
|
-
}
|
|
3740
|
-
function extendsWithBaseline(current, hasHolds) {
|
|
3741
|
-
const withoutBaseline = current.filter((entry) => entry !== LINT_BASELINE_SPECIFIER);
|
|
3742
|
-
return hasHolds ? [...withoutBaseline, LINT_BASELINE_SPECIFIER] : withoutBaseline;
|
|
3743
|
-
}
|
|
3744
|
-
function describeHolds(holds) {
|
|
3745
|
-
if (holds.length === 0) return "";
|
|
3746
|
-
const total = holds.reduce((sum, hold) => sum + hold.count, 0);
|
|
3747
|
-
const listed = holds.slice(0, 5).map(({ rule, count }) => `${rule} (${count})`).join(", ");
|
|
3748
|
-
const rest = holds.length > 5 ? `, and ${holds.length - 5} more` : "";
|
|
3749
|
-
return `held ${holds.length} rule(s) at \`warn\` for this module, covering ${total} violation(s) that were already there: ${listed}${rest}. They are written to ${LINT_BASELINE_FILE}, they still report, and they return to \`error\` once fixed and re-initialized`;
|
|
3750
|
-
}
|
|
3751
|
-
|
|
3752
|
-
// src/roles/lint/extra-layers.ts
|
|
4187
|
+
import { existsSync as existsSync15, readFileSync as readFileSync14 } from "fs";
|
|
4188
|
+
import { isAbsolute as isAbsolute2, join as join18, resolve as resolve3 } from "path";
|
|
3753
4189
|
function ruleCount(cwd, specifier) {
|
|
3754
4190
|
const path = isAbsolute2(specifier) ? specifier : resolve3(cwd, specifier);
|
|
3755
|
-
if (!
|
|
4191
|
+
if (!existsSync15(path)) return void 0;
|
|
3756
4192
|
try {
|
|
3757
4193
|
const parsed = parseJsonc(
|
|
3758
|
-
|
|
4194
|
+
readFileSync14(path, "utf8"),
|
|
3759
4195
|
specifier
|
|
3760
4196
|
);
|
|
3761
4197
|
return Object.keys(parsed.rules ?? {}).length;
|
|
@@ -3775,7 +4211,7 @@ function extraLayers(cwd, extendsList) {
|
|
|
3775
4211
|
function committedExtendsList(cwd, configFile) {
|
|
3776
4212
|
try {
|
|
3777
4213
|
const parsed = parseJsonc(
|
|
3778
|
-
|
|
4214
|
+
readFileSync14(join18(cwd, configFile), "utf8"),
|
|
3779
4215
|
configFile
|
|
3780
4216
|
);
|
|
3781
4217
|
return Array.isArray(parsed.extends) ? parsed.extends.filter((entry) => typeof entry === "string") : [];
|
|
@@ -3834,8 +4270,8 @@ function errorCountsByConfigRule(stdout) {
|
|
|
3834
4270
|
}
|
|
3835
4271
|
|
|
3836
4272
|
// src/core/config/read-adoption.ts
|
|
3837
|
-
import { existsSync as
|
|
3838
|
-
import { join as
|
|
4273
|
+
import { existsSync as existsSync17, readFileSync as readFileSync16 } from "fs";
|
|
4274
|
+
import { join as join20 } from "path";
|
|
3839
4275
|
|
|
3840
4276
|
// src/core/config/owned-keys.ts
|
|
3841
4277
|
function presetOwnedKeys(config, permitted, presetSets) {
|
|
@@ -3850,12 +4286,12 @@ function localOnlyKeys(config, permitted, presetSets) {
|
|
|
3850
4286
|
}
|
|
3851
4287
|
|
|
3852
4288
|
// src/core/config/resolve-config-target.ts
|
|
3853
|
-
import { existsSync as
|
|
3854
|
-
import { join as
|
|
4289
|
+
import { existsSync as existsSync16, readFileSync as readFileSync15 } from "fs";
|
|
4290
|
+
import { join as join19 } from "path";
|
|
3855
4291
|
function readExtends(absolutePath) {
|
|
3856
4292
|
let parsed;
|
|
3857
4293
|
try {
|
|
3858
|
-
parsed = parseJsonc(
|
|
4294
|
+
parsed = parseJsonc(readFileSync15(absolutePath, "utf8"), absolutePath);
|
|
3859
4295
|
} catch {
|
|
3860
4296
|
return [];
|
|
3861
4297
|
}
|
|
@@ -3869,8 +4305,8 @@ function resolveConfigTarget(moduleDir, { candidates, markers, fallback }) {
|
|
|
3869
4305
|
let existing;
|
|
3870
4306
|
let existingExtendsSomething = false;
|
|
3871
4307
|
for (const candidate of candidates) {
|
|
3872
|
-
const absolutePath =
|
|
3873
|
-
if (!
|
|
4308
|
+
const absolutePath = join19(moduleDir, candidate);
|
|
4309
|
+
if (!existsSync16(absolutePath)) continue;
|
|
3874
4310
|
const chain = readExtends(absolutePath);
|
|
3875
4311
|
if (existing === void 0) {
|
|
3876
4312
|
existing = candidate;
|
|
@@ -3903,12 +4339,12 @@ var NOT_ADOPTED3 = (configFile, unreadable = null) => ({
|
|
|
3903
4339
|
});
|
|
3904
4340
|
function readAdoption(cwd, options) {
|
|
3905
4341
|
const target = resolveConfigTarget(cwd, options);
|
|
3906
|
-
if (target.reason === "none" || !
|
|
4342
|
+
if (target.reason === "none" || !existsSync17(join20(cwd, target.path))) {
|
|
3907
4343
|
return NOT_ADOPTED3(target.reason === "none" ? null : target.path);
|
|
3908
4344
|
}
|
|
3909
4345
|
let parsed;
|
|
3910
4346
|
try {
|
|
3911
|
-
parsed = parseJsonc(
|
|
4347
|
+
parsed = parseJsonc(readFileSync16(join20(cwd, target.path), "utf8"), target.path);
|
|
3912
4348
|
} catch (error) {
|
|
3913
4349
|
const reason = error instanceof Error ? error.message : String(error);
|
|
3914
4350
|
return NOT_ADOPTED3(target.path, `${target.path} could not be parsed (${reason})`);
|
|
@@ -3942,9 +4378,9 @@ function readLintAdoption(cwd) {
|
|
|
3942
4378
|
}
|
|
3943
4379
|
|
|
3944
4380
|
// src/roles/lint/resolve-oxlint.ts
|
|
3945
|
-
import { existsSync as
|
|
4381
|
+
import { existsSync as existsSync18 } from "fs";
|
|
3946
4382
|
import { createRequire as createRequire3 } from "module";
|
|
3947
|
-
import { delimiter as delimiter2, dirname as dirname5, join as
|
|
4383
|
+
import { delimiter as delimiter2, dirname as dirname5, join as join21 } from "path";
|
|
3948
4384
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3949
4385
|
var PACKAGE_OF = {
|
|
3950
4386
|
oxlint: "oxlint",
|
|
@@ -3969,32 +4405,31 @@ function tsgolintShim(cwd) {
|
|
|
3969
4405
|
for (const owner of ["oxlint-tsgolint", "oxlint"]) {
|
|
3970
4406
|
try {
|
|
3971
4407
|
const packageDir = dirname5(require3.resolve(`${owner}/package.json`));
|
|
3972
|
-
candidates.push(
|
|
3973
|
-
candidates.push(
|
|
4408
|
+
candidates.push(join21(packageDir, "node_modules", ".bin", "tsgolint"));
|
|
4409
|
+
candidates.push(join21(packageDir, "..", ".bin", "tsgolint"));
|
|
3974
4410
|
} catch {
|
|
3975
4411
|
}
|
|
3976
4412
|
}
|
|
3977
4413
|
try {
|
|
3978
4414
|
const ownRoot = dirname5(require3.resolve("@hublo/sentinel/package.json"));
|
|
3979
|
-
candidates.push(
|
|
4415
|
+
candidates.push(join21(ownRoot, "node_modules", ".bin", "tsgolint"));
|
|
3980
4416
|
} catch {
|
|
3981
4417
|
candidates.push(resolveBin(dirname5(fileURLToPath2(import.meta.url)), "tsgolint") ?? "");
|
|
3982
4418
|
}
|
|
3983
|
-
return candidates.find((candidate) => candidate !== "" &&
|
|
4419
|
+
return candidates.find((candidate) => candidate !== "" && existsSync18(candidate));
|
|
3984
4420
|
}
|
|
3985
4421
|
function oxlintSearchPath(cwd) {
|
|
3986
4422
|
return binSearchPath(cwd);
|
|
3987
4423
|
}
|
|
3988
4424
|
|
|
3989
4425
|
// src/roles/lint/adapters/oxlint/plan.ts
|
|
3990
|
-
import { existsSync as
|
|
3991
|
-
import { join as
|
|
4426
|
+
import { existsSync as existsSync20, readFileSync as readFileSync19 } from "fs";
|
|
4427
|
+
import { join as join24 } from "path";
|
|
3992
4428
|
|
|
3993
4429
|
// src/roles/lint/eslint-ignores.ts
|
|
3994
|
-
import { existsSync as
|
|
3995
|
-
import { join as
|
|
3996
|
-
|
|
3997
|
-
var STRING_LITERAL = /['"`]([^'"`]+)['"`]/g;
|
|
4430
|
+
import { existsSync as existsSync19, readFileSync as readFileSync17 } from "fs";
|
|
4431
|
+
import { join as join22 } from "path";
|
|
4432
|
+
import { parseSync as parseSync2 } from "oxc-parser";
|
|
3998
4433
|
var OPAQUE_SOURCE = /\b(includeIgnoreFile)\s*\([^)]*\)/g;
|
|
3999
4434
|
function toOxlintPattern(pattern) {
|
|
4000
4435
|
return pattern.replace(/^\.\//, "");
|
|
@@ -4005,24 +4440,71 @@ function readRootEslintIgnores(root) {
|
|
|
4005
4440
|
return { patterns: patterns.filter((pattern) => pattern.startsWith("**/")), unresolved };
|
|
4006
4441
|
}
|
|
4007
4442
|
function readEslintIgnores(cwd) {
|
|
4008
|
-
const config = ESLINT_CONFIG_FILES.map((name) =>
|
|
4443
|
+
const config = ESLINT_CONFIG_FILES.map((name) => join22(cwd, name)).find((path) => existsSync19(path));
|
|
4009
4444
|
if (!config) return { patterns: [], unresolved: [] };
|
|
4010
4445
|
let source;
|
|
4011
4446
|
try {
|
|
4012
|
-
source =
|
|
4447
|
+
source = readFileSync17(config, "utf8");
|
|
4013
4448
|
} catch {
|
|
4014
4449
|
return { patterns: [], unresolved: [] };
|
|
4015
4450
|
}
|
|
4016
4451
|
const patterns = /* @__PURE__ */ new Set();
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4452
|
+
const unresolved = new Set([...source.matchAll(OPAQUE_SOURCE)].map((match) => match[0]));
|
|
4453
|
+
const { program, errors } = parseSync2(config, source, { sourceType: "module" });
|
|
4454
|
+
if (errors.length > 0) {
|
|
4455
|
+
return { patterns: [], unresolved: [`${config} could not be parsed`] };
|
|
4456
|
+
}
|
|
4457
|
+
walk(program, (node) => {
|
|
4458
|
+
if (node.type === "CallExpression" && calleeName(node) === "globalIgnores") {
|
|
4459
|
+
for (const literal of stringLiterals(firstArrayArgument(node))) {
|
|
4460
|
+
patterns.add(toOxlintPattern(literal));
|
|
4021
4461
|
}
|
|
4462
|
+
return;
|
|
4022
4463
|
}
|
|
4464
|
+
if (node.type !== "ObjectExpression") return;
|
|
4465
|
+
const keys = objectKeys(node);
|
|
4466
|
+
if (!keys.includes("ignores")) return;
|
|
4467
|
+
const values = arrayValues(node, "ignores");
|
|
4468
|
+
if (keys.length > 1) {
|
|
4469
|
+
unresolved.add(
|
|
4470
|
+
`ignores scoped to one config block (with ${keys.filter((k) => k !== "ignores").join(", ")}): ${values.join(", ")} \u2014 exempt from those RULES, not excluded from linting`
|
|
4471
|
+
);
|
|
4472
|
+
return;
|
|
4473
|
+
}
|
|
4474
|
+
for (const literal of values) patterns.add(toOxlintPattern(literal));
|
|
4475
|
+
});
|
|
4476
|
+
return { patterns: [...patterns], unresolved: [...unresolved] };
|
|
4477
|
+
}
|
|
4478
|
+
function walk(node, visit) {
|
|
4479
|
+
if (!node || typeof node !== "object") return;
|
|
4480
|
+
const current = node;
|
|
4481
|
+
if (typeof current.type === "string") visit(current);
|
|
4482
|
+
for (const key of Object.keys(current)) {
|
|
4483
|
+
const value = current[key];
|
|
4484
|
+
if (Array.isArray(value)) for (const entry of value) walk(entry, visit);
|
|
4485
|
+
else if (value && typeof value === "object") walk(value, visit);
|
|
4023
4486
|
}
|
|
4024
|
-
|
|
4025
|
-
|
|
4487
|
+
}
|
|
4488
|
+
function calleeName(node) {
|
|
4489
|
+
const callee = node.callee;
|
|
4490
|
+
return callee?.type === "Identifier" ? callee.name : void 0;
|
|
4491
|
+
}
|
|
4492
|
+
function firstArrayArgument(node) {
|
|
4493
|
+
const args = node.arguments ?? [];
|
|
4494
|
+
return args.find((argument) => argument?.type === "ArrayExpression");
|
|
4495
|
+
}
|
|
4496
|
+
function stringLiterals(array) {
|
|
4497
|
+
const elements = array?.elements ?? [];
|
|
4498
|
+
return elements.filter((element) => element?.type === "Literal" && typeof element.value === "string").map((element) => element.value);
|
|
4499
|
+
}
|
|
4500
|
+
function objectKeys(node) {
|
|
4501
|
+
const properties = node.properties ?? [];
|
|
4502
|
+
return properties.filter((property) => property?.type === "Property" && !property.computed).map((property) => property.key?.name ?? String(property.key?.value ?? "")).filter(Boolean);
|
|
4503
|
+
}
|
|
4504
|
+
function arrayValues(node, name) {
|
|
4505
|
+
const properties = node.properties ?? [];
|
|
4506
|
+
const found = properties.find((property) => property.key?.name === name);
|
|
4507
|
+
return stringLiterals(found?.value);
|
|
4026
4508
|
}
|
|
4027
4509
|
|
|
4028
4510
|
// src/roles/lint/lint-script.ts
|
|
@@ -4063,8 +4545,8 @@ function lintPresetFor(preset) {
|
|
|
4063
4545
|
}
|
|
4064
4546
|
|
|
4065
4547
|
// src/roles/lint/rename-suppressions.ts
|
|
4066
|
-
import { readdirSync as readdirSync2, readFileSync as
|
|
4067
|
-
import { join as
|
|
4548
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync18, statSync as statSync2 } from "fs";
|
|
4549
|
+
import { join as join23, relative as relative5 } from "path";
|
|
4068
4550
|
var SOURCE_EXTENSIONS = [
|
|
4069
4551
|
".ts",
|
|
4070
4552
|
".tsx",
|
|
@@ -4111,10 +4593,10 @@ function* sourceFiles(dir) {
|
|
|
4111
4593
|
return;
|
|
4112
4594
|
}
|
|
4113
4595
|
for (const entry of entries) {
|
|
4114
|
-
const full =
|
|
4596
|
+
const full = join23(dir, entry);
|
|
4115
4597
|
let isDirectory;
|
|
4116
4598
|
try {
|
|
4117
|
-
isDirectory =
|
|
4599
|
+
isDirectory = statSync2(full).isDirectory();
|
|
4118
4600
|
} catch {
|
|
4119
4601
|
continue;
|
|
4120
4602
|
}
|
|
@@ -4131,7 +4613,7 @@ function findSuppressionRenames(cwd, renames) {
|
|
|
4131
4613
|
for (const file of sourceFiles(cwd)) {
|
|
4132
4614
|
let content;
|
|
4133
4615
|
try {
|
|
4134
|
-
content =
|
|
4616
|
+
content = readFileSync18(file, "utf8");
|
|
4135
4617
|
} catch {
|
|
4136
4618
|
continue;
|
|
4137
4619
|
}
|
|
@@ -4155,7 +4637,7 @@ function findSuppressionRenames(cwd, renames) {
|
|
|
4155
4637
|
}
|
|
4156
4638
|
if (hits.length === 0) return;
|
|
4157
4639
|
found.push({
|
|
4158
|
-
file:
|
|
4640
|
+
file: relative5(cwd, file),
|
|
4159
4641
|
line: index + 1,
|
|
4160
4642
|
from: line,
|
|
4161
4643
|
to: line.replace(rules, renamed.join(", ")),
|
|
@@ -4259,7 +4741,7 @@ function plan2(context) {
|
|
|
4259
4741
|
keys: removableDeps
|
|
4260
4742
|
});
|
|
4261
4743
|
}
|
|
4262
|
-
const eslintConfigs = ESLINT_CONFIG_FILES.filter((name) =>
|
|
4744
|
+
const eslintConfigs = ESLINT_CONFIG_FILES.filter((name) => existsSync20(join24(context.cwd, name)));
|
|
4263
4745
|
for (const name of eslintConfigs) operations.push({ kind: "delete", path: name });
|
|
4264
4746
|
operations.push(
|
|
4265
4747
|
...nxTargetOperations({
|
|
@@ -4357,7 +4839,7 @@ function lintScripts(cwd) {
|
|
|
4357
4839
|
function committedExtends(cwd) {
|
|
4358
4840
|
try {
|
|
4359
4841
|
const parsed = parseJsonc(
|
|
4360
|
-
|
|
4842
|
+
readFileSync19(join24(cwd, LINT_CONFIG_FILE), "utf8"),
|
|
4361
4843
|
LINT_CONFIG_FILE
|
|
4362
4844
|
);
|
|
4363
4845
|
return Array.isArray(parsed.extends) ? parsed.extends.filter((entry) => typeof entry === "string") : [];
|
|
@@ -4368,7 +4850,7 @@ function committedExtends(cwd) {
|
|
|
4368
4850
|
function committedIgnorePatterns(cwd) {
|
|
4369
4851
|
try {
|
|
4370
4852
|
const parsed = parseJsonc(
|
|
4371
|
-
|
|
4853
|
+
readFileSync19(join24(cwd, LINT_CONFIG_FILE), "utf8"),
|
|
4372
4854
|
LINT_CONFIG_FILE
|
|
4373
4855
|
);
|
|
4374
4856
|
return Array.isArray(parsed.ignorePatterns) ? parsed.ignorePatterns.filter((entry) => typeof entry === "string") : [];
|
|
@@ -4380,7 +4862,7 @@ function moduleEslintDependencies(cwd) {
|
|
|
4380
4862
|
const isEslintPackage = (name) => name === "eslint" || name === "@types/eslint" || name === "typescript-eslint" || name.startsWith("@typescript-eslint/") || name.startsWith("eslint-plugin-") || name.startsWith("eslint-config-") || name.startsWith("@eslint/");
|
|
4381
4863
|
let manifest;
|
|
4382
4864
|
try {
|
|
4383
|
-
manifest = JSON.parse(
|
|
4865
|
+
manifest = JSON.parse(readFileSync19(join24(cwd, "package.json"), "utf8"));
|
|
4384
4866
|
} catch {
|
|
4385
4867
|
return [];
|
|
4386
4868
|
}
|
|
@@ -4445,17 +4927,31 @@ var OxlintAdapter = class extends BaseAdapter {
|
|
|
4445
4927
|
return presetOfVariant(presetNameFromPath(readLintAdoption(cwd).preset));
|
|
4446
4928
|
}
|
|
4447
4929
|
/**
|
|
4448
|
-
* The autofix pass
|
|
4449
|
-
*
|
|
4930
|
+
* The autofix pass, and what it did.
|
|
4931
|
+
*
|
|
4932
|
+
* Output is captured rather than streamed: the fix run is noise, and the verification run
|
|
4933
|
+
* that follows is the one a developer reads. But captured is not the same as discarded, and
|
|
4934
|
+
* it used to be discarded, which is why a pass that never ran looked exactly like one that
|
|
4935
|
+
* had nothing to do.
|
|
4936
|
+
*
|
|
4937
|
+
* The exit status cannot be the signal, and neither can the stream. Measured on oxlint: a
|
|
4938
|
+
* healthy run on a module with remaining violations and a run whose `extends` path resolves
|
|
4939
|
+
* nowhere BOTH exit 1 and both write to stdout. One module was checked and one was not, and
|
|
4940
|
+
* nothing in the process result separates them, so the only signal is what the tool said.
|
|
4450
4941
|
*/
|
|
4451
4942
|
fixPass(ctx, oxlint, env) {
|
|
4452
|
-
const
|
|
4943
|
+
const before = snapshotFiles(ctx.cwd, LINTABLE_EXTENSIONS);
|
|
4944
|
+
const result = spawnSync4(oxlint, ["-c", LINT_CONFIG_FILE, "--fix", "."], {
|
|
4453
4945
|
cwd: ctx.cwd,
|
|
4454
4946
|
encoding: "utf8",
|
|
4455
4947
|
env,
|
|
4456
4948
|
maxBuffer: CAPTURE_MAX_BUFFER
|
|
4457
4949
|
});
|
|
4458
|
-
return result.
|
|
4950
|
+
if (result.error) return { changed: [], failure: result.error.message };
|
|
4951
|
+
const failure = readFailure(`${result.stdout ?? ""}
|
|
4952
|
+
${result.stderr ?? ""}`);
|
|
4953
|
+
if (failure !== void 0) return { changed: [], failure };
|
|
4954
|
+
return { changed: changedSince(ctx.cwd, before, LINTABLE_EXTENSIONS), failure: void 0 };
|
|
4459
4955
|
}
|
|
4460
4956
|
/** The module's current `lint` script, so adoption replaces only the eslint part of it. */
|
|
4461
4957
|
/**
|
|
@@ -4478,7 +4974,8 @@ var OxlintAdapter = class extends BaseAdapter {
|
|
|
4478
4974
|
const oxlint = resolveOxlint(ctx.cwd);
|
|
4479
4975
|
if (!oxlint) return;
|
|
4480
4976
|
const env = { ...process.env, PATH: oxlintPath(ctx.cwd, process.env) };
|
|
4481
|
-
this.fixPass(ctx, oxlint, env)
|
|
4977
|
+
process.stderr.write(` ${describeFixOutcome(this.fixPass(ctx, oxlint, env), "lint")}
|
|
4978
|
+
`);
|
|
4482
4979
|
this.writeModuleBaseline(ctx, oxlint, env);
|
|
4483
4980
|
}
|
|
4484
4981
|
/**
|
|
@@ -4499,20 +4996,20 @@ var OxlintAdapter = class extends BaseAdapter {
|
|
|
4499
4996
|
* build the developer can see, rather than a silent half-adoption they cannot.
|
|
4500
4997
|
*/
|
|
4501
4998
|
writeModuleBaseline(ctx, oxlint, env) {
|
|
4502
|
-
const configPath =
|
|
4503
|
-
const baselinePath =
|
|
4504
|
-
if (!
|
|
4999
|
+
const configPath = join25(ctx.cwd, LINT_CONFIG_FILE);
|
|
5000
|
+
const baselinePath = join25(ctx.cwd, LINT_BASELINE_FILE);
|
|
5001
|
+
if (!existsSync21(configPath)) return;
|
|
4505
5002
|
let config;
|
|
4506
5003
|
try {
|
|
4507
5004
|
config = parseJsonc(
|
|
4508
|
-
|
|
5005
|
+
readFileSync20(configPath, "utf8"),
|
|
4509
5006
|
LINT_CONFIG_FILE
|
|
4510
5007
|
);
|
|
4511
5008
|
} catch {
|
|
4512
5009
|
return;
|
|
4513
5010
|
}
|
|
4514
5011
|
const current = Array.isArray(config.extends) ? config.extends : [];
|
|
4515
|
-
const measurePath =
|
|
5012
|
+
const measurePath = join25(ctx.cwd, LINT_MEASURE_FILE);
|
|
4516
5013
|
let measured;
|
|
4517
5014
|
try {
|
|
4518
5015
|
writeFileSync2(
|
|
@@ -4520,7 +5017,7 @@ var OxlintAdapter = class extends BaseAdapter {
|
|
|
4520
5017
|
`${JSON.stringify({ ...config, extends: extendsWithBaseline(current, false) }, null, 2)}
|
|
4521
5018
|
`
|
|
4522
5019
|
);
|
|
4523
|
-
measured =
|
|
5020
|
+
measured = spawnSync4(
|
|
4524
5021
|
oxlint,
|
|
4525
5022
|
[
|
|
4526
5023
|
"-c",
|
|
@@ -4560,7 +5057,7 @@ var OxlintAdapter = class extends BaseAdapter {
|
|
|
4560
5057
|
}
|
|
4561
5058
|
}
|
|
4562
5059
|
async run(ctx) {
|
|
4563
|
-
if (!
|
|
5060
|
+
if (!existsSync21(join25(ctx.cwd, LINT_CONFIG_FILE))) {
|
|
4564
5061
|
process.stderr.write(
|
|
4565
5062
|
`sentinel lint(oxlint): no ${LINT_CONFIG_FILE} in this module; run \`sentinel --init --lint\` to adopt.
|
|
4566
5063
|
`
|
|
@@ -4605,9 +5102,10 @@ var OxlintAdapter = class extends BaseAdapter {
|
|
|
4605
5102
|
valueFlags: OXLINT_VALUE_FLAGS
|
|
4606
5103
|
});
|
|
4607
5104
|
const targets = paths.length > 0 ? paths : ["."];
|
|
5105
|
+
const emptyScope = scopeFlags(paths, passedOptions);
|
|
4608
5106
|
const lint = (extra) => {
|
|
4609
|
-
const passed = [...extra, ...passedOptions];
|
|
4610
|
-
const result =
|
|
5107
|
+
const passed = [...extra, ...emptyScope, ...passedOptions];
|
|
5108
|
+
const result = spawnSync4(oxlint, ["-c", LINT_CONFIG_FILE, ...passed, ...targets], {
|
|
4611
5109
|
cwd: ctx.cwd,
|
|
4612
5110
|
stdio: "inherit",
|
|
4613
5111
|
env
|
|
@@ -4645,6 +5143,33 @@ var OxlintAdapter = class extends BaseAdapter {
|
|
|
4645
5143
|
rule: entry.rule,
|
|
4646
5144
|
reason: entry.reason
|
|
4647
5145
|
})),
|
|
5146
|
+
// A FOURTH state, and the only one that was declared and never reported: rules oxlint
|
|
5147
|
+
// cannot run at all, so they can be neither enforced nor disabled. Each preset has
|
|
5148
|
+
// carried this list since the Svelte work, and nothing read it outside a test.
|
|
5149
|
+
//
|
|
5150
|
+
// The gap that made it matter: `no-restricted-syntax` has no oxlint equivalent, so two
|
|
5151
|
+
// architectural guards a Nest service relied on vanished at adoption with nothing said.
|
|
5152
|
+
// The adopter found it himself and rebuilt them as a local plugin. The next one might
|
|
5153
|
+
// not, which is the whole argument for saying it out loud.
|
|
5154
|
+
uncovered: uncoveredFor(presetVariant(ctx.preset, ctx.cwd)).map((entry) => ({
|
|
5155
|
+
rule: entry.rule,
|
|
5156
|
+
reason: entry.reason
|
|
5157
|
+
})),
|
|
5158
|
+
// What this module HOLDS: the rules its own baseline lowers to `warn`.
|
|
5159
|
+
//
|
|
5160
|
+
// Distinct from `downgraded` just above, which is the PRESET's doing and identical for
|
|
5161
|
+
// every module on it. These are this module's own debt, and leaving them out was a real
|
|
5162
|
+
// gap: `--inspect` could tell you the preset holds 6 rules while saying nothing about
|
|
5163
|
+
// the 3 your module holds on top, so nobody could see their own debt without opening a
|
|
5164
|
+
// file that says "do not edit by hand".
|
|
5165
|
+
//
|
|
5166
|
+
// Listed, not counted. The list is free — it is the committed file — while a count means
|
|
5167
|
+
// running oxlint, which this verb deliberately does not do (that is what keeps a
|
|
5168
|
+
// 441-module sweep at ~8s per role). `--run --lint --json` carries the per-rule counts.
|
|
5169
|
+
held: heldRules(ctx.cwd).map((rule) => ({
|
|
5170
|
+
rule,
|
|
5171
|
+
reason: `held at \`warn\` by this module's ${LINT_BASELINE_FILE}, because it already violated it when it adopted. \`--run --lint --json\` counts what is left.`
|
|
5172
|
+
})),
|
|
4648
5173
|
// What this module enforces BEYOND the preset. Not drift, and not covered by anything
|
|
4649
5174
|
// else here: the stub still holds only `extends` and `ignorePatterns`, so a module with
|
|
4650
5175
|
// a team layer reports `conformant=true drift=[]` and used to say nothing at all about
|
|
@@ -4680,7 +5205,7 @@ var OxlintAdapter = class extends BaseAdapter {
|
|
|
4680
5205
|
let stub;
|
|
4681
5206
|
try {
|
|
4682
5207
|
stub = parseJsonc(
|
|
4683
|
-
|
|
5208
|
+
readFileSync20(join25(cwd, LINT_CONFIG_FILE), "utf8"),
|
|
4684
5209
|
LINT_CONFIG_FILE
|
|
4685
5210
|
);
|
|
4686
5211
|
} catch {
|
|
@@ -4690,7 +5215,7 @@ var OxlintAdapter = class extends BaseAdapter {
|
|
|
4690
5215
|
for (const entry of stub.extends ?? []) {
|
|
4691
5216
|
try {
|
|
4692
5217
|
const preset = parseJsonc(
|
|
4693
|
-
|
|
5218
|
+
readFileSync20(join25(cwd, entry), "utf8"),
|
|
4694
5219
|
entry
|
|
4695
5220
|
);
|
|
4696
5221
|
for (const rule of Object.keys(preset.rules ?? {})) names.add(rule);
|
|
@@ -4707,7 +5232,7 @@ var OxlintAdapter = class extends BaseAdapter {
|
|
|
4707
5232
|
const oxlint = resolveOxlint(ctx.cwd);
|
|
4708
5233
|
if (!oxlint) return { ok: false, code: 1, metrics: { error: "oxlint not found" } };
|
|
4709
5234
|
const typeAware = canRunTypeAware(ctx.cwd) ? ["--type-aware"] : [];
|
|
4710
|
-
const result =
|
|
5235
|
+
const result = spawnSync4(oxlint, ["-c", LINT_CONFIG_FILE, ...typeAware, "-f", "json", "."], {
|
|
4711
5236
|
cwd: ctx.cwd,
|
|
4712
5237
|
encoding: "utf8",
|
|
4713
5238
|
env: { ...process.env, PATH: oxlintPath(ctx.cwd, process.env) },
|
|
@@ -4760,14 +5285,14 @@ var OxlintAdapter = class extends BaseAdapter {
|
|
|
4760
5285
|
let parsed;
|
|
4761
5286
|
try {
|
|
4762
5287
|
parsed = parseJsonc(
|
|
4763
|
-
|
|
5288
|
+
readFileSync20(join25(cwd, LINT_CONFIG_FILE), "utf8"),
|
|
4764
5289
|
LINT_CONFIG_FILE
|
|
4765
5290
|
);
|
|
4766
5291
|
} catch {
|
|
4767
5292
|
return void 0;
|
|
4768
5293
|
}
|
|
4769
5294
|
const targets = Array.isArray(parsed.extends) ? parsed.extends.filter((entry) => typeof entry === "string") : typeof parsed.extends === "string" ? [parsed.extends] : [];
|
|
4770
|
-
return targets.find((target) => !
|
|
5295
|
+
return targets.find((target) => !existsSync21(join25(cwd, target)));
|
|
4771
5296
|
}
|
|
4772
5297
|
/** Announce what is not enforced, so reduced coverage is never silent. */
|
|
4773
5298
|
announceDisabled(preset) {
|
|
@@ -4798,36 +5323,10 @@ function registerLint() {
|
|
|
4798
5323
|
}
|
|
4799
5324
|
|
|
4800
5325
|
// src/roles/typescript/adapters/tsc/tsc.adapter.ts
|
|
4801
|
-
import { spawnSync as
|
|
4802
|
-
import { existsSync as
|
|
5326
|
+
import { spawnSync as spawnSync5 } from "child_process";
|
|
5327
|
+
import { existsSync as existsSync22, readFileSync as readFileSync22 } from "fs";
|
|
4803
5328
|
import { createRequire as createRequire4 } from "module";
|
|
4804
|
-
import { join as
|
|
4805
|
-
|
|
4806
|
-
// src/roles/typescript/presets/base.json
|
|
4807
|
-
var base_default3 = {
|
|
4808
|
-
$schema: "https://json.schemastore.org/tsconfig",
|
|
4809
|
-
compilerOptions: {
|
|
4810
|
-
strict: true,
|
|
4811
|
-
noFallthroughCasesInSwitch: true,
|
|
4812
|
-
forceConsistentCasingInFileNames: true,
|
|
4813
|
-
esModuleInterop: true,
|
|
4814
|
-
skipLibCheck: true
|
|
4815
|
-
},
|
|
4816
|
-
deferred: {
|
|
4817
|
-
noImplicitAny: {
|
|
4818
|
-
phase: 2,
|
|
4819
|
-
reason: "the implicit-any migration (TS70xx)"
|
|
4820
|
-
},
|
|
4821
|
-
noUnusedLocals: {
|
|
4822
|
-
phase: 2,
|
|
4823
|
-
reason: "unused-local cleanup (TS6133)"
|
|
4824
|
-
},
|
|
4825
|
-
noUnusedParameters: {
|
|
4826
|
-
phase: 2,
|
|
4827
|
-
reason: "unused-parameter cleanup (TS6133)"
|
|
4828
|
-
}
|
|
4829
|
-
}
|
|
4830
|
-
};
|
|
5329
|
+
import { join as join27 } from "path";
|
|
4831
5330
|
|
|
4832
5331
|
// src/roles/typescript/presets/nest.json
|
|
4833
5332
|
var nest_default2 = {
|
|
@@ -4878,15 +5377,41 @@ var react_default2 = {
|
|
|
4878
5377
|
}
|
|
4879
5378
|
};
|
|
4880
5379
|
|
|
5380
|
+
// src/roles/typescript/presets/shared.json
|
|
5381
|
+
var shared_default2 = {
|
|
5382
|
+
$schema: "https://json.schemastore.org/tsconfig",
|
|
5383
|
+
compilerOptions: {
|
|
5384
|
+
strict: true,
|
|
5385
|
+
noFallthroughCasesInSwitch: true,
|
|
5386
|
+
forceConsistentCasingInFileNames: true,
|
|
5387
|
+
esModuleInterop: true,
|
|
5388
|
+
skipLibCheck: true
|
|
5389
|
+
},
|
|
5390
|
+
deferred: {
|
|
5391
|
+
noImplicitAny: {
|
|
5392
|
+
phase: 2,
|
|
5393
|
+
reason: "the implicit-any migration (TS70xx)"
|
|
5394
|
+
},
|
|
5395
|
+
noUnusedLocals: {
|
|
5396
|
+
phase: 2,
|
|
5397
|
+
reason: "unused-local cleanup (TS6133)"
|
|
5398
|
+
},
|
|
5399
|
+
noUnusedParameters: {
|
|
5400
|
+
phase: 2,
|
|
5401
|
+
reason: "unused-parameter cleanup (TS6133)"
|
|
5402
|
+
}
|
|
5403
|
+
}
|
|
5404
|
+
};
|
|
5405
|
+
|
|
4881
5406
|
// src/roles/typescript/preset-data.ts
|
|
4882
|
-
var
|
|
5407
|
+
var SHARED = shared_default2;
|
|
4883
5408
|
var LAYERS = {
|
|
4884
5409
|
nest: nest_default2,
|
|
4885
5410
|
node: node_default2,
|
|
4886
5411
|
react: react_default2
|
|
4887
5412
|
};
|
|
4888
5413
|
var SHIPPED_PRESETS = Object.keys(LAYERS).sort();
|
|
4889
|
-
var DEFERRED_RULES = Object.entries(
|
|
5414
|
+
var DEFERRED_RULES = Object.entries(SHARED.deferred ?? {}).map(([rule, entry]) => ({ rule, phase: entry.phase, reason: entry.reason })).sort((left, right) => left.rule.localeCompare(right.rule));
|
|
4890
5415
|
function validate2() {
|
|
4891
5416
|
for (const { rule, phase, reason } of DEFERRED_RULES) {
|
|
4892
5417
|
if (!Number.isInteger(phase) || phase < 2) {
|
|
@@ -4899,9 +5424,9 @@ function validate2() {
|
|
|
4899
5424
|
`deferred rule "${rule}" has no reason; --inspect reports it and would say nothing`
|
|
4900
5425
|
);
|
|
4901
5426
|
}
|
|
4902
|
-
if (rule in
|
|
5427
|
+
if (rule in SHARED.compilerOptions) {
|
|
4903
5428
|
throw new Error(
|
|
4904
|
-
`"${rule}" is deferred and also set by the
|
|
5429
|
+
`"${rule}" is deferred and also set by the shared layer; it would be announced as not enforced while being enforced`
|
|
4905
5430
|
);
|
|
4906
5431
|
}
|
|
4907
5432
|
for (const [preset, layer] of Object.entries(LAYERS)) {
|
|
@@ -4921,8 +5446,8 @@ function tsPresetFor(preset) {
|
|
|
4921
5446
|
const layer = LAYERS[preset];
|
|
4922
5447
|
if (!layer) throw new Error(`no TypeScript preset ships for preset "${preset}"`);
|
|
4923
5448
|
return {
|
|
4924
|
-
...
|
|
4925
|
-
compilerOptions: { ...
|
|
5449
|
+
...SHARED.$schema === void 0 ? {} : { $schema: SHARED.$schema },
|
|
5450
|
+
compilerOptions: { ...SHARED.compilerOptions, ...layer.compilerOptions }
|
|
4926
5451
|
};
|
|
4927
5452
|
}
|
|
4928
5453
|
|
|
@@ -4982,8 +5507,8 @@ function readTsconfigAdoption(cwd) {
|
|
|
4982
5507
|
}
|
|
4983
5508
|
|
|
4984
5509
|
// src/roles/typescript/adapters/tsc/plan.ts
|
|
4985
|
-
import { readFileSync as
|
|
4986
|
-
import { join as
|
|
5510
|
+
import { readFileSync as readFileSync21 } from "fs";
|
|
5511
|
+
import { join as join26 } from "path";
|
|
4987
5512
|
|
|
4988
5513
|
// src/roles/typescript/typecheck-script.ts
|
|
4989
5514
|
var SENTINEL_TYPECHECK_COMMAND = "sentinel --run --typescript";
|
|
@@ -5076,7 +5601,7 @@ function planAdoption(context) {
|
|
|
5076
5601
|
};
|
|
5077
5602
|
}
|
|
5078
5603
|
const existing = parseJsonc(
|
|
5079
|
-
|
|
5604
|
+
readFileSync21(join26(context.cwd, target.path), "utf8"),
|
|
5080
5605
|
target.path
|
|
5081
5606
|
);
|
|
5082
5607
|
const extendsChain = composeExtends(existing.extends, preset);
|
|
@@ -5214,7 +5739,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
5214
5739
|
let chain;
|
|
5215
5740
|
try {
|
|
5216
5741
|
const parsed = parseJsonc(
|
|
5217
|
-
|
|
5742
|
+
readFileSync22(join27(cwd, target.path), "utf8"),
|
|
5218
5743
|
target.path
|
|
5219
5744
|
);
|
|
5220
5745
|
chain = parsed.extends;
|
|
@@ -5227,7 +5752,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
5227
5752
|
);
|
|
5228
5753
|
if (preset === void 0) return void 0;
|
|
5229
5754
|
try {
|
|
5230
|
-
createRequire4(
|
|
5755
|
+
createRequire4(join27(cwd, "noop.js")).resolve(preset);
|
|
5231
5756
|
return void 0;
|
|
5232
5757
|
} catch {
|
|
5233
5758
|
return preset;
|
|
@@ -5268,7 +5793,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
5268
5793
|
return { ok: false, code: 1 };
|
|
5269
5794
|
}
|
|
5270
5795
|
if (options.length > 0) return this.runWithOptions(ctx, tsc, config);
|
|
5271
|
-
const result =
|
|
5796
|
+
const result = spawnSync5(tsc, ["-b", config], { cwd: ctx.cwd, stdio: "inherit" });
|
|
5272
5797
|
if (result.error) {
|
|
5273
5798
|
process.stderr.write(
|
|
5274
5799
|
`sentinel typescript(tsc): could not run tsc (${result.error.message}); is TypeScript installed in the module?
|
|
@@ -5303,7 +5828,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
5303
5828
|
let worst = 0;
|
|
5304
5829
|
for (const project of this.referencedProjects(ctx.cwd, config)) {
|
|
5305
5830
|
const args = ["-p", project, "--noEmit", "--composite", "false", ...ctx.toolArgs ?? []];
|
|
5306
|
-
const result =
|
|
5831
|
+
const result = spawnSync5(tsc, args, { cwd: ctx.cwd, stdio: "inherit" });
|
|
5307
5832
|
if (result.error) {
|
|
5308
5833
|
process.stderr.write(
|
|
5309
5834
|
`sentinel typescript(tsc): could not run tsc (${result.error.message}); is TypeScript installed in the module?
|
|
@@ -5323,7 +5848,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
5323
5848
|
referencedProjects(cwd, config) {
|
|
5324
5849
|
try {
|
|
5325
5850
|
const parsed = parseJsonc(
|
|
5326
|
-
|
|
5851
|
+
readFileSync22(join27(cwd, config), "utf8"),
|
|
5327
5852
|
config
|
|
5328
5853
|
);
|
|
5329
5854
|
const referenced = (parsed.references ?? []).map((reference) => reference.path).filter((path) => typeof path === "string" && path.length > 0);
|
|
@@ -5339,7 +5864,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
5339
5864
|
* check.
|
|
5340
5865
|
*/
|
|
5341
5866
|
typecheckTarget(cwd) {
|
|
5342
|
-
if (
|
|
5867
|
+
if (existsSync22(join27(cwd, "tsconfig.json"))) return "tsconfig.json";
|
|
5343
5868
|
const target = resolveTsconfigTarget(cwd);
|
|
5344
5869
|
return target.reason === "none" ? null : target.path;
|
|
5345
5870
|
}
|
|
@@ -5392,7 +5917,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
5392
5917
|
return { ok: true, code: 0, metrics: _TscAdapter.NOTHING_TO_REPORT };
|
|
5393
5918
|
}
|
|
5394
5919
|
const tsc = resolveBin(ctx.cwd, "tsc") ?? "tsc";
|
|
5395
|
-
const result =
|
|
5920
|
+
const result = spawnSync5(tsc, ["-b", config], { cwd: ctx.cwd, encoding: "utf8" });
|
|
5396
5921
|
if (result.error) {
|
|
5397
5922
|
process.stderr.write(
|
|
5398
5923
|
`sentinel typescript(tsc): could not run tsc (${result.error.message}); is TypeScript installed in the module?
|
|
@@ -5435,7 +5960,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
5435
5960
|
* errors in the output mean the rule must be on.
|
|
5436
5961
|
*/
|
|
5437
5962
|
noImplicitAnyEnabled(cwd, tsc, config, mainOutput) {
|
|
5438
|
-
const shown =
|
|
5963
|
+
const shown = spawnSync5(tsc, ["-p", config, "--showConfig"], { cwd, encoding: "utf8" });
|
|
5439
5964
|
if (shown.status === 0 && shown.stdout) {
|
|
5440
5965
|
try {
|
|
5441
5966
|
const co = parseJsonc(
|
|
@@ -5487,7 +6012,7 @@ function detectFramework(packageJson) {
|
|
|
5487
6012
|
}
|
|
5488
6013
|
|
|
5489
6014
|
// src/core/dispatch.ts
|
|
5490
|
-
import { basename as
|
|
6015
|
+
import { basename as basename3 } from "path";
|
|
5491
6016
|
|
|
5492
6017
|
// src/shared/text.ts
|
|
5493
6018
|
function ensureLines(current, lines) {
|
|
@@ -5543,9 +6068,16 @@ function replaceLines(current, replacements) {
|
|
|
5543
6068
|
}
|
|
5544
6069
|
|
|
5545
6070
|
// src/core/apply-plan.ts
|
|
5546
|
-
import { existsSync as
|
|
6071
|
+
import { existsSync as existsSync23, readFileSync as readFileSync23, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
5547
6072
|
import { resolve as resolve4, sep } from "path";
|
|
5548
6073
|
import { applyEdits, findNodeAtLocation, modify, parseTree } from "jsonc-parser";
|
|
6074
|
+
|
|
6075
|
+
// src/shared/deep-merge.ts
|
|
6076
|
+
function isPlainObject(value) {
|
|
6077
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6078
|
+
}
|
|
6079
|
+
|
|
6080
|
+
// src/core/apply-plan.ts
|
|
5549
6081
|
function resolveWithinRoot(cwd, relativePath) {
|
|
5550
6082
|
const root = resolve4(cwd);
|
|
5551
6083
|
const absolutePath = resolve4(root, relativePath);
|
|
@@ -5555,7 +6087,7 @@ function resolveWithinRoot(cwd, relativePath) {
|
|
|
5555
6087
|
return absolutePath;
|
|
5556
6088
|
}
|
|
5557
6089
|
function readIfExists(absolutePath) {
|
|
5558
|
-
return
|
|
6090
|
+
return existsSync23(absolutePath) ? readFileSync23(absolutePath, "utf8") : void 0;
|
|
5559
6091
|
}
|
|
5560
6092
|
function* leaves(value, prefix = []) {
|
|
5561
6093
|
for (const [key, keyValue] of Object.entries(value)) {
|
|
@@ -5673,8 +6205,8 @@ function applyPlan(cwd, plan3) {
|
|
|
5673
6205
|
}
|
|
5674
6206
|
|
|
5675
6207
|
// src/core/config/preset-evidence.ts
|
|
5676
|
-
import { existsSync as
|
|
5677
|
-
import { join as
|
|
6208
|
+
import { existsSync as existsSync24, readdirSync as readdirSync3, readFileSync as readFileSync24 } from "fs";
|
|
6209
|
+
import { join as join28 } from "path";
|
|
5678
6210
|
var PATH_SIGNALS = [
|
|
5679
6211
|
{
|
|
5680
6212
|
preset: "nest",
|
|
@@ -5688,11 +6220,11 @@ var DEPENDENCY_SIGNALS = [
|
|
|
5688
6220
|
{ preset: "nest", pattern: /^@nestjs\// }
|
|
5689
6221
|
];
|
|
5690
6222
|
function dependencyNames(cwd) {
|
|
5691
|
-
const path =
|
|
5692
|
-
if (!
|
|
6223
|
+
const path = join28(cwd, "package.json");
|
|
6224
|
+
if (!existsSync24(path)) return [];
|
|
5693
6225
|
try {
|
|
5694
6226
|
const manifest = parseJsonc(
|
|
5695
|
-
|
|
6227
|
+
readFileSync24(path, "utf8"),
|
|
5696
6228
|
path
|
|
5697
6229
|
);
|
|
5698
6230
|
return [
|
|
@@ -5715,7 +6247,7 @@ function declaresJsx(cwd) {
|
|
|
5715
6247
|
for (const name of entries) {
|
|
5716
6248
|
try {
|
|
5717
6249
|
const config = parseJsonc(
|
|
5718
|
-
|
|
6250
|
+
readFileSync24(join28(cwd, name), "utf8"),
|
|
5719
6251
|
name
|
|
5720
6252
|
);
|
|
5721
6253
|
if (config.compilerOptions?.jsx !== void 0) return true;
|
|
@@ -5812,13 +6344,26 @@ async function dispatch(opts) {
|
|
|
5812
6344
|
const resolutionPreset = opts.preset ?? declaredPresetFor(opts.target, opts.cwd) ?? detected;
|
|
5813
6345
|
const adapter = resolve(opts.target, resolutionPreset, opts.runner);
|
|
5814
6346
|
const preset = opts.preset ?? adapter.declaredPreset?.(opts.cwd) ?? detected;
|
|
5815
|
-
|
|
5816
|
-
|
|
5817
|
-
|
|
6347
|
+
if (opts.preset !== void 0) {
|
|
6348
|
+
const contradiction = presetContradiction(opts.preset, opts.cwd);
|
|
6349
|
+
if (contradiction !== void 0) {
|
|
6350
|
+
process.stderr.write(`sentinel (${opts.target}): ${contradiction}
|
|
5818
6351
|
`);
|
|
5819
|
-
|
|
6352
|
+
return 1;
|
|
6353
|
+
}
|
|
6354
|
+
}
|
|
6355
|
+
let effective = preset;
|
|
6356
|
+
if (opts.preset === void 0 && adapter.declaredPreset?.(opts.cwd) === void 0) {
|
|
6357
|
+
const [signal] = presetSignals(opts.cwd);
|
|
6358
|
+
if (signal !== void 0 && signal.preset !== preset) {
|
|
6359
|
+
effective = signal.preset;
|
|
6360
|
+
process.stderr.write(
|
|
6361
|
+
` detected "${preset}" from dependencies, but ${signal.evidence}, so adopting as "${signal.preset}". Pass --preset to override.
|
|
6362
|
+
`
|
|
6363
|
+
);
|
|
6364
|
+
}
|
|
5820
6365
|
}
|
|
5821
|
-
const context = { cwd: opts.cwd, preset };
|
|
6366
|
+
const context = { cwd: opts.cwd, preset: effective };
|
|
5822
6367
|
const plan3 = await adapter.plan(context);
|
|
5823
6368
|
if (plan3.blocked) {
|
|
5824
6369
|
process.stderr.write(`sentinel (${opts.target}): ${plan3.blocked}
|
|
@@ -5843,7 +6388,7 @@ async function dispatch(opts) {
|
|
|
5843
6388
|
process.stderr.write(` fixing what ${opts.target} can fix automatically...
|
|
5844
6389
|
`);
|
|
5845
6390
|
await adapter.afterInit({
|
|
5846
|
-
module:
|
|
6391
|
+
module: basename3(opts.cwd),
|
|
5847
6392
|
cwd: opts.cwd,
|
|
5848
6393
|
preset,
|
|
5849
6394
|
ci: false,
|
|
@@ -5865,19 +6410,21 @@ export {
|
|
|
5865
6410
|
resolveBin,
|
|
5866
6411
|
readOwnVersion,
|
|
5867
6412
|
readProjectPackageJson,
|
|
5868
|
-
|
|
6413
|
+
moduleName,
|
|
6414
|
+
buildConfigFile,
|
|
6415
|
+
VERBS,
|
|
6416
|
+
TARGETS,
|
|
6417
|
+
LONG_RUNNING_TARGETS,
|
|
6418
|
+
SWEEPABLE_TARGETS,
|
|
6419
|
+
PRESET_NAMES,
|
|
5869
6420
|
WORKSPACE_ROOT_MARKER,
|
|
5870
6421
|
findWorkspaceRoot,
|
|
5871
6422
|
ensureWorkspacePrep,
|
|
5872
6423
|
inspectWorkspacePrep,
|
|
5873
6424
|
palette,
|
|
5874
|
-
VERBS,
|
|
5875
|
-
TARGETS,
|
|
5876
|
-
SWEEPABLE_TARGETS,
|
|
5877
|
-
PRESET_NAMES,
|
|
5878
6425
|
registerAdapters,
|
|
5879
6426
|
describeFramework,
|
|
5880
6427
|
detectFramework,
|
|
5881
6428
|
dispatch
|
|
5882
6429
|
};
|
|
5883
|
-
//# sourceMappingURL=chunk-
|
|
6430
|
+
//# sourceMappingURL=chunk-7WB7MQHD.js.map
|