import { teardown } from "@astrojs/compiler"; import * as eslexer from "es-module-lexer"; import glob from "fast-glob"; import { bgGreen, bgMagenta, black, green } from "kleur/colors"; import fs from "node:fs"; import path, { extname } from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; import * as vite from "vite"; import { PROPAGATED_ASSET_FLAG } from "../../content/consts.js"; import { hasAnyContentFlag } from "../../content/utils.js"; import { createBuildInternals, eachPageData } from "../../core/build/internal.js"; import { emptyDir, removeEmptyDirs } from "../../core/fs/index.js"; import { appendForwardSlash, prependForwardSlash, removeFileExtension } from "../../core/path.js"; import { isModeServerWithNoAdapter } from "../../core/util.js"; import { runHookBuildSetup } 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 { routeIsRedirect } from "../redirects/index.js"; import { getOutDirWithinCwd } from "./common.js"; import { generatePages } from "./generate.js"; import { trackPageData } from "./internal.js"; import { createPluginContainer } from "./plugin.js"; import { registerAllPlugins } from "./plugins/index.js"; import { RESOLVED_SSR_MANIFEST_VIRTUAL_MODULE_ID } from "./plugins/plugin-manifest.js"; import { ASTRO_PAGE_RESOLVED_MODULE_ID } from "./plugins/plugin-pages.js"; import { RESOLVED_RENDERERS_MODULE_ID } from "./plugins/plugin-renderers.js"; import { RESOLVED_SPLIT_MODULE_ID, RESOLVED_SSR_VIRTUAL_MODULE_ID } from "./plugins/plugin-ssr.js"; import { ASTRO_PAGE_EXTENSION_POST_PATTERN } from "./plugins/util.js"; import { encodeName, getTimeStat, viteBuildReturnToRollupOutputs } from "./util.js"; async function viteBuild(opts) { const { allPages, settings } = opts; if (isModeServerWithNoAdapter(opts.settings)) { throw new AstroError(AstroErrorData.NoAdapterInstalled); } settings.timer.start("SSR build"); const pageInput = /* @__PURE__ */ new Set(); const internals = createBuildInternals(); for (const [component, pageData] of Object.entries(allPages)) { const astroModuleURL = new URL("./" + component, settings.config.root); const astroModuleId = prependForwardSlash(component); trackPageData(internals, component, pageData, astroModuleId, astroModuleURL); if (!routeIsRedirect(pageData.route)) { pageInput.add(astroModuleId); } } if (settings.config?.vite?.build?.emptyOutDir !== false) { emptyDir(settings.config.outDir, new Set(".git")); } const container = createPluginContainer(opts, internals); registerAllPlugins(container); const ssrTime = performance.now(); opts.logger.info("build", `Building ${settings.config.output} entrypoints...`); const ssrOutput = await ssrBuild(opts, internals, pageInput, container); opts.logger.info("build", green(`\u2713 Completed in ${getTimeStat(ssrTime, performance.now())}.`)); settings.timer.end("SSR build"); settings.timer.start("Client build"); const rendererClientEntrypoints = settings.renderers.map((r) => r.clientEntrypoint).filter((a) => typeof a === "string"); const clientInput = /* @__PURE__ */ new Set([ ...internals.cachedClientEntries, ...internals.discoveredHydratedComponents.keys(), ...internals.discoveredClientOnlyComponents.keys(), ...rendererClientEntrypoints, ...internals.discoveredScripts ]); if (settings.scripts.some((script) => script.stage === "page")) { clientInput.add(PAGE_SCRIPT_ID); } const clientOutput = await clientBuild(opts, internals, clientInput, container); const ssrOutputs = viteBuildReturnToRollupOutputs(ssrOutput); const clientOutputs = viteBuildReturnToRollupOutputs(clientOutput ?? []); await runPostBuildHooks(container, ssrOutputs, clientOutputs); settings.timer.end("Client build"); internals.ssrEntryChunk = void 0; if (opts.teardownCompiler) { teardown(); } const ssrOutputChunkNames = []; for (const output of ssrOutputs) { for (const chunk of output.output) { if (chunk.type === "chunk") { ssrOutputChunkNames.push(chunk.fileName); } } } return { internals, ssrOutputChunkNames }; } async function staticBuild(opts, internals, ssrOutputChunkNames) { const { settings } = opts; switch (true) { case settings.config.output === "static": { settings.timer.start("Static generate"); await generatePages(opts, internals); await cleanServerOutput(opts, ssrOutputChunkNames, internals); settings.timer.end("Static generate"); return; } case isServerLikeOutput(settings.config): { settings.timer.start("Server generate"); await generatePages(opts, internals); await cleanStaticOutput(opts, internals, ssrOutputChunkNames); opts.logger.info(null, ` ${bgMagenta(black(" finalizing server assets "))} `); await ssrMoveAssets(opts); settings.timer.end("Server generate"); return; } } } async function ssrBuild(opts, internals, input, container) { const buildID = Date.now().toString(); const { allPages, settings, viteConfig } = opts; const ssr = isServerLikeOutput(settings.config); const out = getOutputDirectory(settings.config); const routes = Object.values(allPages).flatMap((pageData) => pageData.route); const isContentCache = !ssr && settings.config.experimental.contentCollectionCache; const { lastVitePlugins, vitePlugins } = await container.runBeforeHook("server", input); const viteBuildConfig = { ...viteConfig, mode: viteConfig.mode || "production", logLevel: viteConfig.logLevel ?? "error", build: { target: "esnext", // Vite defaults cssMinify to false in SSR by default, but we want to minify it // as the CSS generated are used and served to the client. cssMinify: viteConfig.build?.minify == null ? true : !!viteConfig.build?.minify, ...viteConfig.build, emptyOutDir: false, manifest: false, outDir: fileURLToPath(out), copyPublicDir: !ssr, rollupOptions: { ...viteConfig.build?.rollupOptions, input: [], output: { hoistTransitiveImports: isContentCache, format: "esm", minifyInternalExports: !isContentCache, // Server chunks can't go in the assets (_astro) folder // We need to keep these separate chunkFileNames(chunkInfo) { const { name } = chunkInfo; let prefix = "chunks/"; let suffix = "_[hash].mjs"; if (isContentCache) { prefix += `${buildID}/`; suffix = ".mjs"; } if (isContentCache && name.includes("/content/")) { const parts = name.split("/"); if (parts.at(1) === "content") { return encodeName(parts.slice(1).join("/")); } } if (name.includes(ASTRO_PAGE_EXTENSION_POST_PATTERN)) { const [sanitizedName] = name.split(ASTRO_PAGE_EXTENSION_POST_PATTERN); return [prefix, sanitizedName, suffix].join(""); } if (name.startsWith("pages/")) { const sanitizedName = name.split(".")[0]; return [prefix, sanitizedName, suffix].join(""); } const encoded = encodeName(name); return [prefix, encoded, suffix].join(""); }, assetFileNames: `${settings.config.build.assets}/[name].[hash][extname]`, ...viteConfig.build?.rollupOptions?.output, entryFileNames(chunkInfo) { if (chunkInfo.facadeModuleId?.startsWith(ASTRO_PAGE_RESOLVED_MODULE_ID)) { return makeAstroPageEntryPointFileName( ASTRO_PAGE_RESOLVED_MODULE_ID, chunkInfo.facadeModuleId, routes ); } else if (chunkInfo.facadeModuleId?.startsWith(RESOLVED_SPLIT_MODULE_ID)) { return makeSplitEntryPointFileName(chunkInfo.facadeModuleId, routes); } else if (chunkInfo.facadeModuleId === RESOLVED_SSR_VIRTUAL_MODULE_ID) { return opts.settings.config.build.serverEntry; } else if (chunkInfo.facadeModuleId === RESOLVED_RENDERERS_MODULE_ID) { return "renderers.mjs"; } else if (chunkInfo.facadeModuleId === RESOLVED_SSR_MANIFEST_VIRTUAL_MODULE_ID) { return "manifest_[hash].mjs"; } else if (settings.config.experimental.contentCollectionCache && chunkInfo.facadeModuleId && hasAnyContentFlag(chunkInfo.facadeModuleId)) { const [srcRelative, flag] = chunkInfo.facadeModuleId.split("/src/")[1].split("?"); if (flag === PROPAGATED_ASSET_FLAG) { return encodeName(`${removeFileExtension(srcRelative)}.entry.mjs`); } return encodeName(`${removeFileExtension(srcRelative)}.mjs`); } else { return "[name].mjs"; } } } }, ssr: true, ssrEmitAssets: true, // improve build performance minify: false, modulePreload: { polyfill: false }, reportCompressedSize: false }, plugins: [...vitePlugins, ...viteConfig.plugins || [], ...lastVitePlugins], envPrefix: viteConfig.envPrefix ?? "PUBLIC_", base: settings.config.base }; const updatedViteBuildConfig = await runHookBuildSetup({ config: settings.config, pages: internals.pagesByComponent, vite: viteBuildConfig, target: "server", logger: opts.logger }); return await vite.build(updatedViteBuildConfig); } async function clientBuild(opts, internals, input, container) { const { settings, viteConfig } = opts; const ssr = isServerLikeOutput(settings.config); const out = ssr ? settings.config.build.client : getOutDirWithinCwd(settings.config.outDir); if (!input.size) { if (ssr) { await copyFiles(settings.config.publicDir, out, true); } return null; } const { lastVitePlugins, vitePlugins } = await container.runBeforeHook("client", input); opts.logger.info("SKIP_FORMAT", ` ${bgGreen(black(" building client (vite) "))}`); const viteBuildConfig = { ...viteConfig, mode: viteConfig.mode || "production", build: { target: "esnext", ...viteConfig.build, emptyOutDir: false, outDir: fileURLToPath(out), rollupOptions: { ...viteConfig.build?.rollupOptions, input: Array.from(input), output: { format: "esm", entryFileNames: `${settings.config.build.assets}/[name].[hash].js`, chunkFileNames: `${settings.config.build.assets}/[name].[hash].js`, assetFileNames: `${settings.config.build.assets}/[name].[hash][extname]`, ...viteConfig.build?.rollupOptions?.output }, preserveEntrySignatures: "exports-only" } }, plugins: [...vitePlugins, ...viteConfig.plugins || [], ...lastVitePlugins], envPrefix: viteConfig.envPrefix ?? "PUBLIC_", base: settings.config.base }; await runHookBuildSetup({ config: settings.config, pages: internals.pagesByComponent, vite: viteBuildConfig, target: "client", logger: opts.logger }); const buildResult = await vite.build(viteBuildConfig); return buildResult; } async function runPostBuildHooks(container, ssrOutputs, clientOutputs) { const mutations = await container.runPostHook(ssrOutputs, clientOutputs); const config = container.options.settings.config; const build = container.options.settings.config.build; for (const [fileName, mutation] of mutations) { const root = isServerLikeOutput(config) ? mutation.targets.includes("server") ? build.server : build.client : getOutDirWithinCwd(config.outDir); const fullPath = path.join(fileURLToPath(root), fileName); const fileURL = pathToFileURL(fullPath); await fs.promises.mkdir(new URL("./", fileURL), { recursive: true }); await fs.promises.writeFile(fileURL, mutation.code, "utf-8"); } } async function cleanStaticOutput(opts, internals, ssrOutputChunkNames) { const allStaticFiles = /* @__PURE__ */ new Set(); for (const pageData of eachPageData(internals)) { if (pageData.route.prerender) { const { moduleSpecifier } = pageData; const pageBundleId = internals.pageToBundleMap.get(moduleSpecifier); const entryBundleId = internals.entrySpecifierToBundleMap.get(moduleSpecifier); allStaticFiles.add(pageBundleId ?? entryBundleId); } } const ssr = isServerLikeOutput(opts.settings.config); const out = ssr ? opts.settings.config.build.server : getOutDirWithinCwd(opts.settings.config.outDir); const files = ssrOutputChunkNames.filter((f) => f.endsWith(".mjs")); if (files.length) { await eslexer.init; await Promise.all( files.map(async (filename) => { if (!allStaticFiles.has(filename)) { return; } const url = new URL(filename, out); const text = await fs.promises.readFile(url, { encoding: "utf8" }); const [, exports] = eslexer.parse(text); let value = "const noop = () => {};"; for (const e of exports) { if (e.n === "default") value += ` export default noop;`; else value += ` export const ${e.n} = noop;`; } await fs.promises.writeFile(url, value, { encoding: "utf8" }); }) ); removeEmptyDirs(out); } } async function cleanServerOutput(opts, ssrOutputChunkNames, internals) { const out = getOutDirWithinCwd(opts.settings.config.outDir); const files = ssrOutputChunkNames.filter((f) => f.endsWith(".mjs")); if (internals.manifestFileName) { files.push(internals.manifestFileName); } if (files.length) { await Promise.all( files.map(async (filename) => { const url = new URL(filename, out); await fs.promises.rm(url); }) ); removeEmptyDirs(out); } if (out.toString() !== opts.settings.config.outDir.toString()) { await copyFiles(out, opts.settings.config.outDir, true); await fs.promises.rm(out, { recursive: true }); return; } } async function copyFiles(fromFolder, toFolder, includeDotfiles = false) { const files = await glob("**/*", { cwd: fileURLToPath(fromFolder), dot: includeDotfiles }); if (files.length === 0) return; await Promise.all( files.map(async function copyFile(filename) { const from = new URL(filename, fromFolder); const to = new URL(filename, toFolder); const lastFolder = new URL("./", to); return fs.promises.mkdir(lastFolder, { recursive: true }).then(async function fsCopyFile() { const p = await fs.promises.copyFile(from, to, fs.constants.COPYFILE_FICLONE); return p; }); }) ); } async function ssrMoveAssets(opts) { opts.logger.info("build", "Rearranging server assets..."); const serverRoot = opts.settings.config.output === "static" ? opts.settings.config.build.client : opts.settings.config.build.server; const clientRoot = opts.settings.config.build.client; const assets = opts.settings.config.build.assets; const serverAssets = new URL(`./${assets}/`, appendForwardSlash(serverRoot.toString())); const clientAssets = new URL(`./${assets}/`, appendForwardSlash(clientRoot.toString())); const files = await glob(`**/*`, { cwd: fileURLToPath(serverAssets) }); if (files.length > 0) { await Promise.all( files.map(async function moveAsset(filename) { const currentUrl = new URL(filename, appendForwardSlash(serverAssets.toString())); const clientUrl = new URL(filename, appendForwardSlash(clientAssets.toString())); const dir = new URL(path.parse(clientUrl.href).dir); if (!fs.existsSync(dir)) await fs.promises.mkdir(dir, { recursive: true }); return fs.promises.rename(currentUrl, clientUrl); }) ); removeEmptyDirs(serverAssets); } } function makeAstroPageEntryPointFileName(prefix, facadeModuleId, routes) { const pageModuleId = facadeModuleId.replace(prefix, "").replace(ASTRO_PAGE_EXTENSION_POST_PATTERN, "."); const route = routes.find((routeData) => routeData.component === pageModuleId); const name = route?.route ?? pageModuleId; return `pages${name.replace(/\/$/, "/index").replaceAll(/[\[\]]/g, "_").replaceAll("...", "---")}.astro.mjs`; } function makeSplitEntryPointFileName(facadeModuleId, routes) { const filePath = `${makeAstroPageEntryPointFileName( RESOLVED_SPLIT_MODULE_ID, facadeModuleId, routes )}`; const pathComponents = filePath.split(path.sep); const lastPathComponent = pathComponents.pop(); if (lastPathComponent) { const extension = extname(lastPathComponent); if (extension.length > 0) { const newFileName = `entry.${lastPathComponent}`; return [...pathComponents, newFileName].join(path.sep); } } return filePath; } export { copyFiles, makeAstroPageEntryPointFileName, makeSplitEntryPointFileName, staticBuild, viteBuild };