Repository navigation
Build the website without webpack's cache so new Tailwind classes ship - #1425
Merged
Merged
Conversation
policyengine.org's stylesheet is missing twelve Tailwind classes that the website's source uses, so the homepage tool cards render with no padding, gap or icon box, and supporter card headers aren't centered. A clean build of the same source has all of them. A production build that restored webpack's persistent cache kept CSS compiled from older source when only server-rendered files had changed: reproduced locally by adding a class to homeCardClassName.ts and rebuilding on a warm .next/cache. The client build's cache has no record of those files. Vercel restores that cache before every build. Production builds now run without the cache. Dev runs on Turbopack and is unchanged. The calculator's CSS matches a clean build, so it keeps its cache. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
10 tasks done
…ild workers, and correct the cause - scripts/check-built-css.mjs compares the classes in .next/static/css with a fresh compile of the app's stylesheet, scanned from the app's directory. It fails the build if a class the source uses is missing, or if the build has a Tailwind utility the source doesn't use. Both the website's build.sh and the calculator's build script run it after next build. - Against production's live stylesheet it reports the 12 missing classes. - next.config.ts states the real cause: Tailwind makes src/ a context dependency of globals.css, and webpack can't snapshot a directory holding symlinks to files (src/data/*.json), so the cached CSS module records no inputs and warm builds reuse it whatever changed. - experimental.webpackBuildWorker keeps the build workers that a webpack hook otherwise turns off. - build.sh drops .next/cache/webpack, which production builds no longer read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…racter as an escape An attribute value can only hold a dot inside a string or an escape, so the separate attribute-selector skip did nothing; a mutant removing it survived. The property test now also covers an escaped dot in an unquoted attribute value. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…and harden the selector reader - The config comment and the script's header say the check compares class names, not values. - A test reads Tailwind's source directories for each app and asserts the calculator's hold no file symlinks. - The selector reader skips comments; its property test draws an ASCII-heavy alphabet with :has and two-class wrappers. - The entry guard resolves process.argv[1], so the script still runs through a symlinked path. - The failure message says how to recover on Vercel. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…den the check's entry guard - The calculator's cause test takes webpack's own snapshot verdict, with a production build's options and Next's managedPaths, instead of walking for symlinks. A file link behind a directory link now fails it; the website stays the positive control. - The entry guard compares real paths on both sides and can't throw, so --preserve-symlinks-main and node -e imports behave. - New selector examples: a tab or form feed after a hex escape, upper-case hex, and a comment followed by *. - The fresh-compile fixture has a rule Lightning CSS rejects, which pins optimize: true. - build.sh, the script's header and its failure message say the check compares class names. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The webpack oracle copies the managedPaths Next gives webpack, and its verdicts depend on them. A new test reads Next's build/webpack-config.js and fails if the block that sets webpack's snapshot paths changes. The .ie-hack test now sets NODE_ENV to development, since Tailwind optimizes by default under production and would otherwise hide a check that didn't ask for it. The entry guard's comment now says what happens under `node -`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
Author
|
Merged at the reviewed head bb6f4ae with Gates at merge:
Earlier rounds: 1 (01815e5, ten changes), 2 (5213962, six), 3 (c029781, five) and 4 (348c40d, approved with four nits, fixed in bb6f4ae). Reviews are kept in Next: check |
Contributor
Author
|
Production after the merge (87d3359, deploy
|
2 tasks done
This branch was successfully deployed
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.
Why
policyengine.org is serving stale CSS. The live stylesheet (
/_next/static/css/b76c71cfd6868604.css, fetched 2026-10-25) is missing twelve Tailwind classes that main's website source uses. A cleannext buildof the same source has all of them. The visible damage:HomeToolsPreview, from Link CliffWatch and the OBBBA explorer from the US homepage and main nav #1421 on 10/24). The cards have no padding (p-3xl), the grid has no gap (gap-2xl), and the 40px icon box (w-10,bg-primary-50) stretches to the card's full width with no background. On policyengine.org/us the card body has no padding, the grid gap isnormal, and the icon box spans the card (490 px at a 1024 px window).SupporterCard, from 10/14).sm:items-centerdoesn't apply, so each card's header (logo and text) isn't vertically centered: the header computesalign-items: normal, measured at a 1024 px window. For a supporter with no logo, the card has a spacer,tw:hidden tw:sm:block tw:w-[200px], that lines its text up with the logo cards. Production has nosm:blockrule, so the spacer stays hidden.The full list of missing classes:
HomeToolsPreview:bg-primary-50,focus-visible:outline-none,focus-visible:shadow-[0_0_0_2px_var(--color-primary-500)],gap-2xl,mt-2xl,p-3xl,sm:min-h-[280px],tracking-[0.06em],w-10.SupporterCard:sm:block,sm:items-center,w-[200px].#1424's preview has the same problem: its CSS lacks the focus-outline classes that PR adds.
Cause
Any change at all leaves the CSS stale, not only changes to server-rendered files, as this PR first said. The round-1 review found the trigger: symlinked data files.
Tailwind makes
website/srca context dependency ofglobals.css. Its PostCSS plugin emits adir-dependencymessage for each source root. Next 15.5.12's PostCSS loader passes each one tothis.addContextDependency(build/webpack/loaders/postcss-loader/src/index.js:91-92).The webpack bundled with Next 15.5.12 can't snapshot
website/src.website/src/dataholds six symlinks to files inapp/src/data:apps.json,citations.json,events.json,featuredTools.json,posts/authors.jsonandposts/posts.json.website/src, webpack'sFileSystemInforeturns null, withENOTDIR: not a directory, scandir '…/app/src/data/posts/posts.json'. It reads each symlink's target as a directory.website/scripts,calculator-app/src,calculator-app/scriptsandapp/srcsnapshot fine.The cached CSS then has no record of its inputs. After taking a snapshot, webpack's
NormalModuledrops the module's file, context and missing dependency lists, and keeps only the snapshot, which here is null. I read that code.importModule. The outerglobals.cssmodule records none of the scanned files, and notglobals.csseither, so on a warm build its cached CSS still counts as valid.Reproduced on the real website (this branch's base, with main's config, in a scratch copy). Three builds:
tw:mt-[53px]to the client componentHomeBlogPreview;globals.css.All three emitted the same
f4d325ca88a07d63.css, so neither change reached the CSS.Why production isn't stale forever. Next rotates its build encryption key every 14 days (
server/app-render/encryption-utils-server.js:28). The key is part of webpack's cache version (serverReferenceHashSalt,build/webpack-config.js:1882), so the whole cache is rebuilt at least that often. The review found that the live stylesheet's classes exactly match main's source at 9c98884 (10/14): 586 classes.Corrections to the first version of this PR:
Change
website/next.config.ts):config.cache = falsewhen!dev.cache: false, webpack never reads.next/cache/webpack. It doesn't depend on the symlinks, and it would cover any other way a snapshot fails.build/webpack-build/impl.js:150-176,build/webpack-config.js:2005).next devruns on Turbopack, which doesn't call the hook. It now prints "Webpack is configured while Turbopack is not", which the comment says is expected.webpackhook makes Next run every compilation in the main process unlessexperimental.webpackBuildWorkeris set (build/index.js:835). This PR sets it, so the website builds in the same processes as on main.scripts/check-built-css.mjs). It runs afternext build:.next/static/css.tw:utility the source doesn't use.RichTextBlock.css.build.shand the calculator'sbuildscript both run it. CI and Vercel run those scripts.build.shdeletes.next/cache/webpackbefore building. Production builds no longer read it, but Vercel restored and re-uploaded it. The website's build-cache upload went from 444.70 MB to 341.01 MB.Not changed: the calculator's cache
Webpack can snapshot all three of the calculator's Tailwind source directories, so it keeps its cache. Its production CSS (
9ec0e33bef9ec190.css) is byte-identical to a clean local build, and a probe class added to a server-rendered file reached the CSS on a warm build.Two tests keep it that way:
next-build-cache.test.ts). The test takes the directories webpack must snapshot from Tailwind's owndir-dependencymessages, so it can't drift from the config:app/src,calculator-app/srcandcalculator-app/scripts. Then it asks the webpack bundled with Next to snapshot each one, with a production build's snapshot options and Next'smanagedPaths, and fails if any snapshot comes back null. So a symlink webpack would read as a directory fails it, including one behind a directory link, without the test re-deriving webpack's rules. ThemanagedPathsare copied from Next's build config, and another test fails if Next's copy of that block changes. For the website, the same test expects exactlywebsite/srcto fail, so it can see a failure, and it will say so once a Next upgrade fixes the bug.next build. It catches stale CSS once the source adds a class or drops a Tailwind utility. It misses a change to a value alone, and a hand-written class the source dropped.Cost
Website builds on Vercel, from their build logs. The build step runs from the log's
Running "bash scripts/vercel-build.sh"line to its "Build Completed" line, by the log's timestamps. Round 2's body took 01815e5's first build step as 41 s and round 3's as 42 s: it is 41.5 s, and Vercel's own summary line rounds it to 42 s. Every row below is now the timestamp difference, to a tenth of a second.next.config(build/webpack-build/index.js:93,impl.js:340-355). In one process, the three compilations share one input file system (impl.js:199-251). So workers could cost time.build.sh: 5213962's runs the check and deletes the old packs, which 01815e5's doesn't. Both add time to the builds with workers, so they can't hide a cost. Compile times were 23.5 vs 22.1 s, 26.2 vs 32.4 s, and 34.4 vs 23.2 s. Build steps were 49 vs 47 s, 53 vs 67 s, and 73 vs 49 s.~/reviews/app-v2-pr1425/r3-worker-cost/.Invariants
tw:utilities. This is about class names, not values.isServerfalse and true. The edge compilation calls the hook withisServer: true.-, backslashes, quotes, brackets, tab, U+0001, U+007F).CSS.escape, or every character as a hex escape.:is,:not,:whereand:has, around one class or a list of two; pseudo-classes and pseudo-elements; ID and type selectors./*! … */, an unclosed one, and one followed by*.missingis exactly fresh minus built. The classes only the build has split by prefix into stale utilities and other CSS.website/src, as a positive control. The snapshot options come from two places, and a test pins each: webpack's production default forsnapshot.module, and the block in Next'sbuild/webpack-config.jsthat setsmanagedPathsand nothing else outside Yarn PnP. Nothing after that block, in Next or in either app'swebpackhook, sets them in Next 15.5.12 (read by the round-5 review), but no test checks that yet; a follow-up PR adds one.--preserve-symlinks-main, it still exits 2 without arguments and 1 for a stale build. Imported fromnode -ewith an argument, it runs nothing and doesn't throw. Code read from stdin has no path to compare, so it runs nothing; no build runs it that way.NODE_ENV. A rule Lightning CSS rejects is dropped from it, as the build drops it. Tailwind optimizes by default whenNODE_ENVis "production", which would hide a check that didn't ask for it, so the test setsNODE_ENVto "development" for that compile.Verification
End to end: real website builds in a scratch copy of this branch.
RichTextBlock.css).tw:mt-[61px]toHomeBlogPreview: the CSS lacks it, and the check exits 1, naming it.build.sh, twice, with the probe changed totw:mt-[67px]in between. Both builds pass, both stylesheets contain the probe, and no.next/cache/webpackis left.Production's live stylesheet: the check reports exactly the twelve missing classes listed above, and nothing else.
Calculator, local build: passes the check (783 classes).
Vercel at 52e0dd7, 5213962 and c029781: both apps' builds ran the check and passed. The website printed "has all 598 classes of a fresh compile of src/app/globals.css, plus 1 from other stylesheets"; the calculator, "has all 783 classes".
Website checks at bb6f4ae: full suite (49 files, 454 tests), and the two config test files again with
NODE_ENV=production(42 tests). CI at bb6f4ae: lint, format, type checking and the full suite on Node 26 (49 files, 454 tests).Mutants (
mutants11.py, run against the config and check tests): 44 of 46 fail at least one test, and the four control trees pass. Round 4's set is unchanged (41 of 42); the four new ones are listed last.build.shskips the check, runs it before building, or keeps the old packs; the calculator skips the check.@keyframesare skipped.process.argv[1], leaves the module's URL unresolved, or letsrealpathSyncthrow;calculator-app/src, incalculator-app/scripts, deep inapp/src, or incalculator-app/src/node_modules;calculator-app/srctowebsite/src/data, which holds file links (the round-3 review's case);{timestamp, hash},{timestamp}or{hash}, so it's equivalent here. A separate test pins the options a production build uses. E1 is expected: it shows that the newNODE_ENVstub is what kills N4 under production (below).optimize, run withNODE_ENV=production: killed, now that the test setsNODE_ENV. Without that line it survives, so the line is what kills it;managedPathsemptied, or its regex changed: both killed by the new test of Next's config;calculator-app/srcto a package undernode_modulesthat holds a file link. It passes, because webpack snapshots the package by itspackage.json.calculator-app/srcpointed atapp/src/data/apps.json, which doesn't exist (the file isapp/src/data/apps/apps.json). So it tested a dangling link, and the old walker failed it only becausestatSyncthrew. Its target is fixed here, and the link to a missing file is now its own control.First version's preview (01815e5), at a 1024 px window, measured in the browser:
/ustools grid: gapnormal/usCliffWatch card: body padding/usCliffWatch card: icon boxrgb(230, 255, 250)/us/supporters: the 14 elements withsm:items-centeralign-items: centernormalIndependent review, round 1 (Opus 5.5, at 01815e5) asked for ten changes:
All ten are addressed here. For finding 7, I kept a dev case, renamed for what it covers:
next devwithout--turbopackstill calls the hook, and keeping the cache there keeps rebuilds incremental.Independent review, round 2 (Opus 5.5, at 5213962) asked for six changes. All six are addressed at c029781:
process.argv[1], with a test.:hasand two-class wrappers, plus the three examples the review gave. The mutants N1 to N3 now fail.Independent review, round 3 (Opus 5.5, at c029781) asked for one should-fix and four nits. All five are addressed at 348c40d:
--preserve-symlinks-mainand threw when imported fromnode -ewith an argument. It now compares real paths on both sides and can't throw, with a test for each case.optimize: truestays, and the fixture shows why.build.sh, the script's header and failure message, this body's calculator section and invariant 3.Independent review, round 4 (Opus 5.5, at 348c40d) approved, with four nits. It instrumented real
next builds of the calculator and a minimal app, and on every tree that reached the CSS, the build's snapshot verdicts matched the test's. All four nits are addressed at bb6f4ae:node -: thereargv[1]is-, and code from stdin runs nothing. The comment now says so.managedPathsby hand, and the verdicts depend on them. A new test reads Next'sbuild/webpack-config.jsand fails if that block changes. The pin test's comment now says "could change"..ie-hacktest couldn't catch a check that didn't ask foroptimizewhen run withNODE_ENV=production. It now stubsNODE_ENV.node_modules, doesn't, rightly, because webpack never reads it. The calculator section is reworded.Independent review, round 5 (Opus 5.5, at bb6f4ae) approved, with two nits. First, the new test checks only Next's snapshot block, not options set after it, in either app's
webpackhook, or in a copy of Next only the calculator uses. Nothing sets them there today: the review grepped Next 15.5.12'sdistand read both hooks. That test hardening goes in a follow-up PR, so this fix ships now. Second, two overstatements in this body, fixed above.After merge
The next production deploy builds without the cache, so the homepage tool cards and supporter cards get their styles back. I'll check
/usand/us/supporterson production. Then #1424 syncs with main and gets a fresh preview build.axiom: n/a: website and calculator build tooling only
🤖 Generated with Claude Code