astro
Version:
Astro is a modern site builder with web best practices, performance, and DX front-of-mind.
202 lines (201 loc) • 9.36 kB
JavaScript
import { AsyncLocalStorage } from "node:async_hooks";
import nodeFs from "node:fs";
import { IncomingMessage } from "node:http";
import { isRunnableDevEnvironment } from "vite";
import { ASTRO_VITE_ENVIRONMENT_NAMES, devPrerenderMiddlewareSymbol, devServerAppReadySymbol } from "../core/constants.js";
import { getViteErrorPayload } from "../core/errors/dev/index.js";
import { AstroError, AstroErrorData } from "../core/errors/index.js";
import { createViteLoader } from "../core/module-loader/index.js";
import { matchAllRoutes } from "../core/routing/match.js";
import { SERIALIZED_MANIFEST_ID } from "../manifest/serialized.js";
import { kickOffContentConfigLoad } from "../content/config-prewarm.js";
import { ASTRO_DEV_SERVER_APP_ID } from "../vite-plugin-app/index.js";
import { baseMiddleware } from "./base.js";
import { createController } from "./controller.js";
import { recordServerError } from "./error.js";
import { setRouteError } from "./server-state.js";
import { routeGuardMiddleware } from "./route-guard.js";
import { secFetchMiddleware } from "./sec-fetch.js";
import { trailingSlashMiddleware } from "./trailing-slash.js";
export default function createVitePluginAstroServer({ settings, logger }) {
const disposers = new WeakMap();
return {
name: "astro:server",
applyToEnvironment(environment) {
return environment.name === ASTRO_VITE_ENVIRONMENT_NAMES.ssr;
},
closeBundle() {
const dispose = disposers.get(this.environment);
if (dispose) {
disposers.delete(this.environment);
dispose();
}
},
async configureServer(viteServer) {
// Skip Astro dev server setup when running inside Vitest. The dev server
// middleware (SSR handler, prerender handler, trailing-slash redirects, etc.)
// is only needed for `astro dev` and can crash in Vitest's browser server
// where the module evaluator may not support dynamic imports.
if (process.env.VITEST) {
return;
}
const ssrEnvironment = viteServer.environments[ASTRO_VITE_ENVIRONMENT_NAMES.ssr];
const prerenderEnvironment = viteServer.environments[ASTRO_VITE_ENVIRONMENT_NAMES.prerender];
const runnableSsrEnvironment = isRunnableDevEnvironment(ssrEnvironment) ? ssrEnvironment : undefined;
const runnablePrerenderEnvironment = isRunnableDevEnvironment(prerenderEnvironment) ? prerenderEnvironment : undefined;
async function createHandler(environment) {
const loader = createViteLoader(viteServer, environment);
const { default: createAstroServerApp } = await environment.runner.import(ASTRO_DEV_SERVER_APP_ID);
const controller = createController({ loader });
const { handler } = await createAstroServerApp(controller, settings, loader, logger);
const { manifest } = await environment.runner.import(SERIALIZED_MANIFEST_ID);
return {
controller,
handler,
loader,
manifest,
environment
};
}
// Kick off the content config load and dev server app compile as early
// as possible. Neither is needed until the first request, so install the
// middleware synchronously and let request handlers await the in-flight
// results without blocking server creation. Serialize the app compile
// after the config load so they do not contend for the main thread.
const astroEnvironment = viteServer.environments[ASTRO_VITE_ENVIRONMENT_NAMES.astro];
const runnableAstroEnvironment = isRunnableDevEnvironment(astroEnvironment) ? astroEnvironment : undefined;
const contentConfigLoad = runnableAstroEnvironment ? kickOffContentConfigLoad({
settings,
fs: nodeFs,
logger,
environment: runnableAstroEnvironment
}) : Promise.resolve();
let ssrHandlerPromise = runnableSsrEnvironment ? contentConfigLoad.then(() => createHandler(runnableSsrEnvironment)) : undefined;
let prerenderHandlerPromise = runnablePrerenderEnvironment ? contentConfigLoad.then(() => createHandler(runnablePrerenderEnvironment)) : undefined;
// Background setup uses the runnable environments' module runners. Keep
// shutdown from disconnecting those runners while an import is in flight.
viteServer[devServerAppReadySymbol] = Promise.allSettled([
contentConfigLoad,
ssrHandlerPromise,
prerenderHandlerPromise
].filter(Boolean)).then(() => undefined);
// Compile failures surface here so startup still reports them, and also
// as a rejected lazy await in the request handlers below.
ssrHandlerPromise?.catch((error) => {
logger.error(null, `Failed to create the dev server app: ${error?.message ?? error}`);
});
prerenderHandlerPromise?.catch((error) => {
logger.error(null, `Failed to create the prerender server app: ${error?.message ?? error}`);
});
const localStorage = new AsyncLocalStorage();
async function handleUnhandledRejection(rejection) {
const error = AstroError.is(rejection) ? rejection : new AstroError({
...AstroErrorData.UnhandledRejection,
message: AstroErrorData.UnhandledRejection.message(rejection?.stack || rejection)
});
const store = localStorage.getStore();
const handlers = [];
if (ssrHandlerPromise) handlers.push(await ssrHandlerPromise.catch(() => undefined));
if (prerenderHandlerPromise) handlers.push(await prerenderHandlerPromise.catch(() => undefined));
for (const currentHandler of handlers) {
if (!currentHandler) continue;
if (store instanceof IncomingMessage) {
setRouteError(currentHandler.controller.state, store.url, error);
}
const { errorWithMetadata } = recordServerError(currentHandler.loader, currentHandler.manifest, logger, error);
setTimeout(async () => currentHandler.loader.webSocketSend(await getViteErrorPayload(errorWithMetadata)), 200);
}
}
if (runnableSsrEnvironment || runnablePrerenderEnvironment) {
process.on("unhandledRejection", handleUnhandledRejection);
}
if (ssrEnvironment) {
disposers.set(ssrEnvironment, () => {
process.off("unhandledRejection", handleUnhandledRejection);
ssrHandlerPromise = undefined;
prerenderHandlerPromise = undefined;
});
}
return () => {
const shouldHandlePrerenderInCore = Boolean(viteServer[devPrerenderMiddlewareSymbol]);
// Push this middleware to the front of the stack so that it can intercept responses.
// fix(#6067): always inject this to ensure zombie base handling is killed after restarts
viteServer.middlewares.stack.unshift({
route: "",
handle: baseMiddleware(settings, logger)
});
viteServer.middlewares.stack.unshift({
route: "",
handle: trailingSlashMiddleware(settings)
});
// Prevent serving files outside srcDir/publicDir (e.g., /README.md at project root)
viteServer.middlewares.stack.unshift({
route: "",
handle: routeGuardMiddleware(settings)
});
// Validate Sec-Fetch metadata headers to restrict cross-origin subresource requests
viteServer.middlewares.stack.unshift({
route: "",
handle: secFetchMiddleware(logger, settings.config.security?.allowedDomains)
});
if (runnablePrerenderEnvironment && shouldHandlePrerenderInCore) {
viteServer.middlewares.use(async function astroDevPrerenderHandler(request, response, next) {
if (request.url === undefined || !request.method) {
response.writeHead(500, "Incomplete request");
response.end();
return;
}
if (request.url.startsWith("/@") || request.url.startsWith("/__")) {
return next();
}
if (request.url.includes("/node_modules/")) {
return next();
}
const currentPrerenderHandlerPromise = prerenderHandlerPromise;
if (!currentPrerenderHandlerPromise) {
return next();
}
try {
const prerenderHandler = await currentPrerenderHandlerPromise;
const pathname = decodeURI(new URL(request.url, "http://localhost").pathname);
const { routes } = await prerenderHandler.environment.runner.import("virtual:astro:routes");
const routesList = { routes: routes.map((route) => route.routeData) };
// This is intentionally a broad prerender gate. The real dev route
// resolution happens in handleDevRequest(), which also
// checks getStaticPaths() and prerender tie-breaks that matchRoute()
// cannot express here.
const matches = matchAllRoutes(pathname, routesList);
if (!matches.some((route) => route.prerender)) {
return next();
}
const handled = await localStorage.run(request, () => prerenderHandler.handler(request, response, { prerenderOnly: true }));
if (!handled) {
return next();
}
} catch (err) {
next(err);
}
});
}
if (runnableSsrEnvironment) {
// Note that this function has a name so other middleware can find it.
viteServer.middlewares.use(async function astroDevHandler(request, response, next) {
if (request.url === undefined || !request.method) {
response.writeHead(500, "Incomplete request");
response.end();
return;
}
const currentSsrHandlerPromise = ssrHandlerPromise;
if (!currentSsrHandlerPromise) {
return next();
}
const ssrHandler = await currentSsrHandlerPromise;
localStorage.run(request, () => {
ssrHandler.handler(request, response);
});
});
}
};
}
};
}