refactor: use pipeline in Development mode (#8115)

This commit is contained in:
Emanuele Stoppa 2023-08-17 16:33:56 +01:00 committed by GitHub
parent cdebbded0c
commit bbf0b7470b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 254 additions and 195 deletions

View file

@ -16,12 +16,7 @@ import {
removeTrailingForwardSlash, removeTrailingForwardSlash,
} from '../path.js'; } from '../path.js';
import { RedirectSinglePageBuiltModule } from '../redirects/index.js'; import { RedirectSinglePageBuiltModule } from '../redirects/index.js';
import { import { createEnvironment, createRenderContext, type RenderContext } from '../render/index.js';
createEnvironment,
createRenderContext,
tryRenderRoute,
type RenderContext,
} from '../render/index.js';
import { RouteCache } from '../render/route-cache.js'; import { RouteCache } from '../render/route-cache.js';
import { import {
createAssetLink, createAssetLink,
@ -282,11 +277,7 @@ export class App {
status status
); );
const page = (await mod.page()) as any; const page = (await mod.page()) as any;
const response = (await tryRenderRoute( const response = await this.#pipeline.renderRoute(newRenderContext, page);
newRenderContext,
this.#pipeline.env,
page
)) as Response;
return this.#mergeResponses(response, originalResponse); return this.#mergeResponses(response, originalResponse);
} catch {} } catch {}
} }

View file

@ -59,7 +59,7 @@ export class Pipeline {
/** /**
* Returns the current environment * Returns the current environment
*/ */
getEnvironment() { getEnvironment(): Readonly<Environment> {
return this.env; return this.env;
} }

View file

@ -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 { LogOptions } from '../logger/core.js';
import type { ModuleLoader } from '../module-loader';
import type { RouteCache } from './route-cache.js'; import type { RouteCache } from './route-cache.js';
/** /**
@ -38,8 +37,3 @@ export type CreateEnvironmentArgs = Environment;
export function createEnvironment(options: CreateEnvironmentArgs): Environment { export function createEnvironment(options: CreateEnvironmentArgs): Environment {
return options; return options;
} }
export type DevelopmentEnvironment = Environment & {
loader: ModuleLoader;
settings: AstroSettings;
};

View file

@ -1,18 +1,17 @@
import type { AstroMiddlewareInstance, ComponentInstance, RouteData } from '../../@types/astro'; import type { AstroMiddlewareInstance, ComponentInstance, RouteData } from '../../@types/astro';
import type { DevelopmentEnvironment } from './environment';
export { createRenderContext } from './context.js'; export { createRenderContext } from './context.js';
export type { RenderContext } from './context.js'; export type { RenderContext } from './context.js';
export { tryRenderRoute } from './core.js'; export { tryRenderRoute } from './core.js';
export type { Environment } from './environment'; import type { Environment } from './environment';
export { createEnvironment } from './environment.js'; export { createEnvironment } from './environment.js';
export { getParamsAndProps } from './params-and-props.js'; export { getParamsAndProps } from './params-and-props.js';
export { loadRenderer, loadRenderers } from './renderer.js'; export { loadRenderer } from './renderer.js';
export type { DevelopmentEnvironment };
export type { Environment };
export interface SSROptions { export interface SSROptions {
/** The environment instance */ /** The environment instance */
env: DevelopmentEnvironment; env: Environment;
/** location of file on disk */ /** location of file on disk */
filePath: URL; filePath: URL;
/** the web request (needed for dynamic routes) */ /** the web request (needed for dynamic routes) */

View file

@ -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'; import type { ModuleLoader } from '../module-loader/index.js';
export async function loadRenderers(
settings: AstroSettings,
moduleLoader: ModuleLoader
): Promise<SSRLoadedRenderer[]> {
const renderers = await Promise.all(settings.renderers.map((r) => loadRenderer(r, moduleLoader)));
return renderers.filter(Boolean) as SSRLoadedRenderer[];
}
export async function loadRenderer( export async function loadRenderer(
renderer: AstroRenderer, renderer: AstroRenderer,
moduleLoader: ModuleLoader moduleLoader: ModuleLoader

View file

@ -1,22 +1,22 @@
import type { AstroSettings, ComponentInstance, RouteData } from '../@types/astro'; import type { AstroSettings, ComponentInstance, RouteData } from '../@types/astro';
import { RedirectComponentInstance, routeIsRedirect } from '../core/redirects/index.js'; import { RedirectComponentInstance, routeIsRedirect } from '../core/redirects/index.js';
import type { DevelopmentEnvironment } from '../core/render';
import { preload } from '../vite-plugin-astro-server/index.js'; import { preload } from '../vite-plugin-astro-server/index.js';
import { getPrerenderStatus } from './metadata.js'; import { getPrerenderStatus } from './metadata.js';
import type DevPipeline from '../vite-plugin-astro-server/devPipeline';
type GetSortedPreloadedMatchesParams = { type GetSortedPreloadedMatchesParams = {
env: DevelopmentEnvironment; pipeline: DevPipeline;
matches: RouteData[]; matches: RouteData[];
settings: AstroSettings; settings: AstroSettings;
}; };
export async function getSortedPreloadedMatches({ export async function getSortedPreloadedMatches({
env, pipeline,
matches, matches,
settings, settings,
}: GetSortedPreloadedMatchesParams) { }: GetSortedPreloadedMatchesParams) {
return ( return (
await preloadAndSetPrerenderStatus({ await preloadAndSetPrerenderStatus({
env, pipeline,
matches, matches,
settings, settings,
}) })
@ -24,7 +24,7 @@ export async function getSortedPreloadedMatches({
} }
type PreloadAndSetPrerenderStatusParams = { type PreloadAndSetPrerenderStatusParams = {
env: DevelopmentEnvironment; pipeline: DevPipeline;
matches: RouteData[]; matches: RouteData[];
settings: AstroSettings; settings: AstroSettings;
}; };
@ -36,7 +36,7 @@ type PreloadAndSetPrerenderStatusResult = {
}; };
async function preloadAndSetPrerenderStatus({ async function preloadAndSetPrerenderStatus({
env, pipeline,
matches, matches,
settings, settings,
}: PreloadAndSetPrerenderStatusParams): Promise<PreloadAndSetPrerenderStatusResult[]> { }: PreloadAndSetPrerenderStatusParams): Promise<PreloadAndSetPrerenderStatusResult[]> {
@ -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 // gets the prerender metadata set by the `astro:scanner` vite plugin
const prerenderStatus = getPrerenderStatus({ const prerenderStatus = getPrerenderStatus({
filePath, filePath,
loader: env.loader, loader: pipeline.getModuleLoader(),
}); });
if (prerenderStatus !== undefined) { if (prerenderStatus !== undefined) {

View file

@ -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<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,
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<Response> {
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;
}
}

View file

@ -1,7 +1,7 @@
import type { AstroSettings, RuntimeMode, SSRManifest } from '../@types/astro.js'; import type { AstroSettings, RuntimeMode, SSRManifest } from '../@types/astro.js';
import type { LogOptions } from '../core/logger/core.js'; import type { LogOptions } from '../core/logger/core.js';
import type { ModuleLoader } from '../core/module-loader'; 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 { createEnvironment } from '../core/render/index.js';
import { RouteCache } from '../core/render/route-cache.js'; import { RouteCache } from '../core/render/route-cache.js';
import { isServerLikeOutput } from '../prerender/utils.js'; import { isServerLikeOutput } from '../prerender/utils.js';
@ -12,9 +12,9 @@ export function createDevelopmentEnvironment(
settings: AstroSettings, settings: AstroSettings,
logging: LogOptions, logging: LogOptions,
loader: ModuleLoader loader: ModuleLoader
): DevelopmentEnvironment { ): Environment {
const mode: RuntimeMode = 'development'; const mode: RuntimeMode = 'development';
let env = createEnvironment({ return createEnvironment({
adapterName: manifest.adapterName, adapterName: manifest.adapterName,
logging, logging,
mode, mode,
@ -28,10 +28,4 @@ export function createDevelopmentEnvironment(
ssr: isServerLikeOutput(settings.config), ssr: isServerLikeOutput(settings.config),
streaming: true, streaming: true,
}); });
return {
...env,
loader,
settings,
};
} }

View file

@ -1,26 +1,22 @@
import type { ComponentInstance } from '../@types/astro.js'; import type { ComponentInstance } from '../@types/astro.js';
import { enhanceViteSSRError } from '../core/errors/dev/index.js'; import { enhanceViteSSRError } from '../core/errors/dev/index.js';
import { AggregateError, CSSError, MarkdownError } from '../core/errors/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 { viteID } from '../core/util.js';
import type DevPipeline from './devPipeline';
export async function preload({ export async function preload({
env, pipeline,
filePath, filePath,
}: { }: {
env: DevelopmentEnvironment; pipeline: DevPipeline;
filePath: URL; filePath: URL;
}): Promise<ComponentInstance> { }): Promise<ComponentInstance> {
// Important: This needs to happen first, in case a renderer provides polyfills. // Important: This needs to happen first, in case a renderer provides polyfills.
const renderers = await loadRenderers(env.settings, env.loader); await pipeline.loadRenderers();
// Override the environment's renderers. This ensures that if renderers change (HMR)
// The new instances are passed through.
env.renderers = renderers;
try { try {
// Load the module from the Vite SSR Runtime. // 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; return mod;
} catch (error) { } catch (error) {
@ -29,7 +25,7 @@ export async function preload({
throw error; throw error;
} }
throw enhanceViteSSRError({ error, filePath, loader: env.loader }); throw enhanceViteSSRError({ error, filePath, loader: pipeline.getModuleLoader() });
} }
} }

View file

@ -7,8 +7,8 @@ import { createViteLoader } from '../core/module-loader/index.js';
import { createRouteManifest } from '../core/routing/index.js'; import { createRouteManifest } from '../core/routing/index.js';
import { baseMiddleware } from './base.js'; import { baseMiddleware } from './base.js';
import { createController } from './controller.js'; import { createController } from './controller.js';
import { createDevelopmentEnvironment } from './environment.js';
import { handleRequest } from './request.js'; import { handleRequest } from './request.js';
import DevPipeline from './devPipeline.js';
export interface AstroPluginOptions { export interface AstroPluginOptions {
settings: AstroSettings; settings: AstroSettings;
@ -26,13 +26,13 @@ export default function createVitePluginAstroServer({
configureServer(viteServer) { configureServer(viteServer) {
const loader = createViteLoader(viteServer); const loader = createViteLoader(viteServer);
const manifest = createDevelopmentManifest(settings); 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); let manifestData: ManifestData = createRouteManifest({ settings, fsMod }, logging);
const controller = createController({ loader }); const controller = createController({ loader });
/** rebuild the route cache + manifest, as needed. */ /** rebuild the route cache + manifest, as needed. */
function rebuildManifest(needsManifestRebuild: boolean) { function rebuildManifest(needsManifestRebuild: boolean) {
env.routeCache.clearAll(); pipeline.clearRouteCache();
if (needsManifestRebuild) { if (needsManifestRebuild) {
manifestData = createRouteManifest({ settings }, logging); manifestData = createRouteManifest({ settings }, logging);
} }
@ -57,7 +57,7 @@ export default function createVitePluginAstroServer({
return; return;
} }
handleRequest({ handleRequest({
env, pipeline,
manifestData, manifestData,
controller, controller,
incomingRequest: request, incomingRequest: request,

View file

@ -1,11 +1,8 @@
import type http from 'node:http'; import type http from 'node:http';
import type { ManifestData, SSRManifest } from '../@types/astro'; import type { ManifestData, SSRManifest } from '../@types/astro';
import type { DevelopmentEnvironment } from '../core/render/index';
import type { DevServerController } from './controller'; import type { DevServerController } from './controller';
import { collectErrorMetadata } from '../core/errors/dev/index.js'; import { collectErrorMetadata } from '../core/errors/dev/index.js';
import { createSafeError } from '../core/errors/index.js'; import { createSafeError } from '../core/errors/index.js';
import { error } from '../core/logger/core.js';
import * as msg from '../core/messages.js'; import * as msg from '../core/messages.js';
import { collapseDuplicateSlashes, removeTrailingForwardSlash } from '../core/path.js'; import { collapseDuplicateSlashes, removeTrailingForwardSlash } from '../core/path.js';
import { eventError, telemetry } from '../events/index.js'; import { eventError, telemetry } from '../events/index.js';
@ -13,9 +10,10 @@ import { isServerLikeOutput } from '../prerender/utils.js';
import { runWithErrorHandling } from './controller.js'; import { runWithErrorHandling } from './controller.js';
import { handle500Response } from './response.js'; import { handle500Response } from './response.js';
import { handleRoute, matchRoute } from './route.js'; import { handleRoute, matchRoute } from './route.js';
import type DevPipeline from './devPipeline';
type HandleRequest = { type HandleRequest = {
env: DevelopmentEnvironment; pipeline: DevPipeline;
manifestData: ManifestData; manifestData: ManifestData;
controller: DevServerController; controller: DevServerController;
incomingRequest: http.IncomingMessage; incomingRequest: http.IncomingMessage;
@ -25,15 +23,15 @@ type HandleRequest = {
/** The main logic to route dev server requests to pages in Astro. */ /** The main logic to route dev server requests to pages in Astro. */
export async function handleRequest({ export async function handleRequest({
env, pipeline,
manifestData, manifestData,
controller, controller,
incomingRequest, incomingRequest,
incomingResponse, incomingResponse,
manifest, manifest,
}: HandleRequest) { }: HandleRequest) {
const { settings, loader: moduleLoader } = env; const config = pipeline.getConfig();
const { config } = settings; const moduleLoader = pipeline.getModuleLoader();
const origin = `${moduleLoader.isHttps() ? 'https' : 'http'}://${incomingRequest.headers.host}`; const origin = `${moduleLoader.isHttps() ? 'https' : 'http'}://${incomingRequest.headers.host}`;
const buildingToSSR = isServerLikeOutput(config); const buildingToSSR = isServerLikeOutput(config);
@ -75,7 +73,7 @@ export async function handleRequest({
controller, controller,
pathname, pathname,
async run() { async run() {
const matchedRoute = await matchRoute(pathname, env, manifestData); const matchedRoute = await matchRoute(pathname, manifestData, pipeline);
const resolvedPathname = matchedRoute?.resolvedPathname ?? pathname; const resolvedPathname = matchedRoute?.resolvedPathname ?? pathname;
return await handleRoute({ return await handleRoute({
matchedRoute, matchedRoute,
@ -83,7 +81,7 @@ export async function handleRequest({
pathname: resolvedPathname, pathname: resolvedPathname,
body, body,
origin, origin,
env, pipeline,
manifestData, manifestData,
incomingRequest: incomingRequest, incomingRequest: incomingRequest,
incomingResponse: incomingResponse, 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 // This could be a runtime error from Vite's SSR module, so try to fix it here
try { try {
env.loader.fixStacktrace(err); moduleLoader.fixStacktrace(err);
} catch {} } catch {}
// This is our last line of defense regarding errors where we still might have some information about the request // 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 })); 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); handle500Response(moduleLoader, incomingResponse, errorWithMetadata);
return err; return err;

View file

@ -1,25 +1,15 @@
import mime from 'mime';
import type http from 'node:http'; import type http from 'node:http';
import type { import type {
ComponentInstance, ComponentInstance,
ManifestData, ManifestData,
MiddlewareResponseHandler, MiddlewareEndpointHandler,
RouteData, RouteData,
SSRElement, SSRElement,
SSRManifest, SSRManifest,
} from '../@types/astro'; } from '../@types/astro';
import { attachCookiesToResponse } from '../core/cookies/index.js';
import { AstroErrorData, isAstroError } from '../core/errors/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 { loadMiddleware } from '../core/middleware/loadMiddleware.js';
import { isEndpointResult } from '../core/render/core.js'; import { createRenderContext, getParamsAndProps, type SSROptions } from '../core/render/index.js';
import {
createRenderContext,
getParamsAndProps,
tryRenderRoute,
type DevelopmentEnvironment,
type SSROptions,
} from '../core/render/index.js';
import { createRequest } from '../core/request.js'; import { createRequest } from '../core/request.js';
import { matchAllRoutes } from '../core/routing/index.js'; import { matchAllRoutes } from '../core/routing/index.js';
import { isPage, resolveIdToUrl, viteID } from '../core/util.js'; import { isPage, resolveIdToUrl, viteID } from '../core/util.js';
@ -32,6 +22,7 @@ import { preload } from './index.js';
import { getComponentMetadata } from './metadata.js'; import { getComponentMetadata } from './metadata.js';
import { handle404Response, writeSSRResult, writeWebResponse } from './response.js'; import { handle404Response, writeSSRResult, writeWebResponse } from './response.js';
import { getScriptsForURL } from './scripts.js'; import { getScriptsForURL } from './scripts.js';
import type DevPipeline from './devPipeline.js';
const clientLocalsSymbol = Symbol.for('astro.locals'); const clientLocalsSymbol = Symbol.for('astro.locals');
@ -56,12 +47,17 @@ function getCustom404Route(manifestData: ManifestData): RouteData | undefined {
export async function matchRoute( export async function matchRoute(
pathname: string, pathname: string,
env: DevelopmentEnvironment, manifestData: ManifestData,
manifestData: ManifestData pipeline: DevPipeline
): Promise<MatchedRoute | undefined> { ): Promise<MatchedRoute | undefined> {
const { logging, settings, routeCache } = env; const env = pipeline.getEnvironment();
const { routeCache, logging } = env;
const matches = matchAllRoutes(pathname, manifestData); 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) { for await (const { preloadedComponent, route: maybeRoute, filePath } of preloadedMatches) {
// attempt to get static paths // attempt to get static paths
@ -73,7 +69,7 @@ export async function matchRoute(
routeCache, routeCache,
pathname: pathname, pathname: pathname,
logging, logging,
ssr: isServerLikeOutput(settings.config), ssr: isServerLikeOutput(pipeline.getConfig()),
}); });
return { return {
route: maybeRoute, route: maybeRoute,
@ -96,14 +92,13 @@ export async function matchRoute(
// build formats, and is necessary based on how the manifest tracks build targets. // build formats, and is necessary based on how the manifest tracks build targets.
const altPathname = pathname.replace(/(index)?\.html$/, ''); const altPathname = pathname.replace(/(index)?\.html$/, '');
if (altPathname !== pathname) { if (altPathname !== pathname) {
return await matchRoute(altPathname, env, manifestData); return await matchRoute(altPathname, manifestData, pipeline);
} }
if (matches.length) { if (matches.length) {
const possibleRoutes = matches.flatMap((route) => route.component); const possibleRoutes = matches.flatMap((route) => route.component);
warn( pipeline.logger.warn(
logging,
'getStaticPaths', 'getStaticPaths',
`${AstroErrorData.NoMatchingStaticPathFound.message( `${AstroErrorData.NoMatchingStaticPathFound.message(
pathname pathname
@ -115,8 +110,8 @@ export async function matchRoute(
const custom404 = getCustom404Route(manifestData); const custom404 = getCustom404Route(manifestData);
if (custom404) { if (custom404) {
const filePath = new URL(`./${custom404.component}`, settings.config.root); const filePath = new URL(`./${custom404.component}`, pipeline.getConfig().root);
const preloadedComponent = await preload({ env, filePath }); const preloadedComponent = await preload({ pipeline, filePath });
return { return {
route: custom404, route: custom404,
@ -136,12 +131,12 @@ type HandleRoute = {
pathname: string; pathname: string;
body: ArrayBuffer | undefined; body: ArrayBuffer | undefined;
origin: string; origin: string;
env: DevelopmentEnvironment;
manifestData: ManifestData; manifestData: ManifestData;
incomingRequest: http.IncomingMessage; incomingRequest: http.IncomingMessage;
incomingResponse: http.ServerResponse; incomingResponse: http.ServerResponse;
manifest: SSRManifest; manifest: SSRManifest;
status?: 404 | 500; status?: 404 | 500;
pipeline: DevPipeline;
}; };
export async function handleRoute({ export async function handleRoute({
@ -151,18 +146,21 @@ export async function handleRoute({
status = getStatus(matchedRoute), status = getStatus(matchedRoute),
body, body,
origin, origin,
env, pipeline,
manifestData, manifestData,
incomingRequest, incomingRequest,
incomingResponse, incomingResponse,
manifest, manifest,
}: HandleRoute): Promise<void> { }: HandleRoute): Promise<void> {
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) { if (!matchedRoute) {
return handle404Response(origin, incomingRequest, incomingResponse); return handle404Response(origin, incomingRequest, incomingResponse);
} }
const { config } = settings;
const filePath: URL | undefined = matchedRoute.filePath; const filePath: URL | undefined = matchedRoute.filePath;
const { route, preloadedComponent } = matchedRoute; const { route, preloadedComponent } = matchedRoute;
const buildingToSSR = isServerLikeOutput(config); const buildingToSSR = isServerLikeOutput(config);
@ -192,14 +190,14 @@ export async function handleRoute({
request, request,
route, route,
}; };
const middleware = await loadMiddleware(env.loader, env.settings.config.srcDir); const middleware = await loadMiddleware(moduleLoader, settings.config.srcDir);
if (middleware) { if (middleware) {
options.middleware = middleware; options.middleware = middleware;
} }
const mod = options.preload; const mod = options.preload;
const { scripts, links, styles, metadata } = await getScriptsAndStyles({ const { scripts, links, styles, metadata } = await getScriptsAndStyles({
env: options.env, pipeline,
filePath: options.filePath, filePath: options.filePath,
}); });
@ -214,70 +212,32 @@ export async function handleRoute({
mod, mod,
env, 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); let response = await pipeline.renderRoute(renderContext, mod);
if (isEndpointResult(result, route.type)) { if (response.status === 404) {
if (result.type === 'response') { const fourOhFourRoute = await matchRoute('/404', manifestData, pipeline);
if (result.response.headers.get('X-Astro-Response') === 'Not-Found') { return handleRoute({
const fourOhFourRoute = await matchRoute('/404', env, manifestData); ...options,
return handleRoute({ matchedRoute: fourOhFourRoute,
matchedRoute: fourOhFourRoute, url: new URL(pathname, url),
url: new URL('/404', url), status: 404,
pathname: '/404', body,
status: 404, origin,
body, pipeline,
origin, manifestData,
env, incomingRequest,
manifestData, incomingResponse,
incomingRequest, manifest,
incomingResponse, });
manifest, }
}); if (route.type === 'endpoint') {
} await writeWebResponse(incomingResponse, response);
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);
}
} else { } 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 ( if (
// We are in a recursion, and it's possible that this function is called itself with a status code // 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. // By default, the status code passed via parameters is computed by the matched route.
@ -291,23 +251,26 @@ export async function handleRoute({
return; return;
} else if (status && response.status !== status && (status === 404 || status === 500)) { } else if (status && response.status !== status && (status === 404 || status === 500)) {
// Response.status is read-only, so a clone is required to override // 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); await writeSSRResult(request, response, incomingResponse);
} }
} }
interface GetScriptsAndStylesParams { interface GetScriptsAndStylesParams {
env: DevelopmentEnvironment; pipeline: DevPipeline;
filePath: URL; 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 // 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 // Inject HMR scripts
if (isPage(filePath, env.settings) && env.mode === 'development') { if (isPage(filePath, settings) && mode === 'development') {
scripts.add({ scripts.add({
props: { type: 'module', src: '/@vite/client' }, props: { type: 'module', src: '/@vite/client' },
children: '', children: '',
@ -315,20 +278,20 @@ async function getScriptsAndStyles({ env, filePath }: GetScriptsAndStylesParams)
scripts.add({ scripts.add({
props: { props: {
type: 'module', type: 'module',
src: await resolveIdToUrl(env.loader, 'astro/runtime/client/hmr.js'), src: await resolveIdToUrl(moduleLoader, 'astro/runtime/client/hmr.js'),
}, },
children: '', children: '',
}); });
} }
// TODO: We should allow adding generic HTML elements to the head, not just scripts // 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') { if (script.stage === 'head-inline') {
scripts.add({ scripts.add({
props: {}, props: {},
children: script.content, children: script.content,
}); });
} else if (script.stage === 'page' && isPage(filePath, env.settings)) { } else if (script.stage === 'page' && isPage(filePath, settings)) {
scripts.add({ scripts.add({
props: { type: 'module', src: `/@id/${PAGE_SCRIPT_ID}` }, props: { type: 'module', src: `/@id/${PAGE_SCRIPT_ID}` },
children: '', children: '',
@ -337,7 +300,7 @@ async function getScriptsAndStyles({ env, filePath }: GetScriptsAndStylesParams)
} }
// Pass framework CSS in as style tags to be appended to the page. // 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<SSRElement>(); let links = new Set<SSRElement>();
[...styleUrls].forEach((href) => { [...styleUrls].forEach((href) => {
links.add({ links.add({
@ -364,13 +327,13 @@ async function getScriptsAndStyles({ env, filePath }: GetScriptsAndStylesParams)
props: { props: {
type: 'text/css', type: 'text/css',
// Track the ID so we can match it to Vite's injected style later // 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, children: content,
}); });
}); });
const metadata = await getComponentMetadata(filePath, env.loader); const metadata = await getComponentMetadata(filePath, moduleLoader);
return { scripts, styles, links, metadata }; return { scripts, styles, links, metadata };
} }

View file

@ -12,8 +12,8 @@ import { createContainer } from '../../../dist/core/dev/container.js';
import * as cheerio from 'cheerio'; import * as cheerio from 'cheerio';
import testAdapter from '../../test-adapter.js'; import testAdapter from '../../test-adapter.js';
import { getSortedPreloadedMatches } from '../../../dist/prerender/routing.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 { 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 root = new URL('../../fixtures/alias/', import.meta.url);
const fileSystem = { const fileSystem = {
@ -124,7 +124,7 @@ const fileSystem = {
}; };
describe('Route matching', () => { describe('Route matching', () => {
let env; let pipeline;
let manifestData; let manifestData;
let container; let container;
let settings; let settings;
@ -145,7 +145,7 @@ describe('Route matching', () => {
const loader = createViteLoader(container.viteServer); const loader = createViteLoader(container.viteServer);
const manifest = createDevelopmentManifest(container.settings); const manifest = createDevelopmentManifest(container.settings);
env = createDevelopmentEnvironment(manifest, container.settings, defaultLogging, loader); pipeline = new DevPipeline({ manifest, logging: defaultLogging, settings, loader });
manifestData = createRouteManifest( manifestData = createRouteManifest(
{ {
cwd: fileURLToPath(root), cwd: fileURLToPath(root),
@ -163,7 +163,7 @@ describe('Route matching', () => {
describe('Matched routes', () => { describe('Matched routes', () => {
it('should be sorted correctly', async () => { it('should be sorted correctly', async () => {
const matches = matchAllRoutes('/try-matching-a-route', manifestData); 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); const sortedRouteNames = preloadedMatches.map((match) => match.route.route);
expect(sortedRouteNames).to.deep.equal([ expect(sortedRouteNames).to.deep.equal([
@ -177,7 +177,7 @@ describe('Route matching', () => {
}); });
it('nested should be sorted correctly', async () => { it('nested should be sorted correctly', async () => {
const matches = matchAllRoutes('/nested/try-matching-a-route', manifestData); 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); const sortedRouteNames = preloadedMatches.map((match) => match.route.route);
expect(sortedRouteNames).to.deep.equal([ expect(sortedRouteNames).to.deep.equal([

View file

@ -1,31 +1,35 @@
import { expect } from 'chai'; import { expect } from 'chai';
import { createLoader } from '../../../dist/core/module-loader/index.js'; import { createLoader } from '../../../dist/core/module-loader/index.js';
import { createRouteManifest } from '../../../dist/core/routing/index.js'; import { createRouteManifest } from '../../../dist/core/routing/index.js';
import { createComponent, render } from '../../../dist/runtime/server/index.js'; import { createComponent, render } from '../../../dist/runtime/server/index.js';
import { createController, handleRequest } from '../../../dist/vite-plugin-astro-server/index.js'; import { createController, handleRequest } from '../../../dist/vite-plugin-astro-server/index.js';
import { import {
createAstroModule, createAstroModule,
createBasicEnvironment,
createBasicSettings, createBasicSettings,
createFs, createFs,
createRequestAndResponse, createRequestAndResponse,
defaultLogging, defaultLogging,
} from '../test-utils.js'; } 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 = {}) { async function createDevPipeline(overrides = {}) {
const env = createBasicEnvironment(); const settings = overrides.settings ?? (await createBasicSettings({ root: '/' }));
env.settings = await createBasicSettings({ root: '/' }); const loader = overrides.loader ?? createLoader();
env.settings.renderers = []; const manifest = createDevelopmentManifest(settings);
env.loader = createLoader();
Object.assign(env, overrides); return new DevPipeline({
return env; manifest,
settings,
logging: defaultLogging,
loader,
});
} }
describe('vite-plugin-astro-server', () => { describe('vite-plugin-astro-server', () => {
describe('request', () => { describe('request', () => {
it('renders a request', async () => { it('renders a request', async () => {
const env = await createDevEnvironment({ const pipeline = await createDevPipeline({
loader: createLoader({ loader: createLoader({
import() { import() {
const Page = createComponent(() => { 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 { req, res, text } = createRequestAndResponse();
const fs = createFs( const fs = createFs(
{ {
@ -47,14 +51,14 @@ describe('vite-plugin-astro-server', () => {
const manifestData = createRouteManifest( const manifestData = createRouteManifest(
{ {
fsMod: fs, fsMod: fs,
settings: env.settings, settings: pipeline.getSettings(),
}, },
defaultLogging defaultLogging
); );
try { try {
await handleRequest({ await handleRequest({
env, pipeline,
manifestData, manifestData,
controller, controller,
incomingRequest: req, incomingRequest: req,