From 990cafa05e6bdb3c64214190febd8f85a077f598 Mon Sep 17 00:00:00 2001 From: Ray Brown Date: Tue, 5 May 2026 08:39:49 -0400 Subject: [PATCH 1/5] fix: allow new Vite v8 `outputOptions` callback shape (#604) Vite v8 (which uses Rolldown) changed the object shape passed to the `assetFileNames` callback. In Vite v7 (which uses Rollup), the callback received a `PreRenderedAsset` object with a singular `name: string | undefined` field. In Vite v8, Vite's internal `vite:css-post` plugin calls the same `assetFileNames` function with a plural `names: string[]` field instead. This matches the Rolldown `PreRenderedAsset` interface. See: https://github.com/rolldown/rolldown/blob/a71934bf/packages/rolldown/src/options/output-options.ts#L66-L77 --- vite-plugin-ruby/src/index.ts | 16 ++++++++++++--- vite-plugin-ruby/tests/index.spec.ts | 30 ++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 3 deletions(-) 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]') + }) + }) }) From 776a49e24a8b4f7c776ac9209e2022c21434617e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1ximo=20Mussini?= Date: Tue, 5 May 2026 09:52:43 -0300 Subject: [PATCH 2/5] chore: add tests for skipProxy with different asset scenarios (JS, CSS, SCSS, images) (#602) * Add tests for skipProxy with Inertia-style asset scenarios (JS, CSS, SCSS, images) Validates that skip_proxy: true correctly generates absolute URLs pointing directly to the Vite dev server for all asset types. Covers manifest lookups, tag helpers (client, JS, CSS, SCSS, images, React refresh), and documents known caveats (CORS, Rails 6 .scss.css, cookies, SSL, Docker networking). https://claude.ai/code/session_01Dfaxd2gi78PhPtXVqcfkcq --------- Co-authored-by: Claude Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: ElMassimo <1158253+ElMassimo@users.noreply.github.com> --- .github/workflows/js.yml | 2 +- Gemfile.lock | 14 +++---- test/helper_test.rb | 87 ++++++++++++++++++++++++++++++++++++++++ test/manifest_test.rb | 68 +++++++++++++++++++++++++++++++ 4 files changed, 163 insertions(+), 8 deletions(-) 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/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/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 From 4f90b082186718eabb577c7df42b606ca8e84b95 Mon Sep 17 00:00:00 2001 From: Andriy Tyurnikov Date: Tue, 5 May 2026 19:27:52 +0300 Subject: [PATCH 3/5] chore(ci): bump runners to Node 22 and enable corepack ruby.yml: - setup-node 20 -> 22. - Enable corepack after setup-node so yarn/pnpm shims are available; GitHub's Node 22 image dropped the pre-bundled yarn binary, and test/test_app's rake task exercises `yarn install --frozen-lockfile`. js.yml: - Matrix node 20 -> [22, 24]. --- .github/workflows/js.yml | 2 +- .github/workflows/ruby.yml | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/js.yml b/.github/workflows/js.yml index 00834b69..4b89d9c5 100644 --- a/.github/workflows/js.yml +++ b/.github/workflows/js.yml @@ -9,7 +9,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - node: [20] + node: [22, 24] runs-on: ${{ matrix.os }} diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index 8a792256..b9acde0c 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -41,7 +41,10 @@ jobs: - uses: actions/setup-node@v4 with: - node-version: '20' + node-version: '22' + + - name: Enable corepack (yarn/pnpm shims for test_app) + run: corepack enable - uses: ruby/setup-ruby@v1 with: From b44e5e69dcf337a9f39add8b9a4194056db21ae3 Mon Sep 17 00:00:00 2001 From: Andriy Tyurnikov Date: Tue, 5 May 2026 19:33:22 +0300 Subject: [PATCH 4/5] chore(ci): install workspace at root and build all packages js.yml: - pnpm install at the workspace root, so workspace deps resolve before any package builds (was per-package install). - Replace per-package build with `pnpm -r build` so every workspace package is built. Forward-compatible with adding more packages to the test matrix later (vite-plugin-rails tests resolve workspace vite-plugin-ruby through its built dist/). --- .github/workflows/js.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/js.yml b/.github/workflows/js.yml index 4b89d9c5..0dac6c55 100644 --- a/.github/workflows/js.yml +++ b/.github/workflows/js.yml @@ -25,10 +25,10 @@ jobs: cache: 'pnpm' node-version: ${{ matrix.node }} - - run: pnpm -C vite-plugin-ruby install --frozen-lockfile + - run: pnpm install --frozen-lockfile - - name: Build - run: pnpm -C vite-plugin-ruby build + - name: Build all workspace packages + run: pnpm -r build - name: Test run: pnpm -C vite-plugin-ruby test From 29a6c1d935b65b0206fb2a74412f23e2ead5cfb9 Mon Sep 17 00:00:00 2001 From: Andriy Tyurnikov Date: Tue, 5 May 2026 19:33:37 +0300 Subject: [PATCH 5/5] chore(ci): pin qltysh/qlty-action/coverage to SHA, tighten upload guard - Pin the action to v2.2.0 SHA instead of @main; pulling untrusted third-party action code from a moving tag on every CI run is a supply-chain risk for a workflow that has access to QLTY_COVERAGE_TOKEN. - Tighten the upload guard from `contains(github.ref, 'main')` to `github.event_name == 'push' && github.ref == 'refs/heads/main'`. The previous predicate matched any ref containing "main" (including PRs from forks pointing at main, branches named "main-*", etc.) and fired on `pull_request` events too; the new predicate uploads only on a push to the main branch. --- .github/workflows/ruby.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index b9acde0c..890e555a 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -55,8 +55,8 @@ jobs: run: bin/rake test - name: Upload coverage to Qlty - if: ${{ contains(github.ref, 'main') }} - uses: qltysh/qlty-action/coverage@main + if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} + uses: qltysh/qlty-action/coverage@a19242102d17e497f437d7466aa01b528537e899 # v2.2.0 with: token: ${{secrets.QLTY_COVERAGE_TOKEN}} files: coverage/.resultset.json