zenstack
Version:
FullStack enhancement for Prisma ORM: seamless integration from database to UI
337 lines • 17.3 kB
JavaScript
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.PluginRunner = void 0;
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-var-requires */
const ast_1 = require("@zenstackhq/language/ast");
const sdk_1 = require("@zenstackhq/sdk");
const colors_1 = __importDefault(require("colors"));
const ora_1 = __importDefault(require("ora"));
const path_1 = __importDefault(require("path"));
const plugin_utils_1 = require("../plugins/plugin-utils");
const telemetry_1 = __importDefault(require("../telemetry"));
const version_utils_1 = require("../utils/version-utils");
/**
* ZenStack plugin runner
*/
class PluginRunner {
/**
* Runs a series of nested generators
*/
run(runnerOptions) {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b;
const version = (0, version_utils_1.getVersion)();
console.log(colors_1.default.bold(`⌛️ ZenStack CLI v${version}, running plugins`));
(0, plugin_utils_1.ensureDefaultOutputFolder)(runnerOptions);
const plugins = [];
const pluginDecls = runnerOptions.schema.declarations.filter((d) => (0, ast_1.isPlugin)(d));
for (const pluginDecl of pluginDecls) {
const pluginProvider = this.getPluginProvider(pluginDecl);
if (!pluginProvider) {
console.error(`Plugin ${pluginDecl.name} has invalid provider option`);
throw new sdk_1.PluginError('', `Plugin ${pluginDecl.name} has invalid provider option`);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let pluginModule;
try {
pluginModule = this.loadPluginModule(pluginProvider, runnerOptions.schemaPath);
}
catch (err) {
console.error(`Unable to load plugin module ${pluginProvider}: ${err}`);
throw new sdk_1.PluginError('', `Unable to load plugin module ${pluginProvider}`);
}
if (!pluginModule.default || typeof pluginModule.default !== 'function') {
console.error(`Plugin provider ${pluginProvider} is missing a default function export`);
throw new sdk_1.PluginError('', `Plugin provider ${pluginProvider} is missing a default function export`);
}
const dependencies = this.getPluginDependencies(pluginModule);
const pluginOptions = {
provider: pluginProvider,
};
pluginDecl.fields.forEach((f) => {
var _a;
const value = (_a = (0, sdk_1.getLiteral)(f.value)) !== null && _a !== void 0 ? _a : (0, sdk_1.getLiteralArray)(f.value);
if (value === undefined) {
throw new sdk_1.PluginError(pluginDecl.name, `Invalid option value for ${f.name}`);
}
pluginOptions[f.name] = value;
});
plugins.push({
name: pluginDecl.name,
description: this.getPluginDescription(pluginModule),
provider: pluginProvider,
dependencies,
options: pluginOptions,
run: pluginModule.default,
module: pluginModule,
});
}
// calculate all plugins (including core plugins implicitly enabled)
const { corePlugins, userPlugins } = this.calculateAllPlugins(runnerOptions, plugins);
const allPlugins = [...corePlugins, ...userPlugins];
// check dependencies
for (const plugin of allPlugins) {
for (const dep of plugin.dependencies) {
if (!allPlugins.find((p) => p.provider === dep)) {
console.error(`Plugin ${plugin.provider} depends on "${dep}" but it's not declared`);
throw new sdk_1.PluginError(plugin.name, `Plugin ${plugin.provider} depends on "${dep}" but it's not declared`);
}
}
}
if (allPlugins.length === 0) {
console.log(colors_1.default.yellow('No plugins configured.'));
return;
}
const warnings = [];
// run core plugins first
let dmmf = undefined;
let shortNameMap;
let prismaClientPath = '@prisma/client';
let prismaClientDtsPath = undefined;
const project = (0, sdk_1.createProject)();
for (const { name, description, run, options: pluginOptions } of corePlugins) {
const options = Object.assign(Object.assign({}, pluginOptions), { prismaClientPath });
const r = yield this.runPlugin(name, description, run, runnerOptions, options, dmmf, shortNameMap, project, true);
warnings.push(...((_a = r === null || r === void 0 ? void 0 : r.warnings) !== null && _a !== void 0 ? _a : [])); // the null-check is for backward compatibility
if (r.dmmf) {
// use the DMMF returned by the plugin
dmmf = r.dmmf;
}
if (r.shortNameMap) {
// use the model short name map returned by the plugin
shortNameMap = r.shortNameMap;
}
if (r.prismaClientPath) {
// use the prisma client path returned by the plugin
prismaClientPath = r.prismaClientPath;
prismaClientDtsPath = r.prismaClientDtsPath;
}
}
// compile code generated by core plugins
yield compileProject(project, runnerOptions);
// run user plugins
for (const { name, description, run, options: pluginOptions } of userPlugins) {
const options = Object.assign(Object.assign({}, pluginOptions), { prismaClientPath, prismaClientDtsPath });
const r = yield this.runPlugin(name, description, run, runnerOptions, options, dmmf, shortNameMap, project, false);
warnings.push(...((_b = r === null || r === void 0 ? void 0 : r.warnings) !== null && _b !== void 0 ? _b : [])); // the null-check is for backward compatibility
}
console.log(colors_1.default.green(colors_1.default.bold('\n👻 All plugins completed successfully!')));
warnings.forEach((w) => console.warn(colors_1.default.yellow(w)));
console.log(`Don't forget to restart your dev server to let the changes take effect.`);
});
}
calculateAllPlugins(options, plugins) {
const corePlugins = [];
let zodImplicitlyAdded = false;
// 1. @core/prisma
const existingPrisma = plugins.find((p) => p.provider === plugin_utils_1.CorePlugins.Prisma);
if (existingPrisma) {
corePlugins.push(existingPrisma);
plugins.splice(plugins.indexOf(existingPrisma), 1);
}
else if (options.defaultPlugins) {
corePlugins.push(this.makeCorePlugin(plugin_utils_1.CorePlugins.Prisma, options.schemaPath, {}));
}
const hasValidation = this.hasValidation(options.schema);
// 2. @core/enhancer
const existingEnhancer = plugins.find((p) => p.provider === plugin_utils_1.CorePlugins.Enhancer);
if (existingEnhancer) {
// enhancer should load zod schemas if there're validation rules
existingEnhancer.options.withZodSchemas = hasValidation;
corePlugins.push(existingEnhancer);
plugins.splice(plugins.indexOf(existingEnhancer), 1);
}
else {
if (options.defaultPlugins) {
corePlugins.push(this.makeCorePlugin(plugin_utils_1.CorePlugins.Enhancer, options.schemaPath, {
// enhancer should load zod schemas if there're validation rules
withZodSchemas: hasValidation,
}));
}
}
// 3. @core/zod
const existingZod = plugins.find((p) => p.provider === plugin_utils_1.CorePlugins.Zod);
if (existingZod && !existingZod.options.output) {
// we can reuse the user-provided zod plugin if it didn't specify a custom output path
plugins.splice(plugins.indexOf(existingZod), 1);
corePlugins.push(existingZod);
}
if (!corePlugins.some((p) => p.provider === plugin_utils_1.CorePlugins.Zod) &&
options.defaultPlugins &&
corePlugins.some((p) => p.provider === plugin_utils_1.CorePlugins.Enhancer) &&
hasValidation) {
// ensure "@core/zod" is enabled if "@core/enhancer" is enabled and there're validation rules
zodImplicitlyAdded = true;
corePlugins.push(this.makeCorePlugin(plugin_utils_1.CorePlugins.Zod, options.schemaPath, { modelOnly: true }));
}
// collect core plugins introduced by dependencies
plugins.forEach((plugin) => {
// TODO: generalize this
const isTrpcPlugin = plugin.provider === '@zenstackhq/trpc' ||
// for testing
(process.env.ZENSTACK_TEST && plugin.provider.includes('trpc'));
for (const dep of plugin.dependencies) {
if (dep.startsWith('@core/')) {
const existing = corePlugins.find((p) => p.provider === dep);
if (existing) {
// TODO: generalize this
if (existing.provider === '@core/zod') {
// Zod plugin can be automatically enabled in `modelOnly` mode, however
// other plugin (tRPC) for now requires it to run in full mode
if (existing.options.modelOnly) {
delete existing.options.modelOnly;
}
if (isTrpcPlugin &&
zodImplicitlyAdded // don't do it for user defined zod plugin
) {
// pass trpc plugin's `generateModels` option down to zod plugin
existing.options.generateModels = plugin.options.generateModels;
}
}
}
else {
// add core dependency
const depOptions = {};
// TODO: generalize this
if (dep === '@core/zod' && isTrpcPlugin) {
// pass trpc plugin's `generateModels` option down to zod plugin
depOptions.generateModels = plugin.options.generateModels;
}
corePlugins.push(this.makeCorePlugin(dep, options.schemaPath, depOptions));
}
}
}
});
return { corePlugins, userPlugins: plugins };
}
makeCorePlugin(provider, schemaPath, options) {
const pluginModule = require(this.getPluginModulePath(provider, schemaPath));
const pluginName = this.getPluginName(pluginModule, provider);
return {
name: pluginName,
description: this.getPluginDescription(pluginModule),
provider: provider,
dependencies: [],
options: Object.assign(Object.assign({}, options), { provider }),
run: pluginModule.default,
module: pluginModule,
};
}
hasValidation(schema) {
return (0, sdk_1.getDataModels)(schema).some((model) => (0, sdk_1.hasValidationAttributes)(model) || this.hasTypeDefFields(model));
}
hasTypeDefFields(model) {
return model.fields.some((f) => { var _a; return (0, ast_1.isTypeDef)((_a = f.type.reference) === null || _a === void 0 ? void 0 : _a.ref); });
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getPluginName(pluginModule, pluginProvider) {
return typeof pluginModule.name === 'string' ? pluginModule.name : pluginProvider;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getPluginDescription(pluginModule) {
return typeof pluginModule.description === 'string' ? pluginModule.description : undefined;
}
getPluginDependencies(pluginModule) {
return Array.isArray(pluginModule.dependencies) ? pluginModule.dependencies : [];
}
getPluginProvider(plugin) {
const providerField = plugin.fields.find((f) => f.name === 'provider');
return (0, sdk_1.getLiteral)(providerField === null || providerField === void 0 ? void 0 : providerField.value);
}
runPlugin(name, description, run, runnerOptions, options, dmmf, shortNameMap, project, isCorePlugin) {
return __awaiter(this, void 0, void 0, function* () {
if (!isCorePlugin && !this.isPluginEnabled(name, runnerOptions)) {
(0, ora_1.default)(`Plugin "${name}" is skipped`).start().warn();
return { warnings: [] };
}
const title = description !== null && description !== void 0 ? description : `Running plugin ${colors_1.default.cyan(name)}`;
const spinner = (0, ora_1.default)(title).start();
try {
const r = yield telemetry_1.default.trackSpan('cli:plugin:start', 'cli:plugin:complete', 'cli:plugin:error', {
plugin: name,
options,
}, () => __awaiter(this, void 0, void 0, function* () {
const finalOptions = Object.assign(Object.assign({}, options), { schemaPath: runnerOptions.schemaPath, shortNameMap });
return yield run(runnerOptions.schema, finalOptions, dmmf, {
output: runnerOptions.output,
compile: runnerOptions.compile,
tsProject: project,
});
}));
spinner.succeed();
if (typeof r === 'object') {
return r;
}
else {
return { warnings: [] };
}
}
catch (err) {
spinner.fail();
throw err;
}
});
}
isPluginEnabled(name, runnerOptions) {
if (runnerOptions.withPlugins && !runnerOptions.withPlugins.includes(name)) {
return false;
}
if (runnerOptions.withoutPlugins && runnerOptions.withoutPlugins.includes(name)) {
return false;
}
return true;
}
getPluginModulePath(provider, schemaPath) {
if (process.env.ZENSTACK_TEST === '1' && provider.startsWith('@zenstackhq/')) {
// test code runs with its own sandbox of node_modules, make sure we don't
// accidentally resolve to the external ones
return path_1.default.resolve(`node_modules/${provider}`);
}
let pluginModulePath = provider;
if (provider.startsWith('@core/')) {
pluginModulePath = provider.replace(/^@core/, path_1.default.join(__dirname, '../plugins'));
}
else {
try {
// direct require
require.resolve(pluginModulePath);
}
catch (_a) {
// relative
pluginModulePath = (0, sdk_1.resolvePath)(provider, { schemaPath });
}
}
return pluginModulePath;
}
loadPluginModule(provider, schemaPath) {
const pluginModulePath = this.getPluginModulePath(provider, schemaPath);
return require(pluginModulePath);
}
}
exports.PluginRunner = PluginRunner;
function compileProject(project, runnerOptions) {
return __awaiter(this, void 0, void 0, function* () {
if (runnerOptions.compile !== false) {
// emit
yield (0, sdk_1.emitProject)(project);
}
else {
// otherwise save ts files
yield (0, sdk_1.saveProject)(project);
}
});
}
//# sourceMappingURL=plugin-runner.js.map
;