@compas/code-gen
Version:
Generate various boring parts of your server
219 lines (192 loc) • 6.72 kB
JavaScript
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`;
}