@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.
Files changed (118) hide show
  1. package/DEPENDENCY_MODEL.md +452 -0
  2. package/README.MD +24 -0
  3. package/__tests__/compatibility.test.ts +321 -0
  4. package/__tests__/engine.test.ts +1002 -0
  5. package/__tests__/fixtures/imports/bin.js +4 -0
  6. package/__tests__/fixtures/imports/export-entry.mjs +1 -0
  7. package/__tests__/fixtures/imports/root-entry.js +3 -0
  8. package/__tests__/fixtures/imports/src/common.cjs +3 -0
  9. package/__tests__/fixtures/imports/src/common.cts +3 -0
  10. package/__tests__/fixtures/imports/src/component.tsx +4 -0
  11. package/__tests__/fixtures/imports/src/index.ts +13 -0
  12. package/__tests__/fixtures/imports/src/module.mjs +3 -0
  13. package/__tests__/fixtures/imports/src/module.mts +3 -0
  14. package/__tests__/fixtures/imports/src/plain.js +3 -0
  15. package/__tests__/fixtures/imports/src/test-only-usage.ts +1 -0
  16. package/__tests__/imports.test.ts +206 -0
  17. package/__tests__/manifest-writer.test.ts +157 -0
  18. package/dist/ansi.d.ts +9 -0
  19. package/dist/ansi.js +24 -0
  20. package/dist/ansi.js.map +1 -0
  21. package/dist/bin/depdoc.d.ts +2 -0
  22. package/dist/bin/depdoc.js +157 -0
  23. package/dist/bin/depdoc.js.map +1 -0
  24. package/dist/collectors/config.d.ts +2 -0
  25. package/dist/collectors/config.js +25 -0
  26. package/dist/collectors/config.js.map +1 -0
  27. package/dist/collectors/external-metadata.d.ts +5 -0
  28. package/dist/collectors/external-metadata.js +110 -0
  29. package/dist/collectors/external-metadata.js.map +1 -0
  30. package/dist/collectors/package-extensions.d.ts +3 -0
  31. package/dist/collectors/package-extensions.js +43 -0
  32. package/dist/collectors/package-extensions.js.map +1 -0
  33. package/dist/collectors/type-providers.d.ts +3 -0
  34. package/dist/collectors/type-providers.js +46 -0
  35. package/dist/collectors/type-providers.js.map +1 -0
  36. package/dist/collectors/workspaces.d.ts +2 -0
  37. package/dist/collectors/workspaces.js +88 -0
  38. package/dist/collectors/workspaces.js.map +1 -0
  39. package/dist/dependency-model.d.ts +11 -0
  40. package/dist/dependency-model.js +18 -0
  41. package/dist/dependency-model.js.map +1 -0
  42. package/dist/index.d.ts +9 -5
  43. package/dist/index.js +13 -33
  44. package/dist/index.js.map +1 -1
  45. package/dist/lib/imports.d.ts +9 -0
  46. package/dist/lib/imports.js +249 -0
  47. package/dist/lib/imports.js.map +1 -0
  48. package/dist/lib/package-json.d.ts +21 -0
  49. package/dist/lib/package-json.js +32 -0
  50. package/dist/lib/package-json.js.map +1 -0
  51. package/dist/model/diagnostics.d.ts +4 -0
  52. package/dist/model/diagnostics.js +273 -0
  53. package/dist/model/diagnostics.js.map +1 -0
  54. package/dist/model/engine.d.ts +5 -0
  55. package/dist/model/engine.js +52 -0
  56. package/dist/model/engine.js.map +1 -0
  57. package/dist/model/expected.d.ts +20 -0
  58. package/dist/model/expected.js +89 -0
  59. package/dist/model/expected.js.map +1 -0
  60. package/dist/model/peer-propagation.d.ts +2 -0
  61. package/dist/model/peer-propagation.js +124 -0
  62. package/dist/model/peer-propagation.js.map +1 -0
  63. package/dist/model/placement.d.ts +9 -0
  64. package/dist/model/placement.js +205 -0
  65. package/dist/model/placement.js.map +1 -0
  66. package/dist/model/rules.d.ts +14 -0
  67. package/dist/model/rules.js +46 -0
  68. package/dist/model/rules.js.map +1 -0
  69. package/dist/model/types.d.ts +117 -0
  70. package/dist/model/types.js +9 -0
  71. package/dist/model/types.js.map +1 -0
  72. package/dist/model/versions.d.ts +3 -0
  73. package/dist/model/versions.js +73 -0
  74. package/dist/model/versions.js.map +1 -0
  75. package/dist/reporting.d.ts +3 -0
  76. package/dist/reporting.js +80 -0
  77. package/dist/reporting.js.map +1 -0
  78. package/dist/runner.d.ts +2 -0
  79. package/dist/runner.js +70 -0
  80. package/dist/runner.js.map +1 -0
  81. package/dist/writer/manifest-writer.d.ts +2 -0
  82. package/dist/writer/manifest-writer.js +72 -0
  83. package/dist/writer/manifest-writer.js.map +1 -0
  84. package/eslint.config.cjs +3 -0
  85. package/jest.config.js +1 -0
  86. package/package.json +7 -3
  87. package/src/ansi.ts +23 -0
  88. package/src/bin/depdoc.ts +213 -0
  89. package/src/collectors/config.ts +26 -0
  90. package/src/collectors/external-metadata.ts +148 -0
  91. package/src/collectors/package-extensions.ts +52 -0
  92. package/src/collectors/type-providers.ts +51 -0
  93. package/src/collectors/workspaces.ts +99 -0
  94. package/src/dependency-model.ts +26 -0
  95. package/src/index.ts +28 -45
  96. package/src/lib/imports.ts +293 -0
  97. package/src/lib/package-json.ts +46 -0
  98. package/src/model/diagnostics.ts +328 -0
  99. package/src/model/engine.ts +120 -0
  100. package/src/model/expected.ts +141 -0
  101. package/src/model/peer-propagation.ts +199 -0
  102. package/src/model/placement.ts +372 -0
  103. package/src/model/rules.ts +73 -0
  104. package/src/model/types.ts +164 -0
  105. package/src/model/versions.ts +109 -0
  106. package/src/reporting.ts +117 -0
  107. package/src/runner.ts +102 -0
  108. package/src/writer/manifest-writer.ts +111 -0
  109. package/tsconfig.build.json +1 -0
  110. package/tsconfig.json +6 -1
  111. package/dist/constraint-schema.d.ts +0 -1
  112. package/dist/constraint-schema.js +0 -17
  113. package/dist/constraint-schema.js.map +0 -1
  114. package/dist/dependency-checker.d.ts +0 -8
  115. package/dist/dependency-checker.js +0 -40
  116. package/dist/dependency-checker.js.map +0 -1
  117. package/src/constraint-schema.ts +0 -20
  118. package/src/dependency-checker.ts +0 -41
@@ -0,0 +1,213 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `depdoc` command-line entrypoint.
4
+ *
5
+ * The CLI only parses commands and orchestrates high-level steps. Dependency
6
+ * placement decisions live in the model engine, while filesystem collection and
7
+ * manifest writes go through the runner.
8
+ */
9
+ import { execSync } from 'node:child_process';
10
+ import { readFileSync } from 'node:fs';
11
+ import path from 'node:path';
12
+
13
+ import { ansi } from '../ansi';
14
+ import {
15
+ explainDependency,
16
+ formatDependencyModelResult,
17
+ runDependencyModel,
18
+ } from '../dependency-model';
19
+
20
+ function printUsage(): void {
21
+ console.error(`${ansi.bold('Usage:')}
22
+ ${ansi.cyan('depdoc')} check [--with-dts]
23
+ ${ansi.cyan('depdoc')} fix [--with-dts]
24
+ ${ansi.cyan('depdoc')} explain <workspace> <dependency>
25
+ ${ansi.cyan('depdoc')} doctor`);
26
+ }
27
+
28
+ function spawnStep(label: string, cmd: string, cwd: string): void {
29
+ console.error(`\n${ansi.cyan('──')} ${ansi.bold(label)}`);
30
+ console.error(`${ansi.dim('$')} ${ansi.cyan(cmd)}`);
31
+ execSync(cmd, { cwd, stdio: 'inherit' });
32
+ }
33
+
34
+ function checkPeerRequirements(cwd: string): boolean {
35
+ const cmd = 'yarn explain peer-requirements';
36
+
37
+ console.error(`\n${ansi.cyan('──')} ${ansi.bold('yarn peer requirements')}`);
38
+ console.error(`${ansi.dim('$')} ${ansi.cyan(cmd)}`);
39
+
40
+ let output: string;
41
+
42
+ try {
43
+ output = execSync(cmd, {
44
+ cwd,
45
+ encoding: 'utf8',
46
+ maxBuffer: 32 * 1024 * 1024,
47
+ });
48
+ } catch (error) {
49
+ const childError = error as { stdout?: string; stderr?: string };
50
+ if (childError.stdout) process.stderr.write(childError.stdout);
51
+ if (childError.stderr) process.stderr.write(childError.stderr);
52
+ return false;
53
+ }
54
+
55
+ const failedRequirements = output
56
+ .split(/\r?\n/)
57
+ .filter((line) => line.includes('✘'));
58
+
59
+ if (failedRequirements.length === 0) {
60
+ console.error(ansi.green('✓ peer requirements are satisfied'));
61
+ return true;
62
+ }
63
+
64
+ console.error(failedRequirements.join('\n'));
65
+ console.error(
66
+ ansi.red(`${failedRequirements.length} peer requirement(s) failed`),
67
+ );
68
+ console.error(
69
+ ansi.dim('Run `yarn explain peer-requirements <hash>` for details.'),
70
+ );
71
+ return false;
72
+ }
73
+
74
+ function snapshotPackageJsons(
75
+ result: ReturnType<typeof runDependencyModel>,
76
+ ): string {
77
+ return [...result.expected.keys()]
78
+ .sort()
79
+ .map((location) =>
80
+ readFileSync(path.join(result.rootDir, location, 'package.json'), 'utf8'),
81
+ )
82
+ .join('\0');
83
+ }
84
+
85
+ function main(): void {
86
+ const rawArgs = process.argv.slice(2);
87
+ if (rawArgs.includes('--help') || rawArgs.includes('-h')) {
88
+ printUsage();
89
+ process.exit(0);
90
+ }
91
+
92
+ const positional = rawArgs.filter((a) => !a.startsWith('--'));
93
+ const [command, firstArg, secondArg] = positional;
94
+ const withDts = rawArgs.includes('--with-dts');
95
+
96
+ if (!command) {
97
+ printUsage();
98
+ process.exit(1);
99
+ }
100
+
101
+ if (command === 'check') {
102
+ const result = runDependencyModel({ withDts });
103
+
104
+ console.error(formatDependencyModelResult(result));
105
+ process.exit(result.violations.length === 0 ? 0 : 1);
106
+ }
107
+
108
+ if (command === 'fix') {
109
+ const before = runDependencyModel({ fix: true, withDts });
110
+ const after = runDependencyModel({ withDts });
111
+ const fixed = Math.max(
112
+ 0,
113
+ before.violations.length - after.violations.length,
114
+ );
115
+
116
+ console.error(formatDependencyModelResult(after));
117
+ if (fixed > 0) {
118
+ console.error(
119
+ ansi.green(`fixed ${fixed} dependency model violation(s)`),
120
+ );
121
+ }
122
+ process.exit(after.violations.length === 0 ? 0 : 1);
123
+ }
124
+
125
+ if (command === 'explain') {
126
+ if (!firstArg || !secondArg) {
127
+ printUsage();
128
+ process.exit(1);
129
+ }
130
+ const result = runDependencyModel();
131
+
132
+ console.log(explainDependency(result, firstArg, secondArg));
133
+ process.exit(0);
134
+ }
135
+
136
+ if (command === 'doctor') {
137
+ runDoctor();
138
+ return;
139
+ }
140
+
141
+ printUsage();
142
+ process.exit(1);
143
+ }
144
+
145
+ function runDoctor(): void {
146
+ console.error(`\n${ansi.cyan('──')} ${ansi.bold('depdoc fix')}`);
147
+ const fixResult = runDependencyModel({ fix: true });
148
+ const afterFix = runDependencyModel();
149
+ const fixed = Math.max(
150
+ 0,
151
+ fixResult.violations.length - afterFix.violations.length,
152
+ );
153
+
154
+ console.error(formatDependencyModelResult(afterFix));
155
+ if (fixed > 0) console.error(ansi.green(`fixed ${fixed} violation(s)`));
156
+
157
+ const { rootDir, config } = afterFix;
158
+
159
+ spawnStep('yarn install', 'yarn install', rootDir);
160
+
161
+ if (config.doctor?.buildCmd) {
162
+ spawnStep('build', config.doctor.buildCmd, rootDir);
163
+ } else {
164
+ console.error(
165
+ `\n${ansi.cyan('──')} ${ansi.yellow('build skipped')} ${ansi.dim('(doctor.buildCmd not configured in .constraints.yml)')}`,
166
+ );
167
+ }
168
+
169
+ const beforeDtsFixSnapshot = snapshotPackageJsons(afterFix);
170
+
171
+ console.error(`\n${ansi.cyan('──')} ${ansi.bold('depdoc fix --with-dts')}`);
172
+ const dtsFix = runDependencyModel({ fix: true, withDts: true });
173
+ const afterDtsFix = runDependencyModel({ withDts: true });
174
+ const afterDtsFixSnapshot = snapshotPackageJsons(afterDtsFix);
175
+ const dtsFixed = Math.max(
176
+ 0,
177
+ dtsFix.violations.length - afterDtsFix.violations.length,
178
+ );
179
+
180
+ console.error(formatDependencyModelResult(afterDtsFix));
181
+ if (dtsFixed > 0) {
182
+ console.error(
183
+ ansi.green(
184
+ `fixed ${dtsFixed} additional violation(s) from .d.ts surface`,
185
+ ),
186
+ );
187
+ }
188
+
189
+ if (beforeDtsFixSnapshot !== afterDtsFixSnapshot) {
190
+ spawnStep('yarn install (post-dts-fix)', 'yarn install', rootDir);
191
+ }
192
+
193
+ if (!checkPeerRequirements(rootDir)) process.exit(1);
194
+
195
+ if (config.doctor?.lintCmd) {
196
+ spawnStep('lint', config.doctor.lintCmd, rootDir);
197
+ }
198
+
199
+ if (config.doctor?.typecheckCmd) {
200
+ spawnStep('typecheck', config.doctor.typecheckCmd, rootDir);
201
+ }
202
+
203
+ const finalViolations = afterDtsFix.violations.length;
204
+
205
+ console.error(
206
+ finalViolations === 0
207
+ ? `\n${ansi.green('✓')} ${ansi.bold('doctor: all checks passed')}`
208
+ : `\n${ansi.red('✖')} ${ansi.bold(`doctor: ${finalViolations} violation(s) remain`)}`,
209
+ );
210
+ process.exit(finalViolations === 0 ? 0 : 1);
211
+ }
212
+
213
+ main();
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Constraints configuration collector.
3
+ *
4
+ * This module is the only place that resolves and parses `.constraints.yml`.
5
+ * It returns raw model configuration; rule semantics are handled later by the
6
+ * pure model layer.
7
+ */
8
+ import { existsSync, readFileSync } from 'node:fs';
9
+ import path from 'node:path';
10
+ import yaml from 'yaml';
11
+
12
+ import type { DependencyModelConfig } from '../model/types';
13
+
14
+ export function loadConfig(
15
+ rootDir: string,
16
+ constraintsPath?: string,
17
+ ): DependencyModelConfig {
18
+ const filepath =
19
+ constraintsPath ??
20
+ process.env.RSDK_YARN_CONSTRAINTS_CFG ??
21
+ path.join(rootDir, '.rsdk/.constraints.yml');
22
+
23
+ if (!existsSync(filepath)) return { rules: [] };
24
+
25
+ return yaml.parse(readFileSync(filepath, 'utf8')) as DependencyModelConfig;
26
+ }
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Installed external package metadata collector.
3
+ *
4
+ * Peer and bundled-type metadata are read from installed packages and Yarn
5
+ * packageExtensions. The engine consumes the resulting maps as facts, keeping
6
+ * node_modules access outside the pure model.
7
+ */
8
+ import path from 'node:path';
9
+
10
+ import { readPackageJson } from '../lib/package-json';
11
+ import type {
12
+ ExpectedWorkspace,
13
+ PackageExtension,
14
+ WorkspaceContext,
15
+ } from '../model/types';
16
+ import { SECTIONS } from '../model/types';
17
+
18
+ import { getMatchingPackageExtensions } from './package-extensions';
19
+
20
+ const externalPeersCache = new Map<string, Map<string, string> | null>();
21
+
22
+ function getExternalRequiredPeers(
23
+ rootDir: string,
24
+ packageExtensions: Map<string, PackageExtension>,
25
+ extIdent: string,
26
+ ): Map<string, string> | null {
27
+ const cacheKey = `${rootDir}:${extIdent}`;
28
+ if (externalPeersCache.has(cacheKey))
29
+ return externalPeersCache.get(cacheKey)!;
30
+
31
+ let peers: Record<string, string>;
32
+ let meta: Record<string, { optional?: boolean }>;
33
+
34
+ try {
35
+ const pkg = readPackageJson(path.join(rootDir, 'node_modules', extIdent));
36
+
37
+ peers = { ...pkg.peerDependencies };
38
+ meta = { ...pkg.peerDependenciesMeta };
39
+ } catch {
40
+ peers = {};
41
+ meta = {};
42
+ }
43
+
44
+ for (const extension of getMatchingPackageExtensions(
45
+ extIdent,
46
+ packageExtensions,
47
+ )) {
48
+ peers = { ...peers, ...extension.peerDependencies };
49
+ meta = { ...meta, ...extension.peerDependenciesMeta };
50
+ }
51
+
52
+ const required = new Map(
53
+ Object.entries(peers).filter(([ident]) => !meta[ident]?.optional),
54
+ );
55
+ const value = required.size > 0 ? required : null;
56
+
57
+ externalPeersCache.set(cacheKey, value);
58
+
59
+ return value;
60
+ }
61
+
62
+ function exportsDeclareTypes(value: unknown): boolean {
63
+ if (!value || typeof value !== 'object') return false;
64
+ if (Array.isArray(value)) return value.some(exportsDeclareTypes);
65
+
66
+ for (const [key, nested] of Object.entries(
67
+ value as Record<string, unknown>,
68
+ )) {
69
+ if ((key === 'types' || key === 'typings') && typeof nested === 'string') {
70
+ return true;
71
+ }
72
+ if (exportsDeclareTypes(nested)) return true;
73
+ }
74
+
75
+ return false;
76
+ }
77
+
78
+ function getExternalTypeMetadata(
79
+ rootDir: string,
80
+ extIdent: string,
81
+ ): { hasBundledTypes: boolean } {
82
+ try {
83
+ const pkg = readPackageJson(path.join(rootDir, 'node_modules', extIdent));
84
+
85
+ return {
86
+ hasBundledTypes:
87
+ typeof pkg.types === 'string' ||
88
+ typeof pkg.typings === 'string' ||
89
+ exportsDeclareTypes(pkg.exports),
90
+ };
91
+ } catch {
92
+ return { hasBundledTypes: false };
93
+ }
94
+ }
95
+
96
+ export function collectExternalProviderIdents(
97
+ contexts: WorkspaceContext[],
98
+ workspaceNames: Set<string>,
99
+ expectedByLocation?: Map<string, ExpectedWorkspace>,
100
+ ): Set<string> {
101
+ const result = new Set<string>();
102
+ const add = (depIdent: string, range?: string): void => {
103
+ if (workspaceNames.has(depIdent) || range?.startsWith('workspace:')) return;
104
+ result.add(depIdent);
105
+ };
106
+
107
+ for (const ctx of contexts) {
108
+ for (const section of SECTIONS) {
109
+ for (const [depIdent, range] of Object.entries(ctx.pkg[section] ?? {})) {
110
+ add(depIdent, range);
111
+ }
112
+ }
113
+ for (const depIdent of ctx.sourceUsage.keys()) add(depIdent);
114
+ for (const depIdent of ctx.dtsImports) add(depIdent);
115
+ }
116
+
117
+ if (expectedByLocation) {
118
+ for (const expected of expectedByLocation.values()) {
119
+ for (const section of SECTIONS) {
120
+ for (const [depIdent, range] of expected.sections[section]) {
121
+ add(depIdent, range);
122
+ }
123
+ }
124
+ }
125
+ }
126
+
127
+ return result;
128
+ }
129
+
130
+ export function collectExternalMetadata(
131
+ rootDir: string,
132
+ packageExtensions: Map<string, PackageExtension>,
133
+ idents: Set<string>,
134
+ externalPeerMetadata: Map<string, Map<string, string> | null>,
135
+ externalTypeMetadata: Map<string, { hasBundledTypes: boolean }>,
136
+ ): void {
137
+ for (const ident of idents) {
138
+ if (!externalPeerMetadata.has(ident)) {
139
+ externalPeerMetadata.set(
140
+ ident,
141
+ getExternalRequiredPeers(rootDir, packageExtensions, ident),
142
+ );
143
+ }
144
+ if (!externalTypeMetadata.has(ident)) {
145
+ externalTypeMetadata.set(ident, getExternalTypeMetadata(rootDir, ident));
146
+ }
147
+ }
148
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Yarn packageExtensions collector.
3
+ *
4
+ * Third-party peer metadata corrections are read from `.yarnrc.yml` here. The
5
+ * model receives these corrections as facts and does not parse Yarn config
6
+ * directly.
7
+ */
8
+ import { existsSync, readFileSync } from 'node:fs';
9
+ import path from 'node:path';
10
+ import yaml from 'yaml';
11
+
12
+ import type { PackageExtension } from '../model/types';
13
+
14
+ export function loadPackageExtensions(
15
+ rootDir: string,
16
+ ): Map<string, PackageExtension> {
17
+ const filepath = path.join(rootDir, '.yarnrc.yml');
18
+ if (!existsSync(filepath)) return new Map();
19
+
20
+ const parsed = yaml.parse(readFileSync(filepath, 'utf8')) as {
21
+ packageExtensions?: Record<string, PackageExtension>;
22
+ };
23
+ const result = new Map<string, PackageExtension>();
24
+
25
+ for (const [selector, extension] of Object.entries(
26
+ parsed.packageExtensions ?? {},
27
+ )) {
28
+ result.set(selector, extension);
29
+ }
30
+
31
+ return result;
32
+ }
33
+
34
+ function extensionSelectorIdent(selector: string): string {
35
+ const atIndex = selector.lastIndexOf('@');
36
+ if (atIndex <= 0) return selector;
37
+
38
+ return selector.slice(0, atIndex);
39
+ }
40
+
41
+ export function getMatchingPackageExtensions(
42
+ depIdent: string,
43
+ packageExtensions: Map<string, PackageExtension>,
44
+ ): PackageExtension[] {
45
+ const result: PackageExtension[] = [];
46
+
47
+ for (const [selector, extension] of packageExtensions) {
48
+ if (extensionSelectorIdent(selector) === depIdent) result.push(extension);
49
+ }
50
+
51
+ return result;
52
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * DefinitelyTyped provider collector.
3
+ *
4
+ * The engine needs to know which `@types/*` packages are available for public
5
+ * `.d.ts` promotion. This module gathers that set from installed packages, root
6
+ * devDependencies, and explicit non-wildcard rules.
7
+ */
8
+ import { readdirSync } from 'node:fs';
9
+ import path from 'node:path';
10
+
11
+ import type { PackageJson } from '../lib/package-json';
12
+ import type { DependencyRule } from '../model/types';
13
+
14
+ function collectInstalledTypeProviders(rootDir: string): Set<string> {
15
+ const result = new Set<string>();
16
+ const typesDir = path.join(rootDir, 'node_modules', '@types');
17
+
18
+ try {
19
+ for (const entry of readdirSync(typesDir, { withFileTypes: true })) {
20
+ if (entry.isDirectory()) result.add(`@types/${entry.name}`);
21
+ }
22
+ } catch {
23
+ // node_modules may not exist yet; root devDeps and rules still contribute.
24
+ }
25
+
26
+ return result;
27
+ }
28
+
29
+ export function collectTypeProviderPackages(
30
+ rootDir: string,
31
+ rootPkg: PackageJson,
32
+ rules: DependencyRule[],
33
+ ): Set<string> {
34
+ const result = collectInstalledTypeProviders(rootDir);
35
+
36
+ for (const depIdent of Object.keys(rootPkg.devDependencies ?? {})) {
37
+ if (depIdent.startsWith('@types/')) result.add(depIdent);
38
+ }
39
+
40
+ for (const rule of rules) {
41
+ const patterns = Array.isArray(rule.match) ? rule.match : [rule.match];
42
+
43
+ for (const pattern of patterns) {
44
+ if (pattern.startsWith('@types/') && !pattern.includes('*')) {
45
+ result.add(pattern);
46
+ }
47
+ }
48
+ }
49
+
50
+ return result;
51
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Workspace and usage collector.
3
+ *
4
+ * This module discovers Yarn workspaces, reads their manifests, and attaches
5
+ * source/`.d.ts` import usage facts. It is deliberately outside the engine
6
+ * because it executes Yarn and reads the filesystem.
7
+ */
8
+ import { execSync } from 'node:child_process';
9
+ import { existsSync } from 'node:fs';
10
+ import path from 'node:path';
11
+
12
+ import { collectDtsImports, collectSourceImports } from '../lib/imports';
13
+ import { readPackageJson } from '../lib/package-json';
14
+ import { isWorkspaceRole } from '../model/rules';
15
+ import type { WorkspaceContext } from '../model/types';
16
+
17
+ interface YarnWorkspaceInfo {
18
+ location: string;
19
+ name: string | null;
20
+ }
21
+
22
+ function collectUsage(workspace: WorkspaceContext, withDts: boolean): void {
23
+ for (const entry of collectSourceImports(workspace.dir, workspace.pkg)) {
24
+ if (!workspace.sourceUsage.has(entry.packageName)) {
25
+ workspace.sourceUsage.set(entry.packageName, {
26
+ files: new Set(),
27
+ runtimeFiles: new Set(),
28
+ typeOnlyFiles: new Set(),
29
+ });
30
+ }
31
+ const usage = workspace.sourceUsage.get(entry.packageName)!;
32
+
33
+ usage.files.add(entry.file);
34
+ if (entry.isTypeOnly) {
35
+ usage.typeOnlyFiles.add(entry.file);
36
+ } else {
37
+ usage.runtimeFiles.add(entry.file);
38
+ }
39
+ }
40
+
41
+ const distDir = path.join(workspace.dir, 'dist');
42
+ if (withDts || existsSync(distDir)) {
43
+ workspace.dtsImports = collectDtsImports(distDir);
44
+ }
45
+ }
46
+
47
+ export function loadWorkspaces(
48
+ rootDir: string,
49
+ withDts: boolean,
50
+ ): WorkspaceContext[] {
51
+ const rootPkg = readPackageJson(rootDir);
52
+ const raw = execSync('yarn workspaces list --json', {
53
+ cwd: rootDir,
54
+ }).toString();
55
+ const listed = raw
56
+ .trim()
57
+ .split('\n')
58
+ .filter(Boolean)
59
+ .map((line) => JSON.parse(line) as YarnWorkspaceInfo)
60
+ .filter((ws) => ws.location !== '.');
61
+
62
+ const root: WorkspaceContext = {
63
+ name: rootPkg.name,
64
+ location: '.',
65
+ dir: rootDir,
66
+ pkg: rootPkg,
67
+ role: undefined,
68
+ isRoot: true,
69
+ sourceUsage: new Map(),
70
+ dtsImports: new Set(),
71
+ hasSrc: false,
72
+ hasDist: false,
73
+ };
74
+
75
+ const workspaces: WorkspaceContext[] = [root];
76
+
77
+ for (const ws of listed) {
78
+ const dir = path.join(rootDir, ws.location);
79
+ const pkg = readPackageJson(dir);
80
+ const role = isWorkspaceRole(pkg.role) ? pkg.role : undefined;
81
+ const ctx: WorkspaceContext = {
82
+ name: ws.name ?? pkg.name,
83
+ location: ws.location,
84
+ dir,
85
+ pkg,
86
+ role,
87
+ isRoot: false,
88
+ sourceUsage: new Map(),
89
+ dtsImports: new Set(),
90
+ hasSrc: existsSync(path.join(dir, 'src')),
91
+ hasDist: existsSync(path.join(dir, 'dist')),
92
+ };
93
+
94
+ collectUsage(ctx, withDts);
95
+ workspaces.push(ctx);
96
+ }
97
+
98
+ return workspaces;
99
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Public dependency-model facade.
3
+ *
4
+ * Historically this file contained the whole implementation. It now keeps the
5
+ * public import path stable while delegating to focused modules: the pure model
6
+ * engine, the filesystem/Yarn runner, and CLI reporting helpers.
7
+ */
8
+ export { deriveDependencyModel } from './model/engine';
9
+ export { runDependencyModel } from './runner';
10
+ export { explainDependency, formatDependencyModelResult } from './reporting';
11
+
12
+ export type {
13
+ DependencyModelConfig,
14
+ DependencyModelFacts,
15
+ DependencyModelOptions,
16
+ DependencyModelOutput,
17
+ DependencyModelResult,
18
+ DependencyRule,
19
+ DependencyViolation,
20
+ DependencyWarning,
21
+ ExpectedWorkspace,
22
+ SectionType,
23
+ UsageSummary,
24
+ WorkspaceFacts,
25
+ WorkspaceRole,
26
+ } from './model/types';
package/src/index.ts CHANGED
@@ -1,46 +1,29 @@
1
- import { Value } from '@sinclair/typebox/value';
2
- import type { Yarn } from '@yarnpkg/types';
3
- import type { Constraints } from '@yarnpkg/types/lib/yarn';
4
- import { access, constants, readFile } from 'node:fs/promises';
5
- import yaml from 'yaml';
1
+ /**
2
+ * Package public API.
3
+ *
4
+ * Consumers should import from this barrel instead of reaching into the model,
5
+ * runner, or collector internals. The split modules remain implementation
6
+ * details unless explicitly exported here.
7
+ */
8
+ export {
9
+ deriveDependencyModel,
10
+ runDependencyModel,
11
+ formatDependencyModelResult,
12
+ explainDependency,
13
+ } from './dependency-model';
6
14
 
7
- import { ConstraintSchema } from './constraint-schema';
8
- import { DependencyChecker } from './dependency-checker';
9
-
10
- const constraintsManifestPath =
11
- process.env.RSDK_YARN_CONSTRAINTS_CFG ?? './.rsdk/.constraints.yml';
12
-
13
- export const createConstraints =
14
- (overrideConfig?: Yarn.Config) =>
15
- async (ctx: Constraints.Context): Promise<void> => {
16
- const { Yarn } = ctx;
17
-
18
- await access(constraintsManifestPath, constants.R_OK);
19
- const constraints = yaml.parse(
20
- await readFile(constraintsManifestPath, 'utf8'),
21
- );
22
- const isValid = Value.Check(ConstraintSchema, constraints);
23
- if (!isValid) {
24
- const cause = [...Value.Errors(ConstraintSchema, constraints)];
25
-
26
- throw new Error(
27
- 'Invalid constraints.yml\n' + JSON.stringify(cause, null, 2),
28
- {
29
- cause: cause,
30
- },
31
- );
32
- }
33
- new DependencyChecker(Yarn).enforceConsistentDependenciesAcrossTheProject(
34
- constraints,
35
- );
36
- overrideConfig?.constraints?.(ctx);
37
- };
38
-
39
- export const config: Yarn.Config = {
40
- constraints: createConstraints(),
41
- };
42
-
43
- export const override = (config: Yarn.Config): Yarn.Config => ({
44
- ...config,
45
- constraints: createConstraints(config),
46
- });
15
+ export type {
16
+ DependencyRule,
17
+ DependencyModelConfig,
18
+ DependencyModelOptions,
19
+ DependencyModelFacts,
20
+ DependencyModelOutput,
21
+ DependencyModelResult,
22
+ DependencyViolation,
23
+ DependencyWarning,
24
+ WorkspaceFacts,
25
+ WorkspaceRole,
26
+ SectionType,
27
+ UsageSummary,
28
+ ExpectedWorkspace,
29
+ } from './dependency-model';