@domainlang/language 0.9.0 → 0.11.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 +44 -102
- package/out/domain-lang-module.d.ts +2 -2
- package/out/domain-lang-module.js +2 -2
- package/out/domain-lang-module.js.map +1 -1
- package/out/index.d.ts +4 -0
- package/out/index.js +4 -0
- package/out/index.js.map +1 -1
- package/out/lsp/domain-lang-completion.js +1 -1
- package/out/lsp/domain-lang-completion.js.map +1 -1
- package/out/lsp/domain-lang-index-manager.d.ts +149 -5
- package/out/lsp/domain-lang-index-manager.js +388 -52
- package/out/lsp/domain-lang-index-manager.js.map +1 -1
- package/out/lsp/domain-lang-refresh.d.ts +35 -0
- package/out/lsp/domain-lang-refresh.js +129 -0
- package/out/lsp/domain-lang-refresh.js.map +1 -0
- package/out/lsp/domain-lang-workspace-manager.d.ts +10 -0
- package/out/lsp/domain-lang-workspace-manager.js +35 -0
- package/out/lsp/domain-lang-workspace-manager.js.map +1 -1
- package/out/lsp/explain.d.ts +18 -0
- package/out/lsp/explain.js +138 -0
- package/out/lsp/explain.js.map +1 -0
- package/out/lsp/tool-handlers.d.ts +113 -0
- package/out/lsp/tool-handlers.js +297 -0
- package/out/lsp/tool-handlers.js.map +1 -0
- package/out/main.js +33 -190
- package/out/main.js.map +1 -1
- package/out/sdk/index.d.ts +2 -0
- package/out/sdk/index.js +2 -0
- package/out/sdk/index.js.map +1 -1
- package/out/sdk/loader-node.js +1 -1
- package/out/sdk/loader-node.js.map +1 -1
- package/out/sdk/serializers.d.ts +110 -0
- package/out/sdk/serializers.js +158 -0
- package/out/sdk/serializers.js.map +1 -0
- package/out/sdk/validator.js +17 -14
- package/out/sdk/validator.js.map +1 -1
- package/out/services/import-resolver.d.ts +67 -17
- package/out/services/import-resolver.js +146 -65
- package/out/services/import-resolver.js.map +1 -1
- package/out/services/lsp-logger.d.ts +42 -0
- package/out/services/lsp-logger.js +50 -0
- package/out/services/lsp-logger.js.map +1 -0
- package/out/services/lsp-runtime-settings.d.ts +20 -0
- package/out/services/lsp-runtime-settings.js +20 -0
- package/out/services/lsp-runtime-settings.js.map +1 -0
- package/out/services/performance-optimizer.d.ts +9 -9
- package/out/services/performance-optimizer.js +17 -41
- package/out/services/performance-optimizer.js.map +1 -1
- package/out/services/workspace-manager.d.ts +22 -1
- package/out/services/workspace-manager.js +57 -9
- package/out/services/workspace-manager.js.map +1 -1
- package/out/utils/import-utils.js +6 -6
- package/out/utils/import-utils.js.map +1 -1
- package/out/validation/constants.d.ts +6 -0
- package/out/validation/constants.js +7 -0
- package/out/validation/constants.js.map +1 -1
- package/out/validation/import.d.ts +13 -3
- package/out/validation/import.js +54 -10
- package/out/validation/import.js.map +1 -1
- package/package.json +1 -1
- package/src/domain-lang-module.ts +3 -3
- package/src/index.ts +4 -0
- package/src/lsp/domain-lang-completion.ts +3 -3
- package/src/lsp/domain-lang-index-manager.ts +438 -56
- package/src/lsp/domain-lang-refresh.ts +205 -0
- package/src/lsp/domain-lang-workspace-manager.ts +45 -0
- package/src/lsp/explain.ts +172 -0
- package/src/lsp/tool-handlers.ts +443 -0
- package/src/main.ts +40 -244
- package/src/sdk/index.ts +11 -0
- package/src/sdk/loader-node.ts +1 -1
- package/src/sdk/serializers.ts +213 -0
- package/src/sdk/validator.ts +17 -13
- package/src/services/import-resolver.ts +196 -89
- package/src/services/lsp-logger.ts +89 -0
- package/src/services/lsp-runtime-settings.ts +34 -0
- package/src/services/performance-optimizer.ts +18 -57
- package/src/services/workspace-manager.ts +62 -10
- package/src/utils/import-utils.ts +6 -6
- package/src/validation/constants.ts +9 -0
- package/src/validation/import.ts +67 -12
package/src/main.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { startLanguageServer } from 'langium/lsp';
|
|
2
2
|
import { NodeFileSystem } from 'langium/node';
|
|
3
|
-
import { createConnection, ProposedFeatures
|
|
3
|
+
import { createConnection, ProposedFeatures } 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 {
|
|
6
|
+
import { registerDomainLangRefresh } from './lsp/domain-lang-refresh.js';
|
|
7
|
+
import { registerToolHandlers } from './lsp/tool-handlers.js';
|
|
7
8
|
import { URI } from 'langium';
|
|
9
|
+
import { setLspRuntimeSettings } from './services/lsp-runtime-settings.js';
|
|
8
10
|
|
|
9
11
|
// Create a connection to the client
|
|
10
12
|
const connection = createConnection(ProposedFeatures.all);
|
|
@@ -12,6 +14,17 @@ const connection = createConnection(ProposedFeatures.all);
|
|
|
12
14
|
// Inject the shared services and language-specific services
|
|
13
15
|
const { shared, DomainLang } = createDomainLangServices({ connection, ...NodeFileSystem });
|
|
14
16
|
|
|
17
|
+
// Register custom LSP request handlers for VS Code Language Model Tools (PRS-015)
|
|
18
|
+
registerToolHandlers(connection, shared);
|
|
19
|
+
|
|
20
|
+
shared.lsp.LanguageServer.onInitialize((params) => {
|
|
21
|
+
applyLspSettings(params.initializationOptions);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
connection.onDidChangeConfiguration((params) => {
|
|
25
|
+
applyLspSettings(params.settings);
|
|
26
|
+
});
|
|
27
|
+
|
|
15
28
|
// Initialize workspace manager when language server initializes
|
|
16
29
|
// Uses Langium's LanguageServer.onInitialize hook (not raw connection handler)
|
|
17
30
|
// This integrates properly with Langium's initialization flow
|
|
@@ -25,7 +38,7 @@ shared.lsp.LanguageServer.onInitialize((params) => {
|
|
|
25
38
|
if (workspaceRoot) {
|
|
26
39
|
// Initialize workspace manager synchronously (just sets root path)
|
|
27
40
|
// Heavy work happens in initializeWorkspace() called by Langium later
|
|
28
|
-
const workspaceManager = DomainLang.imports.
|
|
41
|
+
const workspaceManager = DomainLang.imports.ManifestManager;
|
|
29
42
|
workspaceManager.initialize(workspaceRoot).catch(error => {
|
|
30
43
|
const message = error instanceof Error ? error.message : String(error);
|
|
31
44
|
console.warn(`Failed to initialize workspace: ${message}`);
|
|
@@ -35,247 +48,7 @@ shared.lsp.LanguageServer.onInitialize((params) => {
|
|
|
35
48
|
}
|
|
36
49
|
});
|
|
37
50
|
|
|
38
|
-
|
|
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
|
-
indexManager.clearImportDependencies();
|
|
92
|
-
result.lockFileChanged = true;
|
|
93
|
-
} else if (fileName.endsWith('.dlang')) {
|
|
94
|
-
if (change.type === FileChangeType.Deleted) {
|
|
95
|
-
result.deletedDlangUris.add(uriString);
|
|
96
|
-
console.warn(`DomainLang file deleted: ${uriString}`);
|
|
97
|
-
} else if (change.type === FileChangeType.Created) {
|
|
98
|
-
result.createdDlangUris.add(uriString);
|
|
99
|
-
console.warn(`DomainLang file created: ${uriString}`);
|
|
100
|
-
} else {
|
|
101
|
-
result.changedDlangUris.add(uriString);
|
|
102
|
-
console.warn(`DomainLang file changed: ${uriString}`);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
return result;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Rebuilds documents that depend on changed/deleted/created .dlang files.
|
|
112
|
-
*/
|
|
113
|
-
async function rebuildAffectedDocuments(
|
|
114
|
-
changes: CategorizedChanges,
|
|
115
|
-
indexManager: DomainLangIndexManager,
|
|
116
|
-
sharedServices: typeof shared,
|
|
117
|
-
langServices: typeof DomainLang
|
|
118
|
-
): Promise<void> {
|
|
119
|
-
const hasChanges = changes.changedDlangUris.size > 0 ||
|
|
120
|
-
changes.deletedDlangUris.size > 0 ||
|
|
121
|
-
changes.createdDlangUris.size > 0;
|
|
122
|
-
if (!hasChanges) {
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// CRITICAL: Clear ImportResolver cache BEFORE rebuilding.
|
|
127
|
-
// The WorkspaceCache only clears AFTER linking, but resolution happens
|
|
128
|
-
// DURING linking. Without this, stale cached resolutions would be used.
|
|
129
|
-
langServices.imports.ImportResolver.clearCache();
|
|
130
|
-
|
|
131
|
-
const affectedUris = collectAffectedDocuments(changes, indexManager);
|
|
132
|
-
|
|
133
|
-
if (affectedUris.size === 0) {
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
console.warn(`Rebuilding ${affectedUris.size} documents affected by file changes`);
|
|
138
|
-
|
|
139
|
-
const langiumDocuments = sharedServices.workspace.LangiumDocuments;
|
|
140
|
-
const affectedDocs: URI[] = [];
|
|
141
|
-
|
|
142
|
-
for (const uriString of affectedUris) {
|
|
143
|
-
const uri = URI.parse(uriString);
|
|
144
|
-
if (langiumDocuments.hasDocument(uri)) {
|
|
145
|
-
affectedDocs.push(uri);
|
|
146
|
-
indexManager.markForReprocessing(uriString);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const deletedUriObjects = [...changes.deletedDlangUris].map(u => URI.parse(u));
|
|
151
|
-
if (affectedDocs.length > 0 || deletedUriObjects.length > 0) {
|
|
152
|
-
await sharedServices.workspace.DocumentBuilder.update(affectedDocs, deletedUriObjects);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Collects all document URIs that should be rebuilt based on the changes.
|
|
158
|
-
*
|
|
159
|
-
* Uses targeted matching to avoid expensive full rebuilds:
|
|
160
|
-
* - For edits: rebuild documents that import the changed file (by resolved URI)
|
|
161
|
-
* - For all changes: rebuild documents whose import specifiers match the path
|
|
162
|
-
*
|
|
163
|
-
* The specifier matching handles renamed/moved/created files by comparing
|
|
164
|
-
* import specifiers against path segments (filename, parent/filename, etc.).
|
|
165
|
-
*/
|
|
166
|
-
function collectAffectedDocuments(
|
|
167
|
-
changes: CategorizedChanges,
|
|
168
|
-
indexManager: DomainLangIndexManager
|
|
169
|
-
): Set<string> {
|
|
170
|
-
const allChangedUris = new Set([
|
|
171
|
-
...changes.changedDlangUris,
|
|
172
|
-
...changes.deletedDlangUris,
|
|
173
|
-
...changes.createdDlangUris
|
|
174
|
-
]);
|
|
175
|
-
|
|
176
|
-
// Get documents affected by resolved URI changes (edits to imported files)
|
|
177
|
-
const affectedByUri = indexManager.getAllAffectedDocuments(allChangedUris);
|
|
178
|
-
|
|
179
|
-
// Get documents with import specifiers that match changed paths
|
|
180
|
-
// This catches:
|
|
181
|
-
// - File moves/renames: specifiers that previously resolved but now won't
|
|
182
|
-
// - File creations: specifiers that previously failed but might now resolve
|
|
183
|
-
// Uses fuzzy matching on path segments rather than rebuilding all imports
|
|
184
|
-
const affectedBySpecifier = indexManager.getDocumentsWithPotentiallyAffectedImports(allChangedUris);
|
|
185
|
-
|
|
186
|
-
return new Set([...affectedByUri, ...affectedBySpecifier]);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Handles all file changes including .dlang files, model.yaml, and model.lock.
|
|
191
|
-
*
|
|
192
|
-
* For .dlang files: rebuilds all documents that import the changed file.
|
|
193
|
-
* For config files: invalidates caches and rebuilds workspace as needed.
|
|
194
|
-
*/
|
|
195
|
-
async function handleFileChanges(
|
|
196
|
-
params: { changes: Array<{ uri: string; type: number }> },
|
|
197
|
-
workspaceManager: typeof DomainLang.imports.WorkspaceManager,
|
|
198
|
-
sharedServices: typeof shared,
|
|
199
|
-
langServices: typeof DomainLang
|
|
200
|
-
): Promise<void> {
|
|
201
|
-
const indexManager = sharedServices.workspace.IndexManager as DomainLangIndexManager;
|
|
202
|
-
|
|
203
|
-
// Categorize and process changes
|
|
204
|
-
const changes = categorizeChanges(params, workspaceManager, langServices, indexManager);
|
|
205
|
-
|
|
206
|
-
// Handle lock file changes
|
|
207
|
-
if (changes.lockFileChanged) {
|
|
208
|
-
const lockChange = params.changes.find(c => c.uri.endsWith('model.lock'));
|
|
209
|
-
if (lockChange) {
|
|
210
|
-
await handleLockFileChange(lockChange, workspaceManager);
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// Rebuild documents affected by .dlang file changes
|
|
215
|
-
await rebuildAffectedDocuments(changes, indexManager, sharedServices, langServices);
|
|
216
|
-
|
|
217
|
-
// Handle config file changes
|
|
218
|
-
if (changes.manifestChanged || changes.lockFileChanged) {
|
|
219
|
-
await rebuildWorkspace(sharedServices, workspaceManager, changes.manifestChanged);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* Handles lock file creation, change, or deletion.
|
|
225
|
-
*/
|
|
226
|
-
async function handleLockFileChange(
|
|
227
|
-
change: { uri: string; type: number },
|
|
228
|
-
workspaceManager: typeof DomainLang.imports.WorkspaceManager
|
|
229
|
-
): Promise<void> {
|
|
230
|
-
if (change.type === FileChangeType.Changed || change.type === FileChangeType.Created) {
|
|
231
|
-
await workspaceManager.refreshLockFile();
|
|
232
|
-
} else if (change.type === FileChangeType.Deleted) {
|
|
233
|
-
workspaceManager.invalidateLockCache();
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* Rebuilds the workspace after config file changes.
|
|
239
|
-
* Uses incremental strategy: only full rebuild if dependencies changed.
|
|
240
|
-
*
|
|
241
|
-
* @param sharedServices - Shared Langium services
|
|
242
|
-
* @param workspaceManager - Workspace manager for manifest access
|
|
243
|
-
* @param manifestChanged - Whether model.yaml changed (vs just model.lock)
|
|
244
|
-
*/
|
|
245
|
-
async function rebuildWorkspace(
|
|
246
|
-
sharedServices: typeof shared,
|
|
247
|
-
workspaceManager: typeof DomainLang.imports.WorkspaceManager,
|
|
248
|
-
manifestChanged: boolean
|
|
249
|
-
): Promise<void> {
|
|
250
|
-
try {
|
|
251
|
-
// If only lock file changed, caches are already invalidated - no rebuild needed
|
|
252
|
-
// Lock file changes mean resolved versions changed, but import resolver cache is cleared
|
|
253
|
-
// Documents will re-resolve imports on next access
|
|
254
|
-
if (!manifestChanged) {
|
|
255
|
-
console.warn('Lock file changed - caches invalidated, no rebuild needed');
|
|
256
|
-
return;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// For manifest changes, check if dependencies section actually changed
|
|
260
|
-
// If only metadata changed (name, version, etc.), no rebuild needed
|
|
261
|
-
const manifest = await workspaceManager.getManifest();
|
|
262
|
-
const hasDependencies = manifest?.dependencies && Object.keys(manifest.dependencies).length > 0;
|
|
263
|
-
|
|
264
|
-
if (!hasDependencies) {
|
|
265
|
-
console.warn('Manifest changed but has no dependencies - skipping rebuild');
|
|
266
|
-
return;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// Dependencies exist and manifest changed - do full rebuild
|
|
270
|
-
const documents = sharedServices.workspace.LangiumDocuments.all.toArray();
|
|
271
|
-
const uris = documents.map(doc => doc.uri);
|
|
272
|
-
await sharedServices.workspace.DocumentBuilder.update([], uris);
|
|
273
|
-
console.warn(`Workspace rebuilt: ${documents.length} documents revalidated`);
|
|
274
|
-
} catch (error) {
|
|
275
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
276
|
-
console.error(`Failed to rebuild workspace: ${message}`);
|
|
277
|
-
}
|
|
278
|
-
}
|
|
51
|
+
registerDomainLangRefresh(shared, DomainLang);
|
|
279
52
|
|
|
280
53
|
// Optionally start from a single entry file and follow imports.
|
|
281
54
|
// Configure via env DOMAINLANG_ENTRY (absolute or workspace-relative path)
|
|
@@ -327,3 +100,26 @@ if (entryFile) {
|
|
|
327
100
|
// No entry file configured: start normally
|
|
328
101
|
startLanguageServer(shared);
|
|
329
102
|
}
|
|
103
|
+
|
|
104
|
+
function applyLspSettings(settings: unknown): void {
|
|
105
|
+
const lsp = extractLspSettings(settings);
|
|
106
|
+
setLspRuntimeSettings({
|
|
107
|
+
traceImports: lsp.traceImports,
|
|
108
|
+
infoLogs: lsp.infoLogs,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function extractLspSettings(source: unknown): { traceImports: boolean; infoLogs: boolean } {
|
|
113
|
+
const root = isRecord(source) ? source : {};
|
|
114
|
+
const domainlang = isRecord(root.domainlang) ? root.domainlang : root;
|
|
115
|
+
const lsp = isRecord(domainlang.lsp) ? domainlang.lsp : {};
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
traceImports: typeof lsp.traceImports === 'boolean' ? lsp.traceImports : false,
|
|
119
|
+
infoLogs: typeof lsp.infoLogs === 'boolean' ? lsp.infoLogs : false,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
124
|
+
return typeof value === 'object' && value !== null;
|
|
125
|
+
}
|
package/src/sdk/index.ts
CHANGED
|
@@ -142,6 +142,17 @@ export type {
|
|
|
142
142
|
RelationshipView,
|
|
143
143
|
} from './types.js';
|
|
144
144
|
|
|
145
|
+
// Serializers for tool responses (browser-safe)
|
|
146
|
+
export {
|
|
147
|
+
serializeNode,
|
|
148
|
+
serializeRelationship,
|
|
149
|
+
resolveName,
|
|
150
|
+
resolveMultiReference,
|
|
151
|
+
normalizeEntityType,
|
|
152
|
+
ENTITY_ALIASES,
|
|
153
|
+
} from './serializers.js';
|
|
154
|
+
export type { QueryEntityType, QueryEntityInput, QueryFilters } from './serializers.js';
|
|
155
|
+
|
|
145
156
|
// Node.js-specific exports (will fail in browser environments)
|
|
146
157
|
export { loadModel } from './loader-node.js';
|
|
147
158
|
export { validateFile, validateWorkspace } from './validator.js';
|
package/src/sdk/loader-node.ts
CHANGED
|
@@ -77,7 +77,7 @@ export async function loadModel(
|
|
|
77
77
|
|
|
78
78
|
// Initialize workspace if directory provided
|
|
79
79
|
if (options?.workspaceDir) {
|
|
80
|
-
const workspaceManager = services.imports.
|
|
80
|
+
const workspaceManager = services.imports.ManifestManager;
|
|
81
81
|
await workspaceManager.initialize(options.workspaceDir);
|
|
82
82
|
}
|
|
83
83
|
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AST Serialization Utilities
|
|
3
|
+
*
|
|
4
|
+
* Converts Langium AST nodes to plain JSON objects suitable for:
|
|
5
|
+
* - LSP custom requests (JSON-RPC transport)
|
|
6
|
+
* - MCP tool responses (stdio JSON)
|
|
7
|
+
* - CLI output (JSON/YAML formats)
|
|
8
|
+
*
|
|
9
|
+
* ## Strategy
|
|
10
|
+
*
|
|
11
|
+
* Rather than maintaining a parallel DTO type hierarchy (DomainDto, BoundedContextDto, etc.),
|
|
12
|
+
* we use a **generic serializer** that:
|
|
13
|
+
* - Strips Langium internal properties ($container, $cstNode, $document)
|
|
14
|
+
* - Preserves $type for discriminated output
|
|
15
|
+
* - Resolves Reference<T> to referenced name strings
|
|
16
|
+
* - Resolves MultiReference<T> to arrays of names
|
|
17
|
+
* - Recursively serializes child AstNodes
|
|
18
|
+
* - Adds FQN for named elements via Query
|
|
19
|
+
*
|
|
20
|
+
* For types with SDK-augmented properties (computed values not on raw AST),
|
|
21
|
+
* use augmentation functions that enrich the generic output.
|
|
22
|
+
*
|
|
23
|
+
* @packageDocumentation
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import type { AstNode, Reference } from 'langium';
|
|
27
|
+
import { isAstNode, isReference } from 'langium';
|
|
28
|
+
import type { Query, RelationshipView } from './types.js';
|
|
29
|
+
|
|
30
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
31
|
+
// Types
|
|
32
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Canonical entity types that can be queried.
|
|
36
|
+
* Moved from CLI to SDK for sharing with LSP tools.
|
|
37
|
+
*/
|
|
38
|
+
export type QueryEntityType =
|
|
39
|
+
| 'domains'
|
|
40
|
+
| 'bcs'
|
|
41
|
+
| 'teams'
|
|
42
|
+
| 'classifications'
|
|
43
|
+
| 'relationships'
|
|
44
|
+
| 'context-maps'
|
|
45
|
+
| 'domain-maps';
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* All accepted entity type names, including aliases.
|
|
49
|
+
* Aliases are normalized to canonical types before query execution.
|
|
50
|
+
*/
|
|
51
|
+
export type QueryEntityInput = QueryEntityType
|
|
52
|
+
| 'bounded-contexts' | 'contexts'
|
|
53
|
+
| 'rels'
|
|
54
|
+
| 'cmaps'
|
|
55
|
+
| 'dmaps';
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Query filter options.
|
|
59
|
+
* Moved from CLI to SDK for sharing with LSP tools.
|
|
60
|
+
*/
|
|
61
|
+
export interface QueryFilters {
|
|
62
|
+
/** Filter by name (string or regex) */
|
|
63
|
+
name?: string;
|
|
64
|
+
/** Filter by fully qualified name */
|
|
65
|
+
fqn?: string;
|
|
66
|
+
/** Filter BCs by domain */
|
|
67
|
+
domain?: string;
|
|
68
|
+
/** Filter BCs by team */
|
|
69
|
+
team?: string;
|
|
70
|
+
/** Filter BCs by classification */
|
|
71
|
+
classification?: string;
|
|
72
|
+
/** Filter BCs by metadata key=value */
|
|
73
|
+
metadata?: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Map of entity type aliases to their canonical form.
|
|
78
|
+
*/
|
|
79
|
+
export const ENTITY_ALIASES: Record<string, QueryEntityType> = {
|
|
80
|
+
'bounded-contexts': 'bcs',
|
|
81
|
+
'contexts': 'bcs',
|
|
82
|
+
'rels': 'relationships',
|
|
83
|
+
'cmaps': 'context-maps',
|
|
84
|
+
'dmaps': 'domain-maps',
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Normalize an entity type input (which may be an alias) to its canonical form.
|
|
89
|
+
*/
|
|
90
|
+
export function normalizeEntityType(input: string): QueryEntityType {
|
|
91
|
+
if (input in ENTITY_ALIASES) {
|
|
92
|
+
return ENTITY_ALIASES[input];
|
|
93
|
+
}
|
|
94
|
+
return input as QueryEntityType;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
98
|
+
// Generic AST Serialization
|
|
99
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Serialize any Langium AST node to a plain JSON object.
|
|
103
|
+
*
|
|
104
|
+
* - Strips $-prefixed internal properties ($container, $cstNode, $document)
|
|
105
|
+
* - Preserves $type for discriminated output
|
|
106
|
+
* - Resolves Reference<T> to the referenced name (string)
|
|
107
|
+
* - Resolves MultiReference<T> to an array of names
|
|
108
|
+
* - Recursively serializes child AstNode properties
|
|
109
|
+
* - Serializes arrays of AstNodes/values
|
|
110
|
+
* - Adds FQN for named elements
|
|
111
|
+
*
|
|
112
|
+
* @param node - AST node to serialize
|
|
113
|
+
* @param query - Query instance for FQN resolution
|
|
114
|
+
* @returns Plain JSON object
|
|
115
|
+
*/
|
|
116
|
+
export function serializeNode(node: AstNode, query: Query): Record<string, unknown> {
|
|
117
|
+
const result: Record<string, unknown> = { $type: node.$type };
|
|
118
|
+
|
|
119
|
+
for (const [key, value] of Object.entries(node)) {
|
|
120
|
+
// Skip Langium internals (but preserve $type)
|
|
121
|
+
if (key.startsWith('$') && key !== '$type') {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (isReference(value)) {
|
|
126
|
+
// Reference<T> → name string
|
|
127
|
+
const ref = value.ref;
|
|
128
|
+
result[key] = (ref && 'name' in ref) ? (ref as { name?: string }).name : value.$refText;
|
|
129
|
+
} else if (isAstNode(value)) {
|
|
130
|
+
// Nested AstNode → recurse
|
|
131
|
+
result[key] = serializeNode(value, query);
|
|
132
|
+
} else if (Array.isArray(value)) {
|
|
133
|
+
// Array → map each item
|
|
134
|
+
result[key] = value.map(item => {
|
|
135
|
+
if (isReference(item)) {
|
|
136
|
+
const itemRef = item.ref;
|
|
137
|
+
return (itemRef && 'name' in itemRef) ? (itemRef as { name?: string }).name : item.$refText;
|
|
138
|
+
} else if (isAstNode(item)) {
|
|
139
|
+
return serializeNode(item, query);
|
|
140
|
+
} else {
|
|
141
|
+
return item; // primitive
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
} else {
|
|
145
|
+
// Primitives pass through
|
|
146
|
+
result[key] = value;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Always include FQN for named elements
|
|
151
|
+
if ('name' in node && typeof (node as { name?: unknown }).name === 'string') {
|
|
152
|
+
result.fqn = query.fqn(node);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Augment a serialized RelationshipView with computed properties.
|
|
160
|
+
*
|
|
161
|
+
* RelationshipView is already a clean DTO (not an AstNode), but we format it
|
|
162
|
+
* consistently with other serialized types.
|
|
163
|
+
*
|
|
164
|
+
* @param view - RelationshipView from query.relationships()
|
|
165
|
+
* @returns Serialized relationship object
|
|
166
|
+
*/
|
|
167
|
+
export function serializeRelationship(view: RelationshipView): Record<string, unknown> {
|
|
168
|
+
// RelationshipView.left and .right are BoundedContext (which have name property)
|
|
169
|
+
const leftName = view.left.name;
|
|
170
|
+
const rightName = view.right.name;
|
|
171
|
+
return {
|
|
172
|
+
$type: 'Relationship',
|
|
173
|
+
name: `${leftName} ${view.arrow} ${rightName}`,
|
|
174
|
+
left: leftName,
|
|
175
|
+
right: rightName,
|
|
176
|
+
arrow: view.arrow,
|
|
177
|
+
leftPatterns: view.leftPatterns,
|
|
178
|
+
rightPatterns: view.rightPatterns,
|
|
179
|
+
inferredType: view.inferredType,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
184
|
+
// Helper: Resolve Reference
|
|
185
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Resolve a Reference<T> to its name string.
|
|
189
|
+
* Returns undefined if reference is unresolved.
|
|
190
|
+
*
|
|
191
|
+
* @param ref - Reference to resolve
|
|
192
|
+
* @returns Referenced name or undefined
|
|
193
|
+
*/
|
|
194
|
+
export function resolveName<T extends AstNode & { name?: string }>(ref: Reference<T> | undefined): string | undefined {
|
|
195
|
+
if (!ref) return undefined;
|
|
196
|
+
return ref.ref?.name ?? ref.$refText;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Resolve a MultiReference (array of items with refs) to an array of names.
|
|
201
|
+
* Filters out unresolved references.
|
|
202
|
+
*
|
|
203
|
+
* @param multiRef - Array of items with ref property
|
|
204
|
+
* @returns Array of resolved names
|
|
205
|
+
*/
|
|
206
|
+
export function resolveMultiReference<T extends { ref?: Reference<AstNode & { name?: string }> }>(
|
|
207
|
+
multiRef: T[] | undefined
|
|
208
|
+
): string[] {
|
|
209
|
+
if (!multiRef) return [];
|
|
210
|
+
return multiRef
|
|
211
|
+
.map(item => item.ref?.ref?.name)
|
|
212
|
+
.filter((name): name is string => name !== undefined);
|
|
213
|
+
}
|
package/src/sdk/validator.ts
CHANGED
|
@@ -130,8 +130,8 @@ export async function validateFile(
|
|
|
130
130
|
|
|
131
131
|
// Initialize workspace with the specified directory or file's directory
|
|
132
132
|
const workspaceDir = options.workspaceDir ?? dirname(absolutePath);
|
|
133
|
-
const
|
|
134
|
-
await
|
|
133
|
+
const manifestManager = services.imports.ManifestManager;
|
|
134
|
+
await manifestManager.initialize(workspaceDir);
|
|
135
135
|
|
|
136
136
|
// Load and parse the document
|
|
137
137
|
const uri = URI.file(absolutePath);
|
|
@@ -152,17 +152,21 @@ export async function validateFile(
|
|
|
152
152
|
const allDocuments = Array.from(shared.workspace.LangiumDocuments.all);
|
|
153
153
|
await shared.workspace.DocumentBuilder.build(allDocuments, { validation: true });
|
|
154
154
|
|
|
155
|
-
// Collect diagnostics from
|
|
156
|
-
const diagnostics = document.diagnostics ?? [];
|
|
155
|
+
// Collect diagnostics from all loaded documents (entry + imports)
|
|
157
156
|
const errors: ValidationDiagnostic[] = [];
|
|
158
157
|
const warnings: ValidationDiagnostic[] = [];
|
|
159
158
|
|
|
160
|
-
for (const
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
159
|
+
for (const doc of allDocuments) {
|
|
160
|
+
const diagnostics = doc.diagnostics ?? [];
|
|
161
|
+
const diagnosticFile = doc.uri.fsPath;
|
|
162
|
+
|
|
163
|
+
for (const diagnostic of diagnostics) {
|
|
164
|
+
const validationDiag = toValidationDiagnostic(diagnostic, diagnosticFile);
|
|
165
|
+
if (diagnostic.severity === 1) {
|
|
166
|
+
errors.push(validationDiag);
|
|
167
|
+
} else if (diagnostic.severity === 2) {
|
|
168
|
+
warnings.push(validationDiag);
|
|
169
|
+
}
|
|
166
170
|
}
|
|
167
171
|
}
|
|
168
172
|
|
|
@@ -262,18 +266,18 @@ export async function validateWorkspace(
|
|
|
262
266
|
const servicesObj = createDomainLangServices(NodeFileSystem);
|
|
263
267
|
const shared = servicesObj.shared;
|
|
264
268
|
const services = servicesObj.DomainLang;
|
|
265
|
-
const
|
|
269
|
+
const manifestManager = services.imports.ManifestManager;
|
|
266
270
|
|
|
267
271
|
try {
|
|
268
272
|
// Initialize workspace - this will find and load model.yaml
|
|
269
|
-
await
|
|
273
|
+
await manifestManager.initialize(absolutePath);
|
|
270
274
|
} catch (error) {
|
|
271
275
|
const message = error instanceof Error ? error.message : String(error);
|
|
272
276
|
throw new Error(`Failed to initialize workspace at ${workspaceDir}: ${message}`);
|
|
273
277
|
}
|
|
274
278
|
|
|
275
279
|
// Get the manifest to find the entry file
|
|
276
|
-
const manifest = await
|
|
280
|
+
const manifest = await manifestManager.getManifest();
|
|
277
281
|
let entryFile = 'index.dlang';
|
|
278
282
|
|
|
279
283
|
if (manifest?.model?.entry) {
|