Fix main-entry type re-exports failing under bundler resolution#29
Conversation
Since 0.4.0 the dist/ tree contains both Vite-emitted dist/<name>.js/.cjs files and tsc-emitted dist/<name>/index.d.ts folders side by side. With both on disk, TypeScript's `bundler` module resolution prefers the file over the folder, so `dist/index.d.ts`'s `export * from './primitives'` walks into `./primitives.js` (no types) and silently drops every symbol. Net effect: every consumer importing primitives/layout/charts/visualization/ inputs/display/utils/assets/theme symbols from the main entry of ui-kit @>=0.4.0 fails type-check with "Module '@policyengine/ui-kit' has no exported member 'Badge'" (and the same for Skeleton, Container, Button, …). Fix: pin the source re-exports to the explicit `./<name>/index` folder path. TypeScript's bundler resolution then walks into the folder and finds the types. Verified locally with a fresh `tsc --noEmit` against a file:-installed build of this branch. Subpath imports (`@policyengine/ui-kit/primitives`) were unaffected because the `exports` map already pins them to the index.d.ts folder path. No runtime change — Vite's emit and the package's `exports` map are unchanged. Only the .d.ts re-export paths shift. See microsoft/TypeScript#52146 for the upstream ambiguity. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Excellent fix for a critical TypeScript resolution issue.
Analysis:
The PR correctly identifies and fixes the root cause: with moduleResolution: "bundler", TypeScript prefers files over folders when resolving paths. Since the Vite build outputs dist/primitives.js (no types) alongside dist/primitives/index.d.ts (with types), the ambiguous export * from './primitives' in src/index.ts resolves to the file, silently dropping all type exports.
The fix:
Changing all re-exports from './<name>' to './<name>/index' forces TypeScript to use folder resolution, correctly finding the .d.ts files. This is a surgical, zero-runtime-impact fix that only affects the emitted type declaration paths.
Verification:
- ✅ Typecheck passes cleanly on this branch
- ✅ The fix is minimal and precisely targeted
- ✅ Comment in src/index.ts:1 clearly explains the issue for future maintainers
- ✅ Changelog entry is comprehensive and accurate
- ✅ No changes to runtime behavior or public API surface
- ✅ Subpath imports were already correct via
exportsmap
Impact:
This unblocks all five consumer PRs that failed with Module has no exported member errors after bumping from 0.3.1 to 0.8.0. The fix is backward-compatible and doesn't require consumer changes.
Ship it.
The 0.8.1 fix in #29 only patched the main-entry re-exports. The same file-shadowing-folder ambiguity affected src/legacy/index.ts, where 'export * from "./tokens"' was silently dropping every legacy color/ typography/spacing/chartColors symbol from `@policyengine/ui-kit/legacy`. Mirror the fix: pin the re-exports to './tokens/index' and './charts/index'. Verified with a fresh tsc --noEmit against a file:-installed build. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The 0.8.1 fix in #29 only patched the main-entry re-exports. The same file-shadowing-folder ambiguity affected src/legacy/index.ts, where 'export * from "./tokens"' was silently dropping every legacy color/ typography/spacing/chartColors symbol from `@policyengine/ui-kit/legacy`. Mirror the fix: pin the re-exports to './tokens/index' and './charts/index'. Verified with a fresh tsc --noEmit against a file:-installed build. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Restore legacy colors.blue/colors.success and add consumer-types harness Adversarial review of the merged 0.8.x line surfaced three real follow-ups: 1. The `/legacy` shim's `colors` was missing `blue` (Tailwind sky 50–900) and `success` (#22C55E) that @policyengine/design-system 0.2.0/0.3.0 shipped. The snapshot copied into src/legacy/ for 0.8.0 came from a later workspace build that had already dropped both. Two consumer migrations (PolicyEngine/cbo-baseline-tracker#3, PolicyEngine/uk-spring-statement-2026#22) had to backfill the missing values locally — the canary that the shim wasn't a true 1:1 mirror. Restored under "Semantic colors" / "Blue accent palette" with a comment noting the migration story. 2. The migration JSDoc in src/legacy/index.ts told consumers to map `colors.gray[N]` → `palette.gray[N]` and `colors.text.warning` → `rootColorsLight['--text-warning']` without flagging that both silently change visible hex values (legacy gray is Tailwind-3 #6B7280 etc.; canonical is Slate #64748B etc. — and `--text-warning` was darkened from Mantine orange.9 to Tailwind orange-700 in 0.6.0 for WCAG AA). Annotated each pair as same-hex or value-shifting so bulk sed-replace doesn't silently regress consumers. 3. The bundler-resolution drop fixed in #29 / #30 (dist/<name>.js shadowing dist/<name>/index.d.ts under moduleResolution: "bundler") was masked because nothing in this repo type-checked the package as an external consumer. Added tests/consumer-types/ with a fixture exercising the main entry, the legacy/ subpath, and per-feature subpaths, plus a tsconfig with paths to dist/ and a vitest spawn of tsc --noEmit. CI now runs `bun run build` before `bun run test` so the harness sees fresh dist/ output. Legacy color tests pin colors.blue[50/500/600/900] and colors.success exactly so a future shim regression fails locally instead of in 18+ consumers. 295 tests pass (was 293, +2 for blue/success). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Address round 2 nits: colors.info value drift, skip-hint, soften JSDoc - src/legacy/index.ts: flag colors.info → semanticFills.info as a value-shifting pair (design-system 0.3.0 had Ant blue #1890FF; 0.4.0+ and this shim use PE teal-700 #2C7A7B for brand consistency). Consumers bumping from 0.3.x will see a one-time blue→teal shift on info usages. Also softened the module-level JSDoc framing from 'shim mirroring design-system' to acknowledge that some values shifted between 0.3.0 and 0.4.0 and that --text-{warning,error,success} are shim-only accessibility variants. - tests/consumer-types/typecheck.test.ts: switch from describe.skipIf to an explicit it.skip with an actionable hint ("run `bun run build`") so a dev running `bun run test` cold sees why the harness didn't fire instead of silently missing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Since 0.4.0,
import { Badge } from "@policyengine/ui-kit"(or any other primitive/layout/chart/visualization/input/display/utils/assets symbol) fails type-check in any consumer withmoduleResolution: "bundler"(Next.js, Vite, anything modern):This was masked because nothing in this repo exercises the package as a consumer, and consumers were pinned at
^0.3.1(which didn't have the conflict). Surfaced when bumping consumers to^0.7.0/^0.8.0.Root cause
The Vite multi-entry build emits
dist/<name>.jsanddist/<name>.cjsat the top ofdist/, whiletsc --emitDeclarationOnlyemitsdist/<name>/index.d.tsin folders. Both shapes coexist:With
moduleResolution: "bundler", TypeScript's resolution algorithm prefers the file over the folder, so./primitivesresolves to./primitives.js(no types) and everyexport *silently drops the re-exported symbols. The main-entry.d.tsends up effectively empty.Fix
Change
src/index.tsre-exports from./<name>to./<name>/index. tsc emits the same path verbatim intodist/index.d.ts, and the explicit folder pin forces folder resolution. Verified with a freshtsc --noEmitagainst afile:-installed build of this branch:Subpath imports (
@policyengine/ui-kit/primitives) were unaffected — theexportsmap already pinned those to the index.d.ts folder path. No runtime change. Only the .d.ts re-export paths shift.Test plan
bun run typecheckcleanbun run test— 26 files, 292 tests passbun run buildcleantsc --noEmitfrom a clean consumer withmoduleResolution: "bundler"Why this is urgent
Five PRs across PolicyEngine consumer repos failed CI immediately after their
^0.3.1 → ^0.8.0bumps merged with this exact symptom (council-tax-ctr-map#1, cost-dashboard#26, economic-parameter-atlas#1, crfb-tob-impacts#116, spm-calculator#27 — all open as I file this). Once 0.9.0 ships those bumps just need a re-trigger.🤖 Generated with Claude Code