vite_rails 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ad566a46292f652f6533c40901952359b27afdc98224af3f67bd4414a906a951
4
+ data.tar.gz: f3a035a74e1f3d092b99d46a25dfd32cd2d6cb50071f7dc971473f0958a76893
5
+ SHA512:
6
+ metadata.gz: 77d1dd2b22130bdee130dc814e4453d1c63ad6823c3cc0cd9e6a1f8a17c3f02adb31f8924e2f97d686dc707bbd2b56b327948e6e12f5bcdf2c47132f62293891
7
+ data.tar.gz: 635ea8d95ed3e8cd3dc0e2f44fd962fdf3920578bdeb51f6cabf2be251035555371d20adb1ac9ab5a6e81d6620def27a0b646fc2f3bd8f769869d328c1ef5c40
@@ -0,0 +1,3 @@
1
+ ## Vite Rails 1.0.0
2
+
3
+ Initial Version
@@ -0,0 +1,34 @@
1
+ ## Setting Up a Development Environment
2
+
3
+ 1. Install [Yarn](https://yarnpkg.com/)
4
+
5
+ 2. Run the following commands to set up the development environment.
6
+
7
+ ```
8
+ bundle install
9
+ yarn
10
+ ```
11
+
12
+ ## Making sure your changes pass all tests
13
+ There are a number of automated checks which run on GitHub Actions when a pull request is created.
14
+ You can run those checks on your own locally to make sure that your changes would not break the CI build.
15
+
16
+ ### 1. Check the code for JavaScript style violations
17
+ ```
18
+ yarn lint
19
+ ```
20
+
21
+ ### 2. Check the code for Ruby style violations
22
+ ```
23
+ bundle exec rubocop
24
+ ```
25
+
26
+ ### 3. Run the JavaScript test suite
27
+ ```
28
+ yarn test
29
+ ```
30
+
31
+ ### 4. Run the Ruby test suite
32
+ ```
33
+ bundle exec rake test
34
+ ```
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2020 Maximo Mussini
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,64 @@
1
+ <h1 align="center">
2
+ Vite ⚡️ Rails
3
+ <p align="center">
4
+ <!-- <a href="https://github.com/ElMassimo/vite_rails/actions">
5
+ <img alt="Build Status" src="https://github.com/ElMassimo/vite_rails/workflows/build/badge.svg"/>
6
+ </a> -->
7
+ <!-- <a href="https://codeclimate.com/github/ElMassimo/vite_rails">
8
+ <img alt="Maintainability" src="https://codeclimate.com/github/ElMassimo/vite_rails/badges/gpa.svg"/>
9
+ </a>
10
+ <a href="https://codeclimate.com/github/ElMassimo/vite_rails">
11
+ <img alt="Test Coverage" src="https://codeclimate.com/github/ElMassimo/vite_rails/badges/coverage.svg"/>
12
+ </a> -->
13
+ <a href="https://rubygems.org/gems/vite_rails">
14
+ <img alt="Gem Version" src="https://img.shields.io/gem/v/vite_rails.svg?colorB=e9573f"/>
15
+ </a>
16
+ <a href="https://github.com/ElMassimo/vite_rails/blob/master/LICENSE.txt">
17
+ <img alt="License" src="https://img.shields.io/badge/license-MIT-428F7E.svg"/>
18
+ </a>
19
+ </p>
20
+ </h1>
21
+
22
+ [vite_rails]: https://github.com/ElMassimo/vite_rails
23
+ [webpacker]: https://github.com/rails/webpacker
24
+ [vite]: http://vitejs.dev/
25
+
26
+ [__Vite Rails__][vite_rails] allows you to use [Vite] to power the frontend.
27
+
28
+ [Vite] is to frontend tooling as Ruby to programming, pure joy! 😍
29
+
30
+ ## Features ⚡️
31
+
32
+ - 🤖 Automatic Entrypoint Detection
33
+ - ⚡️ Hot Reload
34
+ - ⚙️ Rake Tasks
35
+ - 🪝 Hooks to <kbd>assets:precompile</kbd> and friends
36
+
37
+ ## Documentation 📖
38
+
39
+ Coming Soon!
40
+
41
+ ## Installation 💿
42
+
43
+ Add this line to your application's Gemfile:
44
+
45
+ ```ruby
46
+ gem 'vite_rails'
47
+ ```
48
+
49
+ Then, run:
50
+
51
+ ```bash
52
+ bundle install
53
+ bin/rake vite:install
54
+ ```
55
+
56
+ This will generate configuration files and a sample setup.
57
+
58
+ ## Inspiration 💡
59
+
60
+ - [webpacker]
61
+
62
+ ## License
63
+
64
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ say 'Copying binstubs'
4
+ directory "#{ __dir__ }/bin", 'bin'
5
+
6
+ chmod 'bin', 0o755 & ~File.umask, verbose: false
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Install ViteRails
4
+ copy_file "#{ __dir__ }/config/vite.json", ViteRails.config.config_path
5
+
6
+ if Dir.exist?(ViteRails.config.source_code_dir)
7
+ say 'The JavaScript app source directory already exists. Move it and try again to see a basic setup.'
8
+ else
9
+ say 'Creating JavaScript app source directory'
10
+ directory "#{ __dir__ }/javascript", ViteRails.config.source_code_dir
11
+ end
12
+
13
+ apply "#{ __dir__ }/binstubs.rb"
14
+
15
+ git_ignore_path = Rails.root.join('.gitignore')
16
+ if git_ignore_path.exist?
17
+ append_to_file(git_ignore_path) {
18
+ <<~GITIGNORE
19
+
20
+ # Vite on Rails
21
+ /public/vite
22
+ /public/vite-dev
23
+ /public/vite-test
24
+ node_modules
25
+ *.local
26
+ .DS_Store
27
+ GITIGNORE
28
+ }
29
+ end
30
+
31
+ install = if Rails.root.join('yarn.lock').exist?
32
+ 'yarn add'
33
+ elsif Rails.root.join('pnpm-lock.yaml').exist?
34
+ 'pnpm install'
35
+ else
36
+ 'npm install'
37
+ end
38
+
39
+ Dir.chdir(Rails.root) do
40
+ say 'Installing JavaScript dependencies for Vite Rails'
41
+ package_json = File.read("#{ __dir__ }/../../package.json")
42
+
43
+ vite_version = package_json.match(/"vite": "(.*)"/)[1]
44
+ plugin_version = package_json.match(/"vite-plugin-ruby": "(.*)"/)[1]
45
+
46
+ say 'Installing vite as direct dependencies'
47
+ run "#{ install } vite@#{ vite_version } vite-plugin-ruby@#{ plugin_version }"
48
+ end
49
+
50
+ if Rails::VERSION::MAJOR == 5 && Rails::VERSION::MINOR > 1
51
+ src = begin
52
+ "#{ ViteRails.config.protocol }://#{ ViteRails.config.host_with_port }"
53
+ rescue StandardError
54
+ 'http://localhost:3036'
55
+ end
56
+ say 'You need to allow vite-dev-server host as allowed origin for connect-src.', :yellow
57
+ say 'This can be done in Rails 5.2+ for development environment in the CSP initializer', :yellow
58
+ say 'config/initializers/content_security_policy.rb with a snippet like this:', :yellow
59
+ say %(policy.connect_src :self, :https, "http://#{ src }", "ws://#{ src }" if Rails.env.development?), :yellow
60
+ end
61
+
62
+ say 'ViteRails successfully installed 🎉 🍰', :green
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support'
4
+ require 'active_support/core_ext/class/attribute_accessors'
5
+
6
+ require 'zeitwerk'
7
+ loader = Zeitwerk::Loader.for_gem
8
+ loader.ignore("#{ __dir__ }/install")
9
+ loader.ignore("#{ __dir__ }/tasks")
10
+ loader.setup
11
+
12
+ class ViteRails
13
+ # Internal: Prefix used for environment variables that modify the configuration.
14
+ ENV_PREFIX = 'VITE_RUBY'
15
+
16
+ # Public: Additional environment variables to pass to Vite.
17
+ #
18
+ # Example:
19
+ # ViteRails.env['VITE_RUBY_CONFIG_PATH'] = 'config/alternate_vite.json'
20
+ cattr_accessor(:env) { ENV.select { |key, _| key.start_with?(ENV_PREFIX) } }
21
+
22
+ cattr_accessor(:logger) { ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(STDOUT)) }
23
+
24
+ class << self
25
+ delegate :config, :builder, :manifest, :commands, :dev_server, to: :instance
26
+ delegate :mode, to: :config
27
+ delegate :bootstrap, :clean, :clobber, :build, to: :commands
28
+
29
+ attr_writer :instance
30
+
31
+ def instance
32
+ @instance ||= ViteRails.new
33
+ end
34
+
35
+ def run(args)
36
+ $stdout.sync = true
37
+ ViteRails::Runner.new(args).run
38
+ end
39
+
40
+ # Public: The proxy for assets should only run in development mode.
41
+ def run_proxy?
42
+ config.mode == 'development'
43
+ rescue StandardError => error
44
+ logger.error("Failed to check mode for Vite: #{ error.message }")
45
+ false
46
+ end
47
+
48
+ def with_node_env(env)
49
+ original = ENV['NODE_ENV']
50
+ ENV['NODE_ENV'] = env
51
+ yield
52
+ ensure
53
+ ENV['NODE_ENV'] = original
54
+ end
55
+
56
+ def ensure_log_goes_to_stdout
57
+ old_logger = ViteRails.logger
58
+ ViteRails.logger = ActiveSupport::Logger.new(STDOUT)
59
+ yield
60
+ ensure
61
+ ViteRails.logger = old_logger
62
+ end
63
+ end
64
+
65
+ # Public: Current instance configuration for Vite.
66
+ def config
67
+ @config ||= ViteRails::Config.resolve_config
68
+ end
69
+
70
+ # Public: Keeps track of watched files and triggers builds as needed.
71
+ def builder
72
+ @builder ||= ViteRails::Builder.new(self)
73
+ end
74
+
75
+ # Public: Allows to check if the Vite development server is running.
76
+ def dev_server
77
+ @dev_server ||= ViteRails::DevServer.new(config)
78
+ end
79
+
80
+ # Public: Enables looking up assets managed by Vite using name and type.
81
+ def manifest
82
+ @manifest ||= ViteRails::Manifest.new(self)
83
+ end
84
+
85
+ # Internal: Helper to run commands related with Vite.
86
+ def commands
87
+ @commands ||= ViteRails::Commands.new(self)
88
+ end
89
+ end
90
+
91
+ ViteRails::Engine if defined?(Rails)
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open3'
4
+ require 'digest/sha1'
5
+
6
+ # Public: Keeps track of watched files and triggers builds as needed.
7
+ class ViteRails::Builder
8
+ def initialize(vite_rails)
9
+ @vite_rails = vite_rails
10
+ end
11
+
12
+ # Public: Checks if the watched files have changed since the last compilation,
13
+ # and triggers a Vite build if any files have changed.
14
+ def build
15
+ if stale?
16
+ build_with_vite.tap { record_files_digest }
17
+ else
18
+ logger.debug 'Skipping build. Vite assets are already up-to-date ⚡️'
19
+ true
20
+ end
21
+ end
22
+
23
+ # Public: Returns true if all the assets built by Vite are up to date.
24
+ def fresh?
25
+ previous_files_digest&.== watched_files_digest
26
+ end
27
+
28
+ # Public: Returns true if any of the assets built by Vite is out of date.
29
+ def stale?
30
+ !fresh?
31
+ end
32
+
33
+ private
34
+
35
+ delegate :config, :logger, to: :@vite_rails
36
+
37
+ # Internal: Writes a digest of the watched files to disk for future checks.
38
+ def record_files_digest
39
+ config.build_cache_dir.mkpath
40
+ files_digest_path.write(watched_files_digest)
41
+ end
42
+
43
+ # Internal: The path of where a digest of the watched files is stored.
44
+ def files_digest_path
45
+ config.build_cache_dir.join("last-compilation-digest-#{ config.mode }")
46
+ end
47
+
48
+ # Internal: Reads a digest of watched files from disk.
49
+ def previous_files_digest
50
+ files_digest_path.read if files_digest_path.exist? && config.manifest_path.exist?
51
+ rescue Errno::ENOENT, Errno::ENOTDIR
52
+ end
53
+
54
+ # Internal: Returns a digest of all the watched files, allowing to detect
55
+ # changes, and skip Vite builds if no files have changed.
56
+ def watched_files_digest
57
+ Dir.chdir File.expand_path(config.root) do
58
+ files = Dir[*watched_paths].reject { |f| File.directory?(f) }
59
+ file_ids = files.sort.map { |f| "#{ File.basename(f) }/#{ Digest::SHA1.file(f).hexdigest }" }
60
+ Digest::SHA1.hexdigest(file_ids.join('/'))
61
+ end
62
+ end
63
+
64
+ # Public: Initiates a Vite build command to generate assets.
65
+ #
66
+ # Returns true if the build is successful, or false if it failed.
67
+ def build_with_vite
68
+ logger.info 'Building with Vite ⚡️'
69
+
70
+ stdout, stderr, status = Open3.capture3(vite_env,
71
+ "#{ which_ruby } ./bin/vite build --mode #{ config.mode }", chdir: File.expand_path(config.root))
72
+
73
+ if status.success?
74
+ logger.info "Build with Vite complete: #{ config.build_output_dir }"
75
+ logger.error(stderr.to_s) unless stderr.empty?
76
+ logger.info(stdout) unless config.hide_build_console_output
77
+ else
78
+ non_empty_streams = [stdout, stderr].delete_if(&:empty?)
79
+ logger.error "Build with Vite failed:\n#{ non_empty_streams.join("\n\n") }"
80
+ end
81
+
82
+ status.success?
83
+ end
84
+
85
+ # Internal: Used to prefix the bin/vite executable file.
86
+ def which_ruby
87
+ bin_vite_path = config.root.join('bin/vite')
88
+ first_line = File.readlines(bin_vite_path).first.chomp
89
+ /ruby/.match?(first_line) ? RbConfig.ruby : ''
90
+ end
91
+
92
+ # Internal: Files and directories that should be watched for changes.
93
+ #
94
+ # NOTE: You can specify additional ones in vite.json using "watchAdditionalPaths": [...]
95
+ def watched_paths
96
+ [
97
+ *config.watch_additional_paths,
98
+ "#{ config.source_code_dir }/**/*",
99
+ 'yarn.lock',
100
+ 'package.json',
101
+ config.config_path,
102
+ ].freeze
103
+ end
104
+
105
+ # Internal: Sets additional environment variables for vite-plugin-ruby.
106
+ def vite_env
107
+ ViteRails.env.merge(
108
+ "#{ ViteRails::ENV_PREFIX }_CONFIG_PATH" => config.config_path,
109
+ "#{ ViteRails::ENV_PREFIX }_MODE" => config.mode,
110
+ "#{ ViteRails::ENV_PREFIX }_ROOT" => config.root,
111
+ ).transform_values(&:to_s)
112
+ end
113
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Public: Encapsulates common tasks, available both programatically and in Rake.
4
+ class ViteRails::Commands
5
+ def initialize(vite_rails)
6
+ @vite_rails = vite_rails
7
+ end
8
+
9
+ # Public: Loads the manifest with all the entries compiled by Vite.
10
+ def bootstrap
11
+ manifest.refresh
12
+ end
13
+
14
+ # Public: Builds all assets that are managed by Vite, from the entrypoints.
15
+ def build
16
+ builder.build.tap { manifest.refresh }
17
+ end
18
+
19
+ # Public: Removes all build cache and previously compiled assets.
20
+ def clobber
21
+ config.build_output_dir.rmtree if config.build_output_dir.exist?
22
+ config.build_cache_dir.rmtree if config.build_cache_dir.exist?
23
+ end
24
+
25
+ # Public: Cleanup old assets in the output directory.
26
+ #
27
+ # keep_up_to - Max amount of backups to preserve.
28
+ # age_in_seconds - Amount of time to look back in order to preserve them.
29
+ #
30
+ # NOTE: By default keeps the last version, or 2 if created in the past hour.
31
+ #
32
+ # Examples:
33
+ # To force only 1 backup to be kept: clean(1, 0)
34
+ # To only keep files created within the last 10 minutes: clean(0, 600)
35
+ def clean(keep_up_to: 2, age_in_seconds: 3600)
36
+ return false unless config.build_output_dir.exist? && config.manifest_path.exist?
37
+
38
+ versions.sort.reverse
39
+ .each_with_index
40
+ .drop_while { |(mtime, _), index|
41
+ max_age = [0, Time.now - Time.at(mtime)].max
42
+ max_age < age_in_seconds || index < keep_up_to
43
+ }
44
+ .each do |(_, files), _index|
45
+ files.each do |file|
46
+ next unless File.file?(file)
47
+
48
+ File.delete(file)
49
+ logger.info("Removed #{ file }")
50
+ end
51
+ end
52
+ true
53
+ end
54
+
55
+ private
56
+
57
+ delegate :config, :builder, :manifest, :logger, to: :@vite_rails
58
+
59
+ def versions
60
+ all_files = Dir.glob("#{ config.build_output_dir }/**/*")
61
+ entries = all_files - [config.manifest_path] - current_version_files
62
+ entries.reject { |file| File.directory?(file) }.group_by { |file| File.mtime(file).utc.to_i }
63
+ end
64
+
65
+ def current_version_files
66
+ Dir.glob(manifest.refresh.values.map { |value| config.build_output_dir.join("#{ value['file'] }*") })
67
+ end
68
+ end