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
JavaScript
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;
}