Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
c704e16
fix: align local test commands with their supported workflows
fhammerschmidt Oct 11, 2026
63e9254
fix: retain localhost in local Cypress URLs
fhammerschmidt Oct 11, 2026
a2bdd1f
refactor: remove obsolete website code and check unused exports
fhammerschmidt Oct 9, 2026
f3a2292
refactor: minimize dead-code entry-point annotations
fhammerschmidt Oct 11, 2026
4ea200d
docs: align contributor guidance with the current monorepo
fhammerschmidt Oct 11, 2026
6b9d76e
test: wait for homepage hydration before prefetch focus
fhammerschmidt Oct 11, 2026
19daf80
fix: align workspace commands with contributor guidance
fhammerschmidt Oct 11, 2026
398ef95
chore: prune redundant package script aliases
fhammerschmidt Oct 12, 2026
291ce19
chore: align preparation alias with the package audit
fhammerschmidt Oct 12, 2026
7e3d001
chore: align performance alias with the package audit
fhammerschmidt Oct 12, 2026
6a7be0c
chore: incorporate the package-script cleanup
fhammerschmidt Oct 12, 2026
21c5a55
chore: incorporate the package-script cleanup
fhammerschmidt Oct 12, 2026
23aca83
docs: remove redundant package command guide
fhammerschmidt Oct 12, 2026
865e649
docs: remove redundant package command guide
fhammerschmidt Oct 12, 2026
b01df23
docs: clarify agent rules and interop examples
fhammerschmidt Oct 12, 2026
faea63a
docs: resolve local test command conflicts with master
fhammerschmidt Oct 14, 2026
b7fca47
docs: reconcile local testing guidance with current master
fhammerschmidt Oct 20, 2026
8aceef3
docs: reconcile agent rules with the merged contributor guidance
fhammerschmidt Oct 20, 2026
f045bfb
refactor: reconcile cleanup with the approved website fixes
fhammerschmidt Oct 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ jobs:
run: yarn ci:generated
- name: Test examples and hrefs
run: yarn test
- name: Check unused ReScript values and exports
run: yarn check:dead-code
- name: Test dead-code diagnostics
run: node --test scripts/__tests__/check-dead-code.test.mjs
# Check the built Docs Pages routes and the Guide local dev server.
Wrangler:
runs-on: ubuntu-24.04
Expand Down
2 changes: 0 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ apps/docs/public/blog/feed.xml
node_modules/
out/
apps/docs/out/
index_data/*.json
apps/docs/index_data/*.json

# Generated via docs test examples script
temp
Expand Down
1 change: 0 additions & 1 deletion .oxfmtrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
"build",
"data/api/**/*.json",
"dist",
"apps/docs/index_data/*.json",
"node_modules/",
"apps/docs/out/",
"apps/docs/public/playground-bundles/",
Expand Down
25 changes: 15 additions & 10 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,9 @@ use the installed `rescript` package, not a separate compiler installation.
- Prefer small functions with a single purpose.
- Use a functional approach but don't make it too hardcore or difficult to understand.
- Keep files and modules small and focused.
- `.res` files must always be capitalized (PascalCase), matching ReScript module conventions.
- Use PascalCase for new `.res` modules. Follow the existing filenames for framework entrypoints such as `root.res` and `routes.res`, and for script entrypoints.
- Use the pipe-first operator (`->`) for chaining, which is idiomatic ReScript.
- Resolve all warnings and treat them as errors. The project has `"error": "+8"` in `rescript.json`.
- Resolve all warnings before committing. `yarn build:res` applies the repository's strict warning checks.
- Never directly edit generated `.jsx` files; edit the corresponding `.res` file and rebuild. Most compiler output is ignored by Git; the guide's `apps/guide/app/root.jsx` and `apps/guide/app/routes.jsx` are tracked and must be regenerated with their sources.

## ReScript Rules
Expand All @@ -83,7 +83,7 @@ use the installed `rescript` package, not a separate compiler installation.
- Reference the abridged documentation for clarification on how ReScript's APIs work: https://rescript-lang.org/llms/manual/llm-small.txt
- If you need more information you can access the full documentation, but do this only when needed as the docs are very large: https://rescript-lang.org/llms/manual/llm-full.txt
- Never use `%raw` unless you are specifically asked to
- Never use `Object.magic`
- Never use `Obj.magic`
- Don't add type annotations unless necessary for clarity or to resolve an error. ReScript's type inference is powerful, and often explicit annotations are not needed.

### ReScript Dependencies
Expand All @@ -96,13 +96,18 @@ use the installed `rescript` package, not a separate compiler installation.
The project uses several patterns for JavaScript interop. Follow the existing conventions:

- **`@module` externals** for binding to npm packages (see `apps/docs/src/bindings/` and `packages/shared/src/` for examples):

```
@module("react-router") external useNavigate: unit => string => unit = "useNavigate"
type navigateOptions = {replace?: bool}
type navigate = (string, ~options: navigateOptions=?) => unit

@module("react-router") external useNavigate: unit => navigate = "useNavigate"
```

- **`@module` with `@react.component`** for binding to external React components:
```
@module("react-router") @react.component
external make: (~children: React.element) => React.element = "Outlet"
external make: unit => React.element = "Outlet"
```
- `%raw` and `%%raw` embed JavaScript; use them only when specifically requested.
- **`@scope`** for binding to object properties like `process.env`.
Expand All @@ -115,12 +120,12 @@ The project uses several patterns for JavaScript interop. Follow the existing co
- The site uses hybrid rendering: most routes are pre-rendered during the build, while `/try` is server-rendered by Cloudflare Pages Functions at runtime.
- Loaders for pre-rendered routes can access the filesystem during the build. Runtime SSR loaders, including shared loader code reachable from `/try`, execute in Cloudflare and must not depend on Node-only filesystem APIs or other build-time-only operations.
- Docs route modules live in `apps/docs/app/routes/`; the guide's route modules live in `apps/guide/app/`. Routes can export a `loader`, `links`, and a `default` component as needed.
- Route modules **require** both a `.res` and a `.resi` (interface) file for Vite HMR to work.
- Only a single React component can be exposed from a module's JS output.
- Give route modules a `.resi` interface that exposes their route exports and keeps helper components private.
- Export a route's page component as `default`. Reusable component modules may export multiple components, as `Markdown.resi` does.

### Route Module Pattern

Every route follows this pattern:
Example of a route with a loader. Routes without loader data can omit the loader and its type:

```rescript
// SomeRoute.res
Expand Down Expand Up @@ -197,7 +202,7 @@ let default: unit => React.element
```

- Components requiring React Router context must be wrapped in `<MemoryRouter>`.
- Do not use `<BrowserRouter>` in tests.
- Do not use `<BrowserRouter>` in component tests. End-to-end tests exercise the app's normal routing and browser history.
- Run `yarn build:res` first so the tests use current compiler output. Run `yarn vitest --browser.headless --run` for docs tests or `yarn vitest:guide --browser.headless --run` for guide tests. Run `yarn ci:test` to check both suites.
- Do not update snapshots without asking for confirmation. Approved docs visual baseline changes use the `Update Visual Regression Screenshots` GitHub Actions workflow against the feature branch; pull and review the generated commit. Local screenshot updates are blocked by the Vitest configuration.

Expand Down Expand Up @@ -229,4 +234,4 @@ let default: unit => React.element
- Do **not** modify generated `.jsx` / `.mjs` files directly — they are ReScript compiler output.
- Do **not** use `@genType` — the project does not use it.
- The `apps/docs/src/ffi/` directory is legacy; prefer bindings in `apps/docs/src/bindings/` or `packages/shared/src/` for new JS interop.
- When editing route files, always update both the `.res` and `.resi` files.
- Keep route `.resi` files in sync when public exports or their types change. Implementation-only edits need only change the `.res` file.
80 changes: 61 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,28 +94,44 @@ Tailwind v4 uses the Vite plugin and [`apps/docs/styles/main.css`](apps/docs/sty

The [recommended VS Code extensions](.vscode/extensions.json) provide ReScript support and [Oxfmt editor integration](https://oxc.rs/docs/guide/usage/formatter/editors.html). Oxfmt configuration lives in [`.oxfmtrc.json`](.oxfmtrc.json).

## Dead-code checks

`yarn check:dead-code` compiles every workspace and runs reanalyze across the
monorepo, including FFI exports. CI fails on unused values and exports. Bindings
whose names start with `_` are treated as intentional discards. Record fields,
variant cases, and argument diagnostics remain advisory because JavaScript and
FFI access can be invisible to the analyzer; inspect them with `yarn reanalyze`.

React Router and Cloudflare call exports from JavaScript. Their entry points are
marked `@live` individually in the `.resi` interface, or in `.res` when there is
no interface. Keep these annotations limited to real entry points and do not
duplicate them between files; check JavaScript imports and MDX mappings before
removing a reported export.

## Common Commands

Run these commands from the repository root. To target a workspace, use
`yarn workspace <name> <command>`; the workspace names are `@rescript-lang/docs`,
`@rescript-lang/guide`, `@rescript-lang/playground`, and `@rescript-lang/shared`.

| Command | Purpose |
| -------------------- | ---------------------------------------------------------------- |
| `yarn dev` | Prepare generated files and run the docs development environment |
| `yarn build` | Build and pre-render the docs site |
| `yarn build:guide` | Build and pre-render the interactive guide |
| `yarn preview` | Build and serve the generated docs client locally |
| `yarn build:res` | Generate route types and compile all four workspaces |
| `yarn build:routes` | Regenerate the shared ReScript route type |
| `yarn check:routes` | Check that the shared route type matches the current routes |
| `yarn dev:res` | Run the ReScript compiler in watch mode |
| `yarn format` | Run Oxfmt and format ReScript across all four workspaces |
| `yarn test` | Run docs markdown example, href, and redirect validation |
| `yarn ci:test` | Run docs and guide Vitest browser tests headlessly |
| `yarn vitest` | Run docs Vitest browser tests in watch mode |
| `yarn vitest:guide` | Run guide Vitest browser tests in watch mode |
| `yarn vitest:update` | Update screenshot baselines headlessly |
| Command | Purpose |
| ------------------- | ---------------------------------------------------------------- |
| `yarn dev` | Prepare generated files and run the docs development environment |
| `yarn build` | Build and pre-render the docs site |
| `yarn build:guide` | Build and pre-render the interactive guide |
| `yarn preview` | Build and serve the generated docs client locally |
| `yarn build:res` | Generate route types and compile all four workspaces |
| `yarn build:routes` | Regenerate the shared ReScript route type |
| `yarn check:routes` | Check that the shared route type matches the current routes |
| `yarn dev:res` | Run the ReScript compiler in watch mode |
| `yarn format` | Run Oxfmt and format ReScript across all four workspaces |
| `yarn test` | Run docs markdown example, href, and redirect validation |
| `yarn ci:test` | Run docs and guide Vitest browser tests headlessly |
| `yarn vitest` | Run docs Vitest browser tests in watch mode |
| `yarn vitest:guide` | Run guide Vitest browser tests in watch mode |
| `yarn cy:open` | Open the full-site Cypress suite against the running dev server |
| `yarn cy:e2e` | Run the full-site Cypress suite headlessly in Chrome |
| `yarn test:e2e` | Build and run homepage Cypress tests with a temporary server |

## Testing

Expand Down Expand Up @@ -161,13 +177,39 @@ yarn ci:test
For a headless run of a single suite, use `yarn vitest --browser.headless --run`
for docs or `yarn vitest:guide --browser.headless --run` for the guide.

To update screenshot baselines, run:
Visual screenshot baselines are generated in CI's pinned Linux browser environment. Local snapshot updates are blocked. Push your feature branch, then open [Update Visual Regression Screenshots](https://github.com/rescript-lang/rescript-lang.org/actions/workflows/update-visual-screenshots.yml), select that branch, and choose **Run workflow**. The workflow refuses `master` and commits any changed PNGs back to the selected branch.

You can also dispatch the workflow with the GitHub CLI, replacing `<feature-branch>` with your pushed branch:

```sh
gh workflow run update-visual-screenshots.yml --repo rescript-lang/rescript-lang.org --ref <feature-branch>
```

After the workflow finishes, pull its commit and review the screenshot diff. Only accept screenshots intentionally affected by your change.

### Cypress E2E Tests

The full-site suite covers documentation, navigation, and the playground. Start the local development server in one terminal:

```sh
yarn dev
```

Wait for Vite to report its local URL. In another terminal, run one of these commands:

```sh
yarn cy:open # Interactive Cypress runner (Electron)
yarn cy:run # Headless run (Electron)
yarn cy:e2e # Headless run (Chrome must be installed)
```

These commands compile the ReScript specs and default to `http://localhost:5173`. If Vite reports another port, or you want to test a deployed site, pass its URL explicitly:

```sh
yarn vitest:update
yarn cy:e2e --config baseUrl=http://localhost:5174
```

Only update screenshots that are intentionally affected by your change.
The homepage suite uses the production build and its own configuration. `yarn test:e2e` builds the site, starts a temporary server at `http://127.0.0.1:4173`, runs the homepage specs in Chrome, and stops the server when the tests finish. It does not require `yarn dev`.

### Markdown Example and Link Checks

Expand Down
29 changes: 0 additions & 29 deletions apps/docs/__tests__/Url_.test.res

This file was deleted.

2 changes: 1 addition & 1 deletion apps/docs/app/DocsRoot.resi
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/** Shared shell, token styles, and the stable Cypress bootstrap slot.
Runtime grammars belong to content routes and the playground. */
/** Keep server-rendered content visible; head stylesheets handle render blocking. */
@react.component
@react.component @live
let default: unit => Jsx.element
3 changes: 2 additions & 1 deletion apps/docs/app/layouts/ContentLayoutRoute.resi
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
type stylesheet = {rel: string, href: string}

@live
let links: unit => array<stylesheet>

/** Own content styles and runtime highlighting without adding a DOM wrapper. */
@react.component
@react.component @live
let default: unit => React.element
2 changes: 1 addition & 1 deletion apps/docs/app/layouts/DocsLayoutRoute.resi
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
@react.component
@react.component @live
let default: unit => React.element
3 changes: 2 additions & 1 deletion apps/docs/app/layouts/HomepageLayoutRoute.resi
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
type stylesheet = {rel: string, href: string}

@live
let links: unit => array<stylesheet>

/** Own homepage styles without adding a DOM wrapper. */
@react.component
@react.component @live
let default: unit => React.element
3 changes: 2 additions & 1 deletion apps/docs/app/routes/ApiDocs.res
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,8 @@ module DocstringsStylize = {
}
}

@react.componentWithProps
// reanalyze does not see the JSX props spread call in ApiRoute.
@live @react.componentWithProps
let make = (props: props) => {
let children = {
open Markdown
Expand Down
4 changes: 2 additions & 2 deletions apps/docs/app/routes/ApiOverviewRoute.resi
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
let categories: array<SidebarNav.Category.t>

type loaderData = {
compiledMdx: CompiledMdx.t,
title: string,
description: string,
}

@live
let loader: ReactRouter.Loader.t<loaderData>

@live
let default: unit => React.element
2 changes: 2 additions & 0 deletions apps/docs/app/routes/ApiRoute.resi
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
type loaderData = ApiDocs.props

/** Loads page content and navigation from the same published compiler dataset. */
@live
let loader: ReactRouter.Loader.t<loaderData>

@live
let default: unit => React.element
2 changes: 2 additions & 0 deletions apps/docs/app/routes/BlogArticleRoute.resi
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ type loaderData = {
title: string,
}

@live
let loader: ReactRouter.Loader.t<loaderData>

@live
let default: unit => React.element
3 changes: 2 additions & 1 deletion apps/docs/app/routes/BlogRoute.resi
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
type loaderData = {posts: array<BlogApi.post>, category: Blog.category}

@live
let loader: ReactRouter.Loader.t<loaderData>

@react.component
@react.component @live
let default: unit => React.element
1 change: 1 addition & 0 deletions apps/docs/app/routes/BrandRoute.resi
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
@live
let default: unit => React.element
2 changes: 2 additions & 0 deletions apps/docs/app/routes/CommunityRoute.resi
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ type loaderData = {
categories: array<SidebarNav.Category.t>,
}

@live
let loader: ReactRouter.Loader.t<loaderData>

@live
let default: unit => React.element
2 changes: 2 additions & 0 deletions apps/docs/app/routes/DocsGuidesRoute.resi
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ type loaderData = {
filePath: string,
}

@live
let loader: ReactRouter.Loader.t<loaderData>

@live
let default: unit => React.element
2 changes: 2 additions & 0 deletions apps/docs/app/routes/DocsManualRoute.resi
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ type loaderData = {
filePath: string,
}

@live
let loader: ReactRouter.Loader.t<loaderData>

@live
let default: unit => React.element
2 changes: 2 additions & 0 deletions apps/docs/app/routes/DocsReactRoute.resi
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ type loaderData = {
filePath: string,
}

@live
let loader: ReactRouter.Loader.t<loaderData>

@live
let default: unit => React.element
4 changes: 3 additions & 1 deletion apps/docs/app/routes/LandingPageRoute.resi
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ type fontPreload = {
}

/** Preload above-the-fold faces, including Inter 700 only at the desktop breakpoint. */
@live
let links: unit => array<fontPreload>

type loaderData = {playgroundData: LandingPagePlayground.playgroundData}

/** Prerender the fixed example so highlighting and URL compression stay off the client. */
@live
let loader: ReactRouter.Loader.t<loaderData>

@react.component
@react.component @live
let default: unit => React.element
4 changes: 3 additions & 1 deletion apps/docs/app/routes/NotFoundRoute.resi
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
@live
let loader: ReactRouter.Loader.t<ReactRouter.data<JSON.t>>
@live
let clientLoader: ReactRouter.Loader.t<JSON.t>

@react.component
@react.component @live
let default: unit => React.element
2 changes: 2 additions & 0 deletions apps/docs/app/routes/PackagesRoute.resi
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
@live
let loader: unit => promise<Packages.props>
@live
let default: unit => React.element
Loading