Skip to content
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
main
------

* Emit the scripts and stylesheets that `index.html` points outside the build (a CDN, a font service) as they are, instead of failing to resolve them against the build output
* Ignore `<script>` tags without a `src` (an inline script) when collecting the build's scripts

0.9.0
------

Expand Down
15 changes: 13 additions & 2 deletions app/helpers/ember_cli_rails_assets_helper.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
require "ember_cli/assets/errors"
require "ember_cli/assets/lookup"
require "ember_cli/assets/paths"
require "ember_cli/assets/url"

module EmberCliRailsAssetsHelper
def include_ember_script_tags(name, prepend: "")
Expand Down Expand Up @@ -39,7 +40,7 @@ def include_ember_stylesheet_tags(name, prepend: "")
assets = EmberCli::Assets::Lookup.new(app)

assets.stylesheet_assets.
map { |src| [prepend, src].join }.
map { |src| mount_asset(src, prepend) }.
map { |src| %{<link rel="stylesheet" href="#{src}">}.html_safe }.
inject(&:+)
end
Expand Down Expand Up @@ -69,11 +70,21 @@ def classic_ember_script_tags(app, prepend)
assets = EmberCli::Assets::Lookup.new(app)

assets.javascript_assets.
map { |src| [prepend, src].join }.
map { |src| mount_asset(src, prepend) }.
map { |src| %{<script src="#{src}"></script>}.html_safe }.
inject(&:+)
end

# `prepend` mounts the build's assets onto a path (or another host), so it only applies to the URLs that point into the build.
# Assets hosted elsewhere (a CDN, a font service) are already absolute.
def mount_asset(src, prepend)
if EmberCli::Assets::Url.remote?(src)
src
else
[prepend, src].join
end
end

# Vite builds boot from ES modules declared in dist/index.html, so extract
# the tags required for startup (including the config meta tag and
# stylesheets) and remap root-absolute paths onto the mount point.
Expand Down
27 changes: 19 additions & 8 deletions lib/ember_cli/assets/asset_map.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
require 'nokogiri'
require "ember_cli/assets/errors"
require "ember_cli/assets/url"

module EmberCli
module Assets
Expand All @@ -13,25 +14,35 @@ def initialize(name:, asset_map:, index_html:)
def javascripts
assert_asset_map!

Nokogiri::HTML(index_html.read).css('script').map {|s|
filename = File.basename(s['src'])
asset_matching(/#{Regexp.escape(filename)}\z/)
}
document.css('script').filter_map { |script| asset_for(script['src']) }
end

def stylesheets
assert_asset_map!

Nokogiri::HTML(index_html.read).css('link[rel="stylesheet"]').map {|s|
filename = File.basename(s['href'])
asset_matching(/#{Regexp.escape(filename)}\z/)
}
document.css('link[rel="stylesheet"]').filter_map { |link| asset_for(link['href']) }
end

private

attr_reader :name, :asset_map, :index_html

def document
@document ||= Nokogiri::HTML(index_html.read)
end

# Assets hosted outside the build (a CDN, a font service) have no entry in the asset map, so their URL is emitted untouched.
# A tag without a URL (an inline `<script>`) references no asset at all.
def asset_for(url)
if url.to_s.empty?
nil
elsif Url.remote?(url)
url
else
asset_matching(/#{Regexp.escape(File.basename(url))}\z/)
end
end

def asset_matching(regex)
matching_asset = files.detect { |asset| asset =~ regex }

Expand Down
14 changes: 14 additions & 0 deletions lib/ember_cli/assets/url.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module EmberCli
module Assets
# URLs in a built `index.html` either point into the build output or somewhere else entirely (a CDN, a font service).
# Only the former are resolved against the build and mounted onto the Rails application.
module Url
# Matches a URL that resolves outside the build: one carrying a scheme (`https://example.com/app.css`, `data:…`), and a protocol-relative one (`//example.com/app.css`).
REMOTE = %r{\A(?:[a-zA-Z][a-zA-Z0-9+.\-]*:|//)}

def self.remote?(url)
REMOTE.match?(url.to_s)
end
end
end
end
52 changes: 52 additions & 0 deletions spec/helpers/ember_cli_rails_assets_helper_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,32 @@
expect(tags).to include(%{src="//cdn.example.com/protocol-relative.js"})
end
end

context "when the application is a classic build" do
it "mounts the build's scripts onto `prepend`, leaving the ones hosted elsewhere untouched" do
app = instance_double(EmberCli::App, build: true, dev_server?: false)
paths = instance_double(EmberCli::Assets::Paths, vite?: false)
lookup = instance_double(
EmberCli::Assets::Lookup,
javascript_assets: [
"assets/vendor-abc123.js",
"https://cdn.example.com/analytics.js",
"//cdn.example.com/protocol-relative.js",
],
)
allow(EmberCli).to receive(:[]).with(:frontend).and_return(app)
allow(EmberCli::Assets::Paths).
to receive(:new).with(app).and_return(paths)
allow(EmberCli::Assets::Lookup).
to receive(:new).with(app).and_return(lookup)

tags = helper.include_ember_script_tags(:frontend, prepend: "http://example.com/")

expect(tags).to include(%{src="http://example.com/assets/vendor-abc123.js"})
expect(tags).to include(%{src="https://cdn.example.com/analytics.js"})
expect(tags).to include(%{src="//cdn.example.com/protocol-relative.js"})
end
end
end

describe "#include_ember_stylesheet_tags" do
Expand Down Expand Up @@ -74,5 +100,31 @@
)
end
end

context "when the application is a classic build" do
it "mounts the build's stylesheets onto `prepend`, leaving the ones hosted elsewhere untouched" do
app = instance_double(EmberCli::App, build: true, dev_server?: false)
paths = instance_double(EmberCli::Assets::Paths, vite?: false)
lookup = instance_double(
EmberCli::Assets::Lookup,
stylesheet_assets: [
"assets/vendor-abc123.css",
"https://fonts.example.com/css?family=Frontend",
"//fonts.example.com/protocol-relative.css",
],
)
allow(EmberCli).to receive(:[]).with(:frontend).and_return(app)
allow(EmberCli::Assets::Paths).
to receive(:new).with(app).and_return(paths)
allow(EmberCli::Assets::Lookup).
to receive(:new).with(app).and_return(lookup)

tags = helper.include_ember_stylesheet_tags(:frontend, prepend: "http://example.com/")

expect(tags).to include(%{href="http://example.com/assets/vendor-abc123.css"})
expect(tags).to include(%{href="https://fonts.example.com/css?family=Frontend"})
expect(tags).to include(%{href="//fonts.example.com/protocol-relative.css"})
end
end
end
end
76 changes: 76 additions & 0 deletions spec/lib/ember_cli/assets/asset_map_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,55 @@
])
end

it "passes scripts hosted outside the build through untouched" do
asset_map = {
"assets" => {
"bar.js" => "bar-abc123.js",
},
"prepend" => "foo/",
}
index_html = StringIO.new(<<~HTML)
<html>
<head>
<script src="https://cdn.example.com/analytics.js"></script>
<script src="//cdn.example.com/protocol-relative.js"></script>
<script src="bar-abc123.js"></script>
</head>
</html>
HTML
assets = build_assets(name: "bar", asset_map: asset_map, index_html: index_html)

javascripts = assets.javascripts

expect(javascripts).to match_array([
"https://cdn.example.com/analytics.js",
"//cdn.example.com/protocol-relative.js",
"foo/bar-abc123.js",
])
end

it "ignores scripts without a `src`" do
asset_map = {
"assets" => {
"bar.js" => "bar-abc123.js",
},
"prepend" => "foo/",
}
index_html = StringIO.new(<<~HTML)
<html>
<head>
<script>window.EmberENV = {};</script>
<script src="bar-abc123.js"></script>
</head>
</html>
HTML
assets = build_assets(name: "bar", asset_map: asset_map, index_html: index_html)

javascripts = assets.javascripts

expect(javascripts).to match_array(["foo/bar-abc123.js"])
end

context "when the asset_map is empty" do
it "raises a BuildError" do
assets = build_assets(asset_map: {}, name: "bar", index_html: StringIO.new)
Expand Down Expand Up @@ -66,6 +115,33 @@
])
end

it "passes stylesheets hosted outside the build through untouched" do
asset_map = {
"assets" => {
"bar.css" => "bar-abc123.css",
},
"prepend" => "foo/",
}
index_html = StringIO.new(<<~HTML)
<html>
<head>
<link rel="stylesheet" href="https://fonts.example.com/css?family=Bar"></link>
<link rel="stylesheet" href="//fonts.example.com/protocol-relative.css"></link>
<link rel="stylesheet" href="bar-abc123.css"></link>
</head>
</html>
HTML
assets = build_assets(name: "bar", asset_map: asset_map, index_html: index_html)

stylesheets = assets.stylesheets

expect(stylesheets).to match_array([
"https://fonts.example.com/css?family=Bar",
"//fonts.example.com/protocol-relative.css",
"foo/bar-abc123.css",
])
end

context "when the asset_map is empty" do
it "raises a BuildError" do
assets = build_assets(asset_map: {}, name: "bar", index_html: StringIO.new)
Expand Down
Loading