diff options
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', |