Skip to content

EPBDS-16211 §9.8: unpin LESS — 3.13 → 4.x, output byte-identical - #48

Merged
AlexSamBY merged 1 commit into
masterfrom
less-unpin
Sep 15, 2026
Merged

AlexSamBY merged 1 commit into
masterfrom
less-unpin

Conversation

@AlexSamBY

Copy link
Copy Markdown
Member

The 3.13 pin is gone — the project is on LESS 4 — and the published static/all.css still has the sha256 it has had all through the exit (dcdb0a40…). Verified before touching the manifest, by compiling the same source through both majors and comparing byte for byte.

Neither anchor was what §9.8 said it was

Step 4 had already measured this, and it holds:

  • javascriptEnabled stays. It is required by our own _variables.less:23 — a `Math.random()` font cache-buster, which reaches static/font.css and never all.css. Not Semantic's requirement, and removing it is a separate decision about font caching.
  • less-plugin-functions stays. It is not vestigial: it is what makes size() and px() callable, at 186 sites across 30 files.

The real blocker was one line of the plugin's

Under its own comment calling it "the most ugly hack ever":

DetachedSet.prototype.type = 'NotDetachedRuleset';

LESS 4 defines that property with only a getter, so the assignment throws and every compile dies. scripts/less-plugin-compat.js makes the assignment a no-op rather than making it work — the hack exists so a mixin can return a detached ruleset, and ours return plain values.

Costed against the alternatives before choosing: rewriting 186 call sites changes a computed value at every one and destroys the readability size() exists for; a fork or patch-package adds something to maintain.

Two further changes the bump forced

  • math: 'always'. LESS 4 defaults to parens-division, under which 1/4 in _variables.less:236 stops being a quotient and round receives a non-number. always is LESS 3's semantics, and it is what keeps the output identical.
  • The node build must be required explicitly. LESS 4's manifest carries a browser field and jest's jsdom environment resolves it — so four CSS suites failed with a jsdom AggregateError from a socket, because the browser build fetches @imports over XHR. That error looks nothing like a LESS problem; the reason is now a comment where the require is.

One module, not ten sites

All three live in scripts/less-options.js, which every LESS compile imports: three webpack configs, build-css.js, the fixture and vendor generators, and four CSS suites. Ten sites used to set these options independently — the same shape that already bit us with the theme.config copy.

A method note

The first LESS 4 trial succeeded against a copy of the plugin I had patched by hand while diagnosing. Restoring it to pristine upstream and re-running is what actually proved the shim does the work. Worth recording: a green result from a tree you have been poking at is not a result.

Gates

jsdom 167 suites / 2540 tests / 38 snapshots on React 16.14, 17 and 18.3, four consecutive full runs with output kept; Chrome 41/41; lint:js, lint:css, css:fixture:check, both docs checks, build-lib and build-css green; published CSS byte-identical.

🤖 Generated with Claude Code

The pin is gone and the published `static/all.css` still has the sha256 it has
had all through the exit (dcdb0a40…). Verified before touching the manifest, by
compiling the same source through both majors and comparing.

NEITHER ANCHOR WAS WHAT §9.8 SAID IT WAS, and the step-4 measurement had already
shown that: `javascriptEnabled` is required by our OWN `_variables.less:23` — a
`Math.random()` font cache-buster that reaches `static/font.css` and never
`all.css` — so it STAYS. `less-plugin-functions` is not vestigial either: it is
what makes `size()` and `px()` callable, at 186 sites across 30 files.

THE REAL BLOCKER WAS ONE LINE OF THE PLUGIN'S, under its own comment calling it
"the most ugly hack ever":

    DetachedSet.prototype.type = 'NotDetachedRuleset';

LESS 4 defines that property with only a getter, so the assignment throws and
every compile dies. `scripts/less-plugin-compat.js` makes the assignment a NO-OP
rather than making it work — the hack exists so a mixin can return a detached
ruleset, and ours return plain values. Costed against the alternatives before
choosing: rewriting 186 call sites changes a computed value at every one and
destroys the readability `size()` exists for; a fork or `patch-package` adds
something to maintain.

TWO FURTHER CHANGES THE BUMP FORCED:
- `math: 'always'`. LESS 4 defaults to `parens-division`, under which `1/4` in
  `_variables.less:236` stops being a quotient and `round` gets a non-number.
  `always` is LESS 3's semantics and is what keeps the output identical.
- The node build must be required EXPLICITLY. LESS 4's manifest carries a
  `browser` field and jest's jsdom environment resolves it, so four CSS suites
  failed with a jsdom `AggregateError` from a socket — the browser build fetches
  `@import`s over XHR. That error looks nothing like a LESS problem and cost a
  detour; the reason is now a comment where the require is.

ALL THREE LIVE IN ONE MODULE. `scripts/less-options.js` is what every LESS
compile imports — three webpack configs, `build-css.js`, the fixture and vendor
generators, and four CSS suites. Ten sites used to set these options
independently, which is the shape that already bit us with the `theme.config`
copy: one implementation cannot drift from another.

A METHOD NOTE, because it nearly produced a false pass: the first LESS 4 trial
succeeded against a copy of the plugin I had patched by hand while diagnosing.
Restoring it to pristine upstream and re-running is what actually proved the shim
does the work.

Gates: jsdom 167 suites / 2540 tests / 38 snapshots on React 16.14, 17 and 18.3,
four consecutive full runs with output kept; Chrome 41/41; `lint:js`,
`lint:css`, `css:fixture:check`, both docs checks, `build-lib` and `build-css`
green; published CSS byte-identical.
@AlexSamBY
AlexSamBY merged commit be02e23 into master Sep 15, 2026
5 checks passed
@AlexSamBY
AlexSamBY deleted the less-unpin branch September 15, 2026 12:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant