UNPKG

browse

Version:

Unified Browserbase CLI for browser automation and cloud APIs.

163 lines (162 loc) 6.08 kB
import archiver from "archiver"; import ignore from "ignore"; import { copyFileSync, createWriteStream, existsSync, mkdirSync, readFileSync, rmSync, } from "node:fs"; import { readFile, readdir, stat } from "node:fs/promises"; import { spawnSync } from "node:child_process"; import { tmpdir } from "node:os"; import { dirname, join, relative } from "node:path"; import { randomUUID } from "node:crypto"; import { fail } from "../errors.js"; import { functionsGet, functionsRequest, pollUntil, resolveEntrypoint, resolveFunctionsApiConfig, } from "./shared.js"; const defaultIgnorePatterns = [ "node_modules/", ".git/", ".env", ".env.*", "*.log", ".DS_Store", "dist/", "build/", "*.zip", "*.tar", "*.tar.gz", ".vscode/", ".idea/", ".browserbase/", ]; export async function publishFunction(options) { const entrypoint = await resolveEntrypoint(options.entrypoint); const config = resolveFunctionsApiConfig(options); const entrypointPath = relative(process.cwd(), entrypoint); if (options.dryRun) { const entries = await listPublishEntries(process.cwd()); console.log(JSON.stringify({ archivePath: null, baseUrl: config.baseUrl, dryRun: true, entrypoint: entrypointPath, files: entries, }, null, 2)); return; } const { archivePath } = await createArchive(process.cwd()); try { const formData = new FormData(); formData.append("metadata", JSON.stringify({ entrypoint: entrypointPath })); formData.append("archive", new Blob([await readFile(archivePath)], { type: "application/gzip" }), "archive.tar.gz"); const uploadResponse = await functionsRequest(config, "/v1/functions/builds", { method: "POST", body: formData, }); const uploaded = (await uploadResponse.json()); if (!uploaded.id) { fail("Build upload completed without returning a build ID."); } const build = await pollUntil(() => functionsGet(config, `/v1/functions/builds/${uploaded.id}`), { done: (result) => !["PENDING", "RUNNING"].includes(result.status), intervalMs: 2_000, maxAttempts: 100, }); console.log(JSON.stringify(build, null, 2)); if (build.status === "FAILED") { process.exitCode = 1; } } finally { rmSync(archivePath, { force: true }); } } async function createArchive(root) { const archivePath = join(tmpdir(), `browserbase-functions-${randomUUID()}.tar.gz`); const sourceEntries = await listPublishEntries(root); const { entries, generatedLockfilePath } = ensureArchiveLockfile(root, sourceEntries); try { await new Promise((resolvePromise, reject) => { const output = createWriteStream(archivePath); const archive = archiver("tar", { gzip: true, gzipOptions: { level: 9 }, }); archive.on("error", reject); archive.on("warning", (warning) => { if (warning.code === "ENOENT") { return; } reject(warning); }); output.on("close", () => resolvePromise()); output.on("error", reject); archive.pipe(output); for (const entry of entries) { if (entry === "package-lock.json" && generatedLockfilePath) { archive.file(generatedLockfilePath, { name: entry }); } else { archive.file(join(root, entry), { name: entry }); } } archive.finalize().catch(reject); }); } finally { if (generatedLockfilePath) { rmSync(dirname(generatedLockfilePath), { recursive: true, force: true }); } } return { archivePath, entries }; } async function listPublishEntries(root) { const ignoreMatcher = await loadIgnoreMatcher(root); return await listArchiveEntries(root, root, ignoreMatcher); } function ensureArchiveLockfile(root, entries) { if (!entries.includes("package.json") || entries.includes("package-lock.json")) { return { entries }; } const tempDir = join(tmpdir(), `bb-functions-lockgen-${randomUUID()}`); mkdirSync(tempDir, { recursive: true }); copyFileSync(join(root, "package.json"), join(tempDir, "package.json")); const result = spawnSync("npm", ["install", "--package-lock-only"], { cwd: tempDir, stdio: "pipe", }); if (result.status !== 0) { rmSync(tempDir, { recursive: true, force: true }); fail("Failed to generate package-lock.json for the Functions build archive."); } return { entries: [...entries, "package-lock.json"].sort(), generatedLockfilePath: join(tempDir, "package-lock.json"), }; } async function loadIgnoreMatcher(root) { const matcher = ignore(); matcher.add(defaultIgnorePatterns); const gitignorePath = join(root, ".gitignore"); if (existsSync(gitignorePath)) { matcher.add(readFileSync(gitignorePath, "utf8")); } return matcher; } async function listArchiveEntries(root, current, matcher) { const entries = await readdir(current, { withFileTypes: true }); const files = []; for (const entry of entries) { const absolutePath = join(current, entry.name); const relativePath = relative(root, absolutePath) || "."; const ignorePath = entry.isDirectory() ? `${relativePath}/` : relativePath; if (relativePath !== "." && matcher.ignores(ignorePath)) { continue; } if (entry.isDirectory()) { files.push(...(await listArchiveEntries(root, absolutePath, matcher))); continue; } const fileStats = await stat(absolutePath); if (fileStats.isFile()) { files.push(relativePath); } } return files.sort(); }