@zenstackhq/cli 3.0.0-alpha.26 → 3.0.0-alpha.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,16 +1,18 @@
1
1
  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';
4
5
  import colors from 'colors';
5
- import fs from 'node:fs';
6
6
  import path from 'node:path';
7
+ import ora from 'ora';
8
+ import { CliError } from '../cli-error';
9
+ import * as corePlugins from '../plugins';
7
10
  import { getPkgJsonConfig, getSchemaFile, loadSchemaDocument } from './action-utils';
8
11
 
9
12
  type Options = {
10
13
  schema?: string;
11
14
  output?: string;
12
15
  silent?: boolean;
13
- savePrismaSchema?: string | boolean;
14
16
  };
15
17
 
16
18
  /**
@@ -24,25 +26,10 @@ export async function run(options: Options) {
24
26
  const model = await loadSchemaDocument(schemaFile);
25
27
  const outputPath = getOutputPath(options, schemaFile);
26
28
 
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);
43
30
 
44
31
  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`));
46
33
  console.log(`You can now create a ZenStack client with it.
47
34
 
48
35
  \`\`\`ts
@@ -68,18 +55,84 @@ function getOutputPath(options: Options, schemaFile: string) {
68
55
  }
69
56
  }
70
57
 
71
- async function runPlugins(model: Model, outputPath: string, tsSchemaFile: string) {
58
+ async function runPlugins(schemaFile: string, model: Model, outputPath: string) {
72
59
  const plugins = model.declarations.filter(isPlugin);
60
+ const processedPlugins: { cliPlugin: CliPlugin; pluginOptions: Record<string, unknown> }[] = [];
61
+
73
62
  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
+ let moduleSpec = provider;
73
+ if (moduleSpec.startsWith('.')) {
74
+ // relative to schema's path
75
+ moduleSpec = path.resolve(path.dirname(schemaFile), moduleSpec);
76
+ }
77
+ try {
78
+ cliPlugin = (await import(moduleSpec)).default as CliPlugin;
79
+ } catch (error) {
80
+ throw new CliError(`Failed to load plugin ${provider}: ${error}`);
81
+ }
82
+ }
83
+
84
+ processedPlugins.push({ cliPlugin, pluginOptions: getPluginOptions(plugin) });
85
+ }
86
+
87
+ const defaultPlugins = [corePlugins['typescript']].reverse();
88
+ defaultPlugins.forEach((d) => {
89
+ if (!processedPlugins.some((p) => p.cliPlugin === d)) {
90
+ processedPlugins.push({ cliPlugin: d, pluginOptions: {} });
91
+ }
92
+ });
93
+
94
+ for (const { cliPlugin, pluginOptions } of processedPlugins) {
95
+ invariant(
96
+ typeof cliPlugin.generate === 'function',
97
+ `Plugin ${cliPlugin.name} does not have a generate function`,
98
+ );
99
+
100
+ // run plugin generator
101
+ const spinner = ora(cliPlugin.statusText ?? `Running plugin ${cliPlugin.name}`).start();
102
+ try {
103
+ await cliPlugin.generate({
104
+ schemaFile,
105
+ model,
106
+ defaultOutputPath: outputPath,
107
+ pluginOptions,
108
+ });
109
+ spinner.succeed();
110
+ } catch (err) {
111
+ spinner.fail();
112
+ console.error(err);
113
+ }
114
+ }
115
+ }
116
+
117
+ function getPluginProvider(plugin: Plugin) {
118
+ const providerField = plugin.fields.find((f) => f.name === 'provider');
119
+ invariant(providerField, `Plugin ${plugin.name} does not have a provider field`);
120
+ const provider = (providerField.value as LiteralExpr).value as string;
121
+ return provider;
122
+ }
123
+
124
+ function getPluginOptions(plugin: Plugin): Record<string, unknown> {
125
+ const result: Record<string, unknown> = {};
126
+ for (const field of plugin.fields) {
127
+ if (field.name === 'provider') {
128
+ continue; // skip provider
129
+ }
130
+ const value = getLiteral(field.value) ?? getLiteralArray(field.value);
131
+ if (value === undefined) {
132
+ console.warn(`Plugin "${plugin.name}" option "${field.name}" has unsupported value, skipping`);
133
+ continue;
80
134
  }
81
- const generator = (await import(useProvider)).default as CliGenerator;
82
- console.log('Running generator:', provider);
83
- await generator({ model, outputPath, tsSchemaFile });
135
+ result[field.name] = value;
84
136
  }
137
+ return result;
85
138
  }
package/src/index.ts CHANGED
@@ -55,12 +55,6 @@ export function createProgram() {
55
55
  .description('Run code generation.')
56
56
  .addOption(schemaOption)
57
57
  .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
- )
64
58
  .addOption(new Option('-o, --output <path>', 'default output directory for core plugins'))
65
59
  .action(generateAction);
66
60
 
@@ -0,0 +1,2 @@
1
+ export { default as prisma } from './prisma';
2
+ export { default as typescript } from './typescript';
@@ -0,0 +1,21 @@
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 outDir = defaultOutputPath;
10
+ if (typeof pluginOptions['output'] === 'string') {
11
+ outDir = path.resolve(defaultOutputPath, pluginOptions['output']);
12
+ if (!fs.existsSync(outDir)) {
13
+ fs.mkdirSync(outDir, { recursive: true });
14
+ }
15
+ }
16
+ const prismaSchema = await new PrismaSchemaGenerator(model).generate();
17
+ fs.writeFileSync(path.join(outDir, 'schema.prisma'), prismaSchema);
18
+ },
19
+ };
20
+
21
+ export default plugin;
@@ -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 outDir = defaultOutputPath;
11
+ if (typeof pluginOptions['output'] === 'string') {
12
+ outDir = path.resolve(defaultOutputPath, pluginOptions['output']);
13
+ if (!fs.existsSync(outDir)) {
14
+ fs.mkdirSync(outDir, { recursive: true });
15
+ }
16
+ }
17
+ await new TsSchemaGenerator().generate(model, outDir);
18
+ },
19
+ };
20
+
21
+ export default plugin;
@@ -30,18 +30,6 @@ describe('CLI generate command test', () => {
30
30
  expect(fs.existsSync(path.join(workDir, 'zenstack/schema.ts'))).toBe(true);
31
31
  });
32
32
 
33
- it('should respect save prisma schema option', () => {
34
- const workDir = createProject(model);
35
- runCli('generate --save-prisma-schema', workDir);
36
- expect(fs.existsSync(path.join(workDir, 'zenstack/schema.prisma'))).toBe(true);
37
- });
38
-
39
- it('should respect save prisma schema custom path option', () => {
40
- const workDir = createProject(model);
41
- runCli('generate --save-prisma-schema "../prisma/schema.prisma"', workDir);
42
- expect(fs.existsSync(path.join(workDir, 'prisma/schema.prisma'))).toBe(true);
43
- });
44
-
45
33
  it('should respect package.json config', () => {
46
34
  const workDir = createProject(model);
47
35
  fs.mkdirSync(path.join(workDir, 'foo'));
@@ -0,0 +1,50 @@
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
+ import { execSync } from 'node:child_process';
6
+
7
+ describe('Custom plugins tests', () => {
8
+ it('runs custom plugin generator', () => {
9
+ const workDir = createProject(`
10
+ plugin custom {
11
+ provider = '../my-plugin.js'
12
+ output = '../custom-output'
13
+ }
14
+
15
+ model User {
16
+ id String @id @default(cuid())
17
+ }
18
+ `);
19
+
20
+ fs.writeFileSync(
21
+ path.join(workDir, 'my-plugin.ts'),
22
+ `
23
+ import type { CliPlugin } from '@zenstackhq/sdk';
24
+ import fs from 'node:fs';
25
+ import path from 'node:path';
26
+
27
+ const plugin: CliPlugin = {
28
+ name: 'Custom Generator',
29
+ statusText: 'Generating foo.txt',
30
+ async generate({ model, defaultOutputPath, pluginOptions }) {
31
+ let outDir = defaultOutputPath;
32
+ if (typeof pluginOptions['output'] === 'string') {
33
+ outDir = path.resolve(defaultOutputPath, pluginOptions['output']);
34
+ if (!fs.existsSync(outDir)) {
35
+ fs.mkdirSync(outDir, { recursive: true });
36
+ }
37
+ }
38
+ fs.writeFileSync(path.join(outDir, 'foo.txt'), 'from my plugin');
39
+ },
40
+ };
41
+
42
+ export default plugin;
43
+ `,
44
+ );
45
+
46
+ execSync('npx tsc', { cwd: workDir });
47
+ runCli('generate', workDir);
48
+ expect(fs.existsSync(path.join(workDir, 'custom-output/foo.txt'))).toBe(true);
49
+ });
50
+ });
@@ -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
+ });
package/tsconfig.json CHANGED
@@ -1,7 +1,4 @@
1
1
  {
2
2
  "extends": "@zenstackhq/typescript-config/base.json",
3
- "compilerOptions": {
4
- "baseUrl": "."
5
- },
6
3
  "include": ["src/**/*.ts"]
7
4
  }