diff --git a/app/helpers/script_helper.rb b/app/helpers/script_helper.rb index 104e3cec3fe..ae35373dc69 100644 --- a/app/helpers/script_helper.rb +++ b/app/helpers/script_helper.rb @@ -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 + 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 diff --git a/app/helpers/stylesheet_helper.rb b/app/helpers/stylesheet_helper.rb index 2becc651754..c607bdac62c 100644 --- a/app/helpers/stylesheet_helper.rb +++ b/app/helpers/stylesheet_helper.rb @@ -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 diff --git a/app/services/asset_preload_linker.rb b/app/services/asset_preload_linker.rb new file mode 100644 index 00000000000..13885272cc7 --- /dev/null +++ b/app/services/asset_preload_linker.rb @@ -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 diff --git a/spec/helpers/script_helper_spec.rb b/spec/helpers/script_helper_spec.rb index a7b256ba506..a13e12aa403 100644 --- a/spec/helpers/script_helper_spec.rb +++ b/spec/helpers/script_helper_spec.rb @@ -82,8 +82,8 @@ render_javascript_pack_once_tags expect(response.headers['link']).to eq( - '; rel=preload; as=script,' \ - '; rel=preload; as=script', + ';rel=preload;as=script,' \ + ';rel=preload;as=script', ) expect(response.headers['link']).to_not include('nopush') end @@ -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(';rel=preload;as=script') + end + end + context 'with attributes' do before do javascript_packs_tag_once('track-errors', defer: true) diff --git a/spec/helpers/stylesheet_helper_spec.rb b/spec/helpers/stylesheet_helper_spec.rb index 9c4a9835bae..8237c2690ea 100644 --- a/spec/helpers/stylesheet_helper_spec.rb +++ b/spec/helpers/stylesheet_helper_spec.rb @@ -34,7 +34,7 @@ it 'adds preload header without nopush attribute' do render_stylesheet_once_tags - expect(response.headers['link']).to eq('; rel=preload; as=style') + expect(response.headers['link']).to eq(';rel=preload;as=style') expect(response.headers['link']).to_not include('nopush') end end diff --git a/spec/services/asset_preload_linker_spec.rb b/spec/services/asset_preload_linker_spec.rb new file mode 100644 index 00000000000..be302316c2c --- /dev/null +++ b/spec/services/asset_preload_linker_spec.rb @@ -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(';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(';rel=preload;as=script') + end + end + + context 'with non-empty link value' do + let(:link) { ';rel=preload;as=script' } + + it 'returns a comma-separated link value of the new and existing link' do + expect(result).to eq(';rel=preload;as=script,;rel=preload;as=script') + end + + context 'with existing link value as frozen string' do + let(:link) { ';rel=preload;as=script'.freeze } + + it 'returns a comma-separated link value of the new and existing link' do + expect(result).to eq(';rel=preload;as=script,;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(';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(';rel=preload;as=script;integrity=abc123') + end + end + end +end