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 24, 2024
1 parent 0ae1c57 commit 77b2c69
Show file tree
Hide file tree
Showing 13 changed files with 139 additions and 139 deletions.
110 changes: 50 additions & 60 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);
}
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}`);
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 @@ -1566,26 +1556,26 @@ var HotwireSpark = (function () {
action,
path
} = _ref;
const fileName = assetNameFromPath(path);
switch (action) {
case "reload_html":
return this.reloadHtml();
case "reload_css":
return this.reloadCss(fileName);
return this.reloadCss(path);
case "reload_stimulus":
return this.reloadStimulus(fileName);
return this.reloadStimulus(path);
default:
throw new Error(`Unknown action: ${action}`);
}
},
reloadHtml() {
return HtmlReloader.reload();
},
reloadCss(fileName) {
reloadCss(path) {
const fileName = assetNameFromPath(path);
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
13 changes: 6 additions & 7 deletions app/javascript/hotwire/spark/channels/monitoring_channel.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,13 @@ consumer.subscriptions.create({ channel: "Hotwire::Spark::Channel" }, {
},

dispatch({ action, path }) {
const fileName = assetNameFromPath(path)

switch(action) {
case "reload_html":
return this.reloadHtml()
case "reload_css":
return this.reloadCss(fileName)
return this.reloadCss(path)
case "reload_stimulus":
return this.reloadStimulus(fileName)
return this.reloadStimulus(path)
default:
throw new Error(`Unknown action: ${action}`)
}
Expand All @@ -36,12 +34,13 @@ consumer.subscriptions.create({ channel: "Hotwire::Spark::Channel" }, {
return HtmlReloader.reload()
},

reloadCss(fileName) {
reloadCss(path) {
const fileName = assetNameFromPath(path)
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
Loading

0 comments on commit 77b2c69

Please sign in to comment.