From e4f8bf0e6b263fafd71f180086c5036db38a489f Mon Sep 17 00:00:00 2001 From: policyengine-bot Date: Sat, 9 May 2026 13:05:56 +0000 Subject: [PATCH 1/2] Add ui-kit maintenance skill for legacy compatibility patterns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Captures learnings from policyengine-ui-kit PR #28 (legacy shim): - How to create backward-compatible /legacy export paths - Multi-entry TypeScript builds with Vite - Deprecation strategy and timeline - Test porting for behavioral parity - package.json exports best practices This will help future maintainers create similar compatibility layers when migrating design system APIs. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../SKILL.md | 334 ++++++++++++++++++ 1 file changed, 334 insertions(+) create mode 100644 skills/frontend/policyengine-ui-kit-maintenance-skill/SKILL.md diff --git a/skills/frontend/policyengine-ui-kit-maintenance-skill/SKILL.md b/skills/frontend/policyengine-ui-kit-maintenance-skill/SKILL.md new file mode 100644 index 0000000..d1ea7cd --- /dev/null +++ b/skills/frontend/policyengine-ui-kit-maintenance-skill/SKILL.md @@ -0,0 +1,334 @@ +--- +name: policyengine-ui-kit-maintenance +description: | + Use this skill when working on the @policyengine/ui-kit package itself (not consumers). + Covers legacy compatibility shims, multi-entry TypeScript builds, package.json exports, + and deprecation strategies for API migrations. + Triggers: "ui-kit package", "legacy shim", "package exports", "deprecate ui-kit", + "multi-entry build", "vite library", "backward compatibility ui-kit" +--- + +# Maintaining @policyengine/ui-kit + +Patterns for maintaining the ui-kit package, creating compatibility layers, and managing migrations. + +## Legacy Compatibility Shim Pattern + +When deprecating an old API surface in favor of a new one, create a `/legacy` compatibility layer to enable zero-code migration for consumers. + +### Structure + +``` +src/ + legacy/ + index.ts ← Main exports (deprecated, re-exports old API) + tokens/ + index.ts + colors.ts + typography.ts + spacing.ts + charts/ + index.ts +tests/ + legacy/ ← Port original tests to verify behavioral parity + colors.test.ts + typography.test.ts + spacing.test.ts + charts.test.ts +``` + +### Implementation Checklist + +1. **Copy source verbatim** + - Copy deprecated files from the old package/location + - Do NOT refactor or "improve" the code + - Goal is byte-for-byte API compatibility + +2. **Add deprecation JSDoc** + ```typescript + /** + * @deprecated Use `palette` from '@policyengine/ui-kit' instead. + * This export mirrors the old @policyengine/design-system/tokens/colors API. + * Migration: import { BLUE_PRIMARY } from '@policyengine/ui-kit/legacy/tokens/colors' + * → import { palette } from '@policyengine/ui-kit' + * const BLUE_PRIMARY = palette.teal[500] + */ + export const BLUE_PRIMARY = "#319795"; + ``` + +3. **Port all tests** + - Copy test files from the deprecated package + - Update imports to use `/legacy` paths + - Ensure all tests pass to verify behavioral parity + - Any test failures indicate breaking changes + +4. **Update package.json exports** + ```json + { + "exports": { + "./legacy": { + "types": "./dist/legacy/index.d.ts", + "import": "./dist/legacy/index.js", + "require": "./dist/legacy/index.cjs" + }, + "./legacy/tokens": { + "types": "./dist/legacy/tokens/index.d.ts", + "import": "./dist/legacy/tokens/index.js", + "require": "./dist/legacy/tokens/index.cjs" + }, + "./legacy/tokens/colors": { + "types": "./dist/legacy/tokens/colors.d.ts", + "import": "./dist/legacy/tokens/colors.js", + "require": "./dist/legacy/tokens/colors.cjs" + } + } + } + ``` + +5. **Configure multi-entry build** + - See "Multi-Entry TypeScript Build" section below + +6. **Document migration path** + - Add migration table to changelog + - Include sed-replace examples for bulk migration + +### Migration Documentation Template + +```markdown +## Migration from @old-package to @new-package/legacy + +Pure import-path rename. No source code changes required. + +| Before | After | +|--------|-------| +| `@old-package` | `@new-package/legacy` | +| `@old-package/tokens` | `@new-package/legacy/tokens` | +| `@old-package/tokens/colors` | `@new-package/legacy/tokens/colors` | + +### Bulk migration + +sed -i '' 's|@old-package|@new-package/legacy|g' **/*.{ts,tsx,js,jsx} +``` + +## Multi-Entry TypeScript Build + +Building a TypeScript package with multiple entry points (subpath exports) using Vite. + +### vite.config.ts Pattern + +```typescript +import { defineConfig } from 'vite' +import { resolve } from 'path' +import dts from 'vite-plugin-dts' + +export default defineConfig({ + plugins: [ + dts({ + tsconfigPath: './tsconfig.json', + rollupTypes: true, + }), + ], + build: { + lib: { + entry: { + // Main entry + index: resolve(__dirname, 'src/index.ts'), + + // Legacy entries + 'legacy/index': resolve(__dirname, 'src/legacy/index.ts'), + 'legacy/tokens/index': resolve(__dirname, 'src/legacy/tokens/index.ts'), + 'legacy/tokens/colors': resolve(__dirname, 'src/legacy/tokens/colors.ts'), + 'legacy/charts/index': resolve(__dirname, 'src/legacy/charts/index.ts'), + }, + formats: ['es', 'cjs'], + fileName: (format, entryName) => { + const ext = format === 'es' ? 'js' : 'cjs' + return `${entryName}.${ext}` + }, + }, + rollupOptions: { + external: ['react', 'react-dom'], + }, + }, +}) +``` + +### Key Points + +1. **Entry object**: Maps output paths to source files + - Keys become directory structure in `dist/` + - `'legacy/tokens/colors'` → `dist/legacy/tokens/colors.{js,cjs,d.ts}` + +2. **fileName function**: Controls extension based on format + - ESM: `.js` + - CommonJS: `.cjs` + - Types: `.d.ts` (handled by vite-plugin-dts) + +3. **External dependencies**: List peer dependencies to avoid bundling + - React, React DOM should always be external + - Any other packages consumers will install + +4. **Type generation**: `vite-plugin-dts` with `rollupTypes: true` + - Generates `.d.ts` for each entry + - Rolls up type dependencies + +### Verification Steps + +After build, verify structure: + +```bash +bun run build +ls -R dist/ + +# Expected: +# dist/ +# index.js +# index.cjs +# index.d.ts +# legacy/ +# index.js +# index.cjs +# index.d.ts +# tokens/ +# index.js +# index.cjs +# index.d.ts +# colors.js +# colors.cjs +# colors.d.ts +``` + +Test imports in consuming project: + +```typescript +import { Button } from '@policyengine/ui-kit' // ✓ +import { palette } from '@policyengine/ui-kit/legacy' // ✓ +import { BLUE_PRIMARY } from '@policyengine/ui-kit/legacy/tokens/colors' // ✓ +``` + +## Deprecation Strategy + +### When to Use Legacy Shims + +Use legacy compatibility shims when: +- 5+ repositories depend on the old API +- Migration requires import path changes across many files +- Old API is stable and well-tested +- New API is semantically different (not just renamed) + +### When to Use Direct Breaking Changes + +Use direct breaking changes (major version bump) when: +- Fewer than 5 consumers +- Old API has known bugs or security issues +- Migration is simple find-replace +- Maintaining two APIs creates confusion + +### Deprecation Timeline + +1. **Release N (current)** + - Ship `/legacy` shim alongside new API + - Mark all legacy exports with `@deprecated` JSDoc + - Document migration path + - Version: 0.8.0 (minor bump, backward compatible) + +2. **Release N+1 (migration period)** + - Keep legacy shim + - Update all first-party consumers to new API + - Log deprecation warnings if possible + - Version: 0.9.0 + +3. **Release N+2 (removal)** + - Remove `/legacy` exports + - Major version bump + - Version: 1.0.0 + +## Testing Compatibility Layers + +### Port Original Tests + +When creating a `/legacy` shim, port ALL tests from the original package: + +```typescript +// tests/legacy/colors.test.ts +import { describe, test, expect } from 'vitest' +import { + BLUE_PRIMARY, + BLUE_LIGHT, + // ... all 50+ color exports +} from '../src/legacy/tokens/colors' + +describe('Legacy color tokens', () => { + test('BLUE_PRIMARY matches design-system value', () => { + expect(BLUE_PRIMARY).toBe('#319795') + }) + + // ... port all 143 original tests +}) +``` + +### Why Port Tests? + +1. **Behavioral parity**: Ensures shim truly mirrors old API +2. **Regression detection**: Any test failure means breaking change +3. **Refactor confidence**: Safe to refactor internals if tests pass +4. **Documentation**: Tests document expected behavior + +### Test Organization + +``` +tests/ + legacy/ ← Tests for /legacy exports + colors.test.ts + typography.test.ts + spacing.test.ts + charts.test.ts + + tokens.test.ts ← Tests for new API + palette.test.ts + theme.test.ts +``` + +Keep legacy tests separate so they can be deleted together when shim is removed. + +## Package.json Best Practices + +### Exports Field + +Always use conditional exports for TypeScript packages: + +```json +{ + "exports": { + "./legacy/tokens/colors": { + "types": "./dist/legacy/tokens/colors.d.ts", + "import": "./dist/legacy/tokens/colors.js", + "require": "./dist/legacy/tokens/colors.cjs" + } + } +} +``` + +Order matters: `types` first, then `import`, then `require`. + +### Files Field + +Limit published files: + +```json +{ + "files": [ + "dist", + "README.md", + "LICENSE" + ] +} +``` + +Do NOT publish `src/`, `tests/`, or config files. + +## Related Skills + +- `policyengine-ui-kit-consumer-skill` — How consumers import and use ui-kit +- `policyengine-design-skill` — Design token values and usage guidelines +- `policyengine-tailwind-shadcn-skill` — Theme authoring for ui-kit itself From b34e5e0a0d05571a312da2f8ff178234fb141ef0 Mon Sep 17 00:00:00 2001 From: policyengine-bot Date: Sat, 9 May 2026 14:09:42 +0000 Subject: [PATCH 2/2] Document ui-kit legacy migration and consumer-types testing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Captures learnings from policyengine-ui-kit#31: 1. Legacy migration section in consumer skill: - Documents /legacy subpath export for backward compatibility - Highlights value-changing migrations (gray palette, warning text) - Notes colors.blue/success restoration in 0.8.1+ 2. New consumer-types-testing.md reference: - Pattern for catching bundler resolution issues before consumers - Fixture + consumer-like tsconfig + tsc spawn test - Prevents silent dist/ shadowing bugs that break external imports - Real example: caught tokens.js shadowing tokens/index.d.ts 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../SKILL.md | 38 ++++ .../references/consumer-types-testing.md | 178 ++++++++++++++++++ 2 files changed, 216 insertions(+) create mode 100644 skills/frontend/policyengine-ui-kit-consumer-skill/references/consumer-types-testing.md diff --git a/skills/frontend/policyengine-ui-kit-consumer-skill/SKILL.md b/skills/frontend/policyengine-ui-kit-consumer-skill/SKILL.md index 8e7021b..05b2eb9 100644 --- a/skills/frontend/policyengine-ui-kit-consumer-skill/SKILL.md +++ b/skills/frontend/policyengine-ui-kit-consumer-skill/SKILL.md @@ -238,6 +238,44 @@ After the two-line import, these are available: | Radius | `rounded-sm` (4px), `rounded-md` (6px), `rounded-lg` (8px) | `@theme inline` | | All Tailwind utilities | `flex`, `grid`, `p-4`, `gap-2`, `hidden`, etc. | `@import "tailwindcss"` | +## Migrating from Legacy Versions + +When upgrading from `@policyengine/design-system` 0.2.x/0.3.x to `@policyengine/ui-kit` 0.8.x+: + +### Legacy Shim Support + +The package provides a `/legacy` subpath export for backward compatibility: + +```tsx +// Old import (still works via legacy shim) +import { colors, spacing } from '@policyengine/ui-kit/legacy' + +// New canonical import (preferred) +import { palette, rootColorsLight } from '@policyengine/ui-kit' +``` + +### Be Aware of Value-Changing Migrations + +Some migration paths preserve the token name but change the underlying hex value. The legacy JSDoc annotations mark these: + +**Gray palette** (Tailwind-3 neutral → Tailwind-4 slate): +```tsx +// Legacy: colors.gray[500] = "#6B7280" +// Canonical: palette.gray[500] = "#64748B" +``` + +**Warning text** (Mantine orange.9 → Tailwind orange-700, improved contrast): +```tsx +// Legacy: colors.text.warning = "#d9480f" (4.30:1, fails WCAG AA at small text) +// Canonical: rootColorsLight['--text-warning'] = "#c2410c" (5.18:1, passes AA) +``` + +When migrating, visually verify areas using these tokens — the change is intentional (better contrast, updated brand), but may need design approval. + +### Legacy Tokens Restored in 0.8.1+ + +If migrating from 0.8.0 specifically, note that `colors.blue` (Tailwind sky 50–900) and `colors.success = "#22C55E"` were missing from the initial legacy shim but are restored in 0.8.1+. Update to the latest patch if you see missing exports. + ## Related Skills - `policyengine-design-skill` — Full token reference (hex values, usage guidelines) diff --git a/skills/frontend/policyengine-ui-kit-consumer-skill/references/consumer-types-testing.md b/skills/frontend/policyengine-ui-kit-consumer-skill/references/consumer-types-testing.md new file mode 100644 index 0000000..0fc0000 --- /dev/null +++ b/skills/frontend/policyengine-ui-kit-consumer-skill/references/consumer-types-testing.md @@ -0,0 +1,178 @@ +# Consumer-Types Testing Pattern + +Pattern for catching bundler resolution issues in design system packages before they reach consumers. + +## The Problem + +When a package ships both `.js` and `.d.ts` files with matching names at different directory levels, bundler-mode TypeScript resolution can fail silently: + +``` +dist/ + tokens.js ← shadows the directory below in some resolvers + tokens/ + index.d.ts ← unreachable from consumers using moduleResolution: "bundler" +``` + +This works fine in the package's own tests (using local source) but breaks for all consumers. The failure mode is silent — no build errors in the package itself. + +## The Solution: Consumer-Types Harness + +Create a test that mimics how external consumers see the package: + +### 1. Fixture that imports as a consumer would + +**`tests/consumer-types/fixture.ts`** + +```typescript +// Import from the built artifacts exactly as a consumer would +import { + palette, + rootColorsLight, + rootColorsDark, + // ... representative slice of all exports +} from '@policyengine/ui-kit' + +import { + colors, + spacing, + // ... legacy shim exports +} from '@policyengine/ui-kit/legacy' + +import { DashboardShell } from '@policyengine/ui-kit/dashboard' +import { InputPanel } from '@policyengine/ui-kit/panels' +// ... per-feature subpath exports + +// Type-only check — no runtime needed +export type Exports = { + palette: typeof palette + colors: typeof colors + DashboardShell: typeof DashboardShell + // ... +} +``` + +### 2. Consumer-like tsconfig + +**`tests/consumer-types/tsconfig.json`** + +```json +{ + "compilerOptions": { + "moduleResolution": "bundler", + "paths": { + "@policyengine/ui-kit": ["../../dist/index.d.ts"], + "@policyengine/ui-kit/*": ["../../dist/*/index.d.ts"] + }, + "skipLibCheck": false, + "noEmit": true + }, + "include": ["fixture.ts"] +} +``` + +Key points: +- `moduleResolution: "bundler"` matches most consumer setups (Next.js, Vite) +- `paths` points to `dist/` (the built artifacts), NOT `src/` +- `skipLibCheck: false` ensures the package's own types are checked +- `noEmit: true` — only type-checking, no output + +### 3. Test that spawns tsc + +**`tests/consumer-types/typecheck.test.ts`** + +```typescript +import { describe, it, expect } from 'vitest' +import { spawnSync } from 'child_process' +import { existsSync } from 'fs' +import path from 'path' + +describe('consumer-types harness', () => { + it('type-checks successfully as an external consumer', () => { + const distIndex = path.resolve(__dirname, '../../dist/index.d.ts') + + if (!existsSync(distIndex)) { + console.warn('dist/ not found — skipping consumer-types check') + console.warn('Run `bun run build` before `bun run test` to enable this check') + return // skip without failing + } + + const result = spawnSync( + 'tsc', + ['--noEmit', '--project', 'tests/consumer-types/tsconfig.json'], + { encoding: 'utf-8', cwd: path.resolve(__dirname, '../..') } + ) + + if (result.status !== 0) { + console.error('Consumer-types check failed:') + console.error(result.stdout) + console.error(result.stderr) + } + + expect(result.status).toBe(0) + }) +}) +``` + +Key points: +- Spawns `tsc --noEmit` against the consumer tsconfig +- Gracefully skips if `dist/` doesn't exist (with actionable message) +- Fails loudly with full tsc output on error + +### 4. Update CI workflow + +Ensure the build happens before tests: + +```yaml +- name: Install dependencies + run: bun install + +- name: Build package + run: bun run build + +- name: Run tests + run: bun run test +``` + +Without this order, the harness skips in CI (no `dist/`). The skip message makes the fix obvious. + +## What This Catches + +1. **Shadowing bugs** — `dist/tokens.js` masking `dist/tokens/index.d.ts` +2. **Missing subpath exports** — `package.json` exports map incomplete +3. **Broken type re-exports** — `export type { Foo } from './internal'` where internal types aren't emitted +4. **Path mapping issues** — tsconfig `paths` that work locally but break for consumers + +## When to Use This Pattern + +Use for any package that: +- Ships `.d.ts` files generated from source +- Has subpath exports (`/legacy`, `/dashboard`, etc.) +- Is consumed by bundler-mode TypeScript projects (Next.js, Vite, etc.) +- Has had bundler resolution issues in the past + +Do NOT use for: +- Pure runtime packages (no TypeScript) +- Packages consumed only via direct source imports +- Packages with a single flat export (no subdirectories) + +## Maintenance + +As you add exports: +1. Add representative imports to `fixture.ts` +2. No need to import everything — one symbol per subpath is sufficient +3. Update the type-only `Exports` type to ensure the imports are actually used + +As you add subpath exports: +1. Add the path mapping to `consumer-types/tsconfig.json` +2. Add an import to `fixture.ts` + +## Example From policyengine-ui-kit + +From PR #31 (0.8.1): + +- **36 main-entry symbols** imported (`palette`, `spacing`, `DashboardShell`, etc.) +- **4 legacy subpath symbols** imported (`colors`, `breakpoints`, etc.) +- **3 per-feature subpath symbols** imported (one from each of `dashboard`, `panels`, `charts`) +- **Caught** the `dist/tokens.js`-shadows-`dist/tokens/index.d.ts` issue that broke 36 consumer PRs + +Runs in ~400ms, fails fast with actionable error messages.