diff --git a/.changeset/pink-ligers-share.md b/.changeset/pink-ligers-share.md
new file mode 100644
index 000000000000..e7923350fb5b
--- /dev/null
+++ b/.changeset/pink-ligers-share.md
@@ -0,0 +1,49 @@
+---
+"astro": minor
+---
+
+Adds experimental rewriting in Astro with a new `rewrite()` function and the middleware `next()` function.
+
+The feature is available via an experimental flag in `astro.config.mjs`:
+
+```js
+export default defineConfig({
+ experimental: {
+ rewriting: true
+ }
+})
+```
+
+When enabled, you can use `rewrite()` to **render** another page without changing the URL of the browser in Astro pages and endpoints.
+
+```astro
+---
+// src/pages/dashboard.astro
+if (!Astro.props.allowed) {
+ return Astro.rewrite("/")
+}
+---
+```
+
+```js
+// src/pages/api.js
+export function GET(ctx) {
+ if (!ctx.locals.allowed) {
+ return ctx.rewrite("/")
+ }
+}
+```
+
+The middleware `next()` function now accepts a parameter with the same type as the `rewrite()` function. For example, with `next("/")`, you can call the next middleware function with a new `Request`.
+
+```js
+// src/middleware.js
+export function onRequest(ctx, next) {
+ if (!ctx.cookies.get("allowed")) {
+ return next("/") // new signature
+ }
+ return next();
+}
+```
+
+> **NOTE**: please [read the RFC](https://github.com/withastro/roadmap/blob/feat/reroute/proposals/0047-rerouting.md) to understand the current expectations of the new APIs.
diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts
index 0cff203cf227..ba9cfe3a7d09 100644
--- a/packages/astro/src/@types/astro.ts
+++ b/packages/astro/src/@types/astro.ts
@@ -250,6 +250,19 @@ export interface AstroGlobal<
* [Astro reference](https://docs.astro.build/en/guides/server-side-rendering/)
*/
redirect: AstroSharedContext['redirect'];
+ /**
+ * It rewrites to another page. As opposed to redirects, the URL won't change, and Astro will render the HTML emitted
+ * by the rewritten URL passed as argument.
+ *
+ * ## Example
+ *
+ * ```js
+ * if (pageIsNotEnabled) {
+ * return Astro.rewrite('/fallback-page')
+ * }
+ * ```
+ */
+ rewrite: AstroSharedContext['rewrite'];
/**
* The element allows a component to reference itself recursively.
*
@@ -1641,7 +1654,7 @@ export interface AstroUserConfig {
domains?: Record;
};
- /** ⚠️ WARNING: SUBJECT TO CHANGE */
+ /** ! WARNING: SUBJECT TO CHANGE */
db?: Config.Database;
/**
@@ -1922,6 +1935,62 @@ export interface AstroUserConfig {
origin?: boolean;
};
};
+
+ /**
+ * @docs
+ * @name experimental.rewriting
+ * @type {boolean}
+ * @default `false`
+ * @version 4.8.0
+ * @description
+ *
+ * Enables a routing feature for rewriting requests in Astro pages, endpoints and Astro middleware, giving you programmatic control over your routes.
+ *
+ * ```js
+ * {
+ * experimental: {
+ * rewriting: true,
+ * },
+ * }
+ * ```
+ *
+ * Use `Astro.rewrite` in your `.astro` files to reroute to a different page:
+ *
+ * ```astro "rewrite"
+ * ---
+ * // src/pages/dashboard.astro
+ * if (!Astro.props.allowed) {
+ * return Astro.rewrite("/")
+ * }
+ * ---
+ * ```
+ *
+ * Use `context.rewrite` in your endpoint files to reroute to a different page:
+ *
+ * ```js
+ * // src/pages/api.js
+ * export function GET(ctx) {
+ * if (!ctx.locals.allowed) {
+ * return ctx.rewrite("/")
+ * }
+ * }
+ * ```
+ *
+ * Use `next("/")` in your middleware file to reroute to a different page, and then call the next middleware function:
+ *
+ * ```js
+ * // src/middleware.js
+ * export function onRequest(ctx, next) {
+ * if (!ctx.cookies.get("allowed")) {
+ * return next("/") // new signature
+ * }
+ * return next();
+ * }
+ * ```
+ *
+ * For a complete overview, and to give feedback on this experimental API, see the [Rerouting RFC](https://github.com/withastro/roadmap/blob/feat/reroute/proposals/0047-rerouting.md).
+ */
+ rewriting: boolean;
};
}
@@ -2491,6 +2560,20 @@ interface AstroSharedContext<
*/
redirect(path: string, status?: ValidRedirectStatus): Response;
+ /**
+ * It rewrites to another page. As opposed to redirects, the URL won't change, and Astro will render the HTML emitted
+ * by the rerouted URL passed as argument.
+ *
+ * ## Example
+ *
+ * ```js
+ * if (pageIsNotEnabled) {
+ * return Astro.rewrite('/fallback-page')
+ * }
+ * ```
+ */
+ rewrite(rewritePayload: RewritePayload): Promise;
+
/**
* Object accessed via Astro middleware
*/
@@ -2605,6 +2688,21 @@ export interface APIContext<
*/
redirect: AstroSharedContext['redirect'];
+ /**
+ * It reroutes to another page. As opposed to redirects, the URL won't change, and Astro will render the HTML emitted
+ * by the rerouted URL passed as argument.
+ *
+ * ## Example
+ *
+ * ```ts
+ * // src/pages/secret.ts
+ * export function GET(ctx) {
+ * return ctx.rewrite(new URL("../"), ctx.url);
+ * }
+ * ```
+ */
+ rewrite: AstroSharedContext['rewrite'];
+
/**
* An object that middlewares can use to store extra information related to the request.
*
@@ -2799,7 +2897,9 @@ export interface AstroIntegration {
};
}
-export type MiddlewareNext = () => Promise;
+export type RewritePayload = string | URL | Request;
+
+export type MiddlewareNext = (rewritePayload?: RewritePayload) => Promise;
export type MiddlewareHandler = (
context: APIContext,
next: MiddlewareNext
diff --git a/packages/astro/src/core/app/index.ts b/packages/astro/src/core/app/index.ts
index 116151610e1c..1ba5d9479833 100644
--- a/packages/astro/src/core/app/index.ts
+++ b/packages/astro/src/core/app/index.ts
@@ -1,13 +1,6 @@
-import type {
- ComponentInstance,
- ManifestData,
- RouteData,
- SSRManifest,
-} from '../../@types/astro.js';
+import type { ManifestData, RouteData, SSRManifest } from '../../@types/astro.js';
import { normalizeTheLocale } from '../../i18n/index.js';
-import type { SinglePageBuiltModule } from '../build/types.js';
import {
- DEFAULT_404_COMPONENT,
REROUTABLE_STATUS_CODES,
REROUTE_DIRECTIVE_HEADER,
clientAddressSymbol,
@@ -26,7 +19,6 @@ import {
prependForwardSlash,
removeTrailingForwardSlash,
} from '../path.js';
-import { RedirectSinglePageBuiltModule } from '../redirects/index.js';
import { RenderContext } from '../render-context.js';
import { createAssetLink } from '../render/ssr-element.js';
import { ensure404Route } from '../routing/astro-designed-error-pages.js';
@@ -96,7 +88,7 @@ export class App {
routes: manifest.routes.map((route) => route.routeData),
});
this.#baseWithoutTrailingSlash = removeTrailingForwardSlash(this.#manifest.base);
- this.#pipeline = this.#createPipeline(streaming);
+ this.#pipeline = this.#createPipeline(this.#manifestData, streaming);
this.#adapterLogger = new AstroIntegrationLogger(
this.#logger.options,
this.#manifest.adapterName
@@ -110,10 +102,11 @@ export class App {
/**
* Creates a pipeline by reading the stored manifest
*
+ * @param manifestData
* @param streaming
* @private
*/
- #createPipeline(streaming = false) {
+ #createPipeline(manifestData: ManifestData, streaming = false) {
if (this.#manifest.checkOrigin) {
this.#manifest.middleware = sequence(
createOriginCheckMiddleware(),
@@ -121,7 +114,7 @@ export class App {
);
}
- return AppPipeline.create({
+ return AppPipeline.create(manifestData, {
logger: this.#logger,
manifest: this.#manifest,
mode: 'production',
@@ -309,7 +302,7 @@ export class App {
}
const pathname = this.#getPathnameFromRequest(request);
const defaultStatus = this.#getDefaultStatusCode(routeData, pathname);
- const mod = await this.#getModuleForRoute(routeData);
+ const mod = await this.#pipeline.getModuleForRoute(routeData);
let response;
try {
@@ -405,7 +398,7 @@ export class App {
return this.#mergeResponses(response, originalResponse, override);
}
- const mod = await this.#getModuleForRoute(errorRouteData);
+ const mod = await this.#pipeline.getModuleForRoute(errorRouteData);
try {
const renderContext = RenderContext.create({
locals,
@@ -493,35 +486,4 @@ export class App {
if (route.endsWith('/500')) return 500;
return 200;
}
-
- async #getModuleForRoute(route: RouteData): Promise {
- if (route.component === DEFAULT_404_COMPONENT) {
- return {
- page: async () =>
- ({ default: () => new Response(null, { status: 404 }) }) as ComponentInstance,
- renderers: [],
- };
- }
- if (route.type === 'redirect') {
- return RedirectSinglePageBuiltModule;
- } else {
- if (this.#manifest.pageMap) {
- const importComponentInstance = this.#manifest.pageMap.get(route.component);
- if (!importComponentInstance) {
- throw new Error(
- `Unexpectedly unable to find a component instance for route ${route.route}`
- );
- }
- const pageModule = await importComponentInstance();
- return pageModule;
- } else if (this.#manifest.pageModule) {
- const importComponentInstance = this.#manifest.pageModule;
- return importComponentInstance;
- } else {
- throw new Error(
- "Astro couldn't find the correct page to render, probably because it wasn't correctly mapped for SSR usage. This is an internal error, please file an issue."
- );
- }
- }
- }
}
diff --git a/packages/astro/src/core/app/pipeline.ts b/packages/astro/src/core/app/pipeline.ts
index b1c615a1eb36..77d2f80b24f2 100644
--- a/packages/astro/src/core/app/pipeline.ts
+++ b/packages/astro/src/core/app/pipeline.ts
@@ -1,21 +1,46 @@
-import type { RouteData, SSRElement, SSRResult } from '../../@types/astro.js';
+import type {
+ ManifestData,
+ RouteData,
+ SSRElement,
+ SSRResult,
+ ComponentInstance,
+ RewritePayload,
+} from '../../@types/astro.js';
import { Pipeline } from '../base-pipeline.js';
+import { DEFAULT_404_COMPONENT } from '../constants.js';
+import { RedirectSinglePageBuiltModule } from '../redirects/component.js';
import { createModuleScriptElement, createStylesheetElementSet } from '../render/ssr-element.js';
+import type { SinglePageBuiltModule } from '../build/types.js';
export class AppPipeline extends Pipeline {
- static create({
- logger,
- manifest,
- mode,
- renderers,
- resolve,
- serverLike,
- streaming,
- }: Pick<
- AppPipeline,
- 'logger' | 'manifest' | 'mode' | 'renderers' | 'resolve' | 'serverLike' | 'streaming'
- >) {
- return new AppPipeline(logger, manifest, mode, renderers, resolve, serverLike, streaming);
+ #manifestData: ManifestData | undefined;
+
+ static create(
+ manifestData: ManifestData,
+ {
+ logger,
+ manifest,
+ mode,
+ renderers,
+ resolve,
+ serverLike,
+ streaming,
+ }: Pick<
+ AppPipeline,
+ 'logger' | 'manifest' | 'mode' | 'renderers' | 'resolve' | 'serverLike' | 'streaming'
+ >
+ ) {
+ const pipeline = new AppPipeline(
+ logger,
+ manifest,
+ mode,
+ renderers,
+ resolve,
+ serverLike,
+ streaming
+ );
+ pipeline.#manifestData = manifestData;
+ return pipeline;
}
headElements(routeData: RouteData): Pick {
@@ -41,4 +66,64 @@ export class AppPipeline extends Pipeline {
}
componentMetadata() {}
+ async getComponentByRoute(routeData: RouteData): Promise {
+ const module = await this.getModuleForRoute(routeData);
+ return module.page();
+ }
+
+ async tryRewrite(payload: RewritePayload): Promise<[RouteData, ComponentInstance]> {
+ let foundRoute;
+
+ for (const route of this.#manifestData!.routes) {
+ if (payload instanceof URL) {
+ if (route.pattern.test(payload.pathname)) {
+ foundRoute = route;
+ break;
+ }
+ } else if (payload instanceof Request) {
+ const url = new URL(payload.url);
+ if (route.pattern.test(url.pathname)) {
+ foundRoute = route;
+ break;
+ }
+ } else if (route.pattern.test(decodeURI(payload))) {
+ foundRoute = route;
+ break;
+ }
+ }
+
+ if (foundRoute) {
+ const componentInstance = await this.getComponentByRoute(foundRoute);
+ return [foundRoute, componentInstance];
+ }
+ throw new Error('Route not found');
+ }
+
+ async getModuleForRoute(route: RouteData): Promise {
+ if (route.component === DEFAULT_404_COMPONENT) {
+ return {
+ page: async () =>
+ ({ default: () => new Response(null, { status: 404 }) }) as ComponentInstance,
+ renderers: [],
+ };
+ }
+ if (route.type === 'redirect') {
+ return RedirectSinglePageBuiltModule;
+ } else {
+ if (this.manifest.pageMap) {
+ const importComponentInstance = this.manifest.pageMap.get(route.component);
+ if (!importComponentInstance) {
+ throw new Error(
+ `Unexpectedly unable to find a component instance for route ${route.route}`
+ );
+ }
+ return await importComponentInstance();
+ } else if (this.manifest.pageModule) {
+ return this.manifest.pageModule;
+ }
+ throw new Error(
+ "Astro couldn't find the correct page to render, probably because it wasn't correctly mapped for SSR usage. This is an internal error, please file an issue."
+ );
+ }
+ }
}
diff --git a/packages/astro/src/core/app/types.ts b/packages/astro/src/core/app/types.ts
index fd56c6f1068f..30134252ef9d 100644
--- a/packages/astro/src/core/app/types.ts
+++ b/packages/astro/src/core/app/types.ts
@@ -65,6 +65,8 @@ export type SSRManifest = {
i18n: SSRManifestI18n | undefined;
middleware: MiddlewareHandler;
checkOrigin: boolean;
+ // TODO: remove once the experimental flag is removed
+ rewritingEnabled: boolean;
};
export type SSRManifestI18n = {
diff --git a/packages/astro/src/core/base-pipeline.ts b/packages/astro/src/core/base-pipeline.ts
index 832823db35fa..11cff7c809f5 100644
--- a/packages/astro/src/core/base-pipeline.ts
+++ b/packages/astro/src/core/base-pipeline.ts
@@ -1,5 +1,7 @@
import type {
+ ComponentInstance,
MiddlewareHandler,
+ RewritePayload,
RouteData,
RuntimeMode,
SSRLoadedRenderer,
@@ -59,6 +61,23 @@ export abstract class Pipeline {
abstract headElements(routeData: RouteData): Promise | HeadElements;
abstract componentMetadata(routeData: RouteData): Promise | void;
+
+ /**
+ * It attempts to retrieve the `RouteData` that matches the input `url`, and the component that belongs to the `RouteData`.
+ *
+ * ## Errors
+ *
+ * - if not `RouteData` is found
+ *
+ * @param {RewritePayload} rewritePayload
+ */
+ abstract tryRewrite(rewritePayload: RewritePayload): Promise<[RouteData, ComponentInstance]>;
+
+ /**
+ * Tells the pipeline how to retrieve a component give a `RouteData`
+ * @param routeData
+ */
+ abstract getComponentByRoute(routeData: RouteData): Promise;
}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts
index ffe799f6e7e4..355d551eaa6c 100644
--- a/packages/astro/src/core/build/generate.ts
+++ b/packages/astro/src/core/build/generate.ts
@@ -35,24 +35,14 @@ import { getOutputDirectory } from '../../prerender/utils.js';
import type { SSRManifestI18n } from '../app/types.js';
import { NoPrerenderedRoutesWithDomains } from '../errors/errors-data.js';
import { AstroError, AstroErrorData } from '../errors/index.js';
-import { routeIsFallback } from '../redirects/helpers.js';
-import {
- RedirectSinglePageBuiltModule,
- getRedirectLocationOrThrow,
- routeIsRedirect,
-} from '../redirects/index.js';
+import { getRedirectLocationOrThrow, routeIsRedirect } from '../redirects/index.js';
import { RenderContext } from '../render-context.js';
import { callGetStaticPaths } from '../render/route-cache.js';
import { createRequest } from '../request.js';
import { matchRoute } from '../routing/match.js';
import { getOutputFilename, isServerLikeOutput } from '../util.js';
import { getOutDirWithinCwd, getOutFile, getOutFolder } from './common.js';
-import {
- cssOrder,
- getEntryFilePathFromComponentPath,
- getPageDataByComponent,
- mergeInlineCss,
-} from './internal.js';
+import { cssOrder, getPageDataByComponent, mergeInlineCss } from './internal.js';
import { BuildPipeline } from './pipeline.js';
import type {
PageBuildData,
@@ -66,46 +56,6 @@ function createEntryURL(filePath: string, outFolder: URL) {
return new URL('./' + filePath + `?time=${Date.now()}`, outFolder);
}
-async function getEntryForRedirectRoute(
- route: RouteData,
- internals: BuildInternals,
- outFolder: URL
-): Promise {
- if (route.type !== 'redirect') {
- throw new Error(`Expected a redirect route.`);
- }
- if (route.redirectRoute) {
- const filePath = getEntryFilePathFromComponentPath(internals, route.redirectRoute.component);
- if (filePath) {
- const url = createEntryURL(filePath, outFolder);
- const ssrEntryPage: SinglePageBuiltModule = await import(url.toString());
- return ssrEntryPage;
- }
- }
-
- return RedirectSinglePageBuiltModule;
-}
-
-async function getEntryForFallbackRoute(
- route: RouteData,
- internals: BuildInternals,
- outFolder: URL
-): Promise {
- if (route.type !== 'fallback') {
- throw new Error(`Expected a redirect route.`);
- }
- if (route.redirectRoute) {
- const filePath = getEntryFilePathFromComponentPath(internals, route.redirectRoute.component);
- if (filePath) {
- const url = createEntryURL(filePath, outFolder);
- const ssrEntryPage: SinglePageBuiltModule = await import(url.toString());
- return ssrEntryPage;
- }
- }
-
- return RedirectSinglePageBuiltModule;
-}
-
// Gives back a facadeId that is relative to the root.
// ie, src/pages/index.astro instead of /Users/name..../src/pages/index.astro
export function rootRelativeFacadeId(facadeId: string, settings: AstroSettings): string {
@@ -185,14 +135,15 @@ export async function generatePages(options: StaticBuildOptions, internals: Buil
});
}
- const ssrEntryURLPage = createEntryURL(filePath, outFolder);
- const ssrEntryPage = await import(ssrEntryURLPage.toString());
+ const ssrEntryPage = await pipeline.retrieveSsrEntry(pageData.route, filePath);
if (options.settings.adapter?.adapterFeatures?.functionPerRoute) {
// forcing to use undefined, so we fail in an expected way if the module is not even there.
+ // @ts-expect-error When building for `functionPerRoute`, the module exports a `pageModule` function instead
const ssrEntry = ssrEntryPage?.pageModule;
if (ssrEntry) {
await generatePage(pageData, ssrEntry, builtPaths, pipeline);
} else {
+ const ssrEntryURLPage = createEntryURL(filePath, outFolder);
throw new Error(
`Unable to find the manifest for the module ${ssrEntryURLPage.toString()}. This is unexpected and likely a bug in Astro, please report.`
);
@@ -205,18 +156,8 @@ export async function generatePages(options: StaticBuildOptions, internals: Buil
}
} else {
for (const [pageData, filePath] of pagesToGenerate) {
- if (routeIsRedirect(pageData.route)) {
- const entry = await getEntryForRedirectRoute(pageData.route, internals, outFolder);
- await generatePage(pageData, entry, builtPaths, pipeline);
- } else if (routeIsFallback(pageData.route)) {
- const entry = await getEntryForFallbackRoute(pageData.route, internals, outFolder);
- await generatePage(pageData, entry, builtPaths, pipeline);
- } else {
- const ssrEntryURLPage = createEntryURL(filePath, outFolder);
- const entry: SinglePageBuiltModule = await import(ssrEntryURLPage.toString());
-
- await generatePage(pageData, entry, builtPaths, pipeline);
- }
+ const entry = await pipeline.retrieveSsrEntry(pageData.route, filePath);
+ await generatePage(pageData, entry, builtPaths, pipeline);
}
}
logger.info(
@@ -232,12 +173,12 @@ export async function generatePages(options: StaticBuildOptions, internals: Buil
.map((x) => x.transforms.size)
.reduce((a, b) => a + b, 0);
const cpuCount = os.cpus().length;
- const assetsCreationpipeline = await prepareAssetsGenerationEnv(pipeline, totalCount);
+ const assetsCreationPipeline = await prepareAssetsGenerationEnv(pipeline, totalCount);
const queue = new PQueue({ concurrency: Math.max(cpuCount, 1) });
const assetsTimer = performance.now();
for (const [originalPath, transforms] of staticImageList) {
- await generateImagesForPath(originalPath, transforms, assetsCreationpipeline, queue);
+ await generateImagesForPath(originalPath, transforms, assetsCreationPipeline, queue);
}
await queue.onIdle();
@@ -615,6 +556,7 @@ function createBuildManifest(
i18n: i18nManifest,
buildFormat: settings.config.build.format,
middleware,
+ rewritingEnabled: settings.config.experimental.rewriting,
checkOrigin: settings.config.experimental.security?.csrfProtection?.origin ?? false,
};
}
diff --git a/packages/astro/src/core/build/pipeline.ts b/packages/astro/src/core/build/pipeline.ts
index a78c8eaf893c..532759f1e426 100644
--- a/packages/astro/src/core/build/pipeline.ts
+++ b/packages/astro/src/core/build/pipeline.ts
@@ -1,4 +1,10 @@
-import type { RouteData, SSRLoadedRenderer, SSRResult } from '../../@types/astro.js';
+import type {
+ ComponentInstance,
+ RewritePayload,
+ RouteData,
+ SSRLoadedRenderer,
+ SSRResult,
+} from '../../@types/astro.js';
import { getOutputDirectory } from '../../prerender/utils.js';
import { BEFORE_HYDRATION_SCRIPT_ID, PAGE_SCRIPT_ID } from '../../vite-plugin-scripts/index.js';
import type { SSRManifest } from '../app/types.js';
@@ -13,20 +19,44 @@ import { isServerLikeOutput } from '../util.js';
import {
type BuildInternals,
cssOrder,
+ getEntryFilePathFromComponentPath,
getPageDataByComponent,
mergeInlineCss,
} from './internal.js';
import { ASTRO_PAGE_MODULE_ID, ASTRO_PAGE_RESOLVED_MODULE_ID } from './plugins/plugin-pages.js';
import { RESOLVED_SPLIT_MODULE_ID } from './plugins/plugin-ssr.js';
-import { getVirtualModulePageNameFromPath } from './plugins/util.js';
-import { ASTRO_PAGE_EXTENSION_POST_PATTERN } from './plugins/util.js';
-import type { PageBuildData, StaticBuildOptions } from './types.js';
+import {
+ ASTRO_PAGE_EXTENSION_POST_PATTERN,
+ getVirtualModulePageNameFromPath,
+} from './plugins/util.js';
+import type { PageBuildData, SinglePageBuiltModule, StaticBuildOptions } from './types.js';
import { i18nHasFallback } from './util.js';
+import { RedirectSinglePageBuiltModule } from '../redirects/index.js';
+import { getOutDirWithinCwd } from './common.js';
+import { RouteNotFound } from '../errors/errors-data.js';
+import { AstroError } from '../errors/index.js';
/**
* The build pipeline is responsible to gather the files emitted by the SSR build and generate the pages by executing these files.
*/
export class BuildPipeline extends Pipeline {
+ #componentsInterner: WeakMap = new WeakMap<
+ RouteData,
+ SinglePageBuiltModule
+ >();
+ /**
+ * This cache is needed to map a single `RouteData` to its file path.
+ * @private
+ */
+ #routesByFilePath: WeakMap = new WeakMap();
+
+ get outFolder() {
+ const ssr = isServerLikeOutput(this.settings.config);
+ return ssr
+ ? this.settings.config.build.server
+ : getOutDirWithinCwd(this.settings.config.outDir);
+ }
+
private constructor(
readonly internals: BuildInternals,
readonly manifest: SSRManifest,
@@ -225,6 +255,113 @@ export class BuildPipeline extends Pipeline {
}
}
+ for (const [buildData, filePath] of pages.entries()) {
+ this.#routesByFilePath.set(buildData.route, filePath);
+ }
+
return pages;
}
+
+ async getComponentByRoute(routeData: RouteData): Promise {
+ if (this.#componentsInterner.has(routeData)) {
+ // SAFETY: checked before
+ const entry = this.#componentsInterner.get(routeData)!;
+ return await entry.page();
+ } else {
+ // SAFETY: the pipeline calls `retrieveRoutesToGenerate`, which is in charge to fill the cache.
+ const filePath = this.#routesByFilePath.get(routeData)!;
+ const module = await this.retrieveSsrEntry(routeData, filePath);
+ return module.page();
+ }
+ }
+
+ async tryRewrite(payload: RewritePayload): Promise<[RouteData, ComponentInstance]> {
+ let foundRoute: RouteData | undefined;
+ // options.manifest is the actual type that contains the information
+ for (const route of this.options.manifest.routes) {
+ if (payload instanceof URL) {
+ if (route.pattern.test(payload.pathname)) {
+ foundRoute = route;
+ break;
+ }
+ } else if (payload instanceof Request) {
+ const url = new URL(payload.url);
+ if (route.pattern.test(url.pathname)) {
+ foundRoute = route;
+ break;
+ }
+ } else if (route.pattern.test(decodeURI(payload))) {
+ foundRoute = route;
+ break;
+ }
+ }
+ if (foundRoute) {
+ const componentInstance = await this.getComponentByRoute(foundRoute);
+ return [foundRoute, componentInstance];
+ } else {
+ throw new AstroError(RouteNotFound);
+ }
+ }
+
+ async retrieveSsrEntry(route: RouteData, filePath: string): Promise {
+ if (this.#componentsInterner.has(route)) {
+ // SAFETY: it is checked inside the if
+ return this.#componentsInterner.get(route)!;
+ }
+ let entry;
+ if (routeIsRedirect(route)) {
+ entry = await this.#getEntryForRedirectRoute(route, this.internals, this.outFolder);
+ } else if (routeIsFallback(route)) {
+ entry = await this.#getEntryForFallbackRoute(route, this.internals, this.outFolder);
+ } else {
+ const ssrEntryURLPage = createEntryURL(filePath, this.outFolder);
+ entry = await import(ssrEntryURLPage.toString());
+ }
+ this.#componentsInterner.set(route, entry);
+ return entry;
+ }
+
+ async #getEntryForFallbackRoute(
+ route: RouteData,
+ internals: BuildInternals,
+ outFolder: URL
+ ): Promise {
+ if (route.type !== 'fallback') {
+ throw new Error(`Expected a redirect route.`);
+ }
+ if (route.redirectRoute) {
+ const filePath = getEntryFilePathFromComponentPath(internals, route.redirectRoute.component);
+ if (filePath) {
+ const url = createEntryURL(filePath, outFolder);
+ const ssrEntryPage: SinglePageBuiltModule = await import(url.toString());
+ return ssrEntryPage;
+ }
+ }
+
+ return RedirectSinglePageBuiltModule;
+ }
+
+ async #getEntryForRedirectRoute(
+ route: RouteData,
+ internals: BuildInternals,
+ outFolder: URL
+ ): Promise {
+ if (route.type !== 'redirect') {
+ throw new Error(`Expected a redirect route.`);
+ }
+ if (route.redirectRoute) {
+ const filePath = getEntryFilePathFromComponentPath(internals, route.redirectRoute.component);
+ if (filePath) {
+ const url = createEntryURL(filePath, outFolder);
+ const ssrEntryPage: SinglePageBuiltModule = await import(url.toString());
+ return ssrEntryPage;
+ }
+ }
+
+ return RedirectSinglePageBuiltModule;
+ }
+}
+
+function createEntryURL(filePath: string, outFolder: URL) {
+ return new URL('./' + filePath + `?time=${Date.now()}`, outFolder);
}
diff --git a/packages/astro/src/core/build/plugins/plugin-manifest.ts b/packages/astro/src/core/build/plugins/plugin-manifest.ts
index 498ccdbb544b..5bb6ddab038a 100644
--- a/packages/astro/src/core/build/plugins/plugin-manifest.ts
+++ b/packages/astro/src/core/build/plugins/plugin-manifest.ts
@@ -277,5 +277,6 @@ function buildManifest(
i18n: i18nManifest,
buildFormat: settings.config.build.format,
checkOrigin: settings.config.experimental.security?.csrfProtection?.origin ?? false,
+ rewritingEnabled: settings.config.experimental.rewriting,
};
}
diff --git a/packages/astro/src/core/config/schema.ts b/packages/astro/src/core/config/schema.ts
index 303846f7608f..0fd4c58e66fd 100644
--- a/packages/astro/src/core/config/schema.ts
+++ b/packages/astro/src/core/config/schema.ts
@@ -87,6 +87,7 @@ const ASTRO_CONFIG_DEFAULTS = {
globalRoutePriority: false,
i18nDomains: false,
security: {},
+ rewriting: false,
},
} satisfies AstroUserConfig & { server: { open: boolean } };
@@ -525,6 +526,7 @@ export const AstroConfigSchema = z.object({
.optional()
.default(ASTRO_CONFIG_DEFAULTS.experimental.security),
i18nDomains: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.i18nDomains),
+ rewriting: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.rewriting),
})
.strict(
`Invalid or outdated experimental feature.\nCheck for incorrect spelling or outdated Astro version.\nSee https://docs.astro.build/en/reference/configuration-reference/#experimental-flags for a list of all current experiments.`
diff --git a/packages/astro/src/core/errors/errors-data.ts b/packages/astro/src/core/errors/errors-data.ts
index 180160064ab1..7ebc3a3831a1 100644
--- a/packages/astro/src/core/errors/errors-data.ts
+++ b/packages/astro/src/core/errors/errors-data.ts
@@ -1483,6 +1483,18 @@ export const UnsupportedConfigTransformError = {
hint: 'See the devalue library for all supported types: https://github.com/rich-harris/devalue',
} satisfies ErrorData;
+/**
+ * @docs
+ * @description
+ *
+ * Astro couldn't find a route matching the one provided by the user
+ */
+export const RouteNotFound = {
+ name: 'RouteNotFound',
+ title: 'Route not found.',
+ message: `Astro could find a route that matches the one you requested.`,
+} satisfies ErrorData;
+
// Generic catch-all - Only use this in extreme cases, like if there was a cosmic ray bit flip.
export const UnknownError = { name: 'UnknownError', title: 'Unknown Error.' } satisfies ErrorData;
diff --git a/packages/astro/src/core/middleware/callMiddleware.ts b/packages/astro/src/core/middleware/callMiddleware.ts
index 0133c13d032d..b92e0f3cb19b 100644
--- a/packages/astro/src/core/middleware/callMiddleware.ts
+++ b/packages/astro/src/core/middleware/callMiddleware.ts
@@ -1,5 +1,11 @@
-import type { APIContext, MiddlewareHandler, MiddlewareNext } from '../../@types/astro.js';
+import type {
+ APIContext,
+ MiddlewareHandler,
+ MiddlewareNext,
+ RewritePayload,
+} from '../../@types/astro.js';
import { AstroError, AstroErrorData } from '../errors/index.js';
+import type { Logger } from '../logger/core.js';
/**
* Utility function that is in charge of calling the middleware.
@@ -38,13 +44,28 @@ import { AstroError, AstroErrorData } from '../errors/index.js';
export async function callMiddleware(
onRequest: MiddlewareHandler,
apiContext: APIContext,
- responseFunction: () => Promise | Response
+ responseFunction: (
+ apiContext: APIContext,
+ rewritePayload?: RewritePayload
+ ) => Promise | Response,
+ // TODO: remove these two arguments once rerouting goes out of experimental
+ enableRerouting: boolean,
+ logger: Logger
): Promise {
let nextCalled = false;
let responseFunctionPromise: Promise | Response | undefined = undefined;
- const next: MiddlewareNext = async () => {
+ const next: MiddlewareNext = async (payload) => {
nextCalled = true;
- responseFunctionPromise = responseFunction();
+ if (enableRerouting) {
+ responseFunctionPromise = responseFunction(apiContext, payload);
+ } else {
+ logger.warn(
+ 'router',
+ 'The rewrite API is experimental. To use this feature, add the `rewriting` flag to the `experimental` object in your Astro config.'
+ );
+ responseFunctionPromise = responseFunction(apiContext);
+ }
+ // We need to pass the APIContext pass to `callMiddleware` because it can be mutated across middleware functions
return responseFunctionPromise;
};
diff --git a/packages/astro/src/core/middleware/index.ts b/packages/astro/src/core/middleware/index.ts
index cb9304bffbe1..17c206d6e9f5 100644
--- a/packages/astro/src/core/middleware/index.ts
+++ b/packages/astro/src/core/middleware/index.ts
@@ -1,17 +1,14 @@
-import type { APIContext, MiddlewareHandler, Params } from '../../@types/astro.js';
+import type { APIContext, MiddlewareHandler, Params, RewritePayload } from '../../@types/astro.js';
import {
computeCurrentLocale,
computePreferredLocale,
computePreferredLocaleList,
} from '../../i18n/utils.js';
-import { ASTRO_VERSION } from '../constants.js';
+import { ASTRO_VERSION, clientLocalsSymbol, clientAddressSymbol } from '../constants.js';
import { AstroCookies } from '../cookies/index.js';
import { AstroError, AstroErrorData } from '../errors/index.js';
import { sequence } from './sequence.js';
-const clientAddressSymbol = Symbol.for('astro.clientAddress');
-const clientLocalsSymbol = Symbol.for('astro.locals');
-
function defineMiddleware(fn: MiddlewareHandler) {
return fn;
}
@@ -49,6 +46,12 @@ function createContext({
const url = new URL(request.url);
const route = url.pathname;
+ // TODO verify that this function works in an edge middleware environment
+ const reroute = (_reroutePayload: RewritePayload) => {
+ // return dummy response
+ return Promise.resolve(new Response(null));
+ };
+
return {
cookies: new AstroCookies(request),
request,
@@ -56,6 +59,7 @@ function createContext({
site: undefined,
generator: `Astro v${ASTRO_VERSION}`,
props: {},
+ rewrite: reroute,
redirect(path, status) {
return new Response(null, {
status: status || 302,
diff --git a/packages/astro/src/core/middleware/sequence.ts b/packages/astro/src/core/middleware/sequence.ts
index 9a68963945ec..ef27d03c2cb4 100644
--- a/packages/astro/src/core/middleware/sequence.ts
+++ b/packages/astro/src/core/middleware/sequence.ts
@@ -1,5 +1,6 @@
-import type { APIContext, MiddlewareHandler } from '../../@types/astro.js';
+import type { APIContext, MiddlewareHandler, RewritePayload } from '../../@types/astro.js';
import { defineMiddleware } from './index.js';
+import { AstroCookies } from '../cookies/cookies.js';
// From SvelteKit: https://github.com/sveltejs/kit/blob/master/packages/kit/src/exports/hooks/sequence.js
/**
@@ -10,13 +11,16 @@ export function sequence(...handlers: MiddlewareHandler[]): MiddlewareHandler {
const filtered = handlers.filter((h) => !!h);
const length = filtered.length;
if (!length) {
- const handler: MiddlewareHandler = defineMiddleware((context, next) => {
+ return defineMiddleware((_context, next) => {
return next();
});
- return handler;
}
return defineMiddleware((context, next) => {
+ /**
+ * This variable is used to carry the rerouting payload across middleware functions.
+ */
+ let carriedPayload: RewritePayload | undefined = undefined;
return applyHandle(0, context);
function applyHandle(i: number, handleContext: APIContext) {
@@ -24,11 +28,28 @@ export function sequence(...handlers: MiddlewareHandler[]): MiddlewareHandler {
// @ts-expect-error
// SAFETY: Usually `next` always returns something in user land, but in `sequence` we are actually
// doing a loop over all the `next` functions, and eventually we call the last `next` that returns the `Response`.
- const result = handle(handleContext, async () => {
+ const result = handle(handleContext, async (payload: RewritePayload) => {
if (i < length - 1) {
+ if (payload) {
+ let newRequest;
+ if (payload instanceof Request) {
+ newRequest = payload;
+ } else if (payload instanceof URL) {
+ newRequest = new Request(payload, handleContext.request);
+ } else {
+ newRequest = new Request(
+ new URL(payload, handleContext.url.origin),
+ handleContext.request
+ );
+ }
+ carriedPayload = payload;
+ handleContext.request = newRequest;
+ handleContext.url = new URL(newRequest.url);
+ handleContext.cookies = new AstroCookies(newRequest);
+ }
return applyHandle(i + 1, handleContext);
} else {
- return next();
+ return next(payload ?? carriedPayload);
}
});
return result;
diff --git a/packages/astro/src/core/render-context.ts b/packages/astro/src/core/render-context.ts
index 5cfc8ef2ede3..279745ac19e4 100644
--- a/packages/astro/src/core/render-context.ts
+++ b/packages/astro/src/core/render-context.ts
@@ -4,6 +4,8 @@ import type {
AstroGlobalPartial,
ComponentInstance,
MiddlewareHandler,
+ MiddlewareNext,
+ RewritePayload,
RouteData,
SSRResult,
} from '../@types/astro.js';
@@ -39,14 +41,23 @@ export class RenderContext {
public locals: App.Locals,
readonly middleware: MiddlewareHandler,
readonly pathname: string,
- readonly request: Request,
- readonly routeData: RouteData,
+ public request: Request,
+ public routeData: RouteData,
public status: number,
- readonly cookies = new AstroCookies(request),
- readonly params = getParams(routeData, pathname),
- readonly url = new URL(request.url)
+ protected cookies = new AstroCookies(request),
+ public params = getParams(routeData, pathname),
+ protected url = new URL(request.url)
) {}
+ /**
+ * A flag that tells the render content if the rewriting was triggered
+ */
+ isRewriting = false;
+ /**
+ * A safety net in case of loops
+ */
+ counter = 0;
+
static create({
locals = {},
middleware,
@@ -56,7 +67,7 @@ export class RenderContext {
routeData,
status = 200,
}: Pick &
- Partial>) {
+ Partial>): RenderContext {
return new RenderContext(
pipeline,
locals,
@@ -80,11 +91,11 @@ export class RenderContext {
* - fallback
*/
async render(componentInstance: ComponentInstance | undefined): Promise {
- const { cookies, middleware, pathname, pipeline, routeData } = this;
+ const { cookies, middleware, pathname, pipeline } = this;
const { logger, routeCache, serverLike, streaming } = pipeline;
const props = await getProps({
mod: componentInstance,
- routeData,
+ routeData: this.routeData,
routeCache,
pathname,
logger,
@@ -92,10 +103,40 @@ export class RenderContext {
});
const apiContext = this.createAPIContext(props);
- const lastNext = async () => {
- switch (routeData.type) {
+ this.counter++;
+ if (this.counter === 4) {
+ return new Response('Loop Detected', {
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/508
+ status: 508,
+ statusText:
+ 'Astro detected a loop where you tried to call the rewriting logic more than four times.',
+ });
+ }
+ const lastNext = async (ctx: APIContext, payload?: RewritePayload) => {
+ if (payload) {
+ if (this.pipeline.manifest.rewritingEnabled) {
+ try {
+ const [routeData, component] = await pipeline.tryRewrite(payload);
+ this.routeData = routeData;
+ componentInstance = component;
+ } catch (e) {
+ return new Response('Not found', {
+ status: 404,
+ statusText: 'Not found',
+ });
+ } finally {
+ this.isRewriting = true;
+ }
+ } else {
+ this.pipeline.logger.warn(
+ 'router',
+ 'The rewrite API is experimental. To use this feature, add the `rewriting` flag to the `experimental` object in your Astro config.'
+ );
+ }
+ }
+ switch (this.routeData.type) {
case 'endpoint':
- return renderEndpoint(componentInstance as any, apiContext, serverLike, logger);
+ return renderEndpoint(componentInstance as any, ctx, serverLike, logger);
case 'redirect':
return renderRedirect(this);
case 'page': {
@@ -108,7 +149,7 @@ export class RenderContext {
props,
{},
streaming,
- routeData
+ this.routeData
);
} catch (e) {
// If there is an error in the page's frontmatter or instantiation of the RenderTemplate fails midway,
@@ -119,7 +160,11 @@ export class RenderContext {
// Signal to the i18n middleware to maybe act on this response
response.headers.set(ROUTE_TYPE_HEADER, 'page');
// Signal to the error-page-rerouting infra to let this response pass through to avoid loops
- if (routeData.route === '/404' || routeData.route === '/500') {
+ if (
+ this.routeData.route === '/404' ||
+ this.routeData.route === '/500' ||
+ this.isRewriting
+ ) {
response.headers.set(REROUTE_DIRECTIVE_HEADER, 'no');
}
return response;
@@ -130,7 +175,13 @@ export class RenderContext {
}
};
- const response = await callMiddleware(middleware, apiContext, lastNext);
+ const response = await callMiddleware(
+ middleware,
+ apiContext,
+ lastNext,
+ this.pipeline.manifest.rewritingEnabled,
+ this.pipeline.logger
+ );
if (response.headers.get(ROUTE_TYPE_HEADER)) {
response.headers.delete(ROUTE_TYPE_HEADER);
}
@@ -143,10 +194,38 @@ export class RenderContext {
createAPIContext(props: APIContext['props']): APIContext {
const renderContext = this;
- const { cookies, params, pipeline, request, url } = this;
+ const { cookies, params, pipeline, url } = this;
const generator = `Astro v${ASTRO_VERSION}`;
const redirect = (path: string, status = 302) =>
new Response(null, { status, headers: { Location: path } });
+
+ const rewrite = async (reroutePayload: RewritePayload) => {
+ pipeline.logger.debug('router', 'Called rewriting to:', reroutePayload);
+ try {
+ const [routeData, component] = await pipeline.tryRewrite(reroutePayload);
+ this.routeData = routeData;
+ if (reroutePayload instanceof Request) {
+ this.request = reroutePayload;
+ } else {
+ this.request = new Request(
+ new URL(routeData.pathname ?? routeData.route, this.url.origin),
+ this.request
+ );
+ }
+ this.url = new URL(this.request.url);
+ this.cookies = new AstroCookies(this.request);
+ this.params = getParams(routeData, url.toString());
+ this.isRewriting = true;
+ return await this.render(component);
+ } catch (e) {
+ pipeline.logger.debug('router', 'Rewrite failed.', e);
+ return new Response('Not found', {
+ status: 404,
+ statusText: 'Not found',
+ });
+ }
+ };
+
return {
cookies,
get clientAddress() {
@@ -167,7 +246,7 @@ export class RenderContext {
renderContext.locals = val;
// we also put it on the original Request object,
// where the adapter might be expecting to read it after the response.
- Reflect.set(request, clientLocalsSymbol, val);
+ Reflect.set(this.request, clientLocalsSymbol, val);
}
},
params,
@@ -179,7 +258,8 @@ export class RenderContext {
},
props,
redirect,
- request,
+ rewrite,
+ request: this.request,
site: pipeline.site,
url,
};
@@ -294,11 +374,11 @@ export class RenderContext {
astroStaticPartial: AstroGlobalPartial
): Omit {
const renderContext = this;
- const { cookies, locals, params, pipeline, request, url } = this;
+ const { cookies, locals, params, pipeline, url } = this;
const { response } = result;
const redirect = (path: string, status = 302) => {
// If the response is already sent, error as we cannot proceed with the redirect.
- if ((request as any)[responseSentSymbol]) {
+ if ((this.request as any)[responseSentSymbol]) {
throw new AstroError({
...AstroErrorData.ResponseSentError,
});
@@ -306,6 +386,33 @@ export class RenderContext {
return new Response(null, { status, headers: { Location: path } });
};
+ const rewrite = async (reroutePayload: RewritePayload) => {
+ try {
+ pipeline.logger.debug('router', 'Calling rewrite: ', reroutePayload);
+ const [routeData, component] = await pipeline.tryRewrite(reroutePayload);
+ this.routeData = routeData;
+ if (reroutePayload instanceof Request) {
+ this.request = reroutePayload;
+ } else {
+ this.request = new Request(
+ new URL(routeData.pathname ?? routeData.route, this.url.origin),
+ this.request
+ );
+ }
+ this.url = new URL(this.request.url);
+ this.cookies = new AstroCookies(this.request);
+ this.params = getParams(routeData, url.toString());
+ this.isRewriting = true;
+ return await this.render(component);
+ } catch (e) {
+ pipeline.logger.debug('router', 'Rerouting failed, returning a 404.', e);
+ return new Response('Not found', {
+ status: 404,
+ statusText: 'Not found',
+ });
+ }
+ };
+
return {
generator: astroStaticPartial.generator,
glob: astroStaticPartial.glob,
@@ -325,7 +432,8 @@ export class RenderContext {
},
locals,
redirect,
- request,
+ rewrite,
+ request: this.request,
response,
site: pipeline.site,
url,
diff --git a/packages/astro/src/prerender/routing.ts b/packages/astro/src/prerender/routing.ts
index e6c09dd70279..cbdddff5c8cb 100644
--- a/packages/astro/src/prerender/routing.ts
+++ b/packages/astro/src/prerender/routing.ts
@@ -54,7 +54,7 @@ async function preloadAndSetPrerenderStatus({
continue;
}
- const preloadedComponent = await pipeline.preload(filePath);
+ const preloadedComponent = await pipeline.preload(route, filePath);
// gets the prerender metadata set by the `astro:scanner` vite plugin
const prerenderStatus = getPrerenderStatus({
diff --git a/packages/astro/src/vite-plugin-astro-server/pipeline.ts b/packages/astro/src/vite-plugin-astro-server/pipeline.ts
index 7ccc63638284..685d13f570c4 100644
--- a/packages/astro/src/vite-plugin-astro-server/pipeline.ts
+++ b/packages/astro/src/vite-plugin-astro-server/pipeline.ts
@@ -1,8 +1,10 @@
-import url from 'node:url';
+import { fileURLToPath } from 'node:url';
import type {
AstroSettings,
ComponentInstance,
DevToolbarMetadata,
+ ManifestData,
+ RewritePayload,
RouteData,
SSRElement,
SSRLoadedRenderer,
@@ -12,10 +14,10 @@ import { getInfoOutput } from '../cli/info/index.js';
import type { HeadElements } from '../core/base-pipeline.js';
import { ASTRO_VERSION, DEFAULT_404_COMPONENT } from '../core/constants.js';
import { enhanceViteSSRError } from '../core/errors/dev/index.js';
-import { AggregateError, CSSError, MarkdownError } from '../core/errors/index.js';
+import { AggregateError, AstroError, CSSError, MarkdownError } from '../core/errors/index.js';
import type { Logger } from '../core/logger/core.js';
import type { ModuleLoader } from '../core/module-loader/index.js';
-import { Pipeline, loadRenderer } from '../core/render/index.js';
+import { loadRenderer, Pipeline } from '../core/render/index.js';
import { isPage, isServerLikeOutput, resolveIdToUrl, viteID } from '../core/util.js';
import { PAGE_SCRIPT_ID } from '../vite-plugin-scripts/index.js';
import { getStylesForURL } from './css.js';
@@ -23,12 +25,20 @@ import { getComponentMetadata } from './metadata.js';
import { createResolve } from './resolve.js';
import { default404Page } from './response.js';
import { getScriptsForURL } from './scripts.js';
+import { RouteNotFound } from '../core/errors/errors-data.js';
export class DevPipeline extends Pipeline {
// renderers are loaded on every request,
// so it needs to be mutable here unlike in other environments
override renderers = new Array();
+ manifestData: ManifestData | undefined;
+
+ componentInterner: WeakMap = new WeakMap<
+ RouteData,
+ ComponentInstance
+ >();
+
private constructor(
readonly loader: ModuleLoader,
readonly logger: Logger,
@@ -43,13 +53,18 @@ export class DevPipeline extends Pipeline {
super(logger, manifest, mode, [], resolve, serverLike, streaming);
}
- static create({
- loader,
- logger,
- manifest,
- settings,
- }: Pick) {
- return new DevPipeline(loader, logger, manifest, settings);
+ static create(
+ manifestData: ManifestData,
+ {
+ loader,
+ logger,
+ manifest,
+ settings,
+ }: Pick
+ ) {
+ const pipeline = new DevPipeline(loader, logger, manifest, settings);
+ pipeline.manifestData = manifestData;
+ return pipeline;
}
async headElements(routeData: RouteData): Promise {
@@ -59,7 +74,7 @@ export class DevPipeline extends Pipeline {
mode,
settings,
} = this;
- const filePath = new URL(`./${routeData.component}`, root);
+ const filePath = new URL(`${routeData.component}`, root);
// Add hoisted script tags, skip if direct rendering with `directRenderScript`
const { scripts } = settings.config.experimental.directRenderScript
? { scripts: new Set() }
@@ -80,7 +95,7 @@ export class DevPipeline extends Pipeline {
scripts.add({ props: { type: 'module', src }, children: '' });
const additionalMetadata: DevToolbarMetadata['__astro_dev_toolbar__'] = {
- root: url.fileURLToPath(settings.config.root),
+ root: fileURLToPath(settings.config.root),
version: ASTRO_VERSION,
latestAstroVersion: settings.latestAstroVersion,
debugInfo: await getInfoOutput({ userConfig: settings.config, print: false }),
@@ -131,11 +146,11 @@ export class DevPipeline extends Pipeline {
config: { root },
loader,
} = this;
- const filePath = new URL(`./${routeData.component}`, root);
+ const filePath = new URL(`${routeData.component}`, root);
return getComponentMetadata(filePath, loader);
}
- async preload(filePath: URL) {
+ async preload(routeData: RouteData, filePath: URL) {
const { loader } = this;
if (filePath.href === new URL(DEFAULT_404_COMPONENT, this.config.root).href) {
return { default: default404Page } as any as ComponentInstance;
@@ -148,7 +163,9 @@ export class DevPipeline extends Pipeline {
try {
// Load the module from the Vite SSR Runtime.
- return (await loader.import(viteID(filePath))) as ComponentInstance;
+ const componentInstance = (await loader.import(viteID(filePath))) as ComponentInstance;
+ this.componentInterner.set(routeData, componentInstance);
+ return componentInstance;
} catch (error) {
// If the error came from Markdown or CSS, we already handled it and there's no need to enhance it
if (MarkdownError.is(error) || CSSError.is(error) || AggregateError.is(error)) {
@@ -161,5 +178,52 @@ export class DevPipeline extends Pipeline {
clearRouteCache() {
this.routeCache.clearAll();
+ this.componentInterner = new WeakMap();
+ }
+
+ async getComponentByRoute(routeData: RouteData): Promise {
+ 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: RewritePayload): Promise<[RouteData, ComponentInstance]> {
+ let foundRoute;
+ if (!this.manifestData) {
+ throw new Error('Missing manifest data. This is an internal error, please file an issue.');
+ }
+
+ for (const route of this.manifestData.routes) {
+ if (payload instanceof URL) {
+ if (route.pattern.test(payload.pathname)) {
+ foundRoute = route;
+ break;
+ }
+ } else if (payload instanceof Request) {
+ const url = new URL(payload.url);
+ if (route.pattern.test(url.pathname)) {
+ foundRoute = route;
+ break;
+ }
+ } else if (route.pattern.test(decodeURI(payload))) {
+ foundRoute = route;
+ break;
+ }
+ }
+
+ if (foundRoute) {
+ const componentInstance = await this.getComponentByRoute(foundRoute);
+ return [foundRoute, componentInstance];
+ } else {
+ throw new AstroError(RouteNotFound);
+ }
+ }
+
+ setManifestData(manifestData: ManifestData) {
+ this.manifestData = manifestData;
}
}
diff --git a/packages/astro/src/vite-plugin-astro-server/plugin.ts b/packages/astro/src/vite-plugin-astro-server/plugin.ts
index 082de6bcebf0..3c6f06ee9a05 100644
--- a/packages/astro/src/vite-plugin-astro-server/plugin.ts
+++ b/packages/astro/src/vite-plugin-astro-server/plugin.ts
@@ -35,10 +35,10 @@ export default function createVitePluginAstroServer({
configureServer(viteServer) {
const loader = createViteLoader(viteServer);
const manifest = createDevelopmentManifest(settings);
- const pipeline = DevPipeline.create({ loader, logger, manifest, settings });
let manifestData: ManifestData = ensure404Route(
createRouteManifest({ settings, fsMod }, logger)
);
+ const pipeline = DevPipeline.create(manifestData, { loader, logger, manifest, settings });
const controller = createController({ loader });
const localStorage = new AsyncLocalStorage();
@@ -47,6 +47,7 @@ export default function createVitePluginAstroServer({
pipeline.clearRouteCache();
if (needsManifestRebuild) {
manifestData = ensure404Route(createRouteManifest({ settings }, logger));
+ pipeline.setManifestData(manifestData);
}
}
// Rebuild route manifest on file change, if needed.
@@ -144,6 +145,7 @@ export function createDevelopmentManifest(settings: AstroSettings): SSRManifest
inlinedScripts: new Map(),
i18n: i18nManifest,
checkOrigin: settings.config.experimental.security?.csrfProtection?.origin ?? false,
+ rewritingEnabled: settings.config.experimental.rewriting,
middleware(_, next) {
return next();
},
diff --git a/packages/astro/src/vite-plugin-astro-server/route.ts b/packages/astro/src/vite-plugin-astro-server/route.ts
index 21053420a754..85bf969f9db9 100644
--- a/packages/astro/src/vite-plugin-astro-server/route.ts
+++ b/packages/astro/src/vite-plugin-astro-server/route.ts
@@ -114,7 +114,7 @@ export async function matchRoute(
if (custom404) {
const filePath = new URL(`./${custom404.component}`, config.root);
- const preloadedComponent = await pipeline.preload(filePath);
+ const preloadedComponent = await pipeline.preload(custom404, filePath);
return {
route: custom404,
@@ -197,40 +197,39 @@ export async function handleRoute({
if (!pathNameHasLocale && pathname !== '/') {
return handle404Response(origin, incomingRequest, incomingResponse);
}
- request = createRequest({
- base: config.base,
- url,
- headers: incomingRequest.headers,
- logger,
- // no route found, so we assume the default for rendering the 404 page
- staticLike: config.output === 'static' || config.output === 'hybrid',
- });
- route = {
- component: '',
- generate(_data: any): string {
- return '';
- },
- params: [],
- // Disable eslint as we only want to generate an empty RegExp
- // eslint-disable-next-line prefer-regex-literals
- pattern: new RegExp(''),
- prerender: false,
- segments: [],
- type: 'fallback',
- route: '',
- fallbackRoutes: [],
- isIndex: false,
- };
- renderContext = RenderContext.create({
- pipeline: pipeline,
- pathname,
- middleware,
- request,
- routeData: route,
- });
- } else {
- return handle404Response(origin, incomingRequest, incomingResponse);
}
+ request = createRequest({
+ base: config.base,
+ url,
+ headers: incomingRequest.headers,
+ logger,
+ // no route found, so we assume the default for rendering the 404 page
+ staticLike: config.output === 'static' || config.output === 'hybrid',
+ });
+ route = {
+ component: '',
+ generate(_data: any): string {
+ return '';
+ },
+ params: [],
+ // Disable eslint as we only want to generate an empty RegExp
+ // eslint-disable-next-line prefer-regex-literals
+ pattern: new RegExp(''),
+ prerender: false,
+ segments: [],
+ type: 'fallback',
+ route: '',
+ fallbackRoutes: [],
+ isIndex: false,
+ };
+
+ renderContext = RenderContext.create({
+ pipeline: pipeline,
+ pathname,
+ middleware,
+ request,
+ routeData: route,
+ });
} else {
const filePath: URL | undefined = matchedRoute.filePath;
const { preloadedComponent } = matchedRoute;
diff --git a/packages/astro/test/fixtures/middleware-virtual/astro.config.mjs b/packages/astro/test/fixtures/middleware-virtual/astro.config.mjs
new file mode 100644
index 000000000000..bc095ecddb69
--- /dev/null
+++ b/packages/astro/test/fixtures/middleware-virtual/astro.config.mjs
@@ -0,0 +1,3 @@
+import { defineConfig } from "astro/config";
+
+export default defineConfig({})
diff --git a/packages/astro/test/fixtures/middleware-virtual/package.json b/packages/astro/test/fixtures/middleware-virtual/package.json
new file mode 100644
index 000000000000..7cfbeb721047
--- /dev/null
+++ b/packages/astro/test/fixtures/middleware-virtual/package.json
@@ -0,0 +1,8 @@
+{
+ "name": "@test/middleware-virtual",
+ "version": "0.0.0",
+ "private": true,
+ "dependencies": {
+ "astro": "workspace:*"
+ }
+}
diff --git a/packages/astro/test/fixtures/middleware-virtual/src/middleware.js b/packages/astro/test/fixtures/middleware-virtual/src/middleware.js
new file mode 100644
index 000000000000..55004a00cfdb
--- /dev/null
+++ b/packages/astro/test/fixtures/middleware-virtual/src/middleware.js
@@ -0,0 +1,6 @@
+import { defineMiddleware } from 'astro:middleware';
+
+export const onRequest = defineMiddleware(async (context, next) => {
+ console.log('[MIDDLEWARE] in ' + context.url.toString());
+ return next();
+});
diff --git a/packages/astro/test/fixtures/middleware-virtual/src/pages/index.astro b/packages/astro/test/fixtures/middleware-virtual/src/pages/index.astro
new file mode 100644
index 000000000000..9bd31f5fde27
--- /dev/null
+++ b/packages/astro/test/fixtures/middleware-virtual/src/pages/index.astro
@@ -0,0 +1,13 @@
+---
+const data = Astro.locals;
+---
+
+
+
+ Index
+
+
+
+Index
+
+
diff --git a/packages/astro/test/fixtures/reroute/astro.config.mjs b/packages/astro/test/fixtures/reroute/astro.config.mjs
new file mode 100644
index 000000000000..af13ef19b477
--- /dev/null
+++ b/packages/astro/test/fixtures/reroute/astro.config.mjs
@@ -0,0 +1,9 @@
+import { defineConfig } from 'astro/config';
+
+// https://astro.build/config
+export default defineConfig({
+ experimental: {
+ rewriting: true
+ },
+ site: "https://example.com"
+});
diff --git a/packages/astro/test/fixtures/reroute/package.json b/packages/astro/test/fixtures/reroute/package.json
new file mode 100644
index 000000000000..ed64e57a97e0
--- /dev/null
+++ b/packages/astro/test/fixtures/reroute/package.json
@@ -0,0 +1,8 @@
+{
+ "name": "@test/reroute",
+ "version": "0.0.0",
+ "private": true,
+ "dependencies": {
+ "astro": "workspace:*"
+ }
+}
diff --git a/packages/astro/test/fixtures/reroute/src/middleware.js b/packages/astro/test/fixtures/reroute/src/middleware.js
new file mode 100644
index 000000000000..4d7c2a7956c8
--- /dev/null
+++ b/packages/astro/test/fixtures/reroute/src/middleware.js
@@ -0,0 +1,33 @@
+import { sequence } from 'astro:middleware';
+
+let contextReroute = false;
+
+export const first = async (context, next) => {
+ if (context.url.pathname.includes('/auth')) {
+ }
+
+ return next();
+};
+
+export const second = async (context, next) => {
+ if (context.url.pathname.includes('/auth')) {
+ if (context.url.pathname.includes('/auth/dashboard')) {
+ contextReroute = true;
+ return await context.rewrite('/');
+ }
+ if (context.url.pathname.includes('/auth/base')) {
+ return await next('/');
+ }
+ }
+ return next();
+};
+
+export const third = async (context, next) => {
+ // just making sure that we are testing the change in context coming from `next()`
+ if (context.url.pathname.startsWith('/') && contextReroute === false) {
+ context.locals.auth = 'Third function called';
+ }
+ return next();
+};
+
+export const onRequest = sequence(first, second, third);
diff --git a/packages/astro/test/fixtures/reroute/src/pages/auth/base.astro b/packages/astro/test/fixtures/reroute/src/pages/auth/base.astro
new file mode 100644
index 000000000000..be31dfb14141
--- /dev/null
+++ b/packages/astro/test/fixtures/reroute/src/pages/auth/base.astro
@@ -0,0 +1,10 @@
+---
+---
+
+
+ Base
+
+
+Base
+
+
diff --git a/packages/astro/test/fixtures/reroute/src/pages/auth/dashboard.astro b/packages/astro/test/fixtures/reroute/src/pages/auth/dashboard.astro
new file mode 100644
index 000000000000..bfa006aa01a7
--- /dev/null
+++ b/packages/astro/test/fixtures/reroute/src/pages/auth/dashboard.astro
@@ -0,0 +1,10 @@
+---
+---
+
+
+ Dashboard
+
+
+Dashboard
+
+
diff --git a/packages/astro/test/fixtures/reroute/src/pages/auth/settings.astro b/packages/astro/test/fixtures/reroute/src/pages/auth/settings.astro
new file mode 100644
index 000000000000..9eee5fe95149
--- /dev/null
+++ b/packages/astro/test/fixtures/reroute/src/pages/auth/settings.astro
@@ -0,0 +1,10 @@
+---
+---
+
+
+ Settings
+
+
+Settings
+
+
diff --git a/packages/astro/test/fixtures/reroute/src/pages/blog/hello/index.astro b/packages/astro/test/fixtures/reroute/src/pages/blog/hello/index.astro
new file mode 100644
index 000000000000..8c38e518a7b7
--- /dev/null
+++ b/packages/astro/test/fixtures/reroute/src/pages/blog/hello/index.astro
@@ -0,0 +1,11 @@
+---
+return Astro.rewrite(new URL("../../", Astro.url))
+---
+
+
+ Blog hello
+
+
+Blog hello
+
+
diff --git a/packages/astro/test/fixtures/reroute/src/pages/blog/oops.astro b/packages/astro/test/fixtures/reroute/src/pages/blog/oops.astro
new file mode 100644
index 000000000000..df1f1f76a331
--- /dev/null
+++ b/packages/astro/test/fixtures/reroute/src/pages/blog/oops.astro
@@ -0,0 +1,11 @@
+---
+return Astro.rewrite("/404")
+---
+
+
+ Blog hello
+
+
+Blog hello
+
+
diff --git a/packages/astro/test/fixtures/reroute/src/pages/blog/salut/index.astro b/packages/astro/test/fixtures/reroute/src/pages/blog/salut/index.astro
new file mode 100644
index 000000000000..89d35ce2564d
--- /dev/null
+++ b/packages/astro/test/fixtures/reroute/src/pages/blog/salut/index.astro
@@ -0,0 +1,11 @@
+---
+return Astro.rewrite(new Request(new URL("../../", Astro.url)))
+---
+
+
+ Blog hello
+
+
+Blog hello
+
+
diff --git a/packages/astro/test/fixtures/reroute/src/pages/dynamic/[id].astro b/packages/astro/test/fixtures/reroute/src/pages/dynamic/[id].astro
new file mode 100644
index 000000000000..8d849de160bf
--- /dev/null
+++ b/packages/astro/test/fixtures/reroute/src/pages/dynamic/[id].astro
@@ -0,0 +1,21 @@
+---
+
+export function getStaticPaths() {
+ return [
+ { params: { id: 'hello' } },
+ ];
+}
+
+
+return Astro.rewrite("/")
+
+---
+
+
+
+ Dynamic [id].astro
+
+
+/dynamic/[id].astro
+
+
diff --git a/packages/astro/test/fixtures/reroute/src/pages/index.astro b/packages/astro/test/fixtures/reroute/src/pages/index.astro
new file mode 100644
index 000000000000..91a6fd0fb0fc
--- /dev/null
+++ b/packages/astro/test/fixtures/reroute/src/pages/index.astro
@@ -0,0 +1,12 @@
+---
+const auth = Astro.locals.auth;
+---
+
+
+ Index
+
+
+ Index
+ {auth ? Called auth
: ""}
+
+
diff --git a/packages/astro/test/fixtures/reroute/src/pages/reroute.astro b/packages/astro/test/fixtures/reroute/src/pages/reroute.astro
new file mode 100644
index 000000000000..dbc7a6ae628a
--- /dev/null
+++ b/packages/astro/test/fixtures/reroute/src/pages/reroute.astro
@@ -0,0 +1,11 @@
+---
+return Astro.rewrite("/")
+---
+
+
+ Reroute
+
+
+ Reroute
+
+
diff --git a/packages/astro/test/fixtures/reroute/src/pages/spread/[...id].astro b/packages/astro/test/fixtures/reroute/src/pages/spread/[...id].astro
new file mode 100644
index 000000000000..0bab88d0f7b1
--- /dev/null
+++ b/packages/astro/test/fixtures/reroute/src/pages/spread/[...id].astro
@@ -0,0 +1,20 @@
+---
+export function getStaticPaths() {
+ return [
+ { params: { id: 'hello' } },
+ ];
+}
+
+return Astro.rewrite("/")
+
+---
+
+
+
+
+ Spread [...id].astro
+
+
+/spread/[...id].astro
+
+
diff --git a/packages/astro/test/i18n-routing-manual.test.js b/packages/astro/test/i18n-routing-manual.test.js
index d664b3797889..1feaf963348c 100644
--- a/packages/astro/test/i18n-routing-manual.test.js
+++ b/packages/astro/test/i18n-routing-manual.test.js
@@ -58,8 +58,6 @@ describe('Dev server manual routing', () => {
describe('SSG manual routing', () => {
/** @type {import('./test-utils').Fixture} */
let fixture;
- /** @type {import('./test-utils').DevServer} */
- let devServer;
before(async () => {
fixture = await loadFixture({
diff --git a/packages/astro/test/rewrite.test.js b/packages/astro/test/rewrite.test.js
new file mode 100644
index 000000000000..39ff084a4c80
--- /dev/null
+++ b/packages/astro/test/rewrite.test.js
@@ -0,0 +1,223 @@
+import { describe, it, before, after } from 'node:test';
+import { loadFixture } from './test-utils.js';
+import { load as cheerioLoad } from 'cheerio';
+import assert from 'node:assert/strict';
+import testAdapter from './test-adapter.js';
+
+describe('Dev reroute', () => {
+ /** @type {import('./test-utils').Fixture} */
+ let fixture;
+ let devServer;
+
+ before(async () => {
+ fixture = await loadFixture({
+ root: './fixtures/reroute/',
+ });
+ devServer = await fixture.startDevServer();
+ });
+
+ after(async () => {
+ await devServer.stop();
+ });
+
+ it('should render the index page when navigating /reroute ', async () => {
+ const html = await fixture.fetch('/reroute').then((res) => res.text());
+ const $ = cheerioLoad(html);
+
+ assert.equal($('h1').text(), 'Index');
+ });
+
+ it('should render the index page when navigating /blog/hello ', async () => {
+ const html = await fixture.fetch('/blog/hello').then((res) => res.text());
+ const $ = cheerioLoad(html);
+
+ assert.equal($('h1').text(), 'Index');
+ });
+
+ it('should render the index page when navigating /blog/salut ', async () => {
+ const html = await fixture.fetch('/blog/salut').then((res) => res.text());
+ const $ = cheerioLoad(html);
+
+ assert.equal($('h1').text(), 'Index');
+ });
+
+ it('should render the index page when navigating dynamic route /dynamic/[id] ', async () => {
+ const html = await fixture.fetch('/dynamic/hello').then((res) => res.text());
+ const $ = cheerioLoad(html);
+
+ assert.equal($('h1').text(), 'Index');
+ });
+
+ it('should render the index page when navigating spread route /spread/[...spread] ', async () => {
+ const html = await fixture.fetch('/spread/hello').then((res) => res.text());
+ const $ = cheerioLoad(html);
+
+ assert.equal($('h1').text(), 'Index');
+ });
+
+ it('should render the 404 built-in page', async () => {
+ const html = await fixture.fetch('/blog/oops').then((res) => res.text());
+ const $ = cheerioLoad(html);
+
+ assert.equal($('h1').text(), '404: Not found');
+ });
+});
+
+describe('Build reroute', () => {
+ /** @type {import('./test-utils').Fixture} */
+ let fixture;
+
+ before(async () => {
+ fixture = await loadFixture({
+ root: './fixtures/reroute/',
+ });
+ await fixture.build();
+ });
+
+ it('should render the index page when navigating /reroute ', async () => {
+ const html = await fixture.readFile('/reroute/index.html');
+ const $ = cheerioLoad(html);
+
+ assert.equal($('h1').text(), 'Index');
+ });
+
+ it('should render the index page when navigating /blog/hello ', async () => {
+ const html = await fixture.readFile('/blog/hello/index.html');
+ const $ = cheerioLoad(html);
+
+ assert.equal($('h1').text(), 'Index');
+ });
+
+ it('should render the index page when navigating /blog/salut ', async () => {
+ const html = await fixture.readFile('/blog/salut/index.html');
+
+ const $ = cheerioLoad(html);
+
+ assert.equal($('h1').text(), 'Index');
+ });
+
+ it('should render the index page when navigating dynamic route /dynamic/[id] ', async () => {
+ const html = await fixture.readFile('/dynamic/hello/index.html');
+ const $ = cheerioLoad(html);
+
+ assert.equal($('h1').text(), 'Index');
+ });
+
+ it('should render the index page when navigating spread route /spread/[...spread] ', async () => {
+ const html = await fixture.readFile('/spread/hello/index.html');
+ const $ = cheerioLoad(html);
+
+ assert.equal($('h1').text(), 'Index');
+ });
+
+ it('should render the 404 built-in page', async () => {
+ try {
+ const html = await fixture.readFile('/spread/oops/index.html');
+ assert.fail('Not found');
+ } catch {
+ assert.ok;
+ }
+ });
+});
+
+describe('SSR reroute', () => {
+ /** @type {import('./test-utils').Fixture} */
+ let fixture;
+ let app;
+
+ before(async () => {
+ fixture = await loadFixture({
+ root: './fixtures/reroute/',
+ output: 'server',
+ adapter: testAdapter(),
+ });
+ await fixture.build();
+ app = await fixture.loadTestAdapterApp();
+ });
+
+ it('should render the index page when navigating /reroute ', async () => {
+ const request = new Request('http://example.com/reroute');
+ const response = await app.render(request);
+ const html = await response.text();
+ const $ = cheerioLoad(html);
+
+ assert.equal($('h1').text(), 'Index');
+ });
+
+ it('should render the index page when navigating /blog/hello ', async () => {
+ const request = new Request('http://example.com/blog/hello');
+ const response = await app.render(request);
+ const html = await response.text();
+ const $ = cheerioLoad(html);
+
+ assert.equal($('h1').text(), 'Index');
+ });
+
+ it('should render the index page when navigating /blog/salut ', async () => {
+ const request = new Request('http://example.com/blog/salut');
+ const response = await app.render(request);
+ const html = await response.text();
+
+ const $ = cheerioLoad(html);
+
+ assert.equal($('h1').text(), 'Index');
+ });
+
+ it('should render the index page when navigating dynamic route /dynamic/[id] ', async () => {
+ const request = new Request('http://example.com/dynamic/hello');
+ const response = await app.render(request);
+ const html = await response.text();
+ const $ = cheerioLoad(html);
+
+ assert.equal($('h1').text(), 'Index');
+ });
+
+ it('should render the index page when navigating spread route /spread/[...spread] ', async () => {
+ const request = new Request('http://example.com/spread/hello');
+ const response = await app.render(request);
+ const html = await response.text();
+ const $ = cheerioLoad(html);
+
+ assert.equal($('h1').text(), 'Index');
+ });
+
+ it('should render the 404 built-in page', async () => {
+ const request = new Request('http://example.com/blog/oops');
+ const response = await app.render(request);
+ const html = await response.text();
+ assert.equal(html, 'Not found');
+ });
+});
+
+describe('Middleware', () => {
+ /** @type {import('./test-utils').Fixture} */
+ let fixture;
+ let devServer;
+
+ before(async () => {
+ fixture = await loadFixture({
+ root: './fixtures/reroute/',
+ });
+ devServer = await fixture.startDevServer();
+ });
+
+ after(async () => {
+ await devServer.stop();
+ });
+
+ it('should render a locals populated in the third middleware function, because we use next("/")', async () => {
+ const html = await fixture.fetch('/auth/base').then((res) => res.text());
+ const $ = cheerioLoad(html);
+
+ assert.equal($('h1').text(), 'Index');
+ assert.equal($('p').text(), 'Called auth');
+ });
+
+ it('should NOT render locals populated in the third middleware function, because we use ctx.reroute("/")', async () => {
+ const html = await fixture.fetch('/auth/dashboard').then((res) => res.text());
+ const $ = cheerioLoad(html);
+
+ assert.equal($('h1').text(), 'Index');
+ assert.equal($('p').text(), '');
+ });
+});
diff --git a/packages/astro/test/units/routing/route-matching.test.js b/packages/astro/test/units/routing/route-matching.test.js
index b2f27d8c9f80..5eafa6c80aea 100644
--- a/packages/astro/test/units/routing/route-matching.test.js
+++ b/packages/astro/test/units/routing/route-matching.test.js
@@ -146,7 +146,7 @@ describe('Route matching', () => {
const loader = createViteLoader(container.viteServer);
const manifest = createDevelopmentManifest(container.settings);
- pipeline = DevPipeline.create({ loader, logger: defaultLogger, manifest, settings });
+ pipeline = DevPipeline.create(undefined, { loader, logger: defaultLogger, manifest, settings });
manifestData = createRouteManifest(
{
cwd: fileURLToPath(root),
diff --git a/packages/astro/test/units/vite-plugin-astro-server/request.test.js b/packages/astro/test/units/vite-plugin-astro-server/request.test.js
index 7ea587f97e2f..f976a9d30b50 100644
--- a/packages/astro/test/units/vite-plugin-astro-server/request.test.js
+++ b/packages/astro/test/units/vite-plugin-astro-server/request.test.js
@@ -22,7 +22,7 @@ async function createDevPipeline(overrides = {}) {
const loader = overrides.loader ?? createLoader();
const manifest = createDevelopmentManifest(settings);
- return DevPipeline.create({ loader, logger: defaultLogger, manifest, settings });
+ return DevPipeline.create(undefined, { loader, logger: defaultLogger, manifest, settings });
}
describe('vite-plugin-astro-server', () => {
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index be362ddc2641..8a32dab848f9 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -3114,6 +3114,12 @@ importers:
specifier: workspace:*
version: link:../../..
+ packages/astro/test/fixtures/middleware-virtual:
+ dependencies:
+ astro:
+ specifier: workspace:*
+ version: link:../../..
+
packages/astro/test/fixtures/minification-html:
dependencies:
astro:
@@ -3312,6 +3318,12 @@ importers:
specifier: workspace:*
version: link:../../..
+ packages/astro/test/fixtures/reroute:
+ dependencies:
+ astro:
+ specifier: workspace:*
+ version: link:../../..
+
packages/astro/test/fixtures/root-srcdir-css:
dependencies:
astro: