import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync, existsSync, appendFileSync, statSync, } from "fs"; import { join } from "path"; import { tmpdir } from "os"; import { createGenerator, type Config } from "ts-json-schema-generator"; import { maxSatisfying, rsort, valid } from "semver"; import { x as extractTar } from "tar"; import type { JSONSchema7 } from "json-schema"; import { fetchWithCache } from "./cache.js"; const REGISTRY_URL = "https://registry.npmjs.org/ai"; const TARGET_TYPE = "UIMessage"; const DEFAULT_MAJOR = 6; const RESOURCE_DIR = join(import.meta.dirname, ".."); const OUTPUT_DIR = join(RESOURCE_DIR, "artifacts", "json-schema"); const OUTPUT_PATH = join(OUTPUT_DIR, "ui-message.json"); const SCHEMA_ID = "https://sandbox-agent/schemas/vercel-ai-sdk/ui-message.json"; interface RegistryResponse { versions?: Record< string, { dist?: { tarball?: string }; dependencies?: Record; peerDependencies?: Record; } >; "dist-tags"?: Record; } interface Args { version: string | null; major: number; } function parseArgs(): Args { const args = process.argv.slice(2); const versionArg = args.find((arg) => arg.startsWith("--version=")); const majorArg = args.find((arg) => arg.startsWith("--major=")); const version = versionArg ? versionArg.split("=")[1] : null; const major = majorArg ? Number(majorArg.split("=")[1]) : DEFAULT_MAJOR; return { version, major: Number.isFinite(major) && major > 0 ? major : DEFAULT_MAJOR, }; } function log(message: string): void { console.log(message); } function ensureOutputDir(): void { if (!existsSync(OUTPUT_DIR)) { mkdirSync(OUTPUT_DIR, { recursive: true }); } } async function fetchRegistry(url: string): Promise { const registry = await fetchWithCache(url); return JSON.parse(registry) as RegistryResponse; } function resolveLatestVersion(registry: RegistryResponse, major: number): string { const versions = Object.keys(registry.versions ?? {}); const candidates = versions.filter((version) => valid(version) && version.startsWith(`${major}.`)); const sorted = rsort(candidates); if (sorted.length === 0) { throw new Error(`No versions found for major ${major}`); } return sorted[0]; } function resolveVersionFromRange(registry: RegistryResponse, range: string): string { if (registry.versions?.[range]) { return range; } const versions = Object.keys(registry.versions ?? {}).filter((version) => valid(version)); const resolved = maxSatisfying(versions, range); if (!resolved) { throw new Error(`No versions satisfy range ${range}`); } return resolved; } async function downloadTarball(url: string, destination: string): Promise { const response = await fetch(url); if (!response.ok) { throw new Error(`Failed to download tarball: ${response.status} ${response.statusText}`); } const buffer = Buffer.from(await response.arrayBuffer()); writeFileSync(destination, buffer); } async function extractPackage(tarballPath: string, targetDir: string): Promise { mkdirSync(targetDir, { recursive: true }); await extractTar({ file: tarballPath, cwd: targetDir, strip: 1, }); } function packageDirFor(name: string, nodeModulesDir: string): string { const parts = name.split("/"); return join(nodeModulesDir, ...parts); } async function installPackage( name: string, versionRange: string, nodeModulesDir: string, installed: Set ): Promise { const encodedName = name.startsWith("@") ? `@${encodeURIComponent(name.slice(1))}` : encodeURIComponent(name); const registryUrl = `https://registry.npmjs.org/${encodedName}`; const registry = await fetchRegistry(registryUrl); const version = resolveVersionFromRange(registry, versionRange); const installKey = `${name}@${version}`; if (installed.has(installKey)) { return; } installed.add(installKey); const tarball = registry.versions?.[version]?.dist?.tarball; if (!tarball) { throw new Error(`No tarball found for ${installKey}`); } const tempDir = mkdtempSync(join(tmpdir(), "vercel-ai-sdk-dep-")); const tarballPath = join(tempDir, `${name.replace("/", "-")}-${version}.tgz`); const packageDir = packageDirFor(name, nodeModulesDir); try { await downloadTarball(tarball, tarballPath); await extractPackage(tarballPath, packageDir); const packageJsonPath = join(packageDir, "package.json"); const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8")) as { dependencies?: Record; peerDependencies?: Record; }; const dependencies = { ...packageJson.dependencies, ...packageJson.peerDependencies, }; for (const [depName, depRange] of Object.entries(dependencies)) { await installPackage(depName, depRange, nodeModulesDir, installed); } } finally { rmSync(tempDir, { recursive: true, force: true }); } } function writeTempTsconfig(tempDir: string): string { const tsconfigPath = join(tempDir, "tsconfig.json"); const tsconfig = { compilerOptions: { target: "ES2022", module: "NodeNext", moduleResolution: "NodeNext", strict: true, skipLibCheck: true, esModuleInterop: true, }, }; writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2)); return tsconfigPath; } function writeEntryFile(tempDir: string): string { const entryPath = join(tempDir, "entry.ts"); const contents = `import type { ${TARGET_TYPE} as AI${TARGET_TYPE} } from "ai";\nexport type ${TARGET_TYPE} = AI${TARGET_TYPE};\n`; writeFileSync(entryPath, contents); return entryPath; } function patchValueOfAlias(nodeModulesDir: string): void { const aiTypesPath = join(nodeModulesDir, "ai", "dist", "index.d.ts"); if (!existsSync(aiTypesPath)) { log(" [warn] ai types not found for ValueOf patch"); return; } const contents = readFileSync(aiTypesPath, "utf-8"); const valueOfMatch = contents.match(/type ValueOf[\\s\\S]*?;/); if (valueOfMatch) { const snippet = valueOfMatch[0].replace(/\\s+/g, " ").slice(0, 200); log(` [debug] ValueOf alias snippet: ${snippet}`); } else { log(" [warn] ValueOf alias declaration not found"); } let patched = contents.replace( /ObjectType\\s*\\[\\s*ValueType\\s*\\]/, "ObjectType[string]" ); if (patched !== contents) { writeFileSync(aiTypesPath, patched); log(" [patch] Adjusted ValueOf alias for schema generation"); return; } const valueOfIndex = contents.indexOf("ValueOf"); const preview = valueOfIndex === -1 ? contents.slice(0, 200) : contents.slice(valueOfIndex, valueOfIndex + 200); log(" [warn] ValueOf alias not found in ai types"); log(` [debug] ai types path: ${aiTypesPath}`); log(` [debug] preview: ${preview.replace(/\\s+/g, " ").slice(0, 200)}`); } function ensureTypeFestShim(nodeModulesDir: string): void { const typeFestDir = join(nodeModulesDir, "type-fest"); if (!existsSync(typeFestDir)) { mkdirSync(typeFestDir, { recursive: true }); } const packageJsonPath = join(typeFestDir, "package.json"); const typesPath = join(typeFestDir, "index.d.ts"); if (!existsSync(packageJsonPath)) { const pkg = { name: "type-fest", version: "0.0.0", types: "index.d.ts", }; writeFileSync(packageJsonPath, JSON.stringify(pkg, null, 2)); } const shim = `export type ValueOf<\n ObjectType,\n ValueType extends keyof ObjectType = keyof ObjectType,\n> = ObjectType[string];\n\nexport type Simplify = { [KeyType in keyof T]: T[KeyType] } & {};\n`; writeFileSync(typesPath, shim); log(" [shim] Wrote type-fest ValueOf shim"); } function generateSchema(entryPath: string, tsconfigPath: string): JSONSchema7 { const config: Config = { path: entryPath, tsconfig: tsconfigPath, type: TARGET_TYPE, expose: "export", skipTypeCheck: true, }; const generator = createGenerator(config); return generator.createSchema(TARGET_TYPE) as JSONSchema7; } function addSchemaMetadata(schema: JSONSchema7, version: string): JSONSchema7 { const withMeta: JSONSchema7 = { ...schema, $schema: schema.$schema ?? "http://json-schema.org/draft-07/schema#", $id: SCHEMA_ID, title: schema.title ?? TARGET_TYPE, description: schema.description ?? `Vercel AI SDK v${version} ${TARGET_TYPE}`, }; return withMeta; } function loadFallback(): JSONSchema7 | null { if (!existsSync(OUTPUT_PATH)) { return null; } try { const content = readFileSync(OUTPUT_PATH, "utf-8"); return JSON.parse(content) as JSONSchema7; } catch { return null; } } function patchUiMessageTypes(nodeModulesDir: string): void { const aiTypesPath = join(nodeModulesDir, "ai", "dist", "index.d.ts"); if (!existsSync(aiTypesPath)) { log(" [warn] ai types not found for UIMessage patch"); return; } const contents = readFileSync(aiTypesPath, "utf-8"); let patched = contents; const replaceAlias = (typeName: string, replacement: string): boolean => { const start = patched.indexOf(`type ${typeName}`); if (start === -1) { log(` [warn] ${typeName} alias not found for patch`); return false; } const end = patched.indexOf(";", start); if (end === -1) { log(` [warn] ${typeName} alias not terminated`); return false; } const snippet = patched.slice(start, Math.min(end + 1, start + 400)).replace(/\\s+/g, " "); log(` [debug] ${typeName} alias snippet: ${snippet}`); patched = patched.slice(0, start) + replacement + patched.slice(end + 1); return true; }; const dataReplaced = replaceAlias( "DataUIPart", "type DataUIPart = {\\n type: `data-${string}`;\\n id?: string;\\n data: unknown;\\n};" ); if (dataReplaced) { log(" [patch] Simplified DataUIPart to avoid indexed access"); } const toolReplaced = replaceAlias( "ToolUIPart", "type ToolUIPart = {\\n type: `tool-${string}`;\\n} & UIToolInvocation;" ); if (toolReplaced) { log(" [patch] Simplified ToolUIPart to avoid indexed access"); } if (patched !== contents) { writeFileSync(aiTypesPath, patched); } } async function main(): Promise { log("Vercel AI SDK UIMessage Schema Extractor"); log("========================================\n"); const args = parseArgs(); ensureOutputDir(); const registry = await fetchRegistry(REGISTRY_URL); const version = args.version ?? resolveLatestVersion(registry, args.major); log(`Target version: ai@${version}`); const tempDir = mkdtempSync(join(tmpdir(), "vercel-ai-sdk-")); const nodeModulesDir = join(tempDir, "node_modules"); try { log(` [debug] temp dir: ${tempDir}`); await installPackage("ai", version, nodeModulesDir, new Set()); ensureTypeFestShim(nodeModulesDir); patchUiMessageTypes(nodeModulesDir); patchValueOfAlias(nodeModulesDir); const tsconfigPath = writeTempTsconfig(tempDir); const entryPath = writeEntryFile(tempDir); log(` [debug] entry path: ${entryPath}`); log(` [debug] tsconfig path: ${tsconfigPath}`); if (existsSync(entryPath)) { const entryStat = statSync(entryPath); log(` [debug] entry size: ${entryStat.size}`); } const schema = generateSchema(entryPath, tsconfigPath); const schemaWithMeta = addSchemaMetadata(schema, version); writeFileSync(OUTPUT_PATH, JSON.stringify(schemaWithMeta, null, 2)); log(`\n [wrote] ${OUTPUT_PATH}`); } catch (error) { const message = error instanceof Error ? error.message : String(error); log(`\n [error] ${message}`); if (error instanceof Error && error.stack) { log(error.stack); } const fallback = loadFallback(); if (fallback) { log(" [fallback] Keeping existing schema artifact"); return; } process.exitCode = 1; } finally { rmSync(tempDir, { recursive: true, force: true }); } } main().catch((error) => { console.error("Fatal error:", error); process.exit(1); });