From 316e2ef476ddc2b00eff40b8951cfede739199f1 Mon Sep 17 00:00:00 2001 From: Sean Doyle Date: Tue, 24 Nov 2015 11:50:01 -0500 Subject: [PATCH] Merge EmberCLI-generated Manifest with Sprockets' Closes [#298] Expose Ember's fingerprinted assets to Sprockets. No longer support Sprockets-based helpers for Rails 3.2, given the difference in how the Asset Pipeline works post-4.0. [#298]: https://github.com/thoughtbot/ember-cli-rails/issues/298 --- CHANGELOG.md | 2 + README.md | 10 ++- app/helpers/ember_rails_helper.rb | 44 ++++++++++-- lib/ember_cli/app.rb | 2 + lib/ember_cli/assets.rb | 56 +++++++++++++++ lib/ember_cli/manifest.rb | 14 ++++ lib/ember_cli/missing_manifest.rb | 14 ++++ lib/ember_cli/path_set.rb | 8 +++ lib/ember_cli/sprockets.rb | 47 ++++++++++++- spec/lib/ember_cli/app_spec.rb | 10 ++- spec/lib/ember_cli/assets_spec.rb | 101 +++++++++++++++++++++++++++ spec/lib/ember_cli/manifest_spec.rb | 27 +++++++ spec/lib/ember_cli/path_set_spec.rb | 21 ++++++ spec/lib/ember_cli/sprockets_spec.rb | 48 +++++++++++++ 14 files changed, 393 insertions(+), 11 deletions(-) create mode 100644 lib/ember_cli/assets.rb create mode 100644 lib/ember_cli/manifest.rb create mode 100644 lib/ember_cli/missing_manifest.rb create mode 100644 spec/lib/ember_cli/assets_spec.rb create mode 100644 spec/lib/ember_cli/manifest_spec.rb create mode 100644 spec/lib/ember_cli/sprockets_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index bd871dd2..0e6424f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,11 @@ master ------ +* Merge EmberCLI-generate manifest into Sprockets'. [#316] * Delete previous build output on application boot instead of on process exit. [#308] +[#316]: https://github.com/thoughtbot/ember-cli-rails/pull/316 [#308]: https://github.com/thoughtbot/ember-cli-rails/pull/308 0.5.6 diff --git a/README.md b/README.md index c55ac879..782c6c6a 100644 --- a/README.md +++ b/README.md @@ -189,6 +189,10 @@ In addition to rendering the EmberCLI generated `index.html`, you can inject the <%= include_ember_stylesheet_tags :frontend %> ``` +**NOTE** + +These helpers are only available for Rails versions `>= 4.0`. + ### Multiple Ember CLI apps In the initializer you may specify multiple Ember CLI apps, each of which can be @@ -510,10 +514,14 @@ jQuery and Handlebars are the main use cases for this flag. This project supports: * Ruby versions `>= 2.1.0` -* Rails `3.2.x` and `>=4.1.x`. +* Rails versions `3.2.x` and `>=4.1.x`. + +[Rendering EmberCLI-generated assets through Sprockets](asset-helpers) is +**NOT** supported for Rails `3.2.x`. To learn more about supported versions and upgrades, read the [upgrading guide]. +[asset-helpers]: #rendering-the-embercli-generated-js-and-css [upgrading guide]: /UPGRADING.md ## Contributing diff --git a/app/helpers/ember_rails_helper.rb b/app/helpers/ember_rails_helper.rb index 8f610a3d..c637a99a 100644 --- a/app/helpers/ember_rails_helper.rb +++ b/app/helpers/ember_rails_helper.rb @@ -2,11 +2,7 @@ module EmberRailsHelper def include_ember_index_html(name, &block) - warn <<-MSG.strip_heredoc - The `include_ember_index_html` helper has been deprecated. - - Rename all invocations to `render_ember_app` - MSG + Warnings.warn_include_index_html render_ember_app(name, &block) end @@ -20,10 +16,44 @@ def render_ember_app(name, &block) end def include_ember_script_tags(name, **options) - javascript_include_tag(*EmberCli[name].sprockets.assets, options) + Warnings.warn_asset_helper + + javascript_include_tag(*EmberCli[name].sprockets.javascript_assets, options) end def include_ember_stylesheet_tags(name, **options) - stylesheet_link_tag(*EmberCli[name].sprockets.assets, options) + Warnings.warn_asset_helper + + stylesheet_link_tag(*EmberCli[name].sprockets.stylesheet_assets, options) + end + + module Warnings + def self.warn_include_index_html + warn <<-MSG.strip_heredoc + The `include_ember_index_html` helper has been deprecated. + + Rename all invocations to `render_ember_app` + MSG + end + + def self.warn_asset_helper + if Rails::VERSION::MAJOR < 4 + warn <<-MSG.strip_heredoc + `ember-cli-rails` no longer supports Sprockets-based helpers for Rails + versions below 4.0. + + Replace usage of + * `include_ember_script_tags` + * `include_ember_stylesheet_tags` + + with `render_ember_app` invocations. + + To learn more, please read: + + * https://github.com/thoughtbot/ember-cli-rails#configuring-the-ember-controller + * https://github.com/thoughtbot/ember-cli-rails/pull/316 + MSG + end + end end end diff --git a/lib/ember_cli/app.rb b/lib/ember_cli/app.rb index 6ad2129b..6ef86376 100644 --- a/lib/ember_cli/app.rb +++ b/lib/ember_cli/app.rb @@ -34,6 +34,7 @@ def compile @shell.compile @build.check! copy_index_html_file + sprockets.update_manifest! true end end @@ -82,6 +83,7 @@ def build_and_watch prepare @shell.build_and_watch copy_index_html_file + sprockets.update_manifest! end def prepare diff --git a/lib/ember_cli/assets.rb b/lib/ember_cli/assets.rb new file mode 100644 index 00000000..fe75d693 --- /dev/null +++ b/lib/ember_cli/assets.rb @@ -0,0 +1,56 @@ +module EmberCli + class Assets + def initialize(app_name:, ember_app_name:, manifest:) + @app_name = app_name + @ember_app_name = ember_app_name + @manifest = manifest + end + + def javascripts + if empty_manifest? + fallback_assets + else + [ + latest_matching(%r{#{app_name}/assets/vendor(.*)\.js\z}), + latest_matching(%r{#{app_name}/assets/#{ember_app_name}(.*)\.js\z}), + ] + end + end + + def stylesheets + if empty_manifest? + fallback_assets + else + [ + latest_matching(%r{#{app_name}/assets/vendor(.*)\.css\z}), + latest_matching(%r{#{app_name}/assets/#{ember_app_name}(.*)\.css\z}), + ] + end + end + + private + + attr_reader :app_name, :ember_app_name, :manifest + + def fallback_assets + ["#{app_name}/assets/vendor", "#{app_name}/assets/#{ember_app_name}"] + end + + def empty_manifest? + files.empty? + end + + def latest_matching(regex) + file, = files. + select { |key, _| key =~ regex }. + sort_by { |_, data| data["mtime"] }. + last + + file + end + + def files + manifest.files + end + end +end diff --git a/lib/ember_cli/manifest.rb b/lib/ember_cli/manifest.rb new file mode 100644 index 00000000..451d5276 --- /dev/null +++ b/lib/ember_cli/manifest.rb @@ -0,0 +1,14 @@ +require "sprockets" + +module EmberCli + class Manifest + def initialize(environment, path) + @manifest = ::Sprockets::Manifest.new(environment, path) + end + + def merge_into!(other) + other.assets.merge!(@manifest.assets) + other.files.merge!(@manifest.files) + end + end +end diff --git a/lib/ember_cli/missing_manifest.rb b/lib/ember_cli/missing_manifest.rb new file mode 100644 index 00000000..11404af9 --- /dev/null +++ b/lib/ember_cli/missing_manifest.rb @@ -0,0 +1,14 @@ +module EmberCli + class MissingManifest + def merge_into!(*) + end + + def files + {} + end + + def assets + {} + end + end +end diff --git a/lib/ember_cli/path_set.rb b/lib/ember_cli/path_set.rb index 8597d5e3..fc68ceca 100644 --- a/lib/ember_cli/path_set.rb +++ b/lib/ember_cli/path_set.rb @@ -75,6 +75,10 @@ def build_error_file @build_error_file ||= tmp.join("error.txt") end + def manifest + manifests.first + end + def bower @bower ||= begin bower_path = app_options.fetch(:bower_path) { configuration.bower_path } @@ -109,6 +113,10 @@ def bundler delegate :name, :options, to: :app, prefix: true + def manifests + Pathname.glob(app_assets.join("assets", "manifest*.json")) + end + def default_root rails_root.join(app_name) end diff --git a/lib/ember_cli/sprockets.rb b/lib/ember_cli/sprockets.rb index ed8f20ad..fbc6c3e3 100644 --- a/lib/ember_cli/sprockets.rb +++ b/lib/ember_cli/sprockets.rb @@ -1,10 +1,15 @@ +require "sprockets" require "ember_cli/errors" require "non-stupid-digest-assets" require "ember_cli/html_page" +require "ember_cli/manifest" +require "ember_cli/missing_manifest" +require "ember_cli/assets" module EmberCli class Sprockets class AssetPipelineError < BuildError; end + def initialize(app) @app = app end @@ -14,6 +19,10 @@ def register! register_or_raise!(NonStupidDigestAssets.whitelist) end + def update_manifest! + ember_manifest.merge_into!(rails_manifest) + end + def index_html(head:, body:) html_page = HtmlPage.new( content: app.index_file.read, @@ -24,14 +33,26 @@ def index_html(head:, body:) html_page.render end - def assets - ["#{app.name}/assets/vendor", "#{app.name}/assets/#{ember_app_name}"] + def javascript_assets + assets.javascripts + end + + def stylesheet_assets + assets.stylesheets end private attr_reader :app + def assets + Assets.new( + app_name: app.name, + ember_app_name: ember_app_name, + manifest: rails_manifest, + ) + end + def ember_app_name @ember_app_name ||= app.options.fetch(:name) { package_json.fetch(:name) } end @@ -41,6 +62,28 @@ def package_json JSON.parse(app.paths.package_json_file.read).with_indifferent_access end + def ember_manifest + @ember_manifest ||= begin + if ember_manifest_path.present? && ember_manifest_path.exist? + Manifest.new(Rails.env, ember_manifest_path) + else + MissingManifest.new + end + end + end + + def ember_manifest_path + app.paths.manifest + end + + def rails_manifest + if Rails.application.respond_to?(:assets_manifest) + Rails.application.assets_manifest + else + MissingManifest.new + end + end + def asset_matcher %r{\A#{app.name}/} end diff --git a/spec/lib/ember_cli/app_spec.rb b/spec/lib/ember_cli/app_spec.rb index 6f9821e8..a3094af6 100644 --- a/spec/lib/ember_cli/app_spec.rb +++ b/spec/lib/ember_cli/app_spec.rb @@ -1,9 +1,17 @@ require "ember-cli-rails" describe EmberCli::App do + describe "#compile" do + it "exits with exit status of 0" do + passed = EmberCli["my-app"].compile + + expect(passed).to be true + end + end + describe "#test" do it "exits with exit status of 0" do - passed = silence_stream(STDOUT) { EmberCli["my-app"].test } + passed = EmberCli["my-app"].test expect(passed).to be true end diff --git a/spec/lib/ember_cli/assets_spec.rb b/spec/lib/ember_cli/assets_spec.rb new file mode 100644 index 00000000..2739a75c --- /dev/null +++ b/spec/lib/ember_cli/assets_spec.rb @@ -0,0 +1,101 @@ +require "ember_cli/assets" + +describe EmberCli::Assets do + describe "#javascripts" do + it "includes the most recent javascript build artifacts" do + manifest = build_manifest( + files: { + "not-a-match" => {}, + "foo/assets/bar-abc123.js" => { "mtime" => 1.day.ago.iso8601 }, + "foo/assets/bar-def456.js" => { "mtime" => 2.days.ago.iso8601 }, + "foo/assets/vendor-def456.js" => { "mtime" => 2.days.ago.iso8601 }, + "foo/assets/vendor-abc123.js" => { "mtime" => 1.day.ago.iso8601 }, + }, + ) + assets = build_assets( + app_name: "foo", + ember_app_name: "bar", + manifest: manifest, + ) + + javascripts = assets.javascripts + + expect(javascripts).to match_array([ + "foo/assets/bar-abc123.js", + "foo/assets/vendor-abc123.js", + ]) + end + + context "when the manifest is empty" do + it "falls back to the default assets" do + assets = build_assets( + manifest: build_empty_manifest, + app_name: "foo", + ember_app_name: "bar", + ) + + javascripts = assets.javascripts + + expect(javascripts).to match_array([ + "foo/assets/vendor", + "foo/assets/bar", + ]) + end + end + end + + describe "#stylesheets" do + it "includes the most recent stylesheet build artifacts" do + manifest = build_manifest( + files: { + "not-a-match" => {}, + "foo/assets/bar-def456.css" => { "mtime" => 2.days.ago.iso8601 }, + "foo/assets/bar-abc123.css" => { "mtime" => 1.day.ago.iso8601 }, + "foo/assets/vendor-abc123.css" => { "mtime" => 1.day.ago.iso8601 }, + "foo/assets/vendor-def456.css" => { "mtime" => 2.days.ago.iso8601 }, + }, + ) + assets = build_assets( + app_name: "foo", + ember_app_name: "bar", + manifest: manifest, + ) + + stylesheets = assets.stylesheets + + expect(stylesheets).to match_array([ + "foo/assets/bar-abc123.css", + "foo/assets/vendor-abc123.css", + ]) + end + + context "when the manifest is empty" do + it "falls back to the default assets" do + assets = build_assets( + manifest: build_empty_manifest, + app_name: "foo", + ember_app_name: "bar", + ) + + stylesheets = assets.stylesheets + + expect(stylesheets).to match_array([ + "foo/assets/vendor", + "foo/assets/bar", + ]) + end + end + end +end + +def build_assets(manifest: build_manifest, **options) + EmberCli::Assets.new(options.merge(manifest: manifest)) +end + +def build_manifest(files: {}) + double(files: files) +end + +def build_empty_manifest + build_manifest +end diff --git a/spec/lib/ember_cli/manifest_spec.rb b/spec/lib/ember_cli/manifest_spec.rb new file mode 100644 index 00000000..c1da11e5 --- /dev/null +++ b/spec/lib/ember_cli/manifest_spec.rb @@ -0,0 +1,27 @@ +require "ember_cli/manifest" + +describe EmberCli::Manifest do + describe "#merge_into!" do + it "merges the manifest into the provided manifest" do + manifest_file = create_json_file( + "assets" => { "asset" => "path/to/asset.js" }, + "files" => { "file" => "path/to/file.js" }, + ) + manifest = EmberCli::Manifest.new("test", manifest_file.path) + rails_manifest = double(assets: {}, files: {}) + + manifest.merge_into!(rails_manifest) + + expect(rails_manifest.assets).to eq("asset" => "path/to/asset.js") + expect(rails_manifest.files).to eq("file" => "path/to/file.js") + end + end + + def create_json_file(json) + tempfile = Tempfile.new("manifest.json") + tempfile.write(JSON.dump(json)) + tempfile.close + + tempfile + end +end diff --git a/spec/lib/ember_cli/path_set_spec.rb b/spec/lib/ember_cli/path_set_spec.rb index 1a74fc09..7414d474 100644 --- a/spec/lib/ember_cli/path_set_spec.rb +++ b/spec/lib/ember_cli/path_set_spec.rb @@ -61,6 +61,21 @@ end end + describe "#manifest" do + it "is a hashed child of #app_assets" do + manifest_path = ember_cli_root.join( + "assets", + "bar", + "assets", + "manifest-abc123.json", + ) + create_file(manifest_path) + path_set = build_path_set(app: double(name: "bar")) + + expect(path_set.manifest).to eq(manifest_path) + end + end + describe "#app_assets" do it "is a child of #assets" do app = double(name: "bar") @@ -197,6 +212,12 @@ def create_executable(path) path end + def create_file(path) + path.parent.mkpath + FileUtils.touch(path) + path + end + def build_app(**options) double( options.reverse_merge( diff --git a/spec/lib/ember_cli/sprockets_spec.rb b/spec/lib/ember_cli/sprockets_spec.rb new file mode 100644 index 00000000..7bd66e9a --- /dev/null +++ b/spec/lib/ember_cli/sprockets_spec.rb @@ -0,0 +1,48 @@ +require "ember_cli/sprockets" + +describe EmberCli::Sprockets do + describe "#update_manifest!" do + it "merges the EmberCLI-generated manifest with Sprockets' manifest" do + assets_manifest = stub_rails_manifest("assets" => {}, "files" => {}) + ember_manifest = create_manifest_file( + "assets" => { "foo.js" => "foo.js" }, + "files" => { "foo.js" => "foo.js" }, + ) + + path_set = double(manifest: ember_manifest) + sprockets = build_sprockets(path_set) + + sprockets.update_manifest! + + expect(assets_manifest).to have_attributes( + assets: { "foo.js" => "foo.js" }, + files: { "foo.js" => "foo.js" }, + ) + end + end + + def build_sprockets(path_set) + app = double(paths: path_set) + + EmberCli::Sprockets.new(app) + end + + def stub_rails_manifest(manifest_hash) + assets_manifest = Sprockets::Manifest.new( + "test", + create_manifest_file(manifest_hash), + ) + application = double(assets_manifest: assets_manifest) + allow(Rails).to receive(:application).and_return(application) + + assets_manifest + end + + def create_manifest_file(json) + tempfile = Tempfile.new("manifest.json") + tempfile.write(JSON.dump(json)) + tempfile.close + + Pathname(tempfile.path) + end +end