vite_rails 1.0.6 → 1.0.11

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +24 -0
  3. data/CONTRIBUTING.md +0 -1
  4. data/README.md +43 -70
  5. data/lib/install/config/vite.config.ts +1 -1
  6. data/lib/install/javascript/entrypoints/application.js +8 -4
  7. data/lib/install/template.rb +3 -3
  8. data/lib/tasks/vite/build.rake +12 -6
  9. data/lib/tasks/vite/clean.rake +1 -3
  10. data/lib/tasks/vite/install_dependencies.rake +3 -9
  11. data/lib/tasks/vite/verify_install.rake +3 -3
  12. data/lib/vite_rails.rb +23 -34
  13. data/lib/vite_rails/builder.rb +11 -13
  14. data/lib/vite_rails/commands.rb +51 -10
  15. data/lib/vite_rails/config.rb +65 -35
  16. data/lib/vite_rails/dev_server_proxy.rb +26 -18
  17. data/lib/vite_rails/helper.rb +17 -8
  18. data/lib/vite_rails/manifest.rb +14 -12
  19. data/lib/vite_rails/runner.rb +3 -6
  20. data/lib/vite_rails/version.rb +1 -1
  21. data/package.json +9 -2
  22. data/package/default.vite.json +2 -1
  23. data/test/builder_test.rb +27 -22
  24. data/test/commands_test.rb +67 -0
  25. data/test/config_test.rb +133 -0
  26. data/test/dev_server_proxy_test.rb +102 -0
  27. data/test/dev_server_test.rb +0 -30
  28. data/test/engine_rake_tasks_test.rb +56 -17
  29. data/test/helper_test.rb +37 -105
  30. data/test/manifest_test.rb +33 -29
  31. data/test/mode_test.rb +6 -11
  32. data/test/mounted_app/test/dummy/config/vite.json +5 -11
  33. data/test/mounted_app/test/dummy/package.json +5 -4
  34. data/test/mounted_app/test/dummy/yarn.lock +208 -0
  35. data/test/rake_tasks_test.rb +7 -21
  36. data/test/runner_test.rb +31 -0
  37. data/test/test_app/app/frontend/entrypoints/application.js +2 -0
  38. data/test/test_app/config/vite.json +0 -2
  39. data/test/test_app/config/vite_additional_paths.json +5 -0
  40. data/test/test_app/config/vite_public_dir.json +5 -0
  41. data/test/test_app/public/vite-production/manifest.json +22 -0
  42. data/test/test_helper.rb +48 -14
  43. metadata +23 -25
  44. data/test/command_test.rb +0 -35
  45. data/test/configuration_test.rb +0 -80
  46. data/test/dev_server_runner_test.rb +0 -83
  47. data/test/test_app/app/javascript/entrypoints/application.js +0 -10
  48. data/test/test_app/app/javascript/entrypoints/multi_entry.css +0 -4
  49. data/test/test_app/app/javascript/entrypoints/multi_entry.js +0 -4
  50. data/test/test_app/config/vite_public_root.yml +0 -20
  51. data/test/test_app/public/vite/manifest.json +0 -36
  52. data/test/vite_runner_test.rb +0 -59
  53. data/test/webpacker_test.rb +0 -15
@@ -11,6 +11,15 @@ class ViteRails::Commands
11
11
  manifest.refresh
12
12
  end
13
13
 
14
+ # Public: Defaults to production, and exits if the build fails.
15
+ def build_from_rake
16
+ with_node_env(ENV.fetch('NODE_ENV', 'production')) {
17
+ ensure_log_goes_to_stdout {
18
+ build || exit!
19
+ }
20
+ }
21
+ end
22
+
14
23
  # Public: Builds all assets that are managed by Vite, from the entrypoints.
15
24
  def build
16
25
  builder.build.tap { manifest.refresh }
@@ -20,6 +29,14 @@ class ViteRails::Commands
20
29
  def clobber
21
30
  config.build_output_dir.rmtree if config.build_output_dir.exist?
22
31
  config.build_cache_dir.rmtree if config.build_cache_dir.exist?
32
+ config.vite_cache_dir.rmtree if config.vite_cache_dir.exist?
33
+ end
34
+
35
+ # Public: Receives arguments from a rake task.
36
+ def clean_from_rake(args)
37
+ ensure_log_goes_to_stdout {
38
+ clean(keep_up_to: Integer(args.keep || 2), age_in_seconds: Integer(args.age || 3600))
39
+ }
23
40
  end
24
41
 
25
42
  # Public: Cleanup old assets in the output directory.
@@ -33,21 +50,16 @@ class ViteRails::Commands
33
50
  # To force only 1 backup to be kept: clean(1, 0)
34
51
  # To only keep files created within the last 10 minutes: clean(0, 600)
35
52
  def clean(keep_up_to: 2, age_in_seconds: 3600)
36
- return false unless config.build_output_dir.exist? && config.manifest_path.exist?
53
+ return false unless may_clean?
37
54
 
38
- versions.sort.reverse
55
+ versions
39
56
  .each_with_index
40
57
  .drop_while { |(mtime, _), index|
41
58
  max_age = [0, Time.now - Time.at(mtime)].max
42
59
  max_age < age_in_seconds || index < keep_up_to
43
60
  }
44
- .each do |(_, files), _index|
45
- files.each do |file|
46
- next unless File.file?(file)
47
-
48
- File.delete(file)
49
- logger.info("Removed #{ file }")
50
- end
61
+ .each do |(_, files), _|
62
+ clean_files(files)
51
63
  end
52
64
  true
53
65
  end
@@ -56,13 +68,42 @@ private
56
68
 
57
69
  delegate :config, :builder, :manifest, :logger, to: :@vite_rails
58
70
 
71
+ def may_clean?
72
+ config.build_output_dir.exist? && config.manifest_path.exist?
73
+ end
74
+
75
+ def clean_files(files)
76
+ files.select { |file| File.file?(file) }.each do |file|
77
+ File.delete(file)
78
+ logger.info("Removed #{ file }")
79
+ end
80
+ end
81
+
59
82
  def versions
60
83
  all_files = Dir.glob("#{ config.build_output_dir }/**/*")
61
84
  entries = all_files - [config.manifest_path] - current_version_files
62
- entries.reject { |file| File.directory?(file) }.group_by { |file| File.mtime(file).utc.to_i }
85
+ entries.reject { |file| File.directory?(file) }
86
+ .group_by { |file| File.mtime(file).utc.to_i }
87
+ .sort.reverse
63
88
  end
64
89
 
65
90
  def current_version_files
66
91
  Dir.glob(manifest.refresh.values.map { |value| config.build_output_dir.join("#{ value['file'] }*") })
67
92
  end
93
+
94
+ def with_node_env(env)
95
+ original = ENV['NODE_ENV']
96
+ ENV['NODE_ENV'] = env
97
+ yield
98
+ ensure
99
+ ENV['NODE_ENV'] = original
100
+ end
101
+
102
+ def ensure_log_goes_to_stdout
103
+ old_logger = ViteRails.logger
104
+ ViteRails.logger = ActiveSupport::Logger.new(STDOUT)
105
+ yield
106
+ ensure
107
+ ViteRails.logger = old_logger
108
+ end
68
109
  end
@@ -1,16 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'json'
4
+
3
5
  # Public: Allows to resolve configuration sourced from `config/vite.json` and
4
6
  # environment variables, combining them with the default options.
5
7
  class ViteRails::Config
6
8
  delegate :as_json, :inspect, to: :@config
7
9
 
8
- def initialize(config)
9
- @config = config.tap { coerce_values(config) }.freeze
10
-
11
- config.each_key do |option|
12
- define_singleton_method(option) { @config[option] }
13
- end
10
+ def initialize(attrs)
11
+ @config = attrs.tap { |config| coerce_values(config) }.freeze
14
12
  end
15
13
 
16
14
  def protocol
@@ -28,7 +26,26 @@ class ViteRails::Config
28
26
 
29
27
  # Public: The directory where Vite will store the built assets.
30
28
  def build_output_dir
31
- public_dir.join(public_output_dir)
29
+ root.join(public_dir, public_output_dir)
30
+ end
31
+
32
+ # Public: The directory where the entries are located.
33
+ def resolved_entrypoints_dir
34
+ root.join(source_code_dir, entrypoints_dir)
35
+ end
36
+
37
+ # Internal: The directory where Vite stores its processing cache.
38
+ def vite_cache_dir
39
+ root.join('node_modules/.vite')
40
+ end
41
+
42
+ # Public: Sets additional environment variables for vite-plugin-ruby.
43
+ def to_env
44
+ CONFIGURABLE_WITH_ENV.each_with_object({}) do |option, env|
45
+ unless (value = @config[option]).nil?
46
+ env["#{ ViteRails::ENV_PREFIX }_#{ option.upcase }"] = value.to_s
47
+ end
48
+ end.merge(ViteRails.env)
32
49
  end
33
50
 
34
51
  private
@@ -36,10 +53,11 @@ private
36
53
  # Internal: Coerces all the configuration values, in case they were passed
37
54
  # as environment variables which are always strings.
38
55
  def coerce_values(config)
39
- coerce_booleans(config, 'auto_build', 'https')
40
- coerce_paths(config, 'assets_dir', 'build_cache_dir', 'config_path', 'public_dir', 'source_code_dir', 'public_output_dir', 'root')
56
+ config['mode'] = config['mode'].to_s
41
57
  config['port'] = config['port'].to_i
42
- config['root'] ||= Rails.root
58
+ config['root'] = Pathname.new(config['root'])
59
+ config['build_cache_dir'] = config['root'].join(config['build_cache_dir'])
60
+ coerce_booleans(config, 'auto_build', 'hide_build_console_output', 'https')
43
61
  end
44
62
 
45
63
  # Internal: Coerces configuration options to boolean.
@@ -47,58 +65,70 @@ private
47
65
  names.each { |name| config[name] = [true, 'true'].include?(config[name]) }
48
66
  end
49
67
 
50
- # Internal: Converts configuration options to pathname.
51
- def coerce_paths(config, *names)
52
- names.each { |name| config[name] = Pathname.new(config[name]) unless config[name].nil? }
53
- end
54
-
55
68
  class << self
56
69
  # Public: Returns the project configuration for Vite.
57
- def resolve_config
58
- new DEFAULT_CONFIG.merge(config_from_file).merge(config_from_env)
59
- rescue Errno::ENOENT => error
60
- warn "Check that your vite.json configuration file is available in the load path. #{ error.message }"
61
- new DEFAULT_CONFIG.merge(config_from_env)
70
+ def resolve_config(**attrs)
71
+ config = attrs.transform_keys(&:to_s).reverse_merge(config_defaults)
72
+ file_path = File.join(config['root'], config['config_path'])
73
+ file_config = config_from_file(file_path, mode: config['mode'])
74
+ new DEFAULT_CONFIG.merge(file_config).merge(config_from_env).merge(config)
62
75
  end
63
76
 
64
77
  private
65
78
 
79
+ # Internal: Default values for a Rails application.
80
+ def config_defaults
81
+ {
82
+ 'asset_host' => option_from_env('asset_host') || Rails.application&.config&.action_controller&.asset_host,
83
+ 'config_path' => option_from_env('config_path') || DEFAULT_CONFIG.fetch('config_path'),
84
+ 'mode' => option_from_env('mode') || Rails.env.to_s,
85
+ 'root' => option_from_env('root') || Rails.root || Dir.pwd,
86
+ }
87
+ end
88
+
66
89
  # Internal: Used to load a JSON file from the specified path.
67
90
  def load_json(path)
68
91
  JSON.parse(File.read(File.expand_path(path))).deep_transform_keys(&:underscore)
69
92
  end
70
93
 
71
94
  # Internal: Retrieves a configuration option from environment variables.
72
- def config_option_from_env(name)
73
- ENV["#{ ViteRails::ENV_PREFIX }_#{ name.upcase }"]
95
+ def option_from_env(name)
96
+ ViteRails.env["#{ ViteRails::ENV_PREFIX }_#{ name.upcase }"]
74
97
  end
75
98
 
76
99
  # Internal: Extracts the configuration options provided as env vars.
77
100
  def config_from_env
78
- CONFIGURABLE_WITH_ENV.each_with_object({}) do |key, env_vars|
79
- if value = config_option_from_env(key)
80
- env_vars[key] = value
101
+ CONFIGURABLE_WITH_ENV.each_with_object({}) do |option, env_vars|
102
+ if value = option_from_env(option)
103
+ env_vars[option] = value
81
104
  end
82
- end.merge(mode: vite_mode)
83
- end
84
-
85
- # Internal: The mode Vite should run on.
86
- def vite_mode
87
- config_option_from_env('mode') || Rails.env.to_s
105
+ end
88
106
  end
89
107
 
90
108
  # Internal: Loads the configuration options provided in a JSON file.
91
- def config_from_file
92
- path = config_option_from_env('config_path') || DEFAULT_CONFIG.fetch('config_path')
109
+ def config_from_file(path, mode:)
93
110
  multi_env_config = load_json(path)
94
111
  multi_env_config.fetch('all', {})
95
- .merge(multi_env_config.fetch(vite_mode, {}))
112
+ .merge(multi_env_config.fetch(mode, {}))
113
+ rescue Errno::ENOENT => error
114
+ warn "Check that your vite.json configuration file is available in the load path. #{ error.message }"
115
+ {}
96
116
  end
97
117
  end
98
118
 
99
119
  # Internal: Shared configuration with the Vite plugin for Ruby.
100
120
  DEFAULT_CONFIG = load_json("#{ __dir__ }/../../package/default.vite.json").freeze
101
121
 
122
+ # Internal: Configuration options that can not be provided as env vars.
123
+ NOT_CONFIGURABLE_WITH_ENV = %w[watch_additional_paths].freeze
124
+
102
125
  # Internal: Configuration options that can be provided as env vars.
103
- CONFIGURABLE_WITH_ENV = (DEFAULT_CONFIG.keys + ['root']).freeze
126
+ CONFIGURABLE_WITH_ENV = (DEFAULT_CONFIG.keys + %w[mode root] - NOT_CONFIGURABLE_WITH_ENV).freeze
127
+
128
+ public
129
+
130
+ # Define getters for the configuration options.
131
+ (CONFIGURABLE_WITH_ENV + NOT_CONFIGURABLE_WITH_ENV).each do |option|
132
+ define_method(option) { @config[option] }
133
+ end
104
134
  end
@@ -14,18 +14,8 @@ class ViteRails::DevServerProxy < Rack::Proxy
14
14
 
15
15
  # Rack: Intercept asset requests and send them to the Vite server.
16
16
  def perform_request(env)
17
- if vite_should_handle?(env['REQUEST_URI'], env['HTTP_REFERER']) && dev_server.running?
18
- env['REQUEST_URI'] = env['REQUEST_URI']
19
- .sub(vite_asset_url_prefix, '/')
20
- .sub('.ts.js', '.ts') # Patch: Rails helpers always append the extension.
21
- env['PATH_INFO'], env['QUERY_STRING'] = env['REQUEST_URI'].split('?')
22
-
23
- env['HTTP_HOST'] = env['HTTP_X_FORWARDED_HOST'] = config.host
24
- env['HTTP_X_FORWARDED_SERVER'] = config.host_with_port
25
- env['HTTP_PORT'] = env['HTTP_X_FORWARDED_PORT'] = config.port.to_s
26
- env['HTTP_X_FORWARDED_PROTO'] = env['HTTP_X_FORWARDED_SCHEME'] = config.protocol
27
- env['HTTPS'] = env['HTTP_X_FORWARDED_SSL'] = 'off' unless config.https
28
- env['SCRIPT_NAME'] = ''
17
+ if vite_should_handle?(env) && dev_server_running?
18
+ forward_to_vite_dev_server(env)
29
19
  super(env)
30
20
  else
31
21
  @app.call(env)
@@ -34,13 +24,31 @@ class ViteRails::DevServerProxy < Rack::Proxy
34
24
 
35
25
  private
36
26
 
37
- delegate :config, :dev_server, to: :@vite_rails
27
+ delegate :config, :dev_server_running?, to: :@vite_rails
38
28
 
39
- def vite_should_handle?(url, referer)
40
- return true if url.start_with?(vite_asset_url_prefix) # Vite Asset
41
- return true if url.start_with?(VITE_DEPENDENCY_PREFIX) # Vite Package Asset
42
- return true if url.include?('?t=') # Hot Reload
43
- return true if referer && URI.parse(referer).path.start_with?(vite_asset_url_prefix) # Entry Imported from another Entry.
29
+ def rewrite_uri_for_vite(env)
30
+ uri = env.fetch('REQUEST_URI') { [env['PATH_INFO'], env['QUERY_STRING']].reject(&:blank?).join('?') }
31
+ .sub(vite_asset_url_prefix, '/')
32
+ env['PATH_INFO'], env['QUERY_STRING'] = (env['REQUEST_URI'] = uri).split('?')
33
+ end
34
+
35
+ def forward_to_vite_dev_server(env)
36
+ rewrite_uri_for_vite(env)
37
+ env['HTTP_HOST'] = env['HTTP_X_FORWARDED_HOST'] = config.host
38
+ env['HTTP_X_FORWARDED_SERVER'] = config.host_with_port
39
+ env['HTTP_PORT'] = env['HTTP_X_FORWARDED_PORT'] = config.port.to_s
40
+ env['HTTP_X_FORWARDED_PROTO'] = env['HTTP_X_FORWARDED_SCHEME'] = config.protocol
41
+ env['HTTPS'] = env['HTTP_X_FORWARDED_SSL'] = 'off' unless config.https
42
+ env['SCRIPT_NAME'] = ''
43
+ end
44
+
45
+ def vite_should_handle?(env)
46
+ path, query, referer = env['PATH_INFO'], env['QUERY_STRING'], env['HTTP_REFERER']
47
+ return true if path.start_with?(vite_asset_url_prefix) # Vite asset
48
+ return true if path.start_with?(VITE_DEPENDENCY_PREFIX) # Packages and imports
49
+ return true if query&.start_with?('t=') # Hot Reload for a stylesheet
50
+ return true if query&.start_with?('import&') # Hot Reload for an imported entrypoint
51
+ return true if referer && URI.parse(referer).path.start_with?(vite_asset_url_prefix) # Entry imported from another entry.
44
52
  end
45
53
 
46
54
  def vite_asset_url_prefix
@@ -2,19 +2,26 @@
2
2
 
3
3
  # Public: Allows to render HTML tags for scripts and styles processed by Vite.
4
4
  module ViteRails::Helper
5
- DEFAULT_VITE_SKIP_PRELOAD_TAGS = Rails::VERSION::MAJOR <= 5 && Rails::VERSION::MINOR < 2
5
+ DEFAULT_VITE_SKIP_PRELOAD_TAGS = Rails.gem_version < Gem::Version.new('5.2.0')
6
6
 
7
7
  # Public: Returns the current Vite Rails instance.
8
8
  def current_vite_instance
9
9
  ViteRails.instance
10
10
  end
11
11
 
12
- # Public: Computes the relative path for the specified given Vite asset.
12
+ # Public: Renders a script tag for vite/client to enable HMR in development.
13
+ def vite_client_tag
14
+ return unless current_vite_instance.dev_server_running?
15
+
16
+ content_tag('script', '', src: current_vite_instance.manifest.prefix_vite_asset('@vite/client'), type: 'module')
17
+ end
18
+
19
+ # Public: Resolves the path for the specified Vite asset.
13
20
  #
14
21
  # Example:
15
22
  # <%= vite_asset_path 'calendar.css' %> # => "/vite/assets/calendar-1016838bab065ae1e122.css"
16
23
  def vite_asset_path(name, **options)
17
- current_vite_instance.manifest.lookup!(name, **options).fetch('file')
24
+ path_to_asset current_vite_instance.manifest.lookup!(name, **options).fetch('file')
18
25
  end
19
26
 
20
27
  # Public: Renders a <script> tag for the specified Vite entrypoints.
@@ -26,15 +33,17 @@ module ViteRails::Helper
26
33
  crossorigin: 'anonymous',
27
34
  **options)
28
35
  js_entries = names.map { |name| current_vite_instance.manifest.lookup!(name, type: asset_type) }
29
- js_tags = javascript_include_tag(*js_entries.map { |entry| entry['file'] }, type: type, crossorigin: crossorigin, **options)
36
+ js_tags = javascript_include_tag(*js_entries.map { |entry| entry['file'] }, crossorigin: crossorigin, type: type, **options)
30
37
 
31
- unless skip_preload_tags || ViteRails.dev_server.running?
38
+ unless skip_preload_tags || current_vite_instance.dev_server_running?
32
39
  preload_paths = js_entries.flat_map { |entry| entry['imports'] }.compact.uniq
33
40
  preload_tags = preload_paths.map { |path| preload_link_tag(path, crossorigin: crossorigin) }
34
41
  end
35
42
 
36
- unless skip_style_tags || ViteRails.dev_server.running?
37
- style_paths = names.map { |name| current_vite_instance.manifest.lookup(name, type: :stylesheet)&.fetch('file') }.compact
43
+ unless skip_style_tags || current_vite_instance.dev_server_running?
44
+ style_paths = names.map { |name|
45
+ current_vite_instance.manifest.lookup(name.delete_suffix('.js'), type: :stylesheet)&.fetch('file')
46
+ }.compact
38
47
  style_tags = stylesheet_link_tag(*style_paths)
39
48
  end
40
49
 
@@ -46,7 +55,7 @@ module ViteRails::Helper
46
55
  # NOTE: Because TypeScript is not a valid target in browsers, we only specify
47
56
  # the ts file when running the Vite development server.
48
57
  def vite_typescript_tag(*names, **options)
49
- vite_javascript_tag(*names, asset_type: :typescript, **options)
58
+ vite_javascript_tag(*names, asset_type: :typescript, extname: false, **options)
50
59
  end
51
60
 
52
61
  # Public: Renders a <link> tag for the specified Vite entrypoints.
@@ -31,7 +31,7 @@ class ViteRails::Manifest
31
31
  # Example:
32
32
  # ViteRails.manifest.lookup('calendar.js')
33
33
  # # { "file" => "/vite/assets/calendar-1016838bab065ae1e122.js", "imports" => [] }
34
- def lookup(name, type:)
34
+ def lookup(name, type: nil)
35
35
  build if should_build?
36
36
 
37
37
  find_manifest_entry(with_file_extension(name, type))
@@ -42,6 +42,11 @@ class ViteRails::Manifest
42
42
  @manifest = load_manifest
43
43
  end
44
44
 
45
+ # Public: Scopes an asset to the output folder in public, as a path.
46
+ def prefix_vite_asset(path)
47
+ File.join("/#{ config.public_output_dir }", path)
48
+ end
49
+
45
50
  private
46
51
 
47
52
  delegate :config, :builder, :dev_server_running?, to: :@vite_rails
@@ -55,7 +60,7 @@ private
55
60
  # Internal: Finds the specified entry in the manifest.
56
61
  def find_manifest_entry(name)
57
62
  if dev_server_running?
58
- { 'file' => "/#{ config.public_output_dir.join(name.to_s) }" }
63
+ { 'file' => prefix_vite_asset(name.to_s) }
59
64
  else
60
65
  manifest[name.to_s]
61
66
  end
@@ -80,8 +85,8 @@ private
80
85
  def load_manifest
81
86
  if config.manifest_path.exist?
82
87
  JSON.parse(config.manifest_path.read).each do |_, entry|
83
- entry['file'] = within_public_output_dir(entry['file'])
84
- entry['imports'] = entry['imports']&.map { |path| within_public_output_dir(path) }
88
+ entry['file'] = prefix_vite_asset(entry['file'])
89
+ entry['imports'] = entry['imports']&.map { |path| prefix_vite_asset(path) }
85
90
  end
86
91
  else
87
92
  {}
@@ -92,12 +97,8 @@ private
92
97
  def with_file_extension(name, entry_type)
93
98
  return name unless File.extname(name.to_s).empty?
94
99
 
95
- "#{ name }.#{ extension_for_type(entry_type) }"
96
- end
97
-
98
- # Internal: Scopes the paths in the manifest to the output folder in public.
99
- def within_public_output_dir(path)
100
- "/#{ config.public_output_dir.join(path) }"
100
+ extension = extension_for_type(entry_type)
101
+ extension ? "#{ name }.#{ extension }" : name
101
102
  end
102
103
 
103
104
  # Internal: Allows to receive :javascript and :stylesheet as :type in helpers.
@@ -106,7 +107,7 @@ private
106
107
  when :javascript then 'js'
107
108
  when :stylesheet then 'css'
108
109
  when :typescript then dev_server_running? ? 'ts' : 'js'
109
- else entry_type.to_s
110
+ else entry_type
110
111
  end
111
112
  end
112
113
 
@@ -129,9 +130,10 @@ private
129
130
  [
130
131
  (dev_server_running? && 'Vite has not yet re-built your latest changes.'),
131
132
  (local && !dev_server_running? && "\"autoBuild\": false in your #{ config.mode } configuration."),
133
+ (local && !dev_server_running? && 'The Vite development server has crashed or is no longer available.'),
132
134
  'You have misconfigured config/vite.json file.',
133
135
  (!local && 'Assets have not been precompiled'),
134
- ].select(&:itself)
136
+ ].compact
135
137
  rescue StandardError
136
138
  []
137
139
  end