Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/swizzle-stylex-setup.md
Original file line number Diff line number Diff line change
@@ -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)
53 changes: 53 additions & 0 deletions packages/cli/docs/styling.doc.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 <Component>` 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',
Expand Down
34 changes: 34 additions & 0 deletions packages/cli/src/commands/swizzle.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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++;
}
Expand All @@ -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);
Expand All @@ -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.
Expand Down
67 changes: 67 additions & 0 deletions packages/cli/src/commands/swizzle.routing.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
Loading