UNPKG

zenstack

Version:

FullStack enhancement for Prisma ORM: seamless integration from database to UI

337 lines 17.3 kB
"use strict"; 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