@domainlang/language 0.1.81 → 0.4.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 (111) hide show
  1. package/README.md +147 -16
  2. package/out/domain-lang-module.d.ts +2 -0
  3. package/out/domain-lang-module.js +11 -3
  4. package/out/domain-lang-module.js.map +1 -1
  5. package/out/generated/ast.d.ts +8 -19
  6. package/out/generated/ast.js +1 -10
  7. package/out/generated/ast.js.map +1 -1
  8. package/out/generated/grammar.d.ts +1 -1
  9. package/out/generated/grammar.js +28 -123
  10. package/out/generated/grammar.js.map +1 -1
  11. package/out/generated/module.d.ts +1 -1
  12. package/out/generated/module.js +1 -1
  13. package/out/index.d.ts +3 -0
  14. package/out/index.js +5 -0
  15. package/out/index.js.map +1 -1
  16. package/out/lsp/domain-lang-code-actions.d.ts +55 -0
  17. package/out/lsp/domain-lang-code-actions.js +143 -0
  18. package/out/lsp/domain-lang-code-actions.js.map +1 -0
  19. package/out/lsp/domain-lang-workspace-manager.d.ts +21 -0
  20. package/out/lsp/domain-lang-workspace-manager.js +93 -0
  21. package/out/lsp/domain-lang-workspace-manager.js.map +1 -0
  22. package/out/lsp/hover/domain-lang-hover.js +0 -4
  23. package/out/lsp/hover/domain-lang-hover.js.map +1 -1
  24. package/out/lsp/manifest-diagnostics.d.ts +82 -0
  25. package/out/lsp/manifest-diagnostics.js +230 -0
  26. package/out/lsp/manifest-diagnostics.js.map +1 -0
  27. package/out/sdk/index.d.ts +1 -1
  28. package/out/sdk/loader-node.d.ts +7 -3
  29. package/out/sdk/loader-node.js +24 -9
  30. package/out/sdk/loader-node.js.map +1 -1
  31. package/out/sdk/types.d.ts +0 -21
  32. package/out/services/dependency-analyzer.d.ts +3 -39
  33. package/out/services/dependency-analyzer.js +22 -47
  34. package/out/services/dependency-analyzer.js.map +1 -1
  35. package/out/services/dependency-resolver.d.ts +68 -45
  36. package/out/services/dependency-resolver.js +243 -43
  37. package/out/services/dependency-resolver.js.map +1 -1
  38. package/out/services/git-url-resolver.browser.d.ts +4 -12
  39. package/out/services/git-url-resolver.browser.js +5 -1
  40. package/out/services/git-url-resolver.browser.js.map +1 -1
  41. package/out/services/git-url-resolver.d.ts +22 -56
  42. package/out/services/git-url-resolver.js +70 -36
  43. package/out/services/git-url-resolver.js.map +1 -1
  44. package/out/services/governance-validator.d.ts +1 -37
  45. package/out/services/governance-validator.js +4 -10
  46. package/out/services/governance-validator.js.map +1 -1
  47. package/out/services/import-resolver.d.ts +65 -6
  48. package/out/services/import-resolver.js +223 -5
  49. package/out/services/import-resolver.js.map +1 -1
  50. package/out/services/performance-optimizer.d.ts +1 -1
  51. package/out/services/semver.d.ts +98 -0
  52. package/out/services/semver.js +195 -0
  53. package/out/services/semver.js.map +1 -0
  54. package/out/services/types.d.ts +340 -0
  55. package/out/services/types.js +46 -0
  56. package/out/services/types.js.map +1 -0
  57. package/out/services/workspace-manager.d.ts +57 -10
  58. package/out/services/workspace-manager.js +187 -21
  59. package/out/services/workspace-manager.js.map +1 -1
  60. package/out/syntaxes/domain-lang.monarch.js +1 -1
  61. package/out/syntaxes/domain-lang.monarch.js.map +1 -1
  62. package/out/utils/import-utils.d.ts +4 -12
  63. package/out/utils/import-utils.js +35 -135
  64. package/out/utils/import-utils.js.map +1 -1
  65. package/out/validation/constants.d.ts +103 -0
  66. package/out/validation/constants.js +141 -2
  67. package/out/validation/constants.js.map +1 -1
  68. package/out/validation/domain.js +46 -1
  69. package/out/validation/domain.js.map +1 -1
  70. package/out/validation/import.d.ts +46 -22
  71. package/out/validation/import.js +187 -85
  72. package/out/validation/import.js.map +1 -1
  73. package/out/validation/manifest.d.ts +144 -0
  74. package/out/validation/manifest.js +327 -0
  75. package/out/validation/manifest.js.map +1 -0
  76. package/out/validation/maps.js +10 -6
  77. package/out/validation/maps.js.map +1 -1
  78. package/out/validation/metadata.js +5 -1
  79. package/out/validation/metadata.js.map +1 -1
  80. package/package.json +23 -10
  81. package/src/domain-lang-module.ts +18 -6
  82. package/src/domain-lang.langium +7 -12
  83. package/src/generated/ast.ts +7 -20
  84. package/src/generated/grammar.ts +28 -123
  85. package/src/generated/module.ts +1 -1
  86. package/src/index.ts +7 -0
  87. package/src/lsp/domain-lang-code-actions.ts +189 -0
  88. package/src/lsp/domain-lang-workspace-manager.ts +104 -0
  89. package/src/lsp/hover/domain-lang-hover.ts +0 -2
  90. package/src/lsp/manifest-diagnostics.ts +290 -0
  91. package/src/sdk/index.ts +0 -2
  92. package/src/sdk/loader-node.ts +29 -9
  93. package/src/sdk/types.ts +0 -23
  94. package/src/services/dependency-analyzer.ts +24 -84
  95. package/src/services/dependency-resolver.ts +301 -84
  96. package/src/services/git-url-resolver.browser.ts +9 -14
  97. package/src/services/git-url-resolver.ts +86 -93
  98. package/src/services/governance-validator.ts +5 -47
  99. package/src/services/import-resolver.ts +270 -8
  100. package/src/services/performance-optimizer.ts +1 -1
  101. package/src/services/semver.ts +213 -0
  102. package/src/services/types.ts +415 -0
  103. package/src/services/workspace-manager.ts +237 -46
  104. package/src/syntaxes/domain-lang.monarch.ts +1 -1
  105. package/src/utils/import-utils.ts +38 -160
  106. package/src/validation/constants.ts +182 -2
  107. package/src/validation/domain.ts +54 -1
  108. package/src/validation/import.ts +228 -104
  109. package/src/validation/manifest.ts +439 -0
  110. package/src/validation/maps.ts +10 -6
  111. package/src/validation/metadata.ts +5 -1
@@ -4,7 +4,15 @@ import YAML from 'yaml';
4
4
  import { DependencyResolver } from './dependency-resolver.js';
5
5
  import { GitUrlResolver } from './git-url-resolver.js';
6
6
  import { getGlobalOptimizer } from './performance-optimizer.js';
7
- import type { LockFile, LockedDependency } from './git-url-resolver.js';
7
+ import type {
8
+ LockFile,
9
+ LockedDependency,
10
+ ModelManifest,
11
+ DependencySpec,
12
+ ExtendedDependencySpec,
13
+ PathAliases,
14
+ WorkspaceManagerOptions
15
+ } from './types.js';
8
16
 
9
17
  const DEFAULT_MANIFEST_FILES = [
10
18
  'model.yaml'
@@ -16,27 +24,6 @@ const DEFAULT_LOCK_FILES = [
16
24
 
17
25
  const JSON_SPACE = 2;
18
26
 
19
- export interface WorkspaceManagerOptions {
20
- readonly autoResolve?: boolean;
21
- readonly manifestFiles?: readonly string[];
22
- readonly lockFiles?: readonly string[];
23
- }
24
-
25
- interface ManifestDependency {
26
- readonly source?: string;
27
- readonly version?: string;
28
- readonly description?: string;
29
- }
30
-
31
- interface ModelManifest {
32
- readonly model?: {
33
- readonly name?: string;
34
- readonly version?: string;
35
- readonly entry?: string;
36
- };
37
- readonly dependencies?: Record<string, ManifestDependency>;
38
- }
39
-
40
27
  interface ManifestCache {
41
28
  readonly manifest: ModelManifest;
42
29
  readonly path: string;
@@ -108,6 +95,15 @@ export class WorkspaceManager {
108
95
  return undefined;
109
96
  }
110
97
 
98
+ /**
99
+ * Returns the parsed manifest when present, otherwise undefined.
100
+ * Uses cached contents when unchanged on disk.
101
+ */
102
+ async getManifest(): Promise<ModelManifest | undefined> {
103
+ await this.ensureInitialized();
104
+ return this.loadManifest();
105
+ }
106
+
111
107
  /**
112
108
  * Returns the cached lock file or triggers resolution when missing.
113
109
  */
@@ -122,12 +118,21 @@ export class WorkspaceManager {
122
118
  if (cached) {
123
119
  this.lockFile = cached;
124
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
+ }
125
127
  await this.generateLockFile();
126
128
  }
127
129
  }
128
130
 
129
131
  if (!this.lockFile) {
130
- throw new Error('Unable to resolve workspace lock file.');
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
+ );
131
136
  }
132
137
 
133
138
  return this.lockFile;
@@ -151,6 +156,41 @@ export class WorkspaceManager {
151
156
  return this.lockFile;
152
157
  }
153
158
 
159
+ /**
160
+ * Invalidates all cached data (manifest and lock file).
161
+ * Call this when config files change externally (e.g., from CLI commands).
162
+ *
163
+ * After invalidation, the next call to getManifest() or getLockFile()
164
+ * will re-read from disk.
165
+ */
166
+ invalidateCache(): void {
167
+ this.manifestCache = undefined;
168
+ 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
+ }
174
+
175
+ /**
176
+ * Invalidates only the manifest cache.
177
+ * Call this when model.yaml changes.
178
+ */
179
+ invalidateManifestCache(): void {
180
+ this.manifestCache = undefined;
181
+ }
182
+
183
+ /**
184
+ * Invalidates only the lock file cache.
185
+ * Call this when model.lock changes.
186
+ */
187
+ invalidateLockCache(): void {
188
+ this.lockFile = undefined;
189
+ if (this.gitResolver) {
190
+ this.gitResolver.setLockFile(undefined);
191
+ }
192
+ }
193
+
154
194
  /**
155
195
  * Provides the shared git URL resolver configured with the current lock file.
156
196
  */
@@ -175,12 +215,44 @@ export class WorkspaceManager {
175
215
  }
176
216
 
177
217
  /**
178
- * Resolves a manifest dependency alias to its git import string.
179
- *
180
- * @param aliasPath - Alias from import statement (may include subpaths)
181
- * @returns Resolved git import string or undefined when alias is unknown
218
+ * Returns the path aliases from the manifest, if present.
182
219
  */
183
- async resolveDependencyImport(aliasPath: string): Promise<string | undefined> {
220
+ async getPathAliases(): Promise<PathAliases | undefined> {
221
+ const manifest = await this.getManifest();
222
+ return manifest?.paths;
223
+ }
224
+
225
+ /**
226
+ * Normalizes a dependency entry to its extended form.
227
+ * Handles both short form (string version) and extended form (object).
228
+ *
229
+ * In the new format, the key IS the owner/package, so source is derived from key
230
+ * ONLY for git dependencies (not for path-based local dependencies).
231
+ */
232
+ private normalizeDependency(key: string, dep: DependencySpec): ExtendedDependencySpec {
233
+ if (typeof dep === 'string') {
234
+ // Short form: "owner/package": "v1.0.0" or "main"
235
+ // Key is the source (owner/package format)
236
+ return { source: key, ref: dep };
237
+ }
238
+ // Extended form:
239
+ // - If has source: use as-is
240
+ // - If has path: it's a local dep, don't set source
241
+ // - If neither: derive source from key (owner/package becomes source)
242
+ if (dep.source || dep.path) {
243
+ return dep;
244
+ }
245
+ return { ...dep, source: key };
246
+ }
247
+
248
+ /**
249
+ * Resolves a manifest dependency to its git import string.
250
+ *
251
+ * NEW FORMAT (PRS-010): Dependencies are keyed by owner/package directly
252
+ * @param specifier - Import specifier (owner/package format, may include subpaths)
253
+ * @returns Resolved git import string or undefined when not found
254
+ */
255
+ async resolveDependencyImport(specifier: string): Promise<string | undefined> {
184
256
  await this.ensureInitialized();
185
257
  const manifest = await this.loadManifest();
186
258
  const dependencies = manifest?.dependencies;
@@ -189,18 +261,28 @@ export class WorkspaceManager {
189
261
  return undefined;
190
262
  }
191
263
 
192
- for (const [alias, dep] of Object.entries(dependencies)) {
193
- if (!dep?.source) {
264
+ // NEW: Dependencies are keyed by owner/package (e.g., "domainlang/core")
265
+ // Import specifier is also owner/package, potentially with subpath
266
+ for (const [key, dep] of Object.entries(dependencies)) {
267
+ const normalized = this.normalizeDependency(key, dep);
268
+
269
+ // Skip path-based dependencies (handled by path aliases)
270
+ if (normalized.path) {
271
+ continue;
272
+ }
273
+
274
+ if (!normalized.source) {
194
275
  continue;
195
276
  }
196
277
 
197
- if (aliasPath === alias || aliasPath.startsWith(`${alias}/`)) {
198
- const suffix = aliasPath.slice(alias.length);
199
- const version = dep.version ?? '';
200
- const versionSegment = version
201
- ? (version.startsWith('@') ? version : `@${version}`)
278
+ // Match if specifier equals key or starts with key/
279
+ if (specifier === key || specifier.startsWith(`${key}/`)) {
280
+ const suffix = specifier.slice(key.length);
281
+ const ref = normalized.ref ?? '';
282
+ const refSegment = ref
283
+ ? (ref.startsWith('@') ? ref : `@${ref}`)
202
284
  : '';
203
- return `${dep.source}${versionSegment}${suffix}`;
285
+ return `${normalized.source}${refSegment}${suffix}`;
204
286
  }
205
287
  }
206
288
 
@@ -208,16 +290,15 @@ export class WorkspaceManager {
208
290
  }
209
291
 
210
292
  private async performInitialization(startPath: string): Promise<void> {
211
- this.workspaceRoot = await this.findWorkspaceRoot(startPath);
212
- if (!this.workspaceRoot) {
213
- throw new Error('Workspace root (directory with model.yaml) not found.');
214
- }
293
+ this.workspaceRoot = await this.findWorkspaceRoot(startPath) ?? path.resolve(startPath);
215
294
 
216
- this.gitResolver = new GitUrlResolver();
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);
217
298
  const loaded = await this.loadLockFileFromDisk();
218
299
  this.applyLockFile(loaded);
219
300
 
220
- if (!this.lockFile && this.options.autoResolve !== false) {
301
+ if (!this.lockFile && this.options.autoResolve !== false && this.options.allowNetwork !== false) {
221
302
  await this.generateLockFile();
222
303
  }
223
304
  }
@@ -255,8 +336,8 @@ export class WorkspaceManager {
255
336
  }
256
337
 
257
338
  const lockFile = await resolver.resolveDependencies();
258
- this.lockFile = lockFile;
259
- this.gitResolver.setLockFile(lockFile);
339
+ this.lockFile = lockFile;
340
+ this.gitResolver.setLockFile(lockFile);
260
341
 
261
342
  // Write JSON lock file
262
343
  await this.writeJsonLockFile(lockFile);
@@ -333,6 +414,10 @@ export class WorkspaceManager {
333
414
 
334
415
  const content = await fs.readFile(manifestPath, 'utf-8');
335
416
  const manifest = (YAML.parse(content) ?? {}) as ModelManifest;
417
+
418
+ // Validate manifest structure
419
+ this.validateManifest(manifest, manifestPath);
420
+
336
421
  this.manifestCache = {
337
422
  manifest,
338
423
  path: manifestPath,
@@ -348,6 +433,111 @@ export class WorkspaceManager {
348
433
  }
349
434
  }
350
435
 
436
+ /**
437
+ * Validates manifest structure and dependency configurations.
438
+ * Throws detailed errors for invalid manifests.
439
+ *
440
+ * Supports both new format (owner/package: version) and extended format.
441
+ */
442
+ private validateManifest(manifest: ModelManifest, manifestPath: string): void {
443
+ // Validate path aliases
444
+ if (manifest.paths) {
445
+ this.validatePathAliases(manifest.paths, manifestPath);
446
+ }
447
+
448
+ if (!manifest.dependencies) {
449
+ return; // No dependencies to validate
450
+ }
451
+
452
+ for (const [key, dep] of Object.entries(manifest.dependencies)) {
453
+ const normalized = this.normalizeDependency(key, dep);
454
+
455
+ // Validate mutually exclusive source and path
456
+ if (normalized.source && normalized.path) {
457
+ throw new Error(
458
+ `Invalid dependency '${key}' in ${manifestPath}:\n` +
459
+ `Cannot specify both 'source' and 'path'.\n` +
460
+ `Hint: Use 'source' for git dependencies or 'path' for local workspace dependencies.`
461
+ );
462
+ }
463
+
464
+ // For string format, source is always derived from key (valid)
465
+ // For extended format without source or path, error
466
+ if (typeof dep !== 'string' && !normalized.source && !normalized.path) {
467
+ throw new Error(
468
+ `Invalid dependency '${key}' in ${manifestPath}:\n` +
469
+ `Must specify either 'source' or 'path'.\n` +
470
+ `Hint: Add 'source: owner/repo' for git dependencies, or 'path: ./local/path' for local packages.`
471
+ );
472
+ }
473
+
474
+ // Validate path is relative and within workspace
475
+ if (normalized.path) {
476
+ this.validateLocalPath(normalized.path, key, manifestPath);
477
+ }
478
+
479
+ // Validate source has ref when specified
480
+ if (normalized.source && !normalized.ref) {
481
+ throw new Error(
482
+ `Invalid dependency '${key}' in ${manifestPath}:\n` +
483
+ `Git dependencies must specify a 'ref' (git reference).\n` +
484
+ `Hint: Add 'ref: v1.0.0' (tag), 'ref: main' (branch), or a commit SHA.`
485
+ );
486
+ }
487
+ }
488
+ }
489
+
490
+ /**
491
+ * Validates path aliases for security and correctness.
492
+ */
493
+ private validatePathAliases(paths: PathAliases, manifestPath: string): void {
494
+ for (const [alias, targetPath] of Object.entries(paths)) {
495
+ // Validate alias starts with @
496
+ if (!alias.startsWith('@')) {
497
+ throw new Error(
498
+ `Invalid path alias '${alias}' in ${manifestPath}:\n` +
499
+ `Path aliases must start with '@'.\n` +
500
+ `Hint: Rename to '@${alias}' in your model.yaml paths section.`
501
+ );
502
+ }
503
+
504
+ // Validate target path doesn't escape workspace
505
+ this.validateLocalPath(targetPath, alias, manifestPath);
506
+ }
507
+ }
508
+
509
+ /**
510
+ * Validates local path dependencies for security.
511
+ * Ensures paths don't escape workspace boundary.
512
+ */
513
+ private validateLocalPath(localPath: string, alias: string, manifestPath: string): void {
514
+ // Reject absolute paths
515
+ if (path.isAbsolute(localPath)) {
516
+ throw new Error(
517
+ `Invalid local path '${alias}' in ${manifestPath}:\n` +
518
+ `Cannot use absolute path '${localPath}'.\n` +
519
+ `Hint: Use relative paths (e.g., './lib', '../shared') for local dependencies.`
520
+ );
521
+ }
522
+
523
+ // Resolve path relative to manifest directory
524
+ const manifestDir = path.dirname(manifestPath);
525
+ const resolvedPath = path.resolve(manifestDir, localPath);
526
+ const workspaceRoot = this.workspaceRoot || manifestDir;
527
+
528
+ // Check if resolved path is within workspace
529
+ const relativePath = path.relative(workspaceRoot, resolvedPath);
530
+ if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
531
+ throw new Error(
532
+ `Invalid local path '${alias}' in ${manifestPath}:\n` +
533
+ `Path '${localPath}' resolves outside workspace boundary.\n` +
534
+ `Resolved: ${resolvedPath}\n` +
535
+ `Workspace: ${workspaceRoot}\n` +
536
+ `Hint: Local dependencies must be within the workspace. Consider moving the dependency or using a git-based source.`
537
+ );
538
+ }
539
+ }
540
+
351
541
  private parseJsonLockFile(content: string): LockFile {
352
542
  const parsed = JSON.parse(content) as Partial<LockFile> & {
353
543
  dependencies?: Record<string, Partial<LockedDependency>>;
@@ -357,11 +547,12 @@ export class WorkspaceManager {
357
547
  const dependencies: Record<string, LockedDependency> = {};
358
548
 
359
549
  for (const [key, value] of Object.entries(parsed.dependencies ?? {})) {
360
- if (!value || typeof value.version !== 'string' || typeof value.resolved !== 'string' || typeof value.commit !== 'string') {
550
+ if (!value || typeof value.ref !== 'string' || typeof value.resolved !== 'string' || typeof value.commit !== 'string') {
361
551
  continue;
362
552
  }
363
553
  dependencies[key] = {
364
- version: value.version,
554
+ ref: value.ref,
555
+ refType: value.refType ?? 'commit', // Default to commit for backwards compatibility
365
556
  resolved: value.resolved,
366
557
  commit: value.commit,
367
558
  integrity: value.integrity,
@@ -1,7 +1,7 @@
1
1
  // Monarch syntax highlighting for the domain-lang language.
2
2
  export default {
3
3
  keywords: [
4
- 'ACL','AntiCorruptionLayer','BBoM','BigBallOfMud','BoundedContext','CF','Classification','Conformist','ContextMap','CustomerSupplier','Decision','Domain','DomainMap','Import','Metadata','Namespace','OHS','OpenHostService','P','PL','Partnership','Policy','PublishedLanguage','Rule','SK','SeparateWays','SharedKernel','Team','Term','UpstreamDownstream','aka','archetype','as','bc','businessModel','by','classification','cmap','contains','decision','decisions','description','dmap','dom','evolution','examples','for','from','glossary','import','in','integrations','integrity','is','meta','metadata','ns','policy','relationships','rule','rules','synonyms','team','term','terminology','this','type','vision'
4
+ 'ACL','AntiCorruptionLayer','BBoM','BigBallOfMud','BoundedContext','CF','Classification','Conformist','ContextMap','CustomerSupplier','Decision','Domain','DomainMap','Import','Metadata','Namespace','OHS','OpenHostService','P','PL','Partnership','Policy','PublishedLanguage','Rule','SK','SeparateWays','SharedKernel','Team','Term','UpstreamDownstream','aka','archetype','as','bc','businessModel','by','classification','cmap','contains','decision','decisions','description','dmap','dom','evolution','examples','for','glossary','import','in','integrations','is','meta','metadata','ns','policy','relationships','rule','rules','synonyms','team','term','terminology','this','type','vision'
5
5
  ],
6
6
  operators: [
7
7
  ',','->','.',':','<-','<->','=','><'
@@ -1,129 +1,66 @@
1
- import fs from 'node:fs/promises';
2
1
  import path from 'node:path';
3
2
  import { URI, type LangiumDocument, type LangiumDocuments } from 'langium';
4
3
  import type { Model } from '../generated/ast.js';
5
- import { GitUrlParser } from '../services/git-url-resolver.js';
6
4
  import type { GitUrlResolver } from '../services/git-url-resolver.js';
7
5
  import { WorkspaceManager } from '../services/workspace-manager.js';
6
+ import { ImportResolver } from '../services/import-resolver.js';
7
+ import type { DomainLangServices } from '../domain-lang-module.js';
8
8
 
9
- // Singleton workspace manager instance
10
- let workspaceManager: WorkspaceManager | undefined;
9
+ /**
10
+ * Lazily initialized workspace manager for standalone (non-LSP) usage.
11
+ * Used by import graph building when services aren't available from DI.
12
+ */
13
+ let standaloneWorkspaceManager: WorkspaceManager | undefined;
14
+ let standaloneImportResolver: ImportResolver | undefined;
15
+ let lastInitializedDir: string | undefined;
11
16
 
12
17
  /**
13
- * Gets or creates the global workspace manager instance.
14
- *
18
+ * Gets or creates a standalone import resolver for non-LSP contexts.
19
+ * Creates its own WorkspaceManager if not previously initialized for this directory.
20
+ *
21
+ * NOTE: In LSP contexts, prefer using services.imports.ImportResolver directly.
22
+ * This function exists for utilities that don't have access to the service container.
23
+ *
15
24
  * @param startDir - Directory to start workspace search from
16
- * @returns Promise resolving to the workspace manager
25
+ * @returns Promise resolving to the import resolver
17
26
  */
18
- async function getWorkspaceManager(startDir: string): Promise<WorkspaceManager> {
19
- if (!workspaceManager) {
20
- workspaceManager = new WorkspaceManager();
21
- const root = await findWorkspaceRoot(startDir);
27
+ async function getStandaloneImportResolver(startDir: string): Promise<ImportResolver> {
28
+ // Re-initialize if directory changed (workspace boundary)
29
+ if (lastInitializedDir !== startDir || !standaloneImportResolver) {
30
+ standaloneWorkspaceManager = new WorkspaceManager({ autoResolve: false, allowNetwork: false });
22
31
  try {
23
- await workspaceManager.initialize(root);
32
+ await standaloneWorkspaceManager.initialize(startDir);
24
33
  } catch (error) {
25
34
  console.warn(`Failed to initialize workspace: ${error instanceof Error ? error.message : String(error)}`);
26
- // Continue without workspace - local imports will still work
27
35
  }
36
+ const services = {
37
+ imports: { WorkspaceManager: standaloneWorkspaceManager }
38
+ } as DomainLangServices;
39
+ standaloneImportResolver = new ImportResolver(services);
40
+ lastInitializedDir = startDir;
28
41
  }
29
- return workspaceManager;
42
+ return standaloneImportResolver;
30
43
  }
31
44
 
32
45
  /**
33
- * Gets the git URL resolver from the workspace manager.
34
- *
46
+ * Gets the git URL resolver from a workspace manager.
47
+ *
35
48
  * @param startDir - Directory to start workspace search from
36
49
  * @returns Promise resolving to the git URL resolver
37
50
  */
38
51
  async function getGitResolver(startDir: string): Promise<GitUrlResolver> {
39
- const manager = await getWorkspaceManager(startDir);
40
- return await manager.getGitResolver();
41
- }
42
-
43
- /**
44
- * Ensures a file path has the .dlang extension.
45
- *
46
- * @param filePath - The raw file path
47
- * @returns Normalized path with .dlang extension
48
- * @throws {Error} If path has an invalid extension
49
- */
50
- function ensureDlangExtension(filePath: string): string {
51
- const ext = path.extname(filePath);
52
-
53
- if (!ext) {
54
- return `${filePath}.dlang`;
55
- }
56
-
57
- if (ext !== '.dlang') {
58
- throw new Error(
59
- `Invalid file extension: ${ext}. Expected .dlang for file: ${filePath}`
60
- );
61
- }
62
-
63
- return filePath;
64
- }
65
-
66
- /**
67
- * Resolves workspace-relative paths (starting with ~/).
68
- *
69
- * @param importPath - Import path that may start with ~/
70
- * @param workspaceRoot - The workspace root directory
71
- * @returns Resolved absolute path
72
- */
73
- function resolveWorkspacePath(importPath: string, workspaceRoot: string): string {
74
- if (importPath.startsWith('~/')) {
75
- return path.join(workspaceRoot, importPath.slice(2));
76
- }
77
- return importPath;
78
- }
79
-
80
- /**
81
- * Finds the workspace root by looking for common workspace indicators.
82
- *
83
- * Searches upward from the document's directory looking for:
84
- * - dlang.toml
85
- * - .git directory
86
- * - package.json with dlang configuration
87
- *
88
- * @param startDir - Directory to start searching from
89
- * @returns Workspace root path or the start directory if not found
90
- */
91
- async function findWorkspaceRoot(startDir: string): Promise<string> {
92
- let currentDir = startDir;
93
- const root = path.parse(currentDir).root;
94
-
95
- while (currentDir !== root) {
96
- // Check for workspace indicators
97
- const indicators = ['dlang.toml', '.git', 'package.json'];
98
-
99
- for (const indicator of indicators) {
100
- const indicatorPath = path.join(currentDir, indicator);
101
- try {
102
- await fs.access(indicatorPath);
103
- return currentDir; // Found workspace root
104
- } catch {
105
- // Continue searching
106
- }
107
- }
108
-
109
- // Move up one directory
110
- const parentDir = path.dirname(currentDir);
111
- if (parentDir === currentDir) break; // Reached root
112
- currentDir = parentDir;
113
- }
114
-
115
- // Default to start directory if no workspace root found
116
- return startDir;
52
+ const workspaceManager = new WorkspaceManager({ autoResolve: false, allowNetwork: false });
53
+ await workspaceManager.initialize(startDir);
54
+ return workspaceManager.getGitResolver();
117
55
  }
118
56
 
119
57
  /**
120
58
  * Resolves an import path to an absolute file URI.
121
59
  *
122
- * Supports:
123
- * - Local relative paths: ./file.dlang, ../other/file.dlang
124
- * - Workspace-relative paths: ~/contexts/sales.dlang
125
- * - Git URLs: gh:owner/repo@v1.0.0/file.dlang
126
- * - Full URLs: https://github.com/owner/repo/blob/v1.0.0/file.dlang
60
+ * Delegates to ImportResolver which implements PRS-010 semantics:
61
+ * - File imports (with .dlang extension): Direct file access
62
+ * - Module imports (no extension): Requires model.yaml in directory
63
+ * - External dependencies: Resolved via manifest and lock file
127
64
  *
128
65
  * @param importingDoc - The document containing the import statement
129
66
  * @param rawImportPath - The raw import path from the import statement
@@ -135,65 +72,8 @@ export async function resolveImportPath(
135
72
  rawImportPath: string
136
73
  ): Promise<URI> {
137
74
  const baseDir = path.dirname(importingDoc.uri.fsPath);
138
-
139
- // Handle manifest dependency aliases (friendly names)
140
- const manager = await getWorkspaceManager(baseDir);
141
- let gitResolver: GitUrlResolver | undefined;
142
-
143
- try {
144
- const manifestImport = await manager.resolveDependencyImport(rawImportPath);
145
- if (manifestImport) {
146
- gitResolver = await manager.getGitResolver();
147
- return await gitResolver.resolve(manifestImport);
148
- }
149
- } catch {
150
- // Ignore manifest resolution issues; fall back to other strategies
151
- }
152
-
153
- // Handle git URLs (shorthand or full)
154
- if (GitUrlParser.isGitUrl(rawImportPath)) {
155
- gitResolver = gitResolver ?? await manager.getGitResolver();
156
- return await gitResolver.resolve(rawImportPath);
157
- }
158
-
159
- // Handle workspace-relative paths (~/)
160
- let resolvedPath = rawImportPath;
161
-
162
- if (rawImportPath.startsWith('~/')) {
163
- const workspaceRoot = await findWorkspaceRoot(baseDir);
164
- resolvedPath = resolveWorkspacePath(rawImportPath, workspaceRoot);
165
- } else if (!path.isAbsolute(rawImportPath)) {
166
- // Handle relative paths
167
- resolvedPath = path.resolve(baseDir, rawImportPath);
168
- }
169
-
170
- // Ensure .dlang extension
171
- const normalized = ensureDlangExtension(resolvedPath);
172
-
173
- // Verify file exists
174
- try {
175
- await fs.access(normalized);
176
- } catch {
177
- throw new Error(
178
- `Import file not found: ${rawImportPath} (resolved to ${normalized})`
179
- );
180
- }
181
-
182
- return URI.file(normalized);
183
- }
184
-
185
- /**
186
- * Legacy function for backward compatibility.
187
- * Use resolveImportPath instead.
188
- *
189
- * @deprecated Use resolveImportPath which supports git URLs and workspace paths
190
- */
191
- export async function resolveLocalImportPath(
192
- importingDoc: LangiumDocument,
193
- rawImportPath: string
194
- ): Promise<string> {
195
- const uri = await resolveImportPath(importingDoc, rawImportPath);
196
- return uri.fsPath;
75
+ const resolver = await getStandaloneImportResolver(baseDir);
76
+ return resolver.resolveFrom(baseDir, rawImportPath);
197
77
  }
198
78
 
199
79
  /**
@@ -270,5 +150,3 @@ export async function clearGitCache(startDir: string = process.cwd()): Promise<v
270
150
  const resolver = await getGitResolver(startDir);
271
151
  return await resolver.clearCache();
272
152
  }
273
-
274
-