A new approach to assets in 2.1.0.rc3

We’ll be making a Hanami 2.1.0.rc3 next month, and it will contain a new approach to asset compilation. I want to explain this here in detail so you can understand what’s coming, so you can know what to test with your apps, and to gather any feedback you may have!

Background

Last year we released Hanami 2.1.0.rc1 and .rc2 and were preparing for the 2.1.0 final release when we encountered an issue with our assets handling: assets from different slices, but with the same base names would conflict and override each other in the final compiled public/assets/ directory.

We explored a fix for this inside our hanami-assets JS package, and while we could have made that work, several downsides remained:

  • We still required a duplicate (and incomplete) implementation of detecting Hanami slices
  • We still required users to keep a redundant slice prefix in front of each slice’s assets (e.g. javascript_tag("admin/app") inside the Admin slice
  • We still merged all slice assets into a single shared manifest, thereby breaking the isolation that slices otherwise provide
  • It required us to keep and maintain a more complex esbuild wrapper, which would increase the chances of bugs or issues in the future.
  • And because our usage of esbuild would have remained off its “golden path”, it increased our risk of hitting an evolutionary dead end for this overall approach.

Given all of this, we decided to step back and try a fresh approach to assets handling. I’ve now proven this approach will work, and I’d like to share it with you in advance of the upcoming rc3 release.


The new approach: the hanami CLI invokes an esbuild process per slice

Previously, we invoked a single Node process that process that found all assets, compiled them with esbuild and then created a merged manifest. In the bug fix attempt I linked above, we looked at invoking an esbuild compilation per slice within the same single Node.js process (using async/await to run them in parallel).

With our new approach, our hanami assets compile and assets watch commands will invoke a single esbuild process per slice, and will pass the relevant slice paths as arguments to each esbuild process. We’ll handle this by forking to a Ruby process per slice.

In this approach, slices will be detected faithfully, since it’s our Ruby code that already knows about all registered slices and their locations.

esbuild compiles a single slice’s assets only, and outputs to a dedicated directory with a dedicated manifest

When our esbuild wrapper is invoked by the CLI, it will compile the assets within a single slice only, and output the compiled files to a dedicated directory for the slice:

  • For the app, this is public/assets/
  • For each slice, this is public/assets/[slice_name]/

A single assets.json manifest will be written into each directory, and will contain entries for that slice’s assets only.

In this way, our usage of esbuild becomes much more typical: one input directory, one output directory. Nice and simple.

Every slice will have its own independent "assets" component

With every slice having its own assets directory and its own assets manifest, this also means that every slice will have its own Hanami::Assets instance registered as a component with the "assets" key, providing access to the assets in that slice’s manifest.

A slice can only access its own assets by default (with those assets no longer redundantly prefixed by the slice name)

Following from the above, this means a slice will have access only to its own assets by default.

This means e.g. that javascript_tag("app.js") in an Admin slice will reference the file at /public/assets/admin/app-[hash].js.

Because each slice has its own assets component and its own underlying assets manifest, that means that you no longer need to provide the redundant slice name prefix that you had to do for the previous 2.1.0 release candidates. This is why you can write javascript_tag("app.js") instead of javascript_tag("admin/app.js").

Assets may be shared across slices via component imports and view context deps

Slices are meant to be serve as a means to isolate code, and the approach with assets will now take this same approach.

However, if you need to access assets from another slice, you can do so by:

  1. Importing the "assets" component from the other slice
  2. Including the imported assets component as a dependency of your slice’s view context
  3. Referencing that assets object directly in your views/helpers: javascript_tag(other_slice_assets["app.js"])

This is not intended to be the mainstream approach, but the fact that it is possible should allow some flexibility where it is needed. See this PR for a clear demonstration.

A slice may have its own config/assets.js

Lastly, if you need to provide custom esbuild options for a particular slice’s asset compilation, you will now be able to do this via a custom config/assets.js that will be picked up and used just for that slice. All you need to do is put the file inside the slice’s own config/ directory: slices/[slice_name]/config/assets.js.

The app’s top-level config/assets.js will be used for all slices that do not have their own custom config.


Where can I see this work?

Check out the following work-in-progress PRs:

When will the rc3 release be?

I’m hoping for first half of February.

I’d love some solid testing for a week or two. If all goes well, 2.1.0 stable will come next.

Thanks for checking this out, and I hope this new approach can provide the most solid foundation for assets in Hanami 2. If you have any feedback, please share it here.

2 Likes

This looks great! Can’t wait to test it.

However, I think that sharing assets between slices might turn out to be more mainstream than you think :wink: (depending on how people use slices, of course) So it’s awesome to see this will be officially supported.

2 Likes

Yeah, I can’t wait to play with it either!

I’ve published a few ideas how slices can be used, but I can’t wait as community will come with other ideas.

For what I see currently, I’d be mostly interested in accessing root application assets in slices by default, and maybe having sth like: “shared” slices, which I’d import assets and other components from.

However, if I’d go with things like shared slices, maybe it would be better to not use app at all.

I’m still exploring cases where App root is useful and I’m not sure how I’ll use it when it comes to slices.

Isn’t app just a “special slice”? I always thought it is - which is why I deleted it :stuck_out_tongue: But I see I will need some kind of shared slice for common assets (and other stuff perhaps) too.

Isn’t app just a “special slice”

@katafrakt yep, a single-app setup means you just have one slice of a cake

Thanks for checking it out!

This is useful input, thank you!

The ability to access assets from another slice was certainly a must have for me in terms of this new approach. However, I have mixed thoughts about how “clunky” it may feel to people: declaring the import in the slice file seems OK, but the need to then add is as a dep in the respective view context class, and then refer to those assets via a some_slice_name_assets["..."] call in your views may feel like too much friction.

Perhaps the this is “positive friction,” though, in that it may lead the user to reconsider other approaches than the wholesale import of another slice’s assets.

Either way, I think I have the right foundations in place now, such that we have the capability to cleanly build more streamlined experiences if we see enough user feedback about the need to share assets.

One thing I might consider before a release is whether there might be a smoother way to access assets from the app level. But I think we might have the ingredients for this already. If you want fully co-mingled assets across your Hanami app, just don’t put asset files into individual slices; keep them all in app/assets/. Then after that, if you add "assets" to the app’s config.shared_app_component_keys, then those single set of assets will be available in every slice.

The work described up top is now fully implemented and available in the main branches of hanami, hanami-assets, hanami-assets-js and hanami-cli.

I can also confirm that the above approach will work if you want to have a single set of shared assets across all slices.