From 29a31277a35181f6446194b267b0c94b77d131b9 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 17 Sep 2026 16:53:05 +0000 Subject: [PATCH 1/9] Own the interpretation of an application's build output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The asset helpers read a built application to work out what it boots from: where its assets live, how `index.html` refers to them, and whether the application is Vite-based. This gem runs the build, so it already knows all of it — it resolves the same paths and carries a Vite check of its own. Move that knowledge here, behind an API add-ons can render: * `EmberCli::App#vite?` * `EmberCli::App#startup_tags` * `EmberCli::App#javascript_assets` and `#stylesheet_assets` `startup_tags` chooses between the development server and the build directory itself, so callers no longer feature-detect the development server. With the knowledge here, this gem no longer needs to depend on the helpers, and the helpers can depend on it instead. Bundler refuses to resolve a dependency that points both ways, so drop it. The feature spec that exercised the helpers from here goes with it; the helpers cover that path in their own suite. Co-Authored-By: Claude Opus 5 --- ember-cli-rails.gemspec | 2 +- lib/ember_cli.rb | 1 - lib/ember_cli/app.rb | 38 ++++++++ lib/ember_cli/asset_map.rb | 75 ++++++++++++++++ lib/ember_cli/path_set.rb | 8 ++ lib/ember_cli/startup_tags.rb | 46 ++++++++++ spec/dummy/app/views/pages/embedded.html.erb | 6 -- spec/dummy/config/routes.rb | 7 -- spec/features/user_views_ember_app_spec.rb | 8 -- spec/lib/ember_cli/asset_map_spec.rb | 92 ++++++++++++++++++++ spec/lib/ember_cli/startup_tags_spec.rb | 55 ++++++++++++ 11 files changed, 315 insertions(+), 23 deletions(-) create mode 100644 lib/ember_cli/asset_map.rb create mode 100644 lib/ember_cli/startup_tags.rb delete mode 100644 spec/dummy/app/views/pages/embedded.html.erb create mode 100644 spec/lib/ember_cli/asset_map_spec.rb create mode 100644 spec/lib/ember_cli/startup_tags_spec.rb diff --git a/ember-cli-rails.gemspec b/ember-cli-rails.gemspec index 4f637db2..68d243f5 100644 --- a/ember-cli-rails.gemspec +++ b/ember-cli-rails.gemspec @@ -15,7 +15,7 @@ Gem::Specification.new do |spec| spec.required_ruby_version = ">= 2.5.0" - spec.add_dependency "ember-cli-rails-assets", ">= 0.9.0", "< 1.0" + spec.add_dependency "nokogiri", ">= 1.13" spec.add_dependency "railties", ">= 4.2" spec.add_dependency "rack", ">= 2.1", "< 4.0" spec.add_dependency "terrapin", ">= 0.6.0", "< 2.0" diff --git a/lib/ember_cli.rb b/lib/ember_cli.rb index dad100f8..f159b174 100644 --- a/lib/ember_cli.rb +++ b/lib/ember_cli.rb @@ -1,6 +1,5 @@ require "fileutils" require "active_support/deprecation" -require "ember-cli-rails-assets" require "ember_cli/engine" require "ember_cli/configuration" require "ember_cli/helpers" diff --git a/lib/ember_cli/app.rb b/lib/ember_cli/app.rb index dab8aba0..e013a7ee 100644 --- a/lib/ember_cli/app.rb +++ b/lib/ember_cli/app.rb @@ -1,7 +1,9 @@ require "json" require "html_page/renderer" +require "ember_cli/asset_map" require "ember_cli/path_set" +require "ember_cli/startup_tags" require "ember_cli/shell" require "ember_cli/build_monitor" require "ember_cli/deploy/dev_server" @@ -75,6 +77,34 @@ def index_html(head:, body:, mount_point: nil) html.render end + # Whether the application is built by Vite (`ember-cli >= 6.8`) rather than + # by the classic Broccoli-based pipeline. + def vite? + paths.vite? + end + + # The tags a Vite-based application needs to boot, as HTML strings, with + # `prepend` joined onto their root-relative URLs. + # When the application is served by the development server, the tags are + # read from the server and address it instead. + def startup_tags(prepend: "") + if dev_server? + StartupTags.new(dev_server.index_html, prefix: dev_server.origin).to_a + else + StartupTags.new(paths.index_html.read, prefix: prepend).to_a + end + end + + # The paths of the assets a classic build boots from, relative to the + # directory the application is served from. + def javascript_assets + asset_map.javascripts + end + + def stylesheet_assets + asset_map.stylesheets + end + def install_dependencies @shell.install end @@ -144,6 +174,14 @@ def dev_server? private + def asset_map + AssetMap.new( + name: name, + index_html: paths.index_html, + assets_path: paths.assets, + ) + end + def package_json_value(*keys) package_json = paths.package_json diff --git a/lib/ember_cli/asset_map.rb b/lib/ember_cli/asset_map.rb new file mode 100644 index 00000000..2a576aa9 --- /dev/null +++ b/lib/ember_cli/asset_map.rb @@ -0,0 +1,75 @@ +require "nokogiri" + +require "ember_cli/errors" + +module EmberCli + # The assets a classic (Broccoli-based) build boots from. + # + # The generated `index.html` refers to its assets by their fingerprinted file + # names, so resolve every reference against the files `ember build` wrote to + # the `assets` directory. + class AssetMap + # `broccoli-asset-rev` fingerprints the assets into a directory of their + # own, which is served alongside `index.html`. + PREPEND = "assets/".freeze + + def initialize(name:, index_html:, assets_path:) + @name = name + @index_html = index_html + @assets_path = assets_path + end + + def javascripts + assets_referenced_by("script", "src") + end + + def stylesheets + assets_referenced_by(%{link[rel="stylesheet"]}, "href") + end + + private + + attr_reader :name, :index_html, :assets_path + + def assets_referenced_by(selector, attribute) + assert_built! + + document.css(selector).map do |tag| + asset_matching(File.basename(tag[attribute].to_s)) + end + end + + def document + Nokogiri::HTML(index_html.read) + end + + def asset_matching(file_name) + pattern = /#{Regexp.escape(file_name)}\z/ + asset = file_names.detect { |candidate| candidate =~ pattern } + + if asset.nil? + fail BuildError, "Failed to find a built asset matching `#{file_name}`" + end + + PREPEND + asset + end + + def file_names + @file_names ||= if assets_path.directory? + assets_path.children.map { |path| path.basename.to_s } + else + [] + end + end + + def assert_built! + if file_names.empty? + fail BuildError, <<~MSG + Missing built assets for #{name.inspect} in `#{assets_path}`. + + Build the application before rendering its assets. + MSG + end + end + end +end diff --git a/lib/ember_cli/path_set.rb b/lib/ember_cli/path_set.rb index 06386afd..5f29895d 100644 --- a/lib/ember_cli/path_set.rb +++ b/lib/ember_cli/path_set.rb @@ -51,6 +51,14 @@ def dist @dist ||= ember_cli_root.join("apps", app_name).tap(&:mkpath) end + def assets + dist.join("assets") + end + + def index_html + dist.join("index.html") + end + def gemfile @gemfile ||= root.join("Gemfile") end diff --git a/lib/ember_cli/startup_tags.rb b/lib/ember_cli/startup_tags.rb new file mode 100644 index 00000000..55aba5db --- /dev/null +++ b/lib/ember_cli/startup_tags.rb @@ -0,0 +1,46 @@ +require "nokogiri" + +module EmberCli + # The tags a Vite-based application (`ember-cli >= 6.8`) needs to boot. + # + # Vite builds declare their entry points in `index.html` — the configuration + # `` tag, the stylesheet and `modulepreload` links, and the ES module + # scripts — so extract them from the document, with `prefix` joined onto + # every root-relative URL. + class StartupTags + SELECTOR = [ + %{meta[name$="/config/environment"]}, + %{link[rel="stylesheet"]}, + %{link[rel="modulepreload"]}, + "script", + ].join(", ").freeze + + URL_ATTRIBUTES = %w(href src).freeze + + def initialize(html, prefix: "") + @html = html + @prefix = prefix.to_s.chomp("/") + end + + def to_a + Nokogiri::HTML5(html).css(SELECTOR).map { |tag| prefix_urls(tag).to_html } + end + + private + + attr_reader :html, :prefix + + # Protocol-relative URLs (`//`) address another origin, so leave them alone. + def prefix_urls(tag) + URL_ATTRIBUTES.each do |attribute| + value = tag[attribute] + + if value&.start_with?("/") && !value.start_with?("//") + tag[attribute] = "#{prefix}#{value}" + end + end + + tag + end + end +end diff --git a/spec/dummy/app/views/pages/embedded.html.erb b/spec/dummy/app/views/pages/embedded.html.erb deleted file mode 100644 index 93cf0d74..00000000 --- a/spec/dummy/app/views/pages/embedded.html.erb +++ /dev/null @@ -1,6 +0,0 @@ -<%= include_ember_script_tags "my-app" %> -<%# For Vite-based apps, `include_ember_script_tags` already emits the - stylesheet tags, and `include_ember_stylesheet_tags` is classic-only. %> -<% unless EmberCli["my-app"].paths.vite? %> - <%= include_ember_stylesheet_tags "my-app" %> -<% end %> diff --git a/spec/dummy/config/routes.rb b/spec/dummy/config/routes.rb index e409b754..35aed829 100644 --- a/spec/dummy/config/routes.rb +++ b/spec/dummy/config/routes.rb @@ -20,13 +20,6 @@ id: "include_index_head_and_body", as: "include_index_head_and_body", ) - - app.mount_ember_app( - "my-app", - to: "/asset-helpers", - id: "embedded", - as: "embedded", - ) end mount_ember_app( diff --git a/spec/features/user_views_ember_app_spec.rb b/spec/features/user_views_ember_app_spec.rb index 4eedd15b..7b81789e 100644 --- a/spec/features/user_views_ember_app_spec.rb +++ b/spec/features/user_views_ember_app_spec.rb @@ -8,14 +8,6 @@ end context "using custom controller" do - scenario "rendering with asset helpers" do - visit embedded_path - - expect(page).to have_client_side_asset - expect(page).to have_javascript_rendered_text - expect(page).to have_no_csrf_tags - end - scenario "rendering with index helper" do visit include_index_path diff --git a/spec/lib/ember_cli/asset_map_spec.rb b/spec/lib/ember_cli/asset_map_spec.rb new file mode 100644 index 00000000..e8082c6e --- /dev/null +++ b/spec/lib/ember_cli/asset_map_spec.rb @@ -0,0 +1,92 @@ +require "fileutils" +require "pathname" +require "tmpdir" +require "ember_cli/asset_map" + +describe EmberCli::AssetMap do + describe "#javascripts" do + it "resolves the scripts of `index.html` against the built assets" do + assets_path = build_assets_path("bar-abc123.js", "vendor-abc123.js", "not-a-match") + index_html = build_index_html(<<~HTML) + + + HTML + asset_map = build_asset_map(index_html: index_html, assets_path: assets_path) + + javascripts = asset_map.javascripts + + expect(javascripts).to match_array([ + "assets/bar-abc123.js", + "assets/vendor-abc123.js", + ]) + end + + it "raises a BuildError when the application has not been built" do + asset_map = build_asset_map(assets_path: build_assets_path) + + expect { asset_map.javascripts }. + to raise_error(EmberCli::BuildError, /my-app/) + end + + it "raises a BuildError when a referenced asset is missing" do + assets_path = build_assets_path("vendor-abc123.js") + index_html = build_index_html(%{}) + asset_map = build_asset_map(index_html: index_html, assets_path: assets_path) + + expect { asset_map.javascripts }. + to raise_error(EmberCli::BuildError, /bar-abc123\.js/) + end + end + + describe "#stylesheets" do + it "resolves the stylesheets of `index.html` against the built assets" do + assets_path = build_assets_path("bar-abc123.css", "vendor-abc123.css") + index_html = build_index_html(<<~HTML) + + + HTML + asset_map = build_asset_map(index_html: index_html, assets_path: assets_path) + + stylesheets = asset_map.stylesheets + + expect(stylesheets).to match_array([ + "assets/bar-abc123.css", + "assets/vendor-abc123.css", + ]) + end + end + + def build_asset_map(index_html: build_index_html(""), assets_path:) + EmberCli::AssetMap.new( + name: "my-app", + index_html: index_html, + assets_path: assets_path, + ) + end + + def build_index_html(head) + path = dist.join("index.html") + + path.write(<<~HTML) + + + #{head} + + + HTML + + path + end + + def build_assets_path(*file_names) + dist.join("assets").tap do |assets| + assets.mkpath + + file_names.each { |file_name| FileUtils.touch(assets.join(file_name)) } + end + end + + def dist + @dist ||= Pathname.new(Dir.mktmpdir) + end +end diff --git a/spec/lib/ember_cli/startup_tags_spec.rb b/spec/lib/ember_cli/startup_tags_spec.rb new file mode 100644 index 00000000..f8a8526a --- /dev/null +++ b/spec/lib/ember_cli/startup_tags_spec.rb @@ -0,0 +1,55 @@ +require "ember_cli/startup_tags" + +describe EmberCli::StartupTags do + it "extracts the tags the application boots from" do + tags = EmberCli::StartupTags.new(<<~HTML).to_a + + + + + + + + + + + HTML + + expect(tags).to match_array([ + %{}, + %{}, + %{}, + %{}, + ]) + end + + it "joins the prefix onto root-relative URLs" do + tags = EmberCli::StartupTags.new(<<~HTML, prefix: "http://127.0.0.1:4200/").to_a + + + + + + + HTML + + expect(tags).to include(%{}) + expect(tags).to include(%{}) + end + + it "leaves absolute and protocol-relative URLs alone" do + tags = EmberCli::StartupTags.new(<<~HTML, prefix: "/admin").to_a + + + + + + + HTML + + expect(tags).to match_array([ + %{}, + %{}, + ]) + end +end From b5c485ea5d04a88541c0fbb5be68e5b7a9fa60f7 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 17 Sep 2026 17:02:59 +0000 Subject: [PATCH 2/9] Document what the split asks of applications The asset helpers no longer arrive with this gem, so an application that renders them has to ask for them. Say so where an upgrading application looks, and correct the README, which still claimed this gem injects the assets itself. Co-Authored-By: Claude Opus 5 --- README.md | 10 ++++++++-- UPGRADING.md | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f460fd47..cf66c16d 100644 --- a/README.md +++ b/README.md @@ -568,8 +568,14 @@ end Rendering EmberCLI applications with `render_ember_app` is the recommended, actively supported method of serving EmberCLI applications. -However, for the sake of backwards compatibility, `ember-cli-rails` supports -injecting the EmberCLI-generated assets into an existing Rails layout. +However, for the sake of backwards compatibility, the EmberCLI-generated assets +can be injected into an existing Rails layout. +The helpers that do that ship in [ember-cli-rails-assets], so add it to the +`Gemfile` alongside this gem: + +```ruby +gem "ember-cli-rails-assets" +``` **Note:** for Vite-based applications (generated with `ember-cli >= 6.8`), use `include_ember_script_tags` on its own. It emits everything the diff --git a/UPGRADING.md b/UPGRADING.md index d8931cf1..62351bac 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -1,3 +1,41 @@ +# Asset helpers + +From `ember-cli-rails@1.0.0` and on, the asset helpers are no longer installed +alongside this gem. + +`ember-cli-rails` interprets a built Ember application and serves it; +`ember-cli-rails-assets` renders what it reports into an existing Rails layout. +Those are separate jobs, and this gem no longer depends on the other to do them, +so an application that renders `include_ember_script_tags` or +`include_ember_stylesheet_tags` has to ask for the helpers itself: + +```ruby +# Gemfile + +gem "ember-cli-rails" +gem "ember-cli-rails-assets" +``` + +Without that line the helpers are undefined, and rendering a layout that calls +them raises `NoMethodError`. + +An application that serves its Ember applications with `render_ember_app`, or +mounts them with `mount_ember_app`, needs no change. +That is the recommended way to serve an EmberCLI application, and it has never +involved the helpers. + +The helpers used to work out for themselves what a built application boots from. +`ember-cli-rails` reports that now, so code that called into their internals +calls this gem instead: + +* `EmberCli::Assets::Paths#vite?` is `EmberCli::App#vite?` +* `EmberCli::Assets::Lookup#javascript_assets` and `#stylesheet_assets` are + `EmberCli::App#javascript_assets` and `#stylesheet_assets` +* `EmberCli::Assets::AssetMap` and `EmberCli::Assets::DirectoryAssetMap` are + `EmberCli::AssetMap`, which the readers above go through +* `EmberCli::Assets::BuildError` is `EmberCli::BuildError`, so rescue that + instead + # EmberCLI support `ember-cli >= 6.8` generates applications that are built with [Vite] instead From 2e5c502ed6164e4032088f3b6247a1144a69b63f Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 17 Sep 2026 17:19:08 +0000 Subject: [PATCH 3/9] Name the failure the missing helpers actually produce Rendering a layout that calls a missing helper fails with `ActionView::Template::Error`, raised from the `NoMethodError` behind it. Nothing else fails, so say that too. Co-Authored-By: Claude Opus 5 --- UPGRADING.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/UPGRADING.md b/UPGRADING.md index 62351bac..b2c546f3 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -17,7 +17,8 @@ gem "ember-cli-rails-assets" ``` Without that line the helpers are undefined, and rendering a layout that calls -them raises `NoMethodError`. +one fails with the `ActionView::Template::Error` raised from its `NoMethodError`. +Everything else keeps working, so only the layouts that call the helpers break. An application that serves its Ember applications with `render_ember_app`, or mounts them with `mount_ember_app`, needs no change. From bffcadc755854717525df0dd1f44f4f59ba6fa98 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 18 Sep 2026 00:47:25 +0000 Subject: [PATCH 4/9] Keep assets hosted outside the build out of the lookup A built `index.html` may point at assets the build does not produce, such as a stylesheet served by a font service or a script served by a CDN. They have no file to resolve against, and an inline `}) + asset_map = build_asset_map(index_html: index_html, assets_path: assets_path) + + javascripts = asset_map.javascripts(prepend: "http://example.com/") + + expect(javascripts).to eq(["http://example.com/assets/bar-abc123.js"]) + end + + it "emits the scripts hosted outside the build as they are" do + assets_path = build_assets_path("bar-abc123.js") + index_html = build_index_html(<<~HTML) + + + + HTML + asset_map = build_asset_map(index_html: index_html, assets_path: assets_path) + + javascripts = asset_map.javascripts(prepend: "http://example.com/") + + expect(javascripts).to match_array([ + "http://example.com/assets/bar-abc123.js", + "https://cdn.example.com/analytics.js", + "//cdn.example.com/protocol-relative.js", + ]) + end + + it "ignores a script that references no asset" do + assets_path = build_assets_path("bar-abc123.js") + index_html = build_index_html(<<~HTML) + + + HTML + asset_map = build_asset_map(index_html: index_html, assets_path: assets_path) + + javascripts = asset_map.javascripts + + expect(javascripts).to eq(["assets/bar-abc123.js"]) + end + it "raises a BuildError when a referenced asset is missing" do assets_path = build_assets_path("vendor-abc123.js") index_html = build_index_html(%{}) @@ -54,6 +95,24 @@ "assets/vendor-abc123.css", ]) end + + it "emits the stylesheets hosted outside the build as they are" do + assets_path = build_assets_path("bar-abc123.css") + index_html = build_index_html(<<~HTML) + + + + HTML + asset_map = build_asset_map(index_html: index_html, assets_path: assets_path) + + stylesheets = asset_map.stylesheets(prepend: "http://example.com/") + + expect(stylesheets).to match_array([ + "http://example.com/assets/bar-abc123.css", + "https://fonts.example.com/css?family=Frontend", + "//fonts.example.com/protocol-relative.css", + ]) + end end def build_asset_map(index_html: build_index_html(""), assets_path:) diff --git a/spec/lib/ember_cli/url_spec.rb b/spec/lib/ember_cli/url_spec.rb new file mode 100644 index 00000000..c7e01e3d --- /dev/null +++ b/spec/lib/ember_cli/url_spec.rb @@ -0,0 +1,23 @@ +require "ember_cli/url" + +describe EmberCli::Url do + describe ".remote?" do + it "is true for a URL carrying a scheme" do + expect(EmberCli::Url).to be_remote("https://cdn.example.com/app.js") + expect(EmberCli::Url).to be_remote("data:text/css,body{}") + end + + it "is true for a protocol-relative URL" do + expect(EmberCli::Url).to be_remote("//cdn.example.com/app.js") + end + + it "is false for a URL that points into the build" do + expect(EmberCli::Url).not_to be_remote("/assets/app.js") + expect(EmberCli::Url).not_to be_remote("assets/app.js") + end + + it "is false for no URL at all" do + expect(EmberCli::Url).not_to be_remote(nil) + end + end +end From aa5ce3f84f823c01c6d6b1ce67db26959e4fd31a Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 18 Sep 2026 01:21:01 +0000 Subject: [PATCH 5/9] Find the assets nested in the build output `ember build` writes what an addon ships in a subdirectory of `assets` into that subdirectory, and `index.html` refers to it by that path. Listing the `assets` directory one level deep represented such a file by the subdirectory that holds it, and matching a reference by its file name alone resolved it to whichever file happened to share that name. Represent every file by its path within `assets`, and resolve a reference by the longest trailing path the document and the build output agree on. A reference that carries the `assets` directory, or the application's `rootURL`, still resolves. This carries what the asset helpers gained in tricknotes/ember-cli-rails-assets#45 into the code that now interprets the build. Co-Authored-By: Claude Opus 5 --- lib/ember_cli/asset_map.rb | 38 ++++++++++----- spec/lib/ember_cli/asset_map_spec.rb | 69 +++++++++++++++++++++++++++- 2 files changed, 94 insertions(+), 13 deletions(-) diff --git a/lib/ember_cli/asset_map.rb b/lib/ember_cli/asset_map.rb index f3de1851..e7a551ed 100644 --- a/lib/ember_cli/asset_map.rb +++ b/lib/ember_cli/asset_map.rb @@ -6,8 +6,8 @@ module EmberCli # The assets a classic (Broccoli-based) build boots from. # - # The generated `index.html` refers to the assets it produced by their - # fingerprinted file names, so resolve every such reference against the files + # The generated `index.html` refers to the assets it produced by their path + # within the build, so resolve every such reference against the files # `ember build` wrote to the `assets` directory, and mount it onto `prepend`. # It may also point at assets the build does not produce, which are emitted # as they are. @@ -56,31 +56,45 @@ def asset_for(url, prepend) elsif Url.remote?(url) url else - [prepend, asset_matching(File.basename(url))].join + [prepend, asset_matching(url)].join end end - def asset_matching(file_name) - pattern = /#{Regexp.escape(file_name)}\z/ - asset = file_names.detect { |candidate| candidate =~ pattern } + # A reference carries directories the build output does not, such as the + # `assets` directory itself or the application's `rootURL`, so resolve it by + # the longest trailing path the document and the build output agree on. + # Matching on the file name alone would resolve an asset an addon ships in a + # subdirectory to whichever file happens to share its basename. + def asset_matching(url) + asset = path_suffixes(url).find { |suffix| file_paths.include?(suffix) } - if asset.nil? - fail BuildError, "Failed to find a built asset matching `#{file_name}`" + unless asset + fail BuildError, "Failed to find a built asset matching `#{url}`" end PREPEND + asset end - def file_names - @file_names ||= if assets_path.directory? - assets_path.children.map { |path| path.basename.to_s } + def path_suffixes(url) + segments = url.split("/").reject(&:empty?) + + segments.each_index.map { |index| segments[index..].join("/") } + end + + # Every file in the `assets` directory, by its path within it, so that the + # assets nested in it are represented too. + def file_paths + @file_paths ||= if assets_path.directory? + assets_path.glob("**/*", File::FNM_DOTMATCH). + select(&:file?). + map { |path| path.relative_path_from(assets_path).to_s } else [] end end def assert_built! - if file_names.empty? + if file_paths.empty? fail BuildError, <<~MSG Missing built assets for #{name.inspect} in `#{assets_path}`. diff --git a/spec/lib/ember_cli/asset_map_spec.rb b/spec/lib/ember_cli/asset_map_spec.rb index 8f3ce037..e87dcef0 100644 --- a/spec/lib/ember_cli/asset_map_spec.rb +++ b/spec/lib/ember_cli/asset_map_spec.rb @@ -77,6 +77,18 @@ expect { asset_map.javascripts }. to raise_error(EmberCli::BuildError, /bar-abc123\.js/) end + + it "resolves a script nested in the build" do + assets_path = build_assets_path("highlight/js/highlight.min.js") + index_html = build_index_html( + %{}, + ) + asset_map = build_asset_map(index_html: index_html, assets_path: assets_path) + + javascripts = asset_map.javascripts + + expect(javascripts).to eq(["assets/highlight/js/highlight.min.js"]) + end end describe "#stylesheets" do @@ -96,6 +108,56 @@ ]) end + it "resolves the stylesheets nested in the build" do + assets_path = build_assets_path( + "bar-abc123.css", + "font-awesome/css/font-awesome.min.css", + ) + index_html = build_index_html(<<~HTML) + + + HTML + asset_map = build_asset_map(index_html: index_html, assets_path: assets_path) + + stylesheets = asset_map.stylesheets + + expect(stylesheets).to match_array([ + "assets/font-awesome/css/font-awesome.min.css", + "assets/bar-abc123.css", + ]) + end + + it "resolves the stylesheets referenced through a `rootURL`" do + assets_path = build_assets_path( + "bar-abc123.css", + "font-awesome/css/font-awesome.min.css", + ) + index_html = build_index_html(<<~HTML) + + + HTML + asset_map = build_asset_map(index_html: index_html, assets_path: assets_path) + + stylesheets = asset_map.stylesheets + + expect(stylesheets).to match_array([ + "assets/font-awesome/css/font-awesome.min.css", + "assets/bar-abc123.css", + ]) + end + + it "prefers the asset whose path matches over one that only shares its basename" do + assets_path = build_assets_path("app.css", "font-awesome/css/app.css") + index_html = build_index_html( + %{}, + ) + asset_map = build_asset_map(index_html: index_html, assets_path: assets_path) + + stylesheets = asset_map.stylesheets + + expect(stylesheets).to eq(["assets/font-awesome/css/app.css"]) + end + it "emits the stylesheets hosted outside the build as they are" do assets_path = build_assets_path("bar-abc123.css") index_html = build_index_html(<<~HTML) @@ -141,7 +203,12 @@ def build_assets_path(*file_names) dist.join("assets").tap do |assets| assets.mkpath - file_names.each { |file_name| FileUtils.touch(assets.join(file_name)) } + file_names.each do |file_name| + path = assets.join(file_name) + + path.dirname.mkpath + FileUtils.touch(path) + end end end From 844d7a470e0837e8a3de069357e1474a0cea29e3 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 18 Sep 2026 01:58:08 +0000 Subject: [PATCH 6/9] Drop the comments that restate the code What stays is the matching rule the code cannot show on its own, the URLs the `REMOTE` pattern covers, and why a prefix applies to root-relative URLs only. Co-Authored-By: Claude Opus 5 --- lib/ember_cli/app.rb | 10 ---------- lib/ember_cli/asset_map.rb | 22 +++------------------- lib/ember_cli/startup_tags.rb | 10 ++-------- lib/ember_cli/url.rb | 9 ++------- 4 files changed, 7 insertions(+), 44 deletions(-) diff --git a/lib/ember_cli/app.rb b/lib/ember_cli/app.rb index 54754009..4d1a424c 100644 --- a/lib/ember_cli/app.rb +++ b/lib/ember_cli/app.rb @@ -77,16 +77,10 @@ def index_html(head:, body:, mount_point: nil) html.render end - # Whether the application is built by Vite (`ember-cli >= 6.8`) rather than - # by the classic Broccoli-based pipeline. def vite? paths.vite? end - # The tags a Vite-based application needs to boot, as HTML strings, with - # `prepend` joined onto their root-relative URLs. - # When the application is served by the development server, the tags are - # read from the server and address it instead. def startup_tags(prepend: "") if dev_server? StartupTags.new(dev_server.index_html, prefix: dev_server.origin).to_a @@ -95,10 +89,6 @@ def startup_tags(prepend: "") end end - # The URLs of the assets a classic build boots from, with `prepend` joined - # onto the ones that point into the build. - # An asset the build does not produce, such as one served by a CDN, is - # reported as the document refers to it. def javascript_assets(prepend: "") asset_map.javascripts(prepend: prepend) end diff --git a/lib/ember_cli/asset_map.rb b/lib/ember_cli/asset_map.rb index e7a551ed..2dfa9478 100644 --- a/lib/ember_cli/asset_map.rb +++ b/lib/ember_cli/asset_map.rb @@ -4,16 +4,7 @@ require "ember_cli/url" module EmberCli - # The assets a classic (Broccoli-based) build boots from. - # - # The generated `index.html` refers to the assets it produced by their path - # within the build, so resolve every such reference against the files - # `ember build` wrote to the `assets` directory, and mount it onto `prepend`. - # It may also point at assets the build does not produce, which are emitted - # as they are. class AssetMap - # `broccoli-asset-rev` fingerprints the assets into a directory of their - # own, which is served alongside `index.html`. PREPEND = "assets/".freeze def initialize(name:, index_html:, assets_path:) @@ -46,10 +37,6 @@ def document @document ||= Nokogiri::HTML(index_html.read) end - # An asset hosted outside the build (a CDN, a font service) has no file to - # resolve against, and `prepend` mounts the build output, so its URL is - # emitted untouched. - # A tag carrying no URL at all (an inline ` + <% end %> + <% app.stylesheet_assets.each do |href| %> + + <% end %> +<% end %> diff --git a/spec/dummy/config/routes.rb b/spec/dummy/config/routes.rb index 35aed829..6b0b5a2e 100644 --- a/spec/dummy/config/routes.rb +++ b/spec/dummy/config/routes.rb @@ -20,6 +20,13 @@ id: "include_index_head_and_body", as: "include_index_head_and_body", ) + + app.mount_ember_app( + "my-app", + to: "/embed", + id: "embedded", + as: "embedded", + ) end mount_ember_app( diff --git a/spec/features/user_views_ember_app_spec.rb b/spec/features/user_views_ember_app_spec.rb index 7b81789e..25b03ec1 100644 --- a/spec/features/user_views_ember_app_spec.rb +++ b/spec/features/user_views_ember_app_spec.rb @@ -8,6 +8,14 @@ end context "using custom controller" do + scenario "embedding the assets the application reports into a page" do + visit embedded_path + + expect(page).to have_client_side_asset + expect(page).to have_javascript_rendered_text + expect(page).to have_no_csrf_tags + end + scenario "rendering with index helper" do visit include_index_path From dff93f5e59e6633c7ca19506836095f9b78a2474 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 26 Sep 2026 05:42:28 +0000 Subject: [PATCH 9/9] Gather the reader API under EmberCli::Embedding `EmberCli::App` gained `startup_tags`, `javascript_assets` and `stylesheet_assets` only for code that embeds an application into a page of its own. Those readers now live on `EmberCli::Embedding`, which wraps an application, so `EmberCli::App` keeps only `vite?` on top of what it already had. `EmberCli::Embedding#startup_tags?` answers whether the application boots from startup tags, whether Vite's development server or a Vite build serves it, so callers no longer combine `dev_server?` and `vite?` themselves. `AssetMap`, `StartupTags` and `Url` move under the new class as private constants, leaving one class and four methods to keep compatible. Their specs now go through `EmberCli::Embedding`. `PathSet#assets` and `#index_html` go away too, as the build directory is reachable from `EmberCli::App#dist_path`. Co-Authored-By: Claude Opus 5.5 --- UPGRADING.md | 8 +- lib/ember_cli.rb | 1 + lib/ember_cli/app.rb | 26 -- lib/ember_cli/asset_map.rb | 90 ----- lib/ember_cli/embedding.rb | 49 +++ lib/ember_cli/embedding/asset_map.rb | 92 ++++++ lib/ember_cli/embedding/startup_tags.rb | 45 +++ lib/ember_cli/embedding/url.rb | 13 + lib/ember_cli/path_set.rb | 8 - lib/ember_cli/startup_tags.rb | 43 --- lib/ember_cli/url.rb | 11 - spec/dummy/app/views/pages/embedded.html.erb | 9 +- spec/lib/ember_cli/asset_map_spec.rb | 218 ------------- spec/lib/ember_cli/embedding_spec.rb | 325 +++++++++++++++++++ spec/lib/ember_cli/startup_tags_spec.rb | 55 ---- spec/lib/ember_cli/url_spec.rb | 23 -- 16 files changed, 534 insertions(+), 482 deletions(-) delete mode 100644 lib/ember_cli/asset_map.rb create mode 100644 lib/ember_cli/embedding.rb create mode 100644 lib/ember_cli/embedding/asset_map.rb create mode 100644 lib/ember_cli/embedding/startup_tags.rb create mode 100644 lib/ember_cli/embedding/url.rb delete mode 100644 lib/ember_cli/startup_tags.rb delete mode 100644 lib/ember_cli/url.rb delete mode 100644 spec/lib/ember_cli/asset_map_spec.rb create mode 100644 spec/lib/ember_cli/embedding_spec.rb delete mode 100644 spec/lib/ember_cli/startup_tags_spec.rb delete mode 100644 spec/lib/ember_cli/url_spec.rb diff --git a/UPGRADING.md b/UPGRADING.md index e173fba7..233b6c8c 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -19,12 +19,12 @@ An application that serves its Ember applications with `render_ember_app`, or mo That is the recommended way to serve an EmberCLI application, and it has never involved the helpers. The helpers used to work out for themselves what a built application boots from. -`ember-cli-rails` reports that now, so code that called into their internals calls this gem instead: +`ember-cli-rails` reports that now, through `EmberCli::Embedding`, which wraps an application (`EmberCli::Embedding.new(EmberCli["frontend"])`) to embed it into a page of your own. +Code that called into the helpers' internals calls this gem instead: * `EmberCli::Assets::Paths#vite?` is `EmberCli::App#vite?` -* `EmberCli::Assets::Lookup#javascript_assets` and `#stylesheet_assets` are `EmberCli::App#javascript_assets` and `#stylesheet_assets`, which take the `prepend:` the helpers used to join on themselves, and join it only onto the assets that point into the build -* `EmberCli::Assets::AssetMap` and `EmberCli::Assets::DirectoryAssetMap` are `EmberCli::AssetMap`, which the readers above go through -* `EmberCli::Assets::Url` is `EmberCli::Url` +* `EmberCli::Assets::Lookup#javascript_assets` and `#stylesheet_assets` are `EmberCli::Embedding#javascript_assets` and `#stylesheet_assets`, which take the `prepend:` the helpers used to join on themselves, and join it only onto the assets that point into the build +* `EmberCli::Assets::AssetMap`, `EmberCli::Assets::DirectoryAssetMap` and `EmberCli::Assets::Url` have no replacement: `EmberCli::Embedding` reads the build with classes it keeps to itself * `EmberCli::Assets::BuildError` is `EmberCli::BuildError`, so rescue that instead # EmberCLI support diff --git a/lib/ember_cli.rb b/lib/ember_cli.rb index f159b174..d2f8d2aa 100644 --- a/lib/ember_cli.rb +++ b/lib/ember_cli.rb @@ -3,6 +3,7 @@ require "ember_cli/engine" require "ember_cli/configuration" require "ember_cli/helpers" +require "ember_cli/embedding" require "ember_cli/errors" module EmberCli diff --git a/lib/ember_cli/app.rb b/lib/ember_cli/app.rb index 4d1a424c..a81b744c 100644 --- a/lib/ember_cli/app.rb +++ b/lib/ember_cli/app.rb @@ -1,9 +1,7 @@ require "json" require "html_page/renderer" -require "ember_cli/asset_map" require "ember_cli/path_set" -require "ember_cli/startup_tags" require "ember_cli/shell" require "ember_cli/build_monitor" require "ember_cli/deploy/dev_server" @@ -81,22 +79,6 @@ def vite? paths.vite? end - def startup_tags(prepend: "") - if dev_server? - StartupTags.new(dev_server.index_html, prefix: dev_server.origin).to_a - else - StartupTags.new(paths.index_html.read, prefix: prepend).to_a - end - end - - def javascript_assets(prepend: "") - asset_map.javascripts(prepend: prepend) - end - - def stylesheet_assets(prepend: "") - asset_map.stylesheets(prepend: prepend) - end - def install_dependencies @shell.install end @@ -166,14 +148,6 @@ def dev_server? private - def asset_map - AssetMap.new( - name: name, - index_html: paths.index_html, - assets_path: paths.assets, - ) - end - def package_json_value(*keys) package_json = paths.package_json diff --git a/lib/ember_cli/asset_map.rb b/lib/ember_cli/asset_map.rb deleted file mode 100644 index 2dfa9478..00000000 --- a/lib/ember_cli/asset_map.rb +++ /dev/null @@ -1,90 +0,0 @@ -require "nokogiri" - -require "ember_cli/errors" -require "ember_cli/url" - -module EmberCli - class AssetMap - PREPEND = "assets/".freeze - - def initialize(name:, index_html:, assets_path:) - @name = name - @index_html = index_html - @assets_path = assets_path - end - - def javascripts(prepend: "") - assets_referenced_by("script", "src", prepend) - end - - def stylesheets(prepend: "") - assets_referenced_by(%{link[rel="stylesheet"]}, "href", prepend) - end - - private - - attr_reader :name, :index_html, :assets_path - - def assets_referenced_by(selector, attribute, prepend) - assert_built! - - document.css(selector).filter_map do |tag| - asset_for(tag[attribute], prepend) - end - end - - def document - @document ||= Nokogiri::HTML(index_html.read) - end - - def asset_for(url, prepend) - if url.to_s.empty? - nil - elsif Url.remote?(url) - url - else - [prepend, asset_matching(url)].join - end - end - - # A reference carries directories the build output does not, such as the - # `assets` directory itself or the application's `rootURL`, so match on the - # longest trailing path the two agree on rather than on the file name, which - # an addon's nested asset can share with another file. - def asset_matching(url) - asset = path_suffixes(url).find { |suffix| file_paths.include?(suffix) } - - unless asset - fail BuildError, "Failed to find a built asset matching `#{url}`" - end - - PREPEND + asset - end - - def path_suffixes(url) - segments = url.split("/").reject(&:empty?) - - segments.each_index.map { |index| segments[index..].join("/") } - end - - def file_paths - @file_paths ||= if assets_path.directory? - assets_path.glob("**/*", File::FNM_DOTMATCH). - select(&:file?). - map { |path| path.relative_path_from(assets_path).to_s } - else - [] - end - end - - def assert_built! - if file_paths.empty? - fail BuildError, <<~MSG - Missing built assets for #{name.inspect} in `#{assets_path}`. - - Build the application before rendering its assets. - MSG - end - end - end -end diff --git a/lib/ember_cli/embedding.rb b/lib/ember_cli/embedding.rb new file mode 100644 index 00000000..951df61e --- /dev/null +++ b/lib/ember_cli/embedding.rb @@ -0,0 +1,49 @@ +require "ember_cli/embedding/asset_map" +require "ember_cli/embedding/startup_tags" +require "ember_cli/embedding/url" + +module EmberCli + class Embedding + private_constant :AssetMap, :StartupTags, :Url + + def initialize(app) + @app = app + end + + def startup_tags? + app.dev_server? || app.vite? + end + + def startup_tags(prepend: "") + if app.dev_server? + StartupTags.new(app.dev_server.index_html, prefix: app.dev_server.origin).to_a + else + StartupTags.new(index_html.read, prefix: prepend).to_a + end + end + + def javascript_assets(prepend: "") + asset_map.javascripts(prepend: prepend) + end + + def stylesheet_assets(prepend: "") + asset_map.stylesheets(prepend: prepend) + end + + private + + attr_reader :app + + def asset_map + AssetMap.new( + name: app.name, + index_html: index_html, + assets_path: app.dist_path.join("assets"), + ) + end + + def index_html + app.dist_path.join("index.html") + end + end +end diff --git a/lib/ember_cli/embedding/asset_map.rb b/lib/ember_cli/embedding/asset_map.rb new file mode 100644 index 00000000..d0e4b907 --- /dev/null +++ b/lib/ember_cli/embedding/asset_map.rb @@ -0,0 +1,92 @@ +require "nokogiri" + +require "ember_cli/errors" +require "ember_cli/embedding/url" + +module EmberCli + class Embedding + class AssetMap + PREPEND = "assets/".freeze + + def initialize(name:, index_html:, assets_path:) + @name = name + @index_html = index_html + @assets_path = assets_path + end + + def javascripts(prepend: "") + assets_referenced_by("script", "src", prepend) + end + + def stylesheets(prepend: "") + assets_referenced_by(%{link[rel="stylesheet"]}, "href", prepend) + end + + private + + attr_reader :name, :index_html, :assets_path + + def assets_referenced_by(selector, attribute, prepend) + assert_built! + + document.css(selector).filter_map do |tag| + asset_for(tag[attribute], prepend) + end + end + + def document + @document ||= Nokogiri::HTML(index_html.read) + end + + def asset_for(url, prepend) + if url.to_s.empty? + nil + elsif Url.remote?(url) + url + else + [prepend, asset_matching(url)].join + end + end + + # A reference carries directories the build output does not, such as the + # `assets` directory itself or the application's `rootURL`, so match on the + # longest trailing path the two agree on rather than on the file name, which + # an addon's nested asset can share with another file. + def asset_matching(url) + asset = path_suffixes(url).find { |suffix| file_paths.include?(suffix) } + + unless asset + fail BuildError, "Failed to find a built asset matching `#{url}`" + end + + PREPEND + asset + end + + def path_suffixes(url) + segments = url.split("/").reject(&:empty?) + + segments.each_index.map { |index| segments[index..].join("/") } + end + + def file_paths + @file_paths ||= if assets_path.directory? + assets_path.glob("**/*", File::FNM_DOTMATCH). + select(&:file?). + map { |path| path.relative_path_from(assets_path).to_s } + else + [] + end + end + + def assert_built! + if file_paths.empty? + fail BuildError, <<~MSG + Missing built assets for #{name.inspect} in `#{assets_path}`. + + Build the application before rendering its assets. + MSG + end + end + end + end +end diff --git a/lib/ember_cli/embedding/startup_tags.rb b/lib/ember_cli/embedding/startup_tags.rb new file mode 100644 index 00000000..0cbd1b85 --- /dev/null +++ b/lib/ember_cli/embedding/startup_tags.rb @@ -0,0 +1,45 @@ +require "nokogiri" + +require "ember_cli/embedding/url" + +module EmberCli + class Embedding + class StartupTags + SELECTOR = [ + %{meta[name$="/config/environment"]}, + %{link[rel="stylesheet"]}, + %{link[rel="modulepreload"]}, + "script", + ].join(", ").freeze + + URL_ATTRIBUTES = %w(href src).freeze + + def initialize(html, prefix: "") + @html = html + @prefix = prefix.to_s.chomp("/") + end + + def to_a + Nokogiri::HTML5(html).css(SELECTOR).map { |tag| prefix_urls(tag).to_html } + end + + private + + attr_reader :html, :prefix + + # Only a root-relative URL points into what this application serves, so a + # URL that resolves elsewhere is left alone. + def prefix_urls(tag) + URL_ATTRIBUTES.each do |attribute| + value = tag[attribute] + + if value&.start_with?("/") && !Url.remote?(value) + tag[attribute] = "#{prefix}#{value}" + end + end + + tag + end + end + end +end diff --git a/lib/ember_cli/embedding/url.rb b/lib/ember_cli/embedding/url.rb new file mode 100644 index 00000000..b88e543f --- /dev/null +++ b/lib/ember_cli/embedding/url.rb @@ -0,0 +1,13 @@ +module EmberCli + class Embedding + module Url + # A scheme (`https://example.com/app.css`, `data:…`), or a protocol-relative + # URL (`//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 diff --git a/lib/ember_cli/path_set.rb b/lib/ember_cli/path_set.rb index 5f29895d..06386afd 100644 --- a/lib/ember_cli/path_set.rb +++ b/lib/ember_cli/path_set.rb @@ -51,14 +51,6 @@ def dist @dist ||= ember_cli_root.join("apps", app_name).tap(&:mkpath) end - def assets - dist.join("assets") - end - - def index_html - dist.join("index.html") - end - def gemfile @gemfile ||= root.join("Gemfile") end diff --git a/lib/ember_cli/startup_tags.rb b/lib/ember_cli/startup_tags.rb deleted file mode 100644 index 7ad0045e..00000000 --- a/lib/ember_cli/startup_tags.rb +++ /dev/null @@ -1,43 +0,0 @@ -require "nokogiri" - -require "ember_cli/url" - -module EmberCli - class StartupTags - SELECTOR = [ - %{meta[name$="/config/environment"]}, - %{link[rel="stylesheet"]}, - %{link[rel="modulepreload"]}, - "script", - ].join(", ").freeze - - URL_ATTRIBUTES = %w(href src).freeze - - def initialize(html, prefix: "") - @html = html - @prefix = prefix.to_s.chomp("/") - end - - def to_a - Nokogiri::HTML5(html).css(SELECTOR).map { |tag| prefix_urls(tag).to_html } - end - - private - - attr_reader :html, :prefix - - # Only a root-relative URL points into what this application serves, so a - # URL that resolves elsewhere is left alone. - def prefix_urls(tag) - URL_ATTRIBUTES.each do |attribute| - value = tag[attribute] - - if value&.start_with?("/") && !Url.remote?(value) - tag[attribute] = "#{prefix}#{value}" - end - end - - tag - end - end -end diff --git a/lib/ember_cli/url.rb b/lib/ember_cli/url.rb deleted file mode 100644 index a7882380..00000000 --- a/lib/ember_cli/url.rb +++ /dev/null @@ -1,11 +0,0 @@ -module EmberCli - module Url - # A scheme (`https://example.com/app.css`, `data:…`), or a protocol-relative - # URL (`//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 diff --git a/spec/dummy/app/views/pages/embedded.html.erb b/spec/dummy/app/views/pages/embedded.html.erb index f048f8b2..149e7c1f 100644 --- a/spec/dummy/app/views/pages/embedded.html.erb +++ b/spec/dummy/app/views/pages/embedded.html.erb @@ -1,12 +1,13 @@ <% app = EmberCli["my-app"] %> <% app.build %> -<% if app.vite? %> - <%= safe_join(app.startup_tags.map(&:html_safe), "\n") %> +<% embedding = EmberCli::Embedding.new(app) %> +<% if embedding.startup_tags? %> + <%= safe_join(embedding.startup_tags.map(&:html_safe), "\n") %> <% else %> - <% app.javascript_assets.each do |src| %> + <% embedding.javascript_assets.each do |src| %> <% end %> - <% app.stylesheet_assets.each do |href| %> + <% embedding.stylesheet_assets.each do |href| %> <% end %> <% end %> diff --git a/spec/lib/ember_cli/asset_map_spec.rb b/spec/lib/ember_cli/asset_map_spec.rb deleted file mode 100644 index e87dcef0..00000000 --- a/spec/lib/ember_cli/asset_map_spec.rb +++ /dev/null @@ -1,218 +0,0 @@ -require "fileutils" -require "pathname" -require "tmpdir" -require "ember_cli/asset_map" - -describe EmberCli::AssetMap do - describe "#javascripts" do - it "resolves the scripts of `index.html` against the built assets" do - assets_path = build_assets_path("bar-abc123.js", "vendor-abc123.js", "not-a-match") - index_html = build_index_html(<<~HTML) - - - HTML - asset_map = build_asset_map(index_html: index_html, assets_path: assets_path) - - javascripts = asset_map.javascripts - - expect(javascripts).to match_array([ - "assets/bar-abc123.js", - "assets/vendor-abc123.js", - ]) - end - - it "raises a BuildError when the application has not been built" do - asset_map = build_asset_map(assets_path: build_assets_path) - - expect { asset_map.javascripts }. - to raise_error(EmberCli::BuildError, /my-app/) - end - - it "mounts the build's scripts onto `prepend`" do - assets_path = build_assets_path("bar-abc123.js") - index_html = build_index_html(%{}) - asset_map = build_asset_map(index_html: index_html, assets_path: assets_path) - - javascripts = asset_map.javascripts(prepend: "http://example.com/") - - expect(javascripts).to eq(["http://example.com/assets/bar-abc123.js"]) - end - - it "emits the scripts hosted outside the build as they are" do - assets_path = build_assets_path("bar-abc123.js") - index_html = build_index_html(<<~HTML) - - - - HTML - asset_map = build_asset_map(index_html: index_html, assets_path: assets_path) - - javascripts = asset_map.javascripts(prepend: "http://example.com/") - - expect(javascripts).to match_array([ - "http://example.com/assets/bar-abc123.js", - "https://cdn.example.com/analytics.js", - "//cdn.example.com/protocol-relative.js", - ]) - end - - it "ignores a script that references no asset" do - assets_path = build_assets_path("bar-abc123.js") - index_html = build_index_html(<<~HTML) - - - HTML - asset_map = build_asset_map(index_html: index_html, assets_path: assets_path) - - javascripts = asset_map.javascripts - - expect(javascripts).to eq(["assets/bar-abc123.js"]) - end - - it "raises a BuildError when a referenced asset is missing" do - assets_path = build_assets_path("vendor-abc123.js") - index_html = build_index_html(%{}) - asset_map = build_asset_map(index_html: index_html, assets_path: assets_path) - - expect { asset_map.javascripts }. - to raise_error(EmberCli::BuildError, /bar-abc123\.js/) - end - - it "resolves a script nested in the build" do - assets_path = build_assets_path("highlight/js/highlight.min.js") - index_html = build_index_html( - %{}, - ) - asset_map = build_asset_map(index_html: index_html, assets_path: assets_path) - - javascripts = asset_map.javascripts - - expect(javascripts).to eq(["assets/highlight/js/highlight.min.js"]) - end - end - - describe "#stylesheets" do - it "resolves the stylesheets of `index.html` against the built assets" do - assets_path = build_assets_path("bar-abc123.css", "vendor-abc123.css") - index_html = build_index_html(<<~HTML) - - - HTML - asset_map = build_asset_map(index_html: index_html, assets_path: assets_path) - - stylesheets = asset_map.stylesheets - - expect(stylesheets).to match_array([ - "assets/bar-abc123.css", - "assets/vendor-abc123.css", - ]) - end - - it "resolves the stylesheets nested in the build" do - assets_path = build_assets_path( - "bar-abc123.css", - "font-awesome/css/font-awesome.min.css", - ) - index_html = build_index_html(<<~HTML) - - - HTML - asset_map = build_asset_map(index_html: index_html, assets_path: assets_path) - - stylesheets = asset_map.stylesheets - - expect(stylesheets).to match_array([ - "assets/font-awesome/css/font-awesome.min.css", - "assets/bar-abc123.css", - ]) - end - - it "resolves the stylesheets referenced through a `rootURL`" do - assets_path = build_assets_path( - "bar-abc123.css", - "font-awesome/css/font-awesome.min.css", - ) - index_html = build_index_html(<<~HTML) - - - HTML - asset_map = build_asset_map(index_html: index_html, assets_path: assets_path) - - stylesheets = asset_map.stylesheets - - expect(stylesheets).to match_array([ - "assets/font-awesome/css/font-awesome.min.css", - "assets/bar-abc123.css", - ]) - end - - it "prefers the asset whose path matches over one that only shares its basename" do - assets_path = build_assets_path("app.css", "font-awesome/css/app.css") - index_html = build_index_html( - %{}, - ) - asset_map = build_asset_map(index_html: index_html, assets_path: assets_path) - - stylesheets = asset_map.stylesheets - - expect(stylesheets).to eq(["assets/font-awesome/css/app.css"]) - end - - it "emits the stylesheets hosted outside the build as they are" do - assets_path = build_assets_path("bar-abc123.css") - index_html = build_index_html(<<~HTML) - - - - HTML - asset_map = build_asset_map(index_html: index_html, assets_path: assets_path) - - stylesheets = asset_map.stylesheets(prepend: "http://example.com/") - - expect(stylesheets).to match_array([ - "http://example.com/assets/bar-abc123.css", - "https://fonts.example.com/css?family=Frontend", - "//fonts.example.com/protocol-relative.css", - ]) - end - end - - def build_asset_map(index_html: build_index_html(""), assets_path:) - EmberCli::AssetMap.new( - name: "my-app", - index_html: index_html, - assets_path: assets_path, - ) - end - - def build_index_html(head) - path = dist.join("index.html") - - path.write(<<~HTML) - - - #{head} - - - HTML - - path - end - - def build_assets_path(*file_names) - dist.join("assets").tap do |assets| - assets.mkpath - - file_names.each do |file_name| - path = assets.join(file_name) - - path.dirname.mkpath - FileUtils.touch(path) - end - end - end - - def dist - @dist ||= Pathname.new(Dir.mktmpdir) - end -end diff --git a/spec/lib/ember_cli/embedding_spec.rb b/spec/lib/ember_cli/embedding_spec.rb new file mode 100644 index 00000000..f01f403d --- /dev/null +++ b/spec/lib/ember_cli/embedding_spec.rb @@ -0,0 +1,325 @@ +require "fileutils" +require "pathname" +require "tmpdir" +require "ember_cli/embedding" + +describe EmberCli::Embedding do + it "keeps the classes it reads a build with to itself" do + expect { EmberCli::Embedding::AssetMap }.to raise_error(NameError) + expect { EmberCli::Embedding::StartupTags }.to raise_error(NameError) + expect { EmberCli::Embedding::Url }.to raise_error(NameError) + end + + describe "#startup_tags?" do + it "is false for a classic build" do + expect(build_embedding).not_to be_startup_tags + end + + it "is true for a Vite build" do + expect(build_embedding(vite?: true)).to be_startup_tags + end + + it "is true for an application served by Vite's development server" do + expect(build_embedding(dev_server: dev_server)).to be_startup_tags + end + end + + describe "#startup_tags" do + it "extracts the tags the application boots from" do + build_index_html(<<~HTML) + + + + + + HTML + embedding = build_embedding(vite?: true) + + tags = embedding.startup_tags + + expect(tags).to match_array([ + %{}, + %{}, + %{}, + %{}, + ]) + end + + it "mounts the root-relative URLs onto `prepend`" do + build_index_html(<<~HTML) + + + HTML + embedding = build_embedding(vite?: true) + + tags = embedding.startup_tags(prepend: "/admin/") + + expect(tags).to match_array([ + %{}, + %{}, + ]) + end + + it "leaves absolute and protocol-relative URLs alone" do + build_index_html(<<~HTML) + + + HTML + embedding = build_embedding(vite?: true) + + tags = embedding.startup_tags(prepend: "/admin") + + expect(tags).to match_array([ + %{}, + %{}, + ]) + end + + it "reads the tags from Vite's development server when it serves the application" do + embedding = build_embedding( + dev_server: dev_server(<<~HTML), + + + + + + + HTML + ) + + tags = embedding.startup_tags(prepend: "/admin") + + expect(tags).to match_array([ + %{}, + %{}, + ]) + end + end + +describe "#javascript_assets" do + it "resolves the scripts of `index.html` against the built assets" do + build_assets_path("bar-abc123.js", "vendor-abc123.js", "not-a-match") + build_index_html(<<~HTML) + + + HTML + embedding = build_embedding + + javascripts = embedding.javascript_assets + + expect(javascripts).to match_array([ + "assets/bar-abc123.js", + "assets/vendor-abc123.js", + ]) + end + + it "raises a BuildError when the application has not been built" do + build_assets_path + embedding = build_embedding + + expect { embedding.javascript_assets }. + to raise_error(EmberCli::BuildError, /my-app/) + end + + it "mounts the build's scripts onto `prepend`" do + build_assets_path("bar-abc123.js") + build_index_html(%{}) + embedding = build_embedding + + javascripts = embedding.javascript_assets(prepend: "http://example.com/") + + expect(javascripts).to eq(["http://example.com/assets/bar-abc123.js"]) + end + + it "emits the scripts hosted outside the build as they are" do + build_assets_path("bar-abc123.js") + build_index_html(<<~HTML) + + + + HTML + embedding = build_embedding + + javascripts = embedding.javascript_assets(prepend: "http://example.com/") + + expect(javascripts).to match_array([ + "http://example.com/assets/bar-abc123.js", + "https://cdn.example.com/analytics.js", + "//cdn.example.com/protocol-relative.js", + ]) + end + + it "ignores a script that references no asset" do + build_assets_path("bar-abc123.js") + build_index_html(<<~HTML) + + + HTML + embedding = build_embedding + + javascripts = embedding.javascript_assets + + expect(javascripts).to eq(["assets/bar-abc123.js"]) + end + + it "raises a BuildError when a referenced asset is missing" do + build_assets_path("vendor-abc123.js") + build_index_html(%{}) + embedding = build_embedding + + expect { embedding.javascript_assets }. + to raise_error(EmberCli::BuildError, /bar-abc123\.js/) + end + + it "resolves a script nested in the build" do + build_assets_path("highlight/js/highlight.min.js") + build_index_html( + %{}, + ) + embedding = build_embedding + + javascripts = embedding.javascript_assets + + expect(javascripts).to eq(["assets/highlight/js/highlight.min.js"]) + end + end + + describe "#stylesheet_assets" do + it "resolves the stylesheets of `index.html` against the built assets" do + build_assets_path("bar-abc123.css", "vendor-abc123.css") + build_index_html(<<~HTML) + + + HTML + embedding = build_embedding + + stylesheets = embedding.stylesheet_assets + + expect(stylesheets).to match_array([ + "assets/bar-abc123.css", + "assets/vendor-abc123.css", + ]) + end + + it "resolves the stylesheets nested in the build" do + build_assets_path( + "bar-abc123.css", + "font-awesome/css/font-awesome.min.css", + ) + build_index_html(<<~HTML) + + + HTML + embedding = build_embedding + + stylesheets = embedding.stylesheet_assets + + expect(stylesheets).to match_array([ + "assets/font-awesome/css/font-awesome.min.css", + "assets/bar-abc123.css", + ]) + end + + it "resolves the stylesheets referenced through a `rootURL`" do + build_assets_path( + "bar-abc123.css", + "font-awesome/css/font-awesome.min.css", + ) + build_index_html(<<~HTML) + + + HTML + embedding = build_embedding + + stylesheets = embedding.stylesheet_assets + + expect(stylesheets).to match_array([ + "assets/font-awesome/css/font-awesome.min.css", + "assets/bar-abc123.css", + ]) + end + + it "prefers the asset whose path matches over one that only shares its basename" do + build_assets_path("app.css", "font-awesome/css/app.css") + build_index_html( + %{}, + ) + embedding = build_embedding + + stylesheets = embedding.stylesheet_assets + + expect(stylesheets).to eq(["assets/font-awesome/css/app.css"]) + end + + it "emits the stylesheets hosted outside the build as they are" do + build_assets_path("bar-abc123.css") + build_index_html(<<~HTML) + + + + HTML + embedding = build_embedding + + stylesheets = embedding.stylesheet_assets(prepend: "http://example.com/") + + expect(stylesheets).to match_array([ + "http://example.com/assets/bar-abc123.css", + "https://fonts.example.com/css?family=Frontend", + "//fonts.example.com/protocol-relative.css", + ]) + end + end + + def build_embedding(dev_server: nil, **stubs) + app = instance_double( + EmberCli::App, + name: "my-app", + dist_path: dist, + dev_server?: !dev_server.nil?, + dev_server: dev_server, + vite?: false, + **stubs, + ) + + EmberCli::Embedding.new(app) + end + + def build_index_html(head) + path = dist.join("index.html") + + path.write(<<~HTML) + + + #{head} + + + HTML + + path + end + + def build_assets_path(*file_names) + dist.join("assets").tap do |assets| + assets.mkpath + + file_names.each do |file_name| + path = assets.join(file_name) + + path.dirname.mkpath + FileUtils.touch(path) + end + end + end + + def dev_server(index_html = "") + instance_double( + EmberCli::DevServer, + index_html: index_html, + origin: "http://127.0.0.1:4200", + ) + end + + def dist + @dist ||= Pathname.new(Dir.mktmpdir) + end +end diff --git a/spec/lib/ember_cli/startup_tags_spec.rb b/spec/lib/ember_cli/startup_tags_spec.rb deleted file mode 100644 index f8a8526a..00000000 --- a/spec/lib/ember_cli/startup_tags_spec.rb +++ /dev/null @@ -1,55 +0,0 @@ -require "ember_cli/startup_tags" - -describe EmberCli::StartupTags do - it "extracts the tags the application boots from" do - tags = EmberCli::StartupTags.new(<<~HTML).to_a - - - - - - - - - - - HTML - - expect(tags).to match_array([ - %{}, - %{}, - %{}, - %{}, - ]) - end - - it "joins the prefix onto root-relative URLs" do - tags = EmberCli::StartupTags.new(<<~HTML, prefix: "http://127.0.0.1:4200/").to_a - - - - - - - HTML - - expect(tags).to include(%{}) - expect(tags).to include(%{}) - end - - it "leaves absolute and protocol-relative URLs alone" do - tags = EmberCli::StartupTags.new(<<~HTML, prefix: "/admin").to_a - - - - - - - HTML - - expect(tags).to match_array([ - %{}, - %{}, - ]) - end -end diff --git a/spec/lib/ember_cli/url_spec.rb b/spec/lib/ember_cli/url_spec.rb deleted file mode 100644 index c7e01e3d..00000000 --- a/spec/lib/ember_cli/url_spec.rb +++ /dev/null @@ -1,23 +0,0 @@ -require "ember_cli/url" - -describe EmberCli::Url do - describe ".remote?" do - it "is true for a URL carrying a scheme" do - expect(EmberCli::Url).to be_remote("https://cdn.example.com/app.js") - expect(EmberCli::Url).to be_remote("data:text/css,body{}") - end - - it "is true for a protocol-relative URL" do - expect(EmberCli::Url).to be_remote("//cdn.example.com/app.js") - end - - it "is false for a URL that points into the build" do - expect(EmberCli::Url).not_to be_remote("/assets/app.js") - expect(EmberCli::Url).not_to be_remote("assets/app.js") - end - - it "is false for no URL at all" do - expect(EmberCli::Url).not_to be_remote(nil) - end - end -end