mirror of
https://github.com/withastro/astro.git
synced 2025-03-24 23:21:57 -05:00
refactor: pipeline lifetime (#9795)
* Base Environment * SSRRoutePipeline -> AppEnvironment * BuildPipeline -> BuildEnvironment * DevPipeline -> DevEnvironment * per-request pipeline * internal middleware: i18n * delete callEndpoint * pipeline access for internal middleware * Address review comments `environment.ts` is now lives directly in `src/core`, rather than `src/core/render`. `environment.createPipeline` is removed. `Pipeline.create` is used instead. Constructors with positional arguments are replaced by `Environment.create` with named arguments. Clarifies the use of `HiddenPipeline`. * migrate some of `RenderContext`'s responsibilities to `Pipeline` * delete renderPage * RenderContext.params -> Pipeline.params * delete `RenderContext` * `Pipeline` -> `RenderContext` * `Environment` -> `Pipeline` * `AppEnvironment` -> `AppPipeline` * `BuildEnvironment` -> `BuildPipeline` * `DevEnvironment` -> `DevPipeline` * provide locals directly to renderContext * add changeset
This commit is contained in:
parent
ea990a5614
commit
5acc3135ba
46 changed files with 831 additions and 1356 deletions
5
.changeset/hungry-rings-argue.md
Normal file
5
.changeset/hungry-rings-argue.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"astro": patch
|
||||
---
|
||||
|
||||
Refactors internals relating to middleware, endpoints, and page rendering.
|
|
@ -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 };
|
||||
|
|
|
@ -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, {
|
||||
|
@ -396,72 +351,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.
|
||||
|
|
33
packages/astro/src/core/app/pipeline.ts
Normal file
33
packages/astro/src/core/app/pipeline.ts
Normal file
|
@ -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() {}
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
import { Pipeline } from '../pipeline.js';
|
||||
|
||||
export class SSRRoutePipeline extends Pipeline {}
|
51
packages/astro/src/core/base-pipeline.ts
Normal file
51
packages/astro/src/core/base-pipeline.ts
Normal file
|
@ -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'> {}
|
|
@ -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 });
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
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)
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
32
packages/astro/src/core/redirects/render.ts
Normal file
32
packages/astro/src/core/redirects/render.ts
Normal file
|
@ -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;
|
||||
}
|
148
packages/astro/src/core/render-context.ts
Normal file
148
packages/astro/src/core/render-context.ts
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -2,27 +2,6 @@ import type { GetStaticPathsItem, Params, RouteData } from '../../@types/astro.j
|
|||
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
|
||||
|
|
|
@ -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);
|
||||
// If the route we're processing is not a page, then we ignore it
|
||||
if (routeData?.type !== 'page' && routeData?.type !== 'fallback') {
|
||||
return await next();
|
||||
}
|
||||
const currentLocale = context.currentLocale;
|
||||
|
||||
const url = context.url;
|
||||
const { locales, defaultLocale, fallback, routing } = i18n;
|
||||
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 (type !== 'page' && type !== 'fallback') {
|
||||
return response
|
||||
}
|
||||
|
||||
const { url, currentLocale } = context;
|
||||
const { locales, defaultLocale, fallback, routing } = i18n;
|
||||
|
||||
if (response instanceof Response) {
|
||||
switch (i18n.routing) {
|
||||
case 'domains-prefix-other-locales': {
|
||||
if (localeHasntDomain(i18n, currentLocale)) {
|
||||
|
@ -207,19 +196,11 @@ 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
|
||||
|
|
|
@ -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
|
|
@ -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) {
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
export const REROUTE_DIRECTIVE_HEADER = 'X-Astro-Reroute';
|
|
@ -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
|
||||
|
|
|
@ -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() {}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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';
|
||||
|
|
137
packages/astro/src/vite-plugin-astro-server/pipeline.ts
Normal file
137
packages/astro/src/vite-plugin-astro-server/pipeline.ts
Normal file
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
mod = preloadedComponent;
|
||||
renderContext = RenderContext.create({ locals, pipeline, pathname, middleware, request, routeData: route });
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { defineConfig} from "astro/config";
|
||||
import { defineConfig } from "astro/config";
|
||||
|
||||
export default defineConfig({
|
||||
i18n: {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 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);
|
||||
|
||||
|
@ -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);
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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',
|
||||
|
|
Loading…
Add table
Reference in a new issue