import { fileURLToPath } from "node:url"; import { getInfoOutput } from "../cli/info/index.js"; import { ASTRO_VERSION } from "../core/constants.js"; import { AstroErrorData, isAstroError } from "../core/errors/index.js"; import { req } from "../core/messages.js"; import { sequence } from "../core/middleware/index.js"; import { loadMiddleware } from "../core/middleware/loadMiddleware.js"; import { createRenderContext, getParamsAndProps } from "../core/render/index.js"; import { createRequest } from "../core/request.js"; import { matchAllRoutes } from "../core/routing/index.js"; import { isPage, resolveIdToUrl } from "../core/util.js"; import { normalizeTheLocale } from "../i18n/index.js"; import { createI18nMiddleware, i18nPipelineHook } from "../i18n/middleware.js"; import { getSortedPreloadedMatches } from "../prerender/routing.js"; import { isServerLikeOutput } from "../prerender/utils.js"; import { PAGE_SCRIPT_ID } from "../vite-plugin-scripts/index.js"; import { getStylesForURL } from "./css.js"; import { preload } from "./index.js"; import { getComponentMetadata } from "./metadata.js"; import { handle404Response, writeSSRResult, writeWebResponse } from "./response.js"; import { getScriptsForURL } from "./scripts.js"; import { REROUTE_DIRECTIVE_HEADER } from "../runtime/server/consts.js"; const clientLocalsSymbol = Symbol.for("astro.locals"); function isLoggedRequest(url) { return url !== "/favicon.ico"; } function getCustom404Route(manifestData) { const route404 = /^\/404\/?$/; return manifestData.routes.find((r) => route404.test(r.route)); } async function matchRoute(pathname, manifestData, pipeline) { const env = pipeline.getEnvironment(); const { routeCache, logger } = env; let matches = matchAllRoutes(pathname, manifestData); const preloadedMatches = await getSortedPreloadedMatches({ pipeline, matches, settings: pipeline.getSettings() }); for await (const { preloadedComponent, route: maybeRoute, filePath } of preloadedMatches) { try { await getParamsAndProps({ mod: preloadedComponent, route: maybeRoute, routeCache, pathname, logger, ssr: isServerLikeOutput(pipeline.getConfig()) }); return { route: maybeRoute, filePath, resolvedPathname: pathname, preloadedComponent, mod: preloadedComponent }; } catch (e) { if (isAstroError(e) && e.title === AstroErrorData.NoMatchingStaticPathFound.title) { continue; } throw e; } } const altPathname = pathname.replace(/(index)?\.html$/, ""); if (altPathname !== pathname) { return await matchRoute(altPathname, manifestData, pipeline); } if (matches.length) { const possibleRoutes = matches.flatMap((route) => route.component); pipeline.logger.warn( "router", `${AstroErrorData.NoMatchingStaticPathFound.message( pathname )} ${AstroErrorData.NoMatchingStaticPathFound.hint(possibleRoutes)}` ); } const custom404 = getCustom404Route(manifestData); if (custom404) { const filePath = new URL(`./${custom404.component}`, pipeline.getConfig().root); const preloadedComponent = await preload({ pipeline, filePath }); return { route: custom404, filePath, resolvedPathname: pathname, preloadedComponent, mod: preloadedComponent }; } return void 0; } async function handleRoute({ matchedRoute, url, pathname, status = getStatus(matchedRoute), body, origin, pipeline, manifestData, incomingRequest, incomingResponse, manifest }) { const timeStart = performance.now(); const env = pipeline.getEnvironment(); const config = pipeline.getConfig(); const moduleLoader = pipeline.getModuleLoader(); const { logger } = env; if (!matchedRoute && !config.i18n) { if (isLoggedRequest(pathname)) { logger.info(null, req({ url: pathname, method: incomingRequest.method, statusCode: 404 })); } return handle404Response(origin, incomingRequest, incomingResponse); } const buildingToSSR = isServerLikeOutput(config); let request; let renderContext; let mod = void 0; let options = void 0; let route; const middleware = await loadMiddleware(moduleLoader); if (!matchedRoute) { if (config.i18n) { const locales = config.i18n.locales; const pathNameHasLocale = pathname.split("/").filter(Boolean).some((segment) => { let found = false; for (const locale of locales) { if (typeof locale === "string") { if (normalizeTheLocale(locale) === normalizeTheLocale(segment)) { found = true; break; } } else { if (locale.path === segment) { found = true; break; } } } return found; }); if (!pathNameHasLocale && pathname !== "/") { return handle404Response(origin, incomingRequest, incomingResponse); } request = createRequest({ url, headers: buildingToSSR ? incomingRequest.headers : new Headers(), logger, ssr: buildingToSSR }); route = { component: "", generate(_data) { return ""; }, params: [], pattern: new RegExp(""), prerender: false, segments: [], type: "fallback", route: "", fallbackRoutes: [], isIndex: false }; renderContext = await createRenderContext({ request, pathname, env, mod, route, locales: manifest.i18n?.locales, routing: manifest.i18n?.routing, defaultLocale: manifest.i18n?.defaultLocale }); } else { return handle404Response(origin, incomingRequest, incomingResponse); } } else { const filePath = matchedRoute.filePath; const { preloadedComponent } = matchedRoute; route = matchedRoute.route; request = createRequest({ url, headers: buildingToSSR ? incomingRequest.headers : new Headers(), method: incomingRequest.method, body, logger, ssr: buildingToSSR, clientAddress: buildingToSSR ? incomingRequest.socket.remoteAddress : void 0, locals: Reflect.get(incomingRequest, clientLocalsSymbol) // Allows adapters to pass in locals in dev mode. }); for (const [name, value] of Object.entries(config.server.headers ?? {})) { if (value) incomingResponse.setHeader(name, value); } options = { env, filePath, preload: preloadedComponent, pathname, request, route, middleware }; mod = options.preload; const { scripts, links, styles, metadata } = await getScriptsAndStyles({ pipeline, filePath: options.filePath }); const i18n = pipeline.getConfig().i18n; renderContext = await createRenderContext({ request: options.request, pathname: options.pathname, scripts, links, styles, componentMetadata: metadata, route: options.route, mod, env, locales: i18n?.locales, routing: i18n?.routing, defaultLocale: i18n?.defaultLocale }); } const onRequest = middleware.onRequest; if (config.i18n) { const i18Middleware = createI18nMiddleware( manifest.i18n, config.base, config.trailingSlash, config.build.format ); if (i18Middleware) { pipeline.setMiddlewareFunction(sequence(i18Middleware, onRequest)); pipeline.onBeforeRenderRoute(i18nPipelineHook); } else { pipeline.setMiddlewareFunction(onRequest); } } else { pipeline.setMiddlewareFunction(onRequest); } let response = await pipeline.renderRoute(renderContext, mod); if (isLoggedRequest(pathname)) { const timeEnd = performance.now(); logger.info( null, req({ url: pathname, method: incomingRequest.method, statusCode: status ?? response.status, reqTime: timeEnd - timeStart }) ); } if (response.status === 404 && has404Route(manifestData) && response.headers.get(REROUTE_DIRECTIVE_HEADER) !== "no") { const fourOhFourRoute = await matchRoute("/404", manifestData, pipeline); if (options && fourOhFourRoute?.route !== options.route) return handleRoute({ ...options, matchedRoute: fourOhFourRoute, url: new URL(pathname, url), status: 404, body, origin, pipeline, manifestData, incomingRequest, incomingResponse, manifest }); } if (route.type === "endpoint") { await writeWebResponse(incomingResponse, response); return; } if (response.status < 400 && response.status >= 300) { await writeSSRResult(request, response, incomingResponse); return; } if (status && response.status !== status && (status === 404 || status === 500)) { response = new Response(response.body, { status, headers: response.headers }); } await writeSSRResult(request, response, incomingResponse); } async function getScriptsAndStyles({ pipeline, filePath }) { const moduleLoader = pipeline.getModuleLoader(); const settings = pipeline.getSettings(); const mode = pipeline.getEnvironment().mode; const scripts = await getScriptsForURL(filePath, settings.config.root, moduleLoader); if (isPage(filePath, settings) && mode === "development") { scripts.add({ props: { type: "module", src: "/@vite/client" }, children: "" }); if (settings.config.devToolbar.enabled && await settings.preferences.get("devToolbar.enabled")) { scripts.add({ props: { type: "module", src: await resolveIdToUrl(moduleLoader, "astro/runtime/client/dev-toolbar/entrypoint.js") }, children: "" }); const additionalMetadata = { root: fileURLToPath(settings.config.root), version: ASTRO_VERSION, debugInfo: await getInfoOutput({ userConfig: settings.config, print: false }) }; scripts.add({ props: {}, children: `window.__astro_dev_toolbar__ = ${JSON.stringify(additionalMetadata)}` }); } } for (const script of settings.scripts) { if (script.stage === "head-inline") { scripts.add({ props: {}, children: script.content }); } else if (script.stage === "page" && isPage(filePath, settings)) { scripts.add({ props: { type: "module", src: `/@id/${PAGE_SCRIPT_ID}` }, children: "" }); } } const { urls: styleUrls, styles: importedStyles } = await getStylesForURL(filePath, moduleLoader); let links = /* @__PURE__ */ new Set(); [...styleUrls].forEach((href) => { links.add({ props: { rel: "stylesheet", href }, children: "" }); }); let styles = /* @__PURE__ */ new Set(); importedStyles.forEach(({ id, url, content }) => { scripts.add({ props: { type: "module", src: url }, children: "" }); styles.add({ props: { "data-vite-dev-id": id }, children: content }); }); const metadata = await getComponentMetadata(filePath, moduleLoader); return { scripts, styles, links, metadata }; } function getStatus(matchedRoute) { if (!matchedRoute) return 404; if (matchedRoute.route.route === "/404") return 404; if (matchedRoute.route.route === "/500") return 500; } function has404Route(manifest) { return manifest.routes.some((route) => route.route === "/404"); } export { handleRoute, matchRoute };