[2.x] fix: add xslt polyfill so forum continues to boot on browsers without XSLT#4359
Merged
[2.x] fix: add xslt polyfill so forum continues to boot on browsers without XSLT#4359
Conversation
Replaces the inline polyfill injection with a small (~250 byte) detector that loads the polyfill via a <script src> tag only when the browser has no working XSLTProcessor. Browsers with working native XSLT pay nothing beyond the detector. The polyfill (^1.0.21) is published into public/assets/xslt-polyfill/ during assets:publish. Both xslt-polyfill.min.js and the lazily-fetched dist/xslt-wasm.js companion are copied, preserving the relative layout the polyfill's currentScript-based wasm loader needs. The polyfill is a temporary measure until s9e/TextFormatter ships an XSLT-free implementation (see s9e/TextFormatter#250).
The polyfill is published to a stable path (assets/xslt-polyfill/...), so without a cache-busting parameter, browsers would keep using the previously cached version after an upgrade until heuristic revalidation kicked in. Append ?v=<package-version> read from the polyfill's package.json so a Flarum upgrade that bumps the polyfill version invalidates the browser cache immediately. Extract the source-locating + version-reading logic into Flarum\Formatter\XsltPolyfill so AssetsPublishCommand and FormatterServiceProvider share one implementation.
Backend CI runs PHP tests without yarn install, so node_modules is empty there. Operator installs of flarum/core get a Composer tarball that also doesn't include node_modules. So locating the polyfill via node_modules worked locally but not in CI or production. Vendor the polyfill into framework/core/js/dist/xslt-polyfill/ — the same directory that already ships forum.js, admin.js, etc. The split into flarum/flarum-core mirrors js/dist/ wholesale, so the polyfill files travel through with no extra plumbing. Add a copy-xslt-polyfill yarn script chained after webpack build, so bumps to the npm dep version flow into the vendored dist on the next yarn build (which the build bot runs on every push). XsltPolyfill::findSource() now looks at one path, relative to its own __DIR__, so it works in both the monorepo (framework/core/js/dist/...) and the split layout (js/dist/... at the flarum-core repo root).
…watch The previous approach resolved UrlGenerator inside the flarum.formatter singleton closure. That caused the URL collection to be touched the first time a formatter was used, which broke unrelated tests in flarum-mentions that interleave formatter usage with route registration. Move the URL wiring to FormatterServiceProvider::boot() — by then all extender-added routes are registered. Add a setXsltPolyfillUrl() setter on Formatter so the URL can be applied to the already-constructed singleton without changing its constructor contract. Also update .bundlewatch.config.json to scope the per-chunk size cap to the actual webpack-emitted directories (admin, common, forum) instead of the unbounded `*/**/*.js` glob, so the vendored polyfill (~500KB gzipped) isn't checked against the 30KB cap intended for code-split chunks.
…stalls UrlGenerator->path() always builds a forum-relative URL. On installs that relocate the flarum-assets disk to S3 / a CDN, the polyfill files end up at the cloud URL but the script tag pointed at the forum URL, 404'ing. Use the disk's own ->url() — same pattern MailServiceProvider uses for the email logo.
Resolving the polyfill URL during boot() was eagerly resolving the flarum-assets disk on the FilesystemManager. That memoised the disk before tests (FilesystemTest::disk_uses_custom_adapter_*) could swap the adapter, which broke them. Switch from a string URL to a closure resolver. The Formatter now holds a resolver and calls it lazily on the first getJs() invocation, caching the result. Boot is back to just registering the resolver function — no disk resolution, no URL generation, no surprises during app startup. Side effect: configurations with no public-URL adapter (e.g. the NullFilesystemDriver / InMemoryFilesystemAdapter used in tests) now silently skip the polyfill loader instead of throwing during boot.
The polyfill must install window.XSLTProcessor *before* forum.js runs — s9e calls `new XSLTProcessor` at module top level, so any race that lets the s9e bundle execute before the polyfill is installed throws a ReferenceError and kills app boot. Earlier attempts in this PR prepended a detector to the formatter JS that was bundled into forum.js, but that puts the detector in the same script execution window as the s9e top-level code — too late. Move the polyfill loading to a document.write-driven inline script in <head>, emitted by Frontend\Document::makeHead() before the JS asset tags. document.write of a <script src> tag during HTML parsing inserts it at the parser position, which the parser then loads and executes synchronously before continuing — guaranteeing the polyfill is installed before forum.js. This is the pattern documented in the xslt-polyfill README's HTML usage example. Browsers with native XSLT pay only the cost of the inline detector (~200 bytes); affected browsers fetch the polyfill itself. The Formatter is back to returning just the s9e bundle from getJs() — no detector, no polyfill URL injection, no resolver. Add XsltPolyfill::publicUrl() helper that encapsulates the flarum-assets disk lookup + version cache-bust, returning null when the disk has no public URL (in-memory test disks, etc.). Both AssetsPublishCommand-the-publishing-side (via findSource) and Document-the-emission-side (via publicUrl) live in the same helper.
5 tasks
imorland
added a commit
that referenced
this pull request
May 8, 2026
… XSLT (#4644) Backports the 2.x polyfill (#4359) for the 1.8 line. Chrome disabled XSLT in Beta from 145 (Dec 2025) and Stable from 158 (Nov 2026); without the polyfill, s9e/TextFormatter's `new XSLTProcessor` at parser-init time throws a ReferenceError that prevents forum.js from booting on those browsers. A ~200-byte detector is emitted into the document <head>. It tries to construct an XSLTProcessor; on failure it document.write()s a script tag pointing at the polyfill bundle. document.write of a script tag during HTML parsing blocks the parser until the polyfill loads, so window.XSLTProcessor is in place before forum.js runs. The polyfill is published into the flarum-assets disk by assets:publish and resolved through the disk's own ->url(), so installs that serve assets from a remote bucket / CDN get the right URL. To stay patch-release-safe, the FilesystemFactory is resolved via the container inside makeXsltPolyfillLoader() rather than added to the Document constructor — same pattern Document already uses for TitleDriverInterface.
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.
Summary
Adds an XSLT polyfill for browsers that have removed native XSLT support, so Flarum continues to work end-to-end on those browsers.
Chrome disabled XSLT by default in the Beta channel from version 145 (Dec 2025) and is removing it from Stable in version 158 (Nov 2026). Firefox has signalled it will follow. Without a polyfill,
forum.jsfails to initialize in those browsers — s9e/TextFormatter referencesXSLTProcessorduring parser init, throws aReferenceError, and takes the entire app boot down with it. Concretely this means:tags,links,ip_info,fof-terms-policies, etc.) throws in the consoleThis is a hard breakage, not a degraded UX, and applies to all users on the affected browser versions — not just those who open the composer.
How it works
A small inline detector (~200 bytes) is emitted in the document
<head>before the JS asset tags. It triesnew XSLTProcessor(); on success it returns immediately and the document parser proceeds normally. On failure (browser has removed XSLT) it usesdocument.writeto synchronously insert a<script src="…/xslt-polyfill.min.js">tag — a long-supported mechanism for blocking polyfill bootstrap during HTML parsing. The browser parser blocks until the polyfill loads and executes (installingwindow.XSLTProcessor), then continues to parse and executeforum.jswith XSLT available.This timing matters: s9e/TextFormatter calls
new XSLTProcessorat module top-level when its bundle parses, so async polyfill loading (e.g. viadocument.createElement('script')+appendChild) loses the race. Thedocument.writepattern is the documented usage in the xslt-polyfill README.The polyfill URL is resolved against the
flarum-assetsdisk, so it's correct on installs that serve assets locally and on those backed by a remote bucket / CDN. The polyfill (xslt-polyfill^1.0.21) is published into the assets disk duringassets:publish, alongside its lazily-fetcheddist/xslt-wasm.jscompanion. Browsers with working native XSLT never download either file — only the inline detector.Why this approach (vs. inlining)
The polyfill is large (~510 KB gzipped). Inlining it into
forum.jsfor every visitor — including the majority on browsers with working XSLT — would roughly triple the JS payload on every page load. Conditionaldocument.writeinsertion restricts the cost to affected browsers and keeps the artifact cacheable as a separate file.Operator note: CORS for remote
flarum-assetsOperators serving the
flarum-assetsdisk from a different origin (S3, CDN, etc.) must allow CORS from their forum origin to the assets origin. The polyfill's bootstrap loads via<script src>(not CORS-checked), but its WASM sibling (dist/xslt-wasm.js) is fetched viafetch()from inside the polyfill, which is subject to CORS. Without the right CORS headers on the assets bucket, the WASM fetch fails and the polyfill silently doesn't install. Same requirement that already applies to web fonts and any other cross-origin asset; just worth being explicit since this PR adds a new file that usesfetch.Temporary measure
The polyfill is a stop-gap until s9e/TextFormatter ships an XSLT-free implementation: s9e/TextFormatter#250. Once that lands, the detector and asset publishing can be removed.
Test plan
XsltPolyfillhelper —findSource()locates the vendored bundle,version()reads the semver frompackage.jsonDocumenthead injection — emits the detector script, referencesXSLTProcessor, usesdocument.write, escapes the closing</script>assets:publish— both polyfill files copied with correct relative layoutforum.jsboots, store types register, extensions initialize, live preview renders