5

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?

ybart
  • 876
  • 8
  • 23
  • It may need some tweaking for modern versions, but when asset digests were first introduced, I used https://github.com/alexspeller/non-stupid-digest-assets to get un-fingerprinted versions of some files that could be referenced during PDF creation. I hope this helps. – Unixmonkey Feb 03 '22 at 18:42
  • @Unixmonkey Thanks for the suggestion. In my case, I'd rather have my assets with digest. It could be useful if I need to to update an audio file. – ybart Feb 03 '22 at 20:48
  • FWIW, I'm pretty sure it leaves the digested assets in place, but also makes an undigested copy. – Unixmonkey Feb 03 '22 at 23:11

0 Answers0