From ce01c1102f1dfae189e7d03f85df775d8fb5ddc9 Mon Sep 17 00:00:00 2001 From: Joey Farina Date: Thu, 2 Jul 2026 22:49:53 +0000 Subject: [PATCH] fix(cli): document StyleX build requirement for swizzled components Swizzled components ship raw StyleX source that needs a build-time StyleX compiler; without one they render unstyled with no error. The swizzle command now prints a StyleX build-setup note after copying (with the Next.js caveat that the StyleX Babel plugin disables SWC and breaks next/font, so an SWC-based transform is required), and `astryx docs styling` gains a StyleX Build Setup section covering per-bundler setup. Fixes #3373 --- .changeset/swizzle-stylex-setup.md | 5 ++ packages/cli/docs/styling.doc.mjs | 53 +++++++++++++++ packages/cli/src/commands/swizzle.mjs | 34 ++++++++++ .../cli/src/commands/swizzle.routing.test.mjs | 67 +++++++++++++++++++ 4 files changed, 159 insertions(+) create mode 100644 .changeset/swizzle-stylex-setup.md diff --git a/.changeset/swizzle-stylex-setup.md b/.changeset/swizzle-stylex-setup.md new file mode 100644 index 000000000000..306e4befef05 --- /dev/null +++ b/.changeset/swizzle-stylex-setup.md @@ -0,0 +1,5 @@ +--- +'@astryxdesign/cli': patch +--- + +[fix] `astryx swizzle`: swizzled components ship raw StyleX source that needs a build-time StyleX compiler, and without one they render unstyled with no error. The command now prints a StyleX build-setup note after copying (including the Next.js caveat that the StyleX Babel plugin disables SWC and breaks `next/font`, so an SWC-based transform is required), and `astryx docs styling` gains a "StyleX Build Setup" section covering per-bundler setup. (#3373) diff --git a/packages/cli/docs/styling.doc.mjs b/packages/cli/docs/styling.doc.mjs index e59c45e29988..3b01ebf6b3e4 100644 --- a/packages/cli/docs/styling.doc.mjs +++ b/packages/cli/docs/styling.doc.mjs @@ -338,6 +338,59 @@ const styles = stylex.create({ }, ], }, + { + title: 'StyleX Build Setup (required for swizzled components)', + category: 'guide', + content: [ + { + type: 'prose', + text: 'Astryx components ship pre-compiled, so consuming the published package needs no StyleX setup. But `astryx swizzle ` copies the raw StyleX *source* into your app, and StyleX source requires a build-time StyleX compiler to produce atomic CSS. Without one the component compiles but renders completely unstyled — no error, no warning. If a swizzled component looks unstyled, a missing StyleX compiler is almost always why. The same applies if you author your own StyleX with `stylex.create()`.', + }, + { + type: 'table', + headers: ['Bundler', 'StyleX plugin'], + rows: [ + ['Webpack', '@stylexjs/webpack-plugin'], + ['Vite / Rollup', '@stylexjs/rollup-plugin (or a community Vite plugin)'], + ['Babel (any bundler)', '@stylexjs/babel-plugin + @stylexjs/postcss-plugin'], + ['Next.js (App Router, SWC)', 'An SWC-based transform — see the Next.js note below'], + ], + }, + { + type: 'prose', + text: 'Next.js (App Router) is the sharp edge. StyleX\u2019s canonical compiler is a Babel plugin, but introducing a Babel config in Next.js disables the SWC compiler, which in turn breaks SWC-dependent features like `next/font`. So the "obvious" Babel setup is actively incompatible with a standard Next 15 App Router app.', + }, + { + type: 'prose', + 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.', + }, + { + type: 'code', + lang: 'js', + label: 'next.config.mjs — SWC-based StyleX transform (keeps next/font working)', + code: `import stylexPlugin from '@stylexswc/nextjs-plugin'; + +export default stylexPlugin({ + rsOptions: { + // Resolve @astryxdesign/core's StyleX so swizzled component source compiles. + aliases: {'@/*': ['./src/*']}, + unstable_moduleResolution: {type: 'commonJS'}, + }, +})({ + // your existing Next.js config +});`, + }, + { + type: 'list', + style: 'unordered', + items: [ + 'Symptom of a missing compiler: swizzled component renders with no styles, but no build or runtime error.', + 'Do NOT add @stylexjs/babel-plugin to a Next.js App Router app — it disables SWC and breaks next/font.', + 'Pure theming (defineTheme + astryx theme build) needs NO StyleX compiler — only swizzled/authored StyleX source does.', + ], + }, + ], + }, { title: 'What NOT to Do', category: 'guide', diff --git a/packages/cli/src/commands/swizzle.mjs b/packages/cli/src/commands/swizzle.mjs index f089e2e57251..93bdddc679b1 100644 --- a/packages/cli/src/commands/swizzle.mjs +++ b/packages/cli/src/commands/swizzle.mjs @@ -26,6 +26,7 @@ import {jsonOut, humanLog} from '../lib/json.mjs'; import {cliError} from '../lib/cli-error.mjs'; import {ERROR_CODES} from '../lib/error-codes.mjs'; import {checkGhCli} from '../utils/github.mjs'; +import {getRunPrefix} from '../utils/package-manager.mjs'; import {Project} from '../lib/project.mjs'; import { CORE_PACKAGE, @@ -339,6 +340,10 @@ export function registerSwizzle(program) { // Copy all non-test, non-doc, non-README files const files = fs.readdirSync(componentDir); let copied = 0; + // Track whether any copied source uses StyleX. Swizzled StyleX source + // needs a build-time StyleX compiler in the consumer's app or it renders + // unstyled with no error — so we surface a setup note after copying. + let usesStyleX = false; for (const file of files) { // Skip test files, doc files, and README @@ -355,6 +360,13 @@ export function registerSwizzle(program) { content = rewriteImports(content, owner.ownerPackage); } + if ( + (file.endsWith('.ts') || file.endsWith('.tsx')) && + content.includes('@stylexjs/stylex') + ) { + usesStyleX = true; + } + fs.writeFileSync(path.join(outputDir, file), content); copied++; } @@ -376,6 +388,7 @@ export function registerSwizzle(program) { outputDir: relOutput, filesCopied: copied, files: copiedFiles.map(f => f), + usesStyleX, }; if (feedback) payload.feedback = feedback; return jsonOut('swizzle.copy', payload); @@ -387,6 +400,27 @@ export function registerSwizzle(program) { ); humanLog('You can now customize the component source freely.\n'); + // StyleX build requirement. Swizzled components ship raw StyleX source, + // which needs a build-time StyleX compiler in the consumer's app to + // produce atomic CSS. Without it the component compiles but renders + // unstyled, with no error — a confusing silent failure, so call it out. + if (usesStyleX) { + humanLog( + '⚠ These components use StyleX and require a StyleX compiler in your build.', + ); + humanLog( + ' Without one they render unstyled (no error). See setup per framework:', + ); + humanLog(` ${getRunPrefix()} astryx docs styling`); + humanLog( + ' Next.js note: the StyleX Babel plugin disables SWC and breaks next/font —', + ); + humanLog( + ' use an SWC-based StyleX transform instead (covered in the guide).', + ); + humanLog(''); + } + // Maintainer feedback note. If we couldn't swizzle cleanly, the team // wants to know — point users at the issue tracker. Skipped when the // owning package ships no issues URL. diff --git a/packages/cli/src/commands/swizzle.routing.test.mjs b/packages/cli/src/commands/swizzle.routing.test.mjs index aab347097d8f..40051eb32e1c 100644 --- a/packages/cli/src/commands/swizzle.routing.test.mjs +++ b/packages/cli/src/commands/swizzle.routing.test.mjs @@ -277,3 +277,70 @@ describe('swizzle — ambiguous ownership', () => { expect(out).toContain(`from '@astryxdesign/core/theme'`); }); }); + +/** + * Build a fake @astryxdesign/core with a component that imports StyleX directly + * (so the swizzle StyleX-build note should fire) and one that doesn't. + */ +function buildStyleXCore(project) { + const core = path.join(project, 'node_modules', '@astryxdesign', 'core'); + // StyleX component. + const styledDir = path.join(core, 'src', 'Styled'); + fs.mkdirSync(styledDir, {recursive: true}); + fs.writeFileSync( + path.join(core, 'package.json'), + '{"name":"@astryxdesign/core","version":"0.0.13"}', + ); + fs.writeFileSync( + path.join(styledDir, 'Styled.tsx'), + [ + `import * as stylex from '@stylexjs/stylex';`, + `const styles = stylex.create({base: {color: 'red'}});`, + `export const Styled = () => null;`, + '', + ].join('\n'), + ); + // Plain component (no StyleX). + const plainDir = path.join(core, 'src', 'Plain'); + fs.mkdirSync(plainDir, {recursive: true}); + fs.writeFileSync( + path.join(plainDir, 'Plain.tsx'), + `export const Plain = () => null;\n`, + ); + return core; +} + +describe('swizzle — StyleX build setup note (#3373)', () => { + it('reports usesStyleX and prints a setup note for StyleX components', () => { + buildStyleXCore(project); + writeProjectPackageJson(project); + + // JSON payload carries the machine-readable flag. + const jsonResult = runCli(['--json', 'swizzle', 'Styled', '-f'], project); + expect(jsonResult.code).toBe(0); + const env = JSON.parse(jsonResult.stdout); + expect(env.data.usesStyleX).toBe(true); + + // Human output surfaces the compiler requirement + Next.js caveat. + const humanResult = runCli(['swizzle', 'Styled', '-f'], project); + expect(humanResult.code).toBe(0); + expect(humanResult.stdout).toMatch(/StyleX compiler/i); + expect(humanResult.stdout).toMatch(/unstyled/i); + expect(humanResult.stdout).toMatch(/next\/font/i); + expect(humanResult.stdout).toMatch(/astryx docs styling/); + }); + + it('does not print the StyleX note for components without StyleX', () => { + buildStyleXCore(project); + writeProjectPackageJson(project); + + const jsonResult = runCli(['--json', 'swizzle', 'Plain', '-f'], project); + expect(jsonResult.code).toBe(0); + const env = JSON.parse(jsonResult.stdout); + expect(env.data.usesStyleX).toBe(false); + + const humanResult = runCli(['swizzle', 'Plain', '-f'], project); + expect(humanResult.code).toBe(0); + expect(humanResult.stdout).not.toMatch(/StyleX compiler/i); + }); +});