diff --git a/.github/workflows/js.yml b/.github/workflows/js.yml index 1bc18a96..00834b69 100644 --- a/.github/workflows/js.yml +++ b/.github/workflows/js.yml @@ -18,7 +18,7 @@ jobs: - uses: pnpm/action-setup@v4 with: - version: 9 + version: 10 - uses: actions/setup-node@v4 with: diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index 8a792256..b7cd37f5 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -12,25 +12,30 @@ jobs: matrix: os: [ubuntu-latest] ruby: [ - "3.1", - "3.2", "3.3", + "3.4", ] gemfile: [ "Gemfile-rails.7.0.x", "Gemfile-rails.7.1.x", "Gemfile-rails.7.2.x", + "Gemfile-rails.8.0.x", + "Gemfile-rails.8.1.x", ] experimental: [false] include: - - ruby: "3.4" + - ruby: "4.0" os: ubuntu-latest - gemfile: "Gemfile-rails.8.0.x" + gemfile: "Gemfile-rails.8.1.x" experimental: true - ruby: "3.4" os: ubuntu-latest gemfile: Gemfile-rails-edge experimental: true + - ruby: "4.0" + os: ubuntu-latest + gemfile: Gemfile-rails-edge + experimental: true env: # $BUNDLE_GEMFILE must be set at the job level, so it is set for all steps BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/${{ matrix.gemfile }} diff --git a/.rubocop.yml b/.rubocop.yml index 6b0592f2..6d8870fc 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -6,14 +6,14 @@ require: - rubocop-minitest inherit_gem: - standard: config/ruby-3.0.yml + standard: config/ruby-3.3.yml inherit_mode: merge: - Exclude - + AllCops: - TargetRubyVersion: 3.0 + TargetRubyVersion: 3.3 SuggestExtensions: false NewCops: enable Exclude: diff --git a/Gemfile.lock b/Gemfile.lock index 9ee071d9..9fa131af 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -101,7 +101,7 @@ GEM ast (2.4.2) base64 (0.3.0) benchmark-ips (2.14.0) - bigdecimal (4.1.0) + bigdecimal (4.1.2) builder (3.3.0) byebug (11.1.3) coderay (1.1.3) @@ -112,14 +112,14 @@ GEM docile (1.4.1) drb (2.2.3) dry-cli (1.4.1) - erb (6.0.2) + erb (6.0.4) erubi (1.13.1) globalid (1.2.1) activesupport (>= 6.1) i18n (1.14.8) concurrent-ruby (~> 1.0) io-console (0.8.2) - irb (1.17.0) + irb (1.18.0) pp (>= 0.6.0) prism (>= 1.3.0) rdoc (>= 4.0.0) @@ -161,7 +161,7 @@ GEM net-smtp (0.4.0) net-protocol nio4r (2.5.9) - nokogiri (1.19.2) + nokogiri (1.19.3) mini_portile2 (~> 2.8.2) racc (~> 1.4) parallel (1.26.3) @@ -182,10 +182,10 @@ GEM date stringio racc (1.8.1) - rack (3.2.5) + rack (3.2.6) rack-proxy (0.7.7) rack - rack-session (2.1.1) + rack-session (2.1.2) base64 (>= 0.1.0) rack (>= 3.0.0) rack-test (2.2.0) @@ -222,7 +222,7 @@ GEM thor (~> 1.0, >= 1.2.2) zeitwerk (~> 2.6) rainbow (3.1.1) - rake (13.3.1) + rake (13.4.2) rdoc (7.2.0) erb psych (>= 4.0.0) diff --git a/examples/hanami_bookshelf/.ruby-version b/examples/hanami_bookshelf/.ruby-version index 2a497469..54978911 100644 --- a/examples/hanami_bookshelf/.ruby-version +++ b/examples/hanami_bookshelf/.ruby-version @@ -1 +1 @@ -ruby-3.1.1 +ruby-3.4.5 diff --git a/examples/hanami_bookshelf/Gemfile b/examples/hanami_bookshelf/Gemfile index 48045499..6c9440ac 100644 --- a/examples/hanami_bookshelf/Gemfile +++ b/examples/hanami_bookshelf/Gemfile @@ -1,6 +1,6 @@ source 'https://rubygems.org' -ruby '3.1.1' +ruby '3.4.5' gem 'rake' gem 'hanami' diff --git a/examples/padrino_blog_tutorial/Gemfile b/examples/padrino_blog_tutorial/Gemfile index 6920d7e7..5e6d8c48 100644 --- a/examples/padrino_blog_tutorial/Gemfile +++ b/examples/padrino_blog_tutorial/Gemfile @@ -3,7 +3,7 @@ source 'https://rubygems.org' gem 'rake' gem 'haml' gem 'puma' -gem 'padrino', '0.15.0' +gem 'padrino', '0.16.1' gem 'vite_padrino', path: '../../vite_padrino' gem 'vite_ruby', path: '../../vite_ruby' gem 'pry-byebug' diff --git a/gemfiles/Gemfile-rails.8.1.x b/gemfiles/Gemfile-rails.8.1.x new file mode 100644 index 00000000..d3eff38f --- /dev/null +++ b/gemfiles/Gemfile-rails.8.1.x @@ -0,0 +1,7 @@ +source 'https://rubygems.org' + +gem 'rails', '~> 8.1.0' + +gemspec path: '../vite_ruby' +gemspec path: '../vite_rails' +gemspec path: '../vite_plugin_legacy' diff --git a/test/dev_server_proxy_test.rb b/test/dev_server_proxy_test.rb index b8bee3a7..5c9a19d2 100644 --- a/test/dev_server_proxy_test.rb +++ b/test/dev_server_proxy_test.rb @@ -11,13 +11,8 @@ def app [200, {"Content-Type" => "application/json"}, env.to_json] } # Avoid actually using the proxy. - if RUBY_VERSION.start_with?("2.4") - Rack::Proxy.send(:remove_method, :perform_request) if Rack::Proxy.method_defined?(:perform_request) - Rack::Proxy.send(:define_method, :perform_request) { |env| capture_app.call(env) } - else - Rack::Proxy.remove_method(:perform_request) - Rack::Proxy.define_method(:perform_request) { |env| capture_app.call(env) } - end + Rack::Proxy.remove_method(:perform_request) + Rack::Proxy.define_method(:perform_request) { |env| capture_app.call(env) } ViteRuby::DevServerProxy.new(capture_app) end diff --git a/test/helper_test.rb b/test/helper_test.rb index 81ae21a2..8d8d8c11 100644 --- a/test/helper_test.rb +++ b/test/helper_test.rb @@ -42,6 +42,11 @@ def with_dev_server_running(&block) refresh_config(mode: "development") super end + + def with_skip_proxy_dev_server_running(&block) + refresh_config(mode: "development", skip_proxy: true) + ViteRuby.instance.stub(:dev_server_running?, true, &block) + end end class LegacyHelperTest < HelperTestCase @@ -190,6 +195,88 @@ def test_vite_image_tag } end + # skipProxy tests: validate that all tag helpers emit absolute URLs pointing + # directly to the Vite dev server when skipProxy is enabled. + + def test_vite_client_tag_with_skip_proxy + assert_nil vite_client_tag + with_skip_proxy_dev_server_running { + origin = ViteRuby.config.origin + + assert_equal %(), vite_client_tag + } + end + + def test_vite_asset_path_with_skip_proxy + with_skip_proxy_dev_server_running { + origin = ViteRuby.config.origin + + assert_equal "#{origin}/vite-dev/entrypoints/main.ts", vite_asset_path("main.ts") + assert_equal "#{origin}/vite-dev/entrypoints/app.css", vite_asset_path("app.css") + assert_equal "#{origin}/vite-dev/images/logo.png", vite_asset_path("images/logo.png") + } + end + + def test_vite_javascript_tag_with_skip_proxy + with_skip_proxy_dev_server_running { + origin = ViteRuby.config.origin + + assert_equal %(), + vite_typescript_tag("main") + + assert_equal %(), + vite_javascript_tag("entrypoints/frameworks/vue") + } + end + + def test_vite_stylesheet_tag_with_skip_proxy + with_skip_proxy_dev_server_running { + origin = ViteRuby.config.origin + + assert_similar link(href: "#{origin}/vite-dev/entrypoints/app.css"), vite_stylesheet_tag("app") + assert_equal vite_stylesheet_tag("app"), vite_stylesheet_tag("app.css") + + if Rails::VERSION::MAJOR >= 7 + assert_similar link(href: "#{origin}/vite-dev/entrypoints/sassy.scss"), vite_stylesheet_tag("sassy.scss") + else + # Rails 6 appends .css to non-.css extensions. Without the proxy to + # normalize .scss.css → .scss, Vite cannot serve this URL. + assert_similar link(href: "#{origin}/vite-dev/entrypoints/sassy.scss.css"), vite_stylesheet_tag("sassy.scss") + end + } + end + + def test_vite_image_tag_with_skip_proxy + with_skip_proxy_dev_server_running { + origin = ViteRuby.config.origin + + assert_equal %(Logo), + vite_image_tag("images/logo.png", alt: "Logo") + + assert_equal %(Logo), + vite_image_tag("images/logo.png", srcset: {"images/logo-2x.png" => "2x"}, alt: "Logo") + } + end + + def test_vite_react_refresh_tag_with_skip_proxy + with_skip_proxy_dev_server_running { + origin = ViteRuby.config.origin + + assert_equal <<~HTML.chomp, vite_react_refresh_tag(nonce: nil) + + HTML + } + end + def test_vite_picture_tag if Rails.gem_version >= Gem::Version.new("7.1.0") assert_equal <<~HTML.gsub(/\n\s*/, ""), vite_picture_tag("images/logo.svg", "images/logo.png", class: "test", image: {alt: "Logo"}) diff --git a/test/manifest_test.rb b/test/manifest_test.rb index ddda3af5..516196b6 100644 --- a/test/manifest_test.rb +++ b/test/manifest_test.rb @@ -195,6 +195,74 @@ def test_vite_client_src } end + # NOTE: skipProxy (experimental since v3.2.12) causes asset URLs to point + # directly to the Vite dev server. Known caveats: + # + # 1. CORS: Browser makes cross-origin requests to Vite. Vite sets permissive + # CORS headers by default, but custom middleware may interfere. + # 2. Rails 6 .scss.css: Without the proxy to normalize .scss.css → .scss, + # Rails 6's stylesheet_link_tag produces URLs Vite can't serve. + # 3. Cookies: Asset requests to a different origin won't carry same-origin + # cookies. Usually not an issue since assets don't require auth. + # 4. SSL: Both Rails and Vite need valid certs when using HTTPS. + # 5. Docker/VM: "localhost:3036" from the browser may not reach Vite inside + # a container. Must configure host to a reachable address. + # 6. vite_asset_url may produce double-origin URLs since path_for already + # returns an absolute URL when skipProxy is enabled. + + def test_lookup_success_with_skip_proxy_and_dev_server_running + refresh_config(mode: "development", skip_proxy: true) + with_dev_server_running { + origin = ViteRuby.config.origin # "https://localhost:3535" + + entry = {"file" => "#{origin}/vite-dev/entrypoints/application.js"} + + assert_equal entry, lookup!("application.js", type: :javascript) + assert_equal entry, lookup!("entrypoints/application.js") + + assert_equal "#{origin}/vite-dev/entrypoints/application.ts", + path_for("application", type: :typescript) + + assert_equal "#{origin}/vite-dev/entrypoints/styles.css", + path_for("styles", type: :stylesheet) + + assert_equal "#{origin}/vite-dev/image/logo.png", + path_for("image/logo.png") + + assert_equal "#{origin}/vite-dev/logo.png", + path_for("~/logo.png") + + assert_equal "#{origin}/vite-dev/@fs#{ViteRuby.config.root}/app/assets/theme.css", + path_for("/app/assets/theme", type: :stylesheet) + } + end + + def test_vite_client_src_with_skip_proxy + refresh_config(mode: "development", skip_proxy: true) + + assert_nil vite_client_src + + with_dev_server_running { + assert_equal "#{ViteRuby.config.origin}/vite-dev/@vite/client", vite_client_src + } + + # Origin from skip_proxy takes precedence over asset_host + refresh_config(asset_host: "http://example.com", mode: "development", skip_proxy: true) + + with_dev_server_running { + assert_equal "#{ViteRuby.config.origin}/vite-dev/@vite/client", vite_client_src + } + end + + def test_skip_proxy_has_no_effect_without_dev_server + refresh_config(skip_proxy: true) + + # Production paths are unchanged — skipProxy only matters when dev server runs + assert_equal prefixed("main.9dcad042.js"), path_for("main", type: :typescript) + assert_equal prefixed("app.517bf154.css"), path_for("app", type: :stylesheet) + assert_equal prefixed("logo.f42fb7ea.png"), path_for("images/logo.png") + end + def test_lookup_nil assert_nil lookup("foo.js") end diff --git a/vite-plugin-ruby/src/index.ts b/vite-plugin-ruby/src/index.ts index 92fa4e36..4a370d2f 100644 --- a/vite-plugin-ruby/src/index.ts +++ b/vite-plugin-ruby/src/index.ts @@ -9,6 +9,15 @@ import { assetsManifestPlugin } from './manifest' export * from './types' +export interface PreRenderedAsset { + type: 'asset' + name?: string // Vite v7, deprecated + names?: string[] // Vite v8 + originalFileName?: string // Vite v7, deprecated + originalFileNames?: string[] // Vite v8 + source: string | Uint8Array +} + // Public: The resolved project root. export const projectRoot = configOptionFromEnv('root') || process.cwd() @@ -107,9 +116,10 @@ function configureServer (server: ViteDevServer) { function outputOptions (assetsDir: string, ssrBuild: boolean) { // Internal: Avoid nesting entrypoints unnecessarily. - const outputFileName = (ext: string) => ({ name }: { name: string }) => { - if (typeof name === 'undefined') return '' - const shortName = basename(name).split('.')[0] + const outputFileName = (ext: string) => (asset: PreRenderedAsset) => { + // Vite v8 uses `names`, earlier versions use `name` + const resolvedName = asset.names?.[0] ?? asset.name ?? '[name]' + const shortName = basename(resolvedName).split('.')[0] return posix.join(assetsDir, `${shortName}-[hash].${ext}`) } diff --git a/vite-plugin-ruby/tests/index.spec.ts b/vite-plugin-ruby/tests/index.spec.ts index 10e8f21a..10f3c572 100644 --- a/vite-plugin-ruby/tests/index.spec.ts +++ b/vite-plugin-ruby/tests/index.spec.ts @@ -28,4 +28,34 @@ describe('config', () => { pluginConfig({ ...defaultConfig, build: { ssr: true } }, { mode: 'production' }) }).toThrow('No SSR entrypoint available') }) + + describe('outputFileName (assetFileNames)', () => { + function getAssetFileNames () { + const plugin = ViteRuby() + const pluginConfig = plugin[0].config + defaultConfig.configPath = './default.vite.json' + const result = pluginConfig(defaultConfig, { mode: 'production' }) + return result.build.rollupOptions.output.assetFileNames as (asset: any) => string + } + + const source = 'content' + + test('legacy `name` shape (Vite v7)', () => { + const assetFileNames = getAssetFileNames() + const result = assetFileNames({ type: 'asset', name: 'application.css', source }) + expect(result).toMatch(/^assets\/application-[^.]+\.\[ext\]$/) + }) + + test('new `names` shape (Vite v8)', () => { + const assetFileNames = getAssetFileNames() + const result = assetFileNames({ type: 'asset', names: ['application.css'], originalFileNames: [], source }) + expect(result).toMatch(/^assets\/application-[^.]+\.\[ext\]$/) + }) + + test('falls back to `[name]` placeholder when both are absent', () => { + const assetFileNames = getAssetFileNames() + const result = assetFileNames({ type: 'asset', source }) + expect(result).toBe('assets/[name]-[hash].[ext]') + }) + }) }) diff --git a/vite_hanami/vite_hanami.gemspec b/vite_hanami/vite_hanami.gemspec index 4b34974a..d611ac0f 100644 --- a/vite_hanami/vite_hanami.gemspec +++ b/vite_hanami/vite_hanami.gemspec @@ -18,7 +18,7 @@ Gem::Specification.new do |s| "rubygems_mfa_required" => "true", } - s.required_ruby_version = Gem::Requirement.new(">= 2.5") + s.required_ruby_version = Gem::Requirement.new(">= 3.3") s.add_dependency "vite_ruby", "~> 3.0" diff --git a/vite_padrino/vite_padrino.gemspec b/vite_padrino/vite_padrino.gemspec index a36f8a60..aeec74ed 100644 --- a/vite_padrino/vite_padrino.gemspec +++ b/vite_padrino/vite_padrino.gemspec @@ -18,7 +18,7 @@ Gem::Specification.new do |s| "rubygems_mfa_required" => "true", } - s.required_ruby_version = Gem::Requirement.new(">= 2.5") + s.required_ruby_version = Gem::Requirement.new(">= 3.3") s.add_dependency "vite_ruby", "~> 3.0" diff --git a/vite_plugin_legacy/vite_plugin_legacy.gemspec b/vite_plugin_legacy/vite_plugin_legacy.gemspec index e5087d3e..5c3f6fb3 100644 --- a/vite_plugin_legacy/vite_plugin_legacy.gemspec +++ b/vite_plugin_legacy/vite_plugin_legacy.gemspec @@ -18,7 +18,7 @@ Gem::Specification.new do |s| "rubygems_mfa_required" => "true", } - s.required_ruby_version = Gem::Requirement.new(">= 2.4") + s.required_ruby_version = Gem::Requirement.new(">= 3.3") s.add_dependency "vite_ruby", "~> 3.0", ">= 3.0.4" diff --git a/vite_rails/vite_rails.gemspec b/vite_rails/vite_rails.gemspec index b7f835db..dec0a0a7 100644 --- a/vite_rails/vite_rails.gemspec +++ b/vite_rails/vite_rails.gemspec @@ -18,7 +18,7 @@ Gem::Specification.new do |s| "rubygems_mfa_required" => "true", } - s.required_ruby_version = Gem::Requirement.new(">= 2.5") + s.required_ruby_version = Gem::Requirement.new(">= 3.3") s.add_dependency "railties", ">= 5.1", "< 9" s.add_dependency "vite_ruby", ">= 3.2.2", "~> 3.0" diff --git a/vite_ruby/vite_ruby.gemspec b/vite_ruby/vite_ruby.gemspec index 2c49f915..f307f333 100644 --- a/vite_ruby/vite_ruby.gemspec +++ b/vite_ruby/vite_ruby.gemspec @@ -17,7 +17,7 @@ Gem::Specification.new do |s| "rubygems_mfa_required" => "true", } - s.required_ruby_version = Gem::Requirement.new(">= 2.5") + s.required_ruby_version = Gem::Requirement.new(">= 3.3") s.add_dependency "dry-cli", ">= 0.7", "< 2" s.add_dependency "logger", "~> 1.6"