Skip to content

feat: first-class design system support#3537

Open
Adebesin-Cell wants to merge 92 commits into
chakra-ui:v2from
Adebesin-Cell:v2-designsystem-all-levels
Open

feat: first-class design system support#3537
Adebesin-Cell wants to merge 92 commits into
chakra-ui:v2from
Adebesin-Cell:v2-designsystem-all-levels

Conversation

@Adebesin-Cell
Copy link
Copy Markdown

@Adebesin-Cell Adebesin-Cell commented May 7, 2026

📝 Description

first-class design system support in panda. one config field for consumers (designSystem), one CLI command for lib authors (panda lib), and a manifest that records the chain so intermediate libs never reach into a parent's node_modules.

⛳️ Current behavior (updates)

a consumer of a panda design system has to coordinate four config fields by hand:

import { acmePreset } from '@acme/lib/preset'

defineConfig({
  presets: ['@pandacss/dev/presets', acmePreset],
  importMap: '@acme/styled-system',
  outdir: '@acme/styled-system',
  include: [
    './src/**/*.{ts,tsx}',
    './node_modules/@acme/lib/dist/panda.buildinfo.json',
  ],
})

and intermediate libs in a chain (marketing-ds extending foundations) had to do the same — import { foundationsPreset } from '@acme/foundations/preset' baked into the lib's source. that's the very node_modules reach-in the feature is meant to remove.

lib authors also ran a three-step build:

panda codegen && panda ship && panda emit-pkg

…and hand-wrote panda.lib.json on top.

🚀 New behavior

lib author flow. one command:

panda lib

produces dist/:

  • panda.buildinfo.json (replaces panda ship)
  • panda.lib.json — manifest including a designSystem field that records the lib's own parent (set when the lib's panda.config.ts declared one)
  • preset.mjs — compiled preset (consumers don't need source .ts shipped)
  • patched package.json exports including ./preset./dist/preset.mjs

panda lib --watch rebuilds on src/ changes.

consumer flow. one field:

defineConfig({
  designSystem: '@acme/lib',
  include: ['./src/**/*.{ts,tsx}'],
})

replaces the four-knob coordination. encoder hydrates from the lib's buildinfo automatically; preset, importMap, and buildinfo path all come from the manifest.

chain composition via manifest walk. intermediate libs declare their parent the same way:

// lib-mid/panda.config.ts
defineConfig({
  designSystem: '@acme/lib',         // record parent in this lib's manifest
  presets: ['@pandacss/dev/presets', midPreset],
  outdir: './styled-system',
})

// lib-mid/preset.ts — declares only mid's own additions
export const midPreset = definePreset({ ... })

panda lib writes that designSystem into panda.lib.json. getResolvedConfig walks the chain by reading each level's manifest — resolving each parent against the previous manifest's directory, not the initial consumer cwd, so transitive parents (chain-N → chain-(N-1) → ... → chain-0) work even when the consumer only depends on the immediate parent.

each lib's preset.ts declares only its own additions. no import { parentPreset } from '@parent/preset'. clean per-lib boundaries.

smart include. bare specifiers in include resolve via Node module resolution. libs without a panda.lib.json get auto-globbed via package.json#files. libs with a panda.lib.json throw — they belong under designSystem, not include. the two fields answer different questions (designSystem = "what's in my design language?", include = "which files use it?"), and silently accepting the wrong shape hid the distinction. errors are batched per getFiles() call, so a config with several misplaced entries surfaces all of them at once instead of failing on the first.

manifest version guard. panda.lib.json carries a schemaVersion. the reader now exact-matches against a single shared constant (CURRENT_LIB_MANIFEST_VERSION in @pandacss/types), with directional error messages:

  • consumer is newer than the lib (schemaVersion < current) → "rebuild the lib with panda lib, or downgrade this project to match"
  • consumer is older than the lib (schemaVersion > current) → "upgrade @pandacss/dev in this project, or rebuild the lib with the matching version"

lib → consumer propagation. the consumer's panda used to ignore the lib's panda.buildinfo.json and panda.lib.json once it had hydrated them at startup. now those resolved paths are pushed onto explicitDeps, so watchConfig picks them up automatically. this closes the loop for every distribution shape:

  • monorepo, lib in panda lib --watch → buildinfo rewrites → consumer's panda --watch regenerates css.
  • standalone consumer, pnpm update @acme/lib → pnpm writes new files into node_modules/@acme/lib/dist/ → file watcher fires → regen.
  • ci-style, single panda codegen after install → no watcher needed; the next codegen reads the new files fresh.

lib version, on the consumer side. panda doesn't enforce, suggest, or check lib versions — the consumer's package.json and lockfile are the only source of truth. no registry calls, no "upgrade nudge", no panda outdated. staying on @acme/lib@1.0.0 forever is a fully supported choice.

what panda does do is emit a one-line backward-looking receipt when the lib's version changed between codegen runs:

[designSystem] @acme/lib: 1.0.0 → 1.1.0

state is persisted at <outdir>/panda.designsystem-state.json. nothing scary, nothing networked — just a receipt so a consumer who suddenly sees different css can correlate it with the bump they ran earlier.

💣 Is this a breaking change (Yes/No):

Yes.

  • panda ship and panda emit-pkg are removed. migration is one command — panda lib does both.
  • presetExport in the manifest replaces the older heuristic for finding which preset a module exports. existing v1 manifests still work via the default-export fallback.

migration:

# v1
panda codegen && panda ship && panda emit-pkg

# v2
panda lib

📝 Additional Information

  • panda lib compiles preset.tsdist/preset.mjs via esbuild (packages: 'external'). the chain's parent ref is read from panda.config.ts designSystem at panda lib time and persisted into the emitted manifest.
  • three sandbox fixtures land alongside, all in real-world shape (no shared workspace styled-system package; each consumer emits its own local ./styled-system/ via outdir):
    • sandbox/v2-ds-example — 1-to-1 lib → app
    • sandbox/v2-ds-fixture — depth-3 chain (liblib-midlib-leaf → app)
    • sandbox/v2-ds-stress — depth-7 chain (chain-0chain-6) + a separate atlas-lib branch wrapping preset-atlaskit, plus a chain-3-alt sibling that shares chain-2 to verify tree-walk semantics
  • strictTokens: true enabled on app demos so the manifest-walked type surface actually enforces at the IDE.
  • supersedes feat: first-class design system support #3536.

test plan

  • pnpm test packages/core/__tests__/atomic-rule.test.ts — sacred CSS snapshot, no changes
  • pnpm test packages/{core,node,config,cli,parser} — green (125 in config+node)
  • depth-1 fixture: panda lib in v2-ds-example/lib produces manifest + buildinfo + compiled preset; consumer extracts CSS with all tokens + recipes
  • depth-3 fixture: panda lib at L1/L2/L3 of v2-ds-fixture produces 5 → 8 → 12 hashes; consumer override beats every level (--colors-brand: #ff00ff); L1's acmeFade keyframe propagates through the full chain
  • depth-7 stress: 7-deep chain composes end-to-end via manifest walk; consumer sees tier0card…tier6card recipes
  • sibling-branch verification: chain-3-alt shares parent with chain-3; each leaf walks back to chain-0 picking up only its own ancestors (no cross-pollution)
  • non-panda lib in include (smart-include): @v2-ds-example/charts (no manifest) gets auto-globbed; its styles land in consumer CSS
  • panda lib in include (hard separation): throws with a directive to use designSystem; multiple misplaced entries report together in a single error
  • propagation: manifest + buildinfo paths surface on explicitDeps, so the consumer's watcher rebuilds when the lib rewrites them
  • version drift receipt: first hydrate writes state without logging; bumped version on second hydrate logs prev → next exactly once
  • manifest schemaVersion guard: older / newer manifest throws with a directional message; matching version passes through

@vercel
Copy link
Copy Markdown

vercel Bot commented May 7, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
panda-docs Error Error May 21, 2026 9:35am

Request Review

@vercel
Copy link
Copy Markdown

vercel Bot commented May 7, 2026

@Adebesin-Cell is attempting to deploy a commit to the Chakra UI Team on Vercel.

A member of the Team first needs to authorize it.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 7, 2026

🦋 Changeset detected

Latest commit: 45f3910

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 29 packages
Name Type
@pandacss/dev Major
@pandacss/node Major
@pandacss/types Major
@pandacss/config Major
@v2-ds-stress/app Patch
@v2-ds-stress/atlas-lib Patch
@pandacss/bench Major
@pandacss/astro-plugin-studio Major
@pandacss/mcp Major
@pandacss/postcss Major
@pandacss/core Major
@pandacss/generator Major
@pandacss/logger Major
@pandacss/parser Major
@pandacss/plugin-lightningcss Major
@pandacss/plugin-svelte Major
@pandacss/plugin-vue Major
@pandacss/preset-atlaskit Major
@pandacss/preset-base Major
@pandacss/preset-open-props Major
@pandacss/preset-panda Major
@pandacss/reporter Major
@pandacss/studio Major
@pandacss/token-dictionary Major
@pandacss/binding-wasm Major
@pandacss/binding Major
@pandacss/extractor Major
@pandacss/is-valid-prop Major
@pandacss/shared Major

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@segunadebayo segunadebayo changed the base branch from main to v2 May 7, 2026 08:52
@Adebesin-Cell Adebesin-Cell changed the base branch from v2 to main May 7, 2026 08:52
@Adebesin-Cell Adebesin-Cell changed the base branch from main to v2 May 7, 2026 08:53
Implements `readLibManifest` in `@pandacss/node` with full TDD coverage.
Resolves `<pkg>/panda.lib.json` via Node module resolution, parses JSON,
and validates the LibManifest shape — throwing descriptive errors for
unresolvable packages, missing exports, malformed JSON, missing fields,
and wrong field types.
Relocate `readLibManifest` (and its tests + fixtures) from `@pandacss/node`
to `@pandacss/config` so that the upcoming `getResolvedConfig` in config can
call the reader without inverting the package dependency direction.

`@pandacss/node` re-exports the symbol from `@pandacss/config` to keep the
existing public surface intact.
Read the manifest via readLibManifest, resolve and bundle the preset,
prepend it to the consumer's preset stack, and concatenate the manifest's
importMap into the consumer's importMap.
When `config.designSystem` is set, PandaContext now reads the lib manifest,
resolves the buildinfo path, and calls `encoder.fromJSON()` to pre-populate
the encoder with the library's extracted hashes. Failures (missing file,
malformed JSON) warn and return without throwing.
Wrap readLibManifest in try/catch so an uninstalled or misconfigured
designSystem package warns gracefully instead of escaping the constructor.
Also replace `cwd as string` cast with nullish coalesce and `parsed as any`
with the precise `EncoderJson` type.
…nSystem path

When the manifest's preset file only has named exports (no default),
bundleNRequire returns the whole module object (e.g. { leafPreset... }).
Add extractPresetFromModule() to detect this and pick the first value
that looks like a Preset, so the full depth-3 preset chain resolves.
Replace the four manual knobs (presets import, importMap, buildinfo in
include) with a single designSystem: '@v2-ds-fixture/lib-leaf' field,
validating the new API produces identical CSS output.
adds sandbox/v2-ds-example with three packages (styled-system, lib, app)
demonstrating the designSystem config key, token override semantics, manifest
format, and buildinfo travel.
@Adebesin-Cell Adebesin-Cell marked this pull request as ready for review May 8, 2026 11:45
#1: lib-manifest cache was module-level + per-process. In watch mode, a parent rebuild + manifest rewrite would not invalidate the cached entry — consumers silently kept reading the old manifest. Reads are sub-millisecond on small JSON, the cache wasn't pulling weight; removed.

chakra-ui#2: panda lib's compilePreset returned false silently on missing source or esbuild error, leaving the manifest pointing at a path that wouldn't resolve at consumer install. Now it throws — lib authors see the failure at build time, consumers don't hit a cryptic resolve error days later.

resolvePresetSource also probes both manifest-relative and cwd-relative locations before bundling, matching detectPresetExport's pre-existing behavior. patchPackageExports now always writes the ./preset entry since compile is no longer optional.
panda.lib.json now records the lib's own designSystem (set when panda lib ran).
getResolvedConfig walks the chain via each level's manifest, so a consumer's
designSystem pulls in the full lineage without intermediate libs reaching into
node_modules for parent presets.

before: each lib's preset.ts imported the parent preset and declared
presets:[parentPreset] for the chain to reach downstream consumers — the very
node_modules reach-in designSystem was meant to remove.

after: each lib's preset.ts declares only its own additions; panda.config.ts
declares designSystem:'@parent'; panda lib writes that into the manifest;
consumers walk it transitively. clean per-lib boundaries, visibility filter
can key into the same chain layer later.

- types: LibManifest gains designSystem?
- manifest-writer: accepts + emits designSystem from caller
- lib-build: passes ctx.config.designSystem to the manifest writer
- get-resolved-config: replaces single-level load with manifest walk;
  collects presets and importMaps parent-first; cycle-detects on visited names
… parent imports

each intermediate lib's preset.ts now declares only its own additions; the
chain is wired in panda.config.ts via designSystem:'@parent'. consumers walk
the full lineage through manifest chain composition.

- lib-mid/preset.ts: no longer imports acmePreset; declares mid additions only
- lib-mid/panda.config.ts: designSystem:'@v2-ds-fixture/lib' added back
- lib-leaf/preset.ts: no longer imports midPreset; declares leaf additions only
- lib-leaf/panda.config.ts: designSystem:'@v2-ds-fixture/lib-mid' added back
…al cwd

deep chains (chain-N → chain-(N-1) → ... → chain-0) failed at depth ≥2
because the consumer only depends on the immediate parent. resolving every
level via `require.resolve` from the initial cwd hits node-resolution dead
ends past depth-1. resolve each level against the previous manifest's
directory instead — transitive parents are found in the parent's own
node_modules.
verifies that two libs sharing a parent (chain-3 and chain-3-alt both
extend chain-2) compose their own chains independently — each leaf walks
back to chain-0 picking up only its own ancestors, no sibling pollution.

confirms designSystem chain is a tree-walk per leaf, not a DAG/diamond
resolution.
- patchPackageExports: normalize string/array exports to object form
  before mutating, so non-object exports aren't silently corrupted
- detectPresetExport: pick from the bundled module's own exports
  instead of guessing via ctx.config.presets (wrong with composed
  non-builtin presets)
- get-resolved-config: bundle parent presets against each lib's own
  dir, not the consumer cwd — fixes pnpm cases where the parent
  isn't hoisted to the consumer's node_modules
- get-resolved-config: cycle detect via realpath(manifestPath), not
  specifier string — catches aliased-cycle cases under pnpm
- get-resolved-config: importMap always returns an array shape
- create-context: hydrateDesignSystemEncoder logs at error level
  when manifest-declared buildinfo is missing or invalid JSON —
  silent warn was masking incomplete consumer CSS
- create-context: smart-include prefers a dist entry in pkg.files
  so source dirs (src) don't get double-extracted alongside dist
- manifest-writer: warn when falling back to panda: "*" so lib
  authors notice an undeclared @pandacss/dev dep
- lib-manifest, create-context: anchor createRequire on
  package.json, not a fictitious noop.js
- lib-build: wrap root package.json JSON.parse in try/catch
- cli tests: delete skipped ship/emit-pkg specs (commands removed)
- docs: replace cli.mdx ship/emit-pkg sections with the lib command
- docs: add v2 callout to component-library.mdx pointing at panda
  lib + designSystem; legacy walkthrough kept for migration
- hard separation: include rejects packages with panda.lib.json (must use
  designSystem). errors batch across getFiles() so multiple misplaced entries
  surface together.

- manifest schemaVersion guard: exact-match against CURRENT_LIB_MANIFEST_VERSION
  in @pandacss/types. directional error tells the consumer to upgrade panda or
  rebuild the lib depending on which side is older.

- lib → consumer propagation: hydrateDesignSystemEncoder now pushes the resolved
  manifest + buildinfo paths onto explicitDeps so the consumer's watcher
  rebuilds when the lib rewrites them. closes the loop for monorepo, standalone
  + pnpm update, and ci flows.

- drift receipt: <outdir>/panda.designsystem-state.json records the lib version
  seen on the previous codegen. on bump, logs one line: '@acme/lib: 1.0.0 →
  1.1.0'. never networked, never a nag — consumer's lockfile remains the only
  source of truth for which version is current.

- bare-specifier outdir warning: outdir like '@scope/name' is interpreted as a
  literal directory and creates a junk nested folder. warn at context init with
  a directive to use a relative path.

- v2-ds-example: drop the shared workspace styled-system package; app, lib each
  generate their own at ./styled-system; charts depends on lib via its
  styled-system export. autocomplete + types now align with the codegen output.

closes the autocomplete miss on consumer-added tokens (the shared workspace
package silently masked any consumer override).
prove the design-system pipeline end-to-end by giving the consuming app a
real browser surface. lib edits hot-update through the consumer's css.

- packages/app: next 15 + react 19 (matches sandbox/next-js-app template).
  page.tsx renders solidButton, outlineButton, heroStyles from the lib +
  an app-local panel with `borderColor: 'brand2'` to demo the override.
- panda wired via postcss plugin; layout imports globals.css with @layer.
- transpilePackages: ['@v2-ds-example/lib', '@v2-ds-example/charts'] so
  next compiles their ts source on the fly (lib ships src as its main
  export, which is the design-system pattern this pr is supporting).
- root sandbox/v2-ds-example/package.json adds a single dev script:
  bootstrap (panda lib + lib codegen + app codegen) → concurrently lib's
  panda lib --watch + app's next dev. one command, the loop runs.
- drop the obsolete src/main.ts scratch entry.

verified: `pnpm --filter @v2-ds-example/app build` succeeds; brand2 lands
in .next/static/css/app/layout.css as `--colors-brand2: #8b5cf6`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant