esbuild Runners
Understand the core script executors that coordinate production releases and hot-reloading dev servers.
Key Files Location:
• Prod Build:./dinou/esbuild/build.mjs
• Dev Server:./dinou/esbuild/dev.mjs
💡 Overview
Dinou exposes two main compiler runners. The production builder compiles minified output modules, while the development server watches for changes to provide Hot Module Replacement (HMR) without reloading the page.
📊 Production Build Runner Flow
The flowchart below traces the production build pipeline:
📊 Development HMR Server Flow
The flowchart below shows how development changes trigger recompilation or HMR updates:
⚙️ build.mjs Code Walkthrough
Below is the full, complete code of the production build runner:
import esbuild from "esbuild";
import fs from "node:fs/promises";
import getConfigEsbuildProd from "./helpers-esbuild/get-config-esbuild-prod.mjs";
import getEsbuildEntries from "./helpers-esbuild/get-esbuild-entries.mjs";
import { fileURLToPath } from "url";
import path from "node:path";
import { updateManifestForModule } from "./helpers-esbuild/update-manifest-for-module.mjs";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const outdir = "dist3";
// 1. Clean previous build states
await fs.rm(outdir, { recursive: true, force: true });
await fs.rm("react_client_manifest", { recursive: true, force: true });
await fs.rm("server_functions_manifest", { recursive: true, force: true });
const absPathToClientRedirect = path.resolve(__dirname, "../core/client-redirect.jsx");
const absPathToLink = path.resolve(__dirname, "../core/link.jsx");
// 2. Define framework entrypoints
const frameworkEntryPoints = {
main: path.resolve(__dirname, "../core/client.jsx"),
error: path.resolve(__dirname, "../core/client-error.jsx"),
serverFunctionProxy: path.resolve(__dirname, "../core/server-function-proxy.js"),
runtime: path.resolve(__dirname, "react-refresh/react-refresh-runtime.mjs"),
"react-refresh-entry": path.resolve(__dirname, "react-refresh/react-refresh-entry.js"),
dinouClientRedirect: absPathToClientRedirect,
dinouLink: absPathToLink,
};
try {
const manifest = {};
// 3. Resolve entrypoints for all client pages, css files, and static assets
const [esbuildEntries, detectedCSSEntries, detectedAssetEntries] =
await getEsbuildEntries({ manifest });
updateManifestForModule(absPathToClientRedirect, await fs.readFile(absPathToClientRedirect, "utf8"), true, manifest);
updateManifestForModule(absPathToLink, await fs.readFile(absPathToLink, "utf8"), true, manifest);
const componentEntryPoints = [...esbuildEntries].reduce((acc, dCE) => ({ ...acc, [dCE.outfileName]: dCE.absPath }), {});
const cssEntryPoints = [...detectedCSSEntries].reduce((acc, dCSSE) => ({ ...acc, [dCSSE.outfileName]: dCSSE.absPath }), {});
const assetEntryPoints = [...detectedAssetEntries].reduce((acc, dAE) => ({ ...acc, [dAE.outfileName]: dAE.absPath }), {});
const entryPoints = {
...frameworkEntryPoints,
...componentEntryPoints,
...cssEntryPoints,
...assetEntryPoints,
};
// 4. Trigger production build
await esbuild.build(
getConfigEsbuildProd({
entryPoints,
manifest,
outdir,
})
);
} catch (err) {
console.error("Error in build:", err);
}⚙️ dev.mjs Code Walkthrough
Below is the full, complete code of the development watch runner:
import esbuild from "esbuild";
import fs from "node:fs/promises";
import getConfigEsbuild from "./helpers-esbuild/get-config-esbuild.mjs";
import getEsbuildEntries from "./helpers-esbuild/get-esbuild-entries.mjs";
import chokidar from "chokidar";
import path from "node:path";
import { regex as assetRegex } from "../core/asset-extensions.js";
import normalizePath from "./helpers-esbuild/normalize-path.mjs";
import { fileURLToPath, pathToFileURL } from "url";
import { updateManifestForModule } from "./helpers-esbuild/update-manifest-for-module.mjs";
import { useServerRegex } from "../constants.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const outdir = "public";
await fs.rm(outdir, { recursive: true, force: true });
await fs.rm("react_client_manifest", { recursive: true, force: true });
await fs.rm("server_functions_manifest", { recursive: true, force: true });
let currentCtx = null;
let debounceTimer = null;
let clientComponentsPaths = [];
let currentServerFiles = new Set();
const absPathToClientRedirect = path.resolve(__dirname, "../core/client-redirect.jsx");
const absPathToLink = path.resolve(__dirname, "../core/link.jsx");
const frameworkEntryPoints = {
main: path.resolve(__dirname, "../core/client.jsx"),
error: path.resolve(__dirname, "../core/client-error.jsx"),
serverFunctionProxy: path.resolve(__dirname, "../core/server-function-proxy.js"),
runtime: path.resolve(__dirname, "react-refresh/react-refresh-runtime.mjs"),
"react-refresh-entry": path.resolve(__dirname, "react-refresh/react-refresh-entry.js"),
dinouClientRedirect: absPathToClientRedirect,
dinouLink: absPathToLink,
};
const changedIds = new Set();
const hmrEngine = { value: null };
// 1. Initialize filesystem watcher
const watcher = chokidar.watch("src", {
ignoreInitial: true,
ignored: /node_modules|dist/,
});
const codeCssRegex = /.(js|jsx|ts|tsx|css|scss|less)$/i;
let manifest = {};
let entryPoints = {};
async function updateEntriesAndComponents() {
manifest = {};
const [esbuildEntries, detectedCSSEntries, detectedAssetEntries, serverFiles] = await getEsbuildEntries({ manifest });
updateManifestForModule(absPathToClientRedirect, await fs.readFile(absPathToClientRedirect, "utf8"), true, manifest);
updateManifestForModule(absPathToLink, await fs.readFile(absPathToLink, "utf8"), true, manifest);
currentServerFiles = new Set(serverFiles.map((f) => normalizePath(path.resolve(f))));
const componentEntryPoints = [...esbuildEntries].reduce((acc, dCE) => ({ ...acc, [dCE.outfileName]: dCE.absPath }), {});
clientComponentsPaths = Object.values(componentEntryPoints);
const cssEntryPoints = [...detectedCSSEntries].reduce((acc, dCSSE) => ({ ...acc, [dCSSE.outfileName]: dCSSE.absPath }), {});
const assetEntryPoints = [...detectedAssetEntries].reduce((acc, dAE) => ({ ...acc, [dAE.outfileName]: dAE.absPath }), {});
entryPoints = {
...frameworkEntryPoints,
...componentEntryPoints,
...cssEntryPoints,
...assetEntryPoints,
};
}
async function createEsbuildContext() {
try {
if (currentCtx) {
await currentCtx.dispose(); // Dispose previous watch thread
}
await fs.rm(outdir, { recursive: true, force: true });
currentCtx = await esbuild.context(
getConfigEsbuild({
entryPoints,
manifest,
changedIds,
hmrEngine,
})
);
await currentCtx.watch();
} catch (err) {
console.error("Error recreating context:", err);
}
}
// 2. Initial compiler setup on watch ready
watcher.on("ready", async () => {
await updateEntriesAndComponents();
await createEsbuildContext();
});
const debounceRecreate = () => {
if (debounceTimer) clearTimeout(debounceTimer);
debounceTimer = setTimeout(async () => {
await createEsbuildContext();
}, 300);
};
const debounceRecreateAndReload = () => {
if (debounceTimer) clearTimeout(debounceTimer);
debounceTimer = setTimeout(async () => {
await createEsbuildContext();
hmrEngine.value.broadcastMessage({ type: "reload" });
}, 300);
};
let reloadTimer = null;
const debounceReload = () => {
if (reloadTimer) clearTimeout(reloadTimer);
reloadTimer = setTimeout(() => {
if (hmrEngine.value) {
hmrEngine.value.broadcastMessage({ type: "reload" });
}
}, 100);
};
// 3. React on filesystem additions, deletions and modifications
watcher.on("add", async (file) => {
const ext = path.extname(file);
if (codeCssRegex.test(ext) || assetRegex.test(ext)) {
await updateEntriesAndComponents();
debounceRecreateAndReload();
}
});
watcher.on("unlink", async (file) => {
const ext = path.extname(file);
if (codeCssRegex.test(ext) || assetRegex.test(ext)) {
await updateEntriesAndComponents();
if (currentCtx) {
await currentCtx.dispose();
currentCtx = null;
}
debounceRecreate();
}
});
watcher.on("change", async (file) => {
const resolvedFile = normalizePath(path.resolve(file));
const oldManifest = { ...manifest };
const oldEntryKeys = JSON.stringify(Object.keys(entryPoints).sort());
await updateEntriesAndComponents();
const newEntryKeys = JSON.stringify(Object.keys(entryPoints).sort());
const entryPointsChanged = oldEntryKeys !== newEntryKeys;
const isClientModule = clientComponentsPaths.includes(resolvedFile);
const isServerModule = currentServerFiles.has(resolvedFile);
// 4. Hot Module Replacement (HMR) bypass logic
if (isClientModule && !isServerModule && oldManifest[pathToFileURL(resolvedFile).href]) {
changedIds.add(resolvedFile); // Trigger client hot-reload updates
return;
}
const fileContent = await fs.readFile(resolvedFile, "utf8").catch(() => "");
if (useServerRegex.test(fileContent.trim())) {
return; // Server action changes do not reload the browser
}
if (entryPointsChanged || file.endsWith(".css") || file.endsWith(".scss")) {
debounceRecreateAndReload();
} else {
debounceReload();
}
});