From 4426cb1007dbb58f4637a4423b1e7d640db96841 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1ximo=20Mussini?= Date: Tue, 16 Jul 2024 22:24:05 -0300 Subject: [PATCH] feat: add `package_manager` config option, experimental support for bun (#481) closes #324 * adds install option, vite install --with pnpm --- docs/src/config/index.md | 12 ++++++++++ vite-plugin-ruby/default.vite.json | 1 + vite_ruby/default.vite.json | 1 + vite_ruby/lib/tasks/vite.rake | 11 +++++---- vite_ruby/lib/vite_ruby/cli/install.rb | 17 +++++++------- .../lib/vite_ruby/cli/upgrade_packages.rb | 3 +-- vite_ruby/lib/vite_ruby/commands.rb | 8 +++---- vite_ruby/lib/vite_ruby/config.rb | 19 +++++++++++---- vite_ruby/lib/vite_ruby/runner.rb | 23 +++++++++---------- 9 files changed, 60 insertions(+), 35 deletions(-) diff --git a/docs/src/config/index.md b/docs/src/config/index.md index 1a60431a..851f28f0 100644 --- a/docs/src/config/index.md +++ b/docs/src/config/index.md @@ -285,6 +285,18 @@ You can customize this behavior using the following options. Allows to skip Vite build output from logs, to keep the noise down. +### packageManager + +- **Default:** auto-detected based on existing lockfiles, otherwise `"npm"` +- **Env Var:** `VITE_RUBY_PACKAGE_MANAGER` + + Allows to specify which package manager to use, such as: + + - `npm` + - `pnpm` + - `yarn` + - `bun` (experimental) + ### root - **Default:** `Rails.root` diff --git a/vite-plugin-ruby/default.vite.json b/vite-plugin-ruby/default.vite.json index c28f575a..947206f2 100644 --- a/vite-plugin-ruby/default.vite.json +++ b/vite-plugin-ruby/default.vite.json @@ -7,6 +7,7 @@ "publicOutputDir": "vite", "configPath": "config/vite.json", "devServerConnectTimeout": 0.01, + "packageManager": null, "publicDir": "public", "entrypointsDir": "entrypoints", "sourceCodeDir": "app/frontend", diff --git a/vite_ruby/default.vite.json b/vite_ruby/default.vite.json index c28f575a..947206f2 100644 --- a/vite_ruby/default.vite.json +++ b/vite_ruby/default.vite.json @@ -7,6 +7,7 @@ "publicOutputDir": "vite", "configPath": "config/vite.json", "devServerConnectTimeout": 0.01, + "packageManager": null, "publicDir": "public", "entrypointsDir": "entrypoints", "sourceCodeDir": "app/frontend", diff --git a/vite_ruby/lib/tasks/vite.rake b/vite_ruby/lib/tasks/vite.rake index f02337c8..65121d98 100644 --- a/vite_ruby/lib/tasks/vite.rake +++ b/vite_ruby/lib/tasks/vite.rake @@ -43,10 +43,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_cmd = case (pkg = ViteRuby.config.package_manager) + when 'npm' then 'npm ci' + else "#{ pkg } install --frozen-lockfile" + end + + system(install_env_args, install_cmd) end desc "Provide information on ViteRuby's environment" diff --git a/vite_ruby/lib/vite_ruby/cli/install.rb b/vite_ruby/lib/vite_ruby/cli/install.rb index 1d8335ba..cb9fa073 100644 --- a/vite_ruby/lib/vite_ruby/cli/install.rb +++ b/vite_ruby/lib/vite_ruby/cli/install.rb @@ -6,7 +6,11 @@ class ViteRuby::CLI::Install < Dry::CLI::Command desc 'Performs the initial configuration setup to get started with Vite Ruby.' - def call(**) + option(:package_manager, values: %w[npm pnpm yarn bun], aliases: %w[package-manager with], desc: 'The package manager to use when installing JS dependencies.') + + def call(package_manager: nil, **) + ENV['VITE_RUBY_PACKAGE_MANAGER'] ||= package_manager if package_manager + $stdout.sync = true say 'Creating binstub' @@ -93,8 +97,7 @@ def install_js_dependencies FileUtils.mv root.join('vite.config.ts'), root.join('vite.config.mts'), force: true, verbose: true end - deps = js_dependencies.join(' ') - run_with_capture("#{ npm_install } -D #{ deps }", stdin_data: "\n") + install_js_packages js_dependencies.join(' ') end # Internal: Adds compilation output dirs to git ignore. @@ -128,12 +131,8 @@ def run_with_capture(*args, **options) end 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' + def install_js_packages(deps) + run_with_capture("#{ config.package_manager } add -D #{ deps }", stdin_data: "\n") end # Internal: Avoid printing warning about missing vite.json, we will create one. diff --git a/vite_ruby/lib/vite_ruby/cli/upgrade_packages.rb b/vite_ruby/lib/vite_ruby/cli/upgrade_packages.rb index 931f9c61..2945e8b7 100644 --- a/vite_ruby/lib/vite_ruby/cli/upgrade_packages.rb +++ b/vite_ruby/lib/vite_ruby/cli/upgrade_packages.rb @@ -5,7 +5,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 }") + install_js_packages js_dependencies.join(' ') end end diff --git a/vite_ruby/lib/vite_ruby/commands.rb b/vite_ruby/lib/vite_ruby/commands.rb index c4d94043..e1c0249c 100644 --- a/vite_ruby/lib/vite_ruby/commands.rb +++ b/vite_ruby/lib/vite_ruby/commands.rb @@ -109,11 +109,11 @@ def print_info $stdout.puts "#{ framework }: #{ Gem.loaded_specs[framework]&.version }" end - $stdout.puts "node: #{ `node --version` }" - $stdout.puts "npm: #{ `npm --version` }" - $stdout.puts "yarn: #{ `yarn --version` rescue nil }" - $stdout.puts "pnpm: #{ `pnpm --version` rescue nil }" $stdout.puts "ruby: #{ `ruby --version` }" + $stdout.puts "node: #{ `node --version` }" + + pkg = config.package_manager + $stdout.puts "#{ pkg }: #{ `#{ pkg } --version` rescue nil }" $stdout.puts "\n" packages = `npm ls vite vite-plugin-ruby` diff --git a/vite_ruby/lib/vite_ruby/config.rb b/vite_ruby/lib/vite_ruby/config.rb index bcc64c7c..7ef4afc0 100644 --- a/vite_ruby/lib/vite_ruby/config.rb +++ b/vite_ruby/lib/vite_ruby/config.rb @@ -96,10 +96,11 @@ def within_root(&block) def coerce_values(config) config['mode'] = config['mode'].to_s config['port'] = config['port'].to_i - config['root'] = Pathname.new(config['root']) - config['build_cache_dir'] = config['root'].join(config['build_cache_dir']) - config['ssr_output_dir'] = config['root'].join(config['ssr_output_dir']) + config['root'] = root = Pathname.new(config['root']) + config['build_cache_dir'] = root.join(config['build_cache_dir']) + config['ssr_output_dir'] = root.join(config['ssr_output_dir']) coerce_booleans(config, 'auto_build', 'hide_build_console_output', 'https', 'skip_compatibility_check', 'skip_proxy') + config['package_manager'] ||= detect_package_manager(root) end # Internal: Coerces configuration options to boolean. @@ -107,6 +108,15 @@ def coerce_booleans(config, *names) names.each { |name| config[name] = [true, 'true'].include?(config[name]) } end + def detect_package_manager(root) + return 'npm' if root.join('package-lock.json').exist? + return 'pnpm' if root.join('pnpm-lock.yaml').exist? + return 'bun' if root.join('bun.lockb').exist? + return 'yarn' if root.join('yarn.lock').exist? + + 'npm' + end + def initialize(attrs) @config = attrs.tap { |config| coerce_values(config) }.freeze ViteRuby::CompatibilityCheck.verify_plugin_version(root) unless skip_compatibility_check @@ -189,6 +199,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 @@ -196,8 +207,8 @@ def config_from_file(path, mode:) tailwind.config.js vite.config.js vite.config.mjs - vite.config.ts vite.config.mts + vite.config.ts windi.config.ts yarn.lock ].freeze diff --git a/vite_ruby/lib/vite_ruby/runner.rb b/vite_ruby/lib/vite_ruby/runner.rb index b12f13eb..60f2830e 100644 --- a/vite_ruby/lib/vite_ruby/runner.rb +++ b/vite_ruby/lib/vite_ruby/runner.rb @@ -28,27 +28,26 @@ 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| - npx_options, vite_args = args.partition { |arg| arg.start_with?('--node-options') } - cmd.push(*vite_executable) - - # NOTE: Vite will parse args, so we need to disambiguate and pass them to - # `npx` before specifying the `vite` executable. - cmd.insert(-2, *npx_options) unless npx_options.empty? - + exec_args, vite_args = args.partition { |arg| arg.start_with?('--node-options') } + cmd.push(*vite_executable(*exec_args)) cmd.push(*vite_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 + def vite_executable(*exec_args) bin_path = config.vite_bin_path return [bin_path] if bin_path && File.exist?(bin_path) - if config.root.join('yarn.lock').exist? - %w[yarn vite] - else - %w[npx vite] + x = case config.package_manager + when 'npm' then %w[npx] + when 'pnpm' then %w[pnpm exec] + when 'bun' then %w[bun x] + when 'yarn' then %w[yarn] + else raise ArgumentError, "Unknown package manager #{ config.package_manager.inspect }" end + + [*x, *exec_args, 'vite'] end end