vite_ruby 3.9.0 → 4.0.0.alpha1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'json'
3
4
  require 'digest/sha1'
4
5
 
5
6
  # Public: Keeps track of watched files and triggers builds as needed.
@@ -11,13 +12,9 @@ class ViteRuby::Builder
11
12
  # Public: Checks if the watched files have changed since the last compilation,
12
13
  # and triggers a Vite build if any files have changed.
13
14
  def build(*args)
14
- last_build = last_build_metadata(ssr: args.include?('--ssr'))
15
-
16
- if args.delete('--force') || last_build.stale? || config.manifest_paths.empty?
17
- stdout, stderr, status = build_with_vite(*args)
18
- log_build_result(stdout, stderr, status)
19
- record_build_metadata(last_build, errors: stderr, success: status.success?)
20
- status.success?
15
+ last_build = last_build_metadata
16
+ if args.delete('--force') || last_build.stale?
17
+ build_with_vite(*args).tap { |success| record_build_metadata(success, last_build) }
21
18
  elsif last_build.success
22
19
  logger.debug "Skipping vite build. Watched files have not changed since the last build at #{ last_build.timestamp }"
23
20
  true
@@ -28,8 +25,8 @@ class ViteRuby::Builder
28
25
  end
29
26
 
30
27
  # Internal: Reads the result of the last compilation from disk.
31
- def last_build_metadata(ssr: false)
32
- ViteRuby::Build.from_previous(last_build_path(ssr: ssr), watched_files_digest)
28
+ def last_build_metadata
29
+ ViteRuby::Build.from_previous(last_build_attrs, watched_files_digest)
33
30
  end
34
31
 
35
32
  private
@@ -38,35 +35,44 @@ private
38
35
 
39
36
  def_delegators :@vite_ruby, :config, :logger, :run
40
37
 
38
+ # Internal: Reads metadata recorded on the last build, if it exists.
39
+ def last_build_attrs
40
+ last_build_path.exist? ? JSON.parse(last_build_path.read.to_s) : {}
41
+ rescue JSON::JSONError, Errno::ENOENT, Errno::ENOTDIR
42
+ {}
43
+ end
44
+
41
45
  # Internal: Writes a digest of the watched files to disk for future checks.
42
- def record_build_metadata(build, **attrs)
46
+ def record_build_metadata(success, build)
43
47
  config.build_cache_dir.mkpath
44
- build.with_result(**attrs).write_to_cache
48
+ last_build_path.write build.with_result(success).to_json
45
49
  end
46
50
 
47
51
  # Internal: The file path where metadata of the last build is stored.
48
- def last_build_path(ssr:)
49
- config.build_cache_dir.join("last#{ '-ssr' if ssr }-build-#{ config.mode }.json")
52
+ def last_build_path
53
+ config.build_cache_dir.join("last-build-#{ config.mode }.json")
50
54
  end
51
55
 
52
56
  # Internal: Returns a digest of all the watched files, allowing to detect
53
57
  # changes, and skip Vite builds if no files have changed.
54
58
  def watched_files_digest
55
- return @last_digest if @last_digest_at && Time.now - @last_digest_at < 1
56
-
57
- config.within_root do
59
+ Dir.chdir File.expand_path(config.root) do
58
60
  files = Dir[*config.watched_paths].reject { |f| File.directory?(f) }
59
61
  file_ids = files.sort.map { |f| "#{ File.basename(f) }/#{ Digest::SHA1.file(f).hexdigest }" }
60
- @last_digest_at = Time.now
61
- @last_digest = Digest::SHA1.hexdigest(file_ids.join('/'))
62
+ Digest::SHA1.hexdigest(file_ids.join('/'))
62
63
  end
63
64
  end
64
65
 
65
66
  # Public: Initiates a Vite build command to generate assets.
67
+ #
68
+ # Returns true if the build is successful, or false if it failed.
66
69
  def build_with_vite(*args)
67
70
  logger.info 'Building with Vite ⚡️'
68
71
 
69
- run(['build', *args])
72
+ stdout, stderr, status = run(['build', *args])
73
+ log_build_result(stdout, stderr.to_s, status)
74
+
75
+ status.success?
70
76
  end
71
77
 
72
78
  # Internal: Outputs the build results.
@@ -75,10 +81,9 @@ private
75
81
  def log_build_result(_stdout, stderr, status)
76
82
  if status.success?
77
83
  logger.info "Build with Vite complete: #{ config.build_output_dir }"
78
- logger.error stderr unless stderr.empty?
84
+ logger.error stderr.to_s unless stderr.empty?
79
85
  else
80
86
  logger.error stderr
81
- logger.error status
82
87
  logger.error 'Build with Vite failed! ❌'
83
88
  logger.error '❌ Check that vite and vite-plugin-ruby are in devDependencies and have been installed. ' if stderr.include?('ERR! canceled')
84
89
  end
@@ -5,10 +5,8 @@ class ViteRuby::CLI::Build < ViteRuby::CLI::Vite
5
5
 
6
6
  desc 'Bundle all entrypoints using Vite.'
7
7
  shared_options
8
- option(:ssr, desc: 'Build the SSR entrypoint instead', type: :boolean)
9
8
  option(:force, desc: 'Force the build even if assets have not changed', type: :boolean)
10
9
  option(:watch, desc: 'Start the Rollup watcher and rebuild on files changes', type: :boolean)
11
- option(:profile, desc: 'Gather performance metrics from the build ', type: :boolean)
12
10
 
13
11
  def call(**options)
14
12
  super { |args| ViteRuby.commands.build_from_task(*args) }
@@ -1,16 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'stringio'
4
- require 'json'
5
4
 
6
5
  class ViteRuby::CLI::Install < Dry::CLI::Command
7
6
  desc 'Performs the initial configuration setup to get started with Vite Ruby.'
8
7
 
9
- option(:package_manager, values: %w[npm pnpm yarn bun], aliases: %w[package-manager with], desc: 'The package manager to use when installing JS dependencies.')
10
-
11
- def call(package_manager: nil, **)
12
- ENV['VITE_RUBY_PACKAGE_MANAGER'] ||= package_manager if package_manager
13
-
8
+ def call(**)
14
9
  $stdout.sync = true
15
10
 
16
11
  say 'Creating binstub'
@@ -84,20 +79,9 @@ private
84
79
  # Internal: Installs vite and vite-plugin-ruby at the project level.
85
80
  def install_js_dependencies
86
81
  package_json = root.join('package.json')
87
- unless package_json.exist?
88
- write package_json, <<~JSON
89
- {
90
- "private": true,
91
- "type": "module"
92
- }
93
- JSON
94
- end
95
-
96
- if (JSON.parse(package_json.read)['type'] != 'module' rescue nil)
97
- FileUtils.mv root.join('vite.config.ts'), root.join('vite.config.mts'), force: true, verbose: true
98
- end
99
-
100
- install_js_packages js_dependencies.join(' ')
82
+ write(package_json, '{}') unless package_json.exist?
83
+ deps = js_dependencies.join(' ')
84
+ run_with_capture("#{ npm_install } -D #{ deps }", stdin_data: "\n")
101
85
  end
102
86
 
103
87
  # Internal: Adds compilation output dirs to git ignore.
@@ -107,7 +91,9 @@ private
107
91
  append(gitignore_file, <<~GITIGNORE)
108
92
 
109
93
  # Vite Ruby
110
- /public/vite*
94
+ /public/vite
95
+ /public/vite-dev
96
+ /public/vite-test
111
97
  node_modules
112
98
  # Vite uses dotenv and suggests to ignore local-only env files. See
113
99
  # https://vitejs.dev/guide/env-and-mode.html#env-files
@@ -127,12 +113,16 @@ private
127
113
  def run_with_capture(*args, **options)
128
114
  Dir.chdir(root) do
129
115
  _, stderr, status = ViteRuby::IO.capture(*args, **options)
130
- say(stderr) unless status.success? || stderr.empty?
116
+ say(stderr) unless status.success? || stderr.to_s.empty?
131
117
  end
132
118
  end
133
119
 
134
- def install_js_packages(deps)
135
- run_with_capture("#{ config.package_manager } add -D #{ deps }", stdin_data: "\n")
120
+ # Internal: Support all popular package managers.
121
+ def npm_install
122
+ return 'yarn add' if root.join('yarn.lock').exist?
123
+ return 'pnpm install' if root.join('pnpm-lock.yaml').exist?
124
+
125
+ 'npm install'
136
126
  end
137
127
 
138
128
  # Internal: Avoid printing warning about missing vite.json, we will create one.
@@ -144,3 +134,8 @@ private
144
134
  $stderr = old_stderr
145
135
  end
146
136
  end
137
+
138
+ # NOTE: This allows framework-specific variants to extend the installation.
139
+ ViteRuby.framework_libraries.each do |_framework, library|
140
+ require "#{ library.name.tr('-', '/') }/installation"
141
+ end
@@ -5,6 +5,7 @@ class ViteRuby::CLI::UpgradePackages < ViteRuby::CLI::Install
5
5
 
6
6
  def call(**)
7
7
  say 'Upgrading npm packages'
8
- install_js_packages js_dependencies.join(' ')
8
+ deps = js_dependencies.join(' ')
9
+ run_with_capture("#{ npm_install } -D #{ deps }")
9
10
  end
10
11
  end
@@ -3,33 +3,18 @@
3
3
  class ViteRuby::CLI::Vite < Dry::CLI::Command
4
4
  CURRENT_ENV = ENV['RACK_ENV'] || ENV['RAILS_ENV']
5
5
 
6
- def self.executable_options
6
+ def self.shared_options
7
7
  option(:mode, default: self::DEFAULT_ENV, values: %w[development production test], aliases: ['m'], desc: 'The build mode for Vite')
8
- option(:node_options, desc: 'Node options for the Vite executable', aliases: ['node-options'])
8
+ option(:clobber, desc: 'Clear cache and previous builds', type: :boolean, aliases: %w[clean clear])
9
+ option(:debug, desc: 'Run Vite in verbose mode, printing all debugging output', aliases: ['verbose'], type: :boolean)
9
10
  option(:inspect, desc: 'Run Vite in a debugging session with node --inspect-brk', aliases: ['inspect-brk'], type: :boolean)
10
11
  option(:trace_deprecation, desc: 'Run Vite in debugging mode with node --trace-deprecation', aliases: ['trace-deprecation'], type: :boolean)
11
12
  end
12
13
 
13
- def self.shared_options
14
- executable_options
15
- option(:debug, desc: 'Run Vite in verbose mode, printing all debugging output', aliases: ['verbose'], type: :boolean)
16
- option(:clobber, desc: 'Clear cache and previous builds', type: :boolean, aliases: %w[clean clear])
17
- end
18
-
19
- def call(mode:, args: [], clobber: false, node_options: nil, inspect: nil, trace_deprecation: nil, **boolean_opts)
14
+ def call(mode:, args: [], clobber: false, **boolean_opts)
20
15
  ViteRuby.env['VITE_RUBY_MODE'] = mode
21
16
  ViteRuby.commands.clobber if clobber
22
-
23
- node_options = [
24
- node_options,
25
- ('--inspect-brk' if inspect),
26
- ('--trace-deprecation' if trace_deprecation),
27
- ].compact.join(' ')
28
-
29
- args << %(--node-options="#{ node_options }") unless node_options.empty?
30
-
31
17
  boolean_opts.map { |name, value| args << "--#{ name }" if value }
32
-
33
18
  yield(args)
34
19
  end
35
20
  end
data/lib/vite_ruby/cli.rb CHANGED
@@ -11,20 +11,7 @@ class ViteRuby::CLI
11
11
  register 'clobber', Clobber, aliases: %w[clean clear]
12
12
  register 'dev', Dev, aliases: %w[d serve]
13
13
  register 'install', Install
14
- register 'ssr', SSR
15
14
  register 'version', Version, aliases: ['v', '-v', '--version', 'info']
16
15
  register 'upgrade', Upgrade, aliases: ['update']
17
16
  register 'upgrade_packages', UpgradePackages, aliases: ['update_packages']
18
-
19
- # Internal: Allows framework-specific variants to extend the CLI.
20
- def self.require_framework_libraries(path = 'cli')
21
- ViteRuby.framework_libraries.each do |_framework, library|
22
- require [library.name.tr('-', '/').to_s, path].compact.join('/')
23
- end
24
- rescue LoadError
25
- require_framework_libraries 'installation' unless path == 'installation'
26
- end
27
17
  end
28
-
29
- # NOTE: This allows framework-specific variants to extend the CLI.
30
- ViteRuby::CLI.require_framework_libraries('cli')
@@ -23,25 +23,47 @@ class ViteRuby::Commands
23
23
 
24
24
  # Public: Removes all build cache and previously compiled assets.
25
25
  def clobber
26
- dirs = [config.build_output_dir, config.ssr_output_dir, config.build_cache_dir, config.vite_cache_dir]
26
+ dirs = [config.build_output_dir, config.build_cache_dir, config.vite_cache_dir]
27
27
  dirs.each { |dir| dir.rmtree if dir.exist? }
28
28
  $stdout.puts "Removed vite cache and output dirs:\n\t#{ dirs.join("\n\t") }"
29
29
  end
30
30
 
31
- # Internal: Installs the binstub for the CLI in the appropriate path.
32
- def install_binstubs
33
- `bundle binstub vite_ruby --path #{ config.root.join('bin') }`
34
- `bundle config --delete bin`
31
+ # Public: Receives arguments from a rake task.
32
+ def clean_from_task(args)
33
+ ensure_log_goes_to_stdout {
34
+ clean(keep_up_to: Integer(args.keep || 2), age_in_seconds: Integer(args.age || 3600))
35
+ }
35
36
  end
36
37
 
37
- # Internal: Checks if the npm version is 6 or lower.
38
- def legacy_npm_version?
39
- `npm --version`.to_i < 7 rescue false
38
+ # Public: Cleanup old assets in the output directory.
39
+ #
40
+ # keep_up_to - Max amount of backups to preserve.
41
+ # age_in_seconds - Amount of time to look back in order to preserve them.
42
+ #
43
+ # NOTE: By default keeps the last version, or 2 if created in the past hour.
44
+ #
45
+ # Examples:
46
+ # To force only 1 backup to be kept: clean(1, 0)
47
+ # To only keep files created within the last 10 minutes: clean(0, 600)
48
+ def clean(keep_up_to: 2, age_in_seconds: 3600)
49
+ return false unless may_clean?
50
+
51
+ versions
52
+ .each_with_index
53
+ .drop_while { |(mtime, _), index|
54
+ max_age = [0, Time.now - Time.at(mtime)].max
55
+ max_age < age_in_seconds || index < keep_up_to
56
+ }
57
+ .each do |(_, files), _|
58
+ clean_files(files)
59
+ end
60
+ true
40
61
  end
41
62
 
42
- # Internal: Checks if the yarn version is 1.x.
43
- def legacy_yarn_version?
44
- `yarn --version`.to_i < 2 rescue false
63
+ # Internal: Installs the binstub for the CLI in the appropriate path.
64
+ def install_binstubs
65
+ `bundle binstub vite_ruby --path #{ config.root.join('bin') }`
66
+ `bundle config --delete bin`
45
67
  end
46
68
 
47
69
  # Internal: Verifies if ViteRuby is properly installed.
@@ -68,7 +90,7 @@ class ViteRuby::Commands
68
90
 
69
91
  # Internal: Prints information about ViteRuby's environment.
70
92
  def print_info
71
- config.within_root do
93
+ Dir.chdir(config.root) do
72
94
  $stdout.puts "bin/vite present?: #{ File.exist? 'bin/vite' }"
73
95
 
74
96
  $stdout.puts "vite_ruby: #{ ViteRuby::VERSION }"
@@ -77,11 +99,11 @@ class ViteRuby::Commands
77
99
  $stdout.puts "#{ framework }: #{ Gem.loaded_specs[framework]&.version }"
78
100
  end
79
101
 
80
- $stdout.puts "ruby: #{ `ruby --version` }"
81
102
  $stdout.puts "node: #{ `node --version` }"
82
-
83
- pkg = config.package_manager
84
- $stdout.puts "#{ pkg }: #{ `#{ pkg } --version` rescue nil }"
103
+ $stdout.puts "npm: #{ `npm --version` }"
104
+ $stdout.puts "yarn: #{ `yarn --version` rescue nil }"
105
+ $stdout.puts "pnpm: #{ `pnpm --version` rescue nil }"
106
+ $stdout.puts "ruby: #{ `ruby --version` }"
85
107
 
86
108
  $stdout.puts "\n"
87
109
  packages = `npm ls vite vite-plugin-ruby`
@@ -98,6 +120,29 @@ private
98
120
 
99
121
  def_delegators :@vite_ruby, :config, :builder, :manifest, :logger, :logger=
100
122
 
123
+ def may_clean?
124
+ config.build_output_dir.exist? && config.manifest_path.exist?
125
+ end
126
+
127
+ def clean_files(files)
128
+ files.select { |file| File.file?(file) }.each do |file|
129
+ File.delete(file)
130
+ logger.info("Removed #{ file }")
131
+ end
132
+ end
133
+
134
+ def versions
135
+ all_files = Dir.glob("#{ config.build_output_dir }/**/*")
136
+ entries = all_files - [config.manifest_path] - current_version_files
137
+ entries.reject { |file| File.directory?(file) }
138
+ .group_by { |file| File.mtime(file).utc.to_i }
139
+ .sort.reverse
140
+ end
141
+
142
+ def current_version_files
143
+ Dir.glob(manifest.refresh.values.map { |value| config.build_output_dir.join("#{ value['file'] }*") })
144
+ end
145
+
101
146
  def with_node_env(env)
102
147
  original = ENV['NODE_ENV']
103
148
  ENV['NODE_ENV'] = env
@@ -24,7 +24,7 @@ module ViteRuby::CompatibilityCheck
24
24
  raise ArgumentError, <<~ERROR
25
25
  vite-plugin-ruby@#{ npm_req } might not be compatible with vite_ruby-#{ ViteRuby::VERSION }
26
26
 
27
- You may disable this check if needed: https://vite-ruby.netlify.app/config/#skipcompatibilitycheck
27
+ You may disable this check if needed: https://vite-ruby.netlify.app/config/#skipCompatibilityCheck
28
28
 
29
29
  You may upgrade both by running:
30
30
 
@@ -5,10 +5,6 @@ require 'json'
5
5
  # Public: Allows to resolve configuration sourced from `config/vite.json` and
6
6
  # environment variables, combining them with the default options.
7
7
  class ViteRuby::Config
8
- def origin
9
- "#{ protocol }://#{ host_with_port }"
10
- end
11
-
12
8
  def protocol
13
9
  https ? 'https' : 'http'
14
10
  end
@@ -17,20 +13,14 @@ class ViteRuby::Config
17
13
  "#{ host }:#{ port }"
18
14
  end
19
15
 
20
- # Internal: Path to the manifest files generated by Vite and vite-plugin-ruby.
21
- def known_manifest_paths
22
- [
23
- # NOTE: Generated by Vite when `manifest: true`, which vite-plugin-ruby enables.
24
- build_output_dir.join('.vite/manifest.json'),
25
-
26
- # NOTE: Path where vite-plugin-ruby outputs the assets manifest file.
27
- build_output_dir.join('.vite/manifest-assets.json'),
28
- ]
16
+ # Internal: Path where Vite outputs the manifest file.
17
+ def manifest_path
18
+ build_output_dir.join('manifest.json')
29
19
  end
30
20
 
31
- # Internal: Path to the manifest files generated by Vite and vite-plugin-ruby.
32
- def manifest_paths
33
- known_manifest_paths.select(&:exist?)
21
+ # Internal: Path where vite-plugin-ruby outputs the assets manifest file.
22
+ def assets_manifest_path
23
+ build_output_dir.join('manifest-assets.json')
34
24
  end
35
25
 
36
26
  # Public: The directory where Vite will store the built assets.
@@ -60,12 +50,12 @@ class ViteRuby::Config
60
50
  end
61
51
 
62
52
  # Public: Sets additional environment variables for vite-plugin-ruby.
63
- def to_env(env_vars = ViteRuby.env)
53
+ def to_env
64
54
  CONFIGURABLE_WITH_ENV.each_with_object({}) do |option, env|
65
55
  unless (value = @config[option]).nil?
66
56
  env["#{ ViteRuby::ENV_PREFIX }_#{ option.upcase }"] = value.to_s
67
57
  end
68
- end.merge(env_vars)
58
+ end.merge(ViteRuby.env)
69
59
  end
70
60
 
71
61
  # Internal: Files and directories that should be watched for changes.
@@ -80,11 +70,6 @@ class ViteRuby::Config
80
70
  ].freeze
81
71
  end
82
72
 
83
- # Internal: Changes the current directory to the root dir.
84
- def within_root(&block)
85
- Dir.chdir(File.expand_path(root), &block)
86
- end
87
-
88
73
  private
89
74
 
90
75
  # Internal: Coerces all the configuration values, in case they were passed
@@ -92,11 +77,9 @@ private
92
77
  def coerce_values(config)
93
78
  config['mode'] = config['mode'].to_s
94
79
  config['port'] = config['port'].to_i
95
- config['root'] = root = Pathname.new(config['root'])
96
- config['build_cache_dir'] = root.join(config['build_cache_dir'])
97
- config['ssr_output_dir'] = root.join(config['ssr_output_dir'])
98
- coerce_booleans(config, 'auto_build', 'hide_build_console_output', 'https', 'skip_compatibility_check', 'skip_proxy')
99
- config['package_manager'] ||= detect_package_manager(root)
80
+ config['root'] = Pathname.new(config['root'])
81
+ config['build_cache_dir'] = config['root'].join(config['build_cache_dir'])
82
+ coerce_booleans(config, 'auto_build', 'hide_build_console_output', 'https', 'skip_compatibility_check')
100
83
  end
101
84
 
102
85
  # Internal: Coerces configuration options to boolean.
@@ -104,15 +87,6 @@ private
104
87
  names.each { |name| config[name] = [true, 'true'].include?(config[name]) }
105
88
  end
106
89
 
107
- def detect_package_manager(root)
108
- return 'npm' if root.join('package-lock.json').exist?
109
- return 'pnpm' if root.join('pnpm-lock.yaml').exist?
110
- return 'bun' if root.join('bun.lockb').exist?
111
- return 'yarn' if root.join('yarn.lock').exist?
112
-
113
- 'npm'
114
- end
115
-
116
90
  def initialize(attrs)
117
91
  @config = attrs.tap { |config| coerce_values(config) }.freeze
118
92
  ViteRuby::CompatibilityCheck.verify_plugin_version(root) unless skip_compatibility_check
@@ -147,7 +121,7 @@ private
147
121
  'config_path' => option_from_env('config_path') || DEFAULT_CONFIG.fetch('config_path'),
148
122
  'mode' => option_from_env('mode') || mode,
149
123
  'root' => option_from_env('root') || root,
150
- }.select { |_, value| value }
124
+ }
151
125
  end
152
126
 
153
127
  # Internal: Used to load a JSON file from the specified path.
@@ -195,15 +169,12 @@ private
195
169
 
196
170
  # Internal: If any of these files is modified the build won't be skipped.
197
171
  DEFAULT_WATCHED_PATHS = %w[
198
- bun.lockb
199
172
  package-lock.json
200
173
  package.json
201
174
  pnpm-lock.yaml
202
175
  postcss.config.js
203
176
  tailwind.config.js
204
177
  vite.config.js
205
- vite.config.mjs
206
- vite.config.mts
207
178
  vite.config.ts
208
179
  windi.config.ts
209
180
  yarn.lock
@@ -38,7 +38,7 @@ private
38
38
  uri
39
39
  .sub(HOST_WITH_PORT_REGEX, '/') # Hanami adds the host and port.
40
40
  .sub('.ts.js', '.ts') # Hanami's javascript helper always adds the extension.
41
- .sub(/\.(sass|scss|styl|stylus|less|pcss|postcss)\.css$/, '.\1') # Rails' stylesheet_link_tag always adds the extension.
41
+ .sub(/(\.(?!min|module)\w+)\.css$/, '\1') # Rails' stylesheet_link_tag always adds the extension.
42
42
  end
43
43
 
44
44
  def forward_to_vite_dev_server(env)
@@ -54,22 +54,17 @@ private
54
54
 
55
55
  def vite_should_handle?(env)
56
56
  path = normalize_uri(env['PATH_INFO'])
57
- return true if path.start_with?(vite_url_prefix) # Vite asset
57
+ return true if path.start_with?(vite_asset_url_prefix) # Vite asset
58
+ return true if path.start_with?(VITE_DEPENDENCY_PREFIX) # Packages and imports
58
59
  return true if file_in_vite_root?(path) # Fallback if Vite can serve the file
59
60
  end
60
61
 
61
- # NOTE: When using an empty 'public_output_dir', we need to rely on a
62
- # filesystem check to check whether Vite should serve the request.
63
62
  def file_in_vite_root?(path)
64
63
  path.include?('.') && # Check for extension, avoid filesystem if possible.
65
64
  config.vite_root_dir.join(path.start_with?('/') ? path[1..-1] : path).file?
66
65
  end
67
66
 
68
- # NOTE: Vite is configured to use 'public_output_dir' as the base, which can
69
- # be customized by the user in development to not match any of the routes.
70
- #
71
- # If the path starts with that prefix, it will be redirected to Vite.
72
- def vite_url_prefix
73
- @vite_url_prefix ||= config.public_output_dir.empty? ? VITE_DEPENDENCY_PREFIX : "/#{ config.public_output_dir }/"
67
+ def vite_asset_url_prefix
68
+ @vite_asset_url_prefix ||= config.public_output_dir.empty? ? "\0" : "/#{ config.public_output_dir }/"
74
69
  end
75
70
  end
data/lib/vite_ruby/io.rb CHANGED
@@ -15,7 +15,7 @@ module ViteRuby::IO
15
15
  stdin.close
16
16
  out = Thread.new { read_lines(stdout, &with_output) }
17
17
  err = Thread.new { stderr.read }
18
- [out.value, err.value.to_s, wait_threads.value]
18
+ [out.value, err.value, wait_threads.value]
19
19
  }
20
20
  end
21
21
 
@@ -7,7 +7,7 @@
7
7
  # lookup_entrypoint('calendar', type: :javascript)
8
8
  # => { "file" => "/vite/assets/calendar-1016838bab065ae1e314.js", "imports" => [] }
9
9
  #
10
- # NOTE: Using `"autoBuild": true` in `config/vite.json` file will trigger a build
10
+ # NOTE: Using "autoBuild": true` in `config/vite.json` file will trigger a build
11
11
  # on demand as needed, before performing any lookup.
12
12
  class ViteRuby::Manifest
13
13
  def initialize(vite_ruby)
@@ -22,16 +22,15 @@ class ViteRuby::Manifest
22
22
  lookup!(name, **options).fetch('file')
23
23
  end
24
24
 
25
- # Public: Returns scripts, imported modules, and stylesheets for the specified
25
+ # Public: Returns entries, imported modules, and stylesheets for the specified
26
26
  # entrypoint files.
27
27
  def resolve_entries(*names, **options)
28
28
  entries = names.map { |name| lookup!(name, **options) }
29
- script_paths = entries.map { |entry| entry.fetch('file') }
30
29
 
31
30
  imports = dev_server_running? ? [] : entries.flat_map { |entry| entry['imports'] }.compact.uniq
32
31
  {
33
- scripts: script_paths,
34
- imports: imports.map { |entry| entry.fetch('file') }.uniq,
32
+ main: entries.map(&TO_ENTRY),
33
+ imports: imports.map(&TO_ENTRY).uniq,
35
34
  stylesheets: dev_server_running? ? [] : (entries + imports).flat_map { |entry| entry['css'] }.compact.uniq,
36
35
  }
37
36
  end
@@ -51,27 +50,21 @@ class ViteRuby::Manifest
51
50
  if dev_server_running?
52
51
  <<~REACT_REFRESH
53
52
  <script type="module">
54
- #{ react_preamble_code }
53
+ import RefreshRuntime from '#{ prefix_asset_with_host('@react-refresh') }'
54
+ RefreshRuntime.injectIntoGlobalHook(window)
55
+ window.$RefreshReg$ = () => {}
56
+ window.$RefreshSig$ = () => (type) => type
57
+ window.__vite_plugin_react_preamble_installed__ = true
55
58
  </script>
56
59
  REACT_REFRESH
57
60
  end
58
61
  end
59
62
 
60
- # Public: Source script for the React Refresh plugin.
61
- def react_preamble_code
62
- if dev_server_running?
63
- <<~REACT_PREAMBLE_CODE
64
- import RefreshRuntime from '#{ prefix_asset_with_host('@react-refresh') }'
65
- RefreshRuntime.injectIntoGlobalHook(window)
66
- window.$RefreshReg$ = () => {}
67
- window.$RefreshSig$ = () => (type) => type
68
- window.__vite_plugin_react_preamble_installed__ = true
69
- REACT_PREAMBLE_CODE
70
- end
71
- end
72
-
73
63
  protected
74
64
 
65
+ # Internal: Returns a [src, attrs] entry.
66
+ TO_ENTRY = ->(entry) { [entry.fetch('file'), entry.slice('integrity').symbolize_keys] }
67
+
75
68
  # Internal: Strict version of lookup.
76
69
  #
77
70
  # Returns a relative path for the asset, or raises an error if not found.
@@ -87,7 +80,7 @@ protected
87
80
  # manifest.lookup('calendar.js')
88
81
  # => { "file" => "/vite/assets/calendar-1016838bab065ae1e122.js", "imports" => [] }
89
82
  def lookup(name, **options)
90
- @build_mutex.synchronize { builder.build || (return nil) } if should_build?
83
+ @build_mutex.synchronize { builder.build } if should_build?
91
84
 
92
85
  find_manifest_entry resolve_entry_name(name, **options)
93
86
  end
@@ -128,24 +121,19 @@ private
128
121
 
129
122
  # Internal: Loads and merges the manifest files, resolving the asset paths.
130
123
  def load_manifest
131
- files = config.manifest_paths
124
+ files = [config.manifest_path, config.assets_manifest_path].select(&:exist?)
132
125
  files.map { |path| JSON.parse(path.read) }.inject({}, &:merge).tap(&method(:resolve_references))
133
126
  end
134
127
 
135
128
  # Internal: Scopes an asset to the output folder in public, as a path.
136
129
  def prefix_vite_asset(path)
137
- File.join(vite_asset_origin || '/', config.public_output_dir, path)
130
+ File.join("/#{ config.public_output_dir }", path)
138
131
  end
139
132
 
140
133
  # Internal: Prefixes an asset with the `asset_host` for tags that do not use
141
134
  # the framework tag helpers.
142
135
  def prefix_asset_with_host(path)
143
- File.join(vite_asset_origin || config.asset_host || '/', config.public_output_dir, path)
144
- end
145
-
146
- # Internal: The origin of assets managed by Vite.
147
- def vite_asset_origin
148
- config.origin if dev_server_running? && config.skip_proxy
136
+ File.join(config.asset_host || '/', config.public_output_dir, path)
149
137
  end
150
138
 
151
139
  # Internal: Resolves the paths that reference a manifest entry.
@@ -212,7 +200,7 @@ private
212
200
 
213
201
  # Internal: Raises a detailed message when an entry is missing in the manifest.
214
202
  def missing_entry_error(name, **options)
215
- raise ViteRuby::MissingEntrypointError.new(
203
+ raise ViteRuby::MissingEntrypointError, OpenStruct.new(
216
204
  file_name: resolve_entry_name(name, **options),
217
205
  last_build: builder.last_build_metadata,
218
206
  manifest: @manifest,