Skip to content

[oss-candidate] fix(cli): stop citing a Babel example app as the SWC StyleX setup - #1

Closed
askalf wants to merge 2 commits into
mainfrom
fix/docs-nextjs-swc-example-reference
Closed

askalf wants to merge 2 commits into
mainfrom
fix/docs-nextjs-swc-example-reference

Conversation

@askalf

@askalf askalf commented Sep 20, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

  • astryx docs styling tells readers the working Next.js App Router path is an SWC StyleX transform, then cites apps/example-nextjs-stylex "for a complete, working Next.js + StyleX + SWC configuration". That app has no SWC transform — it compiles StyleX with @stylexjs/babel-plugin + @stylexjs/postcss-plugin.
  • So the citation sends the reader to the exact Babel setup the same doc section warns against four sentences earlier: "Do NOT add @stylexjs/babel-plugin to a Next.js App Router app; it disables SWC and breaks next/font."
  • No app in the repo carries an SWC transform: all six apps/example-* declare zero @stylexswc/* dependencies. The recommendation itself is right; only the pointer is wrong.
  • Fix is one prose string in packages/cli/assets/docs/styling.doc.mjs: point at the next.config snippet that already follows in the same section, and say what the example app is actually for.
  • Regression test packages/cli/test/drift/docs-example-apps.test.mjs goes in the repo's existing drift-harness directory: a doc sentence may offer an in-repo example app as an SWC setup only when that app's package.json declares a @stylexswc/* compiler. The file holds 7 tests at this head — 2 discriminating, 5 controls — and checks both ends of the pipeline: the doc source, and the text the shipped CLI actually prints.

Current head 4493e26, both arms run against the same final test file:

$ # BASE — styling.doc.mjs restored from origin/main (0faf070d8), test file unchanged
$ vitest run packages/cli/test/drift/docs-example-apps.test.mjs --project node

 ❯  node  packages/cli/test/drift/docs-example-apps.test.mjs (7 tests | 2 failed) 2401ms
     × offers an example app as an SWC setup only when it carries one 360ms
     × renders no such citation through `astryx docs styling` 1985ms
     ✓ (control) reads the example apps that back the claim 2ms
     ✓ (control) reads an SWC compiler from either dependency field 1ms
     ✓ (control) checks every app a sentence names, not just the first 1ms
     ✓ (control) scans prose only, and no other block type cites an app as SWC 45ms
     ✓ (control) is sentence-scoped, so a split claim and a bare "not" slip through 1ms

 Test Files  1 failed (1)
      Tests  2 failed | 5 passed (7)

$ # HEAD 4493e26 — with the fix
$ vitest run packages/cli/test/drift/docs-example-apps.test.mjs --project node

 ✓  node  packages/cli/test/drift/docs-example-apps.test.mjs (7 tests) 2473ms
     ✓ offers an example app as an SWC setup only when it carries one  335ms
     ✓ renders no such citation through `astryx docs styling`  2080ms

 Test Files  1 passed (1)
      Tests  7 passed (7)

Upstream

  • Repository: facebook/astryx
  • Default branch: main
  • Base sha: 0faf070d8ba2ecbb4ea424e4c0b9c3e85adf87f5 (docs(markdown): define extensible plugin protocol (#6338))
  • Files: packages/cli/assets/docs/styling.doc.mjs (section "StyleX Build Setup (required for swizzled components)", the fourth prose block — line 369) and new packages/cli/test/drift/docs-example-apps.test.mjs (228 lines)
  • Branch head: 4493e26 — two commits, 8d7bfab (the one-line fix + the first 2 tests) and 4493e26 (5 further tests added during adversarial verification, test-only: git diff 8d7bfab 4493e26 -- packages/cli/assets/docs/styling.doc.mjs is empty)
  • Related issue: #6376 — the surface this hunt started from (see ## Bug, last paragraph, for the exact relationship; this PR does not claim to fix The documented Turbopack/StyleX setup produces different layout output from the Babel StyleX setup for the same Astryx template. facebook/astryx#6376's rendering symptom).

Bug

packages/cli/assets/docs/styling.doc.mjs:369 reads: "The working path on Next.js is an SWC-based StyleX transform (e.g. the community @stylexswc/nextjs-plugin) wired into next.config, which keeps SWC and next/font intact. See the example app apps/example-nextjs-stylex in the repo for a complete, working Next.js + StyleX + SWC configuration." The trigger is simply reading that sentence and following it. apps/example-nextjs-stylex declares @stylexjs/babel-plugin and @stylexjs/postcss-plugin in devDependencies, ships babel.config.js and postcss.config.js, has an empty next.config.mjs, and its own README titles it "Next.js + StyleX (Dist)"; grep -ril swc apps/example-nextjs-stylex matches nothing. The wrong outcome is that a reader looking for the SWC wiring the paragraph just recommended finds, and copies, a Babel config — the setup the same section explicitly tells them not to use on App Router, because it disables SWC and breaks next/font. The blast radius is every consumer and every coding agent: this file is not an internal comment, it is the payload of the shipped @astryxdesign/cli, rendered verbatim by astryx docs styling, and AGENTS.md makes astryx docs the mandated bootstrap on every branch ("RULE: always run bootstrap on each branch — docs reflect the branch's actual API"). The section is the one a reader reaches after the documented silent-failure symptom ("swizzled component renders with no styles, but no build or runtime error"), i.e. exactly when they are already stuck. git log -S"example-nextjs-stylex" -- packages/cli/assets/docs/styling.doc.mjs dates the claim to 19a921d43 (facebook#4623).

Relationship to facebook#6376, stated plainly: facebook#6376 reports that the same Astryx template renders with different grid spacing under @stylexswc/nextjs-plugin/Turbopack than under @stylexjs/babel-plugin. That difference is between two third-party StyleX compilers Astryx does not ship, and it could not be reproduced here (no Next.js/Turbopack browser harness in this environment), so this PR makes no claim about it. What the hunt did establish is that Astryx's own docs send readers toward the SWC path while citing a Babel app as the reference for it — a defect in this repo, provable in this repo, and the reason a reader ends up hand-rolling the Turbopack config that facebook#6376 describes. facebook#6376 should stay open.

Repro

No app fixture needed — the claim is false against the repo's own tree. On base 0faf070d8:

$ sed -n '369p' packages/cli/assets/docs/styling.doc.mjs
          text: 'The working path on Next.js is an SWC-based StyleX transform (e.g. the community `@stylexswc/nextjs-plugin`) wired into `next.config`, which keeps SWC and `next/font` intact. See the example app `apps/example-nextjs-stylex` in the repo for a complete, working Next.js + StyleX + SWC configuration.',

$ node -e "const p=require('./apps/example-nextjs-stylex/package.json');console.log(JSON.stringify({...p.dependencies,...p.devDependencies},null,1))"
{
 "@stylexjs/stylex": "^0.19.0",
 "@astryxdesign/core": "*",
 "@astryxdesign/theme-neutral": "*",
 "next": "^15.5.21",
 "react": "^19.2.7",
 "react-dom": "^19.2.7",
 "@stylexjs/babel-plugin": "^0.19.0",
 "@stylexjs/postcss-plugin": "^0.19.0",
 "@babel/preset-react": "^8.0.1",
 "@babel/preset-typescript": "^7.29.7",
 "@types/react": "^19.2.17",
 "@types/react-dom": "^19.2.3",
 "typescript": "^6.0.3",
 "autoprefixer": "^10.5.2"
}

$ grep -ril "swc" apps/example-nextjs-stylex || echo "NO SWC REFERENCE AT ALL"
NO SWC REFERENCE AT ALL

$ ls apps/example-nextjs-stylex
README.md  babel.config.js  css.d.ts  next-env.d.ts  next.config.mjs
package.json  postcss.config.js  src  tsconfig.json

$ head -1 apps/example-nextjs-stylex/README.md
# Astryx Example: Next.js + StyleX (Dist)

Every example app, for the same question — nothing in the repo demonstrates SWC:

$ for d in apps/example-*; do n=$(basename $d); r=$(node -e "const p=require('./$d/package.json');const x={...p.dependencies,...p.devDependencies};console.log(Object.keys(x).filter(k=>k.startsWith('@stylexswc/')).join(',')||'NONE')"); printf "%-30s %s\n" "$n" "$r"; done
example-nextjs                 NONE
example-nextjs-source          NONE
example-nextjs-stylex          NONE
example-nextjs-tailwind        NONE
example-vite                   NONE
example-vite-tailwind          NONE

The claim reaches users through the shipped CLI, not just the source file. Both renders below were re-run at the current head 4493e26 (base arm produced by checking styling.doc.mjs out of origin/main, then restoring). The line number is whatever grep -n reports for this terminal; the earlier body quoted sed -n '259p' from the first run's environment — 257 is the number measured here:

$ # BASE: styling.doc.mjs from origin/main
$ node packages/cli/clients/cli/bin/astryx.mjs docs styling | grep -n "The working path on Next.js"
257:The working path on Next.js is an SWC-based StyleX transform (e.g. the community `@stylexswc/nextjs-plugin`) wired into `next.config`, which keeps SWC and `next/font` intact. See the example app `apps/example-nextjs-stylex` in the repo for a complete, working Next.js + StyleX + SWC configuration.

$ # HEAD 4493e26
$ node packages/cli/clients/cli/bin/astryx.mjs docs styling | grep -n "The working path on Next.js"
257:The working path on Next.js is an SWC-based StyleX transform (e.g. the community `@stylexswc/nextjs-plugin`) wired into `next.config`, which keeps SWC and `next/font` intact. The snippet below is the whole wiring. The repo ships no SWC example app: `apps/example-nextjs-stylex` compiles StyleX with the Babel plugin and PostCSS, so read it for the PostCSS layer setup, not for the transform.

That rendered path is now a test, not just a transcript — see test 2 in ## Test evidence.

Fix

One prose string changed; the SWC recommendation and the next.config code block are untouched.

-          text: 'The working path on Next.js is an SWC-based StyleX transform (e.g. the community `@stylexswc/nextjs-plugin`) wired into `next.config`, which keeps SWC and `next/font` intact. See the example app `apps/example-nextjs-stylex` in the repo for a complete, working Next.js + StyleX + SWC configuration.',
+          text: 'The working path on Next.js is an SWC-based StyleX transform (e.g. the community `@stylexswc/nextjs-plugin`) wired into `next.config`, which keeps SWC and `next/font` intact. The snippet below is the whole wiring. The repo ships no SWC example app: `apps/example-nextjs-stylex` compiles StyleX with the Babel plugin and PostCSS, so read it for the PostCSS layer setup, not for the transform.',

This is minimal because the only false thing in the section is the citation. The paragraph already contains the correct answer — the code block immediately after it is the full next.config.mjs SWC wiring — so the fix redirects the reader three lines down instead of out of the file, and states what example-nextjs-stylex is good for so the app is not orphaned from the docs.

Alternatives rejected:

  • Add a real SWC example app (apps/example-nextjs-swc). That is what closed PR #4268 attempted for the adjacent Turbopack case; its author closed it with "Closing as not planned; I'm not going to carry this example forward." A new example app is a maintained artefact with a CI and dependency cost, it needs @stylexswc/* (a community package) in the workspace, and per CONTRIBUTING.md/docs/contributing/pull-requests.md it is a different primary intent. Not a bug fix.
  • Delete the sentence. Loses the useful half — example-nextjs-stylex genuinely is the reference for the PostCSS/useCSSLayers layer ordering, which the reader still needs.
  • Retarget the citation to apps/example-nextjs-source. Also Babel (@astryxdesign/build's babel()/postcss() helpers); it would move the false claim, not remove it.
  • Make the test assert on the exact sentence instead of deriving from package.json. Hardcoding the corrected wording would pass forever regardless of what the apps do, which is the failure mode the repo's other drift guards (scripts/check-theme-template.test.mjs) explicitly avoid: "all of it derived from live sources, never hardcoded".

Test evidence

One new file, packages/cli/test/drift/docs-example-apps.test.mjs (228 lines at head 4493e26), beside the existing docs-drift.test.mjs in the repo's drift-harness directory, following the conventions there and in scripts/check-theme-template.test.mjs (live sources, readable failure report, describe/it with vitest). Tests 1 and 3 came with the fix at 8d7bfab; tests 2, 4, 5, 6 and 7 were added at 4493e26 by an independent adversarial verification run that rebuilt the boundary ledger from the diff.

# Test Kind On base On head
1 offers an example app as an SWC setup only when it carries one discriminating FAIL — reports apps/example-nextjs-stylex from styling.doc.mjs PASS
2 renders no such citation through `astryx docs styling` discriminating (second code path: rendered CLI output) FAIL — reports the same app from real stdout PASS
3 (control) reads the example apps that back the claim control — corpus is real PASS PASS
4 (control) reads an SWC compiler from either dependency field control — dependencies vs devDependencies, and the @stylexjs/ vs @stylexswc/ near-miss PASS PASS
5 (control) checks every app a sentence names, not just the first control — the matchAll loop PASS PASS
6 (control) scans prose only, and no other block type cites an app as SWC control — bounds the prose-only gap over the live corpus PASS PASS
7 (control) is sentence-scoped, so a split claim and a bare "not" slip through control — pins the two known limits, and that the same claim without a disclaimer word is caught PASS PASS

Every control is named (control) and passes on base by design; neither discriminating test passes on base. What each control controls for:

  • 3 — vacuous green. carriesSwcTransform() returns false for a missing package.json exactly as it does for a Babel app, so a corpus that silently went empty would make test 1 green for the wrong reason.
  • 4 — the scope comparison itself, on throwaway fixtures under os.tmpdir(): an SWC dep in dependencies → true, in devDependencies → true, @stylexjs/babel-plugin → false, a package.json with no dependency fields at all → false (no throw on the two undefined spreads).
  • 5 — that a sentence naming two apps checks both, not just the first match.
  • 6 — that proseBlocks() drops tables/lists/code and that no non-prose block in the live corpus makes an SWC example-app claim today, so the omission is bounded rather than assumed.
  • 7 — the two false negatives the rule accepts by design (claim and citation split across sentences; a sentence with an incidental not), plus the positive case proving the rule is not simply dead.

Fails-before — base arm at head 4493e26: styling.doc.mjs checked out from origin/main, the final 7-test file unchanged, then restored. Verbatim:

$ vitest run packages/cli/test/drift/docs-example-apps.test.mjs --project node

 RUN  v4.1.10 /agent-workspace/oss/astryx-wt-1789883308

Built pseudo.json — 370 keys
 ❯  node  packages/cli/test/drift/docs-example-apps.test.mjs (7 tests | 2 failed) 2401ms
     × offers an example app as an SWC setup only when it carries one 360ms
     × renders no such citation through `astryx docs styling` 1985ms
     ✓ (control) reads the example apps that back the claim 2ms
     ✓ (control) reads an SWC compiler from either dependency field 1ms
     ✓ (control) checks every app a sentence names, not just the first 1ms
     ✓ (control) scans prose only, and no other block type cites an app as SWC 45ms
     ✓ (control) is sentence-scoped, so a split claim and a bare "not" slip through 1ms

⎯⎯⎯⎯⎯⎯⎯ Failed Tests 2 ⎯⎯⎯⎯⎯⎯⎯

 FAIL   node  packages/cli/test/drift/docs-example-apps.test.mjs > shipped docs vs the example apps they cite > offers an example app as an SWC setup only when it carries one
AssertionError: expected [ Array(1) ] to deeply equal []

- Expected
+ Received

- []
+ [
+   "packages/cli/assets/docs/styling.doc.mjs: offers apps/example-nextjs-stylex as an SWC configuration, but that app declares no @stylexswc/* compiler",
+ ]

 ❯ packages/cli/test/drift/docs-example-apps.test.mjs:129:19
    127|     );
    128|
    129|     expect(wrong).toEqual([]);
       |                   ^
    130|   });
    131|

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/2]⎯

 FAIL   node  packages/cli/test/drift/docs-example-apps.test.mjs > shipped docs vs the example apps they cite > renders no such citation through `astryx docs styling`
AssertionError: expected [ Array(1) ] to deeply equal []

- Expected
+ Received

- []
+ [
+   "astryx docs styling: offers apps/example-nextjs-stylex as an SWC configuration, but that app declares no @stylexswc/* compiler",
+ ]

 ❯ packages/cli/test/drift/docs-example-apps.test.mjs:141:64
    139|     expect(res.stdout).toMatch(/@stylexswc\/nextjs-plugin/);
    140|
    141|     expect(badSwcCitations(res.stdout, 'astryx docs styling')).toEqual…
       |                                                                ^
    142|   });
    143|

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/2]⎯


 Test Files  1 failed (1)
      Tests  2 failed | 5 passed (7)
   Start at  09:32:09
   Duration  3.27s (transform 309ms, setup 0ms, import 50ms, tests 2.40s, environment 0ms)

Passes-after — head 4493e26, same command, immediately after restoring the file:

$ vitest run packages/cli/test/drift/docs-example-apps.test.mjs --project node

 RUN  v4.1.10 /agent-workspace/oss/astryx-wt-1789883308

Built pseudo.json — 370 keys
 ✓  node  packages/cli/test/drift/docs-example-apps.test.mjs (7 tests) 2473ms
     ✓ offers an example app as an SWC setup only when it carries one  335ms
     ✓ renders no such citation through `astryx docs styling`  2080ms

 Test Files  1 passed (1)
      Tests  7 passed (7)
   Start at  09:32:13
   Duration  3.44s (transform 351ms, setup 0ms, import 120ms, tests 2.47s, environment 0ms)

Sibling file in the same harness directory, untouched by this PR, run at head to show the change does not disturb it:

$ vitest run packages/cli/test/drift/docs-drift.test.mjs --project node

 ✓  node  packages/cli/test/drift/docs-drift.test.mjs (1 test) 2843ms
     ✓ every doc parses and stays in sync with its source of truth  2840ms

 Test Files  1 passed (1)
      Tests  1 passed (1)
   Duration  4.12s

Tooling:

Command Result
pnpm check:repo + lint-staged (via the repo's own husky pre-commit, on both real commits) passed — check:sync, check:knowledge, check:package-boundaries, check:changesets, check:demo-media, check:executable-bits, check:cli-structure, check:use-client, check:portable-scripts, check:i18n-catalog, check:cldr-weekdays, check:fixtures, check:setup-contract
eslint packages/cli/test/drift/docs-example-apps.test.mjs rc=0
prettier --check packages/cli/test/drift/docs-example-apps.test.mjs All matched files use Prettier code style!
prettier --check packages/cli/assets/docs/styling.doc.mjs warns — also warns on the unmodified base copy (checked by writing git show HEAD:... to a temp path and running prettier there). Pre-existing; lint-staged covers only *.{ts,tsx,md}, so .mjs docs are not prettier-enforced. Deliberately not reformatted: that would be an unrelated whole-file diff.

No changeset added. Precedent: 964368929 (facebook#5605) "fix(cli): correct Dialog onClose → onOpenChange in styling docs" — same file, same shape of one-line doc correction — added none, and pnpm check:changesets passes as committed.

Verification method

executed, in this container, twice by two independent runs. Node v24.19.0, pnpm 11.10.0 on PATH (via /agent-workspace/bin; the repo's husky hook calls pnpm and fails 127 without it), vitest 4.1.10, dependencies installed from the committed pnpm-lock.yaml.

  • Head 8d7bfab (author run): the fix, tests 1 and 3, and every ## Repro console block above.
  • Head 4493e26 (adversarial verification run): the boundary ledger below rebuilt from the diff rather than from this body; tests 2, 4, 5, 6 and 7 written for the reachable rows that had none; both arms re-run against the same final 7-test file (transcripts above are that run's, verbatim); the sibling docs-drift.test.mjs; the base/head renders of astryx docs styling; eslint; prettier; and the repo's own pre-commit gate on the commit. Every boundary row marked measured was probed with node -e against the real regexes and the real corpus at this head.

Head integrity measured, not assumed: git diff --stat empty at 4493e26; git diff 8d7bfab 4493e26 -- packages/cli/assets/docs/styling.doc.mjs empty (the verification added no production change); git diff origin/main...4493e26 --stat = 2 files, +229/−1.

Not verified here, and nothing in this change depends on it: the rendering behaviour reported in facebook#6376 (needs a Next.js + Turbopack browser run). This change is text in a .doc.mjs plus a node-project test; it has no runtime, platform, or browser surface.

Fork CI at this sha: gh pr checks 1 --repo askalf/astryx → no checks reported on the 'fix/docs-nextjs-swc-example-reference' branch. The fork's Actions were never enabled (operator card filed at fork creation), so that is absence, not failure; upstream's own CI has not run this branch. Nothing is pending and nothing is red.

Prior art

Search Result
gh search prs --repo facebook/astryx "stylexswc" 1 — facebook#3475 (merged) "dogfooding fixes: document StyleX build requirement for swizzled components", the PR that created this doc section.
gh search prs --repo facebook/astryx "example-nextjs-stylex" 10 — facebook#4268 (closed), facebook#3477 (closed), facebook#3475, facebook#3065, facebook#3060, facebook#3422, facebook#2194, facebook#1421, facebook#626, facebook#1878. None touches this claim.
gh search prs --repo facebook/astryx "styling.doc" no PR proposing a change to this sentence.
gh search issues --repo facebook/astryx "stylexswc" 2 — facebook#6376 (open, this hunt's surface) and facebook#3373 (closed, the issue facebook#3475 answered).
gh pr list --repo facebook/astryx --search "6376 in:body" --state all empty (per the scout; no PR references the issue).
git log -S"example-nextjs-stylex" -- packages/cli/assets/docs/styling.doc.mjs 19a921d43 (facebook#4623) — where the claim entered.
git log --oneline -15 -- packages/cli/assets/docs/styling.doc.mjs 964368929 (facebook#5605) is the precedent for a one-line correction to this exact file.

Closed-unmerged PR read before proceeding, as it is the nearest neighbour: #4268 "feat(examples): add Next.js 16 + Turbopack example (fixes silent unstyled-CSS trap)". It was closed by its own author, not rejected by a maintainer — "Closing as not planned; I'm not going to carry this example forward." It proposed adding a new example app and never touched styling.doc.mjs. Different intent, different files, no overlap with this change; its stated finding (Turbopack does not link a CSS @import of a package stylesheet) is a separate matter and is not asserted here.

No open PR fixes this. Open issue facebook#6376 has no linked PR and is cited, not pre-empted.

Policy

Read: CONTRIBUTING.md, AGENTS.md, CODE_OF_CONDUCT.md, .github/pull_request_template.md, docs/contributing/pull-requests.md, and the wiki page CONTRIBUTING.md links, Contributing with AI Assistants. AI_POLICY.md, AI.md, AGENT_POLICY.md, .github/AI_POLICY.md, .github/CONTRIBUTING.md are all absent (404). No CLA gate found.

AI-assisted contribution is permitted with process, quoted from the wiki's "For AI Assistants" section:

"If you're an AI assistant helping someone contribute to Astryx:"
"2. Don't skip to code. Your human has design intent that needs to be captured in an issue first."
"6. One focused PR > five speculative ones."
"7. Create PRs in draft mode. Always create PRs as drafts (gh pr create --draft). Check with your human that the changes are ready before marking it ready for review."
"8. Self-review before requesting team review. AI-authored contributions must be reviewed by the contributor before requesting maintainer review... Run the linter and tests locally."

Spec protocol, quoted: "Any change to component APIs, new components, or behavioral changes follows this protocol. No exceptions — even for 'small' prop additions." This change is none of those: no component API, no new component, no behavioural change — one documentation string plus a test. docs/contributing/pull-requests.md lists "Correct contributor or consumer documentation" as its own primary intent (documentation.md, minimum evidence: "reader impact, source of truth, rendered or generated result" — supplied above as blast radius, package.json/app tree, and the astryx docs styling render at both shas) and "Restore broken behavior" (bug-fix.md: "reproduction, expected authority, before/after result, unchanged representative path"). Issue-first is satisfied by the existing open issue facebook#6376 on this surface; this PR does not open an upstream issue.

Also honoured: "Keep the intent atomic" — no tagalongs; the prettier noise in styling.doc.mjs is pre-existing and left alone precisely because fixing it would be an independently removable cleanup. "Public repository boundary" — nothing internal in the diff, commit, or this description.

Required tooling run: pnpm check:repo + lint-staged (via the repo's own pre-commit hook), eslint, prettier, and the affected test file under vitest --project node. Per CONTRIBUTING.md the whole suite is pnpm test; it was not run (single-surface change, and the node project's globalSetup builds @astryxdesign/core for every run).

Note for the operator on submission: the wiki requires the upstream PR to be opened as a draft (gh pr create --draft), and .github/pull_request_template.md requires choosing one primary-intent template from .github/PULL_REQUEST_TEMPLATE/ — bug-fix.md or documentation.md fits this change.

Disclosure facts for the operator

Plain facts, for you to write your own disclosure in your own words:

  • An AI agent found this bug. The ticket pointed at issue The documented Turbopack/StyleX setup produces different layout output from the Babel StyleX setup for the same Astryx template. facebook/astryx#6376 (Turbopack/StyleX layout difference); the agent could not reproduce that rendering symptom in a headless container and did not try to pass anything off as a fix for it. It read the Turbopack/StyleX surface in the repo and found the false example-app citation there.
  • The AI wrote the one-line documentation change and the whole of packages/cli/test/drift/docs-example-apps.test.mjs.
  • A second, independent AI run then attacked the candidate: it re-derived the bug from the repo rather than from the description, rebuilt the boundary table from the diff, added five more tests (one discriminating, four controls), and re-ran both arms. That run wrote the current version of this description.
  • The AI ran everything quoted in this document: both A/B arms at the current head, the astryx docs styling renders at both shas, the package.json probes, the sibling drift suite, eslint, prettier, and the repo's own check:repo pre-commit gate. Every console block is copy-pasted output, not reconstructed.
  • Not executed anywhere: any browser or Next.js build. The change has no runtime surface, so nothing in it depends on one.
  • Both commits are authored by askalf; no AI attribution lines are in the commits or the branch name.
  • The AI did not interact with facebook/astryx in any way: no issue, comment, reaction, or PR. All work is in the askalf/astryx fork.

Boundaries

Every predicate, comparison, guard, index expression and truthiness check the diff adds or changes. Rows marked measured were run through the actual regexes/functions at head 4493e26 (node -e probe against the real source strings and the real corpus), not reasoned about. The production diff adds no predicate at all — it is one string literal — so every row below belongs to the new test file.

# Predicate / expression Boundary input Behaviour at head Pinned by
1 SWC_CLAIM = /\bSWC\b/ the base sentence true → app is checked → reported Tests 1, 2 (fail on base) — measured
2 SWC_CLAIM the fixed sentence true (word present) — not sufficient alone; row 8 decides Tests 1, 2 (pass on head) — measured
3 SWC_CLAIM "" (empty sentence from a trailing split) false → skipped, no crash measured; unreachable from real docs, no test
4 SWC_CLAIM "swc" lowercase false → skipped. Deliberate: the docs write the initialism uppercase; a lowercase match would fire on @stylexswc/... package names in ordinary prose measured, no test — no lowercase-only sentence in the corpus
5 SWC_CLAIM word boundary "SWCONFIG" (SWC as prefix of a longer token) false → skipped. \b is load-bearing measured, no test — no such token in the corpus
6 `DISCLAIMED = /\b(not rather than instead of no SWC)\b/`
7 DISCLAIMED the base sentence (no disclaimer) false → sentence checked → base red Tests 1, 2 (base) — measured
7b DISCLAIMED false positive a true defect carrying an incidental not: "Do not skip this: apps/example-babel is the complete SWC setup." true → not reported. Known limit of the heuristic Test 7 — and its third assertion shows the same sentence without not is reported
8 SWC_CLAIM && !DISCLAIMED combined claim without app: "Use an SWC transform." true but matchAll yields zero apps → nothing reported measured; this is sentence 0/1 of the real fixed block, covered by tests 1, 2 on head
9 EXAMPLE_APP = /apps\/(example-[a-z0-9-]+)/g sentence naming two apps both checked independently (loop, not first-match); the SWC one passes, the Babel one is reported Test 5 — measured
10 EXAMPLE_APP app path that does not exist on disk matched, then carriesSwcTransform → false → reported. Correct: citing a nonexistent app as SWC is also wrong Test 3 pins carriesSwcTransform('example-does-not-exist') === false — measured
11 EXAMPLE_APP prose with no apps/ path empty match list → loop body never runs measured (sentences 0–2 of the fixed block); covered by tests 1, 2 on head
12 text.split(/(?<=\.)\s+/) sentence unit the fixed block splits into 4 pieces; "(e.g." splits mid-sentence, harmless — the fragment carries neither an app nor a disclaimer contradiction measured (4 pieces: SWC/no-app, SWC/no-app, neither, SWC+disclaimer+app); covered by tests 1, 2
13 text.split(...) — known limitation claim and citation in separate sentences: "The working path is SWC. See apps/example-babel." not reported (piece 1 has the claim and no app; piece 2 has the app and no claim) Test 7 — pinned deliberately; see note below
14 fs.existsSync(pkgPath) guard apps/<name>/package.json missing early return false, no throw from the later readFileSync/JSON.parse Test 3 — measured
15 {...pkg.dependencies, ...pkg.devDependencies} package.json with neither field two undefined spreads → {} → .some() on empty → false, no throw Test 4 (example-empty fixture)
16 .some(name => name.startsWith('@stylexswc/')) app with zero matching deps (all six example apps today) false Test 3 for the real app, Test 4 for a fixture — measured
17 .some(...) positive case a dep named @stylexswc/nextjs-plugin (the real SWC case) — in dependencies, and separately in devDependencies true → a sentence citing it is accepted. This is the row that lets a future correct citation pass without editing the test Test 4 (both fields, on tmpdir fixtures — no app in the repo declares one, and that absence is the bug)
18 .some(...) near-miss @stylexjs/babel-plugin (present in the cited app) startsWith('@stylexswc/') → false. The two scopes differ by two characters and the whole check rests on this Test 1 on base, plus Test 4's example-babel fixture — measured
19 docs.length assets/docs readable, .mjs filter 28 .mjs files, 21 with a doc export; expect(docs.length).toBeGreaterThan(0) guards a silently empty corpus Test 1's own first assertion — measured
20 mod.doc ?? mod.docs a .mjs exporting neither doc == null → file skipped, no crash. Reached today: 7 of the 28 (layout.doc.dense.mjs, principles.doc.{dense,zh}.mjs, theme.doc.{dense,zh}.mjs, tokens.doc.{dense,zh}.mjs) export docsZh/docsDense instead and are skipped, so dense and Chinese variants are not covered by this guard. Measured at head: all 7 contain zero occurrences of SWC and zero of example-, so none carries a citation to miss; widening the guard to them is a separate change measured, no test
21 proseBlocks() block-type filter a citation inside a table, list or code block dropped before the rule runs → not reported. Bounded, not assumed: the live corpus has non-prose blocks and none of them makes the claim Test 6 (asserts the non-prose corpus is non-empty and clean)
22 spawnSync(node, [BIN, 'docs', 'styling']) exit status CLI fails to run expect(res.status).toBe(0) → the test fails loudly rather than scanning empty stdout Test 2
23 Test 2's stdout sanity anchor CLI prints the topic but the section were removed expect(res.stdout).toMatch(/@stylexswc\/nextjs-plugin/) → a vacuously clean stdout cannot pass Test 2

Rows 13 and 7b are the honest gaps, and both are now pinned by test 7 rather than left implicit. They are a deliberate trade-off: widening the unit from the sentence to the whole prose block is the obvious alternative and it is worse, because the corrected text recommends SWC and names the Babel app in the same block on purpose, so a block-scoped rule reports the fix itself as a defect — it cannot express "this app is the counter-example". The sentence unit makes the correction expressible; the cost is that a claim split across two sentences, or one carrying an incidental not, slips through. A maintainer who prefers the stricter rule would need the corrected prose reworded to drop the app name entirely, and test 7 is where that decision would be recorded.

Suggested upstream PR title

fix(cli): stop citing a Babel example app as the SWC StyleX setup

`astryx docs styling` told readers the working Next.js path is an SWC StyleX
transform and to see `apps/example-nextjs-stylex` "for a complete, working
Next.js + StyleX + SWC configuration". That app carries no SWC transform: it
compiles StyleX with @stylexjs/babel-plugin and @stylexjs/postcss-plugin, the
setup the same section tells readers not to use on App Router. A reader who
follows the citation copies the configuration the docs just warned against.

Point the reader at the snippet that follows instead, and say what the example
app is actually good for. Add a drift guard so a doc sentence can only offer an
in-repo example app as an SWC setup when that app declares a @stylexswc/*
compiler.
@askalf askalf added the oss-candidate Sprayberry Code candidate for upstream label Sep 20, 2026
@askalf
askalf marked this pull request as ready for review September 20, 2026 06:05
Adds an end-to-end variant over the astryx docs styling output (the text a reader actually receives) plus controls for the dependency-field scope, the multi-app loop, the prose-only corpus and the two known sentence-scope limits.
@askalf askalf added the verified Adversarially verified by a fresh run label Sep 20, 2026
@askalf

askalf commented Sep 20, 2026

Copy link
Copy Markdown
Collaborator Author

Verification

Adversarial verification by a fresh run, at head 4493e26. The bug was re-derived from the repo, not read off the body; the ## Boundaries ledger was rebuilt from the diff and every reachable row that had no test now has one.

Re-derived independently. apps/example-nextjs-stylex/package.json declares @stylexjs/babel-plugin + @stylexjs/postcss-plugin and no @stylexswc/*; git grep example-nextjs-stylex finds the claim in exactly one prose block (styling.doc.mjs:369) plus a neutral table row in getting-started.doc.mjs that makes no SWC claim and is correctly out of scope. The false citation reaches the user through the shipped CLI, confirmed at both shas below.

Head moved, production diff did not. git diff 8d7bfab 4493e26 -- packages/cli/assets/docs/styling.doc.mjs is empty — the verification commit is test-only. git diff origin/main...4493e26 --stat = 2 files, +229/−1.

Tests added here (5, on top of the author's 2): one discriminating test over the rendered CLI output — the other code path the fix claims to cover — and four controls, for the dependencies-vs-devDependencies scope and the @stylexjs/ / @stylexswc/ near-miss, the multi-app-per-sentence loop, the prose-only corpus gap, and the two known sentence-scope false negatives. Each control is named (control) and passes on base by design; each discriminating test fails on base.

Base arm — styling.doc.mjs restored from origin/main, the final 7-test file unchanged:

$ vitest run packages/cli/test/drift/docs-example-apps.test.mjs --project node

 ❯  node  packages/cli/test/drift/docs-example-apps.test.mjs (7 tests | 2 failed) 2401ms
     × offers an example app as an SWC setup only when it carries one 360ms
     × renders no such citation through `astryx docs styling` 1985ms
     ✓ (control) reads the example apps that back the claim 2ms
     ✓ (control) reads an SWC compiler from either dependency field 1ms
     ✓ (control) checks every app a sentence names, not just the first 1ms
     ✓ (control) scans prose only, and no other block type cites an app as SWC 45ms
     ✓ (control) is sentence-scoped, so a split claim and a bare "not" slip through 1ms

 FAIL  ... > offers an example app as an SWC setup only when it carries one
AssertionError: expected [ Array(1) ] to deeply equal []
+ [
+   "packages/cli/assets/docs/styling.doc.mjs: offers apps/example-nextjs-stylex as an SWC configuration, but that app declares no @stylexswc/* compiler",
+ ]
 ❯ packages/cli/test/drift/docs-example-apps.test.mjs:129:19

 FAIL  ... > renders no such citation through `astryx docs styling`
AssertionError: expected [ Array(1) ] to deeply equal []
+ [
+   "astryx docs styling: offers apps/example-nextjs-stylex as an SWC configuration, but that app declares no @stylexswc/* compiler",
+ ]
 ❯ packages/cli/test/drift/docs-example-apps.test.mjs:141:64

 Test Files  1 failed (1)
      Tests  2 failed | 5 passed (7)
   Duration  3.27s

Head arm — 4493e26, same command immediately after restoring:

$ vitest run packages/cli/test/drift/docs-example-apps.test.mjs --project node

 ✓  node  packages/cli/test/drift/docs-example-apps.test.mjs (7 tests) 2473ms
     ✓ offers an example app as an SWC setup only when it carries one  335ms
     ✓ renders no such citation through `astryx docs styling`  2080ms

 Test Files  1 passed (1)
      Tests  7 passed (7)
   Duration  3.44s

Rendered CLI, both arms — the user-visible half of the fix:

$ # BASE: styling.doc.mjs from origin/main
$ node packages/cli/clients/cli/bin/astryx.mjs docs styling | grep -n "The working path on Next.js"
257:...which keeps SWC and `next/font` intact. See the example app `apps/example-nextjs-stylex` in the repo for a complete, working Next.js + StyleX + SWC configuration.

$ # HEAD 4493e26
$ node packages/cli/clients/cli/bin/astryx.mjs docs styling | grep -n "The working path on Next.js"
257:...which keeps SWC and `next/font` intact. The snippet below is the whole wiring. The repo ships no SWC example app: `apps/example-nextjs-stylex` compiles StyleX with the Babel plugin and PostCSS, so read it for the PostCSS layer setup, not for the transform.

Sibling harness file, untouched by this PR, green at head:

$ vitest run packages/cli/test/drift/docs-drift.test.mjs --project node

 ✓  node  packages/cli/test/drift/docs-drift.test.mjs (1 test) 2843ms
     ✓ every doc parses and stays in sync with its source of truth  2840ms

 Test Files  1 passed (1)
      Tests  1 passed (1)

Ledger holes found and closed. The previous ledger left rows 9, 15, 17 and 21 (multi-app loop; package.json with no dependency fields; the positive @stylexswc/* case in either dependency field; the prose-only block filter) without tests, and did not list the two assertions inside the new rendered-output test. All are now rows 9, 15, 17, 21, 22, 23 with tests. Row 20 was verified rather than assumed: the 7 dense/zh docs the loader skips contain zero occurrences of SWC and zero of example-, so nothing is hidden behind that skip today.

Behaviour outside the stated bug: none. The production change is a single string literal; the rest of the diff is one new test file. The one-line correction Redline will read is byte-identical to the author's.

Tooling at this head: prettier --check clean on the test file, eslint rc=0, and the repo's own husky pre-commit (pnpm check:repo + lint-staged) ran and passed on the commit. gh pr checks 1 --repo askalf/astryx → no checks reported on the 'fix/docs-nextjs-swc-example-reference' branch: the fork's Actions were never enabled, so that is absence, not a failing job. Nothing red, nothing pending.

@sprayberry-redline sprayberry-redline left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review from the Sprayberry Labs fleet code reviewer.

Reviewed by the GPT gating lane (gating review).

Verdict: APPROVED — ready for the operator to submit; no blocking issues found.

I verified the one-line documentation correction against the base source at 0faf070d8ba2ecbb4ea424e4c0b9c3e85adf87f5: the old packages/cli/assets/docs/styling.doc.mjs:369 text explicitly presented apps/example-nextjs-stylex as “a complete, working Next.js + StyleX + SWC configuration,” while the cited app is the Babel/PostCSS example described in the evidence. The changed packages/cli/assets/docs/styling.doc.mjs:369 text now says the adjacent snippet is the SWC wiring and accurately limits that app to its PostCSS-layer role.

The regression coverage is appropriate and discriminating: packages/cli/test/drift/docs-example-apps.test.mjs:122-137 rejects an SWC citation whose referenced app lacks a @stylexswc/* dependency, and :142-154 separately exercises the rendered astryx docs styling output. The supplied same-test-file base/head evidence shows both variants fail on base and pass at this head. I also checked the candidate facts sheet, base reproduction, commit text, upstream prior-art searches (no matching open upstream PR), policy evidence, scope, and stated boundary ledger. No CI is configured on this fork (gh pr checks reports no checks), but no check is pending or failing.

What's good: the production change is narrowly scoped, preserves the correct SWC recommendation, and makes the user-facing CLI path a tested behavior rather than relying only on the source document.

Notes for the operator:

  • On upstream submission, open the PR as a draft and select the applicable upstream primary-intent template, as the candidate’s policy evidence records.
  • Include the concise rendered-output and before/after test evidence from the facts sheet in the upstream PR description.

@sprayberry-secondread sprayberry-secondread left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review from the Sprayberry Labs fleet code reviewer.

Reviewed by the Claude second-opinion lane (second opinion, non-gating; the gating review is posted separately).

Verdict: READY — the citation correction is accurate; no blocking correctness issue found at 4493e2603acf1245d15679bc674548c53256e628.

Findings

None. I independently confirmed the original documentation defect from the removed sentence and the unchanged example configuration: example-nextjs-stylex declares and configures the Babel plugin, its PostCSS configuration imports that Babel configuration, and its Next configuration is empty. It is not the complete SWC example the base sentence promised. The replacement at packages/cli/assets/docs/styling.doc.mjs:369 accurately distinguishes the PostCSS layer example from the transform and points to the existing snippet. This does not establish a fix for upstream facebook#6376's rendering difference.

Maintainer's-eye read

  • Keep the counter-example; do not require block-wide matching. Naming the Babel example specifically for its useCSSLayers setup is useful and supported by its actual postcss.config.js. A block-wide SWC/app co-occurrence rule would reject that useful explanation. The current sentence heuristic is a bounded regression guard, not a natural-language truth checker. Its split-sentence and incidental-not blind spots are explicitly tested and disclosed; they do not prevent it from detecting the actual removed claim on either the source or rendered-output path.
  • Scope is reasonable, though the test machinery is optional complexity an upstream maintainer may question. The closest precedent, #5605, corrected this very styling payload with one line and rendered-CLI evidence, without a new test or changeset. Conversely, the existing scripts/check-theme-template.test.mjs explicitly favors live-source drift checks, and #4714 established the neighboring Vitest drift harness. The new test follows that live-source/reporting shape. I found no existing SWC-citation helper to reuse in those harnesses. A smaller regression assertion is a possible maintainer preference, not a readiness defect.
  • Submission notes, not findings: the underlying fix(cli): ... title matches facebook#5605 and the scoped titles of recently merged outside contributions #5657 and #6196. Remove the fork's candidate marker when submitting upstream. Those latter behavior fixes include changesets; the directly comparable documentation-only facebook#5605 does not. I would not insist on a changeset here on that evidence. Follow docs/contributing/pull-requests.md's one-primary-intent/template guidance in the upstream submission body.
  • Prior art: independently searched upstream PRs for SWC and SWC/example, and issues for example-nextjs-stylex and SWC/StyleX. Reviewed closed #4268, which adds a Turbopack CSS-loading example rather than correcting this citation. Searches surfaced facebook#3475/facebook#5260 and issue facebook#6376 as adjacent work, not an existing replacement for this one-line fix. Search results are not an exhaustive proof of absence.

Independently rebuilt boundary ledger

The production change is prose only. Below are the new test's decision points, derived from the diff. T1–T7 refer to the seven tests in file order. Inputs are repository-authored modules, strings and manifests, not a public arbitrary-JSON API; numeric limits, negative indices and platform/protocol branches are not introduced here.

Changed decision/expression Boundary behavior Pin / scope
.endsWith('.mjs') Non-module names excluded; empty result cannot silently pass T1 nonempty-doc assertion; extension rejection not isolated
mod.doc ?? mod.docs Null/undefined first export falls back; other values retained Live imports in T1/T6; alternate export cases not isolated
doc != null Neither export means module omitted Live corpus; dense/translated alternate exports are outside this guard, as disclosed
doc.sections ?? [] Absent/null sections produce no blocks; empty list produces none Corpus traversal T1/T6, no malformed-doc fixture
section.content ?? [] Absent/null content produces no blocks Corpus traversal T1/T6, no malformed-section fixture
block?.type === 'prose' Null/non-prose excluded T1 scans prose; T6 separately checks real non-prose corpus
typeof block.text === 'string' Empty string accepted, non-string excluded T1 real strings; no isolated malformed block test
existsSync(pkgPath) Missing manifest returns false before parsing T3 missing app
dependency/devDependency spreads Missing fields yield empty keys; either field can supply SWC T4 empty manifest and both positive fields
.some(...) Zero keys/no match false; one matching key true T3/T4
.startsWith('@stylexswc/') Babel scope rejected; SWC scope accepted T4 near-miss and both SWC fixtures; dependency presence is a proxy, not execution proof
text.split(/(?<=\.)\s+/) Dot+whitespace splits; split claim loses association; e.g. also splits T1/T2 actual paragraph; T7 split-claim control
/\bSWC\b/ Empty/no-token/lowercase-only text skipped; whole uppercase token checked T1/T2/T5/T7; lowercase is outside stated corpus convention
/\b(not|rather than|instead of|no SWC)\b/ Disclaimer excludes sentence, including incidental not T1/T2 corrected paragraph; T7 incidental-not control; alternatives not individually isolated
!SWC_CLAIM.test(...) || DISCLAIMED.test(...) No claim or disclaimer skips app inspection T1/T2/T7; plain positive control keeps rule live
matchAll(/apps\/(example-[a-z0-9-]+)/g) / destructuring No match yields none; all matching paths inspected, capture is app name T5 two-app case and positive-only case; actual paths T1/T2
!hasSwc(app) False emits diagnostic; true does not T5 exact mixed result and SWC-only empty result; T7 positive diagnostic count
docs.length > 0 Zero corpus fails T1 assertion itself
child status === 0 Failure/null status fails before trusting output T2 assertion itself
stdout /@stylexswc\/nextjs-plugin/ Missing topic anchor fails rather than vacuously accepting empty output T2 assertion itself
e.isDirectory() && e.name.startsWith('example-') Files/non-example dirs excluded T3 requires real named example; false limbs not isolated
fixture callback app === 'example-swc' Exact name true, Babel name false Both limbs asserted in T5
block?.type !== 'prose' Other block kinds scanned as serialized data T6 nonempty non-prose assertion plus no invalid citations
nonProse.length > 0 Empty secondary corpus fails T6 assertion itself

I read every new assertion: T1 and T2 are the two discriminating regressions. The status/anchor assertions inside T2 are sanity checks, not claimed independent regressions. T3–T7 are explicitly named controls and intentionally hold with either version of the prose. T7's final positive assertion demonstrates the detector is not simply dead. I found no omitted reachable bug variant of the actual citation correction; this is not a claim that every possible future prose formulation is covered.

What's good: a single production string changes, no compiler recommendation/configuration changes, and both source and user-visible CLI output receive a regression check backed by real manifests.

Validation limits: read the full two-file diff, surrounding snippet, example configs, test precedents, module history and upstream PR evidence. gh pr checks reports no checks. I did not run tests or reproduce the browser/compiler rendering issue; the PR's A/B transcripts remain author-provided execution evidence.

SECOND READ: READY

@askalf askalf added ready-for-operator Gated; operator submits upstream submitted Submitted upstream labels Sep 21, 2026
@askalf

askalf commented Sep 21, 2026

Copy link
Copy Markdown
Collaborator Author

Submitted upstream for review.

@askalf askalf closed this Sep 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

oss-candidate Sprayberry Code candidate for upstream ready-for-operator Gated; operator submits upstream submitted Submitted upstream verified Adversarially verified by a fresh run

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The documented Turbopack/StyleX setup produces different layout output from the Babel StyleX setup for the same Astryx template.

3 participants