rails_vite 0.1.0.beta1
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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +22 -0
- data/LICENSE.txt +21 -0
- data/README.md +271 -0
- data/lib/generators/rails_vite/install/install_generator.rb +75 -0
- data/lib/generators/rails_vite/install/templates/Procfile.dev.tt +2 -0
- data/lib/generators/rails_vite/install/templates/bin/dev +23 -0
- data/lib/generators/rails_vite/install/templates/vite.config.ts.tt +8 -0
- data/lib/rails_vite/auto_build.rb +65 -0
- data/lib/rails_vite/config.rb +61 -0
- data/lib/rails_vite/engine.rb +20 -0
- data/lib/rails_vite/errors.rb +20 -0
- data/lib/rails_vite/manifest.rb +58 -0
- data/lib/rails_vite/tag_helper.rb +114 -0
- data/lib/rails_vite/tasks.rb +62 -0
- data/lib/rails_vite/version.rb +3 -0
- data/lib/rails_vite.rb +30 -0
- data/lib/tasks/rails_vite/build.rake +29 -0
- metadata +63 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: ddf9204a38a0fd3b2a771325dbdb49f94367b8e8e9577851a6346850c7efd499
|
|
4
|
+
data.tar.gz: 941bcd3a4321475a02a3ea735cd41a0e451cd71317c6fbd8a95daee1af011b8e
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: ec476f3602aeaab095dd8cfe2153944a382bd22ee12dec1fcbacc5b3dc0ccac8473c0c6ad24130431bb10a73c658a78e41795c69313809067ceabf02aa340770
|
|
7
|
+
data.tar.gz: f45dcd623a42524e171b98e87d3fca04b9f49b5a76435ffe8a5d56b0c1bf1f07feb84d71a709485e1f17e33bab2ca77d9c4a34c0d35344554176ec220a316b78
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog],
|
|
6
|
+
and this project adheres to [Semantic Versioning].
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0] - 2026-03-07
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Initial release ([@skryukov])
|
|
15
|
+
|
|
16
|
+
[@skryukov]: https://github.com/skryukov
|
|
17
|
+
|
|
18
|
+
[Unreleased]: https://github.com/skryukov/rails_vite/compare/v0.1.0...HEAD
|
|
19
|
+
[0.1.0]: https://github.com/skryukov/rails_vite/commits/v0.1.0
|
|
20
|
+
|
|
21
|
+
[Keep a Changelog]: https://keepachangelog.com/en/1.0.0/
|
|
22
|
+
[Semantic Versioning]: https://semver.org/spec/v2.0.0.html
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Svyatoslav Kryukov
|
|
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.
|
data/README.md
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
# RailsVite
|
|
2
|
+
|
|
3
|
+
Vite integration for Rails, inspired by [Laravel's Vite plugin](https://laravel.com/docs/12.x/vite). No proxy, no config duplication, no magic.
|
|
4
|
+
|
|
5
|
+
## How It Works
|
|
6
|
+
|
|
7
|
+
**Development:** The Vite plugin writes `tmp/rails-vite.json` with the dev server URL. The Rails helper reads it and emits `<script>` tags pointing directly at Vite. The browser talks to Vite — Puma never touches your assets.
|
|
8
|
+
|
|
9
|
+
**Production:** `vite build` outputs fingerprinted assets to `public/vite/` with a standard Vite manifest. The Rails helper reads the manifest and emits the correct tags.
|
|
10
|
+
|
|
11
|
+
No Rack proxy. No `config/vite.json`. No version-locked packages.
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
Add to your Gemfile:
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
gem "rails_vite"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Run the install generator:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
bundle install
|
|
25
|
+
bin/rails generate rails_vite:install
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
This creates `vite.config.ts`, installs dependencies, and updates your layout.
|
|
29
|
+
|
|
30
|
+
Start development:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
bin/dev
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Usage
|
|
37
|
+
|
|
38
|
+
In your layout:
|
|
39
|
+
|
|
40
|
+
```erb
|
|
41
|
+
<%= vite_tags "application.js" %>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Short names are automatically prefixed with `sourceDir` (default: `app/javascript`). Paths containing `/` are used as-is.
|
|
45
|
+
|
|
46
|
+
**Development output** (when `tmp/rails-vite.json` exists):
|
|
47
|
+
|
|
48
|
+
```html
|
|
49
|
+
<script src="http://localhost:5173/@vite/client" type="module"></script>
|
|
50
|
+
<script src="http://localhost:5173/app/javascript/application.js" type="module"></script>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Production output** (reads manifest):
|
|
54
|
+
|
|
55
|
+
```html
|
|
56
|
+
<link rel="modulepreload" href="/vite/assets/vendor-b3c4d5e6.js" />
|
|
57
|
+
<script src="/vite/assets/application-a1b2c3d4.js" type="module"></script>
|
|
58
|
+
<link rel="stylesheet" href="/vite/assets/application-x9y8z7w6.css" />
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Helpers
|
|
62
|
+
|
|
63
|
+
| Helper | Purpose |
|
|
64
|
+
|--------|---------|
|
|
65
|
+
| `vite_tags(*entries, nonce: nil)` | Emits script, stylesheet, and modulepreload tags |
|
|
66
|
+
| `vite_asset_path(name)` | Returns the fingerprinted path from the manifest |
|
|
67
|
+
| `vite_image_tag(name, **options)` | Image tag with manifest-resolved src |
|
|
68
|
+
|
|
69
|
+
### CSS Entry Points
|
|
70
|
+
|
|
71
|
+
CSS files are detected by extension and emit `<link rel="stylesheet">`:
|
|
72
|
+
|
|
73
|
+
```erb
|
|
74
|
+
<%= vite_tags "application.css" %>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### CSP Nonces
|
|
78
|
+
|
|
79
|
+
```erb
|
|
80
|
+
<%= vite_tags "application.js", nonce: content_security_policy_nonce %>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Asset Discovery (Images, Fonts)
|
|
84
|
+
|
|
85
|
+
Use `import.meta.glob` in your entry point to include assets in the Vite manifest:
|
|
86
|
+
|
|
87
|
+
```js
|
|
88
|
+
// app/javascript/application.js
|
|
89
|
+
import.meta.glob(['../assets/images/**']);
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Then reference them in views:
|
|
93
|
+
|
|
94
|
+
```erb
|
|
95
|
+
<%= vite_image_tag "app/assets/images/logo.png", alt: "Logo" %>
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Vite Config
|
|
99
|
+
|
|
100
|
+
The install generator creates a minimal `vite.config.ts`:
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
import { defineConfig } from 'vite';
|
|
104
|
+
import rails from 'rails-vite-plugin';
|
|
105
|
+
|
|
106
|
+
export default defineConfig({
|
|
107
|
+
plugins: [
|
|
108
|
+
rails(),
|
|
109
|
+
],
|
|
110
|
+
});
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Plugin Options
|
|
114
|
+
|
|
115
|
+
| Option | Default | Description |
|
|
116
|
+
|--------|---------|-------------|
|
|
117
|
+
| `input` | auto-detected | Entry point(s). Auto-detects `application.{js,ts,jsx,tsx}` in `sourceDir` |
|
|
118
|
+
| `sourceDir` | `'app/javascript'` | Source directory. Short names are prefixed with this. Also sets the `@` import alias |
|
|
119
|
+
| `ssr` | — | SSR entry point |
|
|
120
|
+
| `ssrOutputDirectory` | `'ssr'` | SSR output directory |
|
|
121
|
+
| `devMetaFile` | `'tmp/rails-vite.json'` | Dev metadata file path |
|
|
122
|
+
| `buildDirectory` | `'vite'` | Build output subdirectory inside `public/` |
|
|
123
|
+
| `publicDirectory` | `'public'` | Public directory |
|
|
124
|
+
| `refresh` | `true` | Paths to watch for full-page reload. `true` watches `app/views/**` and `app/helpers/**` |
|
|
125
|
+
|
|
126
|
+
### Multiple Entry Points
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
rails({
|
|
130
|
+
input: ['application.js', 'admin.js'],
|
|
131
|
+
})
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
```erb
|
|
135
|
+
<!-- In application layout -->
|
|
136
|
+
<%= vite_tags "application.js" %>
|
|
137
|
+
|
|
138
|
+
<!-- In admin layout -->
|
|
139
|
+
<%= vite_tags "admin.js" %>
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Custom Source Directory
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
rails({
|
|
146
|
+
input: ['entrypoints/application.ts', 'entrypoints/admin.ts'],
|
|
147
|
+
sourceDir: 'app/frontend',
|
|
148
|
+
})
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
```erb
|
|
152
|
+
<%= vite_tags "entrypoints/application.ts" %>
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Adding Frameworks
|
|
156
|
+
|
|
157
|
+
### React
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
npm install -D @vitejs/plugin-react
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
import { defineConfig } from 'vite';
|
|
165
|
+
import rails from 'rails-vite-plugin';
|
|
166
|
+
import react from '@vitejs/plugin-react';
|
|
167
|
+
|
|
168
|
+
export default defineConfig({
|
|
169
|
+
plugins: [
|
|
170
|
+
react(),
|
|
171
|
+
rails(),
|
|
172
|
+
],
|
|
173
|
+
});
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
The React Refresh preamble is injected automatically when `@vitejs/plugin-react` is detected — no manual setup needed.
|
|
177
|
+
|
|
178
|
+
### Vue
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
npm install -D @vitejs/plugin-vue
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
import { defineConfig } from 'vite';
|
|
186
|
+
import rails from 'rails-vite-plugin';
|
|
187
|
+
import vue from '@vitejs/plugin-vue';
|
|
188
|
+
|
|
189
|
+
export default defineConfig({
|
|
190
|
+
plugins: [
|
|
191
|
+
vue(),
|
|
192
|
+
rails(),
|
|
193
|
+
],
|
|
194
|
+
});
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## SSR
|
|
198
|
+
|
|
199
|
+
Set `ssr` to the entry point used for server-side rendering. When you run `npx vite build --ssr`, the plugin uses this as the input and outputs to the `ssrOutputDirectory` (default: `ssr/`).
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
rails({
|
|
203
|
+
ssr: 'ssr.tsx',
|
|
204
|
+
})
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Build and run:
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
npx vite build && npx vite build --ssr
|
|
211
|
+
node ssr/ssr.js
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## Auto Build
|
|
215
|
+
|
|
216
|
+
When the Vite dev server is not running, rails_vite automatically rebuilds assets on the first request if sources have changed. This is useful for system tests and quick checks without running `bin/dev`.
|
|
217
|
+
|
|
218
|
+
Disable it:
|
|
219
|
+
|
|
220
|
+
```ruby
|
|
221
|
+
# config/initializers/rails_vite.rb
|
|
222
|
+
Rails.application.config.rails_vite.auto_build = false
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
By default, auto build is enabled in development and test (`Rails.env.local?`).
|
|
226
|
+
|
|
227
|
+
Note: for parallel test runners, disable auto build and use `rake vite:build` before the suite instead.
|
|
228
|
+
|
|
229
|
+
## Testing the Build
|
|
230
|
+
|
|
231
|
+
To verify your production build works in development:
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
rake vite:build # build assets
|
|
235
|
+
bin/rails s # start Rails without Vite dev server
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Without the Vite dev server running (no `tmp/rails-vite.json`), Rails serves built assets from `public/vite/`. To switch back to dev mode, start Vite again — the dev metadata takes priority.
|
|
239
|
+
|
|
240
|
+
Clean up built assets with `rake vite:clobber`.
|
|
241
|
+
|
|
242
|
+
## Custom Paths
|
|
243
|
+
|
|
244
|
+
If you override `build.outDir` in `vite.config.ts`, tell the gem where to find things:
|
|
245
|
+
|
|
246
|
+
```ruby
|
|
247
|
+
# config/initializers/rails_vite.rb
|
|
248
|
+
Rails.application.config.rails_vite.manifest_path = Rails.root.join("public/custom/manifest.json")
|
|
249
|
+
Rails.application.config.rails_vite.asset_prefix = "/custom"
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Defaults match the plugin defaults — no config needed if you follow conventions.
|
|
253
|
+
|
|
254
|
+
## Rake Tasks
|
|
255
|
+
|
|
256
|
+
| Task | Description |
|
|
257
|
+
|------|-------------|
|
|
258
|
+
| `rake vite:build` | Build assets for production |
|
|
259
|
+
| `rake vite:install` | Install JavaScript dependencies |
|
|
260
|
+
| `rake vite:clobber` | Remove `public/vite/` |
|
|
261
|
+
|
|
262
|
+
`vite:build` hooks into `assets:precompile` and `test:prepare` automatically. Skip with `SKIP_VITE_BUILD=1`.
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
## Contributing
|
|
266
|
+
|
|
267
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/skryukov/rails_vite.
|
|
268
|
+
|
|
269
|
+
## License
|
|
270
|
+
|
|
271
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
require "rails/generators"
|
|
2
|
+
|
|
3
|
+
module RailsVite
|
|
4
|
+
module Generators
|
|
5
|
+
class InstallGenerator < Rails::Generators::Base
|
|
6
|
+
source_root File.expand_path("templates", __dir__)
|
|
7
|
+
|
|
8
|
+
def install_dependencies
|
|
9
|
+
say "Installing Vite and rails-vite-plugin..."
|
|
10
|
+
run RailsVite::Tasks.add_command("vite", "rails-vite-plugin")
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def create_vite_config
|
|
14
|
+
template "vite.config.ts.tt", "vite.config.ts"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def create_entrypoint
|
|
18
|
+
unless File.exist?("app/javascript/application.js")
|
|
19
|
+
create_file "app/javascript/application.js", "// Entry point for Vite\n"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def update_gitignore
|
|
24
|
+
return if File.read(".gitignore").include?("public/vite")
|
|
25
|
+
|
|
26
|
+
append_to_file ".gitignore", <<~GITIGNORE
|
|
27
|
+
|
|
28
|
+
# Vite
|
|
29
|
+
/public/vite
|
|
30
|
+
GITIGNORE
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def update_layout
|
|
34
|
+
layout_path = "app/views/layouts/application.html.erb"
|
|
35
|
+
return unless File.exist?(layout_path)
|
|
36
|
+
|
|
37
|
+
gsub_file layout_path,
|
|
38
|
+
/<%=\s*javascript_include_tag\s+["']application["'].*%>/,
|
|
39
|
+
'<%= vite_tags "application" %>'
|
|
40
|
+
|
|
41
|
+
gsub_file layout_path,
|
|
42
|
+
/<%=\s*stylesheet_link_tag\s+["']application["'].*%>/,
|
|
43
|
+
'<%= vite_tags "application.css" %>'
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def setup_procfile
|
|
47
|
+
if File.exist?("Procfile.dev")
|
|
48
|
+
unless File.read("Procfile.dev").include?("vite")
|
|
49
|
+
append_to_file "Procfile.dev", "js: #{vite_dev_command}\n"
|
|
50
|
+
end
|
|
51
|
+
else
|
|
52
|
+
template "Procfile.dev.tt", "Procfile.dev"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def setup_bin_dev
|
|
57
|
+
return if File.exist?("bin/dev")
|
|
58
|
+
|
|
59
|
+
copy_file "bin/dev", "bin/dev"
|
|
60
|
+
chmod "bin/dev", 0o755
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def done
|
|
64
|
+
say ""
|
|
65
|
+
say "Vite installed! Run `bin/dev` to start development.", :green
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def vite_dev_command
|
|
71
|
+
RailsVite::Tasks.dev_command
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
|
|
3
|
+
export PORT="${PORT:-3000}"
|
|
4
|
+
|
|
5
|
+
if command -v overmind 1> /dev/null 2>&1
|
|
6
|
+
then
|
|
7
|
+
overmind start -f Procfile.dev "$@"
|
|
8
|
+
exit $?
|
|
9
|
+
fi
|
|
10
|
+
|
|
11
|
+
if command -v hivemind 1> /dev/null 2>&1
|
|
12
|
+
then
|
|
13
|
+
echo "Hivemind is installed. Running the application with Hivemind..."
|
|
14
|
+
exec hivemind Procfile.dev "$@"
|
|
15
|
+
exit $?
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
if gem list --no-installed --exact --silent foreman; then
|
|
19
|
+
echo "Installing foreman..."
|
|
20
|
+
gem install foreman
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
foreman start -f Procfile.dev "$@"
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
require "find"
|
|
2
|
+
|
|
3
|
+
module RailsVite
|
|
4
|
+
class AutoBuild
|
|
5
|
+
NEVER = Time.at(0).freeze
|
|
6
|
+
SOURCE_CHECK_INTERVAL = 2 # seconds
|
|
7
|
+
|
|
8
|
+
def initialize(app, config)
|
|
9
|
+
@app = app
|
|
10
|
+
@config = config
|
|
11
|
+
@mutex = Mutex.new
|
|
12
|
+
@last_build_at = NEVER
|
|
13
|
+
@cached_source_mtime = nil
|
|
14
|
+
@source_mtime_checked_at = nil
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def call(env)
|
|
18
|
+
build! if stale?
|
|
19
|
+
@app.call(env)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def build!
|
|
25
|
+
@mutex.synchronize do
|
|
26
|
+
return unless stale?
|
|
27
|
+
|
|
28
|
+
Rails.logger.error("rails-vite: build failed") unless system(RailsVite::Tasks.build_command)
|
|
29
|
+
@last_build_at = Time.now
|
|
30
|
+
@cached_source_mtime = nil
|
|
31
|
+
@source_mtime_checked_at = nil
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def stale?
|
|
36
|
+
!File.exist?(@config.manifest_path) ||
|
|
37
|
+
latest_source_mtime > @last_build_at
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def latest_source_mtime
|
|
41
|
+
now = Time.now
|
|
42
|
+
if @cached_source_mtime && @source_mtime_checked_at && (now - @source_mtime_checked_at) < SOURCE_CHECK_INTERVAL
|
|
43
|
+
return @cached_source_mtime
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
@source_mtime_checked_at = now
|
|
47
|
+
@cached_source_mtime = compute_latest_source_mtime
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def compute_latest_source_mtime
|
|
51
|
+
latest = NEVER
|
|
52
|
+
source_path = Rails.root.join(@config.source_dir).to_s
|
|
53
|
+
|
|
54
|
+
Find.find(source_path) do |path|
|
|
55
|
+
next unless File.file?(path)
|
|
56
|
+
mtime = File.mtime(path)
|
|
57
|
+
latest = mtime if mtime > latest
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
latest
|
|
61
|
+
rescue Errno::ENOENT
|
|
62
|
+
Time.now # source dir missing — trigger build to surface the error
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
module RailsVite
|
|
2
|
+
class Config
|
|
3
|
+
META_FILENAME = "rails-vite.json"
|
|
4
|
+
|
|
5
|
+
attr_writer :dev_meta_path, :manifest_path, :asset_prefix, :auto_build
|
|
6
|
+
|
|
7
|
+
def dev_meta_path
|
|
8
|
+
@dev_meta_path || Rails.root.join("tmp", META_FILENAME)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def manifest_path
|
|
12
|
+
@manifest_path || Rails.root.join("public/vite/manifest.json")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def asset_prefix
|
|
16
|
+
@asset_prefix || "/vite"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def source_dir
|
|
20
|
+
plugin_meta["sourceDir"] || "app/javascript"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def auto_build?
|
|
24
|
+
return @auto_build if defined?(@auto_build)
|
|
25
|
+
Rails.env.local?
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def dev_server_running?
|
|
29
|
+
!!dev_server_url
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def dev_server_url
|
|
33
|
+
plugin_meta["url"]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def react_refresh?
|
|
37
|
+
plugin_meta["reactRefresh"] == true
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def plugin_meta
|
|
43
|
+
if Rails.env.local?
|
|
44
|
+
load_plugin_meta
|
|
45
|
+
else
|
|
46
|
+
@plugin_meta ||= load_plugin_meta
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def load_plugin_meta
|
|
51
|
+
JSON.parse(dev_meta_path.read)
|
|
52
|
+
rescue Errno::ENOENT
|
|
53
|
+
build_meta_path = manifest_path.dirname.join(META_FILENAME)
|
|
54
|
+
begin
|
|
55
|
+
JSON.parse(build_meta_path.read)
|
|
56
|
+
rescue Errno::ENOENT, JSON::ParserError
|
|
57
|
+
{}
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module RailsVite
|
|
2
|
+
class Engine < ::Rails::Engine
|
|
3
|
+
initializer "rails_vite.config", before: :load_config_initializers do |app|
|
|
4
|
+
app.config.rails_vite = RailsVite.config
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
initializer "rails_vite.auto_build", after: :load_config_initializers do |app|
|
|
8
|
+
vite_config = RailsVite.config
|
|
9
|
+
if vite_config.auto_build? && !vite_config.dev_server_running?
|
|
10
|
+
app.middleware.use RailsVite::AutoBuild, vite_config
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
initializer "rails_vite.helpers" do
|
|
15
|
+
ActiveSupport.on_load(:action_view) do
|
|
16
|
+
include RailsVite::TagHelper
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module RailsVite
|
|
2
|
+
class Error < StandardError; end
|
|
3
|
+
|
|
4
|
+
class MissingManifestError < Error
|
|
5
|
+
def initialize(path)
|
|
6
|
+
message = if Rails.env.local?
|
|
7
|
+
"Vite manifest not found at #{path}. Start the Vite dev server with `bin/dev` or precompile assets with `rake vite:build`."
|
|
8
|
+
else
|
|
9
|
+
"Vite manifest not found at #{path}. Run `rake vite:build` to compile assets."
|
|
10
|
+
end
|
|
11
|
+
super(message)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
class MissingEntryError < Error
|
|
16
|
+
def initialize(name, path)
|
|
17
|
+
super("Entry \"#{name}\" not found in Vite manifest at #{path}.")
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
module RailsVite
|
|
2
|
+
class Manifest
|
|
3
|
+
NO_MANIFEST_DIGEST = "no-manifest"
|
|
4
|
+
|
|
5
|
+
def initialize(path)
|
|
6
|
+
@path = path
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def lookup(name)
|
|
10
|
+
manifest = data
|
|
11
|
+
entry = manifest[name] || raise(MissingEntryError.new(name, @path))
|
|
12
|
+
|
|
13
|
+
{
|
|
14
|
+
file: entry["file"],
|
|
15
|
+
css: entry.fetch("css", []),
|
|
16
|
+
imports: resolve_imports(entry, Set.new, manifest)
|
|
17
|
+
}
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def path_for(name)
|
|
21
|
+
lookup(name)[:file]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def digest
|
|
25
|
+
Digest::MD5.file(@path).hexdigest
|
|
26
|
+
rescue Errno::ENOENT
|
|
27
|
+
NO_MANIFEST_DIGEST
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def data
|
|
33
|
+
if Rails.env.local?
|
|
34
|
+
load_manifest
|
|
35
|
+
else
|
|
36
|
+
@data ||= load_manifest
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def load_manifest
|
|
41
|
+
JSON.parse(File.read(@path))
|
|
42
|
+
rescue Errno::ENOENT
|
|
43
|
+
raise MissingManifestError.new(@path)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def resolve_imports(entry, seen, manifest)
|
|
47
|
+
entry.fetch("imports", []).flat_map do |import_key|
|
|
48
|
+
next [] if seen.include?(import_key)
|
|
49
|
+
seen.add(import_key)
|
|
50
|
+
|
|
51
|
+
imported = manifest[import_key]
|
|
52
|
+
next [] unless imported
|
|
53
|
+
|
|
54
|
+
[imported["file"]] + resolve_imports(imported, seen, manifest)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
module RailsVite
|
|
2
|
+
module TagHelper
|
|
3
|
+
CSS_EXTENSIONS = /\.(?:css|scss|sass|less|styl|pcss)\z/
|
|
4
|
+
|
|
5
|
+
def vite_tags(*entries, nonce: nil, **options)
|
|
6
|
+
config = RailsVite.config
|
|
7
|
+
resolved = entries.map { |e| resolve_vite_entry(e, config.source_dir) }
|
|
8
|
+
|
|
9
|
+
if config.dev_server_running?
|
|
10
|
+
vite_dev_tags(resolved, config.dev_server_url, nonce: nonce)
|
|
11
|
+
else
|
|
12
|
+
vite_prod_tags(resolved, nonce: nonce)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def vite_asset_path(name)
|
|
17
|
+
vite_asset_url(RailsVite.manifest.path_for(name))
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def vite_image_tag(name, **options)
|
|
21
|
+
image_tag(vite_asset_path(name), **options)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def vite_dev_tags(entries, dev_url, nonce: nil)
|
|
27
|
+
tags = []
|
|
28
|
+
|
|
29
|
+
unless @_vite_client_emitted
|
|
30
|
+
if RailsVite.config.react_refresh?
|
|
31
|
+
tags << tag.script(type: "module", nonce: nonce) {
|
|
32
|
+
<<~JS.squish.html_safe
|
|
33
|
+
import{injectIntoGlobalHook}from'#{dev_url}/@react-refresh';
|
|
34
|
+
injectIntoGlobalHook(window);
|
|
35
|
+
window.$RefreshReg$=()=>{};
|
|
36
|
+
window.$RefreshSig$=()=>(type)=>type;
|
|
37
|
+
JS
|
|
38
|
+
}
|
|
39
|
+
end
|
|
40
|
+
tags << tag.script(src: "#{dev_url}/@vite/client", type: "module", nonce: nonce)
|
|
41
|
+
@_vite_client_emitted = true
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
entries.each do |entry|
|
|
45
|
+
tags << build_asset_tag(entry, "#{dev_url}/#{entry}", nonce: nonce)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
safe_join(tags, "\n")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def vite_prod_tags(entries, nonce: nil)
|
|
52
|
+
tags = []
|
|
53
|
+
preloaded = Set.new
|
|
54
|
+
|
|
55
|
+
entries.each do |entry|
|
|
56
|
+
result = RailsVite.manifest.lookup(entry)
|
|
57
|
+
|
|
58
|
+
Array(result[:imports]).each do |import_file|
|
|
59
|
+
next if preloaded.include?(import_file)
|
|
60
|
+
preloaded.add(import_file)
|
|
61
|
+
tags << tag.link(rel: "modulepreload", href: vite_asset_url(import_file), nonce: nonce)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
tags << build_asset_tag(entry, vite_asset_url(result[:file]), nonce: nonce)
|
|
65
|
+
|
|
66
|
+
Array(result[:css]).each do |css_file|
|
|
67
|
+
tags << tag.link(rel: "stylesheet", href: vite_asset_url(css_file), nonce: nonce)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
safe_join(tags, "\n")
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def build_asset_tag(entry, url, nonce: nil)
|
|
75
|
+
if css_entry?(entry)
|
|
76
|
+
tag.link(rel: "stylesheet", href: url, nonce: nonce)
|
|
77
|
+
else
|
|
78
|
+
tag.script(src: url, type: "module", nonce: nonce)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def resolve_vite_entry(entry, source_dir)
|
|
83
|
+
entry.start_with?("#{source_dir}/", "/") ? entry : "#{source_dir}/#{entry}"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def vite_asset_url(file)
|
|
87
|
+
vite_prefix_asset_host("#{RailsVite.config.asset_prefix}/#{file}")
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def css_entry?(entry)
|
|
91
|
+
CSS_EXTENSIONS.match?(entry)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def vite_prefix_asset_host(path)
|
|
95
|
+
host = vite_resolve_asset_host
|
|
96
|
+
host ? "#{host}#{path}" : path
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Memoized per view instance (one per request). Safe for Proc hosts
|
|
100
|
+
# since each request gets a fresh ActionView::Base instance.
|
|
101
|
+
def vite_resolve_asset_host
|
|
102
|
+
return @_vite_asset_host if defined?(@_vite_asset_host)
|
|
103
|
+
|
|
104
|
+
@_vite_asset_host = begin
|
|
105
|
+
case (asset_host = Rails.application.config.action_controller.asset_host)
|
|
106
|
+
when String then asset_host.chomp("/")
|
|
107
|
+
when Proc then asset_host.call("")&.chomp("/")
|
|
108
|
+
end
|
|
109
|
+
rescue NoMethodError
|
|
110
|
+
nil
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
module RailsVite
|
|
2
|
+
module Tasks
|
|
3
|
+
extend self
|
|
4
|
+
|
|
5
|
+
BUN_CMD = defined?(Bundlebun) ? Bundlebun::Runner.binstub_or_binary_path : "bun"
|
|
6
|
+
|
|
7
|
+
COMMANDS = {
|
|
8
|
+
bun: {install: "#{BUN_CMD} install", add: "#{BUN_CMD} add -D", dev: "#{BUN_CMD} run vite", build: "#{BUN_CMD} run vite build"},
|
|
9
|
+
yarn: {install: "yarn install", add: "yarn add -D", dev: "yarn vite", build: "yarn vite build"},
|
|
10
|
+
pnpm: {install: "pnpm install", add: "pnpm add -D", dev: "pnpm vite", build: "pnpm vite build"},
|
|
11
|
+
npm: {install: "npm install", add: "npm install -D", dev: "npx vite", build: "npx vite build"}
|
|
12
|
+
}.freeze
|
|
13
|
+
|
|
14
|
+
LOCKFILES = {
|
|
15
|
+
bun: %w[bun.lockb bun.lock],
|
|
16
|
+
yarn: %w[yarn.lock],
|
|
17
|
+
pnpm: %w[pnpm-lock.yaml],
|
|
18
|
+
npm: %w[package-lock.json]
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
def install_command
|
|
22
|
+
command_for(:install)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def add_command(*packages)
|
|
26
|
+
"#{command_for(:add)} #{packages.join(" ")}"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def dev_command
|
|
30
|
+
command_for(:dev)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def build_command
|
|
34
|
+
command_for(:build)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def tool
|
|
38
|
+
tool_determined_by_lockfile || tool_determined_by_executable
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def command_for(key)
|
|
44
|
+
COMMANDS.dig(tool, key) ||
|
|
45
|
+
raise("rails_vite: No suitable JS package manager found for '#{key}'. Ensure npm, yarn, pnpm, or bun is available.")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def tool_determined_by_lockfile
|
|
49
|
+
LOCKFILES.each do |tool_name, files|
|
|
50
|
+
return tool_name if files.any? { |f| File.exist?(f) }
|
|
51
|
+
end
|
|
52
|
+
nil
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def tool_determined_by_executable
|
|
56
|
+
COMMANDS.each_key do |exe|
|
|
57
|
+
return exe if system "command -v #{exe} > /dev/null"
|
|
58
|
+
end
|
|
59
|
+
nil
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
data/lib/rails_vite.rb
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "digest"
|
|
3
|
+
|
|
4
|
+
require_relative "rails_vite/errors"
|
|
5
|
+
require_relative "rails_vite/manifest"
|
|
6
|
+
require_relative "rails_vite/config"
|
|
7
|
+
require_relative "rails_vite/tasks"
|
|
8
|
+
require_relative "rails_vite/tag_helper"
|
|
9
|
+
require_relative "rails_vite/version"
|
|
10
|
+
|
|
11
|
+
module RailsVite
|
|
12
|
+
class << self
|
|
13
|
+
def config
|
|
14
|
+
@config ||= Config.new
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def manifest
|
|
18
|
+
@manifest ||= Manifest.new(config.manifest_path)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def digest
|
|
22
|
+
manifest.digest
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def reset!
|
|
26
|
+
@config = nil
|
|
27
|
+
@manifest = nil
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
namespace :vite do
|
|
2
|
+
desc "Install JavaScript dependencies"
|
|
3
|
+
task :install do
|
|
4
|
+
command = RailsVite::Tasks.install_command
|
|
5
|
+
system(command) || raise("rails_vite: Command install failed, ensure #{command.split.first} is installed")
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
desc "Build Vite assets for production"
|
|
9
|
+
task :build do
|
|
10
|
+
command = RailsVite::Tasks.build_command
|
|
11
|
+
system(command) || raise("rails_vite: Command build failed, ensure `#{command}` runs without errors")
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
Rake::Task["vite:build"].prereqs << :install unless ENV["SKIP_VITE_INSTALL"]
|
|
15
|
+
|
|
16
|
+
desc "Remove Vite build artifacts"
|
|
17
|
+
task :clobber do
|
|
18
|
+
rm_rf Rails.root.join("public", RailsVite.config.asset_prefix.delete_prefix("/"))
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
unless ENV["SKIP_VITE_BUILD"]
|
|
23
|
+
%w[assets:precompile test:prepare spec:prepare db:test:prepare].each do |t|
|
|
24
|
+
Rake::Task[t].enhance(["vite:build"]) if Rake::Task.task_defined?(t)
|
|
25
|
+
break if %w[test:prepare spec:prepare].include?(t) && Rake::Task.task_defined?(t)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
Rake::Task["assets:clobber"].enhance(["vite:clobber"]) if Rake::Task.task_defined?("assets:clobber")
|
|
29
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: rails_vite
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0.beta1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Svyatoslav Kryukov
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: Simple Vite integration for Rails, inspired by Laravel. No proxy, no
|
|
13
|
+
config duplication.
|
|
14
|
+
email:
|
|
15
|
+
- me@skryukov.dev
|
|
16
|
+
executables: []
|
|
17
|
+
extensions: []
|
|
18
|
+
extra_rdoc_files: []
|
|
19
|
+
files:
|
|
20
|
+
- CHANGELOG.md
|
|
21
|
+
- LICENSE.txt
|
|
22
|
+
- README.md
|
|
23
|
+
- lib/generators/rails_vite/install/install_generator.rb
|
|
24
|
+
- lib/generators/rails_vite/install/templates/Procfile.dev.tt
|
|
25
|
+
- lib/generators/rails_vite/install/templates/bin/dev
|
|
26
|
+
- lib/generators/rails_vite/install/templates/vite.config.ts.tt
|
|
27
|
+
- lib/rails_vite.rb
|
|
28
|
+
- lib/rails_vite/auto_build.rb
|
|
29
|
+
- lib/rails_vite/config.rb
|
|
30
|
+
- lib/rails_vite/engine.rb
|
|
31
|
+
- lib/rails_vite/errors.rb
|
|
32
|
+
- lib/rails_vite/manifest.rb
|
|
33
|
+
- lib/rails_vite/tag_helper.rb
|
|
34
|
+
- lib/rails_vite/tasks.rb
|
|
35
|
+
- lib/rails_vite/version.rb
|
|
36
|
+
- lib/tasks/rails_vite/build.rake
|
|
37
|
+
homepage: https://github.com/skryukov/rails_vite
|
|
38
|
+
licenses:
|
|
39
|
+
- MIT
|
|
40
|
+
metadata:
|
|
41
|
+
bug_tracker_uri: https://github.com/skryukov/rails_vite/issues
|
|
42
|
+
changelog_uri: https://github.com/skryukov/rails_vite/blob/main/CHANGELOG.md
|
|
43
|
+
documentation_uri: https://github.com/skryukov/rails_vite/blob/main/README.md
|
|
44
|
+
homepage_uri: https://github.com/skryukov/rails_vite
|
|
45
|
+
rubygems_mfa_required: 'true'
|
|
46
|
+
rdoc_options: []
|
|
47
|
+
require_paths:
|
|
48
|
+
- lib
|
|
49
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '3.1'
|
|
54
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
55
|
+
requirements:
|
|
56
|
+
- - ">="
|
|
57
|
+
- !ruby/object:Gem::Version
|
|
58
|
+
version: '0'
|
|
59
|
+
requirements: []
|
|
60
|
+
rubygems_version: 4.0.3
|
|
61
|
+
specification_version: 4
|
|
62
|
+
summary: Vite integration for Rails
|
|
63
|
+
test_files: []
|