381 lines
11 KiB
Plaintext
381 lines
11 KiB
Plaintext
|
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: []
|
||
|
};
|
||
|
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(
|
||
|
config.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
|
||
|
};
|