411 lines
17 KiB
Plaintext
411 lines
17 KiB
Plaintext
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 && !pageData.hasSharedModules) {
|
|
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
|
|
};
|