Skip to content

Commit 6c38482

Browse files
committed
defer external script parsing until after DOMContentLoaded
Fixes #533. External behavior files loaded via <script type="text/hyperscript" src="..."> were being parsed before plugins like hdb.js could register their commands, causing "Unexpected Token: breakpoint" errors. The fix separates fetch from parse: external files are still fetched immediately for performance, but parsing is deferred until after ready() fires, giving synchronous plugin scripts time to register. Signed-off-by: André Ahlert <andre@aex.partners>
1 parent 1f751bd commit 6c38482

3 files changed

Lines changed: 66 additions & 9 deletions

File tree

src/_hyperscript.js

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7628,31 +7628,44 @@
76287628
function browserInit() {
76297629
/** @type {HTMLScriptElement[]} */
76307630
var scripts = Array.from(globalScope.document.querySelectorAll("script[type='text/hyperscript'][src]"))
7631-
Promise.all(
7631+
7632+
// Fetch external .hs files immediately so network latency overlaps
7633+
// with DOMContentLoaded. Parsing is deferred until after ready() so
7634+
// that plugins loaded via synchronous <script> tags (e.g. hdb.js)
7635+
// have a chance to register their commands before behavior files
7636+
// are evaluated. See https://github.com/bigskysoftware/_hyperscript/issues/533
7637+
var fetchedScripts = Promise.all(
76327638
scripts.map(function (script) {
76337639
return fetch(script.src)
76347640
.then(function (res) {
76357641
return res.text();
76367642
});
76377643
})
7638-
)
7639-
.then(script_values => script_values.forEach(sc => _hyperscript(sc)))
7640-
.then(() => ready(function () {
7641-
mergeMetaConfig();
7642-
runtime_.processNode(document.documentElement);
7644+
);
76437645

7644-
document.dispatchEvent(new Event("hyperscript:ready"));
7646+
ready(function () {
7647+
mergeMetaConfig();
76457648

7649+
// Register htmx:load listener immediately so dynamic swaps that
7650+
// occur while external .hs files are still being fetched are not missed.
76467651
globalScope.document.addEventListener("htmx:load", function (/** @type {CustomEvent} */ evt) {
76477652
runtime_.processNode(evt.detail.elt);
76487653
});
7649-
}));
7654+
7655+
fetchedScripts.then(function (script_values) {
7656+
script_values.forEach(function (sc) { _hyperscript(sc); });
7657+
7658+
runtime_.processNode(document.documentElement);
7659+
7660+
document.dispatchEvent(new Event("hyperscript:ready"));
7661+
});
7662+
});
76507663

76517664
function ready(fn) {
76527665
if (document.readyState !== "loading") {
76537666
setTimeout(fn);
76547667
} else {
7655-
document.addEventListener("DOMContentLoaded", fn);
7668+
document.addEventListener("DOMContentLoaded", fn, { once: true });
76567669
}
76577670
}
76587671

test/core/pluginLoading.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
describe("plugin loading order", function () {
2+
3+
it("a command registered via addCommand is available to subsequently parsed scripts", function () {
4+
var executed = false;
5+
6+
_hyperscript.addCommand("__testplugincmd", function (parser, runtime, tokens) {
7+
if (!tokens.matchToken("__testplugincmd")) return;
8+
return {
9+
op: function (ctx) {
10+
executed = true;
11+
return runtime.findNext(this, ctx);
12+
}
13+
};
14+
});
15+
16+
// Parse and execute a script that uses the plugin command directly.
17+
_hyperscript("__testplugincmd");
18+
executed.should.equal(true);
19+
});
20+
21+
it("a feature registered via addFeature is available to subsequently parsed scripts", function () {
22+
var installed = false;
23+
24+
_hyperscript.addFeature("__testpluginfeature", function (parser, runtime, tokens) {
25+
if (!tokens.matchToken("__testpluginfeature")) return;
26+
var name = tokens.requireTokenType("IDENTIFIER").value;
27+
return {
28+
install: function (target, source) {
29+
installed = true;
30+
}
31+
};
32+
});
33+
34+
_hyperscript("__testpluginfeature myFeature");
35+
installed.should.equal(true);
36+
});
37+
38+
it("parsing a script with an unregistered command throws a parse error", function () {
39+
var error = getParseErrorFor("on click __nonexistentcmd");
40+
error.should.not.equal("");
41+
});
42+
43+
});

test/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ <h2>Mocha Test Suite</h2>
6060
<script src="core/runtimeErrors.js"></script>
6161
<script src="core/security.js"></script>
6262
<script src="core/scoping.js"></script>
63+
<script src="core/pluginLoading.js"></script>
6364

6465
<!-- features -->
6566
<script src="features/on.js"></script>

0 commit comments

Comments
 (0)