66 lines · 2.5 KB · TypeScript
RawDownload
import * as ts from "typescript"; export type OutputTypeScriptContract = {  readonly outputTypeScript?: string;  readonly typeScriptDefinitions?: Record<string, string>;}; export type TypeCheckOutputTypeScriptOptions = {  readonly consumerSource?: string;  readonly fileName?: string;  readonly typeName?: string;  readonly valueName?: string;}; export const typeCheckOutputTypeScript = (  contract: OutputTypeScriptContract | null | undefined,  runtimeOutput: unknown,  options: TypeCheckOutputTypeScriptOptions = {},): readonly string[] => {  if (!contract?.outputTypeScript) {    return ["missing outputTypeScript"];  }   const fileName = options.fileName ?? "tool-output-contract.ts";  const typeName = options.typeName ?? "ToolOutput";  const valueName = options.valueName ?? "invokedOutput";  const source = [    ...Object.entries(contract.typeScriptDefinitions ?? {}).map(      ([name, definition]) => `type ${name} = ${definition};`,    ),    `type ${typeName} = ${contract.outputTypeScript};`,    `const ${valueName}: ${typeName} = ${JSON.stringify(runtimeOutput)};`,    options.consumerSource ?? `${valueName};`,  ].join("\n");   const compilerOptions: ts.CompilerOptions = {    module: ts.ModuleKind.ESNext,    noEmit: true,    skipLibCheck: true,    strict: true,    target: ts.ScriptTarget.ES2022,  };  const host = ts.createCompilerHost(compilerOptions);  const originalGetSourceFile = host.getSourceFile.bind(host);  const originalReadFile = host.readFile.bind(host);  const originalFileExists = host.fileExists.bind(host);   host.getSourceFile = (candidate, languageVersion, onError, shouldCreateNewSourceFile) => {    if (candidate === fileName) {      return ts.createSourceFile(candidate, source, languageVersion, true);    }    return originalGetSourceFile(candidate, languageVersion, onError, shouldCreateNewSourceFile);  };  host.readFile = (candidate) => (candidate === fileName ? source : originalReadFile(candidate));  host.fileExists = (candidate) => candidate === fileName || originalFileExists(candidate);   const program = ts.createProgram([fileName], compilerOptions, host);  return ts.getPreEmitDiagnostics(program).map((diagnostic) => {    const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n");    if (!diagnostic.file || diagnostic.start === undefined) {      return message;    }    const position = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);    return `${diagnostic.file.fileName}:${position.line + 1}:${position.character + 1} ${message}`;  });};