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 BuildSettings, resolveBuildSettings } from "./build-settings.ts"; import type { BuildArtifact, BuildStrategy } from "./build-strategy.ts"; import { hasPackageDependency } from "./build-strategy.ts"; import { defaultHttpPortForBuildType } from "./config/frameworks.ts"; import { type BuildCommandIo, runBuildCommand } from "./workspace.ts"; const TANSTACK_START_PACKAGES = [ "@tanstack/react-start", "@tanstack/solid-start", ]; /** * Build strategy for TanStack Start applications targeting the Nitro node * preset. Runs the resolved build command (the user's `package.json` build * script, or `vite build`) and produces an artifact from its output directory. */ export class TanstackStartBuild implements BuildStrategy { readonly #appPath: string; readonly #buildSettings?: BuildSettings; readonly #io?: BuildCommandIo; constructor(options: { appPath: string; buildSettings?: BuildSettings; io?: BuildCommandIo; }) { this.#appPath = options.appPath; this.#buildSettings = options.buildSettings; this.#io = options.io; } async canBuild(signal?: AbortSignal): Promise { return hasPackageDependency(this.#appPath, TANSTACK_START_PACKAGES, signal); } async execute(signal?: AbortSignal): Promise { signal?.throwIfAborted(); const settings = this.#buildSettings ?? (await resolveBuildSettings({ appPath: this.#appPath, buildType: "tanstack-start", signal, })); if (settings.buildCommand) { await runBuildCommand({ appPath: this.#appPath, command: settings.buildCommand, failurePrefix: "TanStack Start", env: this.#io?.env, onOutput: this.#io?.onOutput, signal, }); } const outputDir = path.join(this.#appPath, settings.outputDirectory); const entrypoint = "server/index.mjs"; const entryStat = await stat(path.join(outputDir, entrypoint)).catch( () => null, ); if (!entryStat?.isFile()) { throw new Error( `TanStack Start build did not produce a Nitro node server entrypoint at ${settings.outputDirectory}/${entrypoint}. ` + "Ensure your vite.config includes the tanstackStart() and nitro() plugins with the default node preset.", ); } const outDir = await mkdtemp(path.join(os.tmpdir(), "compute-build-")); try { const artifactDir = path.join(outDir, "app"); await cp(outputDir, 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, defaultPortMapping: { http: defaultHttpPortForBuildType("tanstack-start"), }, cleanup: () => rm(outDir, { recursive: true, force: true }), }; } catch (error) { await rm(outDir, { recursive: true, force: true }); throw error; } } }