Skip to content

Single-File Production Bundle ​

MEGSTAT POS packages both the Hono backend server and the compiled React frontend application into a single self-contained executable file: dist/server.mjs.

Why Single-File? ​

Traditional full-stack deployments often require:

  • A reverse proxy (e.g., Nginx) serving static SPA files on port 80/443.
  • An application server (e.g., Node.js or PM2) running the API on an internal port.
  • Large node_modules folders inside Docker images containing hundreds of megabytes of redundant dependencies.

MEGSTAT POS condenses the entire deployment into one ~2.5MB JavaScript file. The production Docker container ships without node_modules, reducing attack surfaces, image transfer times, and deployment complexity.

How the Bundler Works ​

The build script scripts/bundle-server.mjs executes the following sequence:

text
Step 1: Build React SPA
  frontend/src/ ──(vite build)──▶ frontend/dist/ (HTML, JS, CSS, SVG)

Step 2: Read SPA Assets
  scripts/bundle-server.mjs reads all assets from frontend/dist/

Step 3: Custom esbuild Plugin
  Intercepts import '/spa-assets$/' in backend/src/http/static.ts
  and injects in-memory asset map { "index.html": "...", "assets/app.js": "..." }

Step 4: esbuild Server Compilation
  backend/src/index.ts ──(esbuild bundle)──▶ dist/server.mjs
  • Inlines dotenv and runtime libraries
  • external: [] (Zero node_modules required at runtime)
  • Minifies output

Development vs Production Asset Serving ​

MEGSTAT POS switches between development and production modes cleanly without configuration flags:

Development Mode ​

  1. backend/src/http/spa-assets.ts contains an empty asset registry committed to git:
    ts
    export const spaAssets: Record<string, { content: string; contentType: string }> = {};
  2. When running npm run dev:backend, the server leaves the registry empty.
  3. staticFiles() in backend/src/http/static.ts sees an empty registry and no-ops.
  4. Vite serves the frontend on http://localhost:5173 and proxies API requests /api/* to http://localhost:8787.

Production Mode ​

  1. scripts/bundle-server.mjs runs with a custom esbuild onResolve and onLoad plugin:
    ts
    build.onResolve({ filter: /spa-assets$/ }, args => ({
      path: args.path,
      namespace: 'spa-assets-ns'
    }));
  2. The plugin reads every file in frontend/dist and writes a populated JavaScript map directly into the bundle.
  3. In dist/server.mjs, app.use("*", staticFiles()) serves the inlined assets directly from RAM with proper MIME types and aggressive caching headers.
  4. If a user requests a path that is not an /api endpoint or static asset, staticFiles() serves index.html to enable client-side HTML5 history routing.

Critical Bundler Constraints ​

Keep these hard constraints in mind before modifying the build script:

  1. moduleResolution: "bundler" Requires esbuild: backend/tsconfig.json uses bundler module resolution. tsc emits extensionless ESM imports that Node's native ESM loader rejects (ERR_MODULE_NOT_FOUND). Only the output of esbuild can execute under Node.
  2. Missing Asset 404 Behavior: A missing /assets/* file returns a JSON 404 instead of falling back to index.html. Returning 200 HTML for a missing JavaScript or CSS chunk causes confusing runtime parse errors in the browser.
  3. Specifier Matching in onResolve: The esbuild plugin filter matches the specifier (/spa-assets$/), not an absolute file path. Filtering on an absolute path silently fails to trigger because onResolve executes before file resolution.
  4. Neon Pure-JS WebSocket Driver: Because Neon uses @neondatabase/serverless over WebSockets, no native C++ bindings (such as better-sqlite3 or Prisma query engines) are needed inside dist/server.mjs. The image requires zero C++ compilers (make, g++, python3).

MEGSTAT POS — Built for Retail Stores & Multi-Branch Businesses