Skip to content

Commit 1e8c682

Browse files
committed
feat: cli plugin support
1 parent fb6c03e commit 1e8c682

File tree

24 files changed

+255
-92
lines changed

24 files changed

+255
-92
lines changed

.vscode/launch.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
"request": "launch",
1111
"skipFiles": ["<node_internals>/**"],
1212
"type": "node",
13-
"args": ["generate", "--schema", "${workspaceFolder}/samples/blog/zenstack/schema.zmodel"]
13+
"args": ["generate"],
14+
"cwd": "${workspaceFolder}/samples/blog/zenstack"
1415
},
1516
{
1617
"name": "Debug with TSX",
Lines changed: 79 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
import { invariant } from '@zenstackhq/common-helpers';
2-
import { isPlugin, LiteralExpr, type Model } from '@zenstackhq/language/ast';
3-
import { PrismaSchemaGenerator, TsSchemaGenerator, type CliGenerator } from '@zenstackhq/sdk';
2+
import { isPlugin, LiteralExpr, Plugin, type Model } from '@zenstackhq/language/ast';
3+
import { getLiteral, getLiteralArray } from '@zenstackhq/language/utils';
4+
import { type CliPlugin } from '@zenstackhq/sdk';
45
import colors from 'colors';
5-
import fs from 'node:fs';
66
import path from 'node:path';
7+
import ora from 'ora';
8+
import { CliError } from '../cli-error';
9+
import * as corePlugins from '../plugins';
710
import { getPkgJsonConfig, getSchemaFile, loadSchemaDocument } from './action-utils';
811

912
type Options = {
1013
schema?: string;
1114
output?: string;
1215
silent?: boolean;
13-
savePrismaSchema?: string | boolean;
1416
};
1517

1618
/**
@@ -24,25 +26,10 @@ export async function run(options: Options) {
2426
const model = await loadSchemaDocument(schemaFile);
2527
const outputPath = getOutputPath(options, schemaFile);
2628

27-
// generate TS schema
28-
const tsSchemaFile = path.join(outputPath, 'schema.ts');
29-
await new TsSchemaGenerator().generate(schemaFile, [], outputPath);
30-
31-
await runPlugins(model, outputPath, tsSchemaFile);
32-
33-
// generate Prisma schema
34-
if (options.savePrismaSchema) {
35-
const prismaSchema = await new PrismaSchemaGenerator(model).generate();
36-
let prismaSchemaFile = path.join(outputPath, 'schema.prisma');
37-
if (typeof options.savePrismaSchema === 'string') {
38-
prismaSchemaFile = path.resolve(outputPath, options.savePrismaSchema);
39-
fs.mkdirSync(path.dirname(prismaSchemaFile), { recursive: true });
40-
}
41-
fs.writeFileSync(prismaSchemaFile, prismaSchema);
42-
}
29+
await runPlugins(schemaFile, model, outputPath);
4330

4431
if (!options.silent) {
45-
console.log(colors.green(`Generation completed successfully in ${Date.now() - start}ms.`));
32+
console.log(colors.green(`Generation completed successfully in ${Date.now() - start}ms.\n`));
4633
console.log(`You can now create a ZenStack client with it.
4734
4835
\`\`\`ts
@@ -68,18 +55,79 @@ function getOutputPath(options: Options, schemaFile: string) {
6855
}
6956
}
7057

71-
async function runPlugins(model: Model, outputPath: string, tsSchemaFile: string) {
58+
async function runPlugins(schemaFile: string, model: Model, outputPath: string) {
7259
const plugins = model.declarations.filter(isPlugin);
60+
const processedPlugins: { cliPlugin: CliPlugin; pluginOptions: Record<string, unknown> }[] = [];
61+
7362
for (const plugin of plugins) {
74-
const providerField = plugin.fields.find((f) => f.name === 'provider');
75-
invariant(providerField, `Plugin ${plugin.name} does not have a provider field`);
76-
const provider = (providerField.value as LiteralExpr).value as string;
77-
let useProvider = provider;
78-
if (useProvider.startsWith('@core/')) {
79-
useProvider = `@zenstackhq/runtime/plugins/${useProvider.slice(6)}`;
63+
const provider = getPluginProvider(plugin);
64+
65+
let cliPlugin: CliPlugin;
66+
if (provider.startsWith('@core/')) {
67+
cliPlugin = (corePlugins as any)[provider.slice('@core/'.length)];
68+
if (!cliPlugin) {
69+
throw new CliError(`Unknown core plugin: ${provider}`);
70+
}
71+
} else {
72+
try {
73+
cliPlugin = (await import(provider)).default as CliPlugin;
74+
} catch (error) {
75+
throw new CliError(`Failed to load plugin ${provider}: ${error}`);
76+
}
77+
}
78+
79+
processedPlugins.push({ cliPlugin, pluginOptions: getPluginOptions(plugin) });
80+
}
81+
82+
const defaultPlugins = [corePlugins['typescript']].reverse();
83+
defaultPlugins.forEach((d) => {
84+
if (!processedPlugins.some((p) => p.cliPlugin === d)) {
85+
processedPlugins.push({ cliPlugin: d, pluginOptions: {} });
86+
}
87+
});
88+
89+
for (const { cliPlugin, pluginOptions } of processedPlugins) {
90+
invariant(
91+
typeof cliPlugin.generate === 'function',
92+
`Plugin ${cliPlugin.name} does not have a generate function`,
93+
);
94+
95+
// run plugin generator
96+
const spinner = ora(cliPlugin.statusText ?? `Running plugin ${cliPlugin.name}`).start();
97+
try {
98+
await cliPlugin.generate({
99+
schemaFile,
100+
model,
101+
defaultOutputPath: outputPath,
102+
pluginOptions,
103+
});
104+
spinner.succeed();
105+
} catch (err) {
106+
spinner.fail();
107+
console.error(err);
108+
}
109+
}
110+
}
111+
112+
function getPluginProvider(plugin: Plugin) {
113+
const providerField = plugin.fields.find((f) => f.name === 'provider');
114+
invariant(providerField, `Plugin ${plugin.name} does not have a provider field`);
115+
const provider = (providerField.value as LiteralExpr).value as string;
116+
return provider;
117+
}
118+
119+
function getPluginOptions(plugin: Plugin): Record<string, unknown> {
120+
const result: any = {};
121+
for (const field of plugin.fields) {
122+
if (field.name === 'provider') {
123+
continue; // skip provider
124+
}
125+
const value = getLiteral(field.value) ?? getLiteralArray(field.value);
126+
if (value === undefined) {
127+
console.warn(`Plugin "${plugin.name}" option "${field.name}" has unsupported value, skipping`);
128+
continue;
80129
}
81-
const generator = (await import(useProvider)).default as CliGenerator;
82-
console.log('Running generator:', provider);
83-
await generator({ model, outputPath, tsSchemaFile });
130+
result[field.name] = value;
84131
}
132+
return result;
85133
}

packages/cli/src/index.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,6 @@ export function createProgram() {
5555
.description('Run code generation.')
5656
.addOption(schemaOption)
5757
.addOption(new Option('--silent', 'do not print any output'))
58-
.addOption(
59-
new Option(
60-
'--save-prisma-schema [path]',
61-
'save a Prisma schema file, by default into the output directory',
62-
),
63-
)
6458
.addOption(new Option('-o, --output <path>', 'default output directory for core plugins'))
6559
.action(generateAction);
6660

packages/cli/src/plugins/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { default as prisma } from './prisma';
2+
export { default as typescript } from './typescript';

packages/cli/src/plugins/prisma.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { PrismaSchemaGenerator, type CliPlugin } from '@zenstackhq/sdk';
2+
import fs from 'node:fs';
3+
import path from 'node:path';
4+
5+
const plugin: CliPlugin = {
6+
name: 'Prisma Schema Generator',
7+
statusText: 'Generating Prisma schema',
8+
async generate({ model, defaultOutputPath, pluginOptions }) {
9+
let outFile = path.join(defaultOutputPath, 'schema.prisma');
10+
if (typeof pluginOptions['output'] === 'string') {
11+
const outDir = path.resolve(defaultOutputPath, pluginOptions['output']);
12+
if (!fs.existsSync(outDir)) {
13+
fs.mkdirSync(outDir, { recursive: true });
14+
}
15+
outFile = path.join(outDir, 'schema.prisma');
16+
}
17+
const prismaSchema = await new PrismaSchemaGenerator(model).generate();
18+
fs.writeFileSync(outFile, prismaSchema);
19+
},
20+
};
21+
22+
export default plugin;
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import type { CliPlugin } from '@zenstackhq/sdk';
2+
import { TsSchemaGenerator } from '@zenstackhq/sdk';
3+
import fs from 'node:fs';
4+
import path from 'node:path';
5+
6+
const plugin: CliPlugin = {
7+
name: 'TypeScript Schema Generator',
8+
statusText: 'Generating TypeScript schema',
9+
async generate({ model, defaultOutputPath, pluginOptions }) {
10+
let ourDir = defaultOutputPath;
11+
if (typeof pluginOptions['output'] === 'string') {
12+
ourDir = path.resolve(defaultOutputPath, pluginOptions['output']);
13+
if (!fs.existsSync(ourDir)) {
14+
fs.mkdirSync(ourDir, { recursive: true });
15+
}
16+
}
17+
await new TsSchemaGenerator().generate(model, ourDir);
18+
},
19+
};
20+
21+
export default plugin;
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import fs from 'node:fs';
2+
import path from 'node:path';
3+
import { describe, expect, it } from 'vitest';
4+
import { createProject, runCli } from '../utils';
5+
6+
describe('Core plugins tests', () => {
7+
it('can automatically generate a TypeScript schema with default output', () => {
8+
const workDir = createProject(`
9+
model User {
10+
id String @id @default(cuid())
11+
}
12+
`);
13+
runCli('generate', workDir);
14+
expect(fs.existsSync(path.join(workDir, 'zenstack/schema.ts'))).toBe(true);
15+
});
16+
17+
it('can automatically generate a TypeScript schema with custom output', () => {
18+
const workDir = createProject(`
19+
plugin typescript {
20+
provider = '@core/typescript'
21+
output = '../generated-schema'
22+
}
23+
24+
model User {
25+
id String @id @default(cuid())
26+
}
27+
`);
28+
runCli('generate', workDir);
29+
expect(fs.existsSync(path.join(workDir, 'generated-schema/schema.ts'))).toBe(true);
30+
});
31+
32+
it('can generate a Prisma schema with default output', () => {
33+
const workDir = createProject(`
34+
plugin prisma {
35+
provider = '@core/prisma'
36+
}
37+
38+
model User {
39+
id String @id @default(cuid())
40+
}
41+
`);
42+
runCli('generate', workDir);
43+
expect(fs.existsSync(path.join(workDir, 'zenstack/schema.prisma'))).toBe(true);
44+
});
45+
46+
it('can generate a Prisma schema with custom output', () => {
47+
const workDir = createProject(`
48+
plugin prisma {
49+
provider = '@core/prisma'
50+
output = './prisma'
51+
}
52+
53+
model User {
54+
id String @id @default(cuid())
55+
}
56+
`);
57+
runCli('generate', workDir);
58+
expect(fs.existsSync(path.join(workDir, 'zenstack/prisma/schema.prisma'))).toBe(true);
59+
});
60+
});

packages/cli/tsconfig.json

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
11
{
22
"extends": "@zenstackhq/typescript-config/base.json",
3-
"compilerOptions": {
4-
"baseUrl": "."
5-
},
63
"include": ["src/**/*.ts"]
74
}
Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
11
{
22
"extends": "@zenstackhq/typescript-config/base.json",
3-
"compilerOptions": {
4-
"baseUrl": "."
5-
},
63
"include": ["src/**/*.ts"]
74
}
Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
11
{
22
"extends": "@zenstackhq/typescript-config/base.json",
3-
"compilerOptions": {
4-
"baseUrl": "."
5-
},
63
"include": ["src/**/*.ts"]
74
}

0 commit comments

Comments
 (0)