From dd0b8353891db46920c5eb3c3ac6f8a3dc7cc2c3 Mon Sep 17 00:00:00 2001 From: Dimitri Mitropoulos Date: Thu, 29 Aug 2024 11:57:04 -0400 Subject: [PATCH] much simpler approach! post-process --- .../turbo-types/scripts/generate-schema.ts | 103 +++++------------- 1 file changed, 28 insertions(+), 75 deletions(-) diff --git a/packages/turbo-types/scripts/generate-schema.ts b/packages/turbo-types/scripts/generate-schema.ts index 1e6c253645211..4d679ec5b43d3 100755 --- a/packages/turbo-types/scripts/generate-schema.ts +++ b/packages/turbo-types/scripts/generate-schema.ts @@ -2,96 +2,49 @@ import { writeFileSync } from "node:fs"; import { join } from "node:path"; -import { - DEFAULT_CONFIG, - SchemaGenerator, - createFormatter, - createParser, - createProgram, - ts, // use the reexported TypeScript to avoid version conflicts - type CompletedConfig, -} from "ts-json-schema-generator"; +import { createGenerator } from "ts-json-schema-generator"; const __dirname = new URL(".", import.meta.url).pathname; const packageRoot = join(__dirname, "..", "src"); /** - * Unfortunately, we find ourselves in a world where TSDoc and TypeDoc use `@defaultValue`, expecting backticks around the value, while JSON Schema uses `default` without backticks. - * - * This function replaces `@defaultValue` with `@default` and removes backticks from the value. - * - * Needless to say, this is something that's pretty hacky to do, but ts-json-schema-generator doesn't provide a way to customize this behavior, so modifying the file with the TypeScript API (i.e. before it gets to the generator) is our only option. + * post-process the schema recursively to: + * 1. replace any key named `defaultValue` with `default` + * 1. remove any backticks from the value + * 1. attempt to parsing the value as JSON (falling back, if not) */ -const replaceJSDoc = ( - node: ts.Node, - context: ts.TransformationContext -): ts.Node => { - if ("jsDoc" in node && Array.isArray(node.jsDoc)) { - node.jsDoc.forEach((jsDoc: ts.Node) => { - if (ts.isJSDoc(jsDoc)) { - if (jsDoc.tags !== undefined && jsDoc.tags.length > 0) { - jsDoc.tags.forEach((tag) => { - if (tag.tagName.text === "defaultValue") { - // @ts-expect-error TypeScript doesn't want us to be able to assign to a readonly value here - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call -- we're messing with TypeScript's internals here - tag.tagName.escapedText = tag.tagName.escapedText.replace( - "defaultValue", - "default" - ); - if (typeof tag.comment === "string") { - // @ts-expect-error TypeScript doesn't want us to be able to assign to a readonly value here - tag.comment = tag.comment.replaceAll("`", ""); - } - } - }); +const postProcess = (item: T): T => { + if (typeof item !== "object" || item === null) { + return item; + } + if (Array.isArray(item)) { + return item.map(postProcess) as unknown as T; + } + return Object.fromEntries( + Object.entries(item).map(([key, value]) => { + if (key === "defaultValue" && typeof value === "string") { + const replaced = value.replaceAll(/`/g, ""); + try { + return ["default", JSON.parse(replaced)]; + } catch (e) { + return ["default", replaced]; } } - }); - } - - return ts.visitEachChild( - node, - (child) => replaceJSDoc(child, context), - context - ); -}; - -const updateJSDoc = (program: ts.Program) => { - const sourceFiles = program.getSourceFiles(); - sourceFiles - .filter((sourceFile) => sourceFile.fileName.includes(packageRoot)) - .forEach((sourceFile) => { - ts.transform(sourceFile, [ - (context) => (rootNode) => { - rootNode.forEachChild((node) => { - replaceJSDoc(node, context); - }); - return rootNode; - }, - ]); - }); + return [key, postProcess(value)]; + }) + ) as T; }; const create = (fileName: string, typeName: string) => { - const config: CompletedConfig = { - ...DEFAULT_CONFIG, + const generator = createGenerator({ path: join(packageRoot, "index.ts"), tsconfig: join(__dirname, "../tsconfig.json"), type: "Schema", - }; - - const program = createProgram(config); - - updateJSDoc(program); - - const parser = createParser(program, config); - - const formatter = createFormatter(config); - const generator = new SchemaGenerator(program, parser, formatter, config); - const schema = generator.createSchema(typeName); + extraTags: ["defaultValue"], + }); + const schema = postProcess(generator.createSchema(typeName)); const filePath = join(__dirname, "..", "schemas", fileName); - const fileContents = JSON.stringify(schema, null, 2); - writeFileSync(filePath, fileContents); + writeFileSync(filePath, JSON.stringify(schema, null, 2)); }; create("schema.v1.json", "SchemaV1");