diff --git a/docs/src/config/index.md b/docs/src/config/index.md index f09d6aa6..eed5c47e 100644 --- a/docs/src/config/index.md +++ b/docs/src/config/index.md @@ -274,9 +274,10 @@ You can customize this behavior using the following options. - **Default:** `false` - **Env Var:** `VITE_RUBY_DEV_SERVER_CONNECTION_CHECK` - By default the dev server is detected by reading `tmp/vite-ruby.json`, a - metadata file the Vite plugin writes with the dev server URL when it starts, - and removes when it stops. + By default the dev server is detected by reading `tmp/vite-ruby-.json` + (for example, `tmp/vite-ruby-development.json`), a metadata file the Vite + plugin writes when the dev server starts and removes when it stops. The + metadata must have the configured port for the current mode. Enable this to detect the dev server by opening a TCP connection to the configured `host` and `port` instead. Useful if the metadata file can become diff --git a/docs/src/guide/troubleshooting.md b/docs/src/guide/troubleshooting.md index 2b4413c3..219cda80 100644 --- a/docs/src/guide/troubleshooting.md +++ b/docs/src/guide/troubleshooting.md @@ -166,8 +166,8 @@ First, verify that the dev server is reachable by starting a new console session > ViteRuby.instance.dev_server_running? ``` -By default this reads `tmp/vite-ruby.json`, written by the Vite plugin while the dev server runs. -If it returns `false` while the dev server is running, make sure you are on a recent [vite-plugin-ruby] that writes this file, and that `tmp/` is writable. +By default this reads `tmp/vite-ruby-.json` (for example, `tmp/vite-ruby-development.json`), written by the Vite plugin while the server runs. +If it returns `false` while the dev server is running, make sure you are using vite-plugin-ruby 5.2.5 or later, that `tmp/` is writable, and that the dev server's port matches the configured port for this mode. If you have enabled [devServerConnectionCheck], try increasing the [devServerConnectTimeout], restart the console and retry. In systems with constrained resources the [default timeout][devServerConnectTimeout] might not be enough. diff --git a/test/compatibility_check_test.rb b/test/compatibility_check_test.rb index 49e1bc5f..cca17438 100644 --- a/test/compatibility_check_test.rb +++ b/test/compatibility_check_test.rb @@ -23,6 +23,12 @@ def test_compatible_plugin assert compatible_plugin?(nil, nil) end + def test_requires_plugin_version_with_mode_specific_metadata + assert_equal "^5.2.5", ViteRuby::DEFAULT_PLUGIN_VERSION + refute compatible_plugin?("^5.2.4", ViteRuby::DEFAULT_PLUGIN_VERSION) + assert compatible_plugin?("^5.2.5", ViteRuby::DEFAULT_PLUGIN_VERSION) + end + def test_raise_unless_satisfied assert_raises(ArgumentError) { raise_unless_satisfied("^4.1.0", "^3.0") } raise_unless_satisfied("3.1.0", "^3.0") diff --git a/test/config_test.rb b/test/config_test.rb index e6223990..f3a083c9 100644 --- a/test/config_test.rb +++ b/test/config_test.rb @@ -63,6 +63,12 @@ def test_ruby_config_file assert_equal Gem.loaded_specs["rails"].full_gem_path, ViteRuby.config.to_env["EXAMPLE_PATH"] end + def test_dev_server_meta_path_is_scoped_to_mode + assert_path "test_app/tmp/vite-ruby-production.json", @config.dev_server_meta_path + assert_path "test_app/tmp/vite-ruby-development.json", resolve_config(mode: "development").dev_server_meta_path + assert_path "test_app/tmp/vite-ruby-test.json", resolve_config(mode: "test").dev_server_meta_path + end + def test_manifest_path assert_path "test_app/public/vite-production/.vite/manifest.json", @config.manifest_paths.first end diff --git a/test/dev_server_test.rb b/test/dev_server_test.rb index e04b81e0..acba502f 100644 --- a/test/dev_server_test.rb +++ b/test/dev_server_test.rb @@ -12,10 +12,59 @@ def test_not_running_without_meta_file def test_running_with_meta_file refresh_config(mode: "development") - write_meta_file(url: "http://localhost:3036", host: "localhost", port: 3036, https: false, pid: 1234) + write_meta_file(url: "http://localhost:3535", host: "localhost", port: ViteRuby.config.port, https: false, pid: 1234) assert_predicate ViteRuby.instance, :dev_server_running? - assert_equal "http://localhost:3036", ViteRuby.instance.send(:dev_server_meta)["url"] + assert_equal "http://localhost:3535", ViteRuby.instance.send(:dev_server_meta)["url"] + ensure + remove_meta_file + end + + def test_development_meta_does_not_mark_test_server_as_running + refresh_config(mode: "test") + remove_meta_file + test_port = ViteRuby.config.port + refresh_config(mode: "development", port: test_port) + write_meta_file(port: test_port) + + refresh_config(mode: "test") + + ENV.stub(:[], ->(key) { ENV.fetch(key, nil) unless key == "CI" }) do + refute_predicate ViteRuby.instance, :dev_server_running? + end + ensure + remove_meta_file if ViteRuby.config.mode == "test" + refresh_config(mode: "development") + remove_meta_file + end + + def test_running_with_test_meta_file + refresh_config(mode: "test") + write_meta_file(port: ViteRuby.config.port) + + ENV.stub(:[], ->(key) { ENV.fetch(key, nil) unless key == "CI" }) do + assert_predicate ViteRuby.instance, :dev_server_running? + end + ensure + remove_meta_file + end + + def test_not_running_when_meta_port_does_not_match + refresh_config(mode: "development") + write_meta_file(port: ViteRuby.config.port + 1) + + refute_predicate ViteRuby.instance, :dev_server_running? + ensure + remove_meta_file + end + + def test_not_running_with_invalid_meta_shape + refresh_config(mode: "development") + path = ViteRuby.config.dev_server_meta_path + path.dirname.mkpath + path.write(JSON.generate("not a metadata object")) + + refute_predicate ViteRuby.instance, :dev_server_running? ensure remove_meta_file end diff --git a/vite-plugin-ruby/src/constants.ts b/vite-plugin-ruby/src/constants.ts index 5de158cd..49b8b438 100644 --- a/vite-plugin-ruby/src/constants.ts +++ b/vite-plugin-ruby/src/constants.ts @@ -7,9 +7,6 @@ export const ENV_PREFIX = 'VITE_RUBY' // Internal: Key of the vite.json file that is applied to all environments. export const ALL_ENVS_KEY = 'all' -// Internal: Path, relative to the project root, of the dev server metadata file. -export const DEV_SERVER_META_FILE = 'tmp/vite-ruby.json' - // Internal: Extensions of CSS files or known precompilers. export const KNOWN_CSS_EXTENSIONS = [ 'css', diff --git a/vite-plugin-ruby/src/dev-server.ts b/vite-plugin-ruby/src/dev-server.ts index ac01306c..d6cdfa76 100644 --- a/vite-plugin-ruby/src/dev-server.ts +++ b/vite-plugin-ruby/src/dev-server.ts @@ -1,5 +1,5 @@ import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'fs' -import { dirname } from 'path' +import { dirname, resolve } from 'path' import type { AddressInfo } from 'net' import type { ResolvedConfig } from 'vite' @@ -18,6 +18,11 @@ const WILDCARD_HOSTS = new Set(['', '0.0.0.0', '::', '::1']) let exitHandlersBound = false let ownedMetaPath: string | null = null +// Internal: Keeps metadata for each Ruby mode separate. +export function devServerMetaPath (root: string, mode: string): string { + return resolve(root, 'tmp', `vite-ruby-${mode}.json`) +} + // Internal: Returns true when the address is a resolved TCP address. function isAddressInfo (address: string | AddressInfo | null | undefined): address is AddressInfo { return Boolean(address) && typeof address === 'object' diff --git a/vite-plugin-ruby/src/index.ts b/vite-plugin-ruby/src/index.ts index 8faa71aa..1906d879 100644 --- a/vite-plugin-ruby/src/index.ts +++ b/vite-plugin-ruby/src/index.ts @@ -4,10 +4,10 @@ import type { ConfigEnv, PluginOption, UserConfig, ViteDevServer } from 'vite' import { createDebug } from 'obug' import { cleanConfig, configOptionFromEnv } from './utils' +import type { UnifiedConfig } from './types' import { filterEntrypointsForRollup, loadConfiguration, resolveGlobs } from './config' import { assetsManifestPlugin } from './manifest' -import { bindDevServerCleanup, resolveDevServerMeta, writeDevServerMeta } from './dev-server' -import { DEV_SERVER_META_FILE } from './constants' +import { bindDevServerCleanup, devServerMetaPath, resolveDevServerMeta, writeDevServerMeta } from './dev-server' export * from './types' @@ -105,10 +105,11 @@ function config (userConfig: UserConfig, env: ConfigEnv): UserConfig { function configureServer (server: ViteDevServer) { server.watcher.add(watchAdditionalPaths) - const devServerMetaPath = resolve(projectRoot, DEV_SERVER_META_FILE) + const rubyConfig = server.config as typeof server.config & { viteRuby: UnifiedConfig } + const metaPath = devServerMetaPath(projectRoot, rubyConfig.viteRuby.mode) server.httpServer?.once('listening', () => { - writeDevServerMeta(devServerMetaPath, resolveDevServerMeta(server.httpServer?.address(), server.config)) - bindDevServerCleanup(devServerMetaPath) + writeDevServerMeta(metaPath, resolveDevServerMeta(server.httpServer?.address(), server.config)) + bindDevServerCleanup(metaPath) }) return () => server.middlewares.use((req, res, next) => { diff --git a/vite-plugin-ruby/tests/dev-server.spec.ts b/vite-plugin-ruby/tests/dev-server.spec.ts index 69f9fde8..ca060673 100644 --- a/vite-plugin-ruby/tests/dev-server.spec.ts +++ b/vite-plugin-ruby/tests/dev-server.spec.ts @@ -3,7 +3,7 @@ import { tmpdir } from 'os' import { join } from 'path' import { describe, it, expect } from 'vitest' -import { removeOwnedMeta, resolveDevServerMeta, writeDevServerMeta } from '../src/dev-server' +import { devServerMetaPath, removeOwnedMeta, resolveDevServerMeta, writeDevServerMeta } from '../src/dev-server' const withServer = (server: Record) => ({ server }) as any @@ -23,6 +23,15 @@ describe('resolveDevServerMeta', () => { }) }) +describe('devServerMetaPath', () => { + it('uses separate metadata files for the resolved Ruby modes', () => { + const root = join(tmpdir(), 'vite-app') + + expect(devServerMetaPath(root, 'development')).toBe(join(root, 'tmp/vite-ruby-development.json')) + expect(devServerMetaPath(root, 'test')).toBe(join(root, 'tmp/vite-ruby-test.json')) + }) +}) + describe('removeOwnedMeta', () => { it('only removes the file when the pid matches', () => { const path = join(mkdtempSync(join(tmpdir(), 'vpr-')), 'vite-ruby.json') @@ -34,4 +43,18 @@ describe('removeOwnedMeta', () => { removeOwnedMeta(path, 4242) expect(existsSync(path)).toBe(false) }) + + it('does not remove another mode\'s metadata when a server stops', () => { + const root = mkdtempSync(join(tmpdir(), 'vpr-')) + const devPath = devServerMetaPath(root, 'development') + const testPath = devServerMetaPath(root, 'test') + writeDevServerMeta(devPath, { url: 'x', host: 'h', port: 1, https: false, pid: 4242 }) + writeDevServerMeta(testPath, { url: 'x', host: 'h', port: 2, https: false, pid: 4243 }) + + removeOwnedMeta(devPath, 4242) + + expect(existsSync(devPath)).toBe(false) + expect(existsSync(testPath)).toBe(true) + removeOwnedMeta(testPath, 4243) + }) }) diff --git a/vite_ruby/lib/vite_ruby.rb b/vite_ruby/lib/vite_ruby.rb index 71a59ae8..be2a6625 100644 --- a/vite_ruby/lib/vite_ruby.rb +++ b/vite_ruby/lib/vite_ruby.rb @@ -87,7 +87,8 @@ def dev_server_running? return false unless dev_mode? return dev_server_connected? if config.dev_server_connection_check - !dev_server_meta.nil? + meta = dev_server_meta + meta.is_a?(Hash) && meta["port"] == config.port end # Public: Additional environment variables to pass to Vite. diff --git a/vite_ruby/lib/vite_ruby/config.rb b/vite_ruby/lib/vite_ruby/config.rb index 2f12085f..4db91285 100644 --- a/vite_ruby/lib/vite_ruby/config.rb +++ b/vite_ruby/lib/vite_ruby/config.rb @@ -5,8 +5,8 @@ # Public: Allows to resolve configuration sourced from `config/vite.json` and # environment variables, combining them with the default options. class ViteRuby::Config - # Internal: Name of the metadata file written by the Vite dev server. - DEV_SERVER_META_FILENAME = "vite-ruby.json" + # Internal: Base name of the metadata file written by the Vite dev server. + DEV_SERVER_META_BASENAME = "vite-ruby" def origin "#{protocol}://#{host_with_port}" @@ -43,7 +43,7 @@ def build_output_dir # Internal: Path to the metadata file written by the Vite dev server. def dev_server_meta_path - root.join("tmp", DEV_SERVER_META_FILENAME) + root.join("tmp", "#{DEV_SERVER_META_BASENAME}-#{mode}.json") end # Public: The directory where the entries are located. diff --git a/vite_ruby/lib/vite_ruby/version.rb b/vite_ruby/lib/vite_ruby/version.rb index c96e4ced..097fcb12 100644 --- a/vite_ruby/lib/vite_ruby/version.rb +++ b/vite_ruby/lib/vite_ruby/version.rb @@ -5,5 +5,5 @@ class ViteRuby # Internal: Versions used by default when running `vite install`. DEFAULT_VITE_VERSION = "^8.0.0" - DEFAULT_PLUGIN_VERSION = "^5.2.0" + DEFAULT_PLUGIN_VERSION = "^5.2.5" end