hotwire-spark 0.1.8 → 0.1.10

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