prototypey.org - atproto lexicon typescript toolkit - mirror https://github.com/tylersayshi/prototypey

remove gen-inferred

this will be back later, but the initial take on it
is way wrong

Tyler 8ecbee41 995fae33

-1247
-1
package.json
··· 14 }, 15 "scripts": { 16 "build": "pnpm -r build", 17 - "codegen:samples": "node packages/cli/src/index.ts gen-inferred ./generated/inferred './samples/*.json'", 18 "format": "prettier . --list-different", 19 "format:fix": "prettier . --write", 20 "lint": "pnpm -r lint",
··· 14 }, 15 "scripts": { 16 "build": "pnpm -r build", 17 "format": "prettier . --list-different", 18 "format:fix": "prettier . --write", 19 "lint": "pnpm -r lint",
-32
packages/cli/README.md
··· 16 17 ## Commands 18 19 - ### `gen-inferred` 20 - 21 - Generate type-inferred TypeScript code from JSON lexicon schemas. 22 - 23 - **Usage:** 24 - 25 - ```bash 26 - prototypey gen-inferred <outdir> <schemas...> 27 - ``` 28 - 29 - **Arguments:** 30 - 31 - - `outdir` - Output directory for generated TypeScript files 32 - - `schemas...` - One or more glob patterns matching lexicon JSON schema files 33 - 34 - **Example:** 35 - 36 - ```bash 37 - prototypey gen-inferred ./generated/inferred ./lexicons/**/*.json 38 - ``` 39 - 40 - **What it does:** 41 - 42 - - Reads ATProto lexicon JSON schemas 43 - - Generates TypeScript types that match the schema structure 44 - - Organizes output files by namespace (e.g., `app.bsky.feed.post` → `app/bsky/feed/post.ts`) 45 - - Provides typescript definitions of the lexicons for inference and later: validation 46 - 47 ### `gen-emit` 48 49 Emit JSON lexicon schemas from authored TypeScript files. ··· 78 79 1. **Author lexicons in TypeScript** using the `prototypey` library 80 2. **Emit to JSON** with `gen-emit` for runtime validation and API contracts 81 - 3. **Generate inferred types** with `gen-inferred` for consuming code 82 83 ```bash 84 # Write your lexicons in TypeScript ··· 86 87 # Emit JSON schemas 88 prototypey gen-emit ./schemas ./src/lexicons/**/*.ts 89 - 90 - # Generate TypeScript types from schemas 91 - prototypey gen-inferred ./generated ./schemas/**/*.json 92 ``` 93 94 ## Requirements
··· 16 17 ## Commands 18 19 ### `gen-emit` 20 21 Emit JSON lexicon schemas from authored TypeScript files. ··· 50 51 1. **Author lexicons in TypeScript** using the `prototypey` library 52 2. **Emit to JSON** with `gen-emit` for runtime validation and API contracts 53 54 ```bash 55 # Write your lexicons in TypeScript ··· 57 58 # Emit JSON schemas 59 prototypey gen-emit ./schemas ./src/lexicons/**/*.ts 60 ``` 61 62 ## Requirements
-71
packages/cli/src/commands/gen-inferred.ts
··· 1 - import { glob } from "tinyglobby"; 2 - import { readFile, mkdir, writeFile } from "node:fs/promises"; 3 - import { join, dirname, relative, parse } from "node:path"; 4 - import { generateInferredCode } from "../templates/inferred.ts"; 5 - 6 - interface LexiconSchema { 7 - lexicon: number; 8 - id: string; 9 - defs: Record<string, unknown>; 10 - } 11 - 12 - export async function genInferred( 13 - outdir: string, 14 - schemas: string | string[], 15 - ): Promise<void> { 16 - try { 17 - const schemaPatterns = Array.isArray(schemas) ? schemas : [schemas]; 18 - 19 - // Find all schema files matching the patterns 20 - const schemaFiles = await glob(schemaPatterns, { 21 - absolute: true, 22 - onlyFiles: true, 23 - }); 24 - 25 - if (schemaFiles.length === 0) { 26 - console.log("No schema files found matching patterns:", schemaPatterns); 27 - return; 28 - } 29 - 30 - console.log(`Found ${schemaFiles.length} schema file(s)`); 31 - 32 - // Process each schema file 33 - for (const schemaPath of schemaFiles) { 34 - await processSchema(schemaPath, outdir); 35 - } 36 - 37 - console.log(`\nGenerated inferred types in ${outdir}`); 38 - } catch (error) { 39 - console.error("Error generating inferred types:", error); 40 - process.exit(1); 41 - } 42 - } 43 - 44 - async function processSchema( 45 - schemaPath: string, 46 - outdir: string, 47 - ): Promise<void> { 48 - const content = await readFile(schemaPath, "utf-8"); 49 - const schema: LexiconSchema = JSON.parse(content); 50 - 51 - if (!schema.id || !schema.defs) { 52 - console.warn(`Skipping ${schemaPath}: Missing id or defs`); 53 - return; 54 - } 55 - 56 - // Convert NSID to file path: app.bsky.feed.post -> app/bsky/feed/post.ts 57 - const nsidParts = schema.id.split("."); 58 - const relativePath = join(...nsidParts) + ".ts"; 59 - const outputPath = join(outdir, relativePath); 60 - 61 - // Create directory structure 62 - await mkdir(dirname(outputPath), { recursive: true }); 63 - 64 - // Generate the TypeScript code 65 - const code = generateInferredCode(schema, schemaPath, outdir); 66 - 67 - // Write the file 68 - await writeFile(outputPath, code, "utf-8"); 69 - 70 - console.log(` ✓ ${schema.id} -> ${relativePath}`); 71 - }
···
-7
packages/cli/src/index.ts
··· 1 import { readFile } from "node:fs/promises"; 2 import sade from "sade"; 3 - import { genInferred } from "./commands/gen-inferred.ts"; 4 import { genEmit } from "./commands/gen-emit.ts"; 5 6 const pkg = JSON.parse( ··· 10 const prog = sade("prototypey"); 11 12 prog.version(pkg.version).describe("atproto lexicon typescript toolkit"); 13 - 14 - prog 15 - .command("gen-inferred <outdir> <schemas...>") 16 - .describe("Generate type-inferred code from lexicon schemas") 17 - .example("gen-inferred ./generated/inferred ./lexicons/**/*.json") 18 - .action(genInferred); 19 20 prog 21 .command("gen-emit <outdir> <sources...>")
··· 1 import { readFile } from "node:fs/promises"; 2 import sade from "sade"; 3 import { genEmit } from "./commands/gen-emit.ts"; 4 5 const pkg = JSON.parse( ··· 9 const prog = sade("prototypey"); 10 11 prog.version(pkg.version).describe("atproto lexicon typescript toolkit"); 12 13 prog 14 .command("gen-emit <outdir> <sources...>")
-65
packages/cli/src/templates/inferred.ts
··· 1 - import { relative, dirname } from "node:path"; 2 - 3 - interface LexiconSchema { 4 - lexicon: number; 5 - id: string; 6 - defs: Record<string, unknown>; 7 - } 8 - 9 - export function generateInferredCode( 10 - schema: LexiconSchema, 11 - schemaPath: string, 12 - outdir: string, 13 - ): string { 14 - const { id } = schema; 15 - 16 - // Calculate relative import path from output file to schema file 17 - // We need to go from generated/{nsid}.ts to the original schema 18 - const nsidParts = id.split("."); 19 - const outputDir = dirname([outdir, ...nsidParts].join("/")); 20 - const relativeSchemaPath = relative(outputDir, schemaPath); 21 - 22 - // Generate a clean type name from the NSID 23 - const typeName = generateTypeName(id); 24 - 25 - return `// Generated by prototypey - DO NOT EDIT 26 - // Source: ${id} 27 - import type { Infer } from "prototypey"; 28 - import schema from "${relativeSchemaPath}" with { type: "json" }; 29 - 30 - /** 31 - * Type-inferred from lexicon schema: ${id} 32 - */ 33 - export type ${typeName} = Infer<typeof schema>; 34 - 35 - /** 36 - * The lexicon schema object 37 - */ 38 - export const ${typeName}Schema = schema; 39 - 40 - /** 41 - * Type guard to check if a value is a ${typeName} 42 - */ 43 - export function is${typeName}(v: unknown): v is ${typeName} { 44 - return ( 45 - typeof v === "object" && 46 - v !== null && 47 - "$type" in v && 48 - v.$type === "${id}" 49 - ); 50 - } 51 - `; 52 - } 53 - 54 - function generateTypeName(nsid: string): string { 55 - // Convert app.bsky.feed.post -> Post 56 - // Convert com.atproto.repo.createRecord -> CreateRecord 57 - const parts = nsid.split("."); 58 - const lastPart = parts[parts.length - 1]; 59 - 60 - // Convert kebab-case or camelCase to PascalCase 61 - return lastPart 62 - .split(/[-_]/) 63 - .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) 64 - .join(""); 65 - }
···
-369
packages/cli/tests/commands/gen-inferred.test.ts
··· 1 - import { expect, test, describe, beforeEach, afterEach } from "vitest"; 2 - import { mkdir, writeFile, rm, readFile } from "node:fs/promises"; 3 - import { join } from "node:path"; 4 - import { genInferred } from "../../src/commands/gen-inferred.ts"; 5 - import { tmpdir } from "node:os"; 6 - 7 - describe("genInferred", () => { 8 - let testDir: string; 9 - let outDir: string; 10 - let schemasDir: string; 11 - 12 - beforeEach(async () => { 13 - // Create a temporary directory for test files 14 - testDir = join(tmpdir(), `prototypey-inferred-test-${Date.now()}`); 15 - outDir = join(testDir, "output"); 16 - schemasDir = join(testDir, "schemas"); 17 - await mkdir(testDir, { recursive: true }); 18 - await mkdir(outDir, { recursive: true }); 19 - await mkdir(schemasDir, { recursive: true }); 20 - }); 21 - 22 - afterEach(async () => { 23 - // Clean up test directory 24 - await rm(testDir, { recursive: true, force: true }); 25 - }); 26 - 27 - test("generates inferred types from a simple schema", async () => { 28 - // Create a test schema file 29 - const schemaFile = join(schemasDir, "app.bsky.actor.profile.json"); 30 - await writeFile( 31 - schemaFile, 32 - JSON.stringify( 33 - { 34 - lexicon: 1, 35 - id: "app.bsky.actor.profile", 36 - defs: { 37 - main: { 38 - type: "record", 39 - key: "self", 40 - record: { 41 - type: "object", 42 - properties: { 43 - displayName: { 44 - type: "string", 45 - maxLength: 64, 46 - maxGraphemes: 64, 47 - }, 48 - description: { 49 - type: "string", 50 - maxLength: 256, 51 - maxGraphemes: 256, 52 - }, 53 - }, 54 - }, 55 - }, 56 - }, 57 - }, 58 - null, 59 - "\t", 60 - ), 61 - ); 62 - 63 - // Run the inferred command 64 - await genInferred(outDir, schemaFile); 65 - 66 - // Read the generated TypeScript file 67 - const outputFile = join(outDir, "app/bsky/actor/profile.ts"); 68 - const content = await readFile(outputFile, "utf-8"); 69 - 70 - // Verify the generated code structure 71 - expect(content).toContain("// Generated by prototypey - DO NOT EDIT"); 72 - expect(content).toContain("// Source: app.bsky.actor.profile"); 73 - expect(content).toContain('import type { Infer } from "prototypey"'); 74 - expect(content).toContain('with { type: "json" }'); 75 - expect(content).toContain("export type Profile = Infer<typeof schema>"); 76 - expect(content).toContain("export const ProfileSchema = schema"); 77 - expect(content).toContain( 78 - "export function isProfile(v: unknown): v is Profile", 79 - ); 80 - expect(content).toContain('v.$type === "app.bsky.actor.profile"'); 81 - }); 82 - 83 - test("generates correct directory structure from NSID", async () => { 84 - // Create a test schema with nested NSID 85 - const schemaFile = join(schemasDir, "app.bsky.feed.post.json"); 86 - await writeFile( 87 - schemaFile, 88 - JSON.stringify({ 89 - lexicon: 1, 90 - id: "app.bsky.feed.post", 91 - defs: { 92 - main: { 93 - type: "record", 94 - key: "tid", 95 - record: { 96 - type: "object", 97 - properties: { 98 - text: { type: "string" }, 99 - }, 100 - }, 101 - }, 102 - }, 103 - }), 104 - ); 105 - 106 - await genInferred(outDir, schemaFile); 107 - 108 - // Verify the directory structure matches NSID 109 - const outputFile = join(outDir, "app/bsky/feed/post.ts"); 110 - const content = await readFile(outputFile, "utf-8"); 111 - 112 - expect(content).toBeTruthy(); 113 - expect(content).toContain("export type Post = Infer<typeof schema>"); 114 - }); 115 - 116 - test("handles multiple schema files with glob patterns", async () => { 117 - // Create multiple schema files 118 - await writeFile( 119 - join(schemasDir, "app.bsky.actor.profile.json"), 120 - JSON.stringify({ 121 - lexicon: 1, 122 - id: "app.bsky.actor.profile", 123 - defs: { main: { type: "record" } }, 124 - }), 125 - ); 126 - 127 - await writeFile( 128 - join(schemasDir, "app.bsky.feed.post.json"), 129 - JSON.stringify({ 130 - lexicon: 1, 131 - id: "app.bsky.feed.post", 132 - defs: { main: { type: "record" } }, 133 - }), 134 - ); 135 - 136 - // Run with glob pattern 137 - await genInferred(outDir, `${schemasDir}/*.json`); 138 - 139 - // Verify both files were created 140 - const profileContent = await readFile( 141 - join(outDir, "app/bsky/actor/profile.ts"), 142 - "utf-8", 143 - ); 144 - const postContent = await readFile( 145 - join(outDir, "app/bsky/feed/post.ts"), 146 - "utf-8", 147 - ); 148 - 149 - expect(profileContent).toContain("export type Profile"); 150 - expect(postContent).toContain("export type Post"); 151 - }); 152 - 153 - test("generates correct relative import path", async () => { 154 - // Create a deeply nested schema 155 - const schemaFile = join(schemasDir, "com.atproto.repo.createRecord.json"); 156 - await writeFile( 157 - schemaFile, 158 - JSON.stringify({ 159 - lexicon: 1, 160 - id: "com.atproto.repo.createRecord", 161 - defs: { 162 - main: { 163 - type: "procedure", 164 - input: { encoding: "application/json" }, 165 - }, 166 - }, 167 - }), 168 - ); 169 - 170 - await genInferred(outDir, schemaFile); 171 - 172 - // Read generated file and check the import path is relative 173 - const outputFile = join(outDir, "com/atproto/repo/createRecord.ts"); 174 - const content = await readFile(outputFile, "utf-8"); 175 - 176 - // The import should be relative to the generated file location 177 - expect(content).toContain('import schema from "'); 178 - expect(content).toContain('.json" with { type: "json" }'); 179 - // Should navigate up from com/atproto/repo/ to schemas/ 180 - expect(content).toMatch(/import schema from ".*createRecord\.json"/); 181 - }); 182 - 183 - test("generates proper type name from NSID", async () => { 184 - // Test various NSID formats 185 - const testCases = [ 186 - { id: "app.bsky.feed.post", expectedType: "Post" }, 187 - { id: "com.atproto.repo.createRecord", expectedType: "CreateRecord" }, 188 - { id: "app.bsky.actor.profile", expectedType: "Profile" }, 189 - { 190 - id: "app.bsky.feed.searchPosts", 191 - expectedType: "SearchPosts", 192 - }, 193 - ]; 194 - 195 - for (const { id, expectedType } of testCases) { 196 - const schemaFile = join(schemasDir, `${id}.json`); 197 - await writeFile( 198 - schemaFile, 199 - JSON.stringify({ 200 - lexicon: 1, 201 - id, 202 - defs: { main: { type: "record" } }, 203 - }), 204 - ); 205 - 206 - const testOutDir = join(testDir, `out-${id}`); 207 - await mkdir(testOutDir, { recursive: true }); 208 - await genInferred(testOutDir, schemaFile); 209 - 210 - const nsidParts = id.split("."); 211 - const outputFile = join(testOutDir, ...nsidParts) + ".ts"; 212 - const content = await readFile(outputFile, "utf-8"); 213 - 214 - expect(content).toContain(`export type ${expectedType}`); 215 - expect(content).toContain(`export const ${expectedType}Schema`); 216 - expect(content).toContain(`export function is${expectedType}`); 217 - } 218 - }); 219 - 220 - test("handles schema without id gracefully", async () => { 221 - // Create an invalid schema without id 222 - const schemaFile = join(schemasDir, "invalid.json"); 223 - await writeFile( 224 - schemaFile, 225 - JSON.stringify({ 226 - lexicon: 1, 227 - defs: { main: { type: "record" } }, 228 - }), 229 - ); 230 - 231 - // Should not throw, but should skip the file 232 - await expect(genInferred(outDir, schemaFile)).resolves.not.toThrow(); 233 - 234 - // Output directory should be empty or not contain generated files 235 - const files = await readFile(outDir, "utf-8").catch(() => null); 236 - expect(files).toBeNull(); 237 - }); 238 - 239 - test("handles schema without defs gracefully", async () => { 240 - // Create an invalid schema without defs 241 - const schemaFile = join(schemasDir, "invalid2.json"); 242 - await writeFile( 243 - schemaFile, 244 - JSON.stringify({ 245 - lexicon: 1, 246 - id: "app.test.invalid", 247 - }), 248 - ); 249 - 250 - // Should not throw, but should skip the file 251 - await expect(genInferred(outDir, schemaFile)).resolves.not.toThrow(); 252 - }); 253 - 254 - test("processes array of schema patterns", async () => { 255 - // Create schemas in different directories 256 - const schemasDir1 = join(testDir, "schemas1"); 257 - const schemasDir2 = join(testDir, "schemas2"); 258 - await mkdir(schemasDir1, { recursive: true }); 259 - await mkdir(schemasDir2, { recursive: true }); 260 - 261 - await writeFile( 262 - join(schemasDir1, "app.one.json"), 263 - JSON.stringify({ 264 - lexicon: 1, 265 - id: "app.one", 266 - defs: { main: { type: "record" } }, 267 - }), 268 - ); 269 - 270 - await writeFile( 271 - join(schemasDir2, "app.two.json"), 272 - JSON.stringify({ 273 - lexicon: 1, 274 - id: "app.two", 275 - defs: { main: { type: "record" } }, 276 - }), 277 - ); 278 - 279 - // Run with array of patterns 280 - await genInferred(outDir, [ 281 - `${schemasDir1}/*.json`, 282 - `${schemasDir2}/*.json`, 283 - ]); 284 - 285 - // Verify both were generated 286 - const oneContent = await readFile(join(outDir, "app/one.ts"), "utf-8"); 287 - const twoContent = await readFile(join(outDir, "app/two.ts"), "utf-8"); 288 - 289 - expect(oneContent).toContain("export type One"); 290 - expect(twoContent).toContain("export type Two"); 291 - }); 292 - 293 - test("generates code with all required components", async () => { 294 - // Create a comprehensive schema 295 - const schemaFile = join(schemasDir, "app.test.complete.json"); 296 - await writeFile( 297 - schemaFile, 298 - JSON.stringify({ 299 - lexicon: 1, 300 - id: "app.test.complete", 301 - defs: { 302 - main: { 303 - type: "record", 304 - key: "tid", 305 - record: { 306 - type: "object", 307 - required: ["text"], 308 - properties: { 309 - text: { type: "string", maxLength: 300 }, 310 - tags: { type: "array", items: { type: "string" } }, 311 - }, 312 - }, 313 - }, 314 - }, 315 - }), 316 - ); 317 - 318 - await genInferred(outDir, schemaFile); 319 - 320 - const outputFile = join(outDir, "app/test/complete.ts"); 321 - const content = await readFile(outputFile, "utf-8"); 322 - 323 - // Check all required exports 324 - expect(content).toContain('import type { Infer } from "prototypey"'); 325 - expect(content).toContain("export type Complete = Infer<typeof schema>"); 326 - expect(content).toContain("export const CompleteSchema = schema"); 327 - expect(content).toContain( 328 - "export function isComplete(v: unknown): v is Complete", 329 - ); 330 - 331 - // Check type guard implementation 332 - expect(content).toContain('typeof v === "object"'); 333 - expect(content).toContain("v !== null"); 334 - expect(content).toContain('"$type" in v'); 335 - expect(content).toContain('v.$type === "app.test.complete"'); 336 - 337 - // Check comments 338 - expect(content).toContain("// Generated by prototypey - DO NOT EDIT"); 339 - expect(content).toContain("// Source: app.test.complete"); 340 - expect(content).toContain( 341 - "* Type-inferred from lexicon schema: app.test.complete", 342 - ); 343 - expect(content).toContain("* The lexicon schema object"); 344 - expect(content).toContain("* Type guard to check if a value is a Complete"); 345 - }); 346 - 347 - test("handles kebab-case and mixed-case NSID parts", async () => { 348 - // Test NSID with different casing 349 - const schemaFile = join(schemasDir, "app.test.myCustomType.json"); 350 - await writeFile( 351 - schemaFile, 352 - JSON.stringify({ 353 - lexicon: 1, 354 - id: "app.test.myCustomType", 355 - defs: { main: { type: "record" } }, 356 - }), 357 - ); 358 - 359 - await genInferred(outDir, schemaFile); 360 - 361 - const outputFile = join(outDir, "app/test/myCustomType.ts"); 362 - const content = await readFile(outputFile, "utf-8"); 363 - 364 - // Should convert to PascalCase 365 - expect(content).toContain("export type MyCustomType"); 366 - expect(content).toContain("export const MyCustomTypeSchema"); 367 - expect(content).toContain("export function isMyCustomType"); 368 - }); 369 - });
···
-18
packages/cli/tests/integration/cli.test.ts
··· 21 expect(stdout).toContain(`prototypey, ${pkg.version}`); 22 }); 23 24 - test("shows help for gen-inferred command", async () => { 25 - const { stdout, stderr } = await runCLI(["gen-inferred", "--help"]); 26 - expect(stderr).toBe(""); 27 - expect(stdout).toContain("gen-inferred <outdir> <schemas...>"); 28 - expect(stdout).toContain( 29 - "Generate type-inferred code from lexicon schemas", 30 - ); 31 - }); 32 - 33 test("shows help for gen-emit command", async () => { 34 const { stdout, stderr } = await runCLI(["gen-emit", "--help"]); 35 expect(stderr).toBe(""); ··· 44 expect(code).toBe(1); 45 expect(stderr).toContain("Invalid command: unknown-command"); 46 expect(stderr).toContain("Run `$ prototypey --help` for more info"); 47 - }); 48 - 49 - test("handles missing arguments", async () => { 50 - const { stdout, stderr, code } = await runCLI(["gen-inferred"]); 51 - expect(code).toBe(1); 52 - expect(stderr).toContain("Insufficient arguments!"); 53 - expect(stderr).toContain( 54 - "Run `$ prototypey gen-inferred --help` for more info", 55 - ); 56 }); 57 });
··· 21 expect(stdout).toContain(`prototypey, ${pkg.version}`); 22 }); 23 24 test("shows help for gen-emit command", async () => { 25 const { stdout, stderr } = await runCLI(["gen-emit", "--help"]); 26 expect(stderr).toBe(""); ··· 35 expect(code).toBe(1); 36 expect(stderr).toContain("Invalid command: unknown-command"); 37 expect(stderr).toContain("Run `$ prototypey --help` for more info"); 38 }); 39 });
-101
packages/cli/tests/integration/error-handling.test.ts
··· 24 await rm(testDir, { recursive: true, force: true }); 25 }); 26 27 - test("handles non-existent schema files gracefully", async () => { 28 - const { stdout, stderr, code } = await runCLI([ 29 - "gen-inferred", 30 - outDir, 31 - join(schemasDir, "non-existent.json"), 32 - ]); 33 - 34 - expect(code).toBe(0); // Should not crash 35 - expect(stdout).toContain("No schema files found matching patterns"); 36 - expect(stderr).toBe(""); 37 - }); 38 - 39 - test("handles invalid JSON schema files", async () => { 40 - // Create an invalid JSON file 41 - const invalidSchema = join(schemasDir, "invalid.json"); 42 - await writeFile(invalidSchema, "not valid json"); 43 - 44 - const { stdout, stderr, code } = await runCLI([ 45 - "gen-inferred", 46 - outDir, 47 - invalidSchema, 48 - ]); 49 - 50 - expect(code).toBe(1); // Should exit with error 51 - expect(stderr).toContain("Error generating inferred types"); 52 - }); 53 - 54 - test("handles schema files with missing id", async () => { 55 - // Create a schema with missing id 56 - const schemaFile = join(schemasDir, "missing-id.json"); 57 - await writeFile( 58 - schemaFile, 59 - JSON.stringify({ 60 - lexicon: 1, 61 - defs: { main: { type: "record" } }, 62 - }), 63 - ); 64 - 65 - const { stdout, stderr, code } = await runCLI([ 66 - "gen-inferred", 67 - outDir, 68 - schemaFile, 69 - ]); 70 - 71 - expect(code).toBe(0); // Should not crash 72 - expect(stdout).toContain("Found 1 schema file(s)"); 73 - expect(stdout).toContain("Generated inferred types in"); 74 - // Should skip the invalid file silently 75 - }); 76 - 77 - test("handles schema files with missing defs", async () => { 78 - // Create a schema with missing defs 79 - const schemaFile = join(schemasDir, "missing-defs.json"); 80 - await writeFile( 81 - schemaFile, 82 - JSON.stringify({ 83 - lexicon: 1, 84 - id: "app.test.missing", 85 - }), 86 - ); 87 - 88 - const { stdout, stderr, code } = await runCLI([ 89 - "gen-inferred", 90 - outDir, 91 - schemaFile, 92 - ]); 93 - 94 - expect(code).toBe(0); // Should not crash 95 - expect(stdout).toContain("Found 1 schema file(s)"); 96 - expect(stdout).toContain("Generated inferred types in"); 97 - // Should skip the invalid file silently 98 - }); 99 - 100 test("handles non-existent source files for gen-emit", async () => { 101 const { stdout, stderr, code } = await runCLI([ 102 "gen-emit", ··· 123 expect(code).toBe(0); // Should not crash 124 expect(stdout).toContain("Found 1 source file(s)"); 125 expect(stderr).toContain("No lexicons found"); 126 - }); 127 - 128 - test("handles permission errors when writing output", async () => { 129 - // This test might be platform-specific, so we'll make it lenient 130 - // Create a schema file first 131 - const schemaFile = join(schemasDir, "test.json"); 132 - await writeFile( 133 - schemaFile, 134 - JSON.stringify({ 135 - lexicon: 1, 136 - id: "app.test.permission", 137 - defs: { main: { type: "record" } }, 138 - }), 139 - ); 140 - 141 - // Try to write to a directory that might have permission issues 142 - // We'll use a path that likely won't exist and is invalid 143 - const invalidOutDir = "/invalid/path/that/does/not/exist"; 144 - 145 - const { stdout, stderr, code } = await runCLI([ 146 - "gen-inferred", 147 - invalidOutDir, 148 - schemaFile, 149 - ]); 150 - 151 - // Should handle the error gracefully 152 - expect(code).toBe(1); 153 - expect(stderr).toContain("Error generating inferred types"); 154 }); 155 });
··· 24 await rm(testDir, { recursive: true, force: true }); 25 }); 26 27 test("handles non-existent source files for gen-emit", async () => { 28 const { stdout, stderr, code } = await runCLI([ 29 "gen-emit", ··· 50 expect(code).toBe(0); // Should not crash 51 expect(stdout).toContain("Found 1 source file(s)"); 52 expect(stderr).toContain("No lexicons found"); 53 }); 54 });
-150
packages/cli/tests/integration/filesystem.test.ts
··· 33 // Ignore cleanup errors 34 } 35 }); 36 - 37 - test("creates nested output directories when they don't exist", async () => { 38 - // Create a schema file 39 - const schemaFile = join(schemasDir, "test.json"); 40 - await writeFile( 41 - schemaFile, 42 - JSON.stringify({ 43 - lexicon: 1, 44 - id: "app.deeply.nested.schema", 45 - defs: { main: { type: "record" } }, 46 - }), 47 - ); 48 - 49 - // Use a deeply nested output directory that doesn't exist 50 - const deepOutDir = join(outDir, "very", "deeply", "nested", "path"); 51 - 52 - const { stdout, stderr, code } = await runCLI([ 53 - "gen-inferred", 54 - deepOutDir, 55 - schemaFile, 56 - ]); 57 - 58 - expect(code).toBe(0); 59 - expect(stderr).toBe(""); 60 - expect(stdout).toContain( 61 - "app.deeply.nested.schema -> app/deeply/nested/schema.ts", 62 - ); 63 - 64 - // Verify the file was created in the deeply nested path 65 - const generatedFile = join(deepOutDir, "app/deeply/nested/schema.ts"); 66 - await access(generatedFile, constants.F_OK); 67 - }); 68 - 69 - test("handles special characters in NSID correctly", async () => { 70 - // Create a schema with special characters in the name 71 - const schemaFile = join(schemasDir, "special.json"); 72 - await writeFile( 73 - schemaFile, 74 - JSON.stringify({ 75 - lexicon: 1, 76 - id: "app.test.special-name_with.mixedChars123", 77 - defs: { main: { type: "record" } }, 78 - }), 79 - ); 80 - 81 - const { stdout, stderr, code } = await runCLI([ 82 - "gen-inferred", 83 - outDir, 84 - schemaFile, 85 - ]); 86 - 87 - expect(code).toBe(0); 88 - expect(stderr).toBe(""); 89 - // Should convert to proper PascalCase 90 - expect(stdout).toContain( 91 - "app.test.special-name_with.mixedChars123 -> app/test/special-name_with/mixedChars123.ts", 92 - ); 93 - }); 94 - 95 - test("handles very long NSID paths", async () => { 96 - // Create a schema with a very long NSID 97 - const longNSID = "com." + "verylongdomainname.".repeat(10) + "test"; 98 - const schemaFile = join(schemasDir, "long.json"); 99 - await writeFile( 100 - schemaFile, 101 - JSON.stringify({ 102 - lexicon: 1, 103 - id: longNSID, 104 - defs: { main: { type: "record" } }, 105 - }), 106 - ); 107 - 108 - const { stdout, stderr, code } = await runCLI([ 109 - "gen-inferred", 110 - outDir, 111 - schemaFile, 112 - ]); 113 - 114 - expect(code).toBe(0); 115 - expect(stderr).toBe(""); 116 - expect(stdout).toContain(`Found 1 schema file(s)`); 117 - }); 118 - 119 - test("handles existing files gracefully", async () => { 120 - // Create a schema file 121 - const schemaFile = join(schemasDir, "test.json"); 122 - await writeFile( 123 - schemaFile, 124 - JSON.stringify({ 125 - lexicon: 1, 126 - id: "app.test.overwrite", 127 - defs: { main: { type: "record" } }, 128 - }), 129 - ); 130 - 131 - // Run CLI once 132 - await runCLI(["gen-inferred", outDir, schemaFile]); 133 - 134 - // Verify file exists 135 - const generatedFile = join(outDir, "app/test/overwrite.ts"); 136 - await access(generatedFile, constants.F_OK); 137 - 138 - // Run CLI again (should overwrite) 139 - const { stdout, stderr, code } = await runCLI([ 140 - "gen-inferred", 141 - outDir, 142 - schemaFile, 143 - ]); 144 - 145 - expect(code).toBe(0); 146 - expect(stderr).toBe(""); 147 - expect(stdout).toContain("app.test.overwrite -> app/test/overwrite.ts"); 148 - }); 149 - 150 - test("handles read-only output directory gracefully", async () => { 151 - // This test might be platform-specific and could fail on some systems 152 - // We'll make it lenient to not fail the test suite 153 - 154 - // Create a schema file 155 - const schemaFile = join(schemasDir, "test.json"); 156 - await writeFile( 157 - schemaFile, 158 - JSON.stringify({ 159 - lexicon: 1, 160 - id: "app.test.permission", 161 - defs: { main: { type: "record" } }, 162 - }), 163 - ); 164 - 165 - // Create output directory and make it read-only 166 - const readOnlyDir = join(outDir, "readonly"); 167 - await mkdir(readOnlyDir, { recursive: true }); 168 - 169 - // Try to make read-only (might not work on all systems) 170 - try { 171 - await chmod(readOnlyDir, 0o444); 172 - } catch (error) { 173 - // Ignore if we can't change permissions 174 - } 175 - 176 - const { stdout, stderr, code } = await runCLI([ 177 - "gen-inferred", 178 - readOnlyDir, 179 - schemaFile, 180 - ]); 181 - 182 - // Should handle the error gracefully 183 - // On some systems this might succeed, on others fail - we just want it to not crash 184 - expect([0, 1]).toContain(code); 185 - }); 186 });
··· 33 // Ignore cleanup errors 34 } 35 }); 36 });
-87
packages/cli/tests/integration/workflow.test.ts
··· 24 await rm(testDir, { recursive: true, force: true }); 25 }); 26 27 - test("complete workflow: JSON schema -> inferred types", async () => { 28 - // Step 1: Create JSON schema file directly (avoiding dynamic import issues) 29 - const schemaPath = join(schemasDir, "app.test.profile.json"); 30 - await writeFile( 31 - schemaPath, 32 - JSON.stringify( 33 - { 34 - lexicon: 1, 35 - id: "app.test.profile", 36 - defs: { 37 - main: { 38 - type: "record", 39 - key: "self", 40 - record: { 41 - type: "object", 42 - properties: { 43 - displayName: { type: "string", maxLength: 64 }, 44 - description: { type: "string", maxLength: 256 }, 45 - }, 46 - }, 47 - }, 48 - }, 49 - }, 50 - null, 51 - 2, 52 - ), 53 - ); 54 - 55 - // Step 2: Generate inferred TypeScript from JSON schema 56 - const inferResult = await runCLI([ 57 - "gen-inferred", 58 - generatedDir, 59 - schemaPath, 60 - ]); 61 - 62 - console.log("Infer result code:", inferResult.code); 63 - console.log("Infer stdout:", inferResult.stdout); 64 - console.log("Infer stderr:", inferResult.stderr); 65 - 66 - expect(inferResult.code).toBe(0); 67 - expect(inferResult.stdout).toContain("Generated inferred types in"); 68 - expect(inferResult.stdout).toContain( 69 - "app.test.profile -> app/test/profile.ts", 70 - ); 71 - 72 - // Verify generated TypeScript file 73 - const generatedPath = join(generatedDir, "app/test/profile.ts"); 74 - const generatedContent = await readFile(generatedPath, "utf-8"); 75 - expect(generatedContent).toContain( 76 - 'import type { Infer } from "prototypey"', 77 - ); 78 - expect(generatedContent).toContain( 79 - "export type Profile = Infer<typeof schema>", 80 - ); 81 - expect(generatedContent).toContain("export const ProfileSchema = schema"); 82 - expect(generatedContent).toContain( 83 - "export function isProfile(v: unknown): v is Profile", 84 - ); 85 - }); 86 - 87 test("workflow with multiple schemas", async () => { 88 // Create multiple JSON schema files 89 const postSchema = join(schemasDir, "app.test.post.json"); ··· 159 null, 160 2, 161 ), 162 - ); 163 - 164 - // Generate inferred types 165 - const inferResult = await runCLI([ 166 - "gen-inferred", 167 - generatedDir, 168 - `${schemasDir}/*.json`, 169 - ]); 170 - expect(inferResult.code).toBe(0); 171 - expect(inferResult.stdout).toContain("app.test.post -> app/test/post.ts"); 172 - expect(inferResult.stdout).toContain( 173 - "app.test.searchPosts -> app/test/searchPosts.ts", 174 - ); 175 - 176 - // Verify both generated files exist and have correct content 177 - const postContent = await readFile( 178 - join(generatedDir, "app/test/post.ts"), 179 - "utf-8", 180 - ); 181 - const searchContent = await readFile( 182 - join(generatedDir, "app/test/searchPosts.ts"), 183 - "utf-8", 184 - ); 185 - 186 - expect(postContent).toContain("export type Post = Infer<typeof schema>"); 187 - expect(searchContent).toContain( 188 - "export type SearchPosts = Infer<typeof schema>", 189 ); 190 }); 191 });
··· 24 await rm(testDir, { recursive: true, force: true }); 25 }); 26 27 test("workflow with multiple schemas", async () => { 28 // Create multiple JSON schema files 29 const postSchema = join(schemasDir, "app.test.post.json"); ··· 99 null, 100 2, 101 ), 102 ); 103 }); 104 });
-210
packages/cli/tests/performance/large-schema-set.test.ts
··· 1 - import { expect, test, describe, beforeEach, afterEach } from "vitest"; 2 - import { mkdir, writeFile, rm } from "node:fs/promises"; 3 - import { join } from "node:path"; 4 - import { tmpdir } from "node:os"; 5 - import { runCLI } from "../test-utils.js"; 6 - 7 - describe("CLI Performance", () => { 8 - let testDir: string; 9 - let outDir: string; 10 - let schemasDir: string; 11 - 12 - beforeEach(async () => { 13 - // Create a temporary directory for test files 14 - testDir = join(tmpdir(), `prototypey-perf-test-${Date.now()}`); 15 - outDir = join(testDir, "output"); 16 - schemasDir = join(testDir, "schemas"); 17 - await mkdir(testDir, { recursive: true }); 18 - await mkdir(outDir, { recursive: true }); 19 - await mkdir(schemasDir, { recursive: true }); 20 - }); 21 - 22 - afterEach(async () => { 23 - // Clean up test directory 24 - await rm(testDir, { recursive: true, force: true }); 25 - }); 26 - 27 - test("handles large number of schemas efficiently", async () => { 28 - // Create 50 schema files 29 - const schemaCount = 50; 30 - const schemaFiles = []; 31 - 32 - for (let i = 0; i < schemaCount; i++) { 33 - const schemaFile = join(schemasDir, `test${i}.json`); 34 - await writeFile( 35 - schemaFile, 36 - JSON.stringify({ 37 - lexicon: 1, 38 - id: `app.test.schema${i}`, 39 - defs: { 40 - main: { 41 - type: "record", 42 - key: "tid", 43 - record: { 44 - type: "object", 45 - properties: { 46 - name: { type: "string", maxLength: 64 }, 47 - value: { type: "integer" }, 48 - }, 49 - }, 50 - }, 51 - }, 52 - }), 53 - ); 54 - schemaFiles.push(schemaFile); 55 - } 56 - 57 - const startTime = Date.now(); 58 - const { stdout, stderr, code } = await runCLI([ 59 - "gen-inferred", 60 - outDir, 61 - `${schemasDir}/*.json`, 62 - ]); 63 - const endTime = Date.now(); 64 - 65 - const duration = endTime - startTime; 66 - 67 - expect(code).toBe(0); 68 - expect(stdout).toContain(`Found ${schemaCount} schema file(s)`); 69 - expect(stderr).toBe(""); 70 - 71 - // Should complete within reasonable time (less than 5 seconds for 50 files) 72 - expect(duration).toBeLessThan(5000); 73 - 74 - // Verify some generated files exist 75 - expect(stdout).toContain("app.test.schema0 -> app/test/schema0.ts"); 76 - expect(stdout).toContain("app.test.schema49 -> app/test/schema49.ts"); 77 - }); 78 - 79 - test("memory usage stays reasonable with large schemas", async () => { 80 - // Create a schema with complex nested structure 81 - const complexSchema = join(schemasDir, "complex.json"); 82 - await writeFile( 83 - complexSchema, 84 - JSON.stringify({ 85 - lexicon: 1, 86 - id: "app.test.complex", 87 - defs: { 88 - main: { 89 - type: "record", 90 - key: "tid", 91 - record: { 92 - type: "object", 93 - properties: { 94 - // Create a deeply nested structure 95 - level1: { 96 - type: "object", 97 - properties: { 98 - level2: { 99 - type: "object", 100 - properties: { 101 - level3: { 102 - type: "object", 103 - properties: { 104 - level4: { 105 - type: "object", 106 - properties: { 107 - items: { 108 - type: "array", 109 - items: { 110 - type: "object", 111 - properties: { 112 - id: { type: "string" }, 113 - data: { type: "string" }, 114 - metadata: { 115 - type: "object", 116 - properties: { 117 - created: { 118 - type: "string", 119 - format: "datetime", 120 - }, 121 - updated: { 122 - type: "string", 123 - format: "datetime", 124 - }, 125 - tags: { 126 - type: "array", 127 - items: { type: "string" }, 128 - }, 129 - }, 130 - }, 131 - }, 132 - }, 133 - }, 134 - }, 135 - }, 136 - }, 137 - }, 138 - }, 139 - }, 140 - }, 141 - }, 142 - }, 143 - }, 144 - }, 145 - }, 146 - }), 147 - ); 148 - 149 - const startTime = Date.now(); 150 - const { stdout, stderr, code } = await runCLI([ 151 - "gen-inferred", 152 - outDir, 153 - complexSchema, 154 - ]); 155 - const endTime = Date.now(); 156 - 157 - const duration = endTime - startTime; 158 - 159 - expect(code).toBe(0); 160 - expect(stdout).toContain("Found 1 schema file(s)"); 161 - expect(stdout).toContain("app.test.complex -> app/test/complex.ts"); 162 - expect(stderr).toBe(""); 163 - 164 - // Should complete within reasonable time (less than 2 seconds) 165 - expect(duration).toBeLessThan(2000); 166 - }); 167 - 168 - test("concurrent processing of multiple commands", async () => { 169 - // Create test schemas 170 - const schema1 = join(schemasDir, "test1.json"); 171 - const schema2 = join(schemasDir, "test2.json"); 172 - 173 - await writeFile( 174 - schema1, 175 - JSON.stringify({ 176 - lexicon: 1, 177 - id: "app.test.concurrent1", 178 - defs: { 179 - main: { type: "record", key: "tid", record: { type: "object" } }, 180 - }, 181 - }), 182 - ); 183 - 184 - await writeFile( 185 - schema2, 186 - JSON.stringify({ 187 - lexicon: 1, 188 - id: "app.test.concurrent2", 189 - defs: { 190 - main: { type: "record", key: "tid", record: { type: "object" } }, 191 - }, 192 - }), 193 - ); 194 - 195 - // Run two CLI commands concurrently 196 - const [result1, result2] = await Promise.all([ 197 - runCLI(["gen-inferred", join(outDir, "out1"), schema1]), 198 - runCLI(["gen-inferred", join(outDir, "out2"), schema2]), 199 - ]); 200 - 201 - expect(result1.code).toBe(0); 202 - expect(result2.code).toBe(0); 203 - expect(result1.stdout).toContain( 204 - "app.test.concurrent1 -> app/test/concurrent1.ts", 205 - ); 206 - expect(result2.stdout).toContain( 207 - "app.test.concurrent2 -> app/test/concurrent2.ts", 208 - ); 209 - }); 210 - });
···
-122
packages/cli/tests/unit/template-edge-cases.test.ts
··· 1 - import { expect, test, describe } from "vitest"; 2 - import { generateInferredCode } from "../../src/templates/inferred.ts"; 3 - 4 - describe("Template Edge Cases", () => { 5 - test("handles NSID with trailing numbers correctly", () => { 6 - const schema = { 7 - lexicon: 1, 8 - id: "app.test.v1", 9 - defs: { main: { type: "record" } }, 10 - }; 11 - 12 - const code = generateInferredCode(schema, "/test/v1.json", "/output"); 13 - expect(code).toContain("export type V1 = Infer<typeof schema>"); 14 - expect(code).toContain("export const V1Schema = schema"); 15 - expect(code).toContain("export function isV1(v: unknown): v is V1"); 16 - }); 17 - 18 - test("handles NSID with multiple consecutive separators", () => { 19 - const schema = { 20 - lexicon: 1, 21 - id: "app.test.my--double--dash", 22 - defs: { main: { type: "record" } }, 23 - }; 24 - 25 - const code = generateInferredCode(schema, "/test/double.json", "/output"); 26 - expect(code).toContain("export type MyDoubleDash = Infer<typeof schema>"); 27 - }); 28 - 29 - test("handles single character NSID parts", () => { 30 - const schema = { 31 - lexicon: 1, 32 - id: "a.b.c", 33 - defs: { main: { type: "record" } }, 34 - }; 35 - 36 - const code = generateInferredCode(schema, "/test/single.json", "/output"); 37 - expect(code).toContain("export type C = Infer<typeof schema>"); 38 - }); 39 - 40 - test("handles NSID with underscores and mixed case", () => { 41 - const schema = { 42 - lexicon: 1, 43 - id: "app.test.my_custom_Type_Name", 44 - defs: { main: { type: "record" } }, 45 - }; 46 - 47 - const code = generateInferredCode(schema, "/test/custom.json", "/output"); 48 - expect(code).toContain( 49 - "export type MyCustomTypeName = Infer<typeof schema>", 50 - ); 51 - }); 52 - 53 - test("handles very long NSID name", () => { 54 - const longName = "a".repeat(100); 55 - const schema = { 56 - lexicon: 1, 57 - id: `app.test.${longName}`, 58 - defs: { main: { type: "record" } }, 59 - }; 60 - 61 - const code = generateInferredCode(schema, "/test/long.json", "/output"); 62 - // Should not crash and should generate valid TypeScript 63 - expect(code).toContain("export type"); 64 - expect(code).toContain("Infer<typeof schema>"); 65 - }); 66 - 67 - test("handles schema with no main def", () => { 68 - const schema = { 69 - lexicon: 1, 70 - id: "app.test.no-main", 71 - defs: { 72 - other: { type: "object" }, 73 - }, 74 - }; 75 - 76 - const code = generateInferredCode(schema, "/test/no-main.json", "/output"); 77 - // Should still generate valid code even without main def 78 - expect(code).toContain("export type NoMain = Infer<typeof schema>"); 79 - // The path will be relative with ../../../ prefix 80 - expect(code).toContain( 81 - 'import schema from "../../../test/no-main.json" with { type: "json" };', 82 - ); 83 - }); 84 - 85 - test("generates correct relative paths for deeply nested output", () => { 86 - const schema = { 87 - lexicon: 1, 88 - id: "app.bsky.feed.post", 89 - defs: { main: { type: "record" } }, 90 - }; 91 - 92 - const code = generateInferredCode( 93 - schema, 94 - "/project/schemas/feed.json", 95 - "/project/generated/inferred", 96 - ); 97 - 98 - // Should have correct relative import path 99 - expect(code).toContain( 100 - 'import schema from "../../../../../schemas/feed.json" with { type: "json" };', 101 - ); 102 - }); 103 - 104 - test("handles special characters in import paths", () => { 105 - const schema = { 106 - lexicon: 1, 107 - id: "app.test.special", 108 - defs: { main: { type: "record" } }, 109 - }; 110 - 111 - const code = generateInferredCode( 112 - schema, 113 - "/project/schemas with spaces/special[chars].json", 114 - "/project/generated", 115 - ); 116 - 117 - // Should handle spaces and special characters in paths 118 - expect(code).toContain( 119 - 'import schema from "../../../schemas with spaces/special[chars].json" with { type: "json" };', 120 - ); 121 - }); 122 - });
···
-14
packages/prototypey/README.md
··· 64 65 The `prototypey` package includes a CLI with two main commands: 66 67 - #### `gen-inferred` - Generate TypeScript from JSON schemas 68 - 69 - ```bash 70 - prototypey gen-inferred <outdir> <schemas...> 71 - ``` 72 - 73 - Reads ATProto lexicon JSON schemas and generates TypeScript types. 74 - 75 - **Example:** 76 - 77 - ```bash 78 - prototypey gen-inferred ./generated/inferred ./lexicons/**/*.json 79 - ``` 80 - 81 #### `gen-emit` - Emit JSON schemas from TypeScript 82 83 ```bash
··· 64 65 The `prototypey` package includes a CLI with two main commands: 66 67 #### `gen-emit` - Emit JSON schemas from TypeScript 68 69 ```bash