JavaScript
Brut provides basic bundling using esbuild. You can use any front-end framework with Brut, but you don't have to use one.
Brut provides BrutJS, which is a lightweight library of HTML custom elements and utility code. These elements can provide a fair bit of front-end functionality using progressive enhancement without the need for a framework.
Overview
All your app's JavaScript lives in app/src/front_end/js
, or in modules you bring in via package.json
. Brut will bundle all of that up into a single .js
file that is served up with your app. Brut does this by using esbuild, a stable and standardized tool for bundling JavaScript.
The way esbuild works is to be given an entry point that requires, or transitively requires, all of your JavaScript by using ES6 modules. app/src/front_end/js/index.js
is the entry point for your app.
For example, if you have a Widget
class that uses a Status
class, and you also use the third party library "foobar", here is how all the files would look.
First, package.json
(in your app's root) would include "foobar"
(and it must set "type"
to "module"
):
{
"name": "your-app",
"type": "module",
"license": "UNLICENSED",
"dependencies": {
"foobar": "^0.0.11"
},
"devDependencies": {
"chokidar-cli": "^3.0.0",
"esbuild": "^0.20.2",
"jsdom": "^25.0.1",
"mocha": "^10.7.3",
"playwright": "^1.50.1"
},
}
Next, app/src/front_end/js/index.js
would import both "foobar"
and "Widget"
:
import { foobar } from "foobar"
import Widget from "./Widget"
// ...
Notice that "foobar", since it's brought in as a third party dependency, is imported without a ./
. Be careful here! Every third party library has a different syntax for how to import whatever it is or does. Consult the documentation of each third party library you wish to import.
The second import
uses a ./
because it's importing a file in app/src/front_end/js
, namely Widget.js
. Be careful here, too, as you must be sure to export
the right thing. Here's what app/src/front_end/js/Widget.js
might look like:
import Status from "./extra/Status"
class Widget {
status = new Status()
}
export default Widget
Note that we import Status
here. Unlike Ruby, ES6 modules requires each class that references a class to import it explicitly. Also notice that we do export default Widget
, which allows import Widget
to work.
Finally, app/src/front_end/extra/Status.js
looks like so:
class Status {
}
export default Status
When bin/build-assets
runs, esbuild will use app/src/front_end/js/index.js
as its entry point, and will bundle both Widget.js
and the "foobar" library. When it bundles Widget.js
, it will see that it imports extra/Status.js
and bundle that, too.
This bundle can be included in your app by ensuring this is in your layout:
def view_template
doctype
html(lang: "en") do
head do
script(defer: true, src: asset_path("/js/app.js"))
# ...
The asset_path
helper takes a logical path—/js/app.js
—and returns the actual path the browser can use. More details on this can be found in assets.
Testing
Client-side behavior is best tested with end-to-end tests, however you can simplify your end-to-end tests by creating unit tests of your custom elements. BrutJS provides limited support for this
Recommended Practices
Brut encourages you to use HTML custom elements as progressive enhancements over server-generated views. This sort of client-side code will age well. The toolchain and dependencies are minimal, so you will not have to worry too much about code written this way.
It will be lower level and more verbose than existing frameworks. We would argue that it is not significantly more difficult and the sustainability is worth it.
Technical Notes
IMPORTANT
Technical Notes are for deeper understanding and debugging. While we will try to keep them up-to-date with changes to Brut's internals, the source code is always more correct.
Last Updated May 7, 2025
Currently, Brut only supports a single entry point and bundle. This could be easily made more flexible if there is a desire to finely tweak the JavaScript loaded on specific pages.
Brut also does not expose any esbuild configuration. This could be provided in the future, but for now, it is hard-coded.
Brut may provide more direct support for import maps, but as of now, import maps are not widely used outside of Rails, and tend to cause a lot of problems, especially if you aren't able to field an HTTP/2 web server (or even know what that is).