UNPKG

@compas/code-gen

Version:

Generate various boring parts of your server

536 lines (468 loc) 15.1 kB
import { isNil } from "@compas/stdlib"; import { upperCaseFirst } from "../../utils.js"; import { fileContextCreateGeneric, fileContextGet, fileContextSetIndent, } from "../file/context.js"; import { fileFormatInlineComment } from "../file/format.js"; import { fileWrite, fileWriteInline, fileWriteLinePrefix, fileWriteNewLine, fileWriteRaw, } from "../file/write.js"; import { fileImplementations } from "../processors/file-implementations.js"; import { referenceUtilsGetProperty } from "../processors/reference-utils.js"; import { structureResolveReference } from "../processors/structure.js"; import { typeDefinitionTraverse } from "../processors/type-definition-traverse.js"; import { TypescriptImportCollector } from "../target/typescript.js"; import { typesCacheAdd, typesCacheGet, typesCacheGetUsedNames, } from "./cache.js"; import { typesOptionalityIsOptional } from "./optionality.js"; /** * Resolve the `types.d.ts` output file. * * @param {import("../generate").GenerateContext} generateContext * @returns {import("../file/context").GenerateFile} */ export function typesTypescriptResolveFile(generateContext) { return fileContextGet(generateContext, "common/types.d.ts"); } /** * Create the `types.d.ts` output file. The base settings of the * {@link import("../file/context").GenerateFile} align with the Typescript output. * * @param {import("../generate").GenerateContext} generateContext * @returns {import("../file/context").GenerateFile} */ export function typesTypescriptInitFile(generateContext) { return fileContextCreateGeneric(generateContext, "common/types.d.ts", { importCollector: new TypescriptImportCollector(), }); } /** * Start a `declare global` block to generate all types in the global namespace. This is * only a good fit in the end result of an application, where TS in JSDoc is used, * preventing a bunch of unnecessary inline imports. * * @param {import("../generate").GenerateContext} generateContext * @param {import("../file/context").GenerateFile} file */ export function typesTypescriptStartDeclareGlobal(generateContext, file) { if (generateContext.options.generators.types?.declareGlobalTypes) { fileWrite(file, `declare global {`); fileContextSetIndent(file, 1); } } /** * End global type declarations if necessary. * * @param {import("../generate").GenerateContext} generateContext * @param {import("../file/context").GenerateFile} file */ export function typesTypescriptEndDeclareGlobal(generateContext, file) { if (generateContext.options.generators.types?.declareGlobalTypes) { fileContextSetIndent(file, -1); fileWrite(file, `}`); fileWrite(file, `export {};`); } } /** * Since we always start the types file, we need to check if we declared any types. When * we don't have any types, the type generator will remove this file. * * @param {import("../file/context").GenerateFile} file */ export function typesTypescriptHasDeclaredTypes(file) { if (file.contents.match(/declare global \{\s+$/gim)) { return false; } return file.contents.trim().length !== 0; } /** * Add a named type to the output. * * When generating TS types, we need to make sure that referenced types are resolved * earlier, since references need this to format their reference name. * * @param {import("../generate").GenerateContext} generateContext * @param {import("../types").NamedType< * import("../generated/common/types").ExperimentalTypeSystemDefinition * >} type * @param {import("./generator").GenerateTypeOptions} options */ export function typesTypescriptGenerateNamedType( generateContext, type, options, ) { if (typesCacheGet(generateContext, type, options)) { // We already have this type, so we can skip it. return; } const file = typesTypescriptResolveFile(generateContext); const name = typesTypescriptFormatTypeName(generateContext, type, options); // Make sure that nested references exists before we generate this type. // TODO: check if we should move this logic up in to the generator typeDefinitionTraverse( type, (type, callback) => { callback(type); }, { isInitialType: true, assignResult: false, beforeTraversal: () => {}, afterTraversal: (nestedType) => { if (nestedType.type !== "reference") { return; } // By resolving the type after reversal, all its dependent // types are already resolved. const resolvedReference = structureResolveReference( generateContext.structure, nestedType, ); if (resolvedReference === type) { // A type references itself, so we can ignore it. return; } typesTypescriptGenerateNamedType( generateContext, // @ts-expect-error resolvedReference, options, ); }, }, ); if (type.docString) { fileWrite(file, fileFormatInlineComment(file, type.docString)); } fileWriteLinePrefix(file); if (generateContext.options.generators.types?.declareGlobalTypes) { fileWriteRaw(file, `type ${name} = `); } else { fileWriteRaw(file, `export type ${name} = `); } typesTypescriptFormatType(generateContext, file, type, options); fileWrite(file, `;`); fileWriteNewLine(file); } /** * Format and write the type. Uses inline writes where possible. * * @param {import("../generate").GenerateContext} generateContext * @param {import("../file/context").GenerateFile} file * @param {import("../generated/common/types").ExperimentalTypeSystemDefinition} type * @param {import("./generator").GenerateTypeOptions} options * @returns {void} */ export function typesTypescriptFormatType( generateContext, file, type, options, ) { const isOptional = typesOptionalityIsOptional(generateContext, type, { validatorState: options.validatorState, }); let optionalStr = `|undefined`; if ( referenceUtilsGetProperty(generateContext, type, ["validator", "allowNull"]) ) { optionalStr += "|null"; } if (type.type === "reference") { const resolvedReference = structureResolveReference( generateContext.structure, type, ); const resolvedName = typesCacheGet( generateContext, // @ts-expect-error resolvedReference, options, ); fileWriteInline(file, `${resolvedName}`); if (isOptional) { fileWriteInline(file, optionalStr); } } else if (type.type === "any") { if (type.targets) { let didWrite = false; for (let i = options.targets.length - 1; i >= 0; --i) { const target = type.targets[options.targets[i]]; if (target) { if (options.validatorState === "output") { fileWriteInline(file, target.validatorOutputType); } else { fileWriteInline(file, target.validatorInputType); } didWrite = true; break; } } if (!didWrite) { fileWriteInline(file, `any`); } } else if (type.rawValue) { fileWriteInline(file, type.rawValue); } else { fileWriteInline(file, `any`); } if (isOptional) { fileWriteInline(file, optionalStr); } } else if (type.type === "anyOf") { fileContextSetIndent(file, 1); let didWriteOptional = false; let didWriteNull = false; for (const value of type.values) { didWriteOptional = didWriteOptional || referenceUtilsGetProperty(generateContext, value, ["isOptional"]); didWriteNull = didWriteNull || referenceUtilsGetProperty(generateContext, value, [ "validator", "allowNull", ]); fileWriteNewLine(file); if (value.type !== "anyOf") { // The nested anyOf will be flattened in to this one. fileWriteInline(file, `|`); } typesTypescriptFormatType(generateContext, file, value, options); } if (isOptional && !didWriteOptional) { fileWriteNewLine(file); fileWriteInline(file, `|undefined`); } if ( referenceUtilsGetProperty(generateContext, type, [ "validator", "allowNull", ]) && !didWriteNull ) { fileWriteNewLine(file); fileWriteInline(file, `|null`); } fileContextSetIndent(file, -1); } else if (type.type === "array") { fileWriteInline(file, `(`); typesTypescriptFormatType(generateContext, file, type.values, options); fileWriteInline(file, `)`); fileWriteInline(file, `[]`); if (options.validatorState === "input") { if (type.values.type !== "anyOf") { // AnyOf always starts with a `|`. fileWriteInline(file, "|"); } typesTypescriptFormatType(generateContext, file, type.values, options); } if (isOptional) { fileWriteInline(file, optionalStr); } } else if (type.type === "boolean") { if (!isNil(type.oneOf)) { fileWriteInline(file, `${type.oneOf}`); if (options.validatorState === "input") { fileWriteInline(file, `|"${type.oneOf}"`); } } else { fileWriteInline(file, `boolean`); if (options.validatorState === "input") { fileWriteInline(file, `|"true"|"false"`); } } if (isOptional) { fileWriteInline(file, optionalStr); } } else if (type.type === "date") { if (options.validatorState === "input" && isNil(type.specifier)) { fileWriteInline(file, `Date|string|number`); } else if ( type.specifier || options.targets.includes("tsAxiosBrowser") || options.targets.includes("tsAxiosReactNative") ) { fileWriteInline(file, "string"); } else { fileWriteInline(file, `Date`); } if (isOptional) { fileWriteInline(file, optionalStr); } } else if (type.type === "file") { let didWrite = false; for (let i = options.targets.length - 1; i >= 0; --i) { const target = fileImplementations[options.targets[i]]; if (target) { if (options.validatorState === "output") { fileWriteInline(file, target.validatorOutputType); } else { fileWriteInline(file, target.validatorInputType); } didWrite = true; break; } } if (!didWrite) { fileWriteInline(file, `any`); } } else if (type.type === "generic") { const oneOf = referenceUtilsGetProperty(generateContext, type.keys, [ "oneOf", ]); if (oneOf) { fileWriteInline(file, `Partial<Record<`); } else { fileWriteInline(file, `{ [key: `); } typesTypescriptFormatType(generateContext, file, type.keys, options); if (oneOf) { fileWriteInline(file, `,`); } else { fileWriteInline(file, `]: `); } typesTypescriptFormatType(generateContext, file, type.values, options); if (oneOf) { fileWriteInline(file, `>>`); } else { fileWriteInline(file, `}`); } if (isOptional) { fileWriteInline(file, optionalStr); } } else if (type.type === "number") { if (type.oneOf) { fileWriteInline(file, type.oneOf.join("|")); if (options.validatorState === "input") { fileWriteInline(file, `|"`); fileWriteInline(file, type.oneOf.join(`"|"`)); fileWriteInline(file, `"`); } } else { fileWriteInline(file, `number`); } if (isOptional) { fileWriteInline(file, optionalStr); } } else if (type.type === "object") { fileContextSetIndent(file, 1); fileWriteInline(file, `{`); for (const key of Object.keys(type.keys)) { fileWriteNewLine(file); if (type.keys[key].docString) { fileWrite(file, ""); fileWrite( file, fileFormatInlineComment(file, type.keys[key].docString), ); } const subIsOptional = typesOptionalityIsOptional( generateContext, type.keys[key], { validatorState: options.validatorState, }, ); if (subIsOptional) { fileWriteInline(file, `"${key}"?: `); } else { fileWriteInline(file, `"${key}": `); } typesTypescriptFormatType(generateContext, file, type.keys[key], options); fileWriteInline(file, `;`); } fileContextSetIndent(file, -1); fileWriteNewLine(file); fileWriteInline(file, `}`); if (isOptional) { fileWriteInline(file, optionalStr); } } else if (type.type === "string") { if (type.oneOf) { fileWriteInline(file, `"`); fileWriteInline(file, type.oneOf.join(`"|"`)); fileWriteInline(file, `"`); } else { fileWriteInline(file, `string`); } if (isOptional) { fileWriteInline(file, optionalStr); } } else if (type.type === "uuid") { fileWriteInline(file, `string`); if (isOptional) { fileWriteInline(file, optionalStr); } } } /** * Format a name for the provided type and options. * * We prefer to use the base name. This means that the order of operations in the * generators is important for the cleanest result. If the base name is used, we try a * variant with the suffix. * * When both are used we try to append a `_1`, `_2` etc., until we find an unused unique * type name. * * The used type is directly registered in the cache. * * @param {import("../generate").GenerateContext} generateContext * @param {import("../types").NamedType< * import("../generated/common/types").ExperimentalTypeSystemDefinition * >} type * @param {import("./generator").GenerateTypeOptions} options * @returns {string} */ function typesTypescriptFormatTypeName(generateContext, type, options) { const usedNames = typesCacheGetUsedNames(type); const baseName = `${upperCaseFirst(type.group)}${upperCaseFirst(type.name)}`; if (!usedNames.includes(baseName)) { typesCacheAdd(generateContext, type, options, baseName); return baseName; } const withSuffix = `${baseName}${upperCaseFirst( options.nameSuffixes[options.validatorState], )}`; if (!usedNames.includes(withSuffix)) { typesCacheAdd(generateContext, type, options, withSuffix); return withSuffix; } let numberedSuffix = 1; // eslint-disable-next-line no-constant-condition while (true) { const currentName = `${withSuffix}_${numberedSuffix}`; if (!usedNames.includes(currentName)) { typesCacheAdd(generateContext, type, options, currentName); return currentName; } numberedSuffix += 1; } } /** * Use the provided name in Typescript * * @param {import("../generate").GenerateContext} generateContext * @param {import("../file/context").GenerateFile} file * @param {string} name * @returns {string} */ export function typesTypescriptUseTypeName(generateContext, file, name) { if (generateContext.options.generators.types?.declareGlobalTypes) { return name; } const importCollector = TypescriptImportCollector.getImportCollector(file); importCollector.destructure(`../common/types`, name); return name; }