@domainlang/language 0.5.2 → 0.7.0

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 (116) hide show
  1. package/README.md +1 -1
  2. package/out/domain-lang-module.js +5 -1
  3. package/out/domain-lang-module.js.map +1 -1
  4. package/out/generated/ast.d.ts +24 -0
  5. package/out/generated/ast.js.map +1 -1
  6. package/out/generated/grammar.js +22 -32
  7. package/out/generated/grammar.js.map +1 -1
  8. package/out/index.d.ts +2 -5
  9. package/out/index.js +10 -6
  10. package/out/index.js.map +1 -1
  11. package/out/lsp/domain-lang-code-actions.js +14 -8
  12. package/out/lsp/domain-lang-code-actions.js.map +1 -1
  13. package/out/lsp/domain-lang-completion.d.ts +3 -0
  14. package/out/lsp/domain-lang-completion.js +41 -13
  15. package/out/lsp/domain-lang-completion.js.map +1 -1
  16. package/out/lsp/domain-lang-formatter.js +24 -18
  17. package/out/lsp/domain-lang-formatter.js.map +1 -1
  18. package/out/lsp/domain-lang-index-manager.d.ts +170 -0
  19. package/out/lsp/domain-lang-index-manager.js +389 -0
  20. package/out/lsp/domain-lang-index-manager.js.map +1 -0
  21. package/out/lsp/domain-lang-scope-provider.d.ts +67 -0
  22. package/out/lsp/domain-lang-scope-provider.js +95 -0
  23. package/out/lsp/domain-lang-scope-provider.js.map +1 -0
  24. package/out/lsp/domain-lang-scope.js +31 -17
  25. package/out/lsp/domain-lang-scope.js.map +1 -1
  26. package/out/lsp/domain-lang-workspace-manager.d.ts +76 -9
  27. package/out/lsp/domain-lang-workspace-manager.js +176 -54
  28. package/out/lsp/domain-lang-workspace-manager.js.map +1 -1
  29. package/out/lsp/hover/domain-lang-hover.d.ts +45 -1
  30. package/out/lsp/hover/domain-lang-hover.js +308 -232
  31. package/out/lsp/hover/domain-lang-hover.js.map +1 -1
  32. package/out/lsp/hover/domain-lang-keywords.d.ts +3 -7
  33. package/out/lsp/hover/domain-lang-keywords.js +115 -38
  34. package/out/lsp/hover/domain-lang-keywords.js.map +1 -1
  35. package/out/lsp/manifest-diagnostics.js +95 -50
  36. package/out/lsp/manifest-diagnostics.js.map +1 -1
  37. package/out/main.js +204 -17
  38. package/out/main.js.map +1 -1
  39. package/out/services/import-resolver.d.ts +39 -2
  40. package/out/services/import-resolver.js +77 -12
  41. package/out/services/import-resolver.js.map +1 -1
  42. package/out/services/types.d.ts +2 -2
  43. package/out/services/workspace-manager.d.ts +33 -31
  44. package/out/services/workspace-manager.js +92 -148
  45. package/out/services/workspace-manager.js.map +1 -1
  46. package/out/utils/document-utils.d.ts +41 -0
  47. package/out/utils/document-utils.js +64 -0
  48. package/out/utils/document-utils.js.map +1 -0
  49. package/out/utils/import-utils.d.ts +0 -17
  50. package/out/utils/import-utils.js +2 -32
  51. package/out/utils/import-utils.js.map +1 -1
  52. package/out/utils/manifest-utils.d.ts +56 -0
  53. package/out/utils/manifest-utils.js +119 -0
  54. package/out/utils/manifest-utils.js.map +1 -0
  55. package/out/validation/constants.d.ts +13 -0
  56. package/out/validation/constants.js +18 -0
  57. package/out/validation/constants.js.map +1 -1
  58. package/out/validation/import.d.ts +12 -2
  59. package/out/validation/import.js +95 -22
  60. package/out/validation/import.js.map +1 -1
  61. package/out/validation/maps.js +51 -2
  62. package/out/validation/maps.js.map +1 -1
  63. package/package.json +1 -1
  64. package/src/domain-lang-module.ts +6 -1
  65. package/src/domain-lang.langium +37 -13
  66. package/src/generated/ast.ts +24 -0
  67. package/src/generated/grammar.ts +22 -32
  68. package/src/index.ts +12 -6
  69. package/src/lsp/domain-lang-code-actions.ts +13 -8
  70. package/src/lsp/domain-lang-completion.ts +61 -13
  71. package/src/lsp/domain-lang-formatter.ts +28 -23
  72. package/src/lsp/domain-lang-index-manager.ts +447 -0
  73. package/src/lsp/domain-lang-scope-provider.ts +134 -0
  74. package/src/lsp/domain-lang-scope.ts +29 -17
  75. package/src/lsp/domain-lang-workspace-manager.ts +201 -53
  76. package/src/lsp/hover/domain-lang-hover.ts +332 -226
  77. package/src/lsp/hover/domain-lang-keywords.ts +129 -43
  78. package/src/lsp/manifest-diagnostics.ts +100 -59
  79. package/src/main.ts +258 -16
  80. package/src/services/import-resolver.ts +91 -12
  81. package/src/services/types.ts +2 -2
  82. package/src/services/workspace-manager.ts +101 -175
  83. package/src/utils/document-utils.ts +80 -0
  84. package/src/utils/import-utils.ts +2 -40
  85. package/src/utils/manifest-utils.ts +132 -0
  86. package/src/validation/constants.ts +24 -0
  87. package/src/validation/import.ts +107 -24
  88. package/src/validation/maps.ts +59 -2
  89. package/out/lsp/hover/ddd-pattern-explanations.d.ts +0 -50
  90. package/out/lsp/hover/ddd-pattern-explanations.js +0 -196
  91. package/out/lsp/hover/ddd-pattern-explanations.js.map +0 -1
  92. package/out/services/dependency-analyzer.d.ts +0 -58
  93. package/out/services/dependency-analyzer.js +0 -254
  94. package/out/services/dependency-analyzer.js.map +0 -1
  95. package/out/services/dependency-resolver.d.ts +0 -146
  96. package/out/services/dependency-resolver.js +0 -452
  97. package/out/services/dependency-resolver.js.map +0 -1
  98. package/out/services/git-url-resolver.browser.d.ts +0 -10
  99. package/out/services/git-url-resolver.browser.js +0 -19
  100. package/out/services/git-url-resolver.browser.js.map +0 -1
  101. package/out/services/git-url-resolver.d.ts +0 -158
  102. package/out/services/git-url-resolver.js +0 -416
  103. package/out/services/git-url-resolver.js.map +0 -1
  104. package/out/services/governance-validator.d.ts +0 -44
  105. package/out/services/governance-validator.js +0 -153
  106. package/out/services/governance-validator.js.map +0 -1
  107. package/out/services/semver.d.ts +0 -98
  108. package/out/services/semver.js +0 -195
  109. package/out/services/semver.js.map +0 -1
  110. package/src/lsp/hover/ddd-pattern-explanations.ts +0 -237
  111. package/src/services/dependency-analyzer.ts +0 -321
  112. package/src/services/dependency-resolver.ts +0 -551
  113. package/src/services/git-url-resolver.browser.ts +0 -26
  114. package/src/services/git-url-resolver.ts +0 -517
  115. package/src/services/governance-validator.ts +0 -177
  116. package/src/services/semver.ts +0 -213
@@ -1,9 +1,8 @@
1
1
  import path from 'node:path';
2
2
  import fs from 'node:fs/promises';
3
3
  import YAML from 'yaml';
4
- import { DependencyResolver } from './dependency-resolver.js';
5
- import { GitUrlResolver } from './git-url-resolver.js';
6
4
  import { getGlobalOptimizer } from './performance-optimizer.js';
5
+ import { fileExists as checkFileExists, findWorkspaceRoot as findWorkspaceRootUtil } from '../utils/manifest-utils.js';
7
6
  import type {
8
7
  LockFile,
9
8
  LockedDependency,
@@ -22,8 +21,6 @@ const DEFAULT_LOCK_FILES = [
22
21
  'model.lock'
23
22
  ] as const;
24
23
 
25
- const JSON_SPACE = 2;
26
-
27
24
  interface ManifestCache {
28
25
  readonly manifest: ModelManifest;
29
26
  readonly path: string;
@@ -36,31 +33,37 @@ interface LoadedLockFile {
36
33
  }
37
34
 
38
35
  /**
39
- * Coordinates workspace discovery, lock file lifecycle management, and git resolver configuration.
36
+ * Coordinates workspace discovery and manifest/lock file reading.
37
+ *
38
+ * This is a read-only service for the LSP - it does NOT:
39
+ * - Generate lock files (use CLI: `dlang install`)
40
+ * - Download packages (use CLI: `dlang install`)
41
+ * - Make network requests
42
+ *
43
+ * The LSP uses this to:
44
+ * - Find the workspace root (where model.yaml is)
45
+ * - Read manifest configuration (path aliases, dependencies)
46
+ * - Read lock file (to resolve cached package locations)
40
47
  */
41
48
  export class WorkspaceManager {
42
49
  private readonly manifestFiles: readonly string[];
43
50
  private readonly lockFiles: readonly string[];
44
51
  private workspaceRoot: string | undefined;
45
52
  private lockFile: LockFile | undefined;
46
- private gitResolver: GitUrlResolver | undefined;
47
- private dependencyResolver: DependencyResolver | undefined;
48
53
  private initializePromise: Promise<void> | undefined;
49
54
  private manifestCache: ManifestCache | undefined;
50
55
 
51
- constructor(private readonly options: WorkspaceManagerOptions = {}) {
56
+ constructor(options: WorkspaceManagerOptions = {}) {
52
57
  this.manifestFiles = options.manifestFiles ?? [...DEFAULT_MANIFEST_FILES];
53
58
  this.lockFiles = options.lockFiles ?? [...DEFAULT_LOCK_FILES];
54
59
  }
55
60
 
56
61
  /**
57
- * Finds the workspace root, loads any existing lock file, and prepares the git resolver.
62
+ * Finds the workspace root and loads any existing lock file.
58
63
  * Repeated calls await the same initialization work.
59
64
  */
60
65
  async initialize(startPath: string): Promise<void> {
61
- if (!this.initializePromise) {
62
- this.initializePromise = this.performInitialization(startPath);
63
- }
66
+ this.initializePromise ??= this.performInitialization(startPath);
64
67
  await this.initializePromise;
65
68
  }
66
69
 
@@ -75,6 +78,17 @@ export class WorkspaceManager {
75
78
  return this.workspaceRoot;
76
79
  }
77
80
 
81
+ /**
82
+ * Returns the project-local package cache directory.
83
+ * Per PRS-010: .dlang/packages/
84
+ */
85
+ getCacheDir(): string {
86
+ if (!this.workspaceRoot) {
87
+ throw new Error('WorkspaceManager not initialized. Call initialize() first.');
88
+ }
89
+ return path.join(this.workspaceRoot, '.dlang', 'packages');
90
+ }
91
+
78
92
  /**
79
93
  * Resolves the manifest file path within the workspace, if present.
80
94
  */
@@ -87,7 +101,7 @@ export class WorkspaceManager {
87
101
 
88
102
  for (const manifest of this.manifestFiles) {
89
103
  const candidate = path.join(root, manifest);
90
- if (await this.fileExists(candidate)) {
104
+ if (await checkFileExists(candidate)) {
91
105
  return candidate;
92
106
  }
93
107
  }
@@ -105,41 +119,8 @@ export class WorkspaceManager {
105
119
  }
106
120
 
107
121
  /**
108
- * Returns the cached lock file or triggers resolution when missing.
109
- */
110
- async ensureLockFile(): Promise<LockFile> {
111
- await this.ensureInitialized();
112
-
113
- if (!this.lockFile) {
114
- // Try loading from cache first
115
- const optimizer = getGlobalOptimizer();
116
- const cached = await optimizer.getCachedLockFile(this.workspaceRoot || '');
117
-
118
- if (cached) {
119
- this.lockFile = cached;
120
- } else {
121
- if (this.options.allowNetwork === false) {
122
- throw new Error(
123
- 'Lock file (model.lock) not found and network access is disabled.\n' +
124
- 'Hint: Run \'dlang install\' to generate the lock file.'
125
- );
126
- }
127
- await this.generateLockFile();
128
- }
129
- }
130
-
131
- if (!this.lockFile) {
132
- throw new Error(
133
- 'Unable to resolve workspace lock file.\n' +
134
- 'Hint: Ensure model.yaml exists and run \'dlang install\' to generate model.lock.'
135
- );
136
- }
137
-
138
- return this.lockFile;
139
- }
140
-
141
- /**
142
- * Gets the currently cached lock file without refreshing from disk.
122
+ * Gets the currently cached lock file.
123
+ * Returns undefined if no lock file exists (run `dlang install` to create one).
143
124
  */
144
125
  async getLockFile(): Promise<LockFile | undefined> {
145
126
  await this.ensureInitialized();
@@ -147,12 +128,16 @@ export class WorkspaceManager {
147
128
  }
148
129
 
149
130
  /**
150
- * Reloads the lock file from disk, updating the git resolver.
131
+ * Reloads the lock file from disk.
151
132
  */
152
133
  async refreshLockFile(): Promise<LockFile | undefined> {
153
134
  await this.ensureInitialized();
154
135
  const loaded = await this.loadLockFileFromDisk();
155
- this.applyLockFile(loaded);
136
+ if (loaded) {
137
+ this.lockFile = loaded.lockFile;
138
+ } else {
139
+ this.lockFile = undefined;
140
+ }
156
141
  return this.lockFile;
157
142
  }
158
143
 
@@ -166,10 +151,6 @@ export class WorkspaceManager {
166
151
  invalidateCache(): void {
167
152
  this.manifestCache = undefined;
168
153
  this.lockFile = undefined;
169
- // Re-apply undefined to git resolver to clear its lock file
170
- if (this.gitResolver) {
171
- this.gitResolver.setLockFile(undefined);
172
- }
173
154
  }
174
155
 
175
156
  /**
@@ -186,32 +167,6 @@ export class WorkspaceManager {
186
167
  */
187
168
  invalidateLockCache(): void {
188
169
  this.lockFile = undefined;
189
- if (this.gitResolver) {
190
- this.gitResolver.setLockFile(undefined);
191
- }
192
- }
193
-
194
- /**
195
- * Provides the shared git URL resolver configured with the current lock file.
196
- */
197
- async getGitResolver(): Promise<GitUrlResolver> {
198
- await this.ensureInitialized();
199
- if (!this.gitResolver) {
200
- throw new Error('GitUrlResolver not available. Workspace initialization failed.');
201
- }
202
- return this.gitResolver;
203
- }
204
-
205
- /**
206
- * Forces dependency resolution and regenerates lock files on disk.
207
- */
208
- async regenerateLockFile(): Promise<LockFile> {
209
- await this.ensureInitialized();
210
- await this.generateLockFile(true);
211
- if (!this.lockFile) {
212
- throw new Error('Failed to regenerate workspace lock file.');
213
- }
214
- return this.lockFile;
215
170
  }
216
171
 
217
172
  /**
@@ -229,7 +184,7 @@ export class WorkspaceManager {
229
184
  * In the new format, the key IS the owner/package, so source is derived from key
230
185
  * ONLY for git dependencies (not for path-based local dependencies).
231
186
  */
232
- private normalizeDependency(key: string, dep: DependencySpec): ExtendedDependencySpec {
187
+ normalizeDependency(key: string, dep: DependencySpec): ExtendedDependencySpec {
233
188
  if (typeof dep === 'string') {
234
189
  // Short form: "owner/package": "v1.0.0" or "main"
235
190
  // Key is the source (owner/package format)
@@ -246,14 +201,18 @@ export class WorkspaceManager {
246
201
  }
247
202
 
248
203
  /**
249
- * Resolves a manifest dependency to its git import string.
204
+ * Resolves a dependency import specifier to its cached package path.
250
205
  *
251
- * NEW FORMAT (PRS-010): Dependencies are keyed by owner/package directly
252
206
  * @param specifier - Import specifier (owner/package format, may include subpaths)
253
- * @returns Resolved git import string or undefined when not found
207
+ * @returns Path to the cached package entry point, or undefined if not found
254
208
  */
255
- async resolveDependencyImport(specifier: string): Promise<string | undefined> {
209
+ async resolveDependencyPath(specifier: string): Promise<string | undefined> {
256
210
  await this.ensureInitialized();
211
+
212
+ if (!this.lockFile) {
213
+ return undefined;
214
+ }
215
+
257
216
  const manifest = await this.loadManifest();
258
217
  const dependencies = manifest?.dependencies;
259
218
 
@@ -261,8 +220,7 @@ export class WorkspaceManager {
261
220
  return undefined;
262
221
  }
263
222
 
264
- // NEW: Dependencies are keyed by owner/package (e.g., "domainlang/core")
265
- // Import specifier is also owner/package, potentially with subpath
223
+ // Find matching dependency
266
224
  for (const [key, dep] of Object.entries(dependencies)) {
267
225
  const normalized = this.normalizeDependency(key, dep);
268
226
 
@@ -277,29 +235,51 @@ export class WorkspaceManager {
277
235
 
278
236
  // Match if specifier equals key or starts with key/
279
237
  if (specifier === key || specifier.startsWith(`${key}/`)) {
238
+ // Find in lock file
239
+ const locked = this.lockFile.dependencies[normalized.source];
240
+ if (!locked) {
241
+ return undefined;
242
+ }
243
+
244
+ // Compute cache path
245
+ const [owner, repo] = normalized.source.split('/');
246
+ const packageDir = path.join(this.getCacheDir(), owner, repo, locked.commit);
247
+
248
+ // Handle subpaths
280
249
  const suffix = specifier.slice(key.length);
281
- const ref = normalized.ref ?? '';
282
- const refSegment = ref
283
- ? (ref.startsWith('@') ? ref : `@${ref}`)
284
- : '';
285
- return `${normalized.source}${refSegment}${suffix}`;
250
+ if (suffix) {
251
+ // Import with subpath: owner/package/subpath
252
+ return path.join(packageDir, suffix);
253
+ }
254
+
255
+ // Read entry point from package's model.yaml
256
+ const entryPoint = await this.readPackageEntry(packageDir);
257
+ return path.join(packageDir, entryPoint);
286
258
  }
287
259
  }
288
260
 
289
261
  return undefined;
290
262
  }
291
263
 
264
+ /**
265
+ * Reads the entry point from a cached package's model.yaml.
266
+ */
267
+ private async readPackageEntry(packageDir: string): Promise<string> {
268
+ const manifestPath = path.join(packageDir, 'model.yaml');
269
+ try {
270
+ const content = await fs.readFile(manifestPath, 'utf-8');
271
+ const manifest = YAML.parse(content) as { model?: { entry?: string } };
272
+ return manifest?.model?.entry ?? 'index.dlang';
273
+ } catch {
274
+ return 'index.dlang';
275
+ }
276
+ }
277
+
292
278
  private async performInitialization(startPath: string): Promise<void> {
293
279
  this.workspaceRoot = await this.findWorkspaceRoot(startPath) ?? path.resolve(startPath);
294
-
295
- // Per PRS-010: Project-local cache at .dlang/packages/ (like node_modules)
296
- const cacheDir = path.join(this.workspaceRoot, '.dlang', 'packages');
297
- this.gitResolver = new GitUrlResolver(cacheDir);
298
280
  const loaded = await this.loadLockFileFromDisk();
299
- this.applyLockFile(loaded);
300
-
301
- if (!this.lockFile && this.options.autoResolve !== false && this.options.allowNetwork !== false) {
302
- await this.generateLockFile();
281
+ if (loaded) {
282
+ this.lockFile = loaded.lockFile;
303
283
  }
304
284
  }
305
285
 
@@ -311,72 +291,21 @@ export class WorkspaceManager {
311
291
  }
312
292
  }
313
293
 
314
- private applyLockFile(loaded: LoadedLockFile | undefined): void {
315
- if (!this.gitResolver) {
316
- return;
317
- }
318
-
319
- if (loaded) {
320
- this.lockFile = loaded.lockFile;
321
- this.gitResolver.setLockFile(this.lockFile);
322
- } else {
323
- this.lockFile = undefined;
324
- this.gitResolver.setLockFile(undefined);
325
- }
326
- }
327
-
328
- private async generateLockFile(force = false): Promise<void> {
329
- if (!this.workspaceRoot || !this.gitResolver) {
330
- throw new Error('WorkspaceManager not initialized.');
331
- }
332
-
333
- const resolver = this.ensureDependencyResolver();
334
- if (!force && this.lockFile) {
335
- return;
336
- }
337
-
338
- const lockFile = await resolver.resolveDependencies();
339
- this.lockFile = lockFile;
340
- this.gitResolver.setLockFile(lockFile);
341
-
342
- // Write JSON lock file
343
- await this.writeJsonLockFile(lockFile);
344
- }
345
-
346
- private ensureDependencyResolver(): DependencyResolver {
347
- if (!this.workspaceRoot || !this.gitResolver) {
348
- throw new Error('WorkspaceManager not initialized.');
349
- }
350
-
351
- if (!this.dependencyResolver) {
352
- this.dependencyResolver = new DependencyResolver(this.workspaceRoot, this.gitResolver);
353
- }
354
-
355
- return this.dependencyResolver;
356
- }
357
-
358
- private async writeJsonLockFile(lockFile: LockFile): Promise<void> {
359
- if (!this.workspaceRoot) {
360
- return;
361
- }
362
-
363
- const jsonPath = path.join(this.workspaceRoot, 'model.lock');
364
- const payload = {
365
- version: lockFile.version,
366
- dependencies: lockFile.dependencies,
367
- } satisfies LockFile;
368
-
369
- await fs.writeFile(jsonPath, JSON.stringify(payload, undefined, JSON_SPACE), 'utf-8');
370
- }
371
-
372
294
  private async loadLockFileFromDisk(): Promise<LoadedLockFile | undefined> {
373
295
  if (!this.workspaceRoot) {
374
296
  return undefined;
375
297
  }
376
298
 
299
+ // Try performance optimizer cache first
300
+ const optimizer = getGlobalOptimizer();
301
+ const cached = await optimizer.getCachedLockFile(this.workspaceRoot);
302
+ if (cached) {
303
+ return { lockFile: cached, filePath: path.join(this.workspaceRoot, 'model.lock') };
304
+ }
305
+
377
306
  for (const filename of this.lockFiles) {
378
307
  const filePath = path.join(this.workspaceRoot, filename);
379
- const lockFile = await this.tryReadLockFile(filePath, filename);
308
+ const lockFile = await this.tryReadLockFile(filePath);
380
309
  if (lockFile) {
381
310
  return { lockFile, filePath };
382
311
  }
@@ -385,7 +314,7 @@ export class WorkspaceManager {
385
314
  return undefined;
386
315
  }
387
316
 
388
- private async tryReadLockFile(filePath: string, _filename: string): Promise<LockFile | undefined> {
317
+ private async tryReadLockFile(filePath: string): Promise<LockFile | undefined> {
389
318
  try {
390
319
  const content = await fs.readFile(filePath, 'utf-8');
391
320
  return this.parseJsonLockFile(content);
@@ -406,8 +335,7 @@ export class WorkspaceManager {
406
335
 
407
336
  try {
408
337
  const stat = await fs.stat(manifestPath);
409
- if (this.manifestCache &&
410
- this.manifestCache.path === manifestPath &&
338
+ if (this.manifestCache?.path === manifestPath &&
411
339
  this.manifestCache.mtimeMs === stat.mtimeMs) {
412
340
  return this.manifestCache.manifest;
413
341
  }
@@ -562,7 +490,17 @@ export class WorkspaceManager {
562
490
  return { version, dependencies };
563
491
  }
564
492
 
493
+ /**
494
+ * Finds workspace root by walking up from startPath looking for model.yaml.
495
+ * Uses configurable manifest files if specified in constructor options.
496
+ */
565
497
  private async findWorkspaceRoot(startPath: string): Promise<string | undefined> {
498
+ // Use shared utility for default case (single manifest file)
499
+ if (this.manifestFiles.length === 1 && this.manifestFiles[0] === 'model.yaml') {
500
+ return findWorkspaceRootUtil(startPath);
501
+ }
502
+
503
+ // Custom logic for multiple or non-default manifest files
566
504
  let current = path.resolve(startPath);
567
505
  const { root } = path.parse(current);
568
506
 
@@ -586,22 +524,10 @@ export class WorkspaceManager {
586
524
 
587
525
  private async containsManifest(dir: string): Promise<boolean> {
588
526
  for (const manifest of this.manifestFiles) {
589
- if (await this.fileExists(path.join(dir, manifest))) {
527
+ if (await checkFileExists(path.join(dir, manifest))) {
590
528
  return true;
591
529
  }
592
530
  }
593
531
  return false;
594
532
  }
595
-
596
- private async fileExists(targetPath: string): Promise<boolean> {
597
- try {
598
- await fs.access(targetPath);
599
- return true;
600
- } catch (error) {
601
- if ((error as NodeJS.ErrnoException)?.code === 'ENOENT') {
602
- return false;
603
- }
604
- throw error;
605
- }
606
- }
607
533
  }
@@ -0,0 +1,80 @@
1
+ import { DocumentState, type LangiumDocument } from 'langium';
2
+
3
+ /**
4
+ * Waits for a document to reach a specific state.
5
+ * Useful for ensuring documents are fully linked before accessing cross-references.
6
+ *
7
+ * @param document - The LangiumDocument to wait for
8
+ * @param targetState - The minimum required DocumentState
9
+ * @param timeout - Maximum time to wait in milliseconds (default: 5000ms)
10
+ * @throws {Error} If document doesn't reach target state within timeout
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * // Ensure document is linked before accessing references
15
+ * await waitForState(document, DocumentState.Linked);
16
+ * const domain = bc.domain?.ref; // Now guaranteed to be resolved
17
+ * ```
18
+ *
19
+ * @example
20
+ * ```typescript
21
+ * // Ensure validation is complete
22
+ * await waitForState(document, DocumentState.Validated);
23
+ * const diagnostics = document.diagnostics; // Now includes all validation results
24
+ * ```
25
+ */
26
+ export async function waitForState(
27
+ document: LangiumDocument,
28
+ targetState: DocumentState,
29
+ timeout = 5000
30
+ ): Promise<void> {
31
+ if (document.state >= targetState) {
32
+ return;
33
+ }
34
+
35
+ return new Promise((resolve, reject) => {
36
+ const timer = setTimeout(() => {
37
+ reject(new Error(
38
+ `Document ${document.uri.toString()} did not reach state ${targetState} within ${timeout}ms. ` +
39
+ `Current state: ${document.state}`
40
+ ));
41
+ }, timeout);
42
+
43
+ const checkState = (): void => {
44
+ if (document.state >= targetState) {
45
+ clearTimeout(timer);
46
+ resolve();
47
+ } else {
48
+ // Check every 10ms
49
+ setTimeout(checkState, 10);
50
+ }
51
+ };
52
+
53
+ checkState();
54
+ });
55
+ }
56
+
57
+ /**
58
+ * Waits for multiple documents to reach a specific state.
59
+ * Useful for batch operations where all documents must be ready.
60
+ *
61
+ * @param documents - Array of LangiumDocuments to wait for
62
+ * @param targetState - The minimum required DocumentState
63
+ * @param timeout - Maximum time to wait per document in milliseconds
64
+ * @throws {Error} If any document doesn't reach target state within timeout
65
+ *
66
+ * @example
67
+ * ```typescript
68
+ * // Ensure all imported documents are linked
69
+ * await waitForDocuments(importedDocs, DocumentState.Linked);
70
+ * ```
71
+ */
72
+ export async function waitForDocuments(
73
+ documents: LangiumDocument[],
74
+ targetState: DocumentState,
75
+ timeout = 5000
76
+ ): Promise<void> {
77
+ await Promise.all(
78
+ documents.map(doc => waitForState(doc, targetState, timeout))
79
+ );
80
+ }
@@ -1,7 +1,6 @@
1
1
  import path from 'node:path';
2
2
  import { URI, type LangiumDocument, type LangiumDocuments } from 'langium';
3
3
  import type { Model } from '../generated/ast.js';
4
- import type { GitUrlResolver } from '../services/git-url-resolver.js';
5
4
  import { WorkspaceManager } from '../services/workspace-manager.js';
6
5
  import { ImportResolver } from '../services/import-resolver.js';
7
6
  import type { DomainLangServices } from '../domain-lang-module.js';
@@ -27,7 +26,7 @@ let lastInitializedDir: string | undefined;
27
26
  async function getStandaloneImportResolver(startDir: string): Promise<ImportResolver> {
28
27
  // Re-initialize if directory changed (workspace boundary)
29
28
  if (lastInitializedDir !== startDir || !standaloneImportResolver) {
30
- standaloneWorkspaceManager = new WorkspaceManager({ autoResolve: false, allowNetwork: false });
29
+ standaloneWorkspaceManager = new WorkspaceManager();
31
30
  try {
32
31
  await standaloneWorkspaceManager.initialize(startDir);
33
32
  } catch (error) {
@@ -42,18 +41,6 @@ async function getStandaloneImportResolver(startDir: string): Promise<ImportReso
42
41
  return standaloneImportResolver;
43
42
  }
44
43
 
45
- /**
46
- * Gets the git URL resolver from a workspace manager.
47
- *
48
- * @param startDir - Directory to start workspace search from
49
- * @returns Promise resolving to the git URL resolver
50
- */
51
- async function getGitResolver(startDir: string): Promise<GitUrlResolver> {
52
- const workspaceManager = new WorkspaceManager({ autoResolve: false, allowNetwork: false });
53
- await workspaceManager.initialize(startDir);
54
- return workspaceManager.getGitResolver();
55
- }
56
-
57
44
  /**
58
45
  * Resolves an import path to an absolute file URI.
59
46
  *
@@ -115,7 +102,7 @@ export async function ensureImportGraphFromDocument(
115
102
  for (const imp of model.imports ?? []) {
116
103
  if (!imp.uri) continue;
117
104
 
118
- // Use new resolveImportPath that supports git URLs
105
+ // Use new resolveImportPath that supports external dependencies
119
106
  const resolvedUri = await resolveImportPath(doc, imp.uri);
120
107
  const childDoc = await langiumDocuments.getOrCreateDocument(resolvedUri);
121
108
  await visit(childDoc);
@@ -125,28 +112,3 @@ export async function ensureImportGraphFromDocument(
125
112
  await visit(document);
126
113
  return visited;
127
114
  }
128
-
129
- /**
130
- * Gets cache statistics for git imports.
131
- *
132
- * @returns Cache statistics including size and number of cached repositories
133
- */
134
- export async function getGitCacheStats(startDir: string = process.cwd()): Promise<{
135
- totalSize: number;
136
- repoCount: number;
137
- cacheDir: string;
138
- }> {
139
- const resolver = await getGitResolver(startDir);
140
- return await resolver.getCacheStats();
141
- }
142
-
143
- /**
144
- * Clears the git import cache.
145
- *
146
- * @param startDir - Starting directory for workspace resolution
147
- * @returns Promise that resolves when cache is cleared
148
- */
149
- export async function clearGitCache(startDir: string = process.cwd()): Promise<void> {
150
- const resolver = await getGitResolver(startDir);
151
- return await resolver.clearCache();
152
- }