@hublo/sentinel 1.2.0-alpha.1 → 1.2.0-alpha.2
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 +27 -4
- package/dist/{chunk-FLJ2QNFF.js → chunk-SA2RMTVI.js} +308 -193
- package/dist/index.js +1 -2
- package/dist/roles/build/toolchain.d.ts +5 -0
- package/dist/roles/build/toolchain.js +16 -0
- package/package.json +4 -3
- package/dist/chunk-XVDOQ3G3.js +0 -39
- package/dist/roles/build/react-app.d.ts +0 -112
- package/dist/roles/build/react-app.js +0 -126
package/README.md
CHANGED
|
@@ -151,7 +151,7 @@ pnpm dlx @hublo/sentinel@<exact-version> --inspect --typescript --module <name>
|
|
|
151
151
|
- [`docs/using-sentinel.md`](docs/using-sentinel.md) — every verb for every role, scoping a run to some files, what a fully adopted module looks like, and how versions move between modules.
|
|
152
152
|
- [`docs/lint-adoption.md`](docs/lint-adoption.md) — migrating a module from ESLint to Oxlint: the two steps, what `--init` writes and removes, the two react tiers, and the failures worth recognising.
|
|
153
153
|
- [`docs/format-adoption.md`](docs/format-adoption.md) — migrating a module from Prettier to oxfmt: why this config is materialized rather than a stub, how a module keeps its own formatting, and what did not survive the move.
|
|
154
|
-
- [`docs/build-adoption.md`](docs/build-adoption.md) — moving a React app's Vite toolchain into sentinel:
|
|
154
|
+
- [`docs/build-adoption.md`](docs/build-adoption.md) — moving a React app's Vite toolchain into sentinel: adoption changes the config's IMPORTS and nothing else, what sentinel owns versus what stays the app's, and why reading nothing is what makes losing nothing a guarantee.
|
|
155
155
|
- [`docs/typescript-adoption.md`](docs/typescript-adoption.md) — the adoption cheat sheet: the two adoption steps, the command model (verb x type x location), options, reading a report, and troubleshooting.
|
|
156
156
|
- [`docs/typescript-traces.md`](docs/typescript-traces.md) — a **generated, versioned** reference of live command + output traces (every verb, option, config result and edge case) against the mock monorepo. Regenerate after CLI changes with `pnpm docs:traces`.
|
|
157
157
|
|
|
@@ -161,7 +161,7 @@ Every check is described by three layers:
|
|
|
161
161
|
|
|
162
162
|
| Layer | Flag | What it is | Examples |
|
|
163
163
|
| -------------------- | --------------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
164
|
-
| **target** (role) | `--lint`, `--typescript`, … | the _kind_ of check, stable | `lint` `format` `typescript` `build` `test` `static-analysis` `runtime-analysis`
|
|
164
|
+
| **target** (role) | `--lint`, `--typescript`, … | the _kind_ of check, stable | `lint` `format` `typescript` `build` `dev` `test` `static-analysis` `runtime-analysis` |
|
|
165
165
|
| **runner** (adapter) | `--runner=<tool>` | the _tool_ behind the target, swappable | lint: `eslint`/`biome`/`oxlint` · types: `tsc`/`tsgo` · build: `vite` · test: `vitest` |
|
|
166
166
|
| **preset** (preset) | detected / `--preset` | the _variant_ per stack (strict by default) | `react` `nest` `node` `svelte` (svelte: lint + format; no tsconfig preset yet, so the TypeScript role stands aside on those modules and the other two adopt) |
|
|
167
167
|
|
package/dist/bin/sentinel.d.ts
CHANGED
package/dist/bin/sentinel.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
LONG_RUNNING_TARGETS,
|
|
3
4
|
PRESET_NAMES,
|
|
4
5
|
PresetUnsupportedError,
|
|
5
6
|
SWEEPABLE_TARGETS,
|
|
@@ -20,8 +21,7 @@ import {
|
|
|
20
21
|
registerAdapters,
|
|
21
22
|
resolve,
|
|
22
23
|
resolveBin
|
|
23
|
-
} from "../chunk-
|
|
24
|
-
import "../chunk-XVDOQ3G3.js";
|
|
24
|
+
} from "../chunk-SA2RMTVI.js";
|
|
25
25
|
|
|
26
26
|
// bin/sentinel.ts
|
|
27
27
|
import { program } from "commander";
|
|
@@ -463,6 +463,15 @@ async function runVerb(ctx) {
|
|
|
463
463
|
const { modules, scope } = resolveContext(ctx.cwd, { module: ctx.module, ci: ctx.ci });
|
|
464
464
|
const out = palette(process.stdout);
|
|
465
465
|
const err = palette(process.stderr);
|
|
466
|
+
const longRunning = ctx.targets.filter((target) => LONG_RUNNING_TARGETS.includes(target));
|
|
467
|
+
if (longRunning.length > 0 && modules.length > 1) {
|
|
468
|
+
process.stderr.write(
|
|
469
|
+
err.fail(
|
|
470
|
+
`sentinel: ${longRunning.map((t) => `--${t}`).join(", ")} starts a server that does not return, so it runs against ONE module. This resolved to ${modules.length} [${scope}]. Run it from inside the module's directory, or pass --module <name>.`
|
|
471
|
+
) + "\n"
|
|
472
|
+
);
|
|
473
|
+
return 1;
|
|
474
|
+
}
|
|
466
475
|
const shaped = ctx.verb === "run" && ctx.json ? "report" : ctx.verb;
|
|
467
476
|
const replacement = {
|
|
468
477
|
report: "--run --json",
|
|
@@ -579,6 +588,17 @@ registerAdapters();
|
|
|
579
588
|
var separator = process.argv.indexOf("--");
|
|
580
589
|
var toolArgs = separator === -1 ? [] : process.argv.slice(separator + 1);
|
|
581
590
|
var cliArgv = separator === -1 ? process.argv : process.argv.slice(0, separator);
|
|
591
|
+
var TARGET_HELP = {
|
|
592
|
+
lint: "linting",
|
|
593
|
+
format: "formatting",
|
|
594
|
+
typescript: "type checking",
|
|
595
|
+
build: "build",
|
|
596
|
+
dev: "dev server (long-running: never swept by an unqualified --run)",
|
|
597
|
+
test: "tests",
|
|
598
|
+
"static-analysis": "cycles, complexity, duplication, centrality",
|
|
599
|
+
"runtime-analysis": "bundle, Lighthouse, web vitals",
|
|
600
|
+
arch: "architecture boundaries"
|
|
601
|
+
};
|
|
582
602
|
program.name("sentinel").description("One CLI that guards code health: presets, analysis, and arch checks.").version(readOwnVersion()).configureHelp({ sortOptions: false }).showSuggestionAfterError(true).showHelpAfterError('(run "sentinel --help" for usage)').addHelpText(
|
|
583
603
|
"before",
|
|
584
604
|
[
|
|
@@ -589,7 +609,7 @@ program.name("sentinel").description("One CLI that guards code health: presets,
|
|
|
589
609
|
" --ci (affected), or all modules. --init targets one module only.",
|
|
590
610
|
""
|
|
591
611
|
].join("\n")
|
|
592
|
-
).option("--run", "execute the target tool").option("--inspect", "show the resolved configuration").option("--init", "set up a module: write its config stubs + the workspace prep it needs").option("--migrate", "planned: change an already-initialized setup (not available yet)").option("--report", "deprecated: use --run --json").option("--status", "deprecated: use --inspect").option(
|
|
612
|
+
).option("--run", "execute the target tool").option("--inspect", "show the resolved configuration").option("--init", "set up a module: write its config stubs + the workspace prep it needs").option("--migrate", "planned: change an already-initialized setup (not available yet)").option("--report", "deprecated: use --run --json").option("--status", "deprecated: use --inspect").option(
|
|
593
613
|
"--module <name>",
|
|
594
614
|
"from the workspace root: scope to one module (omit = all; inside a module dir, drop this)"
|
|
595
615
|
).option("--preset <name>", `the stack preset to apply (${PRESET_NAMES.join(", ")})`).option("--flavour <name>", "deprecated: use --preset").option("--runner <tool>", "override the default runner (e.g. eslint, biome)").option("--ci", "CI mode: from the root, only the affected modules; non-zero exit on failure").option("--fix", "auto-fix where applicable").option("--dry-run", "preview the changes without writing (--init)").option("--json", "machine-readable output, for every verb").option(
|
|
@@ -621,7 +641,10 @@ program.name("sentinel").description("One CLI that guards code health: presets,
|
|
|
621
641
|
`Available now: ${available.length ? available.map((t) => `--${t}`).join(", ") : "(none yet)"}`,
|
|
622
642
|
planned.length ? `Planned (ship in later tickets): ${planned.map((t) => `--${t}`).join(", ")}` : ""
|
|
623
643
|
].filter(Boolean).join("\n");
|
|
624
|
-
})
|
|
644
|
+
});
|
|
645
|
+
for (const target of TARGETS) program.option(`--${target}`, TARGET_HELP[target]);
|
|
646
|
+
program.option("--all", "every target");
|
|
647
|
+
program.parse(cliArgv);
|
|
625
648
|
var opts = program.opts();
|
|
626
649
|
function toCamel(flag) {
|
|
627
650
|
return flag.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
REACT_APP_DEFAULTS,
|
|
3
|
-
REQUIREMENTS,
|
|
4
|
-
isPlainObject
|
|
5
|
-
} from "./chunk-XVDOQ3G3.js";
|
|
6
|
-
|
|
7
1
|
// src/core/registry.ts
|
|
8
2
|
var adapters = [];
|
|
9
3
|
var PresetUnsupportedError = class extends Error {
|
|
@@ -223,7 +217,7 @@ function binSearchPath(cwd) {
|
|
|
223
217
|
|
|
224
218
|
// src/roles/build/plan.ts
|
|
225
219
|
import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
|
|
226
|
-
import { join as join7 } from "path";
|
|
220
|
+
import { basename as basename2, join as join7 } from "path";
|
|
227
221
|
|
|
228
222
|
// src/core/config/existing-command.ts
|
|
229
223
|
import { readFileSync as readFileSync3 } from "fs";
|
|
@@ -384,7 +378,85 @@ function keepsOtherCommands(script, sentinelCommand) {
|
|
|
384
378
|
return script.split(SEGMENT_SEPARATOR).filter((_, index) => !isSeparatorAt(index)).some((segment) => segment.trim() !== "" && segment.trim() !== sentinelCommand);
|
|
385
379
|
}
|
|
386
380
|
|
|
381
|
+
// src/roles/build/presets/requirements.json
|
|
382
|
+
var requirements_default = {
|
|
383
|
+
svelte: {
|
|
384
|
+
why: "sentinel ships Vite 8, and @sveltejs/vite-plugin-svelte caps at Vite 6 until its major 7",
|
|
385
|
+
requires: {
|
|
386
|
+
"@sveltejs/vite-plugin-svelte": "7.0.0",
|
|
387
|
+
svelte: "5.46.4"
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
// src/roles/build/presets/toolchain.json
|
|
393
|
+
var toolchain_default = {
|
|
394
|
+
$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.",
|
|
395
|
+
owned: [
|
|
396
|
+
{
|
|
397
|
+
package: "vite",
|
|
398
|
+
specifier: "vite",
|
|
399
|
+
named: ["defineConfig", "loadEnv", "mergeConfig"],
|
|
400
|
+
types: [
|
|
401
|
+
"Plugin",
|
|
402
|
+
"PluginOption",
|
|
403
|
+
"SassPreprocessorOptions",
|
|
404
|
+
"UserConfig",
|
|
405
|
+
"UserConfigExport"
|
|
406
|
+
],
|
|
407
|
+
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."
|
|
408
|
+
},
|
|
409
|
+
{
|
|
410
|
+
package: "@vitejs/plugin-react",
|
|
411
|
+
specifier: "@vitejs/plugin-react",
|
|
412
|
+
default: "react",
|
|
413
|
+
why: "the React transform. Bound to the Vite that runs it, so it has to come from the same install."
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
package: "@tailwindcss/vite",
|
|
417
|
+
specifier: "@tailwindcss/vite",
|
|
418
|
+
default: "tailwindcss",
|
|
419
|
+
why: "the Tailwind pipeline, identical in all three front apps."
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
package: "vite-plugin-svgr",
|
|
423
|
+
specifier: "vite-plugin-svgr",
|
|
424
|
+
default: "svgr",
|
|
425
|
+
why: "SVG as components, identical in all three front apps."
|
|
426
|
+
},
|
|
427
|
+
{
|
|
428
|
+
package: "nitro",
|
|
429
|
+
specifier: "nitro/vite",
|
|
430
|
+
named: ["nitro"],
|
|
431
|
+
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."
|
|
432
|
+
}
|
|
433
|
+
],
|
|
434
|
+
add: [
|
|
435
|
+
{
|
|
436
|
+
package: "@hublo/sentinel",
|
|
437
|
+
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."
|
|
438
|
+
}
|
|
439
|
+
],
|
|
440
|
+
$notOwned: {
|
|
441
|
+
"@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.",
|
|
442
|
+
"@locator/babel-jsx": "same, and same owners.",
|
|
443
|
+
"@tanstack/react-start": "a framework the apps code against: 48 source files import it directly.",
|
|
444
|
+
"@tanstack/router-plugin": "generates the app's own route tree.",
|
|
445
|
+
"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."
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
// src/roles/build/preset-data.ts
|
|
450
|
+
var REQUIREMENTS = requirements_default;
|
|
451
|
+
var OWNED_PACKAGES = toolchain_default.owned;
|
|
452
|
+
|
|
387
453
|
// src/roles/build/config-policy.ts
|
|
454
|
+
var COMPANION_CONFIG_FILES = [
|
|
455
|
+
"vitest.config.ts",
|
|
456
|
+
"vitest.config.mts",
|
|
457
|
+
"vitest.config.js",
|
|
458
|
+
"vitest.config.mjs"
|
|
459
|
+
];
|
|
388
460
|
var BUILD_CONFIG_FILES = [
|
|
389
461
|
"vite.config.ts",
|
|
390
462
|
"vite.config.mts",
|
|
@@ -404,13 +476,9 @@ function buildTarget() {
|
|
|
404
476
|
}
|
|
405
477
|
};
|
|
406
478
|
}
|
|
407
|
-
var SENTINEL_OWNED_BUILD_PACKAGES =
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
"@tailwindcss/vite",
|
|
411
|
-
"vite-plugin-svgr",
|
|
412
|
-
"nitro"
|
|
413
|
-
];
|
|
479
|
+
var SENTINEL_OWNED_BUILD_PACKAGES = OWNED_PACKAGES.map(
|
|
480
|
+
(entry) => entry.package
|
|
481
|
+
);
|
|
414
482
|
|
|
415
483
|
// src/roles/build/dev-script.ts
|
|
416
484
|
var CHAIN = " -- ";
|
|
@@ -418,17 +486,17 @@ var VITE_TOKEN = /(^|\s|\/)vite(\s|$)/;
|
|
|
418
486
|
var DEV_SUBCOMMANDS = /* @__PURE__ */ new Set(["dev", "serve"]);
|
|
419
487
|
function appOptions(rest) {
|
|
420
488
|
const tokens = (rest ?? "").trim().split(/\s+/).filter(Boolean);
|
|
421
|
-
const kept = tokens.filter((token,
|
|
489
|
+
const kept = tokens.filter((token, at) => !(at === 0 && DEV_SUBCOMMANDS.has(token)));
|
|
422
490
|
return kept.join(" ");
|
|
423
491
|
}
|
|
424
492
|
function composeDevScript(existing, command) {
|
|
425
493
|
if (existing === void 0 || existing.trim() === "") return command;
|
|
426
494
|
const chunks = existing.trim().split(CHAIN);
|
|
427
|
-
const
|
|
428
|
-
if (
|
|
429
|
-
const rest = (chunks[
|
|
495
|
+
const at = chunks.map((chunk) => VITE_TOKEN.test(chunk)).lastIndexOf(true);
|
|
496
|
+
if (at === -1) return existing;
|
|
497
|
+
const rest = (chunks[at] ?? "").replace(/^.*?(^|\s|\/)vite(\s|$)/, "");
|
|
430
498
|
const options = appOptions(rest);
|
|
431
|
-
chunks[
|
|
499
|
+
chunks[at] = options === "" ? command : `${command} -- ${options}`;
|
|
432
500
|
return chunks.join(CHAIN);
|
|
433
501
|
}
|
|
434
502
|
|
|
@@ -502,9 +570,9 @@ function atLeast(declared, needed) {
|
|
|
502
570
|
const parse2 = (value) => value.replace(/^[\^~>=<\s]+/, "").split("-")[0].split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
503
571
|
const left = parse2(declared);
|
|
504
572
|
const right = parse2(needed);
|
|
505
|
-
for (let
|
|
506
|
-
const a = left[
|
|
507
|
-
const b = right[
|
|
573
|
+
for (let at = 0; at < Math.max(left.length, right.length); at++) {
|
|
574
|
+
const a = left[at] ?? 0;
|
|
575
|
+
const b = right[at] ?? 0;
|
|
508
576
|
if (a !== b) return a > b;
|
|
509
577
|
}
|
|
510
578
|
return true;
|
|
@@ -525,43 +593,103 @@ function describeUnmet(preset, unmet) {
|
|
|
525
593
|
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\`.`;
|
|
526
594
|
}
|
|
527
595
|
|
|
528
|
-
// src/roles/build/
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
596
|
+
// src/roles/build/rewrite-imports.ts
|
|
597
|
+
import { parseSync } from "oxc-parser";
|
|
598
|
+
var MOVED_SPECIFIERS = new Set(OWNED_PACKAGES.map((entry) => entry.specifier));
|
|
599
|
+
function esmBlocker(fileName, packageType) {
|
|
600
|
+
if (/\.(mts|mjs)$/.test(fileName)) return void 0;
|
|
601
|
+
if (packageType === "module") return void 0;
|
|
602
|
+
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.`;
|
|
603
|
+
}
|
|
604
|
+
var DEFAULT_EXPORT_NAMES = Object.fromEntries(
|
|
605
|
+
OWNED_PACKAGES.filter((entry) => entry.default !== void 0).map((entry) => [
|
|
606
|
+
entry.specifier,
|
|
607
|
+
entry.default
|
|
608
|
+
])
|
|
609
|
+
);
|
|
610
|
+
function renderImport(bindings) {
|
|
611
|
+
const render = (list) => list.map((b) => b.exported === b.local ? b.exported : `${b.exported} as ${b.local}`).sort((left, right) => left.localeCompare(right)).join(", ");
|
|
612
|
+
const values = bindings.filter((b) => !b.typeOnly);
|
|
613
|
+
const types = bindings.filter((b) => b.typeOnly);
|
|
614
|
+
const lines = [];
|
|
615
|
+
if (values.length > 0) lines.push(`import { ${render(values)} } from '${BUILD_PRESET_SPECIFIER}'`);
|
|
616
|
+
if (types.length > 0) {
|
|
617
|
+
lines.push(`import type { ${render(types)} } from '${BUILD_PRESET_SPECIFIER}'`);
|
|
618
|
+
}
|
|
619
|
+
return lines.join("\n");
|
|
620
|
+
}
|
|
621
|
+
function rewriteBuildImports(source, fileName = "vite.config.ts") {
|
|
622
|
+
const { program, errors } = parseSync(fileName, source, { sourceType: "module" });
|
|
623
|
+
if (errors.length > 0) {
|
|
624
|
+
return {
|
|
625
|
+
kind: "blocked",
|
|
626
|
+
why: `${fileName} does not parse: ${errors[0]?.message ?? "unknown"}`
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
const imports = program.body.filter(
|
|
630
|
+
(node) => node.type === "ImportDeclaration"
|
|
631
|
+
);
|
|
632
|
+
if (imports.some((node) => node.source.value === BUILD_PRESET_SPECIFIER)) {
|
|
633
|
+
return { kind: "unchanged", why: "this config already imports its toolchain from sentinel" };
|
|
634
|
+
}
|
|
635
|
+
const moving = imports.filter((node) => MOVED_SPECIFIERS.has(node.source.value));
|
|
636
|
+
if (moving.length === 0) {
|
|
637
|
+
return {
|
|
638
|
+
kind: "unchanged",
|
|
639
|
+
why: "this config imports none of the packages sentinel owns, so there is nothing to move"
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
const bindings = [];
|
|
643
|
+
for (const node of moving) {
|
|
644
|
+
const from = node.source.value;
|
|
645
|
+
const specifiers = node.specifiers ?? [];
|
|
646
|
+
if (specifiers.length === 0) {
|
|
647
|
+
return { kind: "blocked", why: `\`import '${from}'\` has no bindings to move` };
|
|
648
|
+
}
|
|
649
|
+
for (const spec of specifiers) {
|
|
650
|
+
const typeOnly = node.importKind === "type" || spec.importKind === "type";
|
|
651
|
+
if (spec.type === "ImportDefaultSpecifier") {
|
|
652
|
+
const exported = DEFAULT_EXPORT_NAMES[from];
|
|
653
|
+
if (!exported) {
|
|
654
|
+
return {
|
|
655
|
+
kind: "blocked",
|
|
656
|
+
why: `no sentinel export for the default import of \`${from}\``
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
bindings.push({ exported, local: spec.local.name, typeOnly });
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
if (spec.type === "ImportSpecifier") {
|
|
663
|
+
bindings.push({
|
|
664
|
+
exported: spec.imported?.name ?? spec.imported?.value ?? spec.local.name,
|
|
665
|
+
local: spec.local.name,
|
|
666
|
+
typeOnly
|
|
667
|
+
});
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
670
|
+
return {
|
|
671
|
+
kind: "blocked",
|
|
672
|
+
why: `\`import * as ${spec.local.name} from '${from}'\` cannot be mapped onto sentinel's named exports`
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
const spans = moving.map((node) => ({ start: node.start, end: node.end })).sort((left, right) => left.start - right.start);
|
|
677
|
+
const first = spans[0];
|
|
678
|
+
const rest = spans.slice(1);
|
|
679
|
+
let out = source;
|
|
680
|
+
for (const span of rest.slice().reverse()) {
|
|
681
|
+
const after = out.slice(span.end).match(/^\r?\n/)?.[0].length ?? 0;
|
|
682
|
+
out = out.slice(0, span.start) + out.slice(span.end + after);
|
|
683
|
+
}
|
|
684
|
+
out = out.slice(0, first.start) + renderImport(bindings) + out.slice(first.end);
|
|
685
|
+
return {
|
|
686
|
+
kind: "rewritten",
|
|
687
|
+
source: out,
|
|
688
|
+
moved: [...new Set(moving.map((node) => node.source.value))].sort()
|
|
689
|
+
};
|
|
564
690
|
}
|
|
691
|
+
|
|
692
|
+
// src/roles/build/plan.ts
|
|
565
693
|
function ownedBuildDependencies(cwd) {
|
|
566
694
|
const manifest = readProjectPackageJson(cwd);
|
|
567
695
|
const keys = [];
|
|
@@ -590,9 +718,10 @@ function buildScripts(cwd) {
|
|
|
590
718
|
};
|
|
591
719
|
const existingDev = existingCommand(cwd, DEV_SCRIPT_NAME);
|
|
592
720
|
if (existingDev !== void 0) {
|
|
721
|
+
const own2 = basename2(cwd);
|
|
593
722
|
scripts[DEV_SCRIPT_NAME] = composeDevScript(
|
|
594
723
|
existingDev,
|
|
595
|
-
selfCommand(cwd, "--run --dev") ?? SENTINEL_DEV_COMMAND
|
|
724
|
+
`${selfCommand(cwd, "--run --dev") ?? SENTINEL_DEV_COMMAND} --module ${own2}`
|
|
596
725
|
);
|
|
597
726
|
}
|
|
598
727
|
return scripts;
|
|
@@ -616,14 +745,41 @@ function plan(context) {
|
|
|
616
745
|
const operations = [];
|
|
617
746
|
const configFile = buildConfigFile(context.cwd);
|
|
618
747
|
if (configFile === void 0) {
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
`
|
|
622
|
-
|
|
623
|
-
} else
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
);
|
|
748
|
+
return {
|
|
749
|
+
operations: [],
|
|
750
|
+
skipped: `no Vite config in this module, so there is nothing to point at sentinel. This role moves an existing config's imports; it does not introduce a bundler. If this module builds another way (38 projects here build through \`@nx/webpack\`), the build role does not apply to it.`
|
|
751
|
+
};
|
|
752
|
+
} else {
|
|
753
|
+
const notEsm = esmBlocker(configFile, readProjectPackageJson(context.cwd).type);
|
|
754
|
+
if (notEsm !== void 0) return { operations: [], skipped: notEsm };
|
|
755
|
+
const rewrite = rewriteBuildImports(readConfigSource(context.cwd, configFile), configFile);
|
|
756
|
+
if (rewrite.kind === "blocked") {
|
|
757
|
+
return {
|
|
758
|
+
operations: [],
|
|
759
|
+
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.`
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
if (rewrite.kind === "rewritten") {
|
|
763
|
+
operations.push({ kind: "write", path: configFile, contents: rewrite.source });
|
|
764
|
+
}
|
|
765
|
+
for (const name of COMPANION_CONFIG_FILES) {
|
|
766
|
+
const source = readConfigSource(context.cwd, name);
|
|
767
|
+
if (source === "") continue;
|
|
768
|
+
const companion = rewriteBuildImports(source, name);
|
|
769
|
+
if (companion.kind === "rewritten") {
|
|
770
|
+
operations.push({ kind: "write", path: name, contents: companion.source });
|
|
771
|
+
notes.push(`${name} now imports ${companion.moved.join(", ")} from sentinel too.`);
|
|
772
|
+
} else if (companion.kind === "blocked") {
|
|
773
|
+
notes.push(
|
|
774
|
+
`${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.`
|
|
775
|
+
);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
if (rewrite.kind === "rewritten") {
|
|
779
|
+
notes.push(
|
|
780
|
+
`${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.`
|
|
781
|
+
);
|
|
782
|
+
}
|
|
627
783
|
}
|
|
628
784
|
operations.push(manifestOperation(context.cwd, buildScripts(context.cwd)));
|
|
629
785
|
operations.push(...nxTargetOperations({ cwd: context.cwd, targets: buildTarget() }));
|
|
@@ -636,12 +792,12 @@ function plan(context) {
|
|
|
636
792
|
}
|
|
637
793
|
return { operations, notes };
|
|
638
794
|
}
|
|
639
|
-
function
|
|
640
|
-
if (!existsSync7(join7(cwd, configFile))) return
|
|
795
|
+
function readConfigSource(cwd, configFile) {
|
|
796
|
+
if (!existsSync7(join7(cwd, configFile))) return "";
|
|
641
797
|
try {
|
|
642
|
-
return readFileSync5(join7(cwd, configFile), "utf8")
|
|
798
|
+
return readFileSync5(join7(cwd, configFile), "utf8");
|
|
643
799
|
} catch {
|
|
644
|
-
return
|
|
800
|
+
return "";
|
|
645
801
|
}
|
|
646
802
|
}
|
|
647
803
|
|
|
@@ -791,48 +947,6 @@ var ViteDevAdapter = class extends ViteRoleAdapter {
|
|
|
791
947
|
}
|
|
792
948
|
};
|
|
793
949
|
|
|
794
|
-
// src/roles/build/owned-paths.ts
|
|
795
|
-
var PRESET_OWNED_KEYS = [
|
|
796
|
-
"base",
|
|
797
|
-
"root",
|
|
798
|
-
"define",
|
|
799
|
-
"server",
|
|
800
|
-
"build",
|
|
801
|
-
"nitro",
|
|
802
|
-
"resolve",
|
|
803
|
-
"plugins"
|
|
804
|
-
];
|
|
805
|
-
function describeDepartures(config, defaults) {
|
|
806
|
-
const departures = [];
|
|
807
|
-
const build = asRecord(config.build);
|
|
808
|
-
const target = build?.target;
|
|
809
|
-
if (typeof target === "string" && target !== defaults.target) {
|
|
810
|
-
departures.push({
|
|
811
|
-
rule: "build.target",
|
|
812
|
-
reason: `${target}, where the convention is ${defaults.target}`
|
|
813
|
-
});
|
|
814
|
-
}
|
|
815
|
-
const server = asRecord(config.server);
|
|
816
|
-
const hosts = server?.allowedHosts;
|
|
817
|
-
if (Array.isArray(hosts) && !sameStrings(hosts, defaults.allowedHosts)) {
|
|
818
|
-
departures.push({
|
|
819
|
-
rule: "server.allowedHosts",
|
|
820
|
-
reason: `${JSON.stringify(hosts)}, where the convention is ${JSON.stringify(defaults.allowedHosts)}`
|
|
821
|
-
});
|
|
822
|
-
}
|
|
823
|
-
return departures;
|
|
824
|
-
}
|
|
825
|
-
function appAdditions(config) {
|
|
826
|
-
const owned = new Set(PRESET_OWNED_KEYS);
|
|
827
|
-
return Object.keys(config).filter((key) => !owned.has(key)).sort();
|
|
828
|
-
}
|
|
829
|
-
function asRecord(value) {
|
|
830
|
-
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
831
|
-
}
|
|
832
|
-
function sameStrings(a, b) {
|
|
833
|
-
return a.length === b.length && a.every((value, at2) => value === b[at2]);
|
|
834
|
-
}
|
|
835
|
-
|
|
836
950
|
// src/roles/build/prerequisite.ts
|
|
837
951
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
838
952
|
var PREBUILD_SCRIPT_NAME = `pre${BUILD_SCRIPT_NAME}`;
|
|
@@ -859,23 +973,6 @@ function runPrerequisite(cwd, env = process.env) {
|
|
|
859
973
|
return { kind: "ran" };
|
|
860
974
|
}
|
|
861
975
|
|
|
862
|
-
// src/roles/build/resolve-config.ts
|
|
863
|
-
async function resolveBuildConfig(cwd) {
|
|
864
|
-
try {
|
|
865
|
-
const { loadConfigFromFile } = await import("vite");
|
|
866
|
-
const loaded = await loadConfigFromFile(
|
|
867
|
-
{ command: "build", mode: "production" },
|
|
868
|
-
void 0,
|
|
869
|
-
cwd,
|
|
870
|
-
"silent"
|
|
871
|
-
);
|
|
872
|
-
if (!loaded) return { kind: "failed", reason: "Vite found no config file in this module" };
|
|
873
|
-
return { kind: "loaded", config: loaded.config, from: loaded.path };
|
|
874
|
-
} catch (error) {
|
|
875
|
-
return { kind: "failed", reason: error instanceof Error ? error.message : String(error) };
|
|
876
|
-
}
|
|
877
|
-
}
|
|
878
|
-
|
|
879
976
|
// src/roles/build/adapters/vite/vite.adapter.ts
|
|
880
977
|
var ViteAdapter = class extends ViteRoleAdapter {
|
|
881
978
|
target = "build";
|
|
@@ -901,25 +998,30 @@ var ViteAdapter = class extends ViteRoleAdapter {
|
|
|
901
998
|
return this.spawnVite(ctx, ready.vite, ["build"], 1);
|
|
902
999
|
}
|
|
903
1000
|
/**
|
|
904
|
-
* What this module builds with,
|
|
1001
|
+
* What this module builds with, WITHOUT loading its config.
|
|
905
1002
|
*
|
|
906
|
-
*
|
|
907
|
-
*
|
|
908
|
-
*
|
|
1003
|
+
* Everything reported here is read from the manifest and from the config's text: whether the
|
|
1004
|
+
* module imports its toolchain from sentinel, which Vite install would answer, what it still
|
|
1005
|
+
* declares of its own, and whether it has a `prebuild` step.
|
|
909
1006
|
*
|
|
910
|
-
*
|
|
911
|
-
*
|
|
912
|
-
*
|
|
1007
|
+
* It used to also load the config through Vite and report where it departed from a preset
|
|
1008
|
+
* and what it added on top. Both were dropped with the preset itself. Adoption changes a
|
|
1009
|
+
* config's IMPORTS and nothing else, so there is no preset the app signed up to and no
|
|
1010
|
+
* distance from it to measure; and computing it meant EXECUTING an app's config, which is
|
|
1011
|
+
* expensive and can fail on its own terms (career throws without `VITE_BRAND_ID`). Reading
|
|
1012
|
+
* a file to report on it must not be able to break.
|
|
913
1013
|
*
|
|
914
|
-
*
|
|
915
|
-
*
|
|
1014
|
+
* They come back with the shared base, when there is again something an app has chosen to
|
|
1015
|
+
* adopt and can therefore depart from.
|
|
916
1016
|
*/
|
|
917
1017
|
async inspect(ctx) {
|
|
918
1018
|
const adoption = readBuildAdoption(ctx.cwd);
|
|
919
|
-
|
|
1019
|
+
return {
|
|
920
1020
|
runner: "vite",
|
|
921
1021
|
configFile: adoption.configFile,
|
|
922
1022
|
...adoption.unreadable ? { unreadable: adoption.unreadable } : {},
|
|
1023
|
+
// The field nothing else reports: an adopted config takes its plugins from sentinel, so
|
|
1024
|
+
// a module answering `module` here is one build away from the two-copies failure.
|
|
923
1025
|
vite: viteOrigin(ctx.cwd),
|
|
924
1026
|
// Rendered as a labelled block by the shared renderer, the same shape the lint role's
|
|
925
1027
|
// parked rules and the format role's overrides already use.
|
|
@@ -929,36 +1031,6 @@ var ViteAdapter = class extends ViteRoleAdapter {
|
|
|
929
1031
|
})),
|
|
930
1032
|
prerequisite: moduleScripts(ctx.cwd)[PREBUILD_SCRIPT_NAME] ?? null
|
|
931
1033
|
};
|
|
932
|
-
if (adoption.configFile === null) return base;
|
|
933
|
-
const resolved = await resolveBuildConfig(ctx.cwd);
|
|
934
|
-
if (resolved.kind === "failed") {
|
|
935
|
-
return { ...base, configError: resolved.reason };
|
|
936
|
-
}
|
|
937
|
-
const config = resolved.config;
|
|
938
|
-
return {
|
|
939
|
-
...base,
|
|
940
|
-
// What the app departs from, and what it adds, are different facts. A DEPARTURE is a
|
|
941
|
-
// disagreement with an opinion the preset holds; an ADDITION is the app needing
|
|
942
|
-
// something the preset never claimed, which is the preset working rather than being
|
|
943
|
-
// worked around.
|
|
944
|
-
departures: describeDepartures(config, REACT_APP_DEFAULTS),
|
|
945
|
-
additions: appAdditions(config),
|
|
946
|
-
resolved: {
|
|
947
|
-
base: at(config, "base") ?? null,
|
|
948
|
-
target: at(config, "build", "target") ?? null,
|
|
949
|
-
sourcemap: at(config, "build", "sourcemap") ?? null,
|
|
950
|
-
port: at(config, "server", "port") ?? null,
|
|
951
|
-
// The one number nothing else reports and that no formula predicts: career goes DOWN
|
|
952
|
-
// to 9998 where the others go up, which is why it is data rather than `port + 1`.
|
|
953
|
-
hmrPort: at(config, "server", "hmr", "port") ?? null,
|
|
954
|
-
// Counted, not listed. It is 56-67% of every config today, and printing 296 entries
|
|
955
|
-
// would bury everything above it. The count is what tells you whether it moved.
|
|
956
|
-
aliases: countOf(at(config, "resolve", "alias")),
|
|
957
|
-
// Flattened without a depth limit: Vite lets a plugin be an arbitrarily nested array,
|
|
958
|
-
// and a magic number here would silently undercount the day someone nests one deeper.
|
|
959
|
-
plugins: countOf(at(config, "plugins"))
|
|
960
|
-
}
|
|
961
|
-
};
|
|
962
1034
|
}
|
|
963
1035
|
/**
|
|
964
1036
|
* Adoption as data, WITHOUT building.
|
|
@@ -984,17 +1056,6 @@ var ViteAdapter = class extends ViteRoleAdapter {
|
|
|
984
1056
|
};
|
|
985
1057
|
}
|
|
986
1058
|
};
|
|
987
|
-
function at(config, ...path) {
|
|
988
|
-
let value = config;
|
|
989
|
-
for (const key of path) {
|
|
990
|
-
if (typeof value !== "object" || value === null) return void 0;
|
|
991
|
-
value = value[key];
|
|
992
|
-
}
|
|
993
|
-
return value;
|
|
994
|
-
}
|
|
995
|
-
function countOf(value) {
|
|
996
|
-
return Array.isArray(value) ? value.flat(Number.POSITIVE_INFINITY).length : 0;
|
|
997
|
-
}
|
|
998
1059
|
|
|
999
1060
|
// src/roles/build/register.ts
|
|
1000
1061
|
function registerBuild() {
|
|
@@ -4180,8 +4241,7 @@ import { join as join23 } from "path";
|
|
|
4180
4241
|
// src/roles/lint/eslint-ignores.ts
|
|
4181
4242
|
import { existsSync as existsSync18, readFileSync as readFileSync16 } from "fs";
|
|
4182
4243
|
import { join as join21 } from "path";
|
|
4183
|
-
|
|
4184
|
-
var STRING_LITERAL = /['"`]([^'"`]+)['"`]/g;
|
|
4244
|
+
import { parseSync as parseSync2 } from "oxc-parser";
|
|
4185
4245
|
var OPAQUE_SOURCE = /\b(includeIgnoreFile)\s*\([^)]*\)/g;
|
|
4186
4246
|
function toOxlintPattern(pattern) {
|
|
4187
4247
|
return pattern.replace(/^\.\//, "");
|
|
@@ -4201,15 +4261,62 @@ function readEslintIgnores(cwd) {
|
|
|
4201
4261
|
return { patterns: [], unresolved: [] };
|
|
4202
4262
|
}
|
|
4203
4263
|
const patterns = /* @__PURE__ */ new Set();
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4264
|
+
const unresolved = new Set([...source.matchAll(OPAQUE_SOURCE)].map((match) => match[0]));
|
|
4265
|
+
const { program, errors } = parseSync2(config, source, { sourceType: "module" });
|
|
4266
|
+
if (errors.length > 0) {
|
|
4267
|
+
return { patterns: [], unresolved: [`${config} could not be parsed`] };
|
|
4268
|
+
}
|
|
4269
|
+
walk(program, (node) => {
|
|
4270
|
+
if (node.type === "CallExpression" && calleeName(node) === "globalIgnores") {
|
|
4271
|
+
for (const literal of stringLiterals(firstArrayArgument(node))) {
|
|
4272
|
+
patterns.add(toOxlintPattern(literal));
|
|
4208
4273
|
}
|
|
4274
|
+
return;
|
|
4209
4275
|
}
|
|
4276
|
+
if (node.type !== "ObjectExpression") return;
|
|
4277
|
+
const keys = objectKeys(node);
|
|
4278
|
+
if (!keys.includes("ignores")) return;
|
|
4279
|
+
const values = arrayValues(node, "ignores");
|
|
4280
|
+
if (keys.length > 1) {
|
|
4281
|
+
unresolved.add(
|
|
4282
|
+
`ignores scoped to one config block (with ${keys.filter((k) => k !== "ignores").join(", ")}): ${values.join(", ")} \u2014 exempt from those RULES, not excluded from linting`
|
|
4283
|
+
);
|
|
4284
|
+
return;
|
|
4285
|
+
}
|
|
4286
|
+
for (const literal of values) patterns.add(toOxlintPattern(literal));
|
|
4287
|
+
});
|
|
4288
|
+
return { patterns: [...patterns], unresolved: [...unresolved] };
|
|
4289
|
+
}
|
|
4290
|
+
function walk(node, visit) {
|
|
4291
|
+
if (!node || typeof node !== "object") return;
|
|
4292
|
+
const current = node;
|
|
4293
|
+
if (typeof current.type === "string") visit(current);
|
|
4294
|
+
for (const key of Object.keys(current)) {
|
|
4295
|
+
const value = current[key];
|
|
4296
|
+
if (Array.isArray(value)) for (const entry of value) walk(entry, visit);
|
|
4297
|
+
else if (value && typeof value === "object") walk(value, visit);
|
|
4210
4298
|
}
|
|
4211
|
-
|
|
4212
|
-
|
|
4299
|
+
}
|
|
4300
|
+
function calleeName(node) {
|
|
4301
|
+
const callee = node.callee;
|
|
4302
|
+
return callee?.type === "Identifier" ? callee.name : void 0;
|
|
4303
|
+
}
|
|
4304
|
+
function firstArrayArgument(node) {
|
|
4305
|
+
const args = node.arguments ?? [];
|
|
4306
|
+
return args.find((argument) => argument?.type === "ArrayExpression");
|
|
4307
|
+
}
|
|
4308
|
+
function stringLiterals(array) {
|
|
4309
|
+
const elements = array?.elements ?? [];
|
|
4310
|
+
return elements.filter((element) => element?.type === "Literal" && typeof element.value === "string").map((element) => element.value);
|
|
4311
|
+
}
|
|
4312
|
+
function objectKeys(node) {
|
|
4313
|
+
const properties = node.properties ?? [];
|
|
4314
|
+
return properties.filter((property) => property?.type === "Property" && !property.computed).map((property) => property.key?.name ?? String(property.key?.value ?? "")).filter(Boolean);
|
|
4315
|
+
}
|
|
4316
|
+
function arrayValues(node, name) {
|
|
4317
|
+
const properties = node.properties ?? [];
|
|
4318
|
+
const found = properties.find((property) => property.key?.name === name);
|
|
4319
|
+
return stringLiterals(found?.value);
|
|
4213
4320
|
}
|
|
4214
4321
|
|
|
4215
4322
|
// src/roles/lint/lint-script.ts
|
|
@@ -4755,7 +4862,7 @@ ${result.stderr ?? ""}`);
|
|
|
4755
4862
|
} else {
|
|
4756
4863
|
rmSync(baselinePath, { force: true });
|
|
4757
4864
|
}
|
|
4758
|
-
if (next.length !== current.length || next.some((entry,
|
|
4865
|
+
if (next.length !== current.length || next.some((entry, at) => entry !== current[at])) {
|
|
4759
4866
|
config.extends = next;
|
|
4760
4867
|
writeFileSync2(configPath, `${JSON.stringify(config, null, 2)}
|
|
4761
4868
|
`);
|
|
@@ -5717,7 +5824,7 @@ function detectFramework(packageJson) {
|
|
|
5717
5824
|
}
|
|
5718
5825
|
|
|
5719
5826
|
// src/core/dispatch.ts
|
|
5720
|
-
import { basename as
|
|
5827
|
+
import { basename as basename3 } from "path";
|
|
5721
5828
|
|
|
5722
5829
|
// src/shared/text.ts
|
|
5723
5830
|
function ensureLines(current, lines) {
|
|
@@ -5776,6 +5883,13 @@ function replaceLines(current, replacements) {
|
|
|
5776
5883
|
import { existsSync as existsSync22, readFileSync as readFileSync22, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
5777
5884
|
import { resolve as resolve4, sep } from "path";
|
|
5778
5885
|
import { applyEdits, findNodeAtLocation, modify, parseTree } from "jsonc-parser";
|
|
5886
|
+
|
|
5887
|
+
// src/shared/deep-merge.ts
|
|
5888
|
+
function isPlainObject(value) {
|
|
5889
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5890
|
+
}
|
|
5891
|
+
|
|
5892
|
+
// src/core/apply-plan.ts
|
|
5779
5893
|
function resolveWithinRoot(cwd, relativePath) {
|
|
5780
5894
|
const root = resolve4(cwd);
|
|
5781
5895
|
const absolutePath = resolve4(root, relativePath);
|
|
@@ -6086,7 +6200,7 @@ async function dispatch(opts) {
|
|
|
6086
6200
|
process.stderr.write(` fixing what ${opts.target} can fix automatically...
|
|
6087
6201
|
`);
|
|
6088
6202
|
await adapter.afterInit({
|
|
6089
|
-
module:
|
|
6203
|
+
module: basename3(opts.cwd),
|
|
6090
6204
|
cwd: opts.cwd,
|
|
6091
6205
|
preset,
|
|
6092
6206
|
ci: false,
|
|
@@ -6111,6 +6225,7 @@ export {
|
|
|
6111
6225
|
readNxProjectName,
|
|
6112
6226
|
VERBS,
|
|
6113
6227
|
TARGETS,
|
|
6228
|
+
LONG_RUNNING_TARGETS,
|
|
6114
6229
|
SWEEPABLE_TARGETS,
|
|
6115
6230
|
PRESET_NAMES,
|
|
6116
6231
|
WORKSPACE_ROOT_MARKER,
|
|
@@ -6123,4 +6238,4 @@ export {
|
|
|
6123
6238
|
detectFramework,
|
|
6124
6239
|
dispatch
|
|
6125
6240
|
};
|
|
6126
|
-
//# sourceMappingURL=chunk-
|
|
6241
|
+
//# sourceMappingURL=chunk-SA2RMTVI.js.map
|
package/dist/index.js
CHANGED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { default as tailwindcss } from '@tailwindcss/vite';
|
|
2
|
+
export { default as react } from '@vitejs/plugin-react';
|
|
3
|
+
export { nitro } from 'nitro/vite';
|
|
4
|
+
export { default as svgr } from 'vite-plugin-svgr';
|
|
5
|
+
export { Plugin, PluginOption, SassPreprocessorOptions, UserConfig, UserConfigExport, defineConfig, loadEnv, mergeConfig } from 'vite';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// src/roles/build/toolchain.ts
|
|
2
|
+
import { default as default2 } from "@tailwindcss/vite";
|
|
3
|
+
import { default as default3 } from "@vitejs/plugin-react";
|
|
4
|
+
import { nitro } from "nitro/vite";
|
|
5
|
+
import { default as default4 } from "vite-plugin-svgr";
|
|
6
|
+
import { defineConfig, loadEnv, mergeConfig } from "vite";
|
|
7
|
+
export {
|
|
8
|
+
defineConfig,
|
|
9
|
+
loadEnv,
|
|
10
|
+
mergeConfig,
|
|
11
|
+
nitro,
|
|
12
|
+
default3 as react,
|
|
13
|
+
default4 as svgr,
|
|
14
|
+
default2 as tailwindcss
|
|
15
|
+
};
|
|
16
|
+
//# sourceMappingURL=toolchain.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hublo/sentinel",
|
|
3
|
-
"version": "1.2.0-alpha.
|
|
3
|
+
"version": "1.2.0-alpha.2",
|
|
4
4
|
"description": "One CLI that guards code health across Hublo repos: shared lint/typescript/build/test presets, static & dynamic analysis, and architecture checks.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -39,8 +39,8 @@
|
|
|
39
39
|
"default": "./dist/tsconfig/node.json"
|
|
40
40
|
},
|
|
41
41
|
"./build/react": {
|
|
42
|
-
"types": "./dist/roles/build/
|
|
43
|
-
"import": "./dist/roles/build/
|
|
42
|
+
"types": "./dist/roles/build/toolchain.d.ts",
|
|
43
|
+
"import": "./dist/roles/build/toolchain.js"
|
|
44
44
|
}
|
|
45
45
|
},
|
|
46
46
|
"files": [
|
|
@@ -62,6 +62,7 @@
|
|
|
62
62
|
"eslint-plugin-styled-components-a11y": "2.2.0",
|
|
63
63
|
"jsonc-parser": "^3.3.1",
|
|
64
64
|
"nitro": "3.0.260415-beta",
|
|
65
|
+
"oxc-parser": "0.147.0",
|
|
65
66
|
"oxfmt": "0.63.0",
|
|
66
67
|
"oxlint": "1.77.0",
|
|
67
68
|
"oxlint-tsgolint": "7.0.2001",
|
package/dist/chunk-XVDOQ3G3.js
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
// src/roles/build/presets/requirements.json
|
|
2
|
-
var requirements_default = {
|
|
3
|
-
svelte: {
|
|
4
|
-
why: "sentinel ships Vite 8, and @sveltejs/vite-plugin-svelte caps at Vite 6 until its major 7",
|
|
5
|
-
requires: {
|
|
6
|
-
"@sveltejs/vite-plugin-svelte": "7.0.0",
|
|
7
|
-
svelte: "5.46.4"
|
|
8
|
-
}
|
|
9
|
-
}
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
// src/roles/build/presets/shared.json
|
|
13
|
-
var shared_default = {
|
|
14
|
-
target: "chrome89",
|
|
15
|
-
allowedHosts: [".hubpreprod.com", ".playground-hublo.com"],
|
|
16
|
-
publicAssets: [
|
|
17
|
-
{
|
|
18
|
-
dir: "public/branding",
|
|
19
|
-
baseURL: "/branding",
|
|
20
|
-
maxAge: 31536e3
|
|
21
|
-
}
|
|
22
|
-
]
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
// src/roles/build/preset-data.ts
|
|
26
|
-
var REACT_APP_DEFAULTS = shared_default;
|
|
27
|
-
var REQUIREMENTS = requirements_default;
|
|
28
|
-
|
|
29
|
-
// src/shared/deep-merge.ts
|
|
30
|
-
function isPlainObject(value) {
|
|
31
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export {
|
|
35
|
-
REACT_APP_DEFAULTS,
|
|
36
|
-
REQUIREMENTS,
|
|
37
|
-
isPlainObject
|
|
38
|
-
};
|
|
39
|
-
//# sourceMappingURL=chunk-XVDOQ3G3.js.map
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
import { PluginOption, UserConfig } from 'vite';
|
|
2
|
-
export { defineConfig, loadEnv } from 'vite';
|
|
3
|
-
|
|
4
|
-
/** The convention values, in one place so `--inspect` can report what an app departs from. */
|
|
5
|
-
declare const REACT_APP_DEFAULTS: {
|
|
6
|
-
target: string;
|
|
7
|
-
allowedHosts: string[];
|
|
8
|
-
publicAssets: {
|
|
9
|
-
dir: string;
|
|
10
|
-
baseURL: string;
|
|
11
|
-
maxAge: number;
|
|
12
|
-
}[];
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
/** Where the router's routes live and where it writes its generated tree. */
|
|
16
|
-
interface RouterPaths {
|
|
17
|
-
routesDirectory: string;
|
|
18
|
-
generatedRouteTree: string;
|
|
19
|
-
/** Extra lines appended to the generated tree. Two of the three apps use it. */
|
|
20
|
-
routeTreeFileFooter?: string[];
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* The TanStack factories, supplied by the app.
|
|
24
|
-
*
|
|
25
|
-
* Typed loosely on purpose: sentinel decides WHEN to call them and with what, and has no
|
|
26
|
-
* business asserting their return shape. Narrowing this would only couple sentinel to a
|
|
27
|
-
* version of a package it does not own.
|
|
28
|
-
*/
|
|
29
|
-
interface TanstackFactories {
|
|
30
|
-
start: (options: {
|
|
31
|
-
router: RouterPaths;
|
|
32
|
-
}) => PluginOption[];
|
|
33
|
-
router: (options: RouterPaths & {
|
|
34
|
-
target: string;
|
|
35
|
-
autoCodeSplitting: boolean;
|
|
36
|
-
}) => PluginOption;
|
|
37
|
-
}
|
|
38
|
-
/** Everything an app declares. */
|
|
39
|
-
interface ReactAppOptions {
|
|
40
|
-
/** The app's own directory. Always `__dirname`. */
|
|
41
|
-
root: string;
|
|
42
|
-
/** Vite's `mode`, from the config callback. */
|
|
43
|
-
mode: string;
|
|
44
|
-
/** Served under, e.g. `/console/`. */
|
|
45
|
-
base: string;
|
|
46
|
-
/** Dev server port. */
|
|
47
|
-
port: number;
|
|
48
|
-
/** HMR port. Explicit, NOT `port + 1`: career goes DOWN to 9998 where the others go up. */
|
|
49
|
-
hmrPort: number;
|
|
50
|
-
router: RouterPaths;
|
|
51
|
-
tanstack: TanstackFactories;
|
|
52
|
-
/**
|
|
53
|
-
* Sourcemaps: a boolean, however the app decided it.
|
|
54
|
-
*
|
|
55
|
-
* The three apps disagree on how — two use a mode allowlist, one reads an env var. That is
|
|
56
|
-
* an inconsistency in the repo rather than a requirement, and encoding both would make
|
|
57
|
-
* sentinel the owner of a decision it should not hold.
|
|
58
|
-
*/
|
|
59
|
-
sourcemap?: boolean;
|
|
60
|
-
/** Passed through verbatim, never read. See the note above. */
|
|
61
|
-
alias?: unknown[];
|
|
62
|
-
/** The app's own plugins (branding, babel). Appended AFTER the base set. */
|
|
63
|
-
plugins?: PluginOption[];
|
|
64
|
-
/** Conventions the app may override. */
|
|
65
|
-
target?: string;
|
|
66
|
-
allowedHosts?: string[];
|
|
67
|
-
/**
|
|
68
|
-
* Anything else this app needs, merged over the preset LAST.
|
|
69
|
-
*
|
|
70
|
-
* The preset is a starting point, not a cage. Two things an app must be able to do and
|
|
71
|
-
* could not before: override a NESTED attribute without losing its siblings, and add an
|
|
72
|
-
* attribute the preset knows nothing about.
|
|
73
|
-
*
|
|
74
|
-
* Objects merge, everything else replaces — see `merge-config` for why arrays replace
|
|
75
|
-
* rather than concatenate. Whatever lands here is reported by `--inspect --build`, so an
|
|
76
|
-
* override is a visible, countable decision instead of something found by reading a config.
|
|
77
|
-
*
|
|
78
|
-
* Real cases today: console needs `ssr`, `optimizeDeps` and `preview`; career needs
|
|
79
|
-
* `css.preprocessorOptions` for Sass; all three need their own `nitro.publicAssets`.
|
|
80
|
-
*/
|
|
81
|
-
overrides?: Record<string, unknown>;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* The environment, loaded the way all three apps already load it.
|
|
86
|
-
*
|
|
87
|
-
* `production` is normalised to `prd` because that is this repository's deployment tag: it is
|
|
88
|
-
* the `.env.prd` the build reads and the SSM path segment the values come from. Not a
|
|
89
|
-
* preference, a fact about the repo — which is exactly why it belongs in the preset rather
|
|
90
|
-
* than being retyped in every config.
|
|
91
|
-
*/
|
|
92
|
-
declare function appEnv(root: string, mode: string): Record<string, string>;
|
|
93
|
-
/** True when this run is a test rather than a real build. */
|
|
94
|
-
declare function isTestMode(mode: string, env?: NodeJS.ProcessEnv): boolean;
|
|
95
|
-
/**
|
|
96
|
-
* The plugin names in the order they run, which is what `--inspect` reports.
|
|
97
|
-
*
|
|
98
|
-
* `extras` is how many plugins the app appends of its own; they are named positionally
|
|
99
|
-
* because sentinel has no way to name a function someone else passed in.
|
|
100
|
-
*/
|
|
101
|
-
declare function pluginPlan(isTest: boolean, extras?: number): string[];
|
|
102
|
-
/**
|
|
103
|
-
* The Vite config for a React app, minus the plugins.
|
|
104
|
-
*
|
|
105
|
-
* Pure and separately testable: everything here is data in, data out, so the composition
|
|
106
|
-
* rules can be asserted without a bundler anywhere near the test.
|
|
107
|
-
*/
|
|
108
|
-
declare function reactAppConfig(options: ReactAppOptions): Omit<UserConfig, 'plugins'>;
|
|
109
|
-
/** The config an app spreads into `defineConfig`. */
|
|
110
|
-
declare function reactApp(options: ReactAppOptions): UserConfig;
|
|
111
|
-
|
|
112
|
-
export { REACT_APP_DEFAULTS, type ReactAppOptions, type RouterPaths, type TanstackFactories, appEnv, isTestMode, pluginPlan, reactApp, reactAppConfig };
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
REACT_APP_DEFAULTS,
|
|
3
|
-
isPlainObject
|
|
4
|
-
} from "../../chunk-XVDOQ3G3.js";
|
|
5
|
-
|
|
6
|
-
// src/roles/build/react-app.ts
|
|
7
|
-
import tailwindcss from "@tailwindcss/vite";
|
|
8
|
-
import react from "@vitejs/plugin-react";
|
|
9
|
-
import { nitro } from "nitro/vite";
|
|
10
|
-
import { loadEnv } from "vite";
|
|
11
|
-
import svgr from "vite-plugin-svgr";
|
|
12
|
-
|
|
13
|
-
// src/roles/build/merge-config.ts
|
|
14
|
-
function mergeConfig(base, overrides) {
|
|
15
|
-
if (!overrides) return base;
|
|
16
|
-
const out = { ...base };
|
|
17
|
-
for (const [key, value] of Object.entries(overrides)) {
|
|
18
|
-
if (value === void 0) {
|
|
19
|
-
delete out[key];
|
|
20
|
-
continue;
|
|
21
|
-
}
|
|
22
|
-
const current = out[key];
|
|
23
|
-
out[key] = isPlainObject(current) && isPlainObject(value) ? mergeConfig(current, value) : value;
|
|
24
|
-
}
|
|
25
|
-
return out;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// src/roles/build/react-app.ts
|
|
29
|
-
import { defineConfig, loadEnv as loadEnv2 } from "vite";
|
|
30
|
-
function appEnv(root, mode) {
|
|
31
|
-
return loadEnv(mode === "production" ? "prd" : mode, root, "");
|
|
32
|
-
}
|
|
33
|
-
function isTestMode(mode, env = process.env) {
|
|
34
|
-
return mode === "test" || env.VITEST === "true";
|
|
35
|
-
}
|
|
36
|
-
function pluginEntries(options, isTest) {
|
|
37
|
-
const { tanstack, router } = options;
|
|
38
|
-
return [
|
|
39
|
-
{ name: "tailwindcss", build: () => tailwindcss() },
|
|
40
|
-
...isTest ? [
|
|
41
|
-
{
|
|
42
|
-
name: "tanstackRouter",
|
|
43
|
-
build: () => tanstack.router({ ...router, target: "react", autoCodeSplitting: true })
|
|
44
|
-
}
|
|
45
|
-
] : [
|
|
46
|
-
{ name: "tanstackStart", build: () => tanstack.start({ router }) },
|
|
47
|
-
{ name: "nitro", build: () => nitro() }
|
|
48
|
-
],
|
|
49
|
-
{ name: "react", build: () => react() },
|
|
50
|
-
{ name: "svgr", build: () => svgr() }
|
|
51
|
-
];
|
|
52
|
-
}
|
|
53
|
-
function pluginPlan(isTest, extras = 0) {
|
|
54
|
-
const entries = pluginEntries(
|
|
55
|
-
{ tanstack: { start: () => [], router: () => null }, router: STUB_ROUTER },
|
|
56
|
-
isTest
|
|
57
|
-
);
|
|
58
|
-
return [
|
|
59
|
-
...entries.map((entry) => entry.name),
|
|
60
|
-
...Array.from({ length: extras }, (_, at) => `app:${at}`)
|
|
61
|
-
];
|
|
62
|
-
}
|
|
63
|
-
var STUB_ROUTER = { routesDirectory: "", generatedRouteTree: "" };
|
|
64
|
-
function basePlugins(options, isTest) {
|
|
65
|
-
return [
|
|
66
|
-
...pluginEntries(options, isTest).flatMap((entry) => entry.build()),
|
|
67
|
-
...options.plugins ?? []
|
|
68
|
-
];
|
|
69
|
-
}
|
|
70
|
-
function reactAppConfig(options) {
|
|
71
|
-
const preset = {
|
|
72
|
-
base: options.base,
|
|
73
|
-
root: options.root,
|
|
74
|
-
define: {
|
|
75
|
-
// Development only. In a build this would leak a browser global into SSR output.
|
|
76
|
-
...options.mode === "development" ? { global: "window" } : {}
|
|
77
|
-
},
|
|
78
|
-
server: {
|
|
79
|
-
port: options.port,
|
|
80
|
-
allowedHosts: options.allowedHosts ?? [...REACT_APP_DEFAULTS.allowedHosts],
|
|
81
|
-
hmr: { host: "localhost", protocol: "ws", port: options.hmrPort }
|
|
82
|
-
},
|
|
83
|
-
build: {
|
|
84
|
-
target: options.target ?? REACT_APP_DEFAULTS.target,
|
|
85
|
-
sourcemap: options.sourcemap ?? false
|
|
86
|
-
},
|
|
87
|
-
/*
|
|
88
|
-
* Nitro's two settings are sentinel's, and both were measured across the three apps rather
|
|
89
|
-
* than assumed.
|
|
90
|
-
*
|
|
91
|
-
* `baseURL` is always the app's `base`, without exception: `/console/`, `/` and `/admin/`.
|
|
92
|
-
* It is derived, so an app cannot get them out of step.
|
|
93
|
-
*
|
|
94
|
-
* `publicAssets` is byte-identical in all three — the branding directory, its URL and a
|
|
95
|
-
* one-year max-age. That is a convention about where branding lives, not data about an
|
|
96
|
-
* app, and it was sitting in three `overrides` blocks saying the same thing.
|
|
97
|
-
*
|
|
98
|
-
* An app that needs MORE from nitro overrides the key: career adds `externals` to keep the
|
|
99
|
-
* SSR sanitisation chain out of the server bundle. `mergeConfig` merges objects, so adding
|
|
100
|
-
* one key does not take these two away.
|
|
101
|
-
*/
|
|
102
|
-
nitro: {
|
|
103
|
-
baseURL: options.base,
|
|
104
|
-
publicAssets: [...REACT_APP_DEFAULTS.publicAssets]
|
|
105
|
-
},
|
|
106
|
-
...options.alias ? { resolve: { alias: options.alias } } : {}
|
|
107
|
-
};
|
|
108
|
-
return mergeConfig(preset, options.overrides);
|
|
109
|
-
}
|
|
110
|
-
function reactApp(options) {
|
|
111
|
-
return {
|
|
112
|
-
...reactAppConfig(options),
|
|
113
|
-
plugins: basePlugins(options, isTestMode(options.mode))
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
export {
|
|
117
|
-
REACT_APP_DEFAULTS,
|
|
118
|
-
appEnv,
|
|
119
|
-
defineConfig,
|
|
120
|
-
isTestMode,
|
|
121
|
-
loadEnv2 as loadEnv,
|
|
122
|
-
pluginPlan,
|
|
123
|
-
reactApp,
|
|
124
|
-
reactAppConfig
|
|
125
|
-
};
|
|
126
|
-
//# sourceMappingURL=react-app.js.map
|