summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.changeset/hungry-rings-argue.md5
-rw-r--r--packages/astro/src/assets/build/generate.ts5
-rw-r--r--packages/astro/src/core/app/index.ts176
-rw-r--r--packages/astro/src/core/app/pipeline.ts33
-rw-r--r--packages/astro/src/core/app/ssrPipeline.ts3
-rw-r--r--packages/astro/src/core/base-pipeline.ts51
-rw-r--r--packages/astro/src/core/build/generate.ts157
-rw-r--r--packages/astro/src/core/build/index.ts4
-rw-r--r--packages/astro/src/core/build/pipeline.ts (renamed from packages/astro/src/core/build/buildPipeline.ts)157
-rw-r--r--packages/astro/src/core/build/types.ts2
-rw-r--r--packages/astro/src/core/constants.ts15
-rw-r--r--packages/astro/src/core/endpoint/index.ts52
-rw-r--r--packages/astro/src/core/middleware/callMiddleware.ts16
-rw-r--r--packages/astro/src/core/middleware/index.ts78
-rw-r--r--packages/astro/src/core/pipeline.ts132
-rw-r--r--packages/astro/src/core/redirects/helpers.ts37
-rw-r--r--packages/astro/src/core/redirects/index.ts3
-rw-r--r--packages/astro/src/core/redirects/render.ts32
-rw-r--r--packages/astro/src/core/render-context.ts148
-rw-r--r--packages/astro/src/core/render/core.ts83
-rw-r--r--packages/astro/src/core/render/environment.ts39
-rw-r--r--packages/astro/src/core/render/index.ts21
-rw-r--r--packages/astro/src/core/render/params-and-props.ts53
-rw-r--r--packages/astro/src/core/render/result.ts19
-rw-r--r--packages/astro/src/core/routing/index.ts1
-rw-r--r--packages/astro/src/core/routing/params.ts21
-rw-r--r--packages/astro/src/i18n/middleware.ts35
-rw-r--r--packages/astro/src/i18n/utils.ts (renamed from packages/astro/src/core/render/context.ts)103
-rw-r--r--packages/astro/src/prerender/routing.ts7
-rw-r--r--packages/astro/src/runtime/server/consts.ts1
-rw-r--r--packages/astro/src/runtime/server/endpoint.ts4
-rw-r--r--packages/astro/src/vite-plugin-astro-server/devPipeline.ts91
-rw-r--r--packages/astro/src/vite-plugin-astro-server/error.ts8
-rw-r--r--packages/astro/src/vite-plugin-astro-server/index.ts31
-rw-r--r--packages/astro/src/vite-plugin-astro-server/pipeline.ts137
-rw-r--r--packages/astro/src/vite-plugin-astro-server/plugin.ts5
-rw-r--r--packages/astro/src/vite-plugin-astro-server/request.ts16
-rw-r--r--packages/astro/src/vite-plugin-astro-server/route.ts227
-rw-r--r--packages/astro/test/fixtures/i18n-routing-prefix-always/astro.config.mjs2
-rw-r--r--packages/astro/test/units/i18n/astro_i18n.test.js2
-rw-r--r--packages/astro/test/units/render/head.test.js55
-rw-r--r--packages/astro/test/units/render/jsx.test.js48
-rw-r--r--packages/astro/test/units/routing/route-matching.test.js4
-rw-r--r--packages/astro/test/units/test-utils.js41
-rw-r--r--packages/astro/test/units/vite-plugin-astro-server/request.test.js19
-rw-r--r--packages/astro/test/units/vite-plugin-astro-server/response.test.js2
46 files changed, 828 insertions, 1353 deletions
diff --git a/.changeset/hungry-rings-argue.md b/.changeset/hungry-rings-argue.md
new file mode 100644
index 000000000..1d7252e67
--- /dev/null
+++ b/.changeset/hungry-rings-argue.md
@@ -0,0 +1,5 @@
+---
+"astro": patch
+---
+
+Refactors internals relating to middleware, endpoints, and page rendering.
diff --git a/packages/astro/src/assets/build/generate.ts b/packages/astro/src/assets/build/generate.ts
index a73ef11f2..f106a56cc 100644
--- a/packages/astro/src/assets/build/generate.ts
+++ b/packages/astro/src/assets/build/generate.ts
@@ -3,7 +3,7 @@ import fs, { readFileSync } from 'node:fs';
import { basename, join } from 'node:path/posix';
import type PQueue from 'p-queue';
import type { AstroConfig } from '../../@types/astro.js';
-import type { BuildPipeline } from '../../core/build/buildPipeline.js';
+import type { BuildPipeline } from '../../core/build/pipeline.js';
import { getOutDirWithinCwd } from '../../core/build/common.js';
import { getTimeStat } from '../../core/build/util.js';
import { AstroError } from '../../core/errors/errors.js';
@@ -50,8 +50,7 @@ export async function prepareAssetsGenerationEnv(
pipeline: BuildPipeline,
totalCount: number
): Promise<AssetEnv> {
- const config = pipeline.getConfig();
- const logger = pipeline.getLogger();
+ const { config, logger } = pipeline;
let useCache = true;
const assetsCacheDir = new URL('assets/', config.cacheDir);
const count = { total: totalCount, current: 1 };
diff --git a/packages/astro/src/core/app/index.ts b/packages/astro/src/core/app/index.ts
index d3f287efd..481fffb41 100644
--- a/packages/astro/src/core/app/index.ts
+++ b/packages/astro/src/core/app/index.ts
@@ -1,17 +1,12 @@
import type {
- EndpointHandler,
ManifestData,
RouteData,
- SSRElement,
SSRManifest,
} from '../../@types/astro.js';
-import { createI18nMiddleware, i18nPipelineHook } from '../../i18n/middleware.js';
-import { REROUTE_DIRECTIVE_HEADER } from '../../runtime/server/consts.js';
import type { SinglePageBuiltModule } from '../build/types.js';
import { getSetCookiesFromResponse } from '../cookies/index.js';
import { consoleLogDestination } from '../logger/console.js';
import { AstroIntegrationLogger, Logger } from '../logger/core.js';
-import { sequence } from '../middleware/index.js';
import {
appendForwardSlash,
collapseDuplicateSlashes,
@@ -20,29 +15,15 @@ import {
removeTrailingForwardSlash,
} from '../path.js';
import { RedirectSinglePageBuiltModule } from '../redirects/index.js';
-import { createEnvironment, createRenderContext, type RenderContext } from '../render/index.js';
-import { RouteCache } from '../render/route-cache.js';
-import {
- createAssetLink,
- createModuleScriptElement,
- createStylesheetElementSet,
-} from '../render/ssr-element.js';
+import { createAssetLink } from '../render/ssr-element.js';
import { matchRoute } from '../routing/match.js';
-import { SSRRoutePipeline } from './ssrPipeline.js';
-import type { RouteInfo } from './types.js';
+import { AppPipeline } from './pipeline.js';
import { normalizeTheLocale } from '../../i18n/index.js';
+import { RenderContext } from '../render-context.js';
+import { clientAddressSymbol, clientLocalsSymbol, responseSentSymbol, REROUTABLE_STATUS_CODES, REROUTE_DIRECTIVE_HEADER } from '../constants.js';
+import { AstroError, AstroErrorData } from '../errors/index.js';
export { deserializeManifest } from './common.js';
-const localsSymbol = Symbol.for('astro.locals');
-const clientAddressSymbol = Symbol.for('astro.clientAddress');
-const responseSentSymbol = Symbol.for('astro.responseSent');
-
-/**
- * A response with one of these status codes will be rewritten
- * with the result of rendering the respective error page.
- */
-const REROUTABLE_STATUS_CODES = new Set([404, 500]);
-
export interface RenderOptions {
/**
* Whether to automatically add all cookies written by `Astro.cookie.set()` to the response headers.
@@ -86,18 +67,14 @@ export interface RenderErrorOptions {
}
export class App {
- /**
- * The current environment of the application
- */
#manifest: SSRManifest;
#manifestData: ManifestData;
- #routeDataToRouteInfo: Map<RouteData, RouteInfo>;
#logger = new Logger({
dest: consoleLogDestination,
level: 'info',
});
#baseWithoutTrailingSlash: string;
- #pipeline: SSRRoutePipeline;
+ #pipeline: AppPipeline;
#adapterLogger: AstroIntegrationLogger;
#renderOptionsDeprecationWarningShown = false;
@@ -106,9 +83,8 @@ export class App {
this.#manifestData = {
routes: manifest.routes.map((route) => route.routeData),
};
- this.#routeDataToRouteInfo = new Map(manifest.routes.map((route) => [route.routeData, route]));
this.#baseWithoutTrailingSlash = removeTrailingForwardSlash(this.#manifest.base);
- this.#pipeline = new SSRRoutePipeline(this.#createEnvironment(streaming));
+ this.#pipeline = this.#createPipeline(streaming);
this.#adapterLogger = new AstroIntegrationLogger(
this.#logger.options,
this.#manifest.adapterName
@@ -120,19 +96,17 @@ export class App {
}
/**
- * Creates an environment by reading the stored manifest
+ * Creates a pipeline by reading the stored manifest
*
* @param streaming
* @private
*/
- #createEnvironment(streaming = false) {
- return createEnvironment({
- adapterName: this.#manifest.adapterName,
+ #createPipeline(streaming = false) {
+ return AppPipeline.create({
logger: this.#logger,
+ manifest: this.#manifest,
mode: 'production',
- compressHTML: this.#manifest.compressHTML,
renderers: this.#manifest.renderers,
- clientDirectives: this.#manifest.clientDirectives,
resolve: async (specifier: string) => {
if (!(specifier in this.#manifest.entryModules)) {
throw new Error(`Unable to resolve [${specifier}]`);
@@ -148,11 +122,9 @@ export class App {
}
}
},
- routeCache: new RouteCache(this.#logger),
- site: this.#manifest.site,
- ssr: true,
+ serverLike: true,
streaming,
- });
+ })
}
set setManifestData(newManifestData: ManifestData) {
@@ -297,7 +269,11 @@ export class App {
}
}
if (locals) {
- Reflect.set(request, localsSymbol, locals);
+ if (typeof locals !== 'object') {
+ this.#logger.error(null, new AstroError(AstroErrorData.LocalsNotAnObject).stack!);
+ return this.#renderError(request, { status: 500 });
+ }
+ Reflect.set(request, clientLocalsSymbol, locals);
}
if (clientAddress) {
Reflect.set(request, clientAddressSymbol, clientAddress);
@@ -316,38 +292,17 @@ export class App {
const defaultStatus = this.#getDefaultStatusCode(routeData, pathname);
const mod = await this.#getModuleForRoute(routeData);
- const pageModule = (await mod.page()) as any;
- const url = new URL(request.url);
-
- const renderContext = await this.#createRenderContext(
- url,
- request,
- routeData,
- mod,
- defaultStatus
- );
let response;
try {
- const i18nMiddleware = createI18nMiddleware(
- this.#manifest.i18n,
- this.#manifest.base,
- this.#manifest.trailingSlash,
- this.#manifest.buildFormat
- );
- if (i18nMiddleware) {
- this.#pipeline.setMiddlewareFunction(sequence(i18nMiddleware, this.#manifest.middleware));
- this.#pipeline.onBeforeRenderRoute(i18nPipelineHook);
- } else {
- this.#pipeline.setMiddlewareFunction(this.#manifest.middleware);
- }
- response = await this.#pipeline.renderRoute(renderContext, pageModule);
+ const renderContext = RenderContext.create({ pipeline: this.#pipeline, locals, pathname, request, routeData, status: defaultStatus })
+ response = await renderContext.render(await mod.page());
} catch (err: any) {
this.#logger.error(null, err.stack || err.message || String(err));
return this.#renderError(request, { status: 500 });
}
if (
- REROUTABLE_STATUS_CODES.has(response.status) &&
+ REROUTABLE_STATUS_CODES.includes(response.status) &&
response.headers.get(REROUTE_DIRECTIVE_HEADER) !== 'no'
) {
return this.#renderError(request, {
@@ -397,72 +352,6 @@ export class App {
static getSetCookieFromResponse = getSetCookiesFromResponse;
/**
- * Creates the render context of the current route
- */
- async #createRenderContext(
- url: URL,
- request: Request,
- routeData: RouteData,
- page: SinglePageBuiltModule,
- status = 200
- ): Promise<RenderContext> {
- if (routeData.type === 'endpoint') {
- const pathname = '/' + this.removeBase(url.pathname);
- const mod = await page.page();
- const handler = mod as unknown as EndpointHandler;
-
- return await createRenderContext({
- request,
- pathname,
- route: routeData,
- status,
- env: this.#pipeline.env,
- mod: handler as any,
- locales: this.#manifest.i18n?.locales,
- routing: this.#manifest.i18n?.routing,
- defaultLocale: this.#manifest.i18n?.defaultLocale,
- });
- } else {
- const pathname = prependForwardSlash(this.removeBase(url.pathname));
- const info = this.#routeDataToRouteInfo.get(routeData)!;
- // may be used in the future for handling rel=modulepreload, rel=icon, rel=manifest etc.
- const links = new Set<never>();
- const styles = createStylesheetElementSet(info.styles);
-
- let scripts = new Set<SSRElement>();
- for (const script of info.scripts) {
- if ('stage' in script) {
- if (script.stage === 'head-inline') {
- scripts.add({
- props: {},
- children: script.children,
- });
- }
- } else {
- scripts.add(createModuleScriptElement(script));
- }
- }
- const mod = await page.page();
-
- return await createRenderContext({
- request,
- pathname,
- componentMetadata: this.#manifest.componentMetadata,
- scripts,
- styles,
- links,
- route: routeData,
- status,
- mod,
- env: this.#pipeline.env,
- locales: this.#manifest.i18n?.locales,
- routing: this.#manifest.i18n?.routing,
- defaultLocale: this.#manifest.i18n?.defaultLocale,
- });
- }
- }
-
- /**
* If it is a known error code, try sending the according page (e.g. 404.astro / 500.astro).
* This also handles pre-rendered /404 or /500 routes
*/
@@ -490,22 +379,15 @@ export class App {
}
const mod = await this.#getModuleForRoute(errorRouteData);
try {
- const newRenderContext = await this.#createRenderContext(
- url,
+ const renderContext = RenderContext.create({
+ pipeline: this.#pipeline,
+ middleware: skipMiddleware ? (_, next) => next() : undefined,
+ pathname: this.#getPathnameFromRequest(request),
request,
- errorRouteData,
- mod,
- status
- );
- const page = (await mod.page()) as any;
- if (skipMiddleware === false) {
- this.#pipeline.setMiddlewareFunction(this.#manifest.middleware);
- }
- if (skipMiddleware) {
- // make sure middleware set by other requests is cleared out
- this.#pipeline.unsetMiddlewareFunction();
- }
- const response = await this.#pipeline.renderRoute(newRenderContext, page);
+ routeData: errorRouteData,
+ status,
+ })
+ const response = await renderContext.render(await mod.page());
return this.#mergeResponses(response, originalResponse);
} catch {
// Middleware may be the cause of the error, so we try rendering 404/500.astro without it.
diff --git a/packages/astro/src/core/app/pipeline.ts b/packages/astro/src/core/app/pipeline.ts
new file mode 100644
index 000000000..74fa95ec6
--- /dev/null
+++ b/packages/astro/src/core/app/pipeline.ts
@@ -0,0 +1,33 @@
+import type { RouteData, SSRElement, SSRResult } from "../../@types/astro.js";
+import { Pipeline } from "../base-pipeline.js";
+import { createModuleScriptElement, createStylesheetElementSet } from "../render/ssr-element.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);
+ }
+
+ headElements(routeData: RouteData): Pick<SSRResult, 'scripts' | 'styles' | 'links'> {
+ const routeInfo = this.manifest.routes.find(route => route.routeData === routeData);
+ // may be used in the future for handling rel=modulepreload, rel=icon, rel=manifest etc.
+ const links = new Set<never>();
+ const scripts = new Set<SSRElement>();
+ const styles = createStylesheetElementSet(routeInfo?.styles ?? []);
+
+ for (const script of routeInfo?.scripts ?? []) {
+ if ('stage' in script) {
+ if (script.stage === 'head-inline') {
+ scripts.add({
+ props: {},
+ children: script.children,
+ });
+ }
+ } else {
+ scripts.add(createModuleScriptElement(script));
+ }
+ }
+ return { links, styles, scripts }
+ }
+
+ componentMetadata() {}
+}
diff --git a/packages/astro/src/core/app/ssrPipeline.ts b/packages/astro/src/core/app/ssrPipeline.ts
deleted file mode 100644
index f31636f9a..000000000
--- a/packages/astro/src/core/app/ssrPipeline.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import { Pipeline } from '../pipeline.js';
-
-export class SSRRoutePipeline extends Pipeline {}
diff --git a/packages/astro/src/core/base-pipeline.ts b/packages/astro/src/core/base-pipeline.ts
new file mode 100644
index 000000000..5fcf63903
--- /dev/null
+++ b/packages/astro/src/core/base-pipeline.ts
@@ -0,0 +1,51 @@
+import type { MiddlewareHandler, RouteData, RuntimeMode, SSRLoadedRenderer, SSRManifest, SSRResult } from '../@types/astro.js';
+import type { Logger } from './logger/core.js';
+import { RouteCache } from './render/route-cache.js';
+import { createI18nMiddleware } from '../i18n/middleware.js';
+
+/**
+ * The `Pipeline` represents the static parts of rendering that do not change between requests.
+ * These are mostly known when the server first starts up and do not change.
+ *
+ * Thus, a `Pipeline` is created once at process start and then used by every `RenderContext`.
+ */
+export abstract class Pipeline {
+ readonly internalMiddleware: MiddlewareHandler[];
+
+ constructor(
+ readonly logger: Logger,
+ readonly manifest: SSRManifest,
+ /**
+ * "development" or "production"
+ */
+ readonly mode: RuntimeMode,
+ readonly renderers: SSRLoadedRenderer[],
+ readonly resolve: (s: string) => Promise<string>,
+ /**
+ * Based on Astro config's `output` option, `true` if "server" or "hybrid".
+ */
+ readonly serverLike: boolean,
+ readonly streaming: boolean,
+ /**
+ * Used to provide better error messages for `Astro.clientAddress`
+ */
+ readonly adapterName = manifest.adapterName,
+ readonly clientDirectives = manifest.clientDirectives,
+ readonly compressHTML = manifest.compressHTML,
+ readonly i18n = manifest.i18n,
+ readonly middleware = manifest.middleware,
+ readonly routeCache = new RouteCache(logger, mode),
+ /**
+ * Used for `Astro.site`.
+ */
+ readonly site = manifest.site,
+ ) {
+ this.internalMiddleware = [ createI18nMiddleware(i18n, manifest.base, manifest.trailingSlash, manifest.buildFormat) ];
+ }
+
+ abstract headElements(routeData: RouteData): Promise<HeadElements> | HeadElements
+ abstract componentMetadata(routeData: RouteData): Promise<SSRResult['componentMetadata']> | void
+}
+
+// eslint-disable-next-line @typescript-eslint/no-empty-interface
+export interface HeadElements extends Pick<SSRResult, 'scripts' | 'styles' | 'links'> {}
diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts
index b108da5f5..14462a412 100644
--- a/packages/astro/src/core/build/generate.ts
+++ b/packages/astro/src/core/build/generate.ts
@@ -29,30 +29,21 @@ import {
removeLeadingForwardSlash,
removeTrailingForwardSlash,
} from '../../core/path.js';
-import { createI18nMiddleware, i18nPipelineHook } from '../../i18n/middleware.js';
import { runHookBuildGenerated } from '../../integrations/index.js';
import { getOutputDirectory, isServerLikeOutput } from '../../prerender/utils.js';
-import { PAGE_SCRIPT_ID } from '../../vite-plugin-scripts/index.js';
import type { SSRManifestI18n } from '../app/types.js';
import { AstroError, AstroErrorData } from '../errors/index.js';
-import { sequence } from '../middleware/index.js';
import { routeIsFallback } from '../redirects/helpers.js';
import {
RedirectSinglePageBuiltModule,
getRedirectLocationOrThrow,
routeIsRedirect,
} from '../redirects/index.js';
-import { createRenderContext } from '../render/index.js';
import { callGetStaticPaths } from '../render/route-cache.js';
-import {
- createAssetLink,
- createModuleScriptsSet,
- createStylesheetElementSet,
-} from '../render/ssr-element.js';
import { createRequest } from '../request.js';
import { matchRoute } from '../routing/match.js';
import { getOutputFilename } from '../util.js';
-import { BuildPipeline } from './buildPipeline.js';
+import { BuildPipeline } from './pipeline.js';
import { getOutDirWithinCwd, getOutFile, getOutFolder } from './common.js';
import {
cssOrder,
@@ -68,6 +59,7 @@ import type {
} from './types.js';
import { getTimeStat, shouldAppendForwardSlash } from './util.js';
import { NoPrerenderedRoutesWithDomains } from '../errors/errors-data.js';
+import { RenderContext } from '../render-context.js';
function createEntryURL(filePath: string, outFolder: URL) {
return new URL('./' + filePath + `?time=${Date.now()}`, outFolder);
@@ -138,14 +130,14 @@ export function chunkIsPage(
return false;
}
-export async function generatePages(opts: StaticBuildOptions, internals: BuildInternals) {
+export async function generatePages(options: StaticBuildOptions, internals: BuildInternals) {
const generatePagesTimer = performance.now();
- const ssr = isServerLikeOutput(opts.settings.config);
+ const ssr = isServerLikeOutput(options.settings.config);
let manifest: SSRManifest;
if (ssr) {
- manifest = await BuildPipeline.retrieveManifest(opts, internals);
+ manifest = await BuildPipeline.retrieveManifest(options, internals);
} else {
- const baseDirectory = getOutputDirectory(opts.settings.config);
+ const baseDirectory = getOutputDirectory(options.settings.config);
const renderersEntryUrl = new URL('renderers.mjs', baseDirectory);
const renderers = await import(renderersEntryUrl.toString());
let middleware: MiddlewareHandler = (_, next) => next();
@@ -157,19 +149,19 @@ export async function generatePages(opts: StaticBuildOptions, internals: BuildIn
);
} catch {}
manifest = createBuildManifest(
- opts.settings,
+ options.settings,
internals,
renderers.renderers as SSRLoadedRenderer[],
middleware
);
}
- const pipeline = new BuildPipeline(opts, internals, manifest);
+ const pipeline = BuildPipeline.create({ internals, manifest, options });
+ const { config, logger } = pipeline;
const outFolder = ssr
- ? opts.settings.config.build.server
- : getOutDirWithinCwd(opts.settings.config.outDir);
+ ? options.settings.config.build.server
+ : getOutDirWithinCwd(options.settings.config.outDir);
- const logger = pipeline.getLogger();
// HACK! `astro:assets` relies on a global to know if its running in dev, prod, ssr, ssg, full moon
// If we don't delete it here, it's technically not impossible (albeit improbable) for it to leak
if (ssr && !hasPrerenderedPages(internals)) {
@@ -181,7 +173,6 @@ export async function generatePages(opts: StaticBuildOptions, internals: BuildIn
logger.info('SKIP_FORMAT', `\n${bgGreen(black(` ${verb} static routes `))}`);
const builtPaths = new Set<string>();
const pagesToGenerate = pipeline.retrieveRoutesToGenerate();
- const config = pipeline.getConfig();
if (ssr) {
for (const [pageData, filePath] of pagesToGenerate) {
if (pageData.route.prerender) {
@@ -195,7 +186,7 @@ export async function generatePages(opts: StaticBuildOptions, internals: BuildIn
const ssrEntryURLPage = createEntryURL(filePath, outFolder);
const ssrEntryPage = await import(ssrEntryURLPage.toString());
- if (opts.settings.adapter?.adapterFeatures?.functionPerRoute) {
+ if (options.settings.adapter?.adapterFeatures?.functionPerRoute) {
// forcing to use undefined, so we fail in an expected way if the module is not even there.
const ssrEntry = ssrEntryPage?.pageModule;
if (ssrEntry) {
@@ -240,12 +231,12 @@ export async function generatePages(opts: StaticBuildOptions, internals: BuildIn
.map((x) => x.transforms.size)
.reduce((a, b) => a + b, 0);
const cpuCount = os.cpus().length;
- const assetsCreationEnvironment = 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, assetsCreationEnvironment, queue);
+ await generateImagesForPath(originalPath, transforms, assetsCreationpipeline, queue);
}
await queue.onIdle();
@@ -255,10 +246,7 @@ export async function generatePages(opts: StaticBuildOptions, internals: BuildIn
delete globalThis?.astroAsset?.addStaticImage;
}
- await runHookBuildGenerated({
- config: opts.settings.config,
- logger: pipeline.getLogger(),
- });
+ await runHookBuildGenerated({ config, logger });
}
async function generatePage(
@@ -268,12 +256,9 @@ async function generatePage(
pipeline: BuildPipeline
) {
// prepare information we need
- const logger = pipeline.getLogger();
- const config = pipeline.getConfig();
- const manifest = pipeline.getManifest();
+ const { config, internals, logger } = pipeline;
const pageModulePromise = ssrEntry.page;
- const onRequest = manifest.middleware;
- const pageInfo = getPageDataByComponent(pipeline.getInternals(), pageData.route.component);
+ const pageInfo = getPageDataByComponent(internals, pageData.route.component);
// Calculate information of the page, like scripts, links and styles
const styles = pageData.styles
@@ -283,19 +268,6 @@ async function generatePage(
// may be used in the future for handling rel=modulepreload, rel=icon, rel=manifest etc.
const linkIds: [] = [];
const scripts = pageInfo?.hoistedScript ?? null;
- // prepare the middleware
- const i18nMiddleware = createI18nMiddleware(
- manifest.i18n,
- manifest.base,
- manifest.trailingSlash,
- manifest.buildFormat
- );
- if (config.i18n && i18nMiddleware) {
- pipeline.setMiddlewareFunction(sequence(i18nMiddleware, onRequest));
- pipeline.onBeforeRenderRoute(i18nPipelineHook);
- } else {
- pipeline.setMiddlewareFunction(onRequest);
- }
if (!pageModulePromise) {
throw new Error(
`Unable to find the module for ${pageData.component}. This is unexpected and likely a bug in Astro, please report.`
@@ -322,8 +294,8 @@ async function generatePage(
let prevTimeEnd = timeStart;
for (let i = 0; i < paths.length; i++) {
const path = paths[i];
- pipeline.getEnvironment().logger.debug('build', `Generating: ${path}`);
- const filePath = getOutputFilename(pipeline.getConfig(), path, pageData.route.type);
+ pipeline.logger.debug('build', `Generating: ${path}`);
+ const filePath = getOutputFilename(config, path, pageData.route.type);
const lineIcon = i === paths.length - 1 ? '└─' : '├─';
logger.info(null, ` ${blue(lineIcon)} ${dim(filePath)}`, false);
await generatePath(path, pipeline, generationOptions, route);
@@ -349,8 +321,7 @@ async function getPathsForRoute(
pipeline: BuildPipeline,
builtPaths: Set<string>
): Promise<Array<string>> {
- const opts = pipeline.getStaticBuildOptions();
- const logger = pipeline.getLogger();
+ const { logger, options, routeCache, serverLike } = pipeline;
let paths: Array<string> = [];
if (route.pathname) {
paths.push(route.pathname);
@@ -365,9 +336,9 @@ async function getPathsForRoute(
const staticPaths = await callGetStaticPaths({
mod,
route,
- routeCache: opts.routeCache,
+ routeCache,
logger,
- ssr: isServerLikeOutput(opts.settings.config),
+ ssr: serverLike,
}).catch((err) => {
logger.debug('build', `├── ${bold(red('✗'))} ${route.component}`);
throw err;
@@ -401,7 +372,7 @@ async function getPathsForRoute(
// NOTE: The same URL may match multiple routes in the manifest.
// Routing priority needs to be verified here for any duplicate
// paths to ensure routing priority rules are enforced in the final build.
- const matchedRoute = matchRoute(staticPath, opts.manifest);
+ const matchedRoute = matchRoute(staticPath, options.manifest);
return matchedRoute === route;
});
@@ -504,84 +475,36 @@ async function generatePath(
gopts: GeneratePathOptions,
route: RouteData
) {
- const { mod, scripts: hoistedScripts, styles: _styles } = gopts;
- const manifest = pipeline.getManifest();
- const logger = pipeline.getLogger();
+ const { mod } = gopts;
+ const { config, logger, options, serverLike } = pipeline;
logger.debug('build', `Generating: ${pathname}`);
- const links = new Set<never>();
- const scripts = createModuleScriptsSet(
- hoistedScripts ? [hoistedScripts] : [],
- manifest.base,
- manifest.assetsPrefix
- );
- const styles = createStylesheetElementSet(_styles, manifest.base, manifest.assetsPrefix);
-
- if (pipeline.getSettings().scripts.some((script) => script.stage === 'page')) {
- const hashedFilePath = pipeline.getInternals().entrySpecifierToBundleMap.get(PAGE_SCRIPT_ID);
- if (typeof hashedFilePath !== 'string') {
- throw new Error(`Cannot find the built path for ${PAGE_SCRIPT_ID}`);
- }
- const src = createAssetLink(hashedFilePath, manifest.base, manifest.assetsPrefix);
- scripts.add({
- props: { type: 'module', src },
- children: '',
- });
- }
-
- // Add all injected scripts to the page.
- for (const script of pipeline.getSettings().scripts) {
- if (script.stage === 'head-inline') {
- scripts.add({
- props: {},
- children: script.content,
- });
- }
- }
-
// This adds the page name to the array so it can be shown as part of stats.
if (route.type === 'page') {
- addPageName(pathname, pipeline.getStaticBuildOptions());
+ addPageName(pathname, options);
}
- const ssr = isServerLikeOutput(pipeline.getConfig());
const url = getUrlForPath(
pathname,
- pipeline.getConfig().base,
- pipeline.getStaticBuildOptions().origin,
- pipeline.getConfig().build.format,
- pipeline.getConfig().trailingSlash,
+ config.base,
+ options.origin,
+ config.build.format,
+ config.trailingSlash,
route.type
);
const request = createRequest({
url,
headers: new Headers(),
- logger: pipeline.getLogger(),
- ssr,
- });
- const i18n = pipeline.getConfig().i18n;
-
- const renderContext = await createRenderContext({
- pathname,
- request,
- componentMetadata: manifest.componentMetadata,
- scripts,
- styles,
- links,
- route,
- env: pipeline.getEnvironment(),
- mod,
- locales: i18n?.locales,
- routing: i18n?.routing,
- defaultLocale: i18n?.defaultLocale,
+ logger,
+ ssr: serverLike,
});
+ const renderContext = RenderContext.create({ pipeline, pathname, request, routeData: route })
let body: string | Uint8Array;
-
let response: Response;
try {
- response = await pipeline.renderRoute(renderContext, mod);
+ response = await renderContext.render(mod);
} catch (err) {
if (!AstroError.is(err) && !(err as SSRError).id && typeof err === 'object') {
(err as SSRError).id = route.component;
@@ -591,13 +514,13 @@ async function generatePath(
if (response.status >= 300 && response.status < 400) {
// If redirects is set to false, don't output the HTML
- if (!pipeline.getConfig().build.redirects) {
+ if (!config.build.redirects) {
return;
}
const locationSite = getRedirectLocationOrThrow(response.headers);
- const siteURL = pipeline.getConfig().site;
+ const siteURL = config.site;
const location = siteURL ? new URL(locationSite, siteURL) : locationSite;
- const fromPath = new URL(renderContext.request.url).pathname;
+ const fromPath = new URL(request.url).pathname;
// A short delay causes Google to interpret the redirect as temporary.
// https://developers.google.com/search/docs/crawling-indexing/301-redirects#metarefresh
const delay = response.status === 302 ? 2 : 0;
@@ -609,7 +532,7 @@ async function generatePath(
<body>
<a href="${location}">Redirecting from <code>${fromPath}</code> to <code>${location}</code></a>
</body>`;
- if (pipeline.getConfig().compressHTML === true) {
+ if (config.compressHTML === true) {
body = body.replaceAll('\n', '');
}
// A dynamic redirect, set the location so that integrations know about it.
@@ -622,8 +545,8 @@ async function generatePath(
body = Buffer.from(await response.arrayBuffer());
}
- const outFolder = getOutFolder(pipeline.getConfig(), pathname, route);
- const outFile = getOutFile(pipeline.getConfig(), outFolder, pathname, route);
+ const outFolder = getOutFolder(config, pathname, route);
+ const outFile = getOutFile(config, outFolder, pathname, route);
route.distURL = outFile;
await fs.promises.mkdir(outFolder, { recursive: true });
diff --git a/packages/astro/src/core/build/index.ts b/packages/astro/src/core/build/index.ts
index 553497bc5..7e245726a 100644
--- a/packages/astro/src/core/build/index.ts
+++ b/packages/astro/src/core/build/index.ts
@@ -27,7 +27,6 @@ import { createVite } from '../create-vite.js';
import type { Logger } from '../logger/core.js';
import { levels, timerMessage } from '../logger/core.js';
import { apply as applyPolyfill } from '../polyfill.js';
-import { RouteCache } from '../render/route-cache.js';
import { createRouteManifest } from '../routing/index.js';
import { collectPagesData } from './page-data.js';
import { staticBuild, viteBuild } from './static-build.js';
@@ -98,7 +97,6 @@ class AstroBuilder {
private logger: Logger;
private mode: RuntimeMode = 'production';
private origin: string;
- private routeCache: RouteCache;
private manifest: ManifestData;
private timer: Record<string, number>;
private teardownCompiler: boolean;
@@ -110,7 +108,6 @@ class AstroBuilder {
this.settings = settings;
this.logger = options.logger;
this.teardownCompiler = options.teardownCompiler ?? true;
- this.routeCache = new RouteCache(this.logger);
this.origin = settings.config.site
? new URL(settings.config.site).origin
: `http://localhost:${settings.config.server.port}`;
@@ -195,7 +192,6 @@ class AstroBuilder {
mode: this.mode,
origin: this.origin,
pageNames,
- routeCache: this.routeCache,
teardownCompiler: this.teardownCompiler,
viteConfig,
};
diff --git a/packages/astro/src/core/build/buildPipeline.ts b/packages/astro/src/core/build/pipeline.ts
index 60527b27f..a2647e456 100644
--- a/packages/astro/src/core/build/buildPipeline.ts
+++ b/packages/astro/src/core/build/pipeline.ts
@@ -1,13 +1,11 @@
-import type { AstroConfig, AstroSettings, SSRLoadedRenderer } from '../../@types/astro.js';
+import type { RouteData, SSRLoadedRenderer, SSRResult } from '../../@types/astro.js';
import { getOutputDirectory, isServerLikeOutput } from '../../prerender/utils.js';
-import { BEFORE_HYDRATION_SCRIPT_ID } from '../../vite-plugin-scripts/index.js';
+import { BEFORE_HYDRATION_SCRIPT_ID, PAGE_SCRIPT_ID } from '../../vite-plugin-scripts/index.js';
import type { SSRManifest } from '../app/types.js';
-import type { Logger } from '../logger/core.js';
-import { Pipeline } from '../pipeline.js';
import { routeIsFallback, routeIsRedirect } from '../redirects/helpers.js';
-import { createEnvironment } from '../render/index.js';
-import { createAssetLink } from '../render/ssr-element.js';
-import type { BuildInternals } from './internal.js';
+import { Pipeline } from '../render/index.js';
+import { createAssetLink, createModuleScriptsSet, createStylesheetElementSet } from '../render/ssr-element.js';
+import { getPageDataByComponent, type BuildInternals, cssOrder, mergeInlineCss } from './internal.js';
import {
ASTRO_PAGE_RESOLVED_MODULE_ID,
getVirtualModulePageNameFromPath,
@@ -18,79 +16,42 @@ import type { PageBuildData, StaticBuildOptions } from './types.js';
import { i18nHasFallback } from './util.js';
/**
- * This pipeline is responsible to gather the files emitted by the SSR build and generate the pages by executing these files.
+ * 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 {
- #internals: BuildInternals;
- #staticBuildOptions: StaticBuildOptions;
- #manifest: SSRManifest;
-
- constructor(
- staticBuildOptions: StaticBuildOptions,
- internals: BuildInternals,
- manifest: SSRManifest
+ private constructor(
+ readonly internals: BuildInternals,
+ readonly manifest: SSRManifest,
+ readonly options: StaticBuildOptions,
+ readonly config = options.settings.config,
+ readonly settings = options.settings
) {
- const ssr = isServerLikeOutput(staticBuildOptions.settings.config);
const resolveCache = new Map<string, string>();
- super(
- createEnvironment({
- adapterName: manifest.adapterName,
- logger: staticBuildOptions.logger,
- mode: staticBuildOptions.mode,
- renderers: manifest.renderers,
- clientDirectives: manifest.clientDirectives,
- compressHTML: manifest.compressHTML,
- async resolve(specifier: string) {
- if (resolveCache.has(specifier)) {
- return resolveCache.get(specifier)!;
- }
- const hashedFilePath = manifest.entryModules[specifier];
- if (typeof hashedFilePath !== 'string' || hashedFilePath === '') {
- // If no "astro:scripts/before-hydration.js" script exists in the build,
- // then we can assume that no before-hydration scripts are needed.
- if (specifier === BEFORE_HYDRATION_SCRIPT_ID) {
- resolveCache.set(specifier, '');
- return '';
- }
- throw new Error(`Cannot find the built path for ${specifier}`);
- }
- const assetLink = createAssetLink(hashedFilePath, manifest.base, manifest.assetsPrefix);
- resolveCache.set(specifier, assetLink);
- return assetLink;
- },
- routeCache: staticBuildOptions.routeCache,
- site: manifest.site,
- ssr,
- streaming: true,
- })
- );
- this.#internals = internals;
- this.#staticBuildOptions = staticBuildOptions;
- this.#manifest = manifest;
- }
-
- getInternals(): Readonly<BuildInternals> {
- return this.#internals;
- }
-
- getSettings(): Readonly<AstroSettings> {
- return this.#staticBuildOptions.settings;
- }
-
- getStaticBuildOptions(): Readonly<StaticBuildOptions> {
- return this.#staticBuildOptions;
- }
-
- getConfig(): AstroConfig {
- return this.#staticBuildOptions.settings.config;
- }
-
- getManifest(): SSRManifest {
- return this.#manifest;
+ async function resolve(specifier: string) {
+ if (resolveCache.has(specifier)) {
+ return resolveCache.get(specifier)!;
+ }
+ const hashedFilePath = manifest.entryModules[specifier];
+ if (typeof hashedFilePath !== 'string' || hashedFilePath === '') {
+ // If no "astro:scripts/before-hydration.js" script exists in the build,
+ // then we can assume that no before-hydration scripts are needed.
+ if (specifier === BEFORE_HYDRATION_SCRIPT_ID) {
+ resolveCache.set(specifier, '');
+ return '';
+ }
+ throw new Error(`Cannot find the built path for ${specifier}`);
+ }
+ const assetLink = createAssetLink(hashedFilePath, manifest.base, manifest.assetsPrefix);
+ resolveCache.set(specifier, assetLink);
+ return assetLink;
+ }
+ const serverLike = isServerLikeOutput(config);
+ const streaming = true;
+ super(options.logger, manifest, options.mode, manifest.renderers, resolve, serverLike, streaming)
}
- getLogger(): Logger {
- return this.getEnvironment().logger;
+ static create({ internals, manifest, options }: Pick<BuildPipeline, 'internals' | 'manifest' | 'options'>) {
+ return new BuildPipeline(internals, manifest, options);
}
/**
@@ -144,6 +105,44 @@ export class BuildPipeline extends Pipeline {
};
}
+ headElements(routeData: RouteData): Pick<SSRResult, 'scripts' | 'styles' | 'links'> {
+ const { internals, manifest: { assetsPrefix, base }, settings } = this
+ const links = new Set<never>();
+ const pageBuildData = getPageDataByComponent(internals, routeData.component)
+ const scripts = createModuleScriptsSet(
+ pageBuildData?.hoistedScript ? [pageBuildData.hoistedScript] : [],
+ base,
+ assetsPrefix
+ );
+ const sortedCssAssets = pageBuildData?.styles.sort(cssOrder).map(({ sheet }) => sheet).reduce(mergeInlineCss, []);
+ const styles = createStylesheetElementSet(sortedCssAssets ?? [], base, assetsPrefix);
+
+ if (settings.scripts.some((script) => script.stage === 'page')) {
+ const hashedFilePath = internals.entrySpecifierToBundleMap.get(PAGE_SCRIPT_ID);
+ if (typeof hashedFilePath !== 'string') {
+ throw new Error(`Cannot find the built path for ${PAGE_SCRIPT_ID}`);
+ }
+ const src = createAssetLink(hashedFilePath, base, assetsPrefix);
+ scripts.add({
+ props: { type: 'module', src },
+ children: '',
+ });
+ }
+
+ // Add all injected scripts to the page.
+ for (const script of settings.scripts) {
+ if (script.stage === 'head-inline') {
+ scripts.add({
+ props: {},
+ children: script.content,
+ });
+ }
+ }
+ return { scripts, styles, links }
+ }
+
+ componentMetadata() {}
+
/**
* It collects the routes to generate during the build.
*
@@ -152,7 +151,7 @@ export class BuildPipeline extends Pipeline {
retrieveRoutesToGenerate(): Map<PageBuildData, string> {
const pages = new Map<PageBuildData, string>();
- for (const [entrypoint, filePath] of this.#internals.entrySpecifierToBundleMap) {
+ for (const [entrypoint, filePath] of this.internals.entrySpecifierToBundleMap) {
// virtual pages can be emitted with different prefixes:
// - the classic way are pages emitted with prefix ASTRO_PAGE_RESOLVED_MODULE_ID -> plugin-pages
// - pages emitted using `build.split`, in this case pages are emitted with prefix RESOLVED_SPLIT_MODULE_ID
@@ -161,7 +160,7 @@ export class BuildPipeline extends Pipeline {
entrypoint.includes(RESOLVED_SPLIT_MODULE_ID)
) {
const [, pageName] = entrypoint.split(':');
- const pageData = this.#internals.pagesByComponent.get(
+ const pageData = this.internals.pagesByComponent.get(
`${pageName.replace(ASTRO_PAGE_EXTENSION_POST_PATTERN, '.')}`
);
if (!pageData) {
@@ -174,12 +173,12 @@ export class BuildPipeline extends Pipeline {
}
}
- for (const [path, pageData] of this.#internals.pagesByComponent.entries()) {
+ for (const [path, pageData] of this.internals.pagesByComponent.entries()) {
if (routeIsRedirect(pageData.route)) {
pages.set(pageData, path);
} else if (
routeIsFallback(pageData.route) &&
- (i18nHasFallback(this.getConfig()) ||
+ (i18nHasFallback(this.config) ||
(routeIsFallback(pageData.route) && pageData.route.route === '/'))
) {
// The original component is transformed during the first build, so we have to retrieve
@@ -190,7 +189,7 @@ export class BuildPipeline extends Pipeline {
// Here, we take the component path and transform it in the virtual module name
const moduleSpecifier = getVirtualModulePageNameFromPath(path);
// We retrieve the original JS module
- const filePath = this.#internals.entrySpecifierToBundleMap.get(moduleSpecifier);
+ const filePath = this.internals.entrySpecifierToBundleMap.get(moduleSpecifier);
if (filePath) {
// it exists, added it to pages to render, using the file path that we jus retrieved
pages.set(pageData, filePath);
diff --git a/packages/astro/src/core/build/types.ts b/packages/astro/src/core/build/types.ts
index b67d7d222..9608ba04c 100644
--- a/packages/astro/src/core/build/types.ts
+++ b/packages/astro/src/core/build/types.ts
@@ -11,7 +11,6 @@ import type {
SSRLoadedRenderer,
} from '../../@types/astro.js';
import type { Logger } from '../logger/core.js';
-import type { RouteCache } from '../render/route-cache.js';
export type ComponentPath = string;
export type ViteID = string;
@@ -43,7 +42,6 @@ export interface StaticBuildOptions {
mode: RuntimeMode;
origin: string;
pageNames: string[];
- routeCache: RouteCache;
viteConfig: InlineConfig;
teardownCompiler: boolean;
}
diff --git a/packages/astro/src/core/constants.ts b/packages/astro/src/core/constants.ts
index af00d655d..1466ab86a 100644
--- a/packages/astro/src/core/constants.ts
+++ b/packages/astro/src/core/constants.ts
@@ -1,6 +1,19 @@
// process.env.PACKAGE_VERSION is injected when we build and publish the astro package.
export const ASTRO_VERSION = process.env.PACKAGE_VERSION ?? 'development';
+export const REROUTE_DIRECTIVE_HEADER = 'X-Astro-Reroute';
+export const ROUTE_TYPE_HEADER = 'X-Astro-Route-Type';
+
+/**
+ * A response with one of these status codes will be rewritten
+ * with the result of rendering the respective error page.
+ */
+export const REROUTABLE_STATUS_CODES = [404, 500];
+
+export const clientAddressSymbol = Symbol.for('astro.clientAddress');
+export const clientLocalsSymbol = Symbol.for('astro.locals');
+export const responseSentSymbol = Symbol.for('astro.responseSent');
+
// possible extensions for markdown files
export const SUPPORTED_MARKDOWN_FILE_EXTENSIONS = [
'.markdown',
@@ -13,5 +26,3 @@ export const SUPPORTED_MARKDOWN_FILE_EXTENSIONS = [
// The folder name where to find the middleware
export const MIDDLEWARE_PATH_SEGMENT_NAME = 'middleware';
-
-export const ROUTE_DATA_SYMBOL = 'astro.routeData';
diff --git a/packages/astro/src/core/endpoint/index.ts b/packages/astro/src/core/endpoint/index.ts
index e8264e881..7a64366a3 100644
--- a/packages/astro/src/core/endpoint/index.ts
+++ b/packages/astro/src/core/endpoint/index.ts
@@ -1,26 +1,18 @@
import type {
APIContext,
- EndpointHandler,
Locales,
- MiddlewareHandler,
Params,
} from '../../@types/astro.js';
-import { renderEndpoint } from '../../runtime/server/index.js';
-import { ASTRO_VERSION } from '../constants.js';
-import { AstroCookies, attachCookiesToResponse } from '../cookies/index.js';
+import { ASTRO_VERSION, clientAddressSymbol, clientLocalsSymbol } from '../constants.js';
+import type { AstroCookies } from '../cookies/index.js';
import { AstroError, AstroErrorData } from '../errors/index.js';
-import { callMiddleware } from '../middleware/callMiddleware.js';
import {
computeCurrentLocale,
computePreferredLocale,
computePreferredLocaleList,
-} from '../render/context.js';
-import { type Environment, type RenderContext } from '../render/index.js';
+} from '../../i18n/utils.js';
import type { RoutingStrategies } from '../config/schema.js';
-const clientAddressSymbol = Symbol.for('astro.clientAddress');
-const clientLocalsSymbol = Symbol.for('astro.locals');
-
type CreateAPIContext = {
request: Request;
params: Params;
@@ -30,6 +22,8 @@ type CreateAPIContext = {
locales: Locales | undefined;
routingStrategy: RoutingStrategies | undefined;
defaultLocale: string | undefined;
+ route: string;
+ cookies: AstroCookies
};
/**
@@ -46,13 +40,15 @@ export function createAPIContext({
locales,
routingStrategy,
defaultLocale,
+ route,
+ cookies
}: CreateAPIContext): APIContext {
let preferredLocale: string | undefined = undefined;
let preferredLocaleList: string[] | undefined = undefined;
let currentLocale: string | undefined = undefined;
const context = {
- cookies: new AstroCookies(request),
+ cookies,
request,
params,
site: site ? new URL(site) : undefined,
@@ -93,7 +89,7 @@ export function createAPIContext({
return currentLocale;
}
if (locales) {
- currentLocale = computeCurrentLocale(request, locales, routingStrategy, defaultLocale);
+ currentLocale = computeCurrentLocale(route, locales, routingStrategy, defaultLocale);
}
return currentLocale;
@@ -138,33 +134,3 @@ export function createAPIContext({
return context;
}
-
-export async function callEndpoint(
- mod: EndpointHandler,
- env: Environment,
- ctx: RenderContext,
- onRequest: MiddlewareHandler | undefined
-): Promise<Response> {
- const context = createAPIContext({
- request: ctx.request,
- params: ctx.params,
- props: ctx.props,
- site: env.site,
- adapterName: env.adapterName,
- routingStrategy: ctx.routing,
- defaultLocale: ctx.defaultLocale,
- locales: ctx.locales,
- });
-
- let response;
- if (onRequest) {
- response = await callMiddleware(onRequest, context, async () => {
- return await renderEndpoint(mod, context, env.ssr, env.logger);
- });
- } else {
- response = await renderEndpoint(mod, context, env.ssr, env.logger);
- }
-
- attachCookiesToResponse(response, context.cookies);
- return response;
-}
diff --git a/packages/astro/src/core/middleware/callMiddleware.ts b/packages/astro/src/core/middleware/callMiddleware.ts
index 4d79cd566..0133c13d0 100644
--- a/packages/astro/src/core/middleware/callMiddleware.ts
+++ b/packages/astro/src/core/middleware/callMiddleware.ts
@@ -1,5 +1,4 @@
import type { APIContext, MiddlewareHandler, MiddlewareNext } from '../../@types/astro.js';
-import { attachCookiesToResponse, responseHasCookies } from '../cookies/index.js';
import { AstroError, AstroErrorData } from '../errors/index.js';
/**
@@ -39,10 +38,10 @@ import { AstroError, AstroErrorData } from '../errors/index.js';
export async function callMiddleware(
onRequest: MiddlewareHandler,
apiContext: APIContext,
- responseFunction: () => Promise<Response>
+ responseFunction: () => Promise<Response> | Response
): Promise<Response> {
let nextCalled = false;
- let responseFunctionPromise: Promise<Response> | undefined = undefined;
+ let responseFunctionPromise: Promise<Response> | Response | undefined = undefined;
const next: MiddlewareNext = async () => {
nextCalled = true;
responseFunctionPromise = responseFunction();
@@ -67,7 +66,7 @@ export async function callMiddleware(
if (value instanceof Response === false) {
throw new AstroError(AstroErrorData.MiddlewareNotAResponse);
}
- return ensureCookiesAttached(apiContext, value);
+ return value;
} else {
/**
* Here we handle the case where `next` was called and returned nothing.
@@ -90,14 +89,7 @@ export async function callMiddleware(
throw new AstroError(AstroErrorData.MiddlewareNotAResponse);
} else {
// Middleware did not call resolve and returned a value
- return ensureCookiesAttached(apiContext, value);
+ return value;
}
});
}
-
-function ensureCookiesAttached(apiContext: APIContext, response: Response): Response {
- if (apiContext.cookies !== undefined && !responseHasCookies(response)) {
- attachCookiesToResponse(response, apiContext.cookies);
- }
- return response;
-}
diff --git a/packages/astro/src/core/middleware/index.ts b/packages/astro/src/core/middleware/index.ts
index ffaafb3e5..b72a13f0a 100644
--- a/packages/astro/src/core/middleware/index.ts
+++ b/packages/astro/src/core/middleware/index.ts
@@ -1,6 +1,16 @@
-import type { MiddlewareHandler, Params } from '../../@types/astro.js';
-import { createAPIContext } from '../endpoint/index.js';
+import type { APIContext, MiddlewareHandler, Params } from '../../@types/astro.js';
+import { AstroCookies } from '../cookies/index.js';
import { sequence } from './sequence.js';
+import { ASTRO_VERSION } from '../constants.js';
+import { AstroError, AstroErrorData } from '../errors/index.js';
+import {
+ computeCurrentLocale,
+ computePreferredLocale,
+ computePreferredLocaleList,
+} from '../../i18n/utils.js';
+
+const clientAddressSymbol = Symbol.for('astro.clientAddress');
+const clientLocalsSymbol = Symbol.for('astro.locals');
function defineMiddleware(fn: MiddlewareHandler) {
return fn;
@@ -28,16 +38,64 @@ export type CreateContext = {
/**
* Creates a context to be passed to Astro middleware `onRequest` function.
*/
-function createContext({ request, params, userDefinedLocales = [] }: CreateContext) {
- return createAPIContext({
+function createContext({ request, params = {}, userDefinedLocales = [] }: CreateContext): APIContext {
+ let preferredLocale: string | undefined = undefined;
+ let preferredLocaleList: string[] | undefined = undefined;
+ let currentLocale: string | undefined = undefined;
+ const url = new URL(request.url);
+ const route = url.pathname
+
+ return {
+ cookies: new AstroCookies(request),
request,
- params: params ?? {},
- props: {},
+ params,
site: undefined,
- locales: userDefinedLocales,
- defaultLocale: undefined,
- routingStrategy: undefined,
- });
+ generator: `Astro v${ASTRO_VERSION}`,
+ props: {},
+ redirect(path, status) {
+ return new Response(null, {
+ status: status || 302,
+ headers: {
+ Location: path,
+ },
+ });
+ },
+ get preferredLocale(): string | undefined {
+ return preferredLocale ??= computePreferredLocale(request, userDefinedLocales);
+ },
+ get preferredLocaleList(): string[] | undefined {
+ return preferredLocaleList ??= computePreferredLocaleList(request, userDefinedLocales);
+ },
+ get currentLocale(): string | undefined {
+ return currentLocale ??= computeCurrentLocale(route, userDefinedLocales, undefined, undefined);
+ },
+ url,
+ get clientAddress() {
+ if (clientAddressSymbol in request) {
+ return Reflect.get(request, clientAddressSymbol) as string;
+ }
+ throw new AstroError(AstroErrorData.StaticClientAddressNotAvailable);
+ },
+ get locals() {
+ let locals = Reflect.get(request, clientLocalsSymbol);
+ if (locals === undefined) {
+ locals = {};
+ Reflect.set(request, clientLocalsSymbol, locals);
+ }
+ if (typeof locals !== 'object') {
+ throw new AstroError(AstroErrorData.LocalsNotAnObject);
+ }
+ return locals;
+ },
+ // We define a custom property, so we can check the value passed to locals
+ set locals(val) {
+ if (typeof val !== 'object') {
+ throw new AstroError(AstroErrorData.LocalsNotAnObject);
+ } else {
+ Reflect.set(request, clientLocalsSymbol, val);
+ }
+ },
+ };
}
/**
diff --git a/packages/astro/src/core/pipeline.ts b/packages/astro/src/core/pipeline.ts
deleted file mode 100644
index 88b8e800d..000000000
--- a/packages/astro/src/core/pipeline.ts
+++ /dev/null
@@ -1,132 +0,0 @@
-import type { ComponentInstance, EndpointHandler, MiddlewareHandler } from '../@types/astro.js';
-import { callEndpoint, createAPIContext } from './endpoint/index.js';
-import { callMiddleware } from './middleware/callMiddleware.js';
-import { renderPage } from './render/core.js';
-import { type Environment, type RenderContext } from './render/index.js';
-
-type PipelineHooks = {
- before: PipelineHookFunction[];
-};
-
-export type PipelineHookFunction = (ctx: RenderContext, mod: ComponentInstance | undefined) => void;
-
-/**
- * This is the basic class of a pipeline.
- *
- * Check the {@link ./README.md|README} for more information about the pipeline.
- */
-export class Pipeline {
- env: Environment;
- #onRequest?: MiddlewareHandler;
- #hooks: PipelineHooks = {
- before: [],
- };
-
- /**
- * When creating a pipeline, an environment is mandatory.
- * The environment won't change for the whole lifetime of the pipeline.
- */
- constructor(env: Environment) {
- this.env = env;
- }
-
- setEnvironment() {}
-
- /**
- * A middleware function that will be called before each request.
- */
- setMiddlewareFunction(onRequest: MiddlewareHandler) {
- this.#onRequest = onRequest;
- }
-
- /**
- * Removes the current middleware function. Subsequent requests won't trigger any middleware.
- */
- unsetMiddlewareFunction() {
- this.#onRequest = undefined;
- }
- /**
- * Returns the current environment
- */
- getEnvironment(): Readonly<Environment> {
- return this.env;
- }
-
- /**
- * The main function of the pipeline. Use this function to render any route known to Astro;
- */
- async renderRoute(
- renderContext: RenderContext,
- componentInstance: ComponentInstance | undefined
- ): Promise<Response> {
- for (const hook of this.#hooks.before) {
- hook(renderContext, componentInstance);
- }
- return await this.#tryRenderRoute(renderContext, this.env, componentInstance, this.#onRequest);
- }
-
- /**
- * It attempts to render a route. A route can be a:
- * - page
- * - redirect
- * - endpoint
- *
- * ## Errors
- *
- * It throws an error if the page can't be rendered.
- */
- async #tryRenderRoute(
- renderContext: Readonly<RenderContext>,
- env: Readonly<Environment>,
- mod: Readonly<ComponentInstance> | undefined,
- onRequest?: MiddlewareHandler
- ): Promise<Response> {
- const apiContext = createAPIContext({
- request: renderContext.request,
- params: renderContext.params,
- props: renderContext.props,
- site: env.site,
- adapterName: env.adapterName,
- locales: renderContext.locales,
- routingStrategy: renderContext.routing,
- defaultLocale: renderContext.defaultLocale,
- });
-
- switch (renderContext.route.type) {
- case 'page':
- case 'fallback':
- case 'redirect': {
- if (onRequest) {
- return await callMiddleware(onRequest, apiContext, () => {
- return renderPage({
- mod,
- renderContext,
- env,
- cookies: apiContext.cookies,
- });
- });
- } else {
- return await renderPage({
- mod,
- renderContext,
- env,
- cookies: apiContext.cookies,
- });
- }
- }
- case 'endpoint': {
- return await callEndpoint(mod as any as EndpointHandler, env, renderContext, onRequest);
- }
- default:
- throw new Error(`Couldn't find route of type [${renderContext.route.type}]`);
- }
- }
-
- /**
- * Store a function that will be called before starting the rendering phase.
- * @param fn
- */
- onBeforeRenderRoute(fn: PipelineHookFunction) {
- this.#hooks.before.push(fn);
- }
-}
diff --git a/packages/astro/src/core/redirects/helpers.ts b/packages/astro/src/core/redirects/helpers.ts
index e171aebe6..a55eacfdf 100644
--- a/packages/astro/src/core/redirects/helpers.ts
+++ b/packages/astro/src/core/redirects/helpers.ts
@@ -1,9 +1,4 @@
-import type {
- Params,
- RedirectRouteData,
- RouteData,
- ValidRedirectStatus,
-} from '../../@types/astro.js';
+import type { RedirectRouteData, RouteData } from '../../@types/astro.js';
export function routeIsRedirect(route: RouteData | undefined): route is RedirectRouteData {
return route?.type === 'redirect';
@@ -13,33 +8,3 @@ export function routeIsFallback(route: RouteData | undefined): route is Redirect
return route?.type === 'fallback';
}
-export function redirectRouteGenerate(redirectRoute: RouteData, data: Params): string {
- const routeData = redirectRoute.redirectRoute;
- const route = redirectRoute.redirect;
-
- if (typeof routeData !== 'undefined') {
- return routeData?.generate(data) || routeData?.pathname || '/';
- } else if (typeof route === 'string') {
- // TODO: this logic is duplicated between here and manifest/create.ts
- let target = route;
- for (const param of Object.keys(data)) {
- const paramValue = data[param]!;
- target = target.replace(`[${param}]`, paramValue);
- target = target.replace(`[...${param}]`, paramValue);
- }
- return target;
- } else if (typeof route === 'undefined') {
- return '/';
- }
- return route.destination;
-}
-
-export function redirectRouteStatus(redirectRoute: RouteData, method = 'GET'): ValidRedirectStatus {
- const routeData = redirectRoute.redirectRoute;
- if (routeData && typeof redirectRoute.redirect === 'object') {
- return redirectRoute.redirect.status;
- } else if (method !== 'GET') {
- return 308;
- }
- return 301;
-}
diff --git a/packages/astro/src/core/redirects/index.ts b/packages/astro/src/core/redirects/index.ts
index 4f705afdf..321195cbd 100644
--- a/packages/astro/src/core/redirects/index.ts
+++ b/packages/astro/src/core/redirects/index.ts
@@ -1,3 +1,4 @@
export { RedirectComponentInstance, RedirectSinglePageBuiltModule } from './component.js';
-export { redirectRouteGenerate, redirectRouteStatus, routeIsRedirect } from './helpers.js';
+export { routeIsRedirect } from './helpers.js';
export { getRedirectLocationOrThrow } from './validate.js';
+export { renderRedirect } from './render.js';
diff --git a/packages/astro/src/core/redirects/render.ts b/packages/astro/src/core/redirects/render.ts
new file mode 100644
index 000000000..08cf90850
--- /dev/null
+++ b/packages/astro/src/core/redirects/render.ts
@@ -0,0 +1,32 @@
+import type { RenderContext } from '../render-context.js';
+
+export async function renderRedirect(renderContext: RenderContext) {
+ const { request: { method }, routeData } = renderContext;
+ const { redirect, redirectRoute } = routeData;
+ const status =
+ redirectRoute && typeof redirect === "object" ? redirect.status
+ : method === "GET" ? 301
+ : 308
+ const headers = { location: redirectRouteGenerate(renderContext) };
+ return new Response(null, { status, headers });
+}
+
+function redirectRouteGenerate(renderContext: RenderContext): string {
+ const { params, routeData: { redirect, redirectRoute } } = renderContext;
+
+ if (typeof redirectRoute !== 'undefined') {
+ return redirectRoute?.generate(params) || redirectRoute?.pathname || '/';
+ } else if (typeof redirect === 'string') {
+ // TODO: this logic is duplicated between here and manifest/create.ts
+ let target = redirect;
+ for (const param of Object.keys(params)) {
+ const paramValue = params[param]!;
+ target = target.replace(`[${param}]`, paramValue);
+ target = target.replace(`[...${param}]`, paramValue);
+ }
+ return target;
+ } else if (typeof redirect === 'undefined') {
+ return '/';
+ }
+ return redirect.destination;
+}
diff --git a/packages/astro/src/core/render-context.ts b/packages/astro/src/core/render-context.ts
new file mode 100644
index 000000000..fd9ed3d1c
--- /dev/null
+++ b/packages/astro/src/core/render-context.ts
@@ -0,0 +1,148 @@
+import type { APIContext, ComponentInstance, MiddlewareHandler, RouteData } from '../@types/astro.js';
+import { renderEndpoint } from '../runtime/server/endpoint.js';
+import { attachCookiesToResponse } from './cookies/index.js';
+import { callMiddleware } from './middleware/callMiddleware.js';
+import { sequence } from './middleware/index.js';
+import { AstroCookies } from './cookies/index.js';
+import { createResult } from './render/index.js';
+import { renderPage } from '../runtime/server/index.js';
+import { ASTRO_VERSION, ROUTE_TYPE_HEADER, clientAddressSymbol, clientLocalsSymbol } from './constants.js';
+import { getParams, getProps, type Pipeline } from './render/index.js';
+import { AstroError, AstroErrorData } from './errors/index.js';
+import {
+ computeCurrentLocale,
+ computePreferredLocale,
+ computePreferredLocaleList,
+} from '../i18n/utils.js';
+import { renderRedirect } from './redirects/render.js';
+
+export class RenderContext {
+ private constructor(
+ readonly pipeline: Pipeline,
+ public locals: App.Locals,
+ readonly middleware: MiddlewareHandler,
+ readonly pathname: string,
+ readonly request: Request,
+ readonly routeData: RouteData,
+ public status: number,
+ readonly cookies = new AstroCookies(request),
+ readonly params = getParams(routeData, pathname),
+ ) {}
+
+ static create({ locals = {}, middleware, pathname, pipeline, request, routeData, status = 200 }: Pick<RenderContext, 'pathname' |'pipeline' | 'request' | 'routeData'> & Partial<Pick<RenderContext, 'locals' | 'middleware' | 'status'>>) {
+ return new RenderContext(pipeline, locals, sequence(...pipeline.internalMiddleware, middleware ?? pipeline.middleware), pathname, request, routeData, status);
+ }
+
+ /**
+ * The main function of the RenderContext.
+ *
+ * Use this function to render any route known to Astro.
+ * It attempts to render a route. A route can be a:
+ *
+ * - page
+ * - redirect
+ * - endpoint
+ * - fallback
+ */
+ async render(componentInstance: ComponentInstance | undefined): Promise<Response> {
+ const { cookies, middleware, pathname, pipeline, routeData } = this;
+ const { logger, routeCache, serverLike, streaming } = pipeline;
+ const props = await getProps({ mod: componentInstance, routeData, routeCache, pathname, logger, serverLike });
+ const apiContext = this.createAPIContext(props);
+ const { type } = routeData;
+
+ const lastNext =
+ type === 'endpoint' ? () => renderEndpoint(componentInstance as any, apiContext, serverLike, logger) :
+ type === 'redirect' ? () => renderRedirect(this) :
+ type === 'page' ? async () => {
+ const result = await this.createResult(componentInstance!);
+ const response = await renderPage(result, componentInstance?.default as any, props, {}, streaming, routeData);
+ response.headers.set(ROUTE_TYPE_HEADER, "page");
+ return response;
+ } :
+ type === 'fallback' ? () => new Response(null, { status: 500, headers: { [ROUTE_TYPE_HEADER]: "fallback" } }) :
+ () => { throw new Error("Unknown type of route: " + type) }
+
+ const response = await callMiddleware(middleware, apiContext, lastNext);
+ if (response.headers.get(ROUTE_TYPE_HEADER)) {
+ response.headers.delete(ROUTE_TYPE_HEADER)
+ }
+ // LEGACY: we put cookies on the response object,
+ // where the adapter might be expecting to read it.
+ // New code should be using `app.render({ addCookieHeader: true })` instead.
+ attachCookiesToResponse(response, cookies);
+ return response;
+ }
+
+ createAPIContext(props: APIContext['props']): APIContext {
+ const renderContext = this;
+ const { cookies, i18nData, params, pipeline, request } = this;
+ const { currentLocale, preferredLocale, preferredLocaleList } = i18nData;
+ const generator = `Astro v${ASTRO_VERSION}`;
+ const redirect = (path: string, status = 302) => new Response(null, { status, headers: { Location: path } });
+ const site = pipeline.site ? new URL(pipeline.site) : undefined;
+ const url = new URL(request.url);
+ return {
+ cookies, currentLocale, generator, params, preferredLocale, preferredLocaleList, props, redirect, request, site, url,
+ get clientAddress() {
+ if (clientAddressSymbol in request) {
+ return Reflect.get(request, clientAddressSymbol) as string;
+ }
+ if (pipeline.adapterName) {
+ throw new AstroError({
+ ...AstroErrorData.ClientAddressNotAvailable,
+ message: AstroErrorData.ClientAddressNotAvailable.message(pipeline.adapterName),
+ });
+ } else {
+ throw new AstroError(AstroErrorData.StaticClientAddressNotAvailable);
+ }
+ },
+ get locals() {
+ return renderContext.locals;
+ },
+ // TODO(breaking): disallow replacing the locals object
+ set locals(val) {
+ if (typeof val !== 'object') {
+ throw new AstroError(AstroErrorData.LocalsNotAnObject);
+ } else {
+ 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);
+ }
+ }
+ }
+ }
+
+ async createResult(mod: ComponentInstance) {
+ const { cookies, locals, params, pathname, pipeline, request, routeData, status } = this;
+ const { adapterName, clientDirectives, compressHTML, i18n, manifest, logger, renderers, resolve, site, serverLike } = pipeline;
+ const { links, scripts, styles } = await pipeline.headElements(routeData);
+ const componentMetadata = await pipeline.componentMetadata(routeData) ?? manifest.componentMetadata;
+ const { defaultLocale, locales, routing: routingStrategy } = i18n ?? {};
+ const partial = Boolean(mod.partial);
+ return createResult({ adapterName, clientDirectives, componentMetadata, compressHTML, cookies, defaultLocale, locales, locals, logger, links, params, partial, pathname, renderers, resolve, request, route: routeData.route, routingStrategy, site, scripts, ssr: serverLike, status, styles });
+ }
+
+ /**
+ * API Context may be created multiple times per request, i18n data needs to be computed only once.
+ * So, it is computed and saved here on creation of the first APIContext and reused for later ones.
+ */
+ #i18nData?: Pick<APIContext, "currentLocale" | "preferredLocale" | "preferredLocaleList">
+
+ get i18nData() {
+ if (this.#i18nData) return this.#i18nData
+ const { pipeline: { i18n }, request, routeData } = this;
+ if (!i18n) return {
+ currentLocale: undefined,
+ preferredLocale: undefined,
+ preferredLocaleList: undefined
+ }
+ const { defaultLocale, locales, routing } = i18n
+ return this.#i18nData = {
+ currentLocale: computeCurrentLocale(routeData.route, locales, routing, defaultLocale),
+ preferredLocale: computePreferredLocale(request, locales),
+ preferredLocaleList: computePreferredLocaleList(request, locales)
+ }
+ }
+}
diff --git a/packages/astro/src/core/render/core.ts b/packages/astro/src/core/render/core.ts
deleted file mode 100644
index 1175f55d7..000000000
--- a/packages/astro/src/core/render/core.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-import type { AstroCookies, ComponentInstance } from '../../@types/astro.js';
-import { renderPage as runtimeRenderPage } from '../../runtime/server/index.js';
-import { attachCookiesToResponse } from '../cookies/index.js';
-import { CantRenderPage } from '../errors/errors-data.js';
-import { AstroError } from '../errors/index.js';
-import { routeIsFallback } from '../redirects/helpers.js';
-import { redirectRouteGenerate, redirectRouteStatus, routeIsRedirect } from '../redirects/index.js';
-import type { RenderContext } from './context.js';
-import type { Environment } from './environment.js';
-import { createResult } from './result.js';
-
-export type RenderPage = {
- mod: ComponentInstance | undefined;
- renderContext: RenderContext;
- env: Environment;
- cookies: AstroCookies;
-};
-
-export async function renderPage({ mod, renderContext, env, cookies }: RenderPage) {
- if (routeIsRedirect(renderContext.route)) {
- return new Response(null, {
- status: redirectRouteStatus(renderContext.route, renderContext.request.method),
- headers: {
- location: redirectRouteGenerate(renderContext.route, renderContext.params),
- },
- });
- } else if (routeIsFallback(renderContext.route)) {
- // We return a 404 because fallback routes don't exist.
- // It's responsibility of the middleware to catch them and re-route the requests
- return new Response(null, {
- status: 404,
- });
- } else if (!mod) {
- throw new AstroError(CantRenderPage);
- }
-
- // Validate the page component before rendering the page
- const Component = mod.default;
- if (!Component)
- throw new Error(`Expected an exported Astro component but received typeof ${typeof Component}`);
-
- const result = createResult({
- adapterName: env.adapterName,
- links: renderContext.links,
- styles: renderContext.styles,
- logger: env.logger,
- params: renderContext.params,
- pathname: renderContext.pathname,
- componentMetadata: renderContext.componentMetadata,
- resolve: env.resolve,
- renderers: env.renderers,
- clientDirectives: env.clientDirectives,
- compressHTML: env.compressHTML,
- request: renderContext.request,
- partial: !!mod.partial,
- site: env.site,
- scripts: renderContext.scripts,
- ssr: env.ssr,
- status: renderContext.status ?? 200,
- cookies,
- locals: renderContext.locals ?? {},
- locales: renderContext.locales,
- defaultLocale: renderContext.defaultLocale,
- routingStrategy: renderContext.routing,
- });
-
- const response = await runtimeRenderPage(
- result,
- Component,
- renderContext.props,
- {},
- env.streaming,
- renderContext.route
- );
-
- // If there is an Astro.cookies instance, attach it to the response so that
- // adapters can grab the Set-Cookie headers.
- if (result.cookies) {
- attachCookiesToResponse(response, result.cookies);
- }
-
- return response;
-}
diff --git a/packages/astro/src/core/render/environment.ts b/packages/astro/src/core/render/environment.ts
deleted file mode 100644
index 582ee6129..000000000
--- a/packages/astro/src/core/render/environment.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-import type { RuntimeMode, SSRLoadedRenderer } from '../../@types/astro.js';
-import type { Logger } from '../logger/core.js';
-import type { RouteCache } from './route-cache.js';
-
-/**
- * An environment represents the static parts of rendering that do not change
- * between requests. These are mostly known when the server first starts up and do not change.
- * Thus, they can be created once and passed through to renderPage on each request.
- */
-export interface Environment {
- /**
- * Used to provide better error messages for `Astro.clientAddress`
- */
- adapterName?: string;
- /** logging options */
- logger: Logger;
- /** "development" or "production" */
- mode: RuntimeMode;
- compressHTML: boolean;
- renderers: SSRLoadedRenderer[];
- clientDirectives: Map<string, string>;
- resolve: (s: string) => Promise<string>;
- routeCache: RouteCache;
- /**
- * Used for `Astro.site`
- */
- site?: string;
- /**
- * Value of Astro config's `output` option, true if "server" or "hybrid"
- */
- ssr: boolean;
- streaming: boolean;
-}
-
-export type CreateEnvironmentArgs = Environment;
-
-export function createEnvironment(options: CreateEnvironmentArgs): Environment {
- return options;
-}
diff --git a/packages/astro/src/core/render/index.ts b/packages/astro/src/core/render/index.ts
index 5f3a702a3..266abf696 100644
--- a/packages/astro/src/core/render/index.ts
+++ b/packages/astro/src/core/render/index.ts
@@ -1,16 +1,13 @@
-import type { AstroMiddlewareInstance, ComponentInstance, RouteData } from '../../@types/astro.js';
-import type { Environment } from './environment.js';
-export { computePreferredLocale, createRenderContext } from './context.js';
-export type { RenderContext } from './context.js';
-export { createEnvironment } from './environment.js';
-export { getParamsAndProps } from './params-and-props.js';
+import type { ComponentInstance, RouteData } from '../../@types/astro.js';
+import type { Pipeline } from '../base-pipeline.js';
+export { Pipeline } from '../base-pipeline.js';
+export { getParams, getProps } from './params-and-props.js';
export { loadRenderer } from './renderer.js';
-
-export type { Environment };
+export { createResult } from './result.js';
export interface SSROptions {
- /** The environment instance */
- env: Environment;
+ /** The pipeline instance */
+ pipeline: Pipeline;
/** location of file on disk */
filePath: URL;
/** the web request (needed for dynamic routes) */
@@ -21,8 +18,4 @@ export interface SSROptions {
request: Request;
/** optional, in case we need to render something outside a dev server */
route: RouteData;
- /**
- * Optional middlewares
- */
- middleware?: AstroMiddlewareInstance;
}
diff --git a/packages/astro/src/core/render/params-and-props.ts b/packages/astro/src/core/render/params-and-props.ts
index 3532c5f83..ff901cd84 100644
--- a/packages/astro/src/core/render/params-and-props.ts
+++ b/packages/astro/src/core/render/params-and-props.ts
@@ -3,35 +3,34 @@ import { AstroError, AstroErrorData } from '../errors/index.js';
import type { Logger } from '../logger/core.js';
import { routeIsFallback } from '../redirects/helpers.js';
import { routeIsRedirect } from '../redirects/index.js';
-import { getParams } from '../routing/params.js';
import type { RouteCache } from './route-cache.js';
import { callGetStaticPaths, findPathItemByKey } from './route-cache.js';
interface GetParamsAndPropsOptions {
mod: ComponentInstance | undefined;
- route?: RouteData | undefined;
+ routeData?: RouteData | undefined;
routeCache: RouteCache;
pathname: string;
logger: Logger;
- ssr: boolean;
+ serverLike: boolean;
}
-export async function getParamsAndProps(opts: GetParamsAndPropsOptions): Promise<[Params, Props]> {
- const { logger, mod, route, routeCache, pathname, ssr } = opts;
+export async function getProps(opts: GetParamsAndPropsOptions): Promise<Props> {
+ const { logger, mod, routeData: route, routeCache, pathname, serverLike } = opts;
// If there's no route, or if there's a pathname (e.g. a static `src/pages/normal.astro` file),
// then we know for sure they don't have params and props, return a fallback value.
if (!route || route.pathname) {
- return [{}, {}];
+ return {};
}
- // This is a dynamic route, start getting the params
- const params = getRouteParams(route, pathname) ?? {};
-
+
if (routeIsRedirect(route) || routeIsFallback(route)) {
- return [params, {}];
+ return {};
}
-
+
+ // This is a dynamic route, start getting the params
+ const params = getParams(route, pathname);
if (mod) {
validatePrerenderEndpointCollision(route, mod, params);
}
@@ -43,11 +42,11 @@ export async function getParamsAndProps(opts: GetParamsAndPropsOptions): Promise
route,
routeCache,
logger,
- ssr,
+ ssr: serverLike,
});
const matchedStaticPath = findPathItemByKey(staticPaths, params, route, logger);
- if (!matchedStaticPath && (ssr ? route.prerender : true)) {
+ if (!matchedStaticPath && (serverLike ? route.prerender : true)) {
throw new AstroError({
...AstroErrorData.NoMatchingStaticPathFound,
message: AstroErrorData.NoMatchingStaticPathFound.message(pathname),
@@ -57,18 +56,28 @@ export async function getParamsAndProps(opts: GetParamsAndPropsOptions): Promise
const props: Props = matchedStaticPath?.props ? { ...matchedStaticPath.props } : {};
- return [params, props];
+ return props;
}
-function getRouteParams(route: RouteData, pathname: string): Params | undefined {
- if (route.params.length) {
- // The RegExp pattern expects a decoded string, but the pathname is encoded
- // when the URL contains non-English characters.
- const paramsMatch = route.pattern.exec(decodeURIComponent(pathname));
- if (paramsMatch) {
- return getParams(route.params)(paramsMatch);
+/**
+ * When given a route with the pattern `/[x]/[y]/[z]/svelte`, and a pathname `/a/b/c/svelte`,
+ * returns the params object: { x: "a", y: "b", z: "c" }.
+ */
+export function getParams(route: RouteData, pathname: string): Params {
+ if (!route.params.length) return {};
+ // The RegExp pattern expects a decoded string, but the pathname is encoded
+ // when the URL contains non-English characters.
+ const paramsMatch = route.pattern.exec(decodeURIComponent(pathname));
+ if (!paramsMatch) return {};
+ const params: Params = {};
+ route.params.forEach((key, i) => {
+ if (key.startsWith('...')) {
+ params[key.slice(3)] = paramsMatch[i + 1] ? paramsMatch[i + 1] : undefined;
+ } else {
+ params[key] = paramsMatch[i + 1];
}
- }
+ });
+ return params;
}
/**
diff --git a/packages/astro/src/core/render/result.ts b/packages/astro/src/core/render/result.ts
index 5faa6442c..6c1314f3c 100644
--- a/packages/astro/src/core/render/result.ts
+++ b/packages/astro/src/core/render/result.ts
@@ -17,11 +17,9 @@ import {
computeCurrentLocale,
computePreferredLocale,
computePreferredLocaleList,
-} from './context.js';
+} from '../../i18n/utils.js';
import type { RoutingStrategies } from '../config/schema.js';
-
-const clientAddressSymbol = Symbol.for('astro.clientAddress');
-const responseSentSymbol = Symbol.for('astro.responseSent');
+import { clientAddressSymbol, responseSentSymbol } from '../constants.js';
export interface CreateResultArgs {
/**
@@ -44,16 +42,17 @@ export interface CreateResultArgs {
* Used for `Astro.site`
*/
site: string | undefined;
- links?: Set<SSRElement>;
- scripts?: Set<SSRElement>;
- styles?: Set<SSRElement>;
- componentMetadata?: SSRResult['componentMetadata'];
+ links: Set<SSRElement>;
+ scripts: Set<SSRElement>;
+ styles: Set<SSRElement>;
+ componentMetadata: SSRResult['componentMetadata'];
request: Request;
status: number;
locals: App.Locals;
- cookies?: AstroCookies;
+ cookies: AstroCookies;
locales: Locales | undefined;
defaultLocale: string | undefined;
+ route: string;
routingStrategy: RoutingStrategies | undefined;
}
@@ -233,7 +232,7 @@ export function createResult(args: CreateResultArgs): SSRResult {
}
if (args.locales) {
currentLocale = computeCurrentLocale(
- request,
+ url.pathname,
args.locales,
args.routingStrategy,
args.defaultLocale
diff --git a/packages/astro/src/core/routing/index.ts b/packages/astro/src/core/routing/index.ts
index b568bb121..3ddc559ad 100644
--- a/packages/astro/src/core/routing/index.ts
+++ b/packages/astro/src/core/routing/index.ts
@@ -1,5 +1,4 @@
export { createRouteManifest } from './manifest/create.js';
export { deserializeRouteData, serializeRouteData } from './manifest/serialization.js';
export { matchAllRoutes, matchRoute } from './match.js';
-export { getParams } from './params.js';
export { validateDynamicRouteModule, validateGetStaticPathsResult } from './validation.js';
diff --git a/packages/astro/src/core/routing/params.ts b/packages/astro/src/core/routing/params.ts
index 973f7f2b5..de2a8e979 100644
--- a/packages/astro/src/core/routing/params.ts
+++ b/packages/astro/src/core/routing/params.ts
@@ -3,27 +3,6 @@ import { trimSlashes } from '../path.js';
import { validateGetStaticPathsParameter } from './validation.js';
/**
- * given an array of params like `['x', 'y', 'z']` for
- * src/routes/[x]/[y]/[z]/svelte, create a function
- * that turns a RegExpExecArray into ({ x, y, z })
- */
-export function getParams(array: string[]) {
- const fn = (match: RegExpExecArray) => {
- const params: Params = {};
- array.forEach((key, i) => {
- if (key.startsWith('...')) {
- params[key.slice(3)] = match[i + 1] ? match[i + 1] : undefined;
- } else {
- params[key] = match[i + 1];
- }
- });
- return params;
- };
-
- return fn;
-}
-
-/**
* given a route's Params object, validate parameter
* values and create a stringified key for the route
* that can be used to match request routes
diff --git a/packages/astro/src/i18n/middleware.ts b/packages/astro/src/i18n/middleware.ts
index 73a43d471..91091cbec 100644
--- a/packages/astro/src/i18n/middleware.ts
+++ b/packages/astro/src/i18n/middleware.ts
@@ -1,18 +1,9 @@
import { appendForwardSlash, joinPaths } from '@astrojs/internal-helpers/path';
-import type {
- APIContext,
- Locales,
- MiddlewareHandler,
- RouteData,
- SSRManifest,
-} from '../@types/astro.js';
-import type { PipelineHookFunction } from '../core/pipeline.js';
+import type { APIContext, Locales, MiddlewareHandler, SSRManifest } from '../@types/astro.js';
import { getPathByLocale, normalizeTheLocale } from './index.js';
import { shouldAppendForwardSlash } from '../core/build/util.js';
-import { ROUTE_DATA_SYMBOL } from '../core/constants.js';
-import type { SSRManifestI18n } from '../core/app/types.js';
-
-const routeDataSymbol = Symbol.for(ROUTE_DATA_SYMBOL);
+import type { SSRManifestI18n } from '../core/app/types.js'
+import { ROUTE_TYPE_HEADER } from '../core/constants.js';
// Checks if the pathname has any locale, exception for the defaultLocale, which is ignored on purpose.
function pathnameHasLocale(pathname: string, locales: Locales): boolean {
@@ -107,18 +98,16 @@ export function createI18nMiddleware(
};
return async (context, next) => {
- const routeData: RouteData | undefined = Reflect.get(context.request, routeDataSymbol);
+ const response = await next();
+ const type = response.headers.get(ROUTE_TYPE_HEADER);
// If the route we're processing is not a page, then we ignore it
- if (routeData?.type !== 'page' && routeData?.type !== 'fallback') {
- return await next();
+ if (type !== 'page' && type !== 'fallback') {
+ return response
}
- const currentLocale = context.currentLocale;
- const url = context.url;
+ const { url, currentLocale } = context;
const { locales, defaultLocale, fallback, routing } = i18n;
- const response = await next();
- if (response instanceof Response) {
switch (i18n.routing) {
case 'domains-prefix-other-locales': {
if (localeHasntDomain(i18n, currentLocale)) {
@@ -207,20 +196,12 @@ export function createI18nMiddleware(
return context.redirect(newPathname);
}
}
- }
return response;
};
}
/**
- * This pipeline hook attaches a `RouteData` object to the `Request`
- */
-export const i18nPipelineHook: PipelineHookFunction = (ctx) => {
- Reflect.set(ctx.request, routeDataSymbol, ctx.route);
-};
-
-/**
* Checks if the current locale doesn't belong to a configured domain
* @param i18n
* @param currentLocale
diff --git a/packages/astro/src/core/render/context.ts b/packages/astro/src/i18n/utils.ts
index 8511942f3..4cfec633b 100644
--- a/packages/astro/src/core/render/context.ts
+++ b/packages/astro/src/i18n/utils.ts
@@ -1,92 +1,6 @@
-import type {
- ComponentInstance,
- Locales,
- Params,
- Props,
- RouteData,
- SSRElement,
- SSRResult,
-} from '../../@types/astro.js';
-import { normalizeTheLocale, toCodes } from '../../i18n/index.js';
-import { AstroError, AstroErrorData } from '../errors/index.js';
-import type { Environment } from './environment.js';
-import { getParamsAndProps } from './params-and-props.js';
-import type { RoutingStrategies } from '../config/schema.js';
-import { ROUTE_DATA_SYMBOL } from '../constants.js';
-
-const clientLocalsSymbol = Symbol.for('astro.locals');
-const routeDataSymbol = Symbol.for(ROUTE_DATA_SYMBOL);
-
-/**
- * The RenderContext represents the parts of rendering that are specific to one request.
- */
-export interface RenderContext {
- request: Request;
- pathname: string;
- scripts?: Set<SSRElement>;
- links?: Set<SSRElement>;
- styles?: Set<SSRElement>;
- componentMetadata?: SSRResult['componentMetadata'];
- route: RouteData;
- status?: number;
- params: Params;
- props: Props;
- locals?: object;
- locales: Locales | undefined;
- defaultLocale: string | undefined;
- routing: RoutingStrategies | undefined;
-}
-
-export type CreateRenderContextArgs = Partial<
- Omit<RenderContext, 'params' | 'props' | 'locals'>
-> & {
- route: RouteData;
- request: RenderContext['request'];
- mod: ComponentInstance | undefined;
- env: Environment;
-};
-
-export async function createRenderContext(
- options: CreateRenderContextArgs
-): Promise<RenderContext> {
- const request = options.request;
- const pathname = options.pathname ?? new URL(request.url).pathname;
- const [params, props] = await getParamsAndProps({
- mod: options.mod as any,
- route: options.route,
- routeCache: options.env.routeCache,
- pathname: pathname,
- logger: options.env.logger,
- ssr: options.env.ssr,
- });
-
- const context: RenderContext = {
- ...options,
- pathname,
- params,
- props,
- locales: options.locales,
- routing: options.routing,
- defaultLocale: options.defaultLocale,
- };
-
- // We define a custom property, so we can check the value passed to locals
- Object.defineProperty(context, 'locals', {
- enumerable: true,
- get() {
- return Reflect.get(request, clientLocalsSymbol);
- },
- set(val) {
- if (typeof val !== 'object') {
- throw new AstroError(AstroErrorData.LocalsNotAnObject);
- } else {
- Reflect.set(request, clientLocalsSymbol, val);
- }
- },
- });
-
- return context;
-}
+import type { Locales } from '../@types/astro.js';
+import { normalizeTheLocale, toCodes } from './index.js';
+import type { RoutingStrategies } from '../core/config/schema.js';
type BrowserLocale = {
locale: string;
@@ -240,19 +154,12 @@ export function computePreferredLocaleList(request: Request, locales: Locales):
}
export function computeCurrentLocale(
- request: Request,
+ pathname: string,
locales: Locales,
routingStrategy: RoutingStrategies | undefined,
defaultLocale: string | undefined
): undefined | string {
- const routeData: RouteData | undefined = Reflect.get(request, routeDataSymbol);
- if (!routeData) {
- return defaultLocale;
- }
- // Typically, RouteData::pathname has the correct information in SSR, but it's not available in SSG, so we fall back
- // to use the pathname from the Request
- const pathname = routeData.pathname ?? new URL(request.url).pathname;
- for (const segment of pathname.split('/').filter(Boolean)) {
+ for (const segment of pathname.split('/')) {
for (const locale of locales) {
if (typeof locale === 'string') {
// we skip ta locale that isn't present in the current segment
diff --git a/packages/astro/src/prerender/routing.ts b/packages/astro/src/prerender/routing.ts
index d8250b98f..8ecf20a5f 100644
--- a/packages/astro/src/prerender/routing.ts
+++ b/packages/astro/src/prerender/routing.ts
@@ -1,7 +1,6 @@
import type { AstroSettings, ComponentInstance, RouteData } from '../@types/astro.js';
import { RedirectComponentInstance, routeIsRedirect } from '../core/redirects/index.js';
-import type DevPipeline from '../vite-plugin-astro-server/devPipeline.js';
-import { preload } from '../vite-plugin-astro-server/index.js';
+import type { DevPipeline } from '../vite-plugin-astro-server/pipeline.js';
import { getPrerenderStatus } from './metadata.js';
type GetSortedPreloadedMatchesParams = {
@@ -52,12 +51,12 @@ async function preloadAndSetPrerenderStatus({
continue;
}
- const preloadedComponent = await preload({ pipeline, filePath });
+ const preloadedComponent = await pipeline.preload(filePath);
// gets the prerender metadata set by the `astro:scanner` vite plugin
const prerenderStatus = getPrerenderStatus({
filePath,
- loader: pipeline.getModuleLoader(),
+ loader: pipeline.loader,
});
if (prerenderStatus !== undefined) {
diff --git a/packages/astro/src/runtime/server/consts.ts b/packages/astro/src/runtime/server/consts.ts
deleted file mode 100644
index d8d7ccb82..000000000
--- a/packages/astro/src/runtime/server/consts.ts
+++ /dev/null
@@ -1 +0,0 @@
-export const REROUTE_DIRECTIVE_HEADER = 'X-Astro-Reroute';
diff --git a/packages/astro/src/runtime/server/endpoint.ts b/packages/astro/src/runtime/server/endpoint.ts
index 9b5f3e40e..2afee2f23 100644
--- a/packages/astro/src/runtime/server/endpoint.ts
+++ b/packages/astro/src/runtime/server/endpoint.ts
@@ -1,5 +1,5 @@
import { bold } from 'kleur/colors';
-import { REROUTE_DIRECTIVE_HEADER } from './consts.js';
+import { REROUTABLE_STATUS_CODES, REROUTE_DIRECTIVE_HEADER } from '../../core/constants.js';;
import type { APIContext, EndpointHandler } from '../../@types/astro.js';
import type { Logger } from '../../core/logger/core.js';
@@ -51,7 +51,7 @@ export async function renderEndpoint(
const response = await handler.call(mod, context);
// Endpoints explicitly returning 404 or 500 response status should
// NOT be subject to rerouting to 404.astro or 500.astro.
- if (response.status === 404 || response.status === 500) {
+ if (REROUTABLE_STATUS_CODES.includes(response.status)) {
// Only `Response.redirect` headers are immutable, therefore a `try..catch` is not necessary.
// Note: `Response.redirect` can only be called with HTTP status codes: 301, 302, 303, 307, 308.
// Source: https://developer.mozilla.org/en-US/docs/Web/API/Response/redirect_static#parameters
diff --git a/packages/astro/src/vite-plugin-astro-server/devPipeline.ts b/packages/astro/src/vite-plugin-astro-server/devPipeline.ts
deleted file mode 100644
index 409851eaf..000000000
--- a/packages/astro/src/vite-plugin-astro-server/devPipeline.ts
+++ /dev/null
@@ -1,91 +0,0 @@
-import type {
- AstroConfig,
- AstroSettings,
- RuntimeMode,
- SSRLoadedRenderer,
- SSRManifest,
-} from '../@types/astro.js';
-import type { Logger } from '../core/logger/core.js';
-import type { ModuleLoader } from '../core/module-loader/index.js';
-import { Pipeline } from '../core/pipeline.js';
-import type { Environment } from '../core/render/index.js';
-import { createEnvironment, loadRenderer } from '../core/render/index.js';
-import { RouteCache } from '../core/render/route-cache.js';
-import { isServerLikeOutput } from '../prerender/utils.js';
-import { createResolve } from './resolve.js';
-
-export default class DevPipeline extends Pipeline {
- #settings: AstroSettings;
- #loader: ModuleLoader;
- #devLogger: Logger;
-
- constructor({
- manifest,
- logger,
- settings,
- loader,
- }: {
- manifest: SSRManifest;
- logger: Logger;
- settings: AstroSettings;
- loader: ModuleLoader;
- }) {
- const env = DevPipeline.createDevelopmentEnvironment(manifest, settings, logger, loader);
- super(env);
- this.#devLogger = logger;
- this.#settings = settings;
- this.#loader = loader;
- }
-
- clearRouteCache() {
- this.env.routeCache.clearAll();
- }
-
- getSettings(): Readonly<AstroSettings> {
- return this.#settings;
- }
-
- getConfig(): Readonly<AstroConfig> {
- return this.#settings.config;
- }
-
- getModuleLoader(): Readonly<ModuleLoader> {
- return this.#loader;
- }
-
- get logger(): Readonly<Logger> {
- return this.#devLogger;
- }
-
- async loadRenderers() {
- const renderers = await Promise.all(
- this.#settings.renderers.map((r) => loadRenderer(r, this.#loader))
- );
- this.env.renderers = renderers.filter(Boolean) as SSRLoadedRenderer[];
- }
-
- static createDevelopmentEnvironment(
- manifest: SSRManifest,
- settings: AstroSettings,
- logger: Logger,
- loader: ModuleLoader
- ): Environment {
- const mode: RuntimeMode = 'development';
- return createEnvironment({
- adapterName: manifest.adapterName,
- logger,
- mode,
- // This will be overridden in the dev server
- renderers: [],
- clientDirectives: manifest.clientDirectives,
- compressHTML: manifest.compressHTML,
- resolve: createResolve(loader, settings.config.root),
- routeCache: new RouteCache(logger, mode),
- site: manifest.site,
- ssr: isServerLikeOutput(settings.config),
- streaming: true,
- });
- }
-
- async handleFallback() {}
-}
diff --git a/packages/astro/src/vite-plugin-astro-server/error.ts b/packages/astro/src/vite-plugin-astro-server/error.ts
index 7c478fc6e..3bfd9f5f9 100644
--- a/packages/astro/src/vite-plugin-astro-server/error.ts
+++ b/packages/astro/src/vite-plugin-astro-server/error.ts
@@ -1,6 +1,6 @@
import type { ModuleLoader } from '../core/module-loader/index.js';
import type { AstroConfig } from '../@types/astro.js';
-import type DevPipeline from './devPipeline.js';
+import type { DevPipeline } from './pipeline.js';
import { collectErrorMetadata } from '../core/errors/dev/index.js';
import { createSafeError, AstroErrorData } from '../core/errors/index.js';
@@ -10,7 +10,7 @@ import { eventError, telemetry } from '../events/index.js';
export function recordServerError(
loader: ModuleLoader,
config: AstroConfig,
- pipeline: DevPipeline,
+ { logger }: DevPipeline,
_err: unknown
) {
const err = createSafeError(_err);
@@ -29,9 +29,9 @@ export function recordServerError(
telemetry.record(eventError({ cmd: 'dev', err: errorWithMetadata, isFatal: false }));
}
- pipeline.logger.error(
+ logger.error(
null,
- formatErrorMessage(errorWithMetadata, pipeline.logger.level() === 'debug')
+ formatErrorMessage(errorWithMetadata, logger.level() === 'debug')
);
return {
diff --git a/packages/astro/src/vite-plugin-astro-server/index.ts b/packages/astro/src/vite-plugin-astro-server/index.ts
index 97592d47a..14172e8ae 100644
--- a/packages/astro/src/vite-plugin-astro-server/index.ts
+++ b/packages/astro/src/vite-plugin-astro-server/index.ts
@@ -1,34 +1,3 @@
-import type { ComponentInstance } from '../@types/astro.js';
-import { enhanceViteSSRError } from '../core/errors/dev/index.js';
-import { AggregateError, CSSError, MarkdownError } from '../core/errors/index.js';
-import { viteID } from '../core/util.js';
-import type DevPipeline from './devPipeline.js';
-
-export async function preload({
- pipeline,
- filePath,
-}: {
- pipeline: DevPipeline;
- filePath: URL;
-}): Promise<ComponentInstance> {
- // Important: This needs to happen first, in case a renderer provides polyfills.
- await pipeline.loadRenderers();
-
- try {
- // Load the module from the Vite SSR Runtime.
- const mod = (await pipeline.getModuleLoader().import(viteID(filePath))) as ComponentInstance;
-
- return mod;
- } 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)) {
- throw error;
- }
-
- throw enhanceViteSSRError({ error, filePath, loader: pipeline.getModuleLoader() });
- }
-}
-
export { createController, runWithErrorHandling } from './controller.js';
export { default as vitePluginAstroServer } from './plugin.js';
export { handleRequest } from './request.js';
diff --git a/packages/astro/src/vite-plugin-astro-server/pipeline.ts b/packages/astro/src/vite-plugin-astro-server/pipeline.ts
new file mode 100644
index 000000000..f2a6a1712
--- /dev/null
+++ b/packages/astro/src/vite-plugin-astro-server/pipeline.ts
@@ -0,0 +1,137 @@
+import url from 'node:url'
+import type { AstroSettings, ComponentInstance, DevToolbarMetadata, RouteData, SSRElement, SSRLoadedRenderer, SSRManifest } from '../@types/astro.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 { isPage, resolveIdToUrl, viteID } from '../core/util.js';
+import { isServerLikeOutput } from '../prerender/utils.js';
+import { createResolve } from './resolve.js';
+import { AggregateError, CSSError, MarkdownError } from '../core/errors/index.js';
+import { enhanceViteSSRError } from '../core/errors/dev/index.js';
+import type { HeadElements } from '../core/base-pipeline.js';
+import { getScriptsForURL } from './scripts.js';
+import { ASTRO_VERSION } from '../core/constants.js';
+import { getInfoOutput } from '../cli/info/index.js';
+import { PAGE_SCRIPT_ID } from '../vite-plugin-scripts/index.js';
+import { getStylesForURL } from './css.js';
+import { getComponentMetadata } from './metadata.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<SSRLoadedRenderer>
+
+ private constructor(
+ readonly loader: ModuleLoader,
+ readonly logger: Logger,
+ readonly manifest: SSRManifest,
+ readonly settings: AstroSettings,
+ readonly config = settings.config,
+ ) {
+ const mode = 'development'
+ const resolve = createResolve(loader, config.root);
+ const serverLike = isServerLikeOutput(config);
+ const streaming = true;
+ super(logger, manifest, mode, [], resolve, serverLike, streaming);
+ }
+
+ static create({ loader, logger, manifest, settings }: Pick<DevPipeline, 'loader' | 'logger' | 'manifest' | 'settings'>) {
+ return new DevPipeline(loader, logger, manifest, settings)
+ }
+
+ async headElements(routeData: RouteData): Promise<HeadElements> {
+ const { config: { root }, loader, mode, settings } = this;
+ const filePath = new URL(`./${routeData.component}`, root);
+ const { scripts } = await getScriptsForURL(filePath, root, loader);
+
+ // Inject HMR scripts
+ 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: DevToolbarMetadata['__astro_dev_toolbar__'] = {
+ root: url.fileURLToPath(settings.config.root),
+ version: ASTRO_VERSION,
+ debugInfo: await getInfoOutput({ userConfig: settings.config, print: false }),
+ };
+
+ // Additional data for the dev overlay
+ const children = `window.__astro_dev_toolbar__ = ${JSON.stringify(additionalMetadata)}`;
+ scripts.add({ props: {}, children });
+ }
+ }
+
+ // TODO: We should allow adding generic HTML elements to the head, not just scripts
+ 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: '',
+ });
+ }
+ }
+
+ // Pass framework CSS in as style tags to be appended to the page.
+ const links = new Set<SSRElement>();
+ const { urls, styles: _styles } = await getStylesForURL(filePath, loader);
+ for (const href of urls) {
+ links.add({ props: { rel: 'stylesheet', href }, children: '' });
+ }
+
+ const styles = new Set<SSRElement>();
+ for (const { id, url: src, content } of _styles) {
+ // Vite handles HMR for styles injected as scripts
+ scripts.add({ props: { type: 'module', src }, children: '' });
+ // But we still want to inject the styles to avoid FOUC. The style tags
+ // should emulate what Vite injects so further HMR works as expected.
+ styles.add({ props: { 'data-vite-dev-id': id }, children: content });
+ };
+
+ return { scripts, styles, links }
+ }
+
+ componentMetadata(routeData: RouteData) {
+ const { config: { root }, loader } = this;
+ const filePath = new URL(`./${routeData.component}`, root);
+ return getComponentMetadata(filePath, loader)
+ }
+
+ async preload(filePath: URL) {
+ const { loader } = this;
+
+ // Important: This needs to happen first, in case a renderer provides polyfills.
+ const renderers__ = this.settings.renderers.map((r) => loadRenderer(r, loader));
+ const renderers_ = await Promise.all(renderers__);
+ this.renderers = renderers_.filter((r): r is SSRLoadedRenderer => Boolean(r));
+
+ try {
+ // Load the module from the Vite SSR Runtime.
+ return await loader.import(viteID(filePath)) as 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)) {
+ throw error;
+ }
+
+ throw enhanceViteSSRError({ error, filePath, loader });
+ }
+ }
+
+ clearRouteCache() {
+ this.routeCache.clearAll();
+ }
+}
diff --git a/packages/astro/src/vite-plugin-astro-server/plugin.ts b/packages/astro/src/vite-plugin-astro-server/plugin.ts
index ba33c3ebd..e149acad0 100644
--- a/packages/astro/src/vite-plugin-astro-server/plugin.ts
+++ b/packages/astro/src/vite-plugin-astro-server/plugin.ts
@@ -8,7 +8,7 @@ import { createViteLoader } from '../core/module-loader/index.js';
import { createRouteManifest } from '../core/routing/index.js';
import { baseMiddleware } from './base.js';
import { createController } from './controller.js';
-import DevPipeline from './devPipeline.js';
+import { DevPipeline } from './pipeline.js';
import { handleRequest } from './request.js';
import { AstroError, AstroErrorData } from '../core/errors/index.js';
import { getViteErrorPayload } from '../core/errors/dev/index.js';
@@ -33,7 +33,7 @@ export default function createVitePluginAstroServer({
configureServer(viteServer) {
const loader = createViteLoader(viteServer);
const manifest = createDevelopmentManifest(settings);
- const pipeline = new DevPipeline({ logger, manifest, settings, loader });
+ const pipeline = DevPipeline.create({ loader, logger, manifest, settings });
let manifestData: ManifestData = createRouteManifest({ settings, fsMod }, logger);
const controller = createController({ loader });
const localStorage = new AsyncLocalStorage();
@@ -90,7 +90,6 @@ export default function createVitePluginAstroServer({
controller,
incomingRequest: request,
incomingResponse: response,
- manifest,
});
});
});
diff --git a/packages/astro/src/vite-plugin-astro-server/request.ts b/packages/astro/src/vite-plugin-astro-server/request.ts
index 29ceafa0c..f86609c8c 100644
--- a/packages/astro/src/vite-plugin-astro-server/request.ts
+++ b/packages/astro/src/vite-plugin-astro-server/request.ts
@@ -1,10 +1,10 @@
import type http from 'node:http';
-import type { ManifestData, SSRManifest } from '../@types/astro.js';
+import type { ManifestData } from '../@types/astro.js';
import { collapseDuplicateSlashes, removeTrailingForwardSlash } from '../core/path.js';
import { isServerLikeOutput } from '../prerender/utils.js';
import type { DevServerController } from './controller.js';
import { runWithErrorHandling } from './controller.js';
-import type DevPipeline from './devPipeline.js';
+import type { DevPipeline } from './pipeline.js';
import { handle500Response } from './response.js';
import { handleRoute, matchRoute } from './route.js';
import { recordServerError } from './error.js';
@@ -15,7 +15,6 @@ type HandleRequest = {
controller: DevServerController;
incomingRequest: http.IncomingMessage;
incomingResponse: http.ServerResponse;
- manifest: SSRManifest;
};
/** The main logic to route dev server requests to pages in Astro. */
@@ -25,11 +24,9 @@ export async function handleRequest({
controller,
incomingRequest,
incomingResponse,
- manifest,
}: HandleRequest) {
- const config = pipeline.getConfig();
- const moduleLoader = pipeline.getModuleLoader();
- const origin = `${moduleLoader.isHttps() ? 'https' : 'http'}://${incomingRequest.headers.host}`;
+ const { config, loader } = pipeline;
+ const origin = `${loader.isHttps() ? 'https' : 'http'}://${incomingRequest.headers.host}`;
const buildingToSSR = isServerLikeOutput(config);
const url = new URL(collapseDuplicateSlashes(origin + incomingRequest.url));
@@ -82,12 +79,11 @@ export async function handleRequest({
manifestData,
incomingRequest: incomingRequest,
incomingResponse: incomingResponse,
- manifest,
});
},
onError(_err) {
- const { error, errorWithMetadata } = recordServerError(moduleLoader, config, pipeline, _err);
- handle500Response(moduleLoader, incomingResponse, errorWithMetadata);
+ const { error, errorWithMetadata } = recordServerError(loader, config, pipeline, _err);
+ handle500Response(loader, incomingResponse, errorWithMetadata);
return error;
},
});
diff --git a/packages/astro/src/vite-plugin-astro-server/route.ts b/packages/astro/src/vite-plugin-astro-server/route.ts
index 0cc8a8193..ff76d4556 100644
--- a/packages/astro/src/vite-plugin-astro-server/route.ts
+++ b/packages/astro/src/vite-plugin-astro-server/route.ts
@@ -1,43 +1,22 @@
import type http from 'node:http';
-import { fileURLToPath } from 'node:url';
import type {
ComponentInstance,
- DevToolbarMetadata,
ManifestData,
- MiddlewareHandler,
RouteData,
- SSRElement,
- SSRManifest,
} from '../@types/astro.js';
-import { getInfoOutput } from '../cli/info/index.js';
-import { ASTRO_VERSION } from '../core/constants.js';
import { AstroErrorData, isAstroError } from '../core/errors/index.js';
import { req } from '../core/messages.js';
-import { sequence } from '../core/middleware/index.js';
import { loadMiddleware } from '../core/middleware/loadMiddleware.js';
-import {
- createRenderContext,
- getParamsAndProps,
- type RenderContext,
- type SSROptions,
-} from '../core/render/index.js';
+import { getProps, type SSROptions } from '../core/render/index.js';
import { createRequest } from '../core/request.js';
import { matchAllRoutes } from '../core/routing/index.js';
-import { isPage, resolveIdToUrl } from '../core/util.js';
import { normalizeTheLocale } from '../i18n/index.js';
-import { createI18nMiddleware, i18nPipelineHook } from '../i18n/middleware.js';
import { getSortedPreloadedMatches } from '../prerender/routing.js';
import { isServerLikeOutput } from '../prerender/utils.js';
-import { PAGE_SCRIPT_ID } from '../vite-plugin-scripts/index.js';
-import { getStylesForURL } from './css.js';
-import type DevPipeline from './devPipeline.js';
-import { preload } from './index.js';
-import { getComponentMetadata } from './metadata.js';
+import type { DevPipeline } from './pipeline.js';
import { handle404Response, writeSSRResult, writeWebResponse } from './response.js';
-import { getScriptsForURL } from './scripts.js';
-import { REROUTE_DIRECTIVE_HEADER } from '../runtime/server/consts.js';
-
-const clientLocalsSymbol = Symbol.for('astro.locals');
+import { REROUTE_DIRECTIVE_HEADER, clientLocalsSymbol } from '../core/constants.js';
+import { RenderContext } from '../core/render-context.js';
type AsyncReturnType<T extends (...args: any) => Promise<any>> = T extends (
...args: any
@@ -67,27 +46,22 @@ export async function matchRoute(
manifestData: ManifestData,
pipeline: DevPipeline
): Promise<MatchedRoute | undefined> {
- const env = pipeline.getEnvironment();
- const { routeCache, logger } = env;
- let matches = matchAllRoutes(pathname, manifestData);
+ const { config, logger, routeCache, serverLike, settings } = pipeline;
+ const matches = matchAllRoutes(pathname, manifestData);
- const preloadedMatches = await getSortedPreloadedMatches({
- pipeline,
- matches,
- settings: pipeline.getSettings(),
- });
+ const preloadedMatches = await getSortedPreloadedMatches({ pipeline, matches, settings });
for await (const { preloadedComponent, route: maybeRoute, filePath } of preloadedMatches) {
// attempt to get static paths
// if this fails, we have a bad URL match!
try {
- await getParamsAndProps({
+ await getProps({
mod: preloadedComponent,
- route: maybeRoute,
+ routeData: maybeRoute,
routeCache,
pathname: pathname,
logger,
- ssr: isServerLikeOutput(pipeline.getConfig()),
+ serverLike,
});
return {
route: maybeRoute,
@@ -116,7 +90,7 @@ export async function matchRoute(
if (matches.length) {
const possibleRoutes = matches.flatMap((route) => route.component);
- pipeline.logger.warn(
+ logger.warn(
'router',
`${AstroErrorData.NoMatchingStaticPathFound.message(
pathname
@@ -127,8 +101,8 @@ export async function matchRoute(
const custom404 = getCustom404Route(manifestData);
if (custom404) {
- const filePath = new URL(`./${custom404.component}`, pipeline.getConfig().root);
- const preloadedComponent = await preload({ pipeline, filePath });
+ const filePath = new URL(`./${custom404.component}`, config.root);
+ const preloadedComponent = await pipeline.preload(filePath);
return {
route: custom404,
@@ -151,7 +125,6 @@ type HandleRoute = {
manifestData: ManifestData;
incomingRequest: http.IncomingMessage;
incomingResponse: http.ServerResponse;
- manifest: SSRManifest;
status?: 404 | 500;
pipeline: DevPipeline;
};
@@ -167,13 +140,9 @@ export async function handleRoute({
manifestData,
incomingRequest,
incomingResponse,
- manifest,
}: HandleRoute): Promise<void> {
const timeStart = performance.now();
- const env = pipeline.getEnvironment();
- const config = pipeline.getConfig();
- const moduleLoader = pipeline.getModuleLoader();
- const { logger } = env;
+ const { config, loader, logger } = pipeline;
if (!matchedRoute && !config.i18n) {
if (isLoggedRequest(pathname)) {
logger.info(null, req({ url: pathname, method: incomingRequest.method, statusCode: 404 }));
@@ -188,8 +157,8 @@ export async function handleRoute({
let mod: ComponentInstance | undefined = undefined;
let options: SSROptions | undefined = undefined;
let route: RouteData;
- const middleware = await loadMiddleware(moduleLoader);
-
+ const middleware = (await loadMiddleware(loader)).onRequest;
+
if (!matchedRoute) {
if (config.i18n) {
const locales = config.i18n.locales;
@@ -239,16 +208,7 @@ export async function handleRoute({
fallbackRoutes: [],
isIndex: false,
};
- renderContext = await createRenderContext({
- request,
- pathname,
- env,
- mod,
- route,
- locales: manifest.i18n?.locales,
- routing: manifest.i18n?.routing,
- defaultLocale: manifest.i18n?.defaultLocale,
- });
+ renderContext = RenderContext.create({ pipeline: pipeline, pathname, middleware, request, routeData: route });
} else {
return handle404Response(origin, incomingRequest, incomingResponse);
}
@@ -256,16 +216,17 @@ export async function handleRoute({
const filePath: URL | undefined = matchedRoute.filePath;
const { preloadedComponent } = matchedRoute;
route = matchedRoute.route;
- // Headers are only available when using SSR.
+ // Allows adapters to pass in locals in dev mode.
+ const locals = Reflect.get(incomingRequest, clientLocalsSymbol)
request = createRequest({
url,
+ // Headers are only available when using SSR.
headers: buildingToSSR ? incomingRequest.headers : new Headers(),
method: incomingRequest.method,
body,
logger,
ssr: buildingToSSR,
clientAddress: buildingToSSR ? incomingRequest.socket.remoteAddress : undefined,
- locals: Reflect.get(incomingRequest, clientLocalsSymbol), // Allows adapters to pass in locals in dev mode.
});
// Set user specified headers to response object.
@@ -274,60 +235,19 @@ export async function handleRoute({
}
options = {
- env,
+ pipeline,
filePath,
preload: preloadedComponent,
pathname,
request,
route,
- middleware,
};
- mod = options.preload;
-
- const { scripts, links, styles, metadata } = await getScriptsAndStyles({
- pipeline,
- filePath: options.filePath,
- });
-
- const i18n = pipeline.getConfig().i18n;
-
- renderContext = await createRenderContext({
- request: options.request,
- pathname: options.pathname,
- scripts,
- links,
- styles,
- componentMetadata: metadata,
- route: options.route,
- mod,
- env,
- locales: i18n?.locales,
- routing: i18n?.routing,
- defaultLocale: i18n?.defaultLocale,
- });
- }
-
- const onRequest: MiddlewareHandler = middleware.onRequest;
- if (config.i18n) {
- const i18Middleware = createI18nMiddleware(
- manifest.i18n,
- config.base,
- config.trailingSlash,
- config.build.format
- );
-
- if (i18Middleware) {
- pipeline.setMiddlewareFunction(sequence(i18Middleware, onRequest));
- pipeline.onBeforeRenderRoute(i18nPipelineHook);
- } else {
- pipeline.setMiddlewareFunction(onRequest);
- }
- } else {
- pipeline.setMiddlewareFunction(onRequest);
+ mod = preloadedComponent;
+ renderContext = RenderContext.create({ locals, pipeline, pathname, middleware, request, routeData: route });
}
- let response = await pipeline.renderRoute(renderContext, mod);
+ let response = await renderContext.render(mod);
if (isLoggedRequest(pathname)) {
const timeEnd = performance.now();
logger.info(
@@ -358,7 +278,6 @@ export async function handleRoute({
manifestData,
incomingRequest,
incomingResponse,
- manifest,
});
}
if (route.type === 'endpoint') {
@@ -385,104 +304,6 @@ export async function handleRoute({
await writeSSRResult(request, response, incomingResponse);
}
-interface GetScriptsAndStylesParams {
- pipeline: DevPipeline;
- filePath: URL;
-}
-
-async function getScriptsAndStyles({ pipeline, filePath }: GetScriptsAndStylesParams) {
- const moduleLoader = pipeline.getModuleLoader();
- const settings = pipeline.getSettings();
- const mode = pipeline.getEnvironment().mode;
- // Add hoisted script tags
- const { scripts } = await getScriptsForURL(filePath, settings.config.root, moduleLoader);
-
- // Inject HMR scripts
- if (isPage(filePath, settings) && mode === 'development') {
- scripts.add({
- props: { type: 'module', src: '/@vite/client' },
- children: '',
- });
-
- if (
- settings.config.devToolbar.enabled &&
- (await settings.preferences.get('devToolbar.enabled'))
- ) {
- scripts.add({
- props: {
- type: 'module',
- src: await resolveIdToUrl(moduleLoader, 'astro/runtime/client/dev-toolbar/entrypoint.js'),
- },
- children: '',
- });
-
- const additionalMetadata: DevToolbarMetadata['__astro_dev_toolbar__'] = {
- root: fileURLToPath(settings.config.root),
- version: ASTRO_VERSION,
- debugInfo: await getInfoOutput({ userConfig: settings.config, print: false }),
- };
-
- // Additional data for the dev overlay
- scripts.add({
- props: {},
- children: `window.__astro_dev_toolbar__ = ${JSON.stringify(additionalMetadata)}`,
- });
- }
- }
-
- // TODO: We should allow adding generic HTML elements to the head, not just scripts
- 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: '',
- });
- }
- }
-
- // Pass framework CSS in as style tags to be appended to the page.
- const { urls: styleUrls, styles: importedStyles } = await getStylesForURL(filePath, moduleLoader);
- let links = new Set<SSRElement>();
- [...styleUrls].forEach((href) => {
- links.add({
- props: {
- rel: 'stylesheet',
- href,
- },
- children: '',
- });
- });
-
- let styles = new Set<SSRElement>();
- importedStyles.forEach(({ id, url, content }) => {
- // Vite handles HMR for styles injected as scripts
- scripts.add({
- props: {
- type: 'module',
- src: url,
- },
- children: '',
- });
- // But we still want to inject the styles to avoid FOUC. The style tags
- // should emulate what Vite injects so further HMR works as expected.
- styles.add({
- props: {
- 'data-vite-dev-id': id,
- },
- children: content,
- });
- });
-
- const metadata = await getComponentMetadata(filePath, moduleLoader);
-
- return { scripts, styles, links, metadata };
-}
-
function getStatus(matchedRoute?: MatchedRoute): 404 | 500 | undefined {
if (!matchedRoute) return 404;
if (matchedRoute.route.route === '/404') return 404;
diff --git a/packages/astro/test/fixtures/i18n-routing-prefix-always/astro.config.mjs b/packages/astro/test/fixtures/i18n-routing-prefix-always/astro.config.mjs
index 7a612488d..cc445b396 100644
--- a/packages/astro/test/fixtures/i18n-routing-prefix-always/astro.config.mjs
+++ b/packages/astro/test/fixtures/i18n-routing-prefix-always/astro.config.mjs
@@ -1,4 +1,4 @@
-import { defineConfig} from "astro/config";
+import { defineConfig } from "astro/config";
export default defineConfig({
i18n: {
diff --git a/packages/astro/test/units/i18n/astro_i18n.test.js b/packages/astro/test/units/i18n/astro_i18n.test.js
index 56be06194..d7702f7fd 100644
--- a/packages/astro/test/units/i18n/astro_i18n.test.js
+++ b/packages/astro/test/units/i18n/astro_i18n.test.js
@@ -4,7 +4,7 @@ import {
getLocaleAbsoluteUrl,
getLocaleAbsoluteUrlList,
} from '../../../dist/i18n/index.js';
-import { parseLocale } from '../../../dist/core/render/context.js';
+import { parseLocale } from '../../../dist/i18n/utils.js';
import { describe, it } from 'node:test';
import * as assert from 'node:assert/strict';
import { validateConfig } from '../../../dist/core/config/config.js';
diff --git a/packages/astro/test/units/render/head.test.js b/packages/astro/test/units/render/head.test.js
index ab84534aa..67077c9dd 100644
--- a/packages/astro/test/units/render/head.test.js
+++ b/packages/astro/test/units/render/head.test.js
@@ -9,18 +9,22 @@ import {
renderHead,
Fragment,
} from '../../../dist/runtime/server/index.js';
-import { createRenderContext } from '../../../dist/core/render/index.js';
-import { createBasicEnvironment } from '../test-utils.js';
+import { RenderContext } from '../../../dist/core/render-context.js';
+import { createBasicPipeline } from '../test-utils.js';
import * as cheerio from 'cheerio';
-import { Pipeline } from '../../../dist/core/pipeline.js';
const createAstroModule = (AstroComponent) => ({ default: AstroComponent });
describe('core/render', () => {
describe('Injected head contents', () => {
- let env;
+ let pipeline;
before(async () => {
- env = createBasicEnvironment();
+ pipeline = createBasicPipeline();
+ pipeline.headElements = () => ({
+ links: new Set([{ name: 'link', props: { rel: 'stylesheet', href: '/main.css' }, children: '' }]),
+ scripts: new Set,
+ styles: new Set
+ });
});
it('Multi-level layouts and head injection, with explicit head', async () => {
@@ -90,16 +94,10 @@ describe('core/render', () => {
});
const PageModule = createAstroModule(Page);
- const ctx = await createRenderContext({
- route: { type: 'page', pathname: '/index', component: 'src/pages/index.astro' },
- request: new Request('http://example.com/'),
- links: [{ name: 'link', props: { rel: 'stylesheet', href: '/main.css' }, children: '' }],
- mod: PageModule,
- env,
- });
-
- const pipeline = new Pipeline(env);
- const response = await pipeline.renderRoute(ctx, PageModule);
+ const request = new Request('http://example.com/');
+ const routeData = { type: 'page', pathname: '/index', component: 'src/pages/index.astro', params: {} };
+ const renderContext = RenderContext.create({ pipeline, request, routeData });
+ const response = await renderContext.render(PageModule);
const html = await response.text();
const $ = cheerio.load(html);
@@ -172,17 +170,11 @@ describe('core/render', () => {
});
const PageModule = createAstroModule(Page);
- const ctx = await createRenderContext({
- route: { type: 'page', pathname: '/index', component: 'src/pages/index.astro' },
- request: new Request('http://example.com/'),
- links: [{ name: 'link', props: { rel: 'stylesheet', href: '/main.css' }, children: '' }],
- env,
- mod: PageModule,
- });
-
- const pipeline = new Pipeline(env);
+ const request = new Request('http://example.com/');
+ const routeData = { type: 'page', pathname: '/index', component: 'src/pages/index.astro', params: {} };
+ const renderContext = RenderContext.create({ pipeline, request, routeData });
+ const response = await renderContext.render(PageModule);
- const response = await pipeline.renderRoute(ctx, PageModule);
const html = await response.text();
const $ = cheerio.load(html);
@@ -221,16 +213,11 @@ describe('core/render', () => {
});
const PageModule = createAstroModule(Page);
- const ctx = await createRenderContext({
- route: { type: 'page', pathname: '/index', component: 'src/pages/index.astro' },
- request: new Request('http://example.com/'),
- links: [{ name: 'link', props: { rel: 'stylesheet', href: '/main.css' }, children: '' }],
- env,
- mod: PageModule,
- });
+ const request = new Request('http://example.com/');
+ const routeData = { type: 'page', pathname: '/index', component: 'src/pages/index.astro', params: {} };
+ const renderContext = RenderContext.create({ pipeline, request, routeData });
+ const response = await renderContext.render(PageModule);
- const pipeline = new Pipeline(env);
- const response = await pipeline.renderRoute(ctx, PageModule);
const html = await response.text();
const $ = cheerio.load(html);
diff --git a/packages/astro/test/units/render/jsx.test.js b/packages/astro/test/units/render/jsx.test.js
index 757a0e35d..ca2485a45 100644
--- a/packages/astro/test/units/render/jsx.test.js
+++ b/packages/astro/test/units/render/jsx.test.js
@@ -7,19 +7,19 @@ import {
renderSlot,
} from '../../../dist/runtime/server/index.js';
import { jsx } from '../../../dist/jsx-runtime/index.js';
-import { createRenderContext, loadRenderer } from '../../../dist/core/render/index.js';
+import { loadRenderer } from '../../../dist/core/render/index.js';
+import { RenderContext } from '../../../dist/core/render-context.js';
import { createAstroJSXComponent, renderer as jsxRenderer } from '../../../dist/jsx/index.js';
-import { createBasicEnvironment } from '../test-utils.js';
-import { Pipeline } from '../../../dist/core/pipeline.js';
+import { createBasicPipeline } from '../test-utils.js';
const createAstroModule = (AstroComponent) => ({ default: AstroComponent });
const loadJSXRenderer = () => loadRenderer(jsxRenderer, { import: (s) => import(s) });
describe('core/render', () => {
describe('Astro JSX components', () => {
- let env;
+ let pipeline;
before(async () => {
- env = createBasicEnvironment({
+ pipeline = createBasicPipeline({
renderers: [await loadJSXRenderer()],
});
});
@@ -42,15 +42,10 @@ describe('core/render', () => {
});
const mod = createAstroModule(Page);
- const ctx = await createRenderContext({
- route: { type: 'page', pathname: '/index', component: 'src/pages/index.mdx' },
- request: new Request('http://example.com/'),
- env,
- mod,
- });
-
- const pipeline = new Pipeline(env);
- const response = await pipeline.renderRoute(ctx, mod);
+ const request = new Request('http://example.com/');
+ const routeData = { type: 'page', pathname: '/index', component: 'src/pages/index.mdx', params: {} };
+ const renderContext = RenderContext.create({ pipeline, request, routeData });
+ const response = await renderContext.render(mod);
assert.equal(response.status, 200);
@@ -89,14 +84,10 @@ describe('core/render', () => {
});
const mod = createAstroModule(Page);
- const ctx = await createRenderContext({
- route: { type: 'page', pathname: '/index', component: 'src/pages/index.mdx' },
- request: new Request('http://example.com/'),
- env,
- mod,
- });
- const pipeline = new Pipeline(env);
- const response = await pipeline.renderRoute(ctx, mod);
+ const request = new Request('http://example.com/');
+ const routeData = { type: 'page', pathname: '/index', component: 'src/pages/index.mdx', params: {} };
+ const renderContext = RenderContext.create({ pipeline, request, routeData });
+ const response = await renderContext.render(mod);
assert.equal(response.status, 200);
@@ -119,15 +110,10 @@ describe('core/render', () => {
});
const mod = createAstroModule(Page);
- const ctx = await createRenderContext({
- route: { type: 'page', pathname: '/index', component: 'src/pages/index.mdx' },
- request: new Request('http://example.com/'),
- env,
- mod,
- });
-
- const pipeline = new Pipeline(env);
- const response = await pipeline.renderRoute(ctx, mod);
+ const request = new Request('http://example.com/');
+ const routeData = { type: 'page', pathname: '/index', component: 'src/pages/index.mdx', params: {} };
+ const renderContext = RenderContext.create({ pipeline, request, routeData });
+ const response = await renderContext.render(mod);
try {
await response.text();
diff --git a/packages/astro/test/units/routing/route-matching.test.js b/packages/astro/test/units/routing/route-matching.test.js
index 0eeb47ca5..01b1d000a 100644
--- a/packages/astro/test/units/routing/route-matching.test.js
+++ b/packages/astro/test/units/routing/route-matching.test.js
@@ -14,7 +14,7 @@ import * as cheerio from 'cheerio';
import testAdapter from '../../test-adapter.js';
import { getSortedPreloadedMatches } from '../../../dist/prerender/routing.js';
import { createDevelopmentManifest } from '../../../dist/vite-plugin-astro-server/plugin.js';
-import DevPipeline from '../../../dist/vite-plugin-astro-server/devPipeline.js';
+import { DevPipeline } from '../../../dist/vite-plugin-astro-server/pipeline.js';
const root = new URL('../../fixtures/alias/', import.meta.url);
const fileSystem = {
@@ -146,7 +146,7 @@ describe('Route matching', () => {
const loader = createViteLoader(container.viteServer);
const manifest = createDevelopmentManifest(container.settings);
- pipeline = new DevPipeline({ manifest, logger: defaultLogger, settings, loader });
+ pipeline = DevPipeline.create({ loader, logger: defaultLogger, manifest, settings });
manifestData = createRouteManifest(
{
cwd: fileURLToPath(root),
diff --git a/packages/astro/test/units/test-utils.js b/packages/astro/test/units/test-utils.js
index d81e42a72..d9d7c7c4e 100644
--- a/packages/astro/test/units/test-utils.js
+++ b/packages/astro/test/units/test-utils.js
@@ -6,7 +6,7 @@ import npath from 'node:path';
import { fileURLToPath } from 'node:url';
import { getDefaultClientDirectives } from '../../dist/core/client-directive/index.js';
import { nodeLogDestination } from '../../dist/core/logger/node.js';
-import { createEnvironment } from '../../dist/core/render/index.js';
+import { Pipeline } from '../../dist/core/render/index.js';
import { RouteCache } from '../../dist/core/render/route-cache.js';
import { resolveConfig } from '../../dist/core/config/index.js';
import { createBaseSettings } from '../../dist/core/config/settings.js';
@@ -181,25 +181,30 @@ export function buffersToString(buffers) {
export const createAstroModule = (AstroComponent) => ({ default: AstroComponent });
/**
- * @param {Partial<import('../../src/core/render/environment.js').CreateEnvironmentArgs>} options
- * @returns {import('../../src/core/render/environment.js').Environment}
+ * @param {Partial<Pipeline>} options
+ * @returns {Pipeline}
*/
-export function createBasicEnvironment(options = {}) {
+export function createBasicPipeline(options = {}) {
const mode = options.mode ?? 'development';
- return createEnvironment({
- ...options,
- markdown: {
- ...(options.markdown ?? {}),
- },
- mode,
- renderers: options.renderers ?? [],
- clientDirectives: getDefaultClientDirectives(),
- resolve: options.resolve ?? ((s) => Promise.resolve(s)),
- routeCache: new RouteCache(options.logging, mode),
- logger: options.logger ?? defaultLogger,
- ssr: options.ssr ?? true,
- streaming: options.streaming ?? true,
- });
+ const pipeline = new Pipeline(
+ options.logger ?? defaultLogger,
+ options.manifest ?? {},
+ options.mode ?? 'development',
+ options.renderers ?? [],
+ options.resolve ?? (s => Promise.resolve(s)),
+ options.serverLike ?? true,
+ options.streaming ?? true,
+ options.adapterName,
+ options.clientDirectives ?? getDefaultClientDirectives(),
+ options.compressHTML,
+ options.i18n,
+ options.middleware,
+ options.routeCache ?? new RouteCache(options.logging, mode),
+ options.site
+ );
+ pipeline.headElements = () => ({ scripts: new Set, styles: new Set, links: new Set });
+ pipeline.componentMetadata = () => {};
+ return pipeline
}
/**
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 f79c86f84..36f05c41a 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
@@ -12,19 +12,14 @@ import {
defaultLogger,
} from '../test-utils.js';
import { createDevelopmentManifest } from '../../../dist/vite-plugin-astro-server/plugin.js';
-import DevPipeline from '../../../dist/vite-plugin-astro-server/devPipeline.js';
+import { DevPipeline } from '../../../dist/vite-plugin-astro-server/pipeline.js';
async function createDevPipeline(overrides = {}) {
const settings = overrides.settings ?? (await createBasicSettings({ root: '/' }));
const loader = overrides.loader ?? createLoader();
const manifest = createDevelopmentManifest(settings);
- return new DevPipeline({
- manifest,
- settings,
- logger: defaultLogger,
- loader,
- });
+ return DevPipeline.create({ loader, logger: defaultLogger, manifest, settings });
}
describe('vite-plugin-astro-server', () => {
@@ -32,7 +27,10 @@ describe('vite-plugin-astro-server', () => {
it('renders a request', async () => {
const pipeline = await createDevPipeline({
loader: createLoader({
- import() {
+ import(id) {
+ if (id === '\0astro-internal:middleware') {
+ return { onRequest: (_, next) => next() }
+ }
const Page = createComponent(() => {
return render`<div id="test">testing</div>`;
});
@@ -40,7 +38,7 @@ describe('vite-plugin-astro-server', () => {
},
}),
});
- const controller = createController({ loader: pipeline.getModuleLoader() });
+ const controller = createController({ loader: pipeline.loader });
const { req, res, text } = createRequestAndResponse();
const fs = createFs(
{
@@ -52,7 +50,7 @@ describe('vite-plugin-astro-server', () => {
const manifestData = createRouteManifest(
{
fsMod: fs,
- settings: pipeline.getSettings(),
+ settings: pipeline.settings,
},
defaultLogger
);
@@ -64,6 +62,7 @@ describe('vite-plugin-astro-server', () => {
controller,
incomingRequest: req,
incomingResponse: res,
+ manifest: {}
});
} catch (err) {
assert.equal(err.message, undefined);
diff --git a/packages/astro/test/units/vite-plugin-astro-server/response.test.js b/packages/astro/test/units/vite-plugin-astro-server/response.test.js
index bb7afbc37..42cd90e88 100644
--- a/packages/astro/test/units/vite-plugin-astro-server/response.test.js
+++ b/packages/astro/test/units/vite-plugin-astro-server/response.test.js
@@ -84,7 +84,7 @@ describe('endpoints', () => {
});
});
- it('Headers with multiple values (set-cookie special case)', async () => {
+ it('Can bail on streaming', async () => {
const { req, res, done } = createRequestAndResponse({
method: 'GET',
url: '/streaming',