From b49f73065e8bc912e53c26bc9972950a77663d04 Mon Sep 17 00:00:00 2001 From: Andriy Tyurnikov Date: Mon, 27 Apr 2026 20:19:14 +0300 Subject: [PATCH 1/4] fix(vite-plugin-ruby): stop forcing sourcemap=true in production (close #588) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The plugin was setting `build.sourcemap = !isLocal`, which inverted Vite's safe default: - production / staging → sourcemap=true (publishes .js.map publicly, exposing application source code) - development / test → sourcemap=false (breaks debugging where you'd actually want sourcemaps) Drop the override entirely. Vite's default (`false`) now applies in every mode, and consumers opt in to sourcemaps via `build.sourcemap` in their own vite.config when they actually want them — for production via external upload to e.g. Sentry, or for development debugging. The unit test in vite-plugin-ruby/tests/index.spec.ts had been encoding the inverted behavior; updated to assert `sourcemap` is no longer set by the plugin. The integration test in vite-plugin-rails drops the `.map` entries from the expected file list since the example build no longer emits them. --- vite-plugin-rails/tests/build.spec.ts | 3 --- vite-plugin-ruby/src/index.ts | 1 - vite-plugin-ruby/tests/index.spec.ts | 10 ++++++---- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/vite-plugin-rails/tests/build.spec.ts b/vite-plugin-rails/tests/build.spec.ts index 4373364d..bd59b2ed 100644 --- a/vite-plugin-rails/tests/build.spec.ts +++ b/vite-plugin-rails/tests/build.spec.ts @@ -22,7 +22,6 @@ describe('config', () => { 'assets/external-BwssHmjP.js', 'assets/external-BwssHmjP.js.br', 'assets/external-BwssHmjP.js.gz', - 'assets/external-BwssHmjP.js.map', 'assets/index-qTzjl5TV.js', 'assets/index-qTzjl5TV.js.br', 'assets/index-qTzjl5TV.js.gz', @@ -34,7 +33,6 @@ describe('config', () => { 'assets/main-Ddvw3iap.js', 'assets/main-Ddvw3iap.js.br', 'assets/main-Ddvw3iap.js.gz', - 'assets/main-Ddvw3iap.js.map', 'assets/sassy-D5kz_As0.css', 'assets/sassy-D5kz_As0.css.br', 'assets/sassy-D5kz_As0.css.gz', @@ -47,7 +45,6 @@ describe('config', () => { 'assets/vue-CoJ_KGkH.js', 'assets/vue-CoJ_KGkH.js.br', 'assets/vue-CoJ_KGkH.js.gz', - 'assets/vue-CoJ_KGkH.js.map', 'index.html', 'index.html.br', 'index.html.gz', diff --git a/vite-plugin-ruby/src/index.ts b/vite-plugin-ruby/src/index.ts index 4a370d2f..2c9d3654 100644 --- a/vite-plugin-ruby/src/index.ts +++ b/vite-plugin-ruby/src/index.ts @@ -60,7 +60,6 @@ function config (userConfig: UserConfig, env: ConfigEnv): UserConfig { const build = { emptyOutDir: userConfig.build?.emptyOutDir ?? (ssrBuild || isLocal), - sourcemap: !isLocal, ...userConfig.build, assetsDir, manifest: !ssrBuild, diff --git a/vite-plugin-ruby/tests/index.spec.ts b/vite-plugin-ruby/tests/index.spec.ts index 10f3c572..22597c67 100644 --- a/vite-plugin-ruby/tests/index.spec.ts +++ b/vite-plugin-ruby/tests/index.spec.ts @@ -8,21 +8,23 @@ describe('config', () => { const pluginConfig = plugin[0].config defaultConfig.configPath = './default.vite.json' + // Sourcemap is no longer set by the plugin (Vite's safe default of `false` + // applies in all modes). Users opt in via `build.sourcemap` in their vite.config. const productionConfig = pluginConfig(defaultConfig, { mode: 'production' }) expect(productionConfig.build.emptyOutDir).toBe(false) - expect(productionConfig.build.sourcemap).toBe(true) + expect(productionConfig.build.sourcemap).toBeUndefined() const stagingConfig = pluginConfig(defaultConfig, { mode: 'staging' }) expect(stagingConfig.build.emptyOutDir).toBe(false) - expect(stagingConfig.build.sourcemap).toBe(true) + expect(stagingConfig.build.sourcemap).toBeUndefined() const developmentConfig = pluginConfig(defaultConfig, { mode: 'development' }) expect(developmentConfig.build.emptyOutDir).toBe(true) - expect(developmentConfig.build.sourcemap).toBe(false) + expect(developmentConfig.build.sourcemap).toBeUndefined() const testConfig = pluginConfig(defaultConfig, { mode: 'test' }) expect(testConfig.build.emptyOutDir).toBe(true) - expect(testConfig.build.sourcemap).toBe(false) + expect(testConfig.build.sourcemap).toBeUndefined() expect(() => { pluginConfig({ ...defaultConfig, build: { ssr: true } }, { mode: 'production' }) From e07b80eaac57b2e0577a702c14e8c87f9ba3e41d Mon Sep 17 00:00:00 2001 From: Andriy Tyurnikov Date: Mon, 27 Apr 2026 16:12:32 +0300 Subject: [PATCH 2/4] fix(examples/rails): gate engine alias on env var to satisfy Vite 8 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The example used a TypeScript non-null assertion on process.env.ADMINISTRATOR_ASSETS_PATH that is unset at build time when no Rails engine is wired up, leaving [undefined] in server.fs.allow and an alias resolving to "undefined/...". Vite ≤7 silently tolerated those undefined entries; Vite 8 strictly path-resolves server.fs.allow and crashes with "paths[1] argument must be of type string". Make both the alias and the fs.allow entry conditional on the env var actually being present, so the build works whether or not the engine fixture is wired up. --- examples/rails/vite.config.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/examples/rails/vite.config.ts b/examples/rails/vite.config.ts index b6c707e7..2c1d831a 100644 --- a/examples/rails/vite.config.ts +++ b/examples/rails/vite.config.ts @@ -4,6 +4,8 @@ import rails from 'vite-plugin-rails' import WindiCSS from 'vite-plugin-windicss' import BugsnagPlugins from './plugins/bugsnag' +const administratorAssetsPath = process.env.ADMINISTRATOR_ASSETS_PATH + export default defineConfig({ plugins: [ BugsnagPlugins, @@ -26,13 +28,13 @@ export default defineConfig({ ], // Example: Importing assets from arbitrary paths. resolve: { - alias: { - '@administrator/': `${process.env.ADMINISTRATOR_ASSETS_PATH}/`, - }, + alias: administratorAssetsPath + ? { '@administrator/': `${administratorAssetsPath}/` } + : {}, }, server: { fs: { - allow: [process.env.ADMINISTRATOR_ASSETS_PATH!], + allow: administratorAssetsPath ? [administratorAssetsPath] : [], }, }, }) From cda8a608a9c601e25a594b9e535b61684a77e254 Mon Sep 17 00:00:00 2001 From: Andriy Tyurnikov Date: Tue, 5 May 2026 19:09:58 +0300 Subject: [PATCH 3/4] fix(vite-plugin-ruby): return after res.end() in dev /index.html 404 fallback Without the explicit return, control falls through to next() after the response has already been ended, allowing downstream middleware to write to a closed response. --- vite-plugin-ruby/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/vite-plugin-ruby/src/index.ts b/vite-plugin-ruby/src/index.ts index 2c9d3654..fb2f372e 100644 --- a/vite-plugin-ruby/src/index.ts +++ b/vite-plugin-ruby/src/index.ts @@ -107,6 +107,7 @@ function configureServer (server: ViteDevServer) { res.statusCode = 404 const file = readFileSync(resolve(__dirname, 'dev-server-index.html'), 'utf-8') res.end(file) + return } next() From 7d4ddf7c5a52e2ace4cd0f034ccfc19723e424ab Mon Sep 17 00:00:00 2001 From: Andriy Tyurnikov Date: Tue, 5 May 2026 19:10:44 +0300 Subject: [PATCH 4/4] fix(vite_ruby): guard dev_server_running? cache with a Mutex The prior implementation read `@running` and wrote `@running_checked_at` without synchronization, so a cold concurrent call from a multi-threaded server (e.g. Puma) could read `@running` after it was set but before `@running_checked_at` was, and the subsequent `Time.now - @running_checked_at` on the next call could raise NoMethodError on nil. Wrap the check-or-refresh in `@running_mutex.synchronize` so the cache read and write are atomic. --- vite_ruby/lib/vite_ruby.rb | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/vite_ruby/lib/vite_ruby.rb b/vite_ruby/lib/vite_ruby.rb index 9c16e02a..daf6631f 100644 --- a/vite_ruby/lib/vite_ruby.rb +++ b/vite_ruby/lib/vite_ruby.rb @@ -69,6 +69,7 @@ def framework_libraries def initialize(**config_options) @config_options = config_options + @running_mutex = Mutex.new end def logger @@ -85,15 +86,18 @@ def digest # NOTE: Checks only once every second since every lookup calls this method. def dev_server_running? return false unless run_proxy? - return @running if defined?(@running) && Time.now - @running_checked_at < 1 - - begin - Socket.tcp(config.host, config.port, connect_timeout: config.dev_server_connect_timeout).close - @running = true - rescue - @running = false - ensure - @running_checked_at = Time.now + + @running_mutex.synchronize do + return @running if defined?(@running) && Time.now - @running_checked_at < 1 + + begin + Socket.tcp(config.host, config.port, connect_timeout: config.dev_server_connect_timeout).close + @running = true + rescue + @running = false + ensure + @running_checked_at = Time.now + end end end