diff --git a/packages/cli/src/actions/action-utils.ts b/packages/cli/src/actions/action-utils.ts index 49655622..287c5593 100644 --- a/packages/cli/src/actions/action-utils.ts +++ b/packages/cli/src/actions/action-utils.ts @@ -1,13 +1,10 @@ -import { createZModelServices, loadDocument, type ZModelServices } from '@zenstackhq/language'; -import { isDataSource, isPlugin, Model } from '@zenstackhq/language/ast'; -import { getLiteral } from '@zenstackhq/language/utils'; +import { loadDocument } from '@zenstackhq/language'; +import { isDataSource } from '@zenstackhq/language/ast'; import { PrismaSchemaGenerator } from '@zenstackhq/sdk'; import colors from 'colors'; import fs from 'node:fs'; import path from 'node:path'; -import { fileURLToPath } from 'node:url'; import { CliError } from '../cli-error'; -import { PLUGIN_MODULE_NAME } from '../constants'; export function getSchemaFile(file?: string) { if (file) { @@ -37,9 +34,7 @@ export function getSchemaFile(file?: string) { } export async function loadSchemaDocument(schemaFile: string) { - const { ZModelLanguage: services } = createZModelServices(); - const pluginDocs = await getPluginDocuments(services, schemaFile); - const loadResult = await loadDocument(schemaFile, pluginDocs); + const loadResult = await loadDocument(schemaFile); if (!loadResult.success) { loadResult.errors.forEach((err) => { console.error(colors.red(err)); @@ -52,63 +47,6 @@ export async function loadSchemaDocument(schemaFile: string) { return loadResult.model; } -export async function getPluginDocuments(services: ZModelServices, fileName: string): Promise { - // parse the user document (without validation) - const parseResult = services.parser.LangiumParser.parse(fs.readFileSync(fileName, { encoding: 'utf-8' })); - const parsed = parseResult.value as Model; - - // balk if there are syntax errors - if (parseResult.lexerErrors.length > 0 || parseResult.parserErrors.length > 0) { - return []; - } - - // traverse plugins and collect "plugin.zmodel" documents - const result: string[] = []; - for (const decl of parsed.declarations.filter(isPlugin)) { - const providerField = decl.fields.find((f) => f.name === 'provider'); - if (!providerField) { - continue; - } - - const provider = getLiteral(providerField.value); - if (!provider) { - continue; - } - - let pluginModelFile: string | undefined; - - // first try to treat provider as a path - let providerPath = path.resolve(path.dirname(fileName), provider); - if (fs.existsSync(providerPath)) { - if (fs.statSync(providerPath).isDirectory()) { - providerPath = path.join(providerPath, 'index.js'); - } - - // try plugin.zmodel next to the provider file - pluginModelFile = path.resolve(path.dirname(providerPath), PLUGIN_MODULE_NAME); - if (!fs.existsSync(pluginModelFile)) { - // try to find upwards - pluginModelFile = findUp([PLUGIN_MODULE_NAME], path.dirname(providerPath)); - } - } - - if (!pluginModelFile) { - // try loading it as a ESM module - try { - const resolvedUrl = import.meta.resolve(`${provider}/${PLUGIN_MODULE_NAME}`); - pluginModelFile = fileURLToPath(resolvedUrl); - } catch { - // noop - } - } - - if (pluginModelFile && fs.existsSync(pluginModelFile)) { - result.push(pluginModelFile); - } - } - return result; -} - export function handleSubProcessError(err: unknown) { if (err instanceof Error && 'status' in err && typeof err.status === 'number') { process.exit(err.status); diff --git a/packages/ide/vscode/package.json b/packages/ide/vscode/package.json index ac3667af..44f22fcc 100644 --- a/packages/ide/vscode/package.json +++ b/packages/ide/vscode/package.json @@ -11,6 +11,7 @@ }, "scripts": { "build": "tsc --noEmit && tsup", + "watch": "tsup --watch", "lint": "eslint src --ext ts", "vscode:publish": "pnpm build && vsce publish --no-dependencies --follow-symlinks", "vscode:package": "pnpm build && vsce package --no-dependencies --follow-symlinks" diff --git a/packages/ide/vscode/src/language-server/main.ts b/packages/ide/vscode/src/language-server/main.ts index b9ac6998..efa21569 100644 --- a/packages/ide/vscode/src/language-server/main.ts +++ b/packages/ide/vscode/src/language-server/main.ts @@ -7,10 +7,13 @@ import { createConnection, ProposedFeatures } from 'vscode-languageserver/node.j const connection = createConnection(ProposedFeatures.all); // Inject the shared services and language-specific services -const { shared } = createZModelLanguageServices({ - connection, - ...NodeFileSystem, -}); +const { shared } = createZModelLanguageServices( + { + connection, + ...NodeFileSystem, + }, + true, +); // Start the language server with the shared services startLanguageServer(shared); diff --git a/packages/language/src/document.ts b/packages/language/src/document.ts new file mode 100644 index 00000000..b8405c48 --- /dev/null +++ b/packages/language/src/document.ts @@ -0,0 +1,202 @@ +import { isAstNode, URI, type AstNode, type LangiumDocument, type LangiumDocuments, type Mutable } from 'langium'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { isDataSource, type Model } from './ast'; +import { STD_LIB_MODULE_NAME } from './constants'; +import { createZModelServices } from './module'; +import { getDataModelAndTypeDefs, getDocument, hasAttribute, resolveImport, resolveTransitiveImports } from './utils'; + +/** + * Loads ZModel document from the given file name. Include the additional document + * files if given. + */ +export async function loadDocument( + fileName: string, + additionalModelFiles: string[] = [], +): Promise< + { success: true; model: Model; warnings: string[] } | { success: false; errors: string[]; warnings: string[] } +> { + const { ZModelLanguage: services } = createZModelServices(false); + const extensions = services.LanguageMetaData.fileExtensions; + if (!extensions.includes(path.extname(fileName))) { + return { + success: false, + errors: ['invalid schema file extension'], + warnings: [], + }; + } + + if (!fs.existsSync(fileName)) { + return { + success: false, + errors: ['schema file does not exist'], + warnings: [], + }; + } + + // load standard library + + // isomorphic __dirname + const _dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + const stdLib = await services.shared.workspace.LangiumDocuments.getOrCreateDocument( + URI.file(path.resolve(path.join(_dirname, '../res', STD_LIB_MODULE_NAME))), + ); + + // load the document + const langiumDocuments = services.shared.workspace.LangiumDocuments; + const document = await langiumDocuments.getOrCreateDocument(URI.file(path.resolve(fileName))); + + // load imports + const importedURIs = await loadImports(document, langiumDocuments); + const importedDocuments: LangiumDocument[] = []; + for (const uri of importedURIs) { + importedDocuments.push(await langiumDocuments.getOrCreateDocument(uri)); + } + + // build the document together with standard library, additional modules, and imported documents + + // load additional model files + const additionalDocs = await Promise.all( + additionalModelFiles.map((file) => + services.shared.workspace.LangiumDocuments.getOrCreateDocument(URI.file(path.resolve(file))), + ), + ); + + await services.shared.workspace.DocumentBuilder.build([stdLib, ...additionalDocs, document, ...importedDocuments], { + validation: { + stopAfterLexingErrors: true, + stopAfterParsingErrors: true, + stopAfterLinkingErrors: true, + }, + }); + + const diagnostics = langiumDocuments.all + .flatMap((doc) => (doc.diagnostics ?? []).map((diag) => ({ doc, diag }))) + .filter(({ diag }) => diag.severity === 1 || diag.severity === 2) + .toArray(); + + const errors: string[] = []; + const warnings: string[] = []; + + if (diagnostics.length > 0) { + for (const { doc, diag } of diagnostics) { + const message = `${path.relative(process.cwd(), doc.uri.fsPath)}:${ + diag.range.start.line + 1 + }:${diag.range.start.character + 1} - ${diag.message}`; + + if (diag.severity === 1) { + errors.push(message); + } else { + warnings.push(message); + } + } + } + + if (errors.length > 0) { + return { + success: false, + errors, + warnings, + }; + } + + const model = document.parseResult.value as Model; + + // merge all declarations into the main document + const imported = mergeImportsDeclarations(langiumDocuments, model); + + // remove imported documents + imported.forEach((model) => { + langiumDocuments.deleteDocument(model.$document!.uri); + services.shared.workspace.IndexManager.remove(model.$document!.uri); + }); + + // extra validation after merging imported declarations + const additionalErrors = validationAfterImportMerge(model); + if (additionalErrors.length > 0) { + return { + success: false, + errors: additionalErrors, + warnings, + }; + } + + return { + success: true, + model: document.parseResult.value as Model, + warnings, + }; +} + +async function loadImports(document: LangiumDocument, documents: LangiumDocuments, uris: Set = new Set()) { + const uriString = document.uri.toString(); + if (!uris.has(uriString)) { + uris.add(uriString); + const model = document.parseResult.value as Model; + for (const imp of model.imports) { + const importedModel = resolveImport(documents, imp); + if (importedModel) { + const importedDoc = getDocument(importedModel); + await loadImports(importedDoc, documents, uris); + } + } + } + return Array.from(uris) + .filter((x) => uriString != x) + .map((e) => URI.parse(e)); +} + +function mergeImportsDeclarations(documents: LangiumDocuments, model: Model) { + const importedModels = resolveTransitiveImports(documents, model); + + const importedDeclarations = importedModels.flatMap((m) => m.declarations); + model.declarations.push(...importedDeclarations); + + // remove import directives + model.imports = []; + + // fix $container, $containerIndex, and $containerProperty + linkContentToContainer(model); + + return importedModels; +} + +function linkContentToContainer(node: AstNode): void { + for (const [name, value] of Object.entries(node)) { + if (!name.startsWith('$')) { + if (Array.isArray(value)) { + value.forEach((item, index) => { + if (isAstNode(item)) { + (item as Mutable).$container = node; + (item as Mutable).$containerProperty = name; + (item as Mutable).$containerIndex = index; + } + }); + } else if (isAstNode(value)) { + (value as Mutable).$container = node; + (value as Mutable).$containerProperty = name; + } + } + } +} + +function validationAfterImportMerge(model: Model) { + const errors: string[] = []; + const dataSources = model.declarations.filter((d) => isDataSource(d)); + if (dataSources.length === 0) { + errors.push('Validation error: schema must have a datasource declaration'); + } else { + if (dataSources.length > 1) { + errors.push('Validation error: multiple datasource declarations are not allowed'); + } + } + + // at most one `@@auth` model + const decls = getDataModelAndTypeDefs(model, true); + const authDecls = decls.filter((d) => hasAttribute(d, '@@auth')); + if (authDecls.length > 1) { + errors.push('Validation error: Multiple `@@auth` declarations are not allowed'); + } + return errors; +} diff --git a/packages/language/src/index.ts b/packages/language/src/index.ts index d6e0d72f..6edc0494 100644 --- a/packages/language/src/index.ts +++ b/packages/language/src/index.ts @@ -1,210 +1,2 @@ -import { isAstNode, URI, type LangiumDocument, type LangiumDocuments, type Mutable } from 'langium'; -import { NodeFileSystem } from 'langium/node'; -import fs from 'node:fs'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { isDataSource, type AstNode, type Model } from './ast'; -import { STD_LIB_MODULE_NAME } from './constants'; -import { createZModelLanguageServices } from './module'; -import { getDataModelAndTypeDefs, getDocument, hasAttribute, resolveImport, resolveTransitiveImports } from './utils'; - -export function createZModelServices() { - return createZModelLanguageServices(NodeFileSystem); -} - -export class DocumentLoadError extends Error { - constructor(message: string) { - super(message); - } -} - -export async function loadDocument( - fileName: string, - additionalModelFiles: string[] = [], -): Promise< - { success: true; model: Model; warnings: string[] } | { success: false; errors: string[]; warnings: string[] } -> { - const { ZModelLanguage: services } = createZModelServices(); - const extensions = services.LanguageMetaData.fileExtensions; - if (!extensions.includes(path.extname(fileName))) { - return { - success: false, - errors: ['invalid schema file extension'], - warnings: [], - }; - } - - if (!fs.existsSync(fileName)) { - return { - success: false, - errors: ['schema file does not exist'], - warnings: [], - }; - } - - // load standard library - - // isomorphic __dirname - const _dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); - const stdLib = await services.shared.workspace.LangiumDocuments.getOrCreateDocument( - URI.file(path.resolve(path.join(_dirname, '../res', STD_LIB_MODULE_NAME))), - ); - - // load additional model files - const additionalDocs = await Promise.all( - additionalModelFiles.map((file) => - services.shared.workspace.LangiumDocuments.getOrCreateDocument(URI.file(path.resolve(file))), - ), - ); - - // load the document - const langiumDocuments = services.shared.workspace.LangiumDocuments; - const document = await langiumDocuments.getOrCreateDocument(URI.file(path.resolve(fileName))); - - // load imports - const importedURIs = await loadImports(document, langiumDocuments); - const importedDocuments: LangiumDocument[] = []; - for (const uri of importedURIs) { - importedDocuments.push(await langiumDocuments.getOrCreateDocument(uri)); - } - - // build the document together with standard library, plugin modules, and imported documents - await services.shared.workspace.DocumentBuilder.build([stdLib, ...additionalDocs, document, ...importedDocuments], { - validation: { - stopAfterLexingErrors: true, - stopAfterParsingErrors: true, - stopAfterLinkingErrors: true, - }, - }); - - const diagnostics = langiumDocuments.all - .flatMap((doc) => (doc.diagnostics ?? []).map((diag) => ({ doc, diag }))) - .filter(({ diag }) => diag.severity === 1 || diag.severity === 2) - .toArray(); - - const errors: string[] = []; - const warnings: string[] = []; - - if (diagnostics.length > 0) { - for (const { doc, diag } of diagnostics) { - const message = `${path.relative(process.cwd(), doc.uri.fsPath)}:${ - diag.range.start.line + 1 - }:${diag.range.start.character + 1} - ${diag.message}`; - - if (diag.severity === 1) { - errors.push(message); - } else { - warnings.push(message); - } - } - } - - if (errors.length > 0) { - return { - success: false, - errors, - warnings, - }; - } - - const model = document.parseResult.value as Model; - - // merge all declarations into the main document - const imported = mergeImportsDeclarations(langiumDocuments, model); - - // remove imported documents - imported.forEach((model) => { - langiumDocuments.deleteDocument(model.$document!.uri); - services.shared.workspace.IndexManager.remove(model.$document!.uri); - }); - - // extra validation after merging imported declarations - const additionalErrors = validationAfterImportMerge(model); - if (additionalErrors.length > 0) { - return { - success: false, - errors: additionalErrors, - warnings, - }; - } - - return { - success: true, - model: document.parseResult.value as Model, - warnings, - }; -} - -async function loadImports(document: LangiumDocument, documents: LangiumDocuments, uris: Set = new Set()) { - const uriString = document.uri.toString(); - if (!uris.has(uriString)) { - uris.add(uriString); - const model = document.parseResult.value as Model; - for (const imp of model.imports) { - const importedModel = resolveImport(documents, imp); - if (importedModel) { - const importedDoc = getDocument(importedModel); - await loadImports(importedDoc, documents, uris); - } - } - } - return Array.from(uris) - .filter((x) => uriString != x) - .map((e) => URI.parse(e)); -} - -function mergeImportsDeclarations(documents: LangiumDocuments, model: Model) { - const importedModels = resolveTransitiveImports(documents, model); - - const importedDeclarations = importedModels.flatMap((m) => m.declarations); - model.declarations.push(...importedDeclarations); - - // remove import directives - model.imports = []; - - // fix $container, $containerIndex, and $containerProperty - linkContentToContainer(model); - - return importedModels; -} - -function linkContentToContainer(node: AstNode): void { - for (const [name, value] of Object.entries(node)) { - if (!name.startsWith('$')) { - if (Array.isArray(value)) { - value.forEach((item, index) => { - if (isAstNode(item)) { - (item as Mutable).$container = node; - (item as Mutable).$containerProperty = name; - (item as Mutable).$containerIndex = index; - } - }); - } else if (isAstNode(value)) { - (value as Mutable).$container = node; - (value as Mutable).$containerProperty = name; - } - } - } -} - -function validationAfterImportMerge(model: Model) { - const errors: string[] = []; - const dataSources = model.declarations.filter((d) => isDataSource(d)); - if (dataSources.length === 0) { - errors.push('Validation error: schema must have a datasource declaration'); - } else { - if (dataSources.length > 1) { - errors.push('Validation error: multiple datasource declarations are not allowed'); - } - } - - // at most one `@@auth` model - const decls = getDataModelAndTypeDefs(model, true); - const authDecls = decls.filter((d) => hasAttribute(d, '@@auth')); - if (authDecls.length > 1) { - errors.push('Validation error: Multiple `@@auth` declarations are not allowed'); - } - return errors; -} - +export { loadDocument } from './document'; export * from './module'; diff --git a/packages/language/src/module.ts b/packages/language/src/module.ts index 1b853945..cff4ac0e 100644 --- a/packages/language/src/module.ts +++ b/packages/language/src/module.ts @@ -1,4 +1,4 @@ -import { inject, type DeepPartial, type Module } from 'langium'; +import { DocumentState, inject, URI, type DeepPartial, type Module } from 'langium'; import { createDefaultModule, createDefaultSharedModule, @@ -7,8 +7,13 @@ import { type LangiumSharedServices, type PartialLangiumServices, } from 'langium/lsp'; +import { NodeFileSystem } from 'langium/node'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import type { Model } from './ast'; import { ZModelGeneratedModule, ZModelGeneratedSharedModule, ZModelLanguageMetaData } from './generated/module'; -import { ZModelValidator, registerValidationChecks } from './validator'; +import { getPluginDocuments } from './utils'; +import { registerValidationChecks, ZModelValidator } from './validator'; import { ZModelDocumentBuilder } from './zmodel-document-builder'; import { ZModelLinker } from './zmodel-linker'; import { ZModelScopeComputation, ZModelScopeProvider } from './zmodel-scope'; @@ -70,7 +75,10 @@ export const ZModelSharedModule: Module { + for (const doc of documents) { + if (doc.parseResult.lexerErrors.length > 0 || doc.parseResult.parserErrors.length > 0) { + // balk if there are lexer or parser errors + continue; + } + + const schemaPath = fileURLToPath(doc.uri.toString()); + const pluginSchemas = getPluginDocuments(doc.parseResult.value as Model, schemaPath); + for (const plugin of pluginSchemas) { + // load the plugin model document + const pluginDoc = await shared.workspace.LangiumDocuments.getOrCreateDocument( + URI.file(path.resolve(plugin)), + ); + // add to indexer so the plugin model's definitions are globally visible + shared.workspace.IndexManager.updateContent(pluginDoc); + if (logToConsole) { + console.log(`Loaded plugin model: ${plugin}`); + } + } + } + }); + return { shared, ZModelLanguage }; } + +// TODO: proper logging system +export function createZModelServices(logToConsole = false) { + return createZModelLanguageServices(NodeFileSystem, logToConsole); +} diff --git a/packages/language/src/utils.ts b/packages/language/src/utils.ts index 3e24285f..c361feee 100644 --- a/packages/language/src/utils.ts +++ b/packages/language/src/utils.ts @@ -1,10 +1,10 @@ import { AstUtils, URI, type AstNode, type LangiumDocument, type LangiumDocuments, type Reference } from 'langium'; import fs from 'node:fs'; -import path from 'path'; -import { STD_LIB_MODULE_NAME, type ExpressionContext } from './constants'; +import { createRequire } from 'node:module'; +import path from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; +import { PLUGIN_MODULE_NAME, STD_LIB_MODULE_NAME, type ExpressionContext } from './constants'; import { - BinaryExpr, - ConfigExpr, isArrayExpr, isBinaryExpr, isConfigArrayExpr, @@ -17,15 +17,15 @@ import { isMemberAccessExpr, isModel, isObjectExpr, + isPlugin, isReferenceExpr, isStringLiteral, isTypeDef, - Model, - ModelImport, - ReferenceExpr, type Attribute, type AttributeParam, + type BinaryExpr, type BuiltinType, + type ConfigExpr, type DataField, type DataFieldAttribute, type DataModel, @@ -35,6 +35,9 @@ import { type Expression, type ExpressionType, type FunctionDecl, + type Model, + type ModelImport, + type ReferenceExpr, type TypeDef, } from './generated/ast'; @@ -573,6 +576,91 @@ export function getDocument(node: AstNode): Langium return result as LangiumDocument; } +export function getPluginDocuments(model: Model, schemaPath: string): string[] { + // traverse plugins and collect "plugin.zmodel" documents + const result: string[] = []; + for (const decl of model.declarations.filter(isPlugin)) { + const providerField = decl.fields.find((f) => f.name === 'provider'); + if (!providerField) { + continue; + } + + const provider = getLiteral(providerField.value); + if (!provider) { + continue; + } + + let pluginModelFile: string | undefined; + + // first try to treat provider as a path + let providerPath = path.resolve(path.dirname(schemaPath), provider); + if (fs.existsSync(providerPath)) { + if (fs.statSync(providerPath).isDirectory()) { + providerPath = path.join(providerPath, 'index.js'); + } + + // try plugin.zmodel next to the provider file + pluginModelFile = path.resolve(path.dirname(providerPath), PLUGIN_MODULE_NAME); + if (!fs.existsSync(pluginModelFile)) { + // try to find upwards + pluginModelFile = findUp([PLUGIN_MODULE_NAME], path.dirname(providerPath)); + } + } + + if (!pluginModelFile) { + if (typeof import.meta.resolve === 'function') { + try { + // try loading as a ESM module + const resolvedUrl = import.meta.resolve(`${provider}/${PLUGIN_MODULE_NAME}`); + pluginModelFile = fileURLToPath(resolvedUrl); + } catch { + // noop + } + } + } + + if (!pluginModelFile) { + // try loading as a CJS module + try { + const require = createRequire(pathToFileURL(schemaPath)); + pluginModelFile = require.resolve(`${provider}/${PLUGIN_MODULE_NAME}`); + } catch { + // noop + } + } + + if (pluginModelFile && fs.existsSync(pluginModelFile)) { + result.push(pluginModelFile); + } + } + return result; +} + +type FindUpResult = Multiple extends true ? string[] | undefined : string | undefined; + +function findUp( + names: string[], + cwd: string = process.cwd(), + multiple: Multiple = false as Multiple, + result: string[] = [], +): FindUpResult { + if (!names.some((name) => !!name)) { + return undefined; + } + const target = names.find((name) => fs.existsSync(path.join(cwd, name))); + if (multiple === false && target) { + return path.join(cwd, target) as FindUpResult; + } + if (target) { + result.push(path.join(cwd, target)); + } + const up = path.resolve(cwd, '..'); + if (up === cwd) { + return (multiple && result.length > 0 ? result : undefined) as FindUpResult; + } + return findUp(names, up, multiple, result); +} + /** * Returns the root node of the given AST node by following the `$container` references. */ diff --git a/packages/language/src/zmodel-workspace-manager.ts b/packages/language/src/zmodel-workspace-manager.ts index 7b21b56b..f21db797 100644 --- a/packages/language/src/zmodel-workspace-manager.ts +++ b/packages/language/src/zmodel-workspace-manager.ts @@ -1,7 +1,6 @@ import { DefaultWorkspaceManager, URI, - UriUtils, type AstNode, type LangiumDocument, type LangiumDocumentFactory, @@ -11,9 +10,7 @@ import type { LangiumSharedServices } from 'langium/lsp'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import { isPlugin, type Model } from './ast'; -import { PLUGIN_MODULE_NAME, STD_LIB_MODULE_NAME } from './constants'; -import { getLiteral } from './utils'; +import { STD_LIB_MODULE_NAME } from './constants'; export class ZModelWorkspaceManager extends DefaultWorkspaceManager { private documentFactory: LangiumDocumentFactory; @@ -71,87 +68,5 @@ export class ZModelWorkspaceManager extends DefaultWorkspaceManager { const stdlib = await this.documentFactory.fromUri(URI.file(stdLibPath)); collector(stdlib); - - const documents = this.langiumDocuments.all; - const pluginModels = new Set(); - - // find plugin models - documents.forEach((doc) => { - const parsed = doc.parseResult.value as Model; - parsed.declarations.forEach((decl) => { - if (isPlugin(decl)) { - const providerField = decl.fields.find((f) => f.name === 'provider'); - if (providerField) { - const provider = getLiteral(providerField.value); - if (provider) { - pluginModels.add(provider); - } - } - } - }); - }); - - if (pluginModels.size > 0) { - console.log(`Used plugin modules: ${Array.from(pluginModels)}`); - - // the loaded plugin models would be removed from the set - const pendingPluginModules = new Set(pluginModels); - - await Promise.all( - folders - .map((wf) => [wf, this.getRootFolder(wf)] as [WorkspaceFolder, URI]) - .map(async (entry) => this.loadPluginModels(...entry, pendingPluginModules, collector)), - ); - } - } - - protected async loadPluginModels( - workspaceFolder: WorkspaceFolder, - folderPath: URI, - pendingPluginModels: Set, - collector: (document: LangiumDocument) => void, - ): Promise { - const content = (await this.fileSystemProvider.readDirectory(folderPath)).sort((a, b) => { - // make sure the node_modules folder is always the first one to be checked - // so we can exit early if the plugin is found - if (a.isDirectory && b.isDirectory) { - const aName = UriUtils.basename(a.uri); - if (aName === 'node_modules') { - return -1; - } else { - return 1; - } - } else { - return 0; - } - }); - - for (const entry of content) { - if (entry.isDirectory) { - const name = UriUtils.basename(entry.uri); - if (name === 'node_modules') { - for (const plugin of Array.from(pendingPluginModels)) { - const path = UriUtils.joinPath(entry.uri, plugin, PLUGIN_MODULE_NAME); - try { - await this.fileSystemProvider.readFile(path); - const document = await this.langiumDocuments.getOrCreateDocument(path); - collector(document); - console.log(`Adding plugin document from ${path.path}`); - - pendingPluginModels.delete(plugin); - // early exit if all plugins are loaded - if (pendingPluginModels.size === 0) { - return; - } - } catch { - // no-op. The module might be found in another node_modules folder - // will show the warning message eventually if not found - } - } - } else { - await this.loadPluginModels(workspaceFolder, entry.uri, pendingPluginModels, collector); - } - } - } } } diff --git a/samples/blog/zenstack/schema.zmodel b/samples/blog/zenstack/schema.zmodel index 708afb9e..6cf112e2 100644 --- a/samples/blog/zenstack/schema.zmodel +++ b/samples/blog/zenstack/schema.zmodel @@ -10,6 +10,7 @@ enum Role { } plugin policy { + // due to pnpm layout we can't directly use package name here provider = '../node_modules/@zenstackhq/plugin-policy/dist/index.js' }