All files / lib/prepare bundle.ts

100% Statements 229/229
94.11% Branches 32/34
100% Functions 7/7
100% Lines 229/229

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 2301x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 41x 41x 41x 41x 41x 41x 41x 41x 41x 41x 41x 41x 41x 1x 1x 1x 1x 1x 1x 12x 12x 9x 9x 12x 12x 12x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 12x 12x 12x 12x 12x 12x 1x 1x 1x 1x 1x 1x 1x 1x 12x 12x 1x 1x 1x 1x 1x 1x 1x 1x 1x 17x 17x 17x 17x 17x 17x 17x 17x 17x 17x 17x 17x 17x 17x 1x 1x 1x 1x 1x 17x 17x 1x 1x 1x 1x 1x 1x 1x 1x 2x 33x 17x 17x 17x 17x 33x 2x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 9x 9x 9x 9x 747x 747x 207x 207x 747x 540x 747x 711x 711x 747x 9x 9x 540x 540x 9x 9x  
"use strict";
/**
 * Prepare the SDK once at server start-up: bundle it into a single ES module
 * for the Deno executor, and produce TypeScript declarations used to
 * type-check agent code. All output goes to the oag-mcp workspace; the SDK
 * is only read.
 *
 * @module prepare/bundle
 */
import fs from "node:fs";
import { createRequire } from "node:module";
import path from "node:path";
 
import * as esbuild from "esbuild";
import ts from "typescript";
 
import { OagMcpError } from "../errors.js";
import { listFiles } from "../util/hash.js";
 
const require = createRequire(import.meta.url);
 
/** Exports the runner requires from every SDK bundle. */
export const REQUIRED_RUNTIME_EXPORTS = ["Configuration", "BaseAPI"];
 
/** TypeScript libs made available to agent code. */
export const AGENT_TS_LIBS = [
  "lib.es2022.d.ts",
  "lib.dom.d.ts",
  "lib.dom.iterable.d.ts",
  "lib.dom.asynciterable.d.ts",
];
 
/** Compiler options used for SDK declarations and agent type checks. */
export function sdkCompilerOptions(): ts.CompilerOptions {
  return {
    target: ts.ScriptTarget.ES2022,
    module: ts.ModuleKind.ESNext,
    moduleResolution: ts.ModuleResolutionKind.Bundler,
    lib: AGENT_TS_LIBS,
    types: [],
    strict: false,
    skipLibCheck: true,
    allowJs: false,
    noEmitOnError: false,
    esModuleInterop: true,
  };
}
 
/**
 * Whether a JavaScript entry is CommonJS.
 *
 * @private
 */
function _isCommonJs(entry: string): boolean {
  if (entry.endsWith(".ts")) {
    return false;
  }
  const text = fs.readFileSync(entry, "utf8");
  return !/^\s*export\s/m.test(text) && /\b(exports\.|require\()/.test(text);
}
 
/**
 * Bundle the SDK into a single ESM file with all runtime dependencies.
 *
 * @param {Object} opts - options
 * @param {String} opts.entry - SDK entry module
 * @param {String} opts.root - SDK package root (for resolution; read-only)
 * @param {String} opts.outFile - bundle output path (in the workspace)
 * @param {String} opts.shimDir - workspace directory for a CommonJS entry shim
 * @param {Array} opts.classNames - API classes that must be exported
 * @return {Promise} { exports, bytes }
 */
export async function bundleSdk(opts: {
  entry: string;
  root: string;
  outFile: string;
  shimDir: string;
  classNames: string[];
}): Promise<{ exports: string[]; bytes: number }> {
  fs.mkdirSync(path.dirname(opts.outFile), { recursive: true });
  let entry = opts.entry;
  if (_isCommonJs(entry)) {
    // CommonJS entries lose named exports when bundled to ESM; re-export the
    // names the runner needs from a generated ESM shim.
    fs.mkdirSync(opts.shimDir, { recursive: true });
    const shim = path.join(opts.shimDir, "__oag_mcp_entry.mjs");
    const names = [
      ...new Set([
        ...REQUIRED_RUNTIME_EXPORTS,
        "ResponseError",
        "FetchError",
        "RequiredError",
        ...opts.classNames,
      ]),
    ];
    fs.writeFileSync(
      shim,
      `import __sdk from ${JSON.stringify(entry.split(path.sep).join("/"))};\n` +
        names.map((n) => `export const ${n} = __sdk.${n};`).join("\n") +
        "\n",
    );
    entry = shim;
  }
  let result: esbuild.BuildResult<{ metafile: true }>;
  try {
    result = await esbuild.build({
      entryPoints: [entry],
      absWorkingDir: opts.root,
      bundle: true,
      format: "esm",
      platform: "neutral",
      target: "es2022",
      mainFields: ["module", "main"],
      conditions: ["import", "default"],
      outfile: opts.outFile,
      metafile: true,
      write: true,
      logLevel: "silent",
      legalComments: "inline",
      banner: {
        js: "// oag-mcp SDK execution bundle. Generated from the user's OpenAPI Generator typescript-fetch SDK; do not edit.",
      },
    });
  } catch (err) {
    const errors = (err as esbuild.BuildFailure).errors ?? [];
    throw new OagMcpError(
      "SDK_BUNDLE_FAILED",
      `Bundling the SDK failed: ${errors
        .slice(0, 5)
        .map((e) => e.text)
        .join("; ")}`,
      {
        hint: "If the SDK has runtime dependencies, install them in the SDK directory yourself (e.g. `npm install --omit=dev --ignore-scripts`); oag-mcp never installs packages. Node.js built-in modules cannot be used by the sandboxed SDK.",
        cause: err,
      },
    );
  }
  const out = Object.values(result.metafile.outputs)[0];
  const exports = out?.exports ?? [];
  const missing = [...REQUIRED_RUNTIME_EXPORTS, ...opts.classNames].filter(
    (n) => !exports.includes(n),
  );
  if (missing.length > 0) {
    throw new OagMcpError(
      "SDK_BUNDLE_EXPORTS",
      `The SDK bundle does not export: ${missing.join(", ")}`,
      {
        hint: "The SDK index module must re-export runtime and apis (typescript-fetch does this by default). Check for a modified index.ts.",
      },
    );
  }
  return { exports, bytes: fs.statSync(opts.outFile).size };
}
 
/**
 * Emit TypeScript declarations for an SDK in source layout.
 *
 * @param {String} entry - SDK index.ts
 * @param {String} outDir - declarations output directory
 * @return {Array} non-fatal diagnostics
 */
export function emitDeclarations(entry: string, outDir: string): string[] {
  const options: ts.CompilerOptions = {
    ...sdkCompilerOptions(),
    declaration: true,
    emitDeclarationOnly: true,
    rootDir: path.dirname(entry),
    outDir,
  };
  const program = ts.createProgram([entry], options);
  const emit = program.emit();
  const diags = [...ts.getPreEmitDiagnostics(program), ...emit.diagnostics];
  const messages = diags
    .filter((d) => d.category === ts.DiagnosticCategory.Error)
    .map((d) => ts.flattenDiagnosticMessageText(d.messageText, "\n"));
  if (emit.emitSkipped || !fs.existsSync(path.join(outDir, "index.d.ts"))) {
    throw new OagMcpError(
      "SDK_DECLARATIONS_FAILED",
      `Emitting SDK declarations failed: ${messages.slice(0, 5).join("; ")}`,
    );
  }
  return messages;
}
 
/**
 * Copy compiled declarations into the workspace.
 *
 * @param {String} typesDir - SDK declarations directory (e.g. dist)
 * @param {String} outDir - workspace types directory
 */
export function copyDeclarations(typesDir: string, outDir: string): void {
  for (const rel of listFiles(typesDir, (r) => !r.startsWith("esm/"))) {
    if (rel.endsWith(".d.ts")) {
      const dest = path.join(outDir, rel);
      fs.mkdirSync(path.dirname(dest), { recursive: true });
      fs.copyFileSync(path.join(typesDir, rel), dest);
    }
  }
}
 
/**
 * Copy the transitive closure of TypeScript lib files needed for agent
 * type checks, so type checking works without the TypeScript package's lib
 * directory at runtime.
 *
 * @param {String} outDir - destination directory
 * @return {Array} copied file names
 */
export function copyTsLibs(outDir: string): string[] {
  const libDir = path.dirname(require.resolve("typescript/lib/lib.d.ts"));
  const seen = new Set<string>();
  const queue = [...AGENT_TS_LIBS];
  while (queue.length > 0) {
    const name = queue.shift()!;
    if (seen.has(name)) {
      continue;
    }
    seen.add(name);
    const text = fs.readFileSync(path.join(libDir, name), "utf8");
    for (const m of text.matchAll(/\/\/\/\s*<reference\s+lib="([^"]+)"/g)) {
      queue.push(`lib.${m[1].toLowerCase()}.d.ts`);
    }
  }
  fs.mkdirSync(outDir, { recursive: true });
  for (const name of seen) {
    fs.copyFileSync(path.join(libDir, name), path.join(outDir, name));
  }
  return [...seen].sort();
}