@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.
- package/DEPDOC_MODEL.md +22 -11
- package/__tests__/compatibility.test.ts +74 -15
- package/__tests__/config-validation.test.ts +46 -1
- package/__tests__/engine.test.ts +162 -2
- package/__tests__/fixtures/imports/config/aliases.json +8 -0
- package/__tests__/fixtures/imports/plain-root-file.ts +3 -0
- package/__tests__/fixtures/imports/sidebars.ts +5 -0
- package/__tests__/fixtures/imports/src/aliases.ts +4 -0
- package/__tests__/fixtures/imports/src/virtual.ts +3 -0
- package/__tests__/fixtures/imports/tsconfig.build.json +3 -0
- package/__tests__/fixtures/imports/tsconfig.json +8 -0
- package/__tests__/fixtures/imports/vite.config.ts +3 -0
- package/__tests__/imports.test.ts +72 -0
- package/dist/collectors/tsconfig-aliases.d.ts +1 -0
- package/dist/collectors/tsconfig-aliases.js +40 -0
- package/dist/collectors/tsconfig-aliases.js.map +1 -0
- package/dist/collectors/workspaces.d.ts +2 -2
- package/dist/collectors/workspaces.js +21 -6
- package/dist/collectors/workspaces.js.map +1 -1
- package/dist/lib/imports.d.ts +9 -4
- package/dist/lib/imports.js +62 -9
- package/dist/lib/imports.js.map +1 -1
- package/dist/model/config-validation.d.ts +2 -0
- package/dist/model/config-validation.js +44 -1
- package/dist/model/config-validation.js.map +1 -1
- package/dist/model/diagnostics.d.ts +1 -0
- package/dist/model/diagnostics.js +67 -0
- package/dist/model/diagnostics.js.map +1 -1
- package/dist/model/engine.js +1 -0
- package/dist/model/engine.js.map +1 -1
- package/dist/model/placement.js +12 -1
- package/dist/model/placement.js.map +1 -1
- package/dist/model/types.d.ts +8 -1
- package/dist/model/types.js.map +1 -1
- package/dist/runner.js +1 -1
- package/dist/runner.js.map +1 -1
- package/package.json +2 -2
- package/src/collectors/tsconfig-aliases.ts +45 -0
- package/src/collectors/workspaces.ts +50 -7
- package/src/lib/imports.ts +114 -8
- package/src/model/config-validation.ts +62 -3
- package/src/model/diagnostics.ts +105 -1
- package/src/model/engine.ts +7 -1
- package/src/model/placement.ts +19 -1
- package/src/model/types.ts +16 -0
- package/src/runner.ts +6 -1
package/dist/model/types.d.ts
CHANGED
|
@@ -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;
|
package/dist/model/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/model/types.ts"],"names":[],"mappings":";;;
|
|
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);
|
package/dist/runner.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runner.js","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":";;AAwBA,
|
|
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.
|
|
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": "
|
|
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
|
|
27
|
-
|
|
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(
|
|
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
|
|
package/src/lib/imports.ts
CHANGED
|
@@ -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(
|
|
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(
|
|
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(
|
|
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 {
|
|
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 =
|
|
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
|
|
package/src/model/diagnostics.ts
CHANGED
|
@@ -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[] {
|
package/src/model/engine.ts
CHANGED
|
@@ -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 {
|
|
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),
|