import { access, readFile } from "node:fs/promises"; import path from "node:path"; import { type ComputeAppManifest, computeAppCandidateFilePaths, type DetectedComputeApp, detectComputeApp, } from "./detect-app.ts"; import { frameworkByKey } from "./frameworks.ts"; export interface DetectComputeAppFromDirectoryInput { appPath: string; signal?: AbortSignal; } export interface DetectedComputeDirectoryApp extends DetectedComputeApp { configFile: { path: string; standaloneOutput: boolean; } | null; } /** Detects one Compute app from a local directory. */ export async function detectComputeAppFromDirectory( input: DetectComputeAppFromDirectoryInput, ): Promise { const manifest = await readManifest(input.appPath, input.signal); const candidates = computeAppCandidateFilePaths(manifest); const existingCandidates = await Promise.all( candidates.map(async (candidate) => (await fileExists(input.appPath, candidate, input.signal)) ? candidate : null, ), ); const filePaths = new Set( existingCandidates.filter((candidate) => candidate !== null), ); const detected = detectComputeApp({ root: "", manifest, filePaths }); if (!detected) return null; const descriptor = frameworkByKey(detected.framework); const configPath = descriptor.detectConfigFiles.find((candidate) => filePaths.has(candidate), ); if (!configPath) { return { ...detected, configFile: null }; } let content: string; try { content = await readFile(path.join(input.appPath, configPath), { encoding: "utf8", signal: input.signal, }); } catch (error) { if (input.signal?.aborted) throw error; if ((error as NodeJS.ErrnoException).code === "ENOENT") { return { ...detected, configFile: null }; } throw error; } return { ...detected, configFile: { path: configPath, standaloneOutput: detected.framework === "nextjs" && /\boutput\s*:\s*["'`]standalone["'`]/.test(content), }, }; } async function readManifest( appPath: string, signal?: AbortSignal, ): Promise { signal?.throwIfAborted(); try { const content = await readFile(path.join(appPath, "package.json"), { encoding: "utf8", signal, }); const parsed = JSON.parse(content) as unknown; return parsed && typeof parsed === "object" ? (parsed as ComputeAppManifest) : {}; } catch (error) { if (signal?.aborted) throw error; if ((error as NodeJS.ErrnoException).code === "ENOENT") return {}; throw error; } } async function fileExists( appPath: string, relativePath: string, signal?: AbortSignal, ): Promise { signal?.throwIfAborted(); try { await access(path.join(appPath, relativePath)); signal?.throwIfAborted(); return true; } catch (error) { if (signal?.aborted) throw error; if ((error as NodeJS.ErrnoException).code === "ENOENT") return false; throw error; } }