For the full contribution process — what we accept, how to propose new components, and how API decisions are made — read the Contributing wiki.
Key pages:
- API Conventions — naming, prop patterns, composition rules (read before submitting an RFC)
- Specification Protocol — the 9-phase process for new components
- API Arbitration — how we resolve API design questions
- Contributing with AI — safe zones, spec protocol, and working with AI tools
This file covers local development setup.
Install Node.js v22+ using one of these methods:
Via nvm (recommended):
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
source ~/.zshrc
nvm install 22Via nodejs.org: Download and install from https://nodejs.org
Astryx uses pnpm as its package manager (declared in
the packageManager field of package.json). The easiest way to install
it is via Corepack, which ships
with Node.js:
corepack enableThis makes the pnpm command available with the exact version Astryx pins.
Alternatively, install pnpm directly:
# Via npm
npm install -g pnpm@10
# Via Homebrew (macOS)
brew install pnpm
# Via standalone installer (no npm or Node.js required)
curl -fsSL https://get.pnpm.io/install.sh | env PNPM_VERSION=10.33.4 sh -
# Via GitHub releases (single binary, no dependencies)
# https://github.com/pnpm/pnpm/releases/latestVerify installation:
node --version # v22.x.x
pnpm --version # 10.x.x# Clone the repo
git clone https://github.com/facebook/astryx.git
cd astryx
# Install dependencies
pnpm install
# Build core package first (required for Storybook)
pnpm -F @astryxdesign/core build
# Start Storybook for component development
cd apps/storybook
pnpm devStorybook loads pre-built packages from dist/ folders, so you need to build packages before running Storybook.
First time setup:
# Build all packages
pnpm build
# Or build just core
pnpm -F @astryxdesign/core buildStart Storybook:
cd apps/storybook
pnpm devStorybook will open at http://localhost:6006 with:
- Theme switcher - Toggle between Default and Shadcn themes
- Mode switcher - Toggle between Light and Dark modes
- Component stories - Interactive component examples
If you make changes to @astryxdesign/core:
# Rebuild core package
pnpm -F @astryxdesign/core build
# Restart Storybook to see changes
cd apps/storybook
pnpm devastryx/
├── apps/
│ ├── storybook/ # Component playground (localhost:6006)
│ └── sandbox/ # Development testing
│
├── packages/
│ ├── core/ # Core components (Button, Input, etc.)
│ ├── cli/ # CLI tooling (npx astryx)
│ ├── lab/ # Experimental components (not yet stable)
│ └── themes/ # Theme presets (default, neutral, daily, and more)
│
└── internal/ # Internal tooling (not published)
└── test-utils/ # Shared test helpers
| Command | Description |
|---|---|
pnpm install |
Install all dependencies |
pnpm dev |
Start all dev servers |
pnpm build |
Build all packages |
pnpm test |
Run all tests |
pnpm test:watch |
Run tests in watch mode |
pnpm storybook |
Start Storybook at localhost:6006 |
pnpm lint |
Lint all packages |
Components use colocated tests — test files live alongside the component.
mkdir -p packages/core/src/MyComponentpackages/core/src/MyComponent/
├── MyComponent.tsx # Component implementation
├── MyComponent.test.tsx # Unit tests (colocated)
├── MyComponent.stories.tsx # Storybook stories
└── index.ts # Public exports
// MyComponent.tsx
import {forwardRef, type HTMLAttributes, type ReactNode} from 'react';
export interface MyComponentProps extends HTMLAttributes<HTMLDivElement> {
/** Description for AI-assisted development */
children: ReactNode;
}
/**
* Brief description of the component.
*
* @example
* ```tsx
* <MyComponent>Hello</MyComponent>
* ```
*/
export const MyComponent = forwardRef<HTMLDivElement, MyComponentProps>(
({children, ...props}, ref) => {
return (
<div ref={ref} {...props}>
{children}
</div>
);
},
);
MyComponent.displayName = 'MyComponent';// MyComponent.test.tsx
import {describe, it, expect} from 'vitest';
import {render, screen} from '@testing-library/react';
import {MyComponent} from './MyComponent';
describe('MyComponent', () => {
it('renders children', () => {
render(<MyComponent>Hello</MyComponent>);
expect(screen.getByText('Hello')).toBeInTheDocument();
});
});// MyComponent.stories.tsx
import type {Meta, StoryObj} from '@storybook/react';
import {MyComponent} from './MyComponent';
const meta = {
title: 'Components/MyComponent',
component: MyComponent,
tags: ['autodocs'],
} satisfies Meta<typeof MyComponent>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
children: 'Hello World',
},
};// packages/core/src/index.ts
export * from './MyComponent';Note: Do not manually edit the
"exports"field inpackages/core/package.json. It is auto-generated from thesrc/directory byscripts/sync-exports.jsand committed automatically when changes land onmain. If you need to verify your component will be included, runpnpm sync:exports:check.
# All tests
pnpm test
# Watch mode
pnpm test:watch
# Specific package
pnpm -F @astryxdesign/core test
# With coverage
pnpm test:coverage
# Screenshot tests
pnpm test:screenshotsTests are colocated with components:
src/Button/
├── Button.tsx
└── Button.test.tsx # Tests live here
We use Changesets for versioning, with a thin Astryx layer on top so changelogs stay categorized, contributor-attributed, and aligned with our pre-1.0 conventions.
When you make a change that should be released:
pnpm changeset:newThis wrapper:
- Detects which packages you changed from your git diff and pre-selects them — no hand-enumerating the frontmatter.
- Asks for a category (
breaking,component,feat,fix,perf,docs,chore) — this drives changelog grouping, not the semver bump. - Captures the contributor(s) — defaults to your
gh/git identity, so credit is recorded at authoring time (not reconstructed from the release bot's commit). - Forces a
patchbump while we're pre-1.0 (see below).
It writes a normal .changeset/<id>.md — commit it with your PR. The body looks like:
---
'@astryxdesign/core': patch
---
[fix] Spinner inherits the variant foreground on themed buttons (#2717)
@yourhandleYou can also pass everything as flags for non-interactive use:
pnpm changeset:new --category fix --summary "…" --pr 2717 --contributor yourhandleThe bare
pnpm changesetCLI still works, but you must follow the body convention by hand ([category]first line +@handleline). CI (pnpm check:changesets) rejects changesets missing a category or contributor, or declaring aminor/majorbump while pre-1.0.
- Pre-1.0 (current): always
patch. The Changesets CLI mapsminor→ 0.0.x → 0.1.0 andmajor→ 1.0.0. While we're on 0.0.x we ship every change — features, fixes, and breaking changes alike — as apatch. Signal a breaking change with the[breaking]category, not amajorbump.pnpm changeset:newenforces this;pnpm check:changesetsis the CI backstop. - All publishable packages are a
fixedgroup, so a single change co-bumps them to the same version. Only genuinely-affected packages get a changelog entry — the rest get a clean version-only bump.
pnpm version-packages # changeset version + scripts/format-changelogs.mjsformat-changelogs.mjs rewrites each just-bumped package CHANGELOG into the doc-site format (h1 version, #### <Category> sections in canonical order, and a #### Contributors section aggregated from the changeset @handles). It's idempotent and has a --check mode for CI drift detection.
Labels signal what's open for contribution:
good first issue/help wanted— ready to be picked up; start here.discussion— still being shaped and not ready for contribution. The problem is recorded but the solution isn't decided. Please don't start work on a fix until it's triaged out ofdiscussion. Comments and ideas are welcome.
For pull requests, use GitHub's native Draft state to signal "not ready to review/merge yet" — open the PR as a draft and mark it ready for review when it's done.
- Create a feature branch from
main - Make your changes with tests
- Run
pnpm testandpnpm lint - Add a changeset if needed:
pnpm changeset:new - Open a PR with a clear description
- TypeScript strict mode
- Functional components with
forwardRef - JSDoc comments for AI-assisted development
- Export types alongside components
If corepack enable succeeds but pnpm fails to download its binary
(e.g. ECONNRESET, fetch failed, or 503 from registry.npmjs.org),
your environment likely blocks outbound network access.
Alternative install methods (no registry.npmjs.org needed):
brew install pnpm # Homebrew (macOS)
curl -fsSL https://get.pnpm.io/install.sh | sh - # Standalone installer
npm install -g pnpm@10 # Via npmYou can also download the binary directly from GitHub Releases.
Sandboxed IDE terminals: if your IDE blocks all network, run
corepack enable && pnpm install from a regular terminal first, then
open the project in your IDE — node_modules is on the local filesystem
and doesn't need network to use.
"Failed to fetch dynamically imported module"
- Cause: Core package not built or out of date
- Fix:
pnpm -F @astryxdesign/core buildthen restart Storybook
"React is not defined"
- Cause: Missing React import in preview.tsx
- Fix: Ensure
import * as React from 'react';at top of preview.tsx
"Unexpected 'stylex.defineVars' call at runtime"
- Cause: StyleX code trying to run without compilation
- Fix: Storybook should load from
dist/notsrc/. Check vite.config.ts aliases.
Changes not appearing in Storybook
- Rebuild the package:
pnpm -F @astryxdesign/core build - Hard refresh browser: Cmd+Shift+R (Mac) or Ctrl+Shift+R (Windows)
- Clear Storybook cache: Remove
apps/storybook/node_modules/.cache
In order to accept your pull request, we need you to submit a CLA. You only need to do this once to work on any of Meta's open source projects.
Complete your CLA here: https://code.facebook.com/cla
We use GitHub issues to track public bugs. Please ensure your description is clear and has sufficient instructions to be able to reproduce the issue.
Meta has a bounty program for the safe disclosure of security bugs. In those cases, please go through the process outlined on that page and do not file a public issue.