@domainlang/language 0.7.0 → 0.8.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/out/domain-lang-module.d.ts +2 -0
- package/out/domain-lang-module.js +21 -2
- package/out/domain-lang-module.js.map +1 -1
- package/out/lsp/domain-lang-completion.d.ts +142 -1
- package/out/lsp/domain-lang-completion.js +620 -22
- package/out/lsp/domain-lang-completion.js.map +1 -1
- package/out/lsp/domain-lang-document-symbol-provider.d.ts +79 -0
- package/out/lsp/domain-lang-document-symbol-provider.js +210 -0
- package/out/lsp/domain-lang-document-symbol-provider.js.map +1 -0
- package/out/lsp/domain-lang-index-manager.d.ts +34 -5
- package/out/lsp/domain-lang-index-manager.js +66 -27
- package/out/lsp/domain-lang-index-manager.js.map +1 -1
- package/out/lsp/domain-lang-node-kind-provider.d.ts +27 -0
- package/out/lsp/domain-lang-node-kind-provider.js +87 -0
- package/out/lsp/domain-lang-node-kind-provider.js.map +1 -0
- package/out/lsp/domain-lang-scope-provider.d.ts +53 -20
- package/out/lsp/domain-lang-scope-provider.js +119 -44
- package/out/lsp/domain-lang-scope-provider.js.map +1 -1
- package/out/lsp/domain-lang-workspace-manager.d.ts +23 -2
- package/out/lsp/domain-lang-workspace-manager.js +51 -6
- package/out/lsp/domain-lang-workspace-manager.js.map +1 -1
- package/out/lsp/hover/domain-lang-hover.d.ts +16 -6
- package/out/lsp/hover/domain-lang-hover.js +160 -134
- package/out/lsp/hover/domain-lang-hover.js.map +1 -1
- package/out/lsp/hover/hover-builders.d.ts +57 -0
- package/out/lsp/hover/hover-builders.js +171 -0
- package/out/lsp/hover/hover-builders.js.map +1 -0
- package/out/main.js +2 -1
- package/out/main.js.map +1 -1
- package/out/sdk/index.d.ts +2 -1
- package/out/sdk/index.js +1 -1
- 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/loader.d.ts +55 -2
- package/out/sdk/loader.js +87 -28
- package/out/sdk/loader.js.map +1 -1
- package/out/sdk/query.js +14 -11
- package/out/sdk/query.js.map +1 -1
- package/out/services/package-boundary-detector.d.ts +101 -0
- package/out/services/package-boundary-detector.js +211 -0
- package/out/services/package-boundary-detector.js.map +1 -0
- package/out/services/performance-optimizer.js +6 -2
- package/out/services/performance-optimizer.js.map +1 -1
- package/out/services/types.d.ts +24 -0
- package/out/services/types.js.map +1 -1
- package/out/services/workspace-manager.d.ts +73 -6
- package/out/services/workspace-manager.js +210 -57
- package/out/services/workspace-manager.js.map +1 -1
- package/out/utils/import-utils.d.ts +9 -6
- package/out/utils/import-utils.js +26 -15
- package/out/utils/import-utils.js.map +1 -1
- package/out/validation/constants.d.ts +7 -0
- package/out/validation/constants.js +21 -3
- package/out/validation/constants.js.map +1 -1
- package/out/validation/import.d.ts +11 -1
- package/out/validation/import.js +42 -14
- package/out/validation/import.js.map +1 -1
- package/out/validation/maps.js +50 -1
- package/out/validation/maps.js.map +1 -1
- package/package.json +5 -5
- package/src/domain-lang-module.ts +24 -3
- package/src/lsp/domain-lang-completion.ts +736 -27
- package/src/lsp/domain-lang-document-symbol-provider.ts +254 -0
- package/src/lsp/domain-lang-index-manager.ts +79 -27
- package/src/lsp/domain-lang-node-kind-provider.ts +119 -0
- package/src/lsp/domain-lang-scope-provider.ts +171 -55
- package/src/lsp/domain-lang-workspace-manager.ts +64 -6
- package/src/lsp/hover/domain-lang-hover.ts +189 -131
- package/src/lsp/hover/hover-builders.ts +208 -0
- package/src/main.ts +3 -1
- package/src/sdk/index.ts +2 -1
- package/src/sdk/loader-node.ts +2 -1
- package/src/sdk/loader.ts +125 -34
- package/src/sdk/query.ts +15 -11
- package/src/services/package-boundary-detector.ts +238 -0
- package/src/services/performance-optimizer.ts +6 -2
- package/src/services/types.ts +25 -0
- package/src/services/workspace-manager.ts +259 -62
- package/src/utils/import-utils.ts +27 -15
- package/src/validation/constants.ts +23 -6
- package/src/validation/import.ts +49 -14
- package/src/validation/maps.ts +59 -2
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standalone hover content builder functions.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from DomainLangHoverProvider to reduce class complexity
|
|
5
|
+
* and enable independent testing of hover content generation.
|
|
6
|
+
*
|
|
7
|
+
* Each builder takes typed AST nodes (not generic AstNode) and the helper
|
|
8
|
+
* functions needed for formatting, keeping them pure and testable.
|
|
9
|
+
*
|
|
10
|
+
* @module lsp/hover/hover-builders
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type {
|
|
14
|
+
BoundedContext,
|
|
15
|
+
Classification,
|
|
16
|
+
Domain,
|
|
17
|
+
Relationship,
|
|
18
|
+
Team,
|
|
19
|
+
Type,
|
|
20
|
+
} from '../../generated/ast.js';
|
|
21
|
+
import { effectiveClassification, effectiveTeam } from '../../sdk/resolution.js';
|
|
22
|
+
|
|
23
|
+
// ============================================================================
|
|
24
|
+
// Shared formatting utilities
|
|
25
|
+
// ============================================================================
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Wraps text in a domain-lang fenced code block.
|
|
29
|
+
*/
|
|
30
|
+
export function codeBlock(text: string): string {
|
|
31
|
+
return `\`\`\`domain-lang\n${text}\n\`\`\``;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Formats hover output with a consistent header/body structure.
|
|
36
|
+
*
|
|
37
|
+
* @param commentBlock - Documentation comment prefix (or empty)
|
|
38
|
+
* @param emoji - Emoji icon for the type
|
|
39
|
+
* @param typeName - Lowercase type name
|
|
40
|
+
* @param name - Element name (optional)
|
|
41
|
+
* @param fields - Body content fields
|
|
42
|
+
*/
|
|
43
|
+
export function formatHoverContent(
|
|
44
|
+
commentBlock: string,
|
|
45
|
+
emoji: string,
|
|
46
|
+
typeName: string,
|
|
47
|
+
name: string | undefined,
|
|
48
|
+
fields: string[]
|
|
49
|
+
): string {
|
|
50
|
+
const separator = commentBlock ? `${commentBlock}\n\n---\n\n` : '';
|
|
51
|
+
const nameDisplay = name ? ` ${name}` : '';
|
|
52
|
+
const header = `${emoji} **\`(${typeName})\`${nameDisplay}**`;
|
|
53
|
+
const body = fields.length > 0 ? `\n\n${fields.join('\n\n')}` : '';
|
|
54
|
+
return `${separator}${header}${body}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Callback for creating reference links.
|
|
59
|
+
* Provided by the hover provider which has access to the qualified name provider.
|
|
60
|
+
*/
|
|
61
|
+
export type RefLinkFn = (ref: Type | undefined, label?: string) => string;
|
|
62
|
+
|
|
63
|
+
// ============================================================================
|
|
64
|
+
// Domain hover builder
|
|
65
|
+
// ============================================================================
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Builds a signature string for a domain (e.g., "Domain Sales in Commerce").
|
|
69
|
+
*/
|
|
70
|
+
export function buildDomainSignature(domain: Domain): string {
|
|
71
|
+
const parts = ['Domain', domain.name];
|
|
72
|
+
if (domain.parent?.ref?.name) {
|
|
73
|
+
parts.push('in', domain.parent.ref.name);
|
|
74
|
+
}
|
|
75
|
+
return parts.join(' ');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Builds hover fields for a Domain node.
|
|
80
|
+
*
|
|
81
|
+
* @param domain - The domain AST node
|
|
82
|
+
* @param refLink - Function to create reference links
|
|
83
|
+
* @returns Array of formatted field strings
|
|
84
|
+
*/
|
|
85
|
+
export function buildDomainFields(domain: Domain, refLink: RefLinkFn): string[] {
|
|
86
|
+
const description = domain.description ?? '';
|
|
87
|
+
const vision = domain.vision ?? '';
|
|
88
|
+
const typeRef = domain.type?.ref;
|
|
89
|
+
|
|
90
|
+
const signature = codeBlock(buildDomainSignature(domain));
|
|
91
|
+
const fields: string[] = [signature];
|
|
92
|
+
|
|
93
|
+
if (description) fields.push(description);
|
|
94
|
+
if (vision || typeRef || domain.parent) fields.push('---');
|
|
95
|
+
if (vision) fields.push(`**Vision:** ${vision}`);
|
|
96
|
+
if (typeRef) fields.push(`**Type:** ${refLink(typeRef)}`);
|
|
97
|
+
if (domain.parent?.ref) fields.push(`**Parent:** ${refLink(domain.parent.ref)}`);
|
|
98
|
+
|
|
99
|
+
return fields;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ============================================================================
|
|
103
|
+
// Bounded context hover builder
|
|
104
|
+
// ============================================================================
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Builds a signature string for a bounded context
|
|
108
|
+
* (e.g., "boundedcontext OrderManagement for Sales as Core by SalesTeam").
|
|
109
|
+
*/
|
|
110
|
+
export function buildBcSignature(bc: BoundedContext): string {
|
|
111
|
+
const classification = effectiveClassification(bc);
|
|
112
|
+
const team = effectiveTeam(bc);
|
|
113
|
+
|
|
114
|
+
const parts = ['BoundedContext', bc.name];
|
|
115
|
+
if (bc.domain?.ref?.name) parts.push('for', bc.domain.ref.name);
|
|
116
|
+
if (classification?.name) parts.push('as', classification.name);
|
|
117
|
+
if (team?.name) parts.push('by', team.name);
|
|
118
|
+
return parts.join(' ');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Builds the properties section (domain, classification, team, businessModel, evolution).
|
|
123
|
+
*/
|
|
124
|
+
function buildBcPropertyFields(
|
|
125
|
+
bc: BoundedContext,
|
|
126
|
+
classification: Classification | undefined,
|
|
127
|
+
team: Team | undefined,
|
|
128
|
+
refLink: RefLinkFn
|
|
129
|
+
): string[] {
|
|
130
|
+
const fields: string[] = [];
|
|
131
|
+
const domain = bc.domain?.ref;
|
|
132
|
+
const businessModel = bc.businessModel?.ref;
|
|
133
|
+
const evolution = bc.evolution?.ref;
|
|
134
|
+
|
|
135
|
+
if (domain || classification || team || businessModel || evolution) fields.push('---');
|
|
136
|
+
if (domain) fields.push(`📁 **Domain:** ${refLink(domain)}`);
|
|
137
|
+
if (classification) fields.push(`🔖 **Classification:** ${refLink(classification)}`);
|
|
138
|
+
if (team) fields.push(`👥 **Team:** ${refLink(team)}`);
|
|
139
|
+
if (businessModel) fields.push(`💼 **Business Model:** ${refLink(businessModel)}`);
|
|
140
|
+
if (evolution) fields.push(`🔄 **Evolution:** ${refLink(evolution)}`);
|
|
141
|
+
|
|
142
|
+
return fields;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Builds the relationships section for a bounded context hover.
|
|
147
|
+
*/
|
|
148
|
+
function buildBcRelationshipsSection(
|
|
149
|
+
relationships: readonly Relationship[],
|
|
150
|
+
formatRelationshipLine: (rel: Relationship) => string
|
|
151
|
+
): string[] {
|
|
152
|
+
if (relationships.length === 0) return [];
|
|
153
|
+
const lines = relationships.map(formatRelationshipLine);
|
|
154
|
+
return [`**Relationships:**\n${lines.join('\n')}`];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Builds the terminology section for a bounded context hover.
|
|
159
|
+
*/
|
|
160
|
+
function buildBcTerminologySection(bc: BoundedContext): string[] {
|
|
161
|
+
const terminology = bc.terminology ?? [];
|
|
162
|
+
if (terminology.length === 0) return [];
|
|
163
|
+
const lines = terminology.map(t => `- \`${t.name}\`: ${t.meaning ?? ''}`);
|
|
164
|
+
return [`**Terminology:**\n${lines.join('\n')}`];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Builds the decisions section for a bounded context hover.
|
|
169
|
+
*/
|
|
170
|
+
function buildBcDecisionsSection(bc: BoundedContext): string[] {
|
|
171
|
+
const decisions = bc.decisions ?? [];
|
|
172
|
+
if (decisions.length === 0) return [];
|
|
173
|
+
const lines = decisions.map(d => `- \`${d.name}\`: ${d.value ?? ''}`);
|
|
174
|
+
return [`**Decisions:**\n${lines.join('\n')}`];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Builds hover fields for a BoundedContext node.
|
|
179
|
+
*
|
|
180
|
+
* @param bc - The bounded context AST node
|
|
181
|
+
* @param refLink - Function to create reference links
|
|
182
|
+
* @param formatRelationshipLine - Function to format a relationship line
|
|
183
|
+
* @returns Array of formatted field strings
|
|
184
|
+
*/
|
|
185
|
+
export function buildBcFields(
|
|
186
|
+
bc: BoundedContext,
|
|
187
|
+
refLink: RefLinkFn,
|
|
188
|
+
formatRelationshipLine: (rel: Relationship) => string
|
|
189
|
+
): string[] {
|
|
190
|
+
const description = bc.description ?? '';
|
|
191
|
+
const classification = effectiveClassification(bc);
|
|
192
|
+
const team = effectiveTeam(bc);
|
|
193
|
+
|
|
194
|
+
const signature = codeBlock(buildBcSignature(bc));
|
|
195
|
+
const fields: string[] = [signature];
|
|
196
|
+
|
|
197
|
+
if (description) fields.push(description);
|
|
198
|
+
|
|
199
|
+
const sections = [
|
|
200
|
+
...buildBcPropertyFields(bc, classification, team, refLink),
|
|
201
|
+
...buildBcRelationshipsSection(bc.relationships ?? [], formatRelationshipLine),
|
|
202
|
+
...buildBcTerminologySection(bc),
|
|
203
|
+
...buildBcDecisionsSection(bc),
|
|
204
|
+
];
|
|
205
|
+
fields.push(...sections);
|
|
206
|
+
|
|
207
|
+
return fields;
|
|
208
|
+
}
|
package/src/main.ts
CHANGED
|
@@ -88,6 +88,7 @@ function categorizeChanges(
|
|
|
88
88
|
} else if (fileName === 'model.lock') {
|
|
89
89
|
console.warn(`model.lock changed: ${uriString}`);
|
|
90
90
|
langServices.imports.ImportResolver.clearCache();
|
|
91
|
+
indexManager.clearImportDependencies();
|
|
91
92
|
result.lockFileChanged = true;
|
|
92
93
|
} else if (fileName.endsWith('.dlang')) {
|
|
93
94
|
if (change.type === FileChangeType.Deleted) {
|
|
@@ -290,7 +291,8 @@ if (entryFile) {
|
|
|
290
291
|
try {
|
|
291
292
|
currentGraph = await ensureImportGraphFromEntryFile(
|
|
292
293
|
entryFile,
|
|
293
|
-
shared.workspace.LangiumDocuments
|
|
294
|
+
shared.workspace.LangiumDocuments,
|
|
295
|
+
DomainLang.imports.ImportResolver
|
|
294
296
|
);
|
|
295
297
|
console.warn(`Successfully loaded import graph from ${entryFile}`);
|
|
296
298
|
} catch (error) {
|
package/src/sdk/index.ts
CHANGED
|
@@ -89,7 +89,8 @@
|
|
|
89
89
|
*/
|
|
90
90
|
|
|
91
91
|
// Browser-safe entry points
|
|
92
|
-
export { loadModelFromText } from './loader.js';
|
|
92
|
+
export { loadModelFromText, createModelLoader } from './loader.js';
|
|
93
|
+
export type { ModelLoader } from './loader.js';
|
|
93
94
|
export { fromModel, fromDocument, fromServices, augmentModel } from './query.js';
|
|
94
95
|
|
|
95
96
|
// Note: loadModel() is NOT exported here - it requires Node.js filesystem
|
package/src/sdk/loader-node.ts
CHANGED
|
@@ -99,7 +99,8 @@ export async function loadModel(
|
|
|
99
99
|
// Traverse import graph to load all imported files
|
|
100
100
|
const importedUris = await ensureImportGraphFromDocument(
|
|
101
101
|
document,
|
|
102
|
-
shared.workspace.LangiumDocuments
|
|
102
|
+
shared.workspace.LangiumDocuments,
|
|
103
|
+
services.imports.ImportResolver
|
|
103
104
|
);
|
|
104
105
|
|
|
105
106
|
// Build all imported documents with validation
|
package/src/sdk/loader.ts
CHANGED
|
@@ -4,6 +4,14 @@
|
|
|
4
4
|
* This module provides `loadModelFromText()` which works in both
|
|
5
5
|
* browser and Node.js environments by using Langium's EmptyFileSystem.
|
|
6
6
|
*
|
|
7
|
+
* For repeated parsing (e.g., web playgrounds, REPLs), use `createModelLoader()`
|
|
8
|
+
* to reuse Langium services across multiple parse calls:
|
|
9
|
+
* ```typescript
|
|
10
|
+
* const loader = createModelLoader();
|
|
11
|
+
* const result1 = await loader.loadFromText('Domain A {}');
|
|
12
|
+
* const result2 = await loader.loadFromText('Domain B {}');
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
7
15
|
* For file-based loading in Node.js CLI tools, use:
|
|
8
16
|
* ```typescript
|
|
9
17
|
* import { loadModel } from '@domainlang/language/sdk/loader-node';
|
|
@@ -18,21 +26,134 @@
|
|
|
18
26
|
*/
|
|
19
27
|
|
|
20
28
|
import { EmptyFileSystem, URI } from 'langium';
|
|
29
|
+
import type { LangiumSharedServices } from 'langium/lsp';
|
|
21
30
|
import type { Model } from '../generated/ast.js';
|
|
22
31
|
import { isModel } from '../generated/ast.js';
|
|
23
32
|
import { createDomainLangServices } from '../domain-lang-module.js';
|
|
33
|
+
import type { DomainLangServices } from '../domain-lang-module.js';
|
|
24
34
|
import type { LoadOptions, QueryContext } from './types.js';
|
|
25
35
|
import { augmentModel, fromModel } from './query.js';
|
|
26
36
|
|
|
37
|
+
/**
|
|
38
|
+
* A reusable model loader that maintains Langium services across multiple parse calls.
|
|
39
|
+
*
|
|
40
|
+
* Use this when calling `loadFromText()` repeatedly (e.g., web playgrounds, REPLs,
|
|
41
|
+
* batch processing) to avoid the overhead of recreating Langium services each time.
|
|
42
|
+
*/
|
|
43
|
+
export interface ModelLoader {
|
|
44
|
+
/**
|
|
45
|
+
* Loads a DomainLang model from a text string, reusing internal services.
|
|
46
|
+
*
|
|
47
|
+
* Each call creates a fresh document but shares the underlying parser,
|
|
48
|
+
* linker, and validator infrastructure.
|
|
49
|
+
*
|
|
50
|
+
* @param text - DomainLang source code
|
|
51
|
+
* @returns QueryContext with model and query API
|
|
52
|
+
* @throws Error if parsing fails
|
|
53
|
+
*/
|
|
54
|
+
loadFromText(text: string): Promise<QueryContext>;
|
|
55
|
+
|
|
56
|
+
/** The underlying DomainLang services (for advanced use). */
|
|
57
|
+
readonly services: DomainLangServices;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Internal counter for unique document URIs within a loader. */
|
|
61
|
+
let documentCounter = 0;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Parses text into a QueryContext using the provided services.
|
|
65
|
+
* Shared implementation for both `loadModelFromText` and `ModelLoader.loadFromText`.
|
|
66
|
+
*/
|
|
67
|
+
async function parseTextToContext(
|
|
68
|
+
text: string,
|
|
69
|
+
langServices: DomainLangServices,
|
|
70
|
+
shared: LangiumSharedServices
|
|
71
|
+
): Promise<QueryContext> {
|
|
72
|
+
// Use unique URI per parse to avoid document conflicts
|
|
73
|
+
const uri = URI.parse(`memory:///model-${documentCounter++}.dlang`);
|
|
74
|
+
const document = shared.workspace.LangiumDocumentFactory.fromString<Model>(text, uri);
|
|
75
|
+
|
|
76
|
+
// Register and build document
|
|
77
|
+
shared.workspace.LangiumDocuments.addDocument(document);
|
|
78
|
+
try {
|
|
79
|
+
await shared.workspace.DocumentBuilder.build([document], { validation: true });
|
|
80
|
+
|
|
81
|
+
// Check for parsing errors
|
|
82
|
+
if (document.parseResult.lexerErrors.length > 0) {
|
|
83
|
+
const errors = document.parseResult.lexerErrors.map(e => e.message).join('\n ');
|
|
84
|
+
throw new Error(`Lexer errors:\n ${errors}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (document.parseResult.parserErrors.length > 0) {
|
|
88
|
+
const errors = document.parseResult.parserErrors.map(e => e.message).join('\n ');
|
|
89
|
+
throw new Error(`Parser errors:\n ${errors}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const model = document.parseResult.value;
|
|
93
|
+
if (!isModel(model)) {
|
|
94
|
+
throw new Error(`Document root is not a Model`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Augment AST nodes with SDK properties
|
|
98
|
+
augmentModel(model);
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
model,
|
|
102
|
+
documents: [document.uri],
|
|
103
|
+
query: fromModel(model),
|
|
104
|
+
};
|
|
105
|
+
} finally {
|
|
106
|
+
// Clean up the document to prevent memory leaks across repeated calls
|
|
107
|
+
shared.workspace.LangiumDocuments.deleteDocument(uri);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Creates a reusable model loader that shares Langium services across parse calls.
|
|
113
|
+
*
|
|
114
|
+
* **Browser-safe** - uses in-memory file system (EmptyFileSystem).
|
|
115
|
+
*
|
|
116
|
+
* For applications that parse multiple texts (web playgrounds, REPLs, batch tools),
|
|
117
|
+
* this avoids the overhead of creating new Langium services for each parse call.
|
|
118
|
+
*
|
|
119
|
+
* @returns A ModelLoader instance that can be used repeatedly
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* ```typescript
|
|
123
|
+
* import { createModelLoader } from '@domainlang/language/sdk';
|
|
124
|
+
*
|
|
125
|
+
* const loader = createModelLoader();
|
|
126
|
+
*
|
|
127
|
+
* // Parse multiple texts efficiently - services are reused
|
|
128
|
+
* const result1 = await loader.loadFromText('Domain Sales { vision: "Sales" }');
|
|
129
|
+
* const result2 = await loader.loadFromText('Domain Billing { vision: "Billing" }');
|
|
130
|
+
* ```
|
|
131
|
+
*/
|
|
132
|
+
export function createModelLoader(): ModelLoader {
|
|
133
|
+
const servicesObj = createDomainLangServices(EmptyFileSystem);
|
|
134
|
+
const shared = servicesObj.shared;
|
|
135
|
+
const langServices = servicesObj.DomainLang;
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
async loadFromText(text: string): Promise<QueryContext> {
|
|
139
|
+
return parseTextToContext(text, langServices, shared);
|
|
140
|
+
},
|
|
141
|
+
get services(): DomainLangServices {
|
|
142
|
+
return langServices;
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
27
147
|
/**
|
|
28
148
|
* Loads a DomainLang model from a text string.
|
|
29
149
|
*
|
|
30
150
|
* **Browser-safe** - uses in-memory file system (EmptyFileSystem).
|
|
31
151
|
*
|
|
152
|
+
* For repeated parsing, prefer {@link createModelLoader} to reuse services.
|
|
153
|
+
*
|
|
32
154
|
* Useful for:
|
|
33
155
|
* - Testing
|
|
34
|
-
* -
|
|
35
|
-
* - Web-based editors
|
|
156
|
+
* - One-off parsing
|
|
36
157
|
* - Any environment without file system access
|
|
37
158
|
*
|
|
38
159
|
* @param text - DomainLang source code
|
|
@@ -63,37 +184,7 @@ export async function loadModelFromText(
|
|
|
63
184
|
: createDomainLangServices(EmptyFileSystem);
|
|
64
185
|
|
|
65
186
|
const shared = servicesObj.shared;
|
|
187
|
+
const langServices = servicesObj.DomainLang;
|
|
66
188
|
|
|
67
|
-
|
|
68
|
-
const uri = URI.parse('memory:///model.dlang');
|
|
69
|
-
const document = shared.workspace.LangiumDocumentFactory.fromString<Model>(text, uri);
|
|
70
|
-
|
|
71
|
-
// Register and build document
|
|
72
|
-
shared.workspace.LangiumDocuments.addDocument(document);
|
|
73
|
-
await shared.workspace.DocumentBuilder.build([document], { validation: true });
|
|
74
|
-
|
|
75
|
-
// Check for parsing errors
|
|
76
|
-
if (document.parseResult.lexerErrors.length > 0) {
|
|
77
|
-
const errors = document.parseResult.lexerErrors.map(e => e.message).join('\n ');
|
|
78
|
-
throw new Error(`Lexer errors:\n ${errors}`);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (document.parseResult.parserErrors.length > 0) {
|
|
82
|
-
const errors = document.parseResult.parserErrors.map(e => e.message).join('\n ');
|
|
83
|
-
throw new Error(`Parser errors:\n ${errors}`);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const model = document.parseResult.value;
|
|
87
|
-
if (!isModel(model)) {
|
|
88
|
-
throw new Error(`Document root is not a Model`);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Augment AST nodes with SDK properties
|
|
92
|
-
augmentModel(model);
|
|
93
|
-
|
|
94
|
-
return {
|
|
95
|
-
model,
|
|
96
|
-
documents: [document.uri],
|
|
97
|
-
query: fromModel(model),
|
|
98
|
-
};
|
|
189
|
+
return parseTextToContext(text, langServices, shared);
|
|
99
190
|
}
|
package/src/sdk/query.ts
CHANGED
|
@@ -137,9 +137,7 @@ class QueryImpl implements Query {
|
|
|
137
137
|
* Lazily builds and caches indexes on first access.
|
|
138
138
|
*/
|
|
139
139
|
private getIndexes(): ModelIndexes {
|
|
140
|
-
|
|
141
|
-
this.indexes = buildIndexes(this.model);
|
|
142
|
-
}
|
|
140
|
+
this.indexes ??= buildIndexes(this.model);
|
|
143
141
|
return this.indexes;
|
|
144
142
|
}
|
|
145
143
|
|
|
@@ -202,7 +200,12 @@ class QueryImpl implements Query {
|
|
|
202
200
|
|
|
203
201
|
/** @internal Generator for relationship iteration */
|
|
204
202
|
private *iterateRelationships(): Generator<RelationshipView> {
|
|
205
|
-
|
|
203
|
+
yield* this.collectBoundedContextRelationships();
|
|
204
|
+
yield* this.collectContextMapRelationships();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** @internal Collects relationships from bounded contexts */
|
|
208
|
+
private *collectBoundedContextRelationships(): Generator<RelationshipView> {
|
|
206
209
|
for (const node of AstUtils.streamAllContents(this.model)) {
|
|
207
210
|
if (isBoundedContext(node)) {
|
|
208
211
|
for (const rel of node.relationships) {
|
|
@@ -213,8 +216,10 @@ class QueryImpl implements Query {
|
|
|
213
216
|
}
|
|
214
217
|
}
|
|
215
218
|
}
|
|
219
|
+
}
|
|
216
220
|
|
|
217
|
-
|
|
221
|
+
/** @internal Collects relationships from context maps */
|
|
222
|
+
private *collectContextMapRelationships(): Generator<RelationshipView> {
|
|
218
223
|
for (const node of AstUtils.streamAllContents(this.model)) {
|
|
219
224
|
if (isContextMap(node)) {
|
|
220
225
|
for (const rel of node.relationships) {
|
|
@@ -442,10 +447,9 @@ class QueryBuilderImpl<T> implements QueryBuilder<T> {
|
|
|
442
447
|
}
|
|
443
448
|
|
|
444
449
|
first(): T | undefined {
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
return undefined;
|
|
450
|
+
const iterator = this[Symbol.iterator]();
|
|
451
|
+
const result = iterator.next();
|
|
452
|
+
return result.done ? undefined : result.value;
|
|
449
453
|
}
|
|
450
454
|
|
|
451
455
|
toArray(): T[] {
|
|
@@ -554,7 +558,7 @@ class BcQueryBuilderImpl extends QueryBuilderImpl<BoundedContext> implements BcQ
|
|
|
554
558
|
* Escapes special regex characters in a string.
|
|
555
559
|
*/
|
|
556
560
|
function escapeRegex(str: string): string {
|
|
557
|
-
return str.
|
|
561
|
+
return str.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
|
|
558
562
|
}
|
|
559
563
|
|
|
560
564
|
/**
|
|
@@ -780,7 +784,7 @@ function augmentModelInternal(model: Model): void {
|
|
|
780
784
|
} else if (isContextMap(node)) {
|
|
781
785
|
// Augment relationships in context maps (no containing BC)
|
|
782
786
|
for (const rel of node.relationships) {
|
|
783
|
-
augmentRelationship(rel
|
|
787
|
+
augmentRelationship(rel);
|
|
784
788
|
}
|
|
785
789
|
}
|
|
786
790
|
}
|