|
| 1 | +import { zodToTs } from "zod-to-ts"; |
| 2 | +import ts from "typescript"; |
| 3 | +import prettier from "prettier"; |
| 4 | + |
| 5 | +// Helper to print TypeScript node as string |
| 6 | +function printNode(node: ts.Node): string { |
| 7 | + const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); |
| 8 | + const sourceFile = ts.createSourceFile( |
| 9 | + "temp.ts", |
| 10 | + "", |
| 11 | + ts.ScriptTarget.Latest, |
| 12 | + false, |
| 13 | + ts.ScriptKind.TS |
| 14 | + ); |
| 15 | + return printer.printNode(ts.EmitHint.Unspecified, node, sourceFile); |
| 16 | +} |
| 17 | + |
| 18 | +// Import all schemas |
| 19 | +import { |
| 20 | + ResourceSchema, |
| 21 | + TimestampsSchema, |
| 22 | +} from "../src/types/schemas/base.schema"; |
| 23 | + |
| 24 | +import { LoginResponseSchema } from "../src/types/schemas/auth.schema"; |
| 25 | + |
| 26 | +import { |
| 27 | + SelfSchema, |
| 28 | + ResourceLimitsSchema, |
| 29 | +} from "../src/types/schemas/user.schema"; |
| 30 | + |
| 31 | +import { |
| 32 | + EnvironmentSchema, |
| 33 | + EnvironmentCreateInputSchema, |
| 34 | + EnvironmentUpdateInputSchema, |
| 35 | + EnvironmentValueSchema, |
| 36 | + EnvironmentValueUpdateInputSchema, |
| 37 | +} from "../src/types/schemas/environment.schema"; |
| 38 | + |
| 39 | +import { |
| 40 | + CronSchema, |
| 41 | + CronCreateInputSchema, |
| 42 | + CronUpdateInputSchema, |
| 43 | +} from "../src/types/schemas/cron.schema"; |
| 44 | + |
| 45 | +import { |
| 46 | + DomainSchema, |
| 47 | + DomainCreateInputSchema, |
| 48 | +} from "../src/types/schemas/domain.schema"; |
| 49 | + |
| 50 | +import { |
| 51 | + WorkerSchema, |
| 52 | + WorkerCreateInputSchema, |
| 53 | + WorkerUpdateInputSchema, |
| 54 | + WorkerLanguageSchema, |
| 55 | +} from "../src/types/schemas/worker.schema"; |
| 56 | + |
| 57 | +// Schema definitions to generate |
| 58 | +const schemas = [ |
| 59 | + // Base schemas |
| 60 | + { schema: ResourceSchema, name: "Resource" }, |
| 61 | + { schema: TimestampsSchema, name: "Timestamps" }, |
| 62 | + |
| 63 | + // Auth |
| 64 | + { schema: LoginResponseSchema, name: "LoginResponse" }, |
| 65 | + |
| 66 | + // User |
| 67 | + { schema: SelfSchema, name: "Self" }, |
| 68 | + { schema: ResourceLimitsSchema, name: "ResourceLimits" }, |
| 69 | + |
| 70 | + // Environment |
| 71 | + { schema: EnvironmentSchema, name: "Environment" }, |
| 72 | + { schema: EnvironmentCreateInputSchema, name: "EnvironmentCreateInput" }, |
| 73 | + { schema: EnvironmentUpdateInputSchema, name: "EnvironmentUpdateInput" }, |
| 74 | + { schema: EnvironmentValueSchema, name: "EnvironmentValue" }, |
| 75 | + { |
| 76 | + schema: EnvironmentValueUpdateInputSchema, |
| 77 | + name: "EnvironmentValueUpdateInput", |
| 78 | + }, |
| 79 | + |
| 80 | + // Cron |
| 81 | + { schema: CronSchema, name: "Cron" }, |
| 82 | + { schema: CronCreateInputSchema, name: "CronCreateInput" }, |
| 83 | + { schema: CronUpdateInputSchema, name: "CronUpdateInput" }, |
| 84 | + |
| 85 | + // Domain |
| 86 | + { schema: DomainSchema, name: "Domain" }, |
| 87 | + { schema: DomainCreateInputSchema, name: "DomainCreateInput" }, |
| 88 | + |
| 89 | + // Worker |
| 90 | + { schema: WorkerSchema, name: "Worker" }, |
| 91 | + { schema: WorkerCreateInputSchema, name: "WorkerCreateInput" }, |
| 92 | + { schema: WorkerUpdateInputSchema, name: "WorkerUpdateInput" }, |
| 93 | + { schema: WorkerLanguageSchema, name: "WorkerLanguage" }, |
| 94 | +]; |
| 95 | + |
| 96 | +async function generateTypes() { |
| 97 | + console.log("🔄 Generating TypeScript types from Zod schemas..."); |
| 98 | + |
| 99 | + // Clean dist directory |
| 100 | + const distDir = `${import.meta.dir}/../dist`; |
| 101 | + console.log(" 🧹 Cleaning dist directory..."); |
| 102 | + try { |
| 103 | + await Bun.$`rm -rf ${distDir}`; |
| 104 | + } catch (error) { |
| 105 | + // Ignore error if dist doesn't exist |
| 106 | + } |
| 107 | + |
| 108 | + // FIRST PASS: Generate all types and build a map |
| 109 | + const typesMap = new Map<string, string>(); |
| 110 | + let idCounter = 0; |
| 111 | + const auxiliaryTypeStore = { |
| 112 | + nextId: () => `T${idCounter++}`, |
| 113 | + definitions: new Map(), |
| 114 | + }; |
| 115 | + |
| 116 | + console.log(" 📋 First pass: building types map..."); |
| 117 | + for (const { schema, name } of schemas) { |
| 118 | + try { |
| 119 | + const { node } = zodToTs(schema, { auxiliaryTypeStore }); |
| 120 | + let typeStr = printNode(node); |
| 121 | + // Normalize whitespace for better matching |
| 122 | + typeStr = typeStr.replace(/\s+/g, " ").trim(); |
| 123 | + typesMap.set(typeStr, name); |
| 124 | + } catch (error) { |
| 125 | + console.error(`❌ Failed to generate type for ${name}:`, error); |
| 126 | + } |
| 127 | + } |
| 128 | + |
| 129 | + // SECOND PASS: Replace nested types with references |
| 130 | + console.log(" 🔄 Second pass: replacing nested types..."); |
| 131 | + const finalTypesMap = new Map<string, string>(); |
| 132 | + |
| 133 | + // Sort types by length (longest first) for better matching |
| 134 | + const sortedTypes = Array.from(typesMap.entries()).sort( |
| 135 | + ([a], [b]) => b.length - a.length |
| 136 | + ); |
| 137 | + |
| 138 | + for (const [typeStr, name] of typesMap) { |
| 139 | + let updatedTypeStr = typeStr; |
| 140 | + |
| 141 | + // Replace all occurrences of other types with their names (with I prefix) |
| 142 | + for (const [otherTypeStr, otherName] of sortedTypes) { |
| 143 | + if (otherName !== name && updatedTypeStr.includes(otherTypeStr)) { |
| 144 | + // Use a more precise replacement to avoid partial matches |
| 145 | + updatedTypeStr = updatedTypeStr.replaceAll( |
| 146 | + otherTypeStr, |
| 147 | + `I${otherName}` |
| 148 | + ); |
| 149 | + } |
| 150 | + } |
| 151 | + |
| 152 | + finalTypesMap.set(name, updatedTypeStr); |
| 153 | + } |
| 154 | + |
| 155 | + // THIRD PASS: Recursive replacement until no more changes |
| 156 | + console.log(" 🔁 Third pass: recursive replacement..."); |
| 157 | + let changed = true; |
| 158 | + let iterations = 0; |
| 159 | + const maxIterations = 10; |
| 160 | + |
| 161 | + while (changed && iterations < maxIterations) { |
| 162 | + changed = false; |
| 163 | + iterations++; |
| 164 | + |
| 165 | + for (const [name, typeStr] of finalTypesMap) { |
| 166 | + let updatedTypeStr = typeStr; |
| 167 | + |
| 168 | + for (const [otherTypeStr, otherName] of sortedTypes) { |
| 169 | + if (otherName !== name && updatedTypeStr.includes(otherTypeStr)) { |
| 170 | + const newStr = updatedTypeStr.replaceAll( |
| 171 | + otherTypeStr, |
| 172 | + `I${otherName}` |
| 173 | + ); |
| 174 | + if (newStr !== updatedTypeStr) { |
| 175 | + updatedTypeStr = newStr; |
| 176 | + changed = true; |
| 177 | + } |
| 178 | + } |
| 179 | + } |
| 180 | + |
| 181 | + if (updatedTypeStr !== typeStr) { |
| 182 | + finalTypesMap.set(name, updatedTypeStr); |
| 183 | + } |
| 184 | + } |
| 185 | + } |
| 186 | + |
| 187 | + console.log(` ✓ Completed in ${iterations} iteration(s)`); |
| 188 | + |
| 189 | + // Generate output |
| 190 | + let output = `/** |
| 191 | + * Auto-generated TypeScript types from Zod schemas |
| 192 | + * DO NOT EDIT THIS FILE MANUALLY |
| 193 | + * Generated on: ${new Date().toISOString()} |
| 194 | + */ |
| 195 | +
|
| 196 | +`; |
| 197 | + |
| 198 | + // Add global types |
| 199 | + output += `// Global types |
| 200 | +export type hex = string; |
| 201 | +export type uuid = string; |
| 202 | +export type timestamp = number; |
| 203 | +export type Dictionary<T> = Record<string, T>; |
| 204 | +
|
| 205 | +`; |
| 206 | + |
| 207 | + // Add all final types with "I" prefix |
| 208 | + for (const { name } of schemas) { |
| 209 | + const typeStr = finalTypesMap.get(name); |
| 210 | + if (typeStr) { |
| 211 | + output += `export type I${name} = ${typeStr};\n\n`; |
| 212 | + } |
| 213 | + } |
| 214 | + |
| 215 | + // Format with prettier |
| 216 | + console.log(" 🎨 Formatting with prettier..."); |
| 217 | + const formatted = await prettier.format(output, { |
| 218 | + parser: "typescript", |
| 219 | + semi: true, |
| 220 | + singleQuote: true, |
| 221 | + trailingComma: "all", |
| 222 | + }); |
| 223 | + |
| 224 | + // Write types file |
| 225 | + await Bun.write(`${distDir}/types.d.ts`, formatted); |
| 226 | + |
| 227 | + // Generate package.json for npm publish |
| 228 | + const packageJson = { |
| 229 | + name: "@openworkers/api-types", |
| 230 | + version: "1.0.0", |
| 231 | + license: "MIT", |
| 232 | + type: "module", |
| 233 | + private: false, |
| 234 | + main: "./types.d.ts", |
| 235 | + types: "./types.d.ts", |
| 236 | + exports: { |
| 237 | + ".": "./types.d.ts", |
| 238 | + }, |
| 239 | + description: "TypeScript types for OpenWorkers API", |
| 240 | + keywords: ["openworkers", "types", "typescript"], |
| 241 | + repository: { |
| 242 | + type: "git", |
| 243 | + url: "https://github.com/openworkers/openworkers-api", |
| 244 | + }, |
| 245 | + publishConfig: { |
| 246 | + access: "public", |
| 247 | + }, |
| 248 | + }; |
| 249 | + |
| 250 | + await Bun.write( |
| 251 | + `${distDir}/package.json`, |
| 252 | + JSON.stringify(packageJson, null, 2) |
| 253 | + ); |
| 254 | + |
| 255 | + // Copy LICENSE |
| 256 | + const license = await Bun.file(`${import.meta.dir}/../LICENSE`).text(); |
| 257 | + await Bun.write(`${distDir}/LICENSE`, license); |
| 258 | + |
| 259 | + console.log(`✅ Types package generated successfully`); |
| 260 | + console.log(` 📦 Run "cd dist && npm publish" to publish`); |
| 261 | +} |
| 262 | + |
| 263 | +generateTypes().catch(console.error); |
0 commit comments