|
| 1 | +# CLAUDE.md |
| 2 | + |
| 3 | +## Project Overview |
| 4 | + |
| 5 | +React utility hooks/components library. Monorepo with two packages: |
| 6 | + |
| 7 | +- `react-simplikit` (`packages/core`) — Platform-independent React hooks & components |
| 8 | +- `@react-simplikit/mobile` (`packages/mobile`) — Mobile web utilities (viewport, keyboard, layout) |
| 9 | + |
| 10 | +## Development Quick Start |
| 11 | + |
| 12 | +```bash |
| 13 | +yarn build # Build all packages (tsup) |
| 14 | +yarn test # Run tests (Vitest) |
| 15 | +yarn fix # Auto-fix lint + format (ESLint + Prettier) |
| 16 | +yarn typecheck # Type check (tsc --noEmit) - alias: yarn run test:type |
| 17 | +yarn changeset # Create a changeset |
| 18 | +yarn changeset status # Check pending changesets |
| 19 | +``` |
| 20 | + |
| 21 | +## Architecture Rules |
| 22 | + |
| 23 | +Layer dependency is **unidirectional** — no upward or circular imports allowed: |
| 24 | + |
| 25 | +``` |
| 26 | +components → hooks → utils → _internal |
| 27 | +``` |
| 28 | + |
| 29 | +- Components may use hooks, utils, \_internal |
| 30 | +- Hooks may use utils, \_internal |
| 31 | +- Utils may use \_internal only |
| 32 | +- \_internal has no internal dependencies |
| 33 | +- Mobile may depend on core; core must NOT depend on mobile |
| 34 | + |
| 35 | +## File Structure Convention |
| 36 | + |
| 37 | +Each hook/component/util lives in its own folder with co-located docs: |
| 38 | + |
| 39 | +``` |
| 40 | +src/ |
| 41 | +├── hooks/ |
| 42 | +│ └── useHookName/ |
| 43 | +│ ├── index.ts # Re-export |
| 44 | +│ ├── useHookName.ts # Implementation |
| 45 | +│ ├── useHookName.spec.ts # Tests (core) |
| 46 | +│ ├── useHookName.test.ts # Tests (mobile) |
| 47 | +│ ├── useHookName.ssr.test.ts # SSR safety tests |
| 48 | +│ ├── useHookName.md # English docs |
| 49 | +│ └── ko/ |
| 50 | +│ └── useHookName.md # Korean docs |
| 51 | +├── utils/ |
| 52 | +│ └── utilName/ |
| 53 | +│ ├── index.ts |
| 54 | +│ ├── utilName.ts |
| 55 | +│ └── utilName.test.ts |
| 56 | +└── index.ts # Public API exports (alphabetically sorted) |
| 57 | +``` |
| 58 | + |
| 59 | +## Coding Standards |
| 60 | + |
| 61 | +- **`type` over `interface`** — Always use `type` for type aliases |
| 62 | +- **Named functions in useEffect** — Improves stack traces and readability |
| 63 | + ```ts |
| 64 | + useEffect(function handleResize() { ... }, []); // ✅ |
| 65 | + useEffect(() => { ... }, []); // ❌ |
| 66 | + ``` |
| 67 | +- **Strict boolean checks** — Use explicit comparisons (`value !== undefined`, not `if (value)`) |
| 68 | +- **Import extensions** — Include `.js` in relative imports for ESM compliance |
| 69 | +- **useEffect cleanup** — Always return cleanup to remove listeners/subscriptions |
| 70 | +- **`"use client"` banner** — tsup adds this for RSC compatibility |
| 71 | +- **Named exports only** — No default exports |
| 72 | +- **No `any` types** — Full TypeScript strict mode, no escape hatches |
| 73 | +- **Zero dependencies** — No runtime dependencies in production code |
| 74 | + |
| 75 | +### SSR-Safe Coding Pattern |
| 76 | + |
| 77 | +All hooks/utils accessing browser APIs must be SSR-safe: |
| 78 | + |
| 79 | +```ts |
| 80 | +// Pattern: Fixed initial value + useEffect sync |
| 81 | +const [state, setState] = useState(FIXED_INITIAL_VALUE); |
| 82 | +useEffect(function syncBrowserState() { |
| 83 | + if (isServer()) return; |
| 84 | + setState(getBrowserAPI()); |
| 85 | +}, []); |
| 86 | +``` |
| 87 | + |
| 88 | +Never initialize state with browser API calls (causes hydration mismatch). |
| 89 | + |
| 90 | +### Hook Return Value Convention |
| 91 | + |
| 92 | +- **Single value**: `useDebounce<T>(value, delay): T` |
| 93 | +- **Tuple** (state + action, 2 items): `useToggle(init): [boolean, () => void]` |
| 94 | +- **Object** (3+ items): `usePagination(): { page, nextPage, prevPage }` |
| 95 | + |
| 96 | +### Performance Patterns |
| 97 | + |
| 98 | +- **Throttle** subscriptions at ~16ms (60fps) for viewport/keyboard events |
| 99 | +- **Deduplicate** to skip updates when value hasn't changed |
| 100 | +- **`startTransition`** for non-urgent state updates (React 18+) |
| 101 | + |
| 102 | +## Testing |
| 103 | + |
| 104 | +- **100% coverage mandatory** — Enforced by Vitest coverage threshold, no exceptions |
| 105 | +- **SSR tests required** — All hooks accessing browser APIs must have `.ssr.test.ts` |
| 106 | +- **Core tests**: `.spec.ts` (legacy, will migrate to `.test.ts`) |
| 107 | +- **Mobile tests**: `.test.ts` |
| 108 | +- **SSR pattern**: |
| 109 | + ```ts |
| 110 | + import { renderHookSSR } from '../utils/renderHookSSR'; |
| 111 | + it('is safe on server side rendering', () => { |
| 112 | + const result = renderHookSSR.serverOnly(() => useHookName()); |
| 113 | + expect(result.current).toBeDefined(); |
| 114 | + }); |
| 115 | + ``` |
| 116 | + |
| 117 | +## Documentation |
| 118 | + |
| 119 | +- **Bilingual**: English + Korean (co-located in hook folders as `*.md` / `ko/*.md`) |
| 120 | +- **JSDoc required**: Every public API must have `@description` + `@example` + `@param` + `@returns` |
| 121 | +- **VitePress**: Used for documentation site; rewrites map source docs to clean URLs |
| 122 | +- **Co-location**: Docs live inside package source, not in separate docs/ tree |
| 123 | +- **Homepage**: `https://react-simplikit.slash.page` |
| 124 | + |
| 125 | +## Commit Convention |
| 126 | + |
| 127 | +Format: `<type>(<scope>): <description>` |
| 128 | + |
| 129 | +- **Types**: `feat`, `fix`, `docs`, `chore`, `refactor`, `test` |
| 130 | +- **Scope**: Package name (`core`, `mobile`) or area |
| 131 | +- Examples: `feat(mobile): add useKeyboardHeight hook`, `fix: correct SSR rendering logic` |
| 132 | + |
| 133 | +## PR Checklist |
| 134 | + |
| 135 | +- [ ] Tests pass (`yarn test`) |
| 136 | +- [ ] Lint/format pass (`yarn fix`) |
| 137 | +- [ ] 100% coverage maintained |
| 138 | +- [ ] JSDoc with `@description` + `@example` |
| 139 | +- [ ] Changeset added (if user-facing change) |
| 140 | + |
| 141 | +## Release Flow |
| 142 | + |
| 143 | +Uses **Changesets** + **GitHub Actions OIDC** for automated releases. |
| 144 | + |
| 145 | +``` |
| 146 | +PR with changeset merged → release.yml triggers |
| 147 | + → changesets/action detects changeset → creates "Version PR" |
| 148 | + → Version PR merged → changesets/action: hasChangesets=false |
| 149 | + → "Publish to npm" step → changeset publish (uses npm publish internally) |
| 150 | +``` |
| 151 | + |
| 152 | +### Critical: publishConfig does NOT work with npm |
| 153 | + |
| 154 | +**npm does NOT support `publishConfig` overrides for manifest fields** (`main`, `types`, `module`, `exports`). Only `access`, `registry`, `tag` are supported. See [npm/cli#7586](https://github.com/npm/cli/issues/7586). |
| 155 | + |
| 156 | +- `yarn npm publish` DOES support publishConfig field overrides |
| 157 | +- `changeset publish` internally calls `npm publish` (not yarn) |
| 158 | +- Therefore: **always declare `main`/`types`/`module`/`exports` at the top level** of package.json |
| 159 | +- `publishConfig` should only contain `access: "public"` |
| 160 | + |
| 161 | +### OIDC Trusted Publishing |
| 162 | + |
| 163 | +- npm auth uses GitHub Actions OIDC (no secret tokens needed) |
| 164 | +- Requires `id-token: write` permission in workflow |
| 165 | +- Node 22 ships npm v10 which doesn't support OIDC → `npm install -g npm@latest` upgrades to npm 11+ |
| 166 | +- `changesets/action` must NOT have `publish` option (it overwrites `.npmrc` and breaks OIDC) |
| 167 | +- Publish is done in a separate step after changesets/action |
| 168 | + |
| 169 | +### Snapshot/Canary Releases |
| 170 | + |
| 171 | +```bash |
| 172 | +GITHUB_TOKEN=$(gh auth token) yarn changeset version --snapshot canary |
| 173 | +yarn changeset publish --tag canary # Requires npm login + OTP |
| 174 | +``` |
| 175 | + |
| 176 | +## Package Structure |
| 177 | + |
| 178 | +``` |
| 179 | +packages/ |
| 180 | +├── core/ # react-simplikit |
| 181 | +│ ├── src/ # Source (hooks, components, utils) |
| 182 | +│ ├── dist/ # CJS build output |
| 183 | +│ ├── esm/ # ESM build output |
| 184 | +│ └── package.json |
| 185 | +└── mobile/ # @react-simplikit/mobile |
| 186 | + ├── src/ |
| 187 | + ├── dist/ # CJS + ESM build output |
| 188 | + └── package.json |
| 189 | +``` |
| 190 | + |
| 191 | +## package.json Convention |
| 192 | + |
| 193 | +```jsonc |
| 194 | +{ |
| 195 | + "main": "./dist/index.cjs", // CJS entry (top level, NOT in publishConfig) |
| 196 | + "module": "./esm/index.js", // ESM entry for bundlers |
| 197 | + "types": "./dist/index.d.cts", // TypeScript types |
| 198 | + "exports": { |
| 199 | + // Modern Node.js/bundler resolution |
| 200 | + ".": { |
| 201 | + "import": { "types": "...", "default": "..." }, |
| 202 | + "require": { "types": "...", "default": "..." }, |
| 203 | + }, |
| 204 | + }, |
| 205 | + "publishConfig": { |
| 206 | + "access": "public", // ONLY access here, nothing else |
| 207 | + }, |
| 208 | +} |
| 209 | +``` |
0 commit comments