Skip to content

Commit e8f7a2d

Browse files
authored
chore: more CLI options (#695)
1 parent eba3390 commit e8f7a2d

File tree

18 files changed

+432
-137
lines changed

18 files changed

+432
-137
lines changed

packages/schema/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@
9898
"semver": "^7.3.8",
9999
"sleep-promise": "^9.1.0",
100100
"strip-color": "^0.1.0",
101+
"tiny-invariant": "^1.3.1",
101102
"ts-morph": "^16.0.0",
102103
"ts-pattern": "^4.3.0",
103104
"upper-case-first": "^2.0.2",

packages/schema/src/cli/actions/generate.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import { PluginError } from '@zenstackhq/sdk';
22
import colors from 'colors';
33
import path from 'path';
4-
import { Context } from '../../types';
5-
import { PackageManagers } from '../../utils/pkg-utils';
64
import { CliError } from '../cli-error';
75
import {
86
checkNewVersion,
@@ -11,13 +9,15 @@ import {
119
loadDocument,
1210
requiredPrismaVersion,
1311
} from '../cli-util';
14-
import { PluginRunner } from '../plugin-runner';
12+
import { PluginRunner, PluginRunnerOptions } from '../plugin-runner';
1513

1614
type Options = {
1715
schema: string;
18-
packageManager: PackageManagers | undefined;
16+
output?: string;
1917
dependencyCheck: boolean;
2018
versionCheck: boolean;
19+
compile: boolean;
20+
defaultPlugins: boolean;
2121
};
2222

2323
/**
@@ -53,14 +53,17 @@ export async function generate(projectPath: string, options: Options) {
5353

5454
async function runPlugins(options: Options) {
5555
const model = await loadDocument(options.schema);
56-
const context: Context = {
56+
57+
const runnerOpts: PluginRunnerOptions = {
5758
schema: model,
5859
schemaPath: path.resolve(options.schema),
59-
outDir: path.dirname(options.schema),
60+
defaultPlugins: options.defaultPlugins,
61+
output: options.output,
62+
compile: options.compile,
6063
};
6164

6265
try {
63-
await new PluginRunner().run(context);
66+
await new PluginRunner().run(runnerOpts);
6467
} catch (err) {
6568
if (err instanceof PluginError) {
6669
console.error(colors.red(`${err.plugin}: ${err.message}`));

packages/schema/src/cli/index.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ export function createProgram() {
8181
.addOption(configOption)
8282
.addOption(pmOption)
8383
.addOption(new Option('--prisma <file>', 'location of Prisma schema file to bootstrap from'))
84-
.addOption(new Option('--tag [tag]', 'the NPM package tag to use when installing dependencies'))
84+
.addOption(new Option('--tag <tag>', 'the NPM package tag to use when installing dependencies'))
8585
.addOption(noVersionCheckOption)
8686
.argument('[path]', 'project path', '.')
8787
.action(initAction);
@@ -90,8 +90,10 @@ export function createProgram() {
9090
.command('generate')
9191
.description('Run code generation.')
9292
.addOption(schemaOption)
93+
.addOption(new Option('-o, --output <path>', 'default output directory for built-in plugins'))
9394
.addOption(configOption)
94-
.addOption(pmOption)
95+
.addOption(new Option('--no-default-plugins', 'do not run default plugins'))
96+
.addOption(new Option('--no-compile', 'do not compile the output of built-in plugins'))
9597
.addOption(noVersionCheckOption)
9698
.addOption(noDependencyCheck)
9799
.action(generateAction);

packages/schema/src/cli/plugin-runner.ts

Lines changed: 85 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/* eslint-disable @typescript-eslint/no-explicit-any */
22
/* eslint-disable @typescript-eslint/no-var-requires */
33
import type { DMMF } from '@prisma/generator-helper';
4-
import { isPlugin, Plugin } from '@zenstackhq/language/ast';
4+
import { isPlugin, Model, Plugin } from '@zenstackhq/language/ast';
55
import {
66
getDataModels,
77
getDMMF,
@@ -19,9 +19,7 @@ import ora from 'ora';
1919
import path from 'path';
2020
import { ensureDefaultOutputFolder } from '../plugins/plugin-utils';
2121
import telemetry from '../telemetry';
22-
import type { Context } from '../types';
2322
import { getVersion } from '../utils/version-utils';
24-
import { config } from './config';
2523

2624
type PluginInfo = {
2725
name: string;
@@ -32,23 +30,31 @@ type PluginInfo = {
3230
module: any;
3331
};
3432

33+
export type PluginRunnerOptions = {
34+
schema: Model;
35+
schemaPath: string;
36+
output?: string;
37+
defaultPlugins: boolean;
38+
compile: boolean;
39+
};
40+
3541
/**
3642
* ZenStack plugin runner
3743
*/
3844
export class PluginRunner {
3945
/**
4046
* Runs a series of nested generators
4147
*/
42-
async run(context: Context): Promise<void> {
48+
async run(options: PluginRunnerOptions): Promise<void> {
4349
const version = getVersion();
4450
console.log(colors.bold(`⌛️ ZenStack CLI v${version}, running plugins`));
4551

46-
ensureDefaultOutputFolder();
52+
ensureDefaultOutputFolder(options);
4753

4854
const plugins: PluginInfo[] = [];
49-
const pluginDecls = context.schema.declarations.filter((d): d is Plugin => isPlugin(d));
55+
const pluginDecls = options.schema.declarations.filter((d): d is Plugin => isPlugin(d));
5056

51-
let prismaOutput = resolvePath('./prisma/schema.prisma', { schemaPath: context.schemaPath, name: '' });
57+
let prismaOutput = resolvePath('./prisma/schema.prisma', { schemaPath: options.schemaPath, name: '' });
5258

5359
for (const pluginDecl of pluginDecls) {
5460
const pluginProvider = this.getPluginProvider(pluginDecl);
@@ -73,59 +79,35 @@ export class PluginRunner {
7379

7480
const dependencies = this.getPluginDependencies(pluginModule);
7581
const pluginName = this.getPluginName(pluginModule, pluginProvider);
76-
const options: PluginOptions = { schemaPath: context.schemaPath, name: pluginName };
82+
const pluginOptions: PluginOptions = { schemaPath: options.schemaPath, name: pluginName };
7783

7884
pluginDecl.fields.forEach((f) => {
7985
const value = getLiteral(f.value) ?? getLiteralArray(f.value);
8086
if (value === undefined) {
8187
throw new PluginError(pluginName, `Invalid option value for ${f.name}`);
8288
}
83-
options[f.name] = value;
89+
pluginOptions[f.name] = value;
8490
});
8591

8692
plugins.push({
8793
name: pluginName,
8894
provider: pluginProvider,
8995
dependencies,
90-
options,
96+
options: pluginOptions,
9197
run: pluginModule.default as PluginFunction,
9298
module: pluginModule,
9399
});
94100

95-
if (pluginProvider === '@core/prisma' && typeof options.output === 'string') {
101+
if (pluginProvider === '@core/prisma' && typeof pluginOptions.output === 'string') {
96102
// record custom prisma output path
97-
prismaOutput = resolvePath(options.output, options);
103+
prismaOutput = resolvePath(pluginOptions.output, pluginOptions);
98104
}
99105
}
100106

101-
// make sure prerequisites are included
102-
const corePlugins: Array<{ provider: string; options?: Record<string, unknown> }> = [
103-
{ provider: '@core/prisma' },
104-
{ provider: '@core/model-meta' },
105-
{ provider: '@core/access-policy' },
106-
];
107-
108-
if (getDataModels(context.schema).some((model) => hasValidationAttributes(model))) {
109-
// '@core/zod' plugin is auto-enabled if there're validation rules
110-
corePlugins.push({ provider: '@core/zod', options: { modelOnly: true } });
111-
}
112-
113-
// core plugins introduced by dependencies
114-
plugins
115-
.flatMap((p) => p.dependencies)
116-
.forEach((dep) => {
117-
if (dep.startsWith('@core/')) {
118-
const existing = corePlugins.find((p) => p.provider === dep);
119-
if (existing) {
120-
// reset options to default
121-
existing.options = undefined;
122-
} else {
123-
// add core dependency
124-
corePlugins.push({ provider: dep });
125-
}
126-
}
127-
});
107+
// get core plugins that need to be enabled
108+
const corePlugins = this.calculateCorePlugins(options, plugins);
128109

110+
// shift/insert core plugins to the front
129111
for (const corePlugin of corePlugins.reverse()) {
130112
const existingIdx = plugins.findIndex((p) => p.provider === corePlugin.provider);
131113
if (existingIdx >= 0) {
@@ -141,7 +123,7 @@ export class PluginRunner {
141123
name: pluginName,
142124
provider: corePlugin.provider,
143125
dependencies: [],
144-
options: { schemaPath: context.schemaPath, name: pluginName, ...corePlugin.options },
126+
options: { schemaPath: options.schemaPath, name: pluginName, ...corePlugin.options },
145127
run: pluginModule.default,
146128
module: pluginModule,
147129
});
@@ -161,12 +143,17 @@ export class PluginRunner {
161143
}
162144
}
163145

146+
if (plugins.length === 0) {
147+
console.log(colors.yellow('No plugins configured.'));
148+
return;
149+
}
150+
164151
const warnings: string[] = [];
165152

166153
let dmmf: DMMF.Document | undefined = undefined;
167-
for (const { name, provider, run, options } of plugins) {
154+
for (const { name, provider, run, options: pluginOptions } of plugins) {
168155
// const start = Date.now();
169-
await this.runPlugin(name, run, context, options, dmmf, warnings);
156+
await this.runPlugin(name, run, options, pluginOptions, dmmf, warnings);
170157
// console.log(`✅ Plugin ${colors.bold(name)} (${provider}) completed in ${Date.now() - start}ms`);
171158
if (provider === '@core/prisma') {
172159
// load prisma DMMF
@@ -175,14 +162,64 @@ export class PluginRunner {
175162
});
176163
}
177164
}
178-
179165
console.log(colors.green(colors.bold('\n👻 All plugins completed successfully!')));
180166

181167
warnings.forEach((w) => console.warn(colors.yellow(w)));
182168

183169
console.log(`Don't forget to restart your dev server to let the changes take effect.`);
184170
}
185171

172+
private calculateCorePlugins(options: PluginRunnerOptions, plugins: PluginInfo[]) {
173+
const corePlugins: Array<{ provider: string; options?: Record<string, unknown> }> = [];
174+
175+
if (options.defaultPlugins) {
176+
corePlugins.push(
177+
{ provider: '@core/prisma' },
178+
{ provider: '@core/model-meta' },
179+
{ provider: '@core/access-policy' }
180+
);
181+
} else if (plugins.length > 0) {
182+
// "@core/prisma" plugin is always enabled if any plugin is configured
183+
corePlugins.push({ provider: '@core/prisma' });
184+
}
185+
186+
// "@core/access-policy" has implicit requirements
187+
if ([...plugins, ...corePlugins].find((p) => p.provider === '@core/access-policy')) {
188+
// make sure "@core/model-meta" is enabled
189+
if (!corePlugins.find((p) => p.provider === '@core/model-meta')) {
190+
corePlugins.push({ provider: '@core/model-meta' });
191+
}
192+
193+
// '@core/zod' plugin is auto-enabled by "@core/access-policy"
194+
// if there're validation rules
195+
if (!corePlugins.find((p) => p.provider === '@core/zod') && this.hasValidation(options.schema)) {
196+
corePlugins.push({ provider: '@core/zod', options: { modelOnly: true } });
197+
}
198+
}
199+
200+
// core plugins introduced by dependencies
201+
plugins
202+
.flatMap((p) => p.dependencies)
203+
.forEach((dep) => {
204+
if (dep.startsWith('@core/')) {
205+
const existing = corePlugins.find((p) => p.provider === dep);
206+
if (existing) {
207+
// reset options to default
208+
existing.options = undefined;
209+
} else {
210+
// add core dependency
211+
corePlugins.push({ provider: dep });
212+
}
213+
}
214+
});
215+
216+
return corePlugins;
217+
}
218+
219+
private hasValidation(schema: Model) {
220+
return getDataModels(schema).some((model) => hasValidationAttributes(model));
221+
}
222+
186223
// eslint-disable-next-line @typescript-eslint/no-explicit-any
187224
private getPluginName(pluginModule: any, pluginProvider: string): string {
188225
return typeof pluginModule.name === 'string' ? (pluginModule.name as string) : pluginProvider;
@@ -200,7 +237,7 @@ export class PluginRunner {
200237
private async runPlugin(
201238
name: string,
202239
run: PluginFunction,
203-
context: Context,
240+
runnerOptions: PluginRunnerOptions,
204241
options: PluginOptions,
205242
dmmf: DMMF.Document | undefined,
206243
warnings: string[]
@@ -216,7 +253,10 @@ export class PluginRunner {
216253
options,
217254
},
218255
async () => {
219-
let result = run(context.schema, options, dmmf, config);
256+
let result = run(runnerOptions.schema, options, dmmf, {
257+
output: runnerOptions.output,
258+
compile: runnerOptions.compile,
259+
});
220260
if (result instanceof Promise) {
221261
result = await result;
222262
}
Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import { Model } from '@zenstackhq/language/ast';
2-
import { PluginOptions } from '@zenstackhq/sdk';
1+
import { PluginFunction } from '@zenstackhq/sdk';
32
import PolicyGenerator from './policy-guard-generator';
43

54
export const name = 'Access Policy';
65

7-
export default async function run(model: Model, options: PluginOptions) {
8-
return new PolicyGenerator().generate(model, options);
9-
}
6+
const run: PluginFunction = async (model, options, _dmmf, globalOptions) => {
7+
return new PolicyGenerator().generate(model, options, globalOptions);
8+
};
9+
10+
export default run;

packages/schema/src/plugins/access-policy/policy-guard-generator.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
import {
3131
ExpressionContext,
3232
PluginError,
33+
PluginGlobalOptions,
3334
PluginOptions,
3435
RUNTIME_PACKAGE,
3536
analyzePolicies,
@@ -65,8 +66,8 @@ import { ExpressionWriter, FALSE, TRUE } from './expression-writer';
6566
* Generates source file that contains Prisma query guard objects used for injecting database queries
6667
*/
6768
export default class PolicyGenerator {
68-
async generate(model: Model, options: PluginOptions) {
69-
let output = options.output ? (options.output as string) : getDefaultOutputFolder();
69+
async generate(model: Model, options: PluginOptions, globalOptions?: PluginGlobalOptions) {
70+
let output = options.output ? (options.output as string) : getDefaultOutputFolder(globalOptions);
7071
if (!output) {
7172
throw new PluginError(options.name, `Unable to determine output path, not running plugin`);
7273
}
@@ -147,7 +148,14 @@ export default class PolicyGenerator {
147148

148149
sf.addStatements('export default policy');
149150

150-
const shouldCompile = options.compile !== false;
151+
let shouldCompile = true;
152+
if (typeof options.compile === 'boolean') {
153+
// explicit override
154+
shouldCompile = options.compile;
155+
} else if (globalOptions) {
156+
shouldCompile = globalOptions.compile;
157+
}
158+
151159
if (!shouldCompile || options.preserveTsFiles === true) {
152160
// save ts files
153161
await saveProject(project);

0 commit comments

Comments
 (0)