diff --git a/AGENTS.md b/AGENTS.md index cbf94c8d9..0bc80cdc9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 @@ -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 @@ -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`. @@ -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 @@ -197,7 +202,7 @@ let default: unit => React.element ``` - Components requiring React Router context must be wrapped in ``. -- Do not use `` in tests. +- Do not use `` 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. @@ -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. diff --git a/README.md b/README.md index fc5a91433..0a12a874d 100644 --- a/README.md +++ b/README.md @@ -100,22 +100,24 @@ Run these commands from the repository root. To target a workspace, use `yarn workspace `; 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 @@ -161,13 +163,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 `` with your pushed branch: + +```sh +gh workflow run update-visual-screenshots.yml --repo rescript-lang/rescript-lang.org --ref +``` + +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 diff --git a/apps/docs/cypress.config.mjs b/apps/docs/cypress.config.mjs index 613b3f9db..fbe629e46 100644 --- a/apps/docs/cypress.config.mjs +++ b/apps/docs/cypress.config.mjs @@ -7,7 +7,7 @@ export default defineConfig({ openMode: 0, }, e2e: { - baseUrl: "http://localhost:8080", + baseUrl: "http://localhost:5173", specPattern: "e2e/**/*.cy.jsx", excludeSpecPattern: "e2e/homepage/**", supportFile: "cypress/support/e2e.js", diff --git a/apps/docs/package.json b/apps/docs/package.json index 761b69db1..6bf2e55d6 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -37,8 +37,7 @@ "cy:run": "yarn build:res && cypress run", "cy:e2e": "yarn build:res && cypress run --browser chrome", "vitest": "vitest", - "vitest:visual": "VISUAL_TESTS=1 vitest", - "vitest:update": "VISUAL_TESTS=1 VISUAL_BASELINE_UPDATE=1 vitest --run --browser.headless --update" + "vitest:visual": "VISUAL_TESTS=1 vitest" }, "dependencies": { "@cloudflare/pages-plugin-vercel-og": "^0.1.2", diff --git a/apps/docs/scripts/__tests__/BlogFeed_.test.res b/apps/docs/scripts/__tests__/BlogFeed_.test.res index 94a489811..e1680ff7d 100644 --- a/apps/docs/scripts/__tests__/BlogFeed_.test.res +++ b/apps/docs/scripts/__tests__/BlogFeed_.test.res @@ -15,6 +15,11 @@ let blogDirectory = async () => { let directory = await temporaryDirectory("blog-feed-") let blog = join([directory, "markdown-pages", "blog"]) await mkdir(join([blog, "archived"]), {recursive: true}) + let _ = await Promise.all( + ["community", "docs/manual", "docs/guides", "docs/react", "syntax-lookup"]->Array.map(prefix => + mkdir(join([directory, "markdown-pages", prefix]), {recursive: true}) + ), + ) await write(join([blog, "z-old.mdx"]), fixture("Old post", "2019-11-28")) await write(join([blog, "a-new.mdx"]), fixture("New post", "2026-04-28")) await write(join([blog, "archived", "middle.mdx"]), fixture("Archived post", "2020-01-01")) @@ -50,9 +55,13 @@ test("RSS links match article routes, including archived posts", async () => { "https://rescript-lang.org/blog/archived/middle", "https://rescript-lang.org/blog/z-old", ]) - let routes = MdxFile.scanPaths(~dir=join([directory, "markdown-pages", "blog"]), ~alias="blog") + let routes = DocsRouteConfig.loadRoutes( + ~options={ + contentDirectory: join([directory, "markdown-pages"]), + }, + ).routes->DocsRouteConfig.concretePaths items->Array.forEach(item => { - let route = item.href->String.replace("https://rescript-lang.org/", "") + let route = item.href->String.replace("https://rescript-lang.org", "") expect(routes->Array.includes(route))->toBe(true) }) }) @@ -80,11 +89,11 @@ test("RSS dates use midnight UTC and XML serialization leaves dates unchanged", test("the current blog feed contains ten recent posts with existing routes", async () => { let items = BlogApi.RssFeed.getLatest() - let routes = MdxFile.scanPaths(~dir="markdown-pages/blog", ~alias="blog") + let routes = DocsRouteConfig.loadRoutes().routes->DocsRouteConfig.concretePaths expect(items->Array.length)->toBe(10) items->Array.forEach(item => { expect( - routes->Array.includes(item.href->String.replace("https://rescript-lang.org/", "")), + routes->Array.includes(item.href->String.replace("https://rescript-lang.org", "")), )->toBe(true) }) }) diff --git a/apps/docs/src/bindings/DocsRouteConfig.res b/apps/docs/src/bindings/DocsRouteConfig.res new file mode 100644 index 000000000..797a4eda8 --- /dev/null +++ b/apps/docs/src/bindings/DocsRouteConfig.res @@ -0,0 +1,9 @@ +type route +type config = {routes: array} +type options = {contentDirectory?: string} + +@module("../../route-config.mjs") +external loadRoutes: (~options: options=?) => config = "loadRoutes" + +@module("../../generate-route-types.mjs") +external concretePaths: array => array = "concretePaths" diff --git a/package.json b/package.json index 30c76f813..994428688 100644 --- a/package.json +++ b/package.json @@ -38,8 +38,7 @@ "cy:e2e": "yarn workspace @rescript-lang/docs cy:e2e", "vitest": "yarn workspace @rescript-lang/docs vitest", "vitest:visual": "yarn workspace @rescript-lang/docs vitest:visual", - "vitest:guide": "yarn workspace @rescript-lang/guide vitest", - "vitest:update": "yarn workspace @rescript-lang/docs vitest:update" + "vitest:guide": "yarn workspace @rescript-lang/guide vitest" }, "devDependencies": { "lefthook": "^2.1.4",