diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index 8b190d0f..798c6644 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -39,6 +39,8 @@ jobs: with: node-version: '18' + - uses: oven-sh/setup-bun@v1 + - uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} diff --git a/test/rake_tasks_test.rb b/test/rake_tasks_test.rb index 39583839..a35d98bc 100644 --- a/test/rake_tasks_test.rb +++ b/test/rake_tasks_test.rb @@ -43,6 +43,20 @@ def test_rake_vite_install_dependencies_in_production_environment 'Expected development dependencies to be installed as well' end + def test_rake_vite_install_dependencies_supports_bun + ViteRuby.commands.send(:with_node_env, 'production') do + Dir.chdir(test_app_path) do + `touch bun.lockb` + `bundle exec rake vite:install_dependencies` + end + end + + assert_includes installed_node_module_names, 'right-pad', + 'Expected development dependencies to be installed as well' + ensure + FileUtils.rm_f(File.expand_path('bun.lockb', test_app_path)) + end + private def test_app_path diff --git a/test/runner_test.rb b/test/runner_test.rb index 907d6ba9..15ccd5cf 100644 --- a/test/runner_test.rb +++ b/test/runner_test.rb @@ -22,7 +22,7 @@ def test_build_command_with_argument end def test_command_capture - ViteRuby::Runner.stub_any_instance(:vite_executable, 'echo') { + ViteRuby::PackageManager::Yarn.stub_any_instance(:vite_executable, 'echo') { stdout, stderr, status = ViteRuby.run(['"Hello"']) assert_equal %("Hello" --mode production\n), stdout assert_equal '', stderr diff --git a/test/test_helper.rb b/test/test_helper.rb index 16a6f253..d44986f0 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -10,7 +10,9 @@ require 'minitest/reporters' require 'minitest/stub_any_instance' -Minitest::Reporters.use! [Minitest::Reporters::DefaultReporter.new(color: true, location: true, fast_fail: true)] +unless ENV['RM_INFO'] + Minitest::Reporters.use! [Minitest::Reporters::DefaultReporter.new(color: true, location: true, fast_fail: true)] +end require 'rails' require 'rails/test_help' diff --git a/vite_ruby/lib/tasks/vite.rake b/vite_ruby/lib/tasks/vite.rake index 9258da9b..d9882397 100644 --- a/vite_ruby/lib/tasks/vite.rake +++ b/vite_ruby/lib/tasks/vite.rake @@ -42,11 +42,13 @@ namespace :vite do desc 'Ensure build dependencies like Vite are installed before bundling' task :install_dependencies do - install_env_args = ENV['VITE_RUBY_SKIP_INSTALL_DEV_DEPENDENCIES'] == 'true' ? {} : { 'NODE_ENV' => 'development' } - cmd = ViteRuby.commands.legacy_npm_version? ? 'npx ci --yes' : 'npx --yes ci' - result = system(install_env_args, cmd) - # Fallback to `yarn` if `npx` is not available. - system(install_env_args, 'yarn install --frozen-lockfile') if result.nil? + install_env_args = if ENV['VITE_RUBY_SKIP_INSTALL_DEV_DEPENDENCIES'] == 'true' + {} + else + { 'NODE_ENV' => 'development' } + end + + system(install_env_args, ViteRuby.package_manager.install_dependencies_command) end desc "Provide information on ViteRuby's environment" diff --git a/vite_ruby/lib/vite_ruby.rb b/vite_ruby/lib/vite_ruby.rb index ed0bdb3b..7701b6d6 100644 --- a/vite_ruby/lib/vite_ruby.rb +++ b/vite_ruby/lib/vite_ruby.rb @@ -32,7 +32,7 @@ class ViteRuby class << self extend Forwardable - def_delegators :instance, :config, :configure, :commands, :digest, :env, :run, :run_proxy? + def_delegators :instance, :config, :configure, :commands, :digest, :env, :run, :run_proxy?, :package_manager def_delegators :config, :mode def instance @@ -128,6 +128,10 @@ def commands @commands ||= ViteRuby::Commands.new(self) end + def package_manager + @package_manager ||= ViteRuby::PackageManager.resolve(root: config.root) + end + # Public: Current instance configuration for Vite. def config unless defined?(@config) diff --git a/vite_ruby/lib/vite_ruby/cli/install.rb b/vite_ruby/lib/vite_ruby/cli/install.rb index 3f167fa4..4bb14458 100644 --- a/vite_ruby/lib/vite_ruby/cli/install.rb +++ b/vite_ruby/lib/vite_ruby/cli/install.rb @@ -81,7 +81,7 @@ def install_js_dependencies package_json = root.join('package.json') write(package_json, '{}') unless package_json.exist? deps = js_dependencies.join(' ') - run_with_capture("#{ npm_install } -D #{ deps }", stdin_data: "\n") + run_with_capture("#{ add_dependencies_command } -D #{ deps }", stdin_data: "\n") end # Internal: Adds compilation output dirs to git ignore. @@ -101,7 +101,7 @@ def install_gitignore # Internal: The root path for the Ruby application. def root - @root ||= silent_warnings { config.root } + config.root end def say(*args) @@ -116,19 +116,7 @@ def run_with_capture(*args, **options) end # Internal: Support all popular package managers. - def npm_install - return 'yarn add' if root.join('yarn.lock').exist? - return 'pnpm install' if root.join('pnpm-lock.yaml').exist? - - 'npm install' - end - - # Internal: Avoid printing warning about missing vite.json, we will create one. - def silent_warnings - old_stderr = $stderr - $stderr = StringIO.new - yield - ensure - $stderr = old_stderr + def add_dependencies_command + ViteRuby.package_manager.add_dependencies_command end end diff --git a/vite_ruby/lib/vite_ruby/cli/upgrade_packages.rb b/vite_ruby/lib/vite_ruby/cli/upgrade_packages.rb index 931f9c61..396d2832 100644 --- a/vite_ruby/lib/vite_ruby/cli/upgrade_packages.rb +++ b/vite_ruby/lib/vite_ruby/cli/upgrade_packages.rb @@ -6,6 +6,6 @@ class ViteRuby::CLI::UpgradePackages < ViteRuby::CLI::Install def call(**) say 'Upgrading npm packages' deps = js_dependencies.join(' ') - run_with_capture("#{ npm_install } -D #{ deps }") + run_with_capture("#{ add_dependencies_command } -D #{ deps }") end end diff --git a/vite_ruby/lib/vite_ruby/commands.rb b/vite_ruby/lib/vite_ruby/commands.rb index c4d94043..070c45aa 100644 --- a/vite_ruby/lib/vite_ruby/commands.rb +++ b/vite_ruby/lib/vite_ruby/commands.rb @@ -113,6 +113,7 @@ def print_info $stdout.puts "npm: #{ `npm --version` }" $stdout.puts "yarn: #{ `yarn --version` rescue nil }" $stdout.puts "pnpm: #{ `pnpm --version` rescue nil }" + $stdout.puts "bun: #{ `bun --version` rescue nil }" $stdout.puts "ruby: #{ `ruby --version` }" $stdout.puts "\n" diff --git a/vite_ruby/lib/vite_ruby/config.rb b/vite_ruby/lib/vite_ruby/config.rb index 0932d6fd..df1f1216 100644 --- a/vite_ruby/lib/vite_ruby/config.rb +++ b/vite_ruby/lib/vite_ruby/config.rb @@ -189,6 +189,7 @@ def config_from_file(path, mode:) # Internal: If any of these files is modified the build won't be skipped. DEFAULT_WATCHED_PATHS = %w[ + bun.lockb package-lock.json package.json pnpm-lock.yaml diff --git a/vite_ruby/lib/vite_ruby/package_manager.rb b/vite_ruby/lib/vite_ruby/package_manager.rb new file mode 100644 index 00000000..8218c734 --- /dev/null +++ b/vite_ruby/lib/vite_ruby/package_manager.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module ViteRuby::PackageManager + def self.resolve(root:) + package_manager_name = ENV.fetch('VITE_RUBY_PACKAGE_MANAGER', detect_package_manager(root)) + package_manager_class_for(package_manager_name).new(root: root) + end + + def self.package_manager_class_for(package_manager_name) + case package_manager_name.to_sym + when :bun + ViteRuby::PackageManager::Bun + when :pnpm + ViteRuby::PackageManager::Pnpm + when :yarn + ViteRuby::PackageManager::Yarn + else + ViteRuby::PackageManager::Npm + end + end + + def self.detect_package_manager(root) + if root.join('bun.lockb').exist? + :bun + elsif root.join('pnpm-lock.yaml').exist? + :pnpm + elsif root.join('yarn.lock').exist? + :yarn + else + :npm + end + end +end diff --git a/vite_ruby/lib/vite_ruby/package_manager/base.rb b/vite_ruby/lib/vite_ruby/package_manager/base.rb new file mode 100644 index 00000000..051ad934 --- /dev/null +++ b/vite_ruby/lib/vite_ruby/package_manager/base.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +class ViteRuby::PackageManager::Base + attr_reader :root + + def initialize(root: ViteRuby.config.root) + @root = root + end + + # Internal: Returns an Array with the command to run. + def command_for(args) + [config.to_env(env)].tap do |cmd| + args = args.clone + + # Apply runtime arguments for nodejs + if nodejs_runtime? && (args.include?('--inspect') || args.include?('--trace_deprecation')) + cmd.push('node') + cmd.push('--inspect-brk') if args.delete('--inspect') + cmd.push('--trace-deprecation') if args.delete('--trace_deprecation') + end + + # Add vite executable + cmd.push(*vite_executable) + + # Adds vite's arguments + cmd.push(*args) + + # Force `mode`, a vite's argument, to be set + cmd.push('--mode', config.mode) unless args.include?('--mode') || args.include?('-m') + end + end + +private + + def nodejs_runtime? + true + end + + # Internal: Resolves to an executable for Vite. + def vite_executable + bin_path = config.vite_bin_path + [bin_path] if File.exist?(bin_path) + end + + def commands + ViteRuby.commands + end + + def config + ViteRuby.config + end + + def env + ViteRuby.env + end +end diff --git a/vite_ruby/lib/vite_ruby/package_manager/bun.rb b/vite_ruby/lib/vite_ruby/package_manager/bun.rb new file mode 100644 index 00000000..5a1268d5 --- /dev/null +++ b/vite_ruby/lib/vite_ruby/package_manager/bun.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module ViteRuby::PackageManager + class Bun < Base + def install_dependencies_command(frozen: true) + frozen ? 'bun install --frozen-lockfile' : 'bun install' + end + + def add_dependencies_command + 'bun install' + end + + private + + def nodejs_runtime? + false + end + + def vite_executable + shimmed_vite_executable = super || ['vite'] + + # Forces a script or package to use Bun's runtime instead of Node.js (via symlinking node) + shimmed_vite_executable.unshift('bun', '--bun') + end + end +end diff --git a/vite_ruby/lib/vite_ruby/package_manager/npm.rb b/vite_ruby/lib/vite_ruby/package_manager/npm.rb new file mode 100644 index 00000000..182abd49 --- /dev/null +++ b/vite_ruby/lib/vite_ruby/package_manager/npm.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module ViteRuby::PackageManager + class Npm < Base + def install_dependencies_command(frozen: true) + if frozen + commands.legacy_npm_version? ? 'npm ci --yes' : 'npm --yes ci' + else + 'npm install' + end + end + + def add_dependencies_command + 'npm install' + end + + private + + # Internal: Resolves to an executable for Vite. + def vite_executable + super || ["#{ `npm bin`.chomp }/vite"] + end + end +end diff --git a/vite_ruby/lib/vite_ruby/package_manager/pnpm.rb b/vite_ruby/lib/vite_ruby/package_manager/pnpm.rb new file mode 100644 index 00000000..3c1c1a7c --- /dev/null +++ b/vite_ruby/lib/vite_ruby/package_manager/pnpm.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module ViteRuby::PackageManager + class Pnpm < Npm + def install_dependencies_command(*) + 'pnpm install' + end + + def add_dependencies_command + 'pnpm install' + end + end +end diff --git a/vite_ruby/lib/vite_ruby/package_manager/yarn.rb b/vite_ruby/lib/vite_ruby/package_manager/yarn.rb new file mode 100644 index 00000000..e0dd62f5 --- /dev/null +++ b/vite_ruby/lib/vite_ruby/package_manager/yarn.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module ViteRuby::PackageManager + class Yarn < Base + def install_dependencies_command(frozen: true) + frozen ? 'yarn install --frozen-lockfile' : 'yarn install' + end + + def add_dependencies_command + 'yarn add' + end + + private + + def vite_executable + super || %w[yarn vite] + end + end +end diff --git a/vite_ruby/lib/vite_ruby/runner.rb b/vite_ruby/lib/vite_ruby/runner.rb index fd4993ea..f778da83 100644 --- a/vite_ruby/lib/vite_ruby/runner.rb +++ b/vite_ruby/lib/vite_ruby/runner.rb @@ -27,25 +27,6 @@ def run(argv, exec: false) # Internal: Returns an Array with the command to run. def command_for(args) - [config.to_env(env)].tap do |cmd| - args = args.clone - cmd.push('node', '--inspect-brk') if args.delete('--inspect') - cmd.push('node', '--trace-deprecation') if args.delete('--trace_deprecation') - cmd.push(*vite_executable) - cmd.push(*args) - cmd.push('--mode', config.mode) unless args.include?('--mode') || args.include?('-m') - end - end - - # Internal: Resolves to an executable for Vite. - def vite_executable - bin_path = config.vite_bin_path - return [bin_path] if File.exist?(bin_path) - - if config.root.join('yarn.lock').exist? - %w[yarn vite] - else - ["#{ `npm bin`.chomp }/vite"] - end + @vite_ruby.package_manager.command_for(args) end end