Skip to content

Switch the default Sass transpiler to dart-sass - #2724

Merged
chalin merged 52 commits into
google:mainfrom
chalin:chalin-m24-dartsass-swap-2026-0818
Aug 19, 2026
Merged

Switch the default Sass transpiler to dart-sass#2724
chalin merged 52 commits into
google:mainfrom
chalin:chalin-m24-dartsass-swap-2026-0818

Conversation

@chalin

@chalin chalin commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator
  • Fixes Drop libsass, use dart sass instead #2279
  • Transpiler flip (head-css.html): transpiler: dartsass, with no deprecation silencing: Dart Sass warnings (Bootstrap's @imports and Docsy's own tree, pending its @use refactor) stay visible as fix-me reminders. Hugo deprecated its embedded LibSass in v0.153.0; the consumer escape hatch is overriding back to libsass for as long as their Hugo carries it -- now a three-file override (head-css.html, plus main.scss and _code-dark.scss, which use module features libsass lacks).
  • Sass-source compatibility: dart-sass forbids Sass @import nested under @if/selectors, which _code-dark.scss relied on; it now loads chroma styles with meta.load-css, so raw hugo gen chromastyles dumps keep working unchanged. The entry main.scss moves to @use. Compiled CSS unchanged (verified).
  • Toolchain: sass-embedded exact-pinned in the root package.json beside hugo-extended; the compiler binary arrives via npm's optional-deps platform dispatch (script-free), so install:safe drops --omit=optional (a no-op until now: the lock had zero optional entries). The supply-chain audit inventories the one lock-only install-script package this adds (@parcel/watcher, never installed on platforms we run) and pins the new install:safe form; both changes were proven red first.
  • Consumer path & docs: new "Install Dart Sass" section in the installation prerequisites, linked from the README, get-started, other-options, and the GitHub Pages and Netlify deployment pages; make-site.sh scaffolds install a site-owned sass-embedded and run Hugo through an npm run hugo passthrough script. The smoke suite sanitizes the consumer site's PATH (consumerEnv()) and the CI scaffold moves to $RUNNER_TEMP, so a consumer site can't silently borrow the repo's sass.
  • Golden refresh: exactly the 6 shots the dual-compiler probe predicted diverge (desktop full-page light/dark × home/single/blog, 26-35 px) -- dart-sass rendering Bootstrap's color math at full precision; the other 24 pass unchanged at threshold 0. Review artifacts attached below: per-shot diff PNGs and a one-shot structural CSS diff (6037 rules/side; all changed declarations fall into formatting/precision classes -- fractional-rgb color math, unicode unescaping, calc simplification, leading zeros; 0 td-breadcrumb rules diverge; the only selector-list changes are dart-sass pruning redundant @extend permutations shadowed within the same rule).
  • CI: no compiler wiring needed -- every SCSS-compiling job installs via install:safe, which now provides sass on the npm-script PATH.

@chalin chalin added this to the 0.17.0 milestone Aug 18, 2026
@chalin

chalin commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator Author

Review artifacts: one-shot libsass vs dart-sass structural CSS diff (same branch code, dev-profile builds of docsy.dev, only the toCSS transpiler differing; script + full report in the lane's tmp/, not committed -- this is a transition proof, not a standing gate).

metric value
rules per side 6037 / 6037
changed declarations 356
selector-list changes 2
.td-/--td- rules diverging semantically 0
  • Every changed declaration falls into the formatting/precision classes the dual-compiler probe predicted: full-precision rgb color math (194, incl. inside SVG data-URIs), unicode unescaping in content (60), calc() simplification (11), leading-zero forms, hex vs rgb spellings of the same colors. No property additions/removals, no value semantics changes.
  • All 11 compiled td-breadcrumb rules in this build identical across compilers (the probe counted 9 on the fixture build, same result); every .td- divergence is a Bootstrap-computed color value shifting precision, or a shared selector list with a Bootstrap rule.
  • The 2 selector-list changes are dart-sass pruning redundant @extend permutations, each shadowed by a kept superset selector in the same rule -- no reachable styling change.
  • Visual proof: the 6 refreshed darwin goldens are viewable as image diffs in this PR's Files tab; the other 24 shots pass unchanged at pixelmatch threshold 0. The linux golden set needed no refresh (CI visual green on the pre-refresh set: linux rasterizes the color shifts to identical pixels).

@chalin
chalin marked this pull request as ready for review August 18, 2026 18:24
chalin added 19 commits August 18, 2026 17:04
sass-embedded@1.102.0, exact-pinned in the root package.json beside
hugo-extended: the dart-sass compiler Hugo's dartsass transpiler spawns
(the transpiler flip follows in the next commit).

The compiler binary ships via npm's optional-deps platform dispatch (the
script-free alternative to postinstall downloads), so install:safe drops
--omit=optional: a no-op until now (the lock had zero optional entries),
and afterward exactly one optional package installs, the script-free
platform binary.

Supply-chain audit updates, both proven red first:

- @parcel/watcher joins the install-script inventory: lock-only, an
  optional dep of the pure-JS sass fallback shipped for platforms
  without a prebuilt binary; never installs on any platform we run.
  allowScripts denies it for defense in depth.
- install:safe's pinned form tracks the flag drop.
toCSS in head-css.html now passes transpiler: dartsass, with
silenceDependencyDeprecations (Bootstrap's import/if-function/
global-builtin/color-functions warnings; verified the full set) and
silenceDeprecations: import for Docsy's own ~40-@import tree until its
@use refactor (a later increment).

dart-sass forbids Sass @imports nested under @if or a selector, which
_code-dark.scss relied on for the generated chroma styles. The
generator now wraps each style in a mixin (side-effect-free import),
and _code-dark.scss imports at top level and @includes under the
scheme selectors. Works identically under libsass, keeping the
consumer escape hatch (override head-css.html back to libsass) intact.

Build is warning-free. Visual suite now red on exactly the 6 shots the
dual-compiler probe predicted (desktop full-page light/dark x
home/single/blog, 26-35 px): dart-sass renders Bootstrap's color math
at full precision. Golden refresh follows with the structural-diff
review artifacts.
Exactly the 6 shots the dual-compiler probe predicted (desktop
full-page light/dark x home/single/blog); the 24 others pass unchanged
at threshold 0. Structural CSS diff and per-shot diff PNGs attached to
the PR as review artifacts. Linux goldens follow from the CI
artifact.
The dartsass transpiler shells out to the sass CLI, which a freshly
scaffolded smoke site doesn't have; provide the repo-installed, locked
one, the same sourcing pattern as HUGO. Red-proven by the first CI run
(all new-site jobs failed at the site build); consumer sites install
their own per the changelog entry.
…apper

A site-supplied chroma file holding a raw hugo-gen-chromastyles dump
(the previously documented form) now hits a bare 'Undefined mixin'
under the mixin-shaped theme files, its rules also landing unscoped.
Red-proven with a raw tango dump at the documented override path:

- _code-dark.scss guards with mixin-exists and errors with the required
  wrapper + docs link; the wrapped form builds green with the custom
  style landing scoped (verified).
- lookandfeel.md documents the wrapper with an example; changelog entry
  carries the action.
- silenceDeprecations gains global-builtin: mixin-exists is one of the
  deprecated global built-ins, and the interim @import tree cannot host
  a sass:meta @use; the @use refactor clears both entries.
Supersedes the repo-PATH fix, which made the smoke false-clean: it
injected the maintainer repo's compiler into a job that exists to
simulate a consumer install, hiding exactly the failure mode the swap
introduces (adversarial round 1, finding 3; round 2, P1).

_npm_install now installs sass-embedded in the generated site and puts
the site's own bin dir on PATH, for every Docsy source: the documented
consumer action, exercised end to end. Placed after the --omit=dev
DEPS install, which prunes devDependencies (red-proven: install-first
left no sass binary and the build failed TOCSS-DART; reordered run is
green with the site's sass resolving under the generated .npmrc's
release cooldown).
Selector-nested Sass @imports compile fine under dart-sass (_nav.scss
does it); only control-directive nesting errors. The generated files
now carry both header comments above the mixin wrapper instead of one
inside its body.
Every documented deployment path ran hugo where npm's bin dir is not
on PATH, so a consumer following the changelog remedy alone still
failed to build (adversarial round 1, finding 1):

- Prerequisites gain an Install Dart Sass section (sass-embedded for
  npm sites, Hugo's install guide for the rest) and lose the stale
  'extended supports SCSS' rationale.
- GH Pages workflow exports node_modules/.bin via GITHUB_PATH before
  the bare hugo step; Netlify's documented build commands carry an
  inline PATH prefix (Netlify has no GITHUB_PATH equivalent; round 2,
  P2).
- Official-support matrix names the Dart Sass version: the repo's
  sass-embedded pin, with 1.74.0 as the hard floor (first release with
  the deprecation-silencing options the build uses).
Leads with the sass requirement (linking the new prerequisite and
deployment docs), states the color change accurately (serialization
form, not perceptible shift, with the string-matching caveat), and
names the escape hatch's expiry alongside the fact that Hugo will
never bundle Dart Sass (adversarial round 1, finding 4).
Terse bullets linking their docs homes, per the repo's separation of
concerns; the chroma wrapper becomes its own breaking-change bullet.
The evicted release narrative (what to re-check, escape hatch and its
expiry, BSD caveat, CF Pages snippet) moves to the 0.17 upgrade-post
material at release prep; the post link gets woven in then, per the
0.16.0 entry pattern.
The tarball, registry, and clone consumer simulations inherited the
test process PATH, whose repo node_modules/.bin silently supplied the
compiler the scratch sites never installed: a false-clean on exactly
the defect class the smoke exists to catch (adversarial round 3,
finding 3; red-proven by sanitizing PATH alone, which failed all
consumer builds with the missing-Dart-Sass error).

Consumer Hugo builds now run with the site's own bin dir first and
this checkout's directories stripped, and each simulated consumer
installs sass-embedded from its site root, the documented action.
The submodule, clone, and npm-package guides kept their own
prerequisite section and bare-hugo build commands, so a consumer
following any of them built without a sass provider and failed
(adversarial round 3, finding 1; reproduced with a packed tarball
in a clean-PATH site):

- other-options prerequisites gain an Install Dart Sass pointer with
  the direct-invocation PATH form; the option 1 and 3 hugo commands
  carry it; option 3 installs sass-embedded beside the theme.
- Setup overview: the npm option's 'no extra toolchain' claim now
  names the compiler as part of the npm-only toolchain.
- Root README: Dart Sass prerequisite added; stale 'extended supports
  SCSS' rationale dropped (both spots).
The README renders into the live site, where lychee checks the
absolute URL's fragment against production; the section only exists
once this PR deploys. Page link + named section instead.
@chalin
chalin force-pushed the chalin-m24-dartsass-swap-2026-0818 branch from 98bddff to ae14ae7 Compare August 18, 2026 21:09
chalin added 5 commits August 18, 2026 17:19
- netlify: the build-command lead-in described the PATH prefix as an
  action to take, while the commands below already carry it; now
  descriptive.
- prerequisites: the Dart Sass floor was stated here and in the
  support matrix; the matrix owns versions, this page now points.
- get-started overview: colon for the elaboration; dependency grammar.
- make-site/smoke: comment trims (what-restating clause; duplicate
  rationale now a pointer).
Probe-verified semantics: Hugo feeds the entry via stdin and loads
every other file (Docsy's own tree included) through its importer,
which dart-sass counts as dependency loading. So
silenceDependencyDeprecations already covers _code-dark.scss's
mixin-exists; only the entry file's own import warning needs the
explicit ID. Build verified warning-free without it.
Dependency semantics probe-verified: dart-sass treats files loaded
through importers as dependencies (sass-lang.com Options#quietDeps),
and Hugo resolves every non-entry file through its ImportResolver, so
only the stdin-fed entry is first-party.
The dict lost the option in the previous commit while the comment kept
documenting it; the entry's import warning was back. Build re-verified
warning-free.
@chalin
chalin force-pushed the chalin-m24-dartsass-swap-2026-0818 branch from b7b9abf to 83ed5be Compare August 18, 2026 22:08
chalin added 15 commits August 18, 2026 18:13
The probe guards Hugo API deprecations (its original scope, google#2647);
with the transpiler swap keeping Dart Sass language deprecations
visible as fix-me reminders, its bare /deprecated/i filter tripped on
every Sass line. Red-proven before the exemption, green after; the
Hugo-API signal is unchanged.
sass:meta's load-css is the module-system replacement for nested
Sass @import: an @include, legal under @if, that emits the loaded
file's CSS in place. Probe-verified that an @import-loaded file may
carry @use sass:meta and still read the importer's globals.

Reverts the mixin machinery it obsoletes: generator wrapper, raw
chroma dumps restored, mixin-exists guard, docs wrapper instruction,
and the chroma changelog bullet. The documented consumer path (a raw
hugo-gen-chromastyles dump at the override mount) is no longer a
breaking change: red-proven before this branch's guard, green now
with the custom style landing scoped.

Compiled CSS: rule-identical; the only diff is the generator header
comments now emitting inside the scheme blocks.

LibSass escape-hatch cost: libsass has no sass:meta, so the hatch
grows to a third override (_code-dark.scss, original nested-@import
form); carried in the 0.17 upgrade-post material.
One home for the instructions (the prerequisites page); the README
names the requirement and links.
A pinned allow is load-bearing (a bump's new script must fail the
install until re-reviewed); a pinned deny is bump churn with no
security value, since the answer is false for every version. Matches
otel.io's committed form (fsevents: false). Red-proven via the audit
test's exact-map assertion, which now encodes the principle.
deepEqual already prints the expected list; the message states the
invariant.
Sweep of the messages inherited with google#2725, per the repo's
assert-message guidance: expected-condition phrasing (no 'still',
'unexpected', 'must'), and values that assert.equal already prints on
failure dropped from messages. The parser fixture regexes that match
these messages are updated in step; the full suite is the proof.
The install-script list reads as if it claimed execution; the comment
states the invariant (platform gating is package-controlled metadata,
so lock presence alone earns review). Includes an owner spacing tweak
in head-css.html.
Source-verified against the current build system: the build command
runs under @netlify/build's execa call with preferLocal: true, which
puts the base directory's node_modules/.bin on the child PATH (the
archived focal image's broken 'npm bin' export misled the earlier
fix). With sass-embedded declared per the prerequisites, bare hugo
finds sass. GHA keeps its GITHUB_PATH step: Actions has no
preferLocal equivalent.
sass-embedded also gains code font in the link text, per the link
conventions.
Verified against the rendered HTML: both pages emit
id=install-dart-sass without the annotation; fragment links intact
(link check green).
Best-practice consolidation of the Dart Sass PATH story: npm-route
sites define build/serve scripts (example in the prerequisites) and
run Hugo through them; non-npm sites have sass globally per Hugo's
guide. Removes every inline PATH prefix; the GH Pages workflow's
GITHUB_PATH step collapses into npm run build.
@chalin
chalin force-pushed the chalin-m24-dartsass-swap-2026-0818 branch from 2363bce to 9aee86c Compare August 18, 2026 23:32
chalin added 12 commits August 18, 2026 19:37
One unopinionated script ('hugo': 'hugo') replaces the build/serve
pair: every documented Hugo invocation becomes npm run hugo -- ARGS,
and the docs stop legislating flag choices. Matches the passthrough
pattern docsy.dev and docsy-example already use.
Mirrors the documented consumer pattern: the site carries the hugo
passthrough script and builds via npm run hugo, with sass-embedded
resolving through npm's own script PATH. Harness twists, each
commented: the script body is "$HUGO" (the borrowed repo binary by
absolute path; a bare name would need the repo bin dir on PATH), and
the scaffolded .npmrc pins script-shell=bash for the expansion on
Windows.

Known limit: npm run PATH-walks ancestor node_modules/.bin, so for
sites scaffolded inside the repo tree (CI's tmp/) the repo's sass
remains reachable; provenance enforcement lives in the smoke.mjs
consumer legs, which build outside the repo under a sanitized PATH.
npm run PATH-walks ancestor node_modules/.bin dirs, so a site
scaffolded under the repo's tmp/ can silently borrow the repo's sass,
false-cleaning the consumer legs. The scaffold moves to runner.temp
(referenced via the runner context; a GITHUB_ENV write would trip the
audit's env-isolation rule), and the toolchain-install step is renamed
for what it provides: the pinned Hugo that make-site borrows.
One SCAFFOLD definition exported via GITHUB_ENV, three env.SCAFFOLD
uses (two working-directories, the artifact path); the runner-temp
literal appears once. The audit's blanket GITHUB_ENV denial becomes an
exact-pinned per-line allowance mirroring its GITHUB_PATH pattern,
with the variable's assignment pinned too (the echo pin alone would
leave the value free). Red-proven: the audit tripped on the write
before the pin, green after.
- smoke.mjs: extract installSiteSass(), the consumer sass install
  duplicated verbatim across the tarball and clone legs
- changelog: drop the 1.74.0 floor claim; its rationale (silencing
  options the build uses) went stale with the silencing reversal.
  The support matrix names only the pinned version; the floor moves
  to release-post guidance. Prerequisites pointer follows suit.
- supply-chain audit: each GITHUB_* sink's rationale above its own
  loop; comment dash fixes here and in make-site.sh
- make-site.sh: prune the trailing what-comment on the build line
- smoke workflow: unbend the checkout-role comment's inverted
  sentence
Prune restatements (workflow header restating the matrix, the
create_site_directory banner, the VERBOSE aside), dead commented-out
code, a cross-file parenthetical, a stale cSpell word, and the
passthrough comment's closing overclaim: npm run also walks ancestor
bin dirs, so 'only it' was wrong, and the true part is standard npm
knowledge. Trim the HUGO comment to intent only.

smoke workflow: prune the toolchain-step comment

Dialog residue promoted to a comment: the Hugo borrow is homed at
make-site.sh's HUGO fallback, and the Docsy-from-GitHub sentence
restates the matrix.
Construct-first dash fixes (colons, comma appositive, 'from' in
progress lines; the extended-Hugo assert message also flips to the
affirmative form), two comments pruned to their non-obvious fact.
Includes the installSiteSass helper extraction. Full smoke suite
green.
Review feedback: the Dart Sass exclusion assumes Hugo's log-line
prefix. Record that a prefix change turns this gate red rather than
silently widening it -- the safe direction, so no structural match is
needed.
@chalin
chalin merged commit 24ed808 into google:main Aug 19, 2026
18 checks passed
@chalin
chalin deleted the chalin-m24-dartsass-swap-2026-0818 branch August 19, 2026 02:06
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.

Drop libsass, use dart sass instead

1 participant