vite_ruby 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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