Thanks for your interest in contributing to Tasty! This document covers everything you need to get a development environment running, propose changes, and ship them.
By participating in this project you agree to abide by the Code of Conduct.
- Report a bug — open a GitHub issue with a minimal reproduction (CodeSandbox/StackBlitz link or a small repo is ideal).
- Propose a feature — open an issue describing the use case before opening a PR. For non-trivial changes it's worth discussing the design first to save you time.
- Improve the docs — typo fixes, clarifications, and new examples are all welcome. Docs live under
docs/and the project root. - Submit a fix or feature — see the workflow below.
For security vulnerabilities, do not open a public issue — see SECURITY.md.
- Node.js 22.19+ on the 22.x line or 24.11+ on the 24.x line for repository tooling (the published package supports Node.js 20+)
- pnpm 11+ (the project uses
packageManager: pnpm@11.xinpackage.json) - Git
git clone https://github.com/tenphi/tasty.git
cd tasty
pnpm install
pnpm test:setup
pnpm build
pnpm testpnpm test:setup downloads the headless Chromium binary that part of the test
suite runs against. It is a one-time step per checkout — see
Testing.
src/ Source code
pipeline/ Style rendering pipeline (parse → exclusives → materialize)
ssr/ Server-side rendering integrations
static/ Zero-runtime entry (tastyStatic)
zero/ Babel plugin for build-time extraction
docs/ Public documentation
scripts/ Repo automation (CI helpers)
.changeset/ Pending changelog entries
For a deeper map of the codebase, see AGENTS.md.
| Script | Purpose |
|---|---|
pnpm build |
Build all entry points with tsdown |
pnpm test |
Run the test suite once (both projects) |
pnpm test:node |
Run only the Node project (pure logic, no DOM) |
pnpm test:browser |
Run only the browser project (headless Chromium) |
pnpm test:setup |
Download the Chromium binary the browser project needs |
pnpm test:watch |
Watch mode for tests |
pnpm test:coverage |
Run tests with coverage |
pnpm typecheck |
Run tsc --noEmit |
pnpm lint / pnpm lint:fix |
Lint with ESLint |
pnpm format / pnpm format:check |
Prettier formatting |
pnpm hygiene |
Lint + format check + typecheck (CI mirror) |
pnpm hygiene:fix |
Auto-fix lint + format, then typecheck |
pnpm size |
Run size-limit checks against the built bundles |
pnpm bench |
Run pipeline benchmarks (Node project) |
pnpm bench:browser |
Run component-render benchmarks (headless Chromium) |
pnpm bench:overhead / bench:injection / bench:interaction / bench:tree |
Production-React browser benchmarks (see Runtime Benchmarks) |
pnpm bench:cold-start |
Page-load cold-start benchmark (run pnpm build first) |
pnpm knip |
Dead-code check |
pnpm check:build / check:react-compat / check:babel-compat / check:test-only |
Built-entry, packed React/Babel compatibility, and test-only-code checks (run after pnpm build) |
Run pnpm hygiene before pushing — CI runs the same checks.
The suite is split into two Vitest projects, configured in
vitest.config.ts:
| Project | Environment | Covers |
|---|---|---|
node |
plain Node, no DOM | Parser, style handlers, pipeline, SSR string output, Babel extractor — pure logic |
browser |
headless Chromium (Playwright) | Anything that touches document, renders React, or asserts on CSS the engine actually parsed |
Tasty compiles to CSS, and only a real CSS engine can tell you whether that CSS
is valid. jsdom and happy-dom reject @container, @starting-style,
@property, @function, and CSS nesting, which silently hollowed out the
assertions that depended on them. Running those suites in Chromium means an
invalid declaration shows up as a property the browser dropped.
Adding a test. Put it next to the source it exercises. If it touches
document, renders React, or asserts on injected CSS, add it to the
BROWSER_TESTS list in vitest.config.ts — every *.test.tsx file is already
matched. Everything else runs in the node project automatically, and a DOM
test that ends up there fails immediately with document is not defined.
Writing browser assertions. The engine reserializes the CSS it accepts, so
prefer tolerant matchers over exact strings — oklch(var(--x)/.1) reads back as
oklch(var(--x) / .1). When the point is to pin Tasty's own output verbatim,
configure forceTextInjection: true and read it back with getCSSText().
- Fork the repository and create a feature branch from
main:git checkout -b fix/short-description
- Make your change. Keep PRs focused — one logical change per PR is much easier to review and ship.
- Add or update tests. Bug fixes should include a regression test; features should include coverage for the new behavior. Tests use Vitest and live next to the source they exercise — see Testing for which of the two projects yours belongs in.
- Update the docs. Public-API changes need a corresponding update under
docs/(and usually the README's relevant section). - Add a changeset (see below) describing the user-facing change.
- Run hygiene + tests:
pnpm hygiene pnpm test - Commit and push. Use clear, present-tense commit messages (
fix: …,feat: …,docs: …). Conventional Commits aren't enforced, but they help. - Open a pull request against
main. Describe the motivation, the change, and any trade-offs. Link the related issue if there is one.
Choose the newest compatible, stable, non-deprecated release that has been
published for at least 14 full days. This applies to npm packages, the pinned
pnpm version, and GitHub Actions, except first-party @tenphi/* packages (see
below). Verify npm's time metadata (for example,
pnpm view <package> time --json) or the action's GitHub release publication date;
do not infer age from a dependency PR's creation date or use an unreviewed
@latest update.
Record the UTC cutoff and the selected versions and publication dates in the PR. Review major-version migration guides, Node requirements, and peer dependencies. Upgrade coupled packages together (React and React DOM, Babel, Vitest and its browser provider, Astro and its React integration, and Size Limit plugins). If the newest mature version conflicts with the supported API or tooling, use the newest compatible release and explain the hold in the PR. Keep Node types aligned with the supported LTS tooling. A tooling upgrade does not justify raising public peer minima, narrowing peer ranges, or raising the published Node requirement. Preserve existing consumer ranges unless the package actually needs a newer version; any necessary support change must be explicit in the PR and changeset, with tests against the remaining supported versions. Check packed declarations against older supported React types as well as checking runtime compatibility.
pnpm-workspace.yaml enforces 14 days (minimumReleaseAge: 20160) for direct and
transitive npm dependencies, fails when no eligible version exists, and requires
publication timestamps. Keep these checks enabled when refreshing the lockfile.
First-party @tenphi/* packages (such as @tenphi/eslint-plugin-tasty) come from
the same maintainer, so they are excluded through minimumReleaseAgeExclude and
Dependabot's cooldown.exclude, and a fix in one of them can be adopted the day
it ships. Dependabot uses the same 14-day cooldown for version updates and groups coupled
packages and other minor/patch updates to reduce PR noise. Dependabot security
updates bypass its cooldown; an urgent fix requiring a younger npm release needs
a documented, version-specific minimumReleaseAgeExclude exception reviewed in
the PR. Remove that exception after the release matures.
Run the full submit workflow, both test projects, and the CI checks (including the built-entrypoint, packed React and Babel compatibility, and test-only-code checks). Runtime/type dependency or generated-build changes need a changeset; tooling-only updates do not.
Tasty uses Changesets to manage versions and the changelog. Every PR with a user-visible change must include a changeset:
pnpm changesetThe CLI walks you through selecting the bump type (patch, minor, major) and writing a short summary. The summary becomes the changelog entry, so write it for the consumer, not the implementer:
Fix overlapping selectors when default and custom-state token values coincide but other state values differ.
Internal-only changes (refactors, CI tweaks, doc-only updates) don't need a changeset — note that in the PR description.
- TypeScript first. Keep the public API fully typed; prefer narrow, descriptive types over
any. - No hooks in the core.
tasty(), the style functions, and SSR helpers are intentionally hook-free so they work in React Server Components;useStylesis the one exception, and it only reads context when React provides it. Don't introduce React-only runtime dependencies insrc/outside the React entry points. - Determinism. The selector model relies on mutually exclusive conditions. When touching the pipeline, add tests that exercise selector exclusivity, not just snapshot output.
- Performance. The pipeline has multi-level caches (parser, state-key, simplify, condition). When changing hot paths, run
pnpm benchbefore and after and include the numbers in the PR. - Bundle size.
pnpm sizeenforces per-entry-point limits. If a change increases bundle size, justify it in the PR. - Prettier + ESLint. No style debates — let the tools format the code.
- Audience-first headings (the docs hub at
docs/README.mdis organized by role and task). - Short, real-world code examples over exhaustive enumerations.
- Link to the relevant source files when describing internals.
- Unit-style tests live alongside the code they cover (
*.test.ts(x)). - Pipeline behavior is best tested at the
renderStylesboundary so you catch interactions across stages. - For SSR/RSC behavior, the integration tests under
src/ssr/are the source of truth.
Reviewers look for:
- A clear description of motivation and scope
- Tests that fail without the change and pass with it
- Updated docs and a changeset (when user-visible)
- Green CI: build, lint, format check, typecheck,
knip, built-entry, React and Babel compatibility, and test-only-code checks, tests, size-limit - No unrelated changes (formatting drift, dependency churn, etc.)
PRs that touch the pipeline or SSR collectors will usually take an extra round of review — that's expected.
Releases are automated via the release workflow:
- Merging changesets to
mainopens a Version Packages PR that bumps versions and rewritesCHANGELOG.md. - Merging that PR publishes to npm via OIDC trusted publishing and creates a GitHub Release.
- Snapshot releases are published from PRs under the
pr_<number>dist-tag for early testing.
Maintainers are responsible for merging the Version Packages PR — contributors don't need to do anything beyond adding changesets.
- Open an issue for bugs, concrete proposals, design questions, or general help.
Thanks again — every contribution helps make Tasty better.