vite_ruby-roda 3.9.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +752 -0
- data/LICENSE.txt +21 -0
- data/README.md +84 -0
- data/default.vite.json +26 -0
- data/exe/vite +12 -0
- data/lib/tasks/vite.rake +95 -0
- data/lib/vite_ruby/build.rb +82 -0
- data/lib/vite_ruby/builder.rb +86 -0
- data/lib/vite_ruby/cli/build.rb +16 -0
- data/lib/vite_ruby/cli/clobber.rb +14 -0
- data/lib/vite_ruby/cli/dev.rb +13 -0
- data/lib/vite_ruby/cli/file_utils.rb +132 -0
- data/lib/vite_ruby/cli/install.rb +146 -0
- data/lib/vite_ruby/cli/ssr.rb +27 -0
- data/lib/vite_ruby/cli/upgrade.rb +25 -0
- data/lib/vite_ruby/cli/upgrade_packages.rb +10 -0
- data/lib/vite_ruby/cli/version.rb +9 -0
- data/lib/vite_ruby/cli/vite.rb +35 -0
- data/lib/vite_ruby/cli.rb +30 -0
- data/lib/vite_ruby/commands.rb +118 -0
- data/lib/vite_ruby/compatibility_check.rb +49 -0
- data/lib/vite_ruby/config.rb +219 -0
- data/lib/vite_ruby/dev_server_proxy.rb +76 -0
- data/lib/vite_ruby/error.rb +11 -0
- data/lib/vite_ruby/io.rb +32 -0
- data/lib/vite_ruby/manifest.rb +224 -0
- data/lib/vite_ruby/missing_entrypoint_error.rb +53 -0
- data/lib/vite_ruby/missing_executable_error.rb +13 -0
- data/lib/vite_ruby/runner.rb +53 -0
- data/lib/vite_ruby/version.rb +9 -0
- data/lib/vite_ruby.rb +153 -0
- data/templates/config/vite.config.ts +8 -0
- data/templates/config/vite.json +16 -0
- data/templates/entrypoints/application.js +8 -0
- metadata +258 -0
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rack/proxy"
|
4
|
+
|
5
|
+
# Public: Allows to relay asset requests to the Vite development server.
|
6
|
+
class ViteRuby::DevServerProxy < Rack::Proxy
|
7
|
+
HOST_WITH_PORT_REGEX = %r{^(.+?)(:\d+)/}
|
8
|
+
VITE_DEPENDENCY_PREFIX = "/@"
|
9
|
+
|
10
|
+
def initialize(app = nil, options = {})
|
11
|
+
@vite_ruby = options.delete(:vite_ruby) || ViteRuby.instance
|
12
|
+
options[:streaming] = false if config.mode == "test" && !options.key?(:streaming)
|
13
|
+
super
|
14
|
+
end
|
15
|
+
|
16
|
+
# Rack: Intercept asset requests and send them to the Vite server.
|
17
|
+
def perform_request(env)
|
18
|
+
if vite_should_handle?(env) && dev_server_running?
|
19
|
+
forward_to_vite_dev_server(env)
|
20
|
+
super
|
21
|
+
else
|
22
|
+
@app.call(env)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
extend Forwardable
|
29
|
+
|
30
|
+
def_delegators :@vite_ruby, :config, :dev_server_running?
|
31
|
+
|
32
|
+
def rewrite_uri_for_vite(env)
|
33
|
+
uri = env.fetch("REQUEST_URI") { [env["PATH_INFO"], env["QUERY_STRING"]].reject { |str| str.to_s.strip.empty? }.join("?") }
|
34
|
+
env["PATH_INFO"], env["QUERY_STRING"] = (env["REQUEST_URI"] = normalize_uri(uri)).split("?")
|
35
|
+
end
|
36
|
+
|
37
|
+
def normalize_uri(uri)
|
38
|
+
uri
|
39
|
+
.sub(HOST_WITH_PORT_REGEX, "/") # Hanami adds the host and port.
|
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.
|
42
|
+
end
|
43
|
+
|
44
|
+
def forward_to_vite_dev_server(env)
|
45
|
+
rewrite_uri_for_vite(env)
|
46
|
+
env["QUERY_STRING"] ||= ""
|
47
|
+
env["HTTP_HOST"] = env["HTTP_X_FORWARDED_HOST"] = config.host
|
48
|
+
env["HTTP_X_FORWARDED_SERVER"] = config.host_with_port
|
49
|
+
env["HTTP_PORT"] = env["HTTP_X_FORWARDED_PORT"] = config.port.to_s
|
50
|
+
env["HTTP_X_FORWARDED_PROTO"] = env["HTTP_X_FORWARDED_SCHEME"] = config.protocol
|
51
|
+
env["HTTPS"] = env["HTTP_X_FORWARDED_SSL"] = "off" unless config.https
|
52
|
+
env["SCRIPT_NAME"] = ""
|
53
|
+
end
|
54
|
+
|
55
|
+
def vite_should_handle?(env)
|
56
|
+
path = normalize_uri(env["PATH_INFO"])
|
57
|
+
return true if path.start_with?(vite_url_prefix) # Vite asset
|
58
|
+
|
59
|
+
true if file_in_vite_root?(path) # Fallback if Vite can serve the file
|
60
|
+
end
|
61
|
+
|
62
|
+
# NOTE: When using an empty 'public_output_dir', we need to rely on a
|
63
|
+
# filesystem check to check whether Vite should serve the request.
|
64
|
+
def file_in_vite_root?(path)
|
65
|
+
path.include?(".") && # Check for extension, avoid filesystem if possible.
|
66
|
+
config.vite_root_dir.join(path.start_with?("/") ? path[1..] : path).file?
|
67
|
+
end
|
68
|
+
|
69
|
+
# NOTE: Vite is configured to use 'public_output_dir' as the base, which can
|
70
|
+
# be customized by the user in development to not match any of the routes.
|
71
|
+
#
|
72
|
+
# If the path starts with that prefix, it will be redirected to Vite.
|
73
|
+
def vite_url_prefix
|
74
|
+
@vite_url_prefix ||= config.public_output_dir.empty? ? VITE_DEPENDENCY_PREFIX : "/#{config.public_output_dir}/"
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Internal: Provides common functionality for errors.
|
4
|
+
class ViteRuby::Error < StandardError
|
5
|
+
def message
|
6
|
+
super.sub(":troubleshooting:", <<~MSG)
|
7
|
+
Visit the Troubleshooting guide for more information:
|
8
|
+
https://vite-ruby.netlify.app/guide/troubleshooting.html#troubleshooting
|
9
|
+
MSG
|
10
|
+
end
|
11
|
+
end
|
data/lib/vite_ruby/io.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "open3"
|
4
|
+
|
5
|
+
# Public: Builds on top of Ruby I/O open3 providing a friendlier experience.
|
6
|
+
module ViteRuby::IO
|
7
|
+
class << self
|
8
|
+
# Internal: A modified version of capture3 that can continuosly print stdout.
|
9
|
+
# NOTE: Streaming output provides a better UX when running bin/vite build.
|
10
|
+
def capture(*cmd, with_output: $stdout.method(:puts), stdin_data: "", **opts)
|
11
|
+
return Open3.capture3(*cmd, **opts) unless with_output
|
12
|
+
|
13
|
+
Open3.popen3(*cmd, **opts) { |stdin, stdout, stderr, wait_threads|
|
14
|
+
stdin << stdin_data
|
15
|
+
stdin.close
|
16
|
+
out = Thread.new { read_lines(stdout, &with_output) }
|
17
|
+
err = Thread.new { stderr.read }
|
18
|
+
[out.value, err.value.to_s, wait_threads.value]
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
# Internal: Reads and yield every line in the stream. Returns the full content.
|
23
|
+
def read_lines(io)
|
24
|
+
buffer = +""
|
25
|
+
while line = io.gets
|
26
|
+
buffer << line
|
27
|
+
yield line
|
28
|
+
end
|
29
|
+
buffer
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,224 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Public: Registry for accessing resources managed by Vite, using a generated
|
4
|
+
# manifest file which maps entrypoint names to file paths.
|
5
|
+
#
|
6
|
+
# Example:
|
7
|
+
# lookup_entrypoint('calendar', type: :javascript)
|
8
|
+
# => { "file" => "/vite/assets/calendar-1016838bab065ae1e314.js", "imports" => [] }
|
9
|
+
#
|
10
|
+
# NOTE: Using `"autoBuild": true` in `config/vite.json` file will trigger a build
|
11
|
+
# on demand as needed, before performing any lookup.
|
12
|
+
class ViteRuby::Manifest
|
13
|
+
def initialize(vite_ruby)
|
14
|
+
@vite_ruby = vite_ruby
|
15
|
+
@build_mutex = Mutex.new if config.auto_build
|
16
|
+
end
|
17
|
+
|
18
|
+
# Public: Returns the path for the specified Vite entrypoint file.
|
19
|
+
#
|
20
|
+
# Raises an error if the resource could not be found in the manifest.
|
21
|
+
def path_for(name, **options)
|
22
|
+
lookup!(name, **options).fetch("file")
|
23
|
+
end
|
24
|
+
|
25
|
+
# Public: Returns scripts, imported modules, and stylesheets for the specified
|
26
|
+
# entrypoint files.
|
27
|
+
def resolve_entries(*names, **options)
|
28
|
+
entries = names.map { |name| lookup!(name, **options) }
|
29
|
+
script_paths = entries.map { |entry| entry.fetch("file") }
|
30
|
+
|
31
|
+
imports = dev_server_running? ? [] : entries.flat_map { |entry| entry["imports"] }.compact
|
32
|
+
{
|
33
|
+
scripts: script_paths,
|
34
|
+
imports: imports.filter_map { |entry| entry.fetch("file") }.uniq,
|
35
|
+
stylesheets: dev_server_running? ? [] : (entries + imports).flat_map { |entry| entry["css"] }.compact.uniq,
|
36
|
+
}
|
37
|
+
end
|
38
|
+
|
39
|
+
# Public: Refreshes the cached mappings by reading the updated manifest files.
|
40
|
+
def refresh
|
41
|
+
@manifest = load_manifest
|
42
|
+
end
|
43
|
+
|
44
|
+
# Public: The path from where the browser can download the Vite HMR client.
|
45
|
+
def vite_client_src
|
46
|
+
prefix_asset_with_host("@vite/client") if dev_server_running?
|
47
|
+
end
|
48
|
+
|
49
|
+
# Public: The content of the preamble needed by the React Refresh plugin.
|
50
|
+
def react_refresh_preamble
|
51
|
+
if dev_server_running?
|
52
|
+
<<~REACT_REFRESH
|
53
|
+
<script type="module">
|
54
|
+
#{react_preamble_code}
|
55
|
+
</script>
|
56
|
+
REACT_REFRESH
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
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
|
+
protected
|
74
|
+
|
75
|
+
# Internal: Strict version of lookup.
|
76
|
+
#
|
77
|
+
# Returns a relative path for the asset, or raises an error if not found.
|
78
|
+
def lookup!(name, **options)
|
79
|
+
lookup(name, **options) || missing_entry_error(name, **options)
|
80
|
+
end
|
81
|
+
|
82
|
+
# Internal: Computes the path for a given Vite asset using manifest.json.
|
83
|
+
#
|
84
|
+
# Returns a relative path, or nil if the asset is not found.
|
85
|
+
#
|
86
|
+
# Example:
|
87
|
+
# manifest.lookup('calendar.js')
|
88
|
+
# => { "file" => "/vite/assets/calendar-1016838bab065ae1e122.js", "imports" => [] }
|
89
|
+
def lookup(name, **options)
|
90
|
+
@build_mutex.synchronize { builder.build || (return nil) } if should_build?
|
91
|
+
|
92
|
+
find_manifest_entry resolve_entry_name(name, **options)
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
# Internal: The prefix used by Vite.js to request files with an absolute path.
|
98
|
+
FS_PREFIX = "/@fs/"
|
99
|
+
|
100
|
+
extend Forwardable
|
101
|
+
|
102
|
+
def_delegators :@vite_ruby, :config, :builder, :dev_server_running?
|
103
|
+
|
104
|
+
# NOTE: Auto compilation is convenient when running tests, when the developer
|
105
|
+
# won't focus on the frontend, or when running the Vite server is not desired.
|
106
|
+
def should_build?
|
107
|
+
config.auto_build && !dev_server_running?
|
108
|
+
end
|
109
|
+
|
110
|
+
# Internal: Finds the specified entry in the manifest.
|
111
|
+
def find_manifest_entry(name)
|
112
|
+
if dev_server_running?
|
113
|
+
{"file" => prefix_vite_asset(name)}
|
114
|
+
else
|
115
|
+
manifest[name]
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Internal: The parsed data from manifest.json.
|
120
|
+
#
|
121
|
+
# NOTE: When using build-on-demand in development and testing, the manifest
|
122
|
+
# is reloaded automatically before each lookup, to ensure it's always fresh.
|
123
|
+
def manifest
|
124
|
+
return refresh if config.auto_build
|
125
|
+
|
126
|
+
@manifest ||= load_manifest
|
127
|
+
end
|
128
|
+
|
129
|
+
# Internal: Loads and merges the manifest files, resolving the asset paths.
|
130
|
+
def load_manifest
|
131
|
+
config.manifest_paths
|
132
|
+
.map { |path| JSON.parse(path.read) }
|
133
|
+
.inject({}, &:merge)
|
134
|
+
.tap { |manifest| resolve_references(manifest) }
|
135
|
+
end
|
136
|
+
|
137
|
+
# Internal: Scopes an asset to the output folder in public, as a path.
|
138
|
+
def prefix_vite_asset(path)
|
139
|
+
File.join(vite_asset_origin || "/", config.public_output_dir, path)
|
140
|
+
end
|
141
|
+
|
142
|
+
# Internal: Prefixes an asset with the `asset_host` for tags that do not use
|
143
|
+
# the framework tag helpers.
|
144
|
+
def prefix_asset_with_host(path)
|
145
|
+
File.join(vite_asset_origin || config.asset_host || "/", config.public_output_dir, path)
|
146
|
+
end
|
147
|
+
|
148
|
+
# Internal: The origin of assets managed by Vite.
|
149
|
+
def vite_asset_origin
|
150
|
+
config.origin if dev_server_running? && config.skip_proxy
|
151
|
+
end
|
152
|
+
|
153
|
+
# Internal: Resolves the paths that reference a manifest entry.
|
154
|
+
def resolve_references(manifest)
|
155
|
+
manifest.each_value do |entry|
|
156
|
+
entry["file"] = prefix_vite_asset(entry["file"])
|
157
|
+
%w[css assets].each do |key|
|
158
|
+
entry[key] = entry[key].map { |path| prefix_vite_asset(path) } if entry[key]
|
159
|
+
end
|
160
|
+
entry["imports"]&.map! { |name| manifest.fetch(name) }
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
# Internal: Resolves the manifest entry name for the specified resource.
|
165
|
+
def resolve_entry_name(name, type: nil)
|
166
|
+
return resolve_virtual_entry(name) if type == :virtual
|
167
|
+
|
168
|
+
name = with_file_extension(name.to_s, type)
|
169
|
+
raise ArgumentError, "Asset names can not be relative. Found: #{name}" if name.start_with?(".")
|
170
|
+
|
171
|
+
# Explicit path, relative to the source_code_dir.
|
172
|
+
name.sub(%r{^~/(.+)$}) { return Regexp.last_match(1) }
|
173
|
+
|
174
|
+
# Explicit path, relative to the project root.
|
175
|
+
name.sub(%r{^/(.+)$}) { return resolve_absolute_entry(Regexp.last_match(1)) }
|
176
|
+
|
177
|
+
# Sugar: Prefix with the entrypoints dir if the path is not nested.
|
178
|
+
name.include?("/") ? name : File.join(config.entrypoints_dir, name)
|
179
|
+
end
|
180
|
+
|
181
|
+
# Internal: Entry names in the manifest are relative to the Vite.js.
|
182
|
+
# During develoment, files outside the root must be requested explicitly.
|
183
|
+
def resolve_absolute_entry(name)
|
184
|
+
if dev_server_running?
|
185
|
+
File.join(FS_PREFIX, config.root, name)
|
186
|
+
else
|
187
|
+
config.root.join(name).relative_path_from(config.vite_root_dir).to_s
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
# Internal: Resolves a virtual entry by walking all the manifest keys.
|
192
|
+
def resolve_virtual_entry(name)
|
193
|
+
manifest.keys.find { |file| file.include?(name) } || name
|
194
|
+
end
|
195
|
+
|
196
|
+
# Internal: Adds a file extension to the file name, unless it already has one.
|
197
|
+
def with_file_extension(name, entry_type)
|
198
|
+
if File.extname(name).empty? && (ext = extension_for_type(entry_type))
|
199
|
+
"#{name}.#{ext}"
|
200
|
+
else
|
201
|
+
name
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
# Internal: Allows to receive :javascript and :stylesheet as :type in helpers.
|
206
|
+
def extension_for_type(entry_type)
|
207
|
+
case entry_type
|
208
|
+
when :javascript then "js"
|
209
|
+
when :stylesheet then "css"
|
210
|
+
when :typescript then "ts"
|
211
|
+
else entry_type
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
# Internal: Raises a detailed message when an entry is missing in the manifest.
|
216
|
+
def missing_entry_error(name, **options)
|
217
|
+
raise ViteRuby::MissingEntrypointError.new(
|
218
|
+
file_name: resolve_entry_name(name, **options),
|
219
|
+
last_build: builder.last_build_metadata,
|
220
|
+
manifest: @manifest,
|
221
|
+
config: config,
|
222
|
+
)
|
223
|
+
end
|
224
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Internal: Raised when an entry is not found in the build manifest.
|
4
|
+
#
|
5
|
+
# NOTE: The complexity here is justified by the improved usability of providing
|
6
|
+
# a more specific error message depending on the situation.
|
7
|
+
class ViteRuby::MissingEntrypointError < ViteRuby::Error
|
8
|
+
attr_reader :file_name, :last_build, :manifest, :config
|
9
|
+
|
10
|
+
def initialize(file_name:, last_build:, manifest:, config:)
|
11
|
+
@file_name, @last_build, @manifest, @config = file_name, last_build, manifest, config
|
12
|
+
super <<~MSG
|
13
|
+
Vite Ruby can't find #{file_name} in the manifests.
|
14
|
+
|
15
|
+
Possible causes:
|
16
|
+
#{possible_causes(last_build)}
|
17
|
+
:troubleshooting:
|
18
|
+
#{"Manifest files found:\n#{config.manifest_paths.map { |path| " #{path.relative_path_from(config.root)}" }.join("\n")}\n" if last_build.success}
|
19
|
+
#{"Content in your manifests:\n#{JSON.pretty_generate(manifest)}\n" if last_build.success}
|
20
|
+
#{"Last build in #{config.mode} mode:\n#{last_build.to_json}\n" if last_build.success}
|
21
|
+
MSG
|
22
|
+
end
|
23
|
+
|
24
|
+
def possible_causes(last_build)
|
25
|
+
if last_build.success == false
|
26
|
+
FAILED_BUILD_CAUSES
|
27
|
+
.sub(":mode:", config.mode)
|
28
|
+
.sub(":errors:", last_build.errors.to_s.gsub(/^(?!$)/, " "))
|
29
|
+
elsif config.auto_build
|
30
|
+
DEFAULT_CAUSES
|
31
|
+
else
|
32
|
+
DEFAULT_CAUSES + NO_AUTO_BUILD_CAUSES
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
FAILED_BUILD_CAUSES = <<~MSG
|
37
|
+
- The last build failed. Try running `bin/vite build --clear --mode=:mode:` manually and check for errors.
|
38
|
+
|
39
|
+
Errors:
|
40
|
+
:errors:
|
41
|
+
MSG
|
42
|
+
|
43
|
+
DEFAULT_CAUSES = <<-MSG
|
44
|
+
- The file path is incorrect.
|
45
|
+
- The file is not in the entrypoints directory.
|
46
|
+
- Some files are outside the sourceCodeDir, and have not been added to watchAdditionalPaths.
|
47
|
+
MSG
|
48
|
+
|
49
|
+
NO_AUTO_BUILD_CAUSES = <<-MSG
|
50
|
+
- You have not run `bin/vite dev` to start Vite, or the dev server is not reachable.
|
51
|
+
- "autoBuild" is set to `false` in your config/vite.json for this environment.
|
52
|
+
MSG
|
53
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Internal: Raised when the Vite executable can not be found.
|
4
|
+
class ViteRuby::MissingExecutableError < ViteRuby::Error
|
5
|
+
def initialize(error = nil)
|
6
|
+
super <<~MSG
|
7
|
+
❌ The vite binary is not available. Have you installed the npm packages?
|
8
|
+
|
9
|
+
:troubleshooting:
|
10
|
+
#{error}
|
11
|
+
MSG
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Public: Executes Vite commands, providing conveniences for debugging.
|
4
|
+
class ViteRuby::Runner
|
5
|
+
def initialize(vite_ruby)
|
6
|
+
@vite_ruby = vite_ruby
|
7
|
+
end
|
8
|
+
|
9
|
+
# Public: Executes Vite with the specified arguments.
|
10
|
+
def run(argv, exec: false)
|
11
|
+
config.within_root {
|
12
|
+
cmd = command_for(argv)
|
13
|
+
return Kernel.exec(*cmd) if exec
|
14
|
+
|
15
|
+
log_or_noop = ->(line) { logger.info("vite") { line } } unless config.hide_build_console_output
|
16
|
+
ViteRuby::IO.capture(*cmd, chdir: config.root, with_output: log_or_noop)
|
17
|
+
}
|
18
|
+
rescue Errno::ENOENT => error
|
19
|
+
raise ViteRuby::MissingExecutableError, error
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
extend Forwardable
|
25
|
+
|
26
|
+
def_delegators :@vite_ruby, :config, :logger, :env
|
27
|
+
|
28
|
+
# Internal: Returns an Array with the command to run.
|
29
|
+
def command_for(args)
|
30
|
+
[config.to_env(env)].tap do |cmd|
|
31
|
+
exec_args, vite_args = args.partition { |arg| arg.start_with?("--node-options") }
|
32
|
+
cmd.push(*vite_executable(*exec_args))
|
33
|
+
cmd.push(*vite_args)
|
34
|
+
cmd.push("--mode", config.mode) unless args.include?("--mode") || args.include?("-m")
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Internal: Resolves to an executable for Vite.
|
39
|
+
def vite_executable(*exec_args)
|
40
|
+
bin_path = config.vite_bin_path
|
41
|
+
return [bin_path] if bin_path && File.exist?(bin_path)
|
42
|
+
|
43
|
+
x = case config.package_manager
|
44
|
+
when "npm" then %w[npx]
|
45
|
+
when "pnpm" then %w[pnpm exec]
|
46
|
+
when "bun" then %w[bun x --bun]
|
47
|
+
when "yarn" then %w[yarn]
|
48
|
+
else raise ArgumentError, "Unknown package manager #{config.package_manager.inspect}"
|
49
|
+
end
|
50
|
+
|
51
|
+
[*x, *exec_args, "vite"]
|
52
|
+
end
|
53
|
+
end
|
data/lib/vite_ruby.rb
ADDED
@@ -0,0 +1,153 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "logger"
|
4
|
+
require "forwardable"
|
5
|
+
require "pathname"
|
6
|
+
require "socket"
|
7
|
+
|
8
|
+
require "zeitwerk"
|
9
|
+
loader = Zeitwerk::Loader.for_gem
|
10
|
+
loader.ignore("#{__dir__}/install")
|
11
|
+
loader.ignore("#{__dir__}/tasks")
|
12
|
+
loader.ignore("#{__dir__}/exe")
|
13
|
+
loader.inflector.inflect("cli" => "CLI")
|
14
|
+
loader.inflector.inflect("ssr" => "SSR")
|
15
|
+
loader.inflector.inflect("io" => "IO")
|
16
|
+
loader.setup
|
17
|
+
|
18
|
+
class ViteRuby
|
19
|
+
# Internal: Prefix used for environment variables that modify the configuration.
|
20
|
+
ENV_PREFIX = "VITE_RUBY"
|
21
|
+
|
22
|
+
# Internal: Companion libraries for Vite Ruby, and their target framework.
|
23
|
+
COMPANION_LIBRARIES = {
|
24
|
+
"vite_rails" => "rails",
|
25
|
+
"vite_hanami" => "hanami",
|
26
|
+
"vite_padrino" => "padrino",
|
27
|
+
"jekyll-vite" => "jekyll",
|
28
|
+
"vite_rails_legacy" => "rails",
|
29
|
+
"vite_plugin_legacy" => "rack",
|
30
|
+
"roda-vite" => "roda",
|
31
|
+
}
|
32
|
+
|
33
|
+
class << self
|
34
|
+
extend Forwardable
|
35
|
+
|
36
|
+
def_delegators :instance, :config, :configure, :commands, :digest, :env, :run, :run_proxy?
|
37
|
+
def_delegators :config, :mode
|
38
|
+
|
39
|
+
def instance
|
40
|
+
@instance ||= new
|
41
|
+
end
|
42
|
+
|
43
|
+
# Internal: Refreshes the manifest.
|
44
|
+
def bootstrap
|
45
|
+
instance.manifest.refresh
|
46
|
+
end
|
47
|
+
|
48
|
+
# Internal: Loads all available rake tasks.
|
49
|
+
def install_tasks
|
50
|
+
load File.expand_path("tasks/vite.rake", __dir__)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Internal: Creates a new instance with the specified options.
|
54
|
+
def reload_with(**config_options)
|
55
|
+
@instance = new(**config_options)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Internal: Detects if the application has installed a framework-specific
|
59
|
+
# variant of Vite Ruby.
|
60
|
+
def framework_libraries
|
61
|
+
COMPANION_LIBRARIES.filter_map { |name, framework|
|
62
|
+
if library = Gem.loaded_specs[name]
|
63
|
+
[framework, library]
|
64
|
+
end
|
65
|
+
}
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
attr_writer :logger
|
70
|
+
|
71
|
+
def initialize(**config_options)
|
72
|
+
@config_options = config_options
|
73
|
+
end
|
74
|
+
|
75
|
+
def logger
|
76
|
+
@logger ||= Logger.new($stdout)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Public: Returns a digest of all the watched files, allowing to detect
|
80
|
+
# changes. Useful to perform version checks in single-page applications.
|
81
|
+
def digest
|
82
|
+
builder.send(:watched_files_digest)
|
83
|
+
end
|
84
|
+
|
85
|
+
# Public: Returns true if the Vite development server is currently running.
|
86
|
+
# NOTE: Checks only once every second since every lookup calls this method.
|
87
|
+
def dev_server_running?
|
88
|
+
return false unless run_proxy?
|
89
|
+
return @running if defined?(@running) && Time.now - @running_checked_at < 1
|
90
|
+
|
91
|
+
begin
|
92
|
+
Socket.tcp(config.host, config.port, connect_timeout: config.dev_server_connect_timeout).close
|
93
|
+
@running = true
|
94
|
+
rescue
|
95
|
+
@running = false
|
96
|
+
ensure
|
97
|
+
@running_checked_at = Time.now
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Public: Additional environment variables to pass to Vite.
|
102
|
+
#
|
103
|
+
# Example:
|
104
|
+
# ViteRuby.env['VITE_RUBY_CONFIG_PATH'] = 'config/alternate_vite.json'
|
105
|
+
def env
|
106
|
+
@env ||= ENV.select { |key, _| key.start_with?(ENV_PREFIX) }
|
107
|
+
end
|
108
|
+
|
109
|
+
# Public: The proxy for assets should only run in development mode.
|
110
|
+
def run_proxy?
|
111
|
+
config.mode == "development" || (config.mode == "test" && !ENV["CI"])
|
112
|
+
rescue => error
|
113
|
+
logger.error("Failed to check mode for Vite: #{error.message}")
|
114
|
+
false
|
115
|
+
end
|
116
|
+
|
117
|
+
# Internal: Executes the vite binary.
|
118
|
+
def run(argv, **options)
|
119
|
+
(@runner ||= ViteRuby::Runner.new(self)).run(argv, **options)
|
120
|
+
end
|
121
|
+
|
122
|
+
# Public: Keeps track of watched files and triggers builds as needed.
|
123
|
+
def builder
|
124
|
+
@builder ||= ViteRuby::Builder.new(self)
|
125
|
+
end
|
126
|
+
|
127
|
+
# Internal: Helper to run commands related with Vite.
|
128
|
+
def commands
|
129
|
+
@commands ||= ViteRuby::Commands.new(self)
|
130
|
+
end
|
131
|
+
|
132
|
+
# Public: Current instance configuration for Vite.
|
133
|
+
def config
|
134
|
+
unless defined?(@config)
|
135
|
+
configure
|
136
|
+
@config.load_ruby_config
|
137
|
+
end
|
138
|
+
|
139
|
+
@config
|
140
|
+
end
|
141
|
+
|
142
|
+
# Public: Allows overriding the configuration for this instance.
|
143
|
+
def configure(**options)
|
144
|
+
@config = ViteRuby::Config.resolve_config(**@config_options, **options)
|
145
|
+
end
|
146
|
+
|
147
|
+
# Public: Enables looking up assets managed by Vite using name and type.
|
148
|
+
def manifest
|
149
|
+
@manifest ||= ViteRuby::Manifest.new(self)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
require "vite_ruby/version"
|
@@ -0,0 +1,16 @@
|
|
1
|
+
{
|
2
|
+
"all": {
|
3
|
+
"sourceCodeDir": "frontend",
|
4
|
+
"watchAdditionalPaths": []
|
5
|
+
},
|
6
|
+
"development": {
|
7
|
+
"autoBuild": true,
|
8
|
+
"publicOutputDir": "vite-dev",
|
9
|
+
"port": 3036
|
10
|
+
},
|
11
|
+
"test": {
|
12
|
+
"autoBuild": true,
|
13
|
+
"publicOutputDir": "vite-test",
|
14
|
+
"port": 3037
|
15
|
+
}
|
16
|
+
}
|
@@ -0,0 +1,8 @@
|
|
1
|
+
// To see this message, follow the instructions for your Ruby framework.
|
2
|
+
//
|
3
|
+
// When using a plain API, perhaps it's better to generate an HTML entrypoint
|
4
|
+
// and link to the scripts and stylesheets, and let Vite transform it.
|
5
|
+
console.log('Vite ⚡️ Ruby')
|
6
|
+
|
7
|
+
// Example: Import a stylesheet in <sourceCodeDir>/index.css
|
8
|
+
// import '~/index.css'
|