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:
parent
788825bd8b
commit
ca4cf01100
15 changed files with 690 additions and 401 deletions
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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, {
|
||||
|
|
211
packages/astro/src/core/build/buildPipeline.ts
Normal file
211
packages/astro/src/core/build/buildPipeline.ts
Normal 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 };
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)) {
|
||||
if (pageData.route.type === 'redirect') {
|
||||
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);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const [pageData, filePath] of eachPageDataFromEntryPoint(internals)) {
|
||||
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());
|
||||
const manifest = createBuildManifest(opts.settings, internals, entry.renderers);
|
||||
|
||||
await generatePage(opts, internals, pageData, entry, builtPaths, manifest);
|
||||
await generatePage(pageData, entry, 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,20 +538,9 @@ 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;
|
||||
}
|
||||
} 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) {
|
||||
if (!pipeline.getConfig().build.redirects) {
|
||||
return;
|
||||
}
|
||||
const location = getRedirectLocationOrThrow(response.headers);
|
||||
|
@ -589,13 +563,18 @@ async function generatePath(
|
|||
} else {
|
||||
// If there's no body, do nothing
|
||||
if (!response.body) return;
|
||||
body = await response.text();
|
||||
}
|
||||
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');
|
||||
}
|
||||
|
|
|
@ -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]> {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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));
|
||||
|
|
251
packages/astro/src/core/build/plugins/plugin-manifest.ts
Normal file
251
packages/astro/src/core/build/plugins/plugin-manifest.ts
Normal 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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -3,10 +3,19 @@ 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/',
|
||||
|
@ -16,21 +25,17 @@ describe('Hoisted scripts in SSR', () => {
|
|||
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 html = await fetchHTML(fixture, '/');
|
||||
const $ = cheerioLoad(html);
|
||||
expect($('script').length).to.equal(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('base path', () => {
|
||||
describe('Hoisted scripts in SSR with base path', () => {
|
||||
/** @type {import('./test-utils').Fixture} */
|
||||
let fixture;
|
||||
const base = '/hello';
|
||||
|
||||
before(async () => {
|
||||
|
@ -44,9 +49,8 @@ describe('Hoisted scripts in SSR', () => {
|
|||
});
|
||||
|
||||
it('Inlined scripts get included without base path in the script', async () => {
|
||||
const html = await fetchHTML('/hello/');
|
||||
const html = await fetchHTML(fixture, '/hello/');
|
||||
const $ = cheerioLoad(html);
|
||||
expect($('script').html()).to.equal('console.log("hello world");\n');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue