I use elixir and the Phoenix Framework in
many of my projects along with esbuild
. While Phoenix includes an esbuild
Elixir package that provides a streamlined "out of the box" experience.
But if you want full build customization, you need to write your own build file.
When you have your own build file, you need something like this in your
dev.exs
config file:
node: ["build.mjs", "--watch", cd: Path.expand("../assets", __DIR__)],
This spawns a node process with the build.mjs
build file.
A common issue occurs when the Elixir development server is terminated with
Ctrl-C
: ESBuild processes may remain as zombies. This appears to be related to
how BEAM (Erlang VM) handles subprocess management, specifically regarding
stdin
closure for child processes.
After trying various approaches to properly terminate the ESBuild process, I found a reliable solution: periodically check if the parent process is alive by sending a SIGNULL (signal 0) to it. If the parent doesn't respond, we terminate the ESBuild process.
Here's a working build.mjs
implementation that includes this cleanup mechanism:
import esbuild from 'esbuild';
const args = process.argv.slice(2);
const watch = args.includes('--watch');
const deploy = args.includes('--deploy');
const loader = {
};
const plugins = [
];
// Define esbuild options
let opts = {
nodePaths: ["../deps"],
entryPoints: ["js/app.js"],
bundle: true,
logLevel: "info",
target: "es2017",
outdir: "../priv/static/assets",
external: ["*.css", "fonts/*", "images/*"],
loader: loader,
plugins: plugins,
jsxFactory: 'h', // preact
jsxFragment: 'Fragment',
};
if (deploy) {
opts = {
...opts,
minify: true,
};
}
if (watch) {
opts = {
...opts,
sourcemap: "inline",
};
const ctx = await esbuild.context(opts);
const cleanup = async () => {
await ctx.dispose();
process.exit(0);
};
// 1 The usual solution is this, but it does not work reliably on my setup
process.stdin.resume();
process.stdin.on("close", () => cleanup());
// 2 The parent signal solution using an interval
const check_parent = () => {
try {
// Try to send 0 signal to parent process to check if it exists
process.kill(process.ppid, 0);
} catch (e) {
cleanup().catch(console.error);
}
};
setInterval(check_parent, 1000);
await ctx.watch();
} else {
esbuild.build(opts);
}
Try 1 and if it works, great, if it does not, try 2.
This solution ensures clean process termination when the Phoenix development server stops, preventing zombie processes.