Created
January 16, 2021 21:10
-
-
Save skurfuerst/a07ab23c3e40a45f2268f7700ceeceaf to your computer and use it in GitHub Desktop.
Generate runtypes from TypeScript definitions using ts-morph
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// RUN with a recent node.js version, using "node gen-types.mjs" | |
// DEPENDS on typescript and ts-morph | |
import { Project } from "ts-morph"; | |
import { writeFileSync } from "fs"; | |
const project = new Project({ | |
tsConfigFilePath: "tsconfig.json", | |
}); | |
// 1) DEFINITION which types to extract | |
const buildInstructions = [ | |
{ | |
targetFile: 'lib/api/gitlab/types/Users.gen.ts', | |
sourceTypes: [ | |
{ | |
file: 'node_modules/@gitbeaker/core/dist/types/services/Users.d.ts', | |
type: 'UserDetailSchemaDefault', | |
overrideTypes: { | |
created_at: 'String' | |
} | |
} | |
] | |
}, | |
{ | |
targetFile: 'lib/api/gitlab/types/Pipelines.gen.ts', | |
sourceTypes: [ | |
{ | |
file: 'node_modules/@gitbeaker/core/dist/types/services/Pipelines.d.ts', | |
type: 'PipelineSchemaDefault', | |
overrideTypes: { | |
created_at: 'String', | |
updated_at: 'String' | |
} | |
} | |
] | |
}, | |
]; | |
// 2) print SEMANTIC ERRORS detected by the TypeScript compiler, as they might influence the result. | |
const diagnostics = project.getPreEmitDiagnostics(); | |
// there might be errors in our generated files; so we ignore them (as they will be overridden anyways) | |
const allTargetFiles = buildInstructions.map(buildInstruction => buildInstruction.targetFile); | |
const filteredDiagnostics = diagnostics.filter(diagnostic => !allTargetFiles.some(targetFile => diagnostic.getSourceFile().getFilePath().includes(targetFile))); | |
console.log(project.formatDiagnosticsWithColorAndContext(filteredDiagnostics)); | |
// 3) start the RUNTYPE GENERATION | |
const generatedFilePreamble = ` | |
// this file is AUTOMATICALLY GENERATED by scripts/build-runtypes-from-ts-types.mjs. | |
// It includes Runtypes definitions for TypeScript types; so you can validate them at runtime: | |
// | |
// To regenerate the file, run "yarn gen-types" | |
`; | |
// per output file, we have one buildInstruction. | |
buildInstructions.forEach(buildInstruction => { | |
// to build up the `import {...} from 'runtypes'` line, we collect the variables we need in scope. | |
let requiredImportsFromRuntype = {}; | |
const typeDeclarations = buildInstruction.sourceTypes.map(sourceType => { | |
const typeDeclaration = project.getSourceFileOrThrow(sourceType.file).getInterfaceOrThrow(sourceType.type); | |
const comment = `// This runtime type is automatically generated from ${sourceType.type} in ${sourceType.file}\n`; | |
const runtype = `export const ${sourceType.type} = ${renderType(typeDeclaration.getType(), 0, sourceType.overrideTypes || {}, requiredImportsFromRuntype)};\n`; | |
requiredImportsFromRuntype['Static'] = true; | |
const staticType = `export type ${sourceType.type}Type = Static<typeof ${sourceType.type}>;`; | |
return comment + runtype + staticType; | |
}); | |
const importStatement = `\nimport { ${Object.keys(requiredImportsFromRuntype).sort().join(', ')} } from "runtypes";\n\n`; | |
const fileContents = generatedFilePreamble + importStatement + typeDeclarations.join("\n\n"); | |
writeFileSync(buildInstruction.targetFile, fileContents, "utf8"); | |
console.log(`Generated ${buildInstruction.targetFile}`); | |
}); | |
console.log("All done!"); | |
// Helpers | |
/** | |
* @param name string | |
* @param type {Type} | |
* @param indent number | |
* @param overrideTypes | |
* @param requiredImportsFromRuntype {} | |
*/ | |
function renderType(type, indent, overrideTypes, requiredImportsFromRuntype) { | |
if (type.isUnion()) { | |
// Sort the union types such that Undefined comes last always; taken from https://stackoverflow.com/a/29829370 | |
const [first, ...rest] = type.getUnionTypes().sort((a, b) => (a.isUndefined()) - (b.isUndefined()) || +(a > b) || -(a < b)); | |
if (!first) { | |
requiredImportsFromRuntype["Undefined"] = true; | |
return "Undefined"; | |
} | |
// concatenate the types together with ".Or" to create a union type | |
return renderType(first, indent, overrideTypes, requiredImportsFromRuntype) + rest.map(restEl => '.Or(' + renderType(restEl, indent, overrideTypes, requiredImportsFromRuntype) + ')').join(''); | |
} | |
if (type.isStringLiteral()) { | |
requiredImportsFromRuntype["Literal"] = true; | |
return `Literal(${type.getText()})`; | |
} | |
if (type.isString()) { | |
requiredImportsFromRuntype["String"] = true; | |
return "String"; | |
} | |
if (type.isNumber()) { | |
requiredImportsFromRuntype["Number"] = true; | |
return "Number"; | |
} | |
if (type.isAny()) { | |
requiredImportsFromRuntype["Unknown"] = true; | |
return "Unknown"; | |
} | |
if (type.isUndefined()) { | |
requiredImportsFromRuntype["Undefined"] = true; | |
return "Undefined"; | |
} | |
if (type.isObject()) { | |
// modelled after https://github.com/dsherret/ts-morph/issues/662 | |
const isBuiltInType = type.getSymbolOrThrow().getDeclarations().some(d => d.getSourceFile().getFilePath().includes("node_modules/typescript/lib")); | |
if (isBuiltInType) { | |
requiredImportsFromRuntype["InstanceOf"] = true; | |
return 'InstanceOf(' + type.getText() + ')'; | |
} else { | |
requiredImportsFromRuntype["Record"] = true; | |
return `Record({\n` + (type.getProperties().map(property => { | |
if (typeof overrideTypes[property.getName()] === 'string') { | |
return `${" ".repeat(indent + 1)}${property.getName()}: ${overrideTypes[property.getName()]}`; | |
} | |
return `${" ".repeat(indent + 1)}${property.getName()}: ${renderType(property.getValueDeclarationOrThrow().getType(), indent + 1, overrideTypes[property.getName()] || {}, requiredImportsFromRuntype)}`; | |
})).join(", \n") + `\n${' '.repeat(indent)}})`; | |
} | |
} | |
throw new Error("!!! TYPE " + type.getText() + " NOT PARSED !!!"); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This is created as an alternative to runtypes/runtypes#128 - as I needed this in a project :)