UNPKG

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