@lbu/code-gen
Version:
Generate various boring parts of your server
350 lines (302 loc) • 8.57 kB
JavaScript
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`;
}