astro
Version:
Astro is a modern site builder with web best practices, performance, and DX front-of-mind.
244 lines (243 loc) • 9.18 kB
JavaScript
import { fileURLToPath } from "node:url";
import * as vite from "vite";
import { globalContentLayer } from "../../content/instance.js";
import { attachContentServerListeners } from "../../content/server-listeners.js";
import { eventCliSession, telemetry } from "../../events/index.js";
import { runHookConfigDone, runHookConfigSetup } from "../../integrations/hooks.js";
import { SETTINGS_FILE } from "../../preferences/constants.js";
import { getPrerenderDefault } from "../../prerender/utils.js";
import { createSettings, resolveConfig } from "../config/index.js";
import { clearCrawlCache, createVite } from "../create-vite.js";
import { collectErrorMetadata } from "../errors/dev/utils.js";
import { isAstroConfigZodError } from "../errors/errors.js";
import { createSafeError } from "../errors/index.js";
import { loadOrCreateNodeLogger } from "../logger/load.js";
import { formatErrorMessage, warnIfCspResourceFallbackShadowing, warnIfCspWithShiki } from "../messages/runtime.js";
import { createRoutesList } from "../routing/create-manifest.js";
import { createContainer } from "./container.js";
const configRE = /.*astro.config.(?:mjs|mts|cjs|cts|js|ts)$/;
function shouldRestartContainer({ settings, inlineConfig, restartInFlight }, changedFile) {
if (restartInFlight) return false;
let shouldRestart = false;
const normalizedChangedFile = vite.normalizePath(changedFile);
if (inlineConfig.configFile) {
shouldRestart = vite.normalizePath(inlineConfig.configFile) === normalizedChangedFile;
} else {
shouldRestart = configRE.test(normalizedChangedFile);
const settingsPath = vite.normalizePath(fileURLToPath(new URL(SETTINGS_FILE, settings.dotAstroDir)));
if (settingsPath.endsWith(normalizedChangedFile)) {
shouldRestart = settings.preferences.ignoreNextPreferenceReload ? false : true;
settings.preferences.ignoreNextPreferenceReload = false;
}
}
if (!shouldRestart && settings.watchFiles.length > 0) {
shouldRestart = settings.watchFiles.some((path) => vite.normalizePath(path) === vite.normalizePath(changedFile));
}
return shouldRestart;
}
/**
* Restart the dev server in-place by reusing the existing Vite server instance.
*
* Instead of tearing down and recreating the entire container (which creates a
* brand new Vite server), this function re-reads the Astro config, builds a new
* Vite inline config with updated plugins, patches it onto the existing server,
* then calls Vite's own native restart. Vite's restart does an in-place mutation
* of the server object, keeping the same HTTP server / TCP socket alive and
* passing `previousEnvironments` to plugins — allowing adapters like
* `@cloudflare/vite-plugin` to reuse their miniflare instance rather than
* disposing and recreating it.
*/
async function restartContainerInPlace(container) {
const { logger, settings: existingSettings, inlineConfig, fs } = container;
container.restartInFlight = true;
// Clear the crawlFrameworkPkgs cache since node_modules may have changed
clearCrawlCache();
try {
const { astroConfig } = await resolveConfig(inlineConfig, "dev", fs);
warnIfCspWithShiki(astroConfig, logger);
warnIfCspResourceFallbackShadowing(astroConfig, logger);
let settings = await createSettings(astroConfig, inlineConfig.logLevel, fileURLToPath(existingSettings.config.root));
settings = await runHookConfigSetup({
settings,
command: "dev",
logger,
isRestart: true
});
if (!settings.adapter?.adapterFeatures?.buildOutput) {
settings.buildOutput = getPrerenderDefault(settings.config) ? "static" : "server";
}
await runHookConfigDone({
settings,
logger,
command: "dev"
});
const mode = inlineConfig?.mode ?? "development";
const { server: { host, headers, allowedHosts } } = settings.config;
const rendererClientEntries = settings.renderers.map((r) => r.clientEntrypoint).filter(Boolean);
const routesList = await createRoutesList({
settings,
fsMod: fs
}, logger, { dev: true });
const address = container.viteServer.httpServer?.address();
const port = address !== null && typeof address === "object" ? address.port : undefined;
const newViteConfig = await createVite({
server: {
host,
headers,
allowedHosts,
port
},
optimizeDeps: { include: rendererClientEntries }
}, {
settings,
logger,
mode,
command: "dev",
fs,
sync: false,
routesList
});
// Resolve the new inline config into a full ResolvedConfig and assign it
// onto the existing server so Vite's restartServer() uses the new plugins.
container.viteServer.config = await vite.resolveConfig(newViteConfig, "serve");
const previousEnvironments = container.viteServer.environments;
await container.viteServer.restart();
if (container.viteServer.environments !== previousEnvironments) {
releasePreviousEnvironments(previousEnvironments);
}
container.settings = settings;
return settings;
} catch (_err) {
const error = createSafeError(_err);
if (!isAstroConfigZodError(_err)) {
logger.error("config", formatErrorMessage(collectErrorMetadata(error), logger.level() === "debug") + "\n");
}
container.viteServer.environments?.client?.hot?.send({
type: "error",
err: {
message: error.message,
stack: error.stack || ""
}
});
logger.error(null, "Continuing with previous valid configuration\n");
return error;
} finally {
container.restartInFlight = false;
}
}
function releasePreviousEnvironments(environments) {
for (const environment of Object.values(environments)) {
if (environment.mode !== "dev") continue;
const { moduleGraph } = environment;
moduleGraph.urlToModuleMap.clear();
moduleGraph.idToModuleMap.clear();
moduleGraph.etagToModuleMap.clear();
moduleGraph.fileToModulesMap.clear();
moduleGraph._unresolvedUrlToModuleMap?.clear();
}
}
export async function createContainerWithAutomaticRestart({ inlineConfig, fs }) {
const { userConfig, astroConfig } = await resolveConfig(inlineConfig ?? {}, "dev", fs);
// For now, we override only when no inline config has been provided. This won't break tests
const logger = await loadOrCreateNodeLogger(astroConfig, inlineConfig ?? {});
warnIfCspWithShiki(astroConfig, logger);
warnIfCspResourceFallbackShadowing(astroConfig, logger);
telemetry.record(eventCliSession("dev", userConfig));
const settings = await createSettings(astroConfig, inlineConfig?.logLevel, fileURLToPath(astroConfig.root));
const initialContainer = await createContainer({
settings,
logger,
inlineConfig,
fs
});
let resolveRestart;
let restartComplete = new Promise((resolve) => {
resolveRestart = resolve;
});
let restart = {
container: initialContainer,
bindCLIShortcuts() {
const customShortcuts = [
{
key: "r",
description: ""
},
{
key: "u",
description: ""
},
{
key: "c",
description: ""
}
];
customShortcuts.push({
key: "s",
description: "sync content layer",
action: () => {
globalContentLayer.get()?.sync();
}
});
restart.container.viteServer.bindCLIShortcuts({ customShortcuts });
},
restarted() {
return restartComplete;
}
};
function handleChangeRestart(logMsg) {
return async function(changedFile) {
if (shouldRestartContainer(restart.container, changedFile)) {
logger.info(null, (logMsg + " Restarting...").trim());
const result = await restartContainerInPlace(restart.container);
if (result instanceof Error) {
resolveRestart(result);
} else {
setupContainer();
await attachContentServerListeners(restart.container);
await restartContentLayer();
resolveRestart(null);
}
restartComplete = new Promise((resolve) => {
resolveRestart = resolve;
});
}
};
}
// Vite's in-place restart closes the old file watcher, so loaders must be
// re-registered on the new one, using the new settings.
async function restartContentLayer() {
const previousContentLayer = globalContentLayer.get();
if (!previousContentLayer) return;
const contentLayer = globalContentLayer.init({
settings: restart.container.settings,
logger,
watcher: restart.container.viteServer.watcher,
store: previousContentLayer.store
});
contentLayer.watchContentConfig();
// A loader error must not reject the watcher callback that triggered the
// restart, or `restarted()` would never resolve.
try {
await contentLayer.sync();
} catch (err) {
logger.error("content", err instanceof Error ? err.message : String(err));
}
}
let changeHandler;
let unlinkHandler;
let addHandler;
function setupContainer() {
const watcher = restart.container.viteServer.watcher;
if (changeHandler) watcher.off("change", changeHandler);
if (unlinkHandler) watcher.off("unlink", unlinkHandler);
if (addHandler) watcher.off("add", addHandler);
changeHandler = handleChangeRestart("Configuration file updated.");
unlinkHandler = handleChangeRestart("Configuration file removed.");
addHandler = handleChangeRestart("Configuration file added.");
watcher.on("change", changeHandler);
watcher.on("unlink", unlinkHandler);
watcher.on("add", addHandler);
}
setupContainer();
return restart;
}