vite_ruby 1.0.0

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.
@@ -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,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ViteRuby::CLI::Version < Dry::CLI::Command
4
+ desc 'Print version'
5
+
6
+ def call(**)
7
+ ViteRuby.commands.print_info
8
+ end
9
+ 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