Skip to content

Commit c2d9852

Browse files
committed
fix: clean up plugin loading and fix VSCode extension
1 parent 6c8f756 commit c2d9852

File tree

9 files changed

+346
-313
lines changed

9 files changed

+346
-313
lines changed

packages/cli/src/actions/action-utils.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { createZModelServices, loadDocument, type ZModelServices } from '@zenstackhq/language';
1+
import { loadDocument, type ZModelServices } from '@zenstackhq/language';
22
import { isDataSource, isPlugin, Model } from '@zenstackhq/language/ast';
33
import { getLiteral } from '@zenstackhq/language/utils';
44
import { PrismaSchemaGenerator } from '@zenstackhq/sdk';
@@ -37,9 +37,7 @@ export function getSchemaFile(file?: string) {
3737
}
3838

3939
export async function loadSchemaDocument(schemaFile: string) {
40-
const { ZModelLanguage: services } = createZModelServices();
41-
const pluginDocs = await getPluginDocuments(services, schemaFile);
42-
const loadResult = await loadDocument(schemaFile, pluginDocs);
40+
const loadResult = await loadDocument(schemaFile);
4341
if (!loadResult.success) {
4442
loadResult.errors.forEach((err) => {
4543
console.error(colors.red(err));

packages/ide/vscode/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
},
1212
"scripts": {
1313
"build": "tsc --noEmit && tsup",
14+
"watch": "tsup --watch",
1415
"lint": "eslint src --ext ts",
1516
"vscode:publish": "pnpm build && vsce publish --no-dependencies --follow-symlinks",
1617
"vscode:package": "pnpm build && vsce package --no-dependencies --follow-symlinks"

packages/ide/vscode/src/language-server/main.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,13 @@ import { createConnection, ProposedFeatures } from 'vscode-languageserver/node.j
77
const connection = createConnection(ProposedFeatures.all);
88

99
// Inject the shared services and language-specific services
10-
const { shared } = createZModelLanguageServices({
11-
connection,
12-
...NodeFileSystem,
13-
});
10+
const { shared } = createZModelLanguageServices(
11+
{
12+
connection,
13+
...NodeFileSystem,
14+
},
15+
true,
16+
);
1417

1518
// Start the language server with the shared services
1619
startLanguageServer(shared);

packages/language/src/document.ts

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
import { isAstNode, URI, type AstNode, type LangiumDocument, type LangiumDocuments, type Mutable } from 'langium';
2+
import fs from 'node:fs';
3+
import path from 'node:path';
4+
import { fileURLToPath } from 'node:url';
5+
import { isDataSource, Model } from './ast';
6+
import { STD_LIB_MODULE_NAME } from './constants';
7+
import { createZModelServices } from './module';
8+
import { getDataModelAndTypeDefs, getDocument, hasAttribute, resolveImport, resolveTransitiveImports } from './utils';
9+
10+
/**
11+
* Loads ZModel document from the given file name. Include the additional document
12+
* files if given.
13+
*/
14+
export async function loadDocument(
15+
fileName: string,
16+
additionalModelFiles: string[] = [],
17+
): Promise<
18+
{ success: true; model: Model; warnings: string[] } | { success: false; errors: string[]; warnings: string[] }
19+
> {
20+
const { ZModelLanguage: services } = createZModelServices(false);
21+
const extensions = services.LanguageMetaData.fileExtensions;
22+
if (!extensions.includes(path.extname(fileName))) {
23+
return {
24+
success: false,
25+
errors: ['invalid schema file extension'],
26+
warnings: [],
27+
};
28+
}
29+
30+
if (!fs.existsSync(fileName)) {
31+
return {
32+
success: false,
33+
errors: ['schema file does not exist'],
34+
warnings: [],
35+
};
36+
}
37+
38+
// load standard library
39+
40+
// isomorphic __dirname
41+
const _dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url));
42+
const stdLib = await services.shared.workspace.LangiumDocuments.getOrCreateDocument(
43+
URI.file(path.resolve(path.join(_dirname, '../res', STD_LIB_MODULE_NAME))),
44+
);
45+
46+
// load the document
47+
const langiumDocuments = services.shared.workspace.LangiumDocuments;
48+
const document = await langiumDocuments.getOrCreateDocument(URI.file(path.resolve(fileName)));
49+
50+
// load imports
51+
const importedURIs = await loadImports(document, langiumDocuments);
52+
const importedDocuments: LangiumDocument[] = [];
53+
for (const uri of importedURIs) {
54+
importedDocuments.push(await langiumDocuments.getOrCreateDocument(uri));
55+
}
56+
57+
// build the document together with standard library, additional modules, and imported documents
58+
59+
// load additional model files
60+
const additionalDocs = await Promise.all(
61+
additionalModelFiles.map((file) =>
62+
services.shared.workspace.LangiumDocuments.getOrCreateDocument(URI.file(path.resolve(file))),
63+
),
64+
);
65+
66+
await services.shared.workspace.DocumentBuilder.build([stdLib, ...additionalDocs, document, ...importedDocuments], {
67+
validation: {
68+
stopAfterLexingErrors: true,
69+
stopAfterParsingErrors: true,
70+
stopAfterLinkingErrors: true,
71+
},
72+
});
73+
74+
const diagnostics = langiumDocuments.all
75+
.flatMap((doc) => (doc.diagnostics ?? []).map((diag) => ({ doc, diag })))
76+
.filter(({ diag }) => diag.severity === 1 || diag.severity === 2)
77+
.toArray();
78+
79+
const errors: string[] = [];
80+
const warnings: string[] = [];
81+
82+
if (diagnostics.length > 0) {
83+
for (const { doc, diag } of diagnostics) {
84+
const message = `${path.relative(process.cwd(), doc.uri.fsPath)}:${
85+
diag.range.start.line + 1
86+
}:${diag.range.start.character + 1} - ${diag.message}`;
87+
88+
if (diag.severity === 1) {
89+
errors.push(message);
90+
} else {
91+
warnings.push(message);
92+
}
93+
}
94+
}
95+
96+
if (errors.length > 0) {
97+
return {
98+
success: false,
99+
errors,
100+
warnings,
101+
};
102+
}
103+
104+
const model = document.parseResult.value as Model;
105+
106+
// merge all declarations into the main document
107+
const imported = mergeImportsDeclarations(langiumDocuments, model);
108+
109+
// remove imported documents
110+
imported.forEach((model) => {
111+
langiumDocuments.deleteDocument(model.$document!.uri);
112+
services.shared.workspace.IndexManager.remove(model.$document!.uri);
113+
});
114+
115+
// extra validation after merging imported declarations
116+
const additionalErrors = validationAfterImportMerge(model);
117+
if (additionalErrors.length > 0) {
118+
return {
119+
success: false,
120+
errors: additionalErrors,
121+
warnings,
122+
};
123+
}
124+
125+
return {
126+
success: true,
127+
model: document.parseResult.value as Model,
128+
warnings,
129+
};
130+
}
131+
132+
async function loadImports(document: LangiumDocument, documents: LangiumDocuments, uris: Set<string> = new Set()) {
133+
const uriString = document.uri.toString();
134+
if (!uris.has(uriString)) {
135+
uris.add(uriString);
136+
const model = document.parseResult.value as Model;
137+
for (const imp of model.imports) {
138+
const importedModel = resolveImport(documents, imp);
139+
if (importedModel) {
140+
const importedDoc = getDocument(importedModel);
141+
await loadImports(importedDoc, documents, uris);
142+
}
143+
}
144+
}
145+
return Array.from(uris)
146+
.filter((x) => uriString != x)
147+
.map((e) => URI.parse(e));
148+
}
149+
150+
function mergeImportsDeclarations(documents: LangiumDocuments, model: Model) {
151+
const importedModels = resolveTransitiveImports(documents, model);
152+
153+
const importedDeclarations = importedModels.flatMap((m) => m.declarations);
154+
model.declarations.push(...importedDeclarations);
155+
156+
// remove import directives
157+
model.imports = [];
158+
159+
// fix $container, $containerIndex, and $containerProperty
160+
linkContentToContainer(model);
161+
162+
return importedModels;
163+
}
164+
165+
function linkContentToContainer(node: AstNode): void {
166+
for (const [name, value] of Object.entries(node)) {
167+
if (!name.startsWith('$')) {
168+
if (Array.isArray(value)) {
169+
value.forEach((item, index) => {
170+
if (isAstNode(item)) {
171+
(item as Mutable<AstNode>).$container = node;
172+
(item as Mutable<AstNode>).$containerProperty = name;
173+
(item as Mutable<AstNode>).$containerIndex = index;
174+
}
175+
});
176+
} else if (isAstNode(value)) {
177+
(value as Mutable<AstNode>).$container = node;
178+
(value as Mutable<AstNode>).$containerProperty = name;
179+
}
180+
}
181+
}
182+
}
183+
184+
function validationAfterImportMerge(model: Model) {
185+
const errors: string[] = [];
186+
const dataSources = model.declarations.filter((d) => isDataSource(d));
187+
if (dataSources.length === 0) {
188+
errors.push('Validation error: schema must have a datasource declaration');
189+
} else {
190+
if (dataSources.length > 1) {
191+
errors.push('Validation error: multiple datasource declarations are not allowed');
192+
}
193+
}
194+
195+
// at most one `@@auth` model
196+
const decls = getDataModelAndTypeDefs(model, true);
197+
const authDecls = decls.filter((d) => hasAttribute(d, '@@auth'));
198+
if (authDecls.length > 1) {
199+
errors.push('Validation error: Multiple `@@auth` declarations are not allowed');
200+
}
201+
return errors;
202+
}

0 commit comments

Comments
 (0)