UNPKG

astro

Version:

Astro is a modern site builder with web best practices, performance, and DX front-of-mind.

207 lines (206 loc) • 7.78 kB
import colors from "piccolore"; import devServer from "../../core/dev/index.js"; import { pathToFileURL } from "node:url"; import { checkExistingServer, killDevServer, removeLockFile, writeLockFile } from "../../core/dev/lockfile.js"; import { resolveRoot } from "../../core/config/config.js"; import { printHelp } from "../../core/messages/runtime.js"; import { isRunByAgent, supportsAgentAutoBackgrounding } from "../agent.js"; import { createLoggerFromFlags, flagsToAstroInlineConfig } from "../flags.js"; /** * `yargs-parser` camel-cases `--ignore-lock` to `flags.ignoreLock`. */ export function isIgnoreLock(flags) { return flags.ignoreLock === true; } /** * Returns an error message when `--ignore-lock` is combined with an explicit `--background`, * or `null` otherwise. A background server started with `--ignore-lock` could never be found * by `astro dev stop`/`status`/`logs` because the flag skips the lock file entirely. * Background implied by AI agent detection never reaches this check: the CLI drops it * whenever `--ignore-lock` is set. */ export function getBackgroundIgnoreLockConflict(flags) { if (!flags.background) { return null; } return [ "`--ignore-lock` cannot be used together with `--background`.", "", "Background dev servers rely on the lock file so `astro dev stop`, `astro dev status`, and `astro dev logs` can find them.", "Run the dev server in the foreground to use --ignore-lock." ].join("\n"); } /** * Pick the URL to record in the lock file. * * Vite only reports a `local` URL for loopback hosts. With `--host <custom-address>` set to a * specific non-loopback address the URL lands in `network` instead and `local` is empty, so * prefer `local` but fall back to `network` rather than reading `undefined`. * * Returns `null` when the server exposed no usable URL, which leaves the (purely bookkeeping) * lock file unwritten instead of taking down an otherwise healthy dev server. */ export function resolveLockFileUrl(resolvedUrls) { const resolved = resolvedUrls.local[0] ?? resolvedUrls.network[0]; if (!resolved) { return null; } try { return new URL(resolved).origin; } catch { return null; } } /** * `--force` (replace the existing server) and `--ignore-lock` (start alongside it, * untracked) express contradictory intent. Returns an error message if both are set, * or `null` otherwise. */ export function getForceIgnoreLockConflict(flags) { if (!flags.force) { return null; } return [ "`--force` and `--ignore-lock` cannot be used together.", "", "`--force` replaces the existing dev server; `--ignore-lock` starts a new one alongside it without touching the lock file. Choose one." ].join("\n"); } export async function dev({ flags }) { if (flags.help || flags.h) { printHelp({ commandName: "astro dev", usage: "[command] [...flags]", tables: { Commands: [ ["stop", "Stop a running background dev server."], ["status", "Check if a dev server is running."], ["logs [--follow]", "View logs from a background dev server."] ], Flags: [ ["--background", "Start the dev server as a background process."], ["--mode", `Specify the mode of the project. Defaults to "development".`], ["--port", `Specify which port to run on. Defaults to 4321.`], ["--host", `Listen on all addresses, including LAN and public addresses.`], ["--host <custom-address>", `Expose on a network IP address at <custom-address>`], ["--open", "Automatically open the app in the browser on server start"], ["--force", "Clear the content layer cache, forcing a full rebuild."], ["--ignore-lock", "Start the dev server even if another one is already running, without checking or writing the lock file."], ["--allowed-hosts", "Specify a comma-separated list of allowed hosts or allow any hostname."], ["--help (-h)", "See all available flags."] ] }, description: `Check ${colors.cyan("https://docs.astro.build/en/reference/cli-reference/#astro-dev")} for more information.` }); return; } const agentDetected = !process.env.ASTRO_DEV_BACKGROUND && isRunByAgent(); if (agentDetected) { flags.json = true; } const ignoreLock = isIgnoreLock(flags); // Agent-inferred background yields to `--ignore-lock`: the flag means a one-off // foreground server that `stop`/`status`/`logs` won't track. // https://github.com/withastro/astro/issues/17903 const wantsBackground = !!flags.background || supportsAgentAutoBackgrounding(process.platform) && agentDetected && !ignoreLock; const logger = createLoggerFromFlags(flags); const subcommand = flags._[3]?.toString(); // Handle `astro dev stop` if (subcommand === "stop") { const { stop } = await import("./stop.js"); await stop({ flags, logger }); return; } // Handle `astro dev status` if (subcommand === "status") { const { status } = await import("./status.js"); await status({ flags, logger }); return; } // Handle `astro dev logs` if (subcommand === "logs") { const { logs } = await import("./logs.js"); await logs({ flags, logger }); return; } // Reject conflicting flag combinations up front, before starting anything. if (ignoreLock) { const conflict = getBackgroundIgnoreLockConflict(flags) ?? getForceIgnoreLockConflict(flags); if (conflict) { throw new Error(conflict); } } if (wantsBackground) { const { background } = await import("./background.js"); await background({ flags, logger }); return; } // Unknown subcommand — exit with an error before starting the server. if (subcommand) { logger.error("SKIP_FORMAT", `Unknown command: astro dev ${subcommand}\n\nRun \`astro dev --help\` to see available commands.`); process.exit(1); } // Foreground dev server: check lock file, start server, write lock file const root = pathToFileURL(resolveRoot(flags.root) + "/"); // `--ignore-lock` opts this instance out of the lock file entirely: it doesn't block on // an existing server, and it won't be tracked by `astro dev stop`/`status`/`logs`. // We still do a read-only check purely to give the user a heads-up. if (ignoreLock) { const existingServer = await checkExistingServer(root); if (existingServer) { logger.info("SKIP_FORMAT", [`Starting a new dev server alongside the one already running at ${existingServer.url} (pid ${existingServer.pid}).`, "This instance is not tracked by `astro dev stop`, `astro dev status`, or `astro dev logs`."].join("\n")); } const inlineConfig = flagsToAstroInlineConfig(flags); return await devServer(inlineConfig); } const existingServer = await checkExistingServer(root); if (existingServer) { if (flags.force) { // --force: kill the existing server and replace it await killDevServer(root, existingServer); } else { const message = [ "Another astro dev server is already running.", "", ` URL: ${existingServer.url}`, ` PID: ${existingServer.pid}`, "", `Run \`astro dev stop\` to stop it, or use \`astro dev --force\` to replace it.` ].join("\n"); throw new Error(message); } } const inlineConfig = flagsToAstroInlineConfig(flags); const server = await devServer(inlineConfig); // Use Vite's resolved URL which accounts for host and protocol (http/https). const serverUrl = resolveLockFileUrl(server.resolvedUrls); if (serverUrl) { writeLockFile(root, { pid: process.pid, port: server.address.port, url: serverUrl, urls: server.resolvedUrls, background: !!process.env.ASTRO_DEV_BACKGROUND, startedAt: new Date().toISOString() }); // Wrap the original stop to also clean up the lock file const originalStop = server.stop.bind(server); server.stop = async () => { removeLockFile(root); await originalStop(); }; } return server; }