import { cp, mkdtemp, rm, stat } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { normalizeArtifactSymlinks } from "./artifact-postprocess.ts"; import type { BuildArtifact, BuildStrategy } from "./build-strategy.ts"; import { hasPackageDependency, hasRootFile, runPackageCli, } from "./build-strategy.ts"; import { defaultHttpPortForBuildType } from "./config/frameworks.ts"; import type { BuildCommandIo } from "./workspace.ts"; const ASTRO_CONFIG_FILENAMES = [ "astro.config.js", "astro.config.mjs", "astro.config.cjs", "astro.config.ts", "astro.config.mts", ]; /** * Build strategy for Astro applications using the @astrojs/node adapter * in standalone mode. Runs `astro build` and produces an artifact from `dist/`. */ export class AstroBuild implements BuildStrategy { readonly #appPath: string; readonly #io?: BuildCommandIo; constructor(options: { appPath: string; io?: BuildCommandIo }) { this.#appPath = options.appPath; this.#io = options.io; } async canBuild(signal?: AbortSignal): Promise { return ( (await hasRootFile(this.#appPath, ASTRO_CONFIG_FILENAMES, signal)) || (await hasPackageDependency(this.#appPath, ["astro"], signal)) ); } async execute(signal?: AbortSignal): Promise { signal?.throwIfAborted(); await runPackageCli({ appPath: this.#appPath, cliName: "astro", args: ["build"], failurePrefix: "Astro", missingMessage: "Could not find the Astro CLI. Install it with `npm install astro` or ensure npx/bunx is available.", env: this.#io?.env, onOutput: this.#io?.onOutput, signal, }); const distDir = path.join(this.#appPath, "dist"); const entryPath = path.join(distDir, "server", "entry.mjs"); const entryStat = await stat(entryPath).catch(() => null); if (!entryStat?.isFile()) { throw new Error( 'Astro build did not produce a standalone server entrypoint. Install @astrojs/node and configure it with adapter: node({ mode: "standalone" }) in your astro.config file.', ); } const outDir = await mkdtemp(path.join(os.tmpdir(), "compute-build-")); try { const artifactDir = path.join(outDir, "app"); await cp(distDir, artifactDir, { recursive: true, verbatimSymlinks: true, }); // Materialize any symlinks into the app/workspace node_modules so the // artifact is self-contained once unpacked elsewhere. await normalizeArtifactSymlinks(artifactDir, this.#appPath, signal); return { directory: artifactDir, entrypoint: "server/entry.mjs", defaultPortMapping: { http: defaultHttpPortForBuildType("astro") }, cleanup: () => rm(outDir, { recursive: true, force: true }), }; } catch (error) { await rm(outDir, { recursive: true, force: true }); throw error; } } }