Module: Shakapacker::Helper

Defined in:
lib/shakapacker/helper.rb

Instance Method Summary collapse

Instance Method Details

#append_javascript_pack_tag(*names, defer: true, async: false) ⇒ Object



343
344
345
346
347
# File 'lib/shakapacker/helper.rb', line 343

def append_javascript_pack_tag(*names, defer: true, async: false)
  update_javascript_pack_tag_queue(defer: defer, async: async) do |hash_key|
    javascript_pack_tag_queue[hash_key] |= names
  end
end

#append_stylesheet_pack_tag(*names) ⇒ Object



330
331
332
333
334
335
336
337
338
339
340
341
# File 'lib/shakapacker/helper.rb', line 330

def append_stylesheet_pack_tag(*names)
  if @stylesheet_pack_tag_loaded
    raise "You can only call append_stylesheet_pack_tag before stylesheet_pack_tag helper. " \
    "Please refer to https://github.com/shakacode/shakapacker/blob/main/README.md#view-helper-append_javascript_pack_tag-prepend_javascript_pack_tag-and-append_stylesheet_pack_tag for the usage guide"
  end

  @stylesheet_pack_tag_queue ||= []
  @stylesheet_pack_tag_queue.concat names

  # prevent rendering Array#to_s representation when used with <%= … %> syntax
  nil
end

#asset_pack_path(name, **options) ⇒ Object

Computes the relative path for a given Shakapacker asset. Returns the relative path using manifest.json and passes it to path_to_asset helper. This will use path_to_asset internally, so most of their behaviors will be the same.

Example:

<%= asset_pack_path 'calendar.css' %> # => "/packs/calendar-1016838bab065ae1e122.css"


16
17
18
# File 'lib/shakapacker/helper.rb', line 16

def asset_pack_path(name, **options)
  path_to_asset(current_shakapacker_instance.manifest.lookup!(name), options)
end

#asset_pack_url(name, **options) ⇒ Object

Computes the absolute path for a given Shakapacker asset. Returns the absolute path using manifest.json and passes it to url_to_asset helper. This will use url_to_asset internally, so most of their behaviors will be the same.

Example:

<%= asset_pack_url 'calendar.css' %> # => "http://example.com/packs/calendar-1016838bab065ae1e122.css"


27
28
29
# File 'lib/shakapacker/helper.rb', line 27

def asset_pack_url(name, **options)
  url_to_asset(current_shakapacker_instance.manifest.lookup!(name), options)
end

#current_shakapacker_instanceObject

Returns the current Shakapacker instance. Could be overridden to use multiple Shakapacker configurations within the same app (e.g. with engines).



5
6
7
# File 'lib/shakapacker/helper.rb', line 5

def current_shakapacker_instance
  Shakapacker.instance
end

#favicon_pack_tag(name, **options) ⇒ Object

Creates a link tag for a favicon that references the named pack file.

Example:

<%= favicon_pack_tag 'mb-icon.png', rel: 'apple-touch-icon', type: 'image/png' %>
<link href="/packs/mb-icon-k344a6d59eef8632c9d1.png" rel="apple-touch-icon" type="image/png" />


71
72
73
# File 'lib/shakapacker/helper.rb', line 71

def favicon_pack_tag(name, **options)
  favicon_link_tag(resolve_path_to_image(name), options)
end

#image_pack_path(name, **options) ⇒ Object

Computes the relative path for a given Shakapacker image with the same automated processing as image_pack_tag. Returns the relative path using manifest.json and passes it to path_to_asset helper. This will use path_to_asset internally, so most of their behaviors will be the same.



34
35
36
# File 'lib/shakapacker/helper.rb', line 34

def image_pack_path(name, **options)
  resolve_path_to_image(name, **options)
end

#image_pack_tag(name, **options) ⇒ Object

Creates an image tag that references the named pack file.

Example:

<%= image_pack_tag 'application.png', size: '16x10', alt: 'Edit Entry' %>
<img alt='Edit Entry' src='/packs/application-k344a6d59eef8632c9d1.png' width='16' height='10' />

<%= image_pack_tag 'picture.png', srcset: { 'picture-2x.png' => '2x' } %>
<img srcset= "/packs/picture-2x-7cca48e6cae66ec07b8e.png 2x" src="/packs/picture-c38deda30895059837cf.png" >


55
56
57
58
59
60
61
62
63
# File 'lib/shakapacker/helper.rb', line 55

def image_pack_tag(name, **options)
  if options[:srcset] && !options[:srcset].is_a?(String)
    options[:srcset] = options[:srcset].map do |src_name, size|
      "#{resolve_path_to_image(src_name)} #{size}"
    end.join(", ")
  end

  image_tag(resolve_path_to_image(name), options)
end

#image_pack_url(name, **options) ⇒ Object

Computes the absolute path for a given Shakapacker image with the same automated processing as image_pack_tag. Returns the relative path using manifest.json and passes it to path_to_asset helper. This will use path_to_asset internally, so most of their behaviors will be the same.



42
43
44
# File 'lib/shakapacker/helper.rb', line 42

def image_pack_url(name, **options)
  resolve_path_to_image(name, **options.merge(protocol: :request))
end

#javascript_pack_tag(*names, defer: true, async: false, early_hints: nil, **options) ⇒ Object

Creates script tags that reference the js chunks from entrypoints when using split chunks API, as compiled by webpack per the entries list in package/environments/base.js. By default, this list is auto-generated to match everything in app/javascript/entrypoints/*.js and all the dependent chunks. In production mode, the digested reference is automatically looked up. See: webpack.js.org/plugins/split-chunks-plugin/

Example:

<%= javascript_pack_tag 'calendar', 'map', 'data-turbolinks-track': 'reload' %> # =>
<script src="/packs/vendor-16838bab065ae1e314.chunk.js" data-turbolinks-track="reload" defer="true"></script>
<script src="/packs/calendar~runtime-16838bab065ae1e314.chunk.js" data-turbolinks-track="reload" defer="true"></script>
<script src="/packs/calendar-1016838bab065ae1e314.chunk.js" data-turbolinks-track="reload" defer="true"></script>
<script src="/packs/map~runtime-16838bab065ae1e314.chunk.js" data-turbolinks-track="reload" defer="true"></script>
<script src="/packs/map-16838bab065ae1e314.chunk.js" data-turbolinks-track="reload" defer="true"></script>

DO:

<%= javascript_pack_tag 'calendar', 'map' %>

DON’T:

<%= javascript_pack_tag 'calendar' %>
<%= javascript_pack_tag 'map' %>

Early Hints:

By default, HTTP 103 Early Hints are sent automatically when this helper is called,
allowing browsers to preload JavaScript assets in parallel with Rails rendering.

<%= javascript_pack_tag 'application' %>
# Automatically sends early hints for 'application' pack

# Customize handling per pack:
<%= javascript_pack_tag 'application', 'vendor',
      early_hints: { 'application' => 'preload', 'vendor' => 'prefetch' } %>

# Disable early hints:
<%= javascript_pack_tag 'application', early_hints: false %>


112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'lib/shakapacker/helper.rb', line 112

def javascript_pack_tag(*names, defer: true, async: false, early_hints: nil, **options)
  if @javascript_pack_tag_loaded
    raise "To prevent duplicated chunks on the page, you should call javascript_pack_tag only once on the page. " \
    "Please refer to https://github.com/shakacode/shakapacker/blob/main/README.md#view-helpers-javascript_pack_tag-and-stylesheet_pack_tag for the usage guide"
  end

  # Collect all packs (queue + direct args)
  append_javascript_pack_tag(*names, defer: defer, async: async)
  all_packs = javascript_pack_tag_queue.values.flatten.uniq

  # Resolve effective early hints value (nil = use config default)
  effective_hints = resolve_early_hints_value(early_hints, :javascript)

  # Send early hints automatically if enabled
  if early_hints_enabled? && effective_hints && effective_hints != "none"
    hints_config = normalize_pack_hints(all_packs, effective_hints)
    send_javascript_early_hints_internal(hints_config)
    # Flush accumulated hints (sends the single 103 response)
    flush_early_hints
  elsif early_hints_debug_enabled?
    store = early_hints_store
    store[:debug_buffer] ||= []
    store[:debug_buffer] << "<!-- Shakapacker Early Hints (JS): SKIPPED (early_hints: #{effective_hints.inspect}) -->"
  end

  sync = sources_from_manifest_entrypoints(javascript_pack_tag_queue[:sync], type: :javascript)
  async = sources_from_manifest_entrypoints(javascript_pack_tag_queue[:async], type: :javascript) - sync
  deferred = sources_from_manifest_entrypoints(javascript_pack_tag_queue[:deferred], type: :javascript) - sync - async

  @javascript_pack_tag_loaded = true

  capture do
    # Output debug buffer first
    if early_hints_debug_enabled?
      store = early_hints_store
      if store[:debug_buffer] && store[:debug_buffer].any?
        concat store[:debug_buffer].join("\n").html_safe
        concat "\n"
      end
    end

    render_tags(async, :javascript, **options.dup.tap { |o| o[:async] = true })
    concat "\n" if async.any? && deferred.any?
    render_tags(deferred, :javascript, **options.dup.tap { |o| o[:defer] = true })
    concat "\n" if sync.any? && deferred.any?
    render_tags(sync, :javascript, options)
  end
end

#preload_pack_asset(name, **options) ⇒ Object

Creates a link tag, for preloading, that references a given Shakapacker asset. In production mode, the digested reference is automatically looked up. See: developer.mozilla.org/en-US/docs/Web/HTML/Preloading_content

Example:

<%= preload_pack_asset 'fonts/fa-regular-400.woff2' %> # =>
<link rel="preload" href="/packs/fonts/fa-regular-400-944fb546bd7018b07190a32244f67dc9.woff2" as="font" type="font/woff2" crossorigin="anonymous">


169
170
171
172
173
174
175
# File 'lib/shakapacker/helper.rb', line 169

def preload_pack_asset(name, **options)
  if self.class.method_defined?(:preload_link_tag)
    preload_link_tag(current_shakapacker_instance.manifest.lookup!(name), options)
  else
    raise "You need Rails >= 5.2 to use this tag."
  end
end

#prepend_javascript_pack_tag(*names, defer: true, async: false) ⇒ Object



349
350
351
352
353
# File 'lib/shakapacker/helper.rb', line 349

def prepend_javascript_pack_tag(*names, defer: true, async: false)
  update_javascript_pack_tag_queue(defer: defer, async: async) do |hash_key|
    javascript_pack_tag_queue[hash_key].unshift(*names)
  end
end

#send_pack_early_hints(config) ⇒ Object

Sends HTTP 103 Early Hints for specified packs with fine-grained control over JavaScript and CSS handling. This is the “raw” method for maximum flexibility.

Use this in controller actions BEFORE expensive work (database queries, API calls) to maximize parallelism - the browser downloads assets while Rails processes the request.

For simpler cases, use javascript_pack_tag and stylesheet_pack_tag which automatically send hints when called (combining queued + direct pack names).

HTTP 103 Early Hints allows the server to send preliminary responses with Link headers before the final HTTP 200 response, enabling browsers to start downloading critical assets during the server’s “think time”.

Timeline:

1. Browser requests page
2. Controller calls send_pack_early_hints (this method)
3. Server sends HTTP 103 with Link: headers
4. Browser starts downloading assets IN PARALLEL with step 5
5. Rails continues expensive work (queries, rendering)
6. Server sends HTTP 200 with full HTML
7. Assets already downloaded = faster page load

Requires Rails 5.2+, HTTP/2, and server support (Puma 5+, nginx 1.13+). Gracefully degrades if not supported.

References:

Examples:

# Controller pattern: send hints BEFORE expensive work
def show
  send_pack_early_hints({
    "application" => { js: "preload", css: "preload" },
    "vendor" => { js: "prefetch", css: "none" }
  })

  # Browser now downloading assets while we do expensive work
  @posts = Post.includes(:comments, :author).where(complex_conditions)
  # ... more expensive work ...
end

# Supported handling values:
# - "preload": High-priority, browser downloads immediately
# - "prefetch": Low-priority, browser may download when idle
# - "none" or false: Skip this asset type for this pack


224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
# File 'lib/shakapacker/helper.rb', line 224

def send_pack_early_hints(config)
  return nil unless early_hints_supported? && early_hints_enabled?

  # Accumulate both JS and CSS hints, then send ONCE
  config.each do |pack_name, handlers|
    # Accumulate JavaScript hints
    js_handling = handlers[:js] || handlers["js"]
    normalized_js = normalize_hint_value(js_handling)
    if normalized_js
      send_early_hints_internal({ pack_name.to_s => normalized_js }, type: :javascript)
    end

    # Accumulate CSS hints
    css_handling = handlers[:css] || handlers["css"]
    normalized_css = normalize_hint_value(css_handling)
    if normalized_css
      send_early_hints_internal({ pack_name.to_s => normalized_css }, type: :stylesheet)
    end
  end

  # Flush the accumulated hints as a SINGLE 103 response
  # (Browsers only process the first 103)
  flush_early_hints

  nil
end

#stylesheet_pack_tag(*names, early_hints: nil, **options) ⇒ Object

Creates link tags that reference the css chunks from entrypoints when using split chunks API, as compiled by webpack per the entries list in package/environments/base.js. By default, this list is auto-generated to match everything in app/javascript/entrypoints/*.js and all the dependent chunks. In production mode, the digested reference is automatically looked up. See: webpack.js.org/plugins/split-chunks-plugin/

Examples:

<%= stylesheet_pack_tag 'calendar', 'map' %> # =>
<link rel="stylesheet" media="screen" href="/packs/3-8c7ce31a.chunk.css" />
<link rel="stylesheet" media="screen" href="/packs/calendar-8c7ce31a.chunk.css" />
<link rel="stylesheet" media="screen" href="/packs/map-8c7ce31a.chunk.css" />

When using the webpack-dev-server, CSS is inlined so HMR can be turned on for CSS,
including CSS modules
<%= stylesheet_pack_tag 'calendar', 'map' %> # => nil

DO:

<%= stylesheet_pack_tag 'calendar', 'map' %>

DON’T:

<%= stylesheet_pack_tag 'calendar' %>
<%= stylesheet_pack_tag 'map' %>

Early Hints:

By default, HTTP 103 Early Hints are sent automatically when this helper is called,
allowing browsers to preload CSS assets in parallel with Rails rendering.

<%= stylesheet_pack_tag 'application' %>
# Automatically sends early hints for 'application' pack

# Customize handling per pack:
<%= stylesheet_pack_tag 'application', 'vendor',
      early_hints: { 'application' => 'preload', 'vendor' => 'prefetch' } %>

# Disable early hints:
<%= stylesheet_pack_tag 'application', early_hints: false %>


290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
# File 'lib/shakapacker/helper.rb', line 290

def stylesheet_pack_tag(*names, early_hints: nil, **options)
  return "" if Shakapacker.inlining_css?

  # Collect all packs (queue + direct args)
  all_packs = ((@stylesheet_pack_tag_queue || []) + names).uniq

  # Resolve effective early hints value (nil = use config default)
  effective_hints = resolve_early_hints_value(early_hints, :stylesheet)

  # Send early hints automatically if enabled
  if early_hints_enabled? && effective_hints && effective_hints != "none"
    hints_config = normalize_pack_hints(all_packs, effective_hints)
    send_stylesheet_early_hints_internal(hints_config)
    # Flush accumulated hints (sends the single 103 response)
    flush_early_hints
  elsif early_hints_debug_enabled?
    store = early_hints_store
    store[:debug_buffer] ||= []
    store[:debug_buffer] << "<!-- Shakapacker Early Hints (CSS): SKIPPED (early_hints: #{effective_hints.inspect}) -->"
  end

  requested_packs = sources_from_manifest_entrypoints(names, type: :stylesheet)
  appended_packs = available_sources_from_manifest_entrypoints(@stylesheet_pack_tag_queue || [], type: :stylesheet)

  @stylesheet_pack_tag_loaded = true

  capture do
    # Output debug buffer first
    if early_hints_debug_enabled?
      store = early_hints_store
      if store[:debug_buffer] && store[:debug_buffer].any?
        concat store[:debug_buffer].join("\n").html_safe
        concat "\n"
      end
    end

    render_tags(requested_packs | appended_packs, :stylesheet, options)
  end
end