UNPKG

@compas/code-gen

Version:

Generate various boring parts of your server

219 lines (192 loc) 6.72 kB
import { AppError } from "@compas/stdlib"; import { fileFormatInlineComment } from "./docs.js"; import { fileImportsAddPlaceholder, fileImportsStringifyImports, } from "./imports.js"; /** * Represent a work in progress generated file. * We try to keep the options as flat and explicit as possible. All options are required * and have defaults that align more with JavaScript like languages. * * @typedef {object} GenerateFile * @property {string} relativePath The relative file path. * @property {string} contents The file contents the final file always has a trailing * newline, and optionally an initial comment added via * {@link GenerateFile.addGeneratedByComment}. * @property {boolean} addGeneratedByComment Determine if the file should contain an * initial 'Generated by \@compas/code-gen' comment. Defaults to 'true'. * @property {string} [additionToGeneratedByComment] Optional string to append to the * initial generated by comment. * @property {string} indentationValue The indentation value used. Defaults to 2 spaces. * @property {string} inlineCommentPrefix Supported inline comment styles. Defaults * to '// '. If your file format does not support inline comments, you should * make sure that no comment creation function is called. * @property {{ toString(): string }} [importCollector] Each language could provide their * own imports implementation. If this property is set, {@link * fileContextCreateGeneric} will reserve a spot for the imports and call `toString()` * when the file is finalized. * @property {{ toString(): string }} [typeImportCollector] {@link importCollector} but for type imports * @property {string} calculatedLinePrefix The accumulated prefix to write on new lines. * Can be mutated via {@link fileContextSetIndent}, {@link fileContextAddLinePrefix} * and {@link fileContextRemoveLinePrefix} * @property {{ * hasWrittenLinePrefix: boolean, * }} lineState Object to keep track of what the writer already did. */ /** * @typedef {Map<string, GenerateFile>} GenerateFileMap */ /** * Use this constant to reset {@link GenerateFile.calculatedLinePrefix} via * {@link fileContextSetIndent} * * @type {number} */ export const FILE_INDENT_RESET = Number.MIN_SAFE_INTEGER; /** * Create a generic file, most options have defaults that work for JavaScript style * languages. Most options can be overwritten by custom file implementations. * * @param {import("../generate.js").GenerateContext} generateContext * @param {string} relativePath * @param {Partial<GenerateFile>} options * @returns {GenerateFile} */ export function fileContextCreateGeneric( generateContext, relativePath, options, ) { /** @type {GenerateFile} */ const file = { addGeneratedByComment: true, indentationValue: " ", inlineCommentPrefix: "// ", ...options, contents: options.contents ?? "", // Always start with empty contents relativePath, calculatedLinePrefix: "", lineState: { hasWrittenLinePrefix: false, }, }; generateContext.files.set(relativePath, file); fileImportsAddPlaceholder(file); return file; } /** * Get a file by relative path from the context * * @param {import("../generate.js").GenerateContext} generateContext * @param {string} relativePath * @returns {GenerateFile} */ export function fileContextGet(generateContext, relativePath) { const file = generateContext.files.get(relativePath); if (!file) { throw AppError.serverError({ message: `Could not resolve a file for '${relativePath}'. Make sure that it is created.`, }); } return file; } /** * Get a file by relative path from the context, returns undefined if no file is created * yet. * * @param {import("../generate.js").GenerateContext} generateContext * @param {string} relativePath * @returns {GenerateFile|undefined} */ export function fileContextGetOptional(generateContext, relativePath) { return generateContext.files.get(relativePath); } /** * Add or remove indentation for the next lines. A positive delta adds new levels of * indentation. A negative delta removes indentation. Note that removing indentation may * conflict with other utilities that use the {@link GenerateFile.calculatedLinePrefix}. * * Resetting the line prefix and thus the indentation can be done by passing * {@link FILE_INDENT_RESET} as the {@link delta}. * * @param {GenerateFile} file * @param {number} delta */ export function fileContextSetIndent(file, delta) { if (delta >= 0) { fileContextAddLinePrefix(file, file.indentationValue.repeat(delta)); } else { fileContextRemoveLinePrefix( file, file.indentationValue.length * Math.abs(delta), ); } } /** * Add to the total line prefix that is printed on each line. * * @param {GenerateFile} file * @param {string} prefix */ export function fileContextAddLinePrefix(file, prefix) { file.calculatedLinePrefix += prefix; } /** * Remove the {@link prefixLength} number of characters from the end of the line prefix. * * @param {GenerateFile} file * @param {number} prefixLength */ export function fileContextRemoveLinePrefix(file, prefixLength) { file.calculatedLinePrefix = file.calculatedLinePrefix.substring( 0, Math.max(file.calculatedLinePrefix.length - prefixLength, 0), ); } /** * Convert files from the context to output files. * * @param {import("../generate.js").GenerateContext} generateContext * @returns {Array<import("../generate.js").OutputFile>} */ export function fileContextConvertToOutputFiles(generateContext) { /** @type {Array<import("../generate.js").OutputFile>} */ const result = []; for (const [relativePath, file] of generateContext.files.entries()) { result.push({ relativePath, contents: fileContextFinalizeGenerateFile(file), }); } return result; } /** * Add {@link GenerateFile.addGeneratedByComment} if necessary, and return the file with * newline at the end. * * @param {GenerateFile} generateFile * @returns {string} */ export function fileContextFinalizeGenerateFile(generateFile) { let generatedByComment = ""; if (generateFile.addGeneratedByComment) { generatedByComment += fileFormatInlineComment( generateFile, `Generated by @compas/code-gen`, ); } if (generateFile.additionToGeneratedByComment) { generatedByComment += "\n"; generatedByComment += fileFormatInlineComment( generateFile, generateFile.additionToGeneratedByComment, ); } if (generatedByComment) { generatedByComment += "\n\n"; } fileImportsStringifyImports(generateFile); return `${generatedByComment}${generateFile.contents}\n`; }