import * as esbuild from "esbuild"; import grabAllPages from "../../utils/grab-all-pages"; import grabDirNames from "../../utils/grab-dir-names"; import isDevelopment from "../../utils/is-development"; import tailwindEsbuildPlugin from "../server/web-pages/tailwind-esbuild-plugin"; import grabClientHydrationScript from "./grab-client-hydration-script"; import path from "path"; import virtualFilesPlugin from "./plugins/virtual-files-plugin"; import esbuildCTXArtifactTracker from "./plugins/esbuild-ctx-artifact-tracker"; import { logBuildErrorsOnce, } from "../../utils/build-error-log"; import { log } from "../../utils/log"; import grabRootFilePath from "../server/web-pages/grab-root-file-path"; // Snapshot of the entry key set + root file presence of the last build. // When it differs from the freshly computed set, the esbuild context must // be recreated (entry points are fixed at context creation). let last_entry_keys = undefined; let last_root_file_path = undefined; // Local paths of the pages that made it into the current entry set. Pages // skipped at build time (e.g. created empty, no default export yet) are // absent, so the watcher can treat their later edits as structural. let last_entry_pages = new Set(); /** * True when `local_path` is a hydration entry of the current esbuild context. */ export function isHydrationEntryPage(local_path) { return last_entry_pages.has(local_path); } function arraysEqual(a, b) { if (a.length !== b.length) return false; for (let i = 0; i < a.length; i++) { if (a[i] !== b[i]) return false; } return true; } export default async function allPagesESBuildContextBundler(params) { const { HYDRATION_DST_DIR, BUNX_HYDRATION_SRC_DIR } = grabDirNames(); try { const pages = grabAllPages({ exclude_api: true }); global.BUNEXT_PAGE_FILES = pages; const dev = isDevelopment(); const entryToPage = new Map(); for (const page of pages) { const tsx = await grabClientHydrationScript({ page_local_path: page.local_path, }); if (!tsx) { continue; } const entryFile = path.join(BUNX_HYDRATION_SRC_DIR, `${page.url_path}.tsx`); entryToPage.set(entryFile, { ...page, tsx }); } const entryPoints = [...entryToPage.keys()].map((e) => `hydration-virtual:${e}`); const entry_keys = [...entryToPage.keys()].sort(); const { root_file_path } = grabRootFilePath(); const structure_changed = !global.BUNEXT_BUNDLER_CTX || global.BUNEXT_BUNDLER_CTX_DISPOSED || !last_entry_keys || !arraysEqual(last_entry_keys, entry_keys) || last_root_file_path !== root_file_path; if (structure_changed) { try { await global.BUNEXT_BUNDLER_CTX?.dispose(); } catch (error) { } global.BUNEXT_BUNDLER_CTX = undefined; global.BUNEXT_BUNDLER_CTX = await esbuild.context({ entryPoints, outdir: HYDRATION_DST_DIR, bundle: true, minify: !dev, format: "esm", target: "es2020", platform: "browser", define: { "process.env.NODE_ENV": JSON.stringify(dev ? "development" : "production"), }, entryNames: "[dir]/[hash]", metafile: true, plugins: [ forceExternalReact(), tailwindEsbuildPlugin, virtualFilesPlugin({ entryToPage, }), esbuildCTXArtifactTracker({ entryToPage, post_build_fn: params?.post_build_fn, build_only: params?.build_only || params?.start, refresh_props_on_first_build: params?.refresh_props, }), ], jsx: "automatic", splitting: true, treeShaking: true, external: [ "react", "react-dom", "react-dom/client", "react/jsx-runtime", "react/jsx-dev-runtime", ...(global.BUNEXT_CONFIG.page_compiler_excludes || []), ], // esbuild's own error logging prints per attempt; the // artifact-tracker plugin reports errors (deduped, once) itself. logLevel: "silent", }); last_entry_keys = entry_keys; last_root_file_path = root_file_path; last_entry_pages = new Set([...entryToPage.values()].map((p) => p.local_path)); } await global.BUNEXT_BUNDLER_CTX.rebuild(); } catch (error) { global.BUNEXT_BUILD_FAILED = true; // When the build fails with esbuild errors, the artifact-tracker's // onEnd already reported them (deduped). "Build failed with N errors" // is that same failure rethrown by esbuild's API — re-logging it here // would double every incident. if (String(error?.message || "").startsWith("Build failed with")) { return; } logBuildErrorsOnce([ { text: `ESBUILD Error => ${error?.message || String(error)}`, }, ], (line) => log.error(line)); } } function forceExternalReact() { return { name: "force-external-react", setup(build) { build.onResolve({ filter: /^react(-dom)?(\/.*)?$/ }, (args) => { if (args.pluginData?.externalReact) return null; return { path: args.path, external: true, }; }); }, }; }