@rsdk/yarn.constraints 6.0.0-next.4 → 6.0.0-next.41
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/DEPENDENCY_MODEL.md +460 -0
- package/README.MD +85 -21
- package/__tests__/compatibility.test.ts +321 -0
- package/__tests__/config-validation.test.ts +42 -0
- package/__tests__/engine.test.ts +1107 -0
- package/__tests__/fixtures/imports/bin.js +4 -0
- package/__tests__/fixtures/imports/export-entry.mjs +1 -0
- package/__tests__/fixtures/imports/lib/lib-entry.js +3 -0
- package/__tests__/fixtures/imports/root-entry.js +4 -0
- package/__tests__/fixtures/imports/rules/transitive.js +3 -0
- package/__tests__/fixtures/imports/src/common.cjs +3 -0
- package/__tests__/fixtures/imports/src/common.cts +3 -0
- package/__tests__/fixtures/imports/src/component.tsx +4 -0
- package/__tests__/fixtures/imports/src/index.ts +13 -0
- package/__tests__/fixtures/imports/src/module.mjs +3 -0
- package/__tests__/fixtures/imports/src/module.mts +3 -0
- package/__tests__/fixtures/imports/src/plain.js +3 -0
- package/__tests__/fixtures/imports/src/test-only-usage.ts +1 -0
- package/__tests__/fixtures/imports/test/outside.ts +3 -0
- package/__tests__/imports.test.ts +218 -0
- package/__tests__/manifest-writer.test.ts +157 -0
- package/dist/ansi.d.ts +9 -0
- package/dist/ansi.js +24 -0
- package/dist/ansi.js.map +1 -0
- package/dist/bin/depdoc.d.ts +2 -0
- package/dist/bin/depdoc.js +157 -0
- package/dist/bin/depdoc.js.map +1 -0
- package/dist/collectors/config.d.ts +2 -0
- package/dist/collectors/config.js +28 -0
- package/dist/collectors/config.js.map +1 -0
- package/dist/collectors/external-metadata.d.ts +5 -0
- package/dist/collectors/external-metadata.js +110 -0
- package/dist/collectors/external-metadata.js.map +1 -0
- package/dist/collectors/package-extensions.d.ts +3 -0
- package/dist/collectors/package-extensions.js +43 -0
- package/dist/collectors/package-extensions.js.map +1 -0
- package/dist/collectors/type-providers.d.ts +3 -0
- package/dist/collectors/type-providers.js +46 -0
- package/dist/collectors/type-providers.js.map +1 -0
- package/dist/collectors/workspaces.d.ts +2 -0
- package/dist/collectors/workspaces.js +90 -0
- package/dist/collectors/workspaces.js.map +1 -0
- package/dist/dependency-model.d.ts +11 -0
- package/dist/dependency-model.js +18 -0
- package/dist/dependency-model.js.map +1 -0
- package/dist/index.d.ts +9 -5
- package/dist/index.js +13 -33
- package/dist/index.js.map +1 -1
- package/dist/lib/imports.d.ts +11 -0
- package/dist/lib/imports.js +342 -0
- package/dist/lib/imports.js.map +1 -0
- package/dist/lib/package-json.d.ts +21 -0
- package/dist/lib/package-json.js +32 -0
- package/dist/lib/package-json.js.map +1 -0
- package/dist/model/config-validation.d.ts +6 -0
- package/dist/model/config-validation.js +31 -0
- package/dist/model/config-validation.js.map +1 -0
- package/dist/model/diagnostics.d.ts +4 -0
- package/dist/model/diagnostics.js +295 -0
- package/dist/model/diagnostics.js.map +1 -0
- package/dist/model/engine.d.ts +5 -0
- package/dist/model/engine.js +52 -0
- package/dist/model/engine.js.map +1 -0
- package/dist/model/expected.d.ts +20 -0
- package/dist/model/expected.js +89 -0
- package/dist/model/expected.js.map +1 -0
- package/dist/model/peer-propagation.d.ts +2 -0
- package/dist/model/peer-propagation.js +124 -0
- package/dist/model/peer-propagation.js.map +1 -0
- package/dist/model/placement.d.ts +9 -0
- package/dist/model/placement.js +210 -0
- package/dist/model/placement.js.map +1 -0
- package/dist/model/rules.d.ts +15 -0
- package/dist/model/rules.js +52 -0
- package/dist/model/rules.js.map +1 -0
- package/dist/model/types.d.ts +118 -0
- package/dist/model/types.js +9 -0
- package/dist/model/types.js.map +1 -0
- package/dist/model/versions.d.ts +3 -0
- package/dist/model/versions.js +77 -0
- package/dist/model/versions.js.map +1 -0
- package/dist/reporting.d.ts +3 -0
- package/dist/reporting.js +80 -0
- package/dist/reporting.js.map +1 -0
- package/dist/runner.d.ts +2 -0
- package/dist/runner.js +70 -0
- package/dist/runner.js.map +1 -0
- package/dist/writer/manifest-writer.d.ts +2 -0
- package/dist/writer/manifest-writer.js +72 -0
- package/dist/writer/manifest-writer.js.map +1 -0
- package/eslint.config.cjs +3 -0
- package/jest.config.js +1 -0
- package/package.json +7 -3
- package/src/ansi.ts +23 -0
- package/src/bin/depdoc.ts +213 -0
- package/src/collectors/config.ts +33 -0
- package/src/collectors/external-metadata.ts +148 -0
- package/src/collectors/package-extensions.ts +52 -0
- package/src/collectors/type-providers.ts +51 -0
- package/src/collectors/workspaces.ts +107 -0
- package/src/dependency-model.ts +26 -0
- package/src/index.ts +28 -45
- package/src/lib/imports.ts +435 -0
- package/src/lib/package-json.ts +46 -0
- package/src/model/config-validation.ts +49 -0
- package/src/model/diagnostics.ts +358 -0
- package/src/model/engine.ts +120 -0
- package/src/model/expected.ts +141 -0
- package/src/model/peer-propagation.ts +199 -0
- package/src/model/placement.ts +378 -0
- package/src/model/rules.ts +85 -0
- package/src/model/types.ts +165 -0
- package/src/model/versions.ts +114 -0
- package/src/reporting.ts +117 -0
- package/src/runner.ts +102 -0
- package/src/writer/manifest-writer.ts +111 -0
- package/tsconfig.build.json +1 -0
- package/tsconfig.json +6 -1
- package/dist/constraint-schema.d.ts +0 -1
- package/dist/constraint-schema.js +0 -17
- package/dist/constraint-schema.js.map +0 -1
- package/dist/dependency-checker.d.ts +0 -8
- package/dist/dependency-checker.js +0 -40
- package/dist/dependency-checker.js.map +0 -1
- package/src/constraint-schema.ts +0 -20
- package/src/dependency-checker.ts +0 -41
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package manifest helpers.
|
|
3
|
+
*
|
|
4
|
+
* This module intentionally stays small: it reads `package.json` files and
|
|
5
|
+
* locates the monorepo root. Higher-level workspace discovery and model
|
|
6
|
+
* semantics belong to collectors and the engine.
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
9
|
+
import { dirname, join } from 'node:path';
|
|
10
|
+
|
|
11
|
+
export interface PackageJson {
|
|
12
|
+
name: string;
|
|
13
|
+
private?: boolean;
|
|
14
|
+
role?: 'library' | 'service' | 'cli';
|
|
15
|
+
workspaces?: string[] | { packages: string[] };
|
|
16
|
+
main?: string;
|
|
17
|
+
types?: string;
|
|
18
|
+
typings?: string;
|
|
19
|
+
exports?: unknown;
|
|
20
|
+
bin?: string | Record<string, string>;
|
|
21
|
+
dependencies?: Record<string, string>;
|
|
22
|
+
peerDependencies?: Record<string, string>;
|
|
23
|
+
peerDependenciesMeta?: Record<string, { optional?: boolean }>;
|
|
24
|
+
devDependencies?: Record<string, string>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function readPackageJson(dir: string): PackageJson {
|
|
28
|
+
return JSON.parse(
|
|
29
|
+
readFileSync(join(dir, 'package.json'), 'utf8'),
|
|
30
|
+
) as PackageJson;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function findMonorepoRoot(startDir: string): string | null {
|
|
34
|
+
let dir = startDir;
|
|
35
|
+
|
|
36
|
+
while (true) {
|
|
37
|
+
const pkgPath = join(dir, 'package.json');
|
|
38
|
+
if (existsSync(pkgPath)) {
|
|
39
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) as PackageJson;
|
|
40
|
+
if (pkg.workspaces) return dir;
|
|
41
|
+
}
|
|
42
|
+
const parent = dirname(dir);
|
|
43
|
+
if (parent === dir) return null;
|
|
44
|
+
dir = parent;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config validation for rule shapes that the model cannot interpret safely.
|
|
3
|
+
*/
|
|
4
|
+
import type { DependencyModelConfig, DependencyRule } from './types';
|
|
5
|
+
|
|
6
|
+
function getRuleMatches(rule: DependencyRule): string[] {
|
|
7
|
+
return Array.isArray(rule.match) ? rule.match : [rule.match];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function getRuleLabel(rule: DependencyRule, index: number): string {
|
|
11
|
+
return `rules[${index}] (${getRuleMatches(rule).join(', ')})`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function validateDependencyRules(rules: DependencyRule[]): string[] {
|
|
15
|
+
const errors: string[] = [];
|
|
16
|
+
|
|
17
|
+
rules.forEach((rule, index) => {
|
|
18
|
+
const label = getRuleLabel(rule, index);
|
|
19
|
+
const matches = getRuleMatches(rule);
|
|
20
|
+
|
|
21
|
+
if (rule.required === true && matches.some((match) => match.includes('*'))) {
|
|
22
|
+
errors.push(
|
|
23
|
+
`${label}: required: true requires concrete match patterns; wildcard matches are ignored by required dependency expansion`,
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (rule.section === 'devDependencies' && rule.rootOnly !== true) {
|
|
28
|
+
errors.push(
|
|
29
|
+
`${label}: section: devDependencies is only supported together with rootOnly: true; workspace devDependencies are derived by the model`,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
return errors;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function assertValidDependencyModelConfig(
|
|
38
|
+
config: DependencyModelConfig,
|
|
39
|
+
): void {
|
|
40
|
+
const errors = validateDependencyRules(config.rules ?? []);
|
|
41
|
+
|
|
42
|
+
if (errors.length === 0) return;
|
|
43
|
+
|
|
44
|
+
throw new Error(
|
|
45
|
+
['Invalid dependency constraints config:', ...errors.map((e) => `- ${e}`)].join(
|
|
46
|
+
'\n',
|
|
47
|
+
),
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manifest diagnostics.
|
|
3
|
+
*
|
|
4
|
+
* The expected graph is compared with actual package manifests here. This
|
|
5
|
+
* module also owns basic shape checks such as root-only sections, workspace
|
|
6
|
+
* roles, forbidden service/CLI sections, and library mirror diagnostics.
|
|
7
|
+
*/
|
|
8
|
+
import {
|
|
9
|
+
getDeclaredRange,
|
|
10
|
+
getDeclaredSection,
|
|
11
|
+
getExpectedRange,
|
|
12
|
+
getExpectedSection,
|
|
13
|
+
} from './expected';
|
|
14
|
+
import { isExternal } from './placement';
|
|
15
|
+
import { getEffectiveRule, isWorkspaceRole } from './rules';
|
|
16
|
+
import type {
|
|
17
|
+
DependencyRule,
|
|
18
|
+
DependencyViolation,
|
|
19
|
+
DependencyWarning,
|
|
20
|
+
ExpectedWorkspace,
|
|
21
|
+
WorkspaceFacts,
|
|
22
|
+
} from './types';
|
|
23
|
+
import { SECTIONS } from './types';
|
|
24
|
+
|
|
25
|
+
function getWorkspaceLabel(workspace: WorkspaceFacts): string {
|
|
26
|
+
return workspace.isRoot ? '<root>' : workspace.name;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function collectActualDependencyNames(workspace: WorkspaceFacts): Set<string> {
|
|
30
|
+
return new Set(
|
|
31
|
+
SECTIONS.flatMap((section) => Object.keys(workspace.pkg[section] ?? {})),
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function hasExternalCollectedUsage(
|
|
36
|
+
workspace: WorkspaceFacts,
|
|
37
|
+
workspaceNames: Set<string>,
|
|
38
|
+
): boolean {
|
|
39
|
+
for (const depIdent of workspace.sourceUsage.keys()) {
|
|
40
|
+
if (isExternal(depIdent, workspaceNames)) return true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
for (const depIdent of workspace.dtsImports) {
|
|
44
|
+
if (isExternal(depIdent, workspaceNames)) return true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function compareManifest(
|
|
51
|
+
expected: ExpectedWorkspace,
|
|
52
|
+
violations: DependencyViolation[],
|
|
53
|
+
): void {
|
|
54
|
+
const workspace = expected.workspace;
|
|
55
|
+
const actualNames = collectActualDependencyNames(workspace);
|
|
56
|
+
const expectedNames = new Set<string>();
|
|
57
|
+
|
|
58
|
+
for (const section of SECTIONS) {
|
|
59
|
+
for (const depIdent of expected.sections[section].keys()) {
|
|
60
|
+
expectedNames.add(depIdent);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
for (const depIdent of expectedNames) {
|
|
65
|
+
const expectedSection = getExpectedSection(expected, depIdent)!;
|
|
66
|
+
const expectedRange = getExpectedRange(expected, depIdent)!;
|
|
67
|
+
const actualSection = getDeclaredSection(workspace.pkg, depIdent);
|
|
68
|
+
const actualRange = getDeclaredRange(workspace.pkg, depIdent);
|
|
69
|
+
|
|
70
|
+
if (!actualSection) {
|
|
71
|
+
violations.push({
|
|
72
|
+
code: 'missing',
|
|
73
|
+
workspace: getWorkspaceLabel(workspace),
|
|
74
|
+
workspaceLocation: workspace.location,
|
|
75
|
+
dependency: depIdent,
|
|
76
|
+
expectedSection,
|
|
77
|
+
expectedRange,
|
|
78
|
+
message: `${depIdent} is missing from ${expectedSection}`,
|
|
79
|
+
});
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (actualSection !== expectedSection) {
|
|
84
|
+
violations.push({
|
|
85
|
+
code: expected.workspace.isRoot ? 'root-section' : 'wrong-section',
|
|
86
|
+
workspace: getWorkspaceLabel(workspace),
|
|
87
|
+
workspaceLocation: workspace.location,
|
|
88
|
+
dependency: depIdent,
|
|
89
|
+
actualSection,
|
|
90
|
+
expectedSection,
|
|
91
|
+
actualRange: actualRange ?? undefined,
|
|
92
|
+
expectedRange,
|
|
93
|
+
message: `${depIdent} is in ${actualSection}, expected ${expectedSection}`,
|
|
94
|
+
});
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (actualRange !== expectedRange) {
|
|
99
|
+
violations.push({
|
|
100
|
+
code: 'wrong-range',
|
|
101
|
+
workspace: getWorkspaceLabel(workspace),
|
|
102
|
+
workspaceLocation: workspace.location,
|
|
103
|
+
dependency: depIdent,
|
|
104
|
+
actualSection,
|
|
105
|
+
expectedSection,
|
|
106
|
+
actualRange: actualRange ?? undefined,
|
|
107
|
+
expectedRange,
|
|
108
|
+
message: `${depIdent} range is ${actualRange}, expected ${expectedRange}`,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
for (const depIdent of actualNames) {
|
|
114
|
+
if (expectedNames.has(depIdent)) continue;
|
|
115
|
+
const actualSection = getDeclaredSection(workspace.pkg, depIdent);
|
|
116
|
+
const actualRange = getDeclaredRange(workspace.pkg, depIdent);
|
|
117
|
+
const isForbiddenRootSection =
|
|
118
|
+
workspace.isRoot && actualSection && actualSection !== 'devDependencies';
|
|
119
|
+
|
|
120
|
+
violations.push({
|
|
121
|
+
code: isForbiddenRootSection ? 'root-section' : 'stale',
|
|
122
|
+
workspace: getWorkspaceLabel(workspace),
|
|
123
|
+
workspaceLocation: workspace.location,
|
|
124
|
+
dependency: depIdent,
|
|
125
|
+
actualSection: actualSection ?? undefined,
|
|
126
|
+
actualRange: actualRange ?? undefined,
|
|
127
|
+
message: isForbiddenRootSection
|
|
128
|
+
? `${depIdent} is declared in root ${actualSection}; root package.json may contain only devDependencies`
|
|
129
|
+
: `${depIdent} is declared but not required by the derived model`,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
for (const depIdent of Object.keys(
|
|
134
|
+
workspace.pkg.peerDependenciesMeta ?? {},
|
|
135
|
+
)) {
|
|
136
|
+
if (expected.sections.peerDependencies.has(depIdent)) continue;
|
|
137
|
+
|
|
138
|
+
violations.push({
|
|
139
|
+
code: 'stale',
|
|
140
|
+
workspace: getWorkspaceLabel(workspace),
|
|
141
|
+
workspaceLocation: workspace.location,
|
|
142
|
+
dependency: depIdent,
|
|
143
|
+
message:
|
|
144
|
+
`${depIdent} has peerDependenciesMeta but is not expected in peerDependencies`,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function validateBasicShape(
|
|
150
|
+
workspaces: WorkspaceFacts[],
|
|
151
|
+
expectedByLocation: Map<string, ExpectedWorkspace>,
|
|
152
|
+
rules: DependencyRule[],
|
|
153
|
+
violations: DependencyViolation[],
|
|
154
|
+
warnings: DependencyWarning[],
|
|
155
|
+
withDts: boolean,
|
|
156
|
+
): void {
|
|
157
|
+
const workspaceNames = new Set(
|
|
158
|
+
workspaces.filter((ws) => !ws.isRoot).map((ws) => ws.name),
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
for (const workspace of workspaces) {
|
|
162
|
+
if (workspace.isRoot) {
|
|
163
|
+
if (workspace.pkg.private !== true) {
|
|
164
|
+
violations.push({
|
|
165
|
+
code: 'root-private',
|
|
166
|
+
workspace: getWorkspaceLabel(workspace),
|
|
167
|
+
workspaceLocation: workspace.location,
|
|
168
|
+
message: 'root package.json must have private: true',
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (!workspace.pkg.role) {
|
|
175
|
+
violations.push({
|
|
176
|
+
code: 'role-missing',
|
|
177
|
+
workspace: getWorkspaceLabel(workspace),
|
|
178
|
+
workspaceLocation: workspace.location,
|
|
179
|
+
message: 'workspace package.json must declare role',
|
|
180
|
+
});
|
|
181
|
+
} else if (!isWorkspaceRole(workspace.pkg.role)) {
|
|
182
|
+
violations.push({
|
|
183
|
+
code: 'role-invalid',
|
|
184
|
+
workspace: getWorkspaceLabel(workspace),
|
|
185
|
+
workspaceLocation: workspace.location,
|
|
186
|
+
message: `workspace role must be library, service, or cli; got ${workspace.pkg.role}`,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (workspace.pkg.bin && workspace.role !== 'cli') {
|
|
191
|
+
warnings.push({
|
|
192
|
+
code: 'bin-role-mismatch',
|
|
193
|
+
workspace: getWorkspaceLabel(workspace),
|
|
194
|
+
workspaceLocation: workspace.location,
|
|
195
|
+
message: 'workspace declares bin but role is not cli',
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (
|
|
200
|
+
workspace.role &&
|
|
201
|
+
workspace.sourceFileCount !== undefined &&
|
|
202
|
+
workspace.sourceFileCount > 0 &&
|
|
203
|
+
!hasExternalCollectedUsage(workspace, workspaceNames)
|
|
204
|
+
) {
|
|
205
|
+
warnings.push({
|
|
206
|
+
code: 'zero-external-dependencies',
|
|
207
|
+
workspace: getWorkspaceLabel(workspace),
|
|
208
|
+
workspaceLocation: workspace.location,
|
|
209
|
+
message:
|
|
210
|
+
`no external source imports were collected from ${workspace.sourceFileCount} source file(s); check source roots and entrypoints before trusting autofix`,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (workspace.hasSrc && !workspace.hasDist && withDts) {
|
|
215
|
+
violations.push({
|
|
216
|
+
code: 'dist-missing',
|
|
217
|
+
workspace: getWorkspaceLabel(workspace),
|
|
218
|
+
workspaceLocation: workspace.location,
|
|
219
|
+
message: 'dist/ not found; build required before --with-dts check',
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (workspace.role === 'service' || workspace.role === 'cli') {
|
|
224
|
+
for (const section of ['peerDependencies', 'devDependencies'] as const) {
|
|
225
|
+
for (const depIdent of Object.keys(workspace.pkg[section] ?? {})) {
|
|
226
|
+
violations.push({
|
|
227
|
+
code: 'forbidden-section',
|
|
228
|
+
workspace: getWorkspaceLabel(workspace),
|
|
229
|
+
workspaceLocation: workspace.location,
|
|
230
|
+
dependency: depIdent,
|
|
231
|
+
actualSection: section,
|
|
232
|
+
expectedSection:
|
|
233
|
+
section === 'peerDependencies' ? 'dependencies' : undefined,
|
|
234
|
+
message: `${section} are forbidden for ${workspace.role} workspaces`,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (workspace.role === 'library') {
|
|
241
|
+
for (const depIdent of Object.keys(workspace.pkg.dependencies ?? {})) {
|
|
242
|
+
if (workspace.pkg.peerDependencies?.[depIdent] !== undefined) {
|
|
243
|
+
violations.push({
|
|
244
|
+
code: 'forbidden-section',
|
|
245
|
+
workspace: getWorkspaceLabel(workspace),
|
|
246
|
+
workspaceLocation: workspace.location,
|
|
247
|
+
dependency: depIdent,
|
|
248
|
+
actualSection: 'dependencies',
|
|
249
|
+
expectedSection: 'peerDependencies',
|
|
250
|
+
message: `${depIdent} is declared in both dependencies and peerDependencies`,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const expected = expectedByLocation.get(workspace.location);
|
|
256
|
+
if (expected) {
|
|
257
|
+
for (const [depIdent, peerRange] of expected.sections
|
|
258
|
+
.peerDependencies) {
|
|
259
|
+
const devRange = workspace.pkg.devDependencies?.[depIdent];
|
|
260
|
+
if (devRange !== peerRange) {
|
|
261
|
+
violations.push({
|
|
262
|
+
code: 'mirror',
|
|
263
|
+
workspace: getWorkspaceLabel(workspace),
|
|
264
|
+
workspaceLocation: workspace.location,
|
|
265
|
+
dependency: depIdent,
|
|
266
|
+
actualSection: 'devDependencies',
|
|
267
|
+
expectedSection: 'devDependencies',
|
|
268
|
+
actualRange: devRange,
|
|
269
|
+
expectedRange: peerRange,
|
|
270
|
+
message: `${depIdent} must be mirrored from peerDependencies to devDependencies`,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
for (const depIdent of Object.keys(workspace.pkg.devDependencies ?? {})) {
|
|
278
|
+
if (workspace.pkg.dependencies?.[depIdent] !== undefined) {
|
|
279
|
+
violations.push({
|
|
280
|
+
code: 'forbidden-section',
|
|
281
|
+
workspace: getWorkspaceLabel(workspace),
|
|
282
|
+
workspaceLocation: workspace.location,
|
|
283
|
+
dependency: depIdent,
|
|
284
|
+
actualSection: 'devDependencies',
|
|
285
|
+
expectedSection: 'dependencies',
|
|
286
|
+
message: `${depIdent} is declared in both dependencies and devDependencies`,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
for (const section of SECTIONS) {
|
|
292
|
+
for (const depIdent of Object.keys(workspace.pkg[section] ?? {})) {
|
|
293
|
+
const rule = getEffectiveRule(
|
|
294
|
+
rules,
|
|
295
|
+
depIdent,
|
|
296
|
+
workspace.name,
|
|
297
|
+
workspace.location,
|
|
298
|
+
);
|
|
299
|
+
const expected = expectedByLocation.get(workspace.location);
|
|
300
|
+
if (
|
|
301
|
+
rule.rootOnly &&
|
|
302
|
+
isExternal(depIdent, workspaceNames) &&
|
|
303
|
+
!expected?.sections[section].has(depIdent)
|
|
304
|
+
) {
|
|
305
|
+
violations.push({
|
|
306
|
+
code: 'root-only',
|
|
307
|
+
workspace: getWorkspaceLabel(workspace),
|
|
308
|
+
workspaceLocation: workspace.location,
|
|
309
|
+
dependency: depIdent,
|
|
310
|
+
actualSection: section,
|
|
311
|
+
expectedSection: 'devDependencies',
|
|
312
|
+
message: `${depIdent} is rootOnly and must not be declared in workspace package.json`,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const expected = expectedByLocation.get(workspace.location);
|
|
319
|
+
if (expected) {
|
|
320
|
+
for (const [depIdent, reasons] of expected.reasons) {
|
|
321
|
+
if (!isExternal(depIdent, workspaceNames)) continue;
|
|
322
|
+
if (!reasons.some((reason) => reason.kind === 'root-only')) continue;
|
|
323
|
+
|
|
324
|
+
violations.push({
|
|
325
|
+
code: 'root-only-usage',
|
|
326
|
+
workspace: getWorkspaceLabel(workspace),
|
|
327
|
+
workspaceLocation: workspace.location,
|
|
328
|
+
dependency: depIdent,
|
|
329
|
+
expectedSection: 'devDependencies',
|
|
330
|
+
message: `${depIdent} is rootOnly and must not be used by workspace source or peer surface`,
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export function dedupeViolations(
|
|
338
|
+
violations: DependencyViolation[],
|
|
339
|
+
): DependencyViolation[] {
|
|
340
|
+
const result: DependencyViolation[] = [];
|
|
341
|
+
const seen = new Set<string>();
|
|
342
|
+
|
|
343
|
+
for (const violation of violations) {
|
|
344
|
+
const key = [
|
|
345
|
+
violation.code,
|
|
346
|
+
violation.workspaceLocation,
|
|
347
|
+
violation.dependency ?? '',
|
|
348
|
+
violation.actualSection ?? '',
|
|
349
|
+
violation.expectedSection ?? '',
|
|
350
|
+
violation.expectedRange ?? '',
|
|
351
|
+
].join('\0');
|
|
352
|
+
if (seen.has(key)) continue;
|
|
353
|
+
seen.add(key);
|
|
354
|
+
result.push(violation);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return result;
|
|
358
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure dependency model engine.
|
|
3
|
+
*
|
|
4
|
+
* The engine receives complete facts and derives expected manifests plus
|
|
5
|
+
* diagnostics. It must not read files, execute Yarn, or inspect `node_modules`;
|
|
6
|
+
* those concerns belong to collectors and the runner.
|
|
7
|
+
*/
|
|
8
|
+
import { compareManifest, dedupeViolations, validateBasicShape } from './diagnostics';
|
|
9
|
+
import { emptySections } from './expected';
|
|
10
|
+
import { propagatePeers } from './peer-propagation';
|
|
11
|
+
import {
|
|
12
|
+
addRequiredRuleDependencies,
|
|
13
|
+
addUsageDependencies,
|
|
14
|
+
rebuildLibraryMirrors,
|
|
15
|
+
seedRootDevDependencies,
|
|
16
|
+
} from './placement';
|
|
17
|
+
import type {
|
|
18
|
+
DependencyModelFacts,
|
|
19
|
+
DependencyModelOutput,
|
|
20
|
+
DependencyRule,
|
|
21
|
+
ExpectedWorkspace,
|
|
22
|
+
WorkspaceFacts,
|
|
23
|
+
} from './types';
|
|
24
|
+
import {
|
|
25
|
+
applyVersionRules,
|
|
26
|
+
collectUnconstrainedVersionViolations,
|
|
27
|
+
} from './versions';
|
|
28
|
+
|
|
29
|
+
export function buildExpected(
|
|
30
|
+
workspaces: WorkspaceFacts[],
|
|
31
|
+
rules: DependencyRule[],
|
|
32
|
+
externalPeerMetadata: Map<string, Map<string, string> | null>,
|
|
33
|
+
externalTypeMetadata: Map<string, { hasBundledTypes: boolean }>,
|
|
34
|
+
typeProviderPackages: Set<string>,
|
|
35
|
+
): Map<string, ExpectedWorkspace> {
|
|
36
|
+
const expectedByLocation = new Map<string, ExpectedWorkspace>();
|
|
37
|
+
const root = workspaces[0]!;
|
|
38
|
+
const workspaceNames = new Set(
|
|
39
|
+
workspaces.filter((ws) => !ws.isRoot).map((ws) => ws.name),
|
|
40
|
+
);
|
|
41
|
+
const workspacesByName = new Map(
|
|
42
|
+
workspaces.filter((ws) => !ws.isRoot).map((ws) => [ws.name, ws]),
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
for (const workspace of workspaces) {
|
|
46
|
+
expectedByLocation.set(workspace.location, {
|
|
47
|
+
workspace,
|
|
48
|
+
sections: emptySections(),
|
|
49
|
+
reasons: new Map(),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
seedRootDevDependencies(expectedByLocation, workspaceNames, rules);
|
|
54
|
+
addUsageDependencies(
|
|
55
|
+
expectedByLocation,
|
|
56
|
+
root,
|
|
57
|
+
workspaceNames,
|
|
58
|
+
rules,
|
|
59
|
+
externalTypeMetadata,
|
|
60
|
+
typeProviderPackages,
|
|
61
|
+
);
|
|
62
|
+
addRequiredRuleDependencies(
|
|
63
|
+
expectedByLocation,
|
|
64
|
+
root,
|
|
65
|
+
workspaceNames,
|
|
66
|
+
rules,
|
|
67
|
+
);
|
|
68
|
+
propagatePeers(
|
|
69
|
+
expectedByLocation,
|
|
70
|
+
workspacesByName,
|
|
71
|
+
workspaceNames,
|
|
72
|
+
externalPeerMetadata,
|
|
73
|
+
rules,
|
|
74
|
+
);
|
|
75
|
+
applyVersionRules(expectedByLocation, root, workspaceNames, rules);
|
|
76
|
+
rebuildLibraryMirrors(expectedByLocation);
|
|
77
|
+
|
|
78
|
+
return expectedByLocation;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function deriveDependencyModel(
|
|
82
|
+
facts: DependencyModelFacts,
|
|
83
|
+
): DependencyModelOutput {
|
|
84
|
+
const {
|
|
85
|
+
workspaces,
|
|
86
|
+
externalPeerMetadata,
|
|
87
|
+
externalTypeMetadata = new Map(),
|
|
88
|
+
typeProviderPackages = new Set(),
|
|
89
|
+
rules,
|
|
90
|
+
withDts = false,
|
|
91
|
+
} = facts;
|
|
92
|
+
const expected = buildExpected(
|
|
93
|
+
workspaces,
|
|
94
|
+
rules,
|
|
95
|
+
externalPeerMetadata,
|
|
96
|
+
externalTypeMetadata,
|
|
97
|
+
typeProviderPackages,
|
|
98
|
+
);
|
|
99
|
+
const violations: DependencyModelOutput['violations'] = [];
|
|
100
|
+
const warnings: DependencyModelOutput['warnings'] = [];
|
|
101
|
+
|
|
102
|
+
validateBasicShape(
|
|
103
|
+
workspaces,
|
|
104
|
+
expected,
|
|
105
|
+
rules,
|
|
106
|
+
violations,
|
|
107
|
+
warnings,
|
|
108
|
+
withDts,
|
|
109
|
+
);
|
|
110
|
+
for (const item of expected.values()) compareManifest(item, violations);
|
|
111
|
+
violations.push(
|
|
112
|
+
...collectUnconstrainedVersionViolations(workspaces, expected, rules),
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
violations: dedupeViolations(violations),
|
|
117
|
+
warnings,
|
|
118
|
+
expected,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers for expected dependency sections and their explanations.
|
|
3
|
+
*
|
|
4
|
+
* The model derives a fresh expected manifest for each workspace. This module
|
|
5
|
+
* owns the small mutation primitives for those expected sections and keeps the
|
|
6
|
+
* reason trail used by `depdoc explain`.
|
|
7
|
+
*/
|
|
8
|
+
import type { PackageJson } from '../lib/package-json';
|
|
9
|
+
|
|
10
|
+
import { getEffectiveRule } from './rules';
|
|
11
|
+
import {
|
|
12
|
+
type DependencyRule,
|
|
13
|
+
type ExpectedWorkspace,
|
|
14
|
+
type Reason,
|
|
15
|
+
SECTIONS,
|
|
16
|
+
type SectionType,
|
|
17
|
+
type WorkspaceFacts,
|
|
18
|
+
} from './types';
|
|
19
|
+
|
|
20
|
+
export function emptySections(): Record<SectionType, Map<string, string>> {
|
|
21
|
+
return {
|
|
22
|
+
dependencies: new Map(),
|
|
23
|
+
peerDependencies: new Map(),
|
|
24
|
+
devDependencies: new Map(),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function getDeclaredSection(
|
|
29
|
+
pkg: PackageJson,
|
|
30
|
+
depIdent: string,
|
|
31
|
+
): SectionType | null {
|
|
32
|
+
for (const section of SECTIONS) {
|
|
33
|
+
if (pkg[section]?.[depIdent] !== undefined) return section;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function getDeclaredRange(
|
|
40
|
+
pkg: PackageJson,
|
|
41
|
+
depIdent: string,
|
|
42
|
+
): string | null {
|
|
43
|
+
const section = getDeclaredSection(pkg, depIdent);
|
|
44
|
+
|
|
45
|
+
return section ? (pkg[section]?.[depIdent] ?? null) : null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function addReason(
|
|
49
|
+
expected: ExpectedWorkspace,
|
|
50
|
+
depIdent: string,
|
|
51
|
+
reason: Reason,
|
|
52
|
+
): void {
|
|
53
|
+
if (!expected.reasons.has(depIdent)) expected.reasons.set(depIdent, []);
|
|
54
|
+
const list = expected.reasons.get(depIdent)!;
|
|
55
|
+
if (!list.some((r) => r.kind === reason.kind && r.detail === reason.detail)) {
|
|
56
|
+
list.push(reason);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function addRuleReason(
|
|
61
|
+
expected: ExpectedWorkspace,
|
|
62
|
+
depIdent: string,
|
|
63
|
+
rule: ReturnType<typeof getEffectiveRule>,
|
|
64
|
+
): void {
|
|
65
|
+
const details: string[] = [];
|
|
66
|
+
if (rule.section) details.push(`section: ${rule.section}`);
|
|
67
|
+
if (rule.version !== undefined) details.push(`version: ${rule.version}`);
|
|
68
|
+
if (rule.rootOnly) details.push('rootOnly: true');
|
|
69
|
+
if (rule.required) details.push('required: true');
|
|
70
|
+
if (details.length === 0) return;
|
|
71
|
+
|
|
72
|
+
addReason(expected, depIdent, {
|
|
73
|
+
kind: 'rule',
|
|
74
|
+
detail: `${depIdent} matched rule (${details.join(', ')})`,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function setExpectedDependency(
|
|
79
|
+
expected: ExpectedWorkspace,
|
|
80
|
+
section: SectionType,
|
|
81
|
+
depIdent: string,
|
|
82
|
+
range: string,
|
|
83
|
+
reason: Reason,
|
|
84
|
+
): void {
|
|
85
|
+
for (const other of SECTIONS) {
|
|
86
|
+
if (other !== section) expected.sections[other].delete(depIdent);
|
|
87
|
+
}
|
|
88
|
+
expected.sections[section].set(depIdent, range);
|
|
89
|
+
addReason(expected, depIdent, reason);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function addExpectedMirrorDependency(
|
|
93
|
+
expected: ExpectedWorkspace,
|
|
94
|
+
depIdent: string,
|
|
95
|
+
range: string,
|
|
96
|
+
reason: Reason,
|
|
97
|
+
): void {
|
|
98
|
+
expected.sections.devDependencies.set(depIdent, range);
|
|
99
|
+
addReason(expected, depIdent, reason);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function getExpectedSection(
|
|
103
|
+
expected: ExpectedWorkspace,
|
|
104
|
+
depIdent: string,
|
|
105
|
+
): SectionType | null {
|
|
106
|
+
for (const section of SECTIONS) {
|
|
107
|
+
if (expected.sections[section].has(depIdent)) return section;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function getExpectedRange(
|
|
114
|
+
expected: ExpectedWorkspace,
|
|
115
|
+
depIdent: string,
|
|
116
|
+
): string | null {
|
|
117
|
+
const section = getExpectedSection(expected, depIdent);
|
|
118
|
+
|
|
119
|
+
return section ? (expected.sections[section].get(depIdent) ?? null) : null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function getRangeSource(
|
|
123
|
+
root: WorkspaceFacts,
|
|
124
|
+
workspace: WorkspaceFacts,
|
|
125
|
+
depIdent: string,
|
|
126
|
+
rules: DependencyRule[],
|
|
127
|
+
): string {
|
|
128
|
+
const rule = getEffectiveRule(
|
|
129
|
+
rules,
|
|
130
|
+
depIdent,
|
|
131
|
+
workspace.name,
|
|
132
|
+
workspace.location,
|
|
133
|
+
);
|
|
134
|
+
if (rule.version !== undefined) return rule.version;
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
getDeclaredRange(workspace.pkg, depIdent) ??
|
|
138
|
+
getDeclaredRange(root.pkg, depIdent) ??
|
|
139
|
+
'*'
|
|
140
|
+
);
|
|
141
|
+
}
|