UNPKG

@lbu/code-gen

Version:

Generate various boring parts of your server

225 lines (199 loc) 6.8 kB
import { mkdirSync, readFileSync, writeFileSync } from "fs"; import { pathJoin } from "@lbu/stdlib"; import { copyAndSort } from "../generate.js"; import { templateContext } from "../template.js"; import { generateApiClientFiles } from "./apiClient/index.js"; import { exitOnErrorsOrReturn } from "./errors.js"; import { linkupReferencesInStructure } from "./linkup-references.js"; import { generateReactQueryFiles } from "./reactQuery/index.js"; import { generateRouterFiles } from "./router/index.js"; import { addFieldsOfRelations } from "./sql/add-fields.js"; import { createPartialTypes } from "./sql/partial-type.js"; import { generateBaseQueries } from "./sql/query-basics.js"; import { generateQueryPartials } from "./sql/query-partials.js"; import { generateTraversalQueries } from "./sql/query-traversal.js"; import { generateSqlStructure } from "./sql/structure.js"; import { addShortNamesToQueryEnabledObjects, doSqlChecks, } from "./sql/utils.js"; import { createWhereTypes } from "./sql/where-type.js"; import { addRootExportsForStructureFiles, generateStructureFile, } from "./structure.js"; import { generateTypeFile, getTypeNameForType, getTypeSuffixForUseCase, setupMemoizedTypes, } from "./types.js"; import { generateValidatorFile } from "./validator.js"; /** * * @param {Logger} logger * @param {GenerateOpts} options * @param {CodeGenStructure} structure * @returns {Promise<void>} */ export async function generate(logger, options, structure) { // We can always use '.js' as the import extension and TS will happily resolve to the // .ts files Not exactly sure how it all works, but should be good enough for now. The // issue when not providing any extension in imports / exports is that TS won't add // them even when targeting ESNext with moduleResolution Node logger.info({ system: !options.isBrowser ? !options.isNodeServer ? "isNode" : "isNodeServer" : "isBrowser", enabledGenerators: options.enabledGenerators, enabledGroups: options.enabledGroups, }); const isModule = shouldGenerateModules(logger); /** * @type {CodeGenContext} */ const context = { logger, options, structure, extension: options.useTypescript ? ".ts" : ".js", importExtension: isModule ? ".js" : "", outputFiles: [], rootExports: [], errors: [], }; // Structure: // The raw structure that is used to generate all the files. // This contains all information needed to generate again, even if different options // are needed. generateStructureFile(context); addRootExportsForStructureFiles(context); exitOnErrorsOrReturn(context); // Linkup all references, so we don't necessarily have to worry about them in all other // places. linkupReferencesInStructure(context); addFieldsOfRelations(context); exitOnErrorsOrReturn(context); const copy = {}; copyAndSort(context.structure, copy); context.structure = copy; templateContext.globals.getTypeNameForType = getTypeNameForType.bind( undefined, context, ); templateContext.globals.typeSuffix = getTypeSuffixForUseCase(context.options); setupMemoizedTypes(context); exitOnErrorsOrReturn(context); if (context.options.enabledGenerators.indexOf("validator") !== -1) { generateValidatorFile(context); exitOnErrorsOrReturn(context); } if (context.options.enabledGenerators.indexOf("router") !== -1) { generateRouterFiles(context); exitOnErrorsOrReturn(context); } if (context.options.enabledGenerators.indexOf("apiClient") !== -1) { generateApiClientFiles(context); exitOnErrorsOrReturn(context); } if (context.options.enabledGenerators.indexOf("reactQuery") !== -1) { generateReactQueryFiles(context); exitOnErrorsOrReturn(context); } if (context.options.enabledGenerators.indexOf("sql") !== -1) { doSqlChecks(context); exitOnErrorsOrReturn(context); addShortNamesToQueryEnabledObjects(context); generateSqlStructure(context); createWhereTypes(context); createPartialTypes(context); generateQueryPartials(context); generateBaseQueries(context); generateTraversalQueries(context); exitOnErrorsOrReturn(context); } if (context.options.enabledGenerators.indexOf("type") !== -1) { generateTypeFile(context); } // Create all exports so imports all happen via // `{options.outputDirectory}/index${context.extension} generateRootExportsFile(context); // Add provided file headers to all files annotateFilesWithHeader(context); exitOnErrorsOrReturn(context); // TODO: Remove context.options.outputDir before writing if (options.returnFiles) { // Used for making sure we can check if we are all set return context.outputFiles; } writeFiles(context); } /** * Join all root exports in to a single index.js file * @param {CodeGenContext} context */ export function generateRootExportsFile(context) { context.outputFiles.push({ contents: context.rootExports .map((it) => it.trim()) .sort((a, b) => { const aExport = a.startsWith("export") ? 1 : 0; const bExport = b.startsWith("export") ? 1 : 0; return aExport - bExport; }) .join("\n"), relativePath: `./index${context.extension}`, }); } /** * Use the fileHeader from options, and prefix all file contents with it * @param {CodeGenContext} context */ export function annotateFilesWithHeader(context) { for (const file of context.outputFiles) { if (file.relativePath.endsWith(".sql")) { file.contents = `${context.options.fileHeader.replace(/\/\//g, "--")}\n${ file.contents }\n`; } else { file.contents = `${context.options.fileHeader}\n${file.contents}\n`; } } } /** * Write out all files * @param {CodeGenContext} context */ export function writeFiles(context) { for (const file of context.outputFiles) { const fullPath = pathJoin( context.options.outputDirectory, file.relativePath, ); const directory = fullPath.split("/").slice(0, -1).join("/"); mkdirSync(directory, { recursive: true }); writeFileSync(fullPath, file.contents, "utf8"); } } /** * Check if we should generate ES Modules based on the package.json * @param {Logger} logger * @returns {boolean} */ export function shouldGenerateModules(logger) { try { const packageJsonSource = readFileSync( pathJoin(process.cwd(), "package.json"), "utf8", ); const packageJson = JSON.parse(packageJsonSource); return packageJson?.type === "module"; } catch { logger.error( "Could not determine if we should not generate ES Modules. Defaulting to ES modules.\n Make sure you run the generator in the root of your project.", ); return true; } }