diff --git a/CHANGELOG.md b/CHANGELOG.md index e6f788a0..6d149443 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ main * Raise `EmberCli::BuildError` when `ember build` or a dependency installation fails, instead of exiting the process from inside the gem — `rake ember:compile` and `rake ember:install` still exit nonzero +* Remap the root-relative asset URLs in a Vite build's `index.html` onto the + mount point when `mount_ember_app` serves the application from a path other + than `/`, so non-root mounts work without a matching `rootURL` 0.13.1 ------ diff --git a/README.md b/README.md index 9e121b1c..62936e67 100644 --- a/README.md +++ b/README.md @@ -591,7 +591,15 @@ Rails.application.routes.draw do end ``` -Then set each Ember application's `rootURL` to the mount point: +The `index.html` of a Vite build refers to its assets with root-relative URLs. +When the application is served from the build output, `mount_ember_app` remaps +those references onto the mount point, where the assets are served — references +that already carry the mount point, from a `rootURL` configured to match it, +are left alone. Development-server-backed applications are unaffected: their +asset URLs point at the development server absolutely. + +For classic (Broccoli-based) applications, set each Ember application's +`rootURL` to the mount point: ```javascript // frontend/config/environment.js diff --git a/app/helpers/ember_rails_helper.rb b/app/helpers/ember_rails_helper.rb index e145b3ae..2415cbdc 100644 --- a/app/helpers/ember_rails_helper.rb +++ b/app/helpers/ember_rails_helper.rb @@ -8,6 +8,12 @@ def render_ember_app(name, &block) head, body = markup_capturer.capture - render html: EmberCli[name].index_html(head: head, body: body).html_safe + index_html = EmberCli[name].index_html( + head: head, + body: body, + mount_point: params[:ember_mount_point], + ) + + render html: index_html.html_safe end end diff --git a/lib/ember_cli/app.rb b/lib/ember_cli/app.rb index 0a33807a..23dff264 100644 --- a/lib/ember_cli/app.rb +++ b/lib/ember_cli/app.rb @@ -63,11 +63,11 @@ def build end end - def index_html(head:, body:) + def index_html(head:, body:, mount_point: nil) html = HtmlPage::Renderer.new( head: head, body: body, - content: deploy.index_html, + content: remap_to_mount_point(deploy.index_html, mount_point), ) html.render @@ -131,6 +131,25 @@ def test? env.to_s == "test" end + # The `index.html` of a Vite build refers to its assets with root-relative + # URLs. When the application is mounted somewhere other than `/`, remap + # them onto the mount point, where `mount_ember_assets` serves them. + # References that already carry the mount point — a `rootURL` configured + # to match it — are left alone. + def remap_to_mount_point(html, mount_point) + prefix = mount_point.to_s.chomp("/") + + if prefix.empty? || !paths.vite? + return html + end + + already_mounted = Regexp.escape(prefix.delete_prefix("/")) + + html.gsub(%r{(\s)(src|href)=(["'])/(?!/)(?!#{already_mounted}/)}i) do + "#{$1}#{$2}=#{$3}#{prefix}/" + end + end + def deploy deploy_strategy.new(self) end diff --git a/lib/ember_cli/route_helpers.rb b/lib/ember_cli/route_helpers.rb index 3ef5bbd6..26ccb882 100644 --- a/lib/ember_cli/route_helpers.rb +++ b/lib/ember_cli/route_helpers.rb @@ -6,7 +6,7 @@ module Routing class Mapper def mount_ember_app(app_name, to:, **options) routing_options = options.deep_merge( - defaults: { ember_app: app_name }, + defaults: { ember_app: app_name, ember_mount_point: to }, ) routing_options.reverse_merge!( diff --git a/spec/features/user_views_ember_app_spec.rb b/spec/features/user_views_ember_app_spec.rb index 72e2d520..4eedd15b 100644 --- a/spec/features/user_views_ember_app_spec.rb +++ b/spec/features/user_views_ember_app_spec.rb @@ -35,6 +35,17 @@ end end + scenario "remaps the document's asset URLs onto the mount point", js: false do + unless EmberCli["my-app"].paths.vite? + skip "Root-relative asset URLs are only remapped for Vite builds" + end + + visit include_index_path + + expect(page).to have_css(%{script[src^="/no-block/"]}, visible: false) + expect(page).to have_no_css(%{script[src^="/@embroider"]}, visible: false) + end + scenario "is redirected with trailing slash", js: false do expect(include_index_path).to eq("/no-block") diff --git a/spec/lib/ember_cli/app_spec.rb b/spec/lib/ember_cli/app_spec.rb index bc94f0f6..a243fb68 100644 --- a/spec/lib/ember_cli/app_spec.rb +++ b/spec/lib/ember_cli/app_spec.rb @@ -100,6 +100,50 @@ end end + describe "#index_html" do + it "remaps a Vite build's root-relative URLs onto the mount point" do + app = build_app("frontend", vite: true, environment: "test") + stub_deployed_index_html(app, <<~HTML) + + + + + + + + HTML + + index_html = app.index_html(head: "", body: "", mount_point: "/admin") + + expect(index_html).to include(%{src="/admin/@embroider/virtual/vendor.js"}) + expect(index_html).to include(%{href="/admin/assets/app.css"}) + expect(index_html).to include(%{src="https://cdn.example.com/analytics.js"}) + expect(index_html).to include(%{src="//cdn.example.com/protocol-relative.js"}) + expect(index_html).to include(%{href="/admin/already-mounted.css"}) + expect(index_html).not_to include(%{/admin/admin/}) + end + + it "leaves the document alone when mounted at the root" do + app = build_app("frontend", vite: true, environment: "test") + content = %{} + stub_deployed_index_html(app, content) + + index_html = app.index_html(head: "", body: "", mount_point: "/") + + expect(index_html).to include(%{src="/assets/app.js"}) + end + + it "leaves a classic build alone" do + app = build_app("frontend", vite: false, environment: "test") + content = %{} + stub_deployed_index_html(app, content) + + index_html = app.index_html(head: "", body: "", mount_point: "/admin") + + expect(index_html).to include(%{src="/assets/app.js"}) + end + end + describe "#root_path" do it "delegates to PathSet" do root_path = Pathname.new(".") @@ -197,6 +241,10 @@ def build_app(name, vite:, environment:, **options) EmberCli::App.new(name, **options) end + def stub_deployed_index_html(app, html) + allow(app).to receive(:deploy).and_return(double(index_html: html)) + end + def stub_paths(method_to_value) allow_any_instance_of(EmberCli::PathSet). to receive(method_to_value.keys.first).