Skip to content

Per-component RSC granularity: multi-entry build + per-file 'use client' #80

@dgraciac

Description

@dgraciac

Background

`fix/avatar-use-client` (#79) marked the whole published bundle as `"use client"` via a Rollup banner. Avatar uses `createContext` at module top level (D14) and would otherwise crash Next.js RSC build-time analysis. The banner is the same convention shadcn / MUI v5 / Chakra v2 ship with — pragmatic and unblocks adoption.

Trade-off accepted at the time: atoms with no hooks (`Card`, `Separator`, `Badge`, `Input`, `Textarea`) lose the ability to render inside a Server Component. No current consumer (Alexandria) renders them server-side, so the cost is theoretical today.

What this issue tracks

Migrating to real per-component RSC granularity — same shape MUI v6, Chakra v3, Mantine 7 ship today.

Goal state

  • `dist/Avatar.js`, `dist/Card.js`, `dist/Button.js`, … one file per atom (multi-entry Vite build).
  • Each source file with hooks (`Avatar.tsx`, `Button.tsx`, `IconButton.tsx`, `Spinner.tsx`) carries its own `"use client"` directive — preserved per file by `rollup-plugin-preserve-directives` (or built-in once Rollup 5 stabilises preservation for library mode).
  • Stateless atoms (`Card.tsx`, `Separator.tsx`, `Badge.tsx`, `Input.tsx`, `Textarea.tsx`) ship without the directive → consumable from Server Components.
  • `package.json` exports map keeps the public surface identical (`import { Avatar } from "@code-sherpas/pharos-react"` still works) but allows deep imports (`import { Card } from "@code-sherpas/pharos-react/Card"`) for finer tree-shake.

Steps (rough sketch)

  1. Refactor `vite.config.ts`: `build.lib.entry` → multi-entry map, `rollupOptions.output.preserveModules: true` or per-component manual config.
  2. Add `rollup-plugin-preserve-directives` (or equivalent).
  3. Drop the global `banner: "'use client';"` from rollupOptions.
  4. Add `"use client"` to the source of every atom that uses React hooks at module scope or inside the component body that triggers RSC evaluation (Avatar — already has it; Button / IconButton / Spinner — verify, add if needed).
  5. Update `package.json` `exports` map to expose per-component subpaths.
  6. Verify dist contains the per-file directives (`head -1 dist/Card.js` → no banner; `head -1 dist/Avatar.js` → `"use client"`).
  7. Add a regression check to `tests/dist-types-smoke.ts` or a new script that asserts directive presence per file.
  8. Test in Alexandria worktree: confirm a Server Component can import `Card` from Pharos and `next build` succeeds.

Acceptance

  • Every atom with hooks keeps its current behaviour but no longer pulls the whole library client-side.
  • Every atom without hooks is importable from a Server Component in a Next.js App Router project and `next build` completes.
  • Bundle size for consumers shrinks (no Avatar / Button code in bundles that only use Card).
  • Public API surface unchanged at the named-export level; deep imports work additively.

Priority / when

Phase 6 / structural refactor. Not blocking any current adoption. Open it as soon as a real consumer demands a server-rendered atom (rendering performance, edge metadata, etc.) or once the atom catalogue passes ~15 and the bundle-size delta becomes measurable.

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions