Skip to content

Commit

Permalink
Rework stimulus reloader
Browse files Browse the repository at this point in the history
WIP pending to rebase and describe
  • Loading branch information
jorgemanrubia committed Dec 23, 2024
1 parent 0ae1c57 commit e31a423
Show file tree
Hide file tree
Showing 14 changed files with 134 additions and 133 deletions.
104 changes: 47 additions & 57 deletions app/assets/javascripts/hotwire_spark.js
Original file line number Diff line number Diff line change
Expand Up @@ -1396,71 +1396,60 @@ var HotwireSpark = (function () {
}

class StimulusReloader {
static async reload(filePattern) {
const document = await reloadHtmlDocument();
return new StimulusReloader(document, filePattern).reload();
static async reload(path) {
return new StimulusReloader(path).reload();
}
constructor(document) {
let filePattern = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : /./;
this.document = document;
this.filePattern = filePattern;
static async reloadAll() {
Stimulus.controllers.forEach(controller => {
Stimulus.unload(controller.identifier);
Stimulus.register(controller.identifier, controller.constructor);
});
return Promise.resolve();
}
constructor(changedPath) {
this.changedPath = changedPath;
this.application = window.Stimulus;
}
async reload() {
log("Reload Stimulus controllers...");
this.application.stop();
await this.#reloadChangedStimulusControllers();
this.#unloadDeletedStimulusControllers();
try {
await this.#reloadChangedController();
} catch (SourceFileNotFound) {
this.#deregisterChangedController();
}
this.application.start();
}
async #reloadChangedStimulusControllers() {
await Promise.all(this.#stimulusControllerPathsToReload.map(async moduleName => this.#reloadStimulusController(moduleName)));
}
get #stimulusControllerPathsToReload() {
this.controllerPathsToReload = this.controllerPathsToReload || this.#stimulusControllerPaths.filter(path => this.#shouldReloadController(path));
return this.controllerPathsToReload;
}
get #stimulusControllerPaths() {
return Object.keys(this.#stimulusPathsByModule).filter(path => path.endsWith("_controller"));
}
#shouldReloadController(path) {
return this.filePattern.test(path);
async #reloadChangedController() {
const module = await this.#importControllerFromSource(this.changedPath);
await this.#registerController(this.#changedControllerIdentifier, module);
}
get #stimulusPathsByModule() {
this.pathsByModule = this.pathsByModule || this.#parseImportmapJson();
return this.pathsByModule;
}
#parseImportmapJson() {
const importmapScript = this.document.querySelector("script[type=importmap]");
return JSON.parse(importmapScript.text).imports;
}
async #reloadStimulusController(moduleName) {
log(`\t${moduleName}`);
const controllerName = this.#extractControllerName(moduleName);
const path = cacheBustedUrl(this.#pathForModuleName(moduleName));
const module = await import(path);
this.#registerController(controllerName, module);
}
#unloadDeletedStimulusControllers() {
this.#controllersToUnload.forEach(controller => this.#deregisterController(controller.identifier));
}
get #controllersToUnload() {
if (this.#didChangeTriggerAReload) {
return [];
} else {
return this.application.controllers.filter(controller => this.filePattern.test(`${controller.identifier}_controller`));
async #importControllerFromSource(path) {
const response = await fetch(`/spark/source_files/?path=${path}`);
if (response.status === 404) {
throw new SourceFileNotFound(`Source file not found: ${path}`);
}
const sourceCode = await response.text();
const blob = new Blob([sourceCode], {
type: "application/javascript"
});
const moduleUrl = URL.createObjectURL(blob);
const module = await import(moduleUrl);
URL.revokeObjectURL(moduleUrl);
return module;
}
get #didChangeTriggerAReload() {
return this.#stimulusControllerPathsToReload.length > 0;
}
#pathForModuleName(moduleName) {
return this.#stimulusPathsByModule[moduleName];
get #changedControllerIdentifier() {
this.changedControllerIdentifier = this.changedControllerIdentifier || this.#extractControllerName(this.changedPath);
return this.changedControllerIdentifier;
}
#extractControllerName(path) {
return path.replace(/^.*\//, "").replace("_controller", "").replace(/\//g, "--").replace(/_/g, "-");
return path.replace(/^.*\//, "").replace("_controller", "").replace(/\//g, "--").replace(/_/g, "-").replace(/\.js$/, "");
}
#deregisterChangedController() {
this.#deregisterController(this.#changedControllerIdentifier);
}
#registerController(name, module) {
log("\tReloading controller", name);
this.application.unload(name);
this.application.register(name, module.default);
}
Expand All @@ -1469,14 +1458,15 @@ var HotwireSpark = (function () {
this.application.unload(name);
}
}
class SourceFileNotFound extends Error {}

class HtmlReloader {
static async reload() {
return new HtmlReloader().reload();
}
async reload() {
const reloadedDocument = await this.#reloadHtml();
await this.#reloadStimulus(reloadedDocument);
await this.#reloadHtml();
await this.#reloadStimulus();
}
async #reloadHtml() {
log("Reload html...");
Expand All @@ -1487,8 +1477,8 @@ var HotwireSpark = (function () {
#updateBody(newBody) {
Idiomorph.morph(document.body, newBody);
}
async #reloadStimulus(reloadedDocument) {
return new StimulusReloader(reloadedDocument).reload();
async #reloadStimulus() {
await StimulusReloader.reloadAll();
}
}

Expand Down Expand Up @@ -1573,7 +1563,7 @@ var HotwireSpark = (function () {
case "reload_css":
return this.reloadCss(fileName);
case "reload_stimulus":
return this.reloadStimulus(fileName);
return this.reloadStimulus(path);
default:
throw new Error(`Unknown action: ${action}`);
}
Expand All @@ -1584,8 +1574,8 @@ var HotwireSpark = (function () {
reloadCss(fileName) {
return CssReloader.reload(new RegExp(fileName));
},
reloadStimulus(fileName) {
return StimulusReloader.reload(new RegExp(fileName));
reloadStimulus(path) {
return StimulusReloader.reload(path);
}
});

Expand Down
2 changes: 1 addition & 1 deletion app/assets/javascripts/hotwire_spark.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion app/assets/javascripts/hotwire_spark.min.js.map

Large diffs are not rendered by default.

14 changes: 14 additions & 0 deletions app/controllers/hotwire/spark/source_files_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
class Hotwire::Spark::SourceFilesController < ActionController::Base
def show
if File.exist?(path_param)
render plain: File.read(path_param)
else
head :not_found
end
end

private
def path_param
Rails.root.join params[:path]
end
end
6 changes: 3 additions & 3 deletions app/javascript/hotwire/spark/channels/monitoring_channel.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ consumer.subscriptions.create({ channel: "Hotwire::Spark::Channel" }, {
case "reload_css":
return this.reloadCss(fileName)
case "reload_stimulus":
return this.reloadStimulus(fileName)
return this.reloadStimulus(path)
default:
throw new Error(`Unknown action: ${action}`)
}
Expand All @@ -40,8 +40,8 @@ consumer.subscriptions.create({ channel: "Hotwire::Spark::Channel" }, {
return CssReloader.reload(new RegExp(fileName))
},

reloadStimulus(fileName) {
return StimulusReloader.reload(new RegExp(fileName))
reloadStimulus(path) {
return StimulusReloader.reload(path)
}
})

8 changes: 4 additions & 4 deletions app/javascript/hotwire/spark/reloaders/html_reloader.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ export class HtmlReloader {
}

async reload() {
const reloadedDocument = await this.#reloadHtml()
await this.#reloadStimulus(reloadedDocument)
await this.#reloadHtml()
await this.#reloadStimulus()
}

async #reloadHtml() {
Expand All @@ -25,7 +25,7 @@ export class HtmlReloader {
Idiomorph.morph(document.body, newBody)
}

async #reloadStimulus(reloadedDocument) {
return new StimulusReloader(reloadedDocument).reload()
async #reloadStimulus() {
await StimulusReloader.reloadAll()
}
}
104 changes: 45 additions & 59 deletions app/javascript/hotwire/spark/reloaders/stimulus_reloader.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
import { log } from "../logger.js"
import { cacheBustedUrl, reloadHtmlDocument } from "../helpers.js"

export class StimulusReloader {
static async reload(filePattern) {
const document = await reloadHtmlDocument()
return new StimulusReloader(document, filePattern).reload()
static async reload(path) {
return new StimulusReloader(path).reload()
}

constructor(document, filePattern = /./) {
this.document = document
this.filePattern = filePattern
static async reloadAll() {
Stimulus.controllers.forEach(controller => {
Stimulus.unload(controller.identifier)
Stimulus.register(controller.identifier, controller.constructor)
})

return Promise.resolve()
}

constructor(changedPath) {
this.changedPath = changedPath
this.application = window.Stimulus
}

Expand All @@ -18,70 +24,41 @@ export class StimulusReloader {

this.application.stop()

await this.#reloadChangedStimulusControllers()
this.#unloadDeletedStimulusControllers()
try {
await this.#reloadChangedController()
}
catch(SourceFileNotFound) {
this.#deregisterChangedController()
}

this.application.start()
}

async #reloadChangedStimulusControllers() {
await Promise.all(
this.#stimulusControllerPathsToReload.map(async moduleName => this.#reloadStimulusController(moduleName))
)
}

get #stimulusControllerPathsToReload() {
this.controllerPathsToReload = this.controllerPathsToReload || this.#stimulusControllerPaths.filter(path => this.#shouldReloadController(path))
return this.controllerPathsToReload
}

get #stimulusControllerPaths() {
return Object.keys(this.#stimulusPathsByModule).filter(path => path.endsWith("_controller"))
}

#shouldReloadController(path) {
return this.filePattern.test(path)
}

get #stimulusPathsByModule() {
this.pathsByModule = this.pathsByModule || this.#parseImportmapJson()
return this.pathsByModule
async #reloadChangedController() {
const module = await this.#importControllerFromSource(this.changedPath)
await this.#registerController(this.#changedControllerIdentifier, module)
}

#parseImportmapJson() {
const importmapScript = this.document.querySelector("script[type=importmap]")
return JSON.parse(importmapScript.text).imports
}

async #reloadStimulusController(moduleName) {
log(`\t${moduleName}`)
async #importControllerFromSource(path) {
const response = await fetch(`/spark/source_files/?path=${path}`)

const controllerName = this.#extractControllerName(moduleName)
const path = cacheBustedUrl(this.#pathForModuleName(moduleName))
if (response.status === 404) {
throw new SourceFileNotFound(`Source file not found: ${path}`)
}

const module = await import(path)
const sourceCode = await response.text()

this.#registerController(controllerName, module)
}
const blob = new Blob([sourceCode], { type: "application/javascript" })
const moduleUrl = URL.createObjectURL(blob)
const module = await import(moduleUrl)
URL.revokeObjectURL(moduleUrl)

#unloadDeletedStimulusControllers() {
this.#controllersToUnload.forEach(controller => this.#deregisterController(controller.identifier))
}

get #controllersToUnload() {
if (this.#didChangeTriggerAReload) {
return []
} else {
return this.application.controllers.filter(controller => this.filePattern.test(`${controller.identifier}_controller`))
}
return module
}

get #didChangeTriggerAReload() {
return this.#stimulusControllerPathsToReload.length > 0
}

#pathForModuleName(moduleName) {
return this.#stimulusPathsByModule[moduleName]
get #changedControllerIdentifier() {
this.changedControllerIdentifier = this.changedControllerIdentifier || this.#extractControllerName(this.changedPath)
return this.changedControllerIdentifier
}

#extractControllerName(path) {
Expand All @@ -90,9 +67,16 @@ export class StimulusReloader {
.replace("_controller", "")
.replace(/\//g, "--")
.replace(/_/g, "-")
.replace(/\.js$/, "")
}

#deregisterChangedController() {
this.#deregisterController(this.#changedControllerIdentifier)
}

#registerController(name, module) {
log("\tReloading controller", name)

this.application.unload(name)
this.application.register(name, module.default)
}
Expand All @@ -102,3 +86,5 @@ export class StimulusReloader {
this.application.unload(name)
}
}

class SourceFileNotFound extends Error { }
1 change: 1 addition & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
Hotwire::Spark::Engine.routes.draw do
get "/source_files", to: "source_files#show"
end
2 changes: 1 addition & 1 deletion lib/hotwire-spark.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def install_into(application)
end

def enabled?
enabled && defined?(Rails::Server)
enabled && Rails.env.test?
end

def cable_server
Expand Down
6 changes: 5 additions & 1 deletion lib/hotwire/spark/file_watcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,13 @@ def process_changed_files(changed_files)
changed_files.each do |file|
@callbacks_by_path.each do |path, callbacks|
if file.to_s.start_with?(path.to_s)
callbacks.each { |callback| callback.call(file) }
callbacks.each { |callback| callback.call(as_relative_path(file)) }
end
end
end
end

def as_relative_path(path)
Pathname.new(path).relative_path_from(Rails.application.root)
end
end
Loading

0 comments on commit e31a423

Please sign in to comment.