From bbf0b7470bcd8f143d4d7ab8fa169f2bb14a41f0 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Thu, 17 Aug 2023 16:33:56 +0100 Subject: [PATCH] refactor: use pipeline in Development mode (#8115) --- packages/astro/src/core/app/index.ts | 13 +- packages/astro/src/core/pipeline.ts | 2 +- packages/astro/src/core/render/environment.ts | 8 +- packages/astro/src/core/render/index.ts | 11 +- packages/astro/src/core/render/renderer.ts | 10 +- packages/astro/src/prerender/routing.ts | 16 +- .../vite-plugin-astro-server/devPipeline.ts | 128 ++++++++++++++ .../vite-plugin-astro-server/environment.ts | 12 +- .../src/vite-plugin-astro-server/index.ts | 16 +- .../src/vite-plugin-astro-server/plugin.ts | 8 +- .../src/vite-plugin-astro-server/request.ts | 20 +-- .../src/vite-plugin-astro-server/route.ts | 165 +++++++----------- .../test/units/routing/route-matching.test.js | 10 +- .../vite-plugin-astro-server/request.test.js | 30 ++-- 14 files changed, 254 insertions(+), 195 deletions(-) create mode 100644 packages/astro/src/vite-plugin-astro-server/devPipeline.ts diff --git a/packages/astro/src/core/app/index.ts b/packages/astro/src/core/app/index.ts index 92f671b85..86b9c9d41 100644 --- a/packages/astro/src/core/app/index.ts +++ b/packages/astro/src/core/app/index.ts @@ -16,12 +16,7 @@ import { removeTrailingForwardSlash, } from '../path.js'; import { RedirectSinglePageBuiltModule } from '../redirects/index.js'; -import { - createEnvironment, - createRenderContext, - tryRenderRoute, - type RenderContext, -} from '../render/index.js'; +import { createEnvironment, createRenderContext, type RenderContext } from '../render/index.js'; import { RouteCache } from '../render/route-cache.js'; import { createAssetLink, @@ -282,11 +277,7 @@ export class App { status ); const page = (await mod.page()) as any; - const response = (await tryRenderRoute( - newRenderContext, - this.#pipeline.env, - page - )) as Response; + const response = await this.#pipeline.renderRoute(newRenderContext, page); return this.#mergeResponses(response, originalResponse); } catch {} } diff --git a/packages/astro/src/core/pipeline.ts b/packages/astro/src/core/pipeline.ts index b5c66517a..42ed7d5da 100644 --- a/packages/astro/src/core/pipeline.ts +++ b/packages/astro/src/core/pipeline.ts @@ -59,7 +59,7 @@ export class Pipeline { /** * Returns the current environment */ - getEnvironment() { + getEnvironment(): Readonly { return this.env; } diff --git a/packages/astro/src/core/render/environment.ts b/packages/astro/src/core/render/environment.ts index 32dfb454b..f38d98551 100644 --- a/packages/astro/src/core/render/environment.ts +++ b/packages/astro/src/core/render/environment.ts @@ -1,6 +1,5 @@ -import type { AstroSettings, RuntimeMode, SSRLoadedRenderer } from '../../@types/astro'; +import type { RuntimeMode, SSRLoadedRenderer } from '../../@types/astro'; import type { LogOptions } from '../logger/core.js'; -import type { ModuleLoader } from '../module-loader'; import type { RouteCache } from './route-cache.js'; /** @@ -38,8 +37,3 @@ export type CreateEnvironmentArgs = Environment; export function createEnvironment(options: CreateEnvironmentArgs): Environment { return options; } - -export type DevelopmentEnvironment = Environment & { - loader: ModuleLoader; - settings: AstroSettings; -}; diff --git a/packages/astro/src/core/render/index.ts b/packages/astro/src/core/render/index.ts index 20b964fa7..f39c02ae2 100644 --- a/packages/astro/src/core/render/index.ts +++ b/packages/astro/src/core/render/index.ts @@ -1,18 +1,17 @@ import type { AstroMiddlewareInstance, ComponentInstance, RouteData } from '../../@types/astro'; -import type { DevelopmentEnvironment } from './environment'; - export { createRenderContext } from './context.js'; export type { RenderContext } from './context.js'; export { tryRenderRoute } from './core.js'; -export type { Environment } from './environment'; +import type { Environment } from './environment'; export { createEnvironment } from './environment.js'; export { getParamsAndProps } from './params-and-props.js'; -export { loadRenderer, loadRenderers } from './renderer.js'; -export type { DevelopmentEnvironment }; +export { loadRenderer } from './renderer.js'; + +export type { Environment }; export interface SSROptions { /** The environment instance */ - env: DevelopmentEnvironment; + env: Environment; /** location of file on disk */ filePath: URL; /** the web request (needed for dynamic routes) */ diff --git a/packages/astro/src/core/render/renderer.ts b/packages/astro/src/core/render/renderer.ts index 8e5e97202..e64a27ba5 100644 --- a/packages/astro/src/core/render/renderer.ts +++ b/packages/astro/src/core/render/renderer.ts @@ -1,14 +1,6 @@ -import type { AstroRenderer, AstroSettings, SSRLoadedRenderer } from '../../@types/astro'; +import type { AstroRenderer, SSRLoadedRenderer } from '../../@types/astro'; import type { ModuleLoader } from '../module-loader/index.js'; -export async function loadRenderers( - settings: AstroSettings, - moduleLoader: ModuleLoader -): Promise { - const renderers = await Promise.all(settings.renderers.map((r) => loadRenderer(r, moduleLoader))); - return renderers.filter(Boolean) as SSRLoadedRenderer[]; -} - export async function loadRenderer( renderer: AstroRenderer, moduleLoader: ModuleLoader diff --git a/packages/astro/src/prerender/routing.ts b/packages/astro/src/prerender/routing.ts index a3b3c0d54..2fcfe207b 100644 --- a/packages/astro/src/prerender/routing.ts +++ b/packages/astro/src/prerender/routing.ts @@ -1,22 +1,22 @@ import type { AstroSettings, ComponentInstance, RouteData } from '../@types/astro'; import { RedirectComponentInstance, routeIsRedirect } from '../core/redirects/index.js'; -import type { DevelopmentEnvironment } from '../core/render'; import { preload } from '../vite-plugin-astro-server/index.js'; import { getPrerenderStatus } from './metadata.js'; +import type DevPipeline from '../vite-plugin-astro-server/devPipeline'; type GetSortedPreloadedMatchesParams = { - env: DevelopmentEnvironment; + pipeline: DevPipeline; matches: RouteData[]; settings: AstroSettings; }; export async function getSortedPreloadedMatches({ - env, + pipeline, matches, settings, }: GetSortedPreloadedMatchesParams) { return ( await preloadAndSetPrerenderStatus({ - env, + pipeline, matches, settings, }) @@ -24,7 +24,7 @@ export async function getSortedPreloadedMatches({ } type PreloadAndSetPrerenderStatusParams = { - env: DevelopmentEnvironment; + pipeline: DevPipeline; matches: RouteData[]; settings: AstroSettings; }; @@ -36,7 +36,7 @@ type PreloadAndSetPrerenderStatusResult = { }; async function preloadAndSetPrerenderStatus({ - env, + pipeline, matches, settings, }: PreloadAndSetPrerenderStatusParams): Promise { @@ -52,12 +52,12 @@ async function preloadAndSetPrerenderStatus({ }; } - const preloadedComponent = await preload({ env, filePath }); + const preloadedComponent = await preload({ pipeline, filePath }); // gets the prerender metadata set by the `astro:scanner` vite plugin const prerenderStatus = getPrerenderStatus({ filePath, - loader: env.loader, + loader: pipeline.getModuleLoader(), }); if (prerenderStatus !== undefined) { diff --git a/packages/astro/src/vite-plugin-astro-server/devPipeline.ts b/packages/astro/src/vite-plugin-astro-server/devPipeline.ts new file mode 100644 index 000000000..eae6cc1c6 --- /dev/null +++ b/packages/astro/src/vite-plugin-astro-server/devPipeline.ts @@ -0,0 +1,128 @@ +import { Pipeline } from '../core/pipeline.js'; +import type { AstroConfig, AstroSettings, RouteData } from '../@types/astro'; +import type { ModuleLoader } from '../core/module-loader'; +import type { Environment } from '../core/render'; +import { createEnvironment, loadRenderer } from '../core/render/index.js'; +import { createResolve } from './resolve.js'; +import { RouteCache } from '../core/render/route-cache.js'; +import { isServerLikeOutput } from '../prerender/utils.js'; +import type { RuntimeMode, SSRManifest, SSRLoadedRenderer } from '../@types/astro'; +import type { LogOptions } from '../core/logger/core'; +import { Logger } from '../core/logger/core.js'; +import type { EndpointCallResult } from '../core/endpoint/index.js'; +import mime from 'mime'; +import { attachCookiesToResponse } from '../core/cookies/index.js'; + +export default class DevPipeline extends Pipeline { + #settings: AstroSettings; + #loader: ModuleLoader; + #devLogger: Logger; + #currentMatchedRoute: RouteData | undefined; + + constructor({ + manifest, + logging, + settings, + loader, + }: { + manifest: SSRManifest; + logging: LogOptions; + settings: AstroSettings; + loader: ModuleLoader; + }) { + const env = DevPipeline.createDevelopmentEnvironment(manifest, settings, logging, loader); + super(env); + this.#devLogger = new Logger(logging); + this.#settings = settings; + this.#loader = loader; + this.setEndpointHandler(this.#handleEndpointResult); + } + + setCurrentMatchedRoute(route: RouteData) { + this.#currentMatchedRoute = route; + } + + clearRouteCache() { + this.env.routeCache.clearAll(); + } + + getSettings(): Readonly { + return this.#settings; + } + + getConfig(): Readonly { + return this.#settings.config; + } + + getModuleLoader(): Readonly { + return this.#loader; + } + + get logger(): Readonly { + 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, + logging: LogOptions, + loader: ModuleLoader + ): Environment { + const mode: RuntimeMode = 'development'; + + return createEnvironment({ + adapterName: manifest.adapterName, + logging, + 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(logging, mode), + site: manifest.site, + ssr: isServerLikeOutput(settings.config), + streaming: true, + }); + } + + async #handleEndpointResult(_: Request, result: EndpointCallResult): Promise { + if (result.type === 'simple') { + if (!this.#currentMatchedRoute) { + throw new Error( + 'In development mode, you must set the current matched route before handling a endpoint.' + ); + } + let contentType = 'text/plain'; + // Dynamic routes don't include `route.pathname`, so synthesize a path for these (e.g. 'src/pages/[slug].svg') + const filepath = + this.#currentMatchedRoute.pathname || + this.#currentMatchedRoute.segments + .map((segment) => segment.map((p) => p.content).join('')) + .join('/'); + const computedMimeType = mime.getType(filepath); + if (computedMimeType) { + contentType = computedMimeType; + } + const response = new Response( + result.encoding !== 'binary' ? Buffer.from(result.body, result.encoding) : result.body, + { + status: 200, + headers: { + 'Content-Type': `${contentType};charset=utf-8`, + }, + } + ); + attachCookiesToResponse(response, result.cookies); + return response; + } + return result.response; + } +} diff --git a/packages/astro/src/vite-plugin-astro-server/environment.ts b/packages/astro/src/vite-plugin-astro-server/environment.ts index ce7b92662..010c1b96a 100644 --- a/packages/astro/src/vite-plugin-astro-server/environment.ts +++ b/packages/astro/src/vite-plugin-astro-server/environment.ts @@ -1,7 +1,7 @@ import type { AstroSettings, RuntimeMode, SSRManifest } from '../@types/astro.js'; import type { LogOptions } from '../core/logger/core.js'; import type { ModuleLoader } from '../core/module-loader'; -import type { DevelopmentEnvironment } from '../core/render'; +import type { Environment } from '../core/render'; import { createEnvironment } from '../core/render/index.js'; import { RouteCache } from '../core/render/route-cache.js'; import { isServerLikeOutput } from '../prerender/utils.js'; @@ -12,9 +12,9 @@ export function createDevelopmentEnvironment( settings: AstroSettings, logging: LogOptions, loader: ModuleLoader -): DevelopmentEnvironment { +): Environment { const mode: RuntimeMode = 'development'; - let env = createEnvironment({ + return createEnvironment({ adapterName: manifest.adapterName, logging, mode, @@ -28,10 +28,4 @@ export function createDevelopmentEnvironment( ssr: isServerLikeOutput(settings.config), streaming: true, }); - - return { - ...env, - loader, - settings, - }; } diff --git a/packages/astro/src/vite-plugin-astro-server/index.ts b/packages/astro/src/vite-plugin-astro-server/index.ts index 17302ba41..b02c57e1c 100644 --- a/packages/astro/src/vite-plugin-astro-server/index.ts +++ b/packages/astro/src/vite-plugin-astro-server/index.ts @@ -1,26 +1,22 @@ 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 type { DevelopmentEnvironment } from '../core/render/environment'; -import { loadRenderers } from '../core/render/index.js'; import { viteID } from '../core/util.js'; +import type DevPipeline from './devPipeline'; export async function preload({ - env, + pipeline, filePath, }: { - env: DevelopmentEnvironment; + pipeline: DevPipeline; filePath: URL; }): Promise { // Important: This needs to happen first, in case a renderer provides polyfills. - const renderers = await loadRenderers(env.settings, env.loader); - // Override the environment's renderers. This ensures that if renderers change (HMR) - // The new instances are passed through. - env.renderers = renderers; + await pipeline.loadRenderers(); try { // Load the module from the Vite SSR Runtime. - const mod = (await env.loader.import(viteID(filePath))) as ComponentInstance; + const mod = (await pipeline.getModuleLoader().import(viteID(filePath))) as ComponentInstance; return mod; } catch (error) { @@ -29,7 +25,7 @@ export async function preload({ throw error; } - throw enhanceViteSSRError({ error, filePath, loader: env.loader }); + throw enhanceViteSSRError({ error, filePath, loader: pipeline.getModuleLoader() }); } } diff --git a/packages/astro/src/vite-plugin-astro-server/plugin.ts b/packages/astro/src/vite-plugin-astro-server/plugin.ts index dfaf976bf..262d50148 100644 --- a/packages/astro/src/vite-plugin-astro-server/plugin.ts +++ b/packages/astro/src/vite-plugin-astro-server/plugin.ts @@ -7,8 +7,8 @@ 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 { createDevelopmentEnvironment } from './environment.js'; import { handleRequest } from './request.js'; +import DevPipeline from './devPipeline.js'; export interface AstroPluginOptions { settings: AstroSettings; @@ -26,13 +26,13 @@ export default function createVitePluginAstroServer({ configureServer(viteServer) { const loader = createViteLoader(viteServer); const manifest = createDevelopmentManifest(settings); - const env = createDevelopmentEnvironment(manifest, settings, logging, loader); + const pipeline = new DevPipeline({ logging, manifest, settings, loader }); let manifestData: ManifestData = createRouteManifest({ settings, fsMod }, logging); const controller = createController({ loader }); /** rebuild the route cache + manifest, as needed. */ function rebuildManifest(needsManifestRebuild: boolean) { - env.routeCache.clearAll(); + pipeline.clearRouteCache(); if (needsManifestRebuild) { manifestData = createRouteManifest({ settings }, logging); } @@ -57,7 +57,7 @@ export default function createVitePluginAstroServer({ return; } handleRequest({ - env, + pipeline, manifestData, controller, incomingRequest: request, diff --git a/packages/astro/src/vite-plugin-astro-server/request.ts b/packages/astro/src/vite-plugin-astro-server/request.ts index ae476f9be..72ce29b1b 100644 --- a/packages/astro/src/vite-plugin-astro-server/request.ts +++ b/packages/astro/src/vite-plugin-astro-server/request.ts @@ -1,11 +1,8 @@ import type http from 'node:http'; import type { ManifestData, SSRManifest } from '../@types/astro'; -import type { DevelopmentEnvironment } from '../core/render/index'; import type { DevServerController } from './controller'; - import { collectErrorMetadata } from '../core/errors/dev/index.js'; import { createSafeError } from '../core/errors/index.js'; -import { error } from '../core/logger/core.js'; import * as msg from '../core/messages.js'; import { collapseDuplicateSlashes, removeTrailingForwardSlash } from '../core/path.js'; import { eventError, telemetry } from '../events/index.js'; @@ -13,9 +10,10 @@ import { isServerLikeOutput } from '../prerender/utils.js'; import { runWithErrorHandling } from './controller.js'; import { handle500Response } from './response.js'; import { handleRoute, matchRoute } from './route.js'; +import type DevPipeline from './devPipeline'; type HandleRequest = { - env: DevelopmentEnvironment; + pipeline: DevPipeline; manifestData: ManifestData; controller: DevServerController; incomingRequest: http.IncomingMessage; @@ -25,15 +23,15 @@ type HandleRequest = { /** The main logic to route dev server requests to pages in Astro. */ export async function handleRequest({ - env, + pipeline, manifestData, controller, incomingRequest, incomingResponse, manifest, }: HandleRequest) { - const { settings, loader: moduleLoader } = env; - const { config } = settings; + const config = pipeline.getConfig(); + const moduleLoader = pipeline.getModuleLoader(); const origin = `${moduleLoader.isHttps() ? 'https' : 'http'}://${incomingRequest.headers.host}`; const buildingToSSR = isServerLikeOutput(config); @@ -75,7 +73,7 @@ export async function handleRequest({ controller, pathname, async run() { - const matchedRoute = await matchRoute(pathname, env, manifestData); + const matchedRoute = await matchRoute(pathname, manifestData, pipeline); const resolvedPathname = matchedRoute?.resolvedPathname ?? pathname; return await handleRoute({ matchedRoute, @@ -83,7 +81,7 @@ export async function handleRequest({ pathname: resolvedPathname, body, origin, - env, + pipeline, manifestData, incomingRequest: incomingRequest, incomingResponse: incomingResponse, @@ -95,7 +93,7 @@ export async function handleRequest({ // This could be a runtime error from Vite's SSR module, so try to fix it here try { - env.loader.fixStacktrace(err); + moduleLoader.fixStacktrace(err); } catch {} // This is our last line of defense regarding errors where we still might have some information about the request @@ -104,7 +102,7 @@ export async function handleRequest({ telemetry.record(eventError({ cmd: 'dev', err: errorWithMetadata, isFatal: false })); - error(env.logging, null, msg.formatErrorMessage(errorWithMetadata)); + pipeline.logger.error(null, msg.formatErrorMessage(errorWithMetadata)); handle500Response(moduleLoader, incomingResponse, errorWithMetadata); return err; diff --git a/packages/astro/src/vite-plugin-astro-server/route.ts b/packages/astro/src/vite-plugin-astro-server/route.ts index 0bbaacbe2..ae4318092 100644 --- a/packages/astro/src/vite-plugin-astro-server/route.ts +++ b/packages/astro/src/vite-plugin-astro-server/route.ts @@ -1,25 +1,15 @@ -import mime from 'mime'; import type http from 'node:http'; import type { ComponentInstance, ManifestData, - MiddlewareResponseHandler, + MiddlewareEndpointHandler, RouteData, SSRElement, SSRManifest, } from '../@types/astro'; -import { attachCookiesToResponse } from '../core/cookies/index.js'; import { AstroErrorData, isAstroError } from '../core/errors/index.js'; -import { warn } from '../core/logger/core.js'; import { loadMiddleware } from '../core/middleware/loadMiddleware.js'; -import { isEndpointResult } from '../core/render/core.js'; -import { - createRenderContext, - getParamsAndProps, - tryRenderRoute, - type DevelopmentEnvironment, - type SSROptions, -} from '../core/render/index.js'; +import { createRenderContext, getParamsAndProps, type SSROptions } from '../core/render/index.js'; import { createRequest } from '../core/request.js'; import { matchAllRoutes } from '../core/routing/index.js'; import { isPage, resolveIdToUrl, viteID } from '../core/util.js'; @@ -32,6 +22,7 @@ import { preload } from './index.js'; import { getComponentMetadata } from './metadata.js'; import { handle404Response, writeSSRResult, writeWebResponse } from './response.js'; import { getScriptsForURL } from './scripts.js'; +import type DevPipeline from './devPipeline.js'; const clientLocalsSymbol = Symbol.for('astro.locals'); @@ -56,12 +47,17 @@ function getCustom404Route(manifestData: ManifestData): RouteData | undefined { export async function matchRoute( pathname: string, - env: DevelopmentEnvironment, - manifestData: ManifestData + manifestData: ManifestData, + pipeline: DevPipeline ): Promise { - const { logging, settings, routeCache } = env; + const env = pipeline.getEnvironment(); + const { routeCache, logging } = env; const matches = matchAllRoutes(pathname, manifestData); - const preloadedMatches = await getSortedPreloadedMatches({ env, matches, settings }); + const preloadedMatches = await getSortedPreloadedMatches({ + pipeline, + matches, + settings: pipeline.getSettings(), + }); for await (const { preloadedComponent, route: maybeRoute, filePath } of preloadedMatches) { // attempt to get static paths @@ -73,7 +69,7 @@ export async function matchRoute( routeCache, pathname: pathname, logging, - ssr: isServerLikeOutput(settings.config), + ssr: isServerLikeOutput(pipeline.getConfig()), }); return { route: maybeRoute, @@ -96,14 +92,13 @@ export async function matchRoute( // build formats, and is necessary based on how the manifest tracks build targets. const altPathname = pathname.replace(/(index)?\.html$/, ''); if (altPathname !== pathname) { - return await matchRoute(altPathname, env, manifestData); + return await matchRoute(altPathname, manifestData, pipeline); } if (matches.length) { const possibleRoutes = matches.flatMap((route) => route.component); - warn( - logging, + pipeline.logger.warn( 'getStaticPaths', `${AstroErrorData.NoMatchingStaticPathFound.message( pathname @@ -115,8 +110,8 @@ export async function matchRoute( const custom404 = getCustom404Route(manifestData); if (custom404) { - const filePath = new URL(`./${custom404.component}`, settings.config.root); - const preloadedComponent = await preload({ env, filePath }); + const filePath = new URL(`./${custom404.component}`, pipeline.getConfig().root); + const preloadedComponent = await preload({ pipeline, filePath }); return { route: custom404, @@ -136,12 +131,12 @@ type HandleRoute = { pathname: string; body: ArrayBuffer | undefined; origin: string; - env: DevelopmentEnvironment; manifestData: ManifestData; incomingRequest: http.IncomingMessage; incomingResponse: http.ServerResponse; manifest: SSRManifest; status?: 404 | 500; + pipeline: DevPipeline; }; export async function handleRoute({ @@ -151,18 +146,21 @@ export async function handleRoute({ status = getStatus(matchedRoute), body, origin, - env, + pipeline, manifestData, incomingRequest, incomingResponse, manifest, }: HandleRoute): Promise { - const { logging, settings } = env; + const env = pipeline.getEnvironment(); + const settings = pipeline.getSettings(); + const config = pipeline.getConfig(); + const moduleLoader = pipeline.getModuleLoader(); + const { logging } = env; if (!matchedRoute) { return handle404Response(origin, incomingRequest, incomingResponse); } - const { config } = settings; const filePath: URL | undefined = matchedRoute.filePath; const { route, preloadedComponent } = matchedRoute; const buildingToSSR = isServerLikeOutput(config); @@ -192,14 +190,14 @@ export async function handleRoute({ request, route, }; - const middleware = await loadMiddleware(env.loader, env.settings.config.srcDir); + const middleware = await loadMiddleware(moduleLoader, settings.config.srcDir); if (middleware) { options.middleware = middleware; } const mod = options.preload; const { scripts, links, styles, metadata } = await getScriptsAndStyles({ - env: options.env, + pipeline, filePath: options.filePath, }); @@ -214,70 +212,32 @@ export async function handleRoute({ mod, env, }); - const onRequest = options.middleware?.onRequest as MiddlewareResponseHandler | undefined; + const onRequest = options.middleware?.onRequest as MiddlewareEndpointHandler | undefined; + if (onRequest) { + pipeline.setMiddlewareFunction(onRequest); + } + pipeline.setCurrentMatchedRoute(route); - const result = await tryRenderRoute(renderContext, env, mod, onRequest); - if (isEndpointResult(result, route.type)) { - if (result.type === 'response') { - if (result.response.headers.get('X-Astro-Response') === 'Not-Found') { - const fourOhFourRoute = await matchRoute('/404', env, manifestData); - return handleRoute({ - matchedRoute: fourOhFourRoute, - url: new URL('/404', url), - pathname: '/404', - status: 404, - body, - origin, - env, - manifestData, - incomingRequest, - incomingResponse, - manifest, - }); - } - await writeWebResponse(incomingResponse, result.response); - } else { - let contentType = 'text/plain'; - // Dynamic routes don't include `route.pathname`, so synthesize a path for these (e.g. 'src/pages/[slug].svg') - const filepath = - route.pathname || - route.segments.map((segment) => segment.map((p) => p.content).join('')).join('/'); - const computedMimeType = mime.getType(filepath); - if (computedMimeType) { - contentType = computedMimeType; - } - const response = new Response( - result.encoding !== 'binary' ? Buffer.from(result.body, result.encoding) : result.body, - { - status: 200, - headers: { - 'Content-Type': `${contentType};charset=utf-8`, - }, - } - ); - attachCookiesToResponse(response, result.cookies); - await writeWebResponse(incomingResponse, response); - } + let response = await pipeline.renderRoute(renderContext, mod); + if (response.status === 404) { + const fourOhFourRoute = await matchRoute('/404', manifestData, pipeline); + return handleRoute({ + ...options, + matchedRoute: fourOhFourRoute, + url: new URL(pathname, url), + status: 404, + body, + origin, + pipeline, + manifestData, + incomingRequest, + incomingResponse, + manifest, + }); + } + if (route.type === 'endpoint') { + await writeWebResponse(incomingResponse, response); } else { - if (result.status === 404) { - const fourOhFourRoute = await matchRoute('/404', env, manifestData); - return handleRoute({ - ...options, - matchedRoute: fourOhFourRoute, - url: new URL(pathname, url), - status: 404, - body, - origin, - env, - manifestData, - incomingRequest, - incomingResponse, - manifest, - }); - } - - let response = result; - if ( // We are in a recursion, and it's possible that this function is called itself with a status code // By default, the status code passed via parameters is computed by the matched route. @@ -291,23 +251,26 @@ export async function handleRoute({ return; } else if (status && response.status !== status && (status === 404 || status === 500)) { // Response.status is read-only, so a clone is required to override - response = new Response(result.body, { ...result, status }); + response = new Response(response.body, { ...response, status }); } await writeSSRResult(request, response, incomingResponse); } } interface GetScriptsAndStylesParams { - env: DevelopmentEnvironment; + pipeline: DevPipeline; filePath: URL; } -async function getScriptsAndStyles({ env, filePath }: GetScriptsAndStylesParams) { +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, env.settings.config.root, env.loader); + const scripts = await getScriptsForURL(filePath, settings.config.root, moduleLoader); // Inject HMR scripts - if (isPage(filePath, env.settings) && env.mode === 'development') { + if (isPage(filePath, settings) && mode === 'development') { scripts.add({ props: { type: 'module', src: '/@vite/client' }, children: '', @@ -315,20 +278,20 @@ async function getScriptsAndStyles({ env, filePath }: GetScriptsAndStylesParams) scripts.add({ props: { type: 'module', - src: await resolveIdToUrl(env.loader, 'astro/runtime/client/hmr.js'), + src: await resolveIdToUrl(moduleLoader, 'astro/runtime/client/hmr.js'), }, children: '', }); } // TODO: We should allow adding generic HTML elements to the head, not just scripts - for (const script of env.settings.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, env.settings)) { + } else if (script.stage === 'page' && isPage(filePath, settings)) { scripts.add({ props: { type: 'module', src: `/@id/${PAGE_SCRIPT_ID}` }, children: '', @@ -337,7 +300,7 @@ async function getScriptsAndStyles({ env, filePath }: GetScriptsAndStylesParams) } // Pass framework CSS in as style tags to be appended to the page. - const { urls: styleUrls, stylesMap } = await getStylesForURL(filePath, env.loader, env.mode); + const { urls: styleUrls, stylesMap } = await getStylesForURL(filePath, moduleLoader, mode); let links = new Set(); [...styleUrls].forEach((href) => { links.add({ @@ -364,13 +327,13 @@ async function getScriptsAndStyles({ env, filePath }: GetScriptsAndStylesParams) props: { type: 'text/css', // Track the ID so we can match it to Vite's injected style later - 'data-astro-dev-id': viteID(new URL(`.${url}`, env.settings.config.root)), + 'data-astro-dev-id': viteID(new URL(`.${url}`, settings.config.root)), }, children: content, }); }); - const metadata = await getComponentMetadata(filePath, env.loader); + const metadata = await getComponentMetadata(filePath, moduleLoader); return { scripts, styles, links, metadata }; } diff --git a/packages/astro/test/units/routing/route-matching.test.js b/packages/astro/test/units/routing/route-matching.test.js index e1c4df5c5..c0bf7d6e6 100644 --- a/packages/astro/test/units/routing/route-matching.test.js +++ b/packages/astro/test/units/routing/route-matching.test.js @@ -12,8 +12,8 @@ import { createContainer } from '../../../dist/core/dev/container.js'; import * as cheerio from 'cheerio'; import testAdapter from '../../test-adapter.js'; import { getSortedPreloadedMatches } from '../../../dist/prerender/routing.js'; -import { createDevelopmentEnvironment } from '../../../dist/vite-plugin-astro-server/environment.js'; import { createDevelopmentManifest } from '../../../dist/vite-plugin-astro-server/plugin.js'; +import DevPipeline from '../../../dist/vite-plugin-astro-server/devPipeline.js'; const root = new URL('../../fixtures/alias/', import.meta.url); const fileSystem = { @@ -124,7 +124,7 @@ const fileSystem = { }; describe('Route matching', () => { - let env; + let pipeline; let manifestData; let container; let settings; @@ -145,7 +145,7 @@ describe('Route matching', () => { const loader = createViteLoader(container.viteServer); const manifest = createDevelopmentManifest(container.settings); - env = createDevelopmentEnvironment(manifest, container.settings, defaultLogging, loader); + pipeline = new DevPipeline({ manifest, logging: defaultLogging, settings, loader }); manifestData = createRouteManifest( { cwd: fileURLToPath(root), @@ -163,7 +163,7 @@ describe('Route matching', () => { describe('Matched routes', () => { it('should be sorted correctly', async () => { const matches = matchAllRoutes('/try-matching-a-route', manifestData); - const preloadedMatches = await getSortedPreloadedMatches({ env, matches, settings }); + const preloadedMatches = await getSortedPreloadedMatches({ pipeline, matches, settings }); const sortedRouteNames = preloadedMatches.map((match) => match.route.route); expect(sortedRouteNames).to.deep.equal([ @@ -177,7 +177,7 @@ describe('Route matching', () => { }); it('nested should be sorted correctly', async () => { const matches = matchAllRoutes('/nested/try-matching-a-route', manifestData); - const preloadedMatches = await getSortedPreloadedMatches({ env, matches, settings }); + const preloadedMatches = await getSortedPreloadedMatches({ pipeline, matches, settings }); const sortedRouteNames = preloadedMatches.map((match) => match.route.route); expect(sortedRouteNames).to.deep.equal([ 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 48d449ccd..58ad404fd 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 @@ -1,31 +1,35 @@ import { expect } from 'chai'; - import { createLoader } from '../../../dist/core/module-loader/index.js'; import { createRouteManifest } from '../../../dist/core/routing/index.js'; import { createComponent, render } from '../../../dist/runtime/server/index.js'; import { createController, handleRequest } from '../../../dist/vite-plugin-astro-server/index.js'; import { createAstroModule, - createBasicEnvironment, createBasicSettings, createFs, createRequestAndResponse, defaultLogging, } from '../test-utils.js'; +import { createDevelopmentManifest } from '../../../dist/vite-plugin-astro-server/plugin.js'; +import DevPipeline from '../../../dist/vite-plugin-astro-server/devPipeline.js'; -async function createDevEnvironment(overrides = {}) { - const env = createBasicEnvironment(); - env.settings = await createBasicSettings({ root: '/' }); - env.settings.renderers = []; - env.loader = createLoader(); - Object.assign(env, overrides); - return env; +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, + logging: defaultLogging, + loader, + }); } describe('vite-plugin-astro-server', () => { describe('request', () => { it('renders a request', async () => { - const env = await createDevEnvironment({ + const pipeline = await createDevPipeline({ loader: createLoader({ import() { const Page = createComponent(() => { @@ -35,7 +39,7 @@ describe('vite-plugin-astro-server', () => { }, }), }); - const controller = createController({ loader: env.loader }); + const controller = createController({ loader: pipeline.getModuleLoader() }); const { req, res, text } = createRequestAndResponse(); const fs = createFs( { @@ -47,14 +51,14 @@ describe('vite-plugin-astro-server', () => { const manifestData = createRouteManifest( { fsMod: fs, - settings: env.settings, + settings: pipeline.getSettings(), }, defaultLogging ); try { await handleRequest({ - env, + pipeline, manifestData, controller, incomingRequest: req,