@rsdk/yarn.constraints 6.0.0-next.39 → 6.0.0-next.40
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 +452 -0
- package/README.MD +24 -0
- package/__tests__/compatibility.test.ts +321 -0
- package/__tests__/engine.test.ts +1002 -0
- package/__tests__/fixtures/imports/bin.js +4 -0
- package/__tests__/fixtures/imports/export-entry.mjs +1 -0
- package/__tests__/fixtures/imports/root-entry.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__/imports.test.ts +206 -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 +25 -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 +88 -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 +9 -0
- package/dist/lib/imports.js +249 -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/diagnostics.d.ts +4 -0
- package/dist/model/diagnostics.js +273 -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 +205 -0
- package/dist/model/placement.js.map +1 -0
- package/dist/model/rules.d.ts +14 -0
- package/dist/model/rules.js +46 -0
- package/dist/model/rules.js.map +1 -0
- package/dist/model/types.d.ts +117 -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 +73 -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 +26 -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 +99 -0
- package/src/dependency-model.ts +26 -0
- package/src/index.ts +28 -45
- package/src/lib/imports.ts +293 -0
- package/src/lib/package-json.ts +46 -0
- package/src/model/diagnostics.ts +328 -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 +372 -0
- package/src/model/rules.ts +73 -0
- package/src/model/types.ts +164 -0
- package/src/model/versions.ts +109 -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,372 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dependency placement rules.
|
|
3
|
+
*
|
|
4
|
+
* This module converts source and `.d.ts` usage facts into expected manifest
|
|
5
|
+
* sections. It is pure: it receives workspace facts, rules, type metadata, and
|
|
6
|
+
* local workspace names, then mutates only the in-memory expected graph.
|
|
7
|
+
*/
|
|
8
|
+
import {
|
|
9
|
+
addExpectedMirrorDependency,
|
|
10
|
+
addReason,
|
|
11
|
+
addRuleReason,
|
|
12
|
+
getDeclaredRange,
|
|
13
|
+
getRangeSource,
|
|
14
|
+
setExpectedDependency,
|
|
15
|
+
} from './expected';
|
|
16
|
+
import { getEffectiveRule } from './rules';
|
|
17
|
+
import type {
|
|
18
|
+
DependencyRule,
|
|
19
|
+
ExpectedWorkspace,
|
|
20
|
+
Reason,
|
|
21
|
+
WorkspaceFacts,
|
|
22
|
+
} from './types';
|
|
23
|
+
|
|
24
|
+
function isTestFile(file: string): boolean {
|
|
25
|
+
return /(^|[/.])(__test__|__tests__|test|tests|spec|e2e)([/.]|$)|\.(spec|test|e2e)\.[cm]?[tj]sx?$/.test(
|
|
26
|
+
file,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function isExternal(
|
|
31
|
+
depIdent: string,
|
|
32
|
+
workspaceNames: Set<string>,
|
|
33
|
+
): boolean {
|
|
34
|
+
return !workspaceNames.has(depIdent);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getDefinitelyTypedProvider(depIdent: string): string | null {
|
|
38
|
+
if (depIdent.startsWith('@types/')) return null;
|
|
39
|
+
if (depIdent.startsWith('@')) {
|
|
40
|
+
const [scope, name] = depIdent.slice(1).split('/');
|
|
41
|
+
if (!scope || !name) return null;
|
|
42
|
+
return `@types/${scope}__${name}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return `@types/${depIdent}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function shouldAddDefinitelyTypedProvider(
|
|
49
|
+
depIdent: string,
|
|
50
|
+
externalTypeMetadata: Map<string, { hasBundledTypes: boolean }>,
|
|
51
|
+
typeProviderPackages: Set<string>,
|
|
52
|
+
): string | null {
|
|
53
|
+
const provider = getDefinitelyTypedProvider(depIdent);
|
|
54
|
+
if (!provider || !typeProviderPackages.has(provider)) return null;
|
|
55
|
+
if (externalTypeMetadata.get(depIdent)?.hasBundledTypes) return null;
|
|
56
|
+
|
|
57
|
+
return provider;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function getConcreteRuleMatches(rule: DependencyRule): string[] {
|
|
61
|
+
const matches = Array.isArray(rule.match) ? rule.match : [rule.match];
|
|
62
|
+
|
|
63
|
+
return matches.filter((match) => !match.includes('*'));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function addPublicDependency(
|
|
67
|
+
expected: ExpectedWorkspace,
|
|
68
|
+
rootExpected: ExpectedWorkspace,
|
|
69
|
+
root: WorkspaceFacts,
|
|
70
|
+
workspaceNames: Set<string>,
|
|
71
|
+
rules: DependencyRule[],
|
|
72
|
+
depIdent: string,
|
|
73
|
+
reason: Reason,
|
|
74
|
+
options: { allowRootOnly?: boolean } = {},
|
|
75
|
+
): void {
|
|
76
|
+
const workspace = expected.workspace;
|
|
77
|
+
const rule = getEffectiveRule(
|
|
78
|
+
rules,
|
|
79
|
+
depIdent,
|
|
80
|
+
workspace.name,
|
|
81
|
+
workspace.location,
|
|
82
|
+
);
|
|
83
|
+
const isLocal = workspaceNames.has(depIdent);
|
|
84
|
+
const range = isLocal
|
|
85
|
+
? 'workspace:*'
|
|
86
|
+
: getRangeSource(root, workspace, depIdent, rules);
|
|
87
|
+
|
|
88
|
+
addRuleReason(expected, depIdent, rule);
|
|
89
|
+
|
|
90
|
+
if (rule.rootOnly && !isLocal && !options.allowRootOnly) {
|
|
91
|
+
setExpectedDependency(rootExpected, 'devDependencies', depIdent, range, {
|
|
92
|
+
kind: 'root-only',
|
|
93
|
+
detail: `${depIdent} is marked rootOnly`,
|
|
94
|
+
});
|
|
95
|
+
addReason(expected, depIdent, reason);
|
|
96
|
+
addReason(expected, depIdent, {
|
|
97
|
+
kind: 'root-only',
|
|
98
|
+
detail: `${depIdent} is marked rootOnly and cannot be used by ${workspace.name}`,
|
|
99
|
+
});
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (isLocal) {
|
|
104
|
+
setExpectedDependency(expected, 'dependencies', depIdent, 'workspace:*', {
|
|
105
|
+
kind: reason.kind,
|
|
106
|
+
detail: `${reason.detail}; local workspace edge`,
|
|
107
|
+
});
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (
|
|
112
|
+
workspace.role === 'library' &&
|
|
113
|
+
rule.section !== 'dependencies' &&
|
|
114
|
+
rule.section !== 'devDependencies'
|
|
115
|
+
) {
|
|
116
|
+
setExpectedDependency(
|
|
117
|
+
expected,
|
|
118
|
+
'peerDependencies',
|
|
119
|
+
depIdent,
|
|
120
|
+
range,
|
|
121
|
+
reason,
|
|
122
|
+
);
|
|
123
|
+
addExpectedMirrorDependency(expected, depIdent, range, {
|
|
124
|
+
kind: 'mirror',
|
|
125
|
+
detail: `${depIdent} mirrors peerDependencies in devDependencies`,
|
|
126
|
+
});
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
setExpectedDependency(expected, 'dependencies', depIdent, range, reason);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function addPrivateDependency(
|
|
134
|
+
rootExpected: ExpectedWorkspace,
|
|
135
|
+
root: WorkspaceFacts,
|
|
136
|
+
requester: ExpectedWorkspace,
|
|
137
|
+
workspaceNames: Set<string>,
|
|
138
|
+
rules: DependencyRule[],
|
|
139
|
+
depIdent: string,
|
|
140
|
+
reason: Reason,
|
|
141
|
+
): void {
|
|
142
|
+
const workspace = requester.workspace;
|
|
143
|
+
if (workspaceNames.has(depIdent)) return;
|
|
144
|
+
const rule = getEffectiveRule(
|
|
145
|
+
rules,
|
|
146
|
+
depIdent,
|
|
147
|
+
workspace.name,
|
|
148
|
+
workspace.location,
|
|
149
|
+
);
|
|
150
|
+
const range = getRangeSource(root, workspace, depIdent, rules);
|
|
151
|
+
|
|
152
|
+
addRuleReason(rootExpected, depIdent, rule);
|
|
153
|
+
addRuleReason(requester, depIdent, rule);
|
|
154
|
+
|
|
155
|
+
if (rule.rootOnly) {
|
|
156
|
+
addReason(requester, depIdent, {
|
|
157
|
+
kind: 'root-only',
|
|
158
|
+
detail: `${depIdent} is marked rootOnly and cannot be used by ${workspace.name}`,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
setExpectedDependency(
|
|
163
|
+
rootExpected,
|
|
164
|
+
'devDependencies',
|
|
165
|
+
depIdent,
|
|
166
|
+
range,
|
|
167
|
+
reason,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function seedRootDevDependencies(
|
|
172
|
+
expectedByLocation: Map<string, ExpectedWorkspace>,
|
|
173
|
+
workspaceNames: Set<string>,
|
|
174
|
+
rules: DependencyRule[],
|
|
175
|
+
): void {
|
|
176
|
+
const rootExpected = expectedByLocation.get('.')!;
|
|
177
|
+
|
|
178
|
+
for (const [depIdent, declaredRange] of Object.entries(
|
|
179
|
+
rootExpected.workspace.pkg.devDependencies ?? {},
|
|
180
|
+
)) {
|
|
181
|
+
const rule = getEffectiveRule(
|
|
182
|
+
rules,
|
|
183
|
+
depIdent,
|
|
184
|
+
rootExpected.workspace.name,
|
|
185
|
+
rootExpected.workspace.location,
|
|
186
|
+
);
|
|
187
|
+
const range = workspaceNames.has(depIdent)
|
|
188
|
+
? 'workspace:*'
|
|
189
|
+
: (rule.version ?? declaredRange);
|
|
190
|
+
|
|
191
|
+
setExpectedDependency(rootExpected, 'devDependencies', depIdent, range, {
|
|
192
|
+
kind: 'declared',
|
|
193
|
+
detail: `${depIdent} is declared in root devDependencies baseline`,
|
|
194
|
+
});
|
|
195
|
+
addRuleReason(rootExpected, depIdent, rule);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function addUsageDependencies(
|
|
200
|
+
expectedByLocation: Map<string, ExpectedWorkspace>,
|
|
201
|
+
root: WorkspaceFacts,
|
|
202
|
+
workspaceNames: Set<string>,
|
|
203
|
+
rules: DependencyRule[],
|
|
204
|
+
externalTypeMetadata: Map<string, { hasBundledTypes: boolean }>,
|
|
205
|
+
typeProviderPackages: Set<string>,
|
|
206
|
+
): void {
|
|
207
|
+
const rootExpected = expectedByLocation.get('.')!;
|
|
208
|
+
|
|
209
|
+
for (const expected of expectedByLocation.values()) {
|
|
210
|
+
const workspace = expected.workspace;
|
|
211
|
+
if (workspace.isRoot) continue;
|
|
212
|
+
|
|
213
|
+
for (const [depIdent, usage] of workspace.sourceUsage) {
|
|
214
|
+
const publicType = workspace.dtsImports.has(depIdent);
|
|
215
|
+
const runtime = [...usage.runtimeFiles].some((file) => !isTestFile(file));
|
|
216
|
+
const allTestOnly = [...usage.files].every(isTestFile);
|
|
217
|
+
|
|
218
|
+
if (runtime || publicType) {
|
|
219
|
+
addPublicDependency(
|
|
220
|
+
expected,
|
|
221
|
+
rootExpected,
|
|
222
|
+
root,
|
|
223
|
+
workspaceNames,
|
|
224
|
+
rules,
|
|
225
|
+
depIdent,
|
|
226
|
+
{
|
|
227
|
+
kind: publicType ? 'source-public-type' : 'source-runtime',
|
|
228
|
+
detail: publicType
|
|
229
|
+
? `${depIdent} appears in emitted .d.ts`
|
|
230
|
+
: `${depIdent} is imported at runtime`,
|
|
231
|
+
},
|
|
232
|
+
);
|
|
233
|
+
} else {
|
|
234
|
+
addPrivateDependency(
|
|
235
|
+
rootExpected,
|
|
236
|
+
root,
|
|
237
|
+
expected,
|
|
238
|
+
workspaceNames,
|
|
239
|
+
rules,
|
|
240
|
+
depIdent,
|
|
241
|
+
{
|
|
242
|
+
kind: 'source-private-dev',
|
|
243
|
+
detail: allTestOnly
|
|
244
|
+
? `${depIdent} is only used by test files in ${workspace.name}`
|
|
245
|
+
: `${depIdent} is only used as private type/tooling dependency in ${workspace.name}`,
|
|
246
|
+
},
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
for (const depIdent of workspace.dtsImports) {
|
|
252
|
+
addPublicDependency(
|
|
253
|
+
expected,
|
|
254
|
+
rootExpected,
|
|
255
|
+
root,
|
|
256
|
+
workspaceNames,
|
|
257
|
+
rules,
|
|
258
|
+
depIdent,
|
|
259
|
+
{
|
|
260
|
+
kind: 'source-public-type',
|
|
261
|
+
detail: `${depIdent} appears in emitted .d.ts`,
|
|
262
|
+
},
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
const typeProvider = shouldAddDefinitelyTypedProvider(
|
|
266
|
+
depIdent,
|
|
267
|
+
externalTypeMetadata,
|
|
268
|
+
typeProviderPackages,
|
|
269
|
+
);
|
|
270
|
+
if (!typeProvider) continue;
|
|
271
|
+
|
|
272
|
+
addPublicDependency(
|
|
273
|
+
expected,
|
|
274
|
+
rootExpected,
|
|
275
|
+
root,
|
|
276
|
+
workspaceNames,
|
|
277
|
+
rules,
|
|
278
|
+
typeProvider,
|
|
279
|
+
{
|
|
280
|
+
kind: 'dts-provider',
|
|
281
|
+
detail: `${typeProvider} provides public types for ${depIdent}`,
|
|
282
|
+
},
|
|
283
|
+
{ allowRootOnly: true },
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function addRequiredRuleDependencies(
|
|
290
|
+
expectedByLocation: Map<string, ExpectedWorkspace>,
|
|
291
|
+
root: WorkspaceFacts,
|
|
292
|
+
workspaceNames: Set<string>,
|
|
293
|
+
rules: DependencyRule[],
|
|
294
|
+
): void {
|
|
295
|
+
const rootExpected = expectedByLocation.get('.')!;
|
|
296
|
+
const candidates = new Set<string>();
|
|
297
|
+
|
|
298
|
+
for (const rule of rules) {
|
|
299
|
+
if (rule.required === undefined) continue;
|
|
300
|
+
for (const depIdent of getConcreteRuleMatches(rule)) {
|
|
301
|
+
candidates.add(depIdent);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
for (const expected of expectedByLocation.values()) {
|
|
306
|
+
const workspace = expected.workspace;
|
|
307
|
+
if (workspace.isRoot) continue;
|
|
308
|
+
|
|
309
|
+
for (const depIdent of candidates) {
|
|
310
|
+
const rule = getEffectiveRule(
|
|
311
|
+
rules,
|
|
312
|
+
depIdent,
|
|
313
|
+
workspace.name,
|
|
314
|
+
workspace.location,
|
|
315
|
+
);
|
|
316
|
+
if (!rule.required) continue;
|
|
317
|
+
|
|
318
|
+
addPublicDependency(
|
|
319
|
+
expected,
|
|
320
|
+
rootExpected,
|
|
321
|
+
root,
|
|
322
|
+
workspaceNames,
|
|
323
|
+
rules,
|
|
324
|
+
depIdent,
|
|
325
|
+
{
|
|
326
|
+
kind: 'required-rule',
|
|
327
|
+
detail: `${depIdent} is marked required by dependency rule`,
|
|
328
|
+
},
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export function rebuildLibraryMirrors(
|
|
335
|
+
expectedByLocation: Map<string, ExpectedWorkspace>,
|
|
336
|
+
): void {
|
|
337
|
+
for (const expected of expectedByLocation.values()) {
|
|
338
|
+
if (expected.workspace.role !== 'library') continue;
|
|
339
|
+
expected.sections.devDependencies = new Map(
|
|
340
|
+
expected.sections.peerDependencies,
|
|
341
|
+
);
|
|
342
|
+
for (const [depIdent] of expected.sections.devDependencies) {
|
|
343
|
+
addReason(expected, depIdent, {
|
|
344
|
+
kind: 'mirror',
|
|
345
|
+
detail: `${depIdent} mirrors peerDependencies in devDependencies`,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export function resolveExpectedRangeAfterRulePass(
|
|
352
|
+
root: WorkspaceFacts,
|
|
353
|
+
expected: ExpectedWorkspace,
|
|
354
|
+
workspaceNames: Set<string>,
|
|
355
|
+
rules: DependencyRule[],
|
|
356
|
+
depIdent: string,
|
|
357
|
+
currentRange: string,
|
|
358
|
+
): string {
|
|
359
|
+
const rule = getEffectiveRule(
|
|
360
|
+
rules,
|
|
361
|
+
depIdent,
|
|
362
|
+
expected.workspace.name,
|
|
363
|
+
expected.workspace.location,
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
return workspaceNames.has(depIdent)
|
|
367
|
+
? 'workspace:*'
|
|
368
|
+
: (rule.version ??
|
|
369
|
+
getDeclaredRange(expected.workspace.pkg, depIdent) ??
|
|
370
|
+
getDeclaredRange(root.pkg, depIdent) ??
|
|
371
|
+
currentRange);
|
|
372
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule matching and precedence.
|
|
3
|
+
*
|
|
4
|
+
* Dependency rules are the explicit override surface for the model: versions,
|
|
5
|
+
* section overrides, root-only constraints, and workspace-specific exceptions.
|
|
6
|
+
* The helpers here are pure and apply the documented "last matching rule wins"
|
|
7
|
+
* semantics.
|
|
8
|
+
*/
|
|
9
|
+
import type { DependencyRule, EffectiveRule, WorkspaceRole } from './types';
|
|
10
|
+
|
|
11
|
+
export function matchesGlob(value: string, pattern: string): boolean {
|
|
12
|
+
if (!pattern.includes('*')) return value === pattern;
|
|
13
|
+
const escaped = pattern.replaceAll(
|
|
14
|
+
/[-[\]{}()+?.,\\^$|#\s]/g,
|
|
15
|
+
String.raw`\$&`,
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
return new RegExp(
|
|
19
|
+
'^' + escaped.replaceAll('**', '.*').replaceAll('*', '[^/]*') + '$',
|
|
20
|
+
).test(value);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function matchesPatterns(
|
|
24
|
+
value: string,
|
|
25
|
+
pattern: string | string[],
|
|
26
|
+
): boolean {
|
|
27
|
+
const patterns = Array.isArray(pattern) ? pattern : [pattern];
|
|
28
|
+
|
|
29
|
+
return patterns.some((p) => matchesGlob(value, p));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getEffectiveRule(
|
|
33
|
+
rules: DependencyRule[],
|
|
34
|
+
depIdent: string,
|
|
35
|
+
wsIdent: string,
|
|
36
|
+
wsCwd: string,
|
|
37
|
+
): EffectiveRule {
|
|
38
|
+
const effective: EffectiveRule = { rootOnly: false };
|
|
39
|
+
|
|
40
|
+
for (const rule of rules) {
|
|
41
|
+
if (!matchesPatterns(depIdent, rule.match)) continue;
|
|
42
|
+
if (
|
|
43
|
+
rule.workspace &&
|
|
44
|
+
!matchesPatterns(wsIdent, rule.workspace) &&
|
|
45
|
+
!matchesPatterns(wsCwd, rule.workspace)
|
|
46
|
+
)
|
|
47
|
+
continue;
|
|
48
|
+
if (rule.section) effective.section = rule.section;
|
|
49
|
+
if (rule.version !== undefined) effective.version = rule.version;
|
|
50
|
+
if (rule.rootOnly !== undefined) effective.rootOnly = rule.rootOnly;
|
|
51
|
+
if (rule.required !== undefined) effective.required = rule.required;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return effective;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function hasGlobalVersionRule(
|
|
58
|
+
rules: DependencyRule[],
|
|
59
|
+
depIdent: string,
|
|
60
|
+
): boolean {
|
|
61
|
+
return rules.some(
|
|
62
|
+
(rule) =>
|
|
63
|
+
!rule.workspace &&
|
|
64
|
+
rule.version !== undefined &&
|
|
65
|
+
matchesPatterns(depIdent, rule.match),
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function isWorkspaceRole(
|
|
70
|
+
value: string | undefined,
|
|
71
|
+
): value is WorkspaceRole {
|
|
72
|
+
return value === 'library' || value === 'service' || value === 'cli';
|
|
73
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared dependency-model contracts.
|
|
3
|
+
*
|
|
4
|
+
* This module defines the data exchanged between collectors, the pure model
|
|
5
|
+
* engine, the manifest writer, and CLI reporting. It intentionally contains no
|
|
6
|
+
* filesystem or Yarn logic; callers should pass already collected facts.
|
|
7
|
+
*/
|
|
8
|
+
import type { PackageJson } from '../lib/package-json';
|
|
9
|
+
|
|
10
|
+
export type SectionType =
|
|
11
|
+
| 'dependencies'
|
|
12
|
+
| 'peerDependencies'
|
|
13
|
+
| 'devDependencies';
|
|
14
|
+
|
|
15
|
+
export type WorkspaceRole = 'library' | 'service' | 'cli';
|
|
16
|
+
|
|
17
|
+
export interface DependencyRule {
|
|
18
|
+
match: string | string[];
|
|
19
|
+
workspace?: string | string[];
|
|
20
|
+
section?: SectionType;
|
|
21
|
+
version?: string;
|
|
22
|
+
rootOnly?: boolean;
|
|
23
|
+
required?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface DependencyModelConfig {
|
|
27
|
+
version?: number;
|
|
28
|
+
rules?: DependencyRule[];
|
|
29
|
+
doctor?: DoctorConfig;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface DoctorConfig {
|
|
33
|
+
buildCmd?: string;
|
|
34
|
+
lintCmd?: string;
|
|
35
|
+
typecheckCmd?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface DependencyModelOptions {
|
|
39
|
+
rootDir?: string;
|
|
40
|
+
constraintsPath?: string;
|
|
41
|
+
fix?: boolean;
|
|
42
|
+
withDts?: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface DependencyViolation {
|
|
46
|
+
code:
|
|
47
|
+
| 'root-private'
|
|
48
|
+
| 'root-section'
|
|
49
|
+
| 'role-missing'
|
|
50
|
+
| 'role-invalid'
|
|
51
|
+
| 'forbidden-section'
|
|
52
|
+
| 'missing'
|
|
53
|
+
| 'wrong-section'
|
|
54
|
+
| 'wrong-range'
|
|
55
|
+
| 'root-only'
|
|
56
|
+
| 'root-only-usage'
|
|
57
|
+
| 'unconstrained-version'
|
|
58
|
+
| 'mirror'
|
|
59
|
+
| 'stale'
|
|
60
|
+
| 'dist-missing';
|
|
61
|
+
workspace: string;
|
|
62
|
+
workspaceLocation: string;
|
|
63
|
+
dependency?: string | undefined;
|
|
64
|
+
actualSection?: SectionType | undefined;
|
|
65
|
+
expectedSection?: SectionType | undefined;
|
|
66
|
+
actualRange?: string | undefined;
|
|
67
|
+
expectedRange?: string | undefined;
|
|
68
|
+
message: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface DependencyWarning {
|
|
72
|
+
code: 'bin-role-mismatch';
|
|
73
|
+
workspace?: string | undefined;
|
|
74
|
+
workspaceLocation?: string | undefined;
|
|
75
|
+
dependency?: string | undefined;
|
|
76
|
+
message: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface UsageSummary {
|
|
80
|
+
files: Set<string>;
|
|
81
|
+
runtimeFiles: Set<string>;
|
|
82
|
+
typeOnlyFiles: Set<string>;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface WorkspaceFacts {
|
|
86
|
+
name: string;
|
|
87
|
+
location: string;
|
|
88
|
+
pkg: PackageJson;
|
|
89
|
+
role: WorkspaceRole | undefined;
|
|
90
|
+
isRoot: boolean;
|
|
91
|
+
sourceUsage: Map<string, UsageSummary>;
|
|
92
|
+
dtsImports: Set<string>;
|
|
93
|
+
hasSrc: boolean;
|
|
94
|
+
hasDist: boolean;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface WorkspaceContext extends WorkspaceFacts {
|
|
98
|
+
dir: string;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface DependencyModelFacts {
|
|
102
|
+
workspaces: WorkspaceFacts[];
|
|
103
|
+
externalPeerMetadata: Map<string, Map<string, string> | null>;
|
|
104
|
+
externalTypeMetadata?: Map<string, { hasBundledTypes: boolean }>;
|
|
105
|
+
typeProviderPackages?: Set<string>;
|
|
106
|
+
rules: DependencyRule[];
|
|
107
|
+
withDts?: boolean;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface DependencyModelOutput {
|
|
111
|
+
violations: DependencyViolation[];
|
|
112
|
+
warnings: DependencyWarning[];
|
|
113
|
+
expected: Map<string, ExpectedWorkspace>;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface DependencyModelResult {
|
|
117
|
+
rootDir: string;
|
|
118
|
+
violations: DependencyViolation[];
|
|
119
|
+
warnings: DependencyWarning[];
|
|
120
|
+
contexts: WorkspaceContext[];
|
|
121
|
+
expected: Map<string, ExpectedWorkspace>;
|
|
122
|
+
config: DependencyModelConfig;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface Reason {
|
|
126
|
+
kind:
|
|
127
|
+
| 'declared'
|
|
128
|
+
| 'source-runtime'
|
|
129
|
+
| 'source-public-type'
|
|
130
|
+
| 'source-private-dev'
|
|
131
|
+
| 'rule'
|
|
132
|
+
| 'root-only'
|
|
133
|
+
| 'dts-provider'
|
|
134
|
+
| 'peer-propagation'
|
|
135
|
+
| 'mirror'
|
|
136
|
+
| 'version'
|
|
137
|
+
| 'required-rule';
|
|
138
|
+
detail: string;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface ExpectedWorkspace {
|
|
142
|
+
workspace: WorkspaceFacts;
|
|
143
|
+
sections: Record<SectionType, Map<string, string>>;
|
|
144
|
+
reasons: Map<string, Reason[]>;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export interface EffectiveRule {
|
|
148
|
+
section?: SectionType;
|
|
149
|
+
version?: string;
|
|
150
|
+
rootOnly: boolean;
|
|
151
|
+
required?: boolean;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export interface PackageExtension {
|
|
155
|
+
dependencies?: Record<string, string>;
|
|
156
|
+
peerDependencies?: Record<string, string>;
|
|
157
|
+
peerDependenciesMeta?: Record<string, { optional?: boolean }>;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export const SECTIONS: SectionType[] = [
|
|
161
|
+
'dependencies',
|
|
162
|
+
'peerDependencies',
|
|
163
|
+
'devDependencies',
|
|
164
|
+
];
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version normalization and version-rule diagnostics.
|
|
3
|
+
*
|
|
4
|
+
* The placement engine decides where dependencies belong. This module applies
|
|
5
|
+
* version rules after placement and checks the invariant that repeated external
|
|
6
|
+
* dependencies have a global version rule.
|
|
7
|
+
*/
|
|
8
|
+
import { addReason } from './expected';
|
|
9
|
+
import { isExternal, resolveExpectedRangeAfterRulePass } from './placement';
|
|
10
|
+
import { hasGlobalVersionRule } from './rules';
|
|
11
|
+
import type {
|
|
12
|
+
DependencyRule,
|
|
13
|
+
DependencyViolation,
|
|
14
|
+
ExpectedWorkspace,
|
|
15
|
+
SectionType,
|
|
16
|
+
WorkspaceFacts,
|
|
17
|
+
} from './types';
|
|
18
|
+
import { SECTIONS } from './types';
|
|
19
|
+
|
|
20
|
+
export function applyVersionRules(
|
|
21
|
+
expectedByLocation: Map<string, ExpectedWorkspace>,
|
|
22
|
+
root: WorkspaceFacts,
|
|
23
|
+
workspaceNames: Set<string>,
|
|
24
|
+
rules: DependencyRule[],
|
|
25
|
+
): void {
|
|
26
|
+
for (const expected of expectedByLocation.values()) {
|
|
27
|
+
for (const section of SECTIONS) {
|
|
28
|
+
for (const [depIdent, range] of expected.sections[section]) {
|
|
29
|
+
const nextRange = resolveExpectedRangeAfterRulePass(
|
|
30
|
+
root,
|
|
31
|
+
expected,
|
|
32
|
+
workspaceNames,
|
|
33
|
+
rules,
|
|
34
|
+
depIdent,
|
|
35
|
+
range,
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
if (nextRange !== range) {
|
|
39
|
+
expected.sections[section].set(depIdent, nextRange);
|
|
40
|
+
addReason(expected, depIdent, {
|
|
41
|
+
kind: 'version',
|
|
42
|
+
detail: `${depIdent} range resolved to ${nextRange}`,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function collectUnconstrainedVersionViolations(
|
|
51
|
+
workspaces: WorkspaceFacts[],
|
|
52
|
+
expectedByLocation: Map<string, ExpectedWorkspace>,
|
|
53
|
+
rules: DependencyRule[],
|
|
54
|
+
): DependencyViolation[] {
|
|
55
|
+
const workspaceNames = new Set(
|
|
56
|
+
workspaces.filter((ws) => !ws.isRoot).map((ws) => ws.name),
|
|
57
|
+
);
|
|
58
|
+
const occurrences = new Map<string, Set<string>>();
|
|
59
|
+
const ranges = new Map<string, Set<string>>();
|
|
60
|
+
|
|
61
|
+
const addOccurrence = (
|
|
62
|
+
workspace: WorkspaceFacts,
|
|
63
|
+
section: SectionType,
|
|
64
|
+
depIdent: string,
|
|
65
|
+
range: string,
|
|
66
|
+
): void => {
|
|
67
|
+
if (!isExternal(depIdent, workspaceNames) || range.startsWith('workspace:'))
|
|
68
|
+
return;
|
|
69
|
+
if (!occurrences.has(depIdent)) occurrences.set(depIdent, new Set());
|
|
70
|
+
if (!ranges.has(depIdent)) ranges.set(depIdent, new Set());
|
|
71
|
+
occurrences.get(depIdent)!.add(`${workspace.location}:${section}`);
|
|
72
|
+
ranges.get(depIdent)!.add(range);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
for (const workspace of workspaces) {
|
|
76
|
+
for (const section of SECTIONS) {
|
|
77
|
+
for (const [depIdent, range] of Object.entries(
|
|
78
|
+
workspace.pkg[section] ?? {},
|
|
79
|
+
)) {
|
|
80
|
+
addOccurrence(workspace, section, depIdent, range);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
for (const expected of expectedByLocation.values()) {
|
|
86
|
+
for (const section of SECTIONS) {
|
|
87
|
+
for (const [depIdent, range] of expected.sections[section]) {
|
|
88
|
+
addOccurrence(expected.workspace, section, depIdent, range);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return [...occurrences.entries()]
|
|
94
|
+
.filter(
|
|
95
|
+
([depIdent, byOccurrence]) =>
|
|
96
|
+
byOccurrence.size > 1 && !hasGlobalVersionRule(rules, depIdent),
|
|
97
|
+
)
|
|
98
|
+
.map(([depIdent, byOccurrence]) => {
|
|
99
|
+
const rangeList = [...(ranges.get(depIdent) ?? [])].join(', ');
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
code: 'unconstrained-version' as const,
|
|
103
|
+
workspace: '<root>',
|
|
104
|
+
workspaceLocation: '.',
|
|
105
|
+
dependency: depIdent,
|
|
106
|
+
message: `${depIdent} appears ${byOccurrence.size} times without a global version rule (${rangeList})`,
|
|
107
|
+
};
|
|
108
|
+
});
|
|
109
|
+
}
|