Okay so I investigated and found the cause of this problem. Unfortunately I can't suggest an easy solution because there's no Flarum extension that allows doing what needs to be done without writing custom code.
The problem
The problem is actually specific to jQuery v3+ and Flarum.
jQuery supports loading multiple versions at the same time through the use of jQuery.noConflict(true). This system would be commonly used by libraries like ads because it allows them to load the jQuery version they need without causing any issue if the original website already uses a different version or no jQuery.
It would work something like this:
<!-- original site loads first version -->
<script src="https://code.jquery.com/jquery-3.7.0.js"></script>
<!-- original site does some stuff -->
<!-- ads script is injected, loads second version -->
<script src="https://code.jquery.com/jquery-1.6.2.js"></script>
<!-- ads script does stuff and/or copies a reference to the second jQuery for its own usage -->
<script>adsVersion = jQuery.noConflict(true);</script>
<!-- original site can access the original jQuery version from the global scope again -->
And this works fine in Flarum.
The problem starts happening if you try to load another v3+ version of jQuery as the second one:
<!-- Flarum loads first version -->
<script src="https://code.jquery.com/jquery-3.7.0.js"></script>
<!-- Flarum boots -->
<!-- ads script is injected, loads second version -->
<script src="https://code.jquery.com/jquery-3.0.0.js"></script>
<!-- ads script does stuff and copies a reference to the second jQuery for its own usage -->
<script>adsVersion = jQuery.noConflict(true);</script>
<!-- Flarum crashes because the jQuery global objects are now undefined -->
The problem is actually related to the CommonJS support that's baked in jQuery v3+. After all Flarum extensions have loaded, there's a leftover module
object in the global scope.
When the jQuery loader sees that module
object, it adds the second jQuery to it instead of the global scope. At this point the jQuery version added by Flarum is still in the global scope.
Then when the ads code calls jQuery.noConflict(true);
, it actually invokes the method on the original instance of jQuery bundled with Flarum which erases itself since there was no jQuery instance prior to this one.
The same problem could impact other libraries that take a similar approach to jQuery to support CommonJS in their base distribution file.
The solution/workaround
Ideally we should fix this in Flarum. We should clear the leftover module
object after the last extension has been loaded. The module
variable comes from the Frontend extender that loads the javascript code compiled with the Flarum webpack configuration.
But since this code is already being rewritten for Flarum v2 (hopefully without this issue, I haven't tested yet) I don't know if this will be considered for a hotfix in the Flarum 1.x branch.
Luckily, this can also be addressed without modifying Flarum. What needs to be done is run module=undefined
after Flarum has finished loading the forum.js
file. This should be safe to do as the module
variable is not used for anything else and is in fact erased again and again each time an extension is loaded. It just happens to be left there after the last extension.
The challenge with ads code is that you don't control a lot of what they do. If you insert their code in the head, header or footer sections of Flarum you don't actually know at which point they will try to load jQuery. If they load jQuery synchronously before Flarum's forum.js
, everything should actually work fine without any fix since Flarum's jQuery and extensions have not started loading yet. If the ads code tries loading its copy of jQuery while the forum.js
file is being parsed, there is no solution and it will conflict.
So the only reliable solution would be to make sure the ads script runs after forum.js
. There are 2 ways to achieve this: either wrap all of the ads code inside a DOMContentLoaded
listener or insert its HTML at the very bottom of the page.
The challenge with the first solution is that your ads code is likely HTML and not javascript, so you would have to re-write the code as a set of javascript instructions. But it can then be inserted in any of head, header or footer and it wouldn't make any difference since the code will only run after the page has loaded:
<script>
document.addEventListener('DOMContentLoaded',function(){
// Fix the the jQuery conflict
module=undefined;
// Now load the ads code here
// You have to write this yourself
});
</script>
The challenge with the second solution is that there is no Flarum extension that lets you insert HTML below the Flarum boot code. The "custom footer" is in fact above this section so it doesn't work. You can however use the Flarum content extender from PHP to inject the required code. There must be examples floating around on this forum but I don't have one right now.
If you can be certain that the ads code itself waits for DOMContentLoaded
before loading jQuery you could just set a separate listener just to clear the module
variable, but I'm not sure if there's any way to guarantee this listener will run before the listener set by the ads. And if you do this but the ads library doesn't actually wait for DOMContentLoaded
then it will probably still randomly fail from time to time depending on the time the javascript takes to be loaded by the browser.
<script>document.addEventListener('DOMContentLoaded',function(){module=undefined})</script>
<!-- regular ads HTML code here -->