Skip to content

Commit

Permalink
much simpler approach! post-process
Browse files Browse the repository at this point in the history
  • Loading branch information
dimitropoulos committed Aug 29, 2024
1 parent 7d4fef1 commit dd0b835
Showing 1 changed file with 28 additions and 75 deletions.
103 changes: 28 additions & 75 deletions packages/turbo-types/scripts/generate-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = <T>(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");
Expand Down

0 comments on commit dd0b835

Please sign in to comment.