dependency-radar 0.3.1 → 0.5.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.
package/dist/cli.js CHANGED
@@ -90,6 +90,234 @@ async function readJsonFile(filePath) {
90
90
  return undefined;
91
91
  }
92
92
  }
93
+ function stripYamlInlineComment(rawLine) {
94
+ let inSingle = false;
95
+ let inDouble = false;
96
+ for (let i = 0; i < rawLine.length; i += 1) {
97
+ const ch = rawLine[i];
98
+ const prev = i > 0 ? rawLine[i - 1] : "";
99
+ if (ch === "'" && !inDouble) {
100
+ inSingle = !inSingle;
101
+ continue;
102
+ }
103
+ if (ch === '"' && !inSingle && prev !== "\\") {
104
+ inDouble = !inDouble;
105
+ continue;
106
+ }
107
+ if (ch === "#" && !inSingle && !inDouble) {
108
+ return rawLine.slice(0, i);
109
+ }
110
+ }
111
+ return rawLine;
112
+ }
113
+ function unquoteYamlScalar(value) {
114
+ const trimmed = value.trim();
115
+ if ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
116
+ (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
117
+ const quote = trimmed[0];
118
+ const inner = trimmed.slice(1, -1);
119
+ return quote === '"'
120
+ ? inner
121
+ .replace(/\\"/g, '"')
122
+ .replace(/\\\\/g, "\\")
123
+ : inner.replace(/''/g, "'");
124
+ }
125
+ return trimmed;
126
+ }
127
+ function parseYamlScalar(value) {
128
+ const normalized = value.trim();
129
+ if (!normalized)
130
+ return "";
131
+ if (normalized === "{}")
132
+ return {};
133
+ if (normalized === "[]")
134
+ return [];
135
+ if (normalized === "null" || normalized === "~")
136
+ return null;
137
+ if (normalized === "true")
138
+ return true;
139
+ if (normalized === "false")
140
+ return false;
141
+ return unquoteYamlScalar(normalized);
142
+ }
143
+ function findYamlMapSeparator(content) {
144
+ let inSingle = false;
145
+ let inDouble = false;
146
+ for (let i = 0; i < content.length; i += 1) {
147
+ const ch = content[i];
148
+ const prev = i > 0 ? content[i - 1] : "";
149
+ if (ch === "'" && !inDouble) {
150
+ inSingle = !inSingle;
151
+ continue;
152
+ }
153
+ if (ch === '"' && !inSingle && prev !== "\\") {
154
+ inDouble = !inDouble;
155
+ continue;
156
+ }
157
+ if (ch !== ":" || inSingle || inDouble)
158
+ continue;
159
+ const next = content[i + 1];
160
+ if (next === undefined || next === " " || next === "\t") {
161
+ return i;
162
+ }
163
+ }
164
+ return -1;
165
+ }
166
+ function toObjectRecord(value) {
167
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
168
+ return undefined;
169
+ }
170
+ return value;
171
+ }
172
+ function mergeRecordObjects(...objects) {
173
+ const merged = {};
174
+ for (const obj of objects) {
175
+ if (!obj)
176
+ continue;
177
+ for (const [key, val] of Object.entries(obj)) {
178
+ merged[key] = val;
179
+ }
180
+ }
181
+ return Object.keys(merged).length > 0 ? merged : undefined;
182
+ }
183
+ function normalizeStringArray(value) {
184
+ if (typeof value === "string") {
185
+ const single = value.trim();
186
+ return single ? [single] : [];
187
+ }
188
+ if (!Array.isArray(value))
189
+ return [];
190
+ return value
191
+ .map((entry) => (typeof entry === "string" ? entry.trim() : ""))
192
+ .filter(Boolean);
193
+ }
194
+ function parseSimpleYaml(yaml) {
195
+ var _a, _b;
196
+ const lines = [];
197
+ for (const rawLine of yaml.split(/\r?\n/)) {
198
+ const noComment = stripYamlInlineComment(rawLine).replace(/\s+$/, "");
199
+ if (!noComment.trim())
200
+ continue;
201
+ const indent = (_b = (_a = noComment.match(/^(\s*)/)) === null || _a === void 0 ? void 0 : _a[1].length) !== null && _b !== void 0 ? _b : 0;
202
+ lines.push({
203
+ indent,
204
+ content: noComment.trim(),
205
+ });
206
+ }
207
+ let index = 0;
208
+ const parseNode = (indentLevel) => {
209
+ if (index >= lines.length)
210
+ return undefined;
211
+ if (lines[index].indent < indentLevel)
212
+ return undefined;
213
+ if (lines[index].indent === indentLevel &&
214
+ lines[index].content.startsWith("- ")) {
215
+ return parseSequence(indentLevel);
216
+ }
217
+ return parseMapping(indentLevel);
218
+ };
219
+ const parseMapping = (indentLevel) => {
220
+ const out = {};
221
+ while (index < lines.length) {
222
+ const line = lines[index];
223
+ if (line.indent < indentLevel)
224
+ break;
225
+ if (line.indent > indentLevel) {
226
+ index += 1;
227
+ continue;
228
+ }
229
+ if (line.content.startsWith("- "))
230
+ break;
231
+ const colonIndex = findYamlMapSeparator(line.content);
232
+ if (colonIndex <= 0) {
233
+ index += 1;
234
+ continue;
235
+ }
236
+ const key = unquoteYamlScalar(line.content.slice(0, colonIndex));
237
+ const valueToken = line.content.slice(colonIndex + 1).trim();
238
+ index += 1;
239
+ if (valueToken) {
240
+ out[key] = parseYamlScalar(valueToken);
241
+ continue;
242
+ }
243
+ if (index < lines.length && lines[index].indent > indentLevel) {
244
+ out[key] = parseNode(lines[index].indent);
245
+ }
246
+ else {
247
+ out[key] = null;
248
+ }
249
+ }
250
+ return out;
251
+ };
252
+ const parseSequence = (indentLevel) => {
253
+ const values = [];
254
+ while (index < lines.length) {
255
+ const line = lines[index];
256
+ if (line.indent < indentLevel)
257
+ break;
258
+ if (line.indent !== indentLevel || !line.content.startsWith("- "))
259
+ break;
260
+ const valueToken = line.content.slice(2).trim();
261
+ index += 1;
262
+ if (valueToken) {
263
+ values.push(parseYamlScalar(valueToken));
264
+ if (index < lines.length && lines[index].indent > indentLevel) {
265
+ // Consume malformed continuation lines to keep parser state stable.
266
+ parseNode(lines[index].indent);
267
+ }
268
+ continue;
269
+ }
270
+ if (index < lines.length && lines[index].indent > indentLevel) {
271
+ values.push(parseNode(lines[index].indent));
272
+ }
273
+ else {
274
+ values.push(null);
275
+ }
276
+ }
277
+ return values;
278
+ };
279
+ const root = parseNode(0);
280
+ return toObjectRecord(root) || {};
281
+ }
282
+ function parsePnpmWorkspacePackagesFallback(yaml) {
283
+ const patterns = [];
284
+ const lines = yaml.split(/\r?\n/);
285
+ let inPackages = false;
286
+ for (const line of lines) {
287
+ const trimmed = line.trim();
288
+ if (!trimmed)
289
+ continue;
290
+ if (/^packages\s*:\s*$/.test(trimmed)) {
291
+ inPackages = true;
292
+ continue;
293
+ }
294
+ if (inPackages) {
295
+ if (/^[A-Za-z0-9_-]+\s*:/.test(trimmed) && !trimmed.startsWith("-")) {
296
+ inPackages = false;
297
+ continue;
298
+ }
299
+ const m = trimmed.match(/^[-]\s*["']?([^"']+)["']?\s*$/);
300
+ if (m && m[1])
301
+ patterns.push(m[1].trim());
302
+ }
303
+ }
304
+ return patterns;
305
+ }
306
+ function parsePnpmWorkspaceFile(yaml) {
307
+ var _a;
308
+ const parsed = parseSimpleYaml(yaml);
309
+ const fromYaml = normalizeStringArray(parsed.packages);
310
+ const fromFallback = fromYaml.length > 0
311
+ ? fromYaml
312
+ : parsePnpmWorkspacePackagesFallback(yaml);
313
+ const topLevelOverrides = toObjectRecord(parsed.overrides);
314
+ const pnpmOverrides = toObjectRecord((_a = toObjectRecord(parsed.pnpm)) === null || _a === void 0 ? void 0 : _a.overrides);
315
+ const overrides = mergeRecordObjects(topLevelOverrides, pnpmOverrides);
316
+ return {
317
+ packages: fromFallback,
318
+ ...(overrides ? { overrides } : {}),
319
+ };
320
+ }
93
321
  async function getToolVersion(tool, cwd) {
94
322
  try {
95
323
  const result = await (0, utils_1.runCommand)(tool, ["--version"], { cwd });
@@ -110,55 +338,45 @@ function compactToolVersions(versions) {
110
338
  }
111
339
  return Object.keys(out).length > 0 ? out : undefined;
112
340
  }
341
+ async function detectYarnPnP(projectPath) {
342
+ if ((await (0, utils_1.pathExists)(path_1.default.join(projectPath, ".pnp.cjs"))) ||
343
+ (await (0, utils_1.pathExists)(path_1.default.join(projectPath, ".pnp.js")))) {
344
+ return true;
345
+ }
346
+ const yarnrc = path_1.default.join(projectPath, ".yarnrc.yml");
347
+ if (!(await (0, utils_1.pathExists)(yarnrc)))
348
+ return false;
349
+ const content = await promises_1.default.readFile(yarnrc, "utf8").catch(() => "");
350
+ return /nodeLinker\s*:\s*pnp\b/.test(content);
351
+ }
113
352
  async function detectWorkspace(projectPath) {
114
353
  const rootPkgPath = path_1.default.join(projectPath, "package.json");
115
354
  const rootPkg = await readJsonFile(rootPkgPath);
116
355
  const inferredManager = inferPackageManager(rootPkg);
356
+ const hasYarnLock = await (0, utils_1.pathExists)(path_1.default.join(projectPath, "yarn.lock"));
357
+ const hasYarnPnp = await detectYarnPnP(projectPath);
117
358
  const pnpmWorkspacePath = path_1.default.join(projectPath, "pnpm-workspace.yaml");
118
359
  const hasPnpmWorkspace = await (0, utils_1.pathExists)(pnpmWorkspacePath);
119
360
  let type = "none";
120
361
  let patterns = [];
362
+ let pnpmWorkspaceOverrides;
121
363
  if (hasPnpmWorkspace) {
122
364
  type = "pnpm";
123
- // very small YAML parser for the only thing we care about: `packages:` list.
124
365
  const yaml = await promises_1.default.readFile(pnpmWorkspacePath, "utf8");
125
- const lines = yaml.split(/\r?\n/);
126
- let inPackages = false;
127
- for (const line of lines) {
128
- const trimmed = line.trim();
129
- if (!trimmed)
130
- continue;
131
- if (/^packages\s*:\s*$/.test(trimmed)) {
132
- inPackages = true;
133
- continue;
134
- }
135
- if (inPackages) {
136
- // stop when we hit a new top-level key
137
- if (/^[A-Za-z0-9_-]+\s*:/.test(trimmed) && !trimmed.startsWith("-")) {
138
- inPackages = false;
139
- continue;
140
- }
141
- const m = trimmed.match(/^[-]\s*["']?([^"']+)["']?\s*$/);
142
- if (m && m[1])
143
- patterns.push(m[1].trim());
144
- }
145
- }
366
+ const workspaceFile = parsePnpmWorkspaceFile(yaml);
367
+ patterns = workspaceFile.packages;
368
+ pnpmWorkspaceOverrides = workspaceFile.overrides;
369
+ }
370
+ if (hasYarnPnp) {
371
+ return { type: "yarn", packagePaths: [] };
146
372
  }
147
373
  // npm/yarn workspaces
148
374
  if (type === "none" && rootPkg && rootPkg.workspaces) {
149
- type = inferredManager || "npm";
375
+ type = inferredManager || (hasYarnLock ? "yarn" : "npm");
150
376
  if (Array.isArray(rootPkg.workspaces))
151
377
  patterns = rootPkg.workspaces;
152
378
  else if (Array.isArray(rootPkg.workspaces.packages))
153
379
  patterns = rootPkg.workspaces.packages;
154
- // try to detect yarn berry pnp (unsupported) later via .yarnrc.yml
155
- const yarnrc = path_1.default.join(projectPath, ".yarnrc.yml");
156
- if (await (0, utils_1.pathExists)(yarnrc)) {
157
- const y = await promises_1.default.readFile(yarnrc, "utf8");
158
- if (/nodeLinker\s*:\s*pnp/.test(y)) {
159
- return { type: "yarn", packagePaths: [] };
160
- }
161
- }
162
380
  }
163
381
  if (type === "none") {
164
382
  return { type: "none", packagePaths: [projectPath] };
@@ -189,7 +407,11 @@ async function detectWorkspace(projectPath) {
189
407
  }
190
408
  }
191
409
  }
192
- return { type, packagePaths: packagePaths.sort() };
410
+ return {
411
+ type,
412
+ packagePaths: packagePaths.sort(),
413
+ ...(pnpmWorkspaceOverrides ? { pnpmWorkspaceOverrides } : {}),
414
+ };
193
415
  }
194
416
  function inferPackageManager(rootPkg) {
195
417
  const raw = typeof (rootPkg === null || rootPkg === void 0 ? void 0 : rootPkg.packageManager) === "string"
@@ -211,13 +433,8 @@ async function detectPackageManager(projectPath, rootPkg, workspaceType) {
211
433
  return inferred;
212
434
  if (workspaceType === "pnpm" || workspaceType === "yarn")
213
435
  return workspaceType;
214
- const yarnrc = path_1.default.join(projectPath, ".yarnrc.yml");
215
- if (await (0, utils_1.pathExists)(yarnrc)) {
216
- const y = await promises_1.default.readFile(yarnrc, "utf8");
217
- if (/nodeLinker\s*:\s*pnp/.test(y)) {
218
- return "yarn";
219
- }
220
- }
436
+ if (await detectYarnPnP(projectPath))
437
+ return "yarn";
221
438
  if (await (0, utils_1.pathExists)(path_1.default.join(projectPath, "node_modules", ".pnpm")))
222
439
  return "pnpm";
223
440
  if (await (0, utils_1.pathExists)(path_1.default.join(projectPath, "node_modules", ".yarn-state.yml")))
@@ -250,20 +467,120 @@ async function readWorkspacePackageMeta(rootPath, packagePaths) {
250
467
  }
251
468
  return out;
252
469
  }
253
- function mergeDepsFromWorkspace(pkgs) {
254
- var _a, _b, _c;
470
+ function isWorkspaceLocalSpecifier(value) {
471
+ if (typeof value !== "string")
472
+ return false;
473
+ const trimmed = value.trim().toLowerCase();
474
+ return (trimmed.startsWith("workspace:") ||
475
+ trimmed.startsWith("link:") ||
476
+ trimmed.startsWith("file:"));
477
+ }
478
+ function normalizeRelativePath(rootPath, packagePath) {
479
+ const relative = path_1.default.relative(rootPath, packagePath);
480
+ const normalized = relative.split(path_1.default.sep).join("/");
481
+ return normalized && normalized.length > 0 ? normalized : ".";
482
+ }
483
+ function readDependencyEntries(source) {
484
+ if (!source || typeof source !== "object")
485
+ return [];
486
+ const entries = [];
487
+ for (const [name, spec] of Object.entries(source)) {
488
+ if (typeof name !== "string" || !name.trim())
489
+ continue;
490
+ if (typeof spec !== "string" || !spec.trim())
491
+ continue;
492
+ entries.push([name, spec.trim()]);
493
+ }
494
+ return entries;
495
+ }
496
+ function isWorkspaceLocalDependency(dependencyName, spec, workspacePackageNames) {
497
+ return workspacePackageNames.has(dependencyName) || isWorkspaceLocalSpecifier(spec);
498
+ }
499
+ function buildWorkspaceClassification(rootPath, packageMetas) {
500
+ var _a, _b, _c, _d, _e;
501
+ const workspacePackageNames = new Set(packageMetas.map((meta) => meta.name));
502
+ const workspacePackageIds = new Set();
503
+ const workspacePackagePaths = new Set();
504
+ const localDependencyNames = new Set();
505
+ const workspacePackages = [];
506
+ for (const meta of packageMetas) {
507
+ const version = typeof ((_a = meta.pkg) === null || _a === void 0 ? void 0 : _a.version) === "string" && meta.pkg.version.trim().length > 0
508
+ ? meta.pkg.version.trim()
509
+ : "workspace";
510
+ workspacePackageIds.add(`${meta.name}@${version}`);
511
+ workspacePackagePaths.add(path_1.default.resolve(meta.path));
512
+ const runtimeExternal = new Set();
513
+ const devExternal = new Set();
514
+ const runtimeEntries = [
515
+ ...readDependencyEntries((_b = meta.pkg) === null || _b === void 0 ? void 0 : _b.dependencies),
516
+ ...readDependencyEntries((_c = meta.pkg) === null || _c === void 0 ? void 0 : _c.optionalDependencies),
517
+ ];
518
+ const devEntries = readDependencyEntries((_d = meta.pkg) === null || _d === void 0 ? void 0 : _d.devDependencies);
519
+ const peerEntries = readDependencyEntries((_e = meta.pkg) === null || _e === void 0 ? void 0 : _e.peerDependencies);
520
+ for (const [depName, spec] of runtimeEntries) {
521
+ if (isWorkspaceLocalDependency(depName, spec, workspacePackageNames)) {
522
+ localDependencyNames.add(depName);
523
+ continue;
524
+ }
525
+ runtimeExternal.add(depName);
526
+ }
527
+ for (const [depName, spec] of devEntries) {
528
+ if (isWorkspaceLocalDependency(depName, spec, workspacePackageNames)) {
529
+ localDependencyNames.add(depName);
530
+ continue;
531
+ }
532
+ devExternal.add(depName);
533
+ }
534
+ for (const [depName, spec] of peerEntries) {
535
+ if (isWorkspaceLocalDependency(depName, spec, workspacePackageNames)) {
536
+ localDependencyNames.add(depName);
537
+ }
538
+ }
539
+ workspacePackages.push({
540
+ name: meta.name,
541
+ relativePath: normalizeRelativePath(rootPath, meta.path),
542
+ directExternal: {
543
+ runtime: runtimeExternal.size,
544
+ dev: devExternal.size,
545
+ },
546
+ });
547
+ }
548
+ workspacePackages.sort((a, b) => {
549
+ const pathCompare = a.relativePath.localeCompare(b.relativePath);
550
+ if (pathCompare !== 0)
551
+ return pathCompare;
552
+ return a.name.localeCompare(b.name);
553
+ });
554
+ return {
555
+ workspacePackages,
556
+ workspacePackageNames,
557
+ workspacePackageIds,
558
+ workspacePackagePaths,
559
+ localDependencyNames,
560
+ };
561
+ }
562
+ function mergeDepsFromWorkspace(pkgs, workspacePackageNames, localDependencyNames) {
563
+ var _a, _b, _c, _d;
255
564
  const merged = {
256
565
  dependencies: {},
257
566
  devDependencies: {},
258
567
  optionalDependencies: {},
568
+ peerDependencies: {},
569
+ };
570
+ const mergeSection = (target, source) => {
571
+ for (const [depName, spec] of readDependencyEntries(source)) {
572
+ if (isWorkspaceLocalDependency(depName, spec, workspacePackageNames)) {
573
+ localDependencyNames.add(depName);
574
+ continue;
575
+ }
576
+ target[depName] = spec;
577
+ }
259
578
  };
260
579
  for (const entry of pkgs) {
261
- const deps = ((_a = entry.pkg) === null || _a === void 0 ? void 0 : _a.dependencies) || {};
262
- const dev = ((_b = entry.pkg) === null || _b === void 0 ? void 0 : _b.devDependencies) || {};
263
- const opt = ((_c = entry.pkg) === null || _c === void 0 ? void 0 : _c.optionalDependencies) || {};
264
- Object.assign(merged.dependencies, deps);
265
- Object.assign(merged.devDependencies, dev);
266
- Object.assign(merged.optionalDependencies, opt);
580
+ mergeSection(merged.dependencies, (_a = entry.pkg) === null || _a === void 0 ? void 0 : _a.dependencies);
581
+ mergeSection(merged.devDependencies, (_b = entry.pkg) === null || _b === void 0 ? void 0 : _b.devDependencies);
582
+ mergeSection(merged.optionalDependencies, (_c = entry.pkg) === null || _c === void 0 ? void 0 : _c.optionalDependencies);
583
+ mergeSection(merged.peerDependencies, (_d = entry.pkg) === null || _d === void 0 ? void 0 : _d.peerDependencies);
267
584
  }
268
585
  return merged;
269
586
  }
@@ -299,21 +616,6 @@ function mergeAuditResults(results) {
299
616
  }
300
617
  return base;
301
618
  }
302
- function collectDeclaredDeps(pkg) {
303
- const out = new Set();
304
- const sections = [
305
- pkg === null || pkg === void 0 ? void 0 : pkg.dependencies,
306
- pkg === null || pkg === void 0 ? void 0 : pkg.devDependencies,
307
- pkg === null || pkg === void 0 ? void 0 : pkg.optionalDependencies,
308
- pkg === null || pkg === void 0 ? void 0 : pkg.peerDependencies,
309
- ];
310
- for (const deps of sections) {
311
- if (deps && typeof deps === "object") {
312
- Object.keys(deps).forEach((name) => out.add(name));
313
- }
314
- }
315
- return Array.from(out);
316
- }
317
619
  function parseOutdatedData(data, unknownNames) {
318
620
  const entries = [];
319
621
  if (!data || typeof data !== "object")
@@ -513,12 +815,16 @@ function mergeImportGraphs(rootPath, packageMetas, graphs) {
513
815
  }
514
816
  return { files, packages, packageCounts, unresolvedImports };
515
817
  }
516
- function buildWorkspaceUsageMap(packageMetas, dependencyGraphs) {
818
+ function buildWorkspaceUsageMap(packageMetas, dependencyGraphs, workspacePackageNames, localDependencyNames) {
517
819
  var _a, _b, _c, _d;
518
820
  const usage = new Map();
519
821
  const add = (depName, pkgName) => {
520
822
  if (!depName)
521
823
  return;
824
+ if (workspacePackageNames.has(depName))
825
+ return;
826
+ if (localDependencyNames.has(depName))
827
+ return;
522
828
  if (!usage.has(depName))
523
829
  usage.set(depName, new Set());
524
830
  usage.get(depName).add(pkgName);
@@ -687,6 +993,14 @@ function openInBrowser(filePath) {
687
993
  });
688
994
  child.unref();
689
995
  }
996
+ /**
997
+ * Orchestrates the CLI "scan" command to collect, merge, and output dependency data for a project or workspace.
998
+ *
999
+ * Detects workspace type and package manager, runs per-package collectors (audit, dependency tree, import graph, outdated),
1000
+ * merges collected signals into a workspace-level model, and writes a JSON or HTML report to the configured output path.
1001
+ * Manages a temporary working directory (created under the project as .dependency-radar), respects CLI options such as
1002
+ * JSON output, audit/outdated toggles, keeping the temp directory, and optionally opening the generated output with the
1003
+ * system default application. Exits the process with a non-zero code on fatal errors. */
690
1004
  async function run() {
691
1005
  var _a;
692
1006
  const opts = parseArgs(process.argv.slice(2));
@@ -717,15 +1031,32 @@ async function run() {
717
1031
  // ignore, best-effort path normalization
718
1032
  }
719
1033
  const tempDir = path_1.default.join(projectPath, ".dependency-radar");
720
- // Workspace detection and reporting
1034
+ // Stage 1: detect workspace/package-manager context and collect tool versions.
721
1035
  const workspace = await detectWorkspace(projectPath);
1036
+ const yarnPnP = await detectYarnPnP(projectPath);
722
1037
  if (workspace.type === "yarn" && workspace.packagePaths.length === 0) {
723
1038
  console.error("Yarn Plug'n'Play (nodeLinker: pnp) detected. This is not supported yet.");
724
1039
  console.error("Switch to nodeLinker: node-modules or run in a non-PnP environment.");
725
1040
  process.exit(1);
726
1041
  return;
727
1042
  }
1043
+ const hasProjectNodeModules = await (0, utils_1.pathExists)(path_1.default.join(projectPath, "node_modules"));
1044
+ if (!hasProjectNodeModules) {
1045
+ const workspaceHint = workspace.type === "none"
1046
+ ? "single project"
1047
+ : `${workspace.type.toUpperCase()} workspace`;
1048
+ const yarnHint = yarnPnP
1049
+ ? " Yarn Plug'n'Play appears enabled; Dependency Radar currently requires node_modules linker."
1050
+ : "";
1051
+ console.warn(`⚠ node_modules was not found at ${projectPath}. Scan completeness may be reduced for this ${workspaceHint}. Run your package manager install (npm install, pnpm install, or yarn install) before scanning.${yarnHint}`);
1052
+ }
728
1053
  const rootPkg = await readJsonFile(path_1.default.join(projectPath, "package.json"));
1054
+ const projectDependencyPolicy = workspace.pnpmWorkspaceOverrides
1055
+ ? {
1056
+ overrides: workspace.pnpmWorkspaceOverrides,
1057
+ sources: ["pnpm-workspace.yaml#overrides"],
1058
+ }
1059
+ : undefined;
729
1060
  const packageManager = await detectPackageManager(projectPath, rootPkg, workspace.type);
730
1061
  const scanManager = await detectScanManager(projectPath, packageManager);
731
1062
  const packageManagerField = typeof (rootPkg === null || rootPkg === void 0 ? void 0 : rootPkg.packageManager) === "string"
@@ -746,17 +1077,11 @@ async function run() {
746
1077
  : scanManager === "pnpm"
747
1078
  ? pnpmVersion
748
1079
  : yarnVersion;
749
- if (packageManager === "yarn") {
750
- const yarnrc = path_1.default.join(projectPath, ".yarnrc.yml");
751
- if (await (0, utils_1.pathExists)(yarnrc)) {
752
- const y = await promises_1.default.readFile(yarnrc, "utf8");
753
- if (/nodeLinker\s*:\s*pnp/.test(y)) {
754
- console.error("Yarn Plug'n'Play (nodeLinker: pnp) detected. This is not supported yet.");
755
- console.error("Switch to nodeLinker: node-modules or run in a non-PnP environment.");
756
- process.exit(1);
757
- return;
758
- }
759
- }
1080
+ if (packageManager === "yarn" && yarnPnP) {
1081
+ console.error("Yarn Plug'n'Play (nodeLinker: pnp) detected. This is not supported yet.");
1082
+ console.error("Switch to nodeLinker: node-modules or run in a non-PnP environment.");
1083
+ process.exit(1);
1084
+ return;
760
1085
  }
761
1086
  const packagePaths = workspace.packagePaths;
762
1087
  const workspaceLabel = workspace.type === "none"
@@ -769,8 +1094,9 @@ async function run() {
769
1094
  const spinner = startSpinner(`Scanning ${workspaceLabel} at ${projectPath}`);
770
1095
  try {
771
1096
  await (0, utils_1.ensureDir)(tempDir);
772
- // Run tools per package for best coverage.
1097
+ // Stage 2: run per-package collectors and persist raw tool outputs.
773
1098
  const packageMetas = await readWorkspacePackageMeta(projectPath, packagePaths);
1099
+ const workspaceClassification = buildWorkspaceClassification(projectPath, packageMetas);
774
1100
  const perPackageAudit = [];
775
1101
  const perPackageLs = [];
776
1102
  const perPackageImportGraph = [];
@@ -785,6 +1111,7 @@ async function run() {
785
1111
  : Promise.resolve(undefined),
786
1112
  (0, npmLs_1.runNpmLs)(meta.path, pkgTempDir, scanManager, {
787
1113
  contextLabel: meta.name,
1114
+ lockfileSearchRoot: projectPath,
788
1115
  onProgress: (line) => spinner.log(line),
789
1116
  }).catch((err) => ({ ok: false, error: String(err) })),
790
1117
  (0, importGraphRunner_1.runImportGraph)(meta.path, pkgTempDir).catch((err) => ({ ok: false, error: String(err) })),
@@ -797,6 +1124,7 @@ async function run() {
797
1124
  perPackageImportGraph.push(ig);
798
1125
  perPackageOutdated.push({ attempted: Boolean(opts.outdated), result: o });
799
1126
  }
1127
+ // Stage 3: merge per-package results into a workspace-level view.
800
1128
  if (opts.audit) {
801
1129
  const auditOk = perPackageAudit.every((r) => r && r.ok);
802
1130
  if (auditOk) {
@@ -822,7 +1150,7 @@ async function run() {
822
1150
  : undefined
823
1151
  : buildCombinedDependencyGraph(projectPath, packageMetas, perPackageLs.map((r) => (r && r.ok ? r.data : undefined)));
824
1152
  const mergedImportGraphData = mergeImportGraphs(projectPath, packageMetas, perPackageImportGraph.map((r) => (r && r.ok ? r.data : undefined)));
825
- const workspaceUsage = buildWorkspaceUsageMap(packageMetas, perPackageLs.map((r) => (r && r.ok ? r.data : undefined)));
1153
+ const workspaceUsage = buildWorkspaceUsageMap(packageMetas, perPackageLs.map((r) => (r && r.ok ? r.data : undefined)), workspaceClassification.workspacePackageNames, workspaceClassification.localDependencyNames);
826
1154
  const outdatedResult = mergeOutdatedResults(perPackageOutdated);
827
1155
  const auditResult = mergedAuditData
828
1156
  ? { ok: true, data: mergedAuditData }
@@ -830,7 +1158,7 @@ async function run() {
830
1158
  const npmLsResult = { ok: true, data: mergedGraphData };
831
1159
  const importGraphResult = { ok: true, data: mergedImportGraphData };
832
1160
  // Build a merged package.json view for aggregator direct-dep checks.
833
- const mergedPkgForAggregator = mergeDepsFromWorkspace(packageMetas);
1161
+ const mergedPkgForAggregator = mergeDepsFromWorkspace(packageMetas, workspaceClassification.workspacePackageNames, workspaceClassification.localDependencyNames);
834
1162
  const auditFailure = opts.audit
835
1163
  ? perPackageAudit.find((r) => r && !r.ok)
836
1164
  : undefined;
@@ -849,6 +1177,7 @@ async function run() {
849
1177
  if (importFailures.length > 0) {
850
1178
  spinner.log(`Import graph warning: ${importFailures.length} package${importFailures.length === 1 ? "" : "s"} failed (${importFailures[0].error || "import graph failed"})`);
851
1179
  }
1180
+ // Stage 4: aggregate all signals into the final report model.
852
1181
  const aggregated = await (0, aggregator_1.aggregateData)({
853
1182
  projectPath,
854
1183
  auditResult,
@@ -856,6 +1185,8 @@ async function run() {
856
1185
  importGraphResult,
857
1186
  outdatedResult,
858
1187
  pkgOverride: mergedPkgForAggregator,
1188
+ projectPackageJson: rootPkg,
1189
+ ...(projectDependencyPolicy ? { projectDependencyPolicy } : {}),
859
1190
  workspaceUsage,
860
1191
  resolvePaths: [
861
1192
  projectPath,
@@ -864,6 +1195,15 @@ async function run() {
864
1195
  workspaceEnabled: workspace.type !== "none",
865
1196
  workspaceType: workspace.type,
866
1197
  workspacePackageCount: packagePaths.length,
1198
+ ...(workspace.type !== "none"
1199
+ ? {
1200
+ workspacePackages: workspaceClassification.workspacePackages,
1201
+ workspacePackageNames: workspaceClassification.workspacePackageNames,
1202
+ workspacePackageIds: workspaceClassification.workspacePackageIds,
1203
+ workspacePackagePaths: workspaceClassification.workspacePackagePaths,
1204
+ workspaceLocalDependencyNames: workspaceClassification.localDependencyNames,
1205
+ }
1206
+ : {}),
867
1207
  packageManager: scanManager,
868
1208
  packageManagerVersion,
869
1209
  packageManagerField,
package/dist/cta.js ADDED
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CTA_BASE_URL = void 0;
4
+ exports.buildCtaUrl = buildCtaUrl;
5
+ exports.CTA_BASE_URL = 'https://dependency-radar.com/next-steps?source=standalone-report';
6
+ /**
7
+ * Builds a CTA URL that includes the CLI version as a `cli` query parameter.
8
+ *
9
+ * @param version - CLI version string to include; when `undefined`, empty, or whitespace-only, `unknown` is used
10
+ * @returns The CTA URL with an appended `cli` query parameter whose value is the URI-encoded CLI version (or `unknown` when version is missing or empty)
11
+ */
12
+ function buildCtaUrl(version) {
13
+ const normalizedVersion = typeof version === 'string' && version.trim().length > 0
14
+ ? version.trim()
15
+ : 'unknown';
16
+ return `${exports.CTA_BASE_URL}&cli=${encodeURIComponent(normalizedVersion)}`;
17
+ }