From 1e8c68277ca260c88a43e26316716edc55a01978 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Sun, 17 Aug 2025 15:17:30 +0800 Subject: [PATCH 1/4] feat: cli plugin support --- .vscode/launch.json | 3 +- packages/cli/src/actions/generate.ts | 110 +++++++++++++----- packages/cli/src/index.ts | 6 - packages/cli/src/plugins/index.ts | 2 + packages/cli/src/plugins/prisma.ts | 22 ++++ packages/cli/src/plugins/typescript.ts | 21 ++++ .../cli/test/plugins/prisma-plugin.test.ts | 60 ++++++++++ packages/cli/tsconfig.json | 3 - packages/common-helpers/tsconfig.json | 3 - packages/create-zenstack/tsconfig.json | 3 - packages/dialects/sql.js/tsconfig.json | 3 - packages/ide/vscode/tsconfig.json | 3 - packages/language/tsconfig.json | 3 - packages/runtime/test/scripts/generate.ts | 7 +- packages/sdk/src/cli-plugin.ts | 47 ++++++++ packages/sdk/src/generator.ts | 10 -- packages/sdk/src/index.ts | 2 +- packages/sdk/src/ts-schema-generator.ts | 10 +- packages/sdk/tsconfig.json | 1 - packages/tanstack-query/tsconfig.json | 3 - packages/testtools/src/schema.ts | 14 ++- packages/testtools/tsconfig.json | 3 - packages/zod/tsconfig.json | 3 - samples/blog/package.json | 5 +- 24 files changed, 255 insertions(+), 92 deletions(-) create mode 100644 packages/cli/src/plugins/index.ts create mode 100644 packages/cli/src/plugins/prisma.ts create mode 100644 packages/cli/src/plugins/typescript.ts create mode 100644 packages/cli/test/plugins/prisma-plugin.test.ts create mode 100644 packages/sdk/src/cli-plugin.ts delete mode 100644 packages/sdk/src/generator.ts diff --git a/.vscode/launch.json b/.vscode/launch.json index 7a65620b..886089d1 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,7 +10,8 @@ "request": "launch", "skipFiles": ["/**"], "type": "node", - "args": ["generate", "--schema", "${workspaceFolder}/samples/blog/zenstack/schema.zmodel"] + "args": ["generate"], + "cwd": "${workspaceFolder}/samples/blog/zenstack" }, { "name": "Debug with TSX", diff --git a/packages/cli/src/actions/generate.ts b/packages/cli/src/actions/generate.ts index 58c2060a..917837c7 100644 --- a/packages/cli/src/actions/generate.ts +++ b/packages/cli/src/actions/generate.ts @@ -1,16 +1,18 @@ import { invariant } from '@zenstackhq/common-helpers'; -import { isPlugin, LiteralExpr, type Model } from '@zenstackhq/language/ast'; -import { PrismaSchemaGenerator, TsSchemaGenerator, type CliGenerator } from '@zenstackhq/sdk'; +import { isPlugin, LiteralExpr, Plugin, type Model } from '@zenstackhq/language/ast'; +import { getLiteral, getLiteralArray } from '@zenstackhq/language/utils'; +import { type CliPlugin } from '@zenstackhq/sdk'; import colors from 'colors'; -import fs from 'node:fs'; import path from 'node:path'; +import ora from 'ora'; +import { CliError } from '../cli-error'; +import * as corePlugins from '../plugins'; import { getPkgJsonConfig, getSchemaFile, loadSchemaDocument } from './action-utils'; type Options = { schema?: string; output?: string; silent?: boolean; - savePrismaSchema?: string | boolean; }; /** @@ -24,25 +26,10 @@ export async function run(options: Options) { const model = await loadSchemaDocument(schemaFile); const outputPath = getOutputPath(options, schemaFile); - // generate TS schema - const tsSchemaFile = path.join(outputPath, 'schema.ts'); - await new TsSchemaGenerator().generate(schemaFile, [], outputPath); - - await runPlugins(model, outputPath, tsSchemaFile); - - // generate Prisma schema - if (options.savePrismaSchema) { - const prismaSchema = await new PrismaSchemaGenerator(model).generate(); - let prismaSchemaFile = path.join(outputPath, 'schema.prisma'); - if (typeof options.savePrismaSchema === 'string') { - prismaSchemaFile = path.resolve(outputPath, options.savePrismaSchema); - fs.mkdirSync(path.dirname(prismaSchemaFile), { recursive: true }); - } - fs.writeFileSync(prismaSchemaFile, prismaSchema); - } + await runPlugins(schemaFile, model, outputPath); if (!options.silent) { - console.log(colors.green(`Generation completed successfully in ${Date.now() - start}ms.`)); + console.log(colors.green(`Generation completed successfully in ${Date.now() - start}ms.\n`)); console.log(`You can now create a ZenStack client with it. \`\`\`ts @@ -68,18 +55,79 @@ function getOutputPath(options: Options, schemaFile: string) { } } -async function runPlugins(model: Model, outputPath: string, tsSchemaFile: string) { +async function runPlugins(schemaFile: string, model: Model, outputPath: string) { const plugins = model.declarations.filter(isPlugin); + const processedPlugins: { cliPlugin: CliPlugin; pluginOptions: Record }[] = []; + for (const plugin of plugins) { - const providerField = plugin.fields.find((f) => f.name === 'provider'); - invariant(providerField, `Plugin ${plugin.name} does not have a provider field`); - const provider = (providerField.value as LiteralExpr).value as string; - let useProvider = provider; - if (useProvider.startsWith('@core/')) { - useProvider = `@zenstackhq/runtime/plugins/${useProvider.slice(6)}`; + const provider = getPluginProvider(plugin); + + let cliPlugin: CliPlugin; + if (provider.startsWith('@core/')) { + cliPlugin = (corePlugins as any)[provider.slice('@core/'.length)]; + if (!cliPlugin) { + throw new CliError(`Unknown core plugin: ${provider}`); + } + } else { + try { + cliPlugin = (await import(provider)).default as CliPlugin; + } catch (error) { + throw new CliError(`Failed to load plugin ${provider}: ${error}`); + } + } + + processedPlugins.push({ cliPlugin, pluginOptions: getPluginOptions(plugin) }); + } + + const defaultPlugins = [corePlugins['typescript']].reverse(); + defaultPlugins.forEach((d) => { + if (!processedPlugins.some((p) => p.cliPlugin === d)) { + processedPlugins.push({ cliPlugin: d, pluginOptions: {} }); + } + }); + + for (const { cliPlugin, pluginOptions } of processedPlugins) { + invariant( + typeof cliPlugin.generate === 'function', + `Plugin ${cliPlugin.name} does not have a generate function`, + ); + + // run plugin generator + const spinner = ora(cliPlugin.statusText ?? `Running plugin ${cliPlugin.name}`).start(); + try { + await cliPlugin.generate({ + schemaFile, + model, + defaultOutputPath: outputPath, + pluginOptions, + }); + spinner.succeed(); + } catch (err) { + spinner.fail(); + console.error(err); + } + } +} + +function getPluginProvider(plugin: Plugin) { + const providerField = plugin.fields.find((f) => f.name === 'provider'); + invariant(providerField, `Plugin ${plugin.name} does not have a provider field`); + const provider = (providerField.value as LiteralExpr).value as string; + return provider; +} + +function getPluginOptions(plugin: Plugin): Record { + const result: any = {}; + for (const field of plugin.fields) { + if (field.name === 'provider') { + continue; // skip provider + } + const value = getLiteral(field.value) ?? getLiteralArray(field.value); + if (value === undefined) { + console.warn(`Plugin "${plugin.name}" option "${field.name}" has unsupported value, skipping`); + continue; } - const generator = (await import(useProvider)).default as CliGenerator; - console.log('Running generator:', provider); - await generator({ model, outputPath, tsSchemaFile }); + result[field.name] = value; } + return result; } diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index fd5ad01e..a275800d 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -55,12 +55,6 @@ export function createProgram() { .description('Run code generation.') .addOption(schemaOption) .addOption(new Option('--silent', 'do not print any output')) - .addOption( - new Option( - '--save-prisma-schema [path]', - 'save a Prisma schema file, by default into the output directory', - ), - ) .addOption(new Option('-o, --output ', 'default output directory for core plugins')) .action(generateAction); diff --git a/packages/cli/src/plugins/index.ts b/packages/cli/src/plugins/index.ts new file mode 100644 index 00000000..1a09b07d --- /dev/null +++ b/packages/cli/src/plugins/index.ts @@ -0,0 +1,2 @@ +export { default as prisma } from './prisma'; +export { default as typescript } from './typescript'; diff --git a/packages/cli/src/plugins/prisma.ts b/packages/cli/src/plugins/prisma.ts new file mode 100644 index 00000000..3a4de596 --- /dev/null +++ b/packages/cli/src/plugins/prisma.ts @@ -0,0 +1,22 @@ +import { PrismaSchemaGenerator, type CliPlugin } from '@zenstackhq/sdk'; +import fs from 'node:fs'; +import path from 'node:path'; + +const plugin: CliPlugin = { + name: 'Prisma Schema Generator', + statusText: 'Generating Prisma schema', + async generate({ model, defaultOutputPath, pluginOptions }) { + let outFile = path.join(defaultOutputPath, 'schema.prisma'); + if (typeof pluginOptions['output'] === 'string') { + const outDir = path.resolve(defaultOutputPath, pluginOptions['output']); + if (!fs.existsSync(outDir)) { + fs.mkdirSync(outDir, { recursive: true }); + } + outFile = path.join(outDir, 'schema.prisma'); + } + const prismaSchema = await new PrismaSchemaGenerator(model).generate(); + fs.writeFileSync(outFile, prismaSchema); + }, +}; + +export default plugin; diff --git a/packages/cli/src/plugins/typescript.ts b/packages/cli/src/plugins/typescript.ts new file mode 100644 index 00000000..ad38512e --- /dev/null +++ b/packages/cli/src/plugins/typescript.ts @@ -0,0 +1,21 @@ +import type { CliPlugin } from '@zenstackhq/sdk'; +import { TsSchemaGenerator } from '@zenstackhq/sdk'; +import fs from 'node:fs'; +import path from 'node:path'; + +const plugin: CliPlugin = { + name: 'TypeScript Schema Generator', + statusText: 'Generating TypeScript schema', + async generate({ model, defaultOutputPath, pluginOptions }) { + let ourDir = defaultOutputPath; + if (typeof pluginOptions['output'] === 'string') { + ourDir = path.resolve(defaultOutputPath, pluginOptions['output']); + if (!fs.existsSync(ourDir)) { + fs.mkdirSync(ourDir, { recursive: true }); + } + } + await new TsSchemaGenerator().generate(model, ourDir); + }, +}; + +export default plugin; diff --git a/packages/cli/test/plugins/prisma-plugin.test.ts b/packages/cli/test/plugins/prisma-plugin.test.ts new file mode 100644 index 00000000..06f252cb --- /dev/null +++ b/packages/cli/test/plugins/prisma-plugin.test.ts @@ -0,0 +1,60 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { describe, expect, it } from 'vitest'; +import { createProject, runCli } from '../utils'; + +describe('Core plugins tests', () => { + it('can automatically generate a TypeScript schema with default output', () => { + const workDir = createProject(` +model User { + id String @id @default(cuid()) +} +`); + runCli('generate', workDir); + expect(fs.existsSync(path.join(workDir, 'zenstack/schema.ts'))).toBe(true); + }); + + it('can automatically generate a TypeScript schema with custom output', () => { + const workDir = createProject(` +plugin typescript { + provider = '@core/typescript' + output = '../generated-schema' +} + +model User { + id String @id @default(cuid()) +} +`); + runCli('generate', workDir); + expect(fs.existsSync(path.join(workDir, 'generated-schema/schema.ts'))).toBe(true); + }); + + it('can generate a Prisma schema with default output', () => { + const workDir = createProject(` +plugin prisma { + provider = '@core/prisma' +} + +model User { + id String @id @default(cuid()) +} +`); + runCli('generate', workDir); + expect(fs.existsSync(path.join(workDir, 'zenstack/schema.prisma'))).toBe(true); + }); + + it('can generate a Prisma schema with custom output', () => { + const workDir = createProject(` +plugin prisma { + provider = '@core/prisma' + output = './prisma' +} + +model User { + id String @id @default(cuid()) +} +`); + runCli('generate', workDir); + expect(fs.existsSync(path.join(workDir, 'zenstack/prisma/schema.prisma'))).toBe(true); + }); +}); diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json index bd22b363..8ef64682 100644 --- a/packages/cli/tsconfig.json +++ b/packages/cli/tsconfig.json @@ -1,7 +1,4 @@ { "extends": "@zenstackhq/typescript-config/base.json", - "compilerOptions": { - "baseUrl": "." - }, "include": ["src/**/*.ts"] } diff --git a/packages/common-helpers/tsconfig.json b/packages/common-helpers/tsconfig.json index bd22b363..8ef64682 100644 --- a/packages/common-helpers/tsconfig.json +++ b/packages/common-helpers/tsconfig.json @@ -1,7 +1,4 @@ { "extends": "@zenstackhq/typescript-config/base.json", - "compilerOptions": { - "baseUrl": "." - }, "include": ["src/**/*.ts"] } diff --git a/packages/create-zenstack/tsconfig.json b/packages/create-zenstack/tsconfig.json index bd22b363..8ef64682 100644 --- a/packages/create-zenstack/tsconfig.json +++ b/packages/create-zenstack/tsconfig.json @@ -1,7 +1,4 @@ { "extends": "@zenstackhq/typescript-config/base.json", - "compilerOptions": { - "baseUrl": "." - }, "include": ["src/**/*.ts"] } diff --git a/packages/dialects/sql.js/tsconfig.json b/packages/dialects/sql.js/tsconfig.json index 7b457d06..41472d08 100644 --- a/packages/dialects/sql.js/tsconfig.json +++ b/packages/dialects/sql.js/tsconfig.json @@ -1,7 +1,4 @@ { "extends": "@zenstackhq/typescript-config/base.json", - "compilerOptions": { - "baseUrl": "." - }, "include": ["src/**/*"] } diff --git a/packages/ide/vscode/tsconfig.json b/packages/ide/vscode/tsconfig.json index bd22b363..8ef64682 100644 --- a/packages/ide/vscode/tsconfig.json +++ b/packages/ide/vscode/tsconfig.json @@ -1,7 +1,4 @@ { "extends": "@zenstackhq/typescript-config/base.json", - "compilerOptions": { - "baseUrl": "." - }, "include": ["src/**/*.ts"] } diff --git a/packages/language/tsconfig.json b/packages/language/tsconfig.json index bd22b363..8ef64682 100644 --- a/packages/language/tsconfig.json +++ b/packages/language/tsconfig.json @@ -1,7 +1,4 @@ { "extends": "@zenstackhq/typescript-config/base.json", - "compilerOptions": { - "baseUrl": "." - }, "include": ["src/**/*.ts"] } diff --git a/packages/runtime/test/scripts/generate.ts b/packages/runtime/test/scripts/generate.ts index a5cf10e9..a1393e30 100644 --- a/packages/runtime/test/scripts/generate.ts +++ b/packages/runtime/test/scripts/generate.ts @@ -1,3 +1,4 @@ +import { loadDocument } from '@zenstackhq/language'; import { TsSchemaGenerator } from '@zenstackhq/sdk'; import { glob } from 'glob'; import fs from 'node:fs'; @@ -20,7 +21,11 @@ async function generate(schemaPath: string) { const outputDir = path.dirname(schemaPath); const tsPath = path.join(outputDir, 'schema.ts'); const pluginModelFiles = glob.sync(path.resolve(dir, '../../dist/**/plugin.zmodel')); - await generator.generate(schemaPath, pluginModelFiles, outputDir); + const result = await loadDocument(schemaPath, pluginModelFiles); + if (!result.success) { + throw new Error(`Failed to load schema from ${schemaPath}: ${result.errors}`); + } + await generator.generate(result.model, outputDir); const content = fs.readFileSync(tsPath, 'utf-8'); fs.writeFileSync(tsPath, content.replace(/@zenstackhq\/runtime/g, '../../../dist')); console.log('TS schema generated at:', outputDir); diff --git a/packages/sdk/src/cli-plugin.ts b/packages/sdk/src/cli-plugin.ts new file mode 100644 index 00000000..534e0c84 --- /dev/null +++ b/packages/sdk/src/cli-plugin.ts @@ -0,0 +1,47 @@ +import type { Model } from '@zenstackhq/language/ast'; +import type { MaybePromise } from 'langium'; + +/** + * Context passed to CLI plugins when calling `generate`. + */ +export type CliGeneratorContext = { + /** + * ZModel file path. + */ + schemaFile: string; + + /** + * ZModel AST. + */ + model: Model; + + /** + * Default output path for code generation. + */ + defaultOutputPath: string; + + /** + * Plugin options provided by the user. + */ + pluginOptions: Record; +}; + +/** + * Contract for a CLI plugin. + */ +export interface CliPlugin { + /** + * Plugin's display name. + */ + name: string; + + /** + * Text to show during generation. + */ + statusText?: string; + + /** + * Code generation callback. + */ + generate(context: CliGeneratorContext): MaybePromise; +} diff --git a/packages/sdk/src/generator.ts b/packages/sdk/src/generator.ts deleted file mode 100644 index 3868b692..00000000 --- a/packages/sdk/src/generator.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { Model } from '@zenstackhq/language/ast'; -import type { MaybePromise } from 'langium'; - -export type CliGeneratorContext = { - model: Model; - outputPath: string; - tsSchemaFile: string; -}; - -export type CliGenerator = (context: CliGeneratorContext) => MaybePromise; diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 313d15ae..649a7201 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -1,5 +1,5 @@ import * as ModelUtils from './model-utils'; -export * from './generator'; +export * from './cli-plugin'; export { PrismaSchemaGenerator } from './prisma/prisma-schema-generator'; export * from './ts-schema-generator'; export * from './zmodel-code-generator'; diff --git a/packages/sdk/src/ts-schema-generator.ts b/packages/sdk/src/ts-schema-generator.ts index dd8fb49d..d113a3b9 100644 --- a/packages/sdk/src/ts-schema-generator.ts +++ b/packages/sdk/src/ts-schema-generator.ts @@ -1,5 +1,4 @@ import { invariant } from '@zenstackhq/common-helpers'; -import { loadDocument } from '@zenstackhq/language'; import { ArrayExpr, AttributeArg, @@ -53,14 +52,7 @@ import { } from './model-utils'; export class TsSchemaGenerator { - public async generate(schemaFile: string, pluginModelFiles: string[], outputDir: string) { - const loaded = await loadDocument(schemaFile, pluginModelFiles); - if (!loaded.success) { - throw new Error(`Error loading schema:${loaded.errors.join('\n')}`); - } - - const { model } = loaded; - + async generate(model: Model, outputDir: string) { fs.mkdirSync(outputDir, { recursive: true }); // the schema itself diff --git a/packages/sdk/tsconfig.json b/packages/sdk/tsconfig.json index 07f91d53..25cac129 100644 --- a/packages/sdk/tsconfig.json +++ b/packages/sdk/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "@zenstackhq/typescript-config/base.json", "compilerOptions": { - "baseUrl": ".", "noUnusedLocals": false }, "include": ["src/**/*.ts"] diff --git a/packages/tanstack-query/tsconfig.json b/packages/tanstack-query/tsconfig.json index a64b0eb5..e7ce31be 100644 --- a/packages/tanstack-query/tsconfig.json +++ b/packages/tanstack-query/tsconfig.json @@ -1,7 +1,4 @@ { "extends": "@zenstackhq/typescript-config/base.json", - "compilerOptions": { - "baseUrl": "." - }, "include": ["src/**/*.ts", "test/**/*.ts"] } diff --git a/packages/testtools/src/schema.ts b/packages/testtools/src/schema.ts index 48c43b97..788f092c 100644 --- a/packages/testtools/src/schema.ts +++ b/packages/testtools/src/schema.ts @@ -1,3 +1,4 @@ +import { loadDocument } from '@zenstackhq/language'; import { TsSchemaGenerator } from '@zenstackhq/sdk'; import type { SchemaDef } from '@zenstackhq/sdk/schema'; import { glob } from 'glob'; @@ -41,9 +42,13 @@ export async function generateTsSchema( fs.writeFileSync(zmodelPath, `${noPrelude ? '' : makePrelude(provider, dbUrl)}\n\n${schemaText}`); const pluginModelFiles = glob.sync(path.resolve(__dirname, '../../runtime/src/plugins/**/plugin.zmodel')); + const result = await loadDocument(zmodelPath, pluginModelFiles); + if (!result.success) { + throw new Error(`Failed to load schema from ${zmodelPath}: ${result.errors}`); + } const generator = new TsSchemaGenerator(); - await generator.generate(zmodelPath, pluginModelFiles, workDir); + await generator.generate(result.model, workDir); if (extraSourceFiles) { for (const [fileName, content] of Object.entries(extraSourceFiles)) { @@ -76,8 +81,11 @@ export function generateTsSchemaFromFile(filePath: string) { export async function generateTsSchemaInPlace(schemaPath: string) { const workDir = path.dirname(schemaPath); const pluginModelFiles = glob.sync(path.resolve(__dirname, '../../runtime/src/plugins/**/plugin.zmodel')); - + const result = await loadDocument(schemaPath, pluginModelFiles); + if (!result.success) { + throw new Error(`Failed to load schema from ${schemaPath}: ${result.errors}`); + } const generator = new TsSchemaGenerator(); - await generator.generate(schemaPath, pluginModelFiles, workDir); + await generator.generate(result.model, workDir); return compileAndLoad(workDir); } diff --git a/packages/testtools/tsconfig.json b/packages/testtools/tsconfig.json index a64b0eb5..e7ce31be 100644 --- a/packages/testtools/tsconfig.json +++ b/packages/testtools/tsconfig.json @@ -1,7 +1,4 @@ { "extends": "@zenstackhq/typescript-config/base.json", - "compilerOptions": { - "baseUrl": "." - }, "include": ["src/**/*.ts", "test/**/*.ts"] } diff --git a/packages/zod/tsconfig.json b/packages/zod/tsconfig.json index a64b0eb5..e7ce31be 100644 --- a/packages/zod/tsconfig.json +++ b/packages/zod/tsconfig.json @@ -1,7 +1,4 @@ { "extends": "@zenstackhq/typescript-config/base.json", - "compilerOptions": { - "baseUrl": "." - }, "include": ["src/**/*.ts", "test/**/*.ts"] } diff --git a/samples/blog/package.json b/samples/blog/package.json index 8dfec300..6ac9db11 100644 --- a/samples/blog/package.json +++ b/samples/blog/package.json @@ -4,8 +4,9 @@ "description": "", "main": "index.js", "scripts": { - "generate": "zenstack generate", - "db:migrate": "zenstack migrate dev", + "generate": "zen generate", + "db:push": "zen db push", + "db:migrate": "zen migrate dev", "build": "pnpm generate && tsc --noEmit", "dev": "tsx main.ts" }, From 6aa29649877e7ec9fe480d6db42295f3bc4f579d Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Sun, 17 Aug 2025 15:47:08 +0800 Subject: [PATCH 2/4] update --- packages/cli/src/actions/generate.ts | 2 +- packages/cli/src/plugins/typescript.ts | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/cli/src/actions/generate.ts b/packages/cli/src/actions/generate.ts index 917837c7..d94d5b32 100644 --- a/packages/cli/src/actions/generate.ts +++ b/packages/cli/src/actions/generate.ts @@ -117,7 +117,7 @@ function getPluginProvider(plugin: Plugin) { } function getPluginOptions(plugin: Plugin): Record { - const result: any = {}; + const result: Record = {}; for (const field of plugin.fields) { if (field.name === 'provider') { continue; // skip provider diff --git a/packages/cli/src/plugins/typescript.ts b/packages/cli/src/plugins/typescript.ts index ad38512e..4fd5006f 100644 --- a/packages/cli/src/plugins/typescript.ts +++ b/packages/cli/src/plugins/typescript.ts @@ -7,14 +7,14 @@ const plugin: CliPlugin = { name: 'TypeScript Schema Generator', statusText: 'Generating TypeScript schema', async generate({ model, defaultOutputPath, pluginOptions }) { - let ourDir = defaultOutputPath; + let outDir = defaultOutputPath; if (typeof pluginOptions['output'] === 'string') { - ourDir = path.resolve(defaultOutputPath, pluginOptions['output']); - if (!fs.existsSync(ourDir)) { - fs.mkdirSync(ourDir, { recursive: true }); + outDir = path.resolve(defaultOutputPath, pluginOptions['output']); + if (!fs.existsSync(outDir)) { + fs.mkdirSync(outDir, { recursive: true }); } } - await new TsSchemaGenerator().generate(model, ourDir); + await new TsSchemaGenerator().generate(model, outDir); }, }; From 4858033ee14053bf504c7e1e232f03c6aedd0e8a Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Sun, 17 Aug 2025 15:51:45 +0800 Subject: [PATCH 3/4] update --- packages/cli/test/generate.test.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/packages/cli/test/generate.test.ts b/packages/cli/test/generate.test.ts index ad9e0497..701fe4f0 100644 --- a/packages/cli/test/generate.test.ts +++ b/packages/cli/test/generate.test.ts @@ -30,18 +30,6 @@ describe('CLI generate command test', () => { expect(fs.existsSync(path.join(workDir, 'zenstack/schema.ts'))).toBe(true); }); - it('should respect save prisma schema option', () => { - const workDir = createProject(model); - runCli('generate --save-prisma-schema', workDir); - expect(fs.existsSync(path.join(workDir, 'zenstack/schema.prisma'))).toBe(true); - }); - - it('should respect save prisma schema custom path option', () => { - const workDir = createProject(model); - runCli('generate --save-prisma-schema "../prisma/schema.prisma"', workDir); - expect(fs.existsSync(path.join(workDir, 'prisma/schema.prisma'))).toBe(true); - }); - it('should respect package.json config', () => { const workDir = createProject(model); fs.mkdirSync(path.join(workDir, 'foo')); From a4d6ac7ce0f55d5a1722d9097f8b8f97a3b51892 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Sun, 17 Aug 2025 16:24:34 +0800 Subject: [PATCH 4/4] more fixes --- package.json | 2 +- packages/cli/src/actions/generate.ts | 7 ++- packages/cli/src/plugins/prisma.ts | 7 ++- .../cli/test/plugins/custom-plugin.test.ts | 50 +++++++++++++++++++ packages/testtools/package.json | 1 + pnpm-lock.yaml | 8 ++- pnpm-workspace.yaml | 1 + 7 files changed, 69 insertions(+), 7 deletions(-) create mode 100644 packages/cli/test/plugins/custom-plugin.test.ts diff --git a/package.json b/package.json index 1c0ec9ae..b6194515 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "license": "MIT", "devDependencies": { "@eslint/js": "^9.29.0", - "@types/node": "^20.17.24", + "@types/node": "catalog:", "eslint": "~9.29.0", "glob": "^11.0.2", "prettier": "^3.5.3", diff --git a/packages/cli/src/actions/generate.ts b/packages/cli/src/actions/generate.ts index d94d5b32..b70c0cda 100644 --- a/packages/cli/src/actions/generate.ts +++ b/packages/cli/src/actions/generate.ts @@ -69,8 +69,13 @@ async function runPlugins(schemaFile: string, model: Model, outputPath: string) throw new CliError(`Unknown core plugin: ${provider}`); } } else { + let moduleSpec = provider; + if (moduleSpec.startsWith('.')) { + // relative to schema's path + moduleSpec = path.resolve(path.dirname(schemaFile), moduleSpec); + } try { - cliPlugin = (await import(provider)).default as CliPlugin; + cliPlugin = (await import(moduleSpec)).default as CliPlugin; } catch (error) { throw new CliError(`Failed to load plugin ${provider}: ${error}`); } diff --git a/packages/cli/src/plugins/prisma.ts b/packages/cli/src/plugins/prisma.ts index 3a4de596..b471ec36 100644 --- a/packages/cli/src/plugins/prisma.ts +++ b/packages/cli/src/plugins/prisma.ts @@ -6,16 +6,15 @@ const plugin: CliPlugin = { name: 'Prisma Schema Generator', statusText: 'Generating Prisma schema', async generate({ model, defaultOutputPath, pluginOptions }) { - let outFile = path.join(defaultOutputPath, 'schema.prisma'); + let outDir = defaultOutputPath; if (typeof pluginOptions['output'] === 'string') { - const outDir = path.resolve(defaultOutputPath, pluginOptions['output']); + outDir = path.resolve(defaultOutputPath, pluginOptions['output']); if (!fs.existsSync(outDir)) { fs.mkdirSync(outDir, { recursive: true }); } - outFile = path.join(outDir, 'schema.prisma'); } const prismaSchema = await new PrismaSchemaGenerator(model).generate(); - fs.writeFileSync(outFile, prismaSchema); + fs.writeFileSync(path.join(outDir, 'schema.prisma'), prismaSchema); }, }; diff --git a/packages/cli/test/plugins/custom-plugin.test.ts b/packages/cli/test/plugins/custom-plugin.test.ts new file mode 100644 index 00000000..084bf9cd --- /dev/null +++ b/packages/cli/test/plugins/custom-plugin.test.ts @@ -0,0 +1,50 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { describe, expect, it } from 'vitest'; +import { createProject, runCli } from '../utils'; +import { execSync } from 'node:child_process'; + +describe('Custom plugins tests', () => { + it('runs custom plugin generator', () => { + const workDir = createProject(` +plugin custom { + provider = '../my-plugin.js' + output = '../custom-output' +} + +model User { + id String @id @default(cuid()) +} +`); + + fs.writeFileSync( + path.join(workDir, 'my-plugin.ts'), + ` +import type { CliPlugin } from '@zenstackhq/sdk'; +import fs from 'node:fs'; +import path from 'node:path'; + +const plugin: CliPlugin = { + name: 'Custom Generator', + statusText: 'Generating foo.txt', + async generate({ model, defaultOutputPath, pluginOptions }) { + let outDir = defaultOutputPath; + if (typeof pluginOptions['output'] === 'string') { + outDir = path.resolve(defaultOutputPath, pluginOptions['output']); + if (!fs.existsSync(outDir)) { + fs.mkdirSync(outDir, { recursive: true }); + } + } + fs.writeFileSync(path.join(outDir, 'foo.txt'), 'from my plugin'); + }, +}; + +export default plugin; +`, + ); + + execSync('npx tsc', { cwd: workDir }); + runCli('generate', workDir); + expect(fs.existsSync(path.join(workDir, 'custom-output/foo.txt'))).toBe(true); + }); +}); diff --git a/packages/testtools/package.json b/packages/testtools/package.json index 216a916d..80165012 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -41,6 +41,7 @@ "pg": "^8.13.1" }, "devDependencies": { + "@types/node": "catalog:", "@types/tmp": "catalog:", "@zenstackhq/eslint-config": "workspace:*", "@zenstackhq/typescript-config": "workspace:*" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a133573a..cf8f1075 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,6 +6,9 @@ settings: catalogs: default: + '@types/node': + specifier: ^20.17.24 + version: 20.17.24 '@types/tmp': specifier: ^0.2.6 version: 0.2.6 @@ -42,7 +45,7 @@ importers: specifier: ^9.29.0 version: 9.29.0 '@types/node': - specifier: ^20.17.24 + specifier: 'catalog:' version: 20.17.24 eslint: specifier: ~9.29.0 @@ -399,6 +402,9 @@ importers: specifier: 'catalog:' version: 5.8.3 devDependencies: + '@types/node': + specifier: 'catalog:' + version: 20.17.24 '@types/tmp': specifier: 'catalog:' version: 0.2.6 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index de6cfe1b..1bb449e6 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -10,5 +10,6 @@ catalog: langium-cli: 3.5.0 ts-pattern: ^5.7.1 typescript: ^5.0.0 + '@types/node': ^20.17.24 tmp: ^0.2.3 '@types/tmp': ^0.2.6