From b582714965b96b735489c71077b569e986c31a8a Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 7 Sep 2026 11:59:41 +0000 Subject: [PATCH] Pin the Heroku build's NodeJS version to what the app requires MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `package.json` generated for Heroku carried only `cacheDirectories`. Heroku's NodeJS buildpack reads that file — the project root's, not the Ember application's — to decide which NodeJS to build with, and falls back to the current LTS release when it names no `engines.node`. The requirement belongs to the Ember application: the `ember-cli >= 6.8` blueprint needs NodeJS `>= 20.19.0`, and nothing carried that up to the file Heroku actually consults, so `rake ember:compile` could run on a NodeJS the application rejects. Read `engines.node` from each configured application and write the one they agree on. Applications that disagree are reported and left unpinned rather than resolved: one build serves all of them, and picking a single answer would pin the build to a version the others reject. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014rAfkAVGVifbaEoTVeT66u --- CHANGELOG.md | 5 ++ lib/ember_cli/app.rb | 12 +++++ lib/ember_cli/path_set.rb | 4 ++ .../ember/heroku/heroku_generator.rb | 20 ++++++++ .../ember/heroku/templates/package.json.erb | 5 ++ .../ember/heroku/heroku_generator_spec.rb | 48 +++++++++++++++++++ spec/lib/ember_cli/app_spec.rb | 36 ++++++++++++++ 7 files changed, 130 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b7860353..43049c96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ main natively since `5.0`, and every Rails version this gem supports is `>= 5.2`. To upgrade, remove `rails_12factor` from your project's `Gemfile` * Replace `EmberCli::App#yarn_enabled?` with `EmberCli::App#yarn?` +* Pin the NodeJS version in the `package.json` that `rails generate + ember:heroku` writes, from the `engines.node` the Ember applications + declare. Applications that declare different versions are left unpinned +* Add `EmberCli::App#node_engine`, reading `engines.node` from the Ember + application's `package.json` 0.13.2 ------ diff --git a/lib/ember_cli/app.rb b/lib/ember_cli/app.rb index 0349f3a2..9745d06a 100644 --- a/lib/ember_cli/app.rb +++ b/lib/ember_cli/app.rb @@ -1,3 +1,5 @@ +require "json" + require "html_page/renderer" require "ember_cli/path_set" require "ember_cli/shell" @@ -99,6 +101,16 @@ def bower? paths.bower_json.exist? end + def node_engine + package_json = paths.package_json + + if package_json.exist? + JSON.parse(package_json.read).dig("engines", "node") + end + rescue JSON::ParserError + nil + end + def to_rack deploy.to_rack end diff --git a/lib/ember_cli/path_set.rb b/lib/ember_cli/path_set.rb index b14a2c5b..e3936880 100644 --- a/lib/ember_cli/path_set.rb +++ b/lib/ember_cli/path_set.rb @@ -35,6 +35,10 @@ def gemfile @gemfile ||= root.join("Gemfile") end + def package_json + @package_json ||= root.join("package.json") + end + def bower_json root.join("bower.json") end diff --git a/lib/generators/ember/heroku/heroku_generator.rb b/lib/generators/ember/heroku/heroku_generator.rb index 36717e35..9378096e 100644 --- a/lib/generators/ember/heroku/heroku_generator.rb +++ b/lib/generators/ember/heroku/heroku_generator.rb @@ -16,6 +16,26 @@ def identify_as_yarn_project private + def node_engine + return @node_engine if defined?(@node_engine) + + declared = apps.map(&:node_engine).compact.uniq + + @node_engine = + if declared.size > 1 + say_status( + :conflict, + "Ember applications declare different `engines.node` " \ + "(#{declared.join(", ")}); pin one in package.json by hand", + :red, + ) + + nil + else + declared.first + end + end + def cache_directories all_cached_directories.map do |cached_directory| cached_directory.relative_path_from(Rails.root).to_s diff --git a/lib/generators/ember/heroku/templates/package.json.erb b/lib/generators/ember/heroku/templates/package.json.erb index a029a9cb..f9845a0c 100644 --- a/lib/generators/ember/heroku/templates/package.json.erb +++ b/lib/generators/ember/heroku/templates/package.json.erb @@ -4,5 +4,10 @@ "bower": "*" }, <% end %> + <% if node_engine %> + "engines": { + "node": <%= node_engine.to_json %> + }, + <% end %> "cacheDirectories": <%= cache_directories.to_json %> } diff --git a/spec/generators/ember/heroku/heroku_generator_spec.rb b/spec/generators/ember/heroku/heroku_generator_spec.rb index 51d86442..0b07423f 100644 --- a/spec/generators/ember/heroku/heroku_generator_spec.rb +++ b/spec/generators/ember/heroku/heroku_generator_spec.rb @@ -82,6 +82,54 @@ def gemfile_contents end end + describe "engines" do + it "pins the NodeJS version the Ember applications declare" do + setup_destination + configure_applications(node_engine: ">= 20.19.0") + + run_generator + + expect(package_json.fetch("engines")).to eq("node" => ">= 20.19.0") + end + + it "omits engines when no Ember application declares one" do + setup_destination + configure_applications(node_engine: nil) + + run_generator + + expect(package_json.keys).not_to include("engines") + end + + it "omits engines when the Ember applications disagree" do + setup_destination + configure_applications( + { node_engine: ">= 20.19.0" }, + { node_engine: ">= 22.0.0" }, + ) + + run_generator + + expect(package_json.keys).not_to include("engines") + end + + def configure_applications(*attributes) + apps = attributes.map do |app_attributes| + instance_double( + EmberCli::App, + { + bower?: false, + cached_directories: [], + yarn?: false, + }.merge(app_attributes), + ) + end + + allow(EmberCli).to receive(:apps). + and_return(apps.map.with_index { |app, i| ["app-#{i}", app] }.to_h) + end + end + def depend_on_bower(bower_enabled) allow_any_instance_of(EmberCli::App). to receive(:bower?).and_return(bower_enabled) diff --git a/spec/lib/ember_cli/app_spec.rb b/spec/lib/ember_cli/app_spec.rb index 3d906ab6..0ead0291 100644 --- a/spec/lib/ember_cli/app_spec.rb +++ b/spec/lib/ember_cli/app_spec.rb @@ -93,6 +93,42 @@ end end + describe "#node_engine" do + it "reads engines.node from the application's package.json" do + stub_package_json('{"engines":{"node":">= 20.19.0"}}') + app = EmberCli::App.new("with node engine") + + expect(app.node_engine).to eq ">= 20.19.0" + end + + it "returns nil when the package.json declares no engines" do + stub_package_json('{"name":"frontend"}') + app = EmberCli::App.new("without engines") + + expect(app.node_engine).to be_nil + end + + it "returns nil when the package.json is absent" do + stub_paths(package_json: double("Pathname", exist?: false)) + app = EmberCli::App.new("without package json") + + expect(app.node_engine).to be_nil + end + + it "returns nil when the package.json is malformed" do + stub_package_json("{") + app = EmberCli::App.new("with malformed package json") + + expect(app.node_engine).to be_nil + end + + def stub_package_json(contents) + stub_paths( + package_json: double("Pathname", exist?: true, read: contents), + ) + end + end + describe "#compile" do it "exits with exit status of 0" do passed = EmberCli["my-app"].compile