Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add preload_link_tag helper. #31251

Merged
merged 3 commits into from
Nov 29, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions actionpack/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
* Register most popular audio/video/font mime types supported by modern browsers.

*Guillermo Iguaran*

* Fix optimized url helpers when using relative url root

Fixes #31220.
Expand Down
20 changes: 19 additions & 1 deletion actionpack/lib/action_dispatch/http/mime_types.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,33 @@
Mime::Type.register "text/calendar", :ics
Mime::Type.register "text/csv", :csv
Mime::Type.register "text/vcard", :vcf
Mime::Type.register "text/vtt", :vtt, %w(vtt)

Mime::Type.register "image/png", :png, [], %w(png)
Mime::Type.register "image/jpeg", :jpeg, [], %w(jpg jpeg jpe pjpeg)
Mime::Type.register "image/gif", :gif, [], %w(gif)
Mime::Type.register "image/bmp", :bmp, [], %w(bmp)
Mime::Type.register "image/tiff", :tiff, [], %w(tif tiff)
Mime::Type.register "image/svg+xml", :svg
Mime::Type.register "image/webp", :webp, [], %w(webp)

Mime::Type.register "video/mpeg", :mpeg, [], %w(mpg mpeg mpe)
Mime::Type.register "audio/mpeg", :mpg, [], %w(mp1 mp2 mp3)
Mime::Type.register "audio/webm", :weba, [], %w(weba)
Mime::Type.register "audio/ogg", :ogg, [], %w(oga ogg spx opus)
Mime::Type.register "audio/aac", :acc, [], %w(aac)
Mime::Type.register "audio/mp4", :mp4, [], %w(m4a mpg4)
Mime::Type.register "audio/flac", :flac, [], %w(flac)

Mime::Type.register "video/webm", :webm, [], %w(webm)
Mime::Type.register "video/mp4", :mp4, [], %w(mp4 m4v)
Copy link
Member

Choose a reason for hiding this comment

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

It seems that mp4 is still duplicated, is this intentional?
https://travis-ci.org/rails/rails/jobs/308761133#L1969

Copy link
Member Author

Choose a reason for hiding this comment

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

Missed this comment before of merge, will fix it now :/

Mime::Type.register "video/ogg", :ogv, [], %w(ogv)

Mime::Type.register "application/ogx", :ogx, [], %w(ogx)

Mime::Type.register "font/otf", :otf, [], %w(otf)
Mime::Type.register "font/ttf", :ttf, [], %w(ttf)
Mime::Type.register "font/woff", :woff, [], %w(woff)
Mime::Type.register "font/woff2", :woff2, [], %w(woff2)

Mime::Type.register "application/xml", :xml, %w( text/xml application/x-xml )
Mime::Type.register "application/rss+xml", :rss
Expand Down
2 changes: 1 addition & 1 deletion actionpack/test/controller/send_file_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ def test_send_file_headers_guess_type_from_extension
"image.jpg" => "image/jpeg",
"image.tif" => "image/tiff",
"image.gif" => "image/gif",
"movie.mpg" => "video/mpeg",
"movie.mp4" => "video/mp4",
"file.zip" => "application/zip",
"file.unk" => "application/octet-stream",
"zip" => "application/octet-stream"
Expand Down
12 changes: 6 additions & 6 deletions actionpack/test/dispatch/mime_type_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,28 +30,28 @@ class MimeTypeTest < ActiveSupport::TestCase

test "parse text with trailing star at the beginning" do
accept = "text/*, text/html, application/json, multipart/form-data"
expect = [Mime[:html], Mime[:text], Mime[:js], Mime[:css], Mime[:ics], Mime[:csv], Mime[:vcf], Mime[:xml], Mime[:yaml], Mime[:json], Mime[:multipart_form]]
expect = [Mime[:html], Mime[:text], Mime[:js], Mime[:css], Mime[:ics], Mime[:csv], Mime[:vcf], Mime[:vtt], Mime[:xml], Mime[:yaml], Mime[:json], Mime[:multipart_form]]
parsed = Mime::Type.parse(accept)
assert_equal expect, parsed
assert_equal expect.map(&:to_s), parsed.map(&:to_s)
end

test "parse text with trailing star in the end" do
accept = "text/html, application/json, multipart/form-data, text/*"
expect = [Mime[:html], Mime[:json], Mime[:multipart_form], Mime[:text], Mime[:js], Mime[:css], Mime[:ics], Mime[:csv], Mime[:vcf], Mime[:xml], Mime[:yaml]]
expect = [Mime[:html], Mime[:json], Mime[:multipart_form], Mime[:text], Mime[:js], Mime[:css], Mime[:ics], Mime[:csv], Mime[:vcf], Mime[:vtt], Mime[:xml], Mime[:yaml]]
parsed = Mime::Type.parse(accept)
assert_equal expect, parsed
assert_equal expect.map(&:to_s), parsed.map(&:to_s)
end

test "parse text with trailing star" do
accept = "text/*"
expect = [Mime[:html], Mime[:text], Mime[:js], Mime[:css], Mime[:ics], Mime[:csv], Mime[:vcf], Mime[:xml], Mime[:yaml], Mime[:json]]
expect = [Mime[:html], Mime[:text], Mime[:js], Mime[:css], Mime[:ics], Mime[:csv], Mime[:vcf], Mime[:vtt], Mime[:xml], Mime[:yaml], Mime[:json]]
parsed = Mime::Type.parse(accept)
assert_equal expect.map(&:to_s).sort!, parsed.map(&:to_s).sort!
end

test "parse application with trailing star" do
accept = "application/*"
expect = [Mime[:html], Mime[:js], Mime[:xml], Mime[:rss], Mime[:atom], Mime[:yaml], Mime[:url_encoded_form], Mime[:json], Mime[:pdf], Mime[:zip], Mime[:gzip]]
expect = [Mime[:html], Mime[:js], Mime[:xml], Mime[:rss], Mime[:atom], Mime[:yaml], Mime[:url_encoded_form], Mime[:json], Mime[:pdf], Mime[:zip], Mime[:gzip], Mime[:ogx]]
parsed = Mime::Type.parse(accept)
assert_equal expect.map(&:to_s).sort!, parsed.map(&:to_s).sort!
end
Expand Down
8 changes: 8 additions & 0 deletions actionview/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
* Add `preload_link_tag` helper

This helper that allows to the browser to initiate early fetch of resources
(different to the specified in javascript_include_tag and stylesheet_link_tag).
Additionally, this sends Early Hints if supported by browser.

*Guillermo Iguaran*

## Rails 5.2.0.beta2 (November 28, 2017) ##

* No changes.
Expand Down
75 changes: 75 additions & 0 deletions actionview/lib/action_view/helpers/asset_tag_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

require "active_support/core_ext/array/extract_options"
require "active_support/core_ext/hash/keys"
require "active_support/core_ext/object/inclusion"
require "active_support/core_ext/object/try"
require "action_view/helpers/asset_url_helper"
require "action_view/helpers/tag_helper"

Expand Down Expand Up @@ -221,6 +223,67 @@ def favicon_link_tag(source = "favicon.ico", options = {})
}.merge!(options.symbolize_keys))
end

# Returns a link tag that browsers can use to preload the +source+.
# The +source+ can be the path of an resource managed by asset pipeline,
# a full path or an URI.
#
# ==== Options
#
# * <tt>:type</tt> - Override the auto-generated mime type, defaults to the mime type for +source+ extension.
# * <tt>:as</tt> - Override the auto-generated value for as attribute, calculated using +source+ extension and mime type.
# * <tt>:crossorigin</tt> - Specify the crossorigin attribute, required to load cross-origin resources.
# * <tt>:nopush</tt> - Specify if the use of server push is not desired for the resource. Defaults to +false+.
#
# ==== Examples
#
# preload_link_tag("custom_theme.css")
# # => <link rel="preload" href="/assets/custom_theme.css" as="style" type="text/css" />
#
# preload_link_tag("/videos/video.webm")
# # => <link rel="preload" href="/videos/video.mp4" as="video" type="video/webm" />
#
# preload_link_tag(post_path(format: :json), as: "fetch")
# # => <link rel="preload" href="/posts.json" as="fetch" type="application/json" />
#
# preload_link_tag("worker.js", as: "worker")
# # => <link rel="preload" href="/assets/worker.js" as="worker" type="text/javascript" />
#
# preload_link_tag("//example.com/font.woff2")
# # => <link rel="preload" href="//example.com/font.woff2" as="font" type="font/woff2" crossorigin="anonymous"/>
#
# preload_link_tag("//example.com/font.woff2", crossorigin: "use-credentials")
# # => <link rel="preload" href="//example.com/font.woff2" as="font" type="font/woff2" crossorigin="use-credentials" />
#
# preload_link_tag("/media/audio.ogg", nopush: true)
# # => <link rel="preload" href="/media/audio.ogg" as="audio" type="audio/ogg" />
#
def preload_link_tag(source, options = {})
href = asset_path(source, skip_pipeline: options.delete(:skip_pipeline))
extname = File.extname(source).downcase.delete(".")
mime_type = options.delete(:type) || Template::Types[extname].try(:to_s)
as_type = options.delete(:as) || resolve_link_as(extname, mime_type)
crossorigin = options.delete(:crossorigin)
crossorigin = "anonymous" if crossorigin == true || (crossorigin.blank? && as_type == "font")
nopush = options.delete(:nopush) || false

link_tag = tag.link({
rel: "preload",
href: href,
as: as_type,
type: mime_type,
crossorigin: crossorigin
}.merge!(options.symbolize_keys))

early_hints_link = "<#{href}>; rel=preload; as=#{as_type}"
early_hints_link += "; type=#{mime_type}" if mime_type
early_hints_link += "; crossorigin=#{crossorigin}" if crossorigin
early_hints_link += "; nopush" if nopush

request.send_early_hints("Link" => early_hints_link) if respond_to?(:request) && request

link_tag
end

# Returns an HTML image tag for the +source+. The +source+ can be a full
# path, a file or an Active Storage attachment.
#
Expand Down Expand Up @@ -417,6 +480,18 @@ def check_for_image_tag_errors(options)
raise ArgumentError, "Cannot pass a :size option with a :height or :width option"
end
end

def resolve_link_as(extname, mime_type)
if extname == "js"
"script"
elsif extname == "css"
"style"
elsif extname == "vtt"
"track"
elsif (type = mime_type.to_s.split("/")[0]) && type.in?(%w(audio video font))
type
end
end
end
end
end
15 changes: 15 additions & 0 deletions actionview/test/template/asset_tag_helper_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,17 @@ def url_for(*args)
%(favicon_link_tag 'mb-icon.png', :rel => 'apple-touch-icon', :type => 'image/png') => %(<link href="/images/mb-icon.png" rel="apple-touch-icon" type="image/png" />)
}

PreloadLinkToTag = {
%(preload_link_tag '/styles/custom_theme.css') => %(<link rel="preload" href="/styles/custom_theme.css" as="style" type="text/css" />),
%(preload_link_tag '/videos/video.webm') => %(<link rel="preload" href="/videos/video.webm" as="video" type="video/webm" />),
%(preload_link_tag '/posts.json', as: 'fetch') => %(<link rel="preload" href="/posts.json" as="fetch" type="application/json" />),
%(preload_link_tag '/users', as: 'fetch', type: 'application/json') => %(<link rel="preload" href="/users" as="fetch" type="application/json" />),
%(preload_link_tag '//example.com/map?callback=initMap', as: 'fetch', type: 'application/javascript') => %(<link rel="preload" href="//example.com/map?callback=initMap" as="fetch" type="application/javascript" />),
%(preload_link_tag '//example.com/font.woff2') => %(<link rel="preload" href="//example.com/font.woff2" as="font" type="font/woff2" crossorigin="anonymous"/>),
%(preload_link_tag '//example.com/font.woff2', crossorigin: 'use-credentials') => %(<link rel="preload" href="//example.com/font.woff2" as="font" type="font/woff2" crossorigin="use-credentials" />),
%(preload_link_tag '/media/audio.ogg', nopush: true) => %(<link rel="preload" href="/media/audio.ogg" as="audio" type="audio/ogg" />)
}

VideoPathToTag = {
%(video_path("xml")) => %(/videos/xml),
%(video_path("xml.ogg")) => %(/videos/xml.ogg),
Expand Down Expand Up @@ -535,6 +546,10 @@ def test_favicon_link_tag
FaviconLinkToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
end

def test_preload_link_tag
PreloadLinkToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
end

def test_video_path
VideoPathToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
end
Expand Down