@rsdk/yarn.constraints 6.0.0-next.40 → 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 +12 -4
- package/README.MD +70 -30
- package/__tests__/config-validation.test.ts +42 -0
- package/__tests__/engine.test.ts +105 -0
- package/__tests__/fixtures/imports/lib/lib-entry.js +3 -0
- package/__tests__/fixtures/imports/root-entry.js +1 -0
- package/__tests__/fixtures/imports/rules/transitive.js +3 -0
- package/__tests__/fixtures/imports/test/outside.ts +3 -0
- package/__tests__/imports.test.ts +12 -0
- package/dist/collectors/config.js +4 -1
- package/dist/collectors/config.js.map +1 -1
- package/dist/collectors/workspaces.js +3 -1
- package/dist/collectors/workspaces.js.map +1 -1
- package/dist/lib/imports.d.ts +2 -0
- package/dist/lib/imports.js +124 -31
- package/dist/lib/imports.js.map +1 -1
- 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.js +22 -0
- package/dist/model/diagnostics.js.map +1 -1
- package/dist/model/placement.js +12 -7
- package/dist/model/placement.js.map +1 -1
- package/dist/model/rules.d.ts +1 -0
- package/dist/model/rules.js +6 -0
- package/dist/model/rules.js.map +1 -1
- package/dist/model/types.d.ts +2 -1
- package/dist/model/types.js.map +1 -1
- package/dist/model/versions.js +5 -1
- package/dist/model/versions.js.map +1 -1
- package/package.json +2 -2
- package/src/collectors/config.ts +8 -1
- package/src/collectors/workspaces.ts +10 -2
- package/src/lib/imports.ts +173 -31
- package/src/model/config-validation.ts +49 -0
- package/src/model/diagnostics.ts +30 -0
- package/src/model/placement.ts +13 -7
- package/src/model/rules.ts +12 -0
- package/src/model/types.ts +2 -1
- package/src/model/versions.ts +7 -2
package/src/collectors/config.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import yaml from 'yaml';
|
|
11
11
|
|
|
12
|
+
import { assertValidDependencyModelConfig } from '../model/config-validation';
|
|
12
13
|
import type { DependencyModelConfig } from '../model/types';
|
|
13
14
|
|
|
14
15
|
export function loadConfig(
|
|
@@ -22,5 +23,11 @@ export function loadConfig(
|
|
|
22
23
|
|
|
23
24
|
if (!existsSync(filepath)) return { rules: [] };
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
const config = yaml.parse(
|
|
27
|
+
readFileSync(filepath, 'utf8'),
|
|
28
|
+
) as DependencyModelConfig;
|
|
29
|
+
|
|
30
|
+
assertValidDependencyModelConfig(config);
|
|
31
|
+
|
|
32
|
+
return config;
|
|
26
33
|
}
|
|
@@ -9,7 +9,11 @@ import { execSync } from 'node:child_process';
|
|
|
9
9
|
import { existsSync } from 'node:fs';
|
|
10
10
|
import path from 'node:path';
|
|
11
11
|
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
collectDtsImports,
|
|
14
|
+
collectSourceFiles,
|
|
15
|
+
collectSourceImportsFromFiles,
|
|
16
|
+
} from '../lib/imports';
|
|
13
17
|
import { readPackageJson } from '../lib/package-json';
|
|
14
18
|
import { isWorkspaceRole } from '../model/rules';
|
|
15
19
|
import type { WorkspaceContext } from '../model/types';
|
|
@@ -20,7 +24,11 @@ interface YarnWorkspaceInfo {
|
|
|
20
24
|
}
|
|
21
25
|
|
|
22
26
|
function collectUsage(workspace: WorkspaceContext, withDts: boolean): void {
|
|
23
|
-
|
|
27
|
+
const sourceFiles = collectSourceFiles(workspace.dir, workspace.pkg);
|
|
28
|
+
|
|
29
|
+
workspace.sourceFileCount = sourceFiles.length;
|
|
30
|
+
|
|
31
|
+
for (const entry of collectSourceImportsFromFiles(sourceFiles)) {
|
|
24
32
|
if (!workspace.sourceUsage.has(entry.packageName)) {
|
|
25
33
|
workspace.sourceUsage.set(entry.packageName, {
|
|
26
34
|
files: new Set(),
|
package/src/lib/imports.ts
CHANGED
|
@@ -18,6 +18,11 @@ export interface ImportEntry {
|
|
|
18
18
|
file: string;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
interface ModuleReference {
|
|
22
|
+
specifier: string;
|
|
23
|
+
isTypeOnly: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
21
26
|
const BUILTINS = new Set([
|
|
22
27
|
...builtinModules,
|
|
23
28
|
...builtinModules.map((m) => `node:${m}`),
|
|
@@ -35,6 +40,19 @@ const SOURCE_EXTENSIONS = new Set([
|
|
|
35
40
|
]);
|
|
36
41
|
|
|
37
42
|
const DTS_RE = /\.d\.(?:c|m)?ts$/;
|
|
43
|
+
const DEFAULT_SOURCE_ROOTS = ['src', 'test', 'tests', '__tests__'];
|
|
44
|
+
const IGNORED_ENTRYPOINT_DIRS = new Set([
|
|
45
|
+
'node_modules',
|
|
46
|
+
'dist',
|
|
47
|
+
'build',
|
|
48
|
+
'coverage',
|
|
49
|
+
]);
|
|
50
|
+
const IGNORED_WALK_DIRS = new Set([
|
|
51
|
+
'node_modules',
|
|
52
|
+
'fixtures',
|
|
53
|
+
'__fixtures__',
|
|
54
|
+
'__mocks__',
|
|
55
|
+
]);
|
|
38
56
|
|
|
39
57
|
export function getPackageName(specifier: string): string | null {
|
|
40
58
|
if (
|
|
@@ -62,7 +80,7 @@ function walkFiles(dir: string, filter: (name: string) => boolean): string[] {
|
|
|
62
80
|
try {
|
|
63
81
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
64
82
|
const full = path.join(dir, entry.name);
|
|
65
|
-
if (entry.isDirectory() && entry.name
|
|
83
|
+
if (entry.isDirectory() && !IGNORED_WALK_DIRS.has(entry.name)) {
|
|
66
84
|
results.push(...walkFiles(full, filter));
|
|
67
85
|
} else if (entry.isFile() && filter(entry.name)) {
|
|
68
86
|
results.push(full);
|
|
@@ -74,6 +92,22 @@ function walkFiles(dir: string, filter: (name: string) => boolean): string[] {
|
|
|
74
92
|
return results;
|
|
75
93
|
}
|
|
76
94
|
|
|
95
|
+
function isInside(parent: string, child: string): boolean {
|
|
96
|
+
const relative = path.relative(parent, child);
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
relative !== '' &&
|
|
100
|
+
!relative.startsWith('..') &&
|
|
101
|
+
!path.isAbsolute(relative)
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function startsWithIgnoredEntrypointDir(relative: string): boolean {
|
|
106
|
+
const [first] = relative.split('/');
|
|
107
|
+
|
|
108
|
+
return first !== undefined && IGNORED_ENTRYPOINT_DIRS.has(first);
|
|
109
|
+
}
|
|
110
|
+
|
|
77
111
|
function isSourceFile(name: string): boolean {
|
|
78
112
|
return SOURCE_EXTENSIONS.has(path.extname(name)) && !DTS_RE.test(name);
|
|
79
113
|
}
|
|
@@ -105,6 +139,15 @@ function addImport(
|
|
|
105
139
|
results.push({ packageName, isTypeOnly, file });
|
|
106
140
|
}
|
|
107
141
|
|
|
142
|
+
function addModuleReference(
|
|
143
|
+
results: ModuleReference[],
|
|
144
|
+
specifier: string | null,
|
|
145
|
+
isTypeOnly: boolean,
|
|
146
|
+
): void {
|
|
147
|
+
if (!specifier) return;
|
|
148
|
+
results.push({ specifier, isTypeOnly });
|
|
149
|
+
}
|
|
150
|
+
|
|
108
151
|
function isImportDeclarationTypeOnly(node: ts.ImportDeclaration): boolean {
|
|
109
152
|
const clause = node.importClause;
|
|
110
153
|
if (!clause) return false;
|
|
@@ -136,12 +179,12 @@ function collectExternalModuleReference(
|
|
|
136
179
|
return stringLiteralText(node.expression);
|
|
137
180
|
}
|
|
138
181
|
|
|
139
|
-
function
|
|
182
|
+
function extractModuleReferences(
|
|
140
183
|
file: string,
|
|
141
184
|
content: string,
|
|
142
185
|
options: { includeDtsForms?: boolean } = {},
|
|
143
|
-
):
|
|
144
|
-
const results:
|
|
186
|
+
): ModuleReference[] {
|
|
187
|
+
const results: ModuleReference[] = [];
|
|
145
188
|
const sourceFile = ts.createSourceFile(
|
|
146
189
|
file,
|
|
147
190
|
content,
|
|
@@ -152,25 +195,21 @@ function extractImports(
|
|
|
152
195
|
|
|
153
196
|
if (options.includeDtsForms) {
|
|
154
197
|
for (const reference of sourceFile.typeReferenceDirectives) {
|
|
155
|
-
|
|
198
|
+
addModuleReference(results, reference.fileName, true);
|
|
156
199
|
}
|
|
157
200
|
}
|
|
158
201
|
|
|
159
202
|
const visit = (node: ts.Node): void => {
|
|
160
203
|
if (ts.isImportDeclaration(node)) {
|
|
161
204
|
const specifier = stringLiteralText(node.moduleSpecifier);
|
|
162
|
-
|
|
163
|
-
addImport(results, file, specifier, isImportDeclarationTypeOnly(node));
|
|
164
|
-
}
|
|
205
|
+
addModuleReference(results, specifier, isImportDeclarationTypeOnly(node));
|
|
165
206
|
} else if (ts.isExportDeclaration(node)) {
|
|
166
207
|
const specifier = stringLiteralText(node.moduleSpecifier);
|
|
167
|
-
|
|
168
|
-
addImport(results, file, specifier, isExportDeclarationTypeOnly(node));
|
|
169
|
-
}
|
|
208
|
+
addModuleReference(results, specifier, isExportDeclarationTypeOnly(node));
|
|
170
209
|
} else if (ts.isImportEqualsDeclaration(node)) {
|
|
171
210
|
if (ts.isExternalModuleReference(node.moduleReference)) {
|
|
172
211
|
const specifier = collectExternalModuleReference(node.moduleReference);
|
|
173
|
-
|
|
212
|
+
addModuleReference(results, specifier, node.isTypeOnly);
|
|
174
213
|
}
|
|
175
214
|
} else if (ts.isCallExpression(node)) {
|
|
176
215
|
if (
|
|
@@ -178,27 +217,27 @@ function extractImports(
|
|
|
178
217
|
node.arguments.length === 1
|
|
179
218
|
) {
|
|
180
219
|
const specifier = stringLiteralText(node.arguments[0]);
|
|
181
|
-
|
|
220
|
+
addModuleReference(results, specifier, false);
|
|
182
221
|
} else if (
|
|
183
222
|
ts.isIdentifier(node.expression) &&
|
|
184
223
|
node.expression.text === 'require' &&
|
|
185
224
|
node.arguments.length === 1
|
|
186
225
|
) {
|
|
187
226
|
const specifier = stringLiteralText(node.arguments[0]);
|
|
188
|
-
|
|
227
|
+
addModuleReference(results, specifier, false);
|
|
189
228
|
}
|
|
190
229
|
} else if (ts.isImportTypeNode(node)) {
|
|
191
230
|
const argument = node.argument;
|
|
192
231
|
if (ts.isLiteralTypeNode(argument)) {
|
|
193
232
|
const specifier = stringLiteralText(argument.literal);
|
|
194
|
-
|
|
233
|
+
addModuleReference(results, specifier, true);
|
|
195
234
|
}
|
|
196
235
|
} else if (
|
|
197
236
|
options.includeDtsForms &&
|
|
198
237
|
ts.isModuleDeclaration(node) &&
|
|
199
238
|
ts.isStringLiteral(node.name)
|
|
200
239
|
) {
|
|
201
|
-
|
|
240
|
+
addModuleReference(results, node.name.text, true);
|
|
202
241
|
}
|
|
203
242
|
|
|
204
243
|
ts.forEachChild(node, visit);
|
|
@@ -208,6 +247,20 @@ function extractImports(
|
|
|
208
247
|
return results;
|
|
209
248
|
}
|
|
210
249
|
|
|
250
|
+
function extractImports(
|
|
251
|
+
file: string,
|
|
252
|
+
content: string,
|
|
253
|
+
options: { includeDtsForms?: boolean } = {},
|
|
254
|
+
): ImportEntry[] {
|
|
255
|
+
const results: ImportEntry[] = [];
|
|
256
|
+
|
|
257
|
+
for (const reference of extractModuleReferences(file, content, options)) {
|
|
258
|
+
addImport(results, file, reference.specifier, reference.isTypeOnly);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return results;
|
|
262
|
+
}
|
|
263
|
+
|
|
211
264
|
function collectEntrypointStrings(value: unknown, result: Set<string>): void {
|
|
212
265
|
if (typeof value === 'string') {
|
|
213
266
|
result.add(value);
|
|
@@ -243,39 +296,128 @@ function getPackageEntrypointFiles(
|
|
|
243
296
|
const full = path.resolve(workspaceDir, candidate);
|
|
244
297
|
const relative = path.relative(workspaceDir, full);
|
|
245
298
|
if (relative.startsWith('..') || path.isAbsolute(relative)) continue;
|
|
299
|
+
const normalized = relative.split(path.sep).join('/');
|
|
300
|
+
if (startsWithIgnoredEntrypointDir(normalized)) continue;
|
|
246
301
|
if (!existsSync(full) || !isSourceFile(full)) continue;
|
|
247
302
|
|
|
248
|
-
|
|
249
|
-
const isSrc = normalized.startsWith('src/');
|
|
250
|
-
const isRoot = !normalized.includes('/');
|
|
251
|
-
if (isSrc || isRoot) result.add(full);
|
|
303
|
+
result.add(full);
|
|
252
304
|
}
|
|
253
305
|
|
|
254
306
|
return [...result];
|
|
255
307
|
}
|
|
256
308
|
|
|
257
|
-
|
|
309
|
+
function isRelativeSpecifier(specifier: string): boolean {
|
|
310
|
+
return specifier.startsWith('./') || specifier.startsWith('../');
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function getJsToTsCandidates(file: string): string[] {
|
|
314
|
+
const ext = path.extname(file);
|
|
315
|
+
const withoutExt = file.slice(0, -ext.length);
|
|
316
|
+
|
|
317
|
+
if (ext === '.js') return [`${withoutExt}.ts`, `${withoutExt}.tsx`, file];
|
|
318
|
+
if (ext === '.jsx') return [`${withoutExt}.tsx`, file];
|
|
319
|
+
if (ext === '.mjs') return [`${withoutExt}.mts`, file];
|
|
320
|
+
if (ext === '.cjs') return [`${withoutExt}.cts`, file];
|
|
321
|
+
|
|
322
|
+
return [file];
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function resolveRelativeSourceFile(
|
|
326
|
+
workspaceDir: string,
|
|
327
|
+
importer: string,
|
|
328
|
+
specifier: string,
|
|
329
|
+
): string | null {
|
|
330
|
+
const target = path.resolve(path.dirname(importer), specifier);
|
|
331
|
+
const candidates: string[] = [];
|
|
332
|
+
const ext = path.extname(target);
|
|
333
|
+
|
|
334
|
+
if (ext) {
|
|
335
|
+
candidates.push(...getJsToTsCandidates(target));
|
|
336
|
+
} else {
|
|
337
|
+
for (const sourceExt of SOURCE_EXTENSIONS) {
|
|
338
|
+
candidates.push(`${target}${sourceExt}`);
|
|
339
|
+
}
|
|
340
|
+
for (const sourceExt of SOURCE_EXTENSIONS) {
|
|
341
|
+
candidates.push(path.join(target, `index${sourceExt}`));
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
for (const candidate of candidates) {
|
|
346
|
+
if (!isInside(workspaceDir, candidate)) continue;
|
|
347
|
+
if (existsSync(candidate) && isSourceFile(candidate)) return candidate;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function collectEntrypointGraphFiles(
|
|
354
|
+
workspaceDir: string,
|
|
355
|
+
pkg: PackageJson,
|
|
356
|
+
): string[] {
|
|
357
|
+
const result = new Set<string>();
|
|
358
|
+
const stack = getPackageEntrypointFiles(workspaceDir, pkg);
|
|
359
|
+
|
|
360
|
+
while (stack.length > 0) {
|
|
361
|
+
const file = stack.pop()!;
|
|
362
|
+
if (result.has(file)) continue;
|
|
363
|
+
result.add(file);
|
|
364
|
+
|
|
365
|
+
for (const reference of extractModuleReferences(
|
|
366
|
+
file,
|
|
367
|
+
readFileSync(file, 'utf8'),
|
|
368
|
+
)) {
|
|
369
|
+
if (!isRelativeSpecifier(reference.specifier)) continue;
|
|
370
|
+
const resolved = resolveRelativeSourceFile(
|
|
371
|
+
workspaceDir,
|
|
372
|
+
file,
|
|
373
|
+
reference.specifier,
|
|
374
|
+
);
|
|
375
|
+
if (resolved && !result.has(resolved)) stack.push(resolved);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return [...result];
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export function collectSourceFiles(
|
|
258
383
|
srcDirOrWorkspaceDir: string,
|
|
259
384
|
pkg?: PackageJson,
|
|
260
|
-
):
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
const files = new Set(
|
|
265
|
-
walkFiles(srcDir, (name) => isSourceFile(name)),
|
|
266
|
-
);
|
|
385
|
+
): string[] {
|
|
386
|
+
if (!pkg) return walkFiles(srcDirOrWorkspaceDir, (name) => isSourceFile(name));
|
|
387
|
+
|
|
388
|
+
const files = new Set<string>();
|
|
267
389
|
|
|
268
|
-
|
|
269
|
-
for (const file of
|
|
390
|
+
for (const sourceRoot of DEFAULT_SOURCE_ROOTS) {
|
|
391
|
+
for (const file of walkFiles(
|
|
392
|
+
path.join(srcDirOrWorkspaceDir, sourceRoot),
|
|
393
|
+
(name) => isSourceFile(name),
|
|
394
|
+
)) {
|
|
270
395
|
files.add(file);
|
|
271
396
|
}
|
|
272
397
|
}
|
|
273
398
|
|
|
274
|
-
|
|
399
|
+
for (const file of collectEntrypointGraphFiles(srcDirOrWorkspaceDir, pkg)) {
|
|
400
|
+
files.add(file);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return [...files].sort();
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
export function collectSourceImportsFromFiles(files: string[]): ImportEntry[] {
|
|
407
|
+
return files.flatMap((file) =>
|
|
275
408
|
extractImports(file, readFileSync(file, 'utf8')),
|
|
276
409
|
);
|
|
277
410
|
}
|
|
278
411
|
|
|
412
|
+
export function collectSourceImports(
|
|
413
|
+
srcDirOrWorkspaceDir: string,
|
|
414
|
+
pkg?: PackageJson,
|
|
415
|
+
): ImportEntry[] {
|
|
416
|
+
return collectSourceImportsFromFiles(
|
|
417
|
+
collectSourceFiles(srcDirOrWorkspaceDir, pkg),
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
|
|
279
421
|
export function collectDtsImports(distDir: string): Set<string> {
|
|
280
422
|
const files = walkFiles(distDir, (name) => DTS_RE.test(name));
|
|
281
423
|
const result = new Set<string>();
|
|
@@ -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
|
+
}
|
package/src/model/diagnostics.ts
CHANGED
|
@@ -32,6 +32,21 @@ function collectActualDependencyNames(workspace: WorkspaceFacts): Set<string> {
|
|
|
32
32
|
);
|
|
33
33
|
}
|
|
34
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
|
+
|
|
35
50
|
export function compareManifest(
|
|
36
51
|
expected: ExpectedWorkspace,
|
|
37
52
|
violations: DependencyViolation[],
|
|
@@ -181,6 +196,21 @@ export function validateBasicShape(
|
|
|
181
196
|
});
|
|
182
197
|
}
|
|
183
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
|
+
|
|
184
214
|
if (workspace.hasSrc && !workspace.hasDist && withDts) {
|
|
185
215
|
violations.push({
|
|
186
216
|
code: 'dist-missing',
|
package/src/model/placement.ts
CHANGED
|
@@ -71,7 +71,7 @@ function addPublicDependency(
|
|
|
71
71
|
rules: DependencyRule[],
|
|
72
72
|
depIdent: string,
|
|
73
73
|
reason: Reason,
|
|
74
|
-
options: { allowRootOnly?: boolean } = {},
|
|
74
|
+
options: { allowRootOnly?: boolean; rootOnlyRequired?: boolean } = {},
|
|
75
75
|
): void {
|
|
76
76
|
const workspace = expected.workspace;
|
|
77
77
|
const rule = getEffectiveRule(
|
|
@@ -88,15 +88,20 @@ function addPublicDependency(
|
|
|
88
88
|
addRuleReason(expected, depIdent, rule);
|
|
89
89
|
|
|
90
90
|
if (rule.rootOnly && !isLocal && !options.allowRootOnly) {
|
|
91
|
+
addRuleReason(rootExpected, depIdent, rule);
|
|
91
92
|
setExpectedDependency(rootExpected, 'devDependencies', depIdent, range, {
|
|
92
|
-
kind: 'root-only',
|
|
93
|
-
detail:
|
|
93
|
+
kind: options.rootOnlyRequired ? reason.kind : 'root-only',
|
|
94
|
+
detail: options.rootOnlyRequired
|
|
95
|
+
? `${reason.detail} for ${workspace.name}; rootOnly keeps it in root devDependencies`
|
|
96
|
+
: `${depIdent} is marked rootOnly`,
|
|
94
97
|
});
|
|
95
98
|
addReason(expected, depIdent, reason);
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
99
|
+
if (!options.rootOnlyRequired) {
|
|
100
|
+
addReason(expected, depIdent, {
|
|
101
|
+
kind: 'root-only',
|
|
102
|
+
detail: `${depIdent} is marked rootOnly and cannot be used by ${workspace.name}`,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
100
105
|
return;
|
|
101
106
|
}
|
|
102
107
|
|
|
@@ -326,6 +331,7 @@ export function addRequiredRuleDependencies(
|
|
|
326
331
|
kind: 'required-rule',
|
|
327
332
|
detail: `${depIdent} is marked required by dependency rule`,
|
|
328
333
|
},
|
|
334
|
+
{ rootOnlyRequired: rule.rootOnly },
|
|
329
335
|
);
|
|
330
336
|
}
|
|
331
337
|
}
|
package/src/model/rules.ts
CHANGED
|
@@ -66,6 +66,18 @@ export function hasGlobalVersionRule(
|
|
|
66
66
|
);
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
export function hasWorkspaceVersionRule(
|
|
70
|
+
rules: DependencyRule[],
|
|
71
|
+
depIdent: string,
|
|
72
|
+
): boolean {
|
|
73
|
+
return rules.some(
|
|
74
|
+
(rule) =>
|
|
75
|
+
rule.workspace &&
|
|
76
|
+
rule.version !== undefined &&
|
|
77
|
+
matchesPatterns(depIdent, rule.match),
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
69
81
|
export function isWorkspaceRole(
|
|
70
82
|
value: string | undefined,
|
|
71
83
|
): value is WorkspaceRole {
|
package/src/model/types.ts
CHANGED
|
@@ -69,7 +69,7 @@ export interface DependencyViolation {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
export interface DependencyWarning {
|
|
72
|
-
code: 'bin-role-mismatch';
|
|
72
|
+
code: 'bin-role-mismatch' | 'zero-external-dependencies';
|
|
73
73
|
workspace?: string | undefined;
|
|
74
74
|
workspaceLocation?: string | undefined;
|
|
75
75
|
dependency?: string | undefined;
|
|
@@ -92,6 +92,7 @@ export interface WorkspaceFacts {
|
|
|
92
92
|
dtsImports: Set<string>;
|
|
93
93
|
hasSrc: boolean;
|
|
94
94
|
hasDist: boolean;
|
|
95
|
+
sourceFileCount?: number;
|
|
95
96
|
}
|
|
96
97
|
|
|
97
98
|
export interface WorkspaceContext extends WorkspaceFacts {
|
package/src/model/versions.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { addReason } from './expected';
|
|
9
9
|
import { isExternal, resolveExpectedRangeAfterRulePass } from './placement';
|
|
10
|
-
import { hasGlobalVersionRule } from './rules';
|
|
10
|
+
import { hasGlobalVersionRule, hasWorkspaceVersionRule } from './rules';
|
|
11
11
|
import type {
|
|
12
12
|
DependencyRule,
|
|
13
13
|
DependencyViolation,
|
|
@@ -97,13 +97,18 @@ export function collectUnconstrainedVersionViolations(
|
|
|
97
97
|
)
|
|
98
98
|
.map(([depIdent, byOccurrence]) => {
|
|
99
99
|
const rangeList = [...(ranges.get(depIdent) ?? [])].join(', ');
|
|
100
|
+
const occurrencesList = [...byOccurrence].sort().join(', ');
|
|
101
|
+
const workspaceRuleHint = hasWorkspaceVersionRule(rules, depIdent)
|
|
102
|
+
? '; workspace-scoped version rules do not satisfy V1'
|
|
103
|
+
: '';
|
|
100
104
|
|
|
101
105
|
return {
|
|
102
106
|
code: 'unconstrained-version' as const,
|
|
103
107
|
workspace: '<root>',
|
|
104
108
|
workspaceLocation: '.',
|
|
105
109
|
dependency: depIdent,
|
|
106
|
-
message:
|
|
110
|
+
message:
|
|
111
|
+
`${depIdent} appears ${byOccurrence.size} times without a global version rule (${rangeList}); occurrences: ${occurrencesList}${workspaceRuleHint}`,
|
|
107
112
|
};
|
|
108
113
|
});
|
|
109
114
|
}
|