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 %(
),
+ vite_image_tag("images/logo.png", alt: "Logo")
+
+ assert_equal %(
),
+ 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"