feat: first-class design system support#3537
Open
Adebesin-Cell wants to merge 92 commits into
Open
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
@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 detectedLatest commit: 45f3910 The changes in this PR will be included in the next version bump. This PR includes changesets to release 29 packages
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 |
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.
#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.
…esignSystem chain
…designSystem chain
…okens in v2-ds-fixture
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.
745eab9 to
acd5d9c
Compare
- 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`.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
📝 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'snode_modules.⛳️ Current behavior (updates)
a consumer of a panda design system has to coordinate four config fields by hand:
and intermediate libs in a chain (
marketing-dsextendingfoundations) 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:
…and hand-wrote
panda.lib.jsonon top.🚀 New behavior
lib author flow. one command:
produces
dist/:panda.buildinfo.json(replacespanda ship)panda.lib.json— manifest including adesignSystemfield that records the lib's own parent (set when the lib'spanda.config.tsdeclared one)preset.mjs— compiled preset (consumers don't need source.tsshipped)package.jsonexports including./preset→./dist/preset.mjspanda lib --watchrebuilds onsrc/changes.consumer flow. one field:
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:
panda libwrites thatdesignSystemintopanda.lib.json.getResolvedConfigwalks 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.tsdeclares only its own additions. noimport { parentPreset } from '@parent/preset'. clean per-lib boundaries.smart include. bare specifiers in
includeresolve via Node module resolution. libs without apanda.lib.jsonget auto-globbed viapackage.json#files. libs with apanda.lib.jsonthrow — they belong underdesignSystem, notinclude. 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 pergetFiles()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.jsoncarries aschemaVersion. the reader now exact-matches against a single shared constant (CURRENT_LIB_MANIFEST_VERSIONin@pandacss/types), with directional error messages:schemaVersion < current) → "rebuild the lib withpanda lib, or downgrade this project to match"schemaVersion > current) → "upgrade@pandacss/devin 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.jsonandpanda.lib.jsononce it had hydrated them at startup. now those resolved paths are pushed ontoexplicitDeps, sowatchConfigpicks them up automatically. this closes the loop for every distribution shape:panda lib --watch→ buildinfo rewrites → consumer'spanda --watchregenerates css.pnpm update @acme/lib→ pnpm writes new files intonode_modules/@acme/lib/dist/→ file watcher fires → regen.panda codegenafter 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.jsonand lockfile are the only source of truth. no registry calls, no "upgrade nudge", nopanda outdated. staying on@acme/lib@1.0.0forever is a fully supported choice.what panda does do is emit a one-line backward-looking receipt when the lib's
versionchanged between codegen runs: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 shipandpanda emit-pkgare removed. migration is one command —panda libdoes both.presetExportin the manifest replaces the older heuristic for finding which preset a module exports. existing v1 manifests still work via the default-export fallback.migration:
📝 Additional Information
panda libcompilespreset.ts→dist/preset.mjsvia esbuild (packages: 'external'). the chain's parent ref is read frompanda.config.tsdesignSystematpanda libtime and persisted into the emitted manifest.styled-systempackage; each consumer emits its own local./styled-system/viaoutdir):sandbox/v2-ds-example— 1-to-1 lib → appsandbox/v2-ds-fixture— depth-3 chain (lib→lib-mid→lib-leaf→ app)sandbox/v2-ds-stress— depth-7 chain (chain-0…chain-6) + a separateatlas-libbranch wrappingpreset-atlaskit, plus achain-3-altsibling that shareschain-2to verify tree-walk semanticsstrictTokens: trueenabled on app demos so the manifest-walked type surface actually enforces at the IDE.test plan
pnpm test packages/core/__tests__/atomic-rule.test.ts— sacred CSS snapshot, no changespnpm test packages/{core,node,config,cli,parser}— green (125 in config+node)panda libinv2-ds-example/libproduces manifest + buildinfo + compiled preset; consumer extracts CSS with all tokens + recipespanda libat L1/L2/L3 ofv2-ds-fixtureproduces 5 → 8 → 12 hashes; consumer override beats every level (--colors-brand: #ff00ff); L1'sacmeFadekeyframe propagates through the full chaintier0card…tier6cardrecipeschain-3-altshares parent withchain-3; each leaf walks back to chain-0 picking up only its own ancestors (no cross-pollution)include(smart-include):@v2-ds-example/charts(no manifest) gets auto-globbed; its styles land in consumer CSSinclude(hard separation): throws with a directive to usedesignSystem; multiple misplaced entries report together in a single errorexplicitDeps, so the consumer's watcher rebuilds when the lib rewrites themprev → nextexactly once