hotwire-spark 0.1.8 → 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,14 @@
1
+ class Hotwire::Spark::SourceFilesController < ActionController::Base
2
+ def show
3
+ if File.exist?(path_param)
4
+ render plain: File.read(path_param)
5
+ else
6
+ head :not_found
7
+ end
8
+ end
9
+
10
+ private
11
+ def path_param
12
+ Rails.root.join params[:path]
13
+ end
14
+ end
@@ -1,8 +1,9 @@
1
1
  import consumer from "./consumer"
2
2
  import { assetNameFromPath } from "../helpers.js";
3
- import { HtmlReloader } from "../reloaders/html_reloader.js";
3
+ import { MorphHtmlReloader } from "../reloaders/morph_html_reloader.js";
4
4
  import { CssReloader } from "../reloaders/css_reloader.js";
5
5
  import { StimulusReloader } from "../reloaders/stimulus_reloader.js";
6
+ import { ReplaceHtmlReloader } from "../reloaders/replace_html_reloader.js";
6
7
 
7
8
  consumer.subscriptions.create({ channel: "Hotwire::Spark::Channel" }, {
8
9
  connected() {
@@ -18,30 +19,30 @@ consumer.subscriptions.create({ channel: "Hotwire::Spark::Channel" }, {
18
19
  },
19
20
 
20
21
  dispatch({ action, path }) {
21
- const fileName = assetNameFromPath(path)
22
-
23
22
  switch(action) {
24
23
  case "reload_html":
25
24
  return this.reloadHtml()
26
25
  case "reload_css":
27
- return this.reloadCss(fileName)
26
+ return this.reloadCss(path)
28
27
  case "reload_stimulus":
29
- return this.reloadStimulus(fileName)
28
+ return this.reloadStimulus(path)
30
29
  default:
31
30
  throw new Error(`Unknown action: ${action}`)
32
31
  }
33
32
  },
34
33
 
35
34
  reloadHtml() {
36
- return HtmlReloader.reload()
35
+ const htmlReloader = HotwireSpark.config.htmlReloadMethod == "morph" ? MorphHtmlReloader : ReplaceHtmlReloader
36
+ return htmlReloader.reload()
37
37
  },
38
38
 
39
- reloadCss(fileName) {
39
+ reloadCss(path) {
40
+ const fileName = assetNameFromPath(path)
40
41
  return CssReloader.reload(new RegExp(fileName))
41
42
  },
42
43
 
43
- reloadStimulus(fileName) {
44
- return StimulusReloader.reload(new RegExp(fileName))
44
+ reloadStimulus(path) {
45
+ return StimulusReloader.reload(path)
45
46
  }
46
47
  })
47
48
 
@@ -3,12 +3,20 @@ import { getConfigurationProperty } from "./helpers.js";
3
3
 
4
4
  const HotwireSpark = {
5
5
  config: {
6
- loggingEnabled: false
6
+ loggingEnabled: false,
7
+ htmlReloadMethod: "morph"
7
8
  }
8
9
  }
9
10
 
11
+ const configProperties = {
12
+ loggingEnabled: "logging",
13
+ htmlReloadMethod: "html-reload-method",
14
+ }
15
+
10
16
  document.addEventListener("DOMContentLoaded", function() {
11
- HotwireSpark.config.loggingEnabled = getConfigurationProperty("logging");
17
+ Object.entries(configProperties).forEach(([key, property]) => {
18
+ HotwireSpark.config[key] = getConfigurationProperty(property)
19
+ })
12
20
  })
13
21
 
14
22
  export default HotwireSpark
@@ -1,20 +1,20 @@
1
1
  import { Idiomorph } from "idiomorph/dist/idiomorph.esm.js"
2
- import { log } from "../logger.js"
3
2
  import { reloadHtmlDocument } from "../helpers.js"
3
+ import { log } from "../logger.js"
4
4
  import { StimulusReloader } from "./stimulus_reloader.js"
5
5
 
6
- export class HtmlReloader {
6
+ export class MorphHtmlReloader {
7
7
  static async reload() {
8
- return new HtmlReloader().reload()
8
+ return new MorphHtmlReloader().reload()
9
9
  }
10
10
 
11
11
  async reload() {
12
- const reloadedDocument = await this.#reloadHtml()
13
- await this.#reloadStimulus(reloadedDocument)
12
+ await this.#reloadHtml()
13
+ await this.#reloadStimulus()
14
14
  }
15
15
 
16
16
  async #reloadHtml() {
17
- log("Reload html...")
17
+ log("Reload html with morph...")
18
18
 
19
19
  const reloadedDocument = await reloadHtmlDocument()
20
20
  this.#updateBody(reloadedDocument.body)
@@ -25,7 +25,7 @@ export class HtmlReloader {
25
25
  Idiomorph.morph(document.body, newBody)
26
26
  }
27
27
 
28
- async #reloadStimulus(reloadedDocument) {
29
- return new StimulusReloader(reloadedDocument).reload()
28
+ async #reloadStimulus() {
29
+ await StimulusReloader.reloadAll()
30
30
  }
31
31
  }
@@ -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,17 +1,22 @@
1
- import { Application } from "@hotwired/stimulus"
2
1
  import { log } from "../logger.js"
3
- import { cacheBustedUrl, reloadHtmlDocument } from "../helpers.js"
4
2
 
5
3
  export class StimulusReloader {
6
- static async reload(filePattern) {
7
- const document = await reloadHtmlDocument()
8
- return new StimulusReloader(document, filePattern).reload()
4
+ static async reload(path) {
5
+ return new StimulusReloader(path).reload()
9
6
  }
10
7
 
11
- constructor(document, filePattern = /./) {
12
- this.document = document
13
- this.filePattern = filePattern
14
- this.application = window.Stimulus || Application.start()
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
19
+ this.application = window.Stimulus
15
20
  }
16
21
 
17
22
  async reload() {
@@ -19,70 +24,45 @@ export class StimulusReloader {
19
24
 
20
25
  this.application.stop()
21
26
 
22
- await this.#reloadChangedStimulusControllers()
23
- 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
+ }
24
37
 
25
38
  this.application.start()
26
39
  }
27
40
 
28
- async #reloadChangedStimulusControllers() {
29
- await Promise.all(
30
- this.#stimulusControllerPathsToReload.map(async moduleName => this.#reloadStimulusController(moduleName))
31
- )
32
- }
33
-
34
- get #stimulusControllerPathsToReload() {
35
- this.controllerPathsToReload = this.controllerPathsToReload || this.#stimulusControllerPaths.filter(path => this.#shouldReloadController(path))
36
- return this.controllerPathsToReload
37
- }
38
-
39
- get #stimulusControllerPaths() {
40
- return Object.keys(this.#stimulusPathsByModule).filter(path => path.endsWith("_controller"))
41
- }
42
-
43
- #shouldReloadController(path) {
44
- return this.filePattern.test(path)
45
- }
46
-
47
- get #stimulusPathsByModule() {
48
- this.pathsByModule = this.pathsByModule || this.#parseImportmapJson()
49
- return this.pathsByModule
41
+ async #reloadChangedController() {
42
+ const module = await this.#importControllerFromSource(this.changedPath)
43
+ await this.#registerController(this.#changedControllerIdentifier, module)
50
44
  }
51
45
 
52
- #parseImportmapJson() {
53
- const importmapScript = this.document.querySelector("script[type=importmap]")
54
- return JSON.parse(importmapScript.text).imports
55
- }
56
-
57
- async #reloadStimulusController(moduleName) {
58
- log(`\t${moduleName}`)
46
+ async #importControllerFromSource(path) {
47
+ const response = await fetch(`/spark/source_files/?path=${path}`)
59
48
 
60
- const controllerName = this.#extractControllerName(moduleName)
61
- const path = cacheBustedUrl(this.#pathForModuleName(moduleName))
49
+ if (response.status === 404) {
50
+ throw new SourceFileNotFound(`Source file not found: ${path}`)
51
+ }
62
52
 
63
- const module = await import(path)
53
+ const sourceCode = await response.text()
64
54
 
65
- this.#registerController(controllerName, module)
66
- }
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)
67
59
 
68
- #unloadDeletedStimulusControllers() {
69
- this.#controllersToUnload.forEach(controller => this.#deregisterController(controller.identifier))
70
- }
71
-
72
- get #controllersToUnload() {
73
- if (this.#didChangeTriggerAReload) {
74
- return []
75
- } else {
76
- return this.application.controllers.filter(controller => this.filePattern.test(`${controller.identifier}_controller`))
77
- }
60
+ return module
78
61
  }
79
62
 
80
- get #didChangeTriggerAReload() {
81
- return this.#stimulusControllerPathsToReload.length > 0
82
- }
83
-
84
- #pathForModuleName(moduleName) {
85
- return this.#stimulusPathsByModule[moduleName]
63
+ get #changedControllerIdentifier() {
64
+ this.changedControllerIdentifier = this.changedControllerIdentifier || this.#extractControllerName(this.changedPath)
65
+ return this.changedControllerIdentifier
86
66
  }
87
67
 
88
68
  #extractControllerName(path) {
@@ -91,9 +71,16 @@ export class StimulusReloader {
91
71
  .replace("_controller", "")
92
72
  .replace(/\//g, "--")
93
73
  .replace(/_/g, "-")
74
+ .replace(/\.js$/, "")
75
+ }
76
+
77
+ #deregisterChangedController() {
78
+ this.#deregisterController(this.#changedControllerIdentifier)
94
79
  }
95
80
 
96
81
  #registerController(name, module) {
82
+ log("\tReloading controller", name)
83
+
97
84
  this.application.unload(name)
98
85
  this.application.register(name, module.default)
99
86
  }
@@ -103,3 +90,5 @@ export class StimulusReloader {
103
90
  this.application.unload(name)
104
91
  }
105
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
@@ -1,5 +1,5 @@
1
1
  class Hotwire::Spark::ActionCable::Server < ActionCable::Server::Base
2
- def initialize(config: nil)
2
+ def initialize
3
3
  config = ::ActionCable::Server::Base.config.dup
4
4
  config.connection_class = -> { ::ActionCable::Connection::Base }
5
5
  super(config: config)
@@ -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|
@@ -18,6 +15,10 @@ module Hotwire::Spark
18
15
  end
19
16
  end
20
17
 
18
+ initializer "hotwire_spark.assets" do |application|
19
+ application.config.assets.precompile << "hotwire_spark.js"
20
+ end
21
+
21
22
  initializer "hotwire_spark.install" do |application|
22
23
  Hotwire::Spark.install_into application if Hotwire::Spark.enabled?
23
24
  end
@@ -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
@@ -1,20 +1,15 @@
1
1
  class Hotwire::Spark::Installer
2
- attr_reader :file_watcher
3
-
4
2
  def initialize(application)
5
3
  @application = application
6
4
  end
7
5
 
8
6
  def install
9
7
  configure_cable_server
8
+ configure_routes
10
9
  configure_middleware
11
10
  monitor_paths
12
11
  end
13
12
 
14
- def configure_middleware
15
- middleware.use Hotwire::Spark::Middleware
16
- end
17
-
18
13
  private
19
14
  attr_reader :application
20
15
  delegate :middleware, to: :application
@@ -25,20 +20,34 @@ class Hotwire::Spark::Installer
25
20
  end
26
21
  end
27
22
 
23
+ def configure_routes
24
+ application.routes.prepend do
25
+ mount Hotwire::Spark::Engine => "/spark", as: "hotwire_spark"
26
+ end
27
+ end
28
+
29
+ def configure_middleware
30
+ middleware.use Hotwire::Spark::Middleware
31
+ end
32
+
28
33
  def monitor_paths
29
34
  register_monitored_paths
30
35
  file_watcher.start
31
36
  end
32
37
 
33
38
  def register_monitored_paths
34
- monitor :css_paths, action: :reload_css
35
- monitor :html_paths, action: :reload_html
36
- 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
37
42
  end
38
43
 
39
- def monitor(paths_name, action:)
40
- file_watcher.monitor Hotwire::Spark.public_send(paths_name) do |file_path|
41
- 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
42
51
  end
43
52
  end
44
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.8"
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.8
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-21 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,30 +125,33 @@ 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
142
145
  - lib/hotwire/spark/middleware.rb
143
146
  - lib/hotwire/spark/version.rb
144
147
  - lib/tasks/hotwire_spark_tasks.rake
145
- homepage: https://github.com/basecamp/hotwire_spark
148
+ homepage: https://github.com/hotwired/spark
146
149
  licenses:
147
150
  - MIT
148
151
  metadata:
149
- homepage_uri: https://github.com/basecamp/hotwire_spark
150
- source_code_uri: https://github.com/basecamp/hotwire_spark
151
- changelog_uri: https://github.com/basecamp/hotwire_spark
152
+ homepage_uri: https://github.com/hotwired/spark
153
+ source_code_uri: https://github.com/hotwired/spark
154
+ changelog_uri: https://github.com/hotwired/spark
152
155
  post_install_message:
153
156
  rdoc_options: []
154
157
  require_paths: