turbo-hmr 0.0.1
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/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/CLAUDE.md +66 -0
- data/LICENSE.txt +21 -0
- data/README.md +64 -0
- data/Rakefile +8 -0
- data/app/javascript/turbo-hmr.js +149 -0
- data/lib/generators/turbo/hmr/USAGE +10 -0
- data/lib/generators/turbo/hmr/install_generator.rb +54 -0
- data/lib/turbo/hmr/engine.rb +20 -0
- data/lib/turbo/hmr/importmap_helper.rb +14 -0
- data/lib/turbo/hmr/version.rb +7 -0
- data/lib/turbo/hmr.rb +11 -0
- metadata +97 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: e6f12eb486a159355c025fa656c8c3e764379bb6e38c4af5cbc4e3cc3eeaa263
|
|
4
|
+
data.tar.gz: 6965f4bcb66437567a8a8cbd237de05db207f0f4f5e63fba2db330e28bb4a282
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 80061ae7f2144ad3e1c54c016c7567d9c8b6b998ff319c0d8d14f4271690bf0bc03c697dcebb59636f6d50d89fc2bd11b8504d44e5fa87924b83076a078fdff3
|
|
7
|
+
data.tar.gz: 023375f245a878be95e8a633db97eb711392825ae9eea5554970e65ab9de5ba2d5a0222147b23bc34aa55929c3ba84732299dd62d45d87307885cee153a1268a
|
data/.ruby-gemset
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
turbo-hmr
|
data/.ruby-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ruby-3.4.7
|
data/CLAUDE.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## Project Overview
|
|
6
|
+
|
|
7
|
+
turbo-hmr is a Ruby gem that provides Hot Module Replacement (HMR) for Turbo with Importmap. It detects when pinned Stimulus controller modules change across Turbo-driven navigations and attempts to hot-swap them without triggering a full page reload.
|
|
8
|
+
|
|
9
|
+
## Architecture
|
|
10
|
+
|
|
11
|
+
The gem has two main components that work together:
|
|
12
|
+
|
|
13
|
+
**Ruby/Rails side:**
|
|
14
|
+
- `lib/turbo/hmr/engine.rb` - Rails Engine that sets up asset paths and registers helpers
|
|
15
|
+
- `lib/turbo/hmr/importmap_helper.rb` - Overrides the default `javascript_inline_importmap_tag` to remove `data-turbo-track="reload"` attribute, which is essential for HMR to intercept importmap changes
|
|
16
|
+
- `lib/generators/turbo/hmr/install_generator.rb` - Generator that configures the host application
|
|
17
|
+
|
|
18
|
+
**JavaScript side:**
|
|
19
|
+
- `app/javascript/turbo-hmr.js` - Core HMR logic that:
|
|
20
|
+
- Listens to `turbo:before-fetch-response` to extract the incoming page's importmap
|
|
21
|
+
- Compares importmaps in `turbo:before-render` to detect changes
|
|
22
|
+
- Hot-swaps changed Stimulus controllers by calling `application.unload()` and `application.register()` with re-imported modules
|
|
23
|
+
- Falls back to full reload via `Turbo.visit()` if non-controller imports change or hot-swap fails
|
|
24
|
+
- Only hot-swaps modules matching `controllers/*` pattern (configurable via `isControllerImport()`)
|
|
25
|
+
|
|
26
|
+
**Key flow:**
|
|
27
|
+
1. Importmap helper removes `data-turbo-track` attribute to prevent automatic reloads
|
|
28
|
+
2. JavaScript intercepts Turbo navigation events to compare old vs new importmaps
|
|
29
|
+
3. If only controllers changed: hot-swap them without page reload
|
|
30
|
+
4. If anything else changed: trigger full reload
|
|
31
|
+
|
|
32
|
+
## Development Commands
|
|
33
|
+
|
|
34
|
+
**Run tests:**
|
|
35
|
+
```bash
|
|
36
|
+
bundle exec rspec
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**Run specific test:**
|
|
40
|
+
```bash
|
|
41
|
+
bundle exec rspec spec/system/hotswap_system_spec.rb
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**Run single focused test:**
|
|
45
|
+
```bash
|
|
46
|
+
bundle exec rspec spec/system/hotswap_system_spec.rb:<line_number>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Testing
|
|
50
|
+
|
|
51
|
+
The gem uses RSpec with system tests powered by Capybara and Cuprite (headless Chrome driver). The test suite includes a dummy Rails app in `spec/dummy/` that simulates a real Rails application with Turbo and Stimulus.
|
|
52
|
+
|
|
53
|
+
System tests verify HMR behavior by:
|
|
54
|
+
- Modifying controller files at runtime (`write_version_controller`)
|
|
55
|
+
- Navigating between pages with Turbo
|
|
56
|
+
- Verifying controllers hot-swap without full page reloads
|
|
57
|
+
- Checking page load counts using custom `have_been_loaded` matcher
|
|
58
|
+
|
|
59
|
+
The dummy app is essential for testing - it's not just scaffolding but a functioning Rails app that exercises the actual HMR flow.
|
|
60
|
+
|
|
61
|
+
## Important Constraints
|
|
62
|
+
|
|
63
|
+
- Hot-swapping only works for Stimulus controllers (modules under `controllers/` in importmap)
|
|
64
|
+
- Controllers must be safe to re-import (no problematic side-effects during module evaluation)
|
|
65
|
+
- Requires importmap entries to include cache-busting digests (Rails default in development/test)
|
|
66
|
+
- The `data-turbo-track="reload"` attribute MUST be removed from importmap script tags
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Micah Geisel
|
|
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,64 @@
|
|
|
1
|
+
# turbo-hmr
|
|
2
|
+
|
|
3
|
+
Hot Module Replacement for Turbo with Importmap.
|
|
4
|
+
|
|
5
|
+
This gem detects when pinned Stimulus controller modules change across a Turbo-driven navigation (e.g., `Turbo.visit`). If the new page’s importmap updates any Stimulus controllers, the gem attempts to swap in the updated controllers without a full page reload by:
|
|
6
|
+
|
|
7
|
+
- Disconnecting existing controller instances for the changed identifiers
|
|
8
|
+
- Re-importing the updated modules
|
|
9
|
+
- Re-registering/reconnecting controllers so instances start using the new code
|
|
10
|
+
- Preventing the full page reload
|
|
11
|
+
|
|
12
|
+
If any changed importmap item can’t be safely hot-swapped, we fall back upon the default Turbo.visit behavior to ensure correctness.
|
|
13
|
+
|
|
14
|
+
## Why
|
|
15
|
+
|
|
16
|
+
More seamless deployments for long-lived single-page sessions.
|
|
17
|
+
|
|
18
|
+
Turbo’s morphing normally keeps you on the page without a full reload, but the browser doesn’t automatically re-import modules whose specifiers are unchanged while their URLs in the importmap have changed, and instead just does a full page load. This gem bridges that gap by watching for importmap diffs and applying a targeted hot swap.
|
|
19
|
+
|
|
20
|
+
## How It Works
|
|
21
|
+
|
|
22
|
+
This gem disables Turbo's built-in `data-turbo-track="reload"` behavior for importmaps and replaces it with smarter logic:
|
|
23
|
+
|
|
24
|
+
1. During Turbo navigations, it intercepts `turbo:before-fetch-response` to extract the incoming page's importmap
|
|
25
|
+
2. In `turbo:before-render`, it compares importmaps and detects what changed
|
|
26
|
+
3. If only Stimulus controllers changed, it hot-swaps them without a full page reload
|
|
27
|
+
4. Controllers are hot-swapped by calling `Stimulus.unload()` and `Stimulus.register()` with the new module
|
|
28
|
+
5. If any non-controller imports changed, it triggers a full reload via `Turbo.visit()`
|
|
29
|
+
|
|
30
|
+
**Caveat emptor: This process assumes that all stimulus controllers can be safely unloaded and reloaded without problematic side-effects. Users of this gem need to ensure that their controllers are safe to re-import!**
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
1. Add to your Gemfile:
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
gem "turbo-hmr"
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
2. Run the installer:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
bin/rails generate turbo:hmr:install
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
This will:
|
|
47
|
+
- Pin the `turbo-hmr` JavaScript module in `config/importmap.rb`
|
|
48
|
+
- Set it up in `app/javascript/application.js`
|
|
49
|
+
|
|
50
|
+
## Limitations
|
|
51
|
+
|
|
52
|
+
- Only swaps Stimulus controllers; other module changes will trigger a full page reload.
|
|
53
|
+
- Assumes that controllers do not have problematic side-effects during module evaluation. The onus is upon the user of this gem to ensure their controllers are safe to re-import.
|
|
54
|
+
- Assumes importmap entries include cache-busting digests when files change (Rails does this by default in development/test).
|
|
55
|
+
- Requires disabling `data-turbo-track="reload"` on the importmap script tag (this gem monkeypatches importmaps-rails to do this).
|
|
56
|
+
|
|
57
|
+
## Roadmap
|
|
58
|
+
|
|
59
|
+
- Configurable module matching (allow/deny lists)
|
|
60
|
+
- Opt-in HMR for non-controller modules
|
|
61
|
+
|
|
62
|
+
## License
|
|
63
|
+
|
|
64
|
+
MIT
|
data/Rakefile
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
class TurboHmr {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.currentImportmap = null
|
|
4
|
+
this.application = null
|
|
5
|
+
this.pendingImportmap = null
|
|
6
|
+
this.changes = { controllers: [], others: [] }
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
start(application) {
|
|
10
|
+
this.application = application
|
|
11
|
+
this.currentImportmap = this.extractImportmap(document)
|
|
12
|
+
|
|
13
|
+
document.addEventListener("turbo:before-fetch-response", this.handleBeforeFetchResponse.bind(this))
|
|
14
|
+
document.addEventListener("turbo:before-render", this.handleBeforeRender.bind(this))
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async handleBeforeFetchResponse(event) {
|
|
18
|
+
const response = event.detail.fetchResponse
|
|
19
|
+
const html = await response.responseHTML
|
|
20
|
+
const parser = new DOMParser()
|
|
21
|
+
const doc = parser.parseFromString(html, "text/html")
|
|
22
|
+
this.pendingImportmap = this.extractImportmap(doc)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
handleBeforeRender(event) {
|
|
26
|
+
if (!this.pendingImportmap || !this.currentImportmap) return
|
|
27
|
+
|
|
28
|
+
this.detectChanges(this.currentImportmap, this.pendingImportmap)
|
|
29
|
+
|
|
30
|
+
// console.log("turbo-hmr: changes:", this.changes)
|
|
31
|
+
|
|
32
|
+
if (this.changes.others.length > 0) {
|
|
33
|
+
// console.log("turbo-hmr: Non-controller imports changed, triggering reload")
|
|
34
|
+
event.preventDefault()
|
|
35
|
+
location.reload()
|
|
36
|
+
|
|
37
|
+
} else if (this.changes.controllers.length > 0) {
|
|
38
|
+
// console.log("turbo-hmr: Hot-swapping controllers:", this.changes.controllers)
|
|
39
|
+
|
|
40
|
+
// Unload removed controllers before render
|
|
41
|
+
const removedControllers = this.changes.controllers.filter(c => c.newUrl === null)
|
|
42
|
+
for (const change of removedControllers) {
|
|
43
|
+
this.unloadController(change.identifier)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Reload added/changed controllers after render
|
|
47
|
+
requestAnimationFrame(() => {
|
|
48
|
+
const addedOrChangedControllers = this.changes.controllers.filter(c => c.newUrl !== null)
|
|
49
|
+
Promise.all(addedOrChangedControllers.map(change =>
|
|
50
|
+
this.reloadController(change.identifier, change.newUrl)
|
|
51
|
+
))
|
|
52
|
+
.then(() => {
|
|
53
|
+
// console.log("turbo-hmr: successfully hot-swapped controllers")
|
|
54
|
+
this.currentImportmap = this.pendingImportmap
|
|
55
|
+
})
|
|
56
|
+
.catch(error => {
|
|
57
|
+
console.error("turbo-hmr: Failed to hot-swap controllers", error)
|
|
58
|
+
location.reload()
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
} else {
|
|
62
|
+
this.currentImportmap = this.pendingImportmap
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
extractImportmap(doc) {
|
|
67
|
+
const importmapScript = doc.querySelector('script[type="importmap"]')
|
|
68
|
+
if (!importmapScript) return null
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
return JSON.parse(importmapScript.textContent)
|
|
72
|
+
} catch (e) {
|
|
73
|
+
console.error("turbo-hmr: Failed to parse importmap", e)
|
|
74
|
+
return null
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
detectChanges(oldMap, newMap) {
|
|
79
|
+
this.changes = { controllers: [], others: [] }
|
|
80
|
+
|
|
81
|
+
for (const [identifier, url] of Object.entries(newMap.imports)) {
|
|
82
|
+
const oldUrl = oldMap.imports[identifier]
|
|
83
|
+
const isController = this.isControllerImport(identifier)
|
|
84
|
+
|
|
85
|
+
if (!oldUrl) {
|
|
86
|
+
// Added
|
|
87
|
+
const change = { identifier, oldUrl: null, newUrl: url }
|
|
88
|
+
isController ? this.changes.controllers.push(change) : this.changes.others.push(change)
|
|
89
|
+
} else if (oldUrl !== url) {
|
|
90
|
+
// Changed
|
|
91
|
+
const change = { identifier, oldUrl, newUrl: url }
|
|
92
|
+
isController ? this.changes.controllers.push(change) : this.changes.others.push(change)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Removed
|
|
97
|
+
for (const [identifier, url] of Object.entries(oldMap.imports)) {
|
|
98
|
+
if (!newMap.imports[identifier]) {
|
|
99
|
+
const isController = this.isControllerImport(identifier)
|
|
100
|
+
const change = { identifier, oldUrl: url, newUrl: null }
|
|
101
|
+
isController ? this.changes.controllers.push(change) : this.changes.others.push(change)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
isControllerImport(identifier) {
|
|
107
|
+
return identifier.startsWith("controllers/")
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async hotSwapControllers() {
|
|
111
|
+
for (const change of this.changes.controllers) {
|
|
112
|
+
if (change.newUrl) {
|
|
113
|
+
await this.reloadController(change.identifier, change.newUrl)
|
|
114
|
+
} else {
|
|
115
|
+
await this.unloadController(change.identifier)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async reloadController(identifier, url) {
|
|
121
|
+
const controllerName = this.identifierToControllerName(identifier)
|
|
122
|
+
const module = await import(url)
|
|
123
|
+
this.application.unload(controllerName)
|
|
124
|
+
this.application.register(controllerName, module.default)
|
|
125
|
+
// console.log(`turbo-hmr: Reloaded controller "${controllerName}" from ${url}`)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async unloadController(identifier) {
|
|
129
|
+
const controllerName = this.identifierToControllerName(identifier)
|
|
130
|
+
this.application.unload(controllerName)
|
|
131
|
+
// console.log(`turbo-hmr: Unloaded controller "${controllerName}"`)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
identifierToControllerName(identifier) {
|
|
135
|
+
// "controllers/version_controller" => "version"
|
|
136
|
+
// "controllers/admin/users_controller" => "admin--users"
|
|
137
|
+
return identifier
|
|
138
|
+
.replace("controllers/", "")
|
|
139
|
+
.replace("_controller", "")
|
|
140
|
+
.replace(/\//g, "--")
|
|
141
|
+
.replace(/_/g, "-")
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const hotswap = new TurboHmr()
|
|
146
|
+
export function start(application) {
|
|
147
|
+
hotswap.start(application)
|
|
148
|
+
}
|
|
149
|
+
export default { start }
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Description:
|
|
2
|
+
Installs turbo-hmr into your Rails application.
|
|
3
|
+
|
|
4
|
+
This generator will:
|
|
5
|
+
- Create an initializer to disable data-turbo-track on the importmap
|
|
6
|
+
- Pin the turbo_hmr module in config/importmap.rb
|
|
7
|
+
- Add the import to app/javascript/application.js
|
|
8
|
+
|
|
9
|
+
Example:
|
|
10
|
+
bin/rails generate turbo:hmr:install
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Turbo
|
|
4
|
+
module Hmr
|
|
5
|
+
module Generators
|
|
6
|
+
class InstallGenerator < ::Rails::Generators::Base
|
|
7
|
+
source_root File.expand_path("templates", __dir__)
|
|
8
|
+
|
|
9
|
+
desc "Installs turbo-hmr"
|
|
10
|
+
|
|
11
|
+
def update_importmap
|
|
12
|
+
append_to_file "config/importmap.rb", %(pin "turbo-hmr", to: "turbo-hmr.js"\n)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def update_application_js
|
|
16
|
+
application_js_path = "app/javascript/application.js"
|
|
17
|
+
|
|
18
|
+
# Read the existing content to determine where to add the code
|
|
19
|
+
if File.exist?(application_js_path)
|
|
20
|
+
content = File.read(application_js_path)
|
|
21
|
+
|
|
22
|
+
# Add the import at the top if not already present
|
|
23
|
+
unless content.include?('from "turbo-hmr"')
|
|
24
|
+
prepend_to_file application_js_path, "import { start as startTurboHmr } from \"turbo-hmr\"\n"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Add the start call after Stimulus is initialized
|
|
28
|
+
unless content.include?("startTurboHmr")
|
|
29
|
+
# Try to add after Application.start() or window.Stimulus assignment
|
|
30
|
+
if content.match(/(?:const|let|var)\s+(\w+)\s*=.*Application\.start\(\)/)
|
|
31
|
+
app_var = Regexp.last_match(1)
|
|
32
|
+
inject_into_file application_js_path, "\nstartTurboHmr(#{app_var})\n",
|
|
33
|
+
after: /#{app_var}\s*=.*Application\.start\(\)/
|
|
34
|
+
elsif content.match(/window\.Stimulus\s*=.*Application\.start\(\)/)
|
|
35
|
+
inject_into_file application_js_path, "\nstartTurboHmr(window.Stimulus)\n",
|
|
36
|
+
after: /window\.Stimulus\s*=.*Application\.start\(\)/
|
|
37
|
+
else
|
|
38
|
+
append_to_file application_js_path, "\n// Start turbo-hmr (adjust the application variable name as needed)\n// startTurboHmr(application)\n"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
else
|
|
42
|
+
create_file application_js_path, <<~JS
|
|
43
|
+
import { start as startTurboHmr } from "turbo-hmr"
|
|
44
|
+
import { Application } from "@hotwired/stimulus"
|
|
45
|
+
|
|
46
|
+
const application = Application.start()
|
|
47
|
+
startTurboHmr(application)
|
|
48
|
+
JS
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Turbo
|
|
4
|
+
module Hmr
|
|
5
|
+
class Engine < ::Rails::Engine
|
|
6
|
+
isolate_namespace Turbo::Hmr
|
|
7
|
+
|
|
8
|
+
initializer "turbo_hmr.assets" do |app|
|
|
9
|
+
app.config.assets.paths << root.join("app/javascript")
|
|
10
|
+
app.config.assets.precompile += %w[turbo-hmr.js]
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
initializer "turbo_hmr.importmap_helper" do
|
|
14
|
+
ActiveSupport.on_load(:action_controller) do
|
|
15
|
+
helper Turbo::Hmr::ImportmapHelper
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Turbo
|
|
4
|
+
module Hmr
|
|
5
|
+
module ImportmapHelper
|
|
6
|
+
# Override importmap helper to not add data-turbo-track to the importmap script
|
|
7
|
+
# This allows turbo-hmr to handle importmap changes without full page reloads
|
|
8
|
+
def javascript_inline_importmap_tag(importmap_json = Rails.application.importmap.to_json(resolver: self))
|
|
9
|
+
tag.script importmap_json.html_safe,
|
|
10
|
+
type: "importmap", nonce: request&.content_security_policy_nonce
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
data/lib/turbo/hmr.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: turbo-hmr
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Micah Geisel
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: railties
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '6.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '6.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: importmap-rails
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: rspec-rails
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '6.0'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '6.0'
|
|
54
|
+
description: Enables HMR (Hot Module Replacement) for ES modules during Turbo navigations,
|
|
55
|
+
with special support for Stimulus controllers.
|
|
56
|
+
email:
|
|
57
|
+
- micah@botandrose.com
|
|
58
|
+
executables: []
|
|
59
|
+
extensions: []
|
|
60
|
+
extra_rdoc_files: []
|
|
61
|
+
files:
|
|
62
|
+
- ".ruby-gemset"
|
|
63
|
+
- ".ruby-version"
|
|
64
|
+
- CLAUDE.md
|
|
65
|
+
- LICENSE.txt
|
|
66
|
+
- README.md
|
|
67
|
+
- Rakefile
|
|
68
|
+
- app/javascript/turbo-hmr.js
|
|
69
|
+
- lib/generators/turbo/hmr/USAGE
|
|
70
|
+
- lib/generators/turbo/hmr/install_generator.rb
|
|
71
|
+
- lib/turbo/hmr.rb
|
|
72
|
+
- lib/turbo/hmr/engine.rb
|
|
73
|
+
- lib/turbo/hmr/importmap_helper.rb
|
|
74
|
+
- lib/turbo/hmr/version.rb
|
|
75
|
+
homepage: https://github.com/botandrose/turbo-hmr
|
|
76
|
+
licenses:
|
|
77
|
+
- MIT
|
|
78
|
+
metadata:
|
|
79
|
+
homepage_uri: https://github.com/botandrose/turbo-hmr
|
|
80
|
+
rdoc_options: []
|
|
81
|
+
require_paths:
|
|
82
|
+
- lib
|
|
83
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
84
|
+
requirements:
|
|
85
|
+
- - ">="
|
|
86
|
+
- !ruby/object:Gem::Version
|
|
87
|
+
version: 3.2.0
|
|
88
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
89
|
+
requirements:
|
|
90
|
+
- - ">="
|
|
91
|
+
- !ruby/object:Gem::Version
|
|
92
|
+
version: '0'
|
|
93
|
+
requirements: []
|
|
94
|
+
rubygems_version: 3.6.9
|
|
95
|
+
specification_version: 4
|
|
96
|
+
summary: Hot Module Replacement for Turbo
|
|
97
|
+
test_files: []
|