Bundling directory structures in esbuild for a Rails 7 application

How do I import Javascript with directories?

I've set up jsbundling-rails using the esbuild bundler for a Rails 7 project that was previously using sprockets to handle all assets. It appears that I've set it up correctly, because I can load JS files by directly referencing the file.

Though I've got a lot of files and I even want to impose some human logic on the structure of the javascript.

To do this, I want to split up some javascript in directories, like this:

- assets
-- javascript
--- some.js
--- some_more.js
---- base
----- some_other.js
----- some_way_other.js

I'm able to load all of this js with my application.js at app/assets/javascript/application.js file, like this:

import "./some"
import "./some_more"
import "./base/some_other"
import "./base/some_way_other"

the console log responds with:

but I want to be able to load/bundle this JS using this syntax:

import "./some"
import "./some_more"
import "./base"

but from the js watcher, I get:

15:39:22 js.1   | [watch] build started (change: "app/javascript/application.js")
15:39:22 js.1   | ✘ [ERROR] Could not resolve "./base"
15:39:22 js.1   |
15:39:22 js.1   |     app/javascript/application.js:14:7:
15:39:22 js.1   |       14 │ import "./base"
15:39:22 js.1   |          ╵        ~~~~~~~~
15:39:22 js.1   |
15:39:22 js.1   | 1 error
15:39:22 js.1   | [watch] build finished


web: unset PORT && /root/.rbenv/shims/thin start -C config/thin_development.yml
js: yarn build --watch

I mean, is this even possible? I used to be able to do this with sprockets, but it doesn't seem like I can with esbuild. Or should I just be sticking with a massive application.js file and manually import each javascript file?


  • The way to do this is to customize esbuild with a configuration file known as esbuild.config.js and to utilize the plugin esbuild-rails created by Chris at GoRails, found at


    const path = require('path');
    const rails = require('esbuild-rails')
    const watch = process.argv.includes("--watch") && {
      onRebuild(error) {
        if (error) console.error("[watch] build failed", error);
        else console.log("[watch] build finished");
      entryPoints: ["application.js"],
      bundle: true,
      minify: true,
      outdir: path.join(process.cwd(), "app/assets/builds"),
      absWorkingDir: path.join(process.cwd(), "app/javascript"),
      watch: watch,
      write: true,
      loader: { '.js': 'jsx' },
      publicPath: 'assets',
      target: 'es6',
      // custom plugins will be inserted is this array
      plugins: [rails()],
    }).catch(() => process.exit(1));

    Usage in application.js

    import { Application } from "@hotwired/stimulus"
    const application = Application.start()
    import './meta/jquery'
    import './meta/ajax'
    import './controllers'
    import libraries from "./libraries/*.js"
    import custom_files from "./custom/*.js"
    libraries.forEach((controller) => {
        application.register(, controller.module.default)
    custom_files.forEach((controller) => {
        application.register(, controller.module.default)