Repository navigation
feat(utils): shared Next.js StyleX adapter (webpack mode) - #1380
Merged
schickling-assistant merged 4 commits intoSep 24, 2026
Merged
schickling-assistant merged 4 commits into
schickling-assistant merged 4 commits into
Conversation
schickling-assistant
marked this pull request as ready for review
September 24, 2026 15:49
`@overeng/utils/node/stylex/next` exports `createStylexNext`: one shared StyleX babel options object feeds both a webpack `enforce: 'pre'` babel-loader rule scoped to the configured source dirs (keeping SWC and next/font intact — no root Babel config) and the `@stylexjs/postcss-plugin` entry that collects into the carrier stylesheet. Missing carrier imports or `@stylex` at-rules fail the build loudly at config time and on the client compilation, instead of silently shipping without StyleX CSS. `enableInlinedConditionalMerge` matches @stylexjs/unplugin's Vite path so the atomic class names are identical across bundlers for the same source; the fixture build test asserts that equality end-to-end for Next 15.5 and 16 (webpack mode; Turbopack unsupported). agent-identity: dev3.direct.omp.c35amsc9 agent-persona: generalist agent-supervisor: unavailable agent-tool: OMP agent-tool-version: 18.2.11 agent-runtime: OMP 18.2.11 agent-model: openai-codex/gpt-5.6-sol tooling-profile: dotfiles@fa0992e
agent-identity: dev3.direct.omp.mx94s4y2 agent-persona: generalist agent-supervisor: unavailable agent-tool: OMP agent-tool-version: 18.3.0 agent-runtime: OMP 18.3.0 agent-model: openai-codex/gpt-6-sol tooling-profile: dotfiles@0d57ec1
schickling-assistant
force-pushed
the
schickling-assistant/2026-09-24-stylex-next-adapter
branch
from
September 24, 2026 19:09
452fe99 to
09b6454
Compare
agent-identity: dev3.direct.omp.mx94s4y2 agent-persona: generalist agent-supervisor: unavailable agent-tool: OMP agent-tool-version: 18.3.0 agent-runtime: OMP 18.3.0 agent-model: openai-codex/gpt-6-sol tooling-profile: dotfiles@0d57ec1
agent-identity: dev3.direct.omp.mx94s4y2 agent-persona: generalist agent-supervisor: unavailable agent-tool: OMP agent-tool-version: 18.3.0 agent-runtime: OMP 18.3.0 agent-model: openai-codex/gpt-6-sol tooling-profile: dotfiles@0d57ec1
schickling-assistant
deleted the
schickling-assistant/2026-09-24-stylex-next-adapter
branch
September 24, 2026 21:46
This was referenced Sep 24, 2026
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.
Problem
StyleX has a Vite path in
@overeng/utils/node/stylex(createStylexVitePlugins) but nothing for Next.js. The one proven Next integration lives as app-local config in the schickling.dev blog worktree (apps/schickling.dev/stylex.babel.mjs+next.config.mjs+postcss.config.mjs): a webpackenforce: 'pre'babel-loader rule plus@stylexjs/postcss-plugin, hand-wired so the two passes share one Babel options object. Every future Next consumer would copy-paste that wiring with nothing keeping the two sides from drifting — and both of its silent failure modes (carrier import lost,@stylex;at-rule removed) ship an app with no StyleX CSS and a green build. The obvious alternatives are dead ends:@stylexjs/nextjs-pluginis npm-deprecated and frozen at StyleX 0.11.1,@stylexjs/unpluginpublishes no./nextentry (its emission is written against Rollup'semitFile, which webpack does not provide), there is no@stylexjs/swc-pluginon npm, and a rootbabel.config.jsswitches the whole app off SWC and breaksnext/font(measured: the build fails).Goal
@overeng/utils/node/stylex/nextexportscreateStylexNext: one factory, one shared StyleX options object, feeding both the webpack rule and the PostCSS plugin entry — with loud build failures when CSS emission/collection goes missing, proven by a fixture Next app build whose class names match the Vite/Babel path for the same token source.Decisions
next.js/next-types.d.ts), mirroring./node/stylex: Next loadsnext.config.mjs/postcss.config.mjsthrough Node, which refuses TypeScript stripping for packages undernode_modules(fix(stylex): make Vite preset Node-executable #1167), and naming Next/webpack types would publish this package's bundler major as part of its contract.adapter.webpackRule.use[0].options === adapter.postcssPlugin['@stylexjs/postcss-plugin'].babelConfig— asserted by identity in the unit tests. The class names in SSR markup and the selectors in emitted CSS are produced by the same options, so they cannot drift.enableInlinedConditionalMerge: trueon the shared options — not cargo-cult: it is what makes this transform's atomic class names identical to@stylexjs/unplugin's Vite path for the same source (verified by direct@stylexjs/babel-pluginexperiments; the fixture build asserts the equality end-to-end).@stylex;at-rule absent, sourceDir absent, mdx in extensions) and build-time (a webpack guard on the client compilation pushescompilation.errorswhen the carrier is absent from the module graph or an unreplaced@stylexat-rule survives into an emitted CSS asset). The module-graph check is build-only — dev's lazy/HMR graphs can legitimately defer the carrier import; the residue check stays in both modes.ts,tsx,js,jsx,mjs,cjs,mts,cts; a custom list changes both the webpack test and collector globs. Unsupported suffixes (including mdx) are rejected.node_modulespointing outside the app root make Next 16's cold build exit 1 with zero diagnostics — reproduced on a plain-CSS baseline with no StyleX and no webpack hook — while a real install cold-builds cleanly. The fixture type-checks with classictypescript@5.9.3because Next's dependency check requirestypescript/lib/typescript.js, which the catalog's native TS 7 package no longer ships.stylex.*calls stay uncompiled and styling silently degrades to the runtime. Consumers pass--webpack; the adapter documents this at the entry.postcss@8.5.26, parses actual at-rules in the carrier and emitted CSS rather than mistaking comments/strings for rules. The Babel loader and StyleX plugins still resolve from the consuming app.externalPackagesis also returned astranspilePackagesand checked against the Next webpack-hook config, so uncompiled TS/JSX cannot silently bypass Next transpilation. Next exact-pins its internal PostCSS 8.4.31; the two-version exception preserves that pin.Verification
Review fixes were test-first;
vitest run src/node/stylex/next.unit.test.tsfailed 5/21 before the adapter changes:src/**/*.{ts,tsx,js,jsx,mjs,cjs,mts,cts}, got a glob withoutmts,cts; custom extension test also required matching webpack behavior. After: both share one validated set.adapter.transpilePackagesto equal['@scope/pkg'], gotundefined. After: that value is returned and the webpack hook rejects missing/mismatched Nextconfig.transpilePackages.runFixtureCommand is not defined); after, a deliberately non-terminating child is killed at 100 ms and the thrown error names the timed-out fixture. Both fixturespawnSynccalls have explicit child deadlines (90 s install, 180 s build)./* @stylex; */ .x { content: "@stylex;" }produced an erroneous build failure before; after, comments/strings are ignored and a nested real@stylex;is detected./* @stylex; */was accepted before; after, a carrier needs an actual empty@stylex;PostCSS at-rule (not a comment, string or parameterized rule).Post-rebase proof at head
058cdcefd9e6c2f8c97f72af5a3c70b62e8aa89f:vitest run src/node/stylex/next.unit.test.ts src/node/stylex/next.build.test.ts --reporter=dot— 23/23 passed (21 unit + child-timeout and real Next 16 webpack/Vite CSS fixture), duration 12.16 s under a no-Corepack PATH with pinnedPNPM_BIN.packages/@overeng/utils/node_modules/.bin/tsc --noEmit -p packages/@overeng/utils/tsconfig.json: 8 diagnostics before (theexternalPackages/extensionsdeclarations were swallowed by unterminated JSDoc; oneError.codeaccess was unguarded), 0 after. Restored both*/delimiters and narrowedresult.errorbefore readingcode. The package compiler/config is the same TS7 source check underlying@overeng/utils:dist.oxfmt --checkandoxlint --deny-warningson changed source/docs/catalog files — clean;bun genie/ci-scripts/native-dep-policy-audit.ts pnpm-lock.yaml—native dependency policy audit: OK.devenv tasks run genie:run --mode singleand subsequent cold-HOMEgenie:check --mode single— passed, including after rebase; generated Buck lock projections refreshed.36046778829passed 16/18 required checks but Linux/macOS Buck unit tests uncovered a PR-caused fixture install failure (spawnSync corepack ENOENT): the contained action has no Corepack on PATH. Before: the focused real-build fixture failed under a no-Corepack PATH. After: the fixture requires an explicit attestedPNPM_BINbacked by repo-pinned Nix pnpm 12.4.1, and its Next subprocess uses the declaredNODE_BIN; the focused StyleX suite passed 23/23 with Corepack absent from PATH, and the actual local-only Buck//packages/@overeng/utils:testaction passed 257/257 including the Next build (28 files).nix build --no-link .#buck2-capabilitiesand cold-HOMEgenie:checkpassed after regeneration; package TypeScript 7, scoped oxfmt/oxlint, andnixfmt --check flake.nixpassed.36022987531exposed two PR-caused defects: unclassified@next/swc-*/@img/sharp-*lockfile families and JSDoc inference wideningwebpackRule.enforcetostring; the policy now classifies both pure prebuilt families, retains@oxlint/binding, andStylexWebpackRuleexplicitly types the rule. All 13 failed jobs had one of these two roots (the Nix iana-etc retry recovered).devenv tasks run check:*was not run locally under dev3 load; CI owns the authoritative broad lanes.Complexity
One checked-JS module + types + tests. The only added helper is a bounded fixture-child runner; PostCSS handles parsing without a custom CSS grammar.
Concerns
next(plus react/react-dom/stylex build plugins) enter utils' devDependencies; direct runtimepostcss@8.5.26adds parsing to the adapter. Next exact-pins internal PostCSS 8.4.31; the catalog exception records both versions rather than overriding Next.testlane (~20s: registry install from the warm store + webpack build). It needs network for the fixture'spnpm install, and Buck supplies the repo-pinned pnpm through its attestedPNPM_BINcapability rather than ambient Corepack; the store is pre-warmed by the repo install which carries the same versions.!isServer && nextRuntime === 'node'compilations only; an edge-runtime app fails loudly today rather than silently — revisit if an edge consumer appears.Friction & bottlenecks
/srv/bulkwas pathologically slow during this work (a baregit worktree listtook 74s; composed-workspace creation timed out twice at 300s). The worktree lives on the home device instead; store hygiene for it is a follow-up.devenv shell -- geniehit globalcore.hooksPathrejection; runningdevenv tasks run genie:run --mode singleavoided shell-entry hooks. An initial Genie pass exposed the unacknowledged Next/PostCSS duplicate, which is now explicitly recorded.Follow-ups
stylex.babel.mjswiring onto this adapter (separate change in that repo).@stylexswc/nextjs-plugin(separate change).context/stylex(peer work).References
schickling-assistant/2026-09-22-2026-09-22-stylex-blog-chrome(apps/schickling.dev/stylex.babel.mjs,next.config.mjs,postcss.config.mjs)Posted on behalf of @schickling
agent_identitysessionagent_personaagent_supervisoragent_toolagent_tool_versionagent_runtimeagent_modelworktreetooling_profile