@rsdk/depdoc.cli 6.0.0-next.42 → 6.0.0-next.44

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 (46) hide show
  1. package/DEPDOC_MODEL.md +22 -11
  2. package/__tests__/compatibility.test.ts +74 -15
  3. package/__tests__/config-validation.test.ts +46 -1
  4. package/__tests__/engine.test.ts +162 -2
  5. package/__tests__/fixtures/imports/config/aliases.json +8 -0
  6. package/__tests__/fixtures/imports/plain-root-file.ts +3 -0
  7. package/__tests__/fixtures/imports/sidebars.ts +5 -0
  8. package/__tests__/fixtures/imports/src/aliases.ts +4 -0
  9. package/__tests__/fixtures/imports/src/virtual.ts +3 -0
  10. package/__tests__/fixtures/imports/tsconfig.build.json +3 -0
  11. package/__tests__/fixtures/imports/tsconfig.json +8 -0
  12. package/__tests__/fixtures/imports/vite.config.ts +3 -0
  13. package/__tests__/imports.test.ts +72 -0
  14. package/dist/collectors/tsconfig-aliases.d.ts +1 -0
  15. package/dist/collectors/tsconfig-aliases.js +40 -0
  16. package/dist/collectors/tsconfig-aliases.js.map +1 -0
  17. package/dist/collectors/workspaces.d.ts +2 -2
  18. package/dist/collectors/workspaces.js +21 -6
  19. package/dist/collectors/workspaces.js.map +1 -1
  20. package/dist/lib/imports.d.ts +9 -4
  21. package/dist/lib/imports.js +62 -9
  22. package/dist/lib/imports.js.map +1 -1
  23. package/dist/model/config-validation.d.ts +2 -0
  24. package/dist/model/config-validation.js +44 -1
  25. package/dist/model/config-validation.js.map +1 -1
  26. package/dist/model/diagnostics.d.ts +1 -0
  27. package/dist/model/diagnostics.js +67 -0
  28. package/dist/model/diagnostics.js.map +1 -1
  29. package/dist/model/engine.js +1 -0
  30. package/dist/model/engine.js.map +1 -1
  31. package/dist/model/placement.js +12 -1
  32. package/dist/model/placement.js.map +1 -1
  33. package/dist/model/types.d.ts +8 -1
  34. package/dist/model/types.js.map +1 -1
  35. package/dist/runner.js +1 -1
  36. package/dist/runner.js.map +1 -1
  37. package/package.json +2 -2
  38. package/src/collectors/tsconfig-aliases.ts +45 -0
  39. package/src/collectors/workspaces.ts +50 -7
  40. package/src/lib/imports.ts +114 -8
  41. package/src/model/config-validation.ts +62 -3
  42. package/src/model/diagnostics.ts +105 -1
  43. package/src/model/engine.ts +7 -1
  44. package/src/model/placement.ts +19 -1
  45. package/src/model/types.ts +16 -0
  46. package/src/runner.ts +6 -1
@@ -16,8 +16,14 @@ export interface DependencyRule {
16
16
  rootOnly?: boolean;
17
17
  required?: boolean;
18
18
  }
19
+ export interface ToolingFileRule {
20
+ match: string | string[];
21
+ workspace?: string | string[];
22
+ }
19
23
  export interface DependencyModelConfig {
20
24
  version?: number;
25
+ ignoredImports?: string[];
26
+ toolingFiles?: ToolingFileRule[];
21
27
  rules?: DependencyRule[];
22
28
  doctor?: DoctorConfig;
23
29
  }
@@ -33,7 +39,7 @@ export interface DependencyModelOptions {
33
39
  withDts?: boolean;
34
40
  }
35
41
  export interface DependencyViolation {
36
- code: 'root-private' | 'root-section' | 'role-missing' | 'role-invalid' | 'forbidden-section' | 'missing' | 'wrong-section' | 'wrong-range' | 'root-only' | 'root-only-usage' | 'unconstrained-version' | 'mirror' | 'stale' | 'dist-missing';
42
+ code: 'root-private' | 'root-section' | 'role-missing' | 'role-invalid' | 'forbidden-section' | 'missing' | 'wrong-section' | 'wrong-range' | 'root-only' | 'root-only-usage' | 'unconstrained-version' | 'stale-rule' | 'mirror' | 'stale' | 'dist-missing';
37
43
  workspace: string;
38
44
  workspaceLocation: string;
39
45
  dependency?: string | undefined;
@@ -66,6 +72,7 @@ export interface WorkspaceFacts {
66
72
  hasSrc: boolean;
67
73
  hasDist: boolean;
68
74
  sourceFileCount?: number;
75
+ toolingFiles?: Set<string>;
69
76
  }
70
77
  export interface WorkspaceContext extends WorkspaceFacts {
71
78
  dir: string;
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/model/types.ts"],"names":[],"mappings":";;;AAgKa,QAAA,QAAQ,GAAkB;IACrC,cAAc;IACd,kBAAkB;IAClB,iBAAiB;CAClB,CAAC"}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/model/types.ts"],"names":[],"mappings":";;;AAgLa,QAAA,QAAQ,GAAkB;IACrC,cAAc;IACd,kBAAkB;IAClB,iBAAiB;CAClB,CAAC"}
package/dist/runner.js CHANGED
@@ -24,7 +24,7 @@ function runDependencyModel(options = {}) {
24
24
  const config = (0, config_1.loadConfig)(rootDir, options.depdocYamlPath);
25
25
  const rules = config.rules ?? [];
26
26
  const withDts = options.withDts === true;
27
- const contexts = (0, workspaces_1.loadWorkspaces)(rootDir, withDts);
27
+ const contexts = (0, workspaces_1.loadWorkspaces)(rootDir, withDts, config.ignoredImports ?? [], config.toolingFiles ?? []);
28
28
  const packageExtensions = (0, package_extensions_1.loadPackageExtensions)(rootDir);
29
29
  const workspaceNames = new Set(contexts.filter((ws) => !ws.isRoot).map((ws) => ws.name));
30
30
  const typeProviderPackages = (0, type_providers_1.collectTypeProviderPackages)(rootDir, contexts[0].pkg, rules);
@@ -1 +1 @@
1
- {"version":3,"file":"runner.js","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"runner.js","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rsdk/depdoc.cli",
3
3
  "role": "cli",
4
- "version": "6.0.0-next.42",
4
+ "version": "6.0.0-next.44",
5
5
  "description": "CLI for monorepo dependency management",
6
6
  "license": "Apache License 2.0",
7
7
  "publishConfig": {
@@ -18,5 +18,5 @@
18
18
  "typescript": "5.7.3",
19
19
  "yaml": "^2.6.1"
20
20
  },
21
- "gitHead": "ec9e809168d3d70d2bf12f93309b6bc5c5334cf3"
21
+ "gitHead": "6cd75fef09ff91a5aa7963717efee5734f9bd6b5"
22
22
  }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * TypeScript path alias collector.
3
+ *
4
+ * Depdoc treats `compilerOptions.paths` entries as internal module specifiers:
5
+ * they are resolved by TypeScript/bundlers, not package manager dependencies.
6
+ */
7
+ import { type Dirent, readdirSync } from 'node:fs';
8
+ import path from 'node:path';
9
+ import ts = require('typescript');
10
+
11
+ const TSCONFIG_RE = /^tsconfig(?:\..*)?\.json$/;
12
+
13
+ export function collectTsConfigPathAliases(workspaceDir: string): string[] {
14
+ const aliases = new Set<string>();
15
+
16
+ let entries: Dirent[];
17
+
18
+ try {
19
+ entries = readdirSync(workspaceDir, { withFileTypes: true });
20
+ } catch {
21
+ return [];
22
+ }
23
+
24
+ for (const entry of entries) {
25
+ if (!entry.isFile() || !TSCONFIG_RE.test(entry.name)) continue;
26
+
27
+ const configPath = path.join(workspaceDir, entry.name);
28
+ const config = ts.readConfigFile(configPath, ts.sys.readFile);
29
+ if (config.error || !config.config) continue;
30
+
31
+ const parsed = ts.parseJsonConfigFileContent(
32
+ config.config,
33
+ ts.sys,
34
+ path.dirname(configPath),
35
+ undefined,
36
+ configPath,
37
+ );
38
+
39
+ for (const alias of Object.keys(parsed.options.paths ?? {})) {
40
+ aliases.add(alias);
41
+ }
42
+ }
43
+
44
+ return [...aliases].sort();
45
+ }
@@ -11,24 +11,60 @@ import path from 'node:path';
11
11
 
12
12
  import {
13
13
  collectDtsImports,
14
+ collectRootConfigFiles,
14
15
  collectSourceFiles,
15
16
  collectSourceImportsFromFiles,
16
17
  } from '../lib/imports';
17
18
  import { readPackageJson } from '../lib/package-json';
18
- import { isWorkspaceRole } from '../model/rules';
19
- import type { WorkspaceContext } from '../model/types';
19
+ import { isWorkspaceRole, matchesPatterns } from '../model/rules';
20
+ import type { ToolingFileRule, WorkspaceContext } from '../model/types';
21
+
22
+ import { collectTsConfigPathAliases } from './tsconfig-aliases';
20
23
 
21
24
  interface YarnWorkspaceInfo {
22
25
  location: string;
23
26
  name: string | null;
24
27
  }
25
28
 
26
- function collectUsage(workspace: WorkspaceContext, withDts: boolean): void {
27
- const sourceFiles = collectSourceFiles(workspace.dir, workspace.pkg);
29
+ function getToolingPatternsForWorkspace(
30
+ toolingFileRules: readonly ToolingFileRule[],
31
+ wsName: string,
32
+ wsLocation: string,
33
+ ): string[] {
34
+ return toolingFileRules
35
+ .filter(
36
+ (rule) =>
37
+ !rule.workspace ||
38
+ matchesPatterns(wsName, rule.workspace) ||
39
+ matchesPatterns(wsLocation, rule.workspace),
40
+ )
41
+ .flatMap((rule) => (Array.isArray(rule.match) ? rule.match : [rule.match]));
42
+ }
43
+
44
+ function collectUsage(
45
+ workspace: WorkspaceContext,
46
+ withDts: boolean,
47
+ ignoredImports: readonly string[],
48
+ toolingFilePatterns: readonly string[],
49
+ ): void {
50
+ const sourceFiles = collectSourceFiles(
51
+ workspace.dir,
52
+ workspace.pkg,
53
+ toolingFilePatterns,
54
+ );
55
+ const ignoredSpecifiers = [
56
+ ...ignoredImports,
57
+ ...collectTsConfigPathAliases(workspace.dir),
58
+ ];
28
59
 
29
60
  workspace.sourceFileCount = sourceFiles.length;
61
+ workspace.toolingFiles = new Set(
62
+ collectRootConfigFiles(workspace.dir, toolingFilePatterns),
63
+ );
30
64
 
31
- for (const entry of collectSourceImportsFromFiles(sourceFiles)) {
65
+ for (const entry of collectSourceImportsFromFiles(sourceFiles, {
66
+ ignoredSpecifiers,
67
+ })) {
32
68
  if (!workspace.sourceUsage.has(entry.packageName)) {
33
69
  workspace.sourceUsage.set(entry.packageName, {
34
70
  files: new Set(),
@@ -48,13 +84,15 @@ function collectUsage(workspace: WorkspaceContext, withDts: boolean): void {
48
84
 
49
85
  const distDir = path.join(workspace.dir, 'dist');
50
86
  if (withDts || existsSync(distDir)) {
51
- workspace.dtsImports = collectDtsImports(distDir);
87
+ workspace.dtsImports = collectDtsImports(distDir, { ignoredSpecifiers });
52
88
  }
53
89
  }
54
90
 
55
91
  export function loadWorkspaces(
56
92
  rootDir: string,
57
93
  withDts: boolean,
94
+ ignoredImports: readonly string[] = [],
95
+ toolingFileRules: readonly ToolingFileRule[] = [],
58
96
  ): WorkspaceContext[] {
59
97
  const rootPkg = readPackageJson(rootDir);
60
98
  const raw = execSync('yarn workspaces list --json', {
@@ -99,7 +137,12 @@ export function loadWorkspaces(
99
137
  hasDist: existsSync(path.join(dir, 'dist')),
100
138
  };
101
139
 
102
- collectUsage(ctx, withDts);
140
+ collectUsage(
141
+ ctx,
142
+ withDts,
143
+ ignoredImports,
144
+ getToolingPatternsForWorkspace(toolingFileRules, ctx.name, ctx.location),
145
+ );
103
146
  workspaces.push(ctx);
104
147
  }
105
148
 
@@ -5,7 +5,7 @@
5
5
  * import facts. They do not decide dependency placement; they only normalize
6
6
  * specifiers and classify imports as runtime or type-only.
7
7
  */
8
- import { existsSync, readdirSync, readFileSync } from 'node:fs';
8
+ import { type Dirent,existsSync, readdirSync, readFileSync } from 'node:fs';
9
9
  import { builtinModules } from 'node:module';
10
10
  import path from 'node:path';
11
11
  import ts = require('typescript');
@@ -18,6 +18,10 @@ export interface ImportEntry {
18
18
  file: string;
19
19
  }
20
20
 
21
+ export interface ImportCollectionOptions {
22
+ ignoredSpecifiers?: readonly string[];
23
+ }
24
+
21
25
  interface ModuleReference {
22
26
  specifier: string;
23
27
  isTypeOnly: boolean;
@@ -41,6 +45,23 @@ const SOURCE_EXTENSIONS = new Set([
41
45
 
42
46
  const DTS_RE = /\.d\.(?:c|m)?ts$/;
43
47
  const DEFAULT_SOURCE_ROOTS = ['src', 'test', 'tests', '__tests__'];
48
+
49
+ // Build-tool config files conventionally live at the workspace root, outside
50
+ // src/, but still import real runtime dependencies (e.g. vite.config.ts,
51
+ // docusaurus.config.ts). They are scanned non-recursively so unrelated root
52
+ // files aren't picked up. Filenames that don't follow this convention (e.g.
53
+ // Docusaurus' sidebars.ts) aren't matched by default — declare them via
54
+ // depdoc.yml's `toolingFiles` instead.
55
+ const ROOT_CONFIG_FILE_SUFFIXES = [
56
+ '.config.ts',
57
+ '.config.tsx',
58
+ '.config.js',
59
+ '.config.jsx',
60
+ '.config.mjs',
61
+ '.config.cjs',
62
+ '.config.mts',
63
+ '.config.cts',
64
+ ];
44
65
  const IGNORED_ENTRYPOINT_DIRS = new Set([
45
66
  'node_modules',
46
67
  'dist',
@@ -74,6 +95,30 @@ export function getPackageName(specifier: string): string | null {
74
95
  return slash === -1 ? specifier : specifier.slice(0, slash);
75
96
  }
76
97
 
98
+ function globToRegExp(pattern: string): RegExp {
99
+ const escaped = pattern.replaceAll(
100
+ /[-[\]{}()+?.,\\^$|#\s]/g,
101
+ String.raw`\$&`,
102
+ );
103
+
104
+ return new RegExp(`^${escaped.replaceAll('*', '.*')}$`);
105
+ }
106
+
107
+ function matchesSpecifierPattern(specifier: string, pattern: string): boolean {
108
+ return pattern.includes('*')
109
+ ? globToRegExp(pattern).test(specifier)
110
+ : specifier === pattern;
111
+ }
112
+
113
+ function isIgnoredSpecifier(
114
+ specifier: string,
115
+ options: ImportCollectionOptions,
116
+ ): boolean {
117
+ return (options.ignoredSpecifiers ?? []).some((pattern) =>
118
+ matchesSpecifierPattern(specifier, pattern),
119
+ );
120
+ }
121
+
77
122
  function walkFiles(dir: string, filter: (name: string) => boolean): string[] {
78
123
  const results: string[] = [];
79
124
 
@@ -133,9 +178,12 @@ function addImport(
133
178
  file: string,
134
179
  specifier: string,
135
180
  isTypeOnly: boolean,
181
+ options: ImportCollectionOptions,
136
182
  ): void {
183
+ if (isIgnoredSpecifier(specifier, options)) return;
137
184
  const packageName = getPackageName(specifier);
138
185
  if (!packageName) return;
186
+ if (isIgnoredSpecifier(packageName, options)) return;
139
187
  results.push({ packageName, isTypeOnly, file });
140
188
  }
141
189
 
@@ -202,13 +250,16 @@ function extractModuleReferences(
202
250
  const visit = (node: ts.Node): void => {
203
251
  if (ts.isImportDeclaration(node)) {
204
252
  const specifier = stringLiteralText(node.moduleSpecifier);
253
+
205
254
  addModuleReference(results, specifier, isImportDeclarationTypeOnly(node));
206
255
  } else if (ts.isExportDeclaration(node)) {
207
256
  const specifier = stringLiteralText(node.moduleSpecifier);
257
+
208
258
  addModuleReference(results, specifier, isExportDeclarationTypeOnly(node));
209
259
  } else if (ts.isImportEqualsDeclaration(node)) {
210
260
  if (ts.isExternalModuleReference(node.moduleReference)) {
211
261
  const specifier = collectExternalModuleReference(node.moduleReference);
262
+
212
263
  addModuleReference(results, specifier, node.isTypeOnly);
213
264
  }
214
265
  } else if (ts.isCallExpression(node)) {
@@ -217,6 +268,7 @@ function extractModuleReferences(
217
268
  node.arguments.length === 1
218
269
  ) {
219
270
  const specifier = stringLiteralText(node.arguments[0]);
271
+
220
272
  addModuleReference(results, specifier, false);
221
273
  } else if (
222
274
  ts.isIdentifier(node.expression) &&
@@ -224,12 +276,14 @@ function extractModuleReferences(
224
276
  node.arguments.length === 1
225
277
  ) {
226
278
  const specifier = stringLiteralText(node.arguments[0]);
279
+
227
280
  addModuleReference(results, specifier, false);
228
281
  }
229
282
  } else if (ts.isImportTypeNode(node)) {
230
283
  const argument = node.argument;
231
284
  if (ts.isLiteralTypeNode(argument)) {
232
285
  const specifier = stringLiteralText(argument.literal);
286
+
233
287
  addModuleReference(results, specifier, true);
234
288
  }
235
289
  } else if (
@@ -250,12 +304,18 @@ function extractModuleReferences(
250
304
  function extractImports(
251
305
  file: string,
252
306
  content: string,
253
- options: { includeDtsForms?: boolean } = {},
307
+ options: ImportCollectionOptions & { includeDtsForms?: boolean } = {},
254
308
  ): ImportEntry[] {
255
309
  const results: ImportEntry[] = [];
256
310
 
257
311
  for (const reference of extractModuleReferences(file, content, options)) {
258
- addImport(results, file, reference.specifier, reference.isTypeOnly);
312
+ addImport(
313
+ results,
314
+ file,
315
+ reference.specifier,
316
+ reference.isTypeOnly,
317
+ options,
318
+ );
259
319
  }
260
320
 
261
321
  return results;
@@ -379,9 +439,39 @@ function collectEntrypointGraphFiles(
379
439
  return [...result];
380
440
  }
381
441
 
442
+ export function isRootConfigFile(
443
+ name: string,
444
+ extraPatterns: readonly string[] = [],
445
+ ): boolean {
446
+ return (
447
+ ROOT_CONFIG_FILE_SUFFIXES.some((suffix) => name.endsWith(suffix)) ||
448
+ extraPatterns.some((pattern) => matchesSpecifierPattern(name, pattern))
449
+ );
450
+ }
451
+
452
+ export function collectRootConfigFiles(
453
+ workspaceDir: string,
454
+ extraPatterns: readonly string[] = [],
455
+ ): string[] {
456
+ let entries: Dirent[];
457
+
458
+ try {
459
+ entries = readdirSync(workspaceDir, { withFileTypes: true });
460
+ } catch {
461
+ return [];
462
+ }
463
+
464
+ return entries
465
+ .filter(
466
+ (entry) => entry.isFile() && isRootConfigFile(entry.name, extraPatterns),
467
+ )
468
+ .map((entry) => path.join(workspaceDir, entry.name));
469
+ }
470
+
382
471
  export function collectSourceFiles(
383
472
  srcDirOrWorkspaceDir: string,
384
473
  pkg?: PackageJson,
474
+ toolingFilePatterns: readonly string[] = [],
385
475
  ): string[] {
386
476
  if (!pkg) return walkFiles(srcDirOrWorkspaceDir, (name) => isSourceFile(name));
387
477
 
@@ -396,6 +486,13 @@ export function collectSourceFiles(
396
486
  }
397
487
  }
398
488
 
489
+ for (const file of collectRootConfigFiles(
490
+ srcDirOrWorkspaceDir,
491
+ toolingFilePatterns,
492
+ )) {
493
+ files.add(file);
494
+ }
495
+
399
496
  for (const file of collectEntrypointGraphFiles(srcDirOrWorkspaceDir, pkg)) {
400
497
  files.add(file);
401
498
  }
@@ -403,22 +500,31 @@ export function collectSourceFiles(
403
500
  return [...files].sort();
404
501
  }
405
502
 
406
- export function collectSourceImportsFromFiles(files: string[]): ImportEntry[] {
503
+ export function collectSourceImportsFromFiles(
504
+ files: string[],
505
+ options: ImportCollectionOptions = {},
506
+ ): ImportEntry[] {
407
507
  return files.flatMap((file) =>
408
- extractImports(file, readFileSync(file, 'utf8')),
508
+ extractImports(file, readFileSync(file, 'utf8'), options),
409
509
  );
410
510
  }
411
511
 
412
512
  export function collectSourceImports(
413
513
  srcDirOrWorkspaceDir: string,
414
514
  pkg?: PackageJson,
515
+ options: ImportCollectionOptions = {},
516
+ toolingFilePatterns: readonly string[] = [],
415
517
  ): ImportEntry[] {
416
518
  return collectSourceImportsFromFiles(
417
- collectSourceFiles(srcDirOrWorkspaceDir, pkg),
519
+ collectSourceFiles(srcDirOrWorkspaceDir, pkg, toolingFilePatterns),
520
+ options,
418
521
  );
419
522
  }
420
523
 
421
- export function collectDtsImports(distDir: string): Set<string> {
524
+ export function collectDtsImports(
525
+ distDir: string,
526
+ options: ImportCollectionOptions = {},
527
+ ): Set<string> {
422
528
  const files = walkFiles(distDir, (name) => DTS_RE.test(name));
423
529
  const result = new Set<string>();
424
530
 
@@ -426,7 +532,7 @@ export function collectDtsImports(distDir: string): Set<string> {
426
532
  for (const { packageName } of extractImports(
427
533
  file,
428
534
  readFileSync(file, 'utf8'),
429
- { includeDtsForms: true },
535
+ { ...options, includeDtsForms: true },
430
536
  )) {
431
537
  result.add(packageName);
432
538
  }
@@ -1,9 +1,13 @@
1
1
  /**
2
2
  * Config validation for rule shapes that the model cannot interpret safely.
3
3
  */
4
- import type { DependencyModelConfig, DependencyRule } from './types';
4
+ import type {
5
+ DependencyModelConfig,
6
+ DependencyRule,
7
+ ToolingFileRule,
8
+ } from './types';
5
9
 
6
- function getRuleMatches(rule: DependencyRule): string[] {
10
+ function getRuleMatches(rule: DependencyRule | ToolingFileRule): string[] {
7
11
  return Array.isArray(rule.match) ? rule.match : [rule.match];
8
12
  }
9
13
 
@@ -34,10 +38,65 @@ export function validateDependencyRules(rules: DependencyRule[]): string[] {
34
38
  return errors;
35
39
  }
36
40
 
41
+ export function validateIgnoredImports(ignoredImports: unknown): string[] {
42
+ if (ignoredImports === undefined) return [];
43
+ if (!Array.isArray(ignoredImports)) {
44
+ return ['ignoredImports must be an array of module specifier patterns'];
45
+ }
46
+
47
+ return ignoredImports.flatMap((item, index) =>
48
+ typeof item === 'string'
49
+ ? []
50
+ : [`ignoredImports[${index}] must be a string`],
51
+ );
52
+ }
53
+
54
+ function isStringOrStringArray(value: unknown): boolean {
55
+ return (
56
+ typeof value === 'string' ||
57
+ (Array.isArray(value) && value.every((item) => typeof item === 'string'))
58
+ );
59
+ }
60
+
61
+ export function validateToolingFiles(toolingFiles: unknown): string[] {
62
+ if (toolingFiles === undefined) return [];
63
+ if (!Array.isArray(toolingFiles)) {
64
+ return ['toolingFiles must be an array of { match, workspace? } rules'];
65
+ }
66
+
67
+ return toolingFiles.flatMap((item, index) => {
68
+ const label = `toolingFiles[${index}]`;
69
+ if (typeof item !== 'object' || item === null) {
70
+ return [`${label} must be an object with a match field`];
71
+ }
72
+
73
+ const rule = item as Record<string, unknown>;
74
+ const errors: string[] = [];
75
+
76
+ if (!isStringOrStringArray(rule.match)) {
77
+ errors.push(`${label}.match must be a string or an array of strings`);
78
+ }
79
+ if (
80
+ rule.workspace !== undefined &&
81
+ !isStringOrStringArray(rule.workspace)
82
+ ) {
83
+ errors.push(
84
+ `${label}.workspace must be a string or an array of strings`,
85
+ );
86
+ }
87
+
88
+ return errors;
89
+ });
90
+ }
91
+
37
92
  export function assertValidDependencyModelConfig(
38
93
  config: DependencyModelConfig,
39
94
  ): void {
40
- const errors = validateDependencyRules(config.rules ?? []);
95
+ const errors = [
96
+ ...validateDependencyRules(config.rules ?? []),
97
+ ...validateIgnoredImports(config.ignoredImports),
98
+ ...validateToolingFiles(config.toolingFiles),
99
+ ];
41
100
 
42
101
  if (errors.length === 0) return;
43
102
 
@@ -12,7 +12,7 @@ import {
12
12
  getExpectedSection,
13
13
  } from './expected';
14
14
  import { isExternal } from './placement';
15
- import { getEffectiveRule, isWorkspaceRole } from './rules';
15
+ import { getEffectiveRule, isWorkspaceRole, matchesPatterns } from './rules';
16
16
  import type {
17
17
  DependencyRule,
18
18
  DependencyViolation,
@@ -42,6 +42,57 @@ function hasCollectedPackageUsage(workspace: WorkspaceFacts): boolean {
42
42
  return workspace.sourceUsage.size > 0 || workspace.dtsImports.size > 0;
43
43
  }
44
44
 
45
+ function getRuleMatches(rule: DependencyRule): string[] {
46
+ return Array.isArray(rule.match) ? rule.match : [rule.match];
47
+ }
48
+
49
+ function getConcreteRuleMatches(rule: DependencyRule): string[] {
50
+ return getRuleMatches(rule).filter((match) => !match.includes('*'));
51
+ }
52
+
53
+ function getWorkspacePatternLabel(rule: DependencyRule): string {
54
+ if (!rule.workspace) return '<all>';
55
+
56
+ return Array.isArray(rule.workspace)
57
+ ? rule.workspace.join(', ')
58
+ : rule.workspace;
59
+ }
60
+
61
+ function getRuleTargetWorkspaces(
62
+ rule: DependencyRule,
63
+ workspaces: WorkspaceFacts[],
64
+ ): WorkspaceFacts[] {
65
+ if (!rule.workspace) return workspaces;
66
+
67
+ return workspaces.filter(
68
+ (workspace) =>
69
+ matchesPatterns(workspace.name, rule.workspace!) ||
70
+ matchesPatterns(workspace.location, rule.workspace!),
71
+ );
72
+ }
73
+
74
+ function expectedHasDependency(
75
+ expected: ExpectedWorkspace | undefined,
76
+ depIdent: string,
77
+ ): boolean {
78
+ if (!expected) return false;
79
+
80
+ return SECTIONS.some((section) => expected.sections[section].has(depIdent));
81
+ }
82
+
83
+ function workspaceHasDependencySignal(
84
+ workspace: WorkspaceFacts,
85
+ expected: ExpectedWorkspace | undefined,
86
+ depIdent: string,
87
+ ): boolean {
88
+ return (
89
+ workspace.sourceUsage.has(depIdent) ||
90
+ workspace.dtsImports.has(depIdent) ||
91
+ collectActualDependencyNames(workspace).has(depIdent) ||
92
+ expectedHasDependency(expected, depIdent)
93
+ );
94
+ }
95
+
45
96
  export function compareManifest(
46
97
  expected: ExpectedWorkspace,
47
98
  violations: DependencyViolation[],
@@ -330,6 +381,59 @@ export function validateBasicShape(
330
381
  }
331
382
  }
332
383
 
384
+ export function collectStaleRuleViolations(
385
+ workspaces: WorkspaceFacts[],
386
+ expectedByLocation: Map<string, ExpectedWorkspace>,
387
+ rules: DependencyRule[],
388
+ ): DependencyViolation[] {
389
+ const violations: DependencyViolation[] = [];
390
+
391
+ rules.forEach((rule) => {
392
+ const concreteMatches = getConcreteRuleMatches(rule);
393
+ if (concreteMatches.length === 0) return;
394
+
395
+ const targetWorkspaces = getRuleTargetWorkspaces(rule, workspaces);
396
+ const workspaceLabel = getWorkspacePatternLabel(rule);
397
+
398
+ for (const depIdent of concreteMatches) {
399
+ if (targetWorkspaces.length === 0) {
400
+ violations.push({
401
+ code: 'stale-rule',
402
+ workspace: '<root>',
403
+ workspaceLocation: '.',
404
+ dependency: depIdent,
405
+ message:
406
+ `${depIdent} has a workspace-scoped depdoc.yml rule, but no workspace matches ${workspaceLabel}`,
407
+ });
408
+ continue;
409
+ }
410
+
411
+ const isUsed = targetWorkspaces.some((workspace) =>
412
+ workspaceHasDependencySignal(
413
+ workspace,
414
+ expectedByLocation.get(workspace.location),
415
+ depIdent,
416
+ ),
417
+ );
418
+ if (isUsed) continue;
419
+
420
+ const workspace = rule.workspace ? targetWorkspaces[0]! : workspaces[0]!;
421
+
422
+ violations.push({
423
+ code: 'stale-rule',
424
+ workspace: rule.workspace ? getWorkspaceLabel(workspace) : '<root>',
425
+ workspaceLocation: rule.workspace ? workspace.location : '.',
426
+ dependency: depIdent,
427
+ message: rule.workspace
428
+ ? `${depIdent} has a depdoc.yml rule scoped to ${workspaceLabel}, but no matching usage or declaration was found`
429
+ : `${depIdent} has a depdoc.yml rule, but no matching usage or declaration was found`,
430
+ });
431
+ }
432
+ });
433
+
434
+ return violations;
435
+ }
436
+
333
437
  export function dedupeViolations(
334
438
  violations: DependencyViolation[],
335
439
  ): DependencyViolation[] {
@@ -5,7 +5,12 @@
5
5
  * diagnostics. It must not read files, execute Yarn, or inspect `node_modules`;
6
6
  * those concerns belong to collectors and the runner.
7
7
  */
8
- import { compareManifest, dedupeViolations, validateBasicShape } from './diagnostics';
8
+ import {
9
+ collectStaleRuleViolations,
10
+ compareManifest,
11
+ dedupeViolations,
12
+ validateBasicShape,
13
+ } from './diagnostics';
9
14
  import { emptySections } from './expected';
10
15
  import { propagatePeers } from './peer-propagation';
11
16
  import {
@@ -111,6 +116,7 @@ export function deriveDependencyModel(
111
116
  violations.push(
112
117
  ...collectUnconstrainedVersionViolations(workspaces, expected, rules),
113
118
  );
119
+ violations.push(...collectStaleRuleViolations(workspaces, expected, rules));
114
120
 
115
121
  return {
116
122
  violations: dedupeViolations(violations),