diff --git a/.prettierignore b/.prettierignore index b5b85af..01e47ba 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,2 +1,3 @@ -/lib -/pnpm-lock.yaml +lib/ +dist/ +pnpm-lock.yaml diff --git a/packages/cli/src/commands/gen-emit.ts b/packages/cli/src/commands/gen-emit.ts index c426d09..8afe73c 100644 --- a/packages/cli/src/commands/gen-emit.ts +++ b/packages/cli/src/commands/gen-emit.ts @@ -77,7 +77,7 @@ async function processSourceFile( } if (lexicons.length === 0) { - console.warn(` ⚠ ${sourcePath}: No lexicon lexicons found`); + console.warn(` ⚠ ${sourcePath}: No lexicons found`); return; } diff --git a/packages/cli/tests/integration/cli.test.ts b/packages/cli/tests/integration/cli.test.ts new file mode 100644 index 0000000..060adb8 --- /dev/null +++ b/packages/cli/tests/integration/cli.test.ts @@ -0,0 +1,51 @@ +import { expect, test, describe } from "vitest"; +import { runCLI } from "../test-utils.js"; + +describe("CLI Integration", () => { + test("shows error when called without arguments", async () => { + const { stdout, stderr, code } = await runCLI(); + expect(code).toBe(1); + expect(stderr).toContain("No command specified"); + expect(stderr).toContain("Run `$ prototypey --help` for more info"); + }); + + test("shows version", async () => { + const { stdout, stderr } = await runCLI(["--version"]); + expect(stderr).toBe(""); + expect(stdout).toContain("prototypey, 0.0.0"); + }); + + test("shows help for gen-inferred command", async () => { + const { stdout, stderr } = await runCLI(["gen-inferred", "--help"]); + expect(stderr).toBe(""); + expect(stdout).toContain("gen-inferred "); + expect(stdout).toContain( + "Generate type-inferred code from lexicon schemas", + ); + }); + + test("shows help for gen-emit command", async () => { + const { stdout, stderr } = await runCLI(["gen-emit", "--help"]); + expect(stderr).toBe(""); + expect(stdout).toContain("gen-emit "); + expect(stdout).toContain( + "Emit JSON lexicon schemas from authored TypeScript", + ); + }); + + test("handles unknown command", async () => { + const { stdout, stderr, code } = await runCLI(["unknown-command"]); + expect(code).toBe(1); + expect(stderr).toContain("Invalid command: unknown-command"); + expect(stderr).toContain("Run `$ prototypey --help` for more info"); + }); + + test("handles missing arguments", async () => { + const { stdout, stderr, code } = await runCLI(["gen-inferred"]); + expect(code).toBe(1); + expect(stderr).toContain("Insufficient arguments!"); + expect(stderr).toContain( + "Run `$ prototypey gen-inferred --help` for more info", + ); + }); +}); diff --git a/packages/cli/tests/integration/error-handling.test.ts b/packages/cli/tests/integration/error-handling.test.ts new file mode 100644 index 0000000..77cb6c2 --- /dev/null +++ b/packages/cli/tests/integration/error-handling.test.ts @@ -0,0 +1,155 @@ +import { expect, test, describe, beforeEach, afterEach } from "vitest"; +import { mkdir, writeFile, rm } from "node:fs/promises"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +import { runCLI } from "../test-utils.js"; + +describe("CLI Error Handling", () => { + let testDir: string; + let outDir: string; + let schemasDir: string; + + beforeEach(async () => { + // Create a temporary directory for test files + testDir = join(tmpdir(), `prototypey-error-test-${Date.now()}`); + outDir = join(testDir, "output"); + schemasDir = join(testDir, "schemas"); + await mkdir(testDir, { recursive: true }); + await mkdir(outDir, { recursive: true }); + await mkdir(schemasDir, { recursive: true }); + }); + + afterEach(async () => { + // Clean up test directory + await rm(testDir, { recursive: true, force: true }); + }); + + test("handles non-existent schema files gracefully", async () => { + const { stdout, stderr, code } = await runCLI([ + "gen-inferred", + outDir, + join(schemasDir, "non-existent.json"), + ]); + + expect(code).toBe(0); // Should not crash + expect(stdout).toContain("No schema files found matching patterns"); + expect(stderr).toBe(""); + }); + + test("handles invalid JSON schema files", async () => { + // Create an invalid JSON file + const invalidSchema = join(schemasDir, "invalid.json"); + await writeFile(invalidSchema, "not valid json"); + + const { stdout, stderr, code } = await runCLI([ + "gen-inferred", + outDir, + invalidSchema, + ]); + + expect(code).toBe(1); // Should exit with error + expect(stderr).toContain("Error generating inferred types"); + }); + + test("handles schema files with missing id", async () => { + // Create a schema with missing id + const schemaFile = join(schemasDir, "missing-id.json"); + await writeFile( + schemaFile, + JSON.stringify({ + lexicon: 1, + defs: { main: { type: "record" } }, + }), + ); + + const { stdout, stderr, code } = await runCLI([ + "gen-inferred", + outDir, + schemaFile, + ]); + + expect(code).toBe(0); // Should not crash + expect(stdout).toContain("Found 1 schema file(s)"); + expect(stdout).toContain("Generated inferred types in"); + // Should skip the invalid file silently + }); + + test("handles schema files with missing defs", async () => { + // Create a schema with missing defs + const schemaFile = join(schemasDir, "missing-defs.json"); + await writeFile( + schemaFile, + JSON.stringify({ + lexicon: 1, + id: "app.test.missing", + }), + ); + + const { stdout, stderr, code } = await runCLI([ + "gen-inferred", + outDir, + schemaFile, + ]); + + expect(code).toBe(0); // Should not crash + expect(stdout).toContain("Found 1 schema file(s)"); + expect(stdout).toContain("Generated inferred types in"); + // Should skip the invalid file silently + }); + + test("handles non-existent source files for gen-emit", async () => { + const { stdout, stderr, code } = await runCLI([ + "gen-emit", + outDir, + join(schemasDir, "non-existent.ts"), + ]); + + expect(code).toBe(0); // Should not crash + expect(stdout).toContain("No source files found matching patterns"); + expect(stderr).toBe(""); + }); + + test("handles valid TypeScript files with no lexicon exports for gen-emit", async () => { + // Create a valid TypeScript file with no lexicon exports + const validSource = join(schemasDir, "no-namespace.ts"); + await writeFile(validSource, "export const x = 1;"); + + const { stdout, stderr, code } = await runCLI([ + "gen-emit", + outDir, + validSource, + ]); + + expect(code).toBe(0); // Should not crash + expect(stdout).toContain("Found 1 source file(s)"); + expect(stderr).toContain("No lexicons found"); + }); + + test("handles permission errors when writing output", async () => { + // This test might be platform-specific, so we'll make it lenient + // Create a schema file first + const schemaFile = join(schemasDir, "test.json"); + await writeFile( + schemaFile, + JSON.stringify({ + lexicon: 1, + id: "app.test.permission", + defs: { main: { type: "record" } }, + }), + ); + + // Try to write to a directory that might have permission issues + // We'll use a path that likely won't exist and is invalid + const invalidOutDir = "/invalid/path/that/does/not/exist"; + + const { stdout, stderr, code } = await runCLI([ + "gen-inferred", + invalidOutDir, + schemaFile, + ]); + + // Should handle the error gracefully + expect(code).toBe(1); + expect(stderr).toContain("Error generating inferred types"); + }); +}); diff --git a/packages/cli/tests/integration/filesystem.test.ts b/packages/cli/tests/integration/filesystem.test.ts new file mode 100644 index 0000000..2e88a41 --- /dev/null +++ b/packages/cli/tests/integration/filesystem.test.ts @@ -0,0 +1,186 @@ +import { expect, test, describe, beforeEach, afterEach } from "vitest"; +import { + mkdir, + writeFile, + rm, + chmod, + access, + constants, +} from "node:fs/promises"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +import { runCLI } from "../test-utils.js"; + +describe("CLI File System Handling", () => { + let testDir: string; + let outDir: string; + let schemasDir: string; + + beforeEach(async () => { + // Create a temporary directory for test files + testDir = join(tmpdir(), `prototypey-fs-test-${Date.now()}`); + outDir = join(testDir, "output"); + schemasDir = join(testDir, "schemas"); + await mkdir(testDir, { recursive: true }); + await mkdir(schemasDir, { recursive: true }); + }); + + afterEach(async () => { + // Clean up test directory + try { + await rm(testDir, { recursive: true, force: true }); + } catch (error) { + // Ignore cleanup errors + } + }); + + test("creates nested output directories when they don't exist", async () => { + // Create a schema file + const schemaFile = join(schemasDir, "test.json"); + await writeFile( + schemaFile, + JSON.stringify({ + lexicon: 1, + id: "app.deeply.nested.schema", + defs: { main: { type: "record" } }, + }), + ); + + // Use a deeply nested output directory that doesn't exist + const deepOutDir = join(outDir, "very", "deeply", "nested", "path"); + + const { stdout, stderr, code } = await runCLI([ + "gen-inferred", + deepOutDir, + schemaFile, + ]); + + expect(code).toBe(0); + expect(stderr).toBe(""); + expect(stdout).toContain( + "app.deeply.nested.schema -> app/deeply/nested/schema.ts", + ); + + // Verify the file was created in the deeply nested path + const generatedFile = join(deepOutDir, "app/deeply/nested/schema.ts"); + await access(generatedFile, constants.F_OK); + }); + + test("handles special characters in NSID correctly", async () => { + // Create a schema with special characters in the name + const schemaFile = join(schemasDir, "special.json"); + await writeFile( + schemaFile, + JSON.stringify({ + lexicon: 1, + id: "app.test.special-name_with.mixedChars123", + defs: { main: { type: "record" } }, + }), + ); + + const { stdout, stderr, code } = await runCLI([ + "gen-inferred", + outDir, + schemaFile, + ]); + + expect(code).toBe(0); + expect(stderr).toBe(""); + // Should convert to proper PascalCase + expect(stdout).toContain( + "app.test.special-name_with.mixedChars123 -> app/test/special-name_with/mixedChars123.ts", + ); + }); + + test("handles very long NSID paths", async () => { + // Create a schema with a very long NSID + const longNSID = "com." + "verylongdomainname.".repeat(10) + "test"; + const schemaFile = join(schemasDir, "long.json"); + await writeFile( + schemaFile, + JSON.stringify({ + lexicon: 1, + id: longNSID, + defs: { main: { type: "record" } }, + }), + ); + + const { stdout, stderr, code } = await runCLI([ + "gen-inferred", + outDir, + schemaFile, + ]); + + expect(code).toBe(0); + expect(stderr).toBe(""); + expect(stdout).toContain(`Found 1 schema file(s)`); + }); + + test("handles existing files gracefully", async () => { + // Create a schema file + const schemaFile = join(schemasDir, "test.json"); + await writeFile( + schemaFile, + JSON.stringify({ + lexicon: 1, + id: "app.test.overwrite", + defs: { main: { type: "record" } }, + }), + ); + + // Run CLI once + await runCLI(["gen-inferred", outDir, schemaFile]); + + // Verify file exists + const generatedFile = join(outDir, "app/test/overwrite.ts"); + await access(generatedFile, constants.F_OK); + + // Run CLI again (should overwrite) + const { stdout, stderr, code } = await runCLI([ + "gen-inferred", + outDir, + schemaFile, + ]); + + expect(code).toBe(0); + expect(stderr).toBe(""); + expect(stdout).toContain("app.test.overwrite -> app/test/overwrite.ts"); + }); + + test("handles read-only output directory gracefully", async () => { + // This test might be platform-specific and could fail on some systems + // We'll make it lenient to not fail the test suite + + // Create a schema file + const schemaFile = join(schemasDir, "test.json"); + await writeFile( + schemaFile, + JSON.stringify({ + lexicon: 1, + id: "app.test.permission", + defs: { main: { type: "record" } }, + }), + ); + + // Create output directory and make it read-only + const readOnlyDir = join(outDir, "readonly"); + await mkdir(readOnlyDir, { recursive: true }); + + // Try to make read-only (might not work on all systems) + try { + await chmod(readOnlyDir, 0o444); + } catch (error) { + // Ignore if we can't change permissions + } + + const { stdout, stderr, code } = await runCLI([ + "gen-inferred", + readOnlyDir, + schemaFile, + ]); + + // Should handle the error gracefully + // On some systems this might succeed, on others fail - we just want it to not crash + expect([0, 1]).toContain(code); + }); +}); diff --git a/packages/cli/tests/integration/workflow.test.ts b/packages/cli/tests/integration/workflow.test.ts new file mode 100644 index 0000000..11d7c4b --- /dev/null +++ b/packages/cli/tests/integration/workflow.test.ts @@ -0,0 +1,191 @@ +import { expect, test, describe, beforeEach, afterEach } from "vitest"; +import { mkdir, writeFile, rm, readFile } from "node:fs/promises"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +import { runCLI } from "../test-utils.js"; + +describe("CLI End-to-End Workflow", () => { + let testDir: string; + let schemasDir: string; + let generatedDir: string; + + beforeEach(async () => { + // Create a temporary directory for test files + testDir = join(tmpdir(), `prototypey-e2e-test-${Date.now()}`); + schemasDir = join(testDir, "schemas"); + generatedDir = join(testDir, "generated"); + await mkdir(testDir, { recursive: true }); + await mkdir(schemasDir, { recursive: true }); + await mkdir(generatedDir, { recursive: true }); + }); + + afterEach(async () => { + // Clean up test directory + await rm(testDir, { recursive: true, force: true }); + }); + + test("complete workflow: JSON schema -> inferred types", async () => { + // Step 1: Create JSON schema file directly (avoiding dynamic import issues) + const schemaPath = join(schemasDir, "app.test.profile.json"); + await writeFile( + schemaPath, + JSON.stringify( + { + lexicon: 1, + id: "app.test.profile", + defs: { + main: { + type: "record", + key: "self", + record: { + type: "object", + properties: { + displayName: { type: "string", maxLength: 64 }, + description: { type: "string", maxLength: 256 }, + }, + }, + }, + }, + }, + null, + 2, + ), + ); + + // Step 2: Generate inferred TypeScript from JSON schema + const inferResult = await runCLI([ + "gen-inferred", + generatedDir, + schemaPath, + ]); + + console.log("Infer result code:", inferResult.code); + console.log("Infer stdout:", inferResult.stdout); + console.log("Infer stderr:", inferResult.stderr); + + expect(inferResult.code).toBe(0); + expect(inferResult.stdout).toContain("Generated inferred types in"); + expect(inferResult.stdout).toContain( + "app.test.profile -> app/test/profile.ts", + ); + + // Verify generated TypeScript file + const generatedPath = join(generatedDir, "app/test/profile.ts"); + const generatedContent = await readFile(generatedPath, "utf-8"); + expect(generatedContent).toContain( + 'import type { Infer } from "prototypey"', + ); + expect(generatedContent).toContain( + "export type Profile = Infer", + ); + expect(generatedContent).toContain("export const ProfileSchema = schema"); + expect(generatedContent).toContain( + "export function isProfile(v: unknown): v is Profile", + ); + }); + + test("workflow with multiple schemas", async () => { + // Create multiple JSON schema files + const postSchema = join(schemasDir, "app.test.post.json"); + await writeFile( + postSchema, + JSON.stringify( + { + lexicon: 1, + id: "app.test.post", + defs: { + main: { + type: "record", + key: "tid", + record: { + type: "object", + properties: { + text: { type: "string", maxLength: 300, required: true }, + createdAt: { + type: "string", + format: "datetime", + required: true, + }, + }, + }, + }, + }, + }, + null, + 2, + ), + ); + + const searchSchema = join(schemasDir, "app.test.searchPosts.json"); + await writeFile( + searchSchema, + JSON.stringify( + { + lexicon: 1, + id: "app.test.searchPosts", + defs: { + main: { + type: "query", + parameters: { + type: "params", + properties: { + q: { type: "string", required: true }, + limit: { + type: "integer", + minimum: 1, + maximum: 100, + default: 25, + }, + }, + required: ["q"], + }, + output: { + encoding: "application/json", + schema: { + type: "object", + properties: { + posts: { + type: "array", + items: { type: "ref", ref: "app.test.post#main" }, + required: true, + }, + }, + required: ["posts"], + }, + }, + }, + }, + }, + null, + 2, + ), + ); + + // Generate inferred types + const inferResult = await runCLI([ + "gen-inferred", + generatedDir, + `${schemasDir}/*.json`, + ]); + expect(inferResult.code).toBe(0); + expect(inferResult.stdout).toContain("app.test.post -> app/test/post.ts"); + expect(inferResult.stdout).toContain( + "app.test.searchPosts -> app/test/searchPosts.ts", + ); + + // Verify both generated files exist and have correct content + const postContent = await readFile( + join(generatedDir, "app/test/post.ts"), + "utf-8", + ); + const searchContent = await readFile( + join(generatedDir, "app/test/searchPosts.ts"), + "utf-8", + ); + + expect(postContent).toContain("export type Post = Infer"); + expect(searchContent).toContain( + "export type SearchPosts = Infer", + ); + }); +}); diff --git a/packages/cli/tests/performance/large-schema-set.test.ts b/packages/cli/tests/performance/large-schema-set.test.ts new file mode 100644 index 0000000..4d64810 --- /dev/null +++ b/packages/cli/tests/performance/large-schema-set.test.ts @@ -0,0 +1,210 @@ +import { expect, test, describe, beforeEach, afterEach } from "vitest"; +import { mkdir, writeFile, rm } from "node:fs/promises"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +import { runCLI } from "../test-utils.js"; + +describe("CLI Performance", () => { + let testDir: string; + let outDir: string; + let schemasDir: string; + + beforeEach(async () => { + // Create a temporary directory for test files + testDir = join(tmpdir(), `prototypey-perf-test-${Date.now()}`); + outDir = join(testDir, "output"); + schemasDir = join(testDir, "schemas"); + await mkdir(testDir, { recursive: true }); + await mkdir(outDir, { recursive: true }); + await mkdir(schemasDir, { recursive: true }); + }); + + afterEach(async () => { + // Clean up test directory + await rm(testDir, { recursive: true, force: true }); + }); + + test("handles large number of schemas efficiently", async () => { + // Create 50 schema files + const schemaCount = 50; + const schemaFiles = []; + + for (let i = 0; i < schemaCount; i++) { + const schemaFile = join(schemasDir, `test${i}.json`); + await writeFile( + schemaFile, + JSON.stringify({ + lexicon: 1, + id: `app.test.schema${i}`, + defs: { + main: { + type: "record", + key: "tid", + record: { + type: "object", + properties: { + name: { type: "string", maxLength: 64 }, + value: { type: "integer" }, + }, + }, + }, + }, + }), + ); + schemaFiles.push(schemaFile); + } + + const startTime = Date.now(); + const { stdout, stderr, code } = await runCLI([ + "gen-inferred", + outDir, + `${schemasDir}/*.json`, + ]); + const endTime = Date.now(); + + const duration = endTime - startTime; + + expect(code).toBe(0); + expect(stdout).toContain(`Found ${schemaCount} schema file(s)`); + expect(stderr).toBe(""); + + // Should complete within reasonable time (less than 5 seconds for 50 files) + expect(duration).toBeLessThan(5000); + + // Verify some generated files exist + expect(stdout).toContain("app.test.schema0 -> app/test/schema0.ts"); + expect(stdout).toContain("app.test.schema49 -> app/test/schema49.ts"); + }); + + test("memory usage stays reasonable with large schemas", async () => { + // Create a schema with complex nested structure + const complexSchema = join(schemasDir, "complex.json"); + await writeFile( + complexSchema, + JSON.stringify({ + lexicon: 1, + id: "app.test.complex", + defs: { + main: { + type: "record", + key: "tid", + record: { + type: "object", + properties: { + // Create a deeply nested structure + level1: { + type: "object", + properties: { + level2: { + type: "object", + properties: { + level3: { + type: "object", + properties: { + level4: { + type: "object", + properties: { + items: { + type: "array", + items: { + type: "object", + properties: { + id: { type: "string" }, + data: { type: "string" }, + metadata: { + type: "object", + properties: { + created: { + type: "string", + format: "datetime", + }, + updated: { + type: "string", + format: "datetime", + }, + tags: { + type: "array", + items: { type: "string" }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }), + ); + + const startTime = Date.now(); + const { stdout, stderr, code } = await runCLI([ + "gen-inferred", + outDir, + complexSchema, + ]); + const endTime = Date.now(); + + const duration = endTime - startTime; + + expect(code).toBe(0); + expect(stdout).toContain("Found 1 schema file(s)"); + expect(stdout).toContain("app.test.complex -> app/test/complex.ts"); + expect(stderr).toBe(""); + + // Should complete within reasonable time (less than 2 seconds) + expect(duration).toBeLessThan(2000); + }); + + test("concurrent processing of multiple commands", async () => { + // Create test schemas + const schema1 = join(schemasDir, "test1.json"); + const schema2 = join(schemasDir, "test2.json"); + + await writeFile( + schema1, + JSON.stringify({ + lexicon: 1, + id: "app.test.concurrent1", + defs: { + main: { type: "record", key: "tid", record: { type: "object" } }, + }, + }), + ); + + await writeFile( + schema2, + JSON.stringify({ + lexicon: 1, + id: "app.test.concurrent2", + defs: { + main: { type: "record", key: "tid", record: { type: "object" } }, + }, + }), + ); + + // Run two CLI commands concurrently + const [result1, result2] = await Promise.all([ + runCLI(["gen-inferred", join(outDir, "out1"), schema1]), + runCLI(["gen-inferred", join(outDir, "out2"), schema2]), + ]); + + expect(result1.code).toBe(0); + expect(result2.code).toBe(0); + expect(result1.stdout).toContain( + "app.test.concurrent1 -> app/test/concurrent1.ts", + ); + expect(result2.stdout).toContain( + "app.test.concurrent2 -> app/test/concurrent2.ts", + ); + }); +}); diff --git a/packages/cli/tests/test-utils.ts b/packages/cli/tests/test-utils.ts new file mode 100644 index 0000000..57196d5 --- /dev/null +++ b/packages/cli/tests/test-utils.ts @@ -0,0 +1,34 @@ +import { spawn } from "node:child_process"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +export function runCLI( + args: string[] = [], + options?: { cwd?: string; env?: NodeJS.ProcessEnv }, +): Promise<{ stdout: string; stderr: string; code: number }> { + return new Promise((resolve) => { + const cliPath = join( + dirname(fileURLToPath(import.meta.url)), + "../lib/index.js", + ); + const child = spawn("node", [cliPath, ...args], { + cwd: options?.cwd ?? process.cwd(), + env: options?.env ?? process.env, + }); + + let stdout = ""; + let stderr = ""; + + child.stdout.on("data", (data) => { + stdout += data.toString(); + }); + + child.stderr.on("data", (data) => { + stderr += data.toString(); + }); + + child.on("close", (code) => { + resolve({ stdout, stderr, code: code ?? 0 }); + }); + }); +} diff --git a/packages/cli/tests/unit/template-edge-cases.test.ts b/packages/cli/tests/unit/template-edge-cases.test.ts new file mode 100644 index 0000000..11b7403 --- /dev/null +++ b/packages/cli/tests/unit/template-edge-cases.test.ts @@ -0,0 +1,122 @@ +import { expect, test, describe } from "vitest"; +import { generateInferredCode } from "../../src/templates/inferred.ts"; + +describe("Template Edge Cases", () => { + test("handles NSID with trailing numbers correctly", () => { + const schema = { + lexicon: 1, + id: "app.test.v1", + defs: { main: { type: "record" } }, + }; + + const code = generateInferredCode(schema, "/test/v1.json", "/output"); + expect(code).toContain("export type V1 = Infer"); + expect(code).toContain("export const V1Schema = schema"); + expect(code).toContain("export function isV1(v: unknown): v is V1"); + }); + + test("handles NSID with multiple consecutive separators", () => { + const schema = { + lexicon: 1, + id: "app.test.my--double--dash", + defs: { main: { type: "record" } }, + }; + + const code = generateInferredCode(schema, "/test/double.json", "/output"); + expect(code).toContain("export type MyDoubleDash = Infer"); + }); + + test("handles single character NSID parts", () => { + const schema = { + lexicon: 1, + id: "a.b.c", + defs: { main: { type: "record" } }, + }; + + const code = generateInferredCode(schema, "/test/single.json", "/output"); + expect(code).toContain("export type C = Infer"); + }); + + test("handles NSID with underscores and mixed case", () => { + const schema = { + lexicon: 1, + id: "app.test.my_custom_Type_Name", + defs: { main: { type: "record" } }, + }; + + const code = generateInferredCode(schema, "/test/custom.json", "/output"); + expect(code).toContain( + "export type MyCustomTypeName = Infer", + ); + }); + + test("handles very long NSID name", () => { + const longName = "a".repeat(100); + const schema = { + lexicon: 1, + id: `app.test.${longName}`, + defs: { main: { type: "record" } }, + }; + + const code = generateInferredCode(schema, "/test/long.json", "/output"); + // Should not crash and should generate valid TypeScript + expect(code).toContain("export type"); + expect(code).toContain("Infer"); + }); + + test("handles schema with no main def", () => { + const schema = { + lexicon: 1, + id: "app.test.no-main", + defs: { + other: { type: "object" }, + }, + }; + + const code = generateInferredCode(schema, "/test/no-main.json", "/output"); + // Should still generate valid code even without main def + expect(code).toContain("export type NoMain = Infer"); + // The path will be relative with ../../../ prefix + expect(code).toContain( + 'import schema from "../../../test/no-main.json" with { type: "json" };', + ); + }); + + test("generates correct relative paths for deeply nested output", () => { + const schema = { + lexicon: 1, + id: "app.bsky.feed.post", + defs: { main: { type: "record" } }, + }; + + const code = generateInferredCode( + schema, + "/project/schemas/feed.json", + "/project/generated/inferred", + ); + + // Should have correct relative import path + expect(code).toContain( + 'import schema from "../../../../../schemas/feed.json" with { type: "json" };', + ); + }); + + test("handles special characters in import paths", () => { + const schema = { + lexicon: 1, + id: "app.test.special", + defs: { main: { type: "record" } }, + }; + + const code = generateInferredCode( + schema, + "/project/schemas with spaces/special[chars].json", + "/project/generated", + ); + + // Should handle spaces and special characters in paths + expect(code).toContain( + 'import schema from "../../../schemas with spaces/special[chars].json" with { type: "json" };', + ); + }); +}); diff --git a/packages/site/tests/components/Playground.test.tsx b/packages/site/tests/components/Playground.test.tsx index 505606c..05e3378 100644 --- a/packages/site/tests/components/Playground.test.tsx +++ b/packages/site/tests/components/Playground.test.tsx @@ -84,9 +84,7 @@ describe("Playground", () => { const editors = screen.getAllByTestId("monaco-editor"); const inputEditor = editors[0] as HTMLTextAreaElement; - expect(inputEditor.value).toContain( - 'lx.lexicon("app.bsky.actor.profile"', - ); + expect(inputEditor.value).toContain('lx.lexicon("app.bsky.actor.profile"'); }); it("evaluates code and displays output", async () => {