Skip to content

Instantly share code, notes, and snippets.

@barelyhuman
Created January 24, 2026 19:14
Show Gist options
  • Select an option

  • Save barelyhuman/c3a0544fa7241d87a256f57e24f53b68 to your computer and use it in GitHub Desktop.

Select an option

Save barelyhuman/c3a0544fa7241d87a256f57e24f53b68 to your computer and use it in GitHub Desktop.

A simple server and client builder implementation that always builds both the client SPA and server entry points.

Both are bundled at all times and uses existing esbuild plugins for additional functionality.

The current build script expects the following.

.
├── client
│   ├── index.html # basic index.html structure
│   ├── main.js # holds the js app 
├── public
│   ├── app.css # public resources, that don't need building
├── server.js # the server entry point
└── build.js 

The reason for this script is that I don't like using frameworks and nor do I like tiptoeing around black boxes of JS, this solution keeps the original code as is and the only magic here is the relative positioning of the final assets which is where the copy plugin helps us.

import { createContainer } from 'esbuild-multicontext';
import { copy } from 'esbuild-plugin-copy';
import { nodeExternals } from 'esbuild-plugin-node-externals';
import { spawn } from 'node:child_process';
import { setTimeout } from 'node:timers/promises';
import path from 'path';
import glob from 'tiny-glob';
const container = createContainer();
container.createContext('client', {
entryPoints: ['./client/main.js'],
jsx: 'automatic',
jsxImportSource: 'preact',
loader: {
'.js': 'jsx',
},
bundle: true,
platform: 'browser',
format: 'esm',
metafile: true,
outdir: 'dist/server/client',
});
const serverRoutes = (
await glob('./routes/**/*.js', {
absolute: true,
filesOnly: true,
})
).map((d) => d.replace(path.join(process.cwd(), 'routes'), ''));
container.createContext('server', {
entryPoints: ['./server.js'],
jsx: 'automatic',
bundle: true,
define: {
__ROUTES_MAP__: JSON.stringify(serverRoutes),
},
platform: 'node',
format: 'esm',
jsxImportSource: 'preact',
plugins: [
nodeExternals(),
copy({
assets: {
from: ['./client/!(main.js)'],
to: ['./client'],
},
}),
copy({
assets: {
from: ['./public/*'],
to: ['./public'],
},
}),
],
outdir: 'dist/server',
});
const args = process.argv.slice(2);
if (args[0] === 'dev') {
let runningServer;
await container.dev({
dirs: ['./client', './public', './routes'],
async onBuild(result) {
if (runningServer?.pid) {
runningServer.kill('SIGINT');
if (!runningServer.killed) {
setTimeout(2000);
}
}
runningServer = spawn('node', ['--watch', 'dist/server/server.js'], {
stdio: 'inherit',
});
},
});
} else {
await container.build();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment