I wanted to code-split a React component out of my Rails 8 app’s JavaScript bundle. The app uses Bun for JS bundling and Propshaft for asset serving. Unfortunately, both fingerprint chunked javascript assets breaking dynamic imports. Here’s how to configure Bun to work with Propshaft.

Bun supports code splitting with splitting: true, but it requires format: "esm" which means the bundler emits import statements between chunks. The problem is Propshaft fingerprints assets but it does not rewrite import paths inside JavaScript files. So the chunk references break at runtime.

bun.config.jsjavascript
const config = {
  sourcemap: "external",
  entrypoints: ["app/javascript/application.ts"],
  outdir: path.join(process.cwd(), "app/assets/builds"),
  splitting: true,
  format: "esm",
  publicPath: "/assets/",
  naming: {
    entry: "[name].[ext]",
    chunk: "[name]-[hash].digested.[ext]",
  },
};

publicPath: "/assets/" makes Bun emit absolute import paths matching where Propshaft serves from (e.g. import("/assets/chunk-abc.digested.js")). The .digested naming convention tells Propshaft the file is already fingerprinted. It serves it as-is without adding another digest hash.

The entrypoint application.js still gets fingerprinted normally by Propshaft and loaded via javascript_include_tag with type: "module". The chunks sit alongside it in app/assets/builds/ and Propshaft serves them at their exact filenames.

The entrypoint dropped from 1.4MB to about 50KB. React, ReactDOM, and the component are only fetched on the two pages that use them.