@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.
@@ -14,7 +14,7 @@ import {
14
14
  registerAdapters,
15
15
  resolve,
16
16
  resolveBin
17
- } from "../chunk-SK6E5EM4.js";
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
- process.exit(exitCode);
623
+ await exitWithoutTruncating(exitCode);
618
624
  }
619
625
  main().catch((error) => {
620
626
  process.stderr.write(`
621
627
  sentinel: ${asMessage(error)}
622
628
  `);
623
- process.exit(1);
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 existsSync5, readFileSync as readFileSync4 } from "fs";
143
- import { basename, join as join5 } from "path";
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 existsSync2 } from "fs";
159
- import { dirname as dirname2, join as join2 } from "path";
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 = join2(dir, "node_modules", ".bin", name);
164
- if (existsSync2(candidate)) return candidate;
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 existsSync4, readFileSync as readFileSync3 } from "fs";
202
- import { join as join4 } from "path";
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 existsSync3, readFileSync as readFileSync2 } from "fs";
206
- import { join as join3 } from "path";
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(readFileSync2(absolutePath, "utf8"), absolutePath);
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 = join3(moduleDir, candidate);
226
- if (!existsSync3(absolutePath)) continue;
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) return { path: existing, reason: "other-chain" };
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" || !existsSync4(join4(cwd, target.path))) {
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(readFileSync3(join4(cwd, target.path), "utf8"), target.path);
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
- readFileSync4(join5(context.cwd, target.path), "utf8"),
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 (existsSync5(join5(cwd, "package.json"))) {
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 (existsSync5(join5(cwd, "tsconfig.json"))) return "tsconfig.json";
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 existsSync6, readFileSync as readFileSync5, renameSync, writeFileSync } from "fs";
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 existsSync6(absolutePath) ? readFileSync5(absolutePath, "utf8") : void 0;
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-SK6E5EM4.js.map
923
+ //# sourceMappingURL=chunk-BN3TB6TF.js.map
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  registerAdapters,
8
8
  resolve,
9
9
  setDefaultRunner
10
- } from "./chunk-SK6E5EM4.js";
10
+ } from "./chunk-BN3TB6TF.js";
11
11
  export {
12
12
  BaseAdapter,
13
13
  all,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hublo/sentinel",
3
- "version": "1.0.2",
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",