rails_vite 0.2.3 → 0.3.0.rc1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 87116a45bfd905d7f8e598b6dbf486f7206f39bc62ab39d9a0d43450f2ed2a28
4
- data.tar.gz: f5b4ca52c2c10a2ad14da1bb46b3b43a2f566bf706e1063154d7056e09c16c31
3
+ metadata.gz: 8e7210773488e0deadb365f2fb2ab6782d1b347d77ca2f63007eada5cbe6c59f
4
+ data.tar.gz: 687c142b64c7eb24431696f56c105be88b06c68686689d40005f773f8d70aebf
5
5
  SHA512:
6
- metadata.gz: 206278e4c87c73d4c1529db69778c751b809d904f066f35ba23c06afa127dcdceac52c0fb0e1aba2d35ec5dfdd17d251ae62bca82ab4e7bbd503766a951d509d
7
- data.tar.gz: 1759ef4e320790b3090c18c3476dcf1c253e30a910de32230f80fd5c78460d27f5b6f752812053571c4209b5d1a8724590c6ee26702e79abda2035c560e29074
6
+ metadata.gz: d4a84d843acb2feb2c9f80d26386b5071ca8c4a8093332d8e71880cddbbf608351709f30fb7cef147b25f7114818eb2b09b8d35ea0808affdf19b67594574446
7
+ data.tar.gz: e483f34f03fed33786db337c84b5cb290e7ce2ad65e3c73b7294f5e75b77aabffe934dac00b2cb55d79517b65181b699b43f7c4ef9feedadf2ea1190ec1781a6
data/CHANGELOG.md CHANGED
@@ -5,6 +5,36 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog],
6
6
  and this project adheres to [Semantic Versioning].
7
7
 
8
+ ## rails_vite@0.3.0.rc1 / rails-vite-plugin@0.3.0-rc.1 - 2026-10-11
9
+
10
+ ### Added
11
+
12
+ - `refreshDelay` plugin option for `rails()` and `jsbundling()`. It waits the given milliseconds after the last `refresh` change before the full-page reload, and sends one reload for a burst of changes. Use it when Rails sees template changes late, e.g. with `EventedFileUpdateChecker`. Default: `0`, which reloads at once as before (#45) ([@olivier-thatch])
13
+ - The install generator sets `"type": "module"` in package.json — Vite and rails-vite-plugin are ESM-only, and without it Node fails to load `vite.config.ts` under npm/pnpm/yarn. When package.json pins another `type`, the generator emits `vite.config.mts` instead (#38) ([@skryukov])
14
+ - [aube](https://github.com/aubepkg/aube) package manager support: an `aube-lock.yaml` makes the rake tasks, auto build and the install generator use aube, even next to another lockfile left over from `aube import` (#41) ([@beauraF])
15
+ - `config.rails_vite.vite_executable` sets the executable for `rake vite:build`, test builds and auto builds, so Vite-compatible CLIs with another name, such as Vite+'s `vp`, work. The default is `vite` (#42) ([@cole-robertson])
16
+ - `auto_build_paths` config option: extra paths and globs, relative to `Rails.root`, that auto build checks for changes besides `sourceDir` (for example `app/views` for Tailwind). It defaults to the root-level Vite, PostCSS, Tailwind and TypeScript configs, `package.json`, and the lockfile, so changes to them now trigger a rebuild (#46) ([@olivier-thatch])
17
+ - `config.rails_vite.build_mode` sets the `--mode` for `vite build` in `rake vite:build` and auto builds. The default is unchanged (`"test"` in the test environment, no `--mode` elsewhere); set it to `nil` to pass no `--mode`. The gem now passes its `build_dir` to the plugin in `RAILS_VITE_BUILD_DIR`, so the build directory no longer depends on the mode. Upgrade `rails-vite-plugin` together with the gem: older plugin versions ignore `RAILS_VITE_BUILD_DIR`. With `build_mode` set, `vite:build` runs `vite build --mode` directly instead of the package.json `build` script (#49) ([@olivier-thatch])
18
+ - `vite_asset_url(name)` returns a full URL for a Vite asset, like Rails' `asset_url`, for mailers and other places that need an absolute URL (#36) ([@skryukov])
19
+
20
+ ### Changed
21
+
22
+ - **Breaking:** `rails-vite-plugin` requires Vite 7 or newer and Node.js 20.19+ or 22.12+, like Vite 7 itself. It failed to load on Vite 5 and 6 already ([@skryukov])
23
+ - Vite asset URLs are built with Rails' `path_to_asset`, so `asset_host` works as it does for other Rails assets: procs that take the request, `%d` hosts and per-controller hosts. Mailers now use `config.action_mailer.asset_host` (or `config.asset_host`) instead of the controller's. With `relative_url_root`, tags now include the prefix; set Vite's `base` to match (#36) ([@skryukov])
24
+ - `rake vite:build` loads the Rails environment first, so `config.rails_vite` settings from initializers (such as `vite_executable`) also apply to it and to `bin/rails test` (#42) ([@cole-robertson])
25
+ - The install generator writes `vite dev` instead of `vite` to `Procfile.dev` (#42) ([@cole-robertson])
26
+ - **Breaking:** `vite:build` (and therefore `assets:precompile`) runs the package.json `build` script when one exists, so `"build": "vite build && vite build --ssr"` produces both client and SSR bundles on deploy. Apps without a `build` script keep the bare `vite build`. If package.json already has a `build` script (e.g. one left over from jsbundling-rails), make sure it builds with Vite before upgrading. Test builds and auto-builds always use the bare command, since their extra flags (`--mode test`, `--logLevel warn`) would only reach the last command of a compound script (#38) ([@skryukov])
27
+
28
+ ### Fixed
29
+
30
+ - Vite commands from the rake tasks and auto build run in `Rails.root`, so they work when Rails is started from another directory (#39) ([@janko])
31
+ - The package manager is detected from lockfiles in `Rails.root`, not the current directory ([@skryukov])
32
+ - Chunks loaded at runtime (dynamic `import()` and their preloads) now come from a string `config.action_controller.asset_host`, like the tags: `rake vite:build` and auto builds pass it to the plugin in `RAILS_VITE_ASSET_HOST`, which prepends it to Vite's `base`. Set the variable yourself for a proc or `%d` `asset_host`. Needs both the updated gem and plugin (#35) ([@skryukov])
33
+ - Allow Vitest's internal Vite server to start in CI. `rails()` and `jsbundling()` now skip the dev-server environment guard and dev server setup under Vitest (#43) ([@cole-robertson])
34
+ - Production tags now link the CSS of chunks an entry imports, including nested and shared chunks, not just the entry's own CSS. Chunk CSS comes before the entry's CSS, matching Vite's HTML output, and CSS shared by several entries in one `vite_tags` call is linked once (#40) ([@madogiwa0124])
35
+ - Watch the base directories of the `refresh` globs, so template and helper changes trigger a full reload on Linux. Vite's watcher disables globbing, so the globs were watched as literal paths that don't exist. On Linux this also stopped change events for the nested view directories (#44) ([@olivier-thatch])
36
+ - Ignore a dev metadata file whose Vite process is gone. After a hard kill (SIGKILL, OOM killer), `vite_tags` no longer link to a dead dev server, and Rails started afterwards runs auto builds again. The plugin now records its hostname, and the pid is only checked when Vite runs on the same host, so Vite in another container still counts as running. Needs both the updated gem and plugin (#48) ([@olivier-thatch])
37
+
8
38
  ## rails_vite@0.2.3 / rails-vite-plugin@0.2.5 - 2026-06-09
9
39
 
10
40
  ### Added
@@ -93,6 +123,11 @@ and this project adheres to [Semantic Versioning].
93
123
 
94
124
  [@skryukov]: https://github.com/skryukov
95
125
  [@brodienguyen]: https://github.com/brodienguyen
126
+ [@olivier-thatch]: https://github.com/olivier-thatch
127
+ [@beauraF]: https://github.com/beauraF
128
+ [@cole-robertson]: https://github.com/cole-robertson
129
+ [@madogiwa0124]: https://github.com/madogiwa0124
130
+ [@janko]: https://github.com/janko
96
131
 
97
132
  [Keep a Changelog]: https://keepachangelog.com/en/1.0.0/
98
133
  [Semantic Versioning]: https://semver.org/spec/v2.0.0.html
data/README.md CHANGED
@@ -89,7 +89,8 @@ Short names are automatically prefixed with `sourceDir` (default: `app/javascrip
89
89
  | `vite_javascript_tag(*entries, **options)` | Same as `vite_tags`, appends `.js` to extensionless names |
90
90
  | `vite_stylesheet_tag(*entries, **options)` | Same as `vite_tags`, appends `.css` to extensionless names |
91
91
  | `vite_typescript_tag(*entries, **options)` | Same as `vite_tags`, appends `.ts` to extensionless names |
92
- | `vite_asset_path(name)` | Returns the fingerprinted path from the manifest |
92
+ | `vite_asset_path(name)` | Returns the fingerprinted path from the manifest, with `asset_host` applied like Rails' `asset_path` |
93
+ | `vite_asset_url(name)` | Same as `vite_asset_path`, as a full URL like Rails' `asset_url`. In mailers, set `asset_host` to get a full URL |
93
94
  | `vite_image_tag(name, **options)` | Image tag with manifest-resolved src |
94
95
 
95
96
  All tag helpers accept arbitrary HTML attributes:
@@ -166,6 +167,19 @@ Then reference them in views:
166
167
  <%= vite_image_tag "app/assets/images/logo.png", alt: "Logo" %>
167
168
  ```
168
169
 
170
+ **Migrating from Propshaft?** Vite resolves asset references itself, so you don't need Propshaft's `RAILS_ASSET_URL(...)` — swap the logical path for a path relative to the referencing file, and Vite fingerprints it:
171
+
172
+ ```css
173
+ /* Propshaft */
174
+ background: url(RAILS_ASSET_URL("logo.svg"));
175
+ /* Vite */
176
+ background: url(../assets/images/logo.svg);
177
+ ```
178
+
179
+ - In CSS: works for any stylesheet Vite processes — an entry point, or imported from one.
180
+ - In JS: `import logoUrl from '../assets/images/logo.svg'` (or pull in a whole folder with `import.meta.glob`).
181
+ - In views: `vite_asset_path` / `vite_image_tag`, which read the Vite manifest.
182
+
169
183
  ## Vite Config
170
184
 
171
185
  The install generator creates a minimal `vite.config.ts`:
@@ -181,6 +195,23 @@ export default defineConfig({
181
195
  });
182
196
  ```
183
197
 
198
+ ### Custom Vite Executable
199
+
200
+ The gem runs Vite through the detected package manager (`npx vite`, `yarn vite`, `pnpm vite`, `bun run vite` or `aube exec vite`). To use a Vite-compatible executable with a different name, set `vite_executable`:
201
+
202
+ ```ruby
203
+ # config/initializers/rails_vite.rb
204
+ Rails.application.config.rails_vite.vite_executable = "vp"
205
+ ```
206
+
207
+ `rake vite:build`, `assets:precompile`, test builds and auto builds then run `vp build` (for example, `npx vp build`). A package.json `build` script runs as written, so use `vp build` in it too. In your `Procfile.dev`, run the dev server with `vp dev`:
208
+
209
+ ```
210
+ js: npx vp dev
211
+ ```
212
+
213
+ [Vite+](https://viteplus.dev) installs `vp`, not `vite`, so set `vite_executable = "vp"`. Without it, the package manager may still find a plain Vite (installed by another package, or downloaded by `npx`) and build without Vite+. Package managers may also warn that the plugin's `vite` peer dependency is not met, because Vite+ replaces `vite` with its own package and version.
214
+
184
215
  ### Plugin Options
185
216
 
186
217
  | Option | Default | Description |
@@ -190,9 +221,10 @@ export default defineConfig({
190
221
  | `ssr` | — | SSR entry point |
191
222
  | `ssrOutDir` | `'ssr'` | SSR output directory |
192
223
  | `devMetaFile` | `'tmp/rails-vite.json'` | Dev metadata file path |
193
- | `buildDir` | `'vite'` | Build output subdirectory inside `public/` |
224
+ | `buildDir` | `$RAILS_VITE_BUILD_DIR`, else `'vite-test'` in `test` mode, else `'vite'` | Build output subdirectory inside `public/`. The gem sets `RAILS_VITE_BUILD_DIR` from `config.rails_vite.build_dir` when it runs a build |
194
225
  | `publicDir` | `'public'` | Public directory |
195
226
  | `refresh` | `true` | Paths to watch for full-page reload. `true` watches `app/views/**` and `app/helpers/**` |
227
+ | `refreshDelay` | `0` | Milliseconds to wait after the last `refresh` change before the full-page reload. Changes within the delay send one reload. Set it (e.g. `300`) when Rails sees template changes late, as with `config.file_watcher = ActiveSupport::EventedFileUpdateChecker`, so the reload does not get the old HTML |
196
228
  | `prependSourceDirToEntries` | `true` | When `false`, entries are resolved without the `sourceDir` prefix. Set this when Vite's `root` is your `sourceDir` (see below) |
197
229
 
198
230
  ### Multiple Entry Points
@@ -303,12 +335,34 @@ npx vite build && npx vite build --ssr
303
335
  node ssr/ssr.js
304
336
  ```
305
337
 
338
+ To build the SSR bundle during `assets:precompile`, define a `build` script — `vite:build` runs it when present:
339
+
340
+ ```json
341
+ "scripts": {
342
+ "build": "vite build && vite build --ssr"
343
+ }
344
+ ```
345
+
306
346
  ## Auto Build
307
347
 
308
348
  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`.
309
349
 
310
350
  Freshness is determined by comparing your source files' timestamps against the build manifest's timestamp. Since the manifest lives on disk, unchanged assets are not rebuilt across process restarts — for example, on repeated local system-test runs.
311
351
 
352
+ Besides `sourceDir`, auto build checks these files at the app root by default: `vite.config.*`, `postcss.config.*`, `tailwind.config.*`, `tsconfig*.json`, `package.json`, and the package manager lockfile. Use `auto_build_paths` to add other build inputs. Each entry is a path or glob relative to `Rails.root`. Directories are checked recursively, and paths that do not exist are ignored. For example, Tailwind generates CSS from class names in your views, so add the directories it scans:
353
+
354
+ ```ruby
355
+ # config/initializers/rails_vite.rb
356
+ Rails.application.config.rails_vite.auto_build_paths += %w[
357
+ app/views
358
+ app/components
359
+ app/helpers
360
+ app/assets/stylesheets
361
+ ]
362
+ ```
363
+
364
+ Assign the option (`=`) instead of appending to replace the defaults.
365
+
312
366
  Auto builds run quietly (`vite build --logLevel warn`), so they don't clutter your test output; warnings and errors are still shown. Run `rake vite:build` directly for the full build log.
313
367
 
314
368
  Disable it:
@@ -322,6 +376,31 @@ By default, auto build is enabled in development and test (`Rails.env.local?`).
322
376
 
323
377
  Note: for parallel test runners, disable auto build and use `rake vite:build` before the suite instead.
324
378
 
379
+ ## CDN
380
+
381
+ Tags follow `config.action_controller.asset_host`. `rake vite:build` and auto builds also pass a string `asset_host` to the plugin in `RAILS_VITE_ASSET_HOST`, and the plugin prepends it to Vite's `base`, so chunks Vite loads at runtime (dynamic `import()` and their preloads) come from the same host. With a proc or `%d` `asset_host`, set `RAILS_VITE_ASSET_HOST` in the build's environment yourself (`.env` files are not read for it).
382
+
383
+ The host is fixed at build time, so build with the same `asset_host` you deploy with. jsbundling mode doesn't use this.
384
+
385
+ ## Build Mode
386
+
387
+ `rake vite:build` and auto builds pass `--mode test` to `vite build` in the test environment, and no `--mode` elsewhere. This sets `import.meta.env.MODE` and chooses which `.env.[mode]` files Vite loads. If `MODE === 'test'` means "running under Vitest" in your app, a Rails system-test bundle then includes Vitest-only code. Choose another mode, or no mode:
388
+
389
+ ```ruby
390
+ # config/initializers/rails_vite.rb
391
+ if Rails.env.test?
392
+ Rails.application.config.rails_vite.build_mode = nil # no --mode, so Vite uses "production"
393
+ # or
394
+ Rails.application.config.rails_vite.build_mode = "e2e" # --mode e2e
395
+ end
396
+ ```
397
+
398
+ The build mode does not change the build directory: the test environment still builds to `public/vite-test/`. The gem passes its `build_dir` to the plugin in the `RAILS_VITE_BUILD_DIR` environment variable, so the gem and the plugin always use the same directory. If you run `vite build` yourself, set `RAILS_VITE_BUILD_DIR` or the plugin's `buildDir` option to match.
399
+
400
+ Upgrade `rails-vite-plugin` together with the gem: older plugin versions ignore `RAILS_VITE_BUILD_DIR` and build every mode other than `test` into `public/vite/`.
401
+
402
+ With `build_mode` set, `vite:build` runs `vite build --mode` directly instead of your package.json `build` script, because `--mode` would only reach the last command of a compound script. Build other bundles, such as SSR, in a separate step.
403
+
325
404
  ## Testing the Build
326
405
 
327
406
  To verify your production build works in development:
@@ -331,7 +410,7 @@ rake vite:build # build assets
331
410
  bin/rails s # start Rails without Vite dev server
332
411
  ```
333
412
 
334
- 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.
413
+ Without the Vite dev server running (no `tmp/rails-vite.json`, or its Vite process is gone), Rails serves built assets from `public/vite/`. To switch back to dev mode, start Vite again — the dev metadata takes priority.
335
414
 
336
415
  Clean up built assets with `rake vite:clobber`.
337
416
 
@@ -357,6 +436,14 @@ Defaults match the plugin defaults — no config needed if you follow convention
357
436
 
358
437
  `vite:build` hooks into `assets:precompile` and `test:prepare` automatically. Skip with `SKIP_VITE_BUILD=1`.
359
438
 
439
+ `vite:build` prefers your package.json `build` script when one exists (like jsbundling-rails), falling back to a bare `vite build`. Test builds, and builds with `build_mode` set, skip the script and run `vite build` directly.
440
+
441
+ ### Package Manager
442
+
443
+ The rake tasks, auto build and the install generator run your JS package manager, picked by lockfile in this order: `aube-lock.yaml`, `bun.lockb` or `bun.lock`, `yarn.lock`, `pnpm-lock.yaml`, `package-lock.json`. Without a lockfile, the first of bun, yarn, pnpm, npm and aube found on `PATH` is used.
444
+
445
+ [aube](https://github.com/aubepkg/aube) can also use an existing pnpm, yarn, npm or bun lockfile in place, without writing `aube-lock.yaml`. Such a project is detected as the tool that owns the lockfile, so keep that tool installed, or run `aube import` to create `aube-lock.yaml`.
446
+
360
447
 
361
448
  ## jsbundling Mode
362
449
 
@@ -390,6 +477,7 @@ export default defineConfig({
390
477
  | `outputDir` | `'public/assets'` | Public directory for the full Vite build output |
391
478
  | `ssr` | — | SSR entry point. String or `{ entry, outDir }` |
392
479
  | `refresh` | — | Paths to watch for full-page reload. `true` watches `app/views/**` and `app/helpers/**` |
480
+ | `refreshDelay` | `0` | Milliseconds to wait after the last `refresh` change before the full-page reload. Changes within the delay send one reload. Set it (e.g. `300`) when Rails sees template changes late, as with `config.file_watcher = ActiveSupport::EventedFileUpdateChecker`, so the reload does not get the old HTML |
393
481
  | `devMetaFile` | `'tmp/rails-vite.json'` | Dev metadata file path. Set to `false` to disable |
394
482
 
395
483
  ### Replacing esbuild
@@ -10,8 +10,26 @@ module RailsVite
10
10
  run RailsVite::Tasks.add_command("vite", "rails-vite-plugin")
11
11
  end
12
12
 
13
+ def ensure_esm_package
14
+ return unless File.exist?("package.json")
15
+
16
+ package_json = JSON.parse(File.read("package.json"))
17
+ @esm_package = package_json["type"] == "module"
18
+ return if @esm_package
19
+
20
+ if package_json.key?("type")
21
+ say %(package.json sets "type": "#{package_json["type"]}", but Vite and rails-vite-plugin are ESM-only — generating vite.config.mts instead.), :yellow
22
+ else
23
+ package_json["type"] = "module"
24
+ create_file "package.json", JSON.pretty_generate(package_json) + "\n", force: true
25
+ @esm_package = true
26
+ say %(Added "type": "module" to package.json (Vite and rails-vite-plugin are ESM-only).)
27
+ warn_about_commonjs_configs
28
+ end
29
+ end
30
+
13
31
  def create_vite_config
14
- template "vite.config.ts.tt", "vite.config.ts"
32
+ template "vite.config.ts.tt", @esm_package ? "vite.config.ts" : "vite.config.mts"
15
33
  end
16
34
 
17
35
  def create_entrypoint
@@ -65,6 +83,13 @@ module RailsVite
65
83
 
66
84
  private
67
85
 
86
+ def warn_about_commonjs_configs
87
+ commonjs_configs = Dir["*.config.js"].select { |f| File.read(f).match?(/\bmodule\.exports\b|\brequire\(/) }
88
+ return if commonjs_configs.empty?
89
+
90
+ say "These files use CommonJS and will break under \"type\": \"module\": #{commonjs_configs.join(", ")}. Rename them to .cjs.", :yellow
91
+ end
92
+
68
93
  def vite_dev_command
69
94
  RailsVite::Tasks.dev_command
70
95
  end
@@ -26,7 +26,7 @@ module RailsVite
26
26
 
27
27
  # Build quietly: vite's per-build summary is noise on every auto-build
28
28
  # (e.g. each local system-test run). Warnings and errors still stream.
29
- Rails.logger.error("rails-vite: build failed") unless system("#{RailsVite::Tasks.build_command} --logLevel warn")
29
+ Rails.logger.error("rails-vite: build failed") unless system(RailsVite::Tasks.build_env, "#{RailsVite::Tasks.build_command} --logLevel warn", chdir: Rails.root)
30
30
  @cached_source_mtime = nil
31
31
  @source_mtime_checked_at = nil
32
32
  end
@@ -54,7 +54,7 @@ module RailsVite
54
54
  latest = NEVER
55
55
  source_path = Rails.root.join(@config.source_dir).to_s
56
56
 
57
- Find.find(source_path) do |path|
57
+ Find.find(source_path, *auto_build_path_matches) do |path|
58
58
  next unless File.file?(path)
59
59
  mtime = File.mtime(path)
60
60
  latest = mtime if mtime > latest
@@ -64,5 +64,18 @@ module RailsVite
64
64
  rescue Errno::ENOENT
65
65
  Time.now # source dir missing — trigger build to surface the error
66
66
  end
67
+
68
+ def auto_build_path_matches
69
+ root = Rails.root.to_s
70
+ @config.auto_build_paths.flat_map do |pattern|
71
+ Dir.glob(pattern.to_s, base: root).filter_map { |match| existing_realpath(match, root) }
72
+ end
73
+ end
74
+
75
+ def existing_realpath(path, root)
76
+ File.realpath(path, root)
77
+ rescue SystemCallError
78
+ nil
79
+ end
67
80
  end
68
81
  end
@@ -2,7 +2,7 @@ module RailsVite
2
2
  class Config
3
3
  META_FILENAME = "rails-vite.json"
4
4
 
5
- attr_writer :dev_meta_path, :manifest_path, :asset_prefix, :auto_build, :build_dir
5
+ attr_writer :dev_meta_path, :manifest_path, :asset_prefix, :auto_build, :build_dir, :vite_executable, :auto_build_paths, :build_mode
6
6
 
7
7
  def dev_meta_path
8
8
  @dev_meta_path || Rails.root.join("tmp", META_FILENAME)
@@ -12,6 +12,16 @@ module RailsVite
12
12
  @build_dir || (Rails.env.test? ? "vite-test" : "vite")
13
13
  end
14
14
 
15
+ # The `--mode` for `vite build`. nil or false passes no `--mode`, so Vite uses its default.
16
+ def build_mode
17
+ return @build_mode.presence if defined?(@build_mode)
18
+ "test" if Rails.env.test?
19
+ end
20
+
21
+ def vite_executable
22
+ @vite_executable || "vite"
23
+ end
24
+
15
25
  def manifest_path
16
26
  @manifest_path || Rails.root.join("public", build_dir, "manifest.json")
17
27
  end
@@ -33,6 +43,19 @@ module RailsVite
33
43
  Rails.env.local?
34
44
  end
35
45
 
46
+ # Paths or globs relative to Rails.root that auto build checks for changes,
47
+ # in addition to source_dir. Directories are walked recursively.
48
+ def auto_build_paths
49
+ @auto_build_paths ||= [
50
+ "vite.config.*",
51
+ "postcss.config.*",
52
+ "tailwind.config.*",
53
+ "tsconfig*.json",
54
+ "package.json",
55
+ *Tasks::LOCKFILES.values.flatten
56
+ ]
57
+ end
58
+
36
59
  def dev_server_running?
37
60
  return false if Rails.env.test?
38
61
  !!dev_server_url
@@ -61,14 +84,37 @@ module RailsVite
61
84
  end
62
85
 
63
86
  def load_plugin_meta
64
- JSON.parse(dev_meta_path.read)
87
+ meta = JSON.parse(dev_meta_path.read)
88
+ return meta if dev_server_alive?(meta)
89
+
90
+ load_build_meta
65
91
  rescue Errno::ENOENT
66
- build_meta_path = manifest_path.dirname.join(META_FILENAME)
67
- begin
68
- JSON.parse(build_meta_path.read)
69
- rescue Errno::ENOENT, JSON::ParserError
70
- {}
92
+ load_build_meta
93
+ end
94
+
95
+ def load_build_meta
96
+ JSON.parse(manifest_path.dirname.join(META_FILENAME).read)
97
+ rescue Errno::ENOENT, JSON::ParserError
98
+ {}
99
+ end
100
+
101
+ # A hard kill of Vite (SIGKILL, OOM) does not remove the dev meta file.
102
+ # Ignore the file when its process is gone.
103
+ def dev_server_alive?(meta)
104
+ pid = meta["pid"]
105
+ return true unless pid.is_a?(Integer) && pid.positive?
106
+ return true unless meta["hostname"] == Socket.gethostname
107
+
108
+ Process.kill(0, pid)
109
+ true
110
+ rescue Errno::EPERM
111
+ true
112
+ rescue Errno::ESRCH
113
+ unless @stale_dev_meta_pid == pid
114
+ @stale_dev_meta_pid = pid
115
+ Rails.logger&.warn("rails-vite: ignoring #{dev_meta_path}, Vite process #{pid} is not running")
71
116
  end
117
+ false
72
118
  end
73
119
  end
74
120
  end
@@ -14,11 +14,16 @@ module RailsVite
14
14
  entry = manifest[name] || resolve_with_extension(name, manifest) ||
15
15
  raise(MissingEntryError.new(name, @path))
16
16
 
17
+ imports = []
18
+ css = []
19
+ walk_imports(entry, Set.new, manifest, imports, css)
20
+ css.concat(entry.fetch("css", []))
21
+
17
22
  {
18
23
  file: entry["file"],
19
24
  integrity: entry["integrity"],
20
- css: resolve_css(entry, manifest),
21
- imports: resolve_imports(entry, Set.new, manifest)
25
+ css: resolve_css(css.uniq, manifest),
26
+ imports: imports
22
27
  }
23
28
  end
24
29
 
@@ -58,23 +63,24 @@ module RailsVite
58
63
  nil
59
64
  end
60
65
 
61
- def resolve_css(entry, manifest)
62
- entry.fetch("css", []).map do |css_file|
63
- integrity = manifest.values.find { |e| e["file"] == css_file }&.dig("integrity")
64
- {file: css_file, integrity: integrity}
65
- end
66
+ def resolve_css(css_files, manifest)
67
+ return [] if css_files.empty?
68
+
69
+ by_file = {}
70
+ manifest.each_value { |e| by_file[e["file"]] ||= e }
71
+ css_files.map { |css_file| {file: css_file, integrity: by_file[css_file]&.dig("integrity")} }
66
72
  end
67
73
 
68
- def resolve_imports(entry, seen, manifest)
69
- entry.fetch("imports", []).flat_map do |import_key|
70
- next [] if seen.include?(import_key)
71
- seen.add(import_key)
74
+ def walk_imports(chunk, seen, manifest, imports, css)
75
+ chunk.fetch("imports", []).each do |import_key|
76
+ next unless seen.add?(import_key)
72
77
 
73
78
  imported = manifest[import_key]
74
- next [] unless imported
79
+ next unless imported
75
80
 
76
- [{file: imported["file"], integrity: imported["integrity"]}] +
77
- resolve_imports(imported, seen, manifest)
81
+ imports << {file: imported["file"], integrity: imported["integrity"]}
82
+ walk_imports(imported, seen, manifest, imports, css)
83
+ css.concat(imported.fetch("css", []))
78
84
  end
79
85
  end
80
86
  end
@@ -33,10 +33,14 @@ module RailsVite
33
33
  if config.dev_server_running?
34
34
  "#{config.dev_server_url}/#{resolved}"
35
35
  else
36
- vite_asset_url(RailsVite.manifest.path_for(resolved))
36
+ vite_build_path(RailsVite.manifest.path_for(resolved))
37
37
  end
38
38
  end
39
39
 
40
+ def vite_asset_url(name)
41
+ url_to_asset(vite_asset_path(name))
42
+ end
43
+
40
44
  def vite_image_tag(name, **options)
41
45
  image_tag(vite_asset_path(name), **options)
42
46
  end
@@ -77,6 +81,7 @@ module RailsVite
77
81
  def vite_prod_tags(entries, nonce: nil, **options)
78
82
  tags = []
79
83
  preloaded = Set.new
84
+ linked_css = Set.new
80
85
 
81
86
  entries.each do |entry|
82
87
  result = RailsVite.manifest.lookup(entry)
@@ -84,15 +89,16 @@ module RailsVite
84
89
  Array(result[:imports]).each do |import_entry|
85
90
  next if preloaded.include?(import_entry[:file])
86
91
  preloaded.add(import_entry[:file])
87
- tags << tag.link(rel: "modulepreload", href: vite_asset_url(import_entry[:file]),
92
+ tags << tag.link(rel: "modulepreload", href: vite_build_path(import_entry[:file]),
88
93
  nonce: nonce, **sri_attrs(import_entry[:integrity]))
89
94
  end
90
95
 
91
- tags << build_asset_tag(entry, vite_asset_url(result[:file]),
96
+ tags << build_asset_tag(entry, vite_build_path(result[:file]),
92
97
  nonce: nonce, integrity: result[:integrity], **options)
93
98
 
94
99
  Array(result[:css]).each do |css_entry|
95
- tags << tag.link(rel: "stylesheet", href: vite_asset_url(css_entry[:file]),
100
+ next unless linked_css.add?(css_entry[:file])
101
+ tags << tag.link(rel: "stylesheet", href: vite_build_path(css_entry[:file]),
96
102
  nonce: nonce, **sri_attrs(css_entry[:integrity]), **options)
97
103
  end
98
104
  end
@@ -126,32 +132,12 @@ module RailsVite
126
132
  [source_dir, entrypoints_dir, entry].reject { |part| part.nil? || part.empty? }.join("/")
127
133
  end
128
134
 
129
- def vite_asset_url(file)
130
- vite_prefix_asset_host("#{RailsVite.config.asset_prefix}/#{file}")
135
+ def vite_build_path(file)
136
+ path_to_asset("#{RailsVite.config.asset_prefix}/#{file}")
131
137
  end
132
138
 
133
139
  def css_entry?(entry)
134
140
  CSS_EXTENSIONS.match?(entry)
135
141
  end
136
-
137
- def vite_prefix_asset_host(path)
138
- host = vite_resolve_asset_host
139
- host ? "#{host}#{path}" : path
140
- end
141
-
142
- # Memoized per view instance (one per request). Safe for Proc hosts
143
- # since each request gets a fresh ActionView::Base instance.
144
- def vite_resolve_asset_host
145
- return @_vite_asset_host if defined?(@_vite_asset_host)
146
-
147
- @_vite_asset_host = begin
148
- case (asset_host = Rails.application.config.action_controller.asset_host)
149
- when String then asset_host.chomp("/")
150
- when Proc then asset_host.call("")&.chomp("/")
151
- end
152
- rescue NoMethodError
153
- nil
154
- end
155
- end
156
142
  end
157
143
  end
@@ -1,3 +1,5 @@
1
+ require "shellwords"
2
+
1
3
  module RailsVite
2
4
  module Tasks
3
5
  extend self
@@ -5,13 +7,15 @@ module RailsVite
5
7
  BUN_CMD = defined?(Bundlebun) ? Bundlebun::Runner.binstub_or_binary_path : "bun"
6
8
 
7
9
  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"}
10
+ bun: {install: "#{BUN_CMD} install", add: "#{BUN_CMD} add -D", exec: "#{BUN_CMD} run", run: "#{BUN_CMD} run"},
11
+ yarn: {install: "yarn install", add: "yarn add -D", exec: "yarn", run: "yarn run"},
12
+ pnpm: {install: "pnpm install", add: "pnpm add -D", exec: "pnpm", run: "pnpm run"},
13
+ npm: {install: "npm install", add: "npm install -D", exec: "npx", run: "npm run"},
14
+ aube: {install: "aube install", add: "aube add -D", exec: "aube exec", run: "aube run"}
12
15
  }.freeze
13
16
 
14
17
  LOCKFILES = {
18
+ aube: %w[aube-lock.yaml],
15
19
  bun: %w[bun.lockb bun.lock],
16
20
  yarn: %w[yarn.lock],
17
21
  pnpm: %w[pnpm-lock.yaml],
@@ -27,29 +31,55 @@ module RailsVite
27
31
  end
28
32
 
29
33
  def dev_command
30
- command_for(:dev)
34
+ "#{vite_command} dev"
31
35
  end
32
36
 
33
37
  def build_command
34
- cmd = command_for(:build)
35
- cmd += " --mode test" if Rails.env.test?
38
+ cmd = "#{vite_command} build"
39
+ mode = RailsVite.config.build_mode
40
+ cmd += " --mode #{Shellwords.escape(mode)}" if mode
36
41
  cmd
37
42
  end
38
43
 
44
+ def precompile_command
45
+ return build_command if Rails.env.test? || RailsVite.config.build_mode || !package_json_build_script?
46
+ "#{command_for(:run)} build"
47
+ end
48
+
49
+ # Use with build_command. The plugin builds into this dir, so the gem and
50
+ # the plugin use the same dir for all build modes.
51
+ def build_env
52
+ env = {"RAILS_VITE_BUILD_DIR" => RailsVite.config.build_dir}
53
+ asset_host = Rails.application.config.action_controller.asset_host
54
+ env["RAILS_VITE_ASSET_HOST"] = asset_host if asset_host.is_a?(String) && asset_host.present? && !asset_host.include?("%d")
55
+ env
56
+ end
57
+
39
58
  def tool
40
59
  tool_determined_by_lockfile || tool_determined_by_executable
41
60
  end
42
61
 
43
62
  private
44
63
 
64
+ def vite_command
65
+ "#{command_for(:exec)} #{RailsVite.config.vite_executable}"
66
+ end
67
+
68
+ def package_json_build_script?
69
+ path = Rails.root.join("package.json")
70
+ path.exist? && JSON.parse(path.read).dig("scripts", "build").present?
71
+ rescue JSON::ParserError
72
+ false
73
+ end
74
+
45
75
  def command_for(key)
46
76
  COMMANDS.dig(tool, key) ||
47
- raise("rails_vite: No suitable JS package manager found for '#{key}'. Ensure npm, yarn, pnpm, or bun is available.")
77
+ raise("rails_vite: No suitable JS package manager found for '#{key}'. Ensure npm, yarn, pnpm, bun, or aube is available.")
48
78
  end
49
79
 
50
80
  def tool_determined_by_lockfile
51
81
  LOCKFILES.each do |tool_name, files|
52
- return tool_name if files.any? { |f| File.exist?(f) }
82
+ return tool_name if files.any? { |f| Rails.root.join(f).exist? }
53
83
  end
54
84
  nil
55
85
  end
@@ -1,3 +1,3 @@
1
1
  module RailsVite
2
- VERSION = "0.2.3"
2
+ VERSION = "0.3.0.rc1"
3
3
  end
data/lib/rails_vite.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require "json"
2
2
  require "digest"
3
+ require "socket"
3
4
 
4
5
  require_relative "rails_vite/errors"
5
6
  require_relative "rails_vite/manifest"
@@ -2,13 +2,13 @@ namespace :vite do
2
2
  desc "Install JavaScript dependencies"
3
3
  task :install do
4
4
  command = RailsVite::Tasks.install_command
5
- system(command) || raise("rails_vite: Command install failed, ensure #{command.split.first} is installed")
5
+ system(command, chdir: Rails.root) || raise("rails_vite: Command install failed, ensure #{command.split.first} is installed")
6
6
  end
7
7
 
8
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")
9
+ task build: :environment do
10
+ command = RailsVite::Tasks.precompile_command
11
+ system(RailsVite::Tasks.build_env, command, chdir: Rails.root) || raise("rails_vite: Command build failed, ensure `#{command}` runs without errors")
12
12
  end
13
13
 
14
14
  Rake::Task["vite:build"].prereqs << :install unless ENV["SKIP_VITE_INSTALL"]
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_vite
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
4
+ version: 0.3.0.rc1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Svyatoslav Kryukov
@@ -71,7 +71,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
71
71
  - !ruby/object:Gem::Version
72
72
  version: '0'
73
73
  requirements: []
74
- rubygems_version: 4.0.3
74
+ rubygems_version: 4.0.20
75
75
  specification_version: 4
76
76
  summary: Vite integration for Rails
77
77
  test_files: []