UNPKG

@compas/code-gen

Version:

Generate various boring parts of your server

432 lines (380 loc) 11.2 kB
// @ts-nocheck import { isNil } from "@compas/stdlib"; import { structureIteratorNamedTypes } from "../structure/structureIterators.js"; import { upperCaseFirst } from "../utils.js"; import { js } from "./tag/index.js"; /** * @param options * @returns {{apiInput: string, apiResponse: string}} */ export function getTypeSuffixForUseCase(options) { if (options.isBrowser) { return { apiResponse: "Api", apiInput: "Input", }; } return { apiResponse: "ApiResponse", apiInput: "Input", }; } /** * Setup stores for memoized types, so we can reuse types if necessary * * @param {import("../generated/common/types").CodeGenContext} context */ export function setupMemoizedTypes(context) { context.types = { defaultSettings: { isJSON: false, useTypescript: context.options.useTypescript, useConvert: false, useDefaults: true, isNode: context.options.isNode, isBrowser: context.options.isBrowser, suffix: "", fileTypeIO: "outputClient", }, ...context.options.typeCache, }; if (!context.types.typeMap) { Object.assign(context.types, { rawImports: new Set(), typeMap: new Map(), calculatingTypes: new Set(), }); } if (!context.options.isBrowser) { for (const type of structureIteratorNamedTypes(context.structure)) { if (type.type === "route" || type.type === "crud") { continue; } getTypeNameForType(context, type, "", { isTypeFile: true, }); } } } /** * Use the memoized types and the provided settings to create a new type * * @param {import("../generated/common/types").CodeGenContext} context * @param {CodeGenType} type * @param {string} suffix * @param {CodeGenTypeSettings} settings */ export function getTypeNameForType(context, type, suffix, settings) { const hasName = !isNil(type?.uniqueName); // Potential new name, should be registered any way const name = `${type?.uniqueName ?? ""}${upperCaseFirst(suffix ?? "")}`; if (hasName && context.types.typeMap.has(name)) { return name; } // Recursive type handling if (hasName && context.types.calculatingTypes.has(name)) { return name; } // Setup type if (hasName) { context.types.calculatingTypes.add(name); context.types.typeMap.set(name, ""); } const stringOfType = generateTypeDefinition(context, type, { ...context.types.defaultSettings, ...settings, suffix, }); if (!hasName) { return stringOfType; } // Check if the same type value already exists let found = undefined; for (const [foundName, foundValue] of context.types.typeMap.entries()) { if (foundValue === stringOfType) { found = foundName; break; } } if (!found) { context.types.typeMap.set(name, stringOfType); found = name; } else if (found && name !== found) { context.types.typeMap.set(name, found); found = name; } context.types.calculatingTypes.delete(name); return found; } /** * @param {import("../generated/common/types").CodeGenContext} context */ export function generateTypeFile(context) { const declareGlobal = context.options.declareGlobalTypes === false ? false : !context.options.useTypescript; const typeFile = js` ${[...context.types.rawImports]} ${ context.options.useTypescript ? "// An export soo all things work correctly with linters, ts, ...\n export const __generated__ = true;" : "" } ${!declareGlobal ? "" : "declare global {"} ${getMemoizedNamedTypes(context)} ${!declareGlobal ? "" : "}"} `; context.outputFiles.push({ contents: typeFile, relativePath: `./common/types${ context.options.useTypescript ? ".ts" : ".d.ts" }`, }); } /** * @param {import("../generated/common/types").CodeGenContext} context * @param {CodeGenType} type * @param {CodeGenTypeSettings} settings */ export function generateTypeDefinition( context, type, { isJSON, useConvert, useDefaults, useTypescript, isCommonFile, isTypeFile, isNode, isBrowser, suffix, fileTypeIO, } = {}, ) { const recurseSettings = { isJSON: isJSON || false, useConvert, useDefaults, useTypescript, isCommonFile, isTypeFile, isNode, isBrowser, suffix: suffix ?? "", fileTypeIO, }; if (isNil(type)) { type = { type: "any", isOptional: true }; } let result = ""; if (type.isOptional && (!useDefaults || isNil(type.defaultValue))) { result += "undefined|"; } if (type.validator?.allowNull && (!useDefaults || isNil(type.defaultValue))) { result += "null|"; } switch (type.type) { case "any": if (!isNil(type.rawValue)) { result += type.rawValue; if (type.rawValueImport.typeScript || type.rawValueImport.javaScript) { context.types.rawImports.add( type.rawValueImport.typeScript || type.rawValueImport.javaScript, ); } } else { result += "any"; } break; case "anyOf": { let didHaveUndefined = result.startsWith("undefined"); let didHaveNull = result.startsWith("null"); result += type.values .map((it) => { let partial = generateTypeDefinition(context, it, recurseSettings); if (partial.startsWith("undefined")) { if (didHaveUndefined) { partial = partial.substring(10); } else { didHaveUndefined = true; } } if (partial.startsWith("null")) { if (didHaveNull) { partial = partial.substring(10); } else { didHaveNull = true; } } return partial; }) .join("|"); break; } case "array": result += "("; result += generateTypeDefinition(context, type.values, recurseSettings); result += ")[]"; if (type.validator.convert && useConvert) { result += "|("; result += generateTypeDefinition(context, type.values, recurseSettings); result += ")"; } break; case "boolean": if (!isNil(type.oneOf)) { result += type.oneOf; if (type.validator.convert && useConvert) { result += `|"${type.oneOf}"`; } } else { result += `boolean`; if (type.validator.convert && useConvert) { result += `|"true"|"false"`; } } break; case "date": if (isBrowser || isJSON) { result += "string"; } else if (type.specifier) { result += "string"; } else { result += "Date"; } break; case "file": if (fileTypeIO === "input" && isBrowser) { if (context.options.environment.clientRuntime === "browser") { result += `{ name?: string, data: Blob }`; } else if ( context.options.environment.clientRuntime === "react-native" ) { result += `(string | {name?: string, type?: string, uri: string })`; } } else if (fileTypeIO === "input" && isNode) { result += `{ name?: string, data: ReadableStream }`; } else if (fileTypeIO === "outputRouter") { result += `{ size: number, filepath: string, originalFilename?: string, newFilename?: string, mimetype?: string, mtime?: Date, hashAlgorithm?: "sha1" | "md5" | "sha256", hash?: string }`; } else if (fileTypeIO === "outputClient" && isBrowser) { result += "Blob"; } else if (fileTypeIO === "outputClient" && isNode) { result += "ReadableStream"; } else { result += "unknown"; } break; case "generic": if (Array.isArray(type.keys.oneOf)) { result += `{ [ key in `; result += generateTypeDefinition(context, type.keys, recurseSettings); } else if (Array.isArray(type.keys.reference?.oneOf)) { result += `{ [ K in `; result += generateTypeDefinition(context, type.keys, recurseSettings); } else { result += `{ [ key: `; result += generateTypeDefinition(context, type.keys, recurseSettings); } result += "]:"; result += generateTypeDefinition(context, type.values, recurseSettings); result += "}"; break; case "number": if (type.oneOf) { result += type.oneOf.join("|"); if (useConvert && type.validator.convert) { result += `|"${type.oneOf.join(`"|"`)}"`; } } else { result += `number`; if (useConvert && type.validator.convert) { result += `|string`; } } break; case "object": result += "{"; for (const key of Object.keys(type.keys)) { const right = generateTypeDefinition( context, type.keys[key], recurseSettings, ); let separator = ":"; // If right is a reference, it doesn't have to start with 'undefined' // So we need to manually check the reference as well if ( right.startsWith("undefined|") || (type.keys[key].reference?.isOptional && (!useDefaults || isNil(type.keys[key].reference.defaultValue))) ) { separator = "?:"; } result += `"${key}"${separator} ${right}, `; } result += "}"; break; case "reference": { const typeName = getTypeNameForType( context, type.reference, suffix, recurseSettings, ); if (context.options.declareGlobalTypes === false && !isTypeFile) { result += `import("./${ isCommonFile ? "" : "../common/" }types").${typeName}`; } else { result += typeName; } break; } case "string": if (type.oneOf) { result += `"${type.oneOf.join(`"|"`)}"`; } else { result += "string"; } break; case "uuid": result += `string`; break; default: // Just use the 'undefined' flow, so an any type return generateTypeDefinition(context, undefined, recurseSettings); } return result; } /** * @param {import("../generated/common/types").CodeGenContext} context * @returns {string[]} */ function getMemoizedNamedTypes(context) { const result = []; const { useTypescript } = context.types.defaultSettings; const declareGlobal = context.options.declareGlobalTypes === false ? false : !useTypescript; const uniqueNameDocsMap = {}; for (const value of structureIteratorNamedTypes(context.structure)) { if (value.docString && value.docString.length > 0) { uniqueNameDocsMap[upperCaseFirst(value.uniqueName)] = value.docString; } } for (const [name, type] of context.types.typeMap.entries()) { let intermediate = ""; if (uniqueNameDocsMap[name]) { intermediate += `// ${uniqueNameDocsMap[name].replaceAll( "\n", "\n // ", )}\n`; } intermediate += `${!declareGlobal ? "export " : ""}type ${name} = `; intermediate += type; intermediate += `;`; result.push(intermediate); } return result; }