From 91f7fbec861e88acbe60e0da671cbb0d8665bd0a Mon Sep 17 00:00:00 2001 From: Gareth Jones Date: Fri, 27 Oct 2023 15:38:33 +1300 Subject: [PATCH] feat: support other js package managers (#349) * feat: use `package_json` to manage `package.json` * fix: add `require` back maybe? * fix: use `read` and leave the package manager to `package_json` (as it now has an env variable) * fix: update `PackageJson` call * fix: don't check yarn anymore * fix: use bang method & exit early on error * fix: use `package_json` rather than `yarn bin` to exec webpack * fix: replace `check_yarn` with `check_manager` task * fix: show general package manager name & version in info task * refactor: make runners use of `PackageJson` opt-in * refactor: make rake tasks use of `PackageJson` opt-in * refactor: restore `check_yarn` rake task, based on opt-in flag for `PackageJson` * refactor: make use of `PackageJson` in template tasks opt-in * fix: only use `npm` for getting node package version if not using `package_json` gem * fix: preserve old load paths when requiring `package-json` gem It seems that requires which happen after this are mucked up due to the load paths having been changed - this really only seems to impact RSpec which requires the diff library lazily * fix: read `package.json` within app path * fix: address minor bugs discovered from adding specs for when `package_json` is enabled * test: update generator specs * ci: run tests both with and without using `package_json` gem to ensure everything is valid * docs: include a section about `SHAKAPACKER_USE_PACKAGE_JSON_GEM` * fix: properly set babel preset for react * feat: support bun * feat: properly include `package_json` as a dependency * ci: run generator specs with and without `package_json` gem in parallel * docs: add changelog entry --- .github/workflows/generator.yml | 3 + .../workflows/ruby-backward-compatibility.yml | 6 + .github/workflows/ruby.yml | 7 + CHANGELOG.md | 3 + README.md | 15 +- lib/install/template.rb | 72 +++++-- lib/shakapacker/dev_server_runner.rb | 16 +- lib/shakapacker/runner.rb | 20 +- lib/shakapacker/utils/misc.rb | 12 ++ lib/shakapacker/webpack_runner.rb | 17 +- lib/tasks/shakapacker.rake | 4 +- lib/tasks/shakapacker/binstubs.rake | 2 +- lib/tasks/shakapacker/check_manager.rake | 27 +++ lib/tasks/shakapacker/check_yarn.rake | 3 +- lib/tasks/shakapacker/info.rake | 23 ++- lib/tasks/shakapacker/install.rake | 2 +- lib/tasks/shakapacker/verify_install.rake | 2 +- lib/tasks/webpacker/check_yarn.rake | 2 +- shakapacker.gemspec | 1 + .../dev_server_runner_spec.rb | 120 +++++++++--- .../engine_rake_tasks_spec.rb | 36 +++- .../webpack_runner_spec.rb | 54 ++++- spec/generator_specs/e2e_template/template.rb | 41 +++- spec/generator_specs/fake-bin/bun | 10 + spec/generator_specs/fake-bin/npm | 10 + spec/generator_specs/fake-bin/pnpm | 10 + spec/generator_specs/fake-bin/yarn | 10 + spec/generator_specs/generator_spec.rb | 184 ++++++++++++++++-- spec/mounted_app/package.json | 1 + spec/shakapacker/dev_server_runner_spec.rb | 117 ++++++++--- spec/shakapacker/engine_rake_tasks_spec.rb | 33 +++- spec/shakapacker/rake_tasks_spec.rb | 11 +- spec/shakapacker/webpack_runner_spec.rb | 54 ++++- spec/spec_helper.rb | 2 + spec/support/package_json_helpers.rb | 16 ++ 35 files changed, 784 insertions(+), 162 deletions(-) create mode 100644 lib/tasks/shakapacker/check_manager.rake create mode 100755 spec/generator_specs/fake-bin/bun create mode 100755 spec/generator_specs/fake-bin/npm create mode 100755 spec/generator_specs/fake-bin/pnpm create mode 100755 spec/generator_specs/fake-bin/yarn create mode 100644 spec/mounted_app/package.json create mode 100644 spec/support/package_json_helpers.rb diff --git a/.github/workflows/generator.yml b/.github/workflows/generator.yml index b460a43d6..676888f3c 100644 --- a/.github/workflows/generator.yml +++ b/.github/workflows/generator.yml @@ -10,6 +10,7 @@ jobs: matrix: os: [ubuntu-latest] ruby: ['2.6', '2.7', '3.0'] + use_package_json_gem: ['true', 'false'] gemfile: - gemfiles/Gemfile-rails.6.0.x - gemfiles/Gemfile-rails.6.1.x @@ -37,3 +38,5 @@ jobs: - name: Install dependencies run: bundle install - run: bundle exec rake run_spec:generator + env: + SHAKAPACKER_USE_PACKAGE_JSON_GEM: ${{ matrix.use_package_json_gem }} diff --git a/.github/workflows/ruby-backward-compatibility.yml b/.github/workflows/ruby-backward-compatibility.yml index 2a40fb730..58b844e4c 100644 --- a/.github/workflows/ruby-backward-compatibility.yml +++ b/.github/workflows/ruby-backward-compatibility.yml @@ -39,3 +39,9 @@ jobs: - name: Ruby specs - Backward compatibility run: bundle exec rake run_spec:gem_bc + env: + SHAKAPACKER_USE_PACKAGE_JSON_GEM: "false" + - name: Ruby specs - Backward compatibility + run: bundle exec rake run_spec:gem_bc + env: + SHAKAPACKER_USE_PACKAGE_JSON_GEM: "true" diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index b2624453e..641e5fc63 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -39,3 +39,10 @@ jobs: - name: Ruby specs run: bundle exec rake run_spec:gem + env: + SHAKAPACKER_USE_PACKAGE_JSON_GEM: "false" + + - name: Ruby specs + run: bundle exec rake run_spec:gem + env: + SHAKAPACKER_USE_PACKAGE_JSON_GEM: "true" diff --git a/CHANGELOG.md b/CHANGELOG.md index 731dab5fb..df18a74f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ Changes since the last non-beta release. _Please add entries here for your pull requests that are not yet released._ +### Added +- Experimental support for other JS package managers using `package_json` gem [PR 349](https://github.com/shakacode/shakapacker/pull/349) by [G-Rath](https://github.com/g-rath). + ## [v7.1.0] - September 30, 2023 ### Added diff --git a/README.md b/README.md index 40b507f64..485198fa2 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,7 @@ Read the [full review here](https://clutch.co/profile/shakacode#reviews?sort_by= - [Optional support](#optional-support) - [Installation](#installation) - [Rails v6+](#rails-v6) + - [Using alternative package managers](#using-alternative-package-managers) - [Note for Yarn v2 usage](#note-for-yarn-v2-usage) - [Concepts](#concepts) - [Usage](#usage) @@ -163,9 +164,21 @@ yarn add @babel/core @babel/plugin-transform-runtime @babel/preset-env @babel/ru Previously, these "webpack" and "babel" packages were direct dependencies for `shakapacker`. By making these peer dependencies, you have control over the versions used in your webpack and babel configs. +### Using alternative package managers + +There is experimental support for using package managers besides Yarn classic for managing JavaScript dependencies using the [`package_json`](https://github.com/G-Rath/package_json) gem. + +This can be enabled by setting the environment variable `SHAKAPACKER_USE_PACKAGE_JSON_GEM` to `true`; Shakapacker will then use the `package_json` gem which in turn will look for the [`packageManager`](https://nodejs.org/api/packages.html#packagemanager) property in the `package.json` or otherwise the `PACKAGE_JSON_FALLBACK_MANAGER` environment variable to determine which manager to use, defaulting to `npm` if neither are found. + +See [here](https://github.com/G-Rath/package_json#specifying-a-package-manager) for a list of the supported package managers and more information; note that `package_json` does not handle ensuring the manager is installed. + +> **Note** +> +> The rest of the documentation assumes that `package_json` is not being used, and so always references `yarn` - you should instead use the package manager of your choice for these commands. + ### Note for Yarn v2 usage -If you are using Yarn v2 (berry), please note that PnP modules are not supported. +If you are using Yarn v2 (berry), please note that PnP modules are not supported unless you're using `SHAKAPACKER_USE_PACKAGE_JSON_GEM`. To use Shakapacker with Yarn v2, make sure you set `nodeLinker: node-modules` in your `.yarnrc.yml` file as per the [Yarn docs](https://yarnpkg.com/getting-started/migration#step-by-step) to opt out of Plug'n'Play behavior. diff --git a/lib/install/template.rb b/lib/install/template.rb index ecf73696f..0e1e3cfb8 100644 --- a/lib/install/template.rb +++ b/lib/install/template.rb @@ -1,3 +1,4 @@ +require "shakapacker/utils/misc" require "shakapacker/utils/version_syntax_converter" # Install Shakapacker @@ -41,12 +42,28 @@ say %( Add <%= javascript_pack_tag "application" %> within the tag in your custom layout.) end +def package_json + if @package_json.nil? + Shakapacker::Utils::Misc.require_package_json_gem + + @package_json = PackageJson.read + end + + @package_json +end + # Ensure there is `system!("bin/yarn")` command in `./bin/setup` file if (setup_path = Rails.root.join("bin/setup")).exist? - say "Run bin/yarn during bin/setup" + def native_install_command + return "bin/yarn" unless Shakapacker::Utils::Misc.use_package_json_gem + + package_json.manager.native_install_command.join(" ") + end + + say "Run #{native_install_command} during bin/setup" if File.read(setup_path).match? Regexp.escape(" # system('bin/yarn')\n") - gsub_file(setup_path, "# system('bin/yarn')", "system!('bin/yarn')") + gsub_file(setup_path, "# system('bin/yarn')", "system!('#{native_install_command}')") else # Due to the inconsistency of quotation usage in Rails 7 compared to # earlier versions, we check both single and double quotations here. @@ -55,30 +72,58 @@ string_to_add = <<-RUBY # Install JavaScript dependencies - system!("bin/yarn") -RUBY + system!("#{native_install_command}") + RUBY if File.read(setup_path).match? pattern insert_into_file(setup_path, string_to_add, after: pattern) else say <<~MSG, :red It seems your `bin/setup` file doesn't have the expected content. - Please review the file and manually add `system!("bin/yarn")` before any + Please review the file and manually add `system!("#{native_install_command}")` before any other command that requires JavaScript dependencies being already installed. MSG end end end -results = [] +def add_dependencies(dependencies, type) + return package_json.manager.add!(dependencies, type: type) if Shakapacker::Utils::Misc.use_package_json_gem + + # TODO: check that run actually errors + run("yarn add #{dependencies.join(" ")}") if type == :production + run("yarn add --dev #{dependencies.join(" ")}") if type == :dev +rescue PackageJson::Error + say "Shakapacker installation failed 😭 See above for details.", :red + exit 1 +end + +def fetch_peer_dependencies + if Shakapacker::Utils::Misc.use_package_json_gem + return PackageJson.read("#{__dir__}/../../").fetch("peerDependencies") + end + + package_json = File.read("#{__dir__}/../../package.json") + JSON.parse(package_json)["peerDependencies"] +end Dir.chdir(Rails.root) do npm_version = Shakapacker::Utils::VersionSyntaxConverter.new.rubygem_to_npm(Shakapacker::VERSION) say "Installing shakapacker@#{npm_version}" - results << run("yarn add shakapacker@#{npm_version} --exact") + add_dependencies(["shakapacker@#{npm_version}"], :production) + + if Shakapacker::Utils::Misc.use_package_json_gem + package_json.merge! do |pj| + { + "dependencies" => pj["dependencies"].merge({ + # TODO: workaround for test suite - long-run need to actually account for diff pkg manager behaviour + "shakapacker" => pj["dependencies"]["shakapacker"].delete_prefix("^") + }) + } + end + end - package_json = File.read("#{__dir__}/../../package.json") - peers = JSON.parse(package_json)["peerDependencies"] + peers = fetch_peer_dependencies dev_dependency_packages = ["webpack-dev-server"] dependencies_to_add = [] @@ -96,13 +141,8 @@ end say "Adding shakapacker peerDependencies" - results << run("yarn add #{dependencies_to_add.join(' ')}") + add_dependencies(dependencies_to_add, :production) say "Installing webpack-dev-server for live reloading as a development dependency" - results << run("yarn add --dev #{dev_dependencies_to_add.join(' ')}") -end - -unless results.all? - say "Shakapacker installation failed 😭 See above for details.", :red - exit 1 + add_dependencies(dev_dependencies_to_add, :dev) end diff --git a/lib/shakapacker/dev_server_runner.rb b/lib/shakapacker/dev_server_runner.rb index 5133c2b34..6684a0f7a 100644 --- a/lib/shakapacker/dev_server_runner.rb +++ b/lib/shakapacker/dev_server_runner.rb @@ -68,11 +68,7 @@ def execute_cmd env["WEBPACK_SERVE"] = "true" env["NODE_OPTIONS"] = ENV["NODE_OPTIONS"] || "" - cmd = if node_modules_bin_exist? - ["#{@node_modules_bin_path}/webpack", "serve"] - else - ["yarn", "webpack", "serve"] - end + cmd = build_cmd if @argv.include?("--debug-webpacker") Shakapacker.puts_deprecation_message( @@ -98,6 +94,16 @@ def execute_cmd end end + def build_cmd + if Shakapacker::Utils::Misc.use_package_json_gem + return package_json.manager.native_exec_command("webpack", ["serve"]) + end + + return ["#{@node_modules_bin_path}/webpack", "serve"] if node_modules_bin_exist? + + ["yarn", "webpack", "serve"] + end + def node_modules_bin_exist? File.exist?("#{@node_modules_bin_path}/webpack-dev-server") end diff --git a/lib/shakapacker/runner.rb b/lib/shakapacker/runner.rb index 5c8074375..be66a5d09 100644 --- a/lib/shakapacker/runner.rb +++ b/lib/shakapacker/runner.rb @@ -1,3 +1,5 @@ +require "shakapacker/utils/misc" + module Shakapacker class Runner def self.run(argv) @@ -14,7 +16,7 @@ def initialize(argv) Shakapacker.set_shakapacker_env_variables_for_backward_compatibility - @node_modules_bin_path = ENV["SHAKAPACKER_NODE_MODULES_BIN_PATH"] || `yarn bin`.chomp + @node_modules_bin_path = fetch_node_modules_bin_path @shakapacker_config = ENV["SHAKAPACKER_CONFIG"] || File.join(@app_path, "config/shakapacker.yml") @shakapacker_config = Shakapacker.get_config_file_path_with_backward_compatibility(@shakapacker_config) @@ -24,5 +26,21 @@ def initialize(argv) exit! end end + + def fetch_node_modules_bin_path + return nil if Shakapacker::Utils::Misc.use_package_json_gem + + ENV["SHAKAPACKER_NODE_MODULES_BIN_PATH"] || `yarn bin`.chomp + end + + def package_json + if @package_json.nil? + Shakapacker::Utils::Misc.require_package_json_gem + + @package_json = PackageJson.read(@app_path) + end + + @package_json + end end end diff --git a/lib/shakapacker/utils/misc.rb b/lib/shakapacker/utils/misc.rb index c46f60947..9b151a26f 100644 --- a/lib/shakapacker/utils/misc.rb +++ b/lib/shakapacker/utils/misc.rb @@ -8,6 +8,18 @@ module Utils class Misc extend FileUtils + def self.use_package_json_gem + ENV.fetch("SHAKAPACKER_USE_PACKAGE_JSON_GEM", "false").casecmp("true").zero? + end + + def self.require_package_json_gem + unless use_package_json_gem + raise "PackageJson should not be used unless SHAKAPACKER_USE_PACKAGE_JSON_GEM is true" + end + + require "package_json" + end + def self.uncommitted_changes?(message_handler) return false if ENV["COVERAGE"] == "true" diff --git a/lib/shakapacker/webpack_runner.rb b/lib/shakapacker/webpack_runner.rb index 08f4fae00..a7fcfb2bf 100644 --- a/lib/shakapacker/webpack_runner.rb +++ b/lib/shakapacker/webpack_runner.rb @@ -21,11 +21,7 @@ def run env["SHAKAPACKER_CONFIG"] = @shakapacker_config env["NODE_OPTIONS"] = ENV["NODE_OPTIONS"] || "" - cmd = if node_modules_bin_exist? - ["#{@node_modules_bin_path}/webpack"] - else - ["yarn", "webpack"] - end + cmd = build_cmd if @argv.include?("--debug-webpacker") Shakapacker.puts_deprecation_message( @@ -61,6 +57,17 @@ def run end private + + def build_cmd + if Shakapacker::Utils::Misc.use_package_json_gem + return package_json.manager.native_exec_command("webpack") + end + + return ["#{@node_modules_bin_path}/webpack"] if node_modules_bin_exist? + + ["yarn", "webpack"] + end + def node_modules_bin_exist? File.exist?("#{@node_modules_bin_path}/webpack") end diff --git a/lib/tasks/shakapacker.rake b/lib/tasks/shakapacker.rake index 032e9e419..373330241 100644 --- a/lib/tasks/shakapacker.rake +++ b/lib/tasks/shakapacker.rake @@ -1,11 +1,11 @@ tasks = { "shakapacker:info" => "Provides information on Shakapacker's environment", - "shakapacker:install" => "Installs and setup webpack with Yarn", + "shakapacker:install" => "Installs and setup webpack", "shakapacker:compile" => "Compiles webpack bundles based on environment", "shakapacker:clean" => "Remove old compiled bundles", "shakapacker:clobber" => "Removes the webpack compiled output directory", "shakapacker:check_node" => "Verifies if Node.js is installed", - "shakapacker:check_yarn" => "Verifies if Yarn is installed", + "shakapacker:check_manager" => "Verifies if the expected JS package manager is available", "shakapacker:check_binstubs" => "Verifies that bin/shakapacker is present", "shakapacker:binstubs" => "Installs Shakapacker binstubs in this application", "shakapacker:verify_install" => "Verifies if Shakapacker is installed", diff --git a/lib/tasks/shakapacker/binstubs.rake b/lib/tasks/shakapacker/binstubs.rake index 4a32f49a5..3c29be2cb 100644 --- a/lib/tasks/shakapacker/binstubs.rake +++ b/lib/tasks/shakapacker/binstubs.rake @@ -3,7 +3,7 @@ bin_path = ENV["BUNDLE_BIN"] || Rails.root.join("bin") namespace :shakapacker do desc "Installs Shakapacker binstubs in this application" - task binstubs: [:check_node, :check_yarn] do |task| + task binstubs: [:check_node, :check_manager] do |task| prefix = task.name.split(/#|shakapacker:binstubs/).first if Rails::VERSION::MAJOR >= 5 diff --git a/lib/tasks/shakapacker/check_manager.rake b/lib/tasks/shakapacker/check_manager.rake new file mode 100644 index 000000000..7bc543da6 --- /dev/null +++ b/lib/tasks/shakapacker/check_manager.rake @@ -0,0 +1,27 @@ +require "shakapacker/utils/misc" + +namespace :shakapacker do + desc "Verifies if the expected JS package manager is installed" + task :check_manager do |task| + unless Shakapacker::Utils::Misc.use_package_json_gem + prefix = task.name.split(/#|shakapacker:/).first + Rake::Task["#{prefix}shakapacker:check_manager"].invoke + next + end + + require "package_json" + + package_json = PackageJson.read + pm = package_json.manager.binary + + begin + version = package_json.manager.version + + $stdout.puts "using #{pm}@#{version} to manage dependencies and scripts in package.json" + rescue PackageJson::Error + $stderr.puts "#{pm} not installed - please ensure it is installed before trying again" + $stderr.puts "Exiting!" + exit! + end + end +end diff --git a/lib/tasks/shakapacker/check_yarn.rake b/lib/tasks/shakapacker/check_yarn.rake index d0ef257ad..3b9ea98e2 100644 --- a/lib/tasks/shakapacker/check_yarn.rake +++ b/lib/tasks/shakapacker/check_yarn.rake @@ -1,7 +1,8 @@ -require "semantic_range" namespace :shakapacker do desc "Verifies if Yarn is installed" task :check_yarn do + require "semantic_range" + begin which_command = Gem.win_platform? ? "where" : "which" raise Errno::ENOENT if `#{which_command} yarn`.strip.empty? diff --git a/lib/tasks/shakapacker/info.rake b/lib/tasks/shakapacker/info.rake index e5be3c867..903bbf4ba 100644 --- a/lib/tasks/shakapacker/info.rake +++ b/lib/tasks/shakapacker/info.rake @@ -1,4 +1,5 @@ require "shakapacker/version" +require "shakapacker/utils/misc" namespace :shakapacker do desc "Provide information on Shakapacker's environment" @@ -8,14 +9,30 @@ namespace :shakapacker do $stdout.puts "Rails: #{Rails.version}" $stdout.puts "Shakapacker: #{Shakapacker::VERSION}" $stdout.puts "Node: #{`node --version`}" - $stdout.puts "Yarn: #{`yarn --version`}" + if Shakapacker::Utils::Misc.use_package_json_gem + require "package_json" + + pj_manager = PackageJson.read.manager + + $stdout.puts "#{pj_manager.binary}: #{pj_manager.version}" + else + $stdout.puts "Yarn: #{`yarn --version`}" + end + + if Shakapacker::Utils::Misc.use_package_json_gem + node_package_version = Shakapacker::VersionChecker.build.node_package_version.raw + else + node_package_version = `npm list shakapacker version` + end $stdout.puts "\n" - $stdout.puts "shakapacker: \n#{`npm list shakapacker version`}" + $stdout.puts "shakapacker: #{node_package_version}" $stdout.puts "Is bin/shakapacker present?: #{File.exist? 'bin/shakapacker'}" $stdout.puts "Is bin/shakapacker-dev-server present?: #{File.exist? 'bin/shakapacker-dev-server'}" - $stdout.puts "Is bin/yarn present?: #{File.exist? 'bin/yarn'}" + unless Shakapacker::Utils::Misc.use_package_json_gem + $stdout.puts "Is bin/yarn present?: #{File.exist? 'bin/yarn'}" + end end end end diff --git a/lib/tasks/shakapacker/install.rake b/lib/tasks/shakapacker/install.rake index 04aa19a1f..46c18f539 100644 --- a/lib/tasks/shakapacker/install.rake +++ b/lib/tasks/shakapacker/install.rake @@ -3,7 +3,7 @@ bin_path = ENV["BUNDLE_BIN"] || Rails.root.join("bin") namespace :shakapacker do desc "Install Shakapacker in this application" - task install: [:check_node, :check_yarn] do |task| + task install: [:check_node] do |task| Shakapacker::Configuration.installing = true prefix = task.name.split(/#|shakapacker:install/).first diff --git a/lib/tasks/shakapacker/verify_install.rake b/lib/tasks/shakapacker/verify_install.rake index 843aaa16e..5070e9924 100644 --- a/lib/tasks/shakapacker/verify_install.rake +++ b/lib/tasks/shakapacker/verify_install.rake @@ -1,4 +1,4 @@ namespace :shakapacker do desc "Verifies if Shakapacker is installed" - task verify_install: [:verify_config, :check_node, :check_yarn, :check_binstubs] + task verify_install: [:verify_config, :check_node, :check_manager, :check_binstubs] end diff --git a/lib/tasks/webpacker/check_yarn.rake b/lib/tasks/webpacker/check_yarn.rake index d2dac6311..6be642018 100644 --- a/lib/tasks/webpacker/check_yarn.rake +++ b/lib/tasks/webpacker/check_yarn.rake @@ -4,6 +4,6 @@ namespace :webpacker do Shakapacker.puts_rake_deprecation_message(task.name) prefix = task.name.split(/#|webpacker:/).first - Rake::Task["#{prefix}shakapacker:check_yarn"].invoke + Rake::Task["#{prefix}shakapacker:check_manager"].invoke end end diff --git a/shakapacker.gemspec b/shakapacker.gemspec index b095a7b0d..ac1a26da9 100644 --- a/shakapacker.gemspec +++ b/shakapacker.gemspec @@ -18,6 +18,7 @@ Gem::Specification.new do |s| s.required_ruby_version = ">= 2.6.0" s.add_dependency "activesupport", ">= 5.2" + s.add_dependency "package_json" s.add_dependency "railties", ">= 5.2" s.add_dependency "rack-proxy", ">= 0.6.1" s.add_dependency "semantic_range", ">= 2.3.0" diff --git a/spec/backward_compatibility_specs/dev_server_runner_spec.rb b/spec/backward_compatibility_specs/dev_server_runner_spec.rb index 1b4cb4135..cb3e389f1 100644 --- a/spec/backward_compatibility_specs/dev_server_runner_spec.rb +++ b/spec/backward_compatibility_specs/dev_server_runner_spec.rb @@ -17,48 +17,108 @@ let(:test_app_path) { File.expand_path("webpacker_test_app", __dir__) } - it "supports running via node modules" do - cmd = ["#{test_app_path}/node_modules/.bin/webpack", "serve", "--config", "#{test_app_path}/config/webpack/webpack.config.js"] + NODE_PACKAGE_MANAGERS.each do |fallback_manager| + context "when using package_json with #{fallback_manager} as the manager" do + with_use_package_json_gem(enabled: true, fallback_manager: fallback_manager) - verify_command(cmd, use_node_modules: true) - end + let(:package_json) { PackageJson.read(test_app_path) } - it "supports running via yarn" do - cmd = ["yarn", "webpack", "serve", "--config", "#{test_app_path}/config/webpack/webpack.config.js"] + require "package_json" - verify_command(cmd, use_node_modules: false) - end + it "uses the expected package manager", unless: fallback_manager == "yarn_classic" do + cmd = package_json.manager.native_exec_command("webpack", ["serve", "--config", "#{test_app_path}/config/webpack/webpack.config.js"]) - it "passes on arguments" do - cmd = ["#{test_app_path}/node_modules/.bin/webpack", "serve", "--config", "#{test_app_path}/config/webpack/webpack.config.js", "--quiet"] + manager_name = fallback_manager.split("_")[0] - verify_command(cmd, argv: (["--quiet"])) - end + expect(cmd).to start_with(manager_name) + end + + it "runs the command using the manager" do + cmd = package_json.manager.native_exec_command("webpack", ["serve", "--config", "#{test_app_path}/config/webpack/webpack.config.js"]) + + verify_command(cmd) + end + + it "passes on arguments" do + cmd = package_json.manager.native_exec_command("webpack", ["serve", "--config", "#{test_app_path}/config/webpack/webpack.config.js", "--quiet"]) - it "supports the https flag" do - cmd = ["#{test_app_path}/node_modules/.bin/webpack", "serve", "--config", "#{test_app_path}/config/webpack/webpack.config.js", "--https"] + verify_command(cmd, argv: (["--quiet"])) + end + + it "supports the https flag" do + cmd = package_json.manager.native_exec_command("webpack", ["serve", "--config", "#{test_app_path}/config/webpack/webpack.config.js", "--https"]) + + dev_server = double() + allow(dev_server).to receive(:host).and_return("localhost") + allow(dev_server).to receive(:port).and_return("3035") + allow(dev_server).to receive(:pretty?).and_return(false) + allow(dev_server).to receive(:https?).and_return(true) + allow(dev_server).to receive(:hmr?).and_return(false) + + allow(Shakapacker::DevServer).to receive(:new) do + verify_command(cmd, argv: (["--https"])) + end.and_return(dev_server) + end + + it "accepts environment variables" do + cmd = package_json.manager.native_exec_command("webpack", ["serve", "--config", "#{test_app_path}/config/webpack/webpack.config.js"]) + env = Webpacker::Compiler.env.dup - dev_server = double() - allow(dev_server).to receive(:host).and_return("localhost") - allow(dev_server).to receive(:port).and_return("3035") - allow(dev_server).to receive(:pretty?).and_return(false) - allow(dev_server).to receive(:https?).and_return(true) - allow(dev_server).to receive(:hmr?).and_return(false) + # ENV["WEBPACKER_CONFIG"] is the interface and env["SHAKAPACKER_CONFIG"] is internal + ENV["WEBPACKER_CONFIG"] = env["SHAKAPACKER_CONFIG"] = "#{test_app_path}/config/webpacker_other_location.yml" + env["WEBPACK_SERVE"] = "true" - allow(Webpacker::DevServer).to receive(:new) do - verify_command(cmd, argv: (["--https"])) - end.and_return(dev_server) + verify_command(cmd, env: env) + end + end end - it "accepts environment variables" do - cmd = ["#{test_app_path}/node_modules/.bin/webpack", "serve", "--config", "#{test_app_path}/config/webpack/webpack.config.js"] - env = Webpacker::Compiler.env.dup + context "when not using package_json" do + with_use_package_json_gem(enabled: false) - # ENV["WEBPACKER_CONFIG"] is the interface and env["SHAKAPACKER_CONFIG"] is internal - ENV["WEBPACKER_CONFIG"] = env["SHAKAPACKER_CONFIG"] = "#{test_app_path}/config/webpacker_other_location.yml" - env["WEBPACK_SERVE"] = "true" + it "supports running via node modules" do + cmd = ["#{test_app_path}/node_modules/.bin/webpack", "serve", "--config", "#{test_app_path}/config/webpack/webpack.config.js"] - verify_command(cmd, env: env) + verify_command(cmd, use_node_modules: true) + end + + it "supports running via yarn" do + cmd = ["yarn", "webpack", "serve", "--config", "#{test_app_path}/config/webpack/webpack.config.js"] + + verify_command(cmd, use_node_modules: false) + end + + it "passes on arguments" do + cmd = ["#{test_app_path}/node_modules/.bin/webpack", "serve", "--config", "#{test_app_path}/config/webpack/webpack.config.js", "--quiet"] + + verify_command(cmd, argv: (["--quiet"])) + end + + it "supports the https flag" do + cmd = ["#{test_app_path}/node_modules/.bin/webpack", "serve", "--config", "#{test_app_path}/config/webpack/webpack.config.js", "--https"] + + dev_server = double() + allow(dev_server).to receive(:host).and_return("localhost") + allow(dev_server).to receive(:port).and_return("3035") + allow(dev_server).to receive(:pretty?).and_return(false) + allow(dev_server).to receive(:https?).and_return(true) + allow(dev_server).to receive(:hmr?).and_return(false) + + allow(Shakapacker::DevServer).to receive(:new) do + verify_command(cmd, argv: (["--https"])) + end.and_return(dev_server) + end + + it "accepts environment variables" do + cmd = ["#{test_app_path}/node_modules/.bin/webpack", "serve", "--config", "#{test_app_path}/config/webpack/webpack.config.js"] + env = Webpacker::Compiler.env.dup + + # ENV["WEBPACKER_CONFIG"] is the interface and env["SHAKAPACKER_CONFIG"] is internal + ENV["WEBPACKER_CONFIG"] = env["SHAKAPACKER_CONFIG"] = "#{test_app_path}/config/webpacker_other_location.yml" + env["WEBPACK_SERVE"] = "true" + + verify_command(cmd, env: env) + end end private diff --git a/spec/backward_compatibility_specs/engine_rake_tasks_spec.rb b/spec/backward_compatibility_specs/engine_rake_tasks_spec.rb index 347fb9de4..f5cba80ab 100644 --- a/spec/backward_compatibility_specs/engine_rake_tasks_spec.rb +++ b/spec/backward_compatibility_specs/engine_rake_tasks_spec.rb @@ -9,16 +9,38 @@ remove_webpack_binstubs end - it "mounts app:webpacker task successfully" do - output = Dir.chdir(mounted_app_path) { `rake -T` } + NODE_PACKAGE_MANAGERS.each do |fallback_manager| + context "when using package_json with #{fallback_manager} as the manager" do + with_use_package_json_gem(enabled: true, fallback_manager: fallback_manager) - expect(output).to match /app:webpacker.+DEPRECATED/ - expect(output).to match /app:webpacker:binstubs.+DEPRECATED/ + it "mounts app:webpacker task successfully" do + output = Dir.chdir(mounted_app_path) { `rake -T` } + + expect(output).to match /app:webpacker.+DEPRECATED/ + expect(output).to match /app:webpacker:binstubs.+DEPRECATED/ + end + + it "only adds expected files to bin directory when binstubs is run" do + Dir.chdir(mounted_app_path) { `bundle exec rake app:webpacker:binstubs` } + expected_binstub_paths.each { |path| expect(File.exist?(path)).to be true } + end + end end - it "only adds expected files to bin directory when binstubs is run" do - Dir.chdir(mounted_app_path) { `bundle exec rake app:webpacker:binstubs` } - expected_binstub_paths.each { |path| expect(File.exist?(path)).to be true } + context "when not using package_json" do + with_use_package_json_gem(enabled: false) + + it "mounts app:webpacker task successfully" do + output = Dir.chdir(mounted_app_path) { `rake -T` } + + expect(output).to match /app:webpacker.+DEPRECATED/ + expect(output).to match /app:webpacker:binstubs.+DEPRECATED/ + end + + it "only adds expected files to bin directory when binstubs is run" do + Dir.chdir(mounted_app_path) { `bundle exec rake app:webpacker:binstubs` } + expected_binstub_paths.each { |path| expect(File.exist?(path)).to be true } + end end private diff --git a/spec/backward_compatibility_specs/webpack_runner_spec.rb b/spec/backward_compatibility_specs/webpack_runner_spec.rb index 6f741f4f0..1e43e7e94 100644 --- a/spec/backward_compatibility_specs/webpack_runner_spec.rb +++ b/spec/backward_compatibility_specs/webpack_runner_spec.rb @@ -15,22 +15,56 @@ let(:test_app_path) { File.expand_path("./webpacker_test_app", __dir__) } - it "supports running via node_modules" do - cmd = ["#{test_app_path}/node_modules/.bin/webpack", "--config", "#{test_app_path}/config/webpack/webpack.config.js"] + NODE_PACKAGE_MANAGERS.each do |fallback_manager| + context "when using package_json with #{fallback_manager} as the manager" do + with_use_package_json_gem(enabled: true, fallback_manager: fallback_manager) - verify_command(cmd, use_node_modules: true) - end + let(:package_json) { PackageJson.read(test_app_path) } + + require "package_json" + + it "uses the expected package manager", unless: fallback_manager == "yarn_classic" do + cmd = package_json.manager.native_exec_command("webpack", ["--config", "#{test_app_path}/config/webpack/webpack.config.js"]) + + manager_name = fallback_manager.split("_")[0] + + expect(cmd).to start_with(manager_name) + end + + it "runs the command using the manager" do + cmd = package_json.manager.native_exec_command("webpack", ["--config", "#{test_app_path}/config/webpack/webpack.config.js"]) - it "supports running via yarn" do - cmd = ["yarn", "webpack", "--config", "#{test_app_path}/config/webpack/webpack.config.js"] + verify_command(cmd) + end - verify_command(cmd, use_node_modules: false) + it "passes on arguments" do + cmd = package_json.manager.native_exec_command("webpack", ["--config", "#{test_app_path}/config/webpack/webpack.config.js", "--watch"]) + + verify_command(cmd, argv: (["--watch"])) + end + end end - it "passes on arguments" do - cmd = ["#{test_app_path}/node_modules/.bin/webpack", "--config", "#{test_app_path}/config/webpack/webpack.config.js", "--watch"] + context "when not using package_json" do + with_use_package_json_gem(enabled: false) + + it "supports running via node_modules" do + cmd = ["#{test_app_path}/node_modules/.bin/webpack", "--config", "#{test_app_path}/config/webpack/webpack.config.js"] + + verify_command(cmd, use_node_modules: true) + end + + it "supports running via yarn" do + cmd = ["yarn", "webpack", "--config", "#{test_app_path}/config/webpack/webpack.config.js"] - verify_command(cmd, argv: ["--watch"]) + verify_command(cmd, use_node_modules: false) + end + + it "passes on arguments" do + cmd = ["#{test_app_path}/node_modules/.bin/webpack", "--config", "#{test_app_path}/config/webpack/webpack.config.js", "--watch"] + + verify_command(cmd, argv: ["--watch"]) + end end private diff --git a/spec/generator_specs/e2e_template/template.rb b/spec/generator_specs/e2e_template/template.rb index 9e135e2d6..109724da1 100644 --- a/spec/generator_specs/e2e_template/template.rb +++ b/spec/generator_specs/e2e_template/template.rb @@ -1,13 +1,34 @@ -# install react -system("yarn add react react-dom @babel/preset-react") - -# update webpack presets for react -package_json_path = Rails.root.join("./package.json") -insert_into_file( - package_json_path, - %( "@babel/preset-react",\n), - after: /"presets": \[\n/ -) +require "shakapacker/utils/misc" + +if Shakapacker::Utils::Misc.use_package_json_gem + Shakapacker::Utils::Misc.require_package_json_gem + + package_json = PackageJson.new + + # install react + package_json.manager.add(["react", "react-dom", "@babel/preset-react"]) + + # update webpack presets for react + package_json.merge! do |pj| + babel = pj.fetch("babel", {}) + + babel["presets"] ||= [] + babel["presets"].unshift("@babel/preset-react") + + { "babel" => babel } + end +else + # install react + system("yarn add react react-dom @babel/preset-react") + + # update webpack presets for react + package_json_path = Rails.root.join("./package.json") + insert_into_file( + package_json_path, + %( "@babel/preset-react",\n), + after: /"presets": \[\n/ + ) +end # install rspec-rails system("bundle add rspec-rails --group development,test") diff --git a/spec/generator_specs/fake-bin/bun b/spec/generator_specs/fake-bin/bun new file mode 100755 index 000000000..e4072e011 --- /dev/null +++ b/spec/generator_specs/fake-bin/bun @@ -0,0 +1,10 @@ +#!/usr/bin/env ruby + +binary = "bun" +major_version = "1" + +unless ENV["SHAKAPACKER_EXPECTED_PACKAGE_MANGER"] == binary + raise StandardError, "(#{binary}) this is not the package manager you're looking for..." +end + +exec("npx", "-y", "#{binary}@#{major_version}", *ARGV) diff --git a/spec/generator_specs/fake-bin/npm b/spec/generator_specs/fake-bin/npm new file mode 100755 index 000000000..9098c1c4b --- /dev/null +++ b/spec/generator_specs/fake-bin/npm @@ -0,0 +1,10 @@ +#!/usr/bin/env ruby + +binary = "npm" +major_version = "9" + +unless ENV["SHAKAPACKER_EXPECTED_PACKAGE_MANGER"] == binary + raise StandardError, "(#{binary}) this is not the package manager you're looking for..." +end + +exec("npx", "-y", "#{binary}@#{major_version}", *ARGV) diff --git a/spec/generator_specs/fake-bin/pnpm b/spec/generator_specs/fake-bin/pnpm new file mode 100755 index 000000000..9828144a4 --- /dev/null +++ b/spec/generator_specs/fake-bin/pnpm @@ -0,0 +1,10 @@ +#!/usr/bin/env ruby + +binary = "pnpm" +major_version = "8" + +unless ENV["SHAKAPACKER_EXPECTED_PACKAGE_MANGER"] == binary + raise StandardError, "(#{binary}) this is not the package manager you're looking for..." +end + +exec("npx", "-y", "#{binary}@#{major_version}", *ARGV) diff --git a/spec/generator_specs/fake-bin/yarn b/spec/generator_specs/fake-bin/yarn new file mode 100755 index 000000000..35808b1e4 --- /dev/null +++ b/spec/generator_specs/fake-bin/yarn @@ -0,0 +1,10 @@ +#!/usr/bin/env ruby + +binary = "yarn" +major_version = "1" + +unless ENV["SHAKAPACKER_EXPECTED_PACKAGE_MANGER"] == "#{binary}_classic" + raise StandardError, "(#{binary}) this is not the package manager you're looking for..." +end + +exec("npx", "-y", "#{binary}@#{major_version}", *ARGV) diff --git a/spec/generator_specs/generator_spec.rb b/spec/generator_specs/generator_spec.rb index a7f0ddebe..be6e7d63b 100644 --- a/spec/generator_specs/generator_spec.rb +++ b/spec/generator_specs/generator_spec.rb @@ -3,6 +3,7 @@ require "json" require "shakapacker/utils/misc" require "shakapacker/utils/version_syntax_converter" +require "package_json" GEM_ROOT = Pathname.new(File.expand_path("../../..", __FILE__)) SPEC_PATH = Pathname.new(File.expand_path("../", __FILE__)) @@ -12,13 +13,13 @@ describe "Generator" do before :all do # Don't use --skip-git because we want .gitignore file to exist in the project - sh_in_dir(SPEC_PATH, %( + sh_in_dir({}, SPEC_PATH, %( rails new base-rails-app --skip-javascript --skip-bundle --skip-spring rm -rf base-rails-app/.git )) Bundler.with_unbundled_env do - sh_in_dir(BASE_RAILS_APP_PATH, %( + sh_in_dir({}, BASE_RAILS_APP_PATH, %( gem update bundler bundle add shakapacker --path "#{GEM_ROOT}" )) @@ -31,12 +32,144 @@ end describe "shakapacker:install" do - context "in a normal Rails project" do + # TODO: ideally "yarn_berry" should be here too, but it requires more complex setup + NODE_PACKAGE_MANAGERS.reject { |fm| fm == "yarn_berry" }.each do |fallback_manager| + context "when using package_json with #{fallback_manager} as the manager" do + before :all do + sh_opts = { fallback_manager: fallback_manager } + + sh_in_dir(sh_opts, SPEC_PATH, "cp -r '#{BASE_RAILS_APP_PATH}' '#{TEMP_RAILS_APP_PATH}'") + + Bundler.with_unbundled_env do + sh_in_dir(sh_opts, TEMP_RAILS_APP_PATH, "FORCE=true bundle exec rails shakapacker:install") + end + end + + after :all do + Dir.chdir(SPEC_PATH) + FileUtils.rm_rf(TEMP_RAILS_APP_PATH) + end + + it "creates `config/shakapacker.yml`" do + config_file_relative_path = "config/shakapacker.yml" + actual_content = read(path_in_the_app(config_file_relative_path)) + expected_content = read(path_in_the_gem(config_file_relative_path)) + + expect(actual_content).to eq expected_content + end + + it "replaces package.json with the template file" do + package_json = PackageJson.read(path_in_the_app) + + expect(package_json.fetch("name", "")).to eq("app") + end + + it "creates webpack config directory and its files" do + expected_files = [ + "webpack.config.js" + ] + + Dir.chdir(path_in_the_app("config/webpack")) do + existing_files_in_config_webpack_dir = Dir.glob("*") + expect(existing_files_in_config_webpack_dir).to eq expected_files + end + end + + it "adds binstubs" do + expected_binstubs = [] + Dir.chdir(File.join(GEM_ROOT, "lib/install/bin")) do + expected_binstubs = Dir.glob("bin/*") + end + + Dir.chdir(File.join(TEMP_RAILS_APP_PATH, "bin")) do + actual_binstubs = Dir.glob("*") + expect(actual_binstubs).to include(*expected_binstubs) + end + end + + it "modifies .gitignore" do + actual_content = read(path_in_the_app(".gitignore")) + + expect(actual_content).to match ".yarn-integrity" + end + + it 'adds <%= javascript_pack_tag "application" %>' do + actual_content = read(path_in_the_app("app/views/layouts/application.html.erb")) + + expect(actual_content).to match '<%= javascript_pack_tag "application" %>' + end + + it "updates `bin/setup`" do + package_json = PackageJson.read(path_in_the_app) + cmd = package_json.manager.native_install_command.join(" ") + + setup_file_content = read(path_in_the_app("bin/setup")) + + expect(setup_file_content).to match %r(^\s*system!\(['"]#{cmd}['"]\)) + end + + it "adds relevant shakapacker version in package.json depending on gem version" do + npm_version = Shakapacker::Utils::VersionSyntaxConverter.new.rubygem_to_npm(Shakapacker::VERSION) + + package_json = PackageJson.read(path_in_the_app) + actual_version = package_json.fetch("dependencies", {})["shakapacker"] + + expect(actual_version).to eq(npm_version) + end + + it "adds Shakapacker peer dependencies to package.json" do + package_json = PackageJson.read(path_in_the_app) + actual_dependencies = package_json.fetch("dependencies", {}).keys + + expected_dependencies = %w( + @babel/core + @babel/plugin-transform-runtime + @babel/preset-env + @babel/runtime + babel-loader + compression-webpack-plugin + terser-webpack-plugin + webpack + webpack-assets-manifest + webpack-cli + webpack-merge + ) + + expect(actual_dependencies).to include(*expected_dependencies) + end + + it "adds Shakapacker peer dev dependencies to package.json" do + package_json = PackageJson.read(path_in_the_app) + actual_dev_dependencies = package_json.fetch("devDependencies", {}).keys + + expected_dev_dependencies = %w( + webpack-dev-server + ) + + expect(actual_dev_dependencies).to include(*expected_dev_dependencies) + end + + context "with a basic react app setup" do + it "passes the test for rendering react component on the page" do + sh_opts = { fallback_manager: fallback_manager } + + Bundler.with_unbundled_env do + sh_in_dir(sh_opts, TEMP_RAILS_APP_PATH, "./bin/rails app:template LOCATION=../e2e_template/template.rb") + expect(sh_in_dir(sh_opts, TEMP_RAILS_APP_PATH, "bundle exec rspec")).to be_truthy + end + end + end + end + end + + context "when not using package_json" do before :all do - sh_in_dir(SPEC_PATH, "cp -r '#{BASE_RAILS_APP_PATH}' '#{TEMP_RAILS_APP_PATH}'") + sh_opts = { fallback_manager: nil } + + sh_in_dir(sh_opts, SPEC_PATH, "cp -r '#{BASE_RAILS_APP_PATH}' '#{TEMP_RAILS_APP_PATH}'") Bundler.with_unbundled_env do - sh_in_dir(TEMP_RAILS_APP_PATH, "FORCE=true bundle exec rails shakapacker:install") + sh_in_dir(sh_opts, TEMP_RAILS_APP_PATH, "FORCE=true bundle exec rails shakapacker:install") end end @@ -53,10 +186,10 @@ expect(actual_content).to eq expected_content end - it "replaces package.json with the template file" do - actual_content = read(path_in_the_app("package.json")) + it "replaces package.json with template file" do + package_json = PackageJson.read(path_in_the_app) - expect(actual_content).to match /"name": "app",/ + expect(package_json.fetch("name", "")).to eq("app") end it "creates the webpack config directory and its files" do @@ -104,14 +237,15 @@ it "uses the shakapacker version in package.json depending on gem version" do npm_version = Shakapacker::Utils::VersionSyntaxConverter.new.rubygem_to_npm(Shakapacker::VERSION) - actual_content = read(path_in_the_app("package.json")) + package_json = PackageJson.read(path_in_the_app) + actual_version = package_json.fetch("dependencies", {})["shakapacker"] - expect(actual_content).to match /"shakapacker": "#{npm_version}",/ + expect(actual_version).to eq(npm_version) end it "adds Shakapacker peer dependencies to package.json" do - package_json = JSON.parse(File.read(path_in_the_app("package.json"))) - actual_dependencies = package_json["dependencies"]&.keys + package_json = PackageJson.read(path_in_the_app) + actual_dependencies = package_json.fetch("dependencies", {}).keys expected_dependencies = %w( @babel/core @@ -131,8 +265,8 @@ end it "adds Shakapacker peer dev dependencies to package.json" do - package_json = JSON.parse(File.read(path_in_the_app("package.json"))) - actual_dev_dependencies = package_json["devDependencies"]&.keys + package_json = PackageJson.read(path_in_the_app) + actual_dev_dependencies = package_json.fetch("devDependencies", {}).keys expected_dev_dependencies = %w( webpack-dev-server @@ -143,10 +277,12 @@ context "with a basic react app setup" do it "passes the test for rendering react component on the page" do + sh_opts = { fallback_manager: nil } + Bundler.with_unbundled_env do - sh_in_dir(TEMP_RAILS_APP_PATH, "./bin/rails app:template LOCATION=../e2e_template/template.rb") + sh_in_dir(sh_opts, TEMP_RAILS_APP_PATH, "./bin/rails app:template LOCATION=../e2e_template/template.rb") - expect(sh_in_dir(TEMP_RAILS_APP_PATH, "bundle exec rspec")).to be_truthy + expect(sh_in_dir(sh_opts, TEMP_RAILS_APP_PATH, "bundle exec rspec")).to be_truthy end end end @@ -167,7 +303,21 @@ def read(path) File.read(path) end - def sh_in_dir(dir, *shell_commands) + def sort_out_package_json(opts) + ENV["PATH"] = "#{SPEC_PATH}/fake-bin:#{ENV["PATH"]}" + + if opts[:fallback_manager].nil? + ENV["SHAKAPACKER_EXPECTED_PACKAGE_MANGER"] = "yarn_classic" + ENV["SHAKAPACKER_USE_PACKAGE_JSON_GEM"] = "false" + else + ENV["SHAKAPACKER_EXPECTED_PACKAGE_MANGER"] = opts[:fallback_manager] + ENV["SHAKAPACKER_USE_PACKAGE_JSON_GEM"] = "true" + ENV["PACKAGE_JSON_FALLBACK_MANAGER"] = opts[:fallback_manager] + end + end + + def sh_in_dir(opts, dir, *shell_commands) + sort_out_package_json(opts) Shakapacker::Utils::Misc.sh_in_dir(dir, *shell_commands) end end diff --git a/spec/mounted_app/package.json b/spec/mounted_app/package.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/spec/mounted_app/package.json @@ -0,0 +1 @@ +{} diff --git a/spec/shakapacker/dev_server_runner_spec.rb b/spec/shakapacker/dev_server_runner_spec.rb index b070b7e59..25f4a17b4 100644 --- a/spec/shakapacker/dev_server_runner_spec.rb +++ b/spec/shakapacker/dev_server_runner_spec.rb @@ -16,47 +16,106 @@ let(:test_app_path) { File.expand_path("./test_app", __dir__) } - it "supports running via node modules" do - cmd = ["#{test_app_path}/node_modules/.bin/webpack", "serve", "--config", "#{test_app_path}/config/webpack/webpack.config.js"] + NODE_PACKAGE_MANAGERS.each do |fallback_manager| + context "when using package_json with #{fallback_manager} as the manager" do + with_use_package_json_gem(enabled: true, fallback_manager: fallback_manager) - verify_command(cmd, use_node_modules: true) - end + let(:package_json) { PackageJson.read(test_app_path) } - it "supports running via yarn" do - cmd = ["yarn", "webpack", "serve", "--config", "#{test_app_path}/config/webpack/webpack.config.js"] + require "package_json" - verify_command(cmd, use_node_modules: false) - end + it "uses the expected package manager", unless: fallback_manager == "yarn_classic" do + cmd = package_json.manager.native_exec_command("webpack", ["serve", "--config", "#{test_app_path}/config/webpack/webpack.config.js"]) - it "passes on arguments" do - cmd = ["#{test_app_path}/node_modules/.bin/webpack", "serve", "--config", "#{test_app_path}/config/webpack/webpack.config.js", "--quiet"] + manager_name = fallback_manager.split("_")[0] - verify_command(cmd, argv: (["--quiet"])) - end + expect(cmd).to start_with(manager_name) + end + + it "runs the command using the manager" do + cmd = package_json.manager.native_exec_command("webpack", ["serve", "--config", "#{test_app_path}/config/webpack/webpack.config.js"]) + + verify_command(cmd) + end + + it "passes on arguments" do + cmd = package_json.manager.native_exec_command("webpack", ["serve", "--config", "#{test_app_path}/config/webpack/webpack.config.js", "--quiet"]) - it "supports the https flag" do - cmd = ["#{test_app_path}/node_modules/.bin/webpack", "serve", "--config", "#{test_app_path}/config/webpack/webpack.config.js", "--https"] + verify_command(cmd, argv: (["--quiet"])) + end + + it "supports the https flag" do + cmd = package_json.manager.native_exec_command("webpack", ["serve", "--config", "#{test_app_path}/config/webpack/webpack.config.js", "--https"]) + + dev_server = double() + allow(dev_server).to receive(:host).and_return("localhost") + allow(dev_server).to receive(:port).and_return("3035") + allow(dev_server).to receive(:pretty?).and_return(false) + allow(dev_server).to receive(:https?).and_return(true) + allow(dev_server).to receive(:hmr?).and_return(false) + + allow(Shakapacker::DevServer).to receive(:new) do + verify_command(cmd, argv: (["--https"])) + end.and_return(dev_server) + end + + it "accepts environment variables" do + cmd = package_json.manager.native_exec_command("webpack", ["serve", "--config", "#{test_app_path}/config/webpack/webpack.config.js"]) - dev_server = double() - allow(dev_server).to receive(:host).and_return("localhost") - allow(dev_server).to receive(:port).and_return("3035") - allow(dev_server).to receive(:pretty?).and_return(false) - allow(dev_server).to receive(:https?).and_return(true) - allow(dev_server).to receive(:hmr?).and_return(false) + env = Shakapacker::Compiler.env.dup + ENV["SHAKAPACKER_CONFIG"] = env["SHAKAPACKER_CONFIG"] = "#{test_app_path}/config/shakapacker_other_location.yml" + env["WEBPACK_SERVE"] = "true" - allow(Shakapacker::DevServer).to receive(:new) do - verify_command(cmd, argv: (["--https"])) - end.and_return(dev_server) + verify_command(cmd, env: env) + end + end end - it "accepts environment variables" do - cmd = ["#{test_app_path}/node_modules/.bin/webpack", "serve", "--config", "#{test_app_path}/config/webpack/webpack.config.js"] - env = Shakapacker::Compiler.env.dup + context "when not using package_json" do + with_use_package_json_gem(enabled: false) - ENV["SHAKAPACKER_CONFIG"] = env["SHAKAPACKER_CONFIG"] = "#{test_app_path}/config/shakapacker_other_location.yml" - env["WEBPACK_SERVE"] = "true" + it "supports running via node modules" do + cmd = ["#{test_app_path}/node_modules/.bin/webpack", "serve", "--config", "#{test_app_path}/config/webpack/webpack.config.js"] - verify_command(cmd, env: env) + verify_command(cmd, use_node_modules: true) + end + + it "supports running via yarn" do + cmd = ["yarn", "webpack", "serve", "--config", "#{test_app_path}/config/webpack/webpack.config.js"] + + verify_command(cmd, use_node_modules: false) + end + + it "passes on arguments" do + cmd = ["#{test_app_path}/node_modules/.bin/webpack", "serve", "--config", "#{test_app_path}/config/webpack/webpack.config.js", "--quiet"] + + verify_command(cmd, argv: (["--quiet"])) + end + + it "supports the https flag" do + cmd = ["#{test_app_path}/node_modules/.bin/webpack", "serve", "--config", "#{test_app_path}/config/webpack/webpack.config.js", "--https"] + + dev_server = double() + allow(dev_server).to receive(:host).and_return("localhost") + allow(dev_server).to receive(:port).and_return("3035") + allow(dev_server).to receive(:pretty?).and_return(false) + allow(dev_server).to receive(:https?).and_return(true) + allow(dev_server).to receive(:hmr?).and_return(false) + + allow(Shakapacker::DevServer).to receive(:new) do + verify_command(cmd, argv: (["--https"])) + end.and_return(dev_server) + end + + it "accepts environment variables" do + cmd = ["#{test_app_path}/node_modules/.bin/webpack", "serve", "--config", "#{test_app_path}/config/webpack/webpack.config.js"] + env = Shakapacker::Compiler.env.dup + + ENV["SHAKAPACKER_CONFIG"] = env["SHAKAPACKER_CONFIG"] = "#{test_app_path}/config/shakapacker_other_location.yml" + env["WEBPACK_SERVE"] = "true" + + verify_command(cmd, env: env) + end end private diff --git a/spec/shakapacker/engine_rake_tasks_spec.rb b/spec/shakapacker/engine_rake_tasks_spec.rb index 66ef29e78..f98a179ff 100644 --- a/spec/shakapacker/engine_rake_tasks_spec.rb +++ b/spec/shakapacker/engine_rake_tasks_spec.rb @@ -9,15 +9,36 @@ remove_webpack_binstubs end - it "mounts app:shakapacker task successfully" do - output = Dir.chdir(mounted_app_path) { `rake -T` } + NODE_PACKAGE_MANAGERS.each do |fallback_manager| + context "when using package_json with #{fallback_manager} as the manager" do + with_use_package_json_gem(enabled: true, fallback_manager: fallback_manager) - expect(output).to include "app:shakapacker" + it "mounts app:shakapacker task successfully" do + output = Dir.chdir(mounted_app_path) { `rake -T` } + + expect(output).to include "app:shakapacker" + end + + it "only adds expected files to bin directory when binstubs is run" do + Dir.chdir(mounted_app_path) { `bundle exec rake app:shakapacker:binstubs` } + expected_binstub_paths.each { |path| expect(File.exist?(path)).to be true } + end + end end - it "only adds expected files to bin directory when binstubs is run" do - Dir.chdir(mounted_app_path) { `bundle exec rake app:shakapacker:binstubs` } - expected_binstub_paths.each { |path| expect(File.exist?(path)).to be true } + context "when not using package_json" do + with_use_package_json_gem(enabled: false) + + it "mounts app:shakapacker task successfully" do + output = Dir.chdir(mounted_app_path) { `rake -T` } + + expect(output).to include "app:shakapacker" + end + + it "only adds expected files to bin directory when binstubs is run" do + Dir.chdir(mounted_app_path) { `bundle exec rake app:shakapacker:binstubs` } + expected_binstub_paths.each { |path| expect(File.exist?(path)).to be true } + end end private diff --git a/spec/shakapacker/rake_tasks_spec.rb b/spec/shakapacker/rake_tasks_spec.rb index 633546225..e7c34e3b9 100644 --- a/spec/shakapacker/rake_tasks_spec.rb +++ b/spec/shakapacker/rake_tasks_spec.rb @@ -8,7 +8,7 @@ expect(output).to include "shakapacker" expect(output).to include "shakapacker:check_binstubs" expect(output).to include "shakapacker:check_node" - expect(output).to include "shakapacker:check_yarn" + expect(output).to include "shakapacker:check_manager" expect(output).to include "shakapacker:clean" expect(output).to include "shakapacker:clobber" expect(output).to include "shakapacker:compile" @@ -28,11 +28,12 @@ expect(output).to_not include "Shakapacker requires Node.js" end - it "`shakapacker:check_yarn` doesn't get errors related to yarn" do - output = Dir.chdir(TEST_APP_PATH) { `rake shakapacker:check_yarn 2>&1` } + # TODO: currently this test depends on external conditions & PACKAGE_JSON_FALLBACK_MANAGER + it "`shakapacker:check_manager` doesn't get errors related to the package manager" do + output = Dir.chdir(TEST_APP_PATH) { `rake shakapacker:check_manager 2>&1` } - expect(output).to_not include "Yarn not installed" - expect(output).to_not include "Shakapacker requires Yarn" + expect(output).to_not include "not installed" + expect(output).to_not include "Shakapacker requires" end describe "`shakapacker:check_binstubs`" do diff --git a/spec/shakapacker/webpack_runner_spec.rb b/spec/shakapacker/webpack_runner_spec.rb index 33a3b91c7..733b74745 100644 --- a/spec/shakapacker/webpack_runner_spec.rb +++ b/spec/shakapacker/webpack_runner_spec.rb @@ -14,22 +14,56 @@ let(:test_app_path) { File.expand_path("./test_app", __dir__) } - it "supports running via node_modules" do - cmd = ["#{test_app_path}/node_modules/.bin/webpack", "--config", "#{test_app_path}/config/webpack/webpack.config.js"] + NODE_PACKAGE_MANAGERS.each do |fallback_manager| + context "when using package_json with #{fallback_manager} as the manager" do + with_use_package_json_gem(enabled: true, fallback_manager: fallback_manager) - verify_command(cmd, use_node_modules: true) - end + let(:package_json) { PackageJson.read(test_app_path) } + + require "package_json" + + it "uses the expected package manager", unless: fallback_manager == "yarn_classic" do + cmd = package_json.manager.native_exec_command("webpack", ["--config", "#{test_app_path}/config/webpack/webpack.config.js"]) + + manager_name = fallback_manager.split("_")[0] + + expect(cmd).to start_with(manager_name) + end + + it "runs the command using the manager" do + cmd = package_json.manager.native_exec_command("webpack", ["--config", "#{test_app_path}/config/webpack/webpack.config.js"]) - it "supports running via yarn" do - cmd = ["yarn", "webpack", "--config", "#{test_app_path}/config/webpack/webpack.config.js"] + verify_command(cmd) + end - verify_command(cmd, use_node_modules: false) + it "passes on arguments" do + cmd = package_json.manager.native_exec_command("webpack", ["--config", "#{test_app_path}/config/webpack/webpack.config.js", "--watch"]) + + verify_command(cmd, argv: (["--watch"])) + end + end end - it "passes on arguments" do - cmd = ["#{test_app_path}/node_modules/.bin/webpack", "--config", "#{test_app_path}/config/webpack/webpack.config.js", "--watch"] + context "when not using package_json" do + with_use_package_json_gem(enabled: false) + + it "supports running via node_modules" do + cmd = ["#{test_app_path}/node_modules/.bin/webpack", "--config", "#{test_app_path}/config/webpack/webpack.config.js"] + + verify_command(cmd, use_node_modules: true) + end + + it "supports running via yarn" do + cmd = ["yarn", "webpack", "--config", "#{test_app_path}/config/webpack/webpack.config.js"] - verify_command(cmd, argv: ["--watch"]) + verify_command(cmd, use_node_modules: false) + end + + it "passes on arguments" do + cmd = ["#{test_app_path}/node_modules/.bin/webpack", "--config", "#{test_app_path}/config/webpack/webpack.config.js", "--watch"] + + verify_command(cmd, argv: ["--watch"]) + end end private diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 5ae5b6962..4636c74eb 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,3 +1,5 @@ +require_relative "./support/package_json_helpers" + # This file was generated by the `rspec --init` command. Conventionally, all # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. # The generated `.rspec` file contains `--require spec_helper` which will cause diff --git a/spec/support/package_json_helpers.rb b/spec/support/package_json_helpers.rb new file mode 100644 index 000000000..eff837ddf --- /dev/null +++ b/spec/support/package_json_helpers.rb @@ -0,0 +1,16 @@ +NODE_PACKAGE_MANAGERS = ["npm", "yarn_classic", "yarn_berry", "pnpm", "bun"] + +def with_use_package_json_gem(enabled:, fallback_manager: nil) + around do |example| + old_use_package_json_gem_value = ENV["SHAKAPACKER_USE_PACKAGE_JSON_GEM"] + old_package_json_fallback_manager_value = ENV["PACKAGE_JSON_FALLBACK_MANAGER"] + + ENV["SHAKAPACKER_USE_PACKAGE_JSON_GEM"] = enabled.to_s + ENV["PACKAGE_JSON_FALLBACK_MANAGER"] = fallback_manager.to_s + + example.run + + ENV["SHAKAPACKER_USE_PACKAGE_JSON_GEM"] = old_use_package_json_gem_value + ENV["PACKAGE_JSON_FALLBACK_MANAGER"] = old_package_json_fallback_manager_value + end +end