vite_rails 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.
Files changed (57) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +3 -0
  3. data/CONTRIBUTING.md +34 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +64 -0
  6. data/lib/install/binstubs.rb +6 -0
  7. data/lib/install/template.rb +62 -0
  8. data/lib/vite_rails.rb +91 -0
  9. data/lib/vite_rails/builder.rb +113 -0
  10. data/lib/vite_rails/commands.rb +68 -0
  11. data/lib/vite_rails/config.rb +106 -0
  12. data/lib/vite_rails/dev_server.rb +23 -0
  13. data/lib/vite_rails/dev_server_proxy.rb +47 -0
  14. data/lib/vite_rails/engine.rb +40 -0
  15. data/lib/vite_rails/helper.rb +41 -0
  16. data/lib/vite_rails/manifest.rb +134 -0
  17. data/lib/vite_rails/runner.rb +56 -0
  18. data/lib/vite_rails/version.rb +5 -0
  19. data/package.json +28 -0
  20. data/package/default.vite.json +15 -0
  21. data/test/builder_test.rb +72 -0
  22. data/test/command_test.rb +35 -0
  23. data/test/configuration_test.rb +80 -0
  24. data/test/dev_server_runner_test.rb +83 -0
  25. data/test/dev_server_test.rb +39 -0
  26. data/test/engine_rake_tasks_test.rb +42 -0
  27. data/test/helper_test.rb +138 -0
  28. data/test/manifest_test.rb +75 -0
  29. data/test/mode_test.rb +21 -0
  30. data/test/mounted_app/Rakefile +6 -0
  31. data/test/mounted_app/test/dummy/Rakefile +5 -0
  32. data/test/mounted_app/test/dummy/bin/rails +5 -0
  33. data/test/mounted_app/test/dummy/bin/rake +5 -0
  34. data/test/mounted_app/test/dummy/config.ru +7 -0
  35. data/test/mounted_app/test/dummy/config/application.rb +12 -0
  36. data/test/mounted_app/test/dummy/config/environment.rb +5 -0
  37. data/test/mounted_app/test/dummy/config/vite.json +20 -0
  38. data/test/mounted_app/test/dummy/package.json +7 -0
  39. data/test/rake_tasks_test.rb +74 -0
  40. data/test/test_app/Rakefile +5 -0
  41. data/test/test_app/app/javascript/entrypoints/application.js +10 -0
  42. data/test/test_app/app/javascript/entrypoints/multi_entry.css +4 -0
  43. data/test/test_app/app/javascript/entrypoints/multi_entry.js +4 -0
  44. data/test/test_app/bin/vite +17 -0
  45. data/test/test_app/config.ru +7 -0
  46. data/test/test_app/config/application.rb +13 -0
  47. data/test/test_app/config/environment.rb +6 -0
  48. data/test/test_app/config/vite.json +20 -0
  49. data/test/test_app/config/vite_public_root.yml +20 -0
  50. data/test/test_app/package.json +13 -0
  51. data/test/test_app/public/vite/manifest.json +36 -0
  52. data/test/test_app/some.config.js +0 -0
  53. data/test/test_app/yarn.lock +11 -0
  54. data/test/test_helper.rb +34 -0
  55. data/test/vite_runner_test.rb +59 -0
  56. data/test/webpacker_test.rb +15 -0
  57. metadata +234 -0
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Public: Allows to resolve configuration sourced from `config/vite.json` and
4
+ # environment variables, combining them with the default options.
5
+ class ViteRails::Config
6
+ delegate :as_json, :inspect, to: :@config
7
+
8
+ def initialize(config)
9
+ @config = config.tap { coerce_values(config) }.freeze
10
+
11
+ config.each_key do |option|
12
+ define_singleton_method(option) { @config[option] }
13
+ end
14
+ end
15
+
16
+ def protocol
17
+ https ? 'https' : 'http'
18
+ end
19
+
20
+ def host_with_port
21
+ "#{ host }:#{ port }"
22
+ end
23
+
24
+ # Internal: Path where Vite outputs the manifest file.
25
+ def manifest_path
26
+ build_output_dir.join('manifest.json')
27
+ end
28
+
29
+ # Public: The directory where Vite will store the built assets.
30
+ def build_output_dir
31
+ public_dir.join(public_output_dir)
32
+ end
33
+
34
+ private
35
+
36
+ # Internal: Coerces all the configuration values, in case they were passed
37
+ # as environment variables which are always strings.
38
+ def coerce_values(config)
39
+ coerce_booleans(config, 'auto_build', 'https')
40
+ coerce_paths(config, 'assets_dir', 'build_cache_dir', 'config_path', 'public_dir', 'source_code_dir', 'public_output_dir', 'root')
41
+ config['port'] = config['port'].to_i
42
+ config['root'] ||= Rails.root
43
+ end
44
+
45
+ # Internal: Coerces configuration options to boolean.
46
+ def coerce_booleans(config, *names)
47
+ names.each { |name| config[name] = [true, 'true'].include?(config[name]) }
48
+ end
49
+
50
+ # Internal: Converts configuration options to pathname.
51
+ def coerce_paths(config, *names)
52
+ names.each { |name| config[name] = Pathname.new(config[name]) unless config[name].nil? }
53
+ end
54
+
55
+ class << self
56
+ # Public: Returns the project configuration for Vite.
57
+ def resolve_config
58
+ new DEFAULT_CONFIG.merge(config_from_file).merge(config_from_env)
59
+ rescue Errno::ENOENT => error
60
+ warn "Check that your vite.json configuration file is available in the load path. #{ error.message }"
61
+ new DEFAULT_CONFIG.merge(config_from_env)
62
+ end
63
+
64
+ private
65
+
66
+ # Internal: Used to load a JSON file from the specified path.
67
+ def load_json(path)
68
+ JSON.parse(File.read(File.expand_path(path))).deep_transform_keys(&:underscore)
69
+ rescue => error
70
+ (require 'pry-byebug';binding.pry;);
71
+ end
72
+
73
+ # Internal: Retrieves a configuration option from environment variables.
74
+ def config_option_from_env(name)
75
+ ENV["#{ ViteRails::ENV_PREFIX }_#{ name.upcase }"]
76
+ end
77
+
78
+ # Internal: Extracts the configuration options provided as env vars.
79
+ def config_from_env
80
+ CONFIGURABLE_WITH_ENV.each_with_object({}) do |key, env_vars|
81
+ if value = config_option_from_env(key)
82
+ env_vars[key] = value
83
+ end
84
+ end.merge(mode: vite_mode)
85
+ end
86
+
87
+ # Internal: The mode Vite should run on.
88
+ def vite_mode
89
+ config_option_from_env('mode') || Rails.env.to_s
90
+ end
91
+
92
+ # Internal: Loads the configuration options provided in a JSON file.
93
+ def config_from_file
94
+ path = config_option_from_env('config_path') || DEFAULT_CONFIG.fetch('config_path')
95
+ multi_env_config = load_json(path)
96
+ multi_env_config.fetch('all', {})
97
+ .merge(multi_env_config.fetch(vite_mode, {}))
98
+ end
99
+ end
100
+
101
+ # Internal: Shared configuration with the Vite plugin for Ruby.
102
+ DEFAULT_CONFIG = load_json("#{ __dir__ }/../../package/default.vite.json").freeze
103
+
104
+ # Internal: Configuration options that can be provided as env vars.
105
+ CONFIGURABLE_WITH_ENV = (DEFAULT_CONFIG.keys + ['root']).freeze
106
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Public: Allows to verify if a Vite development server is already running.
4
+ class ViteRails::DevServer
5
+ # Public: Configure dev server connection timeout (in seconds).
6
+ # Example:
7
+ # ViteRails.dev_server.connect_timeout = 1
8
+ cattr_accessor(:connect_timeout) { 0.01 }
9
+
10
+ def initialize(config)
11
+ @config = config
12
+ end
13
+
14
+ # Public: Returns true if the Vite development server is reachable.
15
+ def running?
16
+ Socket.tcp(host, port, connect_timeout: connect_timeout).close
17
+ true
18
+ rescue StandardError
19
+ false
20
+ end
21
+
22
+ delegate :host, :port, to: :@config
23
+ end
@@ -0,0 +1,47 @@
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 ViteRails::DevServerProxy < Rack::Proxy
7
+ VITE_DEPENDENCY_PREFIX = '/@'
8
+
9
+ def initialize(app = nil, options = {})
10
+ @vite_rails = options.delete(:vite_rails) || ViteRails.instance
11
+ options[:streaming] = false if Rails.env.test? && !options.key?(:streaming)
12
+ super
13
+ end
14
+
15
+ # Rack: Intercept asset requests and send them to the Vite server.
16
+ def perform_request(env)
17
+ if vite_should_handle?(env['REQUEST_URI']) && dev_server.running?
18
+ env['REQUEST_URI'] = env['REQUEST_URI']
19
+ .sub(vite_asset_url_prefix, '/')
20
+ .sub('.ts.js', '.ts') # Patch: Rails helpers always append the extension.
21
+ env['PATH_INFO'], env['QUERY_STRING'] = env['REQUEST_URI'].split('?')
22
+
23
+ env['HTTP_HOST'] = env['HTTP_X_FORWARDED_HOST'] = config.host
24
+ env['HTTP_X_FORWARDED_SERVER'] = config.host_with_port
25
+ env['HTTP_PORT'] = env['HTTP_X_FORWARDED_PORT'] = config.port.to_s
26
+ env['HTTP_X_FORWARDED_PROTO'] = env['HTTP_X_FORWARDED_SCHEME'] = config.protocol
27
+ env['HTTPS'] = env['HTTP_X_FORWARDED_SSL'] = 'off' unless config.https
28
+ env['SCRIPT_NAME'] = ''
29
+ super(env)
30
+ else
31
+ @app.call(env)
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ delegate :config, :dev_server, to: :@vite_rails
38
+
39
+ def vite_should_handle?(url)
40
+ url.start_with?(vite_asset_url_prefix) || url.start_with?(VITE_DEPENDENCY_PREFIX) ||
41
+ url.include?('?t=') # Direct Hot Reload
42
+ end
43
+
44
+ def vite_asset_url_prefix
45
+ @vite_asset_url_prefix ||= "/#{ config.public_output_dir }/"
46
+ end
47
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/railtie'
4
+
5
+ class ViteRails::Engine < Rails::Engine
6
+ initializer 'vite_rails.proxy' do |app|
7
+ app.middleware.insert_before 0, ViteRails::DevServerProxy, ssl_verify_none: true if ViteRails.run_proxy?
8
+ end
9
+
10
+ initializer 'vite_rails.helper' do
11
+ ActiveSupport.on_load(:action_controller) do
12
+ ActionController::Base.helper(ViteRails::Helper)
13
+ end
14
+
15
+ ActiveSupport.on_load(:action_view) do
16
+ include ViteRails::Helper
17
+ end
18
+ end
19
+
20
+ initializer 'vite_rails.logger' do
21
+ config.after_initialize do
22
+ ViteRails.logger = if ::Rails.logger.respond_to?(:tagged)
23
+ ::Rails.logger
24
+ else
25
+ ActiveSupport::TaggedLogging.new(::Rails.logger)
26
+ end
27
+ end
28
+ end
29
+
30
+ initializer 'vite_rails.bootstrap' do
31
+ if defined?(Rails::Server) || defined?(Rails::Console)
32
+ ViteRails.bootstrap
33
+ if defined?(Spring)
34
+ require 'spring/watcher'
35
+ Spring.after_fork { ViteRails.bootstrap }
36
+ Spring.watch(ViteRails.config.config_path)
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Public: Allows to render HTML tags for scripts and styles processed by Vite.
4
+ module ViteRails::Helper
5
+ # Public: Returns the current Vite Rails instance.
6
+ def current_vite_instance
7
+ ViteRails.instance
8
+ end
9
+
10
+ # Public: Computes the relative path for the specified given Vite asset.
11
+ #
12
+ # Example:
13
+ # <%= vite_asset_path 'calendar.css' %> # => "/vite/assets/calendar-1016838bab065ae1e122.css"
14
+ def vite_asset_path(name, **options)
15
+ current_vite_instance.manifest.lookup!(name, **options)
16
+ end
17
+
18
+ # Public: Renders a <script> tag for the specified Vite entrypoints.
19
+ def vite_javascript_tag(*names, type: 'module', **options)
20
+ javascript_include_tag(*sources_from_vite_manifest_entrypoints(names, type: :javascript), type: type, **options)
21
+ end
22
+
23
+ # Public: Renders a <script> tag for the specified Vite entrypoints.
24
+ #
25
+ # NOTE: Because TypeScript is not a valid target in browsers, we only specify
26
+ # the ts file when running the Vite development server.
27
+ def vite_typescript_tag(*names, type: 'module', **options)
28
+ javascript_include_tag(*sources_from_vite_manifest_entrypoints(names, type: :typescript), type: type, **options)
29
+ end
30
+
31
+ # Public: Renders a <link> tag for the specified Vite entrypoints.
32
+ def vite_stylesheet_tag(*names, **options)
33
+ stylesheet_link_tag(*sources_from_vite_manifest_entrypoints(names, type: :stylesheet), **options)
34
+ end
35
+
36
+ private
37
+
38
+ def sources_from_vite_manifest_entrypoints(names, type:)
39
+ names.flat_map { |name| vite_asset_path(name, type: type) }.uniq
40
+ end
41
+ end
@@ -0,0 +1,134 @@
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
+ # => "/vite/assets/calendar-1016838bab065ae1e314.js"
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 ViteRails::Manifest
13
+ class MissingEntryError < StandardError
14
+ end
15
+
16
+ def initialize(vite_rails)
17
+ @vite_rails = vite_rails
18
+ end
19
+
20
+ # Public: Strict version of lookup.
21
+ #
22
+ # Returns a relative path for the asset, or raises an error if not found.
23
+ def lookup!(*args, **options)
24
+ lookup(*args, **options) || missing_entry_error(*args, **options)
25
+ end
26
+
27
+ # Public: Computes the path for a given Vite asset using manifest.json.
28
+ #
29
+ # Returns a relative path, or nil if the asset is not found.
30
+ #
31
+ # Example:
32
+ # ViteRails.manifest.lookup('calendar.js') # => "/vite/assets/calendar-1016838bab065ae1e122.js"
33
+ def lookup(name, type:)
34
+ build if should_build?
35
+
36
+ find_manifest_entry(with_file_extension(name, type))
37
+ end
38
+
39
+ # Public: Refreshes the cached mappings by reading the updated manifest.
40
+ def refresh
41
+ @manifest = load_manifest
42
+ end
43
+
44
+ private
45
+
46
+ delegate :config, :builder, :dev_server, to: :@vite_rails
47
+
48
+ # Public: Returns true if the Vite development server is running.
49
+ def dev_server_running?
50
+ ViteRails.run_proxy? && dev_server.running?
51
+ end
52
+
53
+ # NOTE: Auto compilation is convenient when running tests, when the developer
54
+ # won't focus on the frontend, or when running the Vite server is not desired.
55
+ def should_build?
56
+ config.auto_build && !dev_server_running?
57
+ end
58
+
59
+ # Internal: Finds the specified entry in the manifest.
60
+ def find_manifest_entry(name)
61
+ if dev_server_running?
62
+ "/#{ config.public_output_dir.join(name.to_s) }"
63
+ elsif file = manifest.dig(name.to_s, 'file')
64
+ "/#{ config.public_output_dir.join(file) }"
65
+ end
66
+ end
67
+
68
+ # Internal: Performs a Vite build.
69
+ def build
70
+ ViteRails.logger.tagged('Vite') { builder.build }
71
+ end
72
+
73
+ # Internal: The parsed data from manifest.json.
74
+ #
75
+ # NOTE: When using build-on-demand in development and testing, the manifest
76
+ # is reloaded automatically before each lookup, to ensure it's always fresh.
77
+ def manifest
78
+ return refresh if config.auto_build
79
+
80
+ @manifest ||= load_manifest
81
+ end
82
+
83
+ # Internal: Returns a Hash with the entries in the manifest.json.
84
+ def load_manifest
85
+ if config.manifest_path.exist?
86
+ JSON.parse(config.manifest_path.read)
87
+ else
88
+ {}
89
+ end
90
+ end
91
+
92
+ # Internal: Adds a file extension to the file name, unless it already has one.
93
+ def with_file_extension(name, entry_type)
94
+ return name unless File.extname(name.to_s).empty?
95
+
96
+ "#{ name }.#{ extension_for_type(entry_type) }"
97
+ end
98
+
99
+ # Internal: Allows to receive :javascript and :stylesheet as :type in helpers.
100
+ def extension_for_type(entry_type)
101
+ case entry_type
102
+ when :javascript then 'js'
103
+ when :stylesheet then 'css'
104
+ when :typescript then dev_server_running? ? 'ts' : 'js'
105
+ else entry_type.to_s
106
+ end
107
+ end
108
+
109
+ # Internal: Raises a detailed message when an entry is missing in the manifest.
110
+ def missing_entry_error(name, type: nil, **_options)
111
+ file_name = with_file_extension(name, type)
112
+ raise ViteRails::Manifest::MissingEntryError, <<~MSG
113
+ Vite Rails can't find #{ file_name } in #{ config.manifest_path }.
114
+
115
+ Possible causes:
116
+ #{ missing_entry_causes.map { |cause| "\t- #{ cause }" }.join("\n") }
117
+
118
+ Your manifest contains:
119
+ #{ JSON.pretty_generate(@manifest) }
120
+ MSG
121
+ end
122
+
123
+ def missing_entry_causes
124
+ local = config.auto_build
125
+ [
126
+ (dev_server_running? && 'Vite has not yet re-built your latest changes.'),
127
+ (local && !dev_server_running? && "\"autoBuild\": false in your #{ config.mode } configuration."),
128
+ 'You have misconfigured config/vite.json file.',
129
+ (!local && 'Assets have not been precompiled'),
130
+ ].select(&:itself)
131
+ rescue StandardError
132
+ []
133
+ end
134
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Public: Executes Vite commands, providing conveniences for debugging.
4
+ class ViteRails::Runner
5
+ def initialize(argv)
6
+ detect_unsupported_switches!(argv)
7
+ @argv = argv
8
+ end
9
+
10
+ # Public: Executes Vite with the specified arguments.
11
+ def run
12
+ execute_command(@argv.clone)
13
+ end
14
+
15
+ private
16
+
17
+ UNSUPPORTED_SWITCHES = %w[--host --port --https --root -r --config -c]
18
+ private_constant :UNSUPPORTED_SWITCHES
19
+
20
+ # Internal: Allows to prevent configuration mistakes by ensuring the Rails app
21
+ # and vite-plugin-ruby are using the same configuration for the dev server.
22
+ def detect_unsupported_switches!(args)
23
+ return unless (unsupported = UNSUPPORTED_SWITCHES & args).any?
24
+
25
+ $stdout.puts "Please set the following switches in your vite.json instead: #{ unsupported }."
26
+ exit!
27
+ end
28
+
29
+ # Internal: Executes the command with the specified arguments.
30
+ def execute_command(args)
31
+ cmd = vite_executable
32
+ cmd.prepend('node', '--inspect-brk') if args.include?('--debug')
33
+ cmd.prepend('node', '--trace-deprecation') if args.delete('--trace-deprecation')
34
+ args.append('--mode', ENV['RAILS_ENV']) unless args.include?('--mode') || args.include?('-m')
35
+ cmd += args
36
+ puts cmd.join(' ')
37
+ Dir.chdir(File.expand_path('.', Dir.pwd)) { Kernel.exec(ViteRails.env, *cmd) }
38
+ end
39
+
40
+ # Internal: Resolves to an executable for Vite.
41
+ def vite_executable
42
+ executable_exists?(path = vite_bin_path) ? [path] : %w[yarn vite]
43
+ end
44
+
45
+ # Internal: Only so that we can easily cover both paths in tests
46
+ def executable_exists?(path)
47
+ File.exist?(path)
48
+ end
49
+
50
+ # Internal: Returns a path where a Vite executable should be found.
51
+ def vite_bin_path
52
+ ENV["#{ ViteRails::ENV_PREFIX }_VITE_BIN_PATH"] || `yarn bin vite`.chomp
53
+ rescue StandardError
54
+ "#{ `npm bin`.chomp }/vite"
55
+ end
56
+ end