@hublo/sentinel 1.0.2 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin/sentinel.js
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
registerAdapters,
|
|
15
15
|
resolve,
|
|
16
16
|
resolveBin
|
|
17
|
-
} from "../chunk-
|
|
17
|
+
} from "../chunk-BN3TB6TF.js";
|
|
18
18
|
|
|
19
19
|
// bin/sentinel.ts
|
|
20
20
|
import { program } from "commander";
|
|
@@ -608,18 +608,24 @@ sentinel (${type}): ${asMessage(err)}
|
|
|
608
608
|
}
|
|
609
609
|
return worst;
|
|
610
610
|
}
|
|
611
|
+
async function exitWithoutTruncating(code) {
|
|
612
|
+
process.exitCode = code;
|
|
613
|
+
const stream = process.stdout;
|
|
614
|
+
if (stream.write("")) return;
|
|
615
|
+
await new Promise((resolve2) => stream.once("drain", () => resolve2()));
|
|
616
|
+
}
|
|
611
617
|
async function main() {
|
|
612
618
|
if (verb === "migrate") {
|
|
613
619
|
program.error("--migrate is planned and not available yet; use --init to set a module up.");
|
|
614
620
|
}
|
|
615
621
|
const runSelectedVerb = verb === "init" ? runInit : runVerb;
|
|
616
622
|
const exitCode = await runSelectedVerb();
|
|
617
|
-
|
|
623
|
+
await exitWithoutTruncating(exitCode);
|
|
618
624
|
}
|
|
619
625
|
main().catch((error) => {
|
|
620
626
|
process.stderr.write(`
|
|
621
627
|
sentinel: ${asMessage(error)}
|
|
622
628
|
`);
|
|
623
|
-
|
|
629
|
+
void exitWithoutTruncating(1);
|
|
624
630
|
});
|
|
625
631
|
//# sourceMappingURL=sentinel.js.map
|
|
@@ -139,8 +139,12 @@ function readNxProjectName(dir) {
|
|
|
139
139
|
|
|
140
140
|
// src/roles/typescript/adapters/tsc/tsc.adapter.ts
|
|
141
141
|
import { spawnSync } from "child_process";
|
|
142
|
-
import { existsSync as
|
|
143
|
-
import { basename, join as
|
|
142
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
|
|
143
|
+
import { basename, join as join6 } from "path";
|
|
144
|
+
|
|
145
|
+
// src/core/config/flavour-evidence.ts
|
|
146
|
+
import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2 } from "fs";
|
|
147
|
+
import { join as join2 } from "path";
|
|
144
148
|
|
|
145
149
|
// src/shared/jsonc.ts
|
|
146
150
|
import { parse, printParseErrorCode } from "jsonc-parser";
|
|
@@ -154,14 +158,92 @@ function parseJsonc(text, source = "config") {
|
|
|
154
158
|
return value;
|
|
155
159
|
}
|
|
156
160
|
|
|
161
|
+
// src/core/config/flavour-evidence.ts
|
|
162
|
+
var PATH_SIGNALS = [
|
|
163
|
+
{
|
|
164
|
+
flavour: "nest",
|
|
165
|
+
pattern: /(^|\/)apps\/nest\//,
|
|
166
|
+
evidence: "it lives under apps/nest"
|
|
167
|
+
}
|
|
168
|
+
];
|
|
169
|
+
var DEPENDENCY_SIGNALS = [
|
|
170
|
+
{ flavour: "react", pattern: /^(react|react-dom|@types\/react)$|^eslint-plugin-react/ },
|
|
171
|
+
{ flavour: "svelte", pattern: /^svelte$|^@sveltejs\// },
|
|
172
|
+
{ flavour: "nest", pattern: /^@nestjs\// }
|
|
173
|
+
];
|
|
174
|
+
function dependencyNames(cwd) {
|
|
175
|
+
const path = join2(cwd, "package.json");
|
|
176
|
+
if (!existsSync2(path)) return [];
|
|
177
|
+
try {
|
|
178
|
+
const manifest = parseJsonc(
|
|
179
|
+
readFileSync2(path, "utf8"),
|
|
180
|
+
path
|
|
181
|
+
);
|
|
182
|
+
return [
|
|
183
|
+
...Object.keys(manifest.dependencies ?? {}),
|
|
184
|
+
...Object.keys(manifest.devDependencies ?? {})
|
|
185
|
+
];
|
|
186
|
+
} catch {
|
|
187
|
+
return [];
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function declaresJsx(cwd) {
|
|
191
|
+
let entries;
|
|
192
|
+
try {
|
|
193
|
+
entries = readdirSync(cwd).filter(
|
|
194
|
+
(name) => name.startsWith("tsconfig") && name.endsWith(".json")
|
|
195
|
+
);
|
|
196
|
+
} catch {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
for (const name of entries) {
|
|
200
|
+
try {
|
|
201
|
+
const config = parseJsonc(
|
|
202
|
+
readFileSync2(join2(cwd, name), "utf8"),
|
|
203
|
+
name
|
|
204
|
+
);
|
|
205
|
+
if (config.compilerOptions?.jsx !== void 0) return true;
|
|
206
|
+
} catch {
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
function flavourSignals(cwd) {
|
|
212
|
+
const signals = [];
|
|
213
|
+
const dependencies = dependencyNames(cwd);
|
|
214
|
+
for (const { flavour, pattern } of DEPENDENCY_SIGNALS) {
|
|
215
|
+
const hit = dependencies.find((name) => pattern.test(name));
|
|
216
|
+
if (hit) signals.push({ flavour, evidence: `it depends on "${hit}"` });
|
|
217
|
+
}
|
|
218
|
+
if (declaresJsx(cwd) && !signals.some((signal) => signal.flavour === "react")) {
|
|
219
|
+
signals.push({ flavour: "react", evidence: 'its tsconfig sets "jsx"' });
|
|
220
|
+
}
|
|
221
|
+
const location = cwd.replaceAll("\\", "/");
|
|
222
|
+
for (const { flavour, pattern, evidence } of PATH_SIGNALS) {
|
|
223
|
+
if (pattern.test(location) && !signals.some((signal) => signal.flavour === flavour)) {
|
|
224
|
+
signals.push({ flavour, evidence });
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return signals;
|
|
228
|
+
}
|
|
229
|
+
function flavourContradiction(declared, cwd) {
|
|
230
|
+
const signals = flavourSignals(cwd);
|
|
231
|
+
if (signals.length === 0) return void 0;
|
|
232
|
+
if (signals.some((signal) => signal.flavour === declared)) return void 0;
|
|
233
|
+
const [first] = signals;
|
|
234
|
+
if (!first) return void 0;
|
|
235
|
+
const evidence = signals.map((signal) => signal.evidence).join(", and ");
|
|
236
|
+
return `--flavour ${declared} does not match this module: ${evidence}, which makes it "${first.flavour}". Nothing was written. Re-run with --flavour ${first.flavour}, or adopt from the module you actually meant.`;
|
|
237
|
+
}
|
|
238
|
+
|
|
157
239
|
// src/shared/resolve-bin.ts
|
|
158
|
-
import { existsSync as
|
|
159
|
-
import { dirname as dirname2, join as
|
|
240
|
+
import { existsSync as existsSync3 } from "fs";
|
|
241
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
160
242
|
function resolveBin(fromDir, name) {
|
|
161
243
|
let dir = fromDir;
|
|
162
244
|
for (; ; ) {
|
|
163
|
-
const candidate =
|
|
164
|
-
if (
|
|
245
|
+
const candidate = join3(dir, "node_modules", ".bin", name);
|
|
246
|
+
if (existsSync3(candidate)) return candidate;
|
|
165
247
|
const parent = dirname2(dir);
|
|
166
248
|
if (parent === dir) return void 0;
|
|
167
249
|
dir = parent;
|
|
@@ -198,18 +280,18 @@ function hasShippedPreset(flavour) {
|
|
|
198
280
|
}
|
|
199
281
|
|
|
200
282
|
// src/roles/typescript/read-adoption.ts
|
|
201
|
-
import { existsSync as
|
|
202
|
-
import { join as
|
|
283
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
|
|
284
|
+
import { join as join5 } from "path";
|
|
203
285
|
|
|
204
286
|
// src/roles/typescript/resolve-tsconfig-target.ts
|
|
205
|
-
import { existsSync as
|
|
206
|
-
import { join as
|
|
287
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
|
|
288
|
+
import { join as join4 } from "path";
|
|
207
289
|
var TARGET_EXTENDS_MARKERS = ["tsconfig.base.json", "@hublo/sentinel/tsconfig/"];
|
|
208
290
|
var CANDIDATES = ["tsconfig.app.json", "tsconfig.json"];
|
|
209
291
|
function readExtends(absolutePath) {
|
|
210
292
|
let parsed;
|
|
211
293
|
try {
|
|
212
|
-
parsed = parseJsonc(
|
|
294
|
+
parsed = parseJsonc(readFileSync3(absolutePath, "utf8"), absolutePath);
|
|
213
295
|
} catch {
|
|
214
296
|
return [];
|
|
215
297
|
}
|
|
@@ -221,11 +303,15 @@ function readExtends(absolutePath) {
|
|
|
221
303
|
}
|
|
222
304
|
function resolveTsconfigTarget(moduleDir) {
|
|
223
305
|
let existing;
|
|
306
|
+
let existingExtendsSomething = false;
|
|
224
307
|
for (const candidate of CANDIDATES) {
|
|
225
|
-
const absolutePath =
|
|
226
|
-
if (!
|
|
227
|
-
existing ??= candidate;
|
|
308
|
+
const absolutePath = join4(moduleDir, candidate);
|
|
309
|
+
if (!existsSync4(absolutePath)) continue;
|
|
228
310
|
const extendsValues = readExtends(absolutePath);
|
|
311
|
+
if (existing === void 0) {
|
|
312
|
+
existing = candidate;
|
|
313
|
+
existingExtendsSomething = extendsValues.length > 0;
|
|
314
|
+
}
|
|
229
315
|
const extendsBase = extendsValues.some(
|
|
230
316
|
(value) => TARGET_EXTENDS_MARKERS.some((marker) => value.includes(marker))
|
|
231
317
|
);
|
|
@@ -233,7 +319,9 @@ function resolveTsconfigTarget(moduleDir) {
|
|
|
233
319
|
return { path: candidate, reason: "extends-base" };
|
|
234
320
|
}
|
|
235
321
|
}
|
|
236
|
-
if (existing)
|
|
322
|
+
if (existing) {
|
|
323
|
+
return { path: existing, reason: existingExtendsSomething ? "other-chain" : "no-extends" };
|
|
324
|
+
}
|
|
237
325
|
return { path: "tsconfig.json", reason: "none" };
|
|
238
326
|
}
|
|
239
327
|
|
|
@@ -254,12 +342,12 @@ var NOT_ADOPTED = (configFile) => ({
|
|
|
254
342
|
});
|
|
255
343
|
function readTsconfigAdoption(cwd) {
|
|
256
344
|
const target = resolveTsconfigTarget(cwd);
|
|
257
|
-
if (target.reason === "none" || !
|
|
345
|
+
if (target.reason === "none" || !existsSync5(join5(cwd, target.path))) {
|
|
258
346
|
return NOT_ADOPTED(target.reason === "none" ? null : target.path);
|
|
259
347
|
}
|
|
260
348
|
let parsed;
|
|
261
349
|
try {
|
|
262
|
-
parsed = parseJsonc(
|
|
350
|
+
parsed = parseJsonc(readFileSync4(join5(cwd, target.path), "utf8"), target.path);
|
|
263
351
|
} catch {
|
|
264
352
|
return NOT_ADOPTED(target.path);
|
|
265
353
|
}
|
|
@@ -342,6 +430,17 @@ var TscAdapter = class extends BaseAdapter {
|
|
|
342
430
|
blocked: `no TypeScript preset for flavour "${context.flavour}" yet (shipped: ${SHIPPED_FLAVOURS.join(", ")}). Nothing was written; this module cannot adopt the TypeScript preset until that flavour ships.`
|
|
343
431
|
};
|
|
344
432
|
}
|
|
433
|
+
const current = this.declaredFlavour(context.cwd);
|
|
434
|
+
if (current !== void 0 && current !== context.flavour) {
|
|
435
|
+
return {
|
|
436
|
+
operations: [],
|
|
437
|
+
blocked: `this module is already adopted as "${current}", and --flavour says "${context.flavour}". Nothing was written. If the change is deliberate, remove the sentinel preset from the tsconfig \`extends\` chain and re-run; otherwise re-run with --flavour ${current}.`
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
const contradiction = flavourContradiction(context.flavour, context.cwd);
|
|
441
|
+
if (contradiction !== void 0) {
|
|
442
|
+
return { operations: [], blocked: contradiction };
|
|
443
|
+
}
|
|
345
444
|
const target = resolveTsconfigTarget(context.cwd);
|
|
346
445
|
const preset = `@hublo/sentinel/tsconfig/${context.flavour}`;
|
|
347
446
|
const addScript = this.packageJsonOperation(context.cwd);
|
|
@@ -350,6 +449,12 @@ var TscAdapter = class extends BaseAdapter {
|
|
|
350
449
|
operations: [],
|
|
351
450
|
blocked: `${target.path} extends a config chain sentinel does not handle, so nothing was written. Point it at the workspace base (or a plain config) and re-run.`
|
|
352
451
|
};
|
|
452
|
+
if (target.reason === "no-extends") {
|
|
453
|
+
return {
|
|
454
|
+
operations: [],
|
|
455
|
+
blocked: `${target.path} extends nothing, so it defines its own complete set of compilerOptions and sentinel will not silently take ownership of them. Nothing was written. Make it extend the workspace base and re-run, and --init will then move the shared options into the preset.`
|
|
456
|
+
};
|
|
457
|
+
}
|
|
353
458
|
}
|
|
354
459
|
if (target.reason === "none") {
|
|
355
460
|
const contents = JSON.stringify({ extends: preset, include: ["src"] }, null, 2) + "\n";
|
|
@@ -359,7 +464,7 @@ var TscAdapter = class extends BaseAdapter {
|
|
|
359
464
|
};
|
|
360
465
|
}
|
|
361
466
|
const existing = parseJsonc(
|
|
362
|
-
|
|
467
|
+
readFileSync5(join6(context.cwd, target.path), "utf8"),
|
|
363
468
|
target.path
|
|
364
469
|
);
|
|
365
470
|
const extendsChain = composeExtends(existing.extends, preset);
|
|
@@ -394,7 +499,7 @@ var TscAdapter = class extends BaseAdapter {
|
|
|
394
499
|
packageJsonOperation(cwd) {
|
|
395
500
|
const own = readOwnPackage();
|
|
396
501
|
const devDependencies = { [own.name]: own.version };
|
|
397
|
-
if (
|
|
502
|
+
if (existsSync6(join6(cwd, "package.json"))) {
|
|
398
503
|
return {
|
|
399
504
|
kind: "merge-json",
|
|
400
505
|
path: "package.json",
|
|
@@ -442,7 +547,7 @@ var TscAdapter = class extends BaseAdapter {
|
|
|
442
547
|
* check.
|
|
443
548
|
*/
|
|
444
549
|
typecheckTarget(cwd) {
|
|
445
|
-
if (
|
|
550
|
+
if (existsSync6(join6(cwd, "tsconfig.json"))) return "tsconfig.json";
|
|
446
551
|
const target = resolveTsconfigTarget(cwd);
|
|
447
552
|
return target.reason === "none" ? null : target.path;
|
|
448
553
|
}
|
|
@@ -626,7 +731,7 @@ function diffLines(before, after) {
|
|
|
626
731
|
}
|
|
627
732
|
|
|
628
733
|
// src/core/apply-plan.ts
|
|
629
|
-
import { existsSync as
|
|
734
|
+
import { existsSync as existsSync7, readFileSync as readFileSync6, renameSync, writeFileSync } from "fs";
|
|
630
735
|
import { resolve as resolve2, sep } from "path";
|
|
631
736
|
import { applyEdits, modify } from "jsonc-parser";
|
|
632
737
|
|
|
@@ -645,7 +750,7 @@ function resolveWithinRoot(cwd, relativePath) {
|
|
|
645
750
|
return absolutePath;
|
|
646
751
|
}
|
|
647
752
|
function readIfExists(absolutePath) {
|
|
648
|
-
return
|
|
753
|
+
return existsSync7(absolutePath) ? readFileSync6(absolutePath, "utf8") : void 0;
|
|
649
754
|
}
|
|
650
755
|
function* leaves(value, prefix = []) {
|
|
651
756
|
for (const [key, keyValue] of Object.entries(value)) {
|
|
@@ -815,4 +920,4 @@ export {
|
|
|
815
920
|
detectFramework,
|
|
816
921
|
dispatch
|
|
817
922
|
};
|
|
818
|
-
//# sourceMappingURL=chunk-
|
|
923
|
+
//# sourceMappingURL=chunk-BN3TB6TF.js.map
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hublo/sentinel",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
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",
|