Skip to content

Commit 54037fa

Browse files
committed
feat: convert to ReScript
1 parent 8aa7253 commit 54037fa

17 files changed

+1509
-1977
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,5 @@ yarn-error.log*
3939
# typescript
4040
*.tsbuildinfo
4141
next-env.d.ts
42+
43+
/lib/

CLAUDE.md

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
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

Comments
 (0)