Some of the more compelling newer features of Rails like Propshaft and importmaps have resulted in the framework no longer having a hard dependency on Node.js for asset bundling. I’ve found that not requiring Node has made Rails even easier for newer developers to get started with, and greatly simplifies deployment for new apps (or existing apps that are updated to take advantage). I may be reading the guides wrong, but it seems like Hanami still has requirements for having Node.js available so I was wondering if the team has any long-term plans for removing that dependency?
Not having Node.js as a dependency is also a goal for me. What I’m building needs to be self-hostable and eliminating the frontend build step would greatly simplify that process. I can’t speak for the long-term planning of Hanami/Hanakai, but I will certainly be taking a stab at it in the next few months, if no one else beats me to it of course.
Hey David! Thanks for checking this out
You’re right: Node.js is currently a requirement for our assets layer. We chose this to begin with because it would support the widest range of frontend arrangements.
I’d love for us to be able to provide a simpler non-Node.js-requiring alternative. What would it look like, in your eyes? Is it like Rails’ import maps support?
To make this happen we’d need someone to come in and champion it. I’d be happy to provide support for the effort. @wout or @davidcelis, if this is either one of you (or both!), that’d be awesome. ![]()
Note: I would not want us to go beyond two blessed first-party ways of doing assets. Any more and it would be confusing to users (I’ve lost track of all of Rails’ ways of doing it) and too hard for us to support.
I’d be happy to help out here, or take on the complete task if no one did so before me. Right now this is not very high on my priority list though, so it would be something for in a few months.
@timriley I completely agree on limiting it to two options. Importmap for simple frontends (e.g. htmx + Alpine.js + plain CSS), and esbuild if there’s a need for preprocessing, tree shaking, and/or legacy support.
The cherry on the cake would be to have something like hotwire-spark to get (hot) reloading via SSE/WebSockets in development, but that’s more long-term, and probably also a separate gem.
Personally I’d like to see this be totally configurable on hanami new something like hanami new my_app –bun or hanami new my_app –vite
I’m admittedly much stronger on the backend than I am on the frontend
I’ve used Rails’ import maps and preferred that to having to bundle, but the frontend ecosystem moves so quickly that I don’t know if something else has come along that’s becoming more of a canonical standard. From @wout’s message, it seems like maybe import maps are still the way to go for this kind of alternative?
As far as I know, importmap is the best option if you want no build step. It’s a web standard so it’s (hopefully) less likely to be replaced by the next shiny new tool like it’s customary in JS land
.
In my experience, it’s really, really hard to get importmaps to work nicely in anything beyond the most rudimentary needs. I actually tried to write a version of an importmaps resolver that fixed a number of issues that Rails’ importmaps approach has; but ultimately the maintenance overhead (especially once you factor in modern packages that are composed by smaller packages, private packages, and dependency updates) killed that project.
I think that ultimately it’s easier from a conceptual & maintenance standpoint for JS bundling to be in node; with a basic entrypoint/configuration approach. It’s easy to explain, automated dependency management can already pick up the changes, and the variety of JS runtimes gives people options.
I can understand the hesitancy to keep something as messy as JS in the dev environment; but I think Hanami has a chance to be good neighbors in the web stack with JS, rather than deriding it.
Thanks for this helping input, @tcannonfodder (and welcome to the forum!).
One thing I should point out is that we also rely on our hanami-assets esbuild plugin to find CSS (via JS entry points in our case), but also images and fonts and any other static asset you want available in the frontend.
These files get built or copied into public/, and are then indexed in assets.json manifest files (one per slice), which our asset helpers reference to generate the relevant URLs or HTML tags.
Any approach that looks to work without Node.js will need to handle all of this logic, too. It does make me wonder how worthwhile it is, given the all limitations I hear about using import maps for real life.
I’ve pointed Konnor Rogers to this post. He seems to be very expert in these things and could have some sage advice to give us. (Update: he does! We can wait until he has time to kindly weigh in).
Yep! Konnor and I have passed Miyazaki memes about importmaps back & forth dozens of times ![]()
![]()
By that, do you mean that you strongly believe they’re an insult to life itself? ![]()
What was going to be a long post is pretty simple: (edit: this still ended up being long…oops.)
Hanami should only support as many bundlers / “asset management” systems as it feels comfortable with out of the box. Every additional “out of the box” way to do it introduces its own level of maintenance burden.
The good news is Hanami already has the plumbing for working with a common JSON schema for assets in the form of assets.json files. At this point, its largely writing plugins for Hanami to make this all come together.
That would mean the plugin is responsible for “gathering” and “writing” as many asset files as it plans to own to the relevant assets.json file for that slice.
The TLDR is that (without looking at code) a working solution is largely plugging APIs, grabbing the assets you need, and then writing them. Yes, that means a plugin also needs to be written for vite, webpacker, rspack, turbo, et al. But as long as the docs are clear on how the “AssetManager” expects you to write files, this is probably fine.
As a final note, importmaps the spec , are good. they let you do a lot of things. And reduce the number of CLI processes you need running which is always nice.
Rails version of importmaps has self-imposed restrictions that make it incompatible with many packages in the NPM registry, particularly those with “multi-entrypoints”.
If anyone has any interest in this, im happy to “consult” with them and help them along the way if they have any questions. Ive pretty much read most of Propshaft, importmaps-rails, Sprockets, jsbundling-rails, Webpacker, and pretty much anything frontend assets related and have even written my own personal attempts as well.
This is my experience as well. I definitely want to have the option of using a bundler in Hanami ![]()
I am one of the two creators and maintainers of https://www.faucet-pipeline.org. The idea is quite simple: Build tools in JS come and go. We provide a wrapper around them, and curate it for you. And you don’t need to change anything when the underlying build tool changes, you just update. The project was on a bit of a hiatus for a time, but we recently started to revive it. But even with the hiatus, it is still used in production (Rails) apps.
I’m happy to propose a solution for Hanami for the “build” (as opposed to the “no build”) path that is flexible, but still easy to maintain for Hanami.
Yes, unfortunately importmaps weren’t really designed in a backwards compatible way with the way Node and bundler module resolution algorithms work.
Importmaps expect a “flat” tree where all modules share the same dependency. NPM is designed with “isolated dependencies per-package” which means in the case where you have 2 packages with conflicting dependency requirements, importmaps will fail.
importmaps will also fail if they’re using non-standard JS syntax like `import “thing.css”` (though with type assertions importmaps, this may be more do-able in the future)
Any thing which requires custom “Transforms” to run (like React components not transpiled to JS for example)
importmaps are good, but limited.
Thanks for your input, @KonnorRogers!
So it sounds like a task for me (or anyone wants to take this on) is to define the “specification” for Hanami Assets: its expected behaviours and outputs. This is what people can refer to when adding support for additional asset management systems.
If someone does get to building a fully working system using importmaps, this sounds like a reasonable candidate to include in our first-party gems, as the option for people who want a non-Node.js setup, though of course I’d want us to be able to appraise it fully, given it would mean we bring it under maintenance along with everything else, and would need to document it alongside our existing setup.
Thanks for your offer of help! We do already support a “build”-based approach in Hanami Assets right now. It’s based around esbuild and is set up by default with new Hanami apps.
Though once we get the above specification out, sounds like you could write support for Faucet, so you can easily use that with Hanami apps ![]()
Makes a lot of sense to me. With faucet, we chose a very simple contract between the framework and the asset pipeline: A JSON file that maps the original name of the file to the name after it has been processed. This is why the Rails plugin I wrote is tiny and works to this day without any adjustments: faucet_pipeline_rails/lib/faucet_pipeline_rails/manifest.rb at main · faucet-pipeline/faucet_pipeline_rails · GitHub
The processed name in most cases is the name with a hash injected. But in some cases, it is more than that: faucet can optimize images, so the input might be a JPG and the output an AVIF file.
The reason we decided to make the hashing the responsibility of the pipeline and not the framework is CSS, Fonts and Images. A font will be fingerprinted, the resulting path will be used in CSS but also might be used in HTML (preloading). So the pipeline needs the resulting font name for producing the CSS, and the framework still needs to look up the path for preloading. Rails went with an imho weird solution of post processing the CSS again to replace the paths in the already processed CSS.
I’ve been working on a pure Bun implementation. So no Node.js dependency, but Bun for everything:
I tested it superficially with Hanami, but today I started on a new Hanami project so if there are any wrinkles, I’ll iron them out. Installation is simple: drop hanami-assets and configure bun_bun_bundle.
We’ve been using this setup it in two production apps without issues (Rails and Lucky). It’s blazing fast.
It’s great to see more activity in this space. Thanks for building bun_bun_bundle, @wout, and for considering Hanami as part of it ![]()
In terms of Hanami integration, I’d love to see whether it might be able to make this work with the Hanami Assets Ruby Gem as opposed to requiring its removal.
I’ve just merged a SPEC.md into Hanami Assets, (read it here), which outlines the expectations for a complete user experience for asset bundlers targeting Hanami, and how they can provide manifest files that allow Hanami’s standard assets layer to work out of the box, and remain consistent for all users.
To be clear, the hanami-assets Ruby Gem is already bundler agnostic. It just supplies the glue for reading those asset manifests. My goal is to make it easy for multiple bundlers to plug into this (including, eventually, into our CLI tools), and this SPEC is the first step.
The SPEC also outlines important details about e.g. how bundlers should be working with assets that are isolated per slice, which is an important part of the Hanami experience.
I’d be keen to hear what you think about the spec! Share me your feedback, and maybe we can use bun_bun_bundle as an opportunity to further the enhance of this area of Hanami ![]()
Thanks for linking the spec, that’s really helpful. ![]()
Short answer: yes, totally on board. Requiring users to rip out hanami-assets is quite drastic anyway.
I just shipped bun_bun_bundle v0.13.0 with the easy fixes:
SRI
--sri=sha256,sha384,... computes digests and embeds them in each manifest entry. The helpers integrity= and crossorigin="anonymous" when present.
Manifest shape
Bumped from {key: "string"} to {key: {url, sri?}}, matching section 5.2.
One question here though: why url and not path? It’s a bit confusing because the spec calls it “Absolute path”. Is that for compatibility with Node.js builders?
SIGINT
The dev server now releases its file watchers and WebSocket clients on Ctrl-C and exits 0 (section 7).
What’s left (to be released under v0.14) is a Hanami adapter. Likely as a hanami-cli command override so it can stay Node.js-free:
Absolute paths
bbb currently writes "url": "js/app-abc.js" (relative, so Rails/Lucky helpers can prepend public_path for asset_host support). The adapter will prepend path_prefix in the hanami output so the manifest meets section 5.2’s absolute-path requirement. Standalone bbb manifests stay relative to keep Rails/Lucky compatibility.
Logical names
bbb keeps the type-dir prefix (js/app.js, images/logo.png). The adapter will strip it for Hanami.
Per-slice invocation
The adapter will create one Bun sub-process per app/slice.
--watch
bbb uses --dev, so there will be a --watch flag as an alias.
A couple of places where bbb’s design collides with the spec:
Importing non-JS assets in JS (section 5.4)
bbb is intentionally three-pipeline (JS, CSS, static), so JS files don’t import CSS or static assets. To be honest, I never liked that approach, so I’d rather not change that. Side-effect asset emission could be pulled through a raw Bun plugin if necessary, but it feels like taking a step back.
Globbing
bbb has a CSS/JS glob syntax (via plugins) for CSS and JS that lets users skip barrel files entirely. Great in CSS, but it’s especially nice for registering Alpine components or Stimulus controllers.
If the implicit goal of the spec is bundler-swap portability, users would lose this when moving off bbb. Not a blocker, just worth flagging.