@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.
- package/README.md +1 -1
- package/out/domain-lang-module.js +5 -1
- package/out/domain-lang-module.js.map +1 -1
- package/out/generated/ast.d.ts +24 -0
- package/out/generated/ast.js.map +1 -1
- package/out/generated/grammar.js +22 -32
- package/out/generated/grammar.js.map +1 -1
- package/out/index.d.ts +2 -5
- package/out/index.js +10 -6
- package/out/index.js.map +1 -1
- package/out/lsp/domain-lang-code-actions.js +14 -8
- package/out/lsp/domain-lang-code-actions.js.map +1 -1
- package/out/lsp/domain-lang-completion.d.ts +3 -0
- package/out/lsp/domain-lang-completion.js +41 -13
- package/out/lsp/domain-lang-completion.js.map +1 -1
- package/out/lsp/domain-lang-formatter.js +24 -18
- package/out/lsp/domain-lang-formatter.js.map +1 -1
- package/out/lsp/domain-lang-index-manager.d.ts +170 -0
- package/out/lsp/domain-lang-index-manager.js +389 -0
- package/out/lsp/domain-lang-index-manager.js.map +1 -0
- package/out/lsp/domain-lang-scope-provider.d.ts +67 -0
- package/out/lsp/domain-lang-scope-provider.js +95 -0
- package/out/lsp/domain-lang-scope-provider.js.map +1 -0
- package/out/lsp/domain-lang-scope.js +31 -17
- package/out/lsp/domain-lang-scope.js.map +1 -1
- package/out/lsp/domain-lang-workspace-manager.d.ts +76 -9
- package/out/lsp/domain-lang-workspace-manager.js +176 -54
- package/out/lsp/domain-lang-workspace-manager.js.map +1 -1
- package/out/lsp/hover/domain-lang-hover.d.ts +45 -1
- package/out/lsp/hover/domain-lang-hover.js +308 -232
- package/out/lsp/hover/domain-lang-hover.js.map +1 -1
- package/out/lsp/hover/domain-lang-keywords.d.ts +3 -7
- package/out/lsp/hover/domain-lang-keywords.js +115 -38
- package/out/lsp/hover/domain-lang-keywords.js.map +1 -1
- package/out/lsp/manifest-diagnostics.js +95 -50
- package/out/lsp/manifest-diagnostics.js.map +1 -1
- package/out/main.js +204 -17
- package/out/main.js.map +1 -1
- package/out/services/import-resolver.d.ts +39 -2
- package/out/services/import-resolver.js +77 -12
- package/out/services/import-resolver.js.map +1 -1
- package/out/services/types.d.ts +2 -2
- package/out/services/workspace-manager.d.ts +33 -31
- package/out/services/workspace-manager.js +92 -148
- package/out/services/workspace-manager.js.map +1 -1
- package/out/utils/document-utils.d.ts +41 -0
- package/out/utils/document-utils.js +64 -0
- package/out/utils/document-utils.js.map +1 -0
- package/out/utils/import-utils.d.ts +0 -17
- package/out/utils/import-utils.js +2 -32
- package/out/utils/import-utils.js.map +1 -1
- package/out/utils/manifest-utils.d.ts +56 -0
- package/out/utils/manifest-utils.js +119 -0
- package/out/utils/manifest-utils.js.map +1 -0
- package/out/validation/constants.d.ts +13 -0
- package/out/validation/constants.js +18 -0
- package/out/validation/constants.js.map +1 -1
- package/out/validation/import.d.ts +12 -2
- package/out/validation/import.js +95 -22
- package/out/validation/import.js.map +1 -1
- package/out/validation/maps.js +51 -2
- package/out/validation/maps.js.map +1 -1
- package/package.json +1 -1
- package/src/domain-lang-module.ts +6 -1
- package/src/domain-lang.langium +37 -13
- package/src/generated/ast.ts +24 -0
- package/src/generated/grammar.ts +22 -32
- package/src/index.ts +12 -6
- package/src/lsp/domain-lang-code-actions.ts +13 -8
- package/src/lsp/domain-lang-completion.ts +61 -13
- package/src/lsp/domain-lang-formatter.ts +28 -23
- package/src/lsp/domain-lang-index-manager.ts +447 -0
- package/src/lsp/domain-lang-scope-provider.ts +134 -0
- package/src/lsp/domain-lang-scope.ts +29 -17
- package/src/lsp/domain-lang-workspace-manager.ts +201 -53
- package/src/lsp/hover/domain-lang-hover.ts +332 -226
- package/src/lsp/hover/domain-lang-keywords.ts +129 -43
- package/src/lsp/manifest-diagnostics.ts +100 -59
- package/src/main.ts +258 -16
- package/src/services/import-resolver.ts +91 -12
- package/src/services/types.ts +2 -2
- package/src/services/workspace-manager.ts +101 -175
- package/src/utils/document-utils.ts +80 -0
- package/src/utils/import-utils.ts +2 -40
- package/src/utils/manifest-utils.ts +132 -0
- package/src/validation/constants.ts +24 -0
- package/src/validation/import.ts +107 -24
- package/src/validation/maps.ts +59 -2
- package/out/lsp/hover/ddd-pattern-explanations.d.ts +0 -50
- package/out/lsp/hover/ddd-pattern-explanations.js +0 -196
- package/out/lsp/hover/ddd-pattern-explanations.js.map +0 -1
- package/out/services/dependency-analyzer.d.ts +0 -58
- package/out/services/dependency-analyzer.js +0 -254
- package/out/services/dependency-analyzer.js.map +0 -1
- package/out/services/dependency-resolver.d.ts +0 -146
- package/out/services/dependency-resolver.js +0 -452
- package/out/services/dependency-resolver.js.map +0 -1
- package/out/services/git-url-resolver.browser.d.ts +0 -10
- package/out/services/git-url-resolver.browser.js +0 -19
- package/out/services/git-url-resolver.browser.js.map +0 -1
- package/out/services/git-url-resolver.d.ts +0 -158
- package/out/services/git-url-resolver.js +0 -416
- package/out/services/git-url-resolver.js.map +0 -1
- package/out/services/governance-validator.d.ts +0 -44
- package/out/services/governance-validator.js +0 -153
- package/out/services/governance-validator.js.map +0 -1
- package/out/services/semver.d.ts +0 -98
- package/out/services/semver.js +0 -195
- package/out/services/semver.js.map +0 -1
- package/src/lsp/hover/ddd-pattern-explanations.ts +0 -237
- package/src/services/dependency-analyzer.ts +0 -321
- package/src/services/dependency-resolver.ts +0 -551
- package/src/services/git-url-resolver.browser.ts +0 -26
- package/src/services/git-url-resolver.ts +0 -517
- package/src/services/governance-validator.ts +0 -177
- package/src/services/semver.ts +0 -213
package/src/main.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { startLanguageServer } from 'langium/lsp';
|
|
2
2
|
import { NodeFileSystem } from 'langium/node';
|
|
3
|
-
import { createConnection, ProposedFeatures } from 'vscode-languageserver/node.js';
|
|
3
|
+
import { createConnection, ProposedFeatures, FileChangeType } from 'vscode-languageserver/node.js';
|
|
4
4
|
import { createDomainLangServices } from './domain-lang-module.js';
|
|
5
5
|
import { ensureImportGraphFromEntryFile } from './utils/import-utils.js';
|
|
6
|
+
import { DomainLangIndexManager } from './lsp/domain-lang-index-manager.js';
|
|
6
7
|
import { URI } from 'langium';
|
|
7
8
|
|
|
8
9
|
// Create a connection to the client
|
|
@@ -11,29 +12,269 @@ const connection = createConnection(ProposedFeatures.all);
|
|
|
11
12
|
// Inject the shared services and language-specific services
|
|
12
13
|
const { shared, DomainLang } = createDomainLangServices({ connection, ...NodeFileSystem });
|
|
13
14
|
|
|
14
|
-
// Initialize workspace
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
// Initialize workspace manager when language server initializes
|
|
16
|
+
// Uses Langium's LanguageServer.onInitialize hook (not raw connection handler)
|
|
17
|
+
// This integrates properly with Langium's initialization flow
|
|
18
|
+
shared.lsp.LanguageServer.onInitialize((params) => {
|
|
19
|
+
// Use workspaceFolders (preferred) over deprecated rootUri
|
|
20
|
+
const folders = params.workspaceFolders;
|
|
21
|
+
const workspaceRoot = folders?.[0]?.uri
|
|
22
|
+
? URI.parse(folders[0].uri).fsPath
|
|
23
|
+
: undefined;
|
|
17
24
|
|
|
18
25
|
if (workspaceRoot) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
console.warn(`DomainLang workspace initialized: ${workspaceRoot}`);
|
|
24
|
-
} catch (error) {
|
|
26
|
+
// Initialize workspace manager synchronously (just sets root path)
|
|
27
|
+
// Heavy work happens in initializeWorkspace() called by Langium later
|
|
28
|
+
const workspaceManager = DomainLang.imports.WorkspaceManager;
|
|
29
|
+
workspaceManager.initialize(workspaceRoot).catch(error => {
|
|
25
30
|
const message = error instanceof Error ? error.message : String(error);
|
|
26
31
|
console.warn(`Failed to initialize workspace: ${message}`);
|
|
27
32
|
// Continue without workspace - local imports will still work
|
|
33
|
+
});
|
|
34
|
+
console.warn(`DomainLang workspace root: ${workspaceRoot}`);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Handle file changes for model.yaml and model.lock (PRS-010)
|
|
39
|
+
// Uses Langium's built-in file watcher which already watches **/* in workspace
|
|
40
|
+
// This invalidates caches when config files change externally
|
|
41
|
+
shared.lsp.DocumentUpdateHandler?.onWatchedFilesChange(async (params) => {
|
|
42
|
+
try {
|
|
43
|
+
await handleFileChanges(params, DomainLang.imports.WorkspaceManager, shared, DomainLang);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
46
|
+
console.error(`Error handling file change notification: ${message}`);
|
|
47
|
+
// Continue - don't crash the server
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
/** Categorized file changes */
|
|
52
|
+
interface CategorizedChanges {
|
|
53
|
+
manifestChanged: boolean;
|
|
54
|
+
lockFileChanged: boolean;
|
|
55
|
+
changedDlangUris: Set<string>;
|
|
56
|
+
deletedDlangUris: Set<string>;
|
|
57
|
+
createdDlangUris: Set<string>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Categorizes file changes by type.
|
|
62
|
+
*/
|
|
63
|
+
function categorizeChanges(
|
|
64
|
+
params: { changes: Array<{ uri: string; type: number }> },
|
|
65
|
+
workspaceManager: typeof DomainLang.imports.WorkspaceManager,
|
|
66
|
+
langServices: typeof DomainLang,
|
|
67
|
+
indexManager: DomainLangIndexManager
|
|
68
|
+
): CategorizedChanges {
|
|
69
|
+
const result: CategorizedChanges = {
|
|
70
|
+
manifestChanged: false,
|
|
71
|
+
lockFileChanged: false,
|
|
72
|
+
changedDlangUris: new Set(),
|
|
73
|
+
deletedDlangUris: new Set(),
|
|
74
|
+
createdDlangUris: new Set()
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
for (const change of params.changes) {
|
|
78
|
+
const uri = URI.parse(change.uri);
|
|
79
|
+
const fileName = uri.path.split('/').pop() ?? '';
|
|
80
|
+
const uriString = change.uri;
|
|
81
|
+
|
|
82
|
+
if (fileName === 'model.yaml') {
|
|
83
|
+
console.warn(`model.yaml changed: ${uriString}`);
|
|
84
|
+
workspaceManager.invalidateManifestCache();
|
|
85
|
+
langServices.imports.ImportResolver.clearCache();
|
|
86
|
+
indexManager.clearImportDependencies();
|
|
87
|
+
result.manifestChanged = true;
|
|
88
|
+
} else if (fileName === 'model.lock') {
|
|
89
|
+
console.warn(`model.lock changed: ${uriString}`);
|
|
90
|
+
langServices.imports.ImportResolver.clearCache();
|
|
91
|
+
result.lockFileChanged = true;
|
|
92
|
+
} else if (fileName.endsWith('.dlang')) {
|
|
93
|
+
if (change.type === FileChangeType.Deleted) {
|
|
94
|
+
result.deletedDlangUris.add(uriString);
|
|
95
|
+
console.warn(`DomainLang file deleted: ${uriString}`);
|
|
96
|
+
} else if (change.type === FileChangeType.Created) {
|
|
97
|
+
result.createdDlangUris.add(uriString);
|
|
98
|
+
console.warn(`DomainLang file created: ${uriString}`);
|
|
99
|
+
} else {
|
|
100
|
+
result.changedDlangUris.add(uriString);
|
|
101
|
+
console.warn(`DomainLang file changed: ${uriString}`);
|
|
102
|
+
}
|
|
28
103
|
}
|
|
29
104
|
}
|
|
105
|
+
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Rebuilds documents that depend on changed/deleted/created .dlang files.
|
|
111
|
+
*/
|
|
112
|
+
async function rebuildAffectedDocuments(
|
|
113
|
+
changes: CategorizedChanges,
|
|
114
|
+
indexManager: DomainLangIndexManager,
|
|
115
|
+
sharedServices: typeof shared,
|
|
116
|
+
langServices: typeof DomainLang
|
|
117
|
+
): Promise<void> {
|
|
118
|
+
const hasChanges = changes.changedDlangUris.size > 0 ||
|
|
119
|
+
changes.deletedDlangUris.size > 0 ||
|
|
120
|
+
changes.createdDlangUris.size > 0;
|
|
121
|
+
if (!hasChanges) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// CRITICAL: Clear ImportResolver cache BEFORE rebuilding.
|
|
126
|
+
// The WorkspaceCache only clears AFTER linking, but resolution happens
|
|
127
|
+
// DURING linking. Without this, stale cached resolutions would be used.
|
|
128
|
+
langServices.imports.ImportResolver.clearCache();
|
|
129
|
+
|
|
130
|
+
const affectedUris = collectAffectedDocuments(changes, indexManager);
|
|
131
|
+
|
|
132
|
+
if (affectedUris.size === 0) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
console.warn(`Rebuilding ${affectedUris.size} documents affected by file changes`);
|
|
137
|
+
|
|
138
|
+
const langiumDocuments = sharedServices.workspace.LangiumDocuments;
|
|
139
|
+
const affectedDocs: URI[] = [];
|
|
140
|
+
|
|
141
|
+
for (const uriString of affectedUris) {
|
|
142
|
+
const uri = URI.parse(uriString);
|
|
143
|
+
if (langiumDocuments.hasDocument(uri)) {
|
|
144
|
+
affectedDocs.push(uri);
|
|
145
|
+
indexManager.markForReprocessing(uriString);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const deletedUriObjects = [...changes.deletedDlangUris].map(u => URI.parse(u));
|
|
150
|
+
if (affectedDocs.length > 0 || deletedUriObjects.length > 0) {
|
|
151
|
+
await sharedServices.workspace.DocumentBuilder.update(affectedDocs, deletedUriObjects);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Collects all document URIs that should be rebuilt based on the changes.
|
|
157
|
+
*
|
|
158
|
+
* Uses targeted matching to avoid expensive full rebuilds:
|
|
159
|
+
* - For edits: rebuild documents that import the changed file (by resolved URI)
|
|
160
|
+
* - For all changes: rebuild documents whose import specifiers match the path
|
|
161
|
+
*
|
|
162
|
+
* The specifier matching handles renamed/moved/created files by comparing
|
|
163
|
+
* import specifiers against path segments (filename, parent/filename, etc.).
|
|
164
|
+
*/
|
|
165
|
+
function collectAffectedDocuments(
|
|
166
|
+
changes: CategorizedChanges,
|
|
167
|
+
indexManager: DomainLangIndexManager
|
|
168
|
+
): Set<string> {
|
|
169
|
+
const allChangedUris = new Set([
|
|
170
|
+
...changes.changedDlangUris,
|
|
171
|
+
...changes.deletedDlangUris,
|
|
172
|
+
...changes.createdDlangUris
|
|
173
|
+
]);
|
|
174
|
+
|
|
175
|
+
// Get documents affected by resolved URI changes (edits to imported files)
|
|
176
|
+
const affectedByUri = indexManager.getAllAffectedDocuments(allChangedUris);
|
|
30
177
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
178
|
+
// Get documents with import specifiers that match changed paths
|
|
179
|
+
// This catches:
|
|
180
|
+
// - File moves/renames: specifiers that previously resolved but now won't
|
|
181
|
+
// - File creations: specifiers that previously failed but might now resolve
|
|
182
|
+
// Uses fuzzy matching on path segments rather than rebuilding all imports
|
|
183
|
+
const affectedBySpecifier = indexManager.getDocumentsWithPotentiallyAffectedImports(allChangedUris);
|
|
184
|
+
|
|
185
|
+
return new Set([...affectedByUri, ...affectedBySpecifier]);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Handles all file changes including .dlang files, model.yaml, and model.lock.
|
|
190
|
+
*
|
|
191
|
+
* For .dlang files: rebuilds all documents that import the changed file.
|
|
192
|
+
* For config files: invalidates caches and rebuilds workspace as needed.
|
|
193
|
+
*/
|
|
194
|
+
async function handleFileChanges(
|
|
195
|
+
params: { changes: Array<{ uri: string; type: number }> },
|
|
196
|
+
workspaceManager: typeof DomainLang.imports.WorkspaceManager,
|
|
197
|
+
sharedServices: typeof shared,
|
|
198
|
+
langServices: typeof DomainLang
|
|
199
|
+
): Promise<void> {
|
|
200
|
+
const indexManager = sharedServices.workspace.IndexManager as DomainLangIndexManager;
|
|
201
|
+
|
|
202
|
+
// Categorize and process changes
|
|
203
|
+
const changes = categorizeChanges(params, workspaceManager, langServices, indexManager);
|
|
204
|
+
|
|
205
|
+
// Handle lock file changes
|
|
206
|
+
if (changes.lockFileChanged) {
|
|
207
|
+
const lockChange = params.changes.find(c => c.uri.endsWith('model.lock'));
|
|
208
|
+
if (lockChange) {
|
|
209
|
+
await handleLockFileChange(lockChange, workspaceManager);
|
|
34
210
|
}
|
|
35
|
-
}
|
|
36
|
-
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Rebuild documents affected by .dlang file changes
|
|
214
|
+
await rebuildAffectedDocuments(changes, indexManager, sharedServices, langServices);
|
|
215
|
+
|
|
216
|
+
// Handle config file changes
|
|
217
|
+
if (changes.manifestChanged || changes.lockFileChanged) {
|
|
218
|
+
await rebuildWorkspace(sharedServices, workspaceManager, changes.manifestChanged);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Handles lock file creation, change, or deletion.
|
|
224
|
+
*/
|
|
225
|
+
async function handleLockFileChange(
|
|
226
|
+
change: { uri: string; type: number },
|
|
227
|
+
workspaceManager: typeof DomainLang.imports.WorkspaceManager
|
|
228
|
+
): Promise<void> {
|
|
229
|
+
if (change.type === FileChangeType.Changed || change.type === FileChangeType.Created) {
|
|
230
|
+
await workspaceManager.refreshLockFile();
|
|
231
|
+
} else if (change.type === FileChangeType.Deleted) {
|
|
232
|
+
workspaceManager.invalidateLockCache();
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Rebuilds the workspace after config file changes.
|
|
238
|
+
* Uses incremental strategy: only full rebuild if dependencies changed.
|
|
239
|
+
*
|
|
240
|
+
* @param sharedServices - Shared Langium services
|
|
241
|
+
* @param workspaceManager - Workspace manager for manifest access
|
|
242
|
+
* @param manifestChanged - Whether model.yaml changed (vs just model.lock)
|
|
243
|
+
*/
|
|
244
|
+
async function rebuildWorkspace(
|
|
245
|
+
sharedServices: typeof shared,
|
|
246
|
+
workspaceManager: typeof DomainLang.imports.WorkspaceManager,
|
|
247
|
+
manifestChanged: boolean
|
|
248
|
+
): Promise<void> {
|
|
249
|
+
try {
|
|
250
|
+
// If only lock file changed, caches are already invalidated - no rebuild needed
|
|
251
|
+
// Lock file changes mean resolved versions changed, but import resolver cache is cleared
|
|
252
|
+
// Documents will re-resolve imports on next access
|
|
253
|
+
if (!manifestChanged) {
|
|
254
|
+
console.warn('Lock file changed - caches invalidated, no rebuild needed');
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// For manifest changes, check if dependencies section actually changed
|
|
259
|
+
// If only metadata changed (name, version, etc.), no rebuild needed
|
|
260
|
+
const manifest = await workspaceManager.getManifest();
|
|
261
|
+
const hasDependencies = manifest?.dependencies && Object.keys(manifest.dependencies).length > 0;
|
|
262
|
+
|
|
263
|
+
if (!hasDependencies) {
|
|
264
|
+
console.warn('Manifest changed but has no dependencies - skipping rebuild');
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Dependencies exist and manifest changed - do full rebuild
|
|
269
|
+
const documents = sharedServices.workspace.LangiumDocuments.all.toArray();
|
|
270
|
+
const uris = documents.map(doc => doc.uri);
|
|
271
|
+
await sharedServices.workspace.DocumentBuilder.update([], uris);
|
|
272
|
+
console.warn(`Workspace rebuilt: ${documents.length} documents revalidated`);
|
|
273
|
+
} catch (error) {
|
|
274
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
275
|
+
console.error(`Failed to rebuild workspace: ${message}`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
37
278
|
|
|
38
279
|
// Optionally start from a single entry file and follow imports.
|
|
39
280
|
// Configure via env DOMAINLANG_ENTRY (absolute or workspace-relative path)
|
|
@@ -63,7 +304,8 @@ if (entryFile) {
|
|
|
63
304
|
};
|
|
64
305
|
|
|
65
306
|
// Initial load from entry file, then start the server
|
|
66
|
-
reloadFromEntry()
|
|
307
|
+
await reloadFromEntry();
|
|
308
|
+
startLanguageServer(shared);
|
|
67
309
|
|
|
68
310
|
// Any change within the loaded graph should trigger a reload from the entry
|
|
69
311
|
shared.workspace.TextDocuments.onDidChangeContent(async (event) => {
|
|
@@ -1,35 +1,110 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { URI, type LangiumDocument } from 'langium';
|
|
3
|
+
import { DocumentState, SimpleCache, WorkspaceCache, URI, type LangiumDocument, type LangiumSharedCoreServices } from 'langium';
|
|
4
4
|
import { WorkspaceManager } from './workspace-manager.js';
|
|
5
5
|
import type { DomainLangServices } from '../domain-lang-module.js';
|
|
6
6
|
import type { LockFile } from './types.js';
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Cache interface for import resolution.
|
|
10
|
+
* Uses WorkspaceCache in LSP mode (clears on ANY document change) or SimpleCache in standalone mode.
|
|
11
|
+
*/
|
|
12
|
+
type ResolverCache = WorkspaceCache<string, URI> | SimpleCache<string, URI>;
|
|
13
|
+
|
|
8
14
|
/**
|
|
9
15
|
* ImportResolver resolves import statements using manifest-centric rules (PRS-010).
|
|
10
16
|
*
|
|
11
17
|
* Import Types (PRS-010):
|
|
12
18
|
* - Local relative: ./path, ../path → Directory-first resolution
|
|
13
19
|
* - Path aliases: @/path, @alias/path → Configurable in model.yaml paths section
|
|
14
|
-
* - External: owner/package
|
|
20
|
+
* - External: dependency key → Manifest dependencies (key can be owner/package or an alias that maps to source)
|
|
15
21
|
*
|
|
16
22
|
* Directory-First Resolution:
|
|
17
23
|
* - ./types → ./types/index.dlang → ./types.dlang
|
|
18
24
|
* - Module entry defaults to index.dlang (no model.yaml required)
|
|
25
|
+
*
|
|
26
|
+
* Caching Strategy (uses Langium standard infrastructure):
|
|
27
|
+
* - LSP mode: Uses `WorkspaceCache` - clears on ANY document change in workspace
|
|
28
|
+
* This is necessary because file moves/deletes affect resolution of OTHER documents
|
|
29
|
+
* - Standalone mode: Uses `SimpleCache` - manual invalidation via clearCache()
|
|
30
|
+
*
|
|
31
|
+
* Why WorkspaceCache (not DocumentCache)?
|
|
32
|
+
* - DocumentCache only invalidates when the KEYED document changes
|
|
33
|
+
* - But import resolution can break when IMPORTED files are moved/deleted
|
|
34
|
+
* - Example: index.dlang imports @domains → domains/index.dlang
|
|
35
|
+
* If domains/index.dlang is moved, index.dlang's cache entry must be cleared
|
|
36
|
+
* DocumentCache wouldn't clear it (index.dlang didn't change)
|
|
37
|
+
* WorkspaceCache clears on ANY change, ensuring correct re-resolution
|
|
38
|
+
*
|
|
39
|
+
* @see https://langium.org/docs/recipes/caching/ for Langium caching patterns
|
|
19
40
|
*/
|
|
20
41
|
export class ImportResolver {
|
|
21
42
|
private readonly workspaceManager: WorkspaceManager;
|
|
43
|
+
/**
|
|
44
|
+
* Workspace-level cache for resolved import URIs.
|
|
45
|
+
* In LSP mode: WorkspaceCache - clears when ANY document changes (correct for imports)
|
|
46
|
+
* In standalone mode: SimpleCache - manual invalidation via clearCache()
|
|
47
|
+
*/
|
|
48
|
+
private readonly resolverCache: ResolverCache;
|
|
22
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Creates an ImportResolver.
|
|
52
|
+
*
|
|
53
|
+
* @param services - DomainLang services. If `services.shared` is present, uses WorkspaceCache
|
|
54
|
+
* for automatic invalidation. Otherwise uses SimpleCache for standalone mode.
|
|
55
|
+
*/
|
|
23
56
|
constructor(services: DomainLangServices) {
|
|
24
57
|
this.workspaceManager = services.imports.WorkspaceManager;
|
|
58
|
+
|
|
59
|
+
// Use Langium's WorkspaceCache when shared services are available (LSP mode)
|
|
60
|
+
// Fall back to SimpleCache for standalone utilities (SDK, CLI)
|
|
61
|
+
const shared = (services as DomainLangServices & { shared?: LangiumSharedCoreServices }).shared;
|
|
62
|
+
if (shared) {
|
|
63
|
+
// LSP mode: WorkspaceCache with DocumentState.Linked
|
|
64
|
+
//
|
|
65
|
+
// This follows the standard pattern used by TypeScript, rust-analyzer, gopls:
|
|
66
|
+
// - Cache is valid for a "workspace snapshot"
|
|
67
|
+
// - Invalidates after a batch of changes completes linking (debounced ~300ms)
|
|
68
|
+
// - Invalidates immediately on file deletion
|
|
69
|
+
// - Does NOT invalidate during typing (would be too expensive)
|
|
70
|
+
//
|
|
71
|
+
// DocumentState.Linked is the right phase because:
|
|
72
|
+
// - Import resolution is needed during linking
|
|
73
|
+
// - By the time linking completes, we know which files exist
|
|
74
|
+
// - File renames appear as delete+create, triggering immediate invalidation
|
|
75
|
+
this.resolverCache = new WorkspaceCache(shared, DocumentState.Linked);
|
|
76
|
+
} else {
|
|
77
|
+
// Standalone mode: simple key-value cache, manual invalidation
|
|
78
|
+
this.resolverCache = new SimpleCache<string, URI>();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Clears the entire import resolution cache.
|
|
84
|
+
* In LSP mode, this is also triggered automatically by WorkspaceCache on any document change.
|
|
85
|
+
* Call explicitly when model.yaml or model.lock changes.
|
|
86
|
+
*/
|
|
87
|
+
clearCache(): void {
|
|
88
|
+
this.resolverCache.clear();
|
|
25
89
|
}
|
|
26
90
|
|
|
27
91
|
/**
|
|
28
92
|
* Resolve an import specifier relative to a Langium document.
|
|
93
|
+
* Results are cached using WorkspaceCache (clears on any workspace change).
|
|
29
94
|
*/
|
|
30
95
|
async resolveForDocument(document: LangiumDocument, specifier: string): Promise<URI> {
|
|
96
|
+
// Cache key combines document URI + specifier for uniqueness
|
|
97
|
+
const cacheKey = `${document.uri.toString()}|${specifier}`;
|
|
98
|
+
const cached = this.resolverCache.get(cacheKey);
|
|
99
|
+
if (cached) {
|
|
100
|
+
return cached;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Resolve and cache
|
|
31
104
|
const baseDir = path.dirname(document.uri.fsPath);
|
|
32
|
-
|
|
105
|
+
const result = await this.resolveFrom(baseDir, specifier);
|
|
106
|
+
this.resolverCache.set(cacheKey, result);
|
|
107
|
+
return result;
|
|
33
108
|
}
|
|
34
109
|
|
|
35
110
|
/**
|
|
@@ -121,7 +196,10 @@ export class ImportResolver {
|
|
|
121
196
|
/**
|
|
122
197
|
* Resolves an external dependency via manifest.
|
|
123
198
|
*
|
|
124
|
-
|
|
199
|
+
* Import specifier is a dependency key from model.yaml.
|
|
200
|
+
* - Recommended: key is owner/package.
|
|
201
|
+
* - Optional: key is an alias with an explicit source.
|
|
202
|
+
* The LSP only resolves to cached packages - no network calls.
|
|
125
203
|
*/
|
|
126
204
|
private async resolveExternalDependency(specifier: string): Promise<URI> {
|
|
127
205
|
const manifest = await this.workspaceManager.getManifest();
|
|
@@ -143,19 +221,20 @@ export class ImportResolver {
|
|
|
143
221
|
);
|
|
144
222
|
}
|
|
145
223
|
|
|
146
|
-
|
|
147
|
-
|
|
224
|
+
// Use WorkspaceManager to resolve from cache (read-only, no network)
|
|
225
|
+
const resolved = await this.workspaceManager.resolveDependencyPath(specifier);
|
|
226
|
+
if (!resolved) {
|
|
148
227
|
throw new Error(
|
|
149
|
-
`Dependency '${specifier}' not found in model.yaml.\n` +
|
|
228
|
+
`Dependency '${specifier}' not found in model.yaml or not installed.\n` +
|
|
150
229
|
`Hint: Add it to your dependencies:\n` +
|
|
151
230
|
` dependencies:\n` +
|
|
152
231
|
` ${specifier}:\n` +
|
|
153
|
-
` ref: v1.0.0`
|
|
232
|
+
` ref: v1.0.0\n` +
|
|
233
|
+
`Then run 'dlang install' to fetch it.`
|
|
154
234
|
);
|
|
155
235
|
}
|
|
156
236
|
|
|
157
|
-
|
|
158
|
-
return git.resolve(mapped, { allowNetwork: false });
|
|
237
|
+
return URI.file(resolved);
|
|
159
238
|
}
|
|
160
239
|
|
|
161
240
|
/**
|
|
@@ -284,8 +363,8 @@ async function assertFileExists(filePath: string, original: string): Promise<voi
|
|
|
284
363
|
await fs.access(filePath);
|
|
285
364
|
} catch {
|
|
286
365
|
throw new Error(
|
|
287
|
-
`Import file not found: '${original}'
|
|
288
|
-
`Resolved path: ${filePath}
|
|
366
|
+
`Import file not found: '${original}'.\n` +
|
|
367
|
+
`Resolved path: ${filePath}\n` +
|
|
289
368
|
`Hint: Check that the file exists and the path is correct.`
|
|
290
369
|
);
|
|
291
370
|
}
|
package/src/services/types.ts
CHANGED
|
@@ -187,8 +187,8 @@ export interface LockedDependency {
|
|
|
187
187
|
* @example
|
|
188
188
|
* ```yaml
|
|
189
189
|
* paths:
|
|
190
|
-
* "
|
|
191
|
-
* "@shared
|
|
190
|
+
* "@": "./src"
|
|
191
|
+
* "@shared": "./libs/shared"
|
|
192
192
|
* ```
|
|
193
193
|
*/
|
|
194
194
|
export type PathAliases = Readonly<Record<string, string>>;
|