shakapacker 9.3.0.beta.4 → 9.3.0.beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +20 -0
- data/CLAUDE.md +13 -0
- data/Gemfile.lock +1 -1
- data/README.md +19 -0
- data/docs/configuration.md +35 -0
- data/docs/early_hints.md +440 -0
- data/docs/early_hints_new_api.md +700 -0
- data/docs/feature_testing.md +492 -0
- data/lib/install/config/shakapacker.yml +27 -0
- data/lib/shakapacker/configuration.rb +4 -0
- data/lib/shakapacker/helper.rb +409 -14
- data/lib/shakapacker/railtie.rb +4 -0
- data/lib/shakapacker/version.rb +1 -1
- data/package/configExporter/buildValidator.ts +1 -2
- data/package/configExporter/cli.ts +2 -1
- data/package-lock.json +2 -2
- data/package.json +1 -1
- data/scripts/remove-use-strict.js +0 -1
- data/test/package/rules/babel.test.js +1 -0
- data/test/package/rules/swc.test.js +1 -0
- metadata +5 -2
data/lib/shakapacker/helper.rb
CHANGED
@@ -95,13 +95,45 @@ module Shakapacker::Helper
|
|
95
95
|
#
|
96
96
|
# <%= javascript_pack_tag 'calendar' %>
|
97
97
|
# <%= javascript_pack_tag 'map' %>
|
98
|
-
|
98
|
+
#
|
99
|
+
# Early Hints:
|
100
|
+
# By default, HTTP 103 Early Hints are sent automatically when this helper is called,
|
101
|
+
# allowing browsers to preload JavaScript assets in parallel with Rails rendering.
|
102
|
+
#
|
103
|
+
# <%= javascript_pack_tag 'application' %>
|
104
|
+
# # Automatically sends early hints for 'application' pack
|
105
|
+
#
|
106
|
+
# # Customize handling per pack:
|
107
|
+
# <%= javascript_pack_tag 'application', 'vendor',
|
108
|
+
# early_hints: { 'application' => 'preload', 'vendor' => 'prefetch' } %>
|
109
|
+
#
|
110
|
+
# # Disable early hints:
|
111
|
+
# <%= javascript_pack_tag 'application', early_hints: false %>
|
112
|
+
def javascript_pack_tag(*names, defer: true, async: false, early_hints: nil, **options)
|
99
113
|
if @javascript_pack_tag_loaded
|
100
114
|
raise "To prevent duplicated chunks on the page, you should call javascript_pack_tag only once on the page. " \
|
101
115
|
"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"
|
102
116
|
end
|
103
117
|
|
118
|
+
# Collect all packs (queue + direct args)
|
104
119
|
append_javascript_pack_tag(*names, defer: defer, async: async)
|
120
|
+
all_packs = javascript_pack_tag_queue.values.flatten.uniq
|
121
|
+
|
122
|
+
# Resolve effective early hints value (nil = use config default)
|
123
|
+
effective_hints = resolve_early_hints_value(early_hints, :javascript)
|
124
|
+
|
125
|
+
# Send early hints automatically if enabled
|
126
|
+
if early_hints_enabled? && effective_hints && effective_hints != "none"
|
127
|
+
hints_config = normalize_pack_hints(all_packs, effective_hints)
|
128
|
+
send_javascript_early_hints_internal(hints_config)
|
129
|
+
# Flush accumulated hints (sends the single 103 response)
|
130
|
+
flush_early_hints
|
131
|
+
elsif early_hints_debug_enabled?
|
132
|
+
store = early_hints_store
|
133
|
+
store[:debug_buffer] ||= []
|
134
|
+
store[:debug_buffer] << "<!-- Shakapacker Early Hints (JS): SKIPPED (early_hints: #{effective_hints.inspect}) -->"
|
135
|
+
end
|
136
|
+
|
105
137
|
sync = sources_from_manifest_entrypoints(javascript_pack_tag_queue[:sync], type: :javascript)
|
106
138
|
async = sources_from_manifest_entrypoints(javascript_pack_tag_queue[:async], type: :javascript) - sync
|
107
139
|
deferred = sources_from_manifest_entrypoints(javascript_pack_tag_queue[:deferred], type: :javascript) - sync - async
|
@@ -109,6 +141,15 @@ module Shakapacker::Helper
|
|
109
141
|
@javascript_pack_tag_loaded = true
|
110
142
|
|
111
143
|
capture do
|
144
|
+
# Output debug buffer first
|
145
|
+
if early_hints_debug_enabled?
|
146
|
+
store = early_hints_store
|
147
|
+
if store[:debug_buffer] && store[:debug_buffer].any?
|
148
|
+
concat store[:debug_buffer].join("\n").html_safe
|
149
|
+
concat "\n"
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
112
153
|
render_tags(async, :javascript, **options.dup.tap { |o| o[:async] = true })
|
113
154
|
concat "\n" if async.any? && deferred.any?
|
114
155
|
render_tags(deferred, :javascript, **options.dup.tap { |o| o[:defer] = true })
|
@@ -133,6 +174,80 @@ module Shakapacker::Helper
|
|
133
174
|
end
|
134
175
|
end
|
135
176
|
|
177
|
+
# Sends HTTP 103 Early Hints for specified packs with fine-grained control over
|
178
|
+
# JavaScript and CSS handling. This is the "raw" method for maximum flexibility.
|
179
|
+
#
|
180
|
+
# Use this in controller actions BEFORE expensive work (database queries, API calls)
|
181
|
+
# to maximize parallelism - the browser downloads assets while Rails processes the request.
|
182
|
+
#
|
183
|
+
# For simpler cases, use javascript_pack_tag and stylesheet_pack_tag which automatically
|
184
|
+
# send hints when called (combining queued + direct pack names).
|
185
|
+
#
|
186
|
+
# HTTP 103 Early Hints allows the server to send preliminary responses with Link headers
|
187
|
+
# before the final HTTP 200 response, enabling browsers to start downloading critical
|
188
|
+
# assets during the server's "think time".
|
189
|
+
#
|
190
|
+
# Timeline:
|
191
|
+
# 1. Browser requests page
|
192
|
+
# 2. Controller calls send_pack_early_hints (this method)
|
193
|
+
# 3. Server sends HTTP 103 with Link: headers
|
194
|
+
# 4. Browser starts downloading assets IN PARALLEL with step 5
|
195
|
+
# 5. Rails continues expensive work (queries, rendering)
|
196
|
+
# 6. Server sends HTTP 200 with full HTML
|
197
|
+
# 7. Assets already downloaded = faster page load
|
198
|
+
#
|
199
|
+
# Requires Rails 5.2+, HTTP/2, and server support (Puma 5+, nginx 1.13+).
|
200
|
+
# Gracefully degrades if not supported.
|
201
|
+
#
|
202
|
+
# References:
|
203
|
+
# - Rails API: https://api.rubyonrails.org/classes/ActionDispatch/Request.html#method-i-send_early_hints
|
204
|
+
# - HTTP 103 Spec: https://datatracker.ietf.org/doc/html/rfc8297
|
205
|
+
#
|
206
|
+
# Examples:
|
207
|
+
#
|
208
|
+
# # Controller pattern: send hints BEFORE expensive work
|
209
|
+
# def show
|
210
|
+
# send_pack_early_hints({
|
211
|
+
# "application" => { js: "preload", css: "preload" },
|
212
|
+
# "vendor" => { js: "prefetch", css: "none" }
|
213
|
+
# })
|
214
|
+
#
|
215
|
+
# # Browser now downloading assets while we do expensive work
|
216
|
+
# @posts = Post.includes(:comments, :author).where(complex_conditions)
|
217
|
+
# # ... more expensive work ...
|
218
|
+
# end
|
219
|
+
#
|
220
|
+
# # Supported handling values:
|
221
|
+
# # - "preload": High-priority, browser downloads immediately
|
222
|
+
# # - "prefetch": Low-priority, browser may download when idle
|
223
|
+
# # - "none" or false: Skip this asset type for this pack
|
224
|
+
def send_pack_early_hints(config)
|
225
|
+
return nil unless early_hints_supported? && early_hints_enabled?
|
226
|
+
|
227
|
+
# Accumulate both JS and CSS hints, then send ONCE
|
228
|
+
config.each do |pack_name, handlers|
|
229
|
+
# Accumulate JavaScript hints
|
230
|
+
js_handling = handlers[:js] || handlers["js"]
|
231
|
+
normalized_js = normalize_hint_value(js_handling)
|
232
|
+
if normalized_js
|
233
|
+
send_early_hints_internal({ pack_name.to_s => normalized_js }, type: :javascript)
|
234
|
+
end
|
235
|
+
|
236
|
+
# Accumulate CSS hints
|
237
|
+
css_handling = handlers[:css] || handlers["css"]
|
238
|
+
normalized_css = normalize_hint_value(css_handling)
|
239
|
+
if normalized_css
|
240
|
+
send_early_hints_internal({ pack_name.to_s => normalized_css }, type: :stylesheet)
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
# Flush the accumulated hints as a SINGLE 103 response
|
245
|
+
# (Browsers only process the first 103)
|
246
|
+
flush_early_hints
|
247
|
+
|
248
|
+
nil
|
249
|
+
end
|
250
|
+
|
136
251
|
# Creates link tags that reference the css chunks from entrypoints when using split chunks API,
|
137
252
|
# as compiled by webpack per the entries list in package/environments/base.js.
|
138
253
|
# By default, this list is auto-generated to match everything in
|
@@ -158,15 +273,56 @@ module Shakapacker::Helper
|
|
158
273
|
#
|
159
274
|
# <%= stylesheet_pack_tag 'calendar' %>
|
160
275
|
# <%= stylesheet_pack_tag 'map' %>
|
161
|
-
|
276
|
+
#
|
277
|
+
# Early Hints:
|
278
|
+
# By default, HTTP 103 Early Hints are sent automatically when this helper is called,
|
279
|
+
# allowing browsers to preload CSS assets in parallel with Rails rendering.
|
280
|
+
#
|
281
|
+
# <%= stylesheet_pack_tag 'application' %>
|
282
|
+
# # Automatically sends early hints for 'application' pack
|
283
|
+
#
|
284
|
+
# # Customize handling per pack:
|
285
|
+
# <%= stylesheet_pack_tag 'application', 'vendor',
|
286
|
+
# early_hints: { 'application' => 'preload', 'vendor' => 'prefetch' } %>
|
287
|
+
#
|
288
|
+
# # Disable early hints:
|
289
|
+
# <%= stylesheet_pack_tag 'application', early_hints: false %>
|
290
|
+
def stylesheet_pack_tag(*names, early_hints: nil, **options)
|
162
291
|
return "" if Shakapacker.inlining_css?
|
163
292
|
|
293
|
+
# Collect all packs (queue + direct args)
|
294
|
+
all_packs = ((@stylesheet_pack_tag_queue || []) + names).uniq
|
295
|
+
|
296
|
+
# Resolve effective early hints value (nil = use config default)
|
297
|
+
effective_hints = resolve_early_hints_value(early_hints, :stylesheet)
|
298
|
+
|
299
|
+
# Send early hints automatically if enabled
|
300
|
+
if early_hints_enabled? && effective_hints && effective_hints != "none"
|
301
|
+
hints_config = normalize_pack_hints(all_packs, effective_hints)
|
302
|
+
send_stylesheet_early_hints_internal(hints_config)
|
303
|
+
# Flush accumulated hints (sends the single 103 response)
|
304
|
+
flush_early_hints
|
305
|
+
elsif early_hints_debug_enabled?
|
306
|
+
store = early_hints_store
|
307
|
+
store[:debug_buffer] ||= []
|
308
|
+
store[:debug_buffer] << "<!-- Shakapacker Early Hints (CSS): SKIPPED (early_hints: #{effective_hints.inspect}) -->"
|
309
|
+
end
|
310
|
+
|
164
311
|
requested_packs = sources_from_manifest_entrypoints(names, type: :stylesheet)
|
165
312
|
appended_packs = available_sources_from_manifest_entrypoints(@stylesheet_pack_tag_queue || [], type: :stylesheet)
|
166
313
|
|
167
314
|
@stylesheet_pack_tag_loaded = true
|
168
315
|
|
169
316
|
capture do
|
317
|
+
# Output debug buffer first
|
318
|
+
if early_hints_debug_enabled?
|
319
|
+
store = early_hints_store
|
320
|
+
if store[:debug_buffer] && store[:debug_buffer].any?
|
321
|
+
concat store[:debug_buffer].join("\n").html_safe
|
322
|
+
concat "\n"
|
323
|
+
end
|
324
|
+
end
|
325
|
+
|
170
326
|
render_tags(requested_packs | appended_packs, :stylesheet, options)
|
171
327
|
end
|
172
328
|
end
|
@@ -253,25 +409,264 @@ module Shakapacker::Helper
|
|
253
409
|
def render_tags(sources, type, options)
|
254
410
|
return unless sources.present? || type.present?
|
255
411
|
|
256
|
-
|
257
|
-
|
412
|
+
# Temporarily disable Rails' built-in early hints for ALL tags
|
413
|
+
# Rails' javascript_include_tag and stylesheet_link_tag call request.send_early_hints
|
414
|
+
# We handle early hints ourselves before render_tags is called
|
415
|
+
patched = false
|
416
|
+
if respond_to?(:request) && request&.respond_to?(:send_early_hints)
|
417
|
+
request.define_singleton_method(:send_early_hints) { |*args| nil }
|
418
|
+
patched = true
|
419
|
+
end
|
420
|
+
|
421
|
+
begin
|
422
|
+
sources.each.with_index do |source, index|
|
423
|
+
# Duplicate options per iteration to avoid leaking integrity/crossorigin between tags
|
424
|
+
local_options = options.dup
|
425
|
+
tag_source = lookup_source(source)
|
426
|
+
|
427
|
+
if current_shakapacker_instance.config.integrity[:enabled]
|
428
|
+
integrity = lookup_integrity(source)
|
258
429
|
|
259
|
-
|
260
|
-
|
430
|
+
if integrity.present?
|
431
|
+
local_options[:integrity] = integrity
|
432
|
+
local_options[:crossorigin] = current_shakapacker_instance.config.integrity[:cross_origin]
|
433
|
+
end
|
434
|
+
end
|
261
435
|
|
262
|
-
if
|
263
|
-
|
264
|
-
|
436
|
+
if type == :javascript
|
437
|
+
concat javascript_include_tag(tag_source, **local_options)
|
438
|
+
else
|
439
|
+
concat stylesheet_link_tag(tag_source, **local_options)
|
265
440
|
end
|
441
|
+
|
442
|
+
concat "\n" unless index == sources.size - 1
|
443
|
+
end
|
444
|
+
ensure
|
445
|
+
# Restore original method by removing the singleton method
|
446
|
+
if patched
|
447
|
+
request.singleton_class.send(:remove_method, :send_early_hints)
|
266
448
|
end
|
449
|
+
end
|
450
|
+
end
|
451
|
+
|
452
|
+
# Check if early hints are supported by Rails and the request object
|
453
|
+
def early_hints_supported?
|
454
|
+
request.respond_to?(:send_early_hints)
|
455
|
+
end
|
456
|
+
|
457
|
+
# Check if early hints are enabled in configuration
|
458
|
+
def early_hints_enabled?
|
459
|
+
config = current_shakapacker_instance.config.early_hints rescue nil
|
460
|
+
return false unless config
|
461
|
+
# Handle both symbol and string keys from YAML config
|
462
|
+
enabled = config[:enabled] || config["enabled"]
|
463
|
+
enabled == true
|
464
|
+
end
|
267
465
|
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
466
|
+
# Check if early hints debug mode is enabled
|
467
|
+
def early_hints_debug_enabled?
|
468
|
+
config = current_shakapacker_instance.config.early_hints rescue nil
|
469
|
+
return false unless config
|
470
|
+
# Handle both symbol and string keys from YAML config
|
471
|
+
debug = config[:debug] || config["debug"]
|
472
|
+
debug == true
|
473
|
+
end
|
474
|
+
|
475
|
+
# Resolve the effective early hints value
|
476
|
+
# If nil or true, read from config; otherwise use the provided value
|
477
|
+
def resolve_early_hints_value(early_hints, asset_type)
|
478
|
+
# If explicitly set to false or "none", use that
|
479
|
+
return "none" if early_hints == false || early_hints == "none"
|
480
|
+
|
481
|
+
# If nil or true, read from config
|
482
|
+
if early_hints.nil? || early_hints == true
|
483
|
+
config = current_shakapacker_instance.config.early_hints rescue nil
|
484
|
+
return "preload" unless config # Default fallback
|
485
|
+
|
486
|
+
# Get type-specific config (js/css), handling both symbol and string keys
|
487
|
+
type_key = asset_type == :javascript ? "js" : "css"
|
488
|
+
value = config[type_key.to_sym] || config[type_key]
|
489
|
+
return value || "preload" # Default to preload if not configured
|
490
|
+
end
|
491
|
+
|
492
|
+
# Return provided value as-is (will be normalized later)
|
493
|
+
early_hints
|
494
|
+
end
|
495
|
+
|
496
|
+
# Normalize pack hints into a hash mapping pack names to validated hint values
|
497
|
+
# Converts booleans, validates strings, and ensures only valid values are used
|
498
|
+
def normalize_pack_hints(packs, early_hints)
|
499
|
+
# Normalize the hint value(s)
|
500
|
+
if early_hints.is_a?(Hash)
|
501
|
+
# Per-pack configuration
|
502
|
+
packs.each_with_object({}) do |pack, result|
|
503
|
+
hint_value = early_hints[pack] || early_hints[pack.to_s] || early_hints[pack.to_sym]
|
504
|
+
result[pack] = normalize_hint_value(hint_value || "preload")
|
272
505
|
end
|
506
|
+
else
|
507
|
+
# Single value for all packs
|
508
|
+
normalized_value = normalize_hint_value(early_hints)
|
509
|
+
packs.each_with_object({}) do |pack, result|
|
510
|
+
result[pack] = normalized_value
|
511
|
+
end
|
512
|
+
end
|
513
|
+
end
|
273
514
|
|
274
|
-
|
515
|
+
# Normalize and validate a single hint value
|
516
|
+
# Converts false/nil to "none", downcases strings, validates against allowed values
|
517
|
+
def normalize_hint_value(value)
|
518
|
+
# Convert booleans and nil
|
519
|
+
return "none" if value == false || value.nil?
|
520
|
+
return "preload" if value == true
|
521
|
+
|
522
|
+
# Downcase and validate string
|
523
|
+
str_value = value.to_s.downcase.strip
|
524
|
+
|
525
|
+
# Only allow valid values
|
526
|
+
valid_values = ["preload", "prefetch", "none"]
|
527
|
+
valid_values.include?(str_value) ? str_value : "none"
|
528
|
+
end
|
529
|
+
|
530
|
+
# Generate reason why early hints were skipped
|
531
|
+
def early_hints_skip_reason
|
532
|
+
unless early_hints_supported?
|
533
|
+
return "Rails request.send_early_hints not available (requires Rails 5.2+)"
|
534
|
+
end
|
535
|
+
unless early_hints_enabled?
|
536
|
+
return "early_hints.enabled is false in config/shakapacker.yml"
|
537
|
+
end
|
538
|
+
"Unknown reason"
|
539
|
+
end
|
540
|
+
|
541
|
+
# Build a Link header value for early hints
|
542
|
+
# Takes the already-resolved source_path to avoid duplicate lookup_source calls
|
543
|
+
def build_link_header(source_path, source, as:, rel: "preload")
|
544
|
+
parts = ["<#{source_path}>", "rel=#{rel}", "as=#{as}"]
|
545
|
+
|
546
|
+
# Add crossorigin and integrity if enabled (consistent with render_tags)
|
547
|
+
if current_shakapacker_instance.config.integrity[:enabled]
|
548
|
+
integrity = lookup_integrity(source)
|
549
|
+
if integrity.present?
|
550
|
+
parts << "integrity=\"#{integrity}\""
|
551
|
+
# Use configured cross_origin value, consistent with render_tags
|
552
|
+
cross_origin = current_shakapacker_instance.config.integrity[:cross_origin]
|
553
|
+
parts << "crossorigin=\"#{cross_origin}\""
|
554
|
+
end
|
555
|
+
elsif ["script", "style", "font"].include?(as)
|
556
|
+
# When integrity not enabled, scripts, styles, and fonts still need crossorigin for CORS
|
557
|
+
parts << "crossorigin=\"anonymous\""
|
275
558
|
end
|
559
|
+
|
560
|
+
parts.join("; ")
|
561
|
+
end
|
562
|
+
|
563
|
+
# Returns the shared early hints storage from request.env
|
564
|
+
# This ensures state is shared across controller/view contexts
|
565
|
+
def early_hints_store
|
566
|
+
request.env["shakapacker.early_hints"] ||= {}
|
567
|
+
end
|
568
|
+
|
569
|
+
# Internal method to accumulate and send early hints
|
570
|
+
# Sends only ONE 103 response (browsers ignore subsequent ones)
|
571
|
+
# config: { "application" => "preload", "vendor" => "prefetch" }
|
572
|
+
# type: :javascript or :stylesheet
|
573
|
+
def send_early_hints_internal(config, type:)
|
574
|
+
return unless early_hints_supported?
|
575
|
+
|
576
|
+
# Use request.env as shared storage across controller/view contexts
|
577
|
+
# This prevents duplicate packs and ensures debug info is preserved
|
578
|
+
store = early_hints_store
|
579
|
+
store[:sent_packs] ||= { javascript: {}, stylesheet: {} }
|
580
|
+
store[:link_buffer] ||= []
|
581
|
+
store[:debug_buffer] ||= []
|
582
|
+
|
583
|
+
# If we've already sent the 103 response, just track for debug
|
584
|
+
if store[:http_103_sent]
|
585
|
+
if early_hints_debug_enabled?
|
586
|
+
store[:debug_buffer] << "<!-- Shakapacker Early Hints (#{type.upcase}): Not sent (103 already sent) -->"
|
587
|
+
store[:debug_buffer] << "<!-- Packs: #{config.keys.join(', ')} -->"
|
588
|
+
end
|
589
|
+
return
|
590
|
+
end
|
591
|
+
|
592
|
+
# Filter to only new packs for THIS type, and skip "none" values
|
593
|
+
new_hints = config.reject { |pack, handling| store[:sent_packs][type].key?(pack) || handling == "none" }
|
594
|
+
|
595
|
+
if early_hints_debug_enabled? && new_hints.empty?
|
596
|
+
store[:debug_buffer] << "<!-- Shakapacker Early Hints (#{type.upcase}): All packs already queued -->"
|
597
|
+
end
|
598
|
+
|
599
|
+
# Accumulate Link headers for this type
|
600
|
+
asset_type = type == :javascript ? "script" : "style"
|
601
|
+
new_hints.each do |pack_name, handling|
|
602
|
+
# Skip if handling is "none" (extra safety check)
|
603
|
+
next if handling == "none"
|
604
|
+
|
605
|
+
begin
|
606
|
+
sources = available_sources_from_manifest_entrypoints([pack_name], type: type)
|
607
|
+
sources.each do |source|
|
608
|
+
source_path = lookup_source(source)
|
609
|
+
store[:link_buffer] << build_link_header(source_path, source, as: asset_type, rel: handling)
|
610
|
+
end
|
611
|
+
# Mark pack as queued for THIS type
|
612
|
+
store[:sent_packs][type][pack_name] = handling
|
613
|
+
rescue Shakapacker::Manifest::MissingEntryError, NoMethodError => e
|
614
|
+
Rails.logger.debug { "Early hints: skipping pack '#{pack_name}' - #{e.class}: #{e.message}" }
|
615
|
+
end
|
616
|
+
end
|
617
|
+
|
618
|
+
# Note: We DON'T flush here - caller must call flush_early_hints explicitly
|
619
|
+
# This allows accumulating multiple calls (JS + CSS) before sending ONE 103
|
620
|
+
end
|
621
|
+
|
622
|
+
# Send accumulated early hints as a SINGLE 103 response
|
623
|
+
# Browsers only process the first 103, so we send everything at once
|
624
|
+
def flush_early_hints
|
625
|
+
store = early_hints_store
|
626
|
+
|
627
|
+
# Guard against multiple flushes - only send once per request
|
628
|
+
return if store[:http_103_sent]
|
629
|
+
return if store[:link_buffer].nil? || store[:link_buffer].empty?
|
630
|
+
|
631
|
+
# Check if response is already committed (headers already sent)
|
632
|
+
if respond_to?(:response) && response&.committed?
|
633
|
+
Rails.logger.debug { "Early hints: Cannot send 103 - response already committed" }
|
634
|
+
return
|
635
|
+
end
|
636
|
+
|
637
|
+
# Set flag BEFORE sending to prevent race conditions
|
638
|
+
store[:http_103_sent] = true
|
639
|
+
|
640
|
+
# Send the 103 response with error handling
|
641
|
+
begin
|
642
|
+
request.send_early_hints({ "Link" => store[:link_buffer].join(", ") })
|
643
|
+
rescue => e
|
644
|
+
Rails.logger.error { "Early hints: Failed to send 103 - #{e.class}: #{e.message}" }
|
645
|
+
end
|
646
|
+
|
647
|
+
if early_hints_debug_enabled?
|
648
|
+
all_packs = (store[:sent_packs][:javascript].keys + store[:sent_packs][:stylesheet].keys).uniq
|
649
|
+
store[:debug_buffer] << "<!-- Shakapacker Early Hints: HTTP/1.1 103 SENT -->"
|
650
|
+
store[:debug_buffer] << "<!-- Total Links: #{store[:link_buffer].size} -->"
|
651
|
+
store[:debug_buffer] << "<!-- Packs: #{all_packs.join(', ')} -->"
|
652
|
+
store[:debug_buffer] << "<!-- JS Packs: #{store[:sent_packs][:javascript].keys.join(', ')} -->"
|
653
|
+
store[:debug_buffer] << "<!-- CSS Packs: #{store[:sent_packs][:stylesheet].keys.join(', ')} -->"
|
654
|
+
store[:debug_buffer] << "<!-- Headers: -->"
|
655
|
+
store[:link_buffer].each do |link|
|
656
|
+
store[:debug_buffer] << "<!-- #{link} -->"
|
657
|
+
end
|
658
|
+
store[:debug_buffer] << "<!-- Note: Browsers only process the FIRST 103 response -->"
|
659
|
+
store[:debug_buffer] << "<!-- Note: Puma only supports HTTP/1.1 Early Hints (not HTTP/2) -->"
|
660
|
+
end
|
661
|
+
end
|
662
|
+
|
663
|
+
# Wrapper for JavaScript early hints
|
664
|
+
def send_javascript_early_hints_internal(config)
|
665
|
+
send_early_hints_internal(config, type: :javascript)
|
666
|
+
end
|
667
|
+
|
668
|
+
# Wrapper for stylesheet early hints
|
669
|
+
def send_stylesheet_early_hints_internal(config)
|
670
|
+
send_early_hints_internal(config, type: :stylesheet)
|
276
671
|
end
|
277
672
|
end
|
data/lib/shakapacker/railtie.rb
CHANGED
@@ -39,6 +39,10 @@ class Shakapacker::Engine < ::Rails::Engine
|
|
39
39
|
end
|
40
40
|
end
|
41
41
|
|
42
|
+
# Early hints are handled automatically by javascript_pack_tag and stylesheet_pack_tag helpers
|
43
|
+
# when early_hints.enabled: true in shakapacker.yml
|
44
|
+
# See docs/early_hints_new_api.md for usage
|
45
|
+
|
42
46
|
initializer "shakapacker.logger" do
|
43
47
|
config.after_initialize do
|
44
48
|
if ::Rails.logger.respond_to?(:tagged)
|
data/lib/shakapacker/version.rb
CHANGED
@@ -226,9 +226,8 @@ export class BuildValidator {
|
|
226
226
|
|
227
227
|
if (isHMR) {
|
228
228
|
return this.validateHMRBuild(build, appRoot, bundler)
|
229
|
-
} else {
|
230
|
-
return this.validateStaticBuild(build, appRoot, bundler)
|
231
229
|
}
|
230
|
+
return this.validateStaticBuild(build, appRoot, bundler)
|
232
231
|
}
|
233
232
|
|
234
233
|
/**
|
@@ -1285,7 +1285,8 @@ function formatConfig(
|
|
1285
1285
|
appRoot
|
1286
1286
|
})
|
1287
1287
|
return serializer.serialize(config, metadata)
|
1288
|
-
}
|
1288
|
+
}
|
1289
|
+
if (options.format === "json") {
|
1289
1290
|
const jsonReplacer = (key: string, value: any): any => {
|
1290
1291
|
if (typeof value === "function") {
|
1291
1292
|
return `[Function: ${value.name || "anonymous"}]`
|
data/package-lock.json
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
{
|
2
2
|
"name": "shakapacker",
|
3
|
-
"version": "9.3.0-beta.
|
3
|
+
"version": "9.3.0-beta.5",
|
4
4
|
"lockfileVersion": 3,
|
5
5
|
"requires": true,
|
6
6
|
"packages": {
|
7
7
|
"": {
|
8
8
|
"name": "shakapacker",
|
9
|
-
"version": "9.3.0-beta.
|
9
|
+
"version": "9.3.0-beta.5",
|
10
10
|
"license": "MIT",
|
11
11
|
"dependencies": {
|
12
12
|
"js-yaml": "^4.1.0",
|
data/package.json
CHANGED
@@ -19,6 +19,7 @@ const babelConfig = require("../../../package/rules/babel")
|
|
19
19
|
|
20
20
|
// Skip tests if babel config is not available (not the active transpiler)
|
21
21
|
if (!babelConfig) {
|
22
|
+
// eslint-disable-next-line jest/no-disabled-tests
|
22
23
|
describe.skip("babel - skipped", () => {
|
23
24
|
test.todo("skipped because babel is not the active transpiler")
|
24
25
|
})
|
@@ -19,6 +19,7 @@ const swcConfig = require("../../../package/rules/swc")
|
|
19
19
|
|
20
20
|
// Skip tests if swc config is not available (not the active transpiler)
|
21
21
|
if (!swcConfig) {
|
22
|
+
// eslint-disable-next-line jest/no-disabled-tests
|
22
23
|
describe.skip("swc - skipped", () => {
|
23
24
|
test.todo("skipped because swc is not the active transpiler")
|
24
25
|
})
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: shakapacker
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 9.3.0.beta.
|
4
|
+
version: 9.3.0.beta.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- David Heinemeier Hansson
|
@@ -180,6 +180,9 @@ files:
|
|
180
180
|
- docs/customizing_babel_config.md
|
181
181
|
- docs/deployment.md
|
182
182
|
- docs/developing_shakapacker.md
|
183
|
+
- docs/early_hints.md
|
184
|
+
- docs/early_hints_new_api.md
|
185
|
+
- docs/feature_testing.md
|
183
186
|
- docs/optional-peer-dependencies.md
|
184
187
|
- docs/peer-dependencies.md
|
185
188
|
- docs/precompile_hook.md
|
@@ -382,7 +385,7 @@ homepage: https://github.com/shakacode/shakapacker
|
|
382
385
|
licenses:
|
383
386
|
- MIT
|
384
387
|
metadata:
|
385
|
-
source_code_uri: https://github.com/shakacode/shakapacker/tree/v9.3.0.beta.
|
388
|
+
source_code_uri: https://github.com/shakacode/shakapacker/tree/v9.3.0.beta.5
|
386
389
|
rdoc_options: []
|
387
390
|
require_paths:
|
388
391
|
- lib
|