refactor: build pipeline (#8088)

* refactor: build pipeline

* refactor: build pipeline

* fix: manifest not extensible and correct directory for renderers.mjs

* fix: correctly resolve output directory

* fix: correctly compute encoding and body

* chore: update documentation

* refactor: change how tests are run

* refactor: fix test regressions!!
This commit is contained in:
Emanuele Stoppa 2023-08-16 16:45:21 +01:00 committed by GitHub
parent 788825bd8b
commit ca4cf01100
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 690 additions and 401 deletions

View file

@ -703,6 +703,7 @@ async function tryToInstallIntegrations({
} catch (err) {
spinner.fail();
debug('add', 'Error installing dependencies', err);
// eslint-disable-next-line no-console
console.error('\n', (err as any).stdout, '\n');
return UpdateResult.failure;
}

View file

@ -16,7 +16,7 @@ export class EndpointNotFoundError extends Error {
}
export class SSRRoutePipeline extends Pipeline {
encoder = new TextEncoder();
#encoder = new TextEncoder();
constructor(env: Environment) {
super(env);
@ -40,7 +40,7 @@ export class SSRRoutePipeline extends Pipeline {
headers.set('Content-Type', 'text/plain;charset=utf-8');
}
const bytes =
response.encoding !== 'binary' ? this.encoder.encode(response.body) : response.body;
response.encoding !== 'binary' ? this.#encoder.encode(response.body) : response.body;
headers.set('Content-Length', bytes.byteLength.toString());
const newResponse = new Response(bytes, {

View file

@ -0,0 +1,211 @@
import { Pipeline } from '../pipeline.js';
import type { BuildInternals } from './internal';
import type { PageBuildData, StaticBuildOptions } from './types';
import { ASTRO_PAGE_RESOLVED_MODULE_ID } from './plugins/plugin-pages.js';
import { RESOLVED_SPLIT_MODULE_ID } from './plugins/plugin-ssr.js';
import { ASTRO_PAGE_EXTENSION_POST_PATTERN } from './plugins/util.js';
import type { SSRManifest } from '../app/types';
import type { AstroConfig, AstroSettings, RouteType, SSRLoadedRenderer } from '../../@types/astro';
import { getOutputDirectory, isServerLikeOutput } from '../../prerender/utils.js';
import type { EndpointCallResult } from '../endpoint';
import { createEnvironment } from '../render/index.js';
import { BEFORE_HYDRATION_SCRIPT_ID } from '../../vite-plugin-scripts/index.js';
import { createAssetLink } from '../render/ssr-element.js';
import type { BufferEncoding } from 'vfile';
/**
* This 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;
#currentEndpointBody?: {
body: string | Uint8Array;
encoding: BufferEncoding;
};
constructor(
staticBuildOptions: StaticBuildOptions,
internals: BuildInternals,
manifest: SSRManifest
) {
const ssr = isServerLikeOutput(staticBuildOptions.settings.config);
super(
createEnvironment({
adapterName: manifest.adapterName,
logging: staticBuildOptions.logging,
mode: staticBuildOptions.mode,
renderers: manifest.renderers,
clientDirectives: manifest.clientDirectives,
compressHTML: manifest.compressHTML,
async resolve(specifier: string) {
const hashedFilePath = manifest.entryModules[specifier];
if (typeof hashedFilePath !== 'string') {
// 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) {
return '';
}
throw new Error(`Cannot find the built path for ${specifier}`);
}
return createAssetLink(hashedFilePath, manifest.base, manifest.assetsPrefix);
},
routeCache: staticBuildOptions.routeCache,
site: manifest.site,
ssr,
streaming: true,
})
);
this.#internals = internals;
this.#staticBuildOptions = staticBuildOptions;
this.#manifest = manifest;
this.setEndpointHandler(this.#handleEndpointResult);
}
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;
}
/**
* The SSR build emits two important files:
* - dist/server/manifest.mjs
* - dist/renderers.mjs
*
* These two files, put together, will be used to generate the pages.
*
* ## Errors
*
* It will throw errors if the previous files can't be found in the file system.
*
* @param staticBuildOptions
*/
static async retrieveManifest(
staticBuildOptions: StaticBuildOptions,
internals: BuildInternals
): Promise<SSRManifest> {
const config = staticBuildOptions.settings.config;
const baseDirectory = getOutputDirectory(config);
const manifestEntryUrl = new URL(
`${internals.manifestFileName}?time=${Date.now()}`,
baseDirectory
);
const { manifest } = await import(manifestEntryUrl.toString());
if (!manifest) {
throw new Error(
"Astro couldn't find the emitted manifest. This is an internal error, please file an issue."
);
}
const renderersEntryUrl = new URL(`renderers.mjs?time=${Date.now()}`, baseDirectory);
const renderers = await import(renderersEntryUrl.toString());
if (!renderers) {
throw new Error(
"Astro couldn't find the emitted renderers. This is an internal error, please file an issue."
);
}
return {
...manifest,
renderers: renderers.renderers as SSRLoadedRenderer[],
};
}
/**
* It collects the routes to generate during the build.
*
* It returns a map of page information and their relative entry point as a string.
*/
retrieveRoutesToGenerate(): Map<PageBuildData, string> {
const pages = new Map<PageBuildData, string>();
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
if (
entryPoint.includes(ASTRO_PAGE_RESOLVED_MODULE_ID) ||
entryPoint.includes(RESOLVED_SPLIT_MODULE_ID)
) {
const [, pageName] = entryPoint.split(':');
const pageData = this.#internals.pagesByComponent.get(
`${pageName.replace(ASTRO_PAGE_EXTENSION_POST_PATTERN, '.')}`
);
if (!pageData) {
throw new Error(
"Build failed. Astro couldn't find the emitted page from " + pageName + ' pattern'
);
}
pages.set(pageData, filePath);
}
}
for (const [path, pageData] of this.#internals.pagesByComponent.entries()) {
if (pageData.route.type === 'redirect') {
pages.set(pageData, path);
}
}
return pages;
}
async #handleEndpointResult(request: Request, response: EndpointCallResult): Promise<Response> {
if (response.type === 'response') {
if (!response.response.body) {
return new Response(null);
}
const ab = await response.response.arrayBuffer();
const body = new Uint8Array(ab);
this.#currentEndpointBody = {
body: body,
encoding: 'utf-8',
};
return response.response;
} else {
if (response.encoding) {
this.#currentEndpointBody = {
body: response.body,
encoding: response.encoding,
};
const headers = new Headers();
headers.set('X-Astro-Encoding', response.encoding);
return new Response(response.body, {
headers,
});
} else {
return new Response(response.body);
}
}
}
async computeBodyAndEncoding(
routeType: RouteType,
response: Response
): Promise<{
body: string | Uint8Array;
encoding: BufferEncoding;
}> {
const encoding = response.headers.get('X-Astro-Encoding') ?? 'utf-8';
if (this.#currentEndpointBody) {
const currentEndpointBody = this.#currentEndpointBody;
this.#currentEndpointBody = undefined;
return currentEndpointBody;
} else {
return { body: await response.text(), encoding: encoding as BufferEncoding };
}
}
}

View file

@ -9,7 +9,7 @@ import type {
ComponentInstance,
GetStaticPathsItem,
ImageTransform,
MiddlewareHandler,
MiddlewareEndpointHandler,
RouteData,
RouteType,
SSRError,
@ -20,12 +20,7 @@ import {
generateImage as generateImageInternal,
getStaticImageList,
} from '../../assets/generate.js';
import {
eachPageDataFromEntryPoint,
eachRedirectPageData,
hasPrerenderedPages,
type BuildInternals,
} from '../../core/build/internal.js';
import { hasPrerenderedPages, type BuildInternals } from '../../core/build/internal.js';
import {
isRelativePath,
joinPaths,
@ -34,13 +29,12 @@ import {
removeTrailingForwardSlash,
} from '../../core/path.js';
import { runHookBuildGenerated } from '../../integrations/index.js';
import { isServerLikeOutput } from '../../prerender/utils.js';
import { BEFORE_HYDRATION_SCRIPT_ID, PAGE_SCRIPT_ID } from '../../vite-plugin-scripts/index.js';
import { getOutputDirectory, isServerLikeOutput } from '../../prerender/utils.js';
import { PAGE_SCRIPT_ID } from '../../vite-plugin-scripts/index.js';
import { AstroError, AstroErrorData } from '../errors/index.js';
import { debug, info } from '../logger/core.js';
import { debug, info, Logger } from '../logger/core.js';
import { RedirectSinglePageBuiltModule, getRedirectLocationOrThrow } from '../redirects/index.js';
import { isEndpointResult } from '../render/core.js';
import { createEnvironment, createRenderContext, tryRenderRoute } from '../render/index.js';
import { createRenderContext } from '../render/index.js';
import { callGetStaticPaths } from '../render/route-cache.js';
import {
createAssetLink,
@ -64,6 +58,8 @@ import type {
StylesheetAsset,
} from './types';
import { getTimeStat } from './util.js';
import { BuildPipeline } from './buildPipeline.js';
import type { BufferEncoding } from 'vfile';
function createEntryURL(filePath: string, outFolder: URL) {
return new URL('./' + filePath + `?time=${Date.now()}`, outFolder);
@ -125,8 +121,23 @@ export function chunkIsPage(
}
export async function generatePages(opts: StaticBuildOptions, internals: BuildInternals) {
const logger = new Logger(opts.logging);
const timer = performance.now();
const ssr = isServerLikeOutput(opts.settings.config);
let manifest: SSRManifest;
if (ssr) {
manifest = await BuildPipeline.retrieveManifest(opts, internals);
} else {
const baseDirectory = getOutputDirectory(opts.settings.config);
const renderersEntryUrl = new URL('renderers.mjs', baseDirectory);
const renderers = await import(renderersEntryUrl.toString());
manifest = createBuildManifest(
opts.settings,
internals,
renderers.renderers as SSRLoadedRenderer[]
);
}
const buildPipeline = new BuildPipeline(opts, internals, manifest);
const outFolder = ssr
? opts.settings.config.build.server
: getOutDirWithinCwd(opts.settings.config.outDir);
@ -140,20 +151,18 @@ export async function generatePages(opts: StaticBuildOptions, internals: BuildIn
const verb = ssr ? 'prerendering' : 'generating';
info(opts.logging, null, `\n${bgGreen(black(` ${verb} static routes `))}`);
const builtPaths = new Set<string>();
const pagesToGenerate = buildPipeline.retrieveRoutesToGenerate();
if (ssr) {
for (const [pageData, filePath] of eachPageDataFromEntryPoint(internals)) {
for (const [pageData, filePath] of pagesToGenerate) {
if (pageData.route.prerender) {
const ssrEntryURLPage = createEntryURL(filePath, outFolder);
const ssrEntryPage = await import(ssrEntryURLPage.toString());
if (opts.settings.config.build.split) {
// forcing to use undefined, so we fail in an expected way if the module is not even there.
const manifest: SSRManifest | undefined = ssrEntryPage.manifest;
const ssrEntry = manifest?.pageModule;
const ssrEntry = ssrEntryPage?.manifest?.pageModule;
if (ssrEntry) {
await generatePage(opts, internals, pageData, ssrEntry, builtPaths, manifest);
await generatePage(pageData, ssrEntry, builtPaths, buildPipeline, logger);
} else {
throw new Error(
`Unable to find the manifest for the module ${ssrEntryURLPage.toString()}. This is unexpected and likely a bug in Astro, please report.`
@ -161,28 +170,25 @@ export async function generatePages(opts: StaticBuildOptions, internals: BuildIn
}
} else {
const ssrEntry = ssrEntryPage as SinglePageBuiltModule;
const manifest = createBuildManifest(opts.settings, internals, ssrEntry.renderers);
await generatePage(opts, internals, pageData, ssrEntry, builtPaths, manifest);
await generatePage(pageData, ssrEntry, builtPaths, buildPipeline, logger);
}
}
}
for (const pageData of eachRedirectPageData(internals)) {
const entry = await getEntryForRedirectRoute(pageData.route, internals, outFolder);
const manifest = createBuildManifest(opts.settings, internals, entry.renderers);
await generatePage(opts, internals, pageData, entry, builtPaths, manifest);
if (pageData.route.type === 'redirect') {
const entry = await getEntryForRedirectRoute(pageData.route, internals, outFolder);
await generatePage(pageData, entry, builtPaths, buildPipeline, logger);
}
}
} else {
for (const [pageData, filePath] of eachPageDataFromEntryPoint(internals)) {
const ssrEntryURLPage = createEntryURL(filePath, outFolder);
const entry: SinglePageBuiltModule = await import(ssrEntryURLPage.toString());
const manifest = createBuildManifest(opts.settings, internals, entry.renderers);
for (const [pageData, filePath] of pagesToGenerate) {
if (pageData.route.type === 'redirect') {
const entry = await getEntryForRedirectRoute(pageData.route, internals, outFolder);
await generatePage(pageData, entry, builtPaths, buildPipeline, logger);
} else {
const ssrEntryURLPage = createEntryURL(filePath, outFolder);
const entry: SinglePageBuiltModule = await import(ssrEntryURLPage.toString());
await generatePage(opts, internals, pageData, entry, builtPaths, manifest);
}
for (const pageData of eachRedirectPageData(internals)) {
const entry = await getEntryForRedirectRoute(pageData.route, internals, outFolder);
const manifest = createBuildManifest(opts.settings, internals, entry.renderers);
await generatePage(opts, internals, pageData, entry, builtPaths, manifest);
await generatePage(pageData, entry, builtPaths, buildPipeline, logger);
}
}
}
@ -219,16 +225,15 @@ async function generateImage(opts: StaticBuildOptions, transform: ImageTransform
}
async function generatePage(
opts: StaticBuildOptions,
internals: BuildInternals,
pageData: PageBuildData,
ssrEntry: SinglePageBuiltModule,
builtPaths: Set<string>,
manifest: SSRManifest
pipeline: BuildPipeline,
logger: Logger
) {
let timeStart = performance.now();
const pageInfo = getPageDataByComponent(internals, pageData.route.component);
const pageInfo = getPageDataByComponent(pipeline.getInternals(), pageData.route.component);
// may be used in the future for handling rel=modulepreload, rel=icon, rel=manifest etc.
const linkIds: [] = [];
@ -240,6 +245,9 @@ async function generatePage(
const pageModulePromise = ssrEntry.page;
const onRequest = ssrEntry.onRequest;
if (onRequest) {
pipeline.setMiddlewareFunction(onRequest as MiddlewareEndpointHandler);
}
if (!pageModulePromise) {
throw new Error(
@ -247,14 +255,13 @@ async function generatePage(
);
}
const pageModule = await pageModulePromise();
if (shouldSkipDraft(pageModule, opts.settings)) {
info(opts.logging, null, `${magenta('⚠️')} Skipping draft ${pageData.route.component}`);
if (shouldSkipDraft(pageModule, pipeline.getSettings())) {
logger.info(null, `${magenta('⚠️')} Skipping draft ${pageData.route.component}`);
return;
}
const generationOptions: Readonly<GeneratePathOptions> = {
pageData,
internals,
linkIds,
scripts,
styles,
@ -263,23 +270,28 @@ async function generatePage(
const icon = pageData.route.type === 'page' ? green('▶') : magenta('λ');
if (isRelativePath(pageData.route.component)) {
info(opts.logging, null, `${icon} ${pageData.route.route}`);
logger.info(null, `${icon} ${pageData.route.route}`);
} else {
info(opts.logging, null, `${icon} ${pageData.route.component}`);
logger.info(null, `${icon} ${pageData.route.component}`);
}
// Get paths for the route, calling getStaticPaths if needed.
const paths = await getPathsForRoute(pageData, pageModule, opts, builtPaths);
const paths = await getPathsForRoute(
pageData,
pageModule,
pipeline.getStaticBuildOptions(),
builtPaths
);
for (let i = 0; i < paths.length; i++) {
const path = paths[i];
await generatePath(path, opts, generationOptions, manifest, onRequest);
await generatePath(path, generationOptions, pipeline);
const timeEnd = performance.now();
const timeChange = getTimeStat(timeStart, timeEnd);
const timeIncrease = `(+${timeChange})`;
const filePath = getOutputFilename(opts.settings.config, path, pageData.route.type);
const filePath = getOutputFilename(pipeline.getConfig(), path, pageData.route.type);
const lineIcon = i === paths.length - 1 ? '└─' : '├─';
info(opts.logging, null, ` ${cyan(lineIcon)} ${dim(filePath)} ${dim(timeIncrease)}`);
logger.info(null, ` ${cyan(lineIcon)} ${dim(filePath)} ${dim(timeIncrease)}`);
}
}
@ -382,7 +394,6 @@ function getInvalidRouteSegmentError(
interface GeneratePathOptions {
pageData: PageBuildData;
internals: BuildInternals;
linkIds: string[];
scripts: { type: 'inline' | 'external'; value: string } | null;
styles: StylesheetAsset[];
@ -446,19 +457,13 @@ function getUrlForPath(
return url;
}
async function generatePath(
pathname: string,
opts: StaticBuildOptions,
gopts: GeneratePathOptions,
manifest: SSRManifest,
onRequest?: MiddlewareHandler<unknown>
) {
const { settings, logging, origin, routeCache } = opts;
const { mod, internals, scripts: hoistedScripts, styles: _styles, pageData } = gopts;
async function generatePath(pathname: string, gopts: GeneratePathOptions, pipeline: BuildPipeline) {
const manifest = pipeline.getManifest();
const { mod, scripts: hoistedScripts, styles: _styles, pageData } = gopts;
// This adds the page name to the array so it can be shown as part of stats.
if (pageData.route.type === 'page') {
addPageName(pathname, opts);
addPageName(pathname, pipeline.getStaticBuildOptions());
}
debug('build', `Generating: ${pathname}`);
@ -472,8 +477,8 @@ async function generatePath(
);
const styles = createStylesheetElementSet(_styles, manifest.base, manifest.assetsPrefix);
if (settings.scripts.some((script) => script.stage === 'page')) {
const hashedFilePath = internals.entrySpecifierToBundleMap.get(PAGE_SCRIPT_ID);
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}`);
}
@ -485,7 +490,7 @@ async function generatePath(
}
// Add all injected scripts to the page.
for (const script of settings.scripts) {
for (const script of pipeline.getSettings().scripts) {
if (script.stage === 'head-inline') {
scripts.add({
props: {},
@ -494,58 +499,38 @@ async function generatePath(
}
}
const ssr = isServerLikeOutput(settings.config);
const ssr = isServerLikeOutput(pipeline.getConfig());
const url = getUrlForPath(
pathname,
opts.settings.config.base,
origin,
opts.settings.config.build.format,
pipeline.getConfig().base,
pipeline.getStaticBuildOptions().origin,
pipeline.getConfig().build.format,
pageData.route.type
);
const env = createEnvironment({
adapterName: manifest.adapterName,
logging,
mode: opts.mode,
renderers: manifest.renderers,
clientDirectives: manifest.clientDirectives,
compressHTML: manifest.compressHTML,
async resolve(specifier: string) {
const hashedFilePath = manifest.entryModules[specifier];
if (typeof hashedFilePath !== 'string') {
// 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) {
return '';
}
throw new Error(`Cannot find the built path for ${specifier}`);
}
return createAssetLink(hashedFilePath, manifest.base, manifest.assetsPrefix);
},
routeCache,
site: manifest.site,
ssr,
streaming: true,
});
const renderContext = await createRenderContext({
pathname,
request: createRequest({ url, headers: new Headers(), logging, ssr }),
request: createRequest({
url,
headers: new Headers(),
logging: pipeline.getStaticBuildOptions().logging,
ssr,
}),
componentMetadata: manifest.componentMetadata,
scripts,
styles,
links,
route: pageData.route,
env,
env: pipeline.getEnvironment(),
mod,
});
let body: string | Uint8Array;
let encoding: BufferEncoding | undefined;
let response;
let response: Response;
try {
response = await tryRenderRoute(renderContext, env, mod, onRequest);
response = await pipeline.renderRoute(renderContext, mod);
} catch (err) {
if (!AstroError.is(err) && !(err as SSRError).id && typeof err === 'object') {
(err as SSRError).id = pageData.component;
@ -553,28 +538,17 @@ async function generatePath(
throw err;
}
if (isEndpointResult(response, pageData.route.type)) {
if (response.type === 'response') {
// If there's no body, do nothing
if (!response.response.body) return;
const ab = await response.response.arrayBuffer();
body = new Uint8Array(ab);
} else {
body = response.body;
encoding = response.encoding;
if (response.status >= 300 && response.status < 400) {
// If redirects is set to false, don't output the HTML
if (!pipeline.getConfig().build.redirects) {
return;
}
} else {
if (response.status >= 300 && response.status < 400) {
// If redirects is set to false, don't output the HTML
if (!opts.settings.config.build.redirects) {
return;
}
const location = getRedirectLocationOrThrow(response.headers);
const fromPath = new URL(renderContext.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;
body = `<!doctype html>
const location = getRedirectLocationOrThrow(response.headers);
const fromPath = new URL(renderContext.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;
body = `<!doctype html>
<title>Redirecting to: ${location}</title>
<meta http-equiv="refresh" content="${delay};url=${location}">
<meta name="robots" content="noindex">
@ -582,20 +556,25 @@ async function generatePath(
<body>
<a href="${location}">Redirecting from <code>${fromPath}</code> to <code>${location}</code></a>
</body>`;
// A dynamic redirect, set the location so that integrations know about it.
if (pageData.route.type !== 'redirect') {
pageData.route.redirect = location;
}
} else {
// If there's no body, do nothing
if (!response.body) return;
body = await response.text();
// A dynamic redirect, set the location so that integrations know about it.
if (pageData.route.type !== 'redirect') {
pageData.route.redirect = location;
}
} else {
// If there's no body, do nothing
if (!response.body) return;
const result = await pipeline.computeBodyAndEncoding(renderContext.route.type, response);
body = result.body;
encoding = result.encoding;
}
const outFolder = getOutFolder(settings.config, pathname, pageData.route.type);
const outFile = getOutFile(settings.config, outFolder, pathname, pageData.route.type);
const outFolder = getOutFolder(pipeline.getConfig(), pathname, pageData.route.type);
const outFile = getOutFile(pipeline.getConfig(), outFolder, pathname, pageData.route.type);
pageData.route.distURL = outFile;
const possibleEncoding = response.headers.get('X-Astro-Encoding');
if (possibleEncoding) {
encoding = possibleEncoding as BufferEncoding;
}
await fs.promises.mkdir(outFolder, { recursive: true });
await fs.promises.writeFile(outFile, body, encoding ?? 'utf-8');
}

View file

@ -85,6 +85,9 @@ export interface BuildInternals {
staticFiles: Set<string>;
// The SSR entry chunk. Kept in internals to share between ssr/client build steps
ssrEntryChunk?: Rollup.OutputChunk;
// The SSR manifest entry chunk.
manifestEntryChunk?: Rollup.OutputChunk;
manifestFileName?: string;
entryPoints: Map<RouteData, URL>;
ssrSplitEntryChunks: Map<string, Rollup.OutputChunk>;
componentMetadata: SSRResult['componentMetadata'];
@ -227,14 +230,6 @@ export function* eachPageData(internals: BuildInternals) {
yield* internals.pagesByComponent.values();
}
export function* eachRedirectPageData(internals: BuildInternals) {
for (const pageData of eachPageData(internals)) {
if (pageData.route.type === 'redirect') {
yield pageData;
}
}
}
export function* eachPageDataFromEntryPoint(
internals: BuildInternals
): Generator<[PageBuildData, string]> {

View file

@ -125,10 +125,13 @@ will look like this:
Of course, all these files will be deleted by Astro at the end build.
## `plugin-ssr` (WIP)
## `plugin-ssr`
This plugin is responsible to create a single `entry.mjs` file that will be used
in SSR.
This plugin is responsible to create the JS files that will be executed in SSR.
### Classic mode
The plugin will emit a single entry point called `entry.mjs`.
This plugin **will emit code** only when building an **SSR** site.
@ -146,4 +149,24 @@ const pageMap = new Map([
```
It will also import the [`renderers`](#plugin-renderers) virtual module
and the [`middleware`](#plugin-middleware) virtual module.
and the [`manifest`](#plugin-manifest) virtual module.
### Split mode
The plugin will emit various entry points. Each route will be an entry point.
Each entry point will contain the necessary code to **render one single route**.
Each entry point will also import the [`renderers`](#plugin-renderers) virtual module
and the [`manifest`](#plugin-manifest) virtual module.
## `plugin-manifest`
This plugin is responsible to create a file called `manifest.mjs`. In SSG, the file is saved
in `config.outDir`, in SSR the file is saved in `config.build.server`.
This file is important to do two things:
- generate the pages during the SSG;
- render the pages in SSR;
The file contains all the information needed to Astro to accomplish the operations mentioned above.

View file

@ -12,12 +12,14 @@ import { pluginPages } from './plugin-pages.js';
import { pluginPrerender } from './plugin-prerender.js';
import { pluginRenderers } from './plugin-renderers.js';
import { pluginSSR, pluginSSRSplit } from './plugin-ssr.js';
import { pluginManifest } from './plugin-manifest.js';
export function registerAllPlugins({ internals, options, register }: AstroBuildPluginContainer) {
register(pluginComponentEntry(internals));
register(pluginAliasResolve(internals));
register(pluginAnalyzer(options, internals));
register(pluginInternals(internals));
register(pluginManifest(options, internals));
register(pluginRenderers(options));
register(pluginMiddleware(options, internals));
register(pluginPages(options, internals));

View file

@ -0,0 +1,251 @@
import { type BuildInternals, cssOrder, mergeInlineCss } from '../internal.js';
import type { AstroBuildPlugin } from '../plugin';
import { type Plugin as VitePlugin } from 'vite';
import { runHookBuildSsr } from '../../../integrations/index.js';
import { addRollupInput } from '../add-rollup-input.js';
import glob from 'fast-glob';
import { fileURLToPath } from 'node:url';
import type { OutputChunk } from 'rollup';
import { getOutFile, getOutFolder } from '../common.js';
import { BEFORE_HYDRATION_SCRIPT_ID, PAGE_SCRIPT_ID } from '../../../vite-plugin-scripts/index.js';
import { joinPaths, prependForwardSlash } from '../../path.js';
import { serializeRouteData } from '../../routing/index.js';
import type { SerializedRouteInfo, SerializedSSRManifest } from '../../app/types';
import type { StaticBuildOptions } from '../types';
const manifestReplace = '@@ASTRO_MANIFEST_REPLACE@@';
const replaceExp = new RegExp(`['"](${manifestReplace})['"]`, 'g');
export const SSR_MANIFEST_VIRTUAL_MODULE_ID = '@astrojs-manifest';
export const RESOLVED_SSR_MANIFEST_VIRTUAL_MODULE_ID = '\0' + SSR_MANIFEST_VIRTUAL_MODULE_ID;
function vitePluginManifest(options: StaticBuildOptions, internals: BuildInternals): VitePlugin {
return {
name: '@astro/plugin-build-manifest',
enforce: 'post',
options(opts) {
return addRollupInput(opts, [SSR_MANIFEST_VIRTUAL_MODULE_ID]);
},
resolveId(id) {
if (id === SSR_MANIFEST_VIRTUAL_MODULE_ID) {
return RESOLVED_SSR_MANIFEST_VIRTUAL_MODULE_ID;
}
},
augmentChunkHash(chunkInfo) {
if (chunkInfo.facadeModuleId === RESOLVED_SSR_MANIFEST_VIRTUAL_MODULE_ID) {
return Date.now().toString();
}
},
async load(id) {
if (id === RESOLVED_SSR_MANIFEST_VIRTUAL_MODULE_ID) {
const imports = [];
const contents = [];
const exports = [];
imports.push(
`import { deserializeManifest as _deserializeManifest } from 'astro/app'`,
`import { _privateSetManifestDontUseThis } from 'astro:ssr-manifest'`
);
contents.push(`
const manifest = _deserializeManifest('${manifestReplace}');
_privateSetManifestDontUseThis(manifest);
`);
exports.push('export { manifest }');
return `${imports.join('\n')}${contents.join('\n')}${exports.join('\n')}`;
}
},
async generateBundle(_opts, bundle) {
for (const [chunkName, chunk] of Object.entries(bundle)) {
if (chunk.type === 'asset') {
continue;
}
if (chunk.modules[RESOLVED_SSR_MANIFEST_VIRTUAL_MODULE_ID]) {
internals.manifestEntryChunk = chunk;
delete bundle[chunkName];
}
if (chunkName.startsWith('manifest')) {
internals.manifestFileName = chunkName;
}
}
},
};
}
export function pluginManifest(
options: StaticBuildOptions,
internals: BuildInternals
): AstroBuildPlugin {
return {
build: 'ssr',
hooks: {
'build:before': () => {
return {
vitePlugin: vitePluginManifest(options, internals),
};
},
'build:post': async ({ mutate }) => {
if (!internals.manifestEntryChunk) {
throw new Error(`Did not generate an entry chunk for SSR`);
}
const manifest = await createManifest(options, internals);
await runHookBuildSsr({
config: options.settings.config,
manifest,
logging: options.logging,
entryPoints: internals.entryPoints,
middlewareEntryPoint: internals.middlewareEntryPoint,
});
// TODO: use the manifest entry chunk instead
const code = injectManifest(manifest, internals.manifestEntryChunk);
mutate(internals.manifestEntryChunk, 'server', code);
},
},
};
}
export async function createManifest(
buildOpts: StaticBuildOptions,
internals: BuildInternals
): Promise<SerializedSSRManifest> {
if (!internals.manifestEntryChunk) {
throw new Error(`Did not generate an entry chunk for SSR`);
}
// Add assets from the client build.
const clientStatics = new Set(
await glob('**/*', {
cwd: fileURLToPath(buildOpts.settings.config.build.client),
})
);
for (const file of clientStatics) {
internals.staticFiles.add(file);
}
const staticFiles = internals.staticFiles;
return buildManifest(buildOpts, internals, Array.from(staticFiles));
}
/**
* It injects the manifest in the given output rollup chunk. It returns the new emitted code
* @param buildOpts
* @param internals
* @param chunk
*/
export function injectManifest(manifest: SerializedSSRManifest, chunk: Readonly<OutputChunk>) {
const code = chunk.code;
return code.replace(replaceExp, () => {
return JSON.stringify(manifest);
});
}
function buildManifest(
opts: StaticBuildOptions,
internals: BuildInternals,
staticFiles: string[]
): SerializedSSRManifest {
const { settings } = opts;
const routes: SerializedRouteInfo[] = [];
const entryModules = Object.fromEntries(internals.entrySpecifierToBundleMap.entries());
if (settings.scripts.some((script) => script.stage === 'page')) {
staticFiles.push(entryModules[PAGE_SCRIPT_ID]);
}
const prefixAssetPath = (pth: string) => {
if (settings.config.build.assetsPrefix) {
return joinPaths(settings.config.build.assetsPrefix, pth);
} else {
return prependForwardSlash(joinPaths(settings.config.base, pth));
}
};
for (const route of opts.manifest.routes) {
if (!route.prerender) continue;
if (!route.pathname) continue;
const outFolder = getOutFolder(opts.settings.config, route.pathname, route.type);
const outFile = getOutFile(opts.settings.config, outFolder, route.pathname, route.type);
const file = outFile.toString().replace(opts.settings.config.build.client.toString(), '');
routes.push({
file,
links: [],
scripts: [],
styles: [],
routeData: serializeRouteData(route, settings.config.trailingSlash),
});
staticFiles.push(file);
}
for (const route of opts.manifest.routes) {
const pageData = internals.pagesByComponent.get(route.component);
if (route.prerender || !pageData) continue;
const scripts: SerializedRouteInfo['scripts'] = [];
if (pageData.hoistedScript) {
const hoistedValue = pageData.hoistedScript.value;
const value = hoistedValue.endsWith('.js') ? prefixAssetPath(hoistedValue) : hoistedValue;
scripts.unshift(
Object.assign({}, pageData.hoistedScript, {
value,
})
);
}
if (settings.scripts.some((script) => script.stage === 'page')) {
const src = entryModules[PAGE_SCRIPT_ID];
scripts.push({
type: 'external',
value: prefixAssetPath(src),
});
}
// may be used in the future for handling rel=modulepreload, rel=icon, rel=manifest etc.
const links: [] = [];
const styles = pageData.styles
.sort(cssOrder)
.map(({ sheet }) => sheet)
.map((s) => (s.type === 'external' ? { ...s, src: prefixAssetPath(s.src) } : s))
.reduce(mergeInlineCss, []);
routes.push({
file: '',
links,
scripts: [
...scripts,
...settings.scripts
.filter((script) => script.stage === 'head-inline')
.map(({ stage, content }) => ({ stage, children: content })),
],
styles,
routeData: serializeRouteData(route, settings.config.trailingSlash),
});
}
// HACK! Patch this special one.
if (!(BEFORE_HYDRATION_SCRIPT_ID in entryModules)) {
// Set this to an empty string so that the runtime knows not to try and load this.
entryModules[BEFORE_HYDRATION_SCRIPT_ID] = '';
}
const ssrManifest: SerializedSSRManifest = {
adapterName: opts.settings.adapter?.name ?? '',
routes,
site: settings.config.site,
base: settings.config.base,
compressHTML: settings.config.compressHTML,
assetsPrefix: settings.config.build.assetsPrefix,
componentMetadata: Array.from(internals.componentMetadata),
renderers: [],
clientDirectives: Array.from(settings.clientDirectives),
entryModules,
assets: staticFiles.map(prefixAssetPath),
};
return ssrManifest;
}

View file

@ -1,28 +1,21 @@
import glob from 'fast-glob';
import { join } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import type { Plugin as VitePlugin } from 'vite';
import type { AstroAdapter, AstroConfig } from '../../../@types/astro';
import { isFunctionPerRouteEnabled, runHookBuildSsr } from '../../../integrations/index.js';
import { isFunctionPerRouteEnabled } from '../../../integrations/index.js';
import { isServerLikeOutput } from '../../../prerender/utils.js';
import { BEFORE_HYDRATION_SCRIPT_ID, PAGE_SCRIPT_ID } from '../../../vite-plugin-scripts/index.js';
import type { SerializedRouteInfo, SerializedSSRManifest } from '../../app/types';
import { joinPaths, prependForwardSlash } from '../../path.js';
import { routeIsRedirect } from '../../redirects/index.js';
import { serializeRouteData } from '../../routing/index.js';
import { addRollupInput } from '../add-rollup-input.js';
import { getOutFile, getOutFolder } from '../common.js';
import { cssOrder, mergeInlineCss, type BuildInternals } from '../internal.js';
import type { BuildInternals } from '../internal.js';
import type { AstroBuildPlugin } from '../plugin';
import type { OutputChunk, StaticBuildOptions } from '../types';
import type { StaticBuildOptions } from '../types';
import { ASTRO_PAGE_MODULE_ID } from './plugin-pages.js';
import { RENDERERS_MODULE_ID } from './plugin-renderers.js';
import { getPathFromVirtualModulePageName, getVirtualModulePageNameFromPath } from './util.js';
import { SSR_MANIFEST_VIRTUAL_MODULE_ID } from './plugin-manifest.js';
export const SSR_VIRTUAL_MODULE_ID = '@astrojs-ssr-virtual-entry';
const RESOLVED_SSR_VIRTUAL_MODULE_ID = '\0' + SSR_VIRTUAL_MODULE_ID;
const manifestReplace = '@@ASTRO_MANIFEST_REPLACE@@';
const replaceExp = new RegExp(`['"](${manifestReplace})['"]`, 'g');
export const RESOLVED_SSR_VIRTUAL_MODULE_ID = '\0' + SSR_VIRTUAL_MODULE_ID;
function vitePluginSSR(
internals: BuildInternals,
@ -85,13 +78,12 @@ function vitePluginSSR(
}
}
for (const [chunkName, chunk] of Object.entries(bundle)) {
for (const [, chunk] of Object.entries(bundle)) {
if (chunk.type === 'asset') {
continue;
}
if (chunk.modules[RESOLVED_SSR_VIRTUAL_MODULE_ID]) {
internals.ssrEntryChunk = chunk;
delete bundle[chunkName];
}
}
},
@ -121,7 +113,7 @@ export function pluginSSR(
vitePlugin,
};
},
'build:post': async ({ mutate }) => {
'build:post': async () => {
if (!ssr) {
return;
}
@ -135,17 +127,6 @@ export function pluginSSR(
}
// Mutate the filename
internals.ssrEntryChunk.fileName = options.settings.config.build.serverEntry;
const manifest = await createManifest(options, internals);
await runHookBuildSsr({
config: options.settings.config,
manifest,
logging: options.logging,
entryPoints: internals.entryPoints,
middlewareEntryPoint: internals.middlewareEntryPoint,
});
const code = injectManifest(manifest, internals.ssrEntryChunk);
mutate(internals.ssrEntryChunk, 'server', code);
},
},
};
@ -209,21 +190,16 @@ function vitePluginSSRSplit(
}
}
for (const [chunkName, chunk] of Object.entries(bundle)) {
for (const [, chunk] of Object.entries(bundle)) {
if (chunk.type === 'asset') {
continue;
}
let shouldDeleteBundle = false;
for (const moduleKey of Object.keys(chunk.modules)) {
if (moduleKey.startsWith(RESOLVED_SPLIT_MODULE_ID)) {
internals.ssrSplitEntryChunks.set(moduleKey, chunk);
storeEntryPoint(moduleKey, options, internals, chunk.fileName);
shouldDeleteBundle = true;
}
}
if (shouldDeleteBundle) {
delete bundle[chunkName];
}
}
},
};
@ -250,31 +226,6 @@ export function pluginSSRSplit(
vitePlugin,
};
},
'build:post': async ({ mutate }) => {
if (!ssr) {
return;
}
if (!options.settings.config.build.split && !functionPerRouteEnabled) {
return;
}
if (internals.ssrSplitEntryChunks.size === 0) {
throw new Error(`Did not generate an entry chunk for SSR serverless`);
}
const manifest = await createManifest(options, internals);
await runHookBuildSsr({
config: options.settings.config,
manifest,
logging: options.logging,
entryPoints: internals.entryPoints,
middlewareEntryPoint: internals.middlewareEntryPoint,
});
for (const [, chunk] of internals.ssrSplitEntryChunks) {
const code = injectManifest(manifest, chunk);
mutate(chunk, 'server', code);
}
},
},
};
}
@ -291,13 +242,11 @@ function generateSSRCode(config: AstroConfig, adapter: AstroAdapter) {
contents.push(`import * as adapter from '${adapter.serverEntrypoint}';
import { renderers } from '${RENDERERS_MODULE_ID}';
import { deserializeManifest as _deserializeManifest } from 'astro/app';
import { _privateSetManifestDontUseThis } from 'astro:ssr-manifest';
const _manifest = Object.assign(_deserializeManifest('${manifestReplace}'), {
import { manifest as defaultManifest} from '${SSR_MANIFEST_VIRTUAL_MODULE_ID}';
const _manifest = Object.assign(defaultManifest, {
${pageMap},
renderers,
});
_privateSetManifestDontUseThis(_manifest);
const _args = ${adapter.args ? JSON.stringify(adapter.args) : 'undefined'};
${
@ -326,51 +275,6 @@ if(_start in adapter) {
};
}
/**
* It injects the manifest in the given output rollup chunk. It returns the new emitted code
* @param buildOpts
* @param internals
* @param chunk
*/
export function injectManifest(manifest: SerializedSSRManifest, chunk: Readonly<OutputChunk>) {
const code = chunk.code;
return code.replace(replaceExp, () => {
return JSON.stringify(manifest);
});
}
export async function createManifest(
buildOpts: StaticBuildOptions,
internals: BuildInternals
): Promise<SerializedSSRManifest> {
if (
buildOpts.settings.config.build.split ||
isFunctionPerRouteEnabled(buildOpts.settings.adapter)
) {
if (internals.ssrSplitEntryChunks.size === 0) {
throw new Error(`Did not generate an entry chunk for SSR in serverless mode`);
}
} else {
if (!internals.ssrEntryChunk) {
throw new Error(`Did not generate an entry chunk for SSR`);
}
}
// Add assets from the client build.
const clientStatics = new Set(
await glob('**/*', {
cwd: fileURLToPath(buildOpts.settings.config.build.client),
})
);
for (const file of clientStatics) {
internals.staticFiles.add(file);
}
const staticFiles = internals.staticFiles;
return buildManifest(buildOpts, internals, Array.from(staticFiles));
}
/**
* Because we delete the bundle from rollup at the end of this function,
* we can't use `writeBundle` hook to get the final file name of the entry point written on disk.
@ -392,109 +296,3 @@ function storeEntryPoint(
}
}
}
function buildManifest(
opts: StaticBuildOptions,
internals: BuildInternals,
staticFiles: string[]
): SerializedSSRManifest {
const { settings } = opts;
const routes: SerializedRouteInfo[] = [];
const entryModules = Object.fromEntries(internals.entrySpecifierToBundleMap.entries());
if (settings.scripts.some((script) => script.stage === 'page')) {
staticFiles.push(entryModules[PAGE_SCRIPT_ID]);
}
const prefixAssetPath = (pth: string) => {
if (settings.config.build.assetsPrefix) {
return joinPaths(settings.config.build.assetsPrefix, pth);
} else {
return prependForwardSlash(joinPaths(settings.config.base, pth));
}
};
for (const route of opts.manifest.routes) {
if (!route.prerender) continue;
if (!route.pathname) continue;
const outFolder = getOutFolder(opts.settings.config, route.pathname, route.type);
const outFile = getOutFile(opts.settings.config, outFolder, route.pathname, route.type);
const file = outFile.toString().replace(opts.settings.config.build.client.toString(), '');
routes.push({
file,
links: [],
scripts: [],
styles: [],
routeData: serializeRouteData(route, settings.config.trailingSlash),
});
staticFiles.push(file);
}
for (const route of opts.manifest.routes) {
const pageData = internals.pagesByComponent.get(route.component);
if (route.prerender || !pageData) continue;
const scripts: SerializedRouteInfo['scripts'] = [];
if (pageData.hoistedScript) {
const hoistedValue = pageData.hoistedScript.value;
const value = hoistedValue.endsWith('.js') ? prefixAssetPath(hoistedValue) : hoistedValue;
scripts.unshift(
Object.assign({}, pageData.hoistedScript, {
value,
})
);
}
if (settings.scripts.some((script) => script.stage === 'page')) {
const src = entryModules[PAGE_SCRIPT_ID];
scripts.push({
type: 'external',
value: prefixAssetPath(src),
});
}
// may be used in the future for handling rel=modulepreload, rel=icon, rel=manifest etc.
const links: [] = [];
const styles = pageData.styles
.sort(cssOrder)
.map(({ sheet }) => sheet)
.map((s) => (s.type === 'external' ? { ...s, src: prefixAssetPath(s.src) } : s))
.reduce(mergeInlineCss, []);
routes.push({
file: '',
links,
scripts: [
...scripts,
...settings.scripts
.filter((script) => script.stage === 'head-inline')
.map(({ stage, content }) => ({ stage, children: content })),
],
styles,
routeData: serializeRouteData(route, settings.config.trailingSlash),
});
}
// HACK! Patch this special one.
if (!(BEFORE_HYDRATION_SCRIPT_ID in entryModules)) {
// Set this to an empty string so that the runtime knows not to try and load this.
entryModules[BEFORE_HYDRATION_SCRIPT_ID] = '';
}
const ssrManifest: SerializedSSRManifest = {
adapterName: opts.settings.adapter!.name,
routes,
site: settings.config.site,
base: settings.config.base,
compressHTML: settings.config.compressHTML,
assetsPrefix: settings.config.build.assetsPrefix,
componentMetadata: Array.from(internals.componentMetadata),
renderers: [],
clientDirectives: Array.from(settings.clientDirectives),
entryModules,
assets: staticFiles.map(prefixAssetPath),
};
return ssrManifest;
}

View file

@ -16,7 +16,7 @@ import { emptyDir, removeEmptyDirs } from '../../core/fs/index.js';
import { appendForwardSlash, prependForwardSlash } from '../../core/path.js';
import { isModeServerWithNoAdapter } from '../../core/util.js';
import { runHookBuildSetup } from '../../integrations/index.js';
import { isServerLikeOutput } from '../../prerender/utils.js';
import { getOutputDirectory, isServerLikeOutput } from '../../prerender/utils.js';
import { PAGE_SCRIPT_ID } from '../../vite-plugin-scripts/index.js';
import { AstroError, AstroErrorData } from '../errors/index.js';
import { info } from '../logger/core.js';
@ -28,10 +28,11 @@ import { createPluginContainer, type AstroBuildPluginContainer } from './plugin.
import { registerAllPlugins } from './plugins/index.js';
import { ASTRO_PAGE_RESOLVED_MODULE_ID } from './plugins/plugin-pages.js';
import { RESOLVED_RENDERERS_MODULE_ID } from './plugins/plugin-renderers.js';
import { RESOLVED_SPLIT_MODULE_ID, SSR_VIRTUAL_MODULE_ID } from './plugins/plugin-ssr.js';
import { RESOLVED_SPLIT_MODULE_ID, RESOLVED_SSR_VIRTUAL_MODULE_ID } from './plugins/plugin-ssr.js';
import { ASTRO_PAGE_EXTENSION_POST_PATTERN } from './plugins/util.js';
import type { PageBuildData, StaticBuildOptions } from './types';
import { getTimeStat } from './util.js';
import { RESOLVED_SSR_MANIFEST_VIRTUAL_MODULE_ID } from './plugins/plugin-manifest.js';
export async function viteBuild(opts: StaticBuildOptions) {
const { allPages, settings } = opts;
@ -147,7 +148,7 @@ async function ssrBuild(
) {
const { allPages, settings, viteConfig } = opts;
const ssr = isServerLikeOutput(settings.config);
const out = ssr ? settings.config.build.server : getOutDirWithinCwd(settings.config.outDir);
const out = getOutputDirectory(settings.config);
const routes = Object.values(allPages).map((pd) => pd.route);
const { lastVitePlugins, vitePlugins } = container.runBeforeHook('ssr', input);
@ -184,10 +185,12 @@ async function ssrBuild(
);
} else if (chunkInfo.facadeModuleId?.startsWith(RESOLVED_SPLIT_MODULE_ID)) {
return makeSplitEntryPointFileName(chunkInfo.facadeModuleId, routes);
} else if (chunkInfo.facadeModuleId === SSR_VIRTUAL_MODULE_ID) {
} else if (chunkInfo.facadeModuleId === RESOLVED_SSR_VIRTUAL_MODULE_ID) {
return opts.settings.config.build.serverEntry;
} else if (chunkInfo.facadeModuleId === RESOLVED_RENDERERS_MODULE_ID) {
return 'renderers.mjs';
} else if (chunkInfo.facadeModuleId === RESOLVED_SSR_MANIFEST_VIRTUAL_MODULE_ID) {
return 'manifest.[hash].mjs';
} else {
return '[name].mjs';
}

View file

@ -133,16 +133,16 @@ export class Logger {
this.options = options;
}
info(label: string, message: string) {
info(label: string | null, message: string) {
info(this.options, label, message);
}
warn(label: string, message: string) {
warn(label: string | null, message: string) {
warn(this.options, label, message);
}
error(label: string, message: string) {
error(label: string | null, message: string) {
error(this.options, label, message);
}
debug(label: string, message: string) {
debug(label: string | null, message: string) {
debug(this.options, label, message);
}
}

View file

@ -23,12 +23,12 @@ type EndpointResultHandler = (
*/
export class Pipeline {
env: Environment;
onRequest?: MiddlewareEndpointHandler;
#onRequest?: MiddlewareEndpointHandler;
/**
* The handler accepts the *original* `Request` and result returned by the endpoint.
* It must return a `Response`.
*/
endpointHandler?: EndpointResultHandler;
#endpointHandler?: EndpointResultHandler;
/**
* When creating a pipeline, an environment is mandatory.
@ -38,20 +38,29 @@ export class Pipeline {
this.env = env;
}
setEnvironment() {}
/**
* When rendering a route, an "endpoint" will a type that needs to be handled and transformed into a `Response`.
*
* Each consumer might have different needs; use this function to set up the handler.
*/
setEndpointHandler(handler: EndpointResultHandler) {
this.endpointHandler = handler;
this.#endpointHandler = handler;
}
/**
* A middleware function that will be called before each request.
*/
setMiddlewareFunction(onRequest: MiddlewareEndpointHandler) {
this.onRequest = onRequest;
this.#onRequest = onRequest;
}
/**
* Returns the current environment
*/
getEnvironment() {
return this.env;
}
/**
@ -65,15 +74,15 @@ export class Pipeline {
renderContext,
this.env,
componentInstance,
this.onRequest
this.#onRequest
);
if (Pipeline.isEndpointResult(result, renderContext.route.type)) {
if (!this.endpointHandler) {
if (!this.#endpointHandler) {
throw new Error(
'You created a pipeline that does not know how to handle the result coming from an endpoint.'
);
}
return this.endpointHandler(renderContext.request, result);
return this.#endpointHandler(renderContext.request, result);
} else {
return result;
}

View file

@ -1,4 +1,5 @@
import type { AstroConfig } from '../@types/astro';
import { getOutDirWithinCwd } from '../core/build/common.js';
export function isServerLikeOutput(config: AstroConfig) {
return config.output === 'server' || config.output === 'hybrid';
@ -7,3 +8,15 @@ export function isServerLikeOutput(config: AstroConfig) {
export function getPrerenderDefault(config: AstroConfig) {
return config.output === 'hybrid';
}
/**
* Returns the correct output directory of hte SSR build based on the configuration
*/
export function getOutputDirectory(config: AstroConfig): URL {
const ssr = isServerLikeOutput(config);
if (ssr) {
return config.build.server;
} else {
return getOutDirWithinCwd(config.outDir);
}
}

View file

@ -63,7 +63,7 @@ describe('Assets Prefix - Static', () => {
});
});
describe('Assets Prefix - Static with path prefix', () => {
describe('Assets Prefix - with path prefix', () => {
let fixture;
before(async () => {
@ -86,7 +86,7 @@ describe('Assets Prefix - Static with path prefix', () => {
});
});
describe('Assets Prefix - Server', () => {
describe('Assets Prefix, server', () => {
let app;
before(async () => {
@ -143,7 +143,7 @@ describe('Assets Prefix - Server', () => {
});
});
describe('Assets Prefix - Server with path prefix', () => {
describe('Assets Prefix, with path prefix', () => {
let app;
before(async () => {

View file

@ -3,50 +3,54 @@ import { load as cheerioLoad } from 'cheerio';
import { loadFixture } from './test-utils.js';
import testAdapter from './test-adapter.js';
async function fetchHTML(fixture, path) {
const app = await fixture.loadTestAdapterApp();
const request = new Request('http://example.com' + path);
const response = await app.render(request);
const html = await response.text();
return html;
}
describe('Hoisted scripts in SSR', () => {
/** @type {import('./test-utils').Fixture} */
let fixture;
describe('without base path', () => {
before(async () => {
fixture = await loadFixture({
root: './fixtures/ssr-hoisted-script/',
output: 'server',
adapter: testAdapter(),
});
await fixture.build();
});
it('Inlined scripts get included', async () => {
const html = await fetchHTML(fixture, '/');
const $ = cheerioLoad(html);
expect($('script').length).to.equal(1);
});
});
});
describe('Hoisted scripts in SSR with base path', () => {
/** @type {import('./test-utils').Fixture} */
let fixture;
const base = '/hello';
before(async () => {
fixture = await loadFixture({
root: './fixtures/ssr-hoisted-script/',
output: 'server',
adapter: testAdapter(),
base,
});
await fixture.build();
});
async function fetchHTML(path) {
const app = await fixture.loadTestAdapterApp();
const request = new Request('http://example.com' + path);
const response = await app.render(request);
const html = await response.text();
return html;
}
it('Inlined scripts get included', async () => {
const html = await fetchHTML('/');
it('Inlined scripts get included without base path in the script', async () => {
const html = await fetchHTML(fixture, '/hello/');
const $ = cheerioLoad(html);
expect($('script').length).to.equal(1);
});
describe('base path', () => {
const base = '/hello';
before(async () => {
fixture = await loadFixture({
root: './fixtures/ssr-hoisted-script/',
output: 'server',
adapter: testAdapter(),
base,
});
await fixture.build();
});
it('Inlined scripts get included without base path in the script', async () => {
const html = await fetchHTML('/hello/');
const $ = cheerioLoad(html);
expect($('script').html()).to.equal('console.log("hello world");\n');
});
expect($('script').html()).to.equal('console.log("hello world");\n');
});
});