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..233b6c8c 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -1,34 +1,49 @@ +# 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 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. +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, 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::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 -`ember-cli >= 6.8` generates applications that are built with [Vite] instead -of the classic Broccoli-based pipeline. `ember-cli-rails` supports both build -systems, and detects the Vite-based build by the presence of a `vite.config.*` -file in the Ember application's root. - -When upgrading an Ember application to the Vite-based blueprint, note the -following differences in how `ember-cli-rails` treats it: - -* Remove `ember-cli-rails-addon` from the application's `package.json`. The - addon is incompatible with the Vite-based build (it forces - `storeConfigInMeta` off and ships an initializer that imports the removed - `ember` module), and `ember-cli-rails` no longer needs it there. -* Use `include_ember_script_tags` on its own instead of pairing it with - `include_ember_stylesheet_tags`: for Vite-based applications it emits the - configuration meta tag, the stylesheet links, and the module script tags - all together, while `include_ember_stylesheet_tags` supports only classic - applications. This requires `ember-cli-rails-assets >= 0.9.0`. -* In development, the application is served by Vite's development server — - the same one the application's `npm start` script runs. `ember-cli-rails` - starts it on the first request and shuts it down when Rails exits, and - rewrites the URLs in the `index.html` it serves so that the browser loads - the application's modules, and Vite's HMR client, from it directly. Changes - are hot-reloaded without restarting Rails. - - The development server is the default — and the recommended — way to - develop a Vite-based application, and needs no configuration. When - configuring it anyway, a fixed `port` lets several Rails workers (or a - hand-started `npm start`) share a single server, and a longer `timeout` - accommodates a slow first boot: +`ember-cli >= 6.8` generates applications that are built with [Vite] instead of the classic Broccoli-based pipeline. +`ember-cli-rails` supports both build systems, and detects the Vite-based build by the presence of a `vite.config.*` file in the Ember application's root. + +When upgrading an Ember application to the Vite-based blueprint, note the following differences in how `ember-cli-rails` treats it: + +* Remove `ember-cli-rails-addon` from the application's `package.json`. + The addon is incompatible with the Vite-based build (it forces `storeConfigInMeta` off and ships an initializer that imports the removed `ember` module), and `ember-cli-rails` no longer needs it there. +* Use `include_ember_script_tags` on its own instead of pairing it with `include_ember_stylesheet_tags`: for Vite-based applications it emits the configuration meta tag, the stylesheet links, and the module script tags all together, while `include_ember_stylesheet_tags` supports only classic applications. + This requires `ember-cli-rails-assets >= 0.9.0`. +* In development, the application is served by Vite's development server — the same one the application's `npm start` script runs. + `ember-cli-rails` starts it on the first request and shuts it down when Rails exits, and rewrites the URLs in the `index.html` it serves so that the browser loads the application's modules, and Vite's HMR client, from it directly. + Changes are hot-reloaded without restarting Rails. + + The development server is the default — and the recommended — way to develop a Vite-based application, and needs no configuration. + When configuring it anyway, a fixed `port` lets several Rails workers (or a hand-started `npm start`) share a single server, and a longer `timeout` accommodates a slow first boot: ```rb EmberCli.configure do |c| @@ -36,13 +51,8 @@ following differences in how `ember-cli-rails` treats it: end ``` - When Rails runs in a container and the browser outside of it, bind the - development server to every interface and name the browser-facing origin - separately: `dev_server: { host: "0.0.0.0", port: 4200, origin: - "http://localhost:4200" }`. - Opening the page on a LAN IP or a custom domain also needs `server.cors` - configured in `vite.config.*` — Vite's default allows localhost origins - only. + When Rails runs in a container and the browser outside of it, bind the development server to every interface and name the browser-facing origin separately: `dev_server: { host: "0.0.0.0", port: 4200, origin: "http://localhost:4200" }`. + Opening the page on a LAN IP or a custom domain also needs `server.cors` configured in `vite.config.*` — Vite's default allows localhost origins only. To opt out of the development server, disable it: @@ -53,32 +63,24 @@ following differences in how `ember-cli-rails` treats it: end ``` -* `include_ember_script_tags` serves its startup tags from the development - server, with absolute URLs pointing at it — nothing is built, and changes - are picked up by reloading the page. With `dev_server: false` it reads the - output of `ember build` instead, built once, synchronously, on the first - request; restart the Rails server to pick up changes. +* `include_ember_script_tags` serves its startup tags from the development server, with absolute URLs pointing at it — nothing is built, and changes are picked up by reloading the page. + With `dev_server: false` it reads the output of `ember build` instead, built once, synchronously, on the first request; restart the Rails server to pick up changes. [Vite]: https://vitejs.dev # Ruby support -According to [these release notes][latest-eol], Ruby versions prior to `2.5.x` -has been end-of-lifed. +According to [these release notes][latest-eol], Ruby versions prior to `2.5.x` has been end-of-lifed. Additionally, this codebase makes use of [(required) keyword arguments][kwargs]. -From `ember-cli-rails@0.4.0` and on, we will no longer support versions of Ruby -prior to `2.1.0`. +From `ember-cli-rails@0.4.0` and on, we will no longer support versions of Ruby prior to `2.1.0`. -`ember-cli-rails@0.8.0` adds support for Rails 5, which depends on `rack@2.0.x`, -which **requires** Ruby `2.2.2` or greater. +`ember-cli-rails@0.8.0` adds support for Rails 5, which depends on `rack@2.0.x`, which **requires** Ruby `2.2.2` or greater. -From `ember-cli-rails@0.8.0` and on, we will no longer support versions of Ruby -prior to `2.2.2`. +From `ember-cli-rails@0.8.0` and on, we will no longer support versions of Ruby prior to `2.2.2`. -From `ember-cli-rails@0.12.0` and on, we will no longer support versions of Ruby -prior to `2.5.x`. +From `ember-cli-rails@0.12.0` and on, we will no longer support versions of Ruby prior to `2.5.x`. To use `ember-cli-rails` with older versions of Ruby, try the `0.3.x` series. @@ -87,15 +89,12 @@ To use `ember-cli-rails` with older versions of Ruby, try the `0.3.x` series. # Rails support -According to the [Rails Maintenance Policy][version-policy], Rails versions -prior to `5.2.x` have been end-of-lifed. Additionally, the `4.0.x` series no -longer receives bug fixes of any sort. +According to the [Rails Maintenance Policy][version-policy], Rails versions prior to `5.2.x` have been end-of-lifed. +Additionally, the `4.0.x` series no longer receives bug fixes of any sort. -From `ember-cli-rails@0.4.0` and on, we will no longer support versions of Rails -prior to `3.2.0`, nor will we support the `4.0.x` series of releases. +From `ember-cli-rails@0.4.0` and on, we will no longer support versions of Rails prior to `3.2.0`, nor will we support the `4.0.x` series of releases. -From `ember-cli-rails@0.12.0` and on, we will no longer support versions of -Rails prior to `5.2.0`. +From `ember-cli-rails@0.12.0` and on, we will no longer support versions of Rails prior to `5.2.0`. To use `ember-cli-rails` with older versions of Rails, try the `0.3.x` series. 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..d2f8d2aa 100644 --- a/lib/ember_cli.rb +++ b/lib/ember_cli.rb @@ -1,9 +1,9 @@ require "fileutils" require "active_support/deprecation" -require "ember-cli-rails-assets" 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 dab8aba0..a81b744c 100644 --- a/lib/ember_cli/app.rb +++ b/lib/ember_cli/app.rb @@ -75,6 +75,10 @@ def index_html(head:, body:, mount_point: nil) html.render end + def vite? + paths.vite? + end + def install_dependencies @shell.install 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/spec/dummy/app/views/pages/embedded.html.erb b/spec/dummy/app/views/pages/embedded.html.erb index 93cf0d74..149e7c1f 100644 --- a/spec/dummy/app/views/pages/embedded.html.erb +++ b/spec/dummy/app/views/pages/embedded.html.erb @@ -1,6 +1,13 @@ -<%= 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" %> +<% app = EmberCli["my-app"] %> +<% app.build %> +<% embedding = EmberCli::Embedding.new(app) %> +<% if embedding.startup_tags? %> + <%= safe_join(embedding.startup_tags.map(&:html_safe), "\n") %> +<% else %> + <% embedding.javascript_assets.each do |src| %> + + <% end %> + <% embedding.stylesheet_assets.each do |href| %> + + <% end %> <% end %> diff --git a/spec/dummy/config/routes.rb b/spec/dummy/config/routes.rb index e409b754..6b0b5a2e 100644 --- a/spec/dummy/config/routes.rb +++ b/spec/dummy/config/routes.rb @@ -23,7 +23,7 @@ app.mount_ember_app( "my-app", - to: "/asset-helpers", + to: "/embed", id: "embedded", as: "embedded", ) diff --git a/spec/features/user_views_ember_app_spec.rb b/spec/features/user_views_ember_app_spec.rb index 4eedd15b..25b03ec1 100644 --- a/spec/features/user_views_ember_app_spec.rb +++ b/spec/features/user_views_ember_app_spec.rb @@ -8,7 +8,7 @@ end context "using custom controller" do - scenario "rendering with asset helpers" do + scenario "embedding the assets the application reports into a page" do visit embedded_path expect(page).to have_client_side_asset 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