Skip to content

Commit

Permalink
Support jsbundling
Browse files Browse the repository at this point in the history
This reworks the approach to remove the dependency. Instead of parsing the importmaps entry, the engine will now expose an endpoint to fetch the source of changed assets. The Stimulus reloader will fetch the changed javascript files using that endpoint so that it won't depend on importmaps anymore.
  • Loading branch information
jorgemanrubia committed Dec 23, 2024
1 parent 0ae1c57 commit 58b182e
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 58b182e

Please sign in to comment.