summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Emanuele Stoppa <my.burning@gmail.com> 2023-08-16 16:45:21 +0100
committerGravatar GitHub <noreply@github.com> 2023-08-16 16:45:21 +0100
commitca4cf01100d7a8f56ad847a808fdebc20a1de924 (patch)
tree0d244f0dfa33534d14ed6c7288db2fbf97e61b85
parent788825bd8bb683595e026f09160a67125f617236 (diff)
downloadastro-ca4cf01100d7a8f56ad847a808fdebc20a1de924.tar.gz
astro-ca4cf01100d7a8f56ad847a808fdebc20a1de924.tar.zst
astro-ca4cf01100d7a8f56ad847a808fdebc20a1de924.zip
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!!
-rw-r--r--packages/astro/src/cli/add/index.ts1
-rw-r--r--packages/astro/src/core/app/ssrPipeline.ts4
-rw-r--r--packages/astro/src/core/build/buildPipeline.ts211
-rw-r--r--packages/astro/src/core/build/generate.ts231
-rw-r--r--packages/astro/src/core/build/internal.ts11
-rw-r--r--packages/astro/src/core/build/plugins/README.md31
-rw-r--r--packages/astro/src/core/build/plugins/index.ts2
-rw-r--r--packages/astro/src/core/build/plugins/plugin-manifest.ts251
-rw-r--r--packages/astro/src/core/build/plugins/plugin-ssr.ts222
-rw-r--r--packages/astro/src/core/build/static-build.ts11
-rw-r--r--packages/astro/src/core/logger/core.ts8
-rw-r--r--packages/astro/src/core/pipeline.ts23
-rw-r--r--packages/astro/src/prerender/utils.ts13
-rw-r--r--packages/astro/test/astro-assets-prefix.test.js6
-rw-r--r--packages/astro/test/ssr-hoisted-script.test.js64
15 files changed, 689 insertions, 400 deletions
diff --git a/packages/astro/src/cli/add/index.ts b/packages/astro/src/cli/add/index.ts
index f09d74a08..fcaeb07c7 100644
--- a/packages/astro/src/cli/add/index.ts
+++ b/packages/astro/src/cli/add/index.ts
@@ -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;
}
diff --git a/packages/astro/src/core/app/ssrPipeline.ts b/packages/astro/src/core/app/ssrPipeline.ts
index cdb95ff7c..5f135e42d 100644
--- a/packages/astro/src/core/app/ssrPipeline.ts
+++ b/packages/astro/src/core/app/ssrPipeline.ts
@@ -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, {
diff --git a/packages/astro/src/core/build/buildPipeline.ts b/packages/astro/src/core/build/buildPipeline.ts
new file mode 100644
index 000000000..4ebf48a9a
--- /dev/null
+++ b/packages/astro/src/core/build/buildPipeline.ts
@@ -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 };
+ }
+ }
+}
diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts
index c99a8881b..00be46ea9 100644
--- a/packages/astro/src/core/build/generate.ts
+++ b/packages/astro/src/core/build/generate.ts
@@ -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');
}
diff --git a/packages/astro/src/core/build/internal.ts b/packages/astro/src/core/build/internal.ts
index 5dff6f3dd..c1123e36b 100644
--- a/packages/astro/src/core/build/internal.ts
+++ b/packages/astro/src/core/build/internal.ts
@@ -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]> {
diff --git a/packages/astro/src/core/build/plugins/README.md b/packages/astro/src/core/build/plugins/README.md
index 145158163..ef73b9e50 100644
--- a/packages/astro/src/core/build/plugins/README.md
+++ b/packages/astro/src/core/build/plugins/README.md
@@ -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.
diff --git a/packages/astro/src/core/build/plugins/index.ts b/packages/astro/src/core/build/plugins/index.ts
index decfefd04..19c952660 100644
--- a/packages/astro/src/core/build/plugins/index.ts
+++ b/packages/astro/src/core/build/plugins/index.ts
@@ -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));
diff --git a/packages/astro/src/core/build/plugins/plugin-manifest.ts b/packages/astro/src/core/build/plugins/plugin-manifest.ts
new file mode 100644
index 000000000..2c2ceb7e1
--- /dev/null
+++ b/packages/astro/src/core/build/plugins/plugin-manifest.ts
@@ -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;
+}
diff --git a/packages/astro/src/core/build/plugins/plugin-ssr.ts b/packages/astro/src/core/build/plugins/plugin-ssr.ts
index ed4cd7b72..098b9dee8 100644
--- a/packages/astro/src/core/build/plugins/plugin-ssr.ts
+++ b/packages/astro/src/core/build/plugins/plugin-ssr.ts
@@ -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'};
${
@@ -327,51 +276,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.
* We use this hook instead.
@@ -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;
-}
diff --git a/packages/astro/src/core/build/static-build.ts b/packages/astro/src/core/build/static-build.ts
index 28f496d91..cbb259e03 100644
--- a/packages/astro/src/core/build/static-build.ts
+++ b/packages/astro/src/core/build/static-build.ts
@@ -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';
}
diff --git a/packages/astro/src/core/logger/core.ts b/packages/astro/src/core/logger/core.ts
index e5d0aee1f..c92cdbb24 100644
--- a/packages/astro/src/core/logger/core.ts
+++ b/packages/astro/src/core/logger/core.ts
@@ -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);
}
}
diff --git a/packages/astro/src/core/pipeline.ts b/packages/astro/src/core/pipeline.ts
index 66fa6bd07..b5c66517a 100644
--- a/packages/astro/src/core/pipeline.ts
+++ b/packages/astro/src/core/pipeline.ts
@@ -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;
}
diff --git a/packages/astro/src/prerender/utils.ts b/packages/astro/src/prerender/utils.ts
index bd6e367ad..a3655eead 100644
--- a/packages/astro/src/prerender/utils.ts
+++ b/packages/astro/src/prerender/utils.ts
@@ -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);
+ }
+}
diff --git a/packages/astro/test/astro-assets-prefix.test.js b/packages/astro/test/astro-assets-prefix.test.js
index 40562afd4..ab42439ae 100644
--- a/packages/astro/test/astro-assets-prefix.test.js
+++ b/packages/astro/test/astro-assets-prefix.test.js
@@ -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 () => {
diff --git a/packages/astro/test/ssr-hoisted-script.test.js b/packages/astro/test/ssr-hoisted-script.test.js
index 49e1e7b2f..e9549151e 100644
--- a/packages/astro/test/ssr-hoisted-script.test.js
+++ b/packages/astro/test/ssr-hoisted-script.test.js
@@ -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;
- before(async () => {
- fixture = await loadFixture({
- root: './fixtures/ssr-hoisted-script/',
- output: 'server',
- adapter: testAdapter(),
- });
- 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('/');
- const $ = cheerioLoad(html);
- expect($('script').length).to.equal(1);
- });
-
- describe('base path', () => {
- const base = '/hello';
-
+ describe('without base path', () => {
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/');
+ it('Inlined scripts get included', async () => {
+ const html = await fetchHTML(fixture, '/');
const $ = cheerioLoad(html);
- expect($('script').html()).to.equal('console.log("hello world");\n');
+ 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();
+ });
+
+ it('Inlined scripts get included without base path in the script', async () => {
+ const html = await fetchHTML(fixture, '/hello/');
+ const $ = cheerioLoad(html);
+ expect($('script').html()).to.equal('console.log("hello world");\n');
+ });
+});