diff --git a/packages/astro/src/core/app/index.ts b/packages/astro/src/core/app/index.ts index 481fffb410cd..094ac97b56fa 100644 --- a/packages/astro/src/core/app/index.ts +++ b/packages/astro/src/core/app/index.ts @@ -1,8 +1,4 @@ -import type { - ManifestData, - RouteData, - SSRManifest, -} from '../../@types/astro.js'; +import type { ManifestData, RouteData, SSRManifest } from '../../@types/astro.js'; import type { SinglePageBuiltModule } from '../build/types.js'; import { getSetCookiesFromResponse } from '../cookies/index.js'; import { consoleLogDestination } from '../logger/console.js'; @@ -20,7 +16,13 @@ import { matchRoute } from '../routing/match.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 { + clientAddressSymbol, + clientLocalsSymbol, + responseSentSymbol, + REROUTABLE_STATUS_CODES, + REROUTE_DIRECTIVE_HEADER, +} from '../constants.js'; import { AstroError, AstroErrorData } from '../errors/index.js'; export { deserializeManifest } from './common.js'; @@ -124,7 +126,7 @@ export class App { }, serverLike: true, streaming, - }) + }); } set setManifestData(newManifestData: ManifestData) { @@ -294,7 +296,14 @@ export class App { let response; try { - const renderContext = RenderContext.create({ pipeline: this.#pipeline, locals, pathname, request, routeData, status: defaultStatus }) + 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)); @@ -386,7 +395,7 @@ export class App { request, routeData: errorRouteData, status, - }) + }); const response = await renderContext.render(await mod.page()); return this.#mergeResponses(response, originalResponse); } catch { diff --git a/packages/astro/src/core/app/pipeline.ts b/packages/astro/src/core/app/pipeline.ts index 74fa95ec6ad2..b1c615a1eb36 100644 --- a/packages/astro/src/core/app/pipeline.ts +++ b/packages/astro/src/core/app/pipeline.ts @@ -1,14 +1,25 @@ -import type { RouteData, SSRElement, SSRResult } from "../../@types/astro.js"; -import { Pipeline } from "../base-pipeline.js"; -import { createModuleScriptElement, createStylesheetElementSet } from "../render/ssr-element.js"; +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) { - return new AppPipeline(logger, manifest, mode, renderers, resolve, serverLike, streaming); - } + 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 { - const routeInfo = this.manifest.routes.find(route => route.routeData === routeData); + headElements(routeData: RouteData): Pick { + 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(); const scripts = new Set(); @@ -26,7 +37,7 @@ export class AppPipeline extends Pipeline { scripts.add(createModuleScriptElement(script)); } } - return { links, styles, scripts } + return { links, styles, scripts }; } componentMetadata() {} diff --git a/packages/astro/src/core/base-pipeline.ts b/packages/astro/src/core/base-pipeline.ts index 5fcf639032cb..139ee9485811 100644 --- a/packages/astro/src/core/base-pipeline.ts +++ b/packages/astro/src/core/base-pipeline.ts @@ -1,4 +1,11 @@ -import type { MiddlewareHandler, RouteData, RuntimeMode, SSRLoadedRenderer, SSRManifest, SSRResult } from '../@types/astro.js'; +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'; @@ -6,7 +13,7 @@ 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 { @@ -38,13 +45,15 @@ export abstract class Pipeline { /** * Used for `Astro.site`. */ - readonly site = manifest.site, + readonly site = manifest.site ) { - this.internalMiddleware = [ createI18nMiddleware(i18n, manifest.base, manifest.trailingSlash, manifest.buildFormat) ]; + this.internalMiddleware = [ + createI18nMiddleware(i18n, manifest.base, manifest.trailingSlash, manifest.buildFormat), + ]; } - abstract headElements(routeData: RouteData): Promise | HeadElements - abstract componentMetadata(routeData: RouteData): Promise | void + abstract headElements(routeData: RouteData): Promise | HeadElements; + abstract componentMetadata(routeData: RouteData): Promise | void; } // eslint-disable-next-line @typescript-eslint/no-empty-interface diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts index 14462a412060..29a32a88b780 100644 --- a/packages/astro/src/core/build/generate.ts +++ b/packages/astro/src/core/build/generate.ts @@ -499,7 +499,7 @@ async function generatePath( logger, ssr: serverLike, }); - const renderContext = RenderContext.create({ pipeline, pathname, request, routeData: route }) + const renderContext = RenderContext.create({ pipeline, pathname, request, routeData: route }); let body: string | Uint8Array; let response: Response; diff --git a/packages/astro/src/core/build/pipeline.ts b/packages/astro/src/core/build/pipeline.ts index a2647e4564a3..e6e51c87549c 100644 --- a/packages/astro/src/core/build/pipeline.ts +++ b/packages/astro/src/core/build/pipeline.ts @@ -4,8 +4,17 @@ import { BEFORE_HYDRATION_SCRIPT_ID, PAGE_SCRIPT_ID } from '../../vite-plugin-sc import type { SSRManifest } from '../app/types.js'; import { routeIsFallback, routeIsRedirect } from '../redirects/helpers.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 { + createAssetLink, + createModuleScriptsSet, + createStylesheetElementSet, +} from '../render/ssr-element.js'; +import { + getPageDataByComponent, + type BuildInternals, + cssOrder, + mergeInlineCss, +} from './internal.js'; import { ASTRO_PAGE_RESOLVED_MODULE_ID, getVirtualModulePageNameFromPath, @@ -47,10 +56,22 @@ export class BuildPipeline extends Pipeline { } const serverLike = isServerLikeOutput(config); const streaming = true; - super(options.logger, manifest, options.mode, manifest.renderers, resolve, serverLike, streaming) + super( + options.logger, + manifest, + options.mode, + manifest.renderers, + resolve, + serverLike, + streaming + ); } - static create({ internals, manifest, options }: Pick) { + static create({ + internals, + manifest, + options, + }: Pick) { return new BuildPipeline(internals, manifest, options); } @@ -106,17 +127,24 @@ export class BuildPipeline extends Pipeline { } headElements(routeData: RouteData): Pick { - const { internals, manifest: { assetsPrefix, base }, settings } = this + const { + internals, + manifest: { assetsPrefix, base }, + settings, + } = this; const links = new Set(); - const pageBuildData = getPageDataByComponent(internals, routeData.component) + 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 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') { @@ -128,7 +156,7 @@ export class BuildPipeline extends Pipeline { children: '', }); } - + // Add all injected scripts to the page. for (const script of settings.scripts) { if (script.stage === 'head-inline') { @@ -138,7 +166,7 @@ export class BuildPipeline extends Pipeline { }); } } - return { scripts, styles, links } + return { scripts, styles, links }; } componentMetadata() {} diff --git a/packages/astro/src/core/endpoint/index.ts b/packages/astro/src/core/endpoint/index.ts index 7a64366a33df..54b5dff4769d 100644 --- a/packages/astro/src/core/endpoint/index.ts +++ b/packages/astro/src/core/endpoint/index.ts @@ -1,8 +1,4 @@ -import type { - APIContext, - Locales, - Params, -} from '../../@types/astro.js'; +import type { APIContext, Locales, Params } from '../../@types/astro.js'; import { ASTRO_VERSION, clientAddressSymbol, clientLocalsSymbol } from '../constants.js'; import type { AstroCookies } from '../cookies/index.js'; import { AstroError, AstroErrorData } from '../errors/index.js'; @@ -23,7 +19,7 @@ type CreateAPIContext = { routingStrategy: RoutingStrategies | undefined; defaultLocale: string | undefined; route: string; - cookies: AstroCookies + cookies: AstroCookies; }; /** @@ -41,7 +37,7 @@ export function createAPIContext({ routingStrategy, defaultLocale, route, - cookies + cookies, }: CreateAPIContext): APIContext { let preferredLocale: string | undefined = undefined; let preferredLocaleList: string[] | undefined = undefined; diff --git a/packages/astro/src/core/middleware/index.ts b/packages/astro/src/core/middleware/index.ts index b72a13f0a66b..138005f96036 100644 --- a/packages/astro/src/core/middleware/index.ts +++ b/packages/astro/src/core/middleware/index.ts @@ -38,12 +38,16 @@ export type CreateContext = { /** * Creates a context to be passed to Astro middleware `onRequest` function. */ -function createContext({ request, params = {}, userDefinedLocales = [] }: CreateContext): APIContext { +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 + const route = url.pathname; return { cookies: new AstroCookies(request), @@ -61,13 +65,18 @@ function createContext({ request, params = {}, userDefinedLocales = [] }: Create }); }, get preferredLocale(): string | undefined { - return preferredLocale ??= computePreferredLocale(request, userDefinedLocales); + return (preferredLocale ??= computePreferredLocale(request, userDefinedLocales)); }, get preferredLocaleList(): string[] | undefined { - return preferredLocaleList ??= computePreferredLocaleList(request, userDefinedLocales); + return (preferredLocaleList ??= computePreferredLocaleList(request, userDefinedLocales)); }, get currentLocale(): string | undefined { - return currentLocale ??= computeCurrentLocale(route, userDefinedLocales, undefined, undefined); + return (currentLocale ??= computeCurrentLocale( + route, + userDefinedLocales, + undefined, + undefined + )); }, url, get clientAddress() { diff --git a/packages/astro/src/core/redirects/helpers.ts b/packages/astro/src/core/redirects/helpers.ts index a55eacfdf416..80f18ae0f123 100644 --- a/packages/astro/src/core/redirects/helpers.ts +++ b/packages/astro/src/core/redirects/helpers.ts @@ -7,4 +7,3 @@ export function routeIsRedirect(route: RouteData | undefined): route is Redirect export function routeIsFallback(route: RouteData | undefined): route is RedirectRouteData { return route?.type === 'fallback'; } - diff --git a/packages/astro/src/core/redirects/render.ts b/packages/astro/src/core/redirects/render.ts index 08cf908500f7..120fab26e4c8 100644 --- a/packages/astro/src/core/redirects/render.ts +++ b/packages/astro/src/core/redirects/render.ts @@ -1,18 +1,22 @@ import type { RenderContext } from '../render-context.js'; export async function renderRedirect(renderContext: RenderContext) { - const { request: { method }, routeData } = 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) }; + 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; + const { + params, + routeData: { redirect, redirectRoute }, + } = renderContext; if (typeof redirectRoute !== 'undefined') { return redirectRoute?.generate(params) || redirectRoute?.pathname || '/'; diff --git a/packages/astro/src/core/render-context.ts b/packages/astro/src/core/render-context.ts index fd9ed3d1c44f..299c188aff3a 100644 --- a/packages/astro/src/core/render-context.ts +++ b/packages/astro/src/core/render-context.ts @@ -1,4 +1,9 @@ -import type { APIContext, ComponentInstance, MiddlewareHandler, RouteData } from '../@types/astro.js'; +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'; @@ -6,7 +11,12 @@ 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 { + 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 { @@ -26,19 +36,36 @@ export class RenderContext { readonly routeData: RouteData, public status: number, readonly cookies = new AstroCookies(request), - readonly params = getParams(routeData, pathname), + readonly params = getParams(routeData, pathname) ) {} - static create({ locals = {}, middleware, pathname, pipeline, request, routeData, status = 200 }: Pick & Partial>) { - return new RenderContext(pipeline, locals, sequence(...pipeline.internalMiddleware, middleware ?? pipeline.middleware), pathname, request, routeData, status); + static create({ + locals = {}, + middleware, + pathname, + pipeline, + request, + routeData, + status = 200, + }: Pick & + Partial>) { + 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 @@ -47,25 +74,46 @@ export class RenderContext { async render(componentInstance: ComponentInstance | undefined): Promise { 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 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) } - + 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) + response.headers.delete(ROUTE_TYPE_HEADER); } // LEGACY: we put cookies on the response object, // where the adapter might be expecting to read it. @@ -79,11 +127,22 @@ export class RenderContext { 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 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, + cookies, + currentLocale, + generator, + params, + preferredLocale, + preferredLocaleList, + props, + redirect, + request, + site, + url, get clientAddress() { if (clientAddressSymbol in request) { return Reflect.get(request, clientAddressSymbol) as string; @@ -110,39 +169,80 @@ export class RenderContext { // 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 { + 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 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 }); + 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 + #i18nData?: Pick; 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 = { + 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) - } + preferredLocaleList: computePreferredLocaleList(request, locales), + }); } } diff --git a/packages/astro/src/core/render/params-and-props.ts b/packages/astro/src/core/render/params-and-props.ts index ff901cd844e9..b0a589ab1655 100644 --- a/packages/astro/src/core/render/params-and-props.ts +++ b/packages/astro/src/core/render/params-and-props.ts @@ -24,11 +24,10 @@ export async function getProps(opts: GetParamsAndPropsOptions): Promise { return {}; } - if (routeIsRedirect(route) || routeIsFallback(route)) { return {}; } - + // This is a dynamic route, start getting the params const params = getParams(route, pathname); if (mod) { diff --git a/packages/astro/src/i18n/middleware.ts b/packages/astro/src/i18n/middleware.ts index 91091cbec4c3..815ac2e9c8a1 100644 --- a/packages/astro/src/i18n/middleware.ts +++ b/packages/astro/src/i18n/middleware.ts @@ -2,7 +2,7 @@ import { appendForwardSlash, joinPaths } from '@astrojs/internal-helpers/path'; import type { APIContext, Locales, MiddlewareHandler, SSRManifest } from '../@types/astro.js'; import { getPathByLocale, normalizeTheLocale } from './index.js'; import { shouldAppendForwardSlash } from '../core/build/util.js'; -import type { SSRManifestI18n } from '../core/app/types.js' +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. @@ -102,100 +102,100 @@ export function createI18nMiddleware( const type = response.headers.get(ROUTE_TYPE_HEADER); // If the route we're processing is not a page, then we ignore it if (type !== 'page' && type !== 'fallback') { - return response + return response; } const { url, currentLocale } = context; const { locales, defaultLocale, fallback, routing } = i18n; - switch (i18n.routing) { - case 'domains-prefix-other-locales': { - if (localeHasntDomain(i18n, currentLocale)) { - const result = prefixOtherLocales(url, response); - if (result) { - return result; - } - } - break; - } - case 'pathname-prefix-other-locales': { + switch (i18n.routing) { + case 'domains-prefix-other-locales': { + if (localeHasntDomain(i18n, currentLocale)) { const result = prefixOtherLocales(url, response); if (result) { return result; } - break; } - - case 'domains-prefix-always-no-redirect': { - if (localeHasntDomain(i18n, currentLocale)) { - const result = prefixAlwaysNoRedirect(url, response); - if (result) { - return result; - } - } - break; + break; + } + case 'pathname-prefix-other-locales': { + const result = prefixOtherLocales(url, response); + if (result) { + return result; } + break; + } - case 'pathname-prefix-always-no-redirect': { + case 'domains-prefix-always-no-redirect': { + if (localeHasntDomain(i18n, currentLocale)) { const result = prefixAlwaysNoRedirect(url, response); if (result) { return result; } - break; } + break; + } + + case 'pathname-prefix-always-no-redirect': { + const result = prefixAlwaysNoRedirect(url, response); + if (result) { + return result; + } + break; + } - case 'pathname-prefix-always': { + case 'pathname-prefix-always': { + const result = prefixAlways(url, response, context); + if (result) { + return result; + } + break; + } + case 'domains-prefix-always': { + if (localeHasntDomain(i18n, currentLocale)) { const result = prefixAlways(url, response, context); if (result) { return result; } - break; - } - case 'domains-prefix-always': { - if (localeHasntDomain(i18n, currentLocale)) { - const result = prefixAlways(url, response, context); - if (result) { - return result; - } - } - break; } + break; } + } + + if (response.status >= 300 && fallback) { + const fallbackKeys = i18n.fallback ? Object.keys(i18n.fallback) : []; - if (response.status >= 300 && fallback) { - const fallbackKeys = i18n.fallback ? Object.keys(i18n.fallback) : []; - - // we split the URL using the `/`, and then check in the returned array we have the locale - const segments = url.pathname.split('/'); - const urlLocale = segments.find((segment) => { - for (const locale of locales) { - if (typeof locale === 'string') { - if (locale === segment) { - return true; - } - } else if (locale.path === segment) { + // we split the URL using the `/`, and then check in the returned array we have the locale + const segments = url.pathname.split('/'); + const urlLocale = segments.find((segment) => { + for (const locale of locales) { + if (typeof locale === 'string') { + if (locale === segment) { return true; } + } else if (locale.path === segment) { + return true; } - return false; - }); - - if (urlLocale && fallbackKeys.includes(urlLocale)) { - const fallbackLocale = fallback[urlLocale]; - // the user might have configured the locale using the granular locales, so we want to retrieve its corresponding path instead - const pathFallbackLocale = getPathByLocale(fallbackLocale, locales); - let newPathname: string; - // If a locale falls back to the default locale, we want to **remove** the locale because - // the default locale doesn't have a prefix - if (pathFallbackLocale === defaultLocale && routing === 'pathname-prefix-other-locales') { - newPathname = url.pathname.replace(`/${urlLocale}`, ``); - } else { - newPathname = url.pathname.replace(`/${urlLocale}`, `/${pathFallbackLocale}`); - } + } + return false; + }); - return context.redirect(newPathname); + if (urlLocale && fallbackKeys.includes(urlLocale)) { + const fallbackLocale = fallback[urlLocale]; + // the user might have configured the locale using the granular locales, so we want to retrieve its corresponding path instead + const pathFallbackLocale = getPathByLocale(fallbackLocale, locales); + let newPathname: string; + // If a locale falls back to the default locale, we want to **remove** the locale because + // the default locale doesn't have a prefix + if (pathFallbackLocale === defaultLocale && routing === 'pathname-prefix-other-locales') { + newPathname = url.pathname.replace(`/${urlLocale}`, ``); + } else { + newPathname = url.pathname.replace(`/${urlLocale}`, `/${pathFallbackLocale}`); } + + return context.redirect(newPathname); } + } return response; }; diff --git a/packages/astro/src/runtime/server/endpoint.ts b/packages/astro/src/runtime/server/endpoint.ts index 2afee2f23594..55dd5d0aefcf 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 { REROUTABLE_STATUS_CODES, REROUTE_DIRECTIVE_HEADER } from '../../core/constants.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'; diff --git a/packages/astro/src/vite-plugin-astro-server/error.ts b/packages/astro/src/vite-plugin-astro-server/error.ts index 3bfd9f5f996b..d096c0422ea9 100644 --- a/packages/astro/src/vite-plugin-astro-server/error.ts +++ b/packages/astro/src/vite-plugin-astro-server/error.ts @@ -29,10 +29,7 @@ export function recordServerError( telemetry.record(eventError({ cmd: 'dev', err: errorWithMetadata, isFatal: false })); } - logger.error( - null, - formatErrorMessage(errorWithMetadata, logger.level() === 'debug') - ); + logger.error(null, formatErrorMessage(errorWithMetadata, logger.level() === 'debug')); return { error: err, diff --git a/packages/astro/src/vite-plugin-astro-server/pipeline.ts b/packages/astro/src/vite-plugin-astro-server/pipeline.ts index f2a6a1712bcc..bf0a666e37b1 100644 --- a/packages/astro/src/vite-plugin-astro-server/pipeline.ts +++ b/packages/astro/src/vite-plugin-astro-server/pipeline.ts @@ -1,5 +1,13 @@ -import url from 'node:url' -import type { AstroSettings, ComponentInstance, DevToolbarMetadata, RouteData, SSRElement, SSRLoadedRenderer, SSRManifest } from '../@types/astro.js'; +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'; @@ -19,28 +27,38 @@ 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 + override renderers = new Array(); private constructor( readonly loader: ModuleLoader, readonly logger: Logger, readonly manifest: SSRManifest, readonly settings: AstroSettings, - readonly config = settings.config, + readonly config = settings.config ) { - const mode = 'development' + 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) { - return new DevPipeline(loader, logger, manifest, settings) + static create({ + loader, + logger, + manifest, + settings, + }: Pick) { + return new DevPipeline(loader, logger, manifest, settings); } async headElements(routeData: RouteData): Promise { - const { config: { root }, loader, mode, settings } = this; + const { + config: { root }, + loader, + mode, + settings, + } = this; const filePath = new URL(`./${routeData.component}`, root); const { scripts } = await getScriptsForURL(filePath, root, loader); @@ -55,9 +73,9 @@ export class DevPipeline extends Pipeline { settings.config.devToolbar.enabled && (await settings.preferences.get('devToolbar.enabled')) ) { - const src = await resolveIdToUrl(loader, 'astro/runtime/client/dev-toolbar/entrypoint.js') + 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, @@ -69,7 +87,7 @@ export class DevPipeline extends Pipeline { 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') { @@ -99,15 +117,18 @@ export class DevPipeline extends Pipeline { // 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 } + } + + return { scripts, styles, links }; } componentMetadata(routeData: RouteData) { - const { config: { root }, loader } = this; + const { + config: { root }, + loader, + } = this; const filePath = new URL(`./${routeData.component}`, root); - return getComponentMetadata(filePath, loader) + return getComponentMetadata(filePath, loader); } async preload(filePath: URL) { @@ -120,13 +141,13 @@ export class DevPipeline extends Pipeline { try { // Load the module from the Vite SSR Runtime. - return await loader.import(viteID(filePath)) as ComponentInstance; + 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 }); } } diff --git a/packages/astro/src/vite-plugin-astro-server/route.ts b/packages/astro/src/vite-plugin-astro-server/route.ts index ff76d4556132..bc9c5de03072 100644 --- a/packages/astro/src/vite-plugin-astro-server/route.ts +++ b/packages/astro/src/vite-plugin-astro-server/route.ts @@ -1,9 +1,5 @@ import type http from 'node:http'; -import type { - ComponentInstance, - ManifestData, - RouteData, -} from '../@types/astro.js'; +import type { ComponentInstance, ManifestData, RouteData } from '../@types/astro.js'; import { AstroErrorData, isAstroError } from '../core/errors/index.js'; import { req } from '../core/messages.js'; import { loadMiddleware } from '../core/middleware/loadMiddleware.js'; @@ -158,7 +154,7 @@ export async function handleRoute({ let options: SSROptions | undefined = undefined; let route: RouteData; const middleware = (await loadMiddleware(loader)).onRequest; - + if (!matchedRoute) { if (config.i18n) { const locales = config.i18n.locales; @@ -208,7 +204,13 @@ export async function handleRoute({ fallbackRoutes: [], isIndex: false, }; - renderContext = RenderContext.create({ pipeline: pipeline, pathname, middleware, request, routeData: route }); + renderContext = RenderContext.create({ + pipeline: pipeline, + pathname, + middleware, + request, + routeData: route, + }); } else { return handle404Response(origin, incomingRequest, incomingResponse); } @@ -217,7 +219,7 @@ export async function handleRoute({ const { preloadedComponent } = matchedRoute; route = matchedRoute.route; // Allows adapters to pass in locals in dev mode. - const locals = Reflect.get(incomingRequest, clientLocalsSymbol) + const locals = Reflect.get(incomingRequest, clientLocalsSymbol); request = createRequest({ url, // Headers are only available when using SSR. @@ -244,7 +246,14 @@ export async function handleRoute({ }; mod = preloadedComponent; - renderContext = RenderContext.create({ locals, pipeline, pathname, middleware, request, routeData: route }); + renderContext = RenderContext.create({ + locals, + pipeline, + pathname, + middleware, + request, + routeData: route, + }); } let response = await renderContext.render(mod); diff --git a/packages/astro/test/units/render/head.test.js b/packages/astro/test/units/render/head.test.js index 67077c9dd6d7..ce494336afa0 100644 --- a/packages/astro/test/units/render/head.test.js +++ b/packages/astro/test/units/render/head.test.js @@ -21,9 +21,11 @@ describe('core/render', () => { before(async () => { pipeline = createBasicPipeline(); pipeline.headElements = () => ({ - links: new Set([{ name: 'link', props: { rel: 'stylesheet', href: '/main.css' }, children: '' }]), - scripts: new Set, - styles: new Set + links: new Set([ + { name: 'link', props: { rel: 'stylesheet', href: '/main.css' }, children: '' }, + ]), + scripts: new Set(), + styles: new Set(), }); }); @@ -95,7 +97,12 @@ describe('core/render', () => { const PageModule = createAstroModule(Page); const request = new Request('http://example.com/'); - const routeData = { type: 'page', pathname: '/index', component: 'src/pages/index.astro', params: {} }; + 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); @@ -171,7 +178,12 @@ describe('core/render', () => { const PageModule = createAstroModule(Page); const request = new Request('http://example.com/'); - const routeData = { type: 'page', pathname: '/index', component: 'src/pages/index.astro', params: {} }; + 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); @@ -214,7 +226,12 @@ describe('core/render', () => { const PageModule = createAstroModule(Page); const request = new Request('http://example.com/'); - const routeData = { type: 'page', pathname: '/index', component: 'src/pages/index.astro', params: {} }; + 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); diff --git a/packages/astro/test/units/render/jsx.test.js b/packages/astro/test/units/render/jsx.test.js index ca2485a458c5..e2df9a60d50e 100644 --- a/packages/astro/test/units/render/jsx.test.js +++ b/packages/astro/test/units/render/jsx.test.js @@ -43,7 +43,12 @@ describe('core/render', () => { const mod = createAstroModule(Page); const request = new Request('http://example.com/'); - const routeData = { type: 'page', pathname: '/index', component: 'src/pages/index.mdx', params: {} }; + 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); @@ -85,7 +90,12 @@ describe('core/render', () => { const mod = createAstroModule(Page); const request = new Request('http://example.com/'); - const routeData = { type: 'page', pathname: '/index', component: 'src/pages/index.mdx', params: {} }; + 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); @@ -111,7 +121,12 @@ describe('core/render', () => { const mod = createAstroModule(Page); const request = new Request('http://example.com/'); - const routeData = { type: 'page', pathname: '/index', component: 'src/pages/index.mdx', params: {} }; + 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); diff --git a/packages/astro/test/units/test-utils.js b/packages/astro/test/units/test-utils.js index d9d7c7c4e5b8..6133687ae3c3 100644 --- a/packages/astro/test/units/test-utils.js +++ b/packages/astro/test/units/test-utils.js @@ -191,7 +191,7 @@ export function createBasicPipeline(options = {}) { options.manifest ?? {}, options.mode ?? 'development', options.renderers ?? [], - options.resolve ?? (s => Promise.resolve(s)), + options.resolve ?? ((s) => Promise.resolve(s)), options.serverLike ?? true, options.streaming ?? true, options.adapterName, @@ -202,9 +202,9 @@ export function createBasicPipeline(options = {}) { options.routeCache ?? new RouteCache(options.logging, mode), options.site ); - pipeline.headElements = () => ({ scripts: new Set, styles: new Set, links: new Set }); + pipeline.headElements = () => ({ scripts: new Set(), styles: new Set(), links: new Set() }); pipeline.componentMetadata = () => {}; - return pipeline + 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 36f05c41a877..63d2d2a5f21c 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 @@ -29,7 +29,7 @@ describe('vite-plugin-astro-server', () => { loader: createLoader({ import(id) { if (id === '\0astro-internal:middleware') { - return { onRequest: (_, next) => next() } + return { onRequest: (_, next) => next() }; } const Page = createComponent(() => { return render`
testing
`; @@ -62,7 +62,7 @@ describe('vite-plugin-astro-server', () => { controller, incomingRequest: req, incomingResponse: res, - manifest: {} + manifest: {}, }); } catch (err) { assert.equal(err.message, undefined);