@domainlang/language 0.1.20
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/README.md +163 -0
- package/out/ast-augmentation.d.ts +6 -0
- package/out/ast-augmentation.js +2 -0
- package/out/ast-augmentation.js.map +1 -0
- package/out/domain-lang-module.d.ts +57 -0
- package/out/domain-lang-module.js +67 -0
- package/out/domain-lang-module.js.map +1 -0
- package/out/generated/ast.d.ts +759 -0
- package/out/generated/ast.js +556 -0
- package/out/generated/ast.js.map +1 -0
- package/out/generated/grammar.d.ts +6 -0
- package/out/generated/grammar.js +2407 -0
- package/out/generated/grammar.js.map +1 -0
- package/out/generated/module.d.ts +13 -0
- package/out/generated/module.js +21 -0
- package/out/generated/module.js.map +1 -0
- package/out/index.d.ts +16 -0
- package/out/index.js +22 -0
- package/out/index.js.map +1 -0
- package/out/lsp/domain-lang-code-actions.d.ts +55 -0
- package/out/lsp/domain-lang-code-actions.js +143 -0
- package/out/lsp/domain-lang-code-actions.js.map +1 -0
- package/out/lsp/domain-lang-completion.d.ts +37 -0
- package/out/lsp/domain-lang-completion.js +452 -0
- package/out/lsp/domain-lang-completion.js.map +1 -0
- package/out/lsp/domain-lang-formatter.d.ts +15 -0
- package/out/lsp/domain-lang-formatter.js +43 -0
- package/out/lsp/domain-lang-formatter.js.map +1 -0
- package/out/lsp/domain-lang-naming.d.ts +34 -0
- package/out/lsp/domain-lang-naming.js +49 -0
- package/out/lsp/domain-lang-naming.js.map +1 -0
- package/out/lsp/domain-lang-scope.d.ts +59 -0
- package/out/lsp/domain-lang-scope.js +102 -0
- package/out/lsp/domain-lang-scope.js.map +1 -0
- package/out/lsp/domain-lang-workspace-manager.d.ts +21 -0
- package/out/lsp/domain-lang-workspace-manager.js +93 -0
- package/out/lsp/domain-lang-workspace-manager.js.map +1 -0
- package/out/lsp/hover/ddd-pattern-explanations.d.ts +50 -0
- package/out/lsp/hover/ddd-pattern-explanations.js +196 -0
- package/out/lsp/hover/ddd-pattern-explanations.js.map +1 -0
- package/out/lsp/hover/domain-lang-hover.d.ts +19 -0
- package/out/lsp/hover/domain-lang-hover.js +302 -0
- package/out/lsp/hover/domain-lang-hover.js.map +1 -0
- package/out/lsp/hover/domain-lang-keywords.d.ts +13 -0
- package/out/lsp/hover/domain-lang-keywords.js +47 -0
- package/out/lsp/hover/domain-lang-keywords.js.map +1 -0
- package/out/lsp/manifest-diagnostics.d.ts +82 -0
- package/out/lsp/manifest-diagnostics.js +230 -0
- package/out/lsp/manifest-diagnostics.js.map +1 -0
- package/out/main-browser.d.ts +1 -0
- package/out/main-browser.js +11 -0
- package/out/main-browser.js.map +1 -0
- package/out/main.d.ts +1 -0
- package/out/main.js +74 -0
- package/out/main.js.map +1 -0
- package/out/sdk/ast-augmentation.d.ts +136 -0
- package/out/sdk/ast-augmentation.js +62 -0
- package/out/sdk/ast-augmentation.js.map +1 -0
- package/out/sdk/index.d.ts +94 -0
- package/out/sdk/index.js +97 -0
- package/out/sdk/index.js.map +1 -0
- package/out/sdk/indexes.d.ts +16 -0
- package/out/sdk/indexes.js +97 -0
- package/out/sdk/indexes.js.map +1 -0
- package/out/sdk/loader-node.d.ts +51 -0
- package/out/sdk/loader-node.js +119 -0
- package/out/sdk/loader-node.js.map +1 -0
- package/out/sdk/loader.d.ts +49 -0
- package/out/sdk/loader.js +85 -0
- package/out/sdk/loader.js.map +1 -0
- package/out/sdk/patterns.d.ts +93 -0
- package/out/sdk/patterns.js +123 -0
- package/out/sdk/patterns.js.map +1 -0
- package/out/sdk/query.d.ts +90 -0
- package/out/sdk/query.js +679 -0
- package/out/sdk/query.js.map +1 -0
- package/out/sdk/resolution.d.ts +52 -0
- package/out/sdk/resolution.js +68 -0
- package/out/sdk/resolution.js.map +1 -0
- package/out/sdk/types.d.ts +280 -0
- package/out/sdk/types.js +8 -0
- package/out/sdk/types.js.map +1 -0
- package/out/services/dependency-analyzer.d.ts +58 -0
- package/out/services/dependency-analyzer.js +254 -0
- package/out/services/dependency-analyzer.js.map +1 -0
- package/out/services/dependency-resolver.d.ts +146 -0
- package/out/services/dependency-resolver.js +452 -0
- package/out/services/dependency-resolver.js.map +1 -0
- package/out/services/git-url-resolver.browser.d.ts +10 -0
- package/out/services/git-url-resolver.browser.js +19 -0
- package/out/services/git-url-resolver.browser.js.map +1 -0
- package/out/services/git-url-resolver.d.ts +158 -0
- package/out/services/git-url-resolver.js +416 -0
- package/out/services/git-url-resolver.js.map +1 -0
- package/out/services/governance-validator.d.ts +44 -0
- package/out/services/governance-validator.js +153 -0
- package/out/services/governance-validator.js.map +1 -0
- package/out/services/import-resolver.d.ts +77 -0
- package/out/services/import-resolver.js +240 -0
- package/out/services/import-resolver.js.map +1 -0
- package/out/services/performance-optimizer.d.ts +60 -0
- package/out/services/performance-optimizer.js +140 -0
- package/out/services/performance-optimizer.js.map +1 -0
- package/out/services/relationship-inference.d.ts +11 -0
- package/out/services/relationship-inference.js +98 -0
- package/out/services/relationship-inference.js.map +1 -0
- package/out/services/semver.d.ts +98 -0
- package/out/services/semver.js +195 -0
- package/out/services/semver.js.map +1 -0
- package/out/services/types.d.ts +340 -0
- package/out/services/types.js +46 -0
- package/out/services/types.js.map +1 -0
- package/out/services/workspace-manager.d.ts +123 -0
- package/out/services/workspace-manager.js +489 -0
- package/out/services/workspace-manager.js.map +1 -0
- package/out/syntaxes/domain-lang.monarch.d.ts +76 -0
- package/out/syntaxes/domain-lang.monarch.js +29 -0
- package/out/syntaxes/domain-lang.monarch.js.map +1 -0
- package/out/utils/import-utils.d.ts +49 -0
- package/out/utils/import-utils.js +128 -0
- package/out/utils/import-utils.js.map +1 -0
- package/out/validation/bounded-context.d.ts +11 -0
- package/out/validation/bounded-context.js +79 -0
- package/out/validation/bounded-context.js.map +1 -0
- package/out/validation/classification.d.ts +3 -0
- package/out/validation/classification.js +3 -0
- package/out/validation/classification.js.map +1 -0
- package/out/validation/constants.d.ts +180 -0
- package/out/validation/constants.js +235 -0
- package/out/validation/constants.js.map +1 -0
- package/out/validation/domain-lang-validator.d.ts +2 -0
- package/out/validation/domain-lang-validator.js +27 -0
- package/out/validation/domain-lang-validator.js.map +1 -0
- package/out/validation/domain.d.ts +11 -0
- package/out/validation/domain.js +63 -0
- package/out/validation/domain.js.map +1 -0
- package/out/validation/import.d.ts +68 -0
- package/out/validation/import.js +237 -0
- package/out/validation/import.js.map +1 -0
- package/out/validation/manifest.d.ts +144 -0
- package/out/validation/manifest.js +327 -0
- package/out/validation/manifest.js.map +1 -0
- package/out/validation/maps.d.ts +21 -0
- package/out/validation/maps.js +60 -0
- package/out/validation/maps.js.map +1 -0
- package/out/validation/metadata.d.ts +7 -0
- package/out/validation/metadata.js +16 -0
- package/out/validation/metadata.js.map +1 -0
- package/out/validation/model.d.ts +12 -0
- package/out/validation/model.js +29 -0
- package/out/validation/model.js.map +1 -0
- package/out/validation/relationships.d.ts +12 -0
- package/out/validation/relationships.js +94 -0
- package/out/validation/relationships.js.map +1 -0
- package/out/validation/shared.d.ts +6 -0
- package/out/validation/shared.js +12 -0
- package/out/validation/shared.js.map +1 -0
- package/package.json +110 -0
- package/src/ast-augmentation.ts +5 -0
- package/src/domain-lang-module.ts +112 -0
- package/src/domain-lang.langium +351 -0
- package/src/generated/ast.ts +986 -0
- package/src/generated/grammar.ts +2409 -0
- package/src/generated/module.ts +25 -0
- package/src/index.ts +24 -0
- package/src/lsp/domain-lang-code-actions.ts +189 -0
- package/src/lsp/domain-lang-completion.ts +514 -0
- package/src/lsp/domain-lang-formatter.ts +51 -0
- package/src/lsp/domain-lang-naming.ts +56 -0
- package/src/lsp/domain-lang-scope.ts +137 -0
- package/src/lsp/domain-lang-workspace-manager.ts +104 -0
- package/src/lsp/hover/ddd-pattern-explanations.ts +237 -0
- package/src/lsp/hover/domain-lang-hover.ts +338 -0
- package/src/lsp/hover/domain-lang-keywords.ts +50 -0
- package/src/lsp/manifest-diagnostics.ts +290 -0
- package/src/main-browser.ts +15 -0
- package/src/main.ts +85 -0
- package/src/sdk/README.md +297 -0
- package/src/sdk/ast-augmentation.ts +157 -0
- package/src/sdk/index.ts +126 -0
- package/src/sdk/indexes.ts +155 -0
- package/src/sdk/loader-node.ts +146 -0
- package/src/sdk/loader.ts +99 -0
- package/src/sdk/patterns.ts +147 -0
- package/src/sdk/query.ts +802 -0
- package/src/sdk/resolution.ts +78 -0
- package/src/sdk/types.ts +323 -0
- package/src/services/dependency-analyzer.ts +321 -0
- package/src/services/dependency-resolver.ts +551 -0
- package/src/services/git-url-resolver.browser.ts +26 -0
- package/src/services/git-url-resolver.ts +517 -0
- package/src/services/governance-validator.ts +177 -0
- package/src/services/import-resolver.ts +292 -0
- package/src/services/performance-optimizer.ts +170 -0
- package/src/services/relationship-inference.ts +121 -0
- package/src/services/semver.ts +213 -0
- package/src/services/types.ts +415 -0
- package/src/services/workspace-manager.ts +607 -0
- package/src/syntaxes/domain-lang.monarch.ts +29 -0
- package/src/utils/import-utils.ts +152 -0
- package/src/validation/bounded-context.ts +99 -0
- package/src/validation/classification.ts +5 -0
- package/src/validation/constants.ts +304 -0
- package/src/validation/domain-lang-validator.ts +33 -0
- package/src/validation/domain.ts +77 -0
- package/src/validation/import.ts +295 -0
- package/src/validation/manifest.ts +439 -0
- package/src/validation/maps.ts +76 -0
- package/src/validation/metadata.ts +18 -0
- package/src/validation/model.ts +37 -0
- package/src/validation/relationships.ts +154 -0
- package/src/validation/shared.ts +14 -0
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dependency Resolution Service
|
|
3
|
+
*
|
|
4
|
+
* Discovers and resolves transitive dependencies for DomainLang packages.
|
|
5
|
+
* Generates lock files for reproducible builds.
|
|
6
|
+
*
|
|
7
|
+
* Algorithm:
|
|
8
|
+
* 1. Parse root model.yaml
|
|
9
|
+
* 2. Download all direct dependencies
|
|
10
|
+
* 3. Parse each dependency's model.yaml
|
|
11
|
+
* 4. Recursively discover transitive dependencies
|
|
12
|
+
* 5. Resolve version constraints using "Latest Wins" strategy
|
|
13
|
+
* 6. Generate lock file with pinned commit hashes
|
|
14
|
+
*
|
|
15
|
+
* Resolution Strategy ("Latest Wins"):
|
|
16
|
+
* - SemVer tags (same major): Pick highest compatible version
|
|
17
|
+
* - Same branch: No conflict, resolve once
|
|
18
|
+
* - Commit pins: Error (explicit pins are intentional)
|
|
19
|
+
* - Major version mismatch: Error
|
|
20
|
+
* - Tag vs Branch: Error (incompatible intent)
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import path from 'node:path';
|
|
24
|
+
import fs from 'node:fs/promises';
|
|
25
|
+
import YAML from 'yaml';
|
|
26
|
+
import { GitUrlParser, GitUrlResolver } from './git-url-resolver.js';
|
|
27
|
+
import { parseSemVer, pickLatestSemVer, detectRefType } from './semver.js';
|
|
28
|
+
import type { SemVer, ResolvingPackage, LockFile, LockedDependency, DependencyGraph } from './types.js';
|
|
29
|
+
|
|
30
|
+
export class DependencyResolver {
|
|
31
|
+
private gitResolver: GitUrlResolver;
|
|
32
|
+
private workspaceRoot: string;
|
|
33
|
+
|
|
34
|
+
constructor(workspaceRoot: string, gitResolver?: GitUrlResolver) {
|
|
35
|
+
this.workspaceRoot = workspaceRoot;
|
|
36
|
+
// Per PRS-010: Project-local cache at .dlang/packages/
|
|
37
|
+
const cacheDir = path.join(workspaceRoot, '.dlang', 'packages');
|
|
38
|
+
this.gitResolver = gitResolver || new GitUrlResolver(cacheDir);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Resolves all dependencies for a workspace.
|
|
43
|
+
*
|
|
44
|
+
* Process:
|
|
45
|
+
* 1. Load root model.yaml
|
|
46
|
+
* 2. Build dependency graph (discover transitive deps)
|
|
47
|
+
* 3. Resolve version constraints
|
|
48
|
+
* 4. Generate lock file
|
|
49
|
+
* 5. Download all dependencies to cache
|
|
50
|
+
*
|
|
51
|
+
* @returns Generated lock file
|
|
52
|
+
*/
|
|
53
|
+
async resolveDependencies(): Promise<LockFile> {
|
|
54
|
+
// Load root package config
|
|
55
|
+
const rootConfig = await this.loadPackageConfig(this.workspaceRoot);
|
|
56
|
+
|
|
57
|
+
if (!rootConfig.dependencies || Object.keys(rootConfig.dependencies).length === 0) {
|
|
58
|
+
// No dependencies
|
|
59
|
+
return { version: '1', dependencies: {} };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Build dependency graph
|
|
63
|
+
const graph = await this.buildDependencyGraph(rootConfig);
|
|
64
|
+
|
|
65
|
+
// Apply overrides before conflict detection
|
|
66
|
+
this.applyOverrides(graph, rootConfig.overrides);
|
|
67
|
+
|
|
68
|
+
// Detect version conflicts and package-level cycles before resolving
|
|
69
|
+
this.detectVersionConflicts(graph);
|
|
70
|
+
this.detectPackageCycles(graph);
|
|
71
|
+
|
|
72
|
+
// Resolve version constraints
|
|
73
|
+
await this.resolveVersions(graph);
|
|
74
|
+
|
|
75
|
+
// Generate and return lock file (caller writes to disk)
|
|
76
|
+
return this.generateLockFile(graph);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Applies ref overrides from model.yaml to resolve conflicts explicitly.
|
|
81
|
+
*
|
|
82
|
+
* Overrides take precedence over all other constraints.
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* ```yaml
|
|
86
|
+
* overrides:
|
|
87
|
+
* domainlang/core: v2.0.0
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
private applyOverrides(graph: DependencyGraph, overrides?: Record<string, string>): void {
|
|
91
|
+
if (!overrides) return;
|
|
92
|
+
|
|
93
|
+
for (const [pkg, overrideRef] of Object.entries(overrides)) {
|
|
94
|
+
const node = graph.nodes[pkg];
|
|
95
|
+
if (node) {
|
|
96
|
+
// Override replaces all constraints with a single definitive ref
|
|
97
|
+
node.constraints = new Set([overrideRef]);
|
|
98
|
+
node.refConstraint = overrideRef;
|
|
99
|
+
|
|
100
|
+
// Track that this was an override for messaging
|
|
101
|
+
this.overrideMessages.push(`Override applied: ${pkg}@${overrideRef}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Override messages for CLI output */
|
|
107
|
+
private overrideMessages: string[] = [];
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Returns any override messages from the last dependency resolution.
|
|
111
|
+
*/
|
|
112
|
+
getOverrideMessages(): string[] {
|
|
113
|
+
return this.overrideMessages;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Builds the complete dependency graph by recursively discovering transitive dependencies.
|
|
118
|
+
*/
|
|
119
|
+
private async buildDependencyGraph(rootConfig: ResolvingPackage): Promise<DependencyGraph> {
|
|
120
|
+
const graph: DependencyGraph = {
|
|
121
|
+
nodes: {},
|
|
122
|
+
root: rootConfig.name || 'root',
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// Process root dependencies
|
|
126
|
+
const queue: Array<{ packageKey: string; refConstraint: string; parent: string }> = [];
|
|
127
|
+
|
|
128
|
+
for (const [depName, refConstraint] of Object.entries(rootConfig.dependencies || {})) {
|
|
129
|
+
queue.push({
|
|
130
|
+
packageKey: depName,
|
|
131
|
+
refConstraint,
|
|
132
|
+
parent: graph.root
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// BFS to discover all transitive dependencies
|
|
137
|
+
const visited = new Set<string>();
|
|
138
|
+
|
|
139
|
+
while (queue.length > 0) {
|
|
140
|
+
const entry = queue.shift();
|
|
141
|
+
if (!entry) break;
|
|
142
|
+
const { packageKey, refConstraint, parent } = entry;
|
|
143
|
+
|
|
144
|
+
// Skip if already processed
|
|
145
|
+
if (visited.has(packageKey)) {
|
|
146
|
+
// Update dependents list and record constraint
|
|
147
|
+
const existing = graph.nodes[packageKey];
|
|
148
|
+
if (!existing.dependents.includes(parent)) {
|
|
149
|
+
existing.dependents.push(parent);
|
|
150
|
+
}
|
|
151
|
+
if (!existing.constraints) existing.constraints = new Set<string>();
|
|
152
|
+
existing.constraints.add(refConstraint);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
visited.add(packageKey);
|
|
156
|
+
|
|
157
|
+
// Parse package identifier
|
|
158
|
+
const gitInfo = GitUrlParser.parse(packageKey);
|
|
159
|
+
|
|
160
|
+
// Download package to get its model.yaml
|
|
161
|
+
const packageUri = await this.gitResolver.resolve(packageKey);
|
|
162
|
+
const packageDir = path.dirname(packageUri.fsPath);
|
|
163
|
+
|
|
164
|
+
// Load package config
|
|
165
|
+
const packageConfig = await this.loadPackageConfig(packageDir);
|
|
166
|
+
|
|
167
|
+
// Add to graph
|
|
168
|
+
graph.nodes[packageKey] = {
|
|
169
|
+
packageKey,
|
|
170
|
+
refConstraint,
|
|
171
|
+
constraints: new Set<string>([refConstraint]),
|
|
172
|
+
repoUrl: gitInfo.repoUrl,
|
|
173
|
+
dependencies: packageConfig.dependencies || {},
|
|
174
|
+
dependents: [parent],
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
// Queue transitive dependencies
|
|
178
|
+
for (const [transDepName, transRefConstraint] of Object.entries(packageConfig.dependencies || {})) {
|
|
179
|
+
queue.push({
|
|
180
|
+
packageKey: transDepName,
|
|
181
|
+
refConstraint: transRefConstraint,
|
|
182
|
+
parent: packageKey,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return graph;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Resolves ref constraints to specific commits.
|
|
192
|
+
*
|
|
193
|
+
* Simple algorithm: Use the ref specified in the constraint.
|
|
194
|
+
* Detects refType (tag, branch, or commit) based on format.
|
|
195
|
+
*/
|
|
196
|
+
private async resolveVersions(graph: DependencyGraph): Promise<void> {
|
|
197
|
+
for (const [packageKey, node] of Object.entries(graph.nodes)) {
|
|
198
|
+
// Parse package to get repo info
|
|
199
|
+
const gitInfo = GitUrlParser.parse(packageKey);
|
|
200
|
+
|
|
201
|
+
// Extract ref from constraint
|
|
202
|
+
const ref = this.extractRefFromConstraint(node.refConstraint);
|
|
203
|
+
|
|
204
|
+
// Detect ref type based on format
|
|
205
|
+
const refType = detectRefType(ref);
|
|
206
|
+
|
|
207
|
+
// Resolve ref to commit hash
|
|
208
|
+
const commitHash = await this.resolveCommitHash(gitInfo.repoUrl, ref);
|
|
209
|
+
|
|
210
|
+
node.resolvedRef = ref;
|
|
211
|
+
node.refType = refType;
|
|
212
|
+
node.commitHash = commitHash;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Extracts a ref from a constraint string.
|
|
218
|
+
*
|
|
219
|
+
* Examples:
|
|
220
|
+
* - "^1.0.0" → "1.0.0" (treated as tag)
|
|
221
|
+
* - "~2.3.0" → "2.3.0" (treated as tag)
|
|
222
|
+
* - "1.5.0" → "1.5.0" (treated as tag)
|
|
223
|
+
* - "main" → "main" (treated as branch)
|
|
224
|
+
* - "abc123def" → "abc123def" (treated as commit)
|
|
225
|
+
* - "owner/repo@1.0.0" → "1.0.0"
|
|
226
|
+
*/
|
|
227
|
+
private extractRefFromConstraint(constraint: string): string {
|
|
228
|
+
// Remove semver operators (legacy support)
|
|
229
|
+
let ref = constraint.replace(/^[\^~>=<]/, '');
|
|
230
|
+
|
|
231
|
+
// Extract ref from full import URL if present
|
|
232
|
+
if (ref.includes('@')) {
|
|
233
|
+
ref = ref.split('@')[1];
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return ref || 'main';
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Resolves a version (tag/branch) to a commit hash using git ls-remote.
|
|
241
|
+
*/
|
|
242
|
+
private async resolveCommitHash(repoUrl: string, version: string): Promise<string> {
|
|
243
|
+
// This is a placeholder - the actual implementation is in GitUrlResolver
|
|
244
|
+
// We need to extract it or call the resolver
|
|
245
|
+
const gitInfo = GitUrlParser.parse(`${repoUrl}@${version}`);
|
|
246
|
+
const uri = await this.gitResolver.resolve(gitInfo.original);
|
|
247
|
+
|
|
248
|
+
// Extract commit hash from cache path
|
|
249
|
+
// Per PRS-010: Project-local cache at .dlang/packages/{owner}/{repo}/{commit}/
|
|
250
|
+
const pathParts = uri.fsPath.split(path.sep);
|
|
251
|
+
const commitHashIndex = pathParts.length - 2; // Second to last segment
|
|
252
|
+
return pathParts[commitHashIndex];
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Generates a lock file from the resolved dependency graph.
|
|
257
|
+
*/
|
|
258
|
+
private generateLockFile(graph: DependencyGraph): LockFile {
|
|
259
|
+
const dependencies: Record<string, LockedDependency> = {};
|
|
260
|
+
|
|
261
|
+
for (const [packageKey, node] of Object.entries(graph.nodes)) {
|
|
262
|
+
if (!node.resolvedRef || !node.commitHash) {
|
|
263
|
+
throw new Error(
|
|
264
|
+
`Failed to resolve ref for '${packageKey}'.\n` +
|
|
265
|
+
`Hint: Check that the package exists and the ref is valid.`
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
dependencies[packageKey] = {
|
|
270
|
+
ref: node.resolvedRef,
|
|
271
|
+
refType: node.refType ?? 'commit',
|
|
272
|
+
resolved: node.repoUrl || '',
|
|
273
|
+
commit: node.commitHash,
|
|
274
|
+
// Future: Calculate integrity hash
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
version: '1',
|
|
280
|
+
dependencies,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Loads and parses a package's model.yaml file.
|
|
286
|
+
*/
|
|
287
|
+
private async loadPackageConfig(packageDir: string): Promise<ResolvingPackage> {
|
|
288
|
+
const yamlPath = path.join(packageDir, 'model.yaml');
|
|
289
|
+
|
|
290
|
+
try {
|
|
291
|
+
const yamlContent = await fs.readFile(yamlPath, 'utf-8');
|
|
292
|
+
return this.parseYaml(yamlContent);
|
|
293
|
+
} catch {
|
|
294
|
+
// No model.yaml found
|
|
295
|
+
return {};
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Parses model.yaml content.
|
|
301
|
+
*
|
|
302
|
+
* Expected structure:
|
|
303
|
+
* model:
|
|
304
|
+
* name: package-name
|
|
305
|
+
* version: 1.0.0
|
|
306
|
+
* entry: index.dlang
|
|
307
|
+
*
|
|
308
|
+
* dependencies:
|
|
309
|
+
* package-name:
|
|
310
|
+
* source: owner/repo
|
|
311
|
+
* ref: v1.0.0
|
|
312
|
+
*/
|
|
313
|
+
private parseYaml(content: string): ResolvingPackage {
|
|
314
|
+
const parsed = YAML.parse(content) as {
|
|
315
|
+
model?: {
|
|
316
|
+
name?: string;
|
|
317
|
+
version?: string;
|
|
318
|
+
entry?: string;
|
|
319
|
+
};
|
|
320
|
+
dependencies?: Record<string, { source?: string; ref?: string }>;
|
|
321
|
+
overrides?: Record<string, string>;
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
const config: ResolvingPackage = {};
|
|
325
|
+
|
|
326
|
+
if (parsed.model) {
|
|
327
|
+
config.name = parsed.model.name;
|
|
328
|
+
config.version = parsed.model.version;
|
|
329
|
+
config.entry = parsed.model.entry;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (parsed.dependencies) {
|
|
333
|
+
config.dependencies = {};
|
|
334
|
+
for (const [, depInfo] of Object.entries(parsed.dependencies)) {
|
|
335
|
+
if (depInfo.source) {
|
|
336
|
+
const refConstraint = depInfo.ref || 'main';
|
|
337
|
+
// Store as "source@ref" for consistency with import resolution
|
|
338
|
+
config.dependencies[depInfo.source] = refConstraint;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Parse overrides section for explicit ref control
|
|
344
|
+
if (parsed.overrides) {
|
|
345
|
+
config.overrides = parsed.overrides;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return config;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Detects ref conflicts and applies "Latest Wins" resolution strategy.
|
|
353
|
+
*
|
|
354
|
+
* Resolution Rules:
|
|
355
|
+
* - SemVer tags (same major): Pick highest version automatically
|
|
356
|
+
* - Same branch refs: No conflict, use single resolution
|
|
357
|
+
* - Commit SHA conflicts: Error (explicit pins are intentional)
|
|
358
|
+
* - Major version mismatch: Error (breaking change)
|
|
359
|
+
* - Tag vs Branch: Error (incompatible intent)
|
|
360
|
+
*
|
|
361
|
+
* Modifies graph nodes in-place to set the resolved constraint.
|
|
362
|
+
* Throws an error only for unresolvable conflicts.
|
|
363
|
+
*/
|
|
364
|
+
private detectVersionConflicts(graph: DependencyGraph): void {
|
|
365
|
+
const resolutionMessages: string[] = [];
|
|
366
|
+
|
|
367
|
+
for (const [pkg, node] of Object.entries(graph.nodes)) {
|
|
368
|
+
const constraints = node.constraints ?? new Set<string>([node.refConstraint]);
|
|
369
|
+
|
|
370
|
+
if (constraints.size <= 1) continue; // No conflict
|
|
371
|
+
|
|
372
|
+
const refs = Array.from(constraints);
|
|
373
|
+
const refTypes = refs.map(ref => ({
|
|
374
|
+
ref,
|
|
375
|
+
type: detectRefType(ref),
|
|
376
|
+
semver: parseSemVer(ref),
|
|
377
|
+
}));
|
|
378
|
+
|
|
379
|
+
// Check for mixed types (tag vs branch vs commit)
|
|
380
|
+
const types = new Set(refTypes.map(r => r.type));
|
|
381
|
+
|
|
382
|
+
// Case 1: All commits - must be exact match
|
|
383
|
+
if (types.size === 1 && types.has('commit')) {
|
|
384
|
+
this.throwConflictError(pkg, refs, node.dependents,
|
|
385
|
+
'Explicit commit pins cannot be automatically resolved.\n' +
|
|
386
|
+
'Add an override in model.yaml:\n\n' +
|
|
387
|
+
' overrides:\n' +
|
|
388
|
+
` ${pkg}: ${refs[0]}`
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Case 2: Mixed types (tag vs branch or tag vs commit)
|
|
393
|
+
if (types.size > 1) {
|
|
394
|
+
this.throwConflictError(pkg, refs, node.dependents,
|
|
395
|
+
'Cannot mix ref types (tags, branches, commits).\n' +
|
|
396
|
+
'Add an override in model.yaml to specify which to use:\n\n' +
|
|
397
|
+
' overrides:\n' +
|
|
398
|
+
` ${pkg}: <ref>`
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Case 3: All branches - must be same branch
|
|
403
|
+
if (types.size === 1 && types.has('branch')) {
|
|
404
|
+
const uniqueBranches = new Set(refs);
|
|
405
|
+
if (uniqueBranches.size > 1) {
|
|
406
|
+
this.throwConflictError(pkg, refs, node.dependents,
|
|
407
|
+
'Different branch refs cannot be automatically resolved.\n' +
|
|
408
|
+
'Add an override in model.yaml:\n\n' +
|
|
409
|
+
' overrides:\n' +
|
|
410
|
+
` ${pkg}: ${refs[0]}`
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
// Same branch - no conflict, continue
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Case 4: All SemVer tags - apply "Latest Wins"
|
|
418
|
+
const semvers = refTypes
|
|
419
|
+
.filter((r): r is typeof r & { semver: SemVer } => r.semver !== undefined)
|
|
420
|
+
.map(r => r.semver);
|
|
421
|
+
|
|
422
|
+
if (semvers.length !== refs.length) {
|
|
423
|
+
// Some refs don't parse as SemVer - can't auto-resolve
|
|
424
|
+
this.throwConflictError(pkg, refs, node.dependents,
|
|
425
|
+
'Not all refs are valid SemVer tags.\n' +
|
|
426
|
+
'Add an override in model.yaml:\n\n' +
|
|
427
|
+
' overrides:\n' +
|
|
428
|
+
` ${pkg}: <ref>`
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Check major version compatibility
|
|
433
|
+
const majors = new Set(semvers.map(s => s.major));
|
|
434
|
+
if (majors.size > 1) {
|
|
435
|
+
const majorList = Array.from(majors).sort().join(' vs ');
|
|
436
|
+
this.throwConflictError(pkg, refs, node.dependents,
|
|
437
|
+
`Major version mismatch (v${majorList}). This may indicate breaking changes.\n` +
|
|
438
|
+
'Add an override in model.yaml if you want to force a version:\n\n' +
|
|
439
|
+
' overrides:\n' +
|
|
440
|
+
` ${pkg}: ${refs[refs.length - 1]}`
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// All same major version - pick latest (Latest Wins!)
|
|
445
|
+
const latest = pickLatestSemVer(refs);
|
|
446
|
+
if (latest && latest !== node.refConstraint) {
|
|
447
|
+
// Update the node to use the resolved ref
|
|
448
|
+
node.refConstraint = latest;
|
|
449
|
+
|
|
450
|
+
// Log the resolution for user feedback
|
|
451
|
+
const otherRefs = refs.filter(r => r !== latest).join(', ');
|
|
452
|
+
resolutionMessages.push(
|
|
453
|
+
`Resolved ${pkg}: using ${latest} (satisfies ${otherRefs})`
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Store resolution messages for later output
|
|
459
|
+
if (resolutionMessages.length > 0) {
|
|
460
|
+
this.resolutionMessages = resolutionMessages;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Throws a formatted conflict error with actionable hints.
|
|
466
|
+
*/
|
|
467
|
+
private throwConflictError(
|
|
468
|
+
pkg: string,
|
|
469
|
+
refs: string[],
|
|
470
|
+
dependents: string[],
|
|
471
|
+
hint: string
|
|
472
|
+
): never {
|
|
473
|
+
const depLines = dependents.map((d, i) =>
|
|
474
|
+
` └─ ${d} requires ${pkg}@${refs[i] || refs[0]}`
|
|
475
|
+
).join('\n');
|
|
476
|
+
|
|
477
|
+
throw new Error(
|
|
478
|
+
`Dependency ref conflict for '${pkg}'\n` +
|
|
479
|
+
depLines + '\n\n' +
|
|
480
|
+
hint
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/** Resolution messages from "Latest Wins" auto-resolution */
|
|
485
|
+
private resolutionMessages: string[] = [];
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Returns any resolution messages from the last dependency resolution.
|
|
489
|
+
* Useful for CLI output to inform users about auto-resolved conflicts.
|
|
490
|
+
*/
|
|
491
|
+
getResolutionMessages(): string[] {
|
|
492
|
+
return this.resolutionMessages;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Detects package-level cycles in the dependency graph and throws a clear error.
|
|
497
|
+
*/
|
|
498
|
+
private detectPackageCycles(graph: DependencyGraph): void {
|
|
499
|
+
const GRAY = 1, BLACK = 2;
|
|
500
|
+
const color: Record<string, number> = {};
|
|
501
|
+
const stack: string[] = [];
|
|
502
|
+
|
|
503
|
+
const visit = (pkg: string): void => {
|
|
504
|
+
color[pkg] = GRAY;
|
|
505
|
+
stack.push(pkg);
|
|
506
|
+
const deps = Object.keys(graph.nodes[pkg]?.dependencies ?? {});
|
|
507
|
+
for (const dep of deps) {
|
|
508
|
+
if (!graph.nodes[dep]) continue; // Unknown dep will resolve later
|
|
509
|
+
if (color[dep] === GRAY) {
|
|
510
|
+
// Found a back edge: cycle
|
|
511
|
+
const cycleStart = stack.indexOf(dep);
|
|
512
|
+
const cyclePath = [...stack.slice(cycleStart), dep].join(' → ');
|
|
513
|
+
throw new Error(
|
|
514
|
+
`Cyclic package dependency detected:\n` +
|
|
515
|
+
` ${cyclePath}\n\n` +
|
|
516
|
+
`Hint: Extract shared types into a separate package that both can depend on.`
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
if (color[dep] !== BLACK) visit(dep);
|
|
520
|
+
}
|
|
521
|
+
stack.pop();
|
|
522
|
+
color[pkg] = BLACK;
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
for (const pkg of Object.keys(graph.nodes)) {
|
|
526
|
+
if (!color[pkg]) visit(pkg);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Loads an existing lock file from disk.
|
|
532
|
+
*/
|
|
533
|
+
static async loadLockFile(workspaceRoot: string): Promise<LockFile | undefined> {
|
|
534
|
+
const lockPath = path.join(workspaceRoot, 'model.lock');
|
|
535
|
+
|
|
536
|
+
try {
|
|
537
|
+
const content = await fs.readFile(lockPath, 'utf-8');
|
|
538
|
+
return JSON.parse(content) as LockFile;
|
|
539
|
+
} catch {
|
|
540
|
+
// No lock file
|
|
541
|
+
return undefined;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Parses a lock file from JSON format.
|
|
547
|
+
*/
|
|
548
|
+
static parseLockFile(content: string): LockFile {
|
|
549
|
+
return JSON.parse(content) as LockFile;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Browser stub for GitUrlResolver
|
|
2
|
+
// Git operations are not available in the browser environment
|
|
3
|
+
|
|
4
|
+
import type { GitImportInfo } from './types.js';
|
|
5
|
+
|
|
6
|
+
// Re-export the type for API consistency
|
|
7
|
+
export type { GitImportInfo } from './types.js';
|
|
8
|
+
|
|
9
|
+
export class GitUrlResolver {
|
|
10
|
+
constructor() {
|
|
11
|
+
throw new Error('GitUrlResolver is not available in the browser.');
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const GitUrlParser = {
|
|
16
|
+
parse(_importStr: string): GitImportInfo {
|
|
17
|
+
throw new Error('GitUrlParser is not available in the browser.');
|
|
18
|
+
},
|
|
19
|
+
isGitUrl(_importStr: string): boolean {
|
|
20
|
+
throw new Error('GitUrlParser is not available in the browser.');
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function loadLockFile(): void {
|
|
25
|
+
throw new Error('loadLockFile is not available in the browser.');
|
|
26
|
+
}
|