diff --git a/arch.png b/arch.png deleted file mode 100644 index dc02b1d..0000000 Binary files a/arch.png and /dev/null differ diff --git a/codegen.png b/codegen.png new file mode 100644 index 0000000..f04cb11 Binary files /dev/null and b/codegen.png differ diff --git a/example/index.ts b/example/index.ts index ad306e5..0da2e7c 100644 --- a/example/index.ts +++ b/example/index.ts @@ -1,9 +1,8 @@ -import { Type, Static } from '@sinclair/typebox' import * as Codegen from '@typebox/codegen' import * as util from 'node:util' function Print(transform: string, code: any) { - const data = typeof code === 'string' ? code : util.inspect(code, false, 100) + const data = typeof code === 'string' ? Codegen.Formatter.Format(code) : util.inspect(code, false, 100) const length = 72 console.log('┌' + '─'.repeat(length) + '┐') console.log('│', transform.padEnd(length - 1) + '│') @@ -12,48 +11,40 @@ function Print(transform: string, code: any) { console.log(data) console.log('') } - const Code = ` -/** - * @description 'A union type' - */ -export type T = string | number - -/** - * @description 'A vector type' - */ -export interface Vector { - /** - * @default 1 - */ - x: number - /** - * @default 2 - */ - y: number - /** - * @default 3 - */ - z: number +export type A = { + x: number, + y: string, + z: boolean } + +export type B = { + a: number, + b: string, + c: boolean +} +export type T = A & B + +type M = {[K in keyof T]: 1 } ` // ---------------------------------------------------------------------------- // Typescript Base // ---------------------------------------------------------------------------- -Print('Typescript code (base)', Code) - +Print('Typescript', Code) // ---------------------------------------------------------------------------- -// Immediate Transform +// TypeBox Transform // ---------------------------------------------------------------------------- Print('TypeScript To TypeBox', Codegen.TypeScriptToTypeBox.Generate(Code)) - // ---------------------------------------------------------------------------- // Model Transform // ---------------------------------------------------------------------------- const model = Codegen.TypeScriptToModel.Generate(Code) -Print('TypeScript To Model', model) -Print('Model To JsonSchema', Codegen.ModelToJsonSchema.Generate(model)) +Print('TypeScript To Inline Model', model) +Print('Model To JsonSchema Inline', Codegen.ModelToJsonSchema.Generate(model)) Print('Model To JavaScript', Codegen.ModelToJavaScript.Generate(model)) Print('Model To TypeScript', Codegen.ModelToTypeScript.Generate(model)) +Print('Model To Valibot', Codegen.ModelToValibot.Generate(model)) Print('Model To Value', Codegen.ModelToValue.Generate(model)) +Print('Model To Yup', Codegen.ModelToYup.Generate(model)) Print('Model To Zod', Codegen.ModelToZod.Generate(model)) +Print('Model To ArkType', Codegen.ModelToArkType.Generate(model)) diff --git a/hammer.mjs b/hammer.mjs index 560aea4..4aa0274 100644 --- a/hammer.mjs +++ b/hammer.mjs @@ -19,12 +19,6 @@ export async function test(testReporter = 'spec', filter = '') { await shell(`node --test-reporter ${testReporter} --test ${pattern} target/test/index.js`) } // ------------------------------------------------------------------------------- -// Serve -// ------------------------------------------------------------------------------- -export async function serve() { - await shell('hammer serve example/index.html --dist target/example') -} -// ------------------------------------------------------------------------------- // Start // ------------------------------------------------------------------------------- export async function start() { @@ -33,6 +27,10 @@ export async function start() { // ------------------------------------------------------------------------------- // Build // ------------------------------------------------------------------------------- -export async function build() { - await shell('tsc -p src/tsconfig.json --outDir target/build --declaration') +export async function build(target = 'target/build') { + await shell(`tsc -p src/tsconfig.json --outDir ${target} --declaration`) + await folder(target).add('package.json') + await folder(target).add('license') + await folder(target).add('readme.md') + await shell(`cd ${target} && npm pack`) } diff --git a/license b/license new file mode 100644 index 0000000..1bea787 --- /dev/null +++ b/license @@ -0,0 +1,21 @@ +@sinclair/typebox-codegen + +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 4765b65..841832e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,17 @@ { "name": "typebox-codegen", - "version": "0.0.1", - "lockfileVersion": 2, + "version": "0.8.0", + "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "typebox-codegen", - "version": "0.0.1", + "version": "0.8.0", "license": "MIT", "dependencies": { - "@sinclair/typebox": "^0.28.10", + "@sinclair/typebox": "^0.30.0-dev-7", "prettier": "^2.8.7", - "typescript": "^5.0.4" + "typescript": "^5.1.6" }, "devDependencies": { "@sinclair/hammer": "^0.17.2", @@ -64,20 +64,20 @@ } }, "node_modules/@sinclair/typebox": { - "version": "0.28.10", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.28.10.tgz", - "integrity": "sha512-ZRpJZFpr2yq1vAenq2qspUKs34CBC97LOMghUuTTEveFONVExQAYEB8Tcjy9NlPj8oVlSysK15Hzkf7Ox6x3lA==" + "version": "0.30.0-dev-7", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.30.0-dev-7.tgz", + "integrity": "sha512-TZcJVBlSs4tFA0XBG/rkyEwJ1qcD0n21k1FPSwufr7SVTijAKIw9Zqknhscnzjf32mqxrfzkTUGWLYGSJo4ynw==" }, "node_modules/@types/node": { - "version": "18.15.11", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz", - "integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==", + "version": "18.17.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.17.1.tgz", + "integrity": "sha512-xlR1jahfizdplZYRU59JlUx9uzF1ARa8jbhM11ccpCJya8kvos5jwdm2ZAgxSCwOl0fq21svP18EVwPBXMQudw==", "dev": true }, "node_modules/@types/prettier": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.2.tgz", - "integrity": "sha512-KufADq8uQqo1pYKVIYzfKbJfBAc0sOeXqGbFaSpv8MRmC/zXgowNZmFcbngndGk922QDmOASEXUZCaY48gs4cg==", + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.3.tgz", + "integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==", "dev": true }, "node_modules/esbuild": { @@ -405,12 +405,12 @@ "node": ">=12" } }, - "node_modules/esbuild-windows-64": { + "node_modules/esbuild-windows-arm64": { "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.18.tgz", - "integrity": "sha512-qinug1iTTaIIrCorAUjR0fcBk24fjzEedFYhhispP8Oc7SFvs+XeW3YpAKiKp8dRpizl4YYAhxMjlftAMJiaUw==", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.18.tgz", + "integrity": "sha512-q9bsYzegpZcLziq0zgUi5KqGVtfhjxGbnksaBFYmWLxeV/S1fK4OLdq2DFYnXcLMjlZw2L0jLsk1eGoB522WXQ==", "cpu": [ - "x64" + "arm64" ], "dev": true, "optional": true, @@ -421,12 +421,12 @@ "node": ">=12" } }, - "node_modules/esbuild-windows-arm64": { + "node_modules/esbuild/node_modules/esbuild-windows-64": { "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.18.tgz", - "integrity": "sha512-q9bsYzegpZcLziq0zgUi5KqGVtfhjxGbnksaBFYmWLxeV/S1fK4OLdq2DFYnXcLMjlZw2L0jLsk1eGoB522WXQ==", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.18.tgz", + "integrity": "sha512-qinug1iTTaIIrCorAUjR0fcBk24fjzEedFYhhispP8Oc7SFvs+XeW3YpAKiKp8dRpizl4YYAhxMjlftAMJiaUw==", "cpu": [ - "arm64" + "x64" ], "dev": true, "optional": true, @@ -438,9 +438,9 @@ } }, "node_modules/prettier": { - "version": "2.8.7", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.7.tgz", - "integrity": "sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw==", + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", "bin": { "prettier": "bin-prettier.js" }, @@ -452,238 +452,16 @@ } }, "node_modules/typescript": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", - "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", + "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=12.20" - } - } - }, - "dependencies": { - "@esbuild/android-arm": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.18.tgz", - "integrity": "sha512-5GT+kcs2WVGjVs7+boataCkO5Fg0y4kCjzkB5bAip7H4jfnOS3dA6KPiww9W1OEKTKeAcUVhdZGvgI65OXmUnw==", - "dev": true, - "optional": true - }, - "@esbuild/linux-loong64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.18.tgz", - "integrity": "sha512-L4jVKS82XVhw2nvzLg/19ClLWg0y27ulRwuP7lcyL6AbUWB5aPglXY3M21mauDQMDfRLs8cQmeT03r/+X3cZYQ==", - "dev": true, - "optional": true - }, - "@sinclair/hammer": { - "version": "0.17.2", - "resolved": "https://registry.npmjs.org/@sinclair/hammer/-/hammer-0.17.2.tgz", - "integrity": "sha512-Nnzq4JuC3VB0JNEfxU4U9W65yCe4+/ft0+FGf6/HBn7BoxW3aigusstFdrzVdhjJ6NVABCwUBMgtYbD9X7Z94g==", - "dev": true, - "requires": { - "esbuild": "^0.15.7" + "node": ">=14.17" } - }, - "@sinclair/typebox": { - "version": "0.28.10", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.28.10.tgz", - "integrity": "sha512-ZRpJZFpr2yq1vAenq2qspUKs34CBC97LOMghUuTTEveFONVExQAYEB8Tcjy9NlPj8oVlSysK15Hzkf7Ox6x3lA==" - }, - "@types/node": { - "version": "18.15.11", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz", - "integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==", - "dev": true - }, - "@types/prettier": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.2.tgz", - "integrity": "sha512-KufADq8uQqo1pYKVIYzfKbJfBAc0sOeXqGbFaSpv8MRmC/zXgowNZmFcbngndGk922QDmOASEXUZCaY48gs4cg==", - "dev": true - }, - "esbuild": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.18.tgz", - "integrity": "sha512-x/R72SmW3sSFRm5zrrIjAhCeQSAWoni3CmHEqfQrZIQTM3lVCdehdwuIqaOtfC2slvpdlLa62GYoN8SxT23m6Q==", - "dev": true, - "requires": { - "@esbuild/android-arm": "0.15.18", - "@esbuild/linux-loong64": "0.15.18", - "esbuild-android-64": "0.15.18", - "esbuild-android-arm64": "0.15.18", - "esbuild-darwin-64": "0.15.18", - "esbuild-darwin-arm64": "0.15.18", - "esbuild-freebsd-64": "0.15.18", - "esbuild-freebsd-arm64": "0.15.18", - "esbuild-linux-32": "0.15.18", - "esbuild-linux-64": "0.15.18", - "esbuild-linux-arm": "0.15.18", - "esbuild-linux-arm64": "0.15.18", - "esbuild-linux-mips64le": "0.15.18", - "esbuild-linux-ppc64le": "0.15.18", - "esbuild-linux-riscv64": "0.15.18", - "esbuild-linux-s390x": "0.15.18", - "esbuild-netbsd-64": "0.15.18", - "esbuild-openbsd-64": "0.15.18", - "esbuild-sunos-64": "0.15.18", - "esbuild-windows-32": "0.15.18", - "esbuild-windows-64": "0.15.18", - "esbuild-windows-arm64": "0.15.18" - } - }, - "esbuild-android-64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.18.tgz", - "integrity": "sha512-wnpt3OXRhcjfIDSZu9bnzT4/TNTDsOUvip0foZOUBG7QbSt//w3QV4FInVJxNhKc/ErhUxc5z4QjHtMi7/TbgA==", - "dev": true, - "optional": true - }, - "esbuild-android-arm64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.18.tgz", - "integrity": "sha512-G4xu89B8FCzav9XU8EjsXacCKSG2FT7wW9J6hOc18soEHJdtWu03L3TQDGf0geNxfLTtxENKBzMSq9LlbjS8OQ==", - "dev": true, - "optional": true - }, - "esbuild-darwin-64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.18.tgz", - "integrity": "sha512-2WAvs95uPnVJPuYKP0Eqx+Dl/jaYseZEUUT1sjg97TJa4oBtbAKnPnl3b5M9l51/nbx7+QAEtuummJZW0sBEmg==", - "dev": true, - "optional": true - }, - "esbuild-darwin-arm64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.18.tgz", - "integrity": "sha512-tKPSxcTJ5OmNb1btVikATJ8NftlyNlc8BVNtyT/UAr62JFOhwHlnoPrhYWz09akBLHI9nElFVfWSTSRsrZiDUA==", - "dev": true, - "optional": true - }, - "esbuild-freebsd-64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.18.tgz", - "integrity": "sha512-TT3uBUxkteAjR1QbsmvSsjpKjOX6UkCstr8nMr+q7zi3NuZ1oIpa8U41Y8I8dJH2fJgdC3Dj3CXO5biLQpfdZA==", - "dev": true, - "optional": true - }, - "esbuild-freebsd-arm64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.18.tgz", - "integrity": "sha512-R/oVr+X3Tkh+S0+tL41wRMbdWtpWB8hEAMsOXDumSSa6qJR89U0S/PpLXrGF7Wk/JykfpWNokERUpCeHDl47wA==", - "dev": true, - "optional": true - }, - "esbuild-linux-32": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.18.tgz", - "integrity": "sha512-lphF3HiCSYtaa9p1DtXndiQEeQDKPl9eN/XNoBf2amEghugNuqXNZA/ZovthNE2aa4EN43WroO0B85xVSjYkbg==", - "dev": true, - "optional": true - }, - "esbuild-linux-64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.18.tgz", - "integrity": "sha512-hNSeP97IviD7oxLKFuii5sDPJ+QHeiFTFLoLm7NZQligur8poNOWGIgpQ7Qf8Balb69hptMZzyOBIPtY09GZYw==", - "dev": true, - "optional": true - }, - "esbuild-linux-arm": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.18.tgz", - "integrity": "sha512-UH779gstRblS4aoS2qpMl3wjg7U0j+ygu3GjIeTonCcN79ZvpPee12Qun3vcdxX+37O5LFxz39XeW2I9bybMVA==", - "dev": true, - "optional": true - }, - "esbuild-linux-arm64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.18.tgz", - "integrity": "sha512-54qr8kg/6ilcxd+0V3h9rjT4qmjc0CccMVWrjOEM/pEcUzt8X62HfBSeZfT2ECpM7104mk4yfQXkosY8Quptug==", - "dev": true, - "optional": true - }, - "esbuild-linux-mips64le": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.18.tgz", - "integrity": "sha512-Mk6Ppwzzz3YbMl/ZZL2P0q1tnYqh/trYZ1VfNP47C31yT0K8t9s7Z077QrDA/guU60tGNp2GOwCQnp+DYv7bxQ==", - "dev": true, - "optional": true - }, - "esbuild-linux-ppc64le": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.18.tgz", - "integrity": "sha512-b0XkN4pL9WUulPTa/VKHx2wLCgvIAbgwABGnKMY19WhKZPT+8BxhZdqz6EgkqCLld7X5qiCY2F/bfpUUlnFZ9w==", - "dev": true, - "optional": true - }, - "esbuild-linux-riscv64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.18.tgz", - "integrity": "sha512-ba2COaoF5wL6VLZWn04k+ACZjZ6NYniMSQStodFKH/Pu6RxzQqzsmjR1t9QC89VYJxBeyVPTaHuBMCejl3O/xg==", - "dev": true, - "optional": true - }, - "esbuild-linux-s390x": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.18.tgz", - "integrity": "sha512-VbpGuXEl5FCs1wDVp93O8UIzl3ZrglgnSQ+Hu79g7hZu6te6/YHgVJxCM2SqfIila0J3k0csfnf8VD2W7u2kzQ==", - "dev": true, - "optional": true - }, - "esbuild-netbsd-64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.18.tgz", - "integrity": "sha512-98ukeCdvdX7wr1vUYQzKo4kQ0N2p27H7I11maINv73fVEXt2kyh4K4m9f35U1K43Xc2QGXlzAw0K9yoU7JUjOg==", - "dev": true, - "optional": true - }, - "esbuild-openbsd-64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.18.tgz", - "integrity": "sha512-yK5NCcH31Uae076AyQAXeJzt/vxIo9+omZRKj1pauhk3ITuADzuOx5N2fdHrAKPxN+zH3w96uFKlY7yIn490xQ==", - "dev": true, - "optional": true - }, - "esbuild-sunos-64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.18.tgz", - "integrity": "sha512-On22LLFlBeLNj/YF3FT+cXcyKPEI263nflYlAhz5crxtp3yRG1Ugfr7ITyxmCmjm4vbN/dGrb/B7w7U8yJR9yw==", - "dev": true, - "optional": true - }, - "esbuild-windows-32": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.18.tgz", - "integrity": "sha512-o+eyLu2MjVny/nt+E0uPnBxYuJHBvho8vWsC2lV61A7wwTWC3jkN2w36jtA+yv1UgYkHRihPuQsL23hsCYGcOQ==", - "dev": true, - "optional": true - }, - "esbuild-windows-64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.18.tgz", - "integrity": "sha512-qinug1iTTaIIrCorAUjR0fcBk24fjzEedFYhhispP8Oc7SFvs+XeW3YpAKiKp8dRpizl4YYAhxMjlftAMJiaUw==", - "dev": true, - "optional": true - }, - "esbuild-windows-arm64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.18.tgz", - "integrity": "sha512-q9bsYzegpZcLziq0zgUi5KqGVtfhjxGbnksaBFYmWLxeV/S1fK4OLdq2DFYnXcLMjlZw2L0jLsk1eGoB522WXQ==", - "dev": true, - "optional": true - }, - "prettier": { - "version": "2.8.7", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.7.tgz", - "integrity": "sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw==" - }, - "typescript": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", - "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==" } } } diff --git a/package.json b/package.json index c8babf3..37fcc4e 100644 --- a/package.json +++ b/package.json @@ -1,20 +1,19 @@ { - "name": "typebox-codegen", - "version": "0.0.1", - "description": "Code Generation for TypeBox Types", - "main": "index.js", - "keywords": [ - "typebox", - "codegen" - ], "scripts": { "build": "hammer task build", "clean": "hammer task clean", "format": "hammer task format", "start": "hammer task start", - "serve": "hammer task serve", "test": "hammer task test" }, + "name": "typebox-codegen", + "version": "0.8.0", + "description": "Code Generation Tools for TypeBox", + "main": "index.js", + "keywords": [ + "typebox", + "codegen" + ], "repository": { "type": "git", "url": "git+https://github.com/sinclairzx81/typebox-codegen.git" @@ -24,13 +23,14 @@ "homepage": "https://github.com/sinclairzx81/typebox-codegen#readme", "devDependencies": { "@sinclair/hammer": "^0.17.2", + "@types/node": "^18.15.11", "@types/prettier": "^2.7.2" }, "dependencies": { - "@sinclair/typebox": "^0.28.10", + "@sinclair/typebox": "^0.30.0-dev-7", "prettier": "^2.8.7", - "typescript": "^5.0.4" + "typescript": "^5.1.6" }, "prettier": { "printWidth": 240, diff --git a/readme.md b/readme.md index 417bf50..446eaca 100644 --- a/readme.md +++ b/readme.md @@ -1,14 +1,76 @@ -# typebox-codegen +
+ +

TypeBox Codegen

+ +

Code Generation Tools for TypeBox

+ + + +
+ +
## Overview -This package provides code generation for TypeBox types +TypeBox-Codegen is a code generation tool that converts TypeScript types into TypeBox types as well as several other schema and library representations. It works by extracting structural type information from the TypeScript compiler and maps into a TypeBox specific model. This model can then be passed on to various code generators to generate a multitude of various type representations by introspecting TypeBox's schematics. + +The library contains various code transformations for libraries such as [zod](https://github.com/colinhacks/zod), [io-ts](https://github.com/gcanti/io-ts), [arktype](https://github.com/arktypeio/arktype) and [valibot](https://github.com/fabian-hiller/valibot), assert function generators for JavaScript and TypeScript (derived from the TypeCompiler) as well as JSON Schema generation derived from TypeBox's raw schematics. + +License MIT + +## Usage -## Architecture +The following is the general usage -The following is the initial architecture for code transformations to be provided by this package +```typescript +import * as Codegen from '@sinclair/typebox-codegen' - +const Code = ` +export type T = { + x: number, + y: number, + z: number +} +` +// ---------------------------------------------------------------------------- +// +// TypeScriptToTypeBox +// +// Generates an immediate TypeScript to TypeBox type code transformation +// +// ---------------------------------------------------------------------------- + +console.log('TypeScript To TypeBox', Codegen.TypeScriptToTypeBox.Generate(Code)) + +// ---------------------------------------------------------------------------- +// +// TypeScriptToModel +// +// Generates an in-memory TypeBox Type Model +// +// ---------------------------------------------------------------------------- + +const model = Codegen.TypeScriptToModel.Generate(Code) + +// ---------------------------------------------------------------------------- +// +// ModelToX +// +// The TypeBox Type Model can be passed to several generators which map the +// Model into varying type representations. +// +// ---------------------------------------------------------------------------- + +console.log('TypeBoxModel', model) +console.log('Model To JsonSchema', Codegen.ModelToJsonSchema.Generate(model)) +console.log('Model To JavaScript', Codegen.ModelToJavaScript.Generate(model)) +console.log('Model To TypeScript', Codegen.ModelToTypeScript.Generate(model)) +console.log('Model To Valibot', Codegen.ModelToValibot.Generate(model)) +console.log('Model To Value', Codegen.ModelToValue.Generate(model)) +console.log('Model To Yup', Codegen.ModelToYup.Generate(model)) +console.log('Model To Zod', Codegen.ModelToZod.Generate(model)) +console.log('Model To ArkType', Codegen.ModelToArkType.Generate(model)) +``` ## Running Local @@ -22,8 +84,6 @@ $ npm format # prettier pass for `src` and `example` $ npm clean # remove the `target` directory. $ npm start # run the `example` script in node - -$ npm serve # run the `example` script in browser ``` ## Formatting hook diff --git a/src/common/encoder.ts b/src/common/encoder.ts index a549e3c..55000c9 100644 --- a/src/common/encoder.ts +++ b/src/common/encoder.ts @@ -1,6 +1,6 @@ /*-------------------------------------------------------------------------- -@typebox/codegen +@sinclair/typebox-codegen The MIT License (MIT) diff --git a/src/common/formatter.ts b/src/common/formatter.ts index e52007a..daad6f6 100644 --- a/src/common/formatter.ts +++ b/src/common/formatter.ts @@ -1,6 +1,6 @@ /*-------------------------------------------------------------------------- -@typebox/codegen +@sinclair/typebox-codegen The MIT License (MIT) diff --git a/src/common/index.ts b/src/common/index.ts index 1e9d171..48da298 100644 --- a/src/common/index.ts +++ b/src/common/index.ts @@ -1,6 +1,6 @@ /*-------------------------------------------------------------------------- -@typebox/codegen +@sinclair/typebox-codegen The MIT License (MIT) diff --git a/src/common/jsdoc.ts b/src/common/jsdoc.ts index 68611ce..19e983f 100644 --- a/src/common/jsdoc.ts +++ b/src/common/jsdoc.ts @@ -1,6 +1,6 @@ /*-------------------------------------------------------------------------- -@typebox/codegen +@sinclair/typebox-codegen The MIT License (MIT) diff --git a/src/index.ts b/src/index.ts index 97885d4..fbd0418 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ /*-------------------------------------------------------------------------- -@typebox/codegen +@sinclair/typebox-codegen The MIT License (MIT) diff --git a/src/model/index.ts b/src/model/index.ts index d2302d0..89682ed 100644 --- a/src/model/index.ts +++ b/src/model/index.ts @@ -1,6 +1,6 @@ /*-------------------------------------------------------------------------- -@typebox/codegen +@sinclair/typebox-codegen The MIT License (MIT) @@ -24,10 +24,14 @@ THE SOFTWARE. ---------------------------------------------------------------------------*/ +export * from './model-to-arktype' +export * from './model-to-io-ts' export * from './model-to-javascript' export * from './model-to-json-schema' export * from './model-to-typebox' export * from './model-to-typescript' +export * from './model-to-valibot' export * from './model-to-value' +export * from './model-to-yup' export * from './model-to-zod' export * from './model' diff --git a/src/model/model-to-arktype.ts b/src/model/model-to-arktype.ts new file mode 100644 index 0000000..3220d13 --- /dev/null +++ b/src/model/model-to-arktype.ts @@ -0,0 +1,266 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox-codegen + +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { Formatter, PropertyEncoder } from '../common/index' +import { TypeBoxModel } from './model' +import * as Types from '@sinclair/typebox' + +// -------------------------------------------------------------------------- +// ModelToArkType +// -------------------------------------------------------------------------- +export namespace ModelToArkType { + // ------------------------------------------------------------------------ + // Constraints + // ------------------------------------------------------------------------ + function Wrap(value: string) { + return `'${value}'` + } + function Unwrap(value: string) { + if (value.indexOf("'") !== 0) return value + return value.slice(1, value.length - 1) + } + // ------------------------------------------------------------------------ + // Composite + // ------------------------------------------------------------------------ + function RequiresType(types: string[]) { + return types.some((type) => type.indexOf("'") !== 0) + } + function Composite(types: string[], operator: string) { + if (RequiresType(types)) { + const mapped = types.join(`, '${operator}', `) + return `[${mapped}]` + } else { + const mapped = types.map((type) => Unwrap(type)).join(` ${operator} `) + return Wrap(mapped) + } + } + // ------------------------------------------------------------------------ + // Constraints + // ------------------------------------------------------------------------ + function ConstrainedSequenceType(type: string, options: { minimum?: number; maximum?: number }) { + if (IsDefined(options.minimum) && IsDefined(options.maximum)) { + return Wrap(`${options.minimum}<=${type}<=${options.maximum}`) + } else if (IsDefined(options.minimum)) { + return Wrap(`${type}>=${options.minimum}`) + } else if (IsDefined(options.maximum)) { + return Wrap(`${type}<=${options.maximum}`) + } else { + return Wrap(`${type}`) + } + } + function ConstrainedNumericType(type: string, options: Types.NumericOptions) { + // prettier-ignore + const minimum = IsDefined(options.exclusiveMinimum) ? (options.exclusiveMinimum + 1) : + IsDefined(options.minimum) ? (options.minimum) : + undefined + // prettier-ignore + const maximum = IsDefined(options.exclusiveMaximum) ? (options.exclusiveMaximum - 1) : + IsDefined(options.maximum) ? (options.maximum) : + undefined + if (IsDefined(minimum) && IsDefined(maximum)) { + return Wrap(`${minimum}<=${type}<=${maximum}`) + } else if (IsDefined(minimum)) { + return Wrap(`${type}>=${minimum}`) + } else if (IsDefined(maximum)) { + return Wrap(`${type}<=${maximum}`) + } else { + return Wrap(`${type}`) + } + } + + function IsDefined(value: unknown): value is T { + return value !== undefined + } + function Any(schema: Types.TAny) { + return Wrap('any') + } + function Array(schema: Types.TArray) { + const type = Visit(schema.items) + const mapped = `${Unwrap(type)}[]` + return ConstrainedSequenceType(mapped, { + minimum: schema.minItems, + maximum: schema.maxItems, + }) + } + function BigInt(schema: Types.TBigInt) { + return Wrap('bigint') + } + function Boolean(schema: Types.TBoolean) { + return Wrap('boolean') + } + function Constructor(schema: Types.TConstructor): string { + return Wrap('Function') + } + function Date(schema: Types.TDate): string { + return Wrap('Date') + } + function Function(schema: Types.TFunction) { + return Wrap('Function') + } + function Integer(schema: Types.TInteger) { + return ConstrainedNumericType('integer', schema) + } + function Intersect(schema: Types.TIntersect) { + const types = schema.allOf.map((schema) => Collect(schema)) + return Composite(types, '&') + } + function Literal(schema: Types.TLiteral) { + return typeof schema.const === 'string' ? Wrap(`"${schema.const}"`) : Wrap(schema.const.toString()) + } + function Never(schema: Types.TNever) { + return Wrap('never') + } + function Null(schema: Types.TNull) { + return Wrap('null') + } + function String(schema: Types.TString) { + return ConstrainedSequenceType('string', { + minimum: schema.minLength, + maximum: schema.maxLength, + }) + } + function Number(schema: Types.TNumber) { + return ConstrainedNumericType('number', schema) + } + function Object(schema: Types.TObject) { + console.log(1, schema) + const properties = globalThis.Object.entries(schema.properties) + .map(([key, schema]) => { + console.log(1, key) + const optional = Types.TypeGuard.TOptional(schema) + const property1 = PropertyEncoder.Encode(key) + const property2 = optional ? `'${property1}?'` : `${property1}` + return `${property2}: ${Visit(schema)}` + }) + .join(`,`) + const buffer: string[] = [] + buffer.push(`{\n${properties}\n}`) + return buffer.join(`\n`) + } + function Promise(schema: Types.TPromise) { + return Wrap('Promise') + } + function Record(schema: Types.TRecord) { + return Wrap('never') // not sure how to express + } + function Ref(schema: Types.TRef) { + return Wrap(schema.$ref) + } + function This(schema: Types.TThis) { + return Wrap(schema.$ref) + } + function Tuple(schema: Types.TTuple) { + if (schema.items === undefined) return `[]` + const items = schema.items.map((schema) => Visit(schema)).join(`, `) + return `[${items}]` + } + function TemplateLiteral(schema: Types.TTemplateLiteral) { + return `/${schema.pattern}/` + } + function UInt8Array(schema: Types.TUint8Array): string { + return `['instanceof', Uint8Array]` + } + function Undefined(schema: Types.TUndefined) { + return Wrap('undefined') + } + function Union(schema: Types.TUnion) { + const types = schema.anyOf.map((schema) => Collect(schema)) + return Composite(types, '|') + } + function Unknown(schema: Types.TUnknown) { + return Wrap('unknown') + } + function Void(schema: Types.TVoid) { + return Wrap('void') + } + function UnsupportedType(schema: Types.TSchema) { + return Wrap('never') + } + function Visit(schema: Types.TSchema): string { + if (schema.$id !== undefined) reference_map.set(schema.$id, schema) + if (schema.$id !== undefined && emitted_types.has(schema.$id!)) return `'${schema.$id!}'` + if (Types.TypeGuard.TAny(schema)) return Any(schema) + if (Types.TypeGuard.TArray(schema)) return Array(schema) + if (Types.TypeGuard.TBigInt(schema)) return BigInt(schema) + if (Types.TypeGuard.TBoolean(schema)) return Boolean(schema) + if (Types.TypeGuard.TConstructor(schema)) return Constructor(schema) + if (Types.TypeGuard.TDate(schema)) return Date(schema) + if (Types.TypeGuard.TFunction(schema)) return Function(schema) + if (Types.TypeGuard.TInteger(schema)) return Integer(schema) + if (Types.TypeGuard.TIntersect(schema)) return Intersect(schema) + if (Types.TypeGuard.TLiteral(schema)) return Literal(schema) + if (Types.TypeGuard.TNever(schema)) return Never(schema) + if (Types.TypeGuard.TNull(schema)) return Null(schema) + if (Types.TypeGuard.TNumber(schema)) return Number(schema) + if (Types.TypeGuard.TObject(schema)) return Object(schema) + if (Types.TypeGuard.TPromise(schema)) return Promise(schema) + if (Types.TypeGuard.TRecord(schema)) return Record(schema) + if (Types.TypeGuard.TRef(schema)) return Ref(schema) + if (Types.TypeGuard.TString(schema)) return String(schema) + if (Types.TypeGuard.TTemplateLiteral(schema)) return TemplateLiteral(schema) + if (Types.TypeGuard.TThis(schema)) return This(schema) + if (Types.TypeGuard.TTuple(schema)) return Tuple(schema) + if (Types.TypeGuard.TUint8Array(schema)) return UInt8Array(schema) + if (Types.TypeGuard.TUndefined(schema)) return Undefined(schema) + if (Types.TypeGuard.TUnion(schema)) return Union(schema) + if (Types.TypeGuard.TUnknown(schema)) return Unknown(schema) + if (Types.TypeGuard.TVoid(schema)) return Void(schema) + return UnsupportedType(schema) + } + function Collect(schema: Types.TSchema) { + return [...Visit(schema)].join(``) + } + function GenerateType(schema: Types.TSchema, references: Types.TSchema[]) { + const buffer: string[] = [] + for (const reference of references) { + if (reference.$id === undefined) return UnsupportedType(schema) + reference_map.set(reference.$id, reference) + } + const type = Collect(schema) + buffer.push(`${schema.$id || `T`}: ${type}`) + if (schema.$id) emitted_types.add(schema.$id) + return buffer.join('\n') + } + const reference_map = new Map() + const emitted_types = new Set() + export function Generate(model: TypeBoxModel): string { + reference_map.clear() + emitted_types.clear() + const buffer: string[] = [] + buffer.push('export const types = scope({') + for (const type of model.types) { + buffer.push(`${GenerateType(type, model.types)},`) + } + buffer.push('}).compile()') + buffer.push('\n') + for (const type of model.types) { + buffer.push(`export type ${type.$id} = typeof ${type.$id}.infer`) + buffer.push(`export const ${type.$id} = types.${type.$id}`) + } + buffer.unshift(`import { scope } from 'arktype'`, '') + return Formatter.Format(buffer.join('\n')) + } +} diff --git a/src/model/model-to-io-ts.ts b/src/model/model-to-io-ts.ts new file mode 100644 index 0000000..abb4b63 --- /dev/null +++ b/src/model/model-to-io-ts.ts @@ -0,0 +1,287 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox-codegen + +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { Formatter, PropertyEncoder } from '../common/index' +import { TypeBoxModel } from './model' +import { ModelToTypeScript } from './model-to-typescript' +import * as Types from '@sinclair/typebox' + +// -------------------------------------------------------------------------- +// ModelToIoTs +// -------------------------------------------------------------------------- +export namespace ModelToIoTs { + function IsDefined(value: unknown): value is T { + return value !== undefined + } + function Type(schema: Types.TSchema, type: string) { + return type + } + function Any(schema: Types.TAny) { + return Type(schema, `t.any`) + } + function Array(schema: Types.TArray) { + const items = Visit(schema.items) + const type = `t.array(${items})` + const refinements: string[] = [] + if (IsDefined(schema.minItems)) refinements.push(`value.length >= ${schema.minItems}`) + if (IsDefined(schema.maxItems)) refinements.push(`value.length <= ${schema.maxItems}`) + return refinements.length === 0 ? type : `t.refinement(${type}, value => ${refinements.join('&&')})` + } + function BigInt(schema: Types.TBigInt) { + return `t.bigint` + } + function Boolean(schema: Types.TBoolean) { + return `t.boolean` + } + const support_types = new Map() + function Date(schema: Types.TDate) { + support_types.set( + 'Date', + ` const t_date = new t.Type( + 'Date', + (value: unknown): value is Date => value instanceof Date, + (value, context) => (value instanceof Date ? t.success(value) : t.failure(value, context)), + t.identity + )`, + ) + return `t_date` + } + function Constructor(schema: Types.TConstructor): string { + support_types.set( + 'Function', + `const t_Function = new t.Type( + 'Promise', + (value: unknown): value is Function => typeof value === 'function', + (value, context) => (typeof value === 'function' ? t.success(value) : t.failure(value, context)), + t.identity + )`, + ) + return `t_Function` + } + function Function(schema: Types.TFunction) { + support_types.set( + 'Function', + `const t_Function = new t.Type( + 'Promise', + (value: unknown): value is Function => typeof value === 'function', + (value, context) => (typeof value === 'function' ? t.success(value) : t.failure(value, context)), + t.identity + )`, + ) + return `t_Function` + } + function Integer(schema: Types.TInteger) { + const type = `t.number` + const refinements: string[] = [] + refinements.push(`value => Number.isInteger(value)`) + if (IsDefined(schema.minimum)) refinements.push(`value >= ${schema.minimum}`) + if (IsDefined(schema.maximum)) refinements.push(`value <= ${schema.maximum}`) + if (IsDefined(schema.exclusiveMaximum)) refinements.push(`value > ${schema.exclusiveMinimum}`) + if (IsDefined(schema.exclusiveMinimum)) refinements.push(`value < ${schema.exclusiveMaximum}`) + if (IsDefined(schema.multipleOf)) refinements.push(`value % ${schema.multipleOf} === 0`) + return `t.refinement(${type}, value => ${refinements.join('&&')})` + } + function Intersect(schema: Types.TIntersect) { + const types = schema.allOf.map((type) => Visit(type)) + return `t.intersection([${types.join(', ')}])` + } + function Literal(schema: Types.TLiteral) { + return typeof schema.const === `string` ? Type(schema, `t.literal('${schema.const}')`) : Type(schema, `t.literal(${schema.const})`) + } + function Never(schema: Types.TNever) { + return Type(schema, `t.never`) + } + function Null(schema: Types.TNull) { + return Type(schema, `t.null`) + } + function String(schema: Types.TString) { + const type = `t.string` + const refinements: string[] = [] + if (IsDefined(schema.minLength)) refinements.push(`value.length >= ${schema.minLength}`) + if (IsDefined(schema.maxLength)) refinements.push(`value.length <= ${schema.maxLength}`) + return refinements.length === 0 ? type : `t.refinement(${type}, value => ${refinements.join('&&')})` + } + function Number(schema: Types.TNumber) { + const type = `t.number` + const refinements: string[] = [] + if (IsDefined(schema.minimum)) refinements.push(`value >= ${schema.minimum}`) + if (IsDefined(schema.maximum)) refinements.push(`value <= ${schema.maximum}`) + if (IsDefined(schema.exclusiveMaximum)) refinements.push(`value > ${schema.exclusiveMinimum}`) + if (IsDefined(schema.exclusiveMinimum)) refinements.push(`value < ${schema.exclusiveMaximum}`) + if (IsDefined(schema.multipleOf)) refinements.push(`value % ${schema.multipleOf} === 0`) + return refinements.length === 0 ? type : `t.refinement(${type}, value => ${refinements.join('&&')})` + } + function Object(schema: Types.TObject) { + // prettier-ignore + const properties = globalThis.Object.entries(schema.properties).map(([key, value]) => { + const optional = Types.TypeGuard.TOptional(value) + const readonly = Types.TypeGuard.TReadonly(value) + const property = PropertyEncoder.Encode(key) + const resolved = optional ? `t.union([t.undefined, ${Visit(value)}])` : Visit(value) + return readonly ? `${property} : t.readonly(${resolved})` : `${property} : ${resolved}` + }).join(`,`) + const buffer: string[] = [] + if (schema.additionalProperties === false) { + buffer.push(`t.strict({\n${properties}\n})`) + } else { + buffer.push(`t.type({\n${properties}\n})`) + } + return Type(schema, buffer.join(``)) + } + function Promise(schema: Types.TPromise) { + support_types.set( + 'Promise', + `const t_Promise = new t.Type, Promise, unknown>( + 'Promise', + (value: unknown): value is Promise => value instanceof Promise, + (value, context) => (value instanceof Promise ? t.success(value) : t.failure(value, context)), + t.identity + )`, + ) + return `t_Promise` + } + function Record(schema: Types.TRecord) { + for (const [key, value] of globalThis.Object.entries(schema.patternProperties)) { + const type = Visit(value) + if (key === `^(0|[1-9][0-9]*)$`) { + return `t.record(t.number, ${type})` + } else { + return `t.record(t.string, ${type})` + } + } + throw Error(`Unreachable`) + } + function Ref(schema: Types.TRef) { + if (!reference_map.has(schema.$ref!)) return UnsupportedType(schema) // throw new ModelToZodNonReferentialType(schema.$ref!) + return schema.$ref + } + function This(schema: Types.TThis) { + if (!reference_map.has(schema.$ref!)) return UnsupportedType(schema) //throw new ModelToZodNonReferentialType(schema.$ref!) + recursive_set.add(schema.$ref) + return schema.$ref + } + function Tuple(schema: Types.TTuple) { + if (schema.items === undefined) return `[]` + const items = schema.items.map((schema) => Visit(schema)).join(`, `) + return Type(schema, `t.tuple([${items}])`) + } + function TemplateLiteral(schema: Types.TTemplateLiteral) { + return Type(schema, `t.refinement(t.string, value => /${schema.pattern}/.test(value))`) + } + function UInt8Array(schema: Types.TUint8Array): string { + support_types.set( + 'Uint8Array', + `const t_Uint8Array = new t.Type( + 'Uint8Array', + (value: unknown): value is Uint8Array => value instanceof Uint8Array, + (value, context) => (value instanceof Uint8Array ? t.success(value) : t.failure(value, context)), + t.identity + )`, + ) + return `t_Uint8Array` + } + function Undefined(schema: Types.TUndefined) { + return Type(schema, `t.undefined`) + } + function Union(schema: Types.TUnion) { + return Type(schema, `t.union([${schema.anyOf.map((schema) => Visit(schema)).join(`, `)}])`) + } + function Unknown(schema: Types.TUnknown) { + return Type(schema, `t.unknown`) + } + function Void(schema: Types.TVoid) { + return Type(schema, `t.void`) + } + function UnsupportedType(schema: Types.TSchema) { + return Type(schema, `t.any /* unresolved */`) + } + function Visit(schema: Types.TSchema): string { + if (schema.$id !== undefined) reference_map.set(schema.$id, schema) + if (schema.$id !== undefined && emitted_set.has(schema.$id!)) return schema.$id! + if (Types.TypeGuard.TAny(schema)) return Any(schema) + if (Types.TypeGuard.TArray(schema)) return Array(schema) + if (Types.TypeGuard.TBigInt(schema)) return BigInt(schema) + if (Types.TypeGuard.TBoolean(schema)) return Boolean(schema) + if (Types.TypeGuard.TDate(schema)) return Date(schema) + if (Types.TypeGuard.TConstructor(schema)) return Constructor(schema) + if (Types.TypeGuard.TFunction(schema)) return Function(schema) + if (Types.TypeGuard.TInteger(schema)) return Integer(schema) + if (Types.TypeGuard.TIntersect(schema)) return Intersect(schema) + if (Types.TypeGuard.TLiteral(schema)) return Literal(schema) + if (Types.TypeGuard.TNever(schema)) return Never(schema) + if (Types.TypeGuard.TNull(schema)) return Null(schema) + if (Types.TypeGuard.TNumber(schema)) return Number(schema) + if (Types.TypeGuard.TObject(schema)) return Object(schema) + if (Types.TypeGuard.TPromise(schema)) return Promise(schema) + if (Types.TypeGuard.TRecord(schema)) return Record(schema) + if (Types.TypeGuard.TRef(schema)) return Ref(schema) + if (Types.TypeGuard.TString(schema)) return String(schema) + if (Types.TypeGuard.TTemplateLiteral(schema)) return TemplateLiteral(schema) + if (Types.TypeGuard.TThis(schema)) return This(schema) + if (Types.TypeGuard.TTuple(schema)) return Tuple(schema) + if (Types.TypeGuard.TUint8Array(schema)) return UInt8Array(schema) + if (Types.TypeGuard.TUndefined(schema)) return Undefined(schema) + if (Types.TypeGuard.TUnion(schema)) return Union(schema) + if (Types.TypeGuard.TUnknown(schema)) return Unknown(schema) + if (Types.TypeGuard.TVoid(schema)) return Void(schema) + return UnsupportedType(schema) + } + function Collect(schema: Types.TSchema) { + return [...Visit(schema)].join(``) + } + function GenerateType(model: TypeBoxModel, schema: Types.TSchema, references: Types.TSchema[]) { + const output: string[] = [] + for (const reference of references) { + if (reference.$id === undefined) return UnsupportedType(schema) + reference_map.set(reference.$id, reference) + } + const type = Collect(schema) + if (recursive_set.has(schema.$id!)) { + output.push(`export ${ModelToTypeScript.GenerateType(model, schema.$id!)}`) + output.push(`export const ${schema.$id || `T`}: t.Type<${schema.$id}> = t.recursion('${schema.$id}', () => ${Formatter.Format(type)})`) + } else { + output.push(`export type ${schema.$id} = t.TypeOf`) + output.push(`export const ${schema.$id || `T`} = ${Formatter.Format(type)}`) + } + if (schema.$id) emitted_set.add(schema.$id) + return output.join('\n') + } + const reference_map = new Map() + const recursive_set = new Set() + const emitted_set = new Set() + export function Generate(model: TypeBoxModel): string { + support_types.clear() + reference_map.clear() + recursive_set.clear() + emitted_set.clear() + const buffer: string[] = [`import t from 'io-ts'`, ''] + const types = model.types.map((type) => GenerateType(model, type, model.types)) + buffer.push(...support_types.values()) + buffer.push('\n') + buffer.push(...types) + return Formatter.Format(buffer.join('\n')) + } +} diff --git a/src/model/model-to-javascript.ts b/src/model/model-to-javascript.ts index cd83c7f..7d4b86c 100644 --- a/src/model/model-to-javascript.ts +++ b/src/model/model-to-javascript.ts @@ -1,6 +1,6 @@ /*-------------------------------------------------------------------------- -@typebox/codegen +@sinclair/typebox-codegen The MIT License (MIT) @@ -25,15 +25,18 @@ THE SOFTWARE. ---------------------------------------------------------------------------*/ import { TypeBoxModel } from './model' -import { Formatter } from '../common/formatter' import { TypeCompiler } from '@sinclair/typebox/compiler' export namespace ModelToJavaScript { export function Generate(model: TypeBoxModel): string { const definitions: string[] = [] + const header = `// @ts-nocheck` for (const type of model.types) { - definitions.push(`export const ${type.$id!} = (function() { ${TypeCompiler.Code(type)} })();`) + definitions.push(`export const ${type.$id!} = (() => { + ${TypeCompiler.Code(type, model.types, { language: 'javascript' })} + })()`) } - return Formatter.Format(definitions.join('\n\n')) + const output = [header, ...definitions] + return output.join('\n\n') } } diff --git a/src/model/model-to-json-schema.ts b/src/model/model-to-json-schema.ts index 0e07964..fce0af1 100644 --- a/src/model/model-to-json-schema.ts +++ b/src/model/model-to-json-schema.ts @@ -1,6 +1,6 @@ /*-------------------------------------------------------------------------- -@typebox/codegen +@sinclair/typebox-codegen The MIT License (MIT) @@ -24,15 +24,144 @@ THE SOFTWARE. ---------------------------------------------------------------------------*/ +import { Formatter } from '../common/index' import { TypeBoxModel } from './model' -import { Formatter } from '../common/formatter' +import * as Types from '@sinclair/typebox' +// -------------------------------------------------------------------------- +// ModelToJsonSchema +// -------------------------------------------------------------------------- export namespace ModelToJsonSchema { + function Any(schema: Types.TAny): Types.TSchema { + return schema + } + function Array(schema: Types.TArray): Types.TSchema { + return schema + } + function BigInt(schema: Types.TBigInt): Types.TSchema { + return schema + } + function Boolean(schema: Types.TBoolean): Types.TSchema { + return schema + } + function Date(schema: Types.TDate): Types.TSchema { + return schema + } + function Constructor(schema: Types.TConstructor): Types.TSchema { + return schema + } + function Function(schema: Types.TFunction): Types.TSchema { + const parameters = schema.parameters.map((schema) => Visit(schema)) + const returns = Visit(schema.returns) + return { ...schema, parameters, returns } + } + function Integer(schema: Types.TInteger): Types.TSchema { + return schema + } + function Intersect(schema: Types.TIntersect): Types.TSchema { + const allOf = schema.allOf.map((schema) => Visit(schema)) + return { ...schema, allOf } + } + function Literal(schema: Types.TLiteral): Types.TSchema { + return schema + } + function Never(schema: Types.TNever): Types.TSchema { + return schema + } + function Null(schema: Types.TNull): Types.TSchema { + return schema + } + function String(schema: Types.TString): Types.TSchema { + return schema + } + function Number(schema: Types.TNumber): Types.TSchema { + return schema + } + function Object(schema: Types.TObject): Types.TSchema { + const properties = globalThis.Object.keys(schema.properties).reduce((acc, key) => { + return { ...acc, [key]: Visit(schema.properties[key]) } + }, {}) + return { ...schema, properties } + } + function Promise(schema: Types.TPromise): Types.TSchema { + const item = Visit(schema.item) + return { ...schema, item } + } + function Record(schema: Types.TRecord): Types.TSchema { + const patternProperties = globalThis.Object.keys(schema.patternProperties).reduce((acc, key) => { + return { ...acc, [key]: Visit(schema.patternProperties[key]) } + }, {}) + return { ...schema, patternProperties } + } + function Ref(schema: Types.TRef): Types.TSchema { + return schema + } + function This(schema: Types.TThis): Types.TSchema { + return schema + } + function Tuple(schema: Types.TTuple): Types.TSchema { + if (schema.items === undefined) return schema + const items = schema.items.map((schema) => Visit(schema)) + return { ...schema, items } + } + function TemplateLiteral(schema: Types.TTemplateLiteral): Types.TSchema { + return schema + } + function UInt8Array(schema: Types.TUint8Array): Types.TSchema { + return schema + } + function Undefined(schema: Types.TUndefined): Types.TSchema { + return schema + } + function Union(schema: Types.TUnion): Types.TSchema { + const anyOf = schema.anyOf.map((schema) => Visit(schema)) + return { ...schema, anyOf } + } + function Unknown(schema: Types.TUnknown): Types.TSchema { + return schema + } + function Void(schema: Types.TVoid) { + return schema + } + function UnsupportedType(schema: Types.TSchema) { + return schema + } + function Visit(schema: Types.TSchema): Types.TSchema { + if (Types.TypeGuard.TAny(schema)) return Any(schema) + if (Types.TypeGuard.TArray(schema)) return Array(schema) + if (Types.TypeGuard.TBigInt(schema)) return BigInt(schema) + if (Types.TypeGuard.TBoolean(schema)) return Boolean(schema) + if (Types.TypeGuard.TDate(schema)) return Date(schema) + if (Types.TypeGuard.TConstructor(schema)) return Constructor(schema) + if (Types.TypeGuard.TFunction(schema)) return Function(schema) + if (Types.TypeGuard.TInteger(schema)) return Integer(schema) + if (Types.TypeGuard.TIntersect(schema)) return Intersect(schema) + if (Types.TypeGuard.TLiteral(schema)) return Literal(schema) + if (Types.TypeGuard.TNever(schema)) return Never(schema) + if (Types.TypeGuard.TNull(schema)) return Null(schema) + if (Types.TypeGuard.TNumber(schema)) return Number(schema) + if (Types.TypeGuard.TObject(schema)) return Object(schema) + if (Types.TypeGuard.TPromise(schema)) return Promise(schema) + if (Types.TypeGuard.TRecord(schema)) return Record(schema) + if (Types.TypeGuard.TRef(schema)) return Ref(schema) + if (Types.TypeGuard.TString(schema)) return String(schema) + if (Types.TypeGuard.TTemplateLiteral(schema)) return TemplateLiteral(schema) + if (Types.TypeGuard.TThis(schema)) return This(schema) + if (Types.TypeGuard.TTuple(schema)) return Tuple(schema) + if (Types.TypeGuard.TUint8Array(schema)) return UInt8Array(schema) + if (Types.TypeGuard.TUndefined(schema)) return Undefined(schema) + if (Types.TypeGuard.TUnion(schema)) return Union(schema) + if (Types.TypeGuard.TUnknown(schema)) return Unknown(schema) + if (Types.TypeGuard.TVoid(schema)) return Void(schema) + return UnsupportedType(schema) + } export function Generate(model: TypeBoxModel): string { - const definitions: string[] = [] + const buffer: string[] = [] for (const type of model.types) { - definitions.push(`export const ${type.$id!} = ${JSON.stringify(type)}`) + const schema = Visit(type) + const encode = JSON.stringify(schema, null, 2) + buffer.push(`export const ${type.$id} = ${encode}`) } - return Formatter.Format(definitions.join('\n\n')) + return Formatter.Format(buffer.join('\n')) } } diff --git a/src/model/model-to-typebox.ts b/src/model/model-to-typebox.ts index b894bd9..15cc294 100644 --- a/src/model/model-to-typebox.ts +++ b/src/model/model-to-typebox.ts @@ -1,6 +1,6 @@ /*-------------------------------------------------------------------------- -@typebox/codegen +@sinclair/typebox-codegen The MIT License (MIT) diff --git a/src/model/model-to-typescript.ts b/src/model/model-to-typescript.ts index 231eb5e..0e79a04 100644 --- a/src/model/model-to-typescript.ts +++ b/src/model/model-to-typescript.ts @@ -1,6 +1,6 @@ /*-------------------------------------------------------------------------- -@typebox/codegen +@sinclair/typebox-codegen The MIT License (MIT) @@ -24,9 +24,10 @@ THE SOFTWARE. ---------------------------------------------------------------------------*/ -import * as Types from '@sinclair/typebox' -import { Formatter } from '../common/index' import { TypeBoxModel } from './model' +import { Formatter } from '../common/formatter' +import { TypeCompiler } from '@sinclair/typebox/compiler' +import * as Types from '@sinclair/typebox' export namespace ModelToTypeScript { function Any(schema: Types.TAny) { @@ -39,21 +40,27 @@ export namespace ModelToTypeScript { function Boolean(schema: Types.TBoolean) { return 'boolean' } + function BigInt(schema: Types.TBigInt) { + return 'bigint' + } function Constructor(schema: Types.TConstructor) { - const params = schema.parameters.map((param, index) => `param${index} : ${Visit(param)}`).join(', ') + const params = schema.parameters.map((param) => Visit(param)).join(', ') const returns = Visit(schema.returns) - return `new (${params}) => ${returns}` + return `(new (${params}) => ${returns})` + } + function Date(schema: Types.TDate) { + return 'Date' } function Function(schema: Types.TFunction) { - const params = schema.parameters.map((param, index) => `param${index} : ${Visit(param)}`).join(', ') + const params = schema.parameters.map((param) => Visit(param)).join(', ') const returns = Visit(schema.returns) - return `(${params}) => ${returns}` + return `((${params}) => ${returns})` } function Integer(schema: Types.TInteger) { return 'number' } function Intersect(schema: Types.TIntersect) { - return `(${schema.allOf.map((schema) => Visit(schema)).join(' & ')})` + return schema.allOf.map((schema) => Visit(schema)).join(' & ') } function Literal(schema: Types.TLiteral) { if (typeof schema.const === 'string') { @@ -89,7 +96,7 @@ export namespace ModelToTypeScript { function Record(schema: Types.TRecord) { for (const [key, value] of globalThis.Object.entries(schema.patternProperties)) { const type = Visit(value) - if (key === Types.PatternNumberExact) { + if (key === '^(0|[1-9][0-9]*)$') { return `Record` } else { return `Record` @@ -115,7 +122,7 @@ export namespace ModelToTypeScript { return `undefined` } function Union(schema: Types.TUnion) { - return `${schema.anyOf.map((schema) => Visit(schema)).join(' | ')}` + return schema.anyOf.map((schema) => Visit(schema)).join(' | ') } function Unknown(schema: Types.TUnknown) { return `unknown` @@ -123,14 +130,15 @@ export namespace ModelToTypeScript { function Void(schema: Types.TVoid) { return `void` } - function UnsupportedType() { - return `never` - } function Visit(schema: Types.TSchema): string { + if (reference_map.has(schema.$id!)) return schema.$id! + if (schema.$id !== undefined) reference_map.set(schema.$id, schema) if (Types.TypeGuard.TAny(schema)) return Any(schema) if (Types.TypeGuard.TArray(schema)) return Array(schema) if (Types.TypeGuard.TBoolean(schema)) return Boolean(schema) + if (Types.TypeGuard.TBigInt(schema)) return BigInt(schema) if (Types.TypeGuard.TConstructor(schema)) return Constructor(schema) + if (Types.TypeGuard.TDate(schema)) return Date(schema) if (Types.TypeGuard.TFunction(schema)) return Function(schema) if (Types.TypeGuard.TInteger(schema)) return Integer(schema) if (Types.TypeGuard.TIntersect(schema)) return Intersect(schema) @@ -142,27 +150,36 @@ export namespace ModelToTypeScript { if (Types.TypeGuard.TPromise(schema)) return Promise(schema) if (Types.TypeGuard.TRecord(schema)) return Record(schema) if (Types.TypeGuard.TRef(schema)) return Ref(schema) - if (Types.TypeGuard.TString(schema)) return String(schema) if (Types.TypeGuard.TThis(schema)) return This(schema) + if (Types.TypeGuard.TString(schema)) return String(schema) if (Types.TypeGuard.TTuple(schema)) return Tuple(schema) if (Types.TypeGuard.TUint8Array(schema)) return UInt8Array(schema) if (Types.TypeGuard.TUndefined(schema)) return Undefined(schema) if (Types.TypeGuard.TUnion(schema)) return Union(schema) if (Types.TypeGuard.TUnknown(schema)) return Unknown(schema) if (Types.TypeGuard.TVoid(schema)) return Void(schema) - return UnsupportedType() - } - /** Generates TypeScript code from TypeBox types */ - export function GenerateType(schema: Types.TSchema, references: Types.TSchema[] = []) { - const buffer: string[] = [] - buffer.push(`export type ${schema.$id || 'T'} = ${[...Visit(schema)].join('')}`) - return Formatter.Format(buffer.join('\n\n')) - } - export function Generate(model: TypeBoxModel) { - const buffer: string[] = [] + return 'unknown' + } + export function GenerateType(model: TypeBoxModel, $id: string) { + reference_map.clear() + const type = model.types.find((type) => type.$id === $id) + if (type === undefined) return `export type ${$id} = unknown` + return `export type ${type.$id!} = ${Visit(type)}` + } + const reference_map = new Map() + export function Generate(model: TypeBoxModel): string { + reference_map.clear() + const definitions: string[] = [] for (const type of model.types) { - buffer.push(GenerateType(type, model.types)) + const definition = `export type ${type.$id!} = ${Visit(type)}` + const assertion = `export const ${type.$id!} = (() => { ${TypeCompiler.Code(type, model.types, { language: 'typescript' })} })();` + const rewritten = assertion.replaceAll(`return function check(value: any): boolean`, `return function check(value: any): value is ${type.$id!}`) + definitions.push(` + ${definition} + ${rewritten} + `) } - return Formatter.Format(buffer.join('\n')) + const output = [...definitions] + return Formatter.Format(output.join('\n\n')) } } diff --git a/src/model/model-to-valibot.ts b/src/model/model-to-valibot.ts new file mode 100644 index 0000000..894bc8e --- /dev/null +++ b/src/model/model-to-valibot.ts @@ -0,0 +1,235 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox-codegen + +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { Formatter, PropertyEncoder } from '../common/index' +import { TypeBoxModel } from './model' +import { ModelToTypeScript } from './model-to-typescript' +import * as Types from '@sinclair/typebox' + +// -------------------------------------------------------------------------- +// ModelToValibot +// -------------------------------------------------------------------------- +export namespace ModelToValibot { + function IsDefined(value: unknown): value is T { + return value !== undefined + } + function Type(type: string, parameter: string | null, constraints: string[]) { + if (constraints.length > 0) { + if (typeof parameter === 'string') { + return `${type}(${parameter}, [${constraints.join(', ')}])` + } else { + return `${type}([${constraints.join(', ')}])` + } + } else { + if (typeof parameter === 'string') { + return `${type}(${parameter})` + } else { + return `${type}()` + } + } + } + function Any(schema: Types.TAny) { + return Type(`v.any`, null, []) + } + function Array(schema: Types.TArray) { + const items = Visit(schema.items) + const constraints: string[] = [] + if (IsDefined(schema.minItems)) constraints.push(`v.minLength(${schema.minItems})`) + if (IsDefined(schema.maxItems)) constraints.push(`v.maxLength(${schema.maxItems})`) + return Type(`v.array`, items, constraints) + } + function BigInt(schema: Types.TBigInt) { + return Type(`v.bigint`, null, []) + } + function Boolean(schema: Types.TBoolean) { + return Type(`v.boolean`, null, []) + } + function Date(schema: Types.TDate) { + return Type(`v.date`, null, []) + } + function Constructor(schema: Types.TConstructor): string { + return UnsupportedType(schema) + } + function Function(schema: Types.TFunction) { + return UnsupportedType(schema) + } + function Integer(schema: Types.TInteger) { + return UnsupportedType(schema) + } + function Intersect(schema: Types.TIntersect) { + const inner = schema.allOf.map((inner) => Visit(inner)) + return Type(`v.merge`, `[${inner.join(', ')}]`, []) + } + function Literal(schema: Types.TLiteral) { + // prettier-ignore + return typeof schema.const === `string` + ? Type(`v.literal`, `'${schema.const}'`, []) + : Type(`v.literal`, `${schema.const}`, []) + } + function Never(schema: Types.TNever) { + return Type(`v.never`, null, []) + } + function Null(schema: Types.TNull) { + return UnsupportedType(schema) + } + function String(schema: Types.TString) { + const constraints: string[] = [] + if (IsDefined(schema.maxLength)) constraints.push(`v.maxLength(${schema.maxLength})`) + if (IsDefined(schema.minLength)) constraints.push(`v.minLength(${schema.minLength})`) + return Type(`v.string`, null, constraints) + } + function Number(schema: Types.TNumber) { + const constraints: string[] = [] + if (IsDefined(schema.minimum)) constraints.push(`v.minValue(${schema.minimum})`) + if (IsDefined(schema.maximum)) constraints.push(`v.maxValue(${schema.maximum})`) + if (IsDefined(schema.exclusiveMinimum)) constraints.push(`v.minValue(${schema.exclusiveMinimum + 1})`) + if (IsDefined(schema.exclusiveMaximum)) constraints.push(`v.maxValue(${schema.exclusiveMaximum - 1})`) + return Type('v.number', null, constraints) + } + function Object(schema: Types.TObject) { + // prettier-ignore + const properties = globalThis.Object.entries(schema.properties).map(([key, value]) => { + const optional = Types.TypeGuard.TOptional(value) + const property = PropertyEncoder.Encode(key) + return optional ? `${property}: v.optional(${Visit(value)})` : `${property}: ${Visit(value)}` + }).join(`,`) + const constraints: string[] = [] + return Type(`v.object`, `{\n${properties}\n}`, constraints) + } + function Promise(schema: Types.TPromise) { + return UnsupportedType(schema) + } + function Record(schema: Types.TRecord) { + for (const [key, value] of globalThis.Object.entries(schema.patternProperties)) { + const type = Visit(value) + if (key === `^(0|[1-9][0-9]*)$`) { + return UnsupportedType(schema) + } else { + return Type(`v.record`, type, []) + } + } + throw Error(`Unreachable`) + } + function Ref(schema: Types.TRef) { + if (!reference_map.has(schema.$ref!)) return UnsupportedType(schema) + return schema.$ref + } + function This(schema: Types.TThis) { + return UnsupportedType(schema) + } + function Tuple(schema: Types.TTuple) { + if (schema.items === undefined) return `[]` + const items = schema.items.map((schema) => Visit(schema)).join(`, `) + return Type(`v.tuple`, `[${items}]`, []) + } + function TemplateLiteral(schema: Types.TTemplateLiteral) { + const constaint = Type(`v.regex`, `/${schema.pattern}/`, []) + return Type(`v.string`, null, [constaint]) + } + function UInt8Array(schema: Types.TUint8Array): string { + return UnsupportedType(schema) + } + function Undefined(schema: Types.TUndefined) { + return UnsupportedType(schema) + } + function Union(schema: Types.TUnion) { + const inner = schema.anyOf.map((schema) => Visit(schema)).join(`, `) + return Type(`v.union`, `[${inner}]`, []) + } + function Unknown(schema: Types.TUnknown) { + return Type(`v.unknown`, null, []) + } + function Void(schema: Types.TVoid) { + return UnsupportedType(schema) + } + function UnsupportedType(schema: Types.TSchema) { + return `${Type(`v.any`, null, [])} /* unsupported */` + } + function Visit(schema: Types.TSchema): string { + if (schema.$id !== undefined) reference_map.set(schema.$id, schema) + if (schema.$id !== undefined && emitted_set.has(schema.$id!)) return schema.$id! + if (Types.TypeGuard.TAny(schema)) return Any(schema) + if (Types.TypeGuard.TArray(schema)) return Array(schema) + if (Types.TypeGuard.TBigInt(schema)) return BigInt(schema) + if (Types.TypeGuard.TBoolean(schema)) return Boolean(schema) + if (Types.TypeGuard.TDate(schema)) return Date(schema) + if (Types.TypeGuard.TConstructor(schema)) return Constructor(schema) + if (Types.TypeGuard.TFunction(schema)) return Function(schema) + if (Types.TypeGuard.TInteger(schema)) return Integer(schema) + if (Types.TypeGuard.TIntersect(schema)) return Intersect(schema) + if (Types.TypeGuard.TLiteral(schema)) return Literal(schema) + if (Types.TypeGuard.TNever(schema)) return Never(schema) + if (Types.TypeGuard.TNull(schema)) return Null(schema) + if (Types.TypeGuard.TNumber(schema)) return Number(schema) + if (Types.TypeGuard.TObject(schema)) return Object(schema) + if (Types.TypeGuard.TPromise(schema)) return Promise(schema) + if (Types.TypeGuard.TRecord(schema)) return Record(schema) + if (Types.TypeGuard.TRef(schema)) return Ref(schema) + if (Types.TypeGuard.TString(schema)) return String(schema) + if (Types.TypeGuard.TTemplateLiteral(schema)) return TemplateLiteral(schema) + if (Types.TypeGuard.TThis(schema)) return This(schema) + if (Types.TypeGuard.TTuple(schema)) return Tuple(schema) + if (Types.TypeGuard.TUint8Array(schema)) return UInt8Array(schema) + if (Types.TypeGuard.TUndefined(schema)) return Undefined(schema) + if (Types.TypeGuard.TUnion(schema)) return Union(schema) + if (Types.TypeGuard.TUnknown(schema)) return Unknown(schema) + if (Types.TypeGuard.TVoid(schema)) return Void(schema) + return UnsupportedType(schema) + } + function Collect(schema: Types.TSchema) { + return [...Visit(schema)].join(``) + } + function GenerateType(model: TypeBoxModel, schema: Types.TSchema, references: Types.TSchema[]) { + const output: string[] = [] + for (const reference of references) { + if (reference.$id === undefined) return UnsupportedType(schema) + reference_map.set(reference.$id, reference) + } + const type = Collect(schema) + if (recursive_set.has(schema.$id!)) { + output.push(`export ${ModelToTypeScript.GenerateType(model, schema.$id!)}`) + output.push(`export const ${schema.$id || `T`}: v.Output<${schema.$id}> = v.lazy(() => ${Formatter.Format(type)})`) + } else { + output.push(`export type ${schema.$id} = v.Output`) + output.push(`export const ${schema.$id || `T`} = ${Formatter.Format(type)}`) + } + if (schema.$id) emitted_set.add(schema.$id) + return output.join('\n') + } + const reference_map = new Map() + const recursive_set = new Set() + const emitted_set = new Set() + export function Generate(model: TypeBoxModel): string { + reference_map.clear() + recursive_set.clear() + emitted_set.clear() + const buffer: string[] = [`import v from 'valibot'`, ''] + for (const type of model.types) { + buffer.push(GenerateType(model, type, model.types)) + } + return Formatter.Format(buffer.join('\n')) + } +} diff --git a/src/model/model-to-value.ts b/src/model/model-to-value.ts index dea4057..1cbaf4b 100644 --- a/src/model/model-to-value.ts +++ b/src/model/model-to-value.ts @@ -1,6 +1,6 @@ /*-------------------------------------------------------------------------- -@typebox/codegen +@sinclair/typebox-codegen The MIT License (MIT) @@ -25,15 +25,18 @@ THE SOFTWARE. ---------------------------------------------------------------------------*/ import { TypeBoxModel } from './model' -import { Formatter } from '../common/formatter' import { Value } from '@sinclair/typebox/value' export namespace ModelToValue { export function Generate(model: TypeBoxModel): string { const definitions: string[] = [] + for (const type of model.types) { - definitions.push(`export const ${type.$id!} = ${JSON.stringify(Value.Create(type))};`) + definitions.push(` + export const ${type.$id!} = ${JSON.stringify(Value.Create(type, model.types))}; + `) } - return Formatter.Format(definitions.join('\n\n')) + const output = [...definitions] + return output.join('\n\n') } } diff --git a/src/model/model-to-yup.ts b/src/model/model-to-yup.ts new file mode 100644 index 0000000..df552b6 --- /dev/null +++ b/src/model/model-to-yup.ts @@ -0,0 +1,232 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox-codegen + +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { Formatter, PropertyEncoder } from '../common/index' +import { TypeBoxModel } from './model' +import { ModelToTypeScript } from './model-to-typescript' +import * as Types from '@sinclair/typebox' + +// -------------------------------------------------------------------------- +// ModelToYup +// -------------------------------------------------------------------------- +export namespace ModelToYup { + function IsDefined(value: unknown): value is T { + return value !== undefined + } + function Type(schema: Types.TSchema, type: string) { + return schema.default === undefined ? type : `${type}.default(${JSON.stringify(schema.default)})` + } + function Any(schema: Types.TAny) { + return Type(schema, `y.mixed((value): value is any => true)`) + } + function Array(schema: Types.TArray) { + const items = Visit(schema.items) + const buffer: string[] = [] + buffer.push(`y.array(${items})`) + if (IsDefined(schema.minItems)) buffer.push(`.min(${schema.minItems})`) + if (IsDefined(schema.maxItems)) buffer.push(`.max(${schema.maxItems})`) + return Type(schema, buffer.join(``)) + } + function BigInt(schema: Types.TBigInt) { + return Type(schema, `y.mixed((value): value is bigint => typeof value === 'bigint')`) + } + function Boolean(schema: Types.TBoolean) { + return Type(schema, `y.boolean()`) + } + function Date(schema: Types.TDate) { + return Type(schema, `y.date()`) + } + function Constructor(schema: Types.TConstructor): string { + return Type(schema, `y.mixed((value): value is Function => typeof value === 'function')`) + } + function Function(schema: Types.TFunction) { + return Type(schema, `y.mixed((value): value is Function => typeof value === 'function')`) + } + function Integer(schema: Types.TInteger) { + const buffer: string[] = [] + buffer.push(`y.number().integer()`) + if (IsDefined(schema.minimum)) buffer.push(`.min(${schema.minimum})`) + if (IsDefined(schema.maximum)) buffer.push(`.max(${schema.maximum})`) + if (IsDefined(schema.exclusiveMaximum)) buffer.push(`.max(${schema.exclusiveMaximum - 1})`) + if (IsDefined(schema.exclusiveMinimum)) buffer.push(`.max(${schema.exclusiveMinimum + 1})`) + return Type(schema, buffer.join(``)) + } + function Intersect(schema: Types.TIntersect) { + const mergable = schema.allOf.every((schema) => Types.TypeGuard.TObject(schema)) + if (!mergable) return UnsupportedType(schema) + const composite = Types.Type.Composite(schema.allOf as any) + return Visit(composite) + } + function Literal(schema: Types.TLiteral) { + return typeof schema.const === `string` ? Type(schema, `y.mixed((value): value is '${schema.const}' => value === '${schema.const}')`) : Type(schema, `y.mixed((value): value is ${schema.const} => value === ${schema.const})`) + } + function Never(schema: Types.TNever) { + return Type(schema, `y.never()`) + } + function Null(schema: Types.TNull) { + return Type(schema, `y.mixed((value): value is any /** null not supported */ => value === null)`) + } + function String(schema: Types.TString) { + const buffer: string[] = [] + buffer.push(`y.string()`) + if (IsDefined(schema.maxLength)) buffer.push(`.max(${schema.maxLength})`) + if (IsDefined(schema.minLength)) buffer.push(`.min(${schema.minLength})`) + return Type(schema, buffer.join(``)) + } + function Number(schema: Types.TNumber) { + const buffer: string[] = [] + buffer.push(`y.number()`) + if (IsDefined(schema.minimum)) buffer.push(`.min(${schema.minimum})`) + if (IsDefined(schema.maximum)) buffer.push(`.max(${schema.maximum})`) + if (IsDefined(schema.exclusiveMaximum)) buffer.push(`.max(${schema.exclusiveMaximum - 1})`) + if (IsDefined(schema.exclusiveMinimum)) buffer.push(`.max(${schema.exclusiveMinimum + 1})`) + if (IsDefined(schema.multipleOf)) buffer.push(`.multipleOf(${schema.multipleOf})`) + return Type(schema, buffer.join(``)) + } + function Object(schema: Types.TObject) { + // prettier-ignore + const properties = globalThis.Object.entries(schema.properties).map(([key, value]) => { + const optional = Types.TypeGuard.TOptional(value) + const property = PropertyEncoder.Encode(key) + return optional ? `${property}: ${Visit(value)}.optional()` : `${property}: ${Visit(value)}.required()` + }).join(`,`) + const buffer: string[] = [] + buffer.push(`y.object({\n${properties}\n})`) + if (schema.additionalProperties === false) buffer.push(`.strict()`) + return Type(schema, buffer.join(``)) + } + function Promise(schema: Types.TPromise) { + const item = Visit(schema.item) + return Type(schema, `y.mixed((value): value is Promise => value instanceof Promise)`) + } + function Record(schema: Types.TRecord) { + for (const [key, value] of globalThis.Object.entries(schema.patternProperties)) { + const type = Visit(value) + if (key === `^(0|[1-9][0-9]*)$`) { + return `y.record(z.number(), ${type})` + } else { + return `y.record(${type})` + } + } + throw Error(`Unreachable`) + } + function Ref(schema: Types.TRef) { + return `${schema.$ref}` + } + function This(schema: Types.TThis) { + return `${Type(schema, `y.mixed()`)} /* unsupported */` + } + function Tuple(schema: Types.TTuple) { + if (schema.items === undefined) return `[]` + const items = schema.items.map((schema) => `${Visit(schema)}.required()`).join(`, `) + return Type(schema, `y.tuple([${items}])`) + } + function TemplateLiteral(schema: Types.TTemplateLiteral) { + return Type(schema, `y.string().matches(/${schema.pattern}/)`) + } + function UInt8Array(schema: Types.TUint8Array): string { + return Type(schema, `y.mixed((value): value is Uint8Array => value instanceof Uint8Array)`) + } + function Undefined(schema: Types.TUndefined) { + return Type(schema, `y.mixed().oneOf([undefined])`) + } + function Union(schema: Types.TUnion) { + return Type(schema, `y.mixed().oneOf([${schema.anyOf.map((schema) => Visit(schema)).join(`, `)}])`) + } + function Unknown(schema: Types.TUnknown) { + return Type(schema, `y.mixed((value): value is unknown => true)`) + } + function Void(schema: Types.TVoid) { + return Type(schema, `y.mixed((value): value is undefined => value === undefined)`) + } + function UnsupportedType(schema: Types.TSchema) { + return `${Type(schema, `y.mixed()`)} /* unresolved */` + } + function Visit(schema: Types.TSchema): string { + if (schema.$id !== undefined) reference_map.set(schema.$id, schema) + if (schema.$id !== undefined && emitted_set.has(schema.$id!)) return schema.$id! + if (Types.TypeGuard.TAny(schema)) return Any(schema) + if (Types.TypeGuard.TArray(schema)) return Array(schema) + if (Types.TypeGuard.TBigInt(schema)) return BigInt(schema) + if (Types.TypeGuard.TBoolean(schema)) return Boolean(schema) + if (Types.TypeGuard.TDate(schema)) return Date(schema) + if (Types.TypeGuard.TConstructor(schema)) return Constructor(schema) + if (Types.TypeGuard.TFunction(schema)) return Function(schema) + if (Types.TypeGuard.TInteger(schema)) return Integer(schema) + if (Types.TypeGuard.TIntersect(schema)) return Intersect(schema) + if (Types.TypeGuard.TLiteral(schema)) return Literal(schema) + if (Types.TypeGuard.TNever(schema)) return Never(schema) + if (Types.TypeGuard.TNull(schema)) return Null(schema) + if (Types.TypeGuard.TNumber(schema)) return Number(schema) + if (Types.TypeGuard.TObject(schema)) return Object(schema) + if (Types.TypeGuard.TPromise(schema)) return Promise(schema) + if (Types.TypeGuard.TRecord(schema)) return Record(schema) + if (Types.TypeGuard.TRef(schema)) return Ref(schema) + if (Types.TypeGuard.TString(schema)) return String(schema) + if (Types.TypeGuard.TTemplateLiteral(schema)) return TemplateLiteral(schema) + if (Types.TypeGuard.TThis(schema)) return This(schema) + if (Types.TypeGuard.TTuple(schema)) return Tuple(schema) + if (Types.TypeGuard.TUint8Array(schema)) return UInt8Array(schema) + if (Types.TypeGuard.TUndefined(schema)) return Undefined(schema) + if (Types.TypeGuard.TUnion(schema)) return Union(schema) + if (Types.TypeGuard.TUnknown(schema)) return Unknown(schema) + if (Types.TypeGuard.TVoid(schema)) return Void(schema) + return UnsupportedType(schema) + } + function Collect(schema: Types.TSchema) { + return [...Visit(schema)].join(``) + } + function GenerateType(model: TypeBoxModel, schema: Types.TSchema, references: Types.TSchema[]) { + const output: string[] = [] + for (const reference of references) { + if (reference.$id === undefined) return UnsupportedType(schema) + reference_map.set(reference.$id, reference) + } + const type = Collect(schema) + if (recursive_set.has(schema.$id!)) { + output.push(`export ${ModelToTypeScript.GenerateType(model, schema.$id!)}`) + output.push(`export const ${schema.$id || `T`}: y.InferType<${schema.$id}> = z.lazy(() => ${Formatter.Format(type)})`) + } else { + output.push(`export type ${schema.$id} = y.InferType`) + output.push(`export const ${schema.$id || `T`} = ${Formatter.Format(type)}`) + } + if (schema.$id) emitted_set.add(schema.$id) + return output.join('\n') + } + const reference_map = new Map() + const recursive_set = new Set() + const emitted_set = new Set() + export function Generate(model: TypeBoxModel): string { + reference_map.clear() + recursive_set.clear() + emitted_set.clear() + const buffer: string[] = [`import y from 'yup'`, ''] + for (const type of model.types) { + buffer.push(GenerateType(model, type, model.types)) + } + return Formatter.Format(buffer.join('\n')) + } +} diff --git a/src/model/model-to-zod.ts b/src/model/model-to-zod.ts index 655e3af..676d973 100644 --- a/src/model/model-to-zod.ts +++ b/src/model/model-to-zod.ts @@ -1,6 +1,6 @@ /*-------------------------------------------------------------------------- -@typebox/codegen +@sinclair/typebox-codegen The MIT License (MIT) @@ -26,6 +26,7 @@ THE SOFTWARE. import { Formatter, PropertyEncoder } from '../common/index' import { TypeBoxModel } from './model' +import { ModelToTypeScript } from './model-to-typescript' import * as Types from '@sinclair/typebox' // -------------------------------------------------------------------------- @@ -35,8 +36,11 @@ export namespace ModelToZod { function IsDefined(value: unknown): value is T { return value !== undefined } + function Type(schema: Types.TSchema, type: string) { + return schema.default === undefined ? type : `${type}.default(${JSON.stringify(schema.default)})` + } function Any(schema: Types.TAny) { - return `z.any()` + return Type(schema, `z.any()`) } function Array(schema: Types.TArray) { const items = Visit(schema.items) @@ -44,10 +48,16 @@ export namespace ModelToZod { buffer.push(`z.array(${items})`) if (IsDefined(schema.minItems)) buffer.push(`.min(${schema.minItems})`) if (IsDefined(schema.maxItems)) buffer.push(`.max(${schema.maxItems})`) - return buffer.join(``) + return Type(schema, buffer.join(``)) + } + function BigInt(schema: Types.TBigInt) { + return Type(schema, `z.bigint()`) } function Boolean(schema: Types.TBoolean) { - return `z.boolean()` + return Type(schema, `z.boolean()`) + } + function Date(schema: Types.TDate) { + return Type(schema, `z.date()`) } function Constructor(schema: Types.TConstructor): string { return UnsupportedType(schema) @@ -55,70 +65,68 @@ export namespace ModelToZod { function Function(schema: Types.TFunction) { const params = schema.parameters.map((param) => Visit(param)).join(`, `) const returns = Visit(schema.returns) - return `z.function().args(${params}).returns(${returns})` + return Type(schema, `z.function().args(${params}).returns(${returns})`) } function Integer(schema: Types.TInteger) { const buffer: string[] = [] buffer.push(`z.number().int()`) if (IsDefined(schema.minimum)) buffer.push(`.min(${schema.minimum})`) if (IsDefined(schema.maximum)) buffer.push(`.max(${schema.maximum})`) + if (IsDefined(schema.exclusiveMinimum)) buffer.push(`.min(${schema.exclusiveMinimum + 1})`) if (IsDefined(schema.exclusiveMaximum)) buffer.push(`.max(${schema.exclusiveMaximum - 1})`) - if (IsDefined(schema.exclusiveMinimum)) buffer.push(`.max(${schema.exclusiveMinimum + 1})`) if (IsDefined(schema.multipleOf)) buffer.push(`.multipleOf(${schema.multipleOf})`) - return buffer.join(``) + return Type(schema, buffer.join(``)) } function Intersect(schema: Types.TIntersect) { - // note: Zod only supports binary intersection. While correct, this is partially at odds with TypeScript's - // ability to distribute across (A & B & C). This code reduces intersection to binary ops. function reduce(rest: Types.TSchema[]): string { if (rest.length === 0) return `z.never()` if (rest.length === 1) return Visit(rest[0]) const [left, right] = [rest[0], rest.slice(1)] - return `z.intersection(${Visit(left)}, ${reduce(right)})` + return Type(schema, `z.intersection(${Visit(left)}, ${reduce(right)})`) } return reduce(schema.allOf) } function Literal(schema: Types.TLiteral) { - return typeof schema.const === `string` ? `z.literal('${schema.const}')` : `z.literal(${schema.const})` + return typeof schema.const === `string` ? Type(schema, `z.literal('${schema.const}')`) : Type(schema, `z.literal(${schema.const})`) } function Never(schema: Types.TNever) { - return `z.never()` + return Type(schema, `z.never()`) } function Null(schema: Types.TNull) { - return `z.null()` + return Type(schema, `z.null()`) } function String(schema: Types.TString) { const buffer: string[] = [] buffer.push(`z.string()`) if (IsDefined(schema.maxLength)) buffer.push(`.max(${schema.maxLength})`) if (IsDefined(schema.minLength)) buffer.push(`.min(${schema.minLength})`) - return buffer.join(``) + return Type(schema, buffer.join(``)) } function Number(schema: Types.TNumber) { const buffer: string[] = [] buffer.push(`z.number()`) if (IsDefined(schema.minimum)) buffer.push(`.min(${schema.minimum})`) if (IsDefined(schema.maximum)) buffer.push(`.max(${schema.maximum})`) + if (IsDefined(schema.exclusiveMinimum)) buffer.push(`.min(${schema.exclusiveMinimum + 1})`) if (IsDefined(schema.exclusiveMaximum)) buffer.push(`.max(${schema.exclusiveMaximum - 1})`) - if (IsDefined(schema.exclusiveMinimum)) buffer.push(`.max(${schema.exclusiveMinimum + 1})`) if (IsDefined(schema.multipleOf)) buffer.push(`.multipleOf(${schema.multipleOf})`) - return buffer.join(``) + return Type(schema, buffer.join(``)) } function Object(schema: Types.TObject) { // prettier-ignore const properties = globalThis.Object.entries(schema.properties).map(([key, value]) => { - const optional = Types.TypeGuard.TOptional(value) || Types.TypeGuard.TReadonlyOptional(value) + const optional = Types.TypeGuard.TOptional(value) const property = PropertyEncoder.Encode(key) return optional ? `${property}: ${Visit(value)}.optional()` : `${property}: ${Visit(value)}` }).join(`,`) const buffer: string[] = [] buffer.push(`z.object({\n${properties}\n})`) if (schema.additionalProperties === false) buffer.push(`.strict()`) - return buffer.join(``) + return Type(schema, buffer.join(``)) } function Promise(schema: Types.TPromise) { const item = Visit(schema.item) - return `${item}.promise()` + return Type(schema, `${item}.promise()`) } function Record(schema: Types.TRecord) { for (const [key, value] of globalThis.Object.entries(schema.patternProperties)) { @@ -129,7 +137,7 @@ export namespace ModelToZod { return `z.record(${type})` } } - throw Error(`TypeBoxToZod: Unreachable`) + throw Error(`Unreachable`) } function Ref(schema: Types.TRef) { if (!reference_map.has(schema.$ref!)) return UnsupportedType(schema) // throw new ModelToZodNonReferentialType(schema.$ref!) @@ -143,35 +151,37 @@ export namespace ModelToZod { function Tuple(schema: Types.TTuple) { if (schema.items === undefined) return `[]` const items = schema.items.map((schema) => Visit(schema)).join(`, `) - return `z.tuple([${items}])` + return Type(schema, `z.tuple([${items}])`) } function TemplateLiteral(schema: Types.TTemplateLiteral) { - return `z.string().regex(new RegExp('${schema.pattern}'))` + return Type(schema, `z.string().regex(/${schema.pattern}/)`) } function UInt8Array(schema: Types.TUint8Array): string { - return `z.instanceof(Uint8Array)` + return Type(schema, `z.instanceof(Uint8Array)`) } function Undefined(schema: Types.TUndefined) { - return `z.undefined()` + return Type(schema, `z.undefined()`) } function Union(schema: Types.TUnion) { - return `z.union([${schema.anyOf.map((schema) => Visit(schema)).join(`, `)}])` + return Type(schema, `z.union([${schema.anyOf.map((schema) => Visit(schema)).join(`, `)}])`) } function Unknown(schema: Types.TUnknown) { - return `z.unknown()` + return Type(schema, `z.unknown()`) } function Void(schema: Types.TVoid) { - return `z.void()` + return Type(schema, `z.void()`) } function UnsupportedType(schema: Types.TSchema) { - return `z.any(/* ${schema[Types.Kind]} */)` + return `${Type(schema, `z.any()`)} /* unresolved */` } function Visit(schema: Types.TSchema): string { if (schema.$id !== undefined) reference_map.set(schema.$id, schema) if (schema.$id !== undefined && emitted_set.has(schema.$id!)) return schema.$id! if (Types.TypeGuard.TAny(schema)) return Any(schema) if (Types.TypeGuard.TArray(schema)) return Array(schema) + if (Types.TypeGuard.TBigInt(schema)) return BigInt(schema) if (Types.TypeGuard.TBoolean(schema)) return Boolean(schema) + if (Types.TypeGuard.TDate(schema)) return Date(schema) if (Types.TypeGuard.TConstructor(schema)) return Constructor(schema) if (Types.TypeGuard.TFunction(schema)) return Function(schema) if (Types.TypeGuard.TInteger(schema)) return Integer(schema) @@ -198,17 +208,16 @@ export namespace ModelToZod { function Collect(schema: Types.TSchema) { return [...Visit(schema)].join(``) } - function GenerateType(schema: Types.TSchema, references: Types.TSchema[]) { + function GenerateType(model: TypeBoxModel, schema: Types.TSchema, references: Types.TSchema[]) { const output: string[] = [] - if (schema.$id === undefined) schema.$id = `T_Generated` for (const reference of references) { - if (reference.$id === undefined) return UnsupportedType(schema) // throw new ModelToZodNonReferentialType(JSON.stringify(reference)) + if (reference.$id === undefined) return UnsupportedType(schema) reference_map.set(reference.$id, reference) } const type = Collect(schema) if (recursive_set.has(schema.$id!)) { - output.push(`export type ${schema.$id} = z.infer`) - output.push(`export const ${schema.$id || `T`} = z.lazy(() => ${Formatter.Format(type)})`) + output.push(`export ${ModelToTypeScript.GenerateType(model, schema.$id!)}`) + output.push(`export const ${schema.$id || `T`}: z.ZodType<${schema.$id}> = z.lazy(() => ${Formatter.Format(type)})`) } else { output.push(`export type ${schema.$id} = z.infer`) output.push(`export const ${schema.$id || `T`} = ${Formatter.Format(type)}`) @@ -225,7 +234,7 @@ export namespace ModelToZod { emitted_set.clear() const buffer: string[] = [`import z from 'zod'`, ''] for (const type of model.types) { - buffer.push(GenerateType(type, model.types)) + buffer.push(GenerateType(model, type, model.types)) } return Formatter.Format(buffer.join('\n')) } diff --git a/src/model/model.ts b/src/model/model.ts index c547cf4..47bfdef 100644 --- a/src/model/model.ts +++ b/src/model/model.ts @@ -1,6 +1,6 @@ /*-------------------------------------------------------------------------- -@typebox/codegen +@sinclair/typebox-codegen The MIT License (MIT) diff --git a/src/typescript/index.ts b/src/typescript/index.ts index 4b9eb10..1c585f1 100644 --- a/src/typescript/index.ts +++ b/src/typescript/index.ts @@ -1,6 +1,6 @@ /*-------------------------------------------------------------------------- -@typebox/codegen +@sinclair/typebox-codegen The MIT License (MIT) diff --git a/src/typescript/typescript-to-model.ts b/src/typescript/typescript-to-model.ts index 4c5962c..954377b 100644 --- a/src/typescript/typescript-to-model.ts +++ b/src/typescript/typescript-to-model.ts @@ -1,6 +1,6 @@ /*-------------------------------------------------------------------------- -@typebox/codegen +@sinclair/typebox-codegen The MIT License (MIT) @@ -25,7 +25,7 @@ THE SOFTWARE. ---------------------------------------------------------------------------*/ import { TypeScriptToTypeBox } from './typescript-to-typebox' -import { Type, TypeClone, TSchema } from '@sinclair/typebox' +import { Type, Kind, TSchema, TypeClone, TypeGuard, TemplateLiteralParser, TemplateLiteralFinite, TemplateLiteralGenerator } from '@sinclair/typebox' import { TypeBoxModel } from '../model/model' import * as ts from 'typescript' @@ -36,8 +36,8 @@ export namespace TypeScriptToModel { } export function Exports(code: string): Map { const exports = {} - const evaluate = new Function('exports', 'Type', 'TypeClone', code) - evaluate(exports, Type, TypeClone) + const evaluate = new Function('exports', 'Type', 'Kind', 'TypeGuard', 'TypeClone', 'TemplateLiteralParser', 'TemplateLiteralFinite', 'TemplateLiteralGenerator', code) + evaluate(exports, Type, Kind, TypeGuard, TypeClone, TemplateLiteralParser, TemplateLiteralFinite, TemplateLiteralGenerator) return new Map(globalThis.Object.entries(exports)) } export function Types(exports: Map): TSchema[] { diff --git a/src/typescript/typescript-to-typebox.ts b/src/typescript/typescript-to-typebox.ts index bf01e3f..9bc5781 100644 --- a/src/typescript/typescript-to-typebox.ts +++ b/src/typescript/typescript-to-typebox.ts @@ -1,6 +1,6 @@ /*-------------------------------------------------------------------------- -@typebox/codegen +@sinclair/typebox-codegen The MIT License (MIT) @@ -35,6 +35,7 @@ export class TypeScriptToTypeBoxError extends Error { // -------------------------------------------------------------------------- // TypeScriptToTypeBox // -------------------------------------------------------------------------- + export interface TypeScriptToTypeBoxOptions { /** * Setting this to true will ensure all types are exports as const values. This setting is @@ -69,6 +70,8 @@ export namespace TypeScriptToTypeBox { // ------------------------------------------------------------------------------------------------------------ // Transpile States // ------------------------------------------------------------------------------------------------------------ + // (auto) tracked on calls to find type name + const typenames = new Set() // (auto) tracked for recursive types and used to associate This type references let recursiveDeclaration: ts.TypeAliasDeclaration | ts.InterfaceDeclaration | null = null // (auto) tracked for scoped block level definitions and used to prevent `export` emit when not in global scope. @@ -79,14 +82,15 @@ export namespace TypeScriptToTypeBox { let useOptions = false // (auto) tracked for injecting TSchema import statements let useGenerics = false + // (auto) tracked for mapped types + let useMapped = false // (auto) tracked for cases where composition requires deep clone let useTypeClone = false - // (auto) tracked for each generated type. - const typeNames = new Set() // (option) export override to ensure all schematics let useExportsEverything = false // (option) inject identifiers let useIdentifiers = false + // (option) specifies if typebox imports should be included let useTypeBoxImport = true // ------------------------------------------------------------------------------------------------------------ // AST Query @@ -94,8 +98,22 @@ export namespace TypeScriptToTypeBox { function FindRecursiveParent(decl: ts.InterfaceDeclaration | ts.TypeAliasDeclaration, node: ts.Node): boolean { return (ts.isTypeReferenceNode(node) && decl.name.getText() === node.typeName.getText()) || node.getChildren().some((node) => FindRecursiveParent(decl, node)) } + function FindRecursiveThis(node: ts.Node): boolean { + return node.getChildren().some((node) => ts.isThisTypeNode(node) || FindRecursiveThis(node)) + } + function FindTypeName(node: ts.Node, name: string): boolean { + const found = + typenames.has(name) || + node.getChildren().some((node) => { + return ((ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node)) && node.name.getText() === name) || FindTypeName(node, name) + }) + if (found) typenames.add(name) + return found + } function IsRecursiveType(decl: ts.InterfaceDeclaration | ts.TypeAliasDeclaration) { - return ts.isTypeAliasDeclaration(decl) ? [decl.type].some((node) => FindRecursiveParent(decl, node)) : decl.members.some((node) => FindRecursiveParent(decl, node)) + const check1 = ts.isTypeAliasDeclaration(decl) ? [decl.type].some((node) => FindRecursiveParent(decl, node)) : decl.members.some((node) => FindRecursiveParent(decl, node)) + const check2 = ts.isInterfaceDeclaration(decl) && FindRecursiveThis(decl) + return check1 || check2 } function IsReadonlyProperty(node: ts.PropertySignature): boolean { return node.modifiers !== undefined && node.modifiers.find((modifier) => modifier.getText() === 'readonly') !== undefined @@ -203,6 +221,13 @@ export namespace TypeScriptToTypeBox { const types = node.types.map((type) => Collect(type)).join(',\n') yield `Type.Union([\n${types}\n])` } + function* MappedTypeNode(node: ts.MappedTypeNode): IterableIterator { + useMapped = true + const K = Collect(node.typeParameter) + const T = Collect(node.type) + const C = Collect(node.typeParameter.constraint) + yield `Mapped(${C}, ${K} => ${T})` + } function* MethodSignature(node: ts.MethodSignature): IterableIterator { const parameters = node.parameters.map((parameter) => (parameter.dotDotDotToken !== undefined ? `...Type.Rest(${Collect(parameter)})` : Collect(parameter))).join(', ') const returnType = node.type === undefined ? `Type.Unknown()` : Collect(node.type) @@ -227,6 +252,9 @@ export namespace TypeScriptToTypeBox { function* TemplateTail(node: ts.TemplateTail) { if (node.text.length > 0) yield `Type.Literal('${node.text}'), ` } + function* ThisTypeNode(node: ts.ThisTypeNode) { + yield `This` + } function* IntersectionTypeNode(node: ts.IntersectionTypeNode): IterableIterator { const types = node.types.map((type) => Collect(type)).join(',\n') yield `Type.Intersect([\n${types}\n])` @@ -260,7 +288,6 @@ export namespace TypeScriptToTypeBox { const enumType = `${exports}enum ${node.name.getText()}Enum { ${members} }` const type = `${exports}const ${node.name.getText()} = Type.Enum(${node.name.getText()}Enum)` yield [enumType, '', type].join('\n') - typeNames.add(node.name.getText()) } function PropertiesFromTypeElementArray(members: ts.NodeArray): string { const properties = members.filter((member) => !ts.isIndexSignatureDeclaration(member)) @@ -311,7 +338,6 @@ export namespace TypeScriptToTypeBox { const typeDeclaration = `${exports}const ${node.name.getText()} = ${type}` yield `${staticDeclaration}\n${typeDeclaration}` } - typeNames.add(node.name.getText()) recursiveDeclaration = null } function* TypeAliasDeclaration(node: ts.TypeAliasDeclaration): IterableIterator { @@ -342,7 +368,6 @@ export namespace TypeScriptToTypeBox { const typeDeclaration = `${exports}const ${node.name.getText()} = ${type_2}` yield `${staticDeclaration}\n${typeDeclaration}` } - typeNames.add(node.name.getText()) recursiveDeclaration = null } function* HeritageClause(node: ts.HeritageClause): IterableIterator { @@ -391,6 +416,7 @@ export namespace TypeScriptToTypeBox { if (name === 'Partial') return yield `Type.Partial${args}` if (name === 'Uint8Array') return yield `Type.Uint8Array()` if (name === 'Date') return yield `Type.Date()` + if (name === 'Function') return yield `Type.Function([], Type.Unknown())` if (name === 'Required') return yield `Type.Required${args}` if (name === 'Omit') return yield `Type.Omit${args}` if (name === 'Pick') return yield `Type.Pick${args}` @@ -398,11 +424,20 @@ export namespace TypeScriptToTypeBox { if (name === 'ReturnType') return yield `Type.ReturnType${args}` if (name === 'InstanceType') return yield `Type.InstanceType${args}` if (name === 'Parameters') return yield `Type.Parameters${args}` + if (name === 'AsyncIterableIterator') return yield `Type.AsyncIterator${args}` + if (name === 'IterableIterator') return yield `Type.Iterator${args}` if (name === 'ConstructorParameters') return yield `Type.ConstructorParameters${args}` if (name === 'Exclude') return yield `Type.Exclude${args}` if (name === 'Extract') return yield `Type.Extract${args}` + if (name === 'Awaited') return yield `Type.Awaited${args}` + if (name === 'Uppercase') return yield `Type.Uppercase${args}` + if (name === 'Lowercase') return yield `Type.Lowercase${args}` + if (name === 'Capitalize') return yield `Type.Capitalize${args}` + if (name === 'Uncapitalize') return yield `Type.Uncapitalize${args}` if (recursiveDeclaration !== null && FindRecursiveParent(recursiveDeclaration, node)) return yield `This` - if (typeNames.has(name)) return yield `${name}${args}` + if (FindTypeName(node.getSourceFile(), name) && args.length === 0 /** non-resolvable */) { + return yield `${name}${args}` + } if (name in globalThis) return yield `Type.Never()` return yield `${name}${args}` } @@ -453,6 +488,7 @@ export namespace TypeScriptToTypeBox { if (ts.isIdentifier(node)) return yield node.getText() if (ts.isIntersectionTypeNode(node)) return yield* IntersectionTypeNode(node) if (ts.isUnionTypeNode(node)) return yield* UnionTypeNode(node) + if (ts.isMappedTypeNode(node)) return yield* MappedTypeNode(node) if (ts.isMethodSignature(node)) return yield* MethodSignature(node) if (ts.isModuleBlock(node)) return yield* ModuleBlock(node) if (ts.isParameter(node)) return yield* Parameter(node) @@ -464,6 +500,7 @@ export namespace TypeScriptToTypeBox { if (ts.isTemplateHead(node)) return yield* TemplateHead(node) if (ts.isTemplateMiddle(node)) return yield* TemplateMiddle(node) if (ts.isTemplateTail(node)) return yield* TemplateTail(node) + if (ts.isThisTypeNode(node)) return yield* ThisTypeNode(node) if (ts.isTypeAliasDeclaration(node)) return yield* TypeAliasDeclaration(node) if (ts.isTypeLiteralNode(node)) return yield* TypeLiteralNode(node) if (ts.isTypeOperatorNode(node)) return yield* TypeOperatorNode(node) @@ -492,20 +529,64 @@ export namespace TypeScriptToTypeBox { } console.warn('Unhandled:', ts.SyntaxKind[node.kind], node.getText()) } - export function ImportStatement(): string { + function ImportStatement(): string { if (!(useImports && useTypeBoxImport)) return '' - const imported = ['Type', 'Static'] - if (useGenerics) imported.push('TSchema') - if (useOptions) imported.push('SchemaOptions') - if (useTypeClone) imported.push('TypeClone') - return `import { ${imported.join(', ')} } from '@sinclair/typebox'` + const set = new Set(['Type', 'Static']) + if (useGenerics) { + set.add('TSchema') + } + if (useOptions) { + set.add('SchemaOptions') + } + if (useTypeClone) { + set.add('TypeClone') + } + if (useMapped) { + set.add('TemplateLiteralFinite') + set.add('TemplateLiteralParser') + set.add('TemplateLiteralGenerator') + set.add('TTemplateLiteral') + set.add('TPropertyKey') + set.add('TypeGuard') + set.add('TSchema') + set.add('TString') + set.add('TNumber') + set.add('TUnion') + set.add('TLiteral') + } + const imports = [...set].join(', ') + return `import { ${imports} } from '@sinclair/typebox'` + } + function MappedSupport() { + return useMapped + ? [ + 'type MappedContraintKey = TNumber | TString | TLiteral', + 'type MappedConstraint = TTemplateLiteral | TUnion | MappedContraintKey', + 'type MappedFunction = (C: C) => S', + '// prettier-ignore', + 'function Mapped>(C: C, F: F) {', + ' return (', + ' TypeGuard.TTemplateLiteral(C) ? (() => {', + ' const E = TemplateLiteralParser.ParseExact(C.pattern)', + ' const K = TemplateLiteralFinite.Check(E) ? [...TemplateLiteralGenerator.Generate(E)] : []', + ' return Type.Object(K.reduce((A, K) => ({ ...A, [K]: F(Type.Literal(K) as any)}), {}))', + ' })() :', + ' TypeGuard.TUnion(C) ? Type.Object(C.anyOf.reduce((A, K) => ({ ...A, [K.const]: F(K as any)}), {})) : ', + ' TypeGuard.TString(C) ? Type.Record(C, F(C)) : ', + ' TypeGuard.TNumber(C) ? Type.Record(C, F(C)) : ', + ' Type.Object({ [C.const]: F(C) })', + ' )', + '}', + ].join('\n') + : '' } /** Generates TypeBox types from TypeScript interface and type definitions */ export function Generate(typescriptCode: string, options?: TypeScriptToTypeBoxOptions) { useExportsEverything = options?.useExportEverything ?? false useIdentifiers = options?.useIdentifiers ?? false useTypeBoxImport = options?.useTypeBoxImport ?? true - typeNames.clear() + typenames.clear() + useMapped = false useImports = false useOptions = false useGenerics = false @@ -514,7 +595,8 @@ export namespace TypeScriptToTypeBox { const source = ts.createSourceFile('types.ts', typescriptCode, ts.ScriptTarget.ESNext, true) const declarations = [...Visit(source)].join('\n\n') const imports = ImportStatement() - const typescript = [imports, '', declarations].join('\n') + const mapped = MappedSupport() + const typescript = [imports, '', mapped, '', declarations].join('\n') const assertion = ts.transpileModule(typescript, transpilerOptions) if (assertion.diagnostics && assertion.diagnostics.length > 0) { throw new TypeScriptToTypeBoxError(assertion.diagnostics)