Skip to content
22 changes: 17 additions & 5 deletions app/helpers/script_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,26 @@ def render_javascript_pack_once_tags(...)
javascript_packs_tag_once(...)
return if @scripts.blank?
concat javascript_assets_tag
crossorigin = local_crossorigin_sources?.presence
@scripts.each do |name, (url_params, attributes)|
asset_sources.get_sources(name).each do |source|
concat javascript_include_tag(
UriService.add_params(source, url_params),
integrity = asset_sources.get_integrity(source)

if attributes[:preload_links_header] != false
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this means that preload_links_header: nil will add the preload header, that seems counterintuitive to me?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this means that preload_links_header: nil will add the preload header, that seems counterintuitive to me?

That would be the same behavior as the default javascript_include_tag behavior [1] [2], though technically it's configurable and respects the configured default.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shoudl we use the same presence trick?

Suggested change
if attributes[:preload_links_header] != false
if attributes[:preload_links_header].present?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that'd be the opposite behavior from what we expect, since we want :preload_links_header to be opt-out, not opt-in.

AssetPreloadLinker.append(
headers: response.headers,
as: :script,
url: source,
crossorigin:,
integrity:,
)
end

concat tag.script(
src: UriService.add_params(source, url_params),
**attributes,
crossorigin: local_crossorigin_sources? ? true : nil,
integrity: asset_sources.get_integrity(source),
nopush: false,
crossorigin:,
integrity:,
)
end
end
Expand Down
8 changes: 7 additions & 1 deletion app/helpers/stylesheet_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,13 @@ def stylesheet_tag_once(*names)
def render_stylesheet_once_tags(*names)
stylesheet_tag_once(*names) if names.present?
return if @stylesheets.blank?
safe_join(@stylesheets.map { |stylesheet| stylesheet_link_tag(stylesheet, nopush: false) })
safe_join(
@stylesheets.map do |stylesheet|
url = stylesheet_path(stylesheet)
AssetPreloadLinker.append(headers: response.headers, as: :style, url:)
tag.link(rel: :stylesheet, href: url)
end,
)
end
end
# rubocop:enable Rails/HelperInstanceVariable
12 changes: 12 additions & 0 deletions app/services/asset_preload_linker.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# frozen_string_literal: true

class AssetPreloadLinker
def self.append(headers:, as:, url:, crossorigin: false, integrity: nil)
header = +headers['link'].to_s
header << ',' if header != ''
header << "<#{url}>;rel=preload;as=#{as}"
header << ';crossorigin' if crossorigin
header << ";integrity=#{integrity}" if integrity
headers['link'] = header
end
end
16 changes: 14 additions & 2 deletions spec/helpers/script_helper_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,8 @@
render_javascript_pack_once_tags

expect(response.headers['link']).to eq(
'</application.js>; rel=preload; as=script,' \
'</document-capture.js>; rel=preload; as=script',
'</application.js>;rel=preload;as=script,' \
'</document-capture.js>;rel=preload;as=script',
Comment on lines -85 to +86
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whitespace is optional per the spec, which allows us to salvage back some of the bytesize increase these changes could incur.

link-value = "<" URI-Reference ">" *( OWS ";" OWS link-param )

https://httpwg.org/specs/rfc8288.html#header

("OWS" being defined as "optional whitespace")

)
expect(response.headers['link']).to_not include('nopush')
end
Expand All @@ -107,6 +107,18 @@
end
end

context 'with preload links header disabled' do
before do
javascript_packs_tag_once('application', preload_links_header: false)
end

it 'does not append preload header' do
render_javascript_pack_once_tags

expect(response.headers['link']).to eq('</document-capture.js>;rel=preload;as=script')
end
end

context 'with attributes' do
before do
javascript_packs_tag_once('track-errors', defer: true)
Expand Down
2 changes: 1 addition & 1 deletion spec/helpers/stylesheet_helper_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
it 'adds preload header without nopush attribute' do
render_stylesheet_once_tags

expect(response.headers['link']).to eq('</stylesheets/styles.css>; rel=preload; as=style')
expect(response.headers['link']).to eq('</stylesheets/styles.css>;rel=preload;as=style')
expect(response.headers['link']).to_not include('nopush')
end
end
Expand Down
63 changes: 63 additions & 0 deletions spec/services/asset_preload_linker_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
require 'rails_helper'

RSpec.describe AssetPreloadLinker do
describe '.append' do
let(:link) { nil }
let(:as) { 'script' }
let(:url) { '/script.js' }
let(:crossorigin) { nil }
let(:integrity) { nil }
let(:headers) { { 'link' => link } }
subject(:result) do
AssetPreloadLinker.append(**{ headers:, as:, url:, crossorigin:, integrity: }.compact)
end

context 'with absent link value' do
let(:link) { nil }

it 'returns a string with only the appended link' do
expect(result).to eq('</script.js>;rel=preload;as=script')
end
end

context 'with empty link value' do
let(:link) { '' }

it 'returns a string with only the appended link' do
expect(result).to eq('</script.js>;rel=preload;as=script')
end
end

context 'with non-empty link value' do
let(:link) { '</a.js>;rel=preload;as=script' }

it 'returns a comma-separated link value of the new and existing link' do
expect(result).to eq('</a.js>;rel=preload;as=script,</script.js>;rel=preload;as=script')
end

context 'with existing link value as frozen string' do
let(:link) { '</a.js>;rel=preload;as=script'.freeze }

it 'returns a comma-separated link value of the new and existing link' do
expect(result).to eq('</a.js>;rel=preload;as=script,</script.js>;rel=preload;as=script')
end
end
end

context 'with crossorigin option' do
let(:crossorigin) { true }

it 'includes crossorigin link param' do
expect(result).to eq('</script.js>;rel=preload;as=script;crossorigin')
end
end

context 'with integrity option' do
let(:integrity) { 'abc123' }

it 'includes integrity link param' do
expect(result).to eq('</script.js>;rel=preload;as=script;integrity=abc123')
end
end
end
end