|
| 1 | +# CLAUDE.md |
| 2 | + |
| 3 | +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. |
| 4 | + |
| 5 | +## Project Overview |
| 6 | + |
| 7 | +This is a Next.js 15 application using the App Router architecture with ReScript integration and Bun as the package manager. The project combines TypeScript (for Next.js components) with ReScript (for functional programming) and uses Biome for code formatting and linting. |
| 8 | + |
| 9 | +## Development Commands |
| 10 | + |
| 11 | +### Running the Application |
| 12 | +- `bun dev` - Start development server (preferred, uses Bun) |
| 13 | +- `npm run dev` - Alternative development server |
| 14 | +- `bun run dev:turbo` - Development with Turbopack (experimental) |
| 15 | + |
| 16 | +### Building |
| 17 | +- `bun run build` - Full production build (compiles ReScript then Next.js) |
| 18 | +- `bun run build:turbo` - Build with Turbopack (experimental) |
| 19 | + |
| 20 | +### ReScript Development |
| 21 | +- `bun run res:build` - Compile ReScript files to JavaScript |
| 22 | +- `bun run res:dev` - Watch mode for ReScript compilation |
| 23 | +- `bun run res:clean` - Clean ReScript build artifacts |
| 24 | + |
| 25 | +### Code Quality |
| 26 | +- `bun run lint` - Run Biome linter and formatter checks |
| 27 | +- `bun run format` - Auto-format code with Biome |
| 28 | + |
| 29 | +### Production |
| 30 | +- `bun run start` - Serve static build with npx serve |
| 31 | +- `bun run start:turbo` - Start Next.js production server |
| 32 | + |
| 33 | +## Architecture |
| 34 | + |
| 35 | +### Hybrid Language Approach |
| 36 | +This project uses both TypeScript and ReScript: |
| 37 | +- **TypeScript**: Next.js App Router components (`src/app/`) |
| 38 | +- **ReScript**: Business logic and utilities (`src/bindings/`) |
| 39 | + |
| 40 | +### ReScript Integration |
| 41 | +- ReScript source files are in `src/` with `.res` extension |
| 42 | +- Compiled to ES modules with `.res.mjs` suffix |
| 43 | +- Output is in-source (alongside `.res` files) |
| 44 | +- Next.js config handles transpilation of ReScript dependencies |
| 45 | +- Uses `@rescript/core` and `@rescript/react` packages |
| 46 | + |
| 47 | +### Next.js Configuration |
| 48 | +The `next.config.ts` includes: |
| 49 | +- Custom webpack rules for ReScript `.mjs` files |
| 50 | +- Transpilation of ReScript packages |
| 51 | +- Client-side fallbacks for Node.js modules (fs, path) |
| 52 | + |
| 53 | +### Directory Structure |
| 54 | +- `src/app/` - Next.js App Router pages and layouts (TypeScript) |
| 55 | +- `src/bindings/` - ReScript bindings for Next.js APIs |
| 56 | +- `lib/bs/` - ReScript build artifacts (auto-generated) |
| 57 | + |
| 58 | +### ReScript Bindings |
| 59 | +The project includes comprehensive Next.js App Router bindings in `src/bindings/NextAppRouter.res` covering: |
| 60 | +- Client-side navigation hooks (useRouter, usePathname, etc.) |
| 61 | +- Link component |
| 62 | +- Metadata types and helpers |
| 63 | +- Error handling |
| 64 | +- Loading states |
| 65 | + |
| 66 | +## Tools and Configuration |
| 67 | + |
| 68 | +### Biome |
| 69 | +- Handles both linting and formatting |
| 70 | +- Configured for Next.js and React |
| 71 | +- 2-space indentation |
| 72 | +- Organizes imports automatically |
| 73 | +- Configuration in `biome.json` |
| 74 | + |
| 75 | +### Package Manager |
| 76 | +- Uses Bun as the primary package manager (`bun.lock` present) |
| 77 | +- Package.json scripts assume Bun availability |
| 78 | + |
| 79 | +## Development Workflow |
| 80 | + |
| 81 | +1. Start ReScript compilation in watch mode: `bun run res:dev` |
| 82 | +2. Start Next.js dev server: `bun dev` |
| 83 | +3. Edit ReScript files in `src/` - they auto-compile to `.res.mjs` |
| 84 | +4. Edit TypeScript components in `src/app/` |
| 85 | +5. Run linting: `bun run lint` |
| 86 | + |
| 87 | +## Common ReScript Compilation Errors |
| 88 | + |
| 89 | +### Inline Record Types Error |
| 90 | +**Error**: "An inline record type declaration is only allowed in a variant constructor's declaration" |
| 91 | + |
| 92 | +**Cause**: ReScript doesn't allow inline record types in regular type definitions like: |
| 93 | +```rescript |
| 94 | +type example = { |
| 95 | + field: array<{name: string, value: int}> // ❌ This fails |
| 96 | +} |
| 97 | +``` |
| 98 | + |
| 99 | +**Fix**: Extract inline records as separate type definitions: |
| 100 | +```rescript |
| 101 | +type innerRecord = {name: string, value: int} |
| 102 | +type example = { |
| 103 | + field: array<innerRecord> // ✅ This works |
| 104 | +} |
| 105 | +``` |
| 106 | + |
| 107 | +### Optional Fields Syntax |
| 108 | +**Error**: Type mismatches with optional record fields |
| 109 | + |
| 110 | +**Cause**: ReScript's `?` optional field syntax doesn't work as expected for JavaScript interop. |
| 111 | + |
| 112 | +**Fix**: Use explicit `option<'a>` types: |
| 113 | +```rescript |
| 114 | +// ❌ Don't use this for JS bindings: |
| 115 | +type metadata = { title?: string } |
| 116 | +
|
| 117 | +// ✅ Use this instead: |
| 118 | +type metadata = { title: option<string> } |
| 119 | +``` |
| 120 | + |
| 121 | +### URLSearchParams and Web APIs |
| 122 | +**Error**: "The module or file URLSearchParams can't be found" |
| 123 | + |
| 124 | +**Cause**: Web API types aren't automatically available in ReScript. |
| 125 | + |
| 126 | +**Fix**: Create abstract type bindings: |
| 127 | +```rescript |
| 128 | +type urlSearchParams // Abstract type for URLSearchParams |
| 129 | +@module("next/navigation") |
| 130 | +external useSearchParams: unit => urlSearchParams = "useSearchParams" |
| 131 | +``` |
| 132 | + |
| 133 | +### Client Component Directives |
| 134 | +**Correct Usage**: ReScript supports the Next.js App Router client directive |
| 135 | + |
| 136 | +**How to use**: Add `@@directive("'use client'")` at the top of ReScript component files: |
| 137 | +```rescript |
| 138 | +@@directive("'use client'") |
| 139 | +
|
| 140 | +@react.component |
| 141 | +let make = (~children) => { |
| 142 | + // Client-side component logic here |
| 143 | + <div className="client-component"> {children} </div> |
| 144 | +} |
| 145 | +``` |
| 146 | + |
| 147 | +**Note**: This marks the entire file as a client component, enabling browser-specific APIs like `useState`, `useEffect`, event handlers, etc. |
| 148 | + |
| 149 | +## Converting TypeScript Components to ReScript |
| 150 | + |
| 151 | +### Component Structure Issues |
| 152 | +**Error**: "Only one component definition is allowed for each module" |
| 153 | + |
| 154 | +**Cause**: Having both external component bindings and component definitions in the same module. |
| 155 | + |
| 156 | +**Fix**: Wrap external bindings in a module: |
| 157 | +```rescript |
| 158 | +// ❌ This fails: |
| 159 | +@module("next/image") @react.component |
| 160 | +external image: (~src: string) => React.element = "default" |
| 161 | +
|
| 162 | +@react.component |
| 163 | +let make = () => <div /> |
| 164 | +
|
| 165 | +// ✅ Use this instead: |
| 166 | +module Image = { |
| 167 | + @module("next/image") @react.component |
| 168 | + external make: (~src: string) => React.element = "default" |
| 169 | +} |
| 170 | +
|
| 171 | +@react.component |
| 172 | +let make = () => <Image src="/logo.png" /> |
| 173 | +``` |
| 174 | + |
| 175 | +### Next.js Font Bindings |
| 176 | +**Error 1**: "Font loaders can't have namespace imports" |
| 177 | +**Error 2**: "Font loaders must be called and assigned to a const in the module scope" |
| 178 | + |
| 179 | +**Cause**: ReScript's module bindings generate either namespace imports or `var` declarations, but Next.js font loaders require: |
| 180 | +1. Direct named imports (not namespace imports) |
| 181 | +2. `const` declarations at module scope |
| 182 | + |
| 183 | +**Fix**: Use `%%raw` to generate the exact JavaScript that Next.js expects: |
| 184 | +```rescript |
| 185 | +// ❌ This fails with namespace/const errors: |
| 186 | +@module("next/font/google") |
| 187 | +external geist: {...} => {...} = "Geist" |
| 188 | +let font = geist({...}) |
| 189 | +
|
| 190 | +// ✅ Use this pattern instead: |
| 191 | +// Font loaders - must be const declarations at module scope for Next.js |
| 192 | +%%raw(` |
| 193 | +import { Geist, Geist_Mono } from "next/font/google"; |
| 194 | +
|
| 195 | +const geistSans = Geist({ |
| 196 | + variable: "--font-geist-sans", |
| 197 | + subsets: ["latin"] |
| 198 | +}); |
| 199 | +
|
| 200 | +const geistMonoFont = Geist_Mono({ |
| 201 | + variable: "--font-geist-mono", |
| 202 | + subsets: ["latin"] |
| 203 | +}); |
| 204 | +`) |
| 205 | +
|
| 206 | +// External bindings to access the font objects from ReScript |
| 207 | +@val external geistSans: {"variable": string} = "geistSans" |
| 208 | +@val external geistMonoFont: {"variable": string} = "geistMonoFont" |
| 209 | +``` |
| 210 | + |
| 211 | +### CSS Imports |
| 212 | +**Pattern**: Use `%%raw` for CSS imports: |
| 213 | +```rescript |
| 214 | +// TypeScript: import "./globals.css" |
| 215 | +// ReScript: |
| 216 | +%%raw(`import "./globals.css"`) |
| 217 | +``` |
| 218 | + |
| 219 | +### Metadata Export |
| 220 | +**Pattern**: Use the Metadata types from bindings: |
| 221 | +```rescript |
| 222 | +open NextAppRouter.Metadata |
| 223 | +
|
| 224 | +let metadata: metadata = { |
| 225 | + title: Some("Page Title"), |
| 226 | + description: Some("Page description"), |
| 227 | + keywords: None, |
| 228 | + // ... set other fields to None |
| 229 | +} |
| 230 | +``` |
| 231 | + |
| 232 | +### Special HTML Attributes |
| 233 | +**Issue**: ReScript doesn't support quoted prop names like `"aria-hidden"` |
| 234 | + |
| 235 | +**Workaround**: Either omit the attribute or create a more complex binding with `@as`: |
| 236 | +```rescript |
| 237 | +// Simple approach - omit if not critical: |
| 238 | +<Image src="/icon.svg" alt="Icon" width={16} height={16} /> |
| 239 | +
|
| 240 | +// Complex approach - use @as decorator (advanced): |
| 241 | +// ~ariaHidden: bool=? @as("aria-hidden") |
| 242 | +``` |
| 243 | + |
| 244 | +### Next.js Configuration Updates |
| 245 | +**Required**: Add ReScript compiled extensions to Next.js config: |
| 246 | +```typescript |
| 247 | +// next.config.ts |
| 248 | +const nextConfig: NextConfig = { |
| 249 | + pageExtensions: ["tsx", "ts", "jsx", "js", "res.mjs"], |
| 250 | + // ... other config |
| 251 | +} |
| 252 | +``` |
| 253 | + |
| 254 | +### String Content |
| 255 | +**Pattern**: Always wrap text content in `React.string()`: |
| 256 | +```rescript |
| 257 | +// ❌ This fails: |
| 258 | +<div>"Hello World"</div> |
| 259 | +
|
| 260 | +// ✅ Use this: |
| 261 | +<div>{React.string("Hello World")}</div> |
| 262 | +``` |
0 commit comments