Load Bootstrap once per skin#123
Merged
Merged
Conversation
malberts
force-pushed
the
bootstrap-per-skin-module
branch
6 times, most recently
from
July 21, 2026 12:18
5750fbc to
94b805c
Compare
malberts
force-pushed
the
bootstrap-per-skin-module
branch
5 times, most recently
from
July 21, 2026 14:26
e11f936 to
b7a65a6
Compare
malberts
marked this pull request as ready for review
July 21, 2026 14:32
Skins that bundle their own Bootstrap (for example Medik and Tweeki) also received Extension:Bootstrap's copy, because BootstrapComponents loaded ext.bootstrap.styles and ext.bootstrap.scripts unconditionally. Two copies of Bootstrap each registered Bootstrap's document data-api, so one click was handled twice and the skin's navbar and action dropdowns opened and immediately closed. From a BeforePageDisplay hook, where the active skin is known, BootstrapComponents now names the modules carrying this skin's Bootstrap in a config variable: the script module to wait on (ext.bootstrap.scripts on skins without their own, and only there is Extension:Bootstrap's copy loaded, the skin's own module on Medik, or Tweeki's configured script module, mirroring SkinTweeki::initPage) and the stylesheet module to load for dynamically rendered content, null on skins that provide their own stylesheet. The shared resolver module waits on the script module with mw.loader.using, loads the stylesheet on demand, and runs the component init scripts on wikipage.content, so live preview and VisualEditor re-renders are covered too. On Tweeki, which exposes no window.bootstrap, the components initialize through Bootstrap's jQuery plugin bridge. Whether a skin provides Bootstrap scripts is derived from the same per-skin map that names the wait module, so the two decisions cannot drift apart. The hook runs only where content was parsed, marked by the styles fix module the parser adds on every parsed page; cache entries have carried it since 5.2.0, so warm parser caches keep working across the upgrade. The load moved off the ParserOutput (it has to, so the decision can depend on the skin, which the shared parser cache cannot), so api.php?action=parse no longer lists ext.bootstrap.styles / ext.bootstrap.scripts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Chameleon registers Extension:Bootstrap's stylesheet under its own module name (zzz.ext.bootstrap.styles), which ResourceLoader cannot deduplicate against ext.bootstrap.styles, so pages carried the Bootstrap CSS twice. Chameleon provides its own stylesheet but consumes Extension:Bootstrap's scripts, so only the stylesheet is skipped there. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Tweeki passes $wgTweekiSkinCustomScriptModule straight to OutputPage::addModules, which also accepts arrays, so a truthy non-string value works there even though nothing documents it. We mirror only the string form; any other truthy value falls back to Extension:Bootstrap's copy, the previously released behavior, instead of fataling on every page view. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Initialization runs on every wikipage.content fire, and new stacks a second instance on an element initialized in an earlier pass, with the first instance's listeners left bound. getOrCreateInstance is idempotent, and it is also order-independent against Bootstrap's own data-api, which instantiates carousels with data-bs-ride itself. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
malberts
force-pushed
the
bootstrap-per-skin-module
branch
from
July 21, 2026 14:35
b7a65a6 to
7c23f6d
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 bundle their own Bootstrap (for example Medik and Tweeki) also received
Extension:Bootstrap's copy, because BootstrapComponents loaded
ext.bootstrap.stylesandext.bootstrap.scriptsunconditionally. Two copies of Bootstrap each registered Bootstrap'sdocument data-api, so one click was handled twice and the skin's navbar and action dropdowns
opened and immediately closed. Chameleon had the CSS half of the same problem: it registers
Extension:Bootstrap's stylesheet under its own module name (
zzz.ext.bootstrap.styles), whichResourceLoader cannot deduplicate against
ext.bootstrap.styles, so pages carried the BootstrapCSS twice.
BootstrapComponents is the extension that pulls in Extension:Bootstrap, so it decides when that
copy is needed. From a
BeforePageDisplayhook, where the active skin is known, it names themodules carrying this skin's Bootstrap in a config variable: the script module to wait on
(
ext.bootstrap.scriptson skins without their own,skins.medik.json Medik, or Tweeki'sconfigured script module, honouring
$wgTweekiSkinCustomScriptModule)and the stylesheet module to load for dynamically rendered content, null on skins that provide
their own stylesheet. The shared
resolver module from #124 (already on master) waits on the script module with
mw.loader.using,then initialises the components on content render. On ordinary page views that module is one the
page already loads, so
using()attaches to the in-flight load; on pages whose content arriveslater (live preview on an edit page) it is fetched on demand. On Tweeki, which
exposes no
window.bootstrap, the components initialise through Bootstrap's jQuery pluginbridge.
The stylesheet and the script are decided separately: Chameleon provides its own stylesheet (the
zzzclone) while consuming Extension:Bootstrap's scripts, so only the stylesheet is skippedthere; Medik and Tweeki provide both. The per-skin maps currently know Chameleon, Medik and
Tweeki; other Bootstrap-bundling skins keep today's behaviour until added, since the default
stays "load Extension:Bootstrap's copy". Whether a skin provides Bootstrap scripts is derived
from the same map that names the wait module, so those two decisions cannot drift apart. The
hook runs only where content was parsed, marked by the
ext.bootstrapComponents.bootstrap.fixstyles module the parser adds on every parsed page; cache entries have carried that module since
5.2.0, so warm parser caches keep working across the upgrade.
Supersedes #122 and #113; both can be closed.
Behaviour changes
api.php?action=parseno longer listsext.bootstrap.styles/ext.bootstrap.scripts; theyload at page render, and no transitive dependency path to them remains. Page views,
VisualEditor and live preview are unaffected.
window.bootstrap(it came from Extension:Bootstrap's copy).Bootstrap's data-api markup is unaffected; only site scripts doing programmatic Bootstrap calls
on Tweeki wikis are affected, and none are known.
previously only content already present when the init scripts first executed was initialised.
Bootstrap component markup outside it (sitenotice, skin chrome) is no longer picked up;
BootstrapComponents' own output always renders inside the content area.
the fix rolls out per page.
Verification
Unit tests cover the per-skin module resolution (including Tweeki's config-driven module), the
styles and scripts provider checks, and the hook's load and marker decisions; CI (MW 1.43 through
master) is green. Automated browser testing (Playwright) on Vector, Chameleon, Medik and Tweeki:
provider skins load a single Bootstrap (
ext.bootstrap.scriptsstays unloaded), Chameleondelivers the Bootstrap stylesheet once (
ext.bootstrap.stylesstays unloaded, its own copyserves the page), all eleven components were exercised behaviourally on all four skins (44
checks; modals show exactly one backdrop, accordions fire single show/shown event pairs), every
chrome dropdown on Chameleon, Medik and Tweeki opens and stays open,
action=parseoutput wasconfirmed free of the
ext.bootstrap.*modules, and the page scope matches released behaviour(chrome-only special pages stay Bootstrap-free). Live preview was driven on the edit
page: first and repeated previews initialise their components and fetch the Bootstrap stylesheet
on demand, which is what publishing both modules on every page enables (
mw.loader.usingforthe script module,
mw.loader.loadfor the stylesheet).A VisualEditor edit on Medik was verified manually: after the in-place save, the accordion,
modal and tooltip in the updated content all function, with a clean console. Each skin was additionally
re-verified as the only installed skin, with identical results.