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) {
|
} catch (err) {
|
||||||
spinner.fail();
|
spinner.fail();
|
||||||
debug('add', 'Error installing dependencies', err);
|
debug('add', 'Error installing dependencies', err);
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
console.error('\n', (err as any).stdout, '\n');
|
console.error('\n', (err as any).stdout, '\n');
|
||||||
return UpdateResult.failure;
|
return UpdateResult.failure;
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ export class EndpointNotFoundError extends Error {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SSRRoutePipeline extends Pipeline {
|
export class SSRRoutePipeline extends Pipeline {
|
||||||
encoder = new TextEncoder();
|
#encoder = new TextEncoder();
|
||||||
|
|
||||||
constructor(env: Environment) {
|
constructor(env: Environment) {
|
||||||
super(env);
|
super(env);
|
||||||
|
@ -40,7 +40,7 @@ export class SSRRoutePipeline extends Pipeline {
|
||||||
headers.set('Content-Type', 'text/plain;charset=utf-8');
|
headers.set('Content-Type', 'text/plain;charset=utf-8');
|
||||||
}
|
}
|
||||||
const bytes =
|
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());
|
headers.set('Content-Length', bytes.byteLength.toString());
|
||||||
|
|
||||||
const newResponse = new Response(bytes, {
|
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,
|
ComponentInstance,
|
||||||
GetStaticPathsItem,
|
GetStaticPathsItem,
|
||||||
ImageTransform,
|
ImageTransform,
|
||||||
MiddlewareHandler,
|
MiddlewareEndpointHandler,
|
||||||
RouteData,
|
RouteData,
|
||||||
RouteType,
|
RouteType,
|
||||||
SSRError,
|
SSRError,
|
||||||
|
@ -20,12 +20,7 @@ import {
|
||||||
generateImage as generateImageInternal,
|
generateImage as generateImageInternal,
|
||||||
getStaticImageList,
|
getStaticImageList,
|
||||||
} from '../../assets/generate.js';
|
} from '../../assets/generate.js';
|
||||||
import {
|
import { hasPrerenderedPages, type BuildInternals } from '../../core/build/internal.js';
|
||||||
eachPageDataFromEntryPoint,
|
|
||||||
eachRedirectPageData,
|
|
||||||
hasPrerenderedPages,
|
|
||||||
type BuildInternals,
|
|
||||||
} from '../../core/build/internal.js';
|
|
||||||
import {
|
import {
|
||||||
isRelativePath,
|
isRelativePath,
|
||||||
joinPaths,
|
joinPaths,
|
||||||
|
@ -34,13 +29,12 @@ import {
|
||||||
removeTrailingForwardSlash,
|
removeTrailingForwardSlash,
|
||||||
} from '../../core/path.js';
|
} from '../../core/path.js';
|
||||||
import { runHookBuildGenerated } from '../../integrations/index.js';
|
import { runHookBuildGenerated } from '../../integrations/index.js';
|
||||||
import { isServerLikeOutput } from '../../prerender/utils.js';
|
import { getOutputDirectory, isServerLikeOutput } from '../../prerender/utils.js';
|
||||||
import { BEFORE_HYDRATION_SCRIPT_ID, PAGE_SCRIPT_ID } from '../../vite-plugin-scripts/index.js';
|
import { PAGE_SCRIPT_ID } from '../../vite-plugin-scripts/index.js';
|
||||||
import { AstroError, AstroErrorData } from '../errors/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 { RedirectSinglePageBuiltModule, getRedirectLocationOrThrow } from '../redirects/index.js';
|
||||||
import { isEndpointResult } from '../render/core.js';
|
import { createRenderContext } from '../render/index.js';
|
||||||
import { createEnvironment, createRenderContext, tryRenderRoute } from '../render/index.js';
|
|
||||||
import { callGetStaticPaths } from '../render/route-cache.js';
|
import { callGetStaticPaths } from '../render/route-cache.js';
|
||||||
import {
|
import {
|
||||||
createAssetLink,
|
createAssetLink,
|
||||||
|
@ -64,6 +58,8 @@ import type {
|
||||||
StylesheetAsset,
|
StylesheetAsset,
|
||||||
} from './types';
|
} from './types';
|
||||||
import { getTimeStat } from './util.js';
|
import { getTimeStat } from './util.js';
|
||||||
|
import { BuildPipeline } from './buildPipeline.js';
|
||||||
|
import type { BufferEncoding } from 'vfile';
|
||||||
|
|
||||||
function createEntryURL(filePath: string, outFolder: URL) {
|
function createEntryURL(filePath: string, outFolder: URL) {
|
||||||
return new URL('./' + filePath + `?time=${Date.now()}`, outFolder);
|
return new URL('./' + filePath + `?time=${Date.now()}`, outFolder);
|
||||||
|
@ -125,8 +121,23 @@ export function chunkIsPage(
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generatePages(opts: StaticBuildOptions, internals: BuildInternals) {
|
export async function generatePages(opts: StaticBuildOptions, internals: BuildInternals) {
|
||||||
|
const logger = new Logger(opts.logging);
|
||||||
const timer = performance.now();
|
const timer = performance.now();
|
||||||
const ssr = isServerLikeOutput(opts.settings.config);
|
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
|
const outFolder = ssr
|
||||||
? opts.settings.config.build.server
|
? opts.settings.config.build.server
|
||||||
: getOutDirWithinCwd(opts.settings.config.outDir);
|
: getOutDirWithinCwd(opts.settings.config.outDir);
|
||||||
|
@ -140,20 +151,18 @@ export async function generatePages(opts: StaticBuildOptions, internals: BuildIn
|
||||||
|
|
||||||
const verb = ssr ? 'prerendering' : 'generating';
|
const verb = ssr ? 'prerendering' : 'generating';
|
||||||
info(opts.logging, null, `\n${bgGreen(black(` ${verb} static routes `))}`);
|
info(opts.logging, null, `\n${bgGreen(black(` ${verb} static routes `))}`);
|
||||||
|
|
||||||
const builtPaths = new Set<string>();
|
const builtPaths = new Set<string>();
|
||||||
|
const pagesToGenerate = buildPipeline.retrieveRoutesToGenerate();
|
||||||
if (ssr) {
|
if (ssr) {
|
||||||
for (const [pageData, filePath] of eachPageDataFromEntryPoint(internals)) {
|
for (const [pageData, filePath] of pagesToGenerate) {
|
||||||
if (pageData.route.prerender) {
|
if (pageData.route.prerender) {
|
||||||
const ssrEntryURLPage = createEntryURL(filePath, outFolder);
|
const ssrEntryURLPage = createEntryURL(filePath, outFolder);
|
||||||
const ssrEntryPage = await import(ssrEntryURLPage.toString());
|
const ssrEntryPage = await import(ssrEntryURLPage.toString());
|
||||||
if (opts.settings.config.build.split) {
|
if (opts.settings.config.build.split) {
|
||||||
// forcing to use undefined, so we fail in an expected way if the module is not even there.
|
// 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 = ssrEntryPage?.manifest?.pageModule;
|
||||||
const ssrEntry = manifest?.pageModule;
|
|
||||||
if (ssrEntry) {
|
if (ssrEntry) {
|
||||||
await generatePage(opts, internals, pageData, ssrEntry, builtPaths, manifest);
|
await generatePage(pageData, ssrEntry, builtPaths, buildPipeline, logger);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Unable to find the manifest for the module ${ssrEntryURLPage.toString()}. This is unexpected and likely a bug in Astro, please report.`
|
`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 {
|
} else {
|
||||||
const ssrEntry = ssrEntryPage as SinglePageBuiltModule;
|
const ssrEntry = ssrEntryPage as SinglePageBuiltModule;
|
||||||
const manifest = createBuildManifest(opts.settings, internals, ssrEntry.renderers);
|
await generatePage(pageData, ssrEntry, builtPaths, buildPipeline, logger);
|
||||||
await generatePage(opts, internals, pageData, ssrEntry, builtPaths, manifest);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
if (pageData.route.type === 'redirect') {
|
||||||
for (const pageData of eachRedirectPageData(internals)) {
|
const entry = await getEntryForRedirectRoute(pageData.route, internals, outFolder);
|
||||||
const entry = await getEntryForRedirectRoute(pageData.route, internals, outFolder);
|
await generatePage(pageData, entry, builtPaths, buildPipeline, logger);
|
||||||
const manifest = createBuildManifest(opts.settings, internals, entry.renderers);
|
}
|
||||||
await generatePage(opts, internals, pageData, entry, builtPaths, manifest);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (const [pageData, filePath] of eachPageDataFromEntryPoint(internals)) {
|
for (const [pageData, filePath] of pagesToGenerate) {
|
||||||
const ssrEntryURLPage = createEntryURL(filePath, outFolder);
|
if (pageData.route.type === 'redirect') {
|
||||||
const entry: SinglePageBuiltModule = await import(ssrEntryURLPage.toString());
|
const entry = await getEntryForRedirectRoute(pageData.route, internals, outFolder);
|
||||||
const manifest = createBuildManifest(opts.settings, internals, entry.renderers);
|
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);
|
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(
|
async function generatePage(
|
||||||
opts: StaticBuildOptions,
|
|
||||||
internals: BuildInternals,
|
|
||||||
pageData: PageBuildData,
|
pageData: PageBuildData,
|
||||||
ssrEntry: SinglePageBuiltModule,
|
ssrEntry: SinglePageBuiltModule,
|
||||||
builtPaths: Set<string>,
|
builtPaths: Set<string>,
|
||||||
manifest: SSRManifest
|
pipeline: BuildPipeline,
|
||||||
|
logger: Logger
|
||||||
) {
|
) {
|
||||||
let timeStart = performance.now();
|
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.
|
// may be used in the future for handling rel=modulepreload, rel=icon, rel=manifest etc.
|
||||||
const linkIds: [] = [];
|
const linkIds: [] = [];
|
||||||
|
@ -240,6 +245,9 @@ async function generatePage(
|
||||||
|
|
||||||
const pageModulePromise = ssrEntry.page;
|
const pageModulePromise = ssrEntry.page;
|
||||||
const onRequest = ssrEntry.onRequest;
|
const onRequest = ssrEntry.onRequest;
|
||||||
|
if (onRequest) {
|
||||||
|
pipeline.setMiddlewareFunction(onRequest as MiddlewareEndpointHandler);
|
||||||
|
}
|
||||||
|
|
||||||
if (!pageModulePromise) {
|
if (!pageModulePromise) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
@ -247,14 +255,13 @@ async function generatePage(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const pageModule = await pageModulePromise();
|
const pageModule = await pageModulePromise();
|
||||||
if (shouldSkipDraft(pageModule, opts.settings)) {
|
if (shouldSkipDraft(pageModule, pipeline.getSettings())) {
|
||||||
info(opts.logging, null, `${magenta('⚠️')} Skipping draft ${pageData.route.component}`);
|
logger.info(null, `${magenta('⚠️')} Skipping draft ${pageData.route.component}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const generationOptions: Readonly<GeneratePathOptions> = {
|
const generationOptions: Readonly<GeneratePathOptions> = {
|
||||||
pageData,
|
pageData,
|
||||||
internals,
|
|
||||||
linkIds,
|
linkIds,
|
||||||
scripts,
|
scripts,
|
||||||
styles,
|
styles,
|
||||||
|
@ -263,23 +270,28 @@ async function generatePage(
|
||||||
|
|
||||||
const icon = pageData.route.type === 'page' ? green('▶') : magenta('λ');
|
const icon = pageData.route.type === 'page' ? green('▶') : magenta('λ');
|
||||||
if (isRelativePath(pageData.route.component)) {
|
if (isRelativePath(pageData.route.component)) {
|
||||||
info(opts.logging, null, `${icon} ${pageData.route.route}`);
|
logger.info(null, `${icon} ${pageData.route.route}`);
|
||||||
} else {
|
} 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.
|
// 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++) {
|
for (let i = 0; i < paths.length; i++) {
|
||||||
const path = paths[i];
|
const path = paths[i];
|
||||||
await generatePath(path, opts, generationOptions, manifest, onRequest);
|
await generatePath(path, generationOptions, pipeline);
|
||||||
const timeEnd = performance.now();
|
const timeEnd = performance.now();
|
||||||
const timeChange = getTimeStat(timeStart, timeEnd);
|
const timeChange = getTimeStat(timeStart, timeEnd);
|
||||||
const timeIncrease = `(+${timeChange})`;
|
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 ? '└─' : '├─';
|
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 {
|
interface GeneratePathOptions {
|
||||||
pageData: PageBuildData;
|
pageData: PageBuildData;
|
||||||
internals: BuildInternals;
|
|
||||||
linkIds: string[];
|
linkIds: string[];
|
||||||
scripts: { type: 'inline' | 'external'; value: string } | null;
|
scripts: { type: 'inline' | 'external'; value: string } | null;
|
||||||
styles: StylesheetAsset[];
|
styles: StylesheetAsset[];
|
||||||
|
@ -446,19 +457,13 @@ function getUrlForPath(
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function generatePath(
|
async function generatePath(pathname: string, gopts: GeneratePathOptions, pipeline: BuildPipeline) {
|
||||||
pathname: string,
|
const manifest = pipeline.getManifest();
|
||||||
opts: StaticBuildOptions,
|
const { mod, scripts: hoistedScripts, styles: _styles, pageData } = gopts;
|
||||||
gopts: GeneratePathOptions,
|
|
||||||
manifest: SSRManifest,
|
|
||||||
onRequest?: MiddlewareHandler<unknown>
|
|
||||||
) {
|
|
||||||
const { settings, logging, origin, routeCache } = opts;
|
|
||||||
const { mod, internals, scripts: hoistedScripts, styles: _styles, pageData } = gopts;
|
|
||||||
|
|
||||||
// This adds the page name to the array so it can be shown as part of stats.
|
// This adds the page name to the array so it can be shown as part of stats.
|
||||||
if (pageData.route.type === 'page') {
|
if (pageData.route.type === 'page') {
|
||||||
addPageName(pathname, opts);
|
addPageName(pathname, pipeline.getStaticBuildOptions());
|
||||||
}
|
}
|
||||||
|
|
||||||
debug('build', `Generating: ${pathname}`);
|
debug('build', `Generating: ${pathname}`);
|
||||||
|
@ -472,8 +477,8 @@ async function generatePath(
|
||||||
);
|
);
|
||||||
const styles = createStylesheetElementSet(_styles, manifest.base, manifest.assetsPrefix);
|
const styles = createStylesheetElementSet(_styles, manifest.base, manifest.assetsPrefix);
|
||||||
|
|
||||||
if (settings.scripts.some((script) => script.stage === 'page')) {
|
if (pipeline.getSettings().scripts.some((script) => script.stage === 'page')) {
|
||||||
const hashedFilePath = internals.entrySpecifierToBundleMap.get(PAGE_SCRIPT_ID);
|
const hashedFilePath = pipeline.getInternals().entrySpecifierToBundleMap.get(PAGE_SCRIPT_ID);
|
||||||
if (typeof hashedFilePath !== 'string') {
|
if (typeof hashedFilePath !== 'string') {
|
||||||
throw new Error(`Cannot find the built path for ${PAGE_SCRIPT_ID}`);
|
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.
|
// 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') {
|
if (script.stage === 'head-inline') {
|
||||||
scripts.add({
|
scripts.add({
|
||||||
props: {},
|
props: {},
|
||||||
|
@ -494,58 +499,38 @@ async function generatePath(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const ssr = isServerLikeOutput(settings.config);
|
const ssr = isServerLikeOutput(pipeline.getConfig());
|
||||||
const url = getUrlForPath(
|
const url = getUrlForPath(
|
||||||
pathname,
|
pathname,
|
||||||
opts.settings.config.base,
|
pipeline.getConfig().base,
|
||||||
origin,
|
pipeline.getStaticBuildOptions().origin,
|
||||||
opts.settings.config.build.format,
|
pipeline.getConfig().build.format,
|
||||||
pageData.route.type
|
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({
|
const renderContext = await createRenderContext({
|
||||||
pathname,
|
pathname,
|
||||||
request: createRequest({ url, headers: new Headers(), logging, ssr }),
|
request: createRequest({
|
||||||
|
url,
|
||||||
|
headers: new Headers(),
|
||||||
|
logging: pipeline.getStaticBuildOptions().logging,
|
||||||
|
ssr,
|
||||||
|
}),
|
||||||
componentMetadata: manifest.componentMetadata,
|
componentMetadata: manifest.componentMetadata,
|
||||||
scripts,
|
scripts,
|
||||||
styles,
|
styles,
|
||||||
links,
|
links,
|
||||||
route: pageData.route,
|
route: pageData.route,
|
||||||
env,
|
env: pipeline.getEnvironment(),
|
||||||
mod,
|
mod,
|
||||||
});
|
});
|
||||||
|
|
||||||
let body: string | Uint8Array;
|
let body: string | Uint8Array;
|
||||||
let encoding: BufferEncoding | undefined;
|
let encoding: BufferEncoding | undefined;
|
||||||
|
|
||||||
let response;
|
let response: Response;
|
||||||
try {
|
try {
|
||||||
response = await tryRenderRoute(renderContext, env, mod, onRequest);
|
response = await pipeline.renderRoute(renderContext, mod);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!AstroError.is(err) && !(err as SSRError).id && typeof err === 'object') {
|
if (!AstroError.is(err) && !(err as SSRError).id && typeof err === 'object') {
|
||||||
(err as SSRError).id = pageData.component;
|
(err as SSRError).id = pageData.component;
|
||||||
|
@ -553,28 +538,17 @@ async function generatePath(
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isEndpointResult(response, pageData.route.type)) {
|
if (response.status >= 300 && response.status < 400) {
|
||||||
if (response.type === 'response') {
|
// If redirects is set to false, don't output the HTML
|
||||||
// If there's no body, do nothing
|
if (!pipeline.getConfig().build.redirects) {
|
||||||
if (!response.response.body) return;
|
return;
|
||||||
const ab = await response.response.arrayBuffer();
|
|
||||||
body = new Uint8Array(ab);
|
|
||||||
} else {
|
|
||||||
body = response.body;
|
|
||||||
encoding = response.encoding;
|
|
||||||
}
|
}
|
||||||
} else {
|
const location = getRedirectLocationOrThrow(response.headers);
|
||||||
if (response.status >= 300 && response.status < 400) {
|
const fromPath = new URL(renderContext.request.url).pathname;
|
||||||
// If redirects is set to false, don't output the HTML
|
// A short delay causes Google to interpret the redirect as temporary.
|
||||||
if (!opts.settings.config.build.redirects) {
|
// https://developers.google.com/search/docs/crawling-indexing/301-redirects#metarefresh
|
||||||
return;
|
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>
|
<title>Redirecting to: ${location}</title>
|
||||||
<meta http-equiv="refresh" content="${delay};url=${location}">
|
<meta http-equiv="refresh" content="${delay};url=${location}">
|
||||||
<meta name="robots" content="noindex">
|
<meta name="robots" content="noindex">
|
||||||
|
@ -582,20 +556,25 @@ async function generatePath(
|
||||||
<body>
|
<body>
|
||||||
<a href="${location}">Redirecting from <code>${fromPath}</code> to <code>${location}</code></a>
|
<a href="${location}">Redirecting from <code>${fromPath}</code> to <code>${location}</code></a>
|
||||||
</body>`;
|
</body>`;
|
||||||
// A dynamic redirect, set the location so that integrations know about it.
|
// A dynamic redirect, set the location so that integrations know about it.
|
||||||
if (pageData.route.type !== 'redirect') {
|
if (pageData.route.type !== 'redirect') {
|
||||||
pageData.route.redirect = location;
|
pageData.route.redirect = location;
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// If there's no body, do nothing
|
|
||||||
if (!response.body) return;
|
|
||||||
body = await response.text();
|
|
||||||
}
|
}
|
||||||
|
} 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 outFolder = getOutFolder(pipeline.getConfig(), pathname, pageData.route.type);
|
||||||
const outFile = getOutFile(settings.config, outFolder, pathname, pageData.route.type);
|
const outFile = getOutFile(pipeline.getConfig(), outFolder, pathname, pageData.route.type);
|
||||||
pageData.route.distURL = outFile;
|
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.mkdir(outFolder, { recursive: true });
|
||||||
await fs.promises.writeFile(outFile, body, encoding ?? 'utf-8');
|
await fs.promises.writeFile(outFile, body, encoding ?? 'utf-8');
|
||||||
}
|
}
|
||||||
|
|
|
@ -85,6 +85,9 @@ export interface BuildInternals {
|
||||||
staticFiles: Set<string>;
|
staticFiles: Set<string>;
|
||||||
// The SSR entry chunk. Kept in internals to share between ssr/client build steps
|
// The SSR entry chunk. Kept in internals to share between ssr/client build steps
|
||||||
ssrEntryChunk?: Rollup.OutputChunk;
|
ssrEntryChunk?: Rollup.OutputChunk;
|
||||||
|
// The SSR manifest entry chunk.
|
||||||
|
manifestEntryChunk?: Rollup.OutputChunk;
|
||||||
|
manifestFileName?: string;
|
||||||
entryPoints: Map<RouteData, URL>;
|
entryPoints: Map<RouteData, URL>;
|
||||||
ssrSplitEntryChunks: Map<string, Rollup.OutputChunk>;
|
ssrSplitEntryChunks: Map<string, Rollup.OutputChunk>;
|
||||||
componentMetadata: SSRResult['componentMetadata'];
|
componentMetadata: SSRResult['componentMetadata'];
|
||||||
|
@ -227,14 +230,6 @@ export function* eachPageData(internals: BuildInternals) {
|
||||||
yield* internals.pagesByComponent.values();
|
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(
|
export function* eachPageDataFromEntryPoint(
|
||||||
internals: BuildInternals
|
internals: BuildInternals
|
||||||
): Generator<[PageBuildData, string]> {
|
): 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.
|
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
|
This plugin is responsible to create the JS files that will be executed in SSR.
|
||||||
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.
|
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
|
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 { pluginPrerender } from './plugin-prerender.js';
|
||||||
import { pluginRenderers } from './plugin-renderers.js';
|
import { pluginRenderers } from './plugin-renderers.js';
|
||||||
import { pluginSSR, pluginSSRSplit } from './plugin-ssr.js';
|
import { pluginSSR, pluginSSRSplit } from './plugin-ssr.js';
|
||||||
|
import { pluginManifest } from './plugin-manifest.js';
|
||||||
|
|
||||||
export function registerAllPlugins({ internals, options, register }: AstroBuildPluginContainer) {
|
export function registerAllPlugins({ internals, options, register }: AstroBuildPluginContainer) {
|
||||||
register(pluginComponentEntry(internals));
|
register(pluginComponentEntry(internals));
|
||||||
register(pluginAliasResolve(internals));
|
register(pluginAliasResolve(internals));
|
||||||
register(pluginAnalyzer(options, internals));
|
register(pluginAnalyzer(options, internals));
|
||||||
register(pluginInternals(internals));
|
register(pluginInternals(internals));
|
||||||
|
register(pluginManifest(options, internals));
|
||||||
register(pluginRenderers(options));
|
register(pluginRenderers(options));
|
||||||
register(pluginMiddleware(options, internals));
|
register(pluginMiddleware(options, internals));
|
||||||
register(pluginPages(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 { join } from 'node:path';
|
||||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||||
import type { Plugin as VitePlugin } from 'vite';
|
import type { Plugin as VitePlugin } from 'vite';
|
||||||
import type { AstroAdapter, AstroConfig } from '../../../@types/astro';
|
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 { 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 { routeIsRedirect } from '../../redirects/index.js';
|
||||||
import { serializeRouteData } from '../../routing/index.js';
|
|
||||||
import { addRollupInput } from '../add-rollup-input.js';
|
import { addRollupInput } from '../add-rollup-input.js';
|
||||||
import { getOutFile, getOutFolder } from '../common.js';
|
import type { BuildInternals } from '../internal.js';
|
||||||
import { cssOrder, mergeInlineCss, type BuildInternals } from '../internal.js';
|
|
||||||
import type { AstroBuildPlugin } from '../plugin';
|
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 { ASTRO_PAGE_MODULE_ID } from './plugin-pages.js';
|
||||||
import { RENDERERS_MODULE_ID } from './plugin-renderers.js';
|
import { RENDERERS_MODULE_ID } from './plugin-renderers.js';
|
||||||
import { getPathFromVirtualModulePageName, getVirtualModulePageNameFromPath } from './util.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';
|
export const SSR_VIRTUAL_MODULE_ID = '@astrojs-ssr-virtual-entry';
|
||||||
const RESOLVED_SSR_VIRTUAL_MODULE_ID = '\0' + SSR_VIRTUAL_MODULE_ID;
|
export const RESOLVED_SSR_VIRTUAL_MODULE_ID = '\0' + SSR_VIRTUAL_MODULE_ID;
|
||||||
const manifestReplace = '@@ASTRO_MANIFEST_REPLACE@@';
|
|
||||||
const replaceExp = new RegExp(`['"](${manifestReplace})['"]`, 'g');
|
|
||||||
|
|
||||||
function vitePluginSSR(
|
function vitePluginSSR(
|
||||||
internals: BuildInternals,
|
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') {
|
if (chunk.type === 'asset') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (chunk.modules[RESOLVED_SSR_VIRTUAL_MODULE_ID]) {
|
if (chunk.modules[RESOLVED_SSR_VIRTUAL_MODULE_ID]) {
|
||||||
internals.ssrEntryChunk = chunk;
|
internals.ssrEntryChunk = chunk;
|
||||||
delete bundle[chunkName];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -121,7 +113,7 @@ export function pluginSSR(
|
||||||
vitePlugin,
|
vitePlugin,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
'build:post': async ({ mutate }) => {
|
'build:post': async () => {
|
||||||
if (!ssr) {
|
if (!ssr) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -135,17 +127,6 @@ export function pluginSSR(
|
||||||
}
|
}
|
||||||
// Mutate the filename
|
// Mutate the filename
|
||||||
internals.ssrEntryChunk.fileName = options.settings.config.build.serverEntry;
|
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') {
|
if (chunk.type === 'asset') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let shouldDeleteBundle = false;
|
|
||||||
for (const moduleKey of Object.keys(chunk.modules)) {
|
for (const moduleKey of Object.keys(chunk.modules)) {
|
||||||
if (moduleKey.startsWith(RESOLVED_SPLIT_MODULE_ID)) {
|
if (moduleKey.startsWith(RESOLVED_SPLIT_MODULE_ID)) {
|
||||||
internals.ssrSplitEntryChunks.set(moduleKey, chunk);
|
internals.ssrSplitEntryChunks.set(moduleKey, chunk);
|
||||||
storeEntryPoint(moduleKey, options, internals, chunk.fileName);
|
storeEntryPoint(moduleKey, options, internals, chunk.fileName);
|
||||||
shouldDeleteBundle = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (shouldDeleteBundle) {
|
|
||||||
delete bundle[chunkName];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -250,31 +226,6 @@ export function pluginSSRSplit(
|
||||||
vitePlugin,
|
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}';
|
contents.push(`import * as adapter from '${adapter.serverEntrypoint}';
|
||||||
import { renderers } from '${RENDERERS_MODULE_ID}';
|
import { renderers } from '${RENDERERS_MODULE_ID}';
|
||||||
import { deserializeManifest as _deserializeManifest } from 'astro/app';
|
import { manifest as defaultManifest} from '${SSR_MANIFEST_VIRTUAL_MODULE_ID}';
|
||||||
import { _privateSetManifestDontUseThis } from 'astro:ssr-manifest';
|
const _manifest = Object.assign(defaultManifest, {
|
||||||
const _manifest = Object.assign(_deserializeManifest('${manifestReplace}'), {
|
|
||||||
${pageMap},
|
${pageMap},
|
||||||
renderers,
|
renderers,
|
||||||
});
|
});
|
||||||
_privateSetManifestDontUseThis(_manifest);
|
|
||||||
const _args = ${adapter.args ? JSON.stringify(adapter.args) : 'undefined'};
|
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,
|
* 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 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 { appendForwardSlash, prependForwardSlash } from '../../core/path.js';
|
||||||
import { isModeServerWithNoAdapter } from '../../core/util.js';
|
import { isModeServerWithNoAdapter } from '../../core/util.js';
|
||||||
import { runHookBuildSetup } from '../../integrations/index.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 { PAGE_SCRIPT_ID } from '../../vite-plugin-scripts/index.js';
|
||||||
import { AstroError, AstroErrorData } from '../errors/index.js';
|
import { AstroError, AstroErrorData } from '../errors/index.js';
|
||||||
import { info } from '../logger/core.js';
|
import { info } from '../logger/core.js';
|
||||||
|
@ -28,10 +28,11 @@ import { createPluginContainer, type AstroBuildPluginContainer } from './plugin.
|
||||||
import { registerAllPlugins } from './plugins/index.js';
|
import { registerAllPlugins } from './plugins/index.js';
|
||||||
import { ASTRO_PAGE_RESOLVED_MODULE_ID } from './plugins/plugin-pages.js';
|
import { ASTRO_PAGE_RESOLVED_MODULE_ID } from './plugins/plugin-pages.js';
|
||||||
import { RESOLVED_RENDERERS_MODULE_ID } from './plugins/plugin-renderers.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 { ASTRO_PAGE_EXTENSION_POST_PATTERN } from './plugins/util.js';
|
||||||
import type { PageBuildData, StaticBuildOptions } from './types';
|
import type { PageBuildData, StaticBuildOptions } from './types';
|
||||||
import { getTimeStat } from './util.js';
|
import { getTimeStat } from './util.js';
|
||||||
|
import { RESOLVED_SSR_MANIFEST_VIRTUAL_MODULE_ID } from './plugins/plugin-manifest.js';
|
||||||
|
|
||||||
export async function viteBuild(opts: StaticBuildOptions) {
|
export async function viteBuild(opts: StaticBuildOptions) {
|
||||||
const { allPages, settings } = opts;
|
const { allPages, settings } = opts;
|
||||||
|
@ -147,7 +148,7 @@ async function ssrBuild(
|
||||||
) {
|
) {
|
||||||
const { allPages, settings, viteConfig } = opts;
|
const { allPages, settings, viteConfig } = opts;
|
||||||
const ssr = isServerLikeOutput(settings.config);
|
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 routes = Object.values(allPages).map((pd) => pd.route);
|
||||||
const { lastVitePlugins, vitePlugins } = container.runBeforeHook('ssr', input);
|
const { lastVitePlugins, vitePlugins } = container.runBeforeHook('ssr', input);
|
||||||
|
|
||||||
|
@ -184,10 +185,12 @@ async function ssrBuild(
|
||||||
);
|
);
|
||||||
} else if (chunkInfo.facadeModuleId?.startsWith(RESOLVED_SPLIT_MODULE_ID)) {
|
} else if (chunkInfo.facadeModuleId?.startsWith(RESOLVED_SPLIT_MODULE_ID)) {
|
||||||
return makeSplitEntryPointFileName(chunkInfo.facadeModuleId, routes);
|
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;
|
return opts.settings.config.build.serverEntry;
|
||||||
} else if (chunkInfo.facadeModuleId === RESOLVED_RENDERERS_MODULE_ID) {
|
} else if (chunkInfo.facadeModuleId === RESOLVED_RENDERERS_MODULE_ID) {
|
||||||
return 'renderers.mjs';
|
return 'renderers.mjs';
|
||||||
|
} else if (chunkInfo.facadeModuleId === RESOLVED_SSR_MANIFEST_VIRTUAL_MODULE_ID) {
|
||||||
|
return 'manifest.[hash].mjs';
|
||||||
} else {
|
} else {
|
||||||
return '[name].mjs';
|
return '[name].mjs';
|
||||||
}
|
}
|
||||||
|
|
|
@ -133,16 +133,16 @@ export class Logger {
|
||||||
this.options = options;
|
this.options = options;
|
||||||
}
|
}
|
||||||
|
|
||||||
info(label: string, message: string) {
|
info(label: string | null, message: string) {
|
||||||
info(this.options, label, message);
|
info(this.options, label, message);
|
||||||
}
|
}
|
||||||
warn(label: string, message: string) {
|
warn(label: string | null, message: string) {
|
||||||
warn(this.options, label, message);
|
warn(this.options, label, message);
|
||||||
}
|
}
|
||||||
error(label: string, message: string) {
|
error(label: string | null, message: string) {
|
||||||
error(this.options, label, message);
|
error(this.options, label, message);
|
||||||
}
|
}
|
||||||
debug(label: string, message: string) {
|
debug(label: string | null, message: string) {
|
||||||
debug(this.options, label, message);
|
debug(this.options, label, message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,12 +23,12 @@ type EndpointResultHandler = (
|
||||||
*/
|
*/
|
||||||
export class Pipeline {
|
export class Pipeline {
|
||||||
env: Environment;
|
env: Environment;
|
||||||
onRequest?: MiddlewareEndpointHandler;
|
#onRequest?: MiddlewareEndpointHandler;
|
||||||
/**
|
/**
|
||||||
* The handler accepts the *original* `Request` and result returned by the endpoint.
|
* The handler accepts the *original* `Request` and result returned by the endpoint.
|
||||||
* It must return a `Response`.
|
* It must return a `Response`.
|
||||||
*/
|
*/
|
||||||
endpointHandler?: EndpointResultHandler;
|
#endpointHandler?: EndpointResultHandler;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When creating a pipeline, an environment is mandatory.
|
* When creating a pipeline, an environment is mandatory.
|
||||||
|
@ -38,20 +38,29 @@ export class Pipeline {
|
||||||
this.env = env;
|
this.env = env;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setEnvironment() {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When rendering a route, an "endpoint" will a type that needs to be handled and transformed into a `Response`.
|
* 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.
|
* Each consumer might have different needs; use this function to set up the handler.
|
||||||
*/
|
*/
|
||||||
setEndpointHandler(handler: EndpointResultHandler) {
|
setEndpointHandler(handler: EndpointResultHandler) {
|
||||||
this.endpointHandler = handler;
|
this.#endpointHandler = handler;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A middleware function that will be called before each request.
|
* A middleware function that will be called before each request.
|
||||||
*/
|
*/
|
||||||
setMiddlewareFunction(onRequest: MiddlewareEndpointHandler) {
|
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,
|
renderContext,
|
||||||
this.env,
|
this.env,
|
||||||
componentInstance,
|
componentInstance,
|
||||||
this.onRequest
|
this.#onRequest
|
||||||
);
|
);
|
||||||
if (Pipeline.isEndpointResult(result, renderContext.route.type)) {
|
if (Pipeline.isEndpointResult(result, renderContext.route.type)) {
|
||||||
if (!this.endpointHandler) {
|
if (!this.#endpointHandler) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'You created a pipeline that does not know how to handle the result coming from an endpoint.'
|
'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 {
|
} else {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import type { AstroConfig } from '../@types/astro';
|
import type { AstroConfig } from '../@types/astro';
|
||||||
|
import { getOutDirWithinCwd } from '../core/build/common.js';
|
||||||
|
|
||||||
export function isServerLikeOutput(config: AstroConfig) {
|
export function isServerLikeOutput(config: AstroConfig) {
|
||||||
return config.output === 'server' || config.output === 'hybrid';
|
return config.output === 'server' || config.output === 'hybrid';
|
||||||
|
@ -7,3 +8,15 @@ export function isServerLikeOutput(config: AstroConfig) {
|
||||||
export function getPrerenderDefault(config: AstroConfig) {
|
export function getPrerenderDefault(config: AstroConfig) {
|
||||||
return config.output === 'hybrid';
|
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;
|
let fixture;
|
||||||
|
|
||||||
before(async () => {
|
before(async () => {
|
||||||
|
@ -86,7 +86,7 @@ describe('Assets Prefix - Static with path prefix', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Assets Prefix - Server', () => {
|
describe('Assets Prefix, server', () => {
|
||||||
let app;
|
let app;
|
||||||
|
|
||||||
before(async () => {
|
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;
|
let app;
|
||||||
|
|
||||||
before(async () => {
|
before(async () => {
|
||||||
|
|
|
@ -3,50 +3,54 @@ import { load as cheerioLoad } from 'cheerio';
|
||||||
import { loadFixture } from './test-utils.js';
|
import { loadFixture } from './test-utils.js';
|
||||||
import testAdapter from './test-adapter.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', () => {
|
describe('Hoisted scripts in SSR', () => {
|
||||||
/** @type {import('./test-utils').Fixture} */
|
/** @type {import('./test-utils').Fixture} */
|
||||||
let fixture;
|
let fixture;
|
||||||
|
|
||||||
|
describe('without base path', () => {
|
||||||
|
before(async () => {
|
||||||
|
fixture = await loadFixture({
|
||||||
|
root: './fixtures/ssr-hoisted-script/',
|
||||||
|
output: 'server',
|
||||||
|
adapter: testAdapter(),
|
||||||
|
});
|
||||||
|
await fixture.build();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Inlined scripts get included', async () => {
|
||||||
|
const html = await fetchHTML(fixture, '/');
|
||||||
|
const $ = cheerioLoad(html);
|
||||||
|
expect($('script').length).to.equal(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Hoisted scripts in SSR with base path', () => {
|
||||||
|
/** @type {import('./test-utils').Fixture} */
|
||||||
|
let fixture;
|
||||||
|
const base = '/hello';
|
||||||
|
|
||||||
before(async () => {
|
before(async () => {
|
||||||
fixture = await loadFixture({
|
fixture = await loadFixture({
|
||||||
root: './fixtures/ssr-hoisted-script/',
|
root: './fixtures/ssr-hoisted-script/',
|
||||||
output: 'server',
|
output: 'server',
|
||||||
adapter: testAdapter(),
|
adapter: testAdapter(),
|
||||||
|
base,
|
||||||
});
|
});
|
||||||
await fixture.build();
|
await fixture.build();
|
||||||
});
|
});
|
||||||
|
|
||||||
async function fetchHTML(path) {
|
it('Inlined scripts get included without base path in the script', async () => {
|
||||||
const app = await fixture.loadTestAdapterApp();
|
const html = await fetchHTML(fixture, '/hello/');
|
||||||
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);
|
const $ = cheerioLoad(html);
|
||||||
expect($('script').length).to.equal(1);
|
expect($('script').html()).to.equal('console.log("hello world");\n');
|
||||||
});
|
|
||||||
|
|
||||||
describe('base path', () => {
|
|
||||||
const base = '/hello';
|
|
||||||
|
|
||||||
before(async () => {
|
|
||||||
fixture = await loadFixture({
|
|
||||||
root: './fixtures/ssr-hoisted-script/',
|
|
||||||
output: 'server',
|
|
||||||
adapter: testAdapter(),
|
|
||||||
base,
|
|
||||||
});
|
|
||||||
await fixture.build();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Inlined scripts get included without base path in the script', async () => {
|
|
||||||
const html = await fetchHTML('/hello/');
|
|
||||||
const $ = cheerioLoad(html);
|
|
||||||
expect($('script').html()).to.equal('console.log("hello world");\n');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue