In an application, I need to instantiate audio files from a JS file (I am using AudioContext API) more or less like this:
playAudio(url) {
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
let data = await fetch(url).then(response => response.arrayBuffer());
let buffer = await this.audioContext.decodeAudioData(data)
const source = this.audioContext.createBufferSource()
source.buffer = buffer
source.connect(this.audioContext.destination)
source.start()
}
This JS file is a Stimulus controller loaded in a new Rails 7 application that uses importmap and Sprockets.
In development environment the JS can guess the path as Sprockets will serve the assets with their canonical name (like /assets/audio/file.wav
). However, in production, during assets precompilation, Sprockets adds a hash after the file name, and the file will be accessed only with a name like /assets/audio/file-f11ef113f11ef113f113.wav
.
This file name cannot be hardcoded as it depends on precompilation (technically I could probably hardcode the path with the hash as the file will not change often, but I do not want to assume anything about this hash).
This file is referenced in the manifest that Sprockets generates during precompilation aside to other assets in the public folder. Using Rails.application.assets_manifest.files
I can access the manifest data and do the mapping safely.
Here is the helper I wrote to do it:
def audio_assets_json
audio_assets = Rails.application.assets_manifest.files.select do |_key, file|
file['logical_path'].start_with?('audio/')
end
JSON.pretty_generate(
audio_assets.to_h { |_k, f| [f['logical_path'], asset_url(f['logical_path'])] }
)
end
But I need to access this data from the JS file and as the manifest also has a hash in its file name, my JS cannot simply load it.
My current solution is to include it in my application layout and this works fine:
<script>
window.assets = <%= audio_assets_json %>
window.asset_url = function(path) {
let result = assets[path]
return result ? result : `/assets/${path}`
}
</script>
The issue with this solution is that the hash is written in every single HTML response from the application server, which is not efficient. Also the helper is called at runtime which is also inefficient: this is dynamically generated at runtime whereas this should be done statically during deployment build.
My initial idea was to generate the list in a .js.erb
file generated by Sprockets at precompile time. So I renamed controllers/application.js
with controllers/application.js.erb
and called the helper this way:
<% environment.context_class.instance_eval { include ApplicationHelper } %>
window.assets = <%= audio_assets_json %>
The JS was correctly generated by Sprockets but somehow importmap
could not see it and the JS console shows the following error:
Unable to resolve specifier 'controllers/application' from http://localhost:3000/assets/controllers/index-2db729dddcc5b979110e98de4b6720f83f91a123172e87281d5a58410fc43806.js
I tried to add this line in config/initializers/assets.rb
:
Sprockets.register_mime_type 'application/javascript', extensions: ['.js.erb']
I tried to add this line in assets/manifest.js
:
//= link_tree ../../javascript .js.erb
But none of this helped.
So my question is: How I can reference assets URL from JS using importmap and Sprockets statically?