Skip to content

Commit b51b74c

Browse files
ymc9claude
andcommitted
fix(cli): report error when plugin module cannot be resolved
Previously, when a plugin module couldn't be found (e.g., package not installed), the error was silently swallowed and the plugin was skipped. Now the CLI reports a clear error message distinguishing between missing modules and other loading failures. Closes #2393 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 87a9f7f commit b51b74c

2 files changed

Lines changed: 63 additions & 4 deletions

File tree

packages/cli/src/actions/generate.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -340,10 +340,15 @@ async function loadPluginModule(provider: string, basePath: string) {
340340

341341
// last resort, try to import as esm directly
342342
try {
343-
return (await import(moduleSpec)).default as CliPlugin;
344-
} catch {
345-
// plugin may not export a generator so we simply ignore the error here
346-
return undefined;
343+
const mod = await import(moduleSpec);
344+
// plugin may not export a generator, return undefined in that case
345+
return mod.default as CliPlugin | undefined;
346+
} catch (err) {
347+
const errorCode = (err as NodeJS.ErrnoException)?.code;
348+
if (errorCode === 'ERR_MODULE_NOT_FOUND' || errorCode === 'MODULE_NOT_FOUND') {
349+
throw new CliError(`Cannot find plugin module "${provider}". Please make sure the package exists.`);
350+
}
351+
throw new CliError(`Failed to load plugin module "${provider}": ${(err as Error).message}`);
347352
}
348353
}
349354

packages/cli/test/generate.test.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,60 @@ model User {
199199
expect(fs.existsSync(path.join(workDir, 'zenstack/input.ts'))).toBe(false);
200200
});
201201

202+
it('should report error for unresolvable plugin module', async () => {
203+
const modelWithMissingPlugin = `
204+
plugin foo {
205+
provider = '@zenstackhq/nonexistent-plugin'
206+
}
207+
208+
model User {
209+
id String @id @default(cuid())
210+
}
211+
`;
212+
const { workDir } = await createProject(modelWithMissingPlugin);
213+
expect(() => runCli('generate', workDir)).toThrow(/Cannot find plugin module/);
214+
});
215+
216+
it('should succeed when plugin module exists but has no CLI generator', async () => {
217+
const modelWithNoGeneratorPlugin = `
218+
plugin foo {
219+
provider = './my-plugin.mjs'
220+
}
221+
222+
model User {
223+
id String @id @default(cuid())
224+
}
225+
`;
226+
const { workDir } = await createProject(modelWithNoGeneratorPlugin);
227+
// Create a plugin module that doesn't export a default CLI generator
228+
fs.writeFileSync(path.join(workDir, 'zenstack/my-plugin.mjs'), 'export const name = "no-generator";');
229+
runCli('generate', workDir);
230+
// Should succeed without error, generating the default typescript output
231+
expect(fs.existsSync(path.join(workDir, 'zenstack/schema.ts'))).toBe(true);
232+
});
233+
234+
it('should succeed when plugin only provides a plugin.zmodel for custom attributes', async () => {
235+
const modelWithZmodelOnlyPlugin = `
236+
plugin myPlugin {
237+
provider = './my-plugin'
238+
}
239+
240+
model User {
241+
id String @id @default(cuid())
242+
@@custom
243+
}
244+
`;
245+
const { workDir } = await createProject(modelWithZmodelOnlyPlugin);
246+
// Create a plugin directory with index.mjs (no default export) and a plugin.zmodel defining a custom attribute
247+
const pluginDir = path.join(workDir, 'zenstack/my-plugin');
248+
fs.mkdirSync(pluginDir, { recursive: true });
249+
fs.writeFileSync(path.join(pluginDir, 'index.mjs'), 'export const name = "my-plugin";');
250+
fs.writeFileSync(path.join(pluginDir, 'plugin.zmodel'), 'attribute @@custom()');
251+
runCli('generate', workDir);
252+
// Should succeed without error, generating the default typescript output
253+
expect(fs.existsSync(path.join(workDir, 'zenstack/schema.ts'))).toBe(true);
254+
});
255+
202256
it('should prefer CLI options over @core/typescript plugin settings for generateModels and generateInput', async () => {
203257
const modelWithPlugin = `
204258
plugin typescript {

0 commit comments

Comments
 (0)