hotwire-spark 0.1.9 → 0.1.10

Sign up to get free protection for your applications and to get access to all the features.
@@ -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