Skip loading Bootstrap when the active skin provides it#113
Closed
malberts wants to merge 2 commits into
Closed
Conversation
malberts
force-pushed
the
fix/skip-bootstrap-for-skins-providing-it
branch
from
July 16, 2026 21:45
9b26c13 to
4ac37e6
Compare
Extension:Bootstrap's ext.bootstrap.styles and ext.bootstrap.scripts were added in ParserAfterParse, which is baked into the skin-agnostic parser cache. Move them to the OutputPageParserOutput hook, where the active skin is known, and skip them for skins that put Bootstrap on the page themselves. Chameleon registers Extension:Bootstrap's styles under the name zzz.ext.bootstrap.styles, which ResourceLoader cannot deduplicate against ext.bootstrap.styles, so a Chameleon page requested the same Bootstrap CSS twice. Medik and Tweeki bundle their own Bootstrap, so they received a second, different build. Skins that do not load Bootstrap keep getting it from Extension:Bootstrap. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
malberts
force-pushed
the
fix/skip-bootstrap-for-skins-providing-it
branch
from
July 17, 2026 09:08
4ac37e6 to
23f5c68
Compare
malberts
force-pushed
the
fix/skip-bootstrap-for-skins-providing-it
branch
2 times, most recently
from
July 20, 2026 13:01
4f3329f to
313c456
Compare
The carousel, modal, popover and tooltip fix modules each ship an init script that calls the Bootstrap JavaScript global, so they must load after Bootstrap. The static ext.bootstrap.scripts dependency guaranteed that ordering but forced Extension:Bootstrap's Bootstrap onto every skin, including skins that bundle their own. On those skins the page carried two copies of Bootstrap, and because each registers Bootstrap's data-api on the document, events fired twice. Route the four modules through BootstrapDependentModule, whose getDependencies() follows the active skin: a skin that bundles its own Bootstrap depends on that skin's module instead of ext.bootstrap.scripts, keeping the load-order guarantee without the duplicate. The map currently swaps medik to skins.medik.js. Other skins keep ext.bootstrap.scripts; Chameleon loads it itself under the same name, so ResourceLoader deduplicates it. Tweeki also bundles Bootstrap but exposes no JavaScript global for the init scripts to reach, so it stays on ext.bootstrap.scripts until the init scripts learn the jQuery plugin bridge. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
malberts
force-pushed
the
fix/skip-bootstrap-for-skins-providing-it
branch
from
July 20, 2026 13:20
313c456 to
a739d8e
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
For #70
Skins that put Bootstrap on the page themselves ended up loading a second copy
from Extension:Bootstrap, once for the stylesheet and once for the script. This
fixes both.
Bootstrap CSS
Extension:Bootstrap's
ext.bootstrap.stylesandext.bootstrap.scriptswereadded in
ParserAfterParse. That is the parse phase, so the decision is bakedinto the skin-agnostic parser cache and cannot depend on the skin. They are now
added in the
OutputPageParserOutputhook, where the active skin is known, andskipped for skins that put Bootstrap on the page themselves. The list of those
skins is internal (Chameleon, Medik, Tweeki) rather than configurable, since
whether a skin loads its own Bootstrap is a fact about the skin, not a per-wiki
policy.
Extension:Bootstrap stays a Composer
require. That keeps a useful lever: theversion constraint on it (
^6.0, i.e. Bootstrap 5.3) pins the Bootstrap thisextension loads for skins that get it from Extension:Bootstrap, and rejects a
future Extension:Bootstrap that moves to a newer Bootstrap. Self-bundling skins
declare no such dependency, so matching their bundled Bootstrap stays the
operator's responsibility (see the known issues). The init gate is untouched,
and
SetupAfterCache/addAllBootstrapModules()is untouched: that call onlyconfigures what Extension:Bootstrap compiles into
ext.bootstrap.styles, itputs nothing on a page. Not skipping remains the safe default, so skipping is an
optimisation rather than a load-bearing correctness decision.
Evidence (CSS)
Distinct stylesheets delivering the Bootstrap library, per skin.
ext.bootstrap.styles+zzz.ext.bootstrap.styles(2)zzz.ext.bootstrap.styles(1)ext.bootstrap.styles+skins.medik(2)skins.medik(1)ext.bootstrap.styles+skins.tweeki.styles(2)skins.tweeki.styles(1)ext.bootstrap.styles(1)ext.bootstrap.styles(1)ext.bootstrap.styles(1)ext.bootstrap.styles(1)ext.bootstrap.styles(1)ext.bootstrap.styles(1)ext.bootstrap.styles(1)ext.bootstrap.styles(1)Chameleon's double request, the case reported in the issue, is gone:
Bootstrap JavaScript
The CSS skip alone does not keep Extension:Bootstrap's JavaScript off a
self-bundling skin.
ext.bootstrapComponents.carousel.fix,.modal.fix,.popover.fixand.tooltip.fixeach ship an init script that calls theBootstrap JavaScript global (
new bootstrap.Carousel( ... ),bootstrap.Modal.getOrCreateInstance( ... ), and so on), so each declaredext.bootstrap.scriptsas a ResourceLoader dependency to guarantee Bootstraploads first. That dependency is load-bearing, dropping it would let the init
script run before Bootstrap exists, but it also forced Extension:Bootstrap's
copy onto every skin. On a skin that bundles its own Bootstrap the page then
carried two copies, and because each registers Bootstrap's data-api on the
document, one click was handled twice: a navbar or action dropdown opened and
immediately closed.
A ResourceLoader dependency cannot be varied per skin in static
extension.json, but it can in a module class. The four modules now useBootstrapDependentModuleand declare no Bootstrap dependency of their own; itsgetDependencies( ?Context )supplies one per skin: the skin's own scriptsmodule when it bundles Bootstrap, otherwise
ext.bootstrap.scripts. The mapcurrently gives
medikitsskins.medik.js. Other skins getext.bootstrap.scripts; Chameleon adds it itself under the same name, soResourceLoader deduplicates it and its JavaScript was never doubled.
The map is a skin list rather than automatic detection because a skin's bundled
Bootstrap version is not knowable server-side, where the module-loading decision
is made. It is only visible in the browser (
window.bootstrap.VERSION), which istoo late to change what loads.
Evidence (JS)
Verified in a browser (automated, Playwright) on Vector, Chameleon, Medik and
Tweeki:
ext.bootstrap.scriptsis no longer loaded (its module state staysregistered); the page carries one Bootstrap, Medik's own, delivered withskins.medik.js. The three skin-emitted chrome dropdowns, user menu, Actions(edit / history / source) and Tools, open and stay open. Force-loading
ext.bootstrap.scriptsto recreate the previous double makes all three openand immediately close again, so the single-Bootstrap state is what fixes them.
Carousel, modal, popover, tooltip and accordion all work, and no
bootstrap ... is not availablewarning appears, so the init scripts stillreach the global.
working.
Tweeki is not covered yet
Tweeki bundles its own Bootstrap but exposes no
window.bootstrapglobal, onlyBootstrap's jQuery plugin bridge. The four init scripts reach Bootstrap only
through the global, so pointing Tweeki's dependency at its own module would leave
them unable to initialise those components. Tweeki therefore stays on
ext.bootstrap.scriptsand still double-loads Bootstrap's JavaScript (confirmedin the browser: two live copies, seen as two modal backdrops). Making Tweeki
clean needs the init scripts to fall back to the jQuery bridge when the global is
absent, which is a separate change.
Known behaviour change:
action=parsewithoutuseskinext.bootstrap.stylesandext.bootstrap.scriptsused to sit on theParserOutput, which travels with the parsed content. They now sit on theOutputPage, which only exists when a page is rendered for a skin. Soapi.php?action=parsewithout auseskinparameter no longer lists them. Thescripts still reach any caller that loads the carousel, modal, popover or
tooltip fix modules, since with no skin those resolve to
ext.bootstrap.scripts.The styles have no such dependency, so they simply go missing.
With
useskinthe hook runs and the answer is correct per skin, which is animprovement on master: master answered the same way for every skin, so on a
Chameleon wiki it told callers to load Bootstrap that Chameleon already provides.
Ordinary page views are unaffected and no known caller depends on this, so it is
left as-is for now.
Tests
OutputPageParserOutputTestasserts the CSS outcome on a realOutputPage:ext.bootstrap.stylesandext.bootstrap.scriptsare present in its modulelists for a skin that does not load Bootstrap, and absent for chameleon, medik
and tweeki. Asserting the resulting module list rather than the calls made to a
mock keeps the tests meaningful across a refactor of how the modules get added.
BootstrapDependentModuleTestasserts the outcome directly on the module:getDependenciesaddsskins.medik.jsunder the medik skin andext.bootstrap.scriptsfor vector, chameleon, tweeki and monobook and when noskin context is available, alongside any other dependencies the module declares.