@rsdk/yarn.constraints 6.0.0-next.4 → 6.0.0-next.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/DEPENDENCY_MODEL.md +460 -0
  2. package/README.MD +85 -21
  3. package/__tests__/compatibility.test.ts +321 -0
  4. package/__tests__/config-validation.test.ts +42 -0
  5. package/__tests__/engine.test.ts +1107 -0
  6. package/__tests__/fixtures/imports/bin.js +4 -0
  7. package/__tests__/fixtures/imports/export-entry.mjs +1 -0
  8. package/__tests__/fixtures/imports/lib/lib-entry.js +3 -0
  9. package/__tests__/fixtures/imports/root-entry.js +4 -0
  10. package/__tests__/fixtures/imports/rules/transitive.js +3 -0
  11. package/__tests__/fixtures/imports/src/common.cjs +3 -0
  12. package/__tests__/fixtures/imports/src/common.cts +3 -0
  13. package/__tests__/fixtures/imports/src/component.tsx +4 -0
  14. package/__tests__/fixtures/imports/src/index.ts +13 -0
  15. package/__tests__/fixtures/imports/src/module.mjs +3 -0
  16. package/__tests__/fixtures/imports/src/module.mts +3 -0
  17. package/__tests__/fixtures/imports/src/plain.js +3 -0
  18. package/__tests__/fixtures/imports/src/test-only-usage.ts +1 -0
  19. package/__tests__/fixtures/imports/test/outside.ts +3 -0
  20. package/__tests__/imports.test.ts +218 -0
  21. package/__tests__/manifest-writer.test.ts +157 -0
  22. package/dist/ansi.d.ts +9 -0
  23. package/dist/ansi.js +24 -0
  24. package/dist/ansi.js.map +1 -0
  25. package/dist/bin/depdoc.d.ts +2 -0
  26. package/dist/bin/depdoc.js +157 -0
  27. package/dist/bin/depdoc.js.map +1 -0
  28. package/dist/collectors/config.d.ts +2 -0
  29. package/dist/collectors/config.js +28 -0
  30. package/dist/collectors/config.js.map +1 -0
  31. package/dist/collectors/external-metadata.d.ts +5 -0
  32. package/dist/collectors/external-metadata.js +110 -0
  33. package/dist/collectors/external-metadata.js.map +1 -0
  34. package/dist/collectors/package-extensions.d.ts +3 -0
  35. package/dist/collectors/package-extensions.js +43 -0
  36. package/dist/collectors/package-extensions.js.map +1 -0
  37. package/dist/collectors/type-providers.d.ts +3 -0
  38. package/dist/collectors/type-providers.js +46 -0
  39. package/dist/collectors/type-providers.js.map +1 -0
  40. package/dist/collectors/workspaces.d.ts +2 -0
  41. package/dist/collectors/workspaces.js +90 -0
  42. package/dist/collectors/workspaces.js.map +1 -0
  43. package/dist/dependency-model.d.ts +11 -0
  44. package/dist/dependency-model.js +18 -0
  45. package/dist/dependency-model.js.map +1 -0
  46. package/dist/index.d.ts +9 -5
  47. package/dist/index.js +13 -33
  48. package/dist/index.js.map +1 -1
  49. package/dist/lib/imports.d.ts +11 -0
  50. package/dist/lib/imports.js +342 -0
  51. package/dist/lib/imports.js.map +1 -0
  52. package/dist/lib/package-json.d.ts +21 -0
  53. package/dist/lib/package-json.js +32 -0
  54. package/dist/lib/package-json.js.map +1 -0
  55. package/dist/model/config-validation.d.ts +6 -0
  56. package/dist/model/config-validation.js +31 -0
  57. package/dist/model/config-validation.js.map +1 -0
  58. package/dist/model/diagnostics.d.ts +4 -0
  59. package/dist/model/diagnostics.js +295 -0
  60. package/dist/model/diagnostics.js.map +1 -0
  61. package/dist/model/engine.d.ts +5 -0
  62. package/dist/model/engine.js +52 -0
  63. package/dist/model/engine.js.map +1 -0
  64. package/dist/model/expected.d.ts +20 -0
  65. package/dist/model/expected.js +89 -0
  66. package/dist/model/expected.js.map +1 -0
  67. package/dist/model/peer-propagation.d.ts +2 -0
  68. package/dist/model/peer-propagation.js +124 -0
  69. package/dist/model/peer-propagation.js.map +1 -0
  70. package/dist/model/placement.d.ts +9 -0
  71. package/dist/model/placement.js +210 -0
  72. package/dist/model/placement.js.map +1 -0
  73. package/dist/model/rules.d.ts +15 -0
  74. package/dist/model/rules.js +52 -0
  75. package/dist/model/rules.js.map +1 -0
  76. package/dist/model/types.d.ts +118 -0
  77. package/dist/model/types.js +9 -0
  78. package/dist/model/types.js.map +1 -0
  79. package/dist/model/versions.d.ts +3 -0
  80. package/dist/model/versions.js +77 -0
  81. package/dist/model/versions.js.map +1 -0
  82. package/dist/reporting.d.ts +3 -0
  83. package/dist/reporting.js +80 -0
  84. package/dist/reporting.js.map +1 -0
  85. package/dist/runner.d.ts +2 -0
  86. package/dist/runner.js +70 -0
  87. package/dist/runner.js.map +1 -0
  88. package/dist/writer/manifest-writer.d.ts +2 -0
  89. package/dist/writer/manifest-writer.js +72 -0
  90. package/dist/writer/manifest-writer.js.map +1 -0
  91. package/eslint.config.cjs +3 -0
  92. package/jest.config.js +1 -0
  93. package/package.json +7 -3
  94. package/src/ansi.ts +23 -0
  95. package/src/bin/depdoc.ts +213 -0
  96. package/src/collectors/config.ts +33 -0
  97. package/src/collectors/external-metadata.ts +148 -0
  98. package/src/collectors/package-extensions.ts +52 -0
  99. package/src/collectors/type-providers.ts +51 -0
  100. package/src/collectors/workspaces.ts +107 -0
  101. package/src/dependency-model.ts +26 -0
  102. package/src/index.ts +28 -45
  103. package/src/lib/imports.ts +435 -0
  104. package/src/lib/package-json.ts +46 -0
  105. package/src/model/config-validation.ts +49 -0
  106. package/src/model/diagnostics.ts +358 -0
  107. package/src/model/engine.ts +120 -0
  108. package/src/model/expected.ts +141 -0
  109. package/src/model/peer-propagation.ts +199 -0
  110. package/src/model/placement.ts +378 -0
  111. package/src/model/rules.ts +85 -0
  112. package/src/model/types.ts +165 -0
  113. package/src/model/versions.ts +114 -0
  114. package/src/reporting.ts +117 -0
  115. package/src/runner.ts +102 -0
  116. package/src/writer/manifest-writer.ts +111 -0
  117. package/tsconfig.build.json +1 -0
  118. package/tsconfig.json +6 -1
  119. package/dist/constraint-schema.d.ts +0 -1
  120. package/dist/constraint-schema.js +0 -17
  121. package/dist/constraint-schema.js.map +0 -1
  122. package/dist/dependency-checker.d.ts +0 -8
  123. package/dist/dependency-checker.js +0 -40
  124. package/dist/dependency-checker.js.map +0 -1
  125. package/src/constraint-schema.ts +0 -20
  126. package/src/dependency-checker.ts +0 -41
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Shared dependency-model contracts.
3
+ *
4
+ * This module defines the data exchanged between collectors, the pure model
5
+ * engine, the manifest writer, and CLI reporting. It intentionally contains no
6
+ * filesystem or Yarn logic; callers should pass already collected facts.
7
+ */
8
+ import type { PackageJson } from '../lib/package-json';
9
+
10
+ export type SectionType =
11
+ | 'dependencies'
12
+ | 'peerDependencies'
13
+ | 'devDependencies';
14
+
15
+ export type WorkspaceRole = 'library' | 'service' | 'cli';
16
+
17
+ export interface DependencyRule {
18
+ match: string | string[];
19
+ workspace?: string | string[];
20
+ section?: SectionType;
21
+ version?: string;
22
+ rootOnly?: boolean;
23
+ required?: boolean;
24
+ }
25
+
26
+ export interface DependencyModelConfig {
27
+ version?: number;
28
+ rules?: DependencyRule[];
29
+ doctor?: DoctorConfig;
30
+ }
31
+
32
+ export interface DoctorConfig {
33
+ buildCmd?: string;
34
+ lintCmd?: string;
35
+ typecheckCmd?: string;
36
+ }
37
+
38
+ export interface DependencyModelOptions {
39
+ rootDir?: string;
40
+ constraintsPath?: string;
41
+ fix?: boolean;
42
+ withDts?: boolean;
43
+ }
44
+
45
+ export interface DependencyViolation {
46
+ code:
47
+ | 'root-private'
48
+ | 'root-section'
49
+ | 'role-missing'
50
+ | 'role-invalid'
51
+ | 'forbidden-section'
52
+ | 'missing'
53
+ | 'wrong-section'
54
+ | 'wrong-range'
55
+ | 'root-only'
56
+ | 'root-only-usage'
57
+ | 'unconstrained-version'
58
+ | 'mirror'
59
+ | 'stale'
60
+ | 'dist-missing';
61
+ workspace: string;
62
+ workspaceLocation: string;
63
+ dependency?: string | undefined;
64
+ actualSection?: SectionType | undefined;
65
+ expectedSection?: SectionType | undefined;
66
+ actualRange?: string | undefined;
67
+ expectedRange?: string | undefined;
68
+ message: string;
69
+ }
70
+
71
+ export interface DependencyWarning {
72
+ code: 'bin-role-mismatch' | 'zero-external-dependencies';
73
+ workspace?: string | undefined;
74
+ workspaceLocation?: string | undefined;
75
+ dependency?: string | undefined;
76
+ message: string;
77
+ }
78
+
79
+ export interface UsageSummary {
80
+ files: Set<string>;
81
+ runtimeFiles: Set<string>;
82
+ typeOnlyFiles: Set<string>;
83
+ }
84
+
85
+ export interface WorkspaceFacts {
86
+ name: string;
87
+ location: string;
88
+ pkg: PackageJson;
89
+ role: WorkspaceRole | undefined;
90
+ isRoot: boolean;
91
+ sourceUsage: Map<string, UsageSummary>;
92
+ dtsImports: Set<string>;
93
+ hasSrc: boolean;
94
+ hasDist: boolean;
95
+ sourceFileCount?: number;
96
+ }
97
+
98
+ export interface WorkspaceContext extends WorkspaceFacts {
99
+ dir: string;
100
+ }
101
+
102
+ export interface DependencyModelFacts {
103
+ workspaces: WorkspaceFacts[];
104
+ externalPeerMetadata: Map<string, Map<string, string> | null>;
105
+ externalTypeMetadata?: Map<string, { hasBundledTypes: boolean }>;
106
+ typeProviderPackages?: Set<string>;
107
+ rules: DependencyRule[];
108
+ withDts?: boolean;
109
+ }
110
+
111
+ export interface DependencyModelOutput {
112
+ violations: DependencyViolation[];
113
+ warnings: DependencyWarning[];
114
+ expected: Map<string, ExpectedWorkspace>;
115
+ }
116
+
117
+ export interface DependencyModelResult {
118
+ rootDir: string;
119
+ violations: DependencyViolation[];
120
+ warnings: DependencyWarning[];
121
+ contexts: WorkspaceContext[];
122
+ expected: Map<string, ExpectedWorkspace>;
123
+ config: DependencyModelConfig;
124
+ }
125
+
126
+ export interface Reason {
127
+ kind:
128
+ | 'declared'
129
+ | 'source-runtime'
130
+ | 'source-public-type'
131
+ | 'source-private-dev'
132
+ | 'rule'
133
+ | 'root-only'
134
+ | 'dts-provider'
135
+ | 'peer-propagation'
136
+ | 'mirror'
137
+ | 'version'
138
+ | 'required-rule';
139
+ detail: string;
140
+ }
141
+
142
+ export interface ExpectedWorkspace {
143
+ workspace: WorkspaceFacts;
144
+ sections: Record<SectionType, Map<string, string>>;
145
+ reasons: Map<string, Reason[]>;
146
+ }
147
+
148
+ export interface EffectiveRule {
149
+ section?: SectionType;
150
+ version?: string;
151
+ rootOnly: boolean;
152
+ required?: boolean;
153
+ }
154
+
155
+ export interface PackageExtension {
156
+ dependencies?: Record<string, string>;
157
+ peerDependencies?: Record<string, string>;
158
+ peerDependenciesMeta?: Record<string, { optional?: boolean }>;
159
+ }
160
+
161
+ export const SECTIONS: SectionType[] = [
162
+ 'dependencies',
163
+ 'peerDependencies',
164
+ 'devDependencies',
165
+ ];
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Version normalization and version-rule diagnostics.
3
+ *
4
+ * The placement engine decides where dependencies belong. This module applies
5
+ * version rules after placement and checks the invariant that repeated external
6
+ * dependencies have a global version rule.
7
+ */
8
+ import { addReason } from './expected';
9
+ import { isExternal, resolveExpectedRangeAfterRulePass } from './placement';
10
+ import { hasGlobalVersionRule, hasWorkspaceVersionRule } from './rules';
11
+ import type {
12
+ DependencyRule,
13
+ DependencyViolation,
14
+ ExpectedWorkspace,
15
+ SectionType,
16
+ WorkspaceFacts,
17
+ } from './types';
18
+ import { SECTIONS } from './types';
19
+
20
+ export function applyVersionRules(
21
+ expectedByLocation: Map<string, ExpectedWorkspace>,
22
+ root: WorkspaceFacts,
23
+ workspaceNames: Set<string>,
24
+ rules: DependencyRule[],
25
+ ): void {
26
+ for (const expected of expectedByLocation.values()) {
27
+ for (const section of SECTIONS) {
28
+ for (const [depIdent, range] of expected.sections[section]) {
29
+ const nextRange = resolveExpectedRangeAfterRulePass(
30
+ root,
31
+ expected,
32
+ workspaceNames,
33
+ rules,
34
+ depIdent,
35
+ range,
36
+ );
37
+
38
+ if (nextRange !== range) {
39
+ expected.sections[section].set(depIdent, nextRange);
40
+ addReason(expected, depIdent, {
41
+ kind: 'version',
42
+ detail: `${depIdent} range resolved to ${nextRange}`,
43
+ });
44
+ }
45
+ }
46
+ }
47
+ }
48
+ }
49
+
50
+ export function collectUnconstrainedVersionViolations(
51
+ workspaces: WorkspaceFacts[],
52
+ expectedByLocation: Map<string, ExpectedWorkspace>,
53
+ rules: DependencyRule[],
54
+ ): DependencyViolation[] {
55
+ const workspaceNames = new Set(
56
+ workspaces.filter((ws) => !ws.isRoot).map((ws) => ws.name),
57
+ );
58
+ const occurrences = new Map<string, Set<string>>();
59
+ const ranges = new Map<string, Set<string>>();
60
+
61
+ const addOccurrence = (
62
+ workspace: WorkspaceFacts,
63
+ section: SectionType,
64
+ depIdent: string,
65
+ range: string,
66
+ ): void => {
67
+ if (!isExternal(depIdent, workspaceNames) || range.startsWith('workspace:'))
68
+ return;
69
+ if (!occurrences.has(depIdent)) occurrences.set(depIdent, new Set());
70
+ if (!ranges.has(depIdent)) ranges.set(depIdent, new Set());
71
+ occurrences.get(depIdent)!.add(`${workspace.location}:${section}`);
72
+ ranges.get(depIdent)!.add(range);
73
+ };
74
+
75
+ for (const workspace of workspaces) {
76
+ for (const section of SECTIONS) {
77
+ for (const [depIdent, range] of Object.entries(
78
+ workspace.pkg[section] ?? {},
79
+ )) {
80
+ addOccurrence(workspace, section, depIdent, range);
81
+ }
82
+ }
83
+ }
84
+
85
+ for (const expected of expectedByLocation.values()) {
86
+ for (const section of SECTIONS) {
87
+ for (const [depIdent, range] of expected.sections[section]) {
88
+ addOccurrence(expected.workspace, section, depIdent, range);
89
+ }
90
+ }
91
+ }
92
+
93
+ return [...occurrences.entries()]
94
+ .filter(
95
+ ([depIdent, byOccurrence]) =>
96
+ byOccurrence.size > 1 && !hasGlobalVersionRule(rules, depIdent),
97
+ )
98
+ .map(([depIdent, byOccurrence]) => {
99
+ const rangeList = [...(ranges.get(depIdent) ?? [])].join(', ');
100
+ const occurrencesList = [...byOccurrence].sort().join(', ');
101
+ const workspaceRuleHint = hasWorkspaceVersionRule(rules, depIdent)
102
+ ? '; workspace-scoped version rules do not satisfy V1'
103
+ : '';
104
+
105
+ return {
106
+ code: 'unconstrained-version' as const,
107
+ workspace: '<root>',
108
+ workspaceLocation: '.',
109
+ dependency: depIdent,
110
+ message:
111
+ `${depIdent} appears ${byOccurrence.size} times without a global version rule (${rangeList}); occurrences: ${occurrencesList}${workspaceRuleHint}`,
112
+ };
113
+ });
114
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * CLI-oriented dependency model reporting.
3
+ *
4
+ * The engine returns structured diagnostics and reason trails. This module
5
+ * turns them into stable human-readable text for `check`, `fix`, and `explain`.
6
+ */
7
+ import {
8
+ getDeclaredRange,
9
+ getDeclaredSection,
10
+ getExpectedRange,
11
+ getExpectedSection,
12
+ } from './model/expected';
13
+ import type { DependencyModelResult } from './model/types';
14
+ import { ansi } from './ansi';
15
+
16
+ export function formatDependencyModelResult(
17
+ result: DependencyModelResult,
18
+ ): string {
19
+ const lines: string[] = [];
20
+
21
+ for (const violation of result.violations) {
22
+ const dep = violation.dependency ? ` ${violation.dependency}` : '';
23
+ const location =
24
+ violation.workspaceLocation === '.'
25
+ ? ''
26
+ : ` (${violation.workspaceLocation})`;
27
+
28
+ lines.push(
29
+ `${ansi.red('✖')} ${ansi.bold(violation.workspace)}${ansi.dim(location)}${ansi.red(dep)}: ${violation.message}`,
30
+ );
31
+ }
32
+
33
+ for (const warning of result.warnings) {
34
+ const workspace = warning.workspace ? `${warning.workspace}: ` : '';
35
+
36
+ lines.push(`${ansi.yellow('⚠')} ${ansi.bold(workspace)}${warning.message}`);
37
+ }
38
+
39
+ if (result.violations.length === 0) {
40
+ lines.push(`${ansi.green('✓')} ${ansi.bold('dependency model is consistent')}`);
41
+ } else {
42
+ lines.push(
43
+ `${ansi.red('✖')} ${ansi.bold(`${result.violations.length} dependency model violation(s)`)}`,
44
+ );
45
+ }
46
+
47
+ if (result.warnings.length > 0) {
48
+ lines.push(
49
+ `${ansi.yellow('⚠')} ${ansi.bold(`${result.warnings.length} warning(s)`)}`,
50
+ );
51
+ }
52
+
53
+ return lines.join('\n');
54
+ }
55
+
56
+ export function explainDependency(
57
+ result: DependencyModelResult,
58
+ workspaceSelector: string,
59
+ depIdent: string,
60
+ ): string {
61
+ const workspace = result.contexts.find(
62
+ (ctx) =>
63
+ ctx.name === workspaceSelector ||
64
+ ctx.location === workspaceSelector ||
65
+ (workspaceSelector === '<root>' && ctx.isRoot),
66
+ );
67
+
68
+ if (!workspace) {
69
+ return `Workspace not found: ${workspaceSelector}`;
70
+ }
71
+
72
+ const expected = result.expected.get(workspace.location);
73
+ if (!expected) {
74
+ return `No dependency model data for workspace: ${workspaceSelector}`;
75
+ }
76
+
77
+ const actualSection = getDeclaredSection(workspace.pkg, depIdent);
78
+ const actualRange = getDeclaredRange(workspace.pkg, depIdent);
79
+ const expectedSection = getExpectedSection(expected, depIdent);
80
+ const expectedRange = getExpectedRange(expected, depIdent);
81
+ const reasons = expected.reasons.get(depIdent) ?? [];
82
+ const lines = [
83
+ `${ansi.bold(workspace.name)} ${ansi.dim(`(${workspace.location})`)}`,
84
+ ansi.cyan(depIdent),
85
+ `${ansi.bold('actual:')} ${actualSection ? `${ansi.yellow(actualSection)}@${actualRange}` : ansi.dim('not declared')}`,
86
+ `${ansi.bold('expected:')} ${expectedSection ? `${ansi.green(expectedSection)}@${expectedRange}` : ansi.dim('not required')}`,
87
+ ];
88
+
89
+ if (actualSection && !expectedSection) {
90
+ lines.push(
91
+ `${ansi.bold('status:')} ${ansi.yellow('actual dependency is stale in the derived model')}`,
92
+ );
93
+ }
94
+
95
+ if (reasons.length > 0) {
96
+ lines.push(ansi.bold('reasons:'));
97
+ for (const reason of reasons) {
98
+ lines.push(` - ${ansi.magenta(reason.kind)}: ${reason.detail}`);
99
+ }
100
+ }
101
+
102
+ const usage = workspace.sourceUsage.get(depIdent);
103
+ if (usage) {
104
+ lines.push(ansi.bold('source imports:'));
105
+ for (const file of [...usage.files].sort()) {
106
+ lines.push(` - ${ansi.dim(file)}`);
107
+ }
108
+ }
109
+
110
+ if (workspace.dtsImports.has(depIdent)) {
111
+ lines.push(
112
+ `${ansi.bold('public type surface:')} ${ansi.green('appears in dist/**/*.d.ts')}`,
113
+ );
114
+ }
115
+
116
+ return lines.join('\n');
117
+ }
package/src/runner.ts ADDED
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Dependency model runner.
3
+ *
4
+ * The runner coordinates impure work: locating the repo root, collecting facts,
5
+ * iterating external metadata discovery, invoking the pure engine, and applying
6
+ * manifest fixes when requested.
7
+ */
8
+ import { loadConfig } from './collectors/config';
9
+ import {
10
+ collectExternalMetadata,
11
+ collectExternalProviderIdents,
12
+ } from './collectors/external-metadata';
13
+ import { loadPackageExtensions } from './collectors/package-extensions';
14
+ import { collectTypeProviderPackages } from './collectors/type-providers';
15
+ import { loadWorkspaces } from './collectors/workspaces';
16
+ import { findMonorepoRoot } from './lib/package-json';
17
+ import { deriveDependencyModel } from './model/engine';
18
+ import type {
19
+ DependencyModelOptions,
20
+ DependencyModelOutput,
21
+ DependencyModelResult,
22
+ } from './model/types';
23
+ import { writeFixes } from './writer/manifest-writer';
24
+
25
+ export function runDependencyModel(
26
+ options: DependencyModelOptions = {},
27
+ ): DependencyModelResult {
28
+ const rootDir = options.rootDir ?? findMonorepoRoot(process.cwd());
29
+ if (!rootDir) {
30
+ throw new Error('Unable to find monorepo root');
31
+ }
32
+
33
+ const config = loadConfig(rootDir, options.constraintsPath);
34
+ const rules = config.rules ?? [];
35
+ const withDts = options.withDts === true;
36
+ const contexts = loadWorkspaces(rootDir, withDts);
37
+ const packageExtensions = loadPackageExtensions(rootDir);
38
+ const workspaceNames = new Set(
39
+ contexts.filter((ws) => !ws.isRoot).map((ws) => ws.name),
40
+ );
41
+ const typeProviderPackages = collectTypeProviderPackages(
42
+ rootDir,
43
+ contexts[0]!.pkg,
44
+ rules,
45
+ );
46
+ const externalPeerMetadata = new Map<string, Map<string, string> | null>();
47
+ const externalTypeMetadata = new Map<string, { hasBundledTypes: boolean }>();
48
+ const providerIdents = collectExternalProviderIdents(
49
+ contexts,
50
+ workspaceNames,
51
+ );
52
+ let output: DependencyModelOutput | null = null;
53
+
54
+ for (let pass = 0; pass < 10; pass++) {
55
+ collectExternalMetadata(
56
+ rootDir,
57
+ packageExtensions,
58
+ providerIdents,
59
+ externalPeerMetadata,
60
+ externalTypeMetadata,
61
+ );
62
+
63
+ output = deriveDependencyModel({
64
+ workspaces: contexts,
65
+ externalPeerMetadata,
66
+ externalTypeMetadata,
67
+ typeProviderPackages,
68
+ rules,
69
+ ...(withDts ? { withDts: true } : {}),
70
+ });
71
+
72
+ let changed = false;
73
+
74
+ for (const ident of collectExternalProviderIdents(
75
+ contexts,
76
+ workspaceNames,
77
+ output.expected,
78
+ )) {
79
+ if (providerIdents.has(ident)) continue;
80
+ providerIdents.add(ident);
81
+ changed = true;
82
+ }
83
+ if (!changed) break;
84
+ }
85
+
86
+ if (!output) {
87
+ throw new Error('Unable to derive dependency model');
88
+ }
89
+
90
+ if (options.fix && output.violations.length > 0) {
91
+ writeFixes(rootDir, output.expected);
92
+ }
93
+
94
+ return {
95
+ rootDir,
96
+ violations: output.violations,
97
+ warnings: output.warnings,
98
+ contexts,
99
+ expected: output.expected,
100
+ config,
101
+ };
102
+ }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Manifest writer.
3
+ *
4
+ * The writer is the only module that mutates package.json files. It preserves
5
+ * unrelated fields, rewrites dependency sections from the expected graph, and
6
+ * skips writes when the normalized section content is unchanged.
7
+ */
8
+ import { writeFileSync } from 'node:fs';
9
+ import path from 'node:path';
10
+
11
+ import type { PackageJson } from '../lib/package-json';
12
+ import type { ExpectedWorkspace } from '../model/types';
13
+ import { SECTIONS } from '../model/types';
14
+
15
+ function sortedRecord(map: Map<string, string>): Record<string, string> {
16
+ return Object.fromEntries(
17
+ [...map.entries()].sort(([a], [b]) => a.localeCompare(b)),
18
+ );
19
+ }
20
+
21
+ function recordsEqual(
22
+ actual: Record<string, string> | undefined,
23
+ expected: Map<string, string>,
24
+ ): boolean {
25
+ const actualEntries = Object.entries(actual ?? {}).sort(([a], [b]) =>
26
+ a.localeCompare(b),
27
+ );
28
+ const expectedEntries = [...expected.entries()].sort(([a], [b]) =>
29
+ a.localeCompare(b),
30
+ );
31
+
32
+ return JSON.stringify(actualEntries) === JSON.stringify(expectedEntries);
33
+ }
34
+
35
+ function peerDependenciesMetaEqual(
36
+ actual: PackageJson['peerDependenciesMeta'] | undefined,
37
+ expected: PackageJson['peerDependenciesMeta'],
38
+ ): boolean {
39
+ const actualEntries = Object.entries(actual ?? {}).sort(([a], [b]) =>
40
+ a.localeCompare(b),
41
+ );
42
+ const expectedEntries = Object.entries(expected ?? {}).sort(([a], [b]) =>
43
+ a.localeCompare(b),
44
+ );
45
+
46
+ return JSON.stringify(actualEntries) === JSON.stringify(expectedEntries);
47
+ }
48
+
49
+ function prunePeerDependenciesMeta(
50
+ pkg: PackageJson,
51
+ expectedPeerDependencies: Map<string, string>,
52
+ ): boolean {
53
+ if (pkg.peerDependenciesMeta === undefined) return false;
54
+
55
+ const nextMeta = Object.fromEntries(
56
+ Object.entries(pkg.peerDependenciesMeta)
57
+ .filter(([depIdent]) => expectedPeerDependencies.has(depIdent))
58
+ .sort(([a], [b]) => a.localeCompare(b)),
59
+ );
60
+
61
+ if (Object.keys(nextMeta).length === 0) {
62
+ delete pkg.peerDependenciesMeta;
63
+ return true;
64
+ }
65
+
66
+ if (peerDependenciesMetaEqual(pkg.peerDependenciesMeta, nextMeta)) {
67
+ return false;
68
+ }
69
+
70
+ pkg.peerDependenciesMeta = nextMeta;
71
+ return true;
72
+ }
73
+
74
+ export function writeFixes(
75
+ rootDir: string,
76
+ expectedByLocation: Map<string, ExpectedWorkspace>,
77
+ ): void {
78
+ for (const expected of expectedByLocation.values()) {
79
+ const pkg = expected.workspace.pkg;
80
+ let changed = false;
81
+
82
+ for (const section of SECTIONS) {
83
+ if (expected.sections[section].size === 0) {
84
+ if (pkg[section] !== undefined) {
85
+ delete pkg[section];
86
+ changed = true;
87
+ }
88
+ continue;
89
+ }
90
+
91
+ if (!recordsEqual(pkg[section], expected.sections[section])) {
92
+ pkg[section] = sortedRecord(expected.sections[section]);
93
+ changed = true;
94
+ }
95
+ }
96
+
97
+ changed =
98
+ prunePeerDependenciesMeta(pkg, expected.sections.peerDependencies) ||
99
+ changed;
100
+
101
+ if (changed) {
102
+ const pkgPath = path.join(
103
+ rootDir,
104
+ expected.workspace.location,
105
+ 'package.json',
106
+ );
107
+
108
+ writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
109
+ }
110
+ }
111
+ }
@@ -4,6 +4,7 @@
4
4
  "node_modules",
5
5
  "dist",
6
6
  "test",
7
+ "__tests__",
7
8
  "**/*.spec.ts",
8
9
  "**/*.test.ts",
9
10
  "**/*.test.e2e.ts",
package/tsconfig.json CHANGED
@@ -3,5 +3,10 @@
3
3
  "compilerOptions": {
4
4
  "declaration": true,
5
5
  "outDir": "dist"
6
- }
6
+ },
7
+ "exclude": [
8
+ "node_modules",
9
+ "dist",
10
+ "__tests__/fixtures"
11
+ ]
7
12
  }
@@ -1 +0,0 @@
1
- export declare const ConstraintSchema: import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, import("@sinclair/typebox").TString>, import("@sinclair/typebox").TString]>>;
@@ -1,17 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ConstraintSchema = void 0;
4
- const typebox_1 = require("@sinclair/typebox");
5
- exports.ConstraintSchema = typebox_1.Type.Record(typebox_1.Type.String({
6
- description: 'scope or package name',
7
- }), typebox_1.Type.Union([
8
- typebox_1.Type.Record(typebox_1.Type.String({
9
- description: 'scope package name',
10
- }), typebox_1.Type.String({
11
- description: 'scope package version',
12
- })),
13
- typebox_1.Type.String({
14
- description: 'package version',
15
- }),
16
- ]));
17
- //# sourceMappingURL=constraint-schema.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"constraint-schema.js","sourceRoot":"","sources":["../src/constraint-schema.ts"],"names":[],"mappings":";;;AAAA,+CAAyC;AAE5B,QAAA,gBAAgB,GAAG,cAAI,CAAC,MAAM,CACzC,cAAI,CAAC,MAAM,CAAC;IACV,WAAW,EAAE,uBAAuB;CACrC,CAAC,EACF,cAAI,CAAC,KAAK,CAAC;IACT,cAAI,CAAC,MAAM,CACT,cAAI,CAAC,MAAM,CAAC;QACV,WAAW,EAAE,oBAAoB;KAClC,CAAC,EACF,cAAI,CAAC,MAAM,CAAC;QACV,WAAW,EAAE,uBAAuB;KACrC,CAAC,CACH;IACD,cAAI,CAAC,MAAM,CAAC;QACV,WAAW,EAAE,iBAAiB;KAC/B,CAAC;CACH,CAAC,CACH,CAAC"}
@@ -1,8 +0,0 @@
1
- import type { Static } from '@sinclair/typebox';
2
- import type { Constraints } from '@yarnpkg/types/lib/yarn';
3
- import type { ConstraintSchema } from './constraint-schema';
4
- export declare class DependencyChecker {
5
- private readonly yarn;
6
- constructor(yarn: Constraints.Yarn);
7
- enforceConsistentDependenciesAcrossTheProject(constraints: Static<typeof ConstraintSchema>, part?: string): void;
8
- }