vite_ruby 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +3 -0
- data/CONTRIBUTING.md +33 -0
- data/LICENSE.txt +21 -0
- data/README.md +82 -0
- data/default.vite.json +17 -0
- data/exe/vite +10 -0
- data/lib/tasks/vite.rake +51 -0
- data/lib/vite_ruby.rb +125 -0
- data/lib/vite_ruby/builder.rb +103 -0
- data/lib/vite_ruby/cli.rb +14 -0
- data/lib/vite_ruby/cli/build.rb +18 -0
- data/lib/vite_ruby/cli/dev.rb +12 -0
- data/lib/vite_ruby/cli/install.rb +122 -0
- data/lib/vite_ruby/cli/version.rb +9 -0
- data/lib/vite_ruby/commands.rb +153 -0
- data/lib/vite_ruby/config.rb +152 -0
- data/lib/vite_ruby/dev_server_proxy.rb +62 -0
- data/lib/vite_ruby/manifest.rb +168 -0
- data/lib/vite_ruby/runner.rb +57 -0
- data/lib/vite_ruby/version.rb +5 -0
- data/templates/config/vite.config.ts +8 -0
- data/templates/config/vite.json +15 -0
- data/templates/entrypoints/application.js +8 -0
- metadata +153 -0
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/cli'
|
4
|
+
|
5
|
+
# Public: Command line interface that allows to install the library, and run
|
6
|
+
# simple commands.
|
7
|
+
class ViteRuby::CLI
|
8
|
+
extend Dry::CLI::Registry
|
9
|
+
|
10
|
+
register 'build', Build, aliases: ['b']
|
11
|
+
register 'dev', Dev, aliases: %w[d serve]
|
12
|
+
register 'install', Install, aliases: %w[setup init]
|
13
|
+
register 'version', Version, aliases: ['v', '-v', '--version', 'info']
|
14
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class ViteRuby::CLI::Build < Dry::CLI::Command
|
4
|
+
CURRENT_ENV = ENV['RACK_ENV'] || ENV['RAILS_ENV']
|
5
|
+
DEFAULT_ENV = CURRENT_ENV || 'production'
|
6
|
+
|
7
|
+
def self.shared_options
|
8
|
+
option(:mode, default: self::DEFAULT_ENV, values: %w[development production], aliases: ['m'], desc: 'The build mode for Vite.')
|
9
|
+
end
|
10
|
+
|
11
|
+
desc 'Bundle all entrypoints using Vite.'
|
12
|
+
shared_options
|
13
|
+
|
14
|
+
def call(mode:)
|
15
|
+
ViteRuby.env['VITE_RUBY_MODE'] = mode
|
16
|
+
block_given? ? yield(mode) : ViteRuby.commands.build_from_task
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class ViteRuby::CLI::Dev < ViteRuby::CLI::Build
|
4
|
+
DEFAULT_ENV = CURRENT_ENV || 'development'
|
5
|
+
|
6
|
+
desc 'Start the Vite development server.'
|
7
|
+
shared_options
|
8
|
+
|
9
|
+
def call(mode:, args: [])
|
10
|
+
super(mode: mode) { ViteRuby.run(args) }
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/cli/utils/files'
|
4
|
+
require 'stringio'
|
5
|
+
require 'open3'
|
6
|
+
|
7
|
+
class ViteRuby::CLI::Install < Dry::CLI::Command
|
8
|
+
desc 'Performs the initial configuration setup to get started with Vite Ruby.'
|
9
|
+
|
10
|
+
def call(**)
|
11
|
+
$stdout.sync = true
|
12
|
+
|
13
|
+
say 'Creating binstub'
|
14
|
+
ViteRuby.commands.install_binstubs
|
15
|
+
|
16
|
+
say 'Creating configuration files'
|
17
|
+
create_configuration_files
|
18
|
+
|
19
|
+
say 'Installing sample files'
|
20
|
+
install_sample_files
|
21
|
+
|
22
|
+
say 'Installing js dependencies'
|
23
|
+
install_js_dependencies
|
24
|
+
|
25
|
+
say 'Adding files to .gitignore'
|
26
|
+
install_gitignore
|
27
|
+
|
28
|
+
say "\nVite ⚡️ Ruby successfully installed! 🎉"
|
29
|
+
end
|
30
|
+
|
31
|
+
protected
|
32
|
+
|
33
|
+
# Internal: Setup for a plain Rack application.
|
34
|
+
def setup_app_files
|
35
|
+
copy_template 'config/vite.json', to: config.config_path
|
36
|
+
|
37
|
+
if (rackup_file = root.join('config.ru')).exist?
|
38
|
+
inject_line_after_last rackup_file, 'require', 'use(ViteRuby::DevServerProxy, ssl_verify_none: true) if ViteRuby.run_proxy?'
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Internal: Create a sample JS file and attempt to inject it in an HTML template.
|
43
|
+
def install_sample_files
|
44
|
+
copy_template 'entrypoints/application.js', to: config.resolved_entrypoints_dir.join('application.js')
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
extend Forwardable
|
50
|
+
|
51
|
+
def_delegators 'ViteRuby', :config
|
52
|
+
|
53
|
+
%i[append cp inject_line_after inject_line_after_last inject_line_before write].each do |util|
|
54
|
+
define_method(util) { |*args, **opts, &block|
|
55
|
+
Dry::CLI::Utils::Files.send(util, *args, **opts, &block) rescue nil
|
56
|
+
}
|
57
|
+
end
|
58
|
+
|
59
|
+
TEMPLATES_PATH = Pathname.new(File.expand_path('../../../templates', __dir__))
|
60
|
+
|
61
|
+
def copy_template(path, to:)
|
62
|
+
cp TEMPLATES_PATH.join(path), to
|
63
|
+
end
|
64
|
+
|
65
|
+
# Internal: Creates the Vite and vite-plugin-ruby configuration files.
|
66
|
+
def create_configuration_files
|
67
|
+
copy_template 'config/vite.config.ts', to: root.join('vite.config.ts')
|
68
|
+
setup_app_files
|
69
|
+
ViteRuby.reload_with('VITE_RUBY_CONFIG_PATH' => config.config_path)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Internal: Installs vite and vite-plugin-ruby at the project level.
|
73
|
+
def install_js_dependencies
|
74
|
+
package_json = root.join('package.json')
|
75
|
+
write(package_json, '{}') unless package_json.exist?
|
76
|
+
Dir.chdir(root) do
|
77
|
+
deps = "vite@#{ ViteRuby::DEFAULT_VITE_VERSION } vite-plugin-ruby@#{ ViteRuby::DEFAULT_PLUGIN_VERSION }"
|
78
|
+
stdout, stderr, status = Open3.capture3({ 'CI' => 'true' }, "npx ni -D #{ deps }")
|
79
|
+
stdout, stderr, = Open3.capture3({}, "yarn add -D #{ deps }") unless status.success?
|
80
|
+
say(stdout, "\n", stderr)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Internal: Adds compilation output dirs to git ignore.
|
85
|
+
def install_gitignore
|
86
|
+
return unless (gitignore_file = root.join('.gitignore')).exist?
|
87
|
+
|
88
|
+
append(gitignore_file, <<~GITIGNORE)
|
89
|
+
|
90
|
+
# Vite Ruby
|
91
|
+
/public/vite
|
92
|
+
/public/vite-dev
|
93
|
+
/public/vite-test
|
94
|
+
node_modules
|
95
|
+
*.local
|
96
|
+
.DS_Store
|
97
|
+
GITIGNORE
|
98
|
+
end
|
99
|
+
|
100
|
+
# Internal: The root path for the Ruby application.
|
101
|
+
def root
|
102
|
+
@root ||= silent_warnings { config.root }
|
103
|
+
end
|
104
|
+
|
105
|
+
def say(*args)
|
106
|
+
$stdout.puts(*args)
|
107
|
+
end
|
108
|
+
|
109
|
+
# Internal: Avoid printing warning about missing vite.json, we will create one.
|
110
|
+
def silent_warnings
|
111
|
+
old_stderr = $stderr
|
112
|
+
$stderr = StringIO.new
|
113
|
+
yield
|
114
|
+
ensure
|
115
|
+
$stderr = old_stderr
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# NOTE: This allows framework-specific variants to extend the installation.
|
120
|
+
ViteRuby.framework_libraries.each do |_framework, library|
|
121
|
+
require "#{ library.name }/installation"
|
122
|
+
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Public: Encapsulates common tasks, available both programatically and from the
|
4
|
+
# CLI and Rake tasks.
|
5
|
+
class ViteRuby::Commands
|
6
|
+
def initialize(vite_ruby)
|
7
|
+
@vite_ruby = vite_ruby
|
8
|
+
end
|
9
|
+
|
10
|
+
# Public: Defaults to production, and exits if the build fails.
|
11
|
+
def build_from_task
|
12
|
+
with_node_env(ENV.fetch('NODE_ENV', 'production')) {
|
13
|
+
ensure_log_goes_to_stdout {
|
14
|
+
build || exit!
|
15
|
+
}
|
16
|
+
}
|
17
|
+
end
|
18
|
+
|
19
|
+
# Public: Builds all assets that are managed by Vite, from the entrypoints.
|
20
|
+
def build
|
21
|
+
builder.build.tap { manifest.refresh }
|
22
|
+
end
|
23
|
+
|
24
|
+
# Public: Removes all build cache and previously compiled assets.
|
25
|
+
def clobber
|
26
|
+
config.build_output_dir.rmtree if config.build_output_dir.exist?
|
27
|
+
config.build_cache_dir.rmtree if config.build_cache_dir.exist?
|
28
|
+
config.vite_cache_dir.rmtree if config.vite_cache_dir.exist?
|
29
|
+
end
|
30
|
+
|
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
|
+
}
|
36
|
+
end
|
37
|
+
|
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
|
61
|
+
end
|
62
|
+
|
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
|
+
end
|
67
|
+
|
68
|
+
# Internal: Verifies if ViteRuby is properly installed.
|
69
|
+
def verify_install
|
70
|
+
unless File.exist?(config.root.join('bin/vite'))
|
71
|
+
warn <<~WARN
|
72
|
+
vite binstub not found.
|
73
|
+
Have you run `bundle binstub vite`?
|
74
|
+
Make sure the bin directory and bin/vite are not included in .gitignore
|
75
|
+
WARN
|
76
|
+
end
|
77
|
+
|
78
|
+
config_path = config.root.join(config.config_path)
|
79
|
+
unless config_path.exist?
|
80
|
+
warn <<~WARN
|
81
|
+
Configuration #{ config_path } file for vite-plugin-ruby not found.
|
82
|
+
Make sure `bundle exec vite install` has run successfully before running dependent tasks.
|
83
|
+
WARN
|
84
|
+
exit!
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# Internal: Prints information about ViteRuby's environment.
|
89
|
+
def print_info
|
90
|
+
Dir.chdir(config.root) do
|
91
|
+
$stdout.puts "Is bin/vite present?: #{ File.exist? 'bin/vite' }"
|
92
|
+
|
93
|
+
$stdout.puts "vite_ruby: #{ ViteRuby::VERSION }"
|
94
|
+
ViteRuby.framework_libraries.each do |framework, library|
|
95
|
+
$stdout.puts "#{ library.name }: #{ library.version }"
|
96
|
+
$stdout.puts "#{ framework }: #{ Gem.loaded_specs[framework]&.version }"
|
97
|
+
end
|
98
|
+
|
99
|
+
$stdout.puts "node: #{ `node --version` }"
|
100
|
+
$stdout.puts "npm: #{ `npm --version` }"
|
101
|
+
$stdout.puts "yarn: #{ `yarn --version` }"
|
102
|
+
$stdout.puts "ruby: #{ `ruby --version` }"
|
103
|
+
|
104
|
+
$stdout.puts "\n"
|
105
|
+
$stdout.puts "vite-plugin-ruby: \n#{ `npm list vite-plugin-ruby version` }"
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
private
|
110
|
+
|
111
|
+
extend Forwardable
|
112
|
+
|
113
|
+
def_delegators :@vite_ruby, :config, :builder, :manifest, :logger, :logger=
|
114
|
+
|
115
|
+
def may_clean?
|
116
|
+
config.build_output_dir.exist? && config.manifest_path.exist?
|
117
|
+
end
|
118
|
+
|
119
|
+
def clean_files(files)
|
120
|
+
files.select { |file| File.file?(file) }.each do |file|
|
121
|
+
File.delete(file)
|
122
|
+
logger.info("Removed #{ file }")
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def versions
|
127
|
+
all_files = Dir.glob("#{ config.build_output_dir }/**/*")
|
128
|
+
entries = all_files - [config.manifest_path] - current_version_files
|
129
|
+
entries.reject { |file| File.directory?(file) }
|
130
|
+
.group_by { |file| File.mtime(file).utc.to_i }
|
131
|
+
.sort.reverse
|
132
|
+
end
|
133
|
+
|
134
|
+
def current_version_files
|
135
|
+
Dir.glob(manifest.refresh.values.map { |value| config.build_output_dir.join("#{ value['file'] }*") })
|
136
|
+
end
|
137
|
+
|
138
|
+
def with_node_env(env)
|
139
|
+
original = ENV['NODE_ENV']
|
140
|
+
ENV['NODE_ENV'] = env
|
141
|
+
yield
|
142
|
+
ensure
|
143
|
+
ENV['NODE_ENV'] = original
|
144
|
+
end
|
145
|
+
|
146
|
+
def ensure_log_goes_to_stdout
|
147
|
+
old_logger = logger
|
148
|
+
self.logger = Logger.new($stdout)
|
149
|
+
yield
|
150
|
+
ensure
|
151
|
+
self.logger = old_logger
|
152
|
+
end
|
153
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
# Public: Allows to resolve configuration sourced from `config/vite.json` and
|
6
|
+
# environment variables, combining them with the default options.
|
7
|
+
class ViteRuby::Config
|
8
|
+
def protocol
|
9
|
+
https ? 'https' : 'http'
|
10
|
+
end
|
11
|
+
|
12
|
+
def host_with_port
|
13
|
+
"#{ host }:#{ port }"
|
14
|
+
end
|
15
|
+
|
16
|
+
# Internal: Path where Vite outputs the manifest file.
|
17
|
+
def manifest_path
|
18
|
+
build_output_dir.join('manifest.json')
|
19
|
+
end
|
20
|
+
|
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')
|
24
|
+
end
|
25
|
+
|
26
|
+
# Public: The directory where Vite will store the built assets.
|
27
|
+
def build_output_dir
|
28
|
+
root.join(public_dir, public_output_dir)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Public: The directory where the entries are located.
|
32
|
+
def resolved_entrypoints_dir
|
33
|
+
root.join(source_code_dir, entrypoints_dir)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Internal: The directory where Vite stores its processing cache.
|
37
|
+
def vite_cache_dir
|
38
|
+
root.join('node_modules/.vite')
|
39
|
+
end
|
40
|
+
|
41
|
+
# Public: Sets additional environment variables for vite-plugin-ruby.
|
42
|
+
def to_env
|
43
|
+
CONFIGURABLE_WITH_ENV.each_with_object({}) do |option, env|
|
44
|
+
unless (value = @config[option]).nil?
|
45
|
+
env["#{ ViteRuby::ENV_PREFIX }_#{ option.upcase }"] = value.to_s
|
46
|
+
end
|
47
|
+
end.merge(ViteRuby.env)
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
# Internal: Coerces all the configuration values, in case they were passed
|
53
|
+
# as environment variables which are always strings.
|
54
|
+
def coerce_values(config)
|
55
|
+
config['mode'] = config['mode'].to_s
|
56
|
+
config['port'] = config['port'].to_i
|
57
|
+
config['root'] = Pathname.new(config['root'])
|
58
|
+
config['build_cache_dir'] = config['root'].join(config['build_cache_dir'])
|
59
|
+
coerce_booleans(config, 'auto_build', 'hide_build_console_output', 'https')
|
60
|
+
end
|
61
|
+
|
62
|
+
# Internal: Coerces configuration options to boolean.
|
63
|
+
def coerce_booleans(config, *names)
|
64
|
+
names.each { |name| config[name] = [true, 'true'].include?(config[name]) }
|
65
|
+
end
|
66
|
+
|
67
|
+
def initialize(attrs)
|
68
|
+
@config = attrs.tap { |config| coerce_values(config) }.freeze
|
69
|
+
end
|
70
|
+
|
71
|
+
class << self
|
72
|
+
private :new
|
73
|
+
|
74
|
+
# Public: Returns the project configuration for Vite.
|
75
|
+
def resolve_config(**attrs)
|
76
|
+
config = config_defaults.merge(attrs.transform_keys(&:to_s))
|
77
|
+
file_path = File.join(config['root'], config['config_path'])
|
78
|
+
file_config = config_from_file(file_path, mode: config['mode'])
|
79
|
+
new DEFAULT_CONFIG.merge(file_config).merge(config_from_env).merge(config)
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
# Internal: Converts camelCase to snake_case.
|
85
|
+
SNAKE_CASE = ->(camel_cased_word) {
|
86
|
+
camel_cased_word.to_s.gsub(/::/, '/')
|
87
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
88
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
89
|
+
.tr('-', '_')
|
90
|
+
.downcase
|
91
|
+
}
|
92
|
+
|
93
|
+
# Internal: Default values for a Ruby application.
|
94
|
+
def config_defaults(asset_host: nil, mode: ENV.fetch('RACK_ENV', 'production'), root: Dir.pwd)
|
95
|
+
{
|
96
|
+
'asset_host' => option_from_env('asset_host') || asset_host,
|
97
|
+
'config_path' => option_from_env('config_path') || DEFAULT_CONFIG.fetch('config_path'),
|
98
|
+
'mode' => option_from_env('mode') || mode,
|
99
|
+
'root' => option_from_env('root') || root,
|
100
|
+
}
|
101
|
+
end
|
102
|
+
|
103
|
+
# Internal: Used to load a JSON file from the specified path.
|
104
|
+
def load_json(path)
|
105
|
+
JSON.parse(File.read(File.expand_path(path))).each do |_env, config|
|
106
|
+
config.transform_keys!(&SNAKE_CASE) if config.is_a?(Hash)
|
107
|
+
end.tap do |config|
|
108
|
+
config.transform_keys!(&SNAKE_CASE)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# Internal: Retrieves a configuration option from environment variables.
|
113
|
+
def option_from_env(name)
|
114
|
+
ViteRuby.env["#{ ViteRuby::ENV_PREFIX }_#{ name.upcase }"]
|
115
|
+
end
|
116
|
+
|
117
|
+
# Internal: Extracts the configuration options provided as env vars.
|
118
|
+
def config_from_env
|
119
|
+
CONFIGURABLE_WITH_ENV.each_with_object({}) do |option, env_vars|
|
120
|
+
if value = option_from_env(option)
|
121
|
+
env_vars[option] = value
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# Internal: Loads the configuration options provided in a JSON file.
|
127
|
+
def config_from_file(path, mode:)
|
128
|
+
multi_env_config = load_json(path)
|
129
|
+
multi_env_config.fetch('all', {})
|
130
|
+
.merge(multi_env_config.fetch(mode, {}))
|
131
|
+
rescue Errno::ENOENT => error
|
132
|
+
warn "Check that your vite.json configuration file is available in the load path:\n\n\t#{ error.message }\n\n"
|
133
|
+
{}
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
# Internal: Shared configuration with the Vite plugin for Ruby.
|
138
|
+
DEFAULT_CONFIG = load_json("#{ __dir__ }/../../default.vite.json").freeze
|
139
|
+
|
140
|
+
# Internal: Configuration options that can not be provided as env vars.
|
141
|
+
NOT_CONFIGURABLE_WITH_ENV = %w[watch_additional_paths].freeze
|
142
|
+
|
143
|
+
# Internal: Configuration options that can be provided as env vars.
|
144
|
+
CONFIGURABLE_WITH_ENV = (DEFAULT_CONFIG.keys + %w[mode root] - NOT_CONFIGURABLE_WITH_ENV).freeze
|
145
|
+
|
146
|
+
public
|
147
|
+
|
148
|
+
# Define getters for the configuration options.
|
149
|
+
(CONFIGURABLE_WITH_ENV + NOT_CONFIGURABLE_WITH_ENV).each do |option|
|
150
|
+
define_method(option) { @config[option] }
|
151
|
+
end
|
152
|
+
end
|