@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,1002 @@
|
|
|
1
|
+
import {
|
|
2
|
+
deriveDependencyModel,
|
|
3
|
+
type DependencyModelFacts,
|
|
4
|
+
type WorkspaceFacts,
|
|
5
|
+
type UsageSummary,
|
|
6
|
+
} from '../src/dependency-model';
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Helpers
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
function makeRoot(pkg?: WorkspaceFacts['pkg']): WorkspaceFacts {
|
|
13
|
+
return {
|
|
14
|
+
name: '@test/root',
|
|
15
|
+
location: '.',
|
|
16
|
+
pkg: pkg ?? {
|
|
17
|
+
name: '@test/root',
|
|
18
|
+
private: true,
|
|
19
|
+
},
|
|
20
|
+
role: undefined,
|
|
21
|
+
isRoot: true,
|
|
22
|
+
sourceUsage: new Map(),
|
|
23
|
+
dtsImports: new Set(),
|
|
24
|
+
hasSrc: false,
|
|
25
|
+
hasDist: false,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function makeWorkspace(
|
|
30
|
+
name: string,
|
|
31
|
+
location: string,
|
|
32
|
+
role: WorkspaceFacts['role'],
|
|
33
|
+
pkg?: WorkspaceFacts['pkg'],
|
|
34
|
+
sourceUsage: Map<string, UsageSummary> = new Map(),
|
|
35
|
+
dtsImports: Set<string> = new Set(),
|
|
36
|
+
hasSrc = false,
|
|
37
|
+
hasDist = false,
|
|
38
|
+
): WorkspaceFacts {
|
|
39
|
+
const basePkg: WorkspaceFacts['pkg'] = { name };
|
|
40
|
+
if (role !== undefined) basePkg.role = role;
|
|
41
|
+
return {
|
|
42
|
+
name,
|
|
43
|
+
location,
|
|
44
|
+
pkg: pkg ?? basePkg,
|
|
45
|
+
role,
|
|
46
|
+
isRoot: false,
|
|
47
|
+
sourceUsage,
|
|
48
|
+
dtsImports,
|
|
49
|
+
hasSrc,
|
|
50
|
+
hasDist,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function runtimeUsage(files: string[]): UsageSummary {
|
|
55
|
+
return {
|
|
56
|
+
files: new Set(files),
|
|
57
|
+
runtimeFiles: new Set(files),
|
|
58
|
+
typeOnlyFiles: new Set(),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function typeOnlyUsage(files: string[]): UsageSummary {
|
|
63
|
+
return {
|
|
64
|
+
files: new Set(files),
|
|
65
|
+
runtimeFiles: new Set(),
|
|
66
|
+
typeOnlyFiles: new Set(files),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function emptyFacts(workspaces: WorkspaceFacts[], rules: DependencyModelFacts['rules'] = []): DependencyModelFacts {
|
|
71
|
+
return {
|
|
72
|
+
workspaces,
|
|
73
|
+
externalPeerMetadata: new Map(),
|
|
74
|
+
rules,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// Tests
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
describe('deriveDependencyModel engine', () => {
|
|
83
|
+
// 1. root-shape
|
|
84
|
+
describe('root-shape', () => {
|
|
85
|
+
it('reports root-private when root lacks private: true', () => {
|
|
86
|
+
const root = makeRoot({ name: '@test/root' }); // no private field
|
|
87
|
+
const facts = emptyFacts([root]);
|
|
88
|
+
const result = deriveDependencyModel(facts);
|
|
89
|
+
expect(result.violations.some((v) => v.code === 'root-private')).toBe(true);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('reports root-section when root has dependencies section', () => {
|
|
93
|
+
const root = makeRoot({
|
|
94
|
+
name: '@test/root',
|
|
95
|
+
private: true,
|
|
96
|
+
dependencies: { lodash: '^4.0.0' },
|
|
97
|
+
});
|
|
98
|
+
const facts = emptyFacts([root]);
|
|
99
|
+
const result = deriveDependencyModel(facts);
|
|
100
|
+
expect(result.violations.some((v) => v.code === 'root-section')).toBe(true);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('preserves root devDependencies as the shared dev baseline', () => {
|
|
104
|
+
const root = makeRoot({
|
|
105
|
+
name: '@test/root',
|
|
106
|
+
private: true,
|
|
107
|
+
devDependencies: { typescript: '5.7.3' },
|
|
108
|
+
});
|
|
109
|
+
const result = deriveDependencyModel(emptyFacts([root]));
|
|
110
|
+
const exp = result.expected.get('.')!;
|
|
111
|
+
|
|
112
|
+
expect(exp.sections.devDependencies.get('typescript')).toBe('5.7.3');
|
|
113
|
+
expect(
|
|
114
|
+
result.violations.some((v) => v.dependency === 'typescript'),
|
|
115
|
+
).toBe(false);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// 2. role-required
|
|
120
|
+
describe('role-required', () => {
|
|
121
|
+
it('reports role-missing when workspace has no role field', () => {
|
|
122
|
+
const root = makeRoot();
|
|
123
|
+
const ws = makeWorkspace('@test/noop', 'packages/noop', undefined, { name: '@test/noop' });
|
|
124
|
+
const facts = emptyFacts([root, ws]);
|
|
125
|
+
const result = deriveDependencyModel(facts);
|
|
126
|
+
expect(result.violations.some((v) => v.code === 'role-missing' && v.workspaceLocation === 'packages/noop')).toBe(true);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('reports role-invalid when workspace role is unrecognised', () => {
|
|
130
|
+
const root = makeRoot();
|
|
131
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
132
|
+
const ws = makeWorkspace('@test/noop', 'packages/noop', undefined, { name: '@test/noop', role: 'invalid' as any });
|
|
133
|
+
const facts = emptyFacts([root, ws]);
|
|
134
|
+
const result = deriveDependencyModel(facts);
|
|
135
|
+
expect(result.violations.some((v) => v.code === 'role-invalid')).toBe(true);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// 3. library-external-default-peer: engine puts runtime import in peerDependencies
|
|
140
|
+
describe('library-external-default-peer', () => {
|
|
141
|
+
it('places runtime-imported external dep in peerDependencies for library', () => {
|
|
142
|
+
const root = makeRoot();
|
|
143
|
+
const ws = makeWorkspace(
|
|
144
|
+
'@test/lib',
|
|
145
|
+
'packages/lib',
|
|
146
|
+
'library',
|
|
147
|
+
undefined,
|
|
148
|
+
new Map([['lodash', runtimeUsage(['src/index.ts'])]]),
|
|
149
|
+
);
|
|
150
|
+
const facts = emptyFacts([root, ws], [
|
|
151
|
+
{ match: 'lodash', version: '^4.17.21' },
|
|
152
|
+
]);
|
|
153
|
+
const result = deriveDependencyModel(facts);
|
|
154
|
+
const exp = result.expected.get('packages/lib')!;
|
|
155
|
+
expect(exp.sections.peerDependencies.get('lodash')).toBe('^4.17.21');
|
|
156
|
+
expect(exp.sections.devDependencies.get('lodash')).toBe('^4.17.21'); // mirror
|
|
157
|
+
expect(exp.sections.dependencies.has('lodash')).toBe(false);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// 4. library-implementation-override: rule forces section: dependencies
|
|
162
|
+
describe('library-implementation-override', () => {
|
|
163
|
+
it('respects section:dependencies rule override for library', () => {
|
|
164
|
+
const root = makeRoot();
|
|
165
|
+
const ws = makeWorkspace(
|
|
166
|
+
'@test/lib',
|
|
167
|
+
'packages/lib',
|
|
168
|
+
'library',
|
|
169
|
+
undefined,
|
|
170
|
+
new Map([['some-lib', runtimeUsage(['src/index.ts'])]]),
|
|
171
|
+
);
|
|
172
|
+
const facts = emptyFacts([root, ws], [
|
|
173
|
+
{ match: 'some-lib', section: 'dependencies', version: '1.0.0' },
|
|
174
|
+
]);
|
|
175
|
+
const result = deriveDependencyModel(facts);
|
|
176
|
+
const exp = result.expected.get('packages/lib')!;
|
|
177
|
+
expect(exp.sections.dependencies.get('some-lib')).toBe('1.0.0');
|
|
178
|
+
expect(exp.sections.peerDependencies.has('some-lib')).toBe(false);
|
|
179
|
+
expect(exp.sections.devDependencies.has('some-lib')).toBe(false);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it(
|
|
183
|
+
'reports stale peerDependenciesMeta when a rule moves a peer to dependencies',
|
|
184
|
+
() => {
|
|
185
|
+
const root = makeRoot();
|
|
186
|
+
const ws = makeWorkspace(
|
|
187
|
+
'@test/lib',
|
|
188
|
+
'packages/lib',
|
|
189
|
+
'library',
|
|
190
|
+
{
|
|
191
|
+
name: '@test/lib',
|
|
192
|
+
role: 'library',
|
|
193
|
+
dependencies: { 'some-lib': '1.0.0' },
|
|
194
|
+
peerDependenciesMeta: { 'some-lib': { optional: true } },
|
|
195
|
+
},
|
|
196
|
+
new Map([['some-lib', runtimeUsage(['src/index.ts'])]]),
|
|
197
|
+
);
|
|
198
|
+
const facts = emptyFacts([root, ws], [
|
|
199
|
+
{ match: 'some-lib', section: 'dependencies', version: '1.0.0' },
|
|
200
|
+
]);
|
|
201
|
+
const result = deriveDependencyModel(facts);
|
|
202
|
+
|
|
203
|
+
expect(
|
|
204
|
+
result.violations.some(
|
|
205
|
+
(v) => v.code === 'stale' && v.dependency === 'some-lib',
|
|
206
|
+
),
|
|
207
|
+
).toBe(true);
|
|
208
|
+
},
|
|
209
|
+
);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// 5. service-runtime-deps
|
|
213
|
+
describe('service-runtime-deps', () => {
|
|
214
|
+
it('places runtime-imported external dep in dependencies for service', () => {
|
|
215
|
+
const root = makeRoot();
|
|
216
|
+
const ws = makeWorkspace(
|
|
217
|
+
'@test/svc',
|
|
218
|
+
'packages/svc',
|
|
219
|
+
'service',
|
|
220
|
+
undefined,
|
|
221
|
+
new Map([['express', runtimeUsage(['src/main.ts'])]]),
|
|
222
|
+
);
|
|
223
|
+
const facts = emptyFacts([root, ws], [
|
|
224
|
+
{ match: 'express', version: '^4.0.0' },
|
|
225
|
+
]);
|
|
226
|
+
const result = deriveDependencyModel(facts);
|
|
227
|
+
const exp = result.expected.get('packages/svc')!;
|
|
228
|
+
expect(exp.sections.dependencies.get('express')).toBe('^4.0.0');
|
|
229
|
+
expect(exp.sections.peerDependencies.has('express')).toBe(false);
|
|
230
|
+
expect(exp.sections.devDependencies.has('express')).toBe(false);
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
describe('declaration-independent-expected-graph', () => {
|
|
235
|
+
it('reports a stale workspace dependency when it is declared but unused', () => {
|
|
236
|
+
const root = makeRoot();
|
|
237
|
+
const ws = makeWorkspace(
|
|
238
|
+
'@test/svc',
|
|
239
|
+
'packages/svc',
|
|
240
|
+
'service',
|
|
241
|
+
{
|
|
242
|
+
name: '@test/svc',
|
|
243
|
+
role: 'service',
|
|
244
|
+
dependencies: { lodash: '^4.17.21' },
|
|
245
|
+
},
|
|
246
|
+
);
|
|
247
|
+
const result = deriveDependencyModel(emptyFacts([root, ws], [
|
|
248
|
+
{ match: 'lodash', version: '^4.17.21' },
|
|
249
|
+
]));
|
|
250
|
+
|
|
251
|
+
expect(
|
|
252
|
+
result.violations.some(
|
|
253
|
+
(v) => v.code === 'stale' && v.dependency === 'lodash',
|
|
254
|
+
),
|
|
255
|
+
).toBe(true);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('keeps an unused declared dependency when a rule marks it required', () => {
|
|
259
|
+
const root = makeRoot();
|
|
260
|
+
const ws = makeWorkspace(
|
|
261
|
+
'@test/svc',
|
|
262
|
+
'packages/svc',
|
|
263
|
+
'service',
|
|
264
|
+
{
|
|
265
|
+
name: '@test/svc',
|
|
266
|
+
role: 'service',
|
|
267
|
+
dependencies: { 'indirect-runtime': '^1.0.0' },
|
|
268
|
+
},
|
|
269
|
+
);
|
|
270
|
+
const result = deriveDependencyModel(emptyFacts([root, ws], [
|
|
271
|
+
{ match: 'indirect-runtime', version: '^1.0.0', required: true },
|
|
272
|
+
]));
|
|
273
|
+
const exp = result.expected.get('packages/svc')!;
|
|
274
|
+
|
|
275
|
+
expect(exp.sections.dependencies.get('indirect-runtime')).toBe('^1.0.0');
|
|
276
|
+
expect(
|
|
277
|
+
result.violations.some(
|
|
278
|
+
(v) => v.code === 'stale' && v.dependency === 'indirect-runtime',
|
|
279
|
+
),
|
|
280
|
+
).toBe(false);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('applies required rules only to matching workspaces', () => {
|
|
284
|
+
const root = makeRoot();
|
|
285
|
+
const fastify = makeWorkspace(
|
|
286
|
+
'@test/fastify',
|
|
287
|
+
'packages/fastify',
|
|
288
|
+
'library',
|
|
289
|
+
{
|
|
290
|
+
name: '@test/fastify',
|
|
291
|
+
role: 'library',
|
|
292
|
+
dependencies: { '@fastify/static': '^6.12.0' },
|
|
293
|
+
},
|
|
294
|
+
);
|
|
295
|
+
const other = makeWorkspace(
|
|
296
|
+
'@test/other',
|
|
297
|
+
'packages/other',
|
|
298
|
+
'library',
|
|
299
|
+
{
|
|
300
|
+
name: '@test/other',
|
|
301
|
+
role: 'library',
|
|
302
|
+
dependencies: { '@fastify/static': '^6.12.0' },
|
|
303
|
+
},
|
|
304
|
+
);
|
|
305
|
+
const result = deriveDependencyModel(emptyFacts([root, fastify, other], [
|
|
306
|
+
{
|
|
307
|
+
match: '@fastify/static',
|
|
308
|
+
workspace: '@test/fastify',
|
|
309
|
+
section: 'dependencies',
|
|
310
|
+
version: '^6.12.0',
|
|
311
|
+
required: true,
|
|
312
|
+
},
|
|
313
|
+
]));
|
|
314
|
+
const exp = result.expected.get('packages/fastify')!;
|
|
315
|
+
|
|
316
|
+
expect(exp.sections.dependencies.get('@fastify/static')).toBe('^6.12.0');
|
|
317
|
+
expect(exp.sections.peerDependencies.has('@fastify/static')).toBe(false);
|
|
318
|
+
expect(exp.sections.devDependencies.has('@fastify/static')).toBe(false);
|
|
319
|
+
expect(
|
|
320
|
+
result.violations.some(
|
|
321
|
+
(v) =>
|
|
322
|
+
v.workspace === '@test/other' &&
|
|
323
|
+
v.code === 'stale' &&
|
|
324
|
+
v.dependency === '@fastify/static',
|
|
325
|
+
),
|
|
326
|
+
).toBe(true);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('moves a wrong-section dependency only when usage requires it', () => {
|
|
330
|
+
const root = makeRoot();
|
|
331
|
+
const used = makeWorkspace(
|
|
332
|
+
'@test/used',
|
|
333
|
+
'packages/used',
|
|
334
|
+
'service',
|
|
335
|
+
{
|
|
336
|
+
name: '@test/used',
|
|
337
|
+
role: 'service',
|
|
338
|
+
devDependencies: { express: '^4.0.0' },
|
|
339
|
+
},
|
|
340
|
+
new Map([['express', runtimeUsage(['src/main.ts'])]]),
|
|
341
|
+
);
|
|
342
|
+
const unused = makeWorkspace(
|
|
343
|
+
'@test/unused',
|
|
344
|
+
'packages/unused',
|
|
345
|
+
'service',
|
|
346
|
+
{
|
|
347
|
+
name: '@test/unused',
|
|
348
|
+
role: 'service',
|
|
349
|
+
devDependencies: { express: '^4.0.0' },
|
|
350
|
+
},
|
|
351
|
+
);
|
|
352
|
+
const result = deriveDependencyModel(emptyFacts([root, used, unused], [
|
|
353
|
+
{ match: 'express', version: '^4.0.0' },
|
|
354
|
+
]));
|
|
355
|
+
|
|
356
|
+
expect(
|
|
357
|
+
result.violations.some(
|
|
358
|
+
(v) =>
|
|
359
|
+
v.workspace === '@test/used' &&
|
|
360
|
+
v.code === 'wrong-section' &&
|
|
361
|
+
v.dependency === 'express' &&
|
|
362
|
+
v.expectedSection === 'dependencies',
|
|
363
|
+
),
|
|
364
|
+
).toBe(true);
|
|
365
|
+
expect(
|
|
366
|
+
result.violations.some(
|
|
367
|
+
(v) =>
|
|
368
|
+
v.workspace === '@test/unused' &&
|
|
369
|
+
v.code === 'stale' &&
|
|
370
|
+
v.dependency === 'express',
|
|
371
|
+
),
|
|
372
|
+
).toBe(true);
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
// 6. local-workspace-deps
|
|
377
|
+
describe('local-workspace-deps', () => {
|
|
378
|
+
it('places local workspace dep in dependencies with workspace:*', () => {
|
|
379
|
+
const root = makeRoot();
|
|
380
|
+
const lib = makeWorkspace('@test/shared', 'packages/shared', 'library');
|
|
381
|
+
const svc = makeWorkspace(
|
|
382
|
+
'@test/svc',
|
|
383
|
+
'packages/svc',
|
|
384
|
+
'service',
|
|
385
|
+
undefined,
|
|
386
|
+
new Map([['@test/shared', runtimeUsage(['src/main.ts'])]]),
|
|
387
|
+
);
|
|
388
|
+
const facts = emptyFacts([root, lib, svc]);
|
|
389
|
+
const result = deriveDependencyModel(facts);
|
|
390
|
+
const exp = result.expected.get('packages/svc')!;
|
|
391
|
+
expect(exp.sections.dependencies.get('@test/shared')).toBe('workspace:*');
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
// 7. peer-propagation-local-library
|
|
396
|
+
describe('peer-propagation-local-library', () => {
|
|
397
|
+
it('propagates peerDependencies from local library to consuming service', () => {
|
|
398
|
+
const root = makeRoot();
|
|
399
|
+
// foo is a library with peerDep on react
|
|
400
|
+
const foo = makeWorkspace(
|
|
401
|
+
'@test/foo',
|
|
402
|
+
'packages/foo',
|
|
403
|
+
'library',
|
|
404
|
+
{ name: '@test/foo', role: 'library' },
|
|
405
|
+
new Map([['react', runtimeUsage(['src/index.ts'])]]),
|
|
406
|
+
);
|
|
407
|
+
// bar is a service that depends on foo at runtime
|
|
408
|
+
const bar = makeWorkspace(
|
|
409
|
+
'@test/bar',
|
|
410
|
+
'packages/bar',
|
|
411
|
+
'service',
|
|
412
|
+
{ name: '@test/bar', role: 'service', dependencies: { '@test/foo': 'workspace:*' } },
|
|
413
|
+
new Map([['@test/foo', runtimeUsage(['src/index.ts'])]]),
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
const facts: DependencyModelFacts = {
|
|
417
|
+
workspaces: [root, foo, bar],
|
|
418
|
+
externalPeerMetadata: new Map(),
|
|
419
|
+
rules: [{ match: 'react', version: '>=18' }],
|
|
420
|
+
};
|
|
421
|
+
const result = deriveDependencyModel(facts);
|
|
422
|
+
const exp = result.expected.get('packages/bar')!;
|
|
423
|
+
// service gets react propagated into dependencies
|
|
424
|
+
expect(exp.sections.dependencies.get('react')).toBeDefined();
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
// 8. rootonly-violation
|
|
429
|
+
describe('rootonly-violation', () => {
|
|
430
|
+
it('reports root-only violation when workspace declares a rootOnly dep', () => {
|
|
431
|
+
const root = makeRoot();
|
|
432
|
+
const ws = makeWorkspace(
|
|
433
|
+
'@test/svc',
|
|
434
|
+
'packages/svc',
|
|
435
|
+
'service',
|
|
436
|
+
{ name: '@test/svc', role: 'service', dependencies: { 'some-tool': '^1.0.0' } },
|
|
437
|
+
);
|
|
438
|
+
const facts = emptyFacts([root, ws], [
|
|
439
|
+
{ match: 'some-tool', rootOnly: true },
|
|
440
|
+
]);
|
|
441
|
+
const result = deriveDependencyModel(facts);
|
|
442
|
+
expect(result.violations.some((v) => v.code === 'root-only' && v.dependency === 'some-tool')).toBe(true);
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it('reports root-only-usage when workspace source imports a rootOnly dep', () => {
|
|
446
|
+
const root = makeRoot();
|
|
447
|
+
const ws = makeWorkspace(
|
|
448
|
+
'@test/svc',
|
|
449
|
+
'packages/svc',
|
|
450
|
+
'service',
|
|
451
|
+
{ name: '@test/svc', role: 'service' },
|
|
452
|
+
new Map([['some-tool', runtimeUsage(['src/main.ts'])]]),
|
|
453
|
+
);
|
|
454
|
+
const facts = emptyFacts([root, ws], [
|
|
455
|
+
{ match: 'some-tool', rootOnly: true },
|
|
456
|
+
]);
|
|
457
|
+
const result = deriveDependencyModel(facts);
|
|
458
|
+
|
|
459
|
+
expect(
|
|
460
|
+
result.violations.some(
|
|
461
|
+
(v) =>
|
|
462
|
+
v.code === 'root-only-usage' &&
|
|
463
|
+
v.dependency === 'some-tool' &&
|
|
464
|
+
v.workspace === '@test/svc',
|
|
465
|
+
),
|
|
466
|
+
).toBe(true);
|
|
467
|
+
});
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
// 9. library-dev-mirror-exact
|
|
471
|
+
describe('library-dev-mirror-exact', () => {
|
|
472
|
+
it('derives devDependencies as exact mirror of peerDependencies', () => {
|
|
473
|
+
const root = makeRoot();
|
|
474
|
+
const ws = makeWorkspace(
|
|
475
|
+
'@test/lib',
|
|
476
|
+
'packages/lib',
|
|
477
|
+
'library',
|
|
478
|
+
undefined,
|
|
479
|
+
new Map([
|
|
480
|
+
['react', runtimeUsage(['src/index.ts'])],
|
|
481
|
+
['vue', runtimeUsage(['src/index.ts'])],
|
|
482
|
+
]),
|
|
483
|
+
);
|
|
484
|
+
const facts = emptyFacts([root, ws], [
|
|
485
|
+
{ match: 'react', version: '^18.0.0' },
|
|
486
|
+
{ match: 'vue', version: '^3.0.0' },
|
|
487
|
+
]);
|
|
488
|
+
const result = deriveDependencyModel(facts);
|
|
489
|
+
const exp = result.expected.get('packages/lib')!;
|
|
490
|
+
expect([...exp.sections.devDependencies.entries()]).toEqual(
|
|
491
|
+
[...exp.sections.peerDependencies.entries()],
|
|
492
|
+
);
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
it('reports mirror violation when devDependencies range differs from peerDependencies', () => {
|
|
496
|
+
const root = makeRoot();
|
|
497
|
+
const ws = makeWorkspace(
|
|
498
|
+
'@test/lib',
|
|
499
|
+
'packages/lib',
|
|
500
|
+
'library',
|
|
501
|
+
{
|
|
502
|
+
name: '@test/lib',
|
|
503
|
+
role: 'library',
|
|
504
|
+
peerDependencies: { react: '^18.0.0' },
|
|
505
|
+
devDependencies: { react: '^17.0.0' },
|
|
506
|
+
},
|
|
507
|
+
new Map([['react', runtimeUsage(['src/index.ts'])]]),
|
|
508
|
+
);
|
|
509
|
+
const facts = emptyFacts([root, ws], [
|
|
510
|
+
{ match: 'react', version: '^18.0.0' },
|
|
511
|
+
]);
|
|
512
|
+
const result = deriveDependencyModel(facts);
|
|
513
|
+
expect(
|
|
514
|
+
result.violations.some((v) => v.code === 'mirror' && v.dependency === 'react'),
|
|
515
|
+
).toBe(true);
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
it('removes extra devDependencies not present in peerDependencies', () => {
|
|
519
|
+
const root = makeRoot();
|
|
520
|
+
const ws = makeWorkspace(
|
|
521
|
+
'@test/lib',
|
|
522
|
+
'packages/lib',
|
|
523
|
+
'library',
|
|
524
|
+
{
|
|
525
|
+
name: '@test/lib',
|
|
526
|
+
role: 'library',
|
|
527
|
+
peerDependencies: { react: '^18.0.0' },
|
|
528
|
+
devDependencies: { react: '^18.0.0', 'extra-dev': '^1.0.0' },
|
|
529
|
+
},
|
|
530
|
+
new Map([['react', runtimeUsage(['src/index.ts'])]]),
|
|
531
|
+
);
|
|
532
|
+
const facts = emptyFacts([root, ws], [
|
|
533
|
+
{ match: 'react', version: '^18.0.0' },
|
|
534
|
+
]);
|
|
535
|
+
const result = deriveDependencyModel(facts);
|
|
536
|
+
const exp = result.expected.get('packages/lib')!;
|
|
537
|
+
expect(exp.sections.devDependencies.has('extra-dev')).toBe(false);
|
|
538
|
+
expect(exp.sections.devDependencies.get('react')).toBe('^18.0.0');
|
|
539
|
+
});
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
// 10. library-private-type-root-dev
|
|
543
|
+
describe('library-private-type-root-dev', () => {
|
|
544
|
+
it('sends type-only imports not in dts to root devDependencies', () => {
|
|
545
|
+
const root = makeRoot();
|
|
546
|
+
const ws = makeWorkspace(
|
|
547
|
+
'@test/lib',
|
|
548
|
+
'packages/lib',
|
|
549
|
+
'library',
|
|
550
|
+
undefined,
|
|
551
|
+
new Map([['some-types', typeOnlyUsage(['src/index.ts'])]]),
|
|
552
|
+
new Set(), // NOT in dts
|
|
553
|
+
);
|
|
554
|
+
const facts = emptyFacts([root, ws], [
|
|
555
|
+
{ match: 'some-types', version: '^1.0.0' },
|
|
556
|
+
]);
|
|
557
|
+
const result = deriveDependencyModel(facts);
|
|
558
|
+
const rootExp = result.expected.get('.')!;
|
|
559
|
+
const wsExp = result.expected.get('packages/lib')!;
|
|
560
|
+
expect(rootExp.sections.devDependencies.get('some-types')).toBe('^1.0.0');
|
|
561
|
+
expect(wsExp.sections.peerDependencies.has('some-types')).toBe(false);
|
|
562
|
+
expect(wsExp.sections.devDependencies.has('some-types')).toBe(false);
|
|
563
|
+
});
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
// 11. library-dts-type-leak
|
|
567
|
+
describe('library-dts-type-leak', () => {
|
|
568
|
+
it('promotes type-only dep appearing in dts to public contract (peer + mirror)', () => {
|
|
569
|
+
const root = makeRoot();
|
|
570
|
+
const ws = makeWorkspace(
|
|
571
|
+
'@test/lib',
|
|
572
|
+
'packages/lib',
|
|
573
|
+
'library',
|
|
574
|
+
undefined,
|
|
575
|
+
new Map([['some-types', typeOnlyUsage(['src/index.ts'])]]),
|
|
576
|
+
new Set(['some-types']), // present in dts → public surface
|
|
577
|
+
);
|
|
578
|
+
const facts = emptyFacts([root, ws], [
|
|
579
|
+
{ match: 'some-types', version: '^1.0.0' },
|
|
580
|
+
]);
|
|
581
|
+
const result = deriveDependencyModel(facts);
|
|
582
|
+
const wsExp = result.expected.get('packages/lib')!;
|
|
583
|
+
expect(wsExp.sections.peerDependencies.get('some-types')).toBe('^1.0.0');
|
|
584
|
+
expect(wsExp.sections.devDependencies.get('some-types')).toBe('^1.0.0'); // mirror
|
|
585
|
+
const rootExp = result.expected.get('.')!;
|
|
586
|
+
expect(rootExp.sections.devDependencies.has('some-types')).toBe(false);
|
|
587
|
+
});
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
// 12. cli-runtime-deps
|
|
591
|
+
describe('cli-runtime-deps', () => {
|
|
592
|
+
it('places runtime dep in dependencies for cli', () => {
|
|
593
|
+
const root = makeRoot();
|
|
594
|
+
const ws = makeWorkspace(
|
|
595
|
+
'@test/cli',
|
|
596
|
+
'packages/cli',
|
|
597
|
+
'cli',
|
|
598
|
+
undefined,
|
|
599
|
+
new Map([['commander', runtimeUsage(['src/index.ts'])]]),
|
|
600
|
+
);
|
|
601
|
+
const facts = emptyFacts([root, ws], [
|
|
602
|
+
{ match: 'commander', version: '^12.0.0' },
|
|
603
|
+
]);
|
|
604
|
+
const result = deriveDependencyModel(facts);
|
|
605
|
+
const exp = result.expected.get('packages/cli')!;
|
|
606
|
+
expect(exp.sections.dependencies.get('commander')).toBe('^12.0.0');
|
|
607
|
+
expect(exp.sections.peerDependencies.size).toBe(0);
|
|
608
|
+
expect(exp.sections.devDependencies.size).toBe(0);
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
it('reports forbidden-section when cli declares peerDependencies', () => {
|
|
612
|
+
const root = makeRoot();
|
|
613
|
+
const ws = makeWorkspace(
|
|
614
|
+
'@test/cli',
|
|
615
|
+
'packages/cli',
|
|
616
|
+
'cli',
|
|
617
|
+
{ name: '@test/cli', role: 'cli', peerDependencies: { commander: '^12.0.0' } },
|
|
618
|
+
);
|
|
619
|
+
const facts = emptyFacts([root, ws]);
|
|
620
|
+
const result = deriveDependencyModel(facts);
|
|
621
|
+
expect(
|
|
622
|
+
result.violations.some(
|
|
623
|
+
(v) => v.code === 'forbidden-section' && v.workspace === '@test/cli',
|
|
624
|
+
),
|
|
625
|
+
).toBe(true);
|
|
626
|
+
});
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
// 13. peer-propagation-external-package
|
|
630
|
+
describe('peer-propagation-external-package', () => {
|
|
631
|
+
it('propagates required peers of an external package to consuming service', () => {
|
|
632
|
+
const root = makeRoot();
|
|
633
|
+
const ws = makeWorkspace(
|
|
634
|
+
'@test/svc',
|
|
635
|
+
'packages/svc',
|
|
636
|
+
'service',
|
|
637
|
+
undefined,
|
|
638
|
+
new Map([['some-framework', runtimeUsage(['src/index.ts'])]]),
|
|
639
|
+
);
|
|
640
|
+
const facts: DependencyModelFacts = {
|
|
641
|
+
workspaces: [root, ws],
|
|
642
|
+
externalPeerMetadata: new Map([
|
|
643
|
+
['some-framework', new Map([['react', '^18.0.0']])],
|
|
644
|
+
]),
|
|
645
|
+
rules: [
|
|
646
|
+
{ match: 'some-framework', version: '^1.0.0' },
|
|
647
|
+
{ match: 'react', version: '^18.0.0' },
|
|
648
|
+
],
|
|
649
|
+
};
|
|
650
|
+
const result = deriveDependencyModel(facts);
|
|
651
|
+
const exp = result.expected.get('packages/svc')!;
|
|
652
|
+
expect(exp.sections.dependencies.get('react')).toBe('^18.0.0');
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
it('propagates required external peers as peer + mirror for library consumers', () => {
|
|
656
|
+
const root = makeRoot();
|
|
657
|
+
const ws = makeWorkspace(
|
|
658
|
+
'@test/lib',
|
|
659
|
+
'packages/lib',
|
|
660
|
+
'library',
|
|
661
|
+
undefined,
|
|
662
|
+
new Map([['some-framework', runtimeUsage(['src/index.ts'])]]),
|
|
663
|
+
);
|
|
664
|
+
const facts: DependencyModelFacts = {
|
|
665
|
+
workspaces: [root, ws],
|
|
666
|
+
externalPeerMetadata: new Map([
|
|
667
|
+
['some-framework', new Map([['react', '^18.0.0']])],
|
|
668
|
+
]),
|
|
669
|
+
rules: [
|
|
670
|
+
{ match: 'some-framework', section: 'dependencies', version: '^1.0.0' },
|
|
671
|
+
{ match: 'react', version: '^18.0.0' },
|
|
672
|
+
],
|
|
673
|
+
};
|
|
674
|
+
const result = deriveDependencyModel(facts);
|
|
675
|
+
const exp = result.expected.get('packages/lib')!;
|
|
676
|
+
expect(exp.sections.peerDependencies.get('react')).toBe('^18.0.0');
|
|
677
|
+
expect(exp.sections.devDependencies.get('react')).toBe('^18.0.0'); // mirror
|
|
678
|
+
});
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
// 14. optional-peers-ignored
|
|
682
|
+
describe('optional-peers-ignored', () => {
|
|
683
|
+
it('does not propagate optional peers (null entry in externalPeerMetadata)', () => {
|
|
684
|
+
const root = makeRoot();
|
|
685
|
+
const ws = makeWorkspace(
|
|
686
|
+
'@test/svc',
|
|
687
|
+
'packages/svc',
|
|
688
|
+
'service',
|
|
689
|
+
undefined,
|
|
690
|
+
new Map([['some-pkg', runtimeUsage(['src/index.ts'])]]),
|
|
691
|
+
);
|
|
692
|
+
// null = коллектор отфильтровал все optional peers; required peers отсутствуют
|
|
693
|
+
const facts: DependencyModelFacts = {
|
|
694
|
+
workspaces: [root, ws],
|
|
695
|
+
externalPeerMetadata: new Map([['some-pkg', null]]),
|
|
696
|
+
rules: [{ match: 'some-pkg', version: '^3.0.0' }],
|
|
697
|
+
};
|
|
698
|
+
const result = deriveDependencyModel(facts);
|
|
699
|
+
const exp = result.expected.get('packages/svc')!;
|
|
700
|
+
expect(exp.sections.dependencies.get('some-pkg')).toBe('^3.0.0');
|
|
701
|
+
expect(exp.sections.dependencies.size).toBe(1); // никакие peers не добавились
|
|
702
|
+
});
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
// 15. repeated-dep-needs-version-rule
|
|
706
|
+
describe('repeated-dep-needs-version-rule', () => {
|
|
707
|
+
it('emits unconstrained-version violation when dep appears in multiple workspaces without a global version rule', () => {
|
|
708
|
+
const root = makeRoot();
|
|
709
|
+
const ws1 = makeWorkspace(
|
|
710
|
+
'@test/a', 'packages/a', 'service',
|
|
711
|
+
{ name: '@test/a', role: 'service' },
|
|
712
|
+
new Map([['lodash', runtimeUsage(['src/a.ts'])]]),
|
|
713
|
+
);
|
|
714
|
+
const ws2 = makeWorkspace(
|
|
715
|
+
'@test/b', 'packages/b', 'service',
|
|
716
|
+
{ name: '@test/b', role: 'service' },
|
|
717
|
+
new Map([['lodash', runtimeUsage(['src/b.ts'])]]),
|
|
718
|
+
);
|
|
719
|
+
const facts = emptyFacts([root, ws1, ws2]); // нет правил → нет версионного правила для lodash
|
|
720
|
+
const result = deriveDependencyModel(facts);
|
|
721
|
+
expect(
|
|
722
|
+
result.violations.some(
|
|
723
|
+
(v) => v.code === 'unconstrained-version' && v.dependency === 'lodash',
|
|
724
|
+
),
|
|
725
|
+
).toBe(true);
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
it('does not warn when a global version rule covers the dep', () => {
|
|
729
|
+
const root = makeRoot();
|
|
730
|
+
const ws1 = makeWorkspace(
|
|
731
|
+
'@test/a', 'packages/a', 'service',
|
|
732
|
+
{ name: '@test/a', role: 'service', dependencies: { lodash: '^4.17.21' } },
|
|
733
|
+
);
|
|
734
|
+
const ws2 = makeWorkspace(
|
|
735
|
+
'@test/b', 'packages/b', 'service',
|
|
736
|
+
{ name: '@test/b', role: 'service', dependencies: { lodash: '^4.17.21' } },
|
|
737
|
+
);
|
|
738
|
+
const facts = emptyFacts([root, ws1, ws2], [
|
|
739
|
+
{ match: 'lodash', version: '^4.17.21' },
|
|
740
|
+
]);
|
|
741
|
+
const result = deriveDependencyModel(facts);
|
|
742
|
+
expect(
|
|
743
|
+
result.violations.some(
|
|
744
|
+
(v) => v.code === 'unconstrained-version' && v.dependency === 'lodash',
|
|
745
|
+
),
|
|
746
|
+
).toBe(false);
|
|
747
|
+
});
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
// 16. rule-precedence-last-wins
|
|
751
|
+
describe('rule-precedence-last-wins', () => {
|
|
752
|
+
it('later version rule overrides earlier one for the same dep', () => {
|
|
753
|
+
const root = makeRoot();
|
|
754
|
+
const ws = makeWorkspace(
|
|
755
|
+
'@test/lib',
|
|
756
|
+
'packages/lib',
|
|
757
|
+
'library',
|
|
758
|
+
undefined,
|
|
759
|
+
new Map([['react', runtimeUsage(['src/index.ts'])]]),
|
|
760
|
+
);
|
|
761
|
+
const facts = emptyFacts([root, ws], [
|
|
762
|
+
{ match: 'react', version: '^17.0.0' },
|
|
763
|
+
{ match: 'react', version: '^18.0.0' }, // последнее правило побеждает
|
|
764
|
+
]);
|
|
765
|
+
const result = deriveDependencyModel(facts);
|
|
766
|
+
const exp = result.expected.get('packages/lib')!;
|
|
767
|
+
expect(exp.sections.peerDependencies.get('react')).toBe('^18.0.0');
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
it('workspace-scoped rule overrides global section rule', () => {
|
|
771
|
+
const root = makeRoot();
|
|
772
|
+
const ws = makeWorkspace(
|
|
773
|
+
'@test/lib',
|
|
774
|
+
'packages/lib',
|
|
775
|
+
'library',
|
|
776
|
+
undefined,
|
|
777
|
+
new Map([['some-pkg', runtimeUsage(['src/index.ts'])]]),
|
|
778
|
+
);
|
|
779
|
+
const facts = emptyFacts([root, ws], [
|
|
780
|
+
{ match: 'some-pkg', version: '^1.0.0' },
|
|
781
|
+
// workspace-overrides переводит dep из peerDeps в deps для конкретной библиотеки
|
|
782
|
+
{ match: 'some-pkg', workspace: '@test/lib', section: 'dependencies', version: '^1.0.0' },
|
|
783
|
+
]);
|
|
784
|
+
const result = deriveDependencyModel(facts);
|
|
785
|
+
const exp = result.expected.get('packages/lib')!;
|
|
786
|
+
expect(exp.sections.dependencies.has('some-pkg')).toBe(true);
|
|
787
|
+
expect(exp.sections.peerDependencies.has('some-pkg')).toBe(false);
|
|
788
|
+
});
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
// 17. wildcard-version-rule (V2)
|
|
792
|
+
describe('wildcard-version-rule', () => {
|
|
793
|
+
it('wildcard pattern counts as a version rule and suppresses unconstrained-version violation', () => {
|
|
794
|
+
const root = makeRoot();
|
|
795
|
+
const ws1 = makeWorkspace(
|
|
796
|
+
'@test/a', 'packages/a', 'service',
|
|
797
|
+
{ name: '@test/a', role: 'service' },
|
|
798
|
+
new Map([['@nestjs/core', runtimeUsage(['src/a.ts'])]]),
|
|
799
|
+
);
|
|
800
|
+
const ws2 = makeWorkspace(
|
|
801
|
+
'@test/b', 'packages/b', 'service',
|
|
802
|
+
{ name: '@test/b', role: 'service' },
|
|
803
|
+
new Map([['@nestjs/core', runtimeUsage(['src/b.ts'])]]),
|
|
804
|
+
);
|
|
805
|
+
const facts = emptyFacts([root, ws1, ws2], [
|
|
806
|
+
{ match: '@nestjs/*', version: '^10.0.0' },
|
|
807
|
+
]);
|
|
808
|
+
const result = deriveDependencyModel(facts);
|
|
809
|
+
expect(
|
|
810
|
+
result.violations.some((v) => v.code === 'unconstrained-version'),
|
|
811
|
+
).toBe(false);
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
it('wildcard version rule applies the version to matching deps', () => {
|
|
815
|
+
const root = makeRoot();
|
|
816
|
+
const ws = makeWorkspace(
|
|
817
|
+
'@test/svc', 'packages/svc', 'service',
|
|
818
|
+
undefined,
|
|
819
|
+
new Map([['@nestjs/core', runtimeUsage(['src/main.ts'])]]),
|
|
820
|
+
);
|
|
821
|
+
const facts = emptyFacts([root, ws], [
|
|
822
|
+
{ match: '@nestjs/*', version: '^10.3.0' },
|
|
823
|
+
]);
|
|
824
|
+
const result = deriveDependencyModel(facts);
|
|
825
|
+
const exp = result.expected.get('packages/svc')!;
|
|
826
|
+
expect(exp.sections.dependencies.get('@nestjs/core')).toBe('^10.3.0');
|
|
827
|
+
});
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
describe('peer-propagation-strictness', () => {
|
|
831
|
+
it('does not propagate optional peers from a local library', () => {
|
|
832
|
+
const root = makeRoot();
|
|
833
|
+
const provider = makeWorkspace(
|
|
834
|
+
'@test/provider',
|
|
835
|
+
'packages/provider',
|
|
836
|
+
'library',
|
|
837
|
+
{
|
|
838
|
+
name: '@test/provider',
|
|
839
|
+
role: 'library',
|
|
840
|
+
peerDependenciesMeta: { react: { optional: true } },
|
|
841
|
+
},
|
|
842
|
+
new Map([['react', runtimeUsage(['src/index.ts'])]]),
|
|
843
|
+
);
|
|
844
|
+
const consumer = makeWorkspace(
|
|
845
|
+
'@test/consumer',
|
|
846
|
+
'packages/consumer',
|
|
847
|
+
'service',
|
|
848
|
+
{ name: '@test/consumer', role: 'service' },
|
|
849
|
+
new Map([['@test/provider', runtimeUsage(['src/main.ts'])]]),
|
|
850
|
+
);
|
|
851
|
+
const result = deriveDependencyModel(emptyFacts([root, provider, consumer], [
|
|
852
|
+
{ match: 'react', version: '^18.0.0' },
|
|
853
|
+
]));
|
|
854
|
+
const exp = result.expected.get('packages/consumer')!;
|
|
855
|
+
|
|
856
|
+
expect(exp.sections.dependencies.has('react')).toBe(false);
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
it('propagates transitive external peer metadata when complete facts are provided', () => {
|
|
860
|
+
const root = makeRoot();
|
|
861
|
+
const ws = makeWorkspace(
|
|
862
|
+
'@test/svc',
|
|
863
|
+
'packages/svc',
|
|
864
|
+
'service',
|
|
865
|
+
undefined,
|
|
866
|
+
new Map([['framework-a', runtimeUsage(['src/main.ts'])]]),
|
|
867
|
+
);
|
|
868
|
+
const facts: DependencyModelFacts = {
|
|
869
|
+
workspaces: [root, ws],
|
|
870
|
+
externalPeerMetadata: new Map([
|
|
871
|
+
['framework-a', new Map([['framework-b', '^2.0.0']])],
|
|
872
|
+
['framework-b', new Map([['framework-c', '^3.0.0']])],
|
|
873
|
+
['framework-c', null],
|
|
874
|
+
]),
|
|
875
|
+
rules: [
|
|
876
|
+
{ match: 'framework-a', version: '^1.0.0' },
|
|
877
|
+
{ match: 'framework-b', version: '^2.0.0' },
|
|
878
|
+
{ match: 'framework-c', version: '^3.0.0' },
|
|
879
|
+
],
|
|
880
|
+
};
|
|
881
|
+
const result = deriveDependencyModel(facts);
|
|
882
|
+
const exp = result.expected.get('packages/svc')!;
|
|
883
|
+
|
|
884
|
+
expect(exp.sections.dependencies.get('framework-b')).toBe('^2.0.0');
|
|
885
|
+
expect(exp.sections.dependencies.get('framework-c')).toBe('^3.0.0');
|
|
886
|
+
});
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
describe('dts-mode', () => {
|
|
890
|
+
it('does not report missing dist when withDts is disabled', () => {
|
|
891
|
+
const root = makeRoot();
|
|
892
|
+
const ws = makeWorkspace(
|
|
893
|
+
'@test/lib',
|
|
894
|
+
'packages/lib',
|
|
895
|
+
'library',
|
|
896
|
+
{ name: '@test/lib', role: 'library' },
|
|
897
|
+
new Map(),
|
|
898
|
+
new Set(),
|
|
899
|
+
true,
|
|
900
|
+
false,
|
|
901
|
+
);
|
|
902
|
+
const result = deriveDependencyModel(emptyFacts([root, ws]));
|
|
903
|
+
|
|
904
|
+
expect(result.violations.some((v) => v.code === 'dist-missing')).toBe(false);
|
|
905
|
+
expect(result.warnings.length).toBe(0);
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
it('uses available dts facts even when withDts is disabled', () => {
|
|
909
|
+
const root = makeRoot();
|
|
910
|
+
const ws = makeWorkspace(
|
|
911
|
+
'@test/lib',
|
|
912
|
+
'packages/lib',
|
|
913
|
+
'library',
|
|
914
|
+
{ name: '@test/lib', role: 'library' },
|
|
915
|
+
new Map(),
|
|
916
|
+
new Set(['some-types']),
|
|
917
|
+
true,
|
|
918
|
+
true,
|
|
919
|
+
);
|
|
920
|
+
const result = deriveDependencyModel(emptyFacts([root, ws], [
|
|
921
|
+
{ match: 'some-types', version: '^1.0.0' },
|
|
922
|
+
]));
|
|
923
|
+
const exp = result.expected.get('packages/lib')!;
|
|
924
|
+
|
|
925
|
+
expect(exp.sections.peerDependencies.get('some-types')).toBe('^1.0.0');
|
|
926
|
+
expect(exp.sections.devDependencies.get('some-types')).toBe('^1.0.0');
|
|
927
|
+
expect(result.violations.some((v) => v.code === 'dist-missing')).toBe(false);
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
it('reports missing dist when withDts is enabled', () => {
|
|
931
|
+
const root = makeRoot();
|
|
932
|
+
const ws = makeWorkspace(
|
|
933
|
+
'@test/lib',
|
|
934
|
+
'packages/lib',
|
|
935
|
+
'library',
|
|
936
|
+
{ name: '@test/lib', role: 'library' },
|
|
937
|
+
new Map(),
|
|
938
|
+
new Set(),
|
|
939
|
+
true,
|
|
940
|
+
false,
|
|
941
|
+
);
|
|
942
|
+
const facts = emptyFacts([root, ws]);
|
|
943
|
+
facts.withDts = true;
|
|
944
|
+
const result = deriveDependencyModel(facts);
|
|
945
|
+
|
|
946
|
+
expect(result.violations.some((v) => v.code === 'dist-missing')).toBe(true);
|
|
947
|
+
});
|
|
948
|
+
|
|
949
|
+
it('promotes matching DefinitelyTyped provider for public dts imports', () => {
|
|
950
|
+
const root = makeRoot({
|
|
951
|
+
name: '@test/root',
|
|
952
|
+
private: true,
|
|
953
|
+
devDependencies: { '@types/lodash': '^4.17.13' },
|
|
954
|
+
});
|
|
955
|
+
const ws = makeWorkspace(
|
|
956
|
+
'@test/lib',
|
|
957
|
+
'packages/lib',
|
|
958
|
+
'library',
|
|
959
|
+
{
|
|
960
|
+
name: '@test/lib',
|
|
961
|
+
role: 'library',
|
|
962
|
+
peerDependencies: {
|
|
963
|
+
lodash: '^4.17.21',
|
|
964
|
+
'@types/lodash': '^4.17.13',
|
|
965
|
+
},
|
|
966
|
+
devDependencies: {
|
|
967
|
+
lodash: '^4.17.21',
|
|
968
|
+
'@types/lodash': '^4.17.13',
|
|
969
|
+
},
|
|
970
|
+
},
|
|
971
|
+
new Map(),
|
|
972
|
+
new Set(['lodash']),
|
|
973
|
+
);
|
|
974
|
+
const facts: DependencyModelFacts = {
|
|
975
|
+
workspaces: [root, ws],
|
|
976
|
+
externalPeerMetadata: new Map(),
|
|
977
|
+
externalTypeMetadata: new Map([
|
|
978
|
+
['lodash', { hasBundledTypes: false }],
|
|
979
|
+
]),
|
|
980
|
+
typeProviderPackages: new Set(['@types/lodash']),
|
|
981
|
+
rules: [
|
|
982
|
+
{ match: 'lodash', version: '^4.17.21' },
|
|
983
|
+
{ match: '@types/*', rootOnly: true },
|
|
984
|
+
{ match: '@types/lodash', version: '^4.17.13' },
|
|
985
|
+
],
|
|
986
|
+
withDts: true,
|
|
987
|
+
};
|
|
988
|
+
const result = deriveDependencyModel(facts);
|
|
989
|
+
const exp = result.expected.get('packages/lib')!;
|
|
990
|
+
|
|
991
|
+
expect(exp.sections.peerDependencies.get('@types/lodash')).toBe('^4.17.13');
|
|
992
|
+
expect(exp.sections.devDependencies.get('@types/lodash')).toBe('^4.17.13');
|
|
993
|
+
expect(
|
|
994
|
+
result.violations.some(
|
|
995
|
+
(v) =>
|
|
996
|
+
v.code === 'root-only-usage' &&
|
|
997
|
+
v.dependency === '@types/lodash',
|
|
998
|
+
),
|
|
999
|
+
).toBe(false);
|
|
1000
|
+
});
|
|
1001
|
+
});
|
|
1002
|
+
});
|