Skip to content

Conversation

@ymc9
Copy link
Member

@ymc9 ymc9 commented Oct 17, 2025

Summary by CodeRabbit

  • New Features

    • Added continuous watch mode for VSCode builds to enable live rebuilding during development.
    • Language tooling now supports an optional console logging toggle for clearer startup feedback.
  • Refactor

    • Simplified document loading and validation flow for more consistent error/warning reporting.
    • Reworked plugin document discovery and integration to streamline model loading and initialization.

Copilot AI review requested due to automatic review settings October 17, 2025 19:19
@coderabbitai
Copy link

coderabbitai bot commented Oct 17, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Centralizes document loading and plugin discovery into the language package: adds a dedicated loadDocument module, introduces plugin resolution and optional console logging in the language module, removes local plugin discovery from CLI and workspace manager, updates language-server call, and adds a vscode watch script and a small schema comment.

Changes

Cohort / File(s) Summary
Language: document loader
packages/language/src/document.ts
New loadDocument(fileName, additionalModelFiles?) implementation that validates extensions, loads stdlib, resolves and loads imports recursively, merges imported declarations, performs post-merge validation, and returns structured success/errors with warnings.
Language: plugin resolution & services
packages/language/src/utils.ts, packages/language/src/module.ts
Added getPluginDocuments(model, schemaPath) and supporting find-up utilities; extended createZModelLanguageServices(context, logToConsole?) to optionally load plugin documents during build with console logging; added createZModelServices(logToConsole) wrapper.
Language: public API surface
packages/language/src/index.ts
Moved loadDocument implementation into document.ts and re-exported it; removed DocumentLoadError and createZModelServices from the index.
CLI: simplify schema loading
packages/cli/src/actions/action-utils.ts
Removed plugin/document-collection logic and helper export (getPluginDocuments); loadSchemaDocument now directly calls loadDocument(schemaFile) and throws CliError on failure.
Workspace manager: remove plugin discovery
packages/language/src/zmodel-workspace-manager.ts
Removed local plugin-model discovery and loading logic (deleted helper and related imports).
IDE: language server integration
packages/ide/vscode/src/language-server/main.ts
Updated call to createZModelLanguageServices(...) to pass true as second argument to enable console logging.
IDE: build script
packages/ide/vscode/package.json
Added "watch": "tsup --watch" npm script for continuous rebuilding.
Samples
samples/blog/zenstack/schema.zmodel
Added a comment line inside plugin policy block (non-functional documentation change).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Changes introduce new loading and plugin-resolution logic, modify public API exports, and remove previously distributed plugin handling across multiple modules; review requires checking new import/resolution strategies, validation rules, and integration points.

Possibly related PRs

Poem

🐰 Hop, I hopped through code and found the seam,

Plugins now gather where the language team dreams.
I nibble imports, merge each tiny part,
Watch tsup rebuild — a live dev's heart. ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.77% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The PR title "fix: clean up plugin loading and fix VSCode extension" directly relates to the primary changes in the changeset. The title accurately identifies two main aspects: plugin infrastructure cleanup (evident in removal of plugin infrastructure from action-utils, reorganization into utils.ts, and refactoring in module.ts) and VSCode extension improvements (evident in vscode package.json watch script addition and language-server initialization changes). The title is concise, specific enough for a teammate scanning history to understand the primary focus, and does not include misleading information. While the PR also includes supporting refactoring such as moving loadDocument to a separate module, the instructions acknowledge that titles need not cover every detail of the changeset.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/cleanup-plugin-loading

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@claude
Copy link

claude bot commented Oct 17, 2025

Claude finished @ymc9's task —— View job


Code Review: Plugin Loading Cleanup and VSCode Extension Fixes

I've conducted a thorough review of this PR which refactors plugin loading and fixes VSCode extension issues. Here's my analysis:

Todo List:

  • Read repository setup and guidelines from CLAUDE.md
  • Analyze the plugin loading refactoring changes
  • Review VSCode extension modifications
  • Check code quality and best practices
  • Identify potential bugs or issues
  • Assess performance considerations
  • Review security implications
  • Evaluate test coverage needs
  • Provide comprehensive feedback and recommendations

🔍 Review Summary

This PR successfully refactors plugin loading logic and improves the VSCode extension. The changes are well-structured with good separation of concerns.

Strengths

1. Excellent Code Organization

  • Clean separation: Moving document loading logic from index.ts to dedicated document.ts file improves maintainability
  • Modular design: The refactoring creates clearer boundaries between modules
  • DRY principle: Plugin loading logic is now consolidated and reused between CLI and language services

2. Robust Plugin Discovery

  • Multi-strategy approach: The plugin loading supports both file paths and npm package resolution
  • Fallback mechanisms: Good error handling with graceful degradation when plugins aren't found
  • Cross-platform compatibility: Uses proper path resolution for both ESM and CommonJS environments

3. VSCode Extension Improvements

  • Better stdlib loading: The workspace manager now properly resolves the standard library from installed packages
  • Graceful fallbacks: Falls back to bundled stdlib when installed package isn't available
  • Improved logging: Helpful console messages for debugging plugin and stdlib resolution

⚠️ Areas for Improvement

1. Code Duplication (utils.ts:578-631 vs action-utils.ts:53-108)

The getPluginDocuments function is duplicated between these files with minor differences:

  • CLI version: Uses parser services directly
  • Language version: Works with AST model directly

Recommendation: Consider creating a shared utility function to eliminate this duplication.

2. Error Handling Improvements

// In utils.ts:611-624
try {
    if (typeof import.meta.resolve === 'function') {
        // ESM resolution
    } else {
        // CJS resolution with require.resolve
    }
} catch {
    // Silent failure
}

Issue: The catch block silently ignores all errors, making debugging difficult.

Recommendation: Add optional logging parameter or differentiate between expected vs unexpected errors.

3. Type Safety (action-utils.ts:140)

let pkgJson: any = undefined;

Issue: Using any type reduces type safety.

Recommendation: Define a proper interface for package.json structure:

interface PackageJsonZenStack {
    zenstack?: {
        schema?: string;
        output?: string;
    };
}

4. File System Race Conditions

Multiple fs.existsSync() calls without proper synchronization could theoretically cause issues in concurrent scenarios, though this is minor for CLI usage.

🚀 Performance Considerations

Positive impacts:

  • Lazy loading: Plugin documents are only loaded when needed
  • Efficient caching: Langium's document management handles caching appropriately
  • Reduced startup time: Better separation allows for more selective loading

No significant performance concerns identified.

🔒 Security Analysis

Good practices observed:

  • Path validation: Proper path resolution prevents directory traversal
  • Module resolution: Uses Node.js built-in resolution mechanisms
  • No code execution: Plugin loading only reads schema files, not executable code

No security vulnerabilities identified.

🧪 Test Coverage Recommendations

The refactoring touches core functionality that should have comprehensive tests:

  1. Plugin Loading Tests:

    • Test plugin discovery from file paths
    • Test npm package resolution
    • Test fallback behavior when plugins aren't found
    • Test error handling for malformed plugin configurations
  2. Document Loading Tests:

    • Test loadDocument function with various schema configurations
    • Test import resolution and merging
    • Test validation after import merging
  3. VSCode Extension Tests:

    • Test stdlib resolution from installed packages
    • Test fallback to bundled stdlib
    • Test workspace folder scenarios

📋 Minor Issues

  1. Inconsistent commenting: Some functions lack JSDoc comments while others are well-documented
  2. Magic strings: Consider extracting 'index.js' and other magic strings to constants
  3. Console logging: The language server logs to console which might not be appropriate for all environments

🎯 Overall Assessment

This is a high-quality refactoring that successfully:

  • ✅ Improves code organization and maintainability
  • ✅ Fixes VSCode extension issues
  • ✅ Maintains backward compatibility
  • ✅ Follows established patterns in the codebase
  • ✅ Handles edge cases appropriately

The changes align well with the ZenStack architecture described in CLAUDE.md and follow the monorepo structure correctly.

Recommendation: Approve with minor suggestions for improvement. The benefits far outweigh the minor issues identified.


Job Run | Branch: fix/cleanup-plugin-loading

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR refactors plugin model loading to occur during document build (fixing plugin resolution issues, especially in the VSCode extension) and cleans up related APIs and usage.

  • Move plugin discovery/loading from the workspace manager into a build-phase hook
  • Introduce getPluginDocuments to resolve plugin model files from provider settings
  • Split document-loading logic into a new document module and simplify CLI usage

Reviewed Changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
samples/blog/zenstack/schema.zmodel Comment update clarifying provider path usage in pnpm layouts
packages/language/src/zmodel-workspace-manager.ts Remove plugin discovery/loading; keep stdlib loading only
packages/language/src/utils.ts Add getPluginDocuments and findUp utilities; update imports
packages/language/src/module.ts Add build-phase hook to auto-load plugin models; add createZModelServices; optional logging
packages/language/src/index.ts Re-export loadDocument from new document module; export module APIs
packages/language/src/document.ts New: encapsulate document loading/building/validation and import merging
packages/ide/vscode/src/language-server/main.ts Use new createZModelLanguageServices signature and enable console logging
packages/ide/vscode/package.json Add watch script
packages/cli/src/actions/action-utils.ts Simplify schema loading; rely on automatic plugin loading

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/language/src/zmodel-workspace-manager.ts (1)

38-51: ESM safety: replace require.resolve with createRequire/import.meta.resolve

In ESM, require is undefined. This path will throw in VS Code extension runtimes. Use createRequire or import.meta.resolve with a CJS fallback.

Apply:

+import { createRequire } from 'node:module';
@@
-                const languagePackagePath = require.resolve('@zenstackhq/language/package.json', {
-                    paths: [folderPath],
-                });
+                // Resolve from the workspace folder in both ESM and CJS
+                let languagePackagePath: string;
+                if (typeof import.meta !== 'undefined' && typeof (import.meta as any).resolve === 'function') {
+                    languagePackagePath = fileURLToPath(
+                        (import.meta as any).resolve('@zenstackhq/language/package.json', pathToFileURL(folderPath)),
+                    );
+                } else {
+                    const req = createRequire(path.join(folderPath, 'package.json'));
+                    languagePackagePath = req.resolve('@zenstackhq/language/package.json');
+                }
🧹 Nitpick comments (11)
packages/language/src/zmodel-workspace-manager.ts (1)

48-49: Gate console logging

These logs can spam users. Gate behind a flag or the LSP connection logger.

Example:

-                    console.log(`Found installed zenstack package stdlib at: ${installedStdlibPath}`);
+                    if (process.env.ZENSTACK_LOG === '1') {
+                        console.log(`Found installed zenstack package stdlib at: ${installedStdlibPath}`);
+                    }
@@
-            console.log(`Using bundled stdlib in extension:`, stdLibPath);
+            if (process.env.ZENSTACK_LOG === '1') {
+                console.log(`Using bundled stdlib in extension: ${stdLibPath}`);
+            }

Also applies to: 66-67

packages/cli/src/actions/action-utils.ts (1)

1-11: Prune unused imports after centralizing loadDocument

Now that plugin loading is handled in the language services, these imports are unused in this file.

Apply:

-import { 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 { fileURLToPath } from 'node:url';
@@
-import { PLUGIN_MODULE_NAME } from '../constants';
packages/language/src/utils.ts (1)

578-631: Deduplicate results to avoid repeated indexing of the same plugin

If multiple plugin declarations resolve to the same file, we may push duplicates.

Apply:

-export function getPluginDocuments(model: Model, schemaPath: string): string[] {
+export function getPluginDocuments(model: Model, schemaPath: string): string[] {
     // traverse plugins and collect "plugin.zmodel" documents
-    const result: string[] = [];
+    const result = new Set<string>();
@@
-        if (pluginModelFile && fs.existsSync(pluginModelFile)) {
-            result.push(pluginModelFile);
+        if (pluginModelFile && fs.existsSync(pluginModelFile)) {
+            result.add(path.resolve(pluginModelFile));
         }
     }
-    return result;
+    return Array.from(result);
}
packages/language/src/index.ts (1)

1-2: Consider surfacing utils from the package root

To discourage deep imports (@zenstackhq/language/utils), consider re-exporting selected utilities (e.g., getPluginDocuments, getLiteral) here.

Example:

 export { loadDocument } from './document';
 export * from './module';
+export * from './utils'; // if you want stable access to helpers
packages/language/src/module.ts (1)

95-118: Plugin doc loading timing is sensible; add dedupe and optional build

Good use of Parsed-phase to prime the index before linking. Two recommendations:

  • Deduplicate plugin files across docs.
  • Optionally schedule a build for plugin docs (link/validate) if they contain cross-refs.

Apply:

-    shared.workspace.DocumentBuilder.onBuildPhase(DocumentState.Parsed, async (documents) => {
+    const loadedPlugins = new Set<string>();
+    shared.workspace.DocumentBuilder.onBuildPhase(DocumentState.Parsed, async (documents) => {
         for (const doc of documents) {
@@
-            const pluginSchemas = getPluginDocuments(doc.parseResult.value as Model, schemaPath);
+            const pluginSchemas = getPluginDocuments(doc.parseResult.value as Model, schemaPath);
             for (const plugin of pluginSchemas) {
+                if (loadedPlugins.has(plugin)) {
+                    continue;
+                }
                 // 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);
+                // Optionally: schedule a build for pluginDoc on next cycle
+                // await shared.workspace.DocumentBuilder.build([pluginDoc]);
                 if (logToConsole) {
                     console.log(`Loaded plugin model: ${plugin}`);
                 }
+                loadedPlugins.add(plugin);
             }
         }
     });

Please confirm linking succeeds when a user file references declarations defined only in a plugin model (no build of plugin doc). If you see unresolved refs, turn on the optional build line above.

packages/language/src/document.ts (6)

21-28: Make extension check case-insensitive and more informative.

Lowercase both sides and include allowed extensions in the error for better DX.

-    const extensions = services.LanguageMetaData.fileExtensions;
-    if (!extensions.includes(path.extname(fileName))) {
+    const allowed = services.LanguageMetaData.fileExtensions.map((e) => e.toLowerCase());
+    const ext = path.extname(fileName).toLowerCase();
+    if (!allowed.includes(ext)) {
         return {
             success: false,
-            errors: ['invalid schema file extension'],
+            errors: [`invalid schema file extension: ${ext}. Allowed: ${allowed.join(', ')}`],
             warnings: [],
         };
     }

53-55: Load imported and additional documents concurrently.

Avoid sequential awaits; cut IO latency with Promise.all.

-    const importedDocuments: LangiumDocument[] = [];
-    for (const uri of importedURIs) {
-        importedDocuments.push(await langiumDocuments.getOrCreateDocument(uri));
-    }
+    const importedDocuments = await Promise.all(
+        importedURIs.map((uri) => langiumDocuments.getOrCreateDocument(uri))
+    );
@@
-    const additionalDocs = await Promise.all(
-        additionalModelFiles.map((file) =>
-            services.shared.workspace.LangiumDocuments.getOrCreateDocument(URI.file(path.resolve(file))),
-        ),
-    );
+    const additionalDocs = await Promise.all(
+        additionalModelFiles.map((file) =>
+            services.shared.workspace.LangiumDocuments.getOrCreateDocument(
+                URI.file(path.resolve(file))
+            )
+        )
+    );

Also applies to: 60-65


74-78: Avoid magic numbers for diagnostic severity.

Use DiagnosticSeverity enum for clarity and safety.

-    const diagnostics = langiumDocuments.all
+    import { DiagnosticSeverity } from 'vscode-languageserver-types';
+    const diagnostics = langiumDocuments.all
         .flatMap((doc) => (doc.diagnostics ?? []).map((diag) => ({ doc, diag })))
-        .filter(({ diag }) => diag.severity === 1 || diag.severity === 2)
+        .filter(
+            ({ diag }) =>
+                diag.severity === DiagnosticSeverity.Error ||
+                diag.severity === DiagnosticSeverity.Warning
+        )
         .toArray();
@@
-            if (diag.severity === 1) {
+            if (diag.severity === DiagnosticSeverity.Error) {
                 errors.push(message);
             } else {
                 warnings.push(message);
             }

Also applies to: 88-94


125-129: Return the existing model variable (nit).

Small clarity win; same object reference.

-    return {
-        success: true,
-        model: document.parseResult.value as Model,
-        warnings,
-    };
+    return { success: true, model, warnings };

14-20: Consider accepting services/logging via params.

Allow passing an existing services instance and/or logToConsole to avoid re‑creating services per call and to aid testing.

Would you like a follow-up diff to add optional params: loadDocument(fileName, additionalModelFiles, opts: { services?: ZModelLanguageServices; logToConsole?: boolean })?


184-202: Enrich post‑merge validation errors with context.

Include names (and counts) of offending declarations to aid debugging.

-    if (dataSources.length === 0) {
-        errors.push('Validation error: schema must have a datasource declaration');
+    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');
+        if (dataSources.length > 1) {
+            const names = dataSources.map((d) => d.name).filter(Boolean).join(', ');
+            errors.push(
+                `Validation error: multiple datasource declarations are not allowed (${dataSources.length})` +
+                (names ? `: ${names}` : '')
+            );
         }
     }
@@
-    if (authDecls.length > 1) {
-        errors.push('Validation error: Multiple `@@auth` declarations are not allowed');
+    if (authDecls.length > 1) {
+        const names = authDecls.map((d) => d.name).filter(Boolean).join(', ');
+        errors.push(
+            `Validation error: Multiple \`@@auth\` declarations are not allowed (${authDecls.length})` +
+            (names ? `: ${names}` : '')
+        );
     }
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6c8f756 and c2d9852.

📒 Files selected for processing (9)
  • packages/cli/src/actions/action-utils.ts (2 hunks)
  • packages/ide/vscode/package.json (1 hunks)
  • packages/ide/vscode/src/language-server/main.ts (1 hunks)
  • packages/language/src/document.ts (1 hunks)
  • packages/language/src/index.ts (1 hunks)
  • packages/language/src/module.ts (4 hunks)
  • packages/language/src/utils.ts (4 hunks)
  • packages/language/src/zmodel-workspace-manager.ts (1 hunks)
  • samples/blog/zenstack/schema.zmodel (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
{packages,samples,tests}/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place packages only under packages/, samples/, or tests/

Files:

  • packages/ide/vscode/package.json
  • packages/language/src/document.ts
  • packages/cli/src/actions/action-utils.ts
  • packages/language/src/zmodel-workspace-manager.ts
  • packages/language/src/module.ts
  • samples/blog/zenstack/schema.zmodel
  • packages/language/src/utils.ts
  • packages/ide/vscode/src/language-server/main.ts
  • packages/language/src/index.ts
packages/ide/vscode/package.json

📄 CodeRabbit inference engine (CLAUDE.md)

The ide/vscode package is intentionally versioned differently from other packages

Files:

  • packages/ide/vscode/package.json
**/schema.zmodel

📄 CodeRabbit inference engine (CLAUDE.md)

Name ZModel schema files schema.zmodel

Files:

  • samples/blog/zenstack/schema.zmodel
🧬 Code graph analysis (5)
packages/language/src/document.ts (4)
packages/language/src/module.ts (1)
  • createZModelServices (124-126)
packages/language/src/constants.ts (1)
  • STD_LIB_MODULE_NAME (21-21)
packages/language/src/utils.ts (5)
  • resolveImport (397-415)
  • getDocument (569-576)
  • resolveTransitiveImports (366-368)
  • getDataModelAndTypeDefs (430-437)
  • hasAttribute (53-55)
packages/language/src/generated/ast.ts (1)
  • isDataSource (417-419)
packages/cli/src/actions/action-utils.ts (1)
packages/language/src/document.ts (1)
  • loadDocument (14-130)
packages/language/src/module.ts (2)
packages/language/src/utils.ts (1)
  • getPluginDocuments (578-631)
packages/language/src/generated/ast.ts (2)
  • Model (559-563)
  • Model (565-565)
packages/language/src/utils.ts (3)
packages/cli/src/actions/action-utils.ts (1)
  • getPluginDocuments (53-108)
packages/language/src/generated/ast.ts (3)
  • Model (559-563)
  • Model (565-565)
  • isPlugin (628-630)
packages/language/src/constants.ts (1)
  • PLUGIN_MODULE_NAME (26-26)
packages/ide/vscode/src/language-server/main.ts (1)
packages/language/src/module.ts (1)
  • createZModelLanguageServices (78-121)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: build-test (20.x, sqlite)
  • GitHub Check: build-test (20.x, postgresql)
  • GitHub Check: claude-review
🔇 Additional comments (6)
packages/ide/vscode/package.json (1)

14-14: LGTM! Standard dev convenience.

The watch script enables live rebuilding during development, which aligns with the PR's focus on improving the development workflow for the VS Code extension.

packages/ide/vscode/src/language-server/main.ts (1)

10-16: LGTM! Plugin loading visibility enabled.

Passing true for the logToConsole parameter enables console logging when plugin models are loaded. This is appropriate for the VS Code extension context where this visibility aids debugging and troubleshooting of plugin discovery.

packages/cli/src/actions/action-utils.ts (2)

39-51: loadSchemaDocument change looks good

Delegating to loadDocument aligns with the new centralized build flow and reduces CLI responsibility. No issues spotted.


53-108: Remove only getPluginDocuments; keep findUp as it's still needed by getPkgJsonConfig

The verification confirms getPluginDocuments (lines 53-108) is dead code and should be removed. However, the suggestion to also remove findUp (lines 155-178) is incorrect. The findUp helper is still actively used by getPkgJsonConfig at line 134, which is exported and imported by packages/cli/src/actions/generate.ts. Removing findUp would break that function.

Action: Remove only getPluginDocuments (lines 53-108). Leave the findUp helper function intact.

Likely an incorrect or invalid review comment.

packages/language/src/document.ts (2)

41-45: Consider new URL approach for ESM, but verify asset packaging is configured.

The review suggestion to use new URL(..., import.meta.url) is valid ESM best practice and avoids __dirname pitfalls in bundled contexts. However, verification shows a potential gap: tsup.config.ts has no asset-copying configuration. While package.json includes res in its files array (making it part of the npm package), the built dist/ directory will not automatically contain stdlib.zmodel without explicit configuration.

The current __dirname fallback approach works because assets exist at the package root, but for bundled/VSIX scenarios to work reliably:

  1. Either update tsup.config.ts to copy/include res/ in the build, or
  2. Ensure the VSIX packaging/build process includes the res/ directory in bundled outputs

Note: This pattern appears in two locations—document.ts:41 and zmodel-workspace-manager.ts:63 (fallback)—both would need updating if implementing the new URL approach.


110-113: Add null-guard for $document; unnecessary to add URI deduplication layer.

The code uses a non-null assertion on $document, but the codebase standard (e.g., schema-validator.ts:31–32) guards with optional chaining. Apply $document?.uri to prevent crashes on null values.

However, the suggested Set-based URI deduplication is unnecessary. The resolveTransitiveImportsInternal function already deduplicates using visited: Set<string> to track processed paths and models: Set<Model> to collect unique models, returning Array.from(models), which prevents duplicate Model objects from being processed. The extra URI deduplication adds complexity without addressing a real risk.

Simpler fix: Replace model.$document!.uri with model.$document?.uri in both calls, and skip the Set wrapper:

-    imported.forEach((model) => {
-        langiumDocuments.deleteDocument(model.$document!.uri);
-        services.shared.workspace.IndexManager.remove(model.$document!.uri);
-    });
+    imported.forEach((model) => {
+        if (model.$document?.uri) {
+            langiumDocuments.deleteDocument(model.$document.uri);
+            services.shared.workspace.IndexManager.remove(model.$document.uri);
+        }
+    });

Likely an incorrect or invalid review comment.

@claude
Copy link

claude bot commented Oct 17, 2025

Claude finished @ymc9's task —— View job


Code Review: Plugin Loading Cleanup and VSCode Extension Fixes

I've conducted a thorough review of this PR which refactors plugin loading and fixes VSCode extension issues. Here's my analysis:

Todo List:

  • Read repository setup and guidelines from CLAUDE.md
  • Analyze the plugin loading refactoring changes
  • Review VSCode extension modifications
  • Check code quality and best practices
  • Identify potential bugs or issues
  • Assess performance considerations
  • Review security implications
  • Evaluate test coverage needs
  • Provide comprehensive feedback and recommendations

🔍 Review Summary

This PR successfully refactors plugin loading logic and improves the VSCode extension. The changes are well-structured with good separation of concerns.

Strengths

1. Excellent Code Organization

  • Clean separation: Moving document loading logic from index.ts to dedicated document.ts file improves maintainability
  • Modular design: The refactoring creates clearer boundaries between modules
  • DRY principle: Plugin loading logic is now consolidated and reused between CLI and language services

2. Robust Plugin Discovery

  • Multi-strategy approach: The plugin loading supports both file paths and npm package resolution
  • Fallback mechanisms: Good error handling with graceful degradation when plugins aren't found
  • Cross-platform compatibility: Uses proper path resolution for both ESM and CommonJS environments

3. VSCode Extension Improvements

  • Better stdlib loading: The workspace manager now properly resolves the standard library from installed packages
  • Graceful fallbacks: Falls back to bundled stdlib when installed package isn't available
  • Improved logging: Helpful console messages for debugging plugin and stdlib resolution

⚠️ Areas for Improvement

1. Code Duplication (utils.ts:578-631 vs action-utils.ts:53-108)

The getPluginDocuments function is duplicated between these files with minor differences:

  • CLI version: Uses parser services directly
  • Language version: Works with AST model directly

Recommendation: Consider creating a shared utility function to eliminate this duplication.

2. Error Handling Improvements

// In utils.ts:611-624
try {
    if (typeof import.meta.resolve === 'function') {
        // ESM resolution
    } else {
        // CJS resolution with require.resolve
    }
} catch {
    // Silent failure
}

Issue: The catch block silently ignores all errors, making debugging difficult.

Recommendation: Add optional logging parameter or differentiate between expected vs unexpected errors.

3. Type Safety (action-utils.ts:140)

let pkgJson: any = undefined;

Issue: Using any type reduces type safety.

Recommendation: Define a proper interface for package.json structure:

interface PackageJsonZenStack {
    zenstack?: {
        schema?: string;
        output?: string;
    };
}

4. File System Race Conditions

Multiple fs.existsSync() calls without proper synchronization could theoretically cause issues in concurrent scenarios, though this is minor for CLI usage.

🚀 Performance Considerations

Positive impacts:

  • Lazy loading: Plugin documents are only loaded when needed
  • Efficient caching: Langium's document management handles caching appropriately
  • Reduced startup time: Better separation allows for more selective loading

No significant performance concerns identified.

🔒 Security Analysis

Good practices observed:

  • Path validation: Proper path resolution prevents directory traversal
  • Module resolution: Uses Node.js built-in resolution mechanisms
  • No code execution: Plugin loading only reads schema files, not executable code

No security vulnerabilities identified.

🧪 Test Coverage Recommendations

The refactoring touches core functionality that should have comprehensive tests:

  1. Plugin Loading Tests:

    • Test plugin discovery from file paths
    • Test npm package resolution
    • Test fallback behavior when plugins aren't found
    • Test error handling for malformed plugin configurations
  2. Document Loading Tests:

    • Test loadDocument function with various schema configurations
    • Test import resolution and merging
    • Test validation after import merging
  3. VSCode Extension Tests:

    • Test stdlib resolution from installed packages
    • Test fallback to bundled stdlib
    • Test workspace folder scenarios

📋 Minor Issues

  1. Inconsistent commenting: Some functions lack JSDoc comments while others are well-documented
  2. Magic strings: Consider extracting 'index.js' and other magic strings to constants
  3. Console logging: The language server logs to console which might not be appropriate for all environments

🎯 Overall Assessment

This is a high-quality refactoring that successfully:

  • ✅ Improves code organization and maintainability
  • ✅ Fixes VSCode extension issues
  • ✅ Maintains backward compatibility
  • ✅ Follows established patterns in the codebase
  • ✅ Handles edge cases appropriately

The changes align well with the ZenStack architecture described in CLAUDE.md and follow the monorepo structure correctly.

Recommendation: Approve with minor suggestions for improvement. The benefits far outweigh the minor issues identified.


Job Run | Branch: fix/cleanup-plugin-loading

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (2)
packages/language/src/document.ts (1)

66-72: Guard DocumentBuilder.build to return structured failures instead of throwing.

Uncaught throws can crash CLI/extension; return { success: false, errors } instead.

-    await services.shared.workspace.DocumentBuilder.build([stdLib, ...additionalDocs, document, ...importedDocuments], {
-        validation: {
-            stopAfterLexingErrors: true,
-            stopAfterParsingErrors: true,
-            stopAfterLinkingErrors: true,
-        },
-    });
+    try {
+        await services.shared.workspace.DocumentBuilder.build(
+            [stdLib, ...additionalDocs, document, ...importedDocuments],
+            {
+                validation: {
+                    stopAfterLexingErrors: true,
+                    stopAfterParsingErrors: true,
+                    stopAfterLinkingErrors: true,
+                },
+            }
+        );
+    } catch (e) {
+        return {
+            success: false,
+            errors: [`Document build failed: ${e instanceof Error ? e.message : String(e)}`],
+            warnings: [],
+        };
+    }
packages/language/src/utils.ts (1)

611-620: Type-safe feature detection for import.meta.resolve.

resolve is not in ImportMeta types on Node <22; cast to any to avoid TS error.

-        if (!pluginModelFile) {
-            if (typeof import.meta.resolve === 'function') {
+        if (!pluginModelFile) {
+            const im: any = import.meta as any;
+            if (typeof im.resolve === 'function') {
                 try {
                     // try loading as a ESM module
-                    const resolvedUrl = import.meta.resolve(`${provider}/${PLUGIN_MODULE_NAME}`);
+                    const resolvedUrl = im.resolve(`${provider}/${PLUGIN_MODULE_NAME}`);
                     pluginModelFile = fileURLToPath(resolvedUrl);
                 } catch {
                     // noop
                 }
             }
         }
🧹 Nitpick comments (6)
packages/cli/src/actions/action-utils.ts (1)

4-4: Optional: avoid String.prototype patching by using colors/safe.

Prevents global side effects in library/CLI contexts.

-import colors from 'colors';
+import colors from 'colors/safe';
packages/language/src/document.ts (2)

22-28: Minor: make file extension check case-insensitive.

Handles rare uppercase extensions on Windows.

-    if (!extensions.includes(path.extname(fileName))) {
+    if (!extensions.includes(path.extname(fileName).toLowerCase())) {

125-129: Nit: return the model variable for clarity.

Same object, but avoids re-reading from document.

-        model: document.parseResult.value as Model,
+        model,
packages/language/src/utils.ts (2)

632-637: Deduplicate discovered plugin documents.

Avoid loading the same plugin model multiple times.

-    const result: string[] = [];
+    const seen = new Set<string>();
+    const result: string[] = [];
@@
-        if (pluginModelFile && fs.existsSync(pluginModelFile)) {
-            result.push(pluginModelFile);
+        if (pluginModelFile && fs.existsSync(pluginModelFile)) {
+            const abs = path.resolve(pluginModelFile);
+            if (!seen.has(abs)) {
+                seen.add(abs);
+                result.push(abs);
+            }
         }

596-607: Optional: widen provider path handling.

Consider checking package.json "exports"/"main" when provider is a folder to better locate plugin.zmodel for ESM packages.

Would you like a small helper to read nearest package.json and probe ${pkgRoot}/${PLUGIN_MODULE_NAME}?

packages/language/src/module.ts (1)

104-106: Use URI.fsPath instead of fileURLToPath(toString()).

Simpler and avoids string round‑trips.

-            const schemaPath = fileURLToPath(doc.uri.toString());
+            const schemaPath = doc.uri.fsPath;
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c2d9852 and 1a1a860.

📒 Files selected for processing (5)
  • packages/cli/src/actions/action-utils.ts (2 hunks)
  • packages/language/src/document.ts (1 hunks)
  • packages/language/src/module.ts (4 hunks)
  • packages/language/src/utils.ts (4 hunks)
  • samples/blog/zenstack/schema.zmodel (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • samples/blog/zenstack/schema.zmodel
🧰 Additional context used
📓 Path-based instructions (1)
{packages,samples,tests}/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place packages only under packages/, samples/, or tests/

Files:

  • packages/language/src/utils.ts
  • packages/language/src/module.ts
  • packages/language/src/document.ts
  • packages/cli/src/actions/action-utils.ts
🧬 Code graph analysis (4)
packages/language/src/utils.ts (2)
packages/language/src/generated/ast.ts (3)
  • Model (559-563)
  • Model (565-565)
  • isPlugin (628-630)
packages/language/src/constants.ts (1)
  • PLUGIN_MODULE_NAME (26-26)
packages/language/src/module.ts (2)
packages/language/src/utils.ts (1)
  • getPluginDocuments (579-637)
packages/language/src/generated/ast.ts (2)
  • Model (559-563)
  • Model (565-565)
packages/language/src/document.ts (4)
packages/language/src/module.ts (1)
  • createZModelServices (124-126)
packages/language/src/constants.ts (1)
  • STD_LIB_MODULE_NAME (21-21)
packages/language/src/utils.ts (5)
  • resolveImport (398-416)
  • getDocument (570-577)
  • resolveTransitiveImports (367-369)
  • getDataModelAndTypeDefs (431-438)
  • hasAttribute (54-56)
packages/language/src/generated/ast.ts (1)
  • isDataSource (417-419)
packages/cli/src/actions/action-utils.ts (1)
packages/language/src/document.ts (1)
  • loadDocument (14-130)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: build-test (20.x, sqlite)
  • GitHub Check: build-test (20.x, postgresql)
🔇 Additional comments (2)
packages/cli/src/actions/action-utils.ts (1)

37-47: Good: CLI now defers schema loading to language package.

Centralized error/warning handling looks correct; throwing CliError on failure is appropriate.

packages/language/src/module.ts (1)

95-119: Code pattern is safe; DocumentBuilder.onBuildPhase awaits async handlers.

Langium 4.x awaits Promises returned by handlers registered with DocumentBuilder.onBuildPhase before proceeding to the next build phase, so the async plugin document loading will complete before the linking phase begins. Plugin docs will not miss the linking window.

@ymc9 ymc9 merged commit 05bf237 into dev Oct 17, 2025
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants