UNPKG

@lbu/code-gen

Version:

Generate various boring parts of your server

350 lines (302 loc) 8.57 kB
import { newLogger, printProcessMemoryUsage } from "@lbu/insight"; import { AppError, isNil } from "@lbu/stdlib"; import { ReferenceType } from "./builders/ReferenceType.js"; import { buildOrInfer } from "./builders/utils.js"; import { addGroupsToGeneratorInput, addToData, hoistNamedItems, } from "./generate.js"; import { validateCodeGenStructure, validateCodeGenType, validatorSetError, } from "./generated/index.js"; import { generate } from "./generator/index.js"; import { getInternalRoutes } from "./generator/router/index.js"; import { lowerCaseFirst } from "./utils.js"; /** * @type {GenerateOpts} */ const defaultGenerateOptionsBrowser = { isBrowser: true, isNodeServer: false, isNode: false, enabledGenerators: ["type", "validator", "apiClient", "reactQuery"], useTypescript: true, dumpStructure: false, dumpPostgres: false, }; /** * @type {GenerateOpts} */ const defaultGenerateOptionsNodeServer = { isBrowser: false, isNodeServer: true, isNode: true, enabledGenerators: ["type", "validator", "sql", "router", "apiClient"], useTypescript: false, dumpStructure: true, dumpPostgres: true, }; /** * @type {GenerateOpts} */ const defaultGenerateOptionsNode = { isBrowser: false, isNodeServer: false, isNode: true, enabledGenerators: ["type", "validator"], useTypescript: false, dumpStructure: false, dumpPostgres: false, }; /** * @class */ export class App { /** * @type {string[]} */ static defaultEslintIgnore = ["no-unused-vars"]; /** * @param {AppOpts} options */ constructor({ verbose }) { /** * @type {string} */ this.fileHeader = `// Generated by @lbu/code-gen\n`; /** * @type {boolean} */ this.verbose = verbose || false; /** * @type {Logger} */ this.logger = newLogger({ ctx: { type: "code_gen", }, }); /** @type {Set<TypeBuilderLike>} */ this.unprocessedData = new Set(); /** @type {CodeGenStructure} */ this.data = {}; } /** * Create a new App instance * * @public * @param {AppOpts} [options={}] Optional options * @returns {App} */ static new(options = {}) { if (!isNil(validateCodeGenType)) { validatorSetError(AppError.validationError); } return new App(options); } /** * @param {...TypeBuilderLike} builders * @returns {this} */ add(...builders) { for (const builder of builders) { this.unprocessedData.add(builder); } return this; } /** * Add relations to the provided reference. * The provided reference must already exist. * This only works when referencing in to structure that you've passed in to * `app.extend`. * * @param {ReferenceType} reference * @param {...RelationType} relations */ addRelations(reference, ...relations) { if (!(reference instanceof ReferenceType)) { throw new Error( `Expected T.relation as a first argument to App.addRelations`, ); } const buildRef = reference.build(); this.processData(); const { group, name } = buildRef?.reference ?? {}; const resolved = this.data[group]?.[name]; if (!resolved) { throw new Error( `Can not resolve ${group}:${name}. Make sure to extend first via app.extend.`, ); } if (resolved.type !== "object") { throw new Error( `Can only add relations to objects. Found '${resolved.type}'.`, ); } for (const relation of relations) { resolved.relations.push(relation.build()); } return this; } /** * @param {object} obj * @returns {this} */ addRaw(obj) { if (!isNil(validateCodeGenType)) { const { data, errors } = validateCodeGenType(obj); if (errors) { this.logger.error(AppError.format(errors[0])); process.exit(1); } this.addToData(data); } else { // No validators present, most likely in development environment of lbu this.addToData(obj); } return this; } /** * @param data * @returns {this} */ extend(data) { if (!isNil(validateCodeGenType)) { const { data: value, errors } = validateCodeGenStructure(data); if (errors) { this.logger.error(AppError.format(errors[0])); process.exit(1); } for (const groupData of Object.values(value)) { for (const item of Object.values(groupData)) { this.addToData(item); } } } else { // No validators present, most likely in development environment of lbu for (const groupData of Object.values(data)) { for (const item of Object.values(groupData)) { this.addToData(item); } } } return this; } /** * @param {GenerateOpts} options * @returns {Promise<void>} */ async generate(options) { if (isNil(options?.outputDirectory)) { throw new Error("Need options.outputDirectory to write files to."); } if ( isNil(options.isBrowser) && isNil(options.isNodeServer) && isNil(options.isNode) && isNil(options.enabledGenerators) ) { throw new Error( `Either options.isBrowser, options.isNodeServer, options.isNode or options.enabledGenerators must be set.`, ); } options.enabledGenerators = options.enabledGenerators || []; const opts = { outputDirectory: options.outputDirectory, fileHeader: this.fileHeader + formatEslint() + (options.fileHeader ?? ""), }; if ( options.isBrowser || options.enabledGenerators.indexOf("reactQuery") !== -1 ) { Object.assign(opts, defaultGenerateOptionsBrowser); } else if ( options.isNodeServer || options.enabledGenerators.indexOf("sql") !== -1 || options.enabledGenerators.indexOf("router") !== -1 ) { Object.assign(opts, defaultGenerateOptionsNodeServer); } else if ( options.isNode || options.enabledGenerators.indexOf("reactQuery") === -1 ) { Object.assign(opts, defaultGenerateOptionsNode); } opts.useTypescript = options.useTypescript ?? !!opts.useTypescript; opts.dumpStructure = options.dumpStructure ?? !!opts.dumpStructure; opts.dumpPostgres = options.dumpPostgres ?? !!opts.dumpPostgres; opts.enabledGenerators = options.enabledGenerators.length > 0 ? options.enabledGenerators : opts.enabledGenerators; // Quick hack so we can test if we have generated // before running the tests. if (options.returnFiles) { opts.returnFiles = true; } // Add internal routes for (const r of getInternalRoutes(opts)) { this.unprocessedData.add(r); } this.processData(); hoistNamedItems(this.data, this.data); opts.enabledGroups = options.enabledGroups ?? Object.keys(this.data); // Make sure to do the same case conversion here as well as to not confuse the user. opts.enabledGroups = opts.enabledGroups.map((it) => lowerCaseFirst(it)); if (opts.enabledGroups.length === 0) { throw new Error("Need at least a single group in enabledGroups"); } if ( opts.enabledGenerators.indexOf("router") !== -1 && opts.enabledGroups.indexOf("lbu") === -1 && opts.dumpStructure ) { opts.enabledGroups.push("lbu"); } // Ensure that we don't mutate the current working data of the user const dataCopy = JSON.parse(JSON.stringify(this.data)); const generatorInput = {}; addGroupsToGeneratorInput(generatorInput, dataCopy, opts.enabledGroups); // validators may not be present, fallback to just stringify if (!isNil(validateCodeGenStructure)) { const { errors } = validateCodeGenStructure(generatorInput); if (errors) { this.logger.error(AppError.format(errors[0])); process.exit(1); } } const result = await generate(this.logger, opts, generatorInput); printProcessMemoryUsage(this.logger); return result; } /** * Process unprocessed list, normalize references * Depends on referentType being available * * @private */ processData() { for (const item of this.unprocessedData) { this.addToData(buildOrInfer(item)); } this.unprocessedData.clear(); } /** * @private * @param item */ addToData(item) { addToData(this.data, item); } } /** * Format eslint-disable comment * * @returns {string} */ function formatEslint() { return `/* eslint-disable ${App.defaultEslintIgnore.join(", ")} */\n`; }