Refactor routing in App component to enhance navigation and improve error handling by integrating dynamic routes and updating the NotFound route.

This commit is contained in:
becarta
2025-05-23 12:43:00 +02:00
parent f40db0f5c9
commit a544759a3b
11127 changed files with 1647032 additions and 0 deletions

View File

@@ -0,0 +1,4 @@
import type * as vite from 'vite';
import type { AstroSettings } from '../@types/astro.js';
import type { Logger } from '../core/logger/core.js';
export declare function baseMiddleware(settings: AstroSettings, logger: Logger): vite.Connect.NextHandleFunction;

View File

@@ -0,0 +1,61 @@
import * as fs from "node:fs";
import path from "node:path";
import { appendForwardSlash } from "@astrojs/internal-helpers/path";
import { bold } from "kleur/colors";
import notFoundTemplate, { subpathNotUsedTemplate } from "../template/4xx.js";
import { writeHtmlResponse } from "./response.js";
function baseMiddleware(settings, logger) {
const { config } = settings;
const site = config.site ? new URL(config.base, config.site) : void 0;
const devRootURL = new URL(config.base, "http://localhost");
const devRoot = site ? site.pathname : devRootURL.pathname;
const devRootReplacement = devRoot.endsWith("/") ? "/" : "";
return function devBaseMiddleware(req, res, next) {
const url = req.url;
let pathname;
try {
pathname = decodeURI(new URL(url, "http://localhost").pathname);
} catch (e) {
return next(e);
}
if (pathname.startsWith(devRoot)) {
req.url = url.replace(devRoot, devRootReplacement);
return next();
}
if (pathname === "/" || pathname === "/index.html") {
const html = subpathNotUsedTemplate(devRoot, pathname);
return writeHtmlResponse(res, 404, html);
}
if (req.headers.accept?.includes("text/html")) {
const html = notFoundTemplate({
statusCode: 404,
title: "Not found",
tabTitle: "404: Not Found",
pathname
});
return writeHtmlResponse(res, 404, html);
}
const publicPath = new URL("." + req.url, config.publicDir);
fs.stat(publicPath, (_err, stats) => {
if (stats) {
const publicDir = appendForwardSlash(
path.posix.relative(config.root.pathname, config.publicDir.pathname)
);
const expectedLocation = new URL(devRootURL.pathname + url, devRootURL).pathname;
logger.error(
"router",
`Request URLs for ${bold(
publicDir
)} assets must also include your base. "${expectedLocation}" expected, but received "${url}".`
);
const html = subpathNotUsedTemplate(devRoot, pathname);
return writeHtmlResponse(res, 404, html);
} else {
next();
}
});
};
}
export {
baseMiddleware
};

View File

@@ -0,0 +1,22 @@
import type { LoaderEvents, ModuleLoader } from '../core/module-loader/index.js';
import type { ServerState } from './server-state.js';
type ReloadFn = () => void;
export interface DevServerController {
state: ServerState;
onFileChange: LoaderEvents['file-change'];
onHMRError: LoaderEvents['hmr-error'];
}
export type CreateControllerParams = {
loader: ModuleLoader;
} | {
reload: ReloadFn;
};
export declare function createController(params: CreateControllerParams): DevServerController;
export interface RunWithErrorHandlingParams {
controller: DevServerController;
pathname: string;
run: () => Promise<any>;
onError: (error: unknown) => Error;
}
export declare function runWithErrorHandling({ controller: { state }, pathname, run, onError, }: RunWithErrorHandlingParams): Promise<void>;
export {};

View File

@@ -0,0 +1,74 @@
import {
clearRouteError,
createServerState,
setRouteError,
setServerError
} from "./server-state.js";
function createController(params) {
if ("loader" in params) {
return createLoaderController(params.loader);
} else {
return createBaseController(params);
}
}
function createBaseController({ reload }) {
const serverState = createServerState();
const onFileChange = () => {
if (serverState.state === "error") {
reload();
}
};
const onHMRError = (payload) => {
let msg = payload?.err?.message ?? "Unknown error";
let stack = payload?.err?.stack ?? "Unknown stack";
let error = new Error(msg);
Object.defineProperty(error, "stack", {
value: stack
});
setServerError(serverState, error);
};
return {
state: serverState,
onFileChange,
onHMRError
};
}
function createLoaderController(loader) {
const controller = createBaseController({
reload() {
loader.clientReload();
}
});
const baseOnFileChange = controller.onFileChange;
controller.onFileChange = (...args) => {
if (controller.state.state === "error") {
loader.eachModule((mod) => {
if (mod.ssrError) {
loader.invalidateModule(mod);
}
});
}
baseOnFileChange(...args);
};
loader.events.on("file-change", controller.onFileChange);
loader.events.on("hmr-error", controller.onHMRError);
return controller;
}
async function runWithErrorHandling({
controller: { state },
pathname,
run,
onError
}) {
try {
await run();
clearRouteError(state, pathname);
} catch (err) {
const error = onError(err);
setRouteError(state, pathname, error);
}
}
export {
createController,
runWithErrorHandling
};

View File

@@ -0,0 +1,13 @@
import type { ModuleLoader } from '../core/module-loader/index.js';
interface ImportedStyle {
id: string;
url: string;
content: string;
}
/** Given a filePath URL, crawl Vites module graph to find all style imports. */
export declare function getStylesForURL(filePath: URL, loader: ModuleLoader): Promise<{
urls: Set<string>;
styles: ImportedStyle[];
crawledFiles: Set<string>;
}>;
export {};

View File

@@ -0,0 +1,48 @@
import { viteID } from "../core/util.js";
import { isBuildableCSSRequest } from "./util.js";
import { crawlGraph } from "./vite.js";
const inlineQueryRE = /(?:\?|&)inline(?:$|&)/;
async function getStylesForURL(filePath, loader) {
const importedCssUrls = /* @__PURE__ */ new Set();
const importedStylesMap = /* @__PURE__ */ new Map();
const crawledFiles = /* @__PURE__ */ new Set();
for await (const importedModule of crawlGraph(loader, viteID(filePath), true)) {
if (importedModule.file) {
crawledFiles.add(importedModule.file);
}
if (isBuildableCSSRequest(importedModule.url)) {
let css = "";
if (typeof importedModule.ssrModule?.default === "string") {
css = importedModule.ssrModule.default;
} else {
let modId = importedModule.url;
if (!inlineQueryRE.test(importedModule.url)) {
if (importedModule.url.includes("?")) {
modId = importedModule.url.replace("?", "?inline&");
} else {
modId += "?inline";
}
}
try {
const ssrModule = await loader.import(modId);
css = ssrModule.default;
} catch {
continue;
}
}
importedStylesMap.set(importedModule.url, {
id: importedModule.id ?? importedModule.url,
url: importedModule.url,
content: css
});
}
}
return {
urls: importedCssUrls,
styles: [...importedStylesMap.values()],
crawledFiles
};
}
export {
getStylesForURL
};

View File

@@ -0,0 +1,7 @@
import type { AstroConfig } from '../@types/astro.js';
import type { ModuleLoader } from '../core/module-loader/index.js';
import type { DevPipeline } from './pipeline.js';
export declare function recordServerError(loader: ModuleLoader, config: AstroConfig, { logger }: DevPipeline, _err: unknown): {
error: Error;
errorWithMetadata: import("../core/errors/errors.js").ErrorWithMetadata;
};

View File

@@ -0,0 +1,19 @@
import { collectErrorMetadata } from "../core/errors/dev/index.js";
import { createSafeError } from "../core/errors/index.js";
import { formatErrorMessage } from "../core/messages.js";
function recordServerError(loader, config, { logger }, _err) {
const err = createSafeError(_err);
try {
loader.fixStacktrace(err);
} catch {
}
const errorWithMetadata = collectErrorMetadata(err, config.root);
logger.error(null, formatErrorMessage(errorWithMetadata, logger.level() === "debug"));
return {
error: err,
errorWithMetadata
};
}
export {
recordServerError
};

View File

@@ -0,0 +1,3 @@
export { createController, runWithErrorHandling } from './controller.js';
export { default as vitePluginAstroServer } from './plugin.js';
export { handleRequest } from './request.js';

View File

@@ -0,0 +1,9 @@
import { createController, runWithErrorHandling } from "./controller.js";
import { default as default2 } from "./plugin.js";
import { handleRequest } from "./request.js";
export {
createController,
handleRequest,
runWithErrorHandling,
default2 as vitePluginAstroServer
};

View File

@@ -0,0 +1,3 @@
import type { SSRResult } from '../@types/astro.js';
import type { ModuleLoader } from '../core/module-loader/index.js';
export declare function getComponentMetadata(filePath: URL, loader: ModuleLoader): Promise<SSRResult['componentMetadata']>;

View File

@@ -0,0 +1,36 @@
import { viteID } from "../core/util.js";
import { getAstroMetadata } from "../vite-plugin-astro/index.js";
import { crawlGraph } from "./vite.js";
async function getComponentMetadata(filePath, loader) {
const map = /* @__PURE__ */ new Map();
const rootID = viteID(filePath);
addMetadata(map, loader.getModuleInfo(rootID));
for await (const moduleNode of crawlGraph(loader, rootID, true)) {
const id = moduleNode.id;
if (id) {
addMetadata(map, loader.getModuleInfo(id));
}
}
return map;
}
function addMetadata(map, modInfo) {
if (modInfo) {
const astro = getAstroMetadata(modInfo);
if (astro) {
let metadata = {
containsHead: false,
propagation: "none"
};
if (astro.propagation) {
metadata.propagation = astro.propagation;
}
if (astro.containsHead) {
metadata.containsHead = astro.containsHead;
}
map.set(modInfo.id, metadata);
}
}
}
export {
getComponentMetadata
};

View File

@@ -0,0 +1,31 @@
import type { AstroSettings, ComponentInstance, ManifestData, RewritePayload, RouteData, SSRLoadedRenderer, SSRManifest } from '../@types/astro.js';
import type { HeadElements, TryRewriteResult } from '../core/base-pipeline.js';
import type { Logger } from '../core/logger/core.js';
import type { ModuleLoader } from '../core/module-loader/index.js';
import { Pipeline } from '../core/render/index.js';
export declare class DevPipeline extends Pipeline {
readonly loader: ModuleLoader;
readonly logger: Logger;
readonly manifest: SSRManifest;
readonly settings: AstroSettings;
readonly config: import("../@types/astro.js").AstroConfig;
readonly defaultRoutes: {
instance: ComponentInstance;
matchesComponent(filePath: URL): boolean;
route: string;
component: string;
}[];
renderers: SSRLoadedRenderer[];
manifestData: ManifestData | undefined;
componentInterner: WeakMap<RouteData, ComponentInstance>;
private constructor();
static create(manifestData: ManifestData, { loader, logger, manifest, settings, }: Pick<DevPipeline, 'loader' | 'logger' | 'manifest' | 'settings'>): DevPipeline;
headElements(routeData: RouteData): Promise<HeadElements>;
componentMetadata(routeData: RouteData): Promise<Map<string, import("../@types/astro.js").SSRComponentMetadata>>;
preload(routeData: RouteData, filePath: URL): Promise<ComponentInstance>;
clearRouteCache(): void;
getComponentByRoute(routeData: RouteData): Promise<ComponentInstance>;
tryRewrite(payload: RewritePayload, request: Request): Promise<TryRewriteResult>;
setManifestData(manifestData: ManifestData): void;
rewriteKnownRoute(route: string, sourceRoute: RouteData): ComponentInstance;
}

View File

@@ -0,0 +1,172 @@
import { fileURLToPath } from "node:url";
import { getInfoOutput } from "../cli/info/index.js";
import { ASTRO_VERSION } from "../core/constants.js";
import { enhanceViteSSRError } from "../core/errors/dev/index.js";
import { AggregateError, CSSError, MarkdownError } from "../core/errors/index.js";
import { Pipeline, loadRenderer } from "../core/render/index.js";
import { createDefaultRoutes } from "../core/routing/default.js";
import { findRouteToRewrite } from "../core/routing/rewrite.js";
import { isPage, isServerLikeOutput, viteID } from "../core/util.js";
import { resolveIdToUrl } from "../core/viteUtils.js";
import { PAGE_SCRIPT_ID } from "../vite-plugin-scripts/index.js";
import { getStylesForURL } from "./css.js";
import { getComponentMetadata } from "./metadata.js";
import { createResolve } from "./resolve.js";
import { getScriptsForURL } from "./scripts.js";
class DevPipeline extends Pipeline {
constructor(loader, logger, manifest, settings, config = settings.config, defaultRoutes = createDefaultRoutes(manifest)) {
const mode = "development";
const resolve = createResolve(loader, config.root);
const serverLike = isServerLikeOutput(config);
const streaming = true;
super(logger, manifest, mode, [], resolve, serverLike, streaming);
this.loader = loader;
this.logger = logger;
this.manifest = manifest;
this.settings = settings;
this.config = config;
this.defaultRoutes = defaultRoutes;
manifest.serverIslandMap = settings.serverIslandMap;
manifest.serverIslandNameMap = settings.serverIslandNameMap;
}
// renderers are loaded on every request,
// so it needs to be mutable here unlike in other environments
renderers = new Array();
manifestData;
componentInterner = /* @__PURE__ */ new WeakMap();
static create(manifestData, {
loader,
logger,
manifest,
settings
}) {
const pipeline = new DevPipeline(loader, logger, manifest, settings);
pipeline.manifestData = manifestData;
return pipeline;
}
async headElements(routeData) {
const {
config: { root },
loader,
mode,
settings
} = this;
const filePath = new URL(`${routeData.component}`, root);
const { scripts } = settings.config.experimental.directRenderScript ? { scripts: /* @__PURE__ */ new Set() } : await getScriptsForURL(filePath, settings.config.root, loader);
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")) {
const src = await resolveIdToUrl(loader, "astro/runtime/client/dev-toolbar/entrypoint.js");
scripts.add({ props: { type: "module", src }, children: "" });
const additionalMetadata = {
root: fileURLToPath(settings.config.root),
version: ASTRO_VERSION,
latestAstroVersion: settings.latestAstroVersion,
debugInfo: await getInfoOutput({ userConfig: settings.config, print: false })
};
const children = `window.__astro_dev_toolbar__ = ${JSON.stringify(additionalMetadata)}`;
scripts.add({ props: {}, children });
}
}
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 links = /* @__PURE__ */ new Set();
const { urls, styles: _styles } = await getStylesForURL(filePath, loader);
for (const href of urls) {
links.add({ props: { rel: "stylesheet", href }, children: "" });
}
const styles = /* @__PURE__ */ new Set();
for (const { id, url: src, content } of _styles) {
scripts.add({ props: { type: "module", src }, children: "" });
styles.add({ props: { "data-vite-dev-id": id }, children: content });
}
return { scripts, styles, links };
}
componentMetadata(routeData) {
const {
config: { root },
loader
} = this;
const filePath = new URL(`${routeData.component}`, root);
return getComponentMetadata(filePath, loader);
}
async preload(routeData, filePath) {
const { loader } = this;
for (const route of this.defaultRoutes) {
if (route.matchesComponent(filePath)) {
return route.instance;
}
}
const renderers__ = this.settings.renderers.map((r) => loadRenderer(r, loader));
const renderers_ = await Promise.all(renderers__);
this.renderers = renderers_.filter((r) => Boolean(r));
try {
const componentInstance = await loader.import(viteID(filePath));
this.componentInterner.set(routeData, componentInstance);
return componentInstance;
} catch (error) {
if (MarkdownError.is(error) || CSSError.is(error) || AggregateError.is(error)) {
throw error;
}
throw enhanceViteSSRError({ error, filePath, loader });
}
}
clearRouteCache() {
this.routeCache.clearAll();
this.componentInterner = /* @__PURE__ */ new WeakMap();
}
async getComponentByRoute(routeData) {
const component = this.componentInterner.get(routeData);
if (component) {
return component;
} else {
const filePath = new URL(`${routeData.component}`, this.config.root);
return await this.preload(routeData, filePath);
}
}
async tryRewrite(payload, request) {
if (!this.manifestData) {
throw new Error("Missing manifest data. This is an internal error, please file an issue.");
}
const { routeData, pathname, newUrl } = findRouteToRewrite({
payload,
request,
routes: this.manifestData?.routes,
trailingSlash: this.config.trailingSlash,
buildFormat: this.config.build.format,
base: this.config.base
});
const componentInstance = await this.getComponentByRoute(routeData);
return { newUrl, pathname, componentInstance, routeData };
}
setManifestData(manifestData) {
this.manifestData = manifestData;
}
rewriteKnownRoute(route, sourceRoute) {
if (isServerLikeOutput(this.config) && sourceRoute.prerender) {
for (let def of this.defaultRoutes) {
if (route === def.route) {
return def.instance;
}
}
}
throw new Error("Unknown route");
}
}
export {
DevPipeline
};

View File

@@ -0,0 +1,17 @@
import type fs from 'node:fs';
import type * as vite from 'vite';
import type { AstroSettings, SSRManifest } from '../@types/astro.js';
import type { Logger } from '../core/logger/core.js';
export interface AstroPluginOptions {
settings: AstroSettings;
logger: Logger;
fs: typeof fs;
}
export default function createVitePluginAstroServer({ settings, logger, fs: fsMod, }: AstroPluginOptions): vite.Plugin;
/**
* It creates a `SSRManifest` from the `AstroSettings`.
*
* Renderers needs to be pulled out from the page module emitted during the build.
* @param settings
*/
export declare function createDevelopmentManifest(settings: AstroSettings): SSRManifest;

View File

@@ -0,0 +1,137 @@
import { AsyncLocalStorage } from "node:async_hooks";
import { IncomingMessage } from "node:http";
import { createKey } from "../core/encryption.js";
import { getViteErrorPayload } from "../core/errors/dev/index.js";
import { AstroError, AstroErrorData } from "../core/errors/index.js";
import { patchOverlay } from "../core/errors/overlay.js";
import { NOOP_MIDDLEWARE_FN } from "../core/middleware/noop-middleware.js";
import { createViteLoader } from "../core/module-loader/index.js";
import { injectDefaultRoutes } from "../core/routing/default.js";
import { createRouteManifest } from "../core/routing/index.js";
import { toFallbackType, toRoutingStrategy } from "../i18n/utils.js";
import { baseMiddleware } from "./base.js";
import { createController } from "./controller.js";
import { recordServerError } from "./error.js";
import { DevPipeline } from "./pipeline.js";
import { handleRequest } from "./request.js";
import { setRouteError } from "./server-state.js";
function createVitePluginAstroServer({
settings,
logger,
fs: fsMod
}) {
return {
name: "astro:server",
configureServer(viteServer) {
const loader = createViteLoader(viteServer);
const manifest = createDevelopmentManifest(settings);
let manifestData = injectDefaultRoutes(
manifest,
createRouteManifest({ settings, fsMod }, logger)
);
const pipeline = DevPipeline.create(manifestData, { loader, logger, manifest, settings });
const controller = createController({ loader });
const localStorage = new AsyncLocalStorage();
function rebuildManifest(needsManifestRebuild) {
pipeline.clearRouteCache();
if (needsManifestRebuild) {
manifestData = injectDefaultRoutes(manifest, createRouteManifest({ settings }, logger));
pipeline.setManifestData(manifestData);
}
}
viteServer.watcher.on("add", rebuildManifest.bind(null, true));
viteServer.watcher.on("unlink", rebuildManifest.bind(null, true));
viteServer.watcher.on("change", rebuildManifest.bind(null, false));
function handleUnhandledRejection(rejection) {
const error = new AstroError({
...AstroErrorData.UnhandledRejection,
message: AstroErrorData.UnhandledRejection.message(rejection?.stack || rejection)
});
const store = localStorage.getStore();
if (store instanceof IncomingMessage) {
const request = store;
setRouteError(controller.state, request.url, error);
}
const { errorWithMetadata } = recordServerError(loader, settings.config, pipeline, error);
setTimeout(
async () => loader.webSocketSend(await getViteErrorPayload(errorWithMetadata)),
200
);
}
process.on("unhandledRejection", handleUnhandledRejection);
viteServer.httpServer?.on("close", () => {
process.off("unhandledRejection", handleUnhandledRejection);
});
return () => {
viteServer.middlewares.stack.unshift({
route: "",
handle: baseMiddleware(settings, logger)
});
viteServer.middlewares.use(async function astroDevHandler(request, response) {
if (request.url === void 0 || !request.method) {
response.writeHead(500, "Incomplete request");
response.end();
return;
}
localStorage.run(request, () => {
handleRequest({
pipeline,
manifestData,
controller,
incomingRequest: request,
incomingResponse: response
});
});
});
};
},
transform(code, id, opts = {}) {
if (opts.ssr) return;
if (!id.includes("vite/dist/client/client.mjs")) return;
return patchOverlay(code);
}
};
}
function createDevelopmentManifest(settings) {
let i18nManifest = void 0;
if (settings.config.i18n) {
i18nManifest = {
fallback: settings.config.i18n.fallback,
strategy: toRoutingStrategy(settings.config.i18n.routing, settings.config.i18n.domains),
defaultLocale: settings.config.i18n.defaultLocale,
locales: settings.config.i18n.locales,
domainLookupTable: {},
fallbackType: toFallbackType(settings.config.i18n.routing)
};
}
return {
hrefRoot: settings.config.root.toString(),
trailingSlash: settings.config.trailingSlash,
buildFormat: settings.config.build.format,
compressHTML: settings.config.compressHTML,
assets: /* @__PURE__ */ new Set(),
entryModules: {},
routes: [],
adapterName: settings?.adapter?.name || "",
clientDirectives: settings.clientDirectives,
renderers: [],
base: settings.config.base,
assetsPrefix: settings.config.build.assetsPrefix,
site: settings.config.site,
componentMetadata: /* @__PURE__ */ new Map(),
inlinedScripts: /* @__PURE__ */ new Map(),
i18n: i18nManifest,
checkOrigin: settings.config.security?.checkOrigin ?? false,
experimentalEnvGetSecretEnabled: false,
key: createKey(),
middleware() {
return {
onRequest: NOOP_MIDDLEWARE_FN
};
}
};
}
export {
createDevelopmentManifest,
createVitePluginAstroServer as default
};

View File

@@ -0,0 +1,14 @@
import type http from 'node:http';
import type { ManifestData } from '../@types/astro.js';
import type { DevServerController } from './controller.js';
import type { DevPipeline } from './pipeline.js';
type HandleRequest = {
pipeline: DevPipeline;
manifestData: ManifestData;
controller: DevServerController;
incomingRequest: http.IncomingMessage;
incomingResponse: http.ServerResponse;
};
/** The main logic to route dev server requests to pages in Astro. */
export declare function handleRequest({ pipeline, manifestData, controller, incomingRequest, incomingResponse, }: HandleRequest): Promise<void>;
export {};

View File

@@ -0,0 +1,60 @@
import { removeTrailingForwardSlash } from "../core/path.js";
import { runWithErrorHandling } from "./controller.js";
import { recordServerError } from "./error.js";
import { handle500Response } from "./response.js";
import { handleRoute, matchRoute } from "./route.js";
async function handleRequest({
pipeline,
manifestData,
controller,
incomingRequest,
incomingResponse
}) {
const { config, loader } = pipeline;
const origin = `${loader.isHttps() ? "https" : "http"}://${incomingRequest.headers[":authority"] ?? incomingRequest.headers.host}`;
const url = new URL(origin + incomingRequest.url);
let pathname;
if (config.trailingSlash === "never" && !incomingRequest.url) {
pathname = "";
} else {
pathname = url.pathname;
}
url.pathname = removeTrailingForwardSlash(config.base) + url.pathname;
let body = void 0;
if (!(incomingRequest.method === "GET" || incomingRequest.method === "HEAD")) {
let bytes = [];
await new Promise((resolve) => {
incomingRequest.on("data", (part) => {
bytes.push(part);
});
incomingRequest.on("end", resolve);
});
body = Buffer.concat(bytes);
}
await runWithErrorHandling({
controller,
pathname,
async run() {
const matchedRoute = await matchRoute(pathname, manifestData, pipeline);
const resolvedPathname = matchedRoute?.resolvedPathname ?? pathname;
return await handleRoute({
matchedRoute,
url,
pathname: resolvedPathname,
body,
pipeline,
manifestData,
incomingRequest,
incomingResponse
});
},
onError(_err) {
const { error, errorWithMetadata } = recordServerError(loader, config, pipeline, _err);
handle500Response(loader, incomingResponse, errorWithMetadata);
return error;
}
});
}
export {
handleRequest
};

View File

@@ -0,0 +1,2 @@
import type { ModuleLoader } from '../core/module-loader/index.js';
export declare function createResolve(loader: ModuleLoader, root: URL): (s: string) => Promise<string>;

View File

@@ -0,0 +1,9 @@
import { resolveIdToUrl } from "../core/viteUtils.js";
function createResolve(loader, root) {
return async function(s) {
return await resolveIdToUrl(loader, s, root);
};
}
export {
createResolve
};

View File

@@ -0,0 +1,8 @@
import type http from 'node:http';
import type { ErrorWithMetadata } from '../core/errors/index.js';
import type { ModuleLoader } from '../core/module-loader/index.js';
export declare function handle404Response(origin: string, req: http.IncomingMessage, res: http.ServerResponse): Promise<void>;
export declare function handle500Response(loader: ModuleLoader, res: http.ServerResponse, err: ErrorWithMetadata): Promise<void>;
export declare function writeHtmlResponse(res: http.ServerResponse, statusCode: number, html: string): void;
export declare function writeWebResponse(res: http.ServerResponse, webResponse: Response): Promise<void>;
export declare function writeSSRResult(webRequest: Request, webResponse: Response, res: http.ServerResponse): Promise<void>;

View File

@@ -0,0 +1,92 @@
import { Http2ServerResponse } from "node:http2";
import { Readable } from "node:stream";
import { getSetCookiesFromResponse } from "../core/cookies/index.js";
import { getViteErrorPayload } from "../core/errors/dev/index.js";
import notFoundTemplate from "../template/4xx.js";
async function handle404Response(origin, req, res) {
const pathname = decodeURI(new URL(origin + req.url).pathname);
const html = notFoundTemplate({
statusCode: 404,
title: "Not found",
tabTitle: "404: Not Found",
pathname
});
writeHtmlResponse(res, 404, html);
}
async function handle500Response(loader, res, err) {
res.on(
"close",
async () => setTimeout(async () => loader.webSocketSend(await getViteErrorPayload(err)), 200)
);
if (res.headersSent) {
res.write(`<script type="module" src="/@vite/client"></script>`);
res.end();
} else {
writeHtmlResponse(
res,
500,
`<title>${err.name}</title><script type="module" src="/@vite/client"></script>`
);
}
}
function writeHtmlResponse(res, statusCode, html) {
res.writeHead(statusCode, {
"Content-Type": "text/html; charset=utf-8",
"Content-Length": Buffer.byteLength(html, "utf-8")
});
res.write(html);
res.end();
}
async function writeWebResponse(res, webResponse) {
const { status, headers, body, statusText } = webResponse;
const setCookieHeaders = Array.from(getSetCookiesFromResponse(webResponse));
if (setCookieHeaders.length) {
res.setHeader("set-cookie", setCookieHeaders);
}
const _headers = Object.fromEntries(headers.entries());
if (headers.has("set-cookie")) {
_headers["set-cookie"] = headers.getSetCookie();
}
if (!(res instanceof Http2ServerResponse)) {
res.statusMessage = statusText;
}
res.writeHead(status, _headers);
if (body) {
if (Symbol.for("astro.responseBody") in webResponse) {
let stream = webResponse[Symbol.for("astro.responseBody")];
for await (const chunk of stream) {
res.write(chunk.toString());
}
} else if (body instanceof Readable) {
body.pipe(res);
return;
} else if (typeof body === "string") {
res.write(body);
} else {
const reader = body.getReader();
res.on("close", () => {
reader.cancel().catch(() => {
});
});
while (true) {
const { done, value } = await reader.read();
if (done) break;
if (value) {
res.write(value);
}
}
}
}
res.end();
}
async function writeSSRResult(webRequest, webResponse, res) {
Reflect.set(webRequest, Symbol.for("astro.responseSent"), true);
return writeWebResponse(res, webResponse);
}
export {
handle404Response,
handle500Response,
writeHtmlResponse,
writeSSRResult,
writeWebResponse
};

View File

@@ -0,0 +1,24 @@
import type http from 'node:http';
import type { ComponentInstance, ManifestData, RouteData } from '../@types/astro.js';
import type { DevPipeline } from './pipeline.js';
type AsyncReturnType<T extends (...args: any) => Promise<any>> = T extends (...args: any) => Promise<infer R> ? R : any;
export interface MatchedRoute {
route: RouteData;
filePath: URL;
resolvedPathname: string;
preloadedComponent: ComponentInstance;
mod: ComponentInstance;
}
export declare function matchRoute(pathname: string, manifestData: ManifestData, pipeline: DevPipeline): Promise<MatchedRoute | undefined>;
type HandleRoute = {
matchedRoute: AsyncReturnType<typeof matchRoute>;
url: URL;
pathname: string;
body: ArrayBuffer | undefined;
manifestData: ManifestData;
incomingRequest: http.IncomingMessage;
incomingResponse: http.ServerResponse;
pipeline: DevPipeline;
};
export declare function handleRoute({ matchedRoute, url, pathname, body, pipeline, manifestData, incomingRequest, incomingResponse, }: HandleRoute): Promise<void>;
export {};

View File

@@ -0,0 +1,223 @@
import {
DEFAULT_404_COMPONENT,
NOOP_MIDDLEWARE_HEADER,
REROUTE_DIRECTIVE_HEADER,
REWRITE_DIRECTIVE_HEADER_KEY,
clientLocalsSymbol
} from "../core/constants.js";
import { AstroErrorData, isAstroError } from "../core/errors/index.js";
import { req } from "../core/messages.js";
import { loadMiddleware } from "../core/middleware/loadMiddleware.js";
import { RenderContext } from "../core/render-context.js";
import { getProps } from "../core/render/index.js";
import { createRequest } from "../core/request.js";
import { matchAllRoutes } from "../core/routing/index.js";
import { getSortedPreloadedMatches } from "../prerender/routing.js";
import { writeSSRResult, writeWebResponse } from "./response.js";
function isLoggedRequest(url) {
return url !== "/favicon.ico";
}
function getCustom404Route(manifestData) {
const route404 = /^\/404\/?$/;
return manifestData.routes.find((r) => route404.test(r.route));
}
function getCustom500Route(manifestData) {
const route500 = /^\/500\/?$/;
return manifestData.routes.find((r) => route500.test(r.route));
}
async function matchRoute(pathname, manifestData, pipeline) {
const { config, logger, routeCache, serverLike, settings } = pipeline;
const matches = matchAllRoutes(pathname, manifestData);
const preloadedMatches = await getSortedPreloadedMatches({ pipeline, matches, settings });
for await (const { preloadedComponent, route: maybeRoute, filePath } of preloadedMatches) {
try {
await getProps({
mod: preloadedComponent,
routeData: maybeRoute,
routeCache,
pathname,
logger,
serverLike
});
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$/, "/").replace(/\.html$/, "");
if (altPathname !== pathname) {
return await matchRoute(altPathname, manifestData, pipeline);
}
if (matches.length) {
const possibleRoutes = matches.flatMap((route) => route.component);
logger.warn(
"router",
`${AstroErrorData.NoMatchingStaticPathFound.message(
pathname
)}
${AstroErrorData.NoMatchingStaticPathFound.hint(possibleRoutes)}`
);
}
const custom404 = getCustom404Route(manifestData);
if (custom404) {
const filePath = new URL(`./${custom404.component}`, config.root);
const preloadedComponent = await pipeline.preload(custom404, filePath);
return {
route: custom404,
filePath,
resolvedPathname: pathname,
preloadedComponent,
mod: preloadedComponent
};
}
return void 0;
}
async function handleRoute({
matchedRoute,
url,
pathname,
body,
pipeline,
manifestData,
incomingRequest,
incomingResponse
}) {
const timeStart = performance.now();
const { config, loader, logger } = pipeline;
if (!matchedRoute) {
throw new Error("No route matched, and default 404 route was not found.");
}
let request;
let renderContext;
let mod = void 0;
let route;
const middleware = (await loadMiddleware(loader)).onRequest;
const locals = Reflect.get(incomingRequest, clientLocalsSymbol);
const { preloadedComponent } = matchedRoute;
route = matchedRoute.route;
request = createRequest({
base: config.base,
url,
headers: incomingRequest.headers,
method: incomingRequest.method,
body,
logger,
clientAddress: incomingRequest.socket.remoteAddress,
staticLike: config.output === "static" || route.prerender
});
for (const [name, value] of Object.entries(config.server.headers ?? {})) {
if (value) incomingResponse.setHeader(name, value);
}
mod = preloadedComponent;
renderContext = await RenderContext.create({
locals,
pipeline,
pathname,
middleware: isDefaultPrerendered404(matchedRoute.route) ? void 0 : middleware,
request,
routeData: route
});
let response;
let statusCode = 200;
let isReroute = false;
let isRewrite = false;
try {
response = await renderContext.render(mod);
isReroute = response.headers.has(REROUTE_DIRECTIVE_HEADER);
isRewrite = response.headers.has(REWRITE_DIRECTIVE_HEADER_KEY);
const statusCodedMatched = getStatusByMatchedRoute(matchedRoute);
statusCode = isRewrite ? (
// Ignore `matchedRoute` status for rewrites
response.status
) : (
// Our internal noop middleware sets a particular header. If the header isn't present, it means that the user have
// their own middleware, so we need to return what the user returns.
!response.headers.has(NOOP_MIDDLEWARE_HEADER) && !isReroute ? response.status : statusCodedMatched ?? response.status
);
} catch (err) {
const custom500 = getCustom500Route(manifestData);
if (!custom500) {
throw err;
}
logger.error("router", err.stack || err.message);
const filePath500 = new URL(`./${custom500.component}`, config.root);
const preloaded500Component = await pipeline.preload(custom500, filePath500);
renderContext.props.error = err;
response = await renderContext.render(preloaded500Component);
statusCode = 500;
}
if (isLoggedRequest(pathname)) {
const timeEnd = performance.now();
logger.info(
null,
req({
url: pathname,
method: incomingRequest.method,
statusCode,
isRewrite,
reqTime: timeEnd - timeStart
})
);
}
if (statusCode === 404 && response.headers.get(REROUTE_DIRECTIVE_HEADER) !== "no") {
const fourOhFourRoute = await matchRoute("/404", manifestData, pipeline);
if (fourOhFourRoute) {
renderContext = await RenderContext.create({
locals,
pipeline,
pathname,
middleware: isDefaultPrerendered404(fourOhFourRoute.route) ? void 0 : middleware,
request,
routeData: fourOhFourRoute.route
});
response = await renderContext.render(fourOhFourRoute.preloadedComponent);
}
}
if (isReroute) {
response.headers.delete(REROUTE_DIRECTIVE_HEADER);
}
if (isRewrite) {
response.headers.delete(REROUTE_DIRECTIVE_HEADER);
}
if (route.type === "endpoint") {
await writeWebResponse(incomingResponse, response);
return;
}
if (isRewrite) {
await writeSSRResult(request, response, incomingResponse);
return;
}
if (response.status < 400 && response.status >= 300) {
await writeSSRResult(request, response, incomingResponse);
return;
}
if (response.status !== statusCode) {
response = new Response(response.body, {
status: statusCode,
headers: response.headers
});
}
await writeSSRResult(request, response, incomingResponse);
}
function getStatusByMatchedRoute(matchedRoute) {
if (matchedRoute?.route.route === "/404") return 404;
if (matchedRoute?.route.route === "/500") return 500;
return void 0;
}
function isDefaultPrerendered404(route) {
return route.route === "/404" && route.prerender && route.component === DEFAULT_404_COMPONENT;
}
export {
handleRoute,
matchRoute
};

View File

@@ -0,0 +1,6 @@
import type { SSRElement } from '../@types/astro.js';
import type { ModuleLoader } from '../core/module-loader/index.js';
export declare function getScriptsForURL(filePath: URL, root: URL, loader: ModuleLoader): Promise<{
scripts: Set<SSRElement>;
crawledFiles: Set<string>;
}>;

View File

@@ -0,0 +1,38 @@
import { createModuleScriptElementWithSrc } from "../core/render/ssr-element.js";
import { viteID } from "../core/util.js";
import { rootRelativePath } from "../core/viteUtils.js";
import { crawlGraph } from "./vite.js";
async function getScriptsForURL(filePath, root, loader) {
const elements = /* @__PURE__ */ new Set();
const crawledFiles = /* @__PURE__ */ new Set();
const rootID = viteID(filePath);
const modInfo = loader.getModuleInfo(rootID);
addHoistedScripts(elements, modInfo, root);
for await (const moduleNode of crawlGraph(loader, rootID, true)) {
if (moduleNode.file) {
crawledFiles.add(moduleNode.file);
}
const id = moduleNode.id;
if (id) {
const info = loader.getModuleInfo(id);
addHoistedScripts(elements, info, root);
}
}
return { scripts: elements, crawledFiles };
}
function addHoistedScripts(set, info, root) {
if (!info?.meta?.astro) {
return;
}
let id = info.id;
const astro = info?.meta?.astro;
for (let i = 0; i < astro.scripts.length; i++) {
let scriptId = `${id}?astro&type=script&index=${i}&lang.ts`;
scriptId = rootRelativePath(root, scriptId);
const element = createModuleScriptElementWithSrc(scriptId);
set.add(element);
}
}
export {
getScriptsForURL
};

View File

@@ -0,0 +1,15 @@
export type ErrorState = 'fresh' | 'error';
export interface RouteState {
state: ErrorState;
error?: Error;
}
export interface ServerState {
routes: Map<string, RouteState>;
state: ErrorState;
error?: Error;
}
export declare function createServerState(): ServerState;
export declare function hasAnyFailureState(serverState: ServerState): boolean;
export declare function setRouteError(serverState: ServerState, pathname: string, error: Error): void;
export declare function setServerError(serverState: ServerState, error: Error): void;
export declare function clearRouteError(serverState: ServerState, pathname: string): void;

View File

@@ -0,0 +1,42 @@
function createServerState() {
return {
routes: /* @__PURE__ */ new Map(),
state: "fresh"
};
}
function hasAnyFailureState(serverState) {
return serverState.state !== "fresh";
}
function setRouteError(serverState, pathname, error) {
if (serverState.routes.has(pathname)) {
const routeState = serverState.routes.get(pathname);
routeState.state = "error";
routeState.error = error;
} else {
const routeState = {
state: "error",
error
};
serverState.routes.set(pathname, routeState);
}
serverState.state = "error";
serverState.error = error;
}
function setServerError(serverState, error) {
serverState.state = "error";
serverState.error = error;
}
function clearRouteError(serverState, pathname) {
if (serverState.routes.has(pathname)) {
serverState.routes.delete(pathname);
}
serverState.state = "fresh";
serverState.error = void 0;
}
export {
clearRouteError,
createServerState,
hasAnyFailureState,
setRouteError,
setServerError
};

View File

@@ -0,0 +1,3 @@
import { isCSSRequest } from 'vite';
export { isCSSRequest };
export declare const isBuildableCSSRequest: (request: string) => boolean;

View File

@@ -0,0 +1,8 @@
import { isCSSRequest } from "vite";
const rawRE = /(?:\?|&)raw(?:&|$)/;
const inlineRE = /(?:\?|&)inline\b/;
const isBuildableCSSRequest = (request) => isCSSRequest(request) && !rawRE.test(request) && !inlineRE.test(request);
export {
isBuildableCSSRequest,
isCSSRequest
};

View File

@@ -0,0 +1,3 @@
import type { ModuleLoader, ModuleNode } from '../core/module-loader/index.js';
/** recursively crawl the module graph to get all style files imported by parent id */
export declare function crawlGraph(loader: ModuleLoader, _id: string, isRootFile: boolean, scanned?: Set<string>): AsyncGenerator<ModuleNode, void, unknown>;

View File

@@ -0,0 +1,72 @@
import npath from "node:path";
import { SUPPORTED_MARKDOWN_FILE_EXTENSIONS } from "../core/constants.js";
import { unwrapId } from "../core/util.js";
import { hasSpecialQueries } from "../vite-plugin-utils/index.js";
import { isCSSRequest } from "./util.js";
const fileExtensionsToSSR = /* @__PURE__ */ new Set([".astro", ".mdoc", ...SUPPORTED_MARKDOWN_FILE_EXTENSIONS]);
const STRIP_QUERY_PARAMS_REGEX = /\?.*$/;
async function* crawlGraph(loader, _id, isRootFile, scanned = /* @__PURE__ */ new Set()) {
const id = unwrapId(_id);
const importedModules = /* @__PURE__ */ new Set();
const moduleEntriesForId = isRootFile ? (
// "getModulesByFile" pulls from a delayed module cache (fun implementation detail),
// So we can get up-to-date info on initial server load.
// Needed for slower CSS preprocessing like Tailwind
loader.getModulesByFile(id) ?? /* @__PURE__ */ new Set()
) : (
// For non-root files, we're safe to pull from "getModuleById" based on testing.
// TODO: Find better invalidation strategy to use "getModuleById" in all cases!
/* @__PURE__ */ new Set([loader.getModuleById(id)])
);
for (const entry of moduleEntriesForId) {
if (!entry) {
continue;
}
if (id === entry.id) {
scanned.add(id);
if (isCSSRequest(id)) {
continue;
}
if (hasSpecialQueries(id)) {
continue;
}
for (const importedModule of entry.importedModules) {
if (!importedModule.id) continue;
const importedModulePathname = importedModule.id.replace(STRIP_QUERY_PARAMS_REGEX, "");
const isFileTypeNeedingSSR = fileExtensionsToSSR.has(npath.extname(importedModulePathname));
const isPropagationStoppingPoint = importedModule.id.includes("?astroPropagatedAssets");
if (isFileTypeNeedingSSR && // Should not SSR a module with ?astroPropagatedAssets
!isPropagationStoppingPoint) {
const mod = loader.getModuleById(importedModule.id);
if (!mod?.ssrModule) {
try {
await loader.import(importedModule.id);
} catch {
}
}
}
if (isImportedBy(id, importedModule) && !isPropagationStoppingPoint) {
importedModules.add(importedModule);
}
}
}
}
for (const importedModule of importedModules) {
if (!importedModule.id || scanned.has(importedModule.id)) {
continue;
}
yield importedModule;
yield* crawlGraph(loader, importedModule.id, false, scanned);
}
}
function isImportedBy(parent, entry) {
for (const importer of entry.importers) {
if (importer.id === parent) {
return true;
}
}
return false;
}
export {
crawlGraph
};