hotwire-spark 0.1.9 → 0.1.10

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.
@@ -0,0 +1,31 @@
1
+ import { log } from "../logger.js"
2
+
3
+ export class ReplaceHtmlReloader {
4
+ static async reload() {
5
+ return new ReplaceHtmlReloader().reload()
6
+ }
7
+
8
+ async reload() {
9
+ await this.#reloadHtml()
10
+ }
11
+
12
+ async #reloadHtml() {
13
+ log("Reload html with Turbo...")
14
+
15
+ this.#maintainScrollPosition()
16
+ await this.#visitCurrentPage()
17
+ }
18
+
19
+ #maintainScrollPosition() {
20
+ document.addEventListener("turbo:before-render", () => {
21
+ Turbo.navigator.currentVisit.scrolled = true
22
+ }, { once: true })
23
+ }
24
+
25
+ #visitCurrentPage() {
26
+ return new Promise(resolve => {
27
+ document.addEventListener("turbo:load", () => resolve(document), { once: true })
28
+ window.Turbo.visit(window.location)
29
+ })
30
+ }
31
+ }
@@ -1,15 +1,21 @@
1
1
  import { log } from "../logger.js"
2
- import { cacheBustedUrl, reloadHtmlDocument } from "../helpers.js"
3
2
 
4
3
  export class StimulusReloader {
5
- static async reload(filePattern) {
6
- const document = await reloadHtmlDocument()
7
- return new StimulusReloader(document, filePattern).reload()
4
+ static async reload(path) {
5
+ return new StimulusReloader(path).reload()
8
6
  }
9
7
 
10
- constructor(document, filePattern = /./) {
11
- this.document = document
12
- this.filePattern = filePattern
8
+ static async reloadAll() {
9
+ Stimulus.controllers.forEach(controller => {
10
+ Stimulus.unload(controller.identifier)
11
+ Stimulus.register(controller.identifier, controller.constructor)
12
+ })
13
+
14
+ return Promise.resolve()
15
+ }
16
+
17
+ constructor(changedPath) {
18
+ this.changedPath = changedPath
13
19
  this.application = window.Stimulus
14
20
  }
15
21
 
@@ -18,70 +24,45 @@ export class StimulusReloader {
18
24
 
19
25
  this.application.stop()
20
26
 
21
- await this.#reloadChangedStimulusControllers()
22
- this.#unloadDeletedStimulusControllers()
27
+ try {
28
+ await this.#reloadChangedController()
29
+ }
30
+ catch(error) {
31
+ if (error instanceof SourceFileNotFound) {
32
+ this.#deregisterChangedController()
33
+ } else {
34
+ console.error("Error reloading controller", error)
35
+ }
36
+ }
23
37
 
24
38
  this.application.start()
25
39
  }
26
40
 
27
- async #reloadChangedStimulusControllers() {
28
- await Promise.all(
29
- this.#stimulusControllerPathsToReload.map(async moduleName => this.#reloadStimulusController(moduleName))
30
- )
31
- }
32
-
33
- get #stimulusControllerPathsToReload() {
34
- this.controllerPathsToReload = this.controllerPathsToReload || this.#stimulusControllerPaths.filter(path => this.#shouldReloadController(path))
35
- return this.controllerPathsToReload
36
- }
37
-
38
- get #stimulusControllerPaths() {
39
- return Object.keys(this.#stimulusPathsByModule).filter(path => path.endsWith("_controller"))
40
- }
41
-
42
- #shouldReloadController(path) {
43
- return this.filePattern.test(path)
44
- }
45
-
46
- get #stimulusPathsByModule() {
47
- this.pathsByModule = this.pathsByModule || this.#parseImportmapJson()
48
- return this.pathsByModule
41
+ async #reloadChangedController() {
42
+ const module = await this.#importControllerFromSource(this.changedPath)
43
+ await this.#registerController(this.#changedControllerIdentifier, module)
49
44
  }
50
45
 
51
- #parseImportmapJson() {
52
- const importmapScript = this.document.querySelector("script[type=importmap]")
53
- return JSON.parse(importmapScript.text).imports
54
- }
55
-
56
- async #reloadStimulusController(moduleName) {
57
- log(`\t${moduleName}`)
46
+ async #importControllerFromSource(path) {
47
+ const response = await fetch(`/spark/source_files/?path=${path}`)
58
48
 
59
- const controllerName = this.#extractControllerName(moduleName)
60
- const path = cacheBustedUrl(this.#pathForModuleName(moduleName))
49
+ if (response.status === 404) {
50
+ throw new SourceFileNotFound(`Source file not found: ${path}`)
51
+ }
61
52
 
62
- const module = await import(path)
53
+ const sourceCode = await response.text()
63
54
 
64
- this.#registerController(controllerName, module)
65
- }
55
+ const blob = new Blob([sourceCode], { type: "application/javascript" })
56
+ const moduleUrl = URL.createObjectURL(blob)
57
+ const module = await import(moduleUrl)
58
+ URL.revokeObjectURL(moduleUrl)
66
59
 
67
- #unloadDeletedStimulusControllers() {
68
- this.#controllersToUnload.forEach(controller => this.#deregisterController(controller.identifier))
69
- }
70
-
71
- get #controllersToUnload() {
72
- if (this.#didChangeTriggerAReload) {
73
- return []
74
- } else {
75
- return this.application.controllers.filter(controller => this.filePattern.test(`${controller.identifier}_controller`))
76
- }
60
+ return module
77
61
  }
78
62
 
79
- get #didChangeTriggerAReload() {
80
- return this.#stimulusControllerPathsToReload.length > 0
81
- }
82
-
83
- #pathForModuleName(moduleName) {
84
- return this.#stimulusPathsByModule[moduleName]
63
+ get #changedControllerIdentifier() {
64
+ this.changedControllerIdentifier = this.changedControllerIdentifier || this.#extractControllerName(this.changedPath)
65
+ return this.changedControllerIdentifier
85
66
  }
86
67
 
87
68
  #extractControllerName(path) {
@@ -90,9 +71,16 @@ export class StimulusReloader {
90
71
  .replace("_controller", "")
91
72
  .replace(/\//g, "--")
92
73
  .replace(/_/g, "-")
74
+ .replace(/\.js$/, "")
75
+ }
76
+
77
+ #deregisterChangedController() {
78
+ this.#deregisterController(this.#changedControllerIdentifier)
93
79
  }
94
80
 
95
81
  #registerController(name, module) {
82
+ log("\tReloading controller", name)
83
+
96
84
  this.application.unload(name)
97
85
  this.application.register(name, module.default)
98
86
  }
@@ -102,3 +90,5 @@ export class StimulusReloader {
102
90
  this.application.unload(name)
103
91
  }
104
92
  }
93
+
94
+ class SourceFileNotFound extends Error { }
data/config/routes.rb CHANGED
@@ -1,2 +1,3 @@
1
1
  Hotwire::Spark::Engine.routes.draw do
2
+ get "/source_files", to: "source_files#show"
2
3
  end
@@ -0,0 +1,36 @@
1
+ class Hotwire::Spark::DefaultOptions
2
+ def initialize
3
+ @config = base_options
4
+
5
+ build
6
+ end
7
+
8
+ def to_h
9
+ @config
10
+ end
11
+
12
+ private
13
+ def base_options
14
+ {
15
+ enabled: Rails.env.development?,
16
+ css_paths: File.directory?("app/assets/builds") ? %w[ app/assets/builds ] : %w[ app/assets/stylesheets ],
17
+ css_extensions: %w[ css ],
18
+ html_paths: %w[ app/controllers app/helpers app/models app/views ],
19
+ html_extensions: %w[ rb erb ],
20
+ stimulus_paths: %w[ app/javascript/controllers ],
21
+ stimulus_extensions: %w[ js ],
22
+ html_reload_method: :morph
23
+ }
24
+ end
25
+
26
+ def build
27
+ configure_jsbundling if defined?(Jsbundling)
28
+ end
29
+
30
+ def configure_jsbundling
31
+ @config[:stimulus_paths] = []
32
+ @config[:html_paths] << "app/assets/builds"
33
+ @config[:html_extensions] << "js"
34
+ @config[:html_reload_method] = :replace
35
+ end
36
+ end
@@ -1,4 +1,5 @@
1
1
  require "action_cable/server/base"
2
+ require "hotwire/spark/default_options"
2
3
 
3
4
  module Hotwire::Spark
4
5
  class Engine < ::Rails::Engine
@@ -6,11 +7,7 @@ module Hotwire::Spark
6
7
 
7
8
  config.hotwire = ActiveSupport::OrderedOptions.new unless config.respond_to?(:hotwire)
8
9
  config.hotwire.spark = ActiveSupport::OrderedOptions.new
9
- config.hotwire.spark.merge! \
10
- enabled: Rails.env.development?,
11
- css_paths: File.directory?("app/assets/builds") ? %w[ app/assets/builds ] : %w[ app/assets/stylesheets ],
12
- html_paths: %w[ app/controllers app/helpers app/models app/views ],
13
- stimulus_paths: %w[ app/javascript/controllers ]
10
+ config.hotwire.spark.merge! Hotwire::Spark::DefaultOptions.new.to_h
14
11
 
15
12
  initializer "hotwire_spark.config" do |application|
16
13
  config.hotwire.spark.each do |key, value|
@@ -36,9 +36,13 @@ class Hotwire::Spark::FileWatcher
36
36
  changed_files.each do |file|
37
37
  @callbacks_by_path.each do |path, callbacks|
38
38
  if file.to_s.start_with?(path.to_s)
39
- callbacks.each { |callback| callback.call(file) }
39
+ callbacks.each { |callback| callback.call(as_relative_path(file)) }
40
40
  end
41
41
  end
42
42
  end
43
43
  end
44
+
45
+ def as_relative_path(path)
46
+ Pathname.new(path).relative_path_from(Rails.application.root)
47
+ end
44
48
  end
@@ -5,6 +5,7 @@ class Hotwire::Spark::Installer
5
5
 
6
6
  def install
7
7
  configure_cable_server
8
+ configure_routes
8
9
  configure_middleware
9
10
  monitor_paths
10
11
  end
@@ -19,6 +20,12 @@ class Hotwire::Spark::Installer
19
20
  end
20
21
  end
21
22
 
23
+ def configure_routes
24
+ application.routes.prepend do
25
+ mount Hotwire::Spark::Engine => "/spark", as: "hotwire_spark"
26
+ end
27
+ end
28
+
22
29
  def configure_middleware
23
30
  middleware.use Hotwire::Spark::Middleware
24
31
  end
@@ -29,14 +36,18 @@ class Hotwire::Spark::Installer
29
36
  end
30
37
 
31
38
  def register_monitored_paths
32
- monitor :css_paths, action: :reload_css
33
- monitor :html_paths, action: :reload_html
34
- monitor :stimulus_paths, action: :reload_stimulus
39
+ monitor :css_paths, action: :reload_css, extensions: Hotwire::Spark.css_extensions
40
+ monitor :html_paths, action: :reload_html, extensions: Hotwire::Spark.html_extensions
41
+ monitor :stimulus_paths, action: :reload_stimulus, extensions: Hotwire::Spark.stimulus_extensions
35
42
  end
36
43
 
37
- def monitor(paths_name, action:)
38
- file_watcher.monitor Hotwire::Spark.public_send(paths_name) do |file_path|
39
- broadcast_reload_action(action, file_path)
44
+ def monitor(paths_name, action:, extensions:)
45
+ paths = Hotwire::Spark.public_send(paths_name)
46
+ if paths.present?
47
+ file_watcher.monitor paths do |file_path|
48
+ pattern = /#{extensions.map { |ext| "\\." + ext }.join("|")}$/
49
+ broadcast_reload_action(action, file_path) if file_path.to_s =~ pattern
50
+ end
40
51
  end
41
52
  end
42
53
 
@@ -7,6 +7,7 @@ class Hotwire::Spark::Middleware
7
7
  status, headers, response = @app.call(env)
8
8
 
9
9
  if html_response?(headers)
10
+ @request = ActionDispatch::Request.new(env)
10
11
  html = html_from(response)
11
12
  html = inject_javascript(html)
12
13
  html = inject_options(html)
@@ -38,18 +39,22 @@ class Hotwire::Spark::Middleware
38
39
  end
39
40
 
40
41
  def view_helpers
41
- ActionController::Base.helpers
42
+ @request.controller_instance.helpers
42
43
  end
43
44
 
44
45
  def inject_options(html)
45
- if Hotwire::Spark.logging
46
- html.sub("</head>", "#{logging_option}</head>")
47
- else
48
- html
49
- end
46
+ html.sub("</head>", "#{options}</head>")
47
+ end
48
+
49
+ def options
50
+ [ logging_option, html_reload_method_option ].compact.join("\n")
50
51
  end
51
52
 
52
53
  def logging_option
53
- view_helpers.tag.meta(name: "hotwire-spark:logging", content: "true")
54
+ view_helpers.tag.meta(name: "hotwire-spark:logging", content: "true") if Hotwire::Spark.logging
55
+ end
56
+
57
+ def html_reload_method_option
58
+ view_helpers.tag.meta(name: "hotwire-spark:html-reload-method", content: Hotwire::Spark.html_reload_method)
54
59
  end
55
60
  end
@@ -1,5 +1,5 @@
1
1
  module Hotwire
2
2
  module Spark
3
- VERSION = "0.1.9"
3
+ VERSION = "0.1.10"
4
4
  end
5
5
  end
data/lib/hotwire-spark.rb CHANGED
@@ -8,10 +8,13 @@ loader.ignore("#{__dir__}/hotwire/spark/version.rb")
8
8
  loader.setup
9
9
 
10
10
  module Hotwire::Spark
11
- mattr_accessor :css_paths, default: []
12
- mattr_accessor :html_paths, default: []
13
- mattr_accessor :stimulus_paths, default: []
11
+ %i[ css html stimulus ].each do |type|
12
+ mattr_accessor "#{type}_paths".to_sym, default: []
13
+ mattr_accessor "#{type}_extensions".to_sym, default: []
14
+ end
15
+
14
16
  mattr_accessor :logging, default: false
17
+ mattr_accessor :html_reload_method, default: :morph
15
18
 
16
19
  mattr_accessor :enabled, default: Rails.env.development?
17
20
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hotwire-spark
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.9
4
+ version: 0.1.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jorge Manrubia
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-12-22 00:00:00.000000000 Z
11
+ date: 2024-12-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 8.0.0
19
+ version: 7.0.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: 8.0.0
26
+ version: 7.0.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: zeitwerk
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -125,17 +125,20 @@ files:
125
125
  - app/assets/javascripts/hotwire_spark.min.js.map
126
126
  - app/assets/stylesheets/hotwire_spark/application.css
127
127
  - app/channels/hotwire/spark/channel.rb
128
+ - app/controllers/hotwire/spark/source_files_controller.rb
128
129
  - app/javascript/hotwire/spark/channels/consumer.js
129
130
  - app/javascript/hotwire/spark/channels/monitoring_channel.js
130
131
  - app/javascript/hotwire/spark/helpers.js
131
132
  - app/javascript/hotwire/spark/index.js
132
133
  - app/javascript/hotwire/spark/logger.js
133
134
  - app/javascript/hotwire/spark/reloaders/css_reloader.js
134
- - app/javascript/hotwire/spark/reloaders/html_reloader.js
135
+ - app/javascript/hotwire/spark/reloaders/morph_html_reloader.js
136
+ - app/javascript/hotwire/spark/reloaders/replace_html_reloader.js
135
137
  - app/javascript/hotwire/spark/reloaders/stimulus_reloader.js
136
138
  - config/routes.rb
137
139
  - lib/hotwire-spark.rb
138
140
  - lib/hotwire/spark/action_cable/server.rb
141
+ - lib/hotwire/spark/default_options.rb
139
142
  - lib/hotwire/spark/engine.rb
140
143
  - lib/hotwire/spark/file_watcher.rb
141
144
  - lib/hotwire/spark/installer.rb