@domainlang/language 0.1.81

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 (188) hide show
  1. package/README.md +32 -0
  2. package/out/ast-augmentation.d.ts +6 -0
  3. package/out/ast-augmentation.js +2 -0
  4. package/out/ast-augmentation.js.map +1 -0
  5. package/out/domain-lang-module.d.ts +55 -0
  6. package/out/domain-lang-module.js +59 -0
  7. package/out/domain-lang-module.js.map +1 -0
  8. package/out/generated/ast.d.ts +770 -0
  9. package/out/generated/ast.js +565 -0
  10. package/out/generated/ast.js.map +1 -0
  11. package/out/generated/grammar.d.ts +6 -0
  12. package/out/generated/grammar.js +2502 -0
  13. package/out/generated/grammar.js.map +1 -0
  14. package/out/generated/module.d.ts +13 -0
  15. package/out/generated/module.js +21 -0
  16. package/out/generated/module.js.map +1 -0
  17. package/out/index.d.ts +13 -0
  18. package/out/index.js +17 -0
  19. package/out/index.js.map +1 -0
  20. package/out/lsp/domain-lang-completion.d.ts +37 -0
  21. package/out/lsp/domain-lang-completion.js +452 -0
  22. package/out/lsp/domain-lang-completion.js.map +1 -0
  23. package/out/lsp/domain-lang-formatter.d.ts +15 -0
  24. package/out/lsp/domain-lang-formatter.js +43 -0
  25. package/out/lsp/domain-lang-formatter.js.map +1 -0
  26. package/out/lsp/domain-lang-naming.d.ts +34 -0
  27. package/out/lsp/domain-lang-naming.js +49 -0
  28. package/out/lsp/domain-lang-naming.js.map +1 -0
  29. package/out/lsp/domain-lang-scope.d.ts +59 -0
  30. package/out/lsp/domain-lang-scope.js +102 -0
  31. package/out/lsp/domain-lang-scope.js.map +1 -0
  32. package/out/lsp/hover/ddd-pattern-explanations.d.ts +50 -0
  33. package/out/lsp/hover/ddd-pattern-explanations.js +196 -0
  34. package/out/lsp/hover/ddd-pattern-explanations.js.map +1 -0
  35. package/out/lsp/hover/domain-lang-hover.d.ts +19 -0
  36. package/out/lsp/hover/domain-lang-hover.js +306 -0
  37. package/out/lsp/hover/domain-lang-hover.js.map +1 -0
  38. package/out/lsp/hover/domain-lang-keywords.d.ts +13 -0
  39. package/out/lsp/hover/domain-lang-keywords.js +47 -0
  40. package/out/lsp/hover/domain-lang-keywords.js.map +1 -0
  41. package/out/main-browser.d.ts +1 -0
  42. package/out/main-browser.js +11 -0
  43. package/out/main-browser.js.map +1 -0
  44. package/out/main.d.ts +1 -0
  45. package/out/main.js +74 -0
  46. package/out/main.js.map +1 -0
  47. package/out/sdk/ast-augmentation.d.ts +136 -0
  48. package/out/sdk/ast-augmentation.js +62 -0
  49. package/out/sdk/ast-augmentation.js.map +1 -0
  50. package/out/sdk/index.d.ts +94 -0
  51. package/out/sdk/index.js +97 -0
  52. package/out/sdk/index.js.map +1 -0
  53. package/out/sdk/indexes.d.ts +16 -0
  54. package/out/sdk/indexes.js +97 -0
  55. package/out/sdk/indexes.js.map +1 -0
  56. package/out/sdk/loader-node.d.ts +47 -0
  57. package/out/sdk/loader-node.js +104 -0
  58. package/out/sdk/loader-node.js.map +1 -0
  59. package/out/sdk/loader.d.ts +49 -0
  60. package/out/sdk/loader.js +85 -0
  61. package/out/sdk/loader.js.map +1 -0
  62. package/out/sdk/patterns.d.ts +93 -0
  63. package/out/sdk/patterns.js +123 -0
  64. package/out/sdk/patterns.js.map +1 -0
  65. package/out/sdk/query.d.ts +90 -0
  66. package/out/sdk/query.js +679 -0
  67. package/out/sdk/query.js.map +1 -0
  68. package/out/sdk/resolution.d.ts +52 -0
  69. package/out/sdk/resolution.js +68 -0
  70. package/out/sdk/resolution.js.map +1 -0
  71. package/out/sdk/types.d.ts +301 -0
  72. package/out/sdk/types.js +8 -0
  73. package/out/sdk/types.js.map +1 -0
  74. package/out/services/dependency-analyzer.d.ts +94 -0
  75. package/out/services/dependency-analyzer.js +279 -0
  76. package/out/services/dependency-analyzer.js.map +1 -0
  77. package/out/services/dependency-resolver.d.ts +123 -0
  78. package/out/services/dependency-resolver.js +252 -0
  79. package/out/services/dependency-resolver.js.map +1 -0
  80. package/out/services/git-url-resolver.browser.d.ts +18 -0
  81. package/out/services/git-url-resolver.browser.js +15 -0
  82. package/out/services/git-url-resolver.browser.js.map +1 -0
  83. package/out/services/git-url-resolver.d.ts +192 -0
  84. package/out/services/git-url-resolver.js +382 -0
  85. package/out/services/git-url-resolver.js.map +1 -0
  86. package/out/services/governance-validator.d.ts +80 -0
  87. package/out/services/governance-validator.js +159 -0
  88. package/out/services/governance-validator.js.map +1 -0
  89. package/out/services/import-resolver.d.ts +18 -0
  90. package/out/services/import-resolver.js +22 -0
  91. package/out/services/import-resolver.js.map +1 -0
  92. package/out/services/performance-optimizer.d.ts +60 -0
  93. package/out/services/performance-optimizer.js +140 -0
  94. package/out/services/performance-optimizer.js.map +1 -0
  95. package/out/services/relationship-inference.d.ts +11 -0
  96. package/out/services/relationship-inference.js +98 -0
  97. package/out/services/relationship-inference.js.map +1 -0
  98. package/out/services/workspace-manager.d.ts +76 -0
  99. package/out/services/workspace-manager.js +323 -0
  100. package/out/services/workspace-manager.js.map +1 -0
  101. package/out/syntaxes/domain-lang.monarch.d.ts +76 -0
  102. package/out/syntaxes/domain-lang.monarch.js +29 -0
  103. package/out/syntaxes/domain-lang.monarch.js.map +1 -0
  104. package/out/utils/import-utils.d.ts +57 -0
  105. package/out/utils/import-utils.js +228 -0
  106. package/out/utils/import-utils.js.map +1 -0
  107. package/out/validation/bounded-context.d.ts +11 -0
  108. package/out/validation/bounded-context.js +79 -0
  109. package/out/validation/bounded-context.js.map +1 -0
  110. package/out/validation/classification.d.ts +3 -0
  111. package/out/validation/classification.js +3 -0
  112. package/out/validation/classification.js.map +1 -0
  113. package/out/validation/constants.d.ts +77 -0
  114. package/out/validation/constants.js +96 -0
  115. package/out/validation/constants.js.map +1 -0
  116. package/out/validation/domain-lang-validator.d.ts +2 -0
  117. package/out/validation/domain-lang-validator.js +27 -0
  118. package/out/validation/domain-lang-validator.js.map +1 -0
  119. package/out/validation/domain.d.ts +11 -0
  120. package/out/validation/domain.js +18 -0
  121. package/out/validation/domain.js.map +1 -0
  122. package/out/validation/import.d.ts +44 -0
  123. package/out/validation/import.js +135 -0
  124. package/out/validation/import.js.map +1 -0
  125. package/out/validation/maps.d.ts +21 -0
  126. package/out/validation/maps.js +56 -0
  127. package/out/validation/maps.js.map +1 -0
  128. package/out/validation/metadata.d.ts +7 -0
  129. package/out/validation/metadata.js +12 -0
  130. package/out/validation/metadata.js.map +1 -0
  131. package/out/validation/model.d.ts +12 -0
  132. package/out/validation/model.js +29 -0
  133. package/out/validation/model.js.map +1 -0
  134. package/out/validation/relationships.d.ts +12 -0
  135. package/out/validation/relationships.js +94 -0
  136. package/out/validation/relationships.js.map +1 -0
  137. package/out/validation/shared.d.ts +6 -0
  138. package/out/validation/shared.js +12 -0
  139. package/out/validation/shared.js.map +1 -0
  140. package/package.json +97 -0
  141. package/src/ast-augmentation.ts +5 -0
  142. package/src/domain-lang-module.ts +100 -0
  143. package/src/domain-lang.langium +356 -0
  144. package/src/generated/ast.ts +999 -0
  145. package/src/generated/grammar.ts +2504 -0
  146. package/src/generated/module.ts +25 -0
  147. package/src/index.ts +17 -0
  148. package/src/lsp/domain-lang-completion.ts +514 -0
  149. package/src/lsp/domain-lang-formatter.ts +51 -0
  150. package/src/lsp/domain-lang-naming.ts +56 -0
  151. package/src/lsp/domain-lang-scope.ts +137 -0
  152. package/src/lsp/hover/ddd-pattern-explanations.ts +237 -0
  153. package/src/lsp/hover/domain-lang-hover.ts +340 -0
  154. package/src/lsp/hover/domain-lang-keywords.ts +50 -0
  155. package/src/main-browser.ts +15 -0
  156. package/src/main.ts +85 -0
  157. package/src/sdk/README.md +297 -0
  158. package/src/sdk/ast-augmentation.ts +157 -0
  159. package/src/sdk/index.ts +128 -0
  160. package/src/sdk/indexes.ts +155 -0
  161. package/src/sdk/loader-node.ts +126 -0
  162. package/src/sdk/loader.ts +99 -0
  163. package/src/sdk/patterns.ts +147 -0
  164. package/src/sdk/query.ts +802 -0
  165. package/src/sdk/resolution.ts +78 -0
  166. package/src/sdk/types.ts +346 -0
  167. package/src/services/dependency-analyzer.ts +381 -0
  168. package/src/services/dependency-resolver.ts +334 -0
  169. package/src/services/git-url-resolver.browser.ts +31 -0
  170. package/src/services/git-url-resolver.ts +524 -0
  171. package/src/services/governance-validator.ts +219 -0
  172. package/src/services/import-resolver.ts +30 -0
  173. package/src/services/performance-optimizer.ts +170 -0
  174. package/src/services/relationship-inference.ts +121 -0
  175. package/src/services/workspace-manager.ts +416 -0
  176. package/src/syntaxes/domain-lang.monarch.ts +29 -0
  177. package/src/utils/import-utils.ts +274 -0
  178. package/src/validation/bounded-context.ts +99 -0
  179. package/src/validation/classification.ts +5 -0
  180. package/src/validation/constants.ts +124 -0
  181. package/src/validation/domain-lang-validator.ts +33 -0
  182. package/src/validation/domain.ts +24 -0
  183. package/src/validation/import.ts +171 -0
  184. package/src/validation/maps.ts +72 -0
  185. package/src/validation/metadata.ts +14 -0
  186. package/src/validation/model.ts +37 -0
  187. package/src/validation/relationships.ts +154 -0
  188. package/src/validation/shared.ts +14 -0
@@ -0,0 +1,381 @@
1
+ /**
2
+ * Dependency Analysis Service
3
+ *
4
+ * Provides tools for visualizing and analyzing dependency relationships:
5
+ * - Dependency tree visualization
6
+ * - Impact analysis (reverse dependencies)
7
+ * - Circular dependency detection
8
+ * - Version policy enforcement
9
+ */
10
+
11
+ import type { LockFile } from './git-url-resolver.js';
12
+ import path from 'node:path';
13
+ import fs from 'node:fs/promises';
14
+ import os from 'node:os';
15
+ import YAML from 'yaml';
16
+
17
+ export interface DependencyTreeNode {
18
+ /** Package identifier */
19
+ packageKey: string;
20
+ /** Locked version */
21
+ version: string;
22
+ /** Commit hash */
23
+ commit: string;
24
+ /** Direct dependencies */
25
+ dependencies: DependencyTreeNode[];
26
+ /** Depth in tree (0 = root) */
27
+ depth: number;
28
+ }
29
+
30
+ export interface ReverseDependency {
31
+ /** Package that depends on the target */
32
+ dependentPackage: string;
33
+ /** Version of the dependent package */
34
+ version: string;
35
+ /** Type: direct or transitive */
36
+ type: 'direct' | 'transitive';
37
+ }
38
+
39
+ export interface VersionPolicy {
40
+ /** Policy name: latest, stable, or specific version */
41
+ policy: 'latest' | 'stable' | 'pinned';
42
+ /** Resolved version */
43
+ version: string;
44
+ /** Available versions for policy */
45
+ availableVersions?: string[];
46
+ }
47
+
48
+ /**
49
+ * Analyzes dependency relationships and provides visualization/analysis tools.
50
+ */
51
+ export class DependencyAnalyzer {
52
+ /**
53
+ * Builds a dependency tree from a lock file.
54
+ *
55
+ * @param lockFile - The lock file to analyze
56
+ * @param workspaceRoot - Workspace root to load dependency metadata
57
+ * @returns Root nodes of the dependency tree
58
+ */
59
+ async buildDependencyTree(lockFile: LockFile, workspaceRoot: string): Promise<DependencyTreeNode[]> {
60
+ const manifestPath = path.join(workspaceRoot, 'model.yaml');
61
+
62
+ // Load root manifest to get direct dependencies
63
+ const rootDeps: Record<string, string> = {};
64
+ try {
65
+ const content = await fs.readFile(manifestPath, 'utf-8');
66
+ const manifest = YAML.parse(content) as {
67
+ dependencies?: Record<string, { source: string; version: string }>;
68
+ };
69
+
70
+ if (manifest.dependencies) {
71
+ for (const [, dep] of Object.entries(manifest.dependencies)) {
72
+ rootDeps[dep.source] = dep.version;
73
+ }
74
+ }
75
+ } catch {
76
+ // No manifest or parse error
77
+ }
78
+
79
+ // Build tree for each direct dependency
80
+ const rootNodes: DependencyTreeNode[] = [];
81
+ const visited = new Set<string>();
82
+
83
+ for (const packageKey of Object.keys(rootDeps)) {
84
+ const node = await this.buildTreeNode(packageKey, lockFile, workspaceRoot, 0, visited);
85
+ if (node) {
86
+ rootNodes.push(node);
87
+ }
88
+ }
89
+
90
+ return rootNodes;
91
+ }
92
+
93
+ /**
94
+ * Recursively builds a dependency tree node.
95
+ */
96
+ private async buildTreeNode(
97
+ packageKey: string,
98
+ lockFile: LockFile,
99
+ workspaceRoot: string,
100
+ depth: number,
101
+ visited: Set<string>
102
+ ): Promise<DependencyTreeNode | null> {
103
+ const locked = lockFile.dependencies[packageKey];
104
+ if (!locked) {
105
+ return null;
106
+ }
107
+
108
+ // Prevent infinite recursion
109
+ if (visited.has(packageKey)) {
110
+ return {
111
+ packageKey,
112
+ version: locked.version,
113
+ commit: locked.commit,
114
+ dependencies: [], // Don't recurse into already visited
115
+ depth,
116
+ };
117
+ }
118
+
119
+ visited.add(packageKey);
120
+
121
+ // Load package dependencies from cache
122
+ const cacheDir = this.getCacheDir(packageKey, locked.commit);
123
+ const packageDeps = await this.loadPackageDependencies(cacheDir);
124
+
125
+ // Build child nodes
126
+ const children: DependencyTreeNode[] = [];
127
+ for (const depKey of Object.keys(packageDeps)) {
128
+ const childNode = await this.buildTreeNode(depKey, lockFile, workspaceRoot, depth + 1, new Set(visited));
129
+ if (childNode) {
130
+ children.push(childNode);
131
+ }
132
+ }
133
+
134
+ return {
135
+ packageKey,
136
+ version: locked.version,
137
+ commit: locked.commit,
138
+ dependencies: children,
139
+ depth,
140
+ };
141
+ }
142
+
143
+ /**
144
+ * Finds all packages that depend on a given package.
145
+ *
146
+ * @param targetPackage - Package to analyze
147
+ * @param lockFile - Lock file with all dependencies
148
+ * @param workspaceRoot - Workspace root
149
+ * @returns List of reverse dependencies
150
+ */
151
+ async findReverseDependencies(
152
+ targetPackage: string,
153
+ lockFile: LockFile,
154
+ workspaceRoot: string
155
+ ): Promise<ReverseDependency[]> {
156
+ const reverseDeps: ReverseDependency[] = [];
157
+ const manifestPath = path.join(workspaceRoot, 'model.yaml');
158
+
159
+ // Check if target is a direct dependency of root
160
+ try {
161
+ const content = await fs.readFile(manifestPath, 'utf-8');
162
+ const manifest = YAML.parse(content) as {
163
+ dependencies?: Record<string, { source: string; version: string }>;
164
+ };
165
+
166
+ if (manifest.dependencies) {
167
+ for (const [, dep] of Object.entries(manifest.dependencies)) {
168
+ if (dep.source === targetPackage) {
169
+ reverseDeps.push({
170
+ dependentPackage: 'root',
171
+ version: 'workspace',
172
+ type: 'direct',
173
+ });
174
+ }
175
+ }
176
+ }
177
+ } catch {
178
+ // Ignore
179
+ }
180
+
181
+ // Check all other packages in lock file
182
+ for (const [packageKey, locked] of Object.entries(lockFile.dependencies)) {
183
+ if (packageKey === targetPackage) {
184
+ continue;
185
+ }
186
+
187
+ const cacheDir = this.getCacheDir(packageKey, locked.commit);
188
+ const packageDeps = await this.loadPackageDependencies(cacheDir);
189
+
190
+ if (packageDeps[targetPackage]) {
191
+ reverseDeps.push({
192
+ dependentPackage: packageKey,
193
+ version: locked.version,
194
+ type: 'direct',
195
+ });
196
+ }
197
+ }
198
+
199
+ return reverseDeps;
200
+ }
201
+
202
+ /**
203
+ * Formats a dependency tree as a readable string.
204
+ */
205
+ formatDependencyTree(nodes: DependencyTreeNode[], options: { showCommits?: boolean } = {}): string {
206
+ const lines: string[] = [];
207
+
208
+ const formatNode = (node: DependencyTreeNode, prefix: string, isLast: boolean): void => {
209
+ const branch = isLast ? '└── ' : '├── ';
210
+ const versionStr = options.showCommits
211
+ ? `${node.version} (${node.commit.substring(0, 7)})`
212
+ : node.version;
213
+
214
+ lines.push(`${prefix}${branch}${node.packageKey}@${versionStr}`);
215
+
216
+ const childPrefix = prefix + (isLast ? ' ' : '│ ');
217
+ node.dependencies.forEach((child, index) => {
218
+ const isLastChild = index === node.dependencies.length - 1;
219
+ formatNode(child, childPrefix, isLastChild);
220
+ });
221
+ };
222
+
223
+ nodes.forEach((node, index) => {
224
+ formatNode(node, '', index === nodes.length - 1);
225
+ });
226
+
227
+ return lines.join('\n');
228
+ }
229
+
230
+ /**
231
+ * Detects circular dependencies in a dependency graph.
232
+ */
233
+ async detectCircularDependencies(lockFile: LockFile): Promise<string[][]> {
234
+ const cycles: string[][] = [];
235
+ const visiting = new Set<string>();
236
+ const completed = new Set<string>();
237
+
238
+ const dfs = async (packageKey: string, path: string[]): Promise<void> => {
239
+ if (visiting.has(packageKey)) {
240
+ const cycleStart = path.indexOf(packageKey);
241
+ const cycle = cycleStart >= 0
242
+ ? [...path.slice(cycleStart), packageKey]
243
+ : [...path, packageKey];
244
+ cycles.push(cycle);
245
+ return;
246
+ }
247
+
248
+ if (completed.has(packageKey)) {
249
+ return;
250
+ }
251
+
252
+ visiting.add(packageKey);
253
+
254
+ const locked = lockFile.dependencies[packageKey];
255
+ if (locked) {
256
+ const cacheDir = this.getCacheDir(packageKey, locked.commit);
257
+ const deps = await this.loadPackageDependencies(cacheDir);
258
+
259
+ for (const depKey of Object.keys(deps)) {
260
+ await dfs(depKey, [...path, packageKey]);
261
+ }
262
+ }
263
+
264
+ visiting.delete(packageKey);
265
+ completed.add(packageKey);
266
+ };
267
+
268
+ for (const packageKey of Object.keys(lockFile.dependencies)) {
269
+ await dfs(packageKey, []);
270
+ }
271
+
272
+ return cycles;
273
+ }
274
+
275
+ /**
276
+ * Resolves version policies (latest, stable) to concrete versions.
277
+ */
278
+ async resolveVersionPolicy(
279
+ packageKey: string,
280
+ policy: string,
281
+ availableVersions: string[]
282
+ ): Promise<VersionPolicy> {
283
+ if (policy === 'latest') {
284
+ // Return the most recent version
285
+ const sorted = this.sortVersions(availableVersions);
286
+ return {
287
+ policy: 'latest',
288
+ version: sorted[0] || 'main',
289
+ availableVersions: sorted,
290
+ };
291
+ }
292
+
293
+ if (policy === 'stable') {
294
+ // Return the most recent stable version (exclude pre-release)
295
+ const stable = availableVersions.filter(v => !this.isPreRelease(v));
296
+ const sorted = this.sortVersions(stable);
297
+ return {
298
+ policy: 'stable',
299
+ version: sorted[0] || 'main',
300
+ availableVersions: sorted,
301
+ };
302
+ }
303
+
304
+ // Pinned version
305
+ return {
306
+ policy: 'pinned',
307
+ version: policy,
308
+ };
309
+ }
310
+
311
+ /**
312
+ * Gets the cache directory for a package.
313
+ */
314
+ private getCacheDir(packageKey: string, commit: string): string {
315
+ const [owner, repo] = packageKey.split('/');
316
+ return path.join(
317
+ os.homedir(),
318
+ '.dlang',
319
+ 'cache',
320
+ 'github', // Assume GitHub for now
321
+ owner,
322
+ repo,
323
+ commit
324
+ );
325
+ }
326
+
327
+ /**
328
+ * Loads dependencies from a cached package.
329
+ */
330
+ private async loadPackageDependencies(cacheDir: string): Promise<Record<string, string>> {
331
+ const manifestPath = path.join(cacheDir, 'model.yaml');
332
+
333
+ try {
334
+ const content = await fs.readFile(manifestPath, 'utf-8');
335
+ const manifest = YAML.parse(content) as {
336
+ dependencies?: Record<string, { source: string; version: string }>;
337
+ };
338
+
339
+ const deps: Record<string, string> = {};
340
+ if (manifest.dependencies) {
341
+ for (const [, dep] of Object.entries(manifest.dependencies)) {
342
+ deps[dep.source] = dep.version;
343
+ }
344
+ }
345
+
346
+ return deps;
347
+ } catch {
348
+ return {};
349
+ }
350
+ }
351
+
352
+ /**
353
+ * Sorts versions in descending order (newest first).
354
+ */
355
+ private sortVersions(versions: string[]): string[] {
356
+ return versions.sort((a, b) => {
357
+ // Simple lexicographic sort (good enough for basic semver)
358
+ // Production: use semver library
359
+ const aParts = a.replace(/^v/, '').split('.').map(Number);
360
+ const bParts = b.replace(/^v/, '').split('.').map(Number);
361
+
362
+ for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
363
+ const aNum = aParts[i] || 0;
364
+ const bNum = bParts[i] || 0;
365
+ if (aNum !== bNum) {
366
+ return bNum - aNum; // Descending
367
+ }
368
+ }
369
+
370
+ return 0;
371
+ });
372
+ }
373
+
374
+ /**
375
+ * Checks if a version is a pre-release.
376
+ */
377
+ private isPreRelease(version: string): boolean {
378
+ const clean = version.replace(/^v/, '');
379
+ return /-(alpha|beta|rc|pre|dev)/.test(clean);
380
+ }
381
+ }
@@ -0,0 +1,334 @@
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 (simple: use latest satisfying version)
13
+ * 6. Generate lock file with pinned commit hashes
14
+ */
15
+
16
+ import path from 'node:path';
17
+ import fs from 'node:fs/promises';
18
+ import YAML from 'yaml';
19
+ import { GitUrlParser, GitUrlResolver, PackageMetadata, LockFile, LockedDependency } from './git-url-resolver.js';
20
+
21
+ /**
22
+ * Dependency graph node representing a package and its dependencies.
23
+ */
24
+ export interface DependencyNode {
25
+ /** Package identifier (owner/repo) */
26
+ packageKey: string;
27
+ /** Version constraint from parent (e.g., "^1.0.0") */
28
+ versionConstraint: string;
29
+ /** Resolved version */
30
+ resolvedVersion?: string;
31
+ /** Resolved commit hash */
32
+ commitHash?: string;
33
+ /** Full git URL */
34
+ repoUrl?: string;
35
+ /** Direct dependencies of this package */
36
+ dependencies: Record<string, string>;
37
+ /** Parent packages that depend on this one */
38
+ dependents: string[];
39
+ }
40
+
41
+ /**
42
+ * Dependency graph for the entire workspace.
43
+ */
44
+ export interface DependencyGraph {
45
+ /** All discovered packages, keyed by owner/repo */
46
+ nodes: Record<string, DependencyNode>;
47
+ /** Root package name */
48
+ root: string;
49
+ }
50
+
51
+ /**
52
+ * Resolves dependencies and generates lock files.
53
+ */
54
+ export type { LockFile, PackageMetadata } from './git-url-resolver.js';
55
+ export class DependencyResolver {
56
+ private gitResolver: GitUrlResolver;
57
+ private workspaceRoot: string;
58
+
59
+ constructor(workspaceRoot: string, gitResolver?: GitUrlResolver) {
60
+ this.workspaceRoot = workspaceRoot;
61
+ this.gitResolver = gitResolver || new GitUrlResolver();
62
+ }
63
+
64
+ /**
65
+ * Resolves all dependencies for a workspace.
66
+ *
67
+ * Process:
68
+ * 1. Load root model.yaml
69
+ * 2. Build dependency graph (discover transitive deps)
70
+ * 3. Resolve version constraints
71
+ * 4. Generate lock file
72
+ * 5. Download all dependencies to cache
73
+ *
74
+ * @returns Generated lock file
75
+ */
76
+ async resolveDependencies(): Promise<LockFile> {
77
+ // Load root package config
78
+ const rootConfig = await this.loadPackageConfig(this.workspaceRoot);
79
+
80
+ if (!rootConfig.dependencies || Object.keys(rootConfig.dependencies).length === 0) {
81
+ // No dependencies
82
+ return { version: '1', dependencies: {} };
83
+ }
84
+
85
+ // Build dependency graph
86
+ const graph = await this.buildDependencyGraph(rootConfig);
87
+
88
+ // Resolve version constraints
89
+ await this.resolveVersions(graph);
90
+
91
+ // Generate and return lock file (caller writes to disk)
92
+ return this.generateLockFile(graph);
93
+ }
94
+
95
+ /**
96
+ * Builds the complete dependency graph by recursively discovering transitive dependencies.
97
+ */
98
+ private async buildDependencyGraph(rootConfig: PackageMetadata): Promise<DependencyGraph> {
99
+ const graph: DependencyGraph = {
100
+ nodes: {},
101
+ root: rootConfig.name || 'root',
102
+ };
103
+
104
+ // Process root dependencies
105
+ const queue: Array<{ packageKey: string; versionConstraint: string; parent: string }> = [];
106
+
107
+ for (const [depName, versionConstraint] of Object.entries(rootConfig.dependencies || {})) {
108
+ queue.push({
109
+ packageKey: depName,
110
+ versionConstraint,
111
+ parent: graph.root
112
+ });
113
+ }
114
+
115
+ // BFS to discover all transitive dependencies
116
+ const visited = new Set<string>();
117
+
118
+ while (queue.length > 0) {
119
+ const entry = queue.shift();
120
+ if (!entry) break;
121
+ const { packageKey, versionConstraint, parent } = entry;
122
+
123
+ // Skip if already processed
124
+ if (visited.has(packageKey)) {
125
+ // Update dependents list
126
+ if (!graph.nodes[packageKey].dependents.includes(parent)) {
127
+ graph.nodes[packageKey].dependents.push(parent);
128
+ }
129
+ continue;
130
+ }
131
+ visited.add(packageKey);
132
+
133
+ // Parse package identifier
134
+ const gitInfo = GitUrlParser.parse(packageKey);
135
+
136
+ // Download package to get its dlang.toml
137
+ const packageUri = await this.gitResolver.resolve(packageKey);
138
+ const packageDir = path.dirname(packageUri.fsPath);
139
+
140
+ // Load package config
141
+ const packageConfig = await this.loadPackageConfig(packageDir);
142
+
143
+ // Add to graph
144
+ graph.nodes[packageKey] = {
145
+ packageKey,
146
+ versionConstraint,
147
+ repoUrl: gitInfo.repoUrl,
148
+ dependencies: packageConfig.dependencies || {},
149
+ dependents: [parent],
150
+ };
151
+
152
+ // Queue transitive dependencies
153
+ for (const [transDepName, transVersionConstraint] of Object.entries(packageConfig.dependencies || {})) {
154
+ queue.push({
155
+ packageKey: transDepName,
156
+ versionConstraint: transVersionConstraint,
157
+ parent: packageKey,
158
+ });
159
+ }
160
+ }
161
+
162
+ return graph;
163
+ }
164
+
165
+ /**
166
+ * Resolves version constraints to specific versions.
167
+ *
168
+ * Simple algorithm: Use the latest version that satisfies all constraints.
169
+ * Future: Implement proper semantic versioning resolution.
170
+ */
171
+ private async resolveVersions(graph: DependencyGraph): Promise<void> {
172
+ for (const [packageKey, node] of Object.entries(graph.nodes)) {
173
+ // Parse package to get repo info
174
+ const gitInfo = GitUrlParser.parse(packageKey);
175
+
176
+ // For now, resolve to the version specified in the constraint
177
+ // Future: Implement proper semver range resolution
178
+ const version = this.extractVersionFromConstraint(node.versionConstraint);
179
+
180
+ // Resolve version to commit hash
181
+ const commitHash = await this.resolveCommitHash(gitInfo.repoUrl, version);
182
+
183
+ node.resolvedVersion = version;
184
+ node.commitHash = commitHash;
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Extracts a version from a constraint string.
190
+ *
191
+ * Examples:
192
+ * - "^1.0.0" → "1.0.0"
193
+ * - "~2.3.0" → "2.3.0"
194
+ * - "1.5.0" → "1.5.0"
195
+ * - "owner/repo@1.0.0" → "1.0.0"
196
+ *
197
+ * Future: Implement proper semver range parsing and resolution.
198
+ */
199
+ private extractVersionFromConstraint(constraint: string): string {
200
+ // Remove semver operators
201
+ let version = constraint.replace(/^[\^~>=<]/, '');
202
+
203
+ // Extract version from full import URL if present
204
+ if (version.includes('@')) {
205
+ version = version.split('@')[1];
206
+ }
207
+
208
+ return version || 'main';
209
+ }
210
+
211
+ /**
212
+ * Resolves a version (tag/branch) to a commit hash using git ls-remote.
213
+ */
214
+ private async resolveCommitHash(repoUrl: string, version: string): Promise<string> {
215
+ // This is a placeholder - the actual implementation is in GitUrlResolver
216
+ // We need to extract it or call the resolver
217
+ const gitInfo = GitUrlParser.parse(`${repoUrl}@${version}`);
218
+ const uri = await this.gitResolver.resolve(gitInfo.original);
219
+
220
+ // Extract commit hash from cache path
221
+ // Cache format: ~/.dlang/cache/{platform}/{owner}/{repo}/{commit-hash}/
222
+ const pathParts = uri.fsPath.split(path.sep);
223
+ const commitHashIndex = pathParts.length - 2; // Second to last segment
224
+ return pathParts[commitHashIndex];
225
+ }
226
+
227
+ /**
228
+ * Generates a lock file from the resolved dependency graph.
229
+ */
230
+ private generateLockFile(graph: DependencyGraph): LockFile {
231
+ const dependencies: Record<string, LockedDependency> = {};
232
+
233
+ for (const [packageKey, node] of Object.entries(graph.nodes)) {
234
+ if (!node.resolvedVersion || !node.commitHash) {
235
+ throw new Error(`Failed to resolve version for ${packageKey}`);
236
+ }
237
+
238
+ dependencies[packageKey] = {
239
+ version: node.resolvedVersion,
240
+ resolved: node.repoUrl || '',
241
+ commit: node.commitHash,
242
+ // Future: Calculate integrity hash
243
+ };
244
+ }
245
+
246
+ return {
247
+ version: '1',
248
+ dependencies,
249
+ };
250
+ }
251
+
252
+ /**
253
+ * Loads and parses a package's model.yaml file.
254
+ */
255
+ private async loadPackageConfig(packageDir: string): Promise<PackageMetadata> {
256
+ const yamlPath = path.join(packageDir, 'model.yaml');
257
+
258
+ try {
259
+ const yamlContent = await fs.readFile(yamlPath, 'utf-8');
260
+ return this.parseYaml(yamlContent);
261
+ } catch {
262
+ // No model.yaml found
263
+ return {};
264
+ }
265
+ }
266
+
267
+ /**
268
+ * Parses model.yaml content.
269
+ *
270
+ * Expected structure:
271
+ * model:
272
+ * name: package-name
273
+ * version: 1.0.0
274
+ * entry: index.dlang
275
+ *
276
+ * dependencies:
277
+ * package-name:
278
+ * source: owner/repo
279
+ * version: ^1.0.0
280
+ */
281
+ private parseYaml(content: string): PackageMetadata {
282
+ const parsed = YAML.parse(content) as {
283
+ model?: {
284
+ name?: string;
285
+ version?: string;
286
+ entry?: string;
287
+ };
288
+ dependencies?: Record<string, { source?: string; version?: string }>;
289
+ };
290
+
291
+ const config: PackageMetadata = {};
292
+
293
+ if (parsed.model) {
294
+ config.name = parsed.model.name;
295
+ config.version = parsed.model.version;
296
+ config.main = parsed.model.entry;
297
+ }
298
+
299
+ if (parsed.dependencies) {
300
+ config.dependencies = {};
301
+ for (const [, depInfo] of Object.entries(parsed.dependencies)) {
302
+ if (depInfo.source) {
303
+ const versionConstraint = depInfo.version || 'main';
304
+ // Store as "source@version" for consistency with import resolution
305
+ config.dependencies[depInfo.source] = versionConstraint;
306
+ }
307
+ }
308
+ }
309
+
310
+ return config;
311
+ }
312
+
313
+ /**
314
+ * Loads an existing lock file from disk.
315
+ */
316
+ static async loadLockFile(workspaceRoot: string): Promise<LockFile | undefined> {
317
+ const lockPath = path.join(workspaceRoot, 'model.lock');
318
+
319
+ try {
320
+ const content = await fs.readFile(lockPath, 'utf-8');
321
+ return JSON.parse(content) as LockFile;
322
+ } catch {
323
+ // No lock file
324
+ return undefined;
325
+ }
326
+ }
327
+
328
+ /**
329
+ * Parses a lock file from JSON format.
330
+ */
331
+ static parseLockFile(content: string): LockFile {
332
+ return JSON.parse(content) as LockFile;
333
+ }
334
+ }