technical-debt-radar 1.7.0 → 1.8.0

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.
Files changed (2) hide show
  1. package/dist/index.js +152 -14
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -12889,6 +12889,7 @@ var require_boundary_checker = __commonJS({
12889
12889
  Object.defineProperty(exports2, "__esModule", { value: true });
12890
12890
  exports2.checkBoundaries = checkBoundaries;
12891
12891
  exports2._resetPathAliasCache = _resetPathAliasCache;
12892
+ exports2._resetModuleParsingCache = _resetModuleParsingCache;
12892
12893
  var shared_1 = require_dist();
12893
12894
  var minimatch_1 = require_commonjs3();
12894
12895
  var ts_morph_1 = require("ts-morph");
@@ -13222,6 +13223,12 @@ var require_boundary_checker = __commonJS({
13222
13223
  continue;
13223
13224
  if (typeOnly)
13224
13225
  continue;
13226
+ if (isNestJS) {
13227
+ const importedNames = getImportedNamesFromSpecifier(sourceFile, specifier);
13228
+ const suppressed = importedNames.length > 0 && importedNames.every((name) => isLegitimateNestJSModuleImport(sourceModule, targetModule, name, input));
13229
+ if (suppressed)
13230
+ continue;
13231
+ }
13225
13232
  violations.push({
13226
13233
  category: "architecture",
13227
13234
  type: "module-boundary-violation",
@@ -13461,6 +13468,117 @@ var require_boundary_checker = __commonJS({
13461
13468
  function _resetPathAliasCache() {
13462
13469
  _cachedAliases = void 0;
13463
13470
  }
13471
+ var _cachedModuleParsing;
13472
+ function parseNestJSModules(input) {
13473
+ if (_cachedModuleParsing)
13474
+ return _cachedModuleParsing;
13475
+ const result = /* @__PURE__ */ new Map();
13476
+ const project = new ts_morph_1.Project({ useInMemoryFileSystem: true });
13477
+ for (const file of input.changedFiles) {
13478
+ if (file.status === "deleted")
13479
+ continue;
13480
+ if (!/\.module\.(ts|js)$/.test(file.path))
13481
+ continue;
13482
+ const sf = project.createSourceFile(file.path, file.content);
13483
+ for (const cls of sf.getClasses()) {
13484
+ const moduleDecorator = cls.getDecorators().find((d) => d.getName() === "Module");
13485
+ if (!moduleDecorator)
13486
+ continue;
13487
+ const className = cls.getName() ?? "";
13488
+ const pathMatch = file.path.match(/src\/([^/]+)\//);
13489
+ const moduleName = pathMatch ? pathMatch[1] : "";
13490
+ if (!moduleName)
13491
+ continue;
13492
+ const callExpr = moduleDecorator.getCallExpression?.();
13493
+ if (!callExpr)
13494
+ continue;
13495
+ const args = callExpr.getArguments();
13496
+ if (args.length === 0 || !ts_morph_1.Node.isObjectLiteralExpression(args[0]))
13497
+ continue;
13498
+ const objLit = args[0];
13499
+ const imports = [];
13500
+ const exports3 = [];
13501
+ const hasForwardRef = /* @__PURE__ */ new Map();
13502
+ const importsProp = objLit.getProperty("imports");
13503
+ if (importsProp && ts_morph_1.Node.isPropertyAssignment(importsProp)) {
13504
+ const init = importsProp.getInitializer();
13505
+ if (init && ts_morph_1.Node.isArrayLiteralExpression(init)) {
13506
+ for (const el of init.getElements()) {
13507
+ if (ts_morph_1.Node.isIdentifier(el)) {
13508
+ imports.push(el.getText());
13509
+ hasForwardRef.set(el.getText(), false);
13510
+ } else if (ts_morph_1.Node.isCallExpression(el)) {
13511
+ const callText = el.getText();
13512
+ const fwdMatch = callText.match(/forwardRef\s*\(\s*\(\)\s*=>\s*(\w+)\s*\)/);
13513
+ if (fwdMatch) {
13514
+ imports.push(fwdMatch[1]);
13515
+ hasForwardRef.set(fwdMatch[1], true);
13516
+ } else {
13517
+ const expr = el.getExpression();
13518
+ if (ts_morph_1.Node.isPropertyAccessExpression(expr)) {
13519
+ const obj = expr.getExpression();
13520
+ if (ts_morph_1.Node.isIdentifier(obj)) {
13521
+ imports.push(obj.getText());
13522
+ hasForwardRef.set(obj.getText(), false);
13523
+ }
13524
+ }
13525
+ }
13526
+ }
13527
+ }
13528
+ }
13529
+ }
13530
+ const exportsProp = objLit.getProperty("exports");
13531
+ if (exportsProp && ts_morph_1.Node.isPropertyAssignment(exportsProp)) {
13532
+ const init = exportsProp.getInitializer();
13533
+ if (init && ts_morph_1.Node.isArrayLiteralExpression(init)) {
13534
+ for (const el of init.getElements()) {
13535
+ if (ts_morph_1.Node.isIdentifier(el)) {
13536
+ exports3.push(el.getText());
13537
+ }
13538
+ }
13539
+ }
13540
+ }
13541
+ result.set(moduleName, { moduleName, className, imports, exports: exports3, hasForwardRef });
13542
+ }
13543
+ project.removeSourceFile(sf);
13544
+ }
13545
+ _cachedModuleParsing = result;
13546
+ return result;
13547
+ }
13548
+ function getImportedNamesFromSpecifier(sourceFile, specifier) {
13549
+ const names = [];
13550
+ for (const decl of sourceFile.getImportDeclarations()) {
13551
+ if (decl.getModuleSpecifierValue() !== specifier)
13552
+ continue;
13553
+ const defaultImport = decl.getDefaultImport();
13554
+ if (defaultImport)
13555
+ names.push(defaultImport.getText());
13556
+ for (const named of decl.getNamedImports()) {
13557
+ names.push(named.getName());
13558
+ }
13559
+ }
13560
+ return names;
13561
+ }
13562
+ function isLegitimateNestJSModuleImport(sourceModuleName, targetModuleName, importedClassName, input) {
13563
+ const modules = parseNestJSModules(input);
13564
+ const importingMod = modules.get(sourceModuleName);
13565
+ if (!importingMod)
13566
+ return false;
13567
+ const targetMod = modules.get(targetModuleName);
13568
+ if (!targetMod)
13569
+ return false;
13570
+ const targetClassName = targetMod.className;
13571
+ if (!importingMod.imports.includes(targetClassName))
13572
+ return false;
13573
+ if (importingMod.hasForwardRef.get(targetClassName))
13574
+ return false;
13575
+ if (!targetMod.exports.includes(importedClassName))
13576
+ return false;
13577
+ return true;
13578
+ }
13579
+ function _resetModuleParsingCache() {
13580
+ _cachedModuleParsing = void 0;
13581
+ }
13464
13582
  function normalizeForMatching(filePath) {
13465
13583
  let normalized = filePath.replace(/\\/g, "/");
13466
13584
  if (normalized.startsWith("/")) {
@@ -19209,7 +19327,10 @@ var require_dead_code_detector = __commonJS({
19209
19327
  for (const [otherFilePath, otherText] of allFileTexts) {
19210
19328
  if (otherFilePath === filePath)
19211
19329
  continue;
19212
- if (callPattern.test(otherText)) {
19330
+ if (!callPattern.test(otherText))
19331
+ continue;
19332
+ const importPattern = new RegExp(`import\\s+.*\\b${className}\\b.*from\\s+`);
19333
+ if (importPattern.test(otherText)) {
19213
19334
  hasExternalCall = true;
19214
19335
  break;
19215
19336
  }
@@ -19650,7 +19771,7 @@ var require_orchestrator = __commonJS({
19650
19771
  (0, reliability_detector_1.detectReliabilityIssues)(filteredInput, policy),
19651
19772
  (0, duplication_detector_1.detectDuplication)(filteredInput),
19652
19773
  projectRoot ? (0, missing_tests_detector_1.detectMissingTests)(filteredInput, projectRoot, buildMissingTestsConfig(policy)) : Promise.resolve(null),
19653
- (0, dead_code_detector_1.detectDeadCode)(filteredInput, {}, projectRoot),
19774
+ (0, dead_code_detector_1.detectDeadCode)(filteredInput, { excludeBarrels: false, excludeTypes: false }, projectRoot),
19654
19775
  projectRoot ? Promise.resolve((0, coverage_delta_detector_1.detectCoverageDelta)(projectRoot)) : Promise.resolve(null)
19655
19776
  ]);
19656
19777
  const runtimeViolations = runtimeResult.violations;
@@ -20355,19 +20476,35 @@ var RadarApiClient = class _RadarApiClient {
20355
20476
  return null;
20356
20477
  }
20357
20478
  async fetch(path9, init) {
20358
- const res = await fetch(`${this.apiUrl}${path9}`, {
20359
- ...init,
20360
- headers: {
20361
- "Authorization": `Bearer ${this.token}`,
20362
- "Content-Type": "application/json",
20363
- ...init?.headers
20364
- }
20365
- });
20479
+ const url = `${this.apiUrl}${path9}`;
20480
+ let res;
20481
+ try {
20482
+ res = await fetch(url, {
20483
+ ...init,
20484
+ headers: {
20485
+ "Authorization": `Bearer ${this.token}`,
20486
+ "Content-Type": "application/json",
20487
+ ...init?.headers
20488
+ }
20489
+ });
20490
+ } catch (err) {
20491
+ const cause = err?.cause?.message || err?.cause?.code || "";
20492
+ throw new Error(`Network error connecting to ${url}: ${err.message}${cause ? ` (${cause})` : ""}`);
20493
+ }
20366
20494
  if (!res.ok) {
20367
20495
  const body = await res.text().catch(() => "");
20368
- throw new Error(`API error ${res.status}: ${body}`);
20496
+ throw new Error(`API error ${res.status} on ${path9}: ${body}`);
20497
+ }
20498
+ const contentType = res.headers.get("content-type") ?? "";
20499
+ const text = await res.text();
20500
+ if (!text || text.trim().length === 0) {
20501
+ return void 0;
20502
+ }
20503
+ try {
20504
+ return JSON.parse(text);
20505
+ } catch {
20506
+ return void 0;
20369
20507
  }
20370
- return res.json();
20371
20508
  }
20372
20509
  async verifyToken() {
20373
20510
  return this.fetch("/cli/auth/verify");
@@ -20674,8 +20811,9 @@ async function scanCommand(targetPath, options) {
20674
20811
  }).catch(() => {
20675
20812
  });
20676
20813
  console.log(import_chalk.default.green(" \u2713 Results synced to dashboard: https://www.technicaldebtradar.com/dashboard"));
20677
- } catch {
20678
- console.log(import_chalk.default.yellow(" \u26A0\uFE0F Could not sync results to dashboard. Check your connection."));
20814
+ } catch (err) {
20815
+ const detail = err?.message ? `: ${err.message}` : "";
20816
+ console.log(import_chalk.default.yellow(` \u26A0\uFE0F Could not sync results to dashboard${detail}`));
20679
20817
  }
20680
20818
  }
20681
20819
  if (mode === "warn") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "technical-debt-radar",
3
- "version": "1.7.0",
3
+ "version": "1.8.0",
4
4
  "description": "Stop Node.js production crashes before merge. 47 detection patterns across 5 categories.",
5
5
  "bin": {
6
6
  "radar": "dist/index.js",