502 lines
18 KiB
Plaintext
502 lines
18 KiB
Plaintext
import { bgGreen, black, blue, bold, dim, green, magenta, red } from "kleur/colors";
|
|
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import { fileURLToPath } from "node:url";
|
|
import PQueue from "p-queue";
|
|
import {
|
|
generateImagesForPath,
|
|
getStaticImageList,
|
|
prepareAssetsGenerationEnv
|
|
} from "../../assets/build/generate.js";
|
|
import { hasPrerenderedPages } from "../../core/build/internal.js";
|
|
import {
|
|
isRelativePath,
|
|
joinPaths,
|
|
prependForwardSlash,
|
|
removeLeadingForwardSlash,
|
|
removeTrailingForwardSlash
|
|
} from "../../core/path.js";
|
|
import { createI18nMiddleware, i18nPipelineHook } from "../../i18n/middleware.js";
|
|
import { runHookBuildGenerated } from "../../integrations/index.js";
|
|
import { getOutputDirectory, isServerLikeOutput } from "../../prerender/utils.js";
|
|
import { PAGE_SCRIPT_ID } from "../../vite-plugin-scripts/index.js";
|
|
import { AstroError, AstroErrorData } from "../errors/index.js";
|
|
import { sequence } from "../middleware/index.js";
|
|
import { routeIsFallback } from "../redirects/helpers.js";
|
|
import {
|
|
RedirectSinglePageBuiltModule,
|
|
getRedirectLocationOrThrow,
|
|
routeIsRedirect
|
|
} from "../redirects/index.js";
|
|
import { createRenderContext } from "../render/index.js";
|
|
import { callGetStaticPaths } from "../render/route-cache.js";
|
|
import {
|
|
createAssetLink,
|
|
createModuleScriptsSet,
|
|
createStylesheetElementSet
|
|
} from "../render/ssr-element.js";
|
|
import { createRequest } from "../request.js";
|
|
import { matchRoute } from "../routing/match.js";
|
|
import { getOutputFilename } from "../util.js";
|
|
import { BuildPipeline } from "./buildPipeline.js";
|
|
import { getOutDirWithinCwd, getOutFile, getOutFolder } from "./common.js";
|
|
import {
|
|
cssOrder,
|
|
getEntryFilePathFromComponentPath,
|
|
getPageDataByComponent,
|
|
mergeInlineCss
|
|
} from "./internal.js";
|
|
import { getTimeStat, shouldAppendForwardSlash } from "./util.js";
|
|
function createEntryURL(filePath, outFolder) {
|
|
return new URL("./" + filePath + `?time=${Date.now()}`, outFolder);
|
|
}
|
|
async function getEntryForRedirectRoute(route, internals, outFolder) {
|
|
if (route.type !== "redirect") {
|
|
throw new Error(`Expected a redirect route.`);
|
|
}
|
|
if (route.redirectRoute) {
|
|
const filePath = getEntryFilePathFromComponentPath(internals, route.redirectRoute.component);
|
|
if (filePath) {
|
|
const url = createEntryURL(filePath, outFolder);
|
|
const ssrEntryPage = await import(url.toString());
|
|
return ssrEntryPage;
|
|
}
|
|
}
|
|
return RedirectSinglePageBuiltModule;
|
|
}
|
|
async function getEntryForFallbackRoute(route, internals, outFolder) {
|
|
if (route.type !== "fallback") {
|
|
throw new Error(`Expected a redirect route.`);
|
|
}
|
|
if (route.redirectRoute) {
|
|
const filePath = getEntryFilePathFromComponentPath(internals, route.redirectRoute.component);
|
|
if (filePath) {
|
|
const url = createEntryURL(filePath, outFolder);
|
|
const ssrEntryPage = await import(url.toString());
|
|
return ssrEntryPage;
|
|
}
|
|
}
|
|
return RedirectSinglePageBuiltModule;
|
|
}
|
|
function rootRelativeFacadeId(facadeId, settings) {
|
|
return facadeId.slice(fileURLToPath(settings.config.root).length);
|
|
}
|
|
function chunkIsPage(settings, output, internals) {
|
|
if (output.type !== "chunk") {
|
|
return false;
|
|
}
|
|
const chunk = output;
|
|
if (chunk.facadeModuleId) {
|
|
const facadeToEntryId = prependForwardSlash(
|
|
rootRelativeFacadeId(chunk.facadeModuleId, settings)
|
|
);
|
|
return internals.entrySpecifierToBundleMap.has(facadeToEntryId);
|
|
}
|
|
return false;
|
|
}
|
|
async function generatePages(opts, internals) {
|
|
const generatePagesTimer = performance.now();
|
|
const ssr = isServerLikeOutput(opts.settings.config);
|
|
let manifest;
|
|
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());
|
|
let middleware = (_, next) => next();
|
|
try {
|
|
middleware = await import(new URL("middleware.mjs", baseDirectory).toString()).then(
|
|
(mod) => mod.onRequest
|
|
);
|
|
} catch {
|
|
}
|
|
manifest = createBuildManifest(
|
|
opts.settings,
|
|
internals,
|
|
renderers.renderers,
|
|
middleware
|
|
);
|
|
}
|
|
const pipeline = new BuildPipeline(opts, internals, manifest);
|
|
const outFolder = ssr ? opts.settings.config.build.server : getOutDirWithinCwd(opts.settings.config.outDir);
|
|
const logger = pipeline.getLogger();
|
|
if (ssr && !hasPrerenderedPages(internals)) {
|
|
delete globalThis?.astroAsset?.addStaticImage;
|
|
return;
|
|
}
|
|
const verb = ssr ? "prerendering" : "generating";
|
|
logger.info("SKIP_FORMAT", `
|
|
${bgGreen(black(` ${verb} static routes `))}`);
|
|
const builtPaths = /* @__PURE__ */ new Set();
|
|
const pagesToGenerate = pipeline.retrieveRoutesToGenerate();
|
|
if (ssr) {
|
|
for (const [pageData, filePath] of pagesToGenerate) {
|
|
if (pageData.route.prerender) {
|
|
const ssrEntryURLPage = createEntryURL(filePath, outFolder);
|
|
const ssrEntryPage = await import(ssrEntryURLPage.toString());
|
|
if (opts.settings.adapter?.adapterFeatures?.functionPerRoute) {
|
|
const ssrEntry = ssrEntryPage?.pageModule;
|
|
if (ssrEntry) {
|
|
await generatePage(pageData, ssrEntry, builtPaths, pipeline);
|
|
} 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.`
|
|
);
|
|
}
|
|
} else {
|
|
const ssrEntry = ssrEntryPage;
|
|
await generatePage(pageData, ssrEntry, builtPaths, pipeline);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
for (const [pageData, filePath] of pagesToGenerate) {
|
|
if (routeIsRedirect(pageData.route)) {
|
|
const entry = await getEntryForRedirectRoute(pageData.route, internals, outFolder);
|
|
await generatePage(pageData, entry, builtPaths, pipeline);
|
|
} else if (routeIsFallback(pageData.route)) {
|
|
const entry = await getEntryForFallbackRoute(pageData.route, internals, outFolder);
|
|
await generatePage(pageData, entry, builtPaths, pipeline);
|
|
} else {
|
|
const ssrEntryURLPage = createEntryURL(filePath, outFolder);
|
|
const entry = await import(ssrEntryURLPage.toString());
|
|
await generatePage(pageData, entry, builtPaths, pipeline);
|
|
}
|
|
}
|
|
}
|
|
logger.info(
|
|
null,
|
|
green(`\u2713 Completed in ${getTimeStat(generatePagesTimer, performance.now())}.
|
|
`)
|
|
);
|
|
const staticImageList = getStaticImageList();
|
|
if (staticImageList.size) {
|
|
logger.info("SKIP_FORMAT", `${bgGreen(black(` generating optimized images `))}`);
|
|
const totalCount = Array.from(staticImageList.values()).map((x) => x.transforms.size).reduce((a, b) => a + b, 0);
|
|
const cpuCount = os.cpus().length;
|
|
const assetsCreationEnvironment = await prepareAssetsGenerationEnv(pipeline, totalCount);
|
|
const queue = new PQueue({ concurrency: Math.max(cpuCount, 1) });
|
|
const assetsTimer = performance.now();
|
|
for (const [originalPath, transforms] of staticImageList) {
|
|
await generateImagesForPath(originalPath, transforms, assetsCreationEnvironment, queue);
|
|
}
|
|
await queue.onIdle();
|
|
const assetsTimeEnd = performance.now();
|
|
logger.info(null, green(`\u2713 Completed in ${getTimeStat(assetsTimer, assetsTimeEnd)}.
|
|
`));
|
|
delete globalThis?.astroAsset?.addStaticImage;
|
|
}
|
|
await runHookBuildGenerated({
|
|
config: opts.settings.config,
|
|
logger: pipeline.getLogger()
|
|
});
|
|
}
|
|
async function generatePage(pageData, ssrEntry, builtPaths, pipeline) {
|
|
const logger = pipeline.getLogger();
|
|
const config = pipeline.getConfig();
|
|
const manifest = pipeline.getManifest();
|
|
const pageModulePromise = ssrEntry.page;
|
|
const onRequest = manifest.middleware;
|
|
const pageInfo = getPageDataByComponent(pipeline.getInternals(), pageData.route.component);
|
|
const styles = pageData.styles.sort(cssOrder).map(({ sheet }) => sheet).reduce(mergeInlineCss, []);
|
|
const linkIds = [];
|
|
const scripts = pageInfo?.hoistedScript ?? null;
|
|
const i18nMiddleware = createI18nMiddleware(
|
|
manifest.i18n,
|
|
manifest.base,
|
|
manifest.trailingSlash,
|
|
manifest.buildFormat
|
|
);
|
|
if (config.i18n && i18nMiddleware) {
|
|
pipeline.setMiddlewareFunction(sequence(i18nMiddleware, onRequest));
|
|
pipeline.onBeforeRenderRoute(i18nPipelineHook);
|
|
} else {
|
|
pipeline.setMiddlewareFunction(onRequest);
|
|
}
|
|
if (!pageModulePromise) {
|
|
throw new Error(
|
|
`Unable to find the module for ${pageData.component}. This is unexpected and likely a bug in Astro, please report.`
|
|
);
|
|
}
|
|
const pageModule = await pageModulePromise();
|
|
const generationOptions = {
|
|
pageData,
|
|
linkIds,
|
|
scripts,
|
|
styles,
|
|
mod: pageModule
|
|
};
|
|
for (const route of eachRouteInRouteData(pageData)) {
|
|
const icon = route.type === "page" || route.type === "redirect" || route.type === "fallback" ? green("\u25B6") : magenta("\u03BB");
|
|
logger.info(null, `${icon} ${getPrettyRouteName(route)}`);
|
|
const paths = await getPathsForRoute(route, pageModule, pipeline, builtPaths);
|
|
let timeStart = performance.now();
|
|
let prevTimeEnd = timeStart;
|
|
for (let i = 0; i < paths.length; i++) {
|
|
const path = paths[i];
|
|
pipeline.getEnvironment().logger.debug("build", `Generating: ${path}`);
|
|
const filePath = getOutputFilename(pipeline.getConfig(), path, pageData.route.type);
|
|
const lineIcon = i === paths.length - 1 ? "\u2514\u2500" : "\u251C\u2500";
|
|
logger.info(null, ` ${blue(lineIcon)} ${dim(filePath)}`, false);
|
|
await generatePath(path, pipeline, generationOptions, route);
|
|
const timeEnd = performance.now();
|
|
const timeChange = getTimeStat(prevTimeEnd, timeEnd);
|
|
const timeIncrease = `(+${timeChange})`;
|
|
logger.info("SKIP_FORMAT", ` ${dim(timeIncrease)}`);
|
|
prevTimeEnd = timeEnd;
|
|
}
|
|
}
|
|
}
|
|
function* eachRouteInRouteData(data) {
|
|
yield data.route;
|
|
for (const fallbackRoute of data.route.fallbackRoutes) {
|
|
yield fallbackRoute;
|
|
}
|
|
}
|
|
async function getPathsForRoute(route, mod, pipeline, builtPaths) {
|
|
const opts = pipeline.getStaticBuildOptions();
|
|
const logger = pipeline.getLogger();
|
|
let paths = [];
|
|
if (route.pathname) {
|
|
paths.push(route.pathname);
|
|
builtPaths.add(route.pathname);
|
|
for (const virtualRoute of route.fallbackRoutes) {
|
|
if (virtualRoute.pathname) {
|
|
paths.push(virtualRoute.pathname);
|
|
builtPaths.add(virtualRoute.pathname);
|
|
}
|
|
}
|
|
} else {
|
|
const staticPaths = await callGetStaticPaths({
|
|
mod,
|
|
route,
|
|
routeCache: opts.routeCache,
|
|
logger,
|
|
ssr: isServerLikeOutput(opts.settings.config)
|
|
}).catch((err) => {
|
|
logger.debug("build", `\u251C\u2500\u2500 ${bold(red("\u2717"))} ${route.component}`);
|
|
throw err;
|
|
});
|
|
const label = staticPaths.length === 1 ? "page" : "pages";
|
|
logger.debug(
|
|
"build",
|
|
`\u251C\u2500\u2500 ${bold(green("\u2714"))} ${route.component} \u2192 ${magenta(`[${staticPaths.length} ${label}]`)}`
|
|
);
|
|
paths = staticPaths.map((staticPath) => {
|
|
try {
|
|
return route.generate(staticPath.params);
|
|
} catch (e) {
|
|
if (e instanceof TypeError) {
|
|
throw getInvalidRouteSegmentError(e, route, staticPath);
|
|
}
|
|
throw e;
|
|
}
|
|
}).filter((staticPath) => {
|
|
if (!builtPaths.has(removeTrailingForwardSlash(staticPath))) {
|
|
return true;
|
|
}
|
|
const matchedRoute = matchRoute(staticPath, opts.manifest);
|
|
return matchedRoute === route;
|
|
});
|
|
for (const staticPath of paths) {
|
|
builtPaths.add(removeTrailingForwardSlash(staticPath));
|
|
}
|
|
}
|
|
return paths;
|
|
}
|
|
function getInvalidRouteSegmentError(e, route, staticPath) {
|
|
const invalidParam = e.message.match(/^Expected "([^"]+)"/)?.[1];
|
|
const received = invalidParam ? staticPath.params[invalidParam] : void 0;
|
|
let hint = "Learn about dynamic routes at https://docs.astro.build/en/core-concepts/routing/#dynamic-routes";
|
|
if (invalidParam && typeof received === "string") {
|
|
const matchingSegment = route.segments.find(
|
|
(segment) => segment[0]?.content === invalidParam
|
|
)?.[0];
|
|
const mightBeMissingSpread = matchingSegment?.dynamic && !matchingSegment?.spread;
|
|
if (mightBeMissingSpread) {
|
|
hint = `If the param contains slashes, try using a rest parameter: **[...${invalidParam}]**. Learn more at https://docs.astro.build/en/core-concepts/routing/#dynamic-routes`;
|
|
}
|
|
}
|
|
return new AstroError({
|
|
...AstroErrorData.InvalidDynamicRoute,
|
|
message: invalidParam ? AstroErrorData.InvalidDynamicRoute.message(
|
|
route.route,
|
|
JSON.stringify(invalidParam),
|
|
JSON.stringify(received)
|
|
) : `Generated path for ${route.route} is invalid.`,
|
|
hint
|
|
});
|
|
}
|
|
function addPageName(pathname, opts) {
|
|
const trailingSlash = opts.settings.config.trailingSlash;
|
|
const buildFormat = opts.settings.config.build.format;
|
|
const pageName = shouldAppendForwardSlash(trailingSlash, buildFormat) ? pathname.replace(/\/?$/, "/").replace(/^\//, "") : pathname.replace(/^\//, "");
|
|
opts.pageNames.push(pageName);
|
|
}
|
|
function getUrlForPath(pathname, base, origin, format, trailingSlash, routeType) {
|
|
const ending = format === "directory" ? trailingSlash === "never" ? "" : "/" : ".html";
|
|
let buildPathname;
|
|
if (pathname === "/" || pathname === "") {
|
|
buildPathname = base;
|
|
} else if (routeType === "endpoint") {
|
|
const buildPathRelative = removeLeadingForwardSlash(pathname);
|
|
buildPathname = joinPaths(base, buildPathRelative);
|
|
} else {
|
|
const buildPathRelative = removeTrailingForwardSlash(removeLeadingForwardSlash(pathname)) + ending;
|
|
buildPathname = joinPaths(base, buildPathRelative);
|
|
}
|
|
const url = new URL(buildPathname, origin);
|
|
return url;
|
|
}
|
|
async function generatePath(pathname, pipeline, gopts, route) {
|
|
const { mod, scripts: hoistedScripts, styles: _styles } = gopts;
|
|
const manifest = pipeline.getManifest();
|
|
const logger = pipeline.getLogger();
|
|
logger.debug("build", `Generating: ${pathname}`);
|
|
const links = /* @__PURE__ */ new Set();
|
|
const scripts = createModuleScriptsSet(
|
|
hoistedScripts ? [hoistedScripts] : [],
|
|
manifest.base,
|
|
manifest.assetsPrefix
|
|
);
|
|
const styles = createStylesheetElementSet(_styles, manifest.base, manifest.assetsPrefix);
|
|
if (pipeline.getSettings().scripts.some((script) => script.stage === "page")) {
|
|
const hashedFilePath = pipeline.getInternals().entrySpecifierToBundleMap.get(PAGE_SCRIPT_ID);
|
|
if (typeof hashedFilePath !== "string") {
|
|
throw new Error(`Cannot find the built path for ${PAGE_SCRIPT_ID}`);
|
|
}
|
|
const src = createAssetLink(hashedFilePath, manifest.base, manifest.assetsPrefix);
|
|
scripts.add({
|
|
props: { type: "module", src },
|
|
children: ""
|
|
});
|
|
}
|
|
for (const script of pipeline.getSettings().scripts) {
|
|
if (script.stage === "head-inline") {
|
|
scripts.add({
|
|
props: {},
|
|
children: script.content
|
|
});
|
|
}
|
|
}
|
|
if (route.type === "page") {
|
|
addPageName(pathname, pipeline.getStaticBuildOptions());
|
|
}
|
|
const ssr = isServerLikeOutput(pipeline.getConfig());
|
|
const url = getUrlForPath(
|
|
pathname,
|
|
pipeline.getConfig().base,
|
|
pipeline.getStaticBuildOptions().origin,
|
|
pipeline.getConfig().build.format,
|
|
pipeline.getConfig().trailingSlash,
|
|
route.type
|
|
);
|
|
const request = createRequest({
|
|
url,
|
|
headers: new Headers(),
|
|
logger: pipeline.getLogger(),
|
|
ssr
|
|
});
|
|
const i18n = pipeline.getConfig().i18n;
|
|
const renderContext = await createRenderContext({
|
|
pathname,
|
|
request,
|
|
componentMetadata: manifest.componentMetadata,
|
|
scripts,
|
|
styles,
|
|
links,
|
|
route,
|
|
env: pipeline.getEnvironment(),
|
|
mod,
|
|
locales: i18n?.locales,
|
|
routing: i18n?.routing,
|
|
defaultLocale: i18n?.defaultLocale
|
|
});
|
|
let body;
|
|
let response;
|
|
try {
|
|
response = await pipeline.renderRoute(renderContext, mod);
|
|
} catch (err) {
|
|
if (!AstroError.is(err) && !err.id && typeof err === "object") {
|
|
err.id = route.component;
|
|
}
|
|
throw err;
|
|
}
|
|
if (response.status >= 300 && response.status < 400) {
|
|
if (!pipeline.getConfig().build.redirects) {
|
|
return;
|
|
}
|
|
const locationSite = getRedirectLocationOrThrow(response.headers);
|
|
const siteURL = pipeline.getConfig().site;
|
|
const location = siteURL ? new URL(locationSite, siteURL) : locationSite;
|
|
const fromPath = new URL(renderContext.request.url).pathname;
|
|
const delay = response.status === 302 ? 2 : 0;
|
|
body = `<!doctype html>
|
|
<title>Redirecting to: ${location}</title>
|
|
<meta http-equiv="refresh" content="${delay};url=${location}">
|
|
<meta name="robots" content="noindex">
|
|
<link rel="canonical" href="${location}">
|
|
<body>
|
|
<a href="${location}">Redirecting from <code>${fromPath}</code> to <code>${location}</code></a>
|
|
</body>`;
|
|
if (pipeline.getConfig().compressHTML === true) {
|
|
body = body.replaceAll("\n", "");
|
|
}
|
|
if (route.type !== "redirect") {
|
|
route.redirect = location.toString();
|
|
}
|
|
} else {
|
|
if (!response.body)
|
|
return;
|
|
body = Buffer.from(await response.arrayBuffer());
|
|
}
|
|
const outFolder = getOutFolder(pipeline.getConfig(), pathname, route.type);
|
|
const outFile = getOutFile(pipeline.getConfig(), outFolder, pathname, route.type);
|
|
route.distURL = outFile;
|
|
await fs.promises.mkdir(outFolder, { recursive: true });
|
|
await fs.promises.writeFile(outFile, body);
|
|
}
|
|
function getPrettyRouteName(route) {
|
|
if (isRelativePath(route.component)) {
|
|
return route.route;
|
|
} else if (route.component.includes("node_modules/")) {
|
|
return route.component.match(/.*node_modules\/(.+)/)?.[1] ?? route.component;
|
|
} else {
|
|
return route.component;
|
|
}
|
|
}
|
|
function createBuildManifest(settings, internals, renderers, middleware) {
|
|
let i18nManifest = void 0;
|
|
if (settings.config.i18n) {
|
|
i18nManifest = {
|
|
fallback: settings.config.i18n.fallback,
|
|
routing: settings.config.i18n.routing,
|
|
defaultLocale: settings.config.i18n.defaultLocale,
|
|
locales: settings.config.i18n.locales
|
|
};
|
|
}
|
|
return {
|
|
trailingSlash: settings.config.trailingSlash,
|
|
assets: /* @__PURE__ */ new Set(),
|
|
entryModules: Object.fromEntries(internals.entrySpecifierToBundleMap.entries()),
|
|
routes: [],
|
|
adapterName: "",
|
|
clientDirectives: settings.clientDirectives,
|
|
compressHTML: settings.config.compressHTML,
|
|
renderers,
|
|
base: settings.config.base,
|
|
assetsPrefix: settings.config.build.assetsPrefix,
|
|
site: settings.config.site ? new URL(settings.config.base, settings.config.site).toString() : settings.config.site,
|
|
componentMetadata: internals.componentMetadata,
|
|
i18n: i18nManifest,
|
|
buildFormat: settings.config.build.format,
|
|
middleware
|
|
};
|
|
}
|
|
export {
|
|
chunkIsPage,
|
|
generatePages,
|
|
rootRelativeFacadeId
|
|
};
|