From ace50108a0361ec65d462b1479181aaed2e6ac60 Mon Sep 17 00:00:00 2001 From: Florian Hammerschmidt Date: Sat, 10 Oct 2026 00:49:41 +0200 Subject: [PATCH 01/10] fix: generate route types from the live route configuration Move the build-time route table to native JavaScript so React Router and route generation share one configuration without requiring compiled ReScript first. Generate the shared Path.res before compilation, handle nested layouts and index routes, omit URL patterns, and detect stale output in CI. Add regression tests for content discovery, deterministic output, and non-writing checks from any working directory. Fixes #1408. Part of #1421. --- .github/workflows/pull-request.yml | 4 + README.md | 11 +- apps/docs/app/DocsRoutes.res | 113 --- apps/docs/app/DocsRoutes.resi | 14 - apps/docs/app/routes.js | 4 +- apps/docs/generate-route-types.mjs | 81 +- apps/docs/package.json | 6 +- apps/docs/react-router.config.mjs | 3 +- apps/docs/route-config.mjs | 104 +++ .../__tests__/generate-route-types.test.mjs | 185 ++++ apps/docs/src/markdown/MdxFile.res | 6 - apps/docs/src/markdown/MdxFile.resi | 6 - apps/guide/package.json | 2 +- package.json | 6 +- packages/shared/src/Path.res | 860 +++++++++--------- packages/shared/src/ReactRouter.res | 2 +- 16 files changed, 818 insertions(+), 589 deletions(-) delete mode 100644 apps/docs/app/DocsRoutes.res delete mode 100644 apps/docs/app/DocsRoutes.resi create mode 100644 apps/docs/route-config.mjs create mode 100644 apps/docs/scripts/__tests__/generate-route-types.test.mjs diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index e11c81136..6b3843706 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -35,6 +35,10 @@ jobs: cache: yarn - name: Install dependencies run: yarn + - name: Check generated route types + run: | + yarn check:routes + git diff --exit-code -- packages/shared/src/Path.res - name: Test examples and hrefs run: yarn test # Confirms that Wrangler can start the Docs Pages and Guide local dev servers. diff --git a/README.md b/README.md index 2630705a2..5d9dc1753 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,9 @@ Tailwind is configured in [`styles/main.css`](styles/main.css). There is no `tai | `yarn dev` | Prepare generated files and run the local development environment | | `yarn build` | Run the full production build | | `yarn preview` | Build and serve the generated static client locally | -| `yarn build:res` | Compile ReScript only | +| `yarn build:res` | Generate route types and compile ReScript | +| `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 Prettier and the ReScript formatter | | `yarn test` | Run markdown example and href validation | @@ -102,6 +104,13 @@ Tailwind is configured in [`styles/main.css`](styles/main.css). There is no `tai ## Testing +The documentation route table lives in `apps/docs/route-config.mjs`. React Router +and `apps/docs/generate-route-types.mjs` read the same configuration, including +MDX pages and API modules. `packages/shared/src/Path.res` is generated before +ReScript builds and when starting the ReScript watcher. After changing routes, +adding or removing MDX pages, or updating API data during a watch session, run +`yarn build:routes` again. CI rejects a stale generated file. + ### Vitest Browser Tests We use [Vitest](https://vitest.dev/) in browser mode with Playwright for component-level tests. Test files live in `__tests__/` and are written in ReScript. diff --git a/apps/docs/app/DocsRoutes.res b/apps/docs/app/DocsRoutes.res deleted file mode 100644 index 35886a279..000000000 --- a/apps/docs/app/DocsRoutes.res +++ /dev/null @@ -1,113 +0,0 @@ -open ReactRouter.Routes - -let stdlibPaths = { - let rawFile = await Node.Fs.readFile("./markdown-pages/docs/api/stdlib.json", "utf-8") - let json = JSON.parseOrThrow(rawFile) - switch json { - | Object(json) => Dict.keysToArray(json) - | _ => [] - } - ->Array.map(key => "docs/manual/api/" ++ key) - ->Array.filter(path => path !== "docs/manual/api/stdlib") -} - -let domPaths = { - let rawFile = await Node.Fs.readFile("./markdown-pages/docs/api/dom.json", "utf-8") - let json = JSON.parseOrThrow(rawFile) - switch json { - | Object(json) => Dict.keysToArray(json) - | _ => [] - } - ->Array.map(key => "docs/manual/api/" ++ key) - ->Array.filter(path => path !== "docs/manual/api/dom") -} - -let beltPaths = { - let rawFile = await Node.Fs.readFile("./markdown-pages/docs/api/belt.json", "utf-8") - let json = JSON.parseOrThrow(rawFile) - switch json { - | Object(json) => Dict.keysToArray(json) - | _ => [] - } - ->Array.map(key => "docs/manual/api/" ++ key) - ->Array.filter(path => path !== "docs/manual/api/belt") -} - -let stdlibRoutes = - stdlibPaths->Array.map(path => route(path, "./routes/ApiRoute.jsx", ~options={id: path})) - -let domRoutes = - domPaths->Array.map(path => route(path, "./routes/ApiRoute.jsx", ~options={id: path})) - -let beltRoutes = - beltPaths->Array.map(path => route(path, "./routes/ApiRoute.jsx", ~options={id: path})) - -let blogArticleRoutes = - MdxFile.scanPaths(~dir="markdown-pages/blog", ~alias="blog")->Array.map(path => - route(path, "./routes/BlogArticleRoute.jsx", ~options={id: path}) - ) - -let docsManualRoutes = - MdxFile.scanPaths(~dir="markdown-pages/docs/manual", ~alias="docs/manual") - ->Array.filter(path => !String.includes(path, "docs/manual/api")) - ->Array.map(path => route(path, "./routes/DocsManualRoute.jsx", ~options={id: path})) - -let docsReactRoutes = - MdxFile.scanPaths(~dir="markdown-pages/docs/react", ~alias="docs/react")->Array.map(path => - route(path, "./routes/DocsReactRoute.jsx", ~options={id: path}) - ) - -let docsGuidesRoutes = - MdxFile.scanPaths(~dir="markdown-pages/docs/guides", ~alias="docs/guides")->Array.map(path => - route(path, "./routes/DocsGuidesRoute.jsx", ~options={id: path}) - ) - -let communityRoutes = - MdxFile.scanPaths(~dir="markdown-pages/community", ~alias="community")->Array.map(path => - route(path, "./routes/CommunityRoute.jsx", ~options={id: path}) - ) - -let syntaxLookupDetailRoutes = - MdxFile.scanPaths(~dir="markdown-pages/syntax-lookup", ~alias="syntax-lookup")->Array.map(path => - route(path, "./routes/SyntaxLookupDetailRoute.jsx", ~options={id: path}) - ) - -let default = [ - layout("./layouts/HomepageLayoutRoute.jsx", [index("./routes/LandingPageRoute.jsx")]), - route("try", "./routes/TryRoute.jsx"), - layout( - "./layouts/ContentLayoutRoute.jsx", - [ - route("packages", "./routes/PackagesRoute.jsx"), - route("brand", "./routes/BrandRoute.jsx"), - route("blog", "./routes/BlogRoute.jsx", ~options={id: "blog-index"}), - route("blog/archived", "./routes/BlogRoute.jsx", ~options={id: "blog-archived"}), - ...blogArticleRoutes, - ...communityRoutes, - route("docs", "./routes/DocsOverview.jsx", ~options={id: "docs-overview"}), - layout( - "./layouts/DocsLayoutRoute.jsx", - [ - route("docs/manual/api", "./routes/ApiOverviewRoute.jsx", ~options={id: "api-overview"}), - route("docs/manual/api/stdlib", "./routes/ApiRoute.jsx", ~options={id: "api-stdlib"}), - route( - "docs/manual/api/introduction", - "./routes/ApiRoute.jsx", - ~options={id: "api-intro"}, - ), - route("docs/manual/api/belt", "./routes/ApiRoute.jsx", ~options={id: "api-belt"}), - route("docs/manual/api/dom", "./routes/ApiRoute.jsx", ~options={id: "api-dom"}), - ...stdlibRoutes, - ...beltRoutes, - ...domRoutes, - ...docsManualRoutes, - ...docsGuidesRoutes, - ...docsReactRoutes, - route("syntax-lookup", "./routes/SyntaxLookupRoute.jsx", ~options={id: "syntax-lookup"}), - ...syntaxLookupDetailRoutes, - ], - ), - ], - ), - route("*", "./routes/NotFoundRoute.jsx"), -] diff --git a/apps/docs/app/DocsRoutes.resi b/apps/docs/app/DocsRoutes.resi deleted file mode 100644 index ba1514a46..000000000 --- a/apps/docs/app/DocsRoutes.resi +++ /dev/null @@ -1,14 +0,0 @@ -let stdlibPaths: array -let domPaths: array -let beltPaths: array -let stdlibRoutes: array -let domRoutes: array -let beltRoutes: array -let blogArticleRoutes: array -let docsManualRoutes: array -let docsReactRoutes: array -let docsGuidesRoutes: array -let communityRoutes: array -let syntaxLookupDetailRoutes: array -/** Keep homepage, playground, and content styles scoped to their route groups. */ -let default: array diff --git a/apps/docs/app/routes.js b/apps/docs/app/routes.js index 7a59de9bb..8fca71115 100644 --- a/apps/docs/app/routes.js +++ b/apps/docs/app/routes.js @@ -1 +1,3 @@ -export { default } from "./DocsRoutes.jsx"; +import { loadRoutes } from "../route-config.mjs"; + +export default loadRoutes().routes; diff --git a/apps/docs/generate-route-types.mjs b/apps/docs/generate-route-types.mjs index 9d9d80e02..2491418b8 100644 --- a/apps/docs/generate-route-types.mjs +++ b/apps/docs/generate-route-types.mjs @@ -1,14 +1,75 @@ -import fs from "fs/promises"; +import { readFileSync, writeFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { loadRoutes } from "./route-config.mjs"; -const { default: routes } = await import("./app/routes.mjs"); +export const outputPath = fileURLToPath( + new URL("../../packages/shared/src/Path.res", import.meta.url), +); -const paths = routes.map((route) => `#"/${route.path}"`).join(" |\n"); +export function concretePaths(routes) { + const paths = new Set(); + function visit(routes, parent = "") { + for (const route of routes) { + const path = route.path?.startsWith("/") + ? route.path + : `${parent}/${route.path ?? ""}`; + const pathname = `/${path.split("/").filter(Boolean).join("/")}`; + // Layouts have no URL of their own. Route patterns aren't link destinations. + if ( + (route.index || route.path !== undefined) && + !/[:*?]/.test(pathname) + ) { + paths.add(pathname); + } + if (route.children) visit(route.children, pathname); + } + } + visit(routes); + return [...paths].sort(); +} -await fs.writeFile( - "src/Path.res", - `type t = [ -#"/" | -${paths} +export function renderPathType(routes) { + const paths = concretePaths(routes); + if (paths.length === 0) throw new Error("No concrete routes found."); + return `// Generated by apps/docs/generate-route-types.mjs. Do not edit. +// Run yarn build:routes after changing routes, MDX pages, or API data. +type t = [ +${paths.map((path) => ` | #${JSON.stringify(path)}`).join("\n")} ] -`, -); +`; +} + +export function generateRouteTypes({ + routes = loadRoutes().routes, + destination = outputPath, + check = false, +} = {}) { + const source = renderPathType(routes); + let current; + try { + current = readFileSync(destination, "utf8"); + } catch (error) { + if (error.code !== "ENOENT") throw error; + } + if (current === source) return true; + if (check) return false; + writeFileSync(destination, source); + return true; +} + +if ( + process.argv[1] && + resolve(process.argv[1]) === fileURLToPath(import.meta.url) +) { + const args = process.argv.slice(2); + if (args.some((arg) => arg !== "--check")) { + throw new Error("Usage: node generate-route-types.mjs [--check]"); + } + if (!generateRouteTypes({ check: args.includes("--check") })) { + console.error( + "Route types are stale. Run yarn build:routes and commit packages/shared/src/Path.res.", + ); + process.exitCode = 1; + } +} diff --git a/apps/docs/package.json b/apps/docs/package.json index e7350a978..07c92c50d 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -9,7 +9,7 @@ "build:scripts": "cd ../.. && yarn dlx tsdown@0.22.0 apps/docs/scripts/*.jsx -d apps/docs/_scripts --no-clean --ext .mjs", "build:generate-llms": "node _scripts/generate_llms.mjs", "build:generate-sitemap": "node _scripts/generate_sitemap.mjs build/client out", - "build:res": "rescript build --warn-error +3+8+11+12+26+27+31+32+33+34+35+39+44+45+110", + "build:res": "node generate-route-types.mjs && rescript build --warn-error +3+8+11+12+26+27+31+32+33+34+35+39+44+45+110", "build:sync-bundles": "node scripts/sync-playground-bundles.mjs", "build:update-index": "yarn build:generate-llms && node _scripts/generate_feed.mjs > public/blog/feed.xml", "build:vite": "react-router build", @@ -19,12 +19,12 @@ "ci:homepage-performance": "node --test scripts/__tests__/homepage-compiler.test.mjs && node scripts/homepage-performance.mjs", "ci:test": "vitest --run --browser.headless", "ci:test:e2e": "cypress run --config-file cypress.homepage.config.mjs --browser chrome", - "ci:test:scripts": "yarn build:scripts && vitest run --config vitest.scripts.config.mjs && node --test scripts/__tests__/generate-llms.test.mjs scripts/__tests__/test-examples.test.mjs scripts/__tests__/test-hrefs.test.mjs scripts/__tests__/test-runner.test.mjs", + "ci:test:scripts": "yarn build:scripts && vitest run --config vitest.scripts.config.mjs && node --test scripts/__tests__/generate-route-types.test.mjs scripts/__tests__/generate-llms.test.mjs scripts/__tests__/test-examples.test.mjs scripts/__tests__/test-hrefs.test.mjs scripts/__tests__/test-runner.test.mjs", "ci:test:visual": "VISUAL_TESTS=1 vitest --run --browser.headless", "ci:test:visual:update": "VISUAL_TESTS=1 VISUAL_BASELINE_UPDATE=1 vitest --run --browser.headless --update", "clean:res": "rescript clean", "convert-images": "auto-convert-images", - "dev:res": "rescript watch", + "dev:res": "node generate-route-types.mjs && rescript watch", "dev:vite": "react-router dev --host", "dev:wrangler": "yarn build:vite && yarn wrangler pages dev build/client", "dev": "yarn prepare && yarn dev:res & yarn dev:vite & yarn dev:wrangler", diff --git a/apps/docs/react-router.config.mjs b/apps/docs/react-router.config.mjs index 88725056f..b7f292c27 100644 --- a/apps/docs/react-router.config.mjs +++ b/apps/docs/react-router.config.mjs @@ -1,6 +1,7 @@ import * as fs from "node:fs"; +import { loadRoutes } from "./route-config.mjs"; -const { stdlibPaths } = await import("./app/DocsRoutes.jsx"); +const { stdlibPaths } = loadRoutes(); export default { ssr: true, diff --git a/apps/docs/route-config.mjs b/apps/docs/route-config.mjs new file mode 100644 index 000000000..48c972ea1 --- /dev/null +++ b/apps/docs/route-config.mjs @@ -0,0 +1,104 @@ +import { readFileSync, readdirSync } from "node:fs"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { index, layout, route } from "@react-router/dev/routes"; + +function mdxPaths(directory) { + return readdirSync(directory, { withFileTypes: true }) + .flatMap((entry) => { + if (entry.isDirectory()) { + return mdxPaths(join(directory, entry.name)).map( + (path) => `${entry.name}/${path}`, + ); + } + return entry.isFile() && entry.name.endsWith(".mdx") + ? [entry.name.slice(0, -4)] + : []; + }) + .sort(); +} + +// Both React Router and route-type generation read this build-time configuration. +// It can run before ReScript compilation and does not depend on the caller's cwd. +export function loadRoutes({ + contentDirectory = fileURLToPath( + new URL("./markdown-pages/", import.meta.url), + ), + apiDirectory = join(contentDirectory, "docs/api"), +} = {}) { + const apiPaths = (library) => + Object.keys( + JSON.parse(readFileSync(join(apiDirectory, `${library}.json`), "utf8")), + ) + .filter((key) => key !== library) + .sort() + .map((key) => `docs/manual/api/${key}`); + const stdlibPaths = apiPaths("stdlib"); + const domPaths = apiPaths("dom"); + const beltPaths = apiPaths("belt"); + const apiRoutes = (paths) => + paths.map((path) => route(path, "./routes/ApiRoute.jsx", { id: path })); + const contentRoutes = (prefix, file, include = () => true) => + mdxPaths(join(contentDirectory, prefix)) + .map((path) => `${prefix}/${path}`) + .filter(include) + .map((path) => route(path, file, { id: path })); + + return { + stdlibPaths, + routes: [ + layout("./layouts/HomepageLayoutRoute.jsx", [ + index("./routes/LandingPageRoute.jsx"), + ]), + route("try", "./routes/TryRoute.jsx"), + layout("./layouts/ContentLayoutRoute.jsx", [ + route("packages", "./routes/PackagesRoute.jsx"), + route("brand", "./routes/BrandRoute.jsx"), + route("blog", "./routes/BlogRoute.jsx", { id: "blog-index" }), + route("blog/archived", "./routes/BlogRoute.jsx", { + id: "blog-archived", + }), + ...contentRoutes("blog", "./routes/BlogArticleRoute.jsx"), + ...contentRoutes("community", "./routes/CommunityRoute.jsx"), + route("docs", "./routes/DocsOverview.jsx", { id: "docs-overview" }), + layout("./layouts/DocsLayoutRoute.jsx", [ + route("docs/manual/api", "./routes/ApiOverviewRoute.jsx", { + id: "api-overview", + }), + route("docs/manual/api/stdlib", "./routes/ApiRoute.jsx", { + id: "api-stdlib", + }), + route("docs/manual/api/introduction", "./routes/ApiRoute.jsx", { + id: "api-intro", + }), + route("docs/manual/api/belt", "./routes/ApiRoute.jsx", { + id: "api-belt", + }), + route("docs/manual/api/dom", "./routes/ApiRoute.jsx", { + id: "api-dom", + }), + ...apiRoutes(stdlibPaths), + ...apiRoutes(beltPaths), + ...apiRoutes(domPaths), + ...contentRoutes( + "docs/manual", + "./routes/DocsManualRoute.jsx", + (path) => + path !== "docs/manual/api" && + !path.startsWith("docs/manual/api/"), + ), + ...contentRoutes("docs/guides", "./routes/DocsGuidesRoute.jsx"), + ...contentRoutes("docs/react", "./routes/DocsReactRoute.jsx"), + route("syntax-lookup", "./routes/SyntaxLookupRoute.jsx", { + id: "syntax-lookup", + }), + ...contentRoutes( + "syntax-lookup", + "./routes/SyntaxLookupDetailRoute.jsx", + ), + ]), + ]), + route("*", "./routes/NotFoundRoute.jsx"), + ], + }; +} diff --git a/apps/docs/scripts/__tests__/generate-route-types.test.mjs b/apps/docs/scripts/__tests__/generate-route-types.test.mjs new file mode 100644 index 000000000..640db3382 --- /dev/null +++ b/apps/docs/scripts/__tests__/generate-route-types.test.mjs @@ -0,0 +1,185 @@ +import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; +import { + mkdtempSync, + mkdirSync, + readFileSync, + rmSync, + writeFileSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; +import routes from "../../app/routes.js"; +import { loadRoutes } from "../../route-config.mjs"; +import { + concretePaths, + generateRouteTypes, + outputPath, + renderPathType, +} from "../../generate-route-types.mjs"; + +function temporaryDirectory(t) { + const directory = mkdtempSync(join(tmpdir(), "rescript-routes-")); + t.after(() => rmSync(directory, { recursive: true, force: true })); + return directory; +} + +test("collects nested routes and index pages through pathless layouts", () => { + assert.deepEqual( + concretePaths([ + { + children: [ + { index: true }, + { + path: "docs", + children: [ + { index: true }, + { children: [{ path: "manual", children: [{ path: "intro" }] }] }, + { path: "/brand" }, + ], + }, + ], + }, + { path: "docs/manual/intro" }, + ]), + ["/", "/brand", "/docs", "/docs/manual", "/docs/manual/intro"], + ); +}); + +test("excludes catch-all and parameter patterns, including their descendants", () => { + assert.deepEqual( + concretePaths([ + { index: true }, + { path: "*" }, + { path: "files/*" }, + { path: "blog/:slug", children: [{ path: "edit" }] }, + { path: ":lang?/docs" }, + { path: "docs?" }, + { children: [] }, + ]), + ["/"], + ); +}); + +test("the committed type matches the actual React Router configuration", () => { + assert.equal(readFileSync(outputPath, "utf8"), renderPathType(routes)); + const paths = concretePaths(routes); + for (const path of [ + "/try", + "/brand", + "/packages", + "/syntax-lookup", + "/docs/manual/api", + "/docs/manual/migrate-to-v12", + "/docs/manual/rescript-for-javascript-developers", + "/docs/manual/api/dom/storage", + "/blog/retreat-recap-2026", + ]) + assert.ok(paths.includes(path), `Missing route: ${path}`); + for (const path of [ + "/undefined", + "/*", + "/syntax-lookup/decorator_obj", + "/syntax-lookup/operators_triangle_pipe", + ]) + assert.ok(!paths.includes(path), `Stale route: ${path}`); +}); + +test("discovers added and removed MDX pages and API modules", (t) => { + const contentDirectory = temporaryDirectory(t); + for (const prefix of [ + "blog", + "community", + "docs/manual/api", + "docs/react", + "docs/guides", + "syntax-lookup", + "docs/api", + ]) + mkdirSync(join(contentDirectory, prefix), { recursive: true }); + for (const library of ["stdlib", "dom", "belt"]) + writeFileSync( + join(contentDirectory, "docs/api", `${library}.json`), + JSON.stringify({ [library]: {}, [`${library}/example`]: {} }), + ); + mkdirSync(join(contentDirectory, "blog/archived")); + writeFileSync( + join(contentDirectory, "blog/archived/old-post.mdx"), + "# Old post", + ); + writeFileSync(join(contentDirectory, "blog/ignored.txt"), "Ignored"); + writeFileSync( + join(contentDirectory, "docs/manual/api/ignored.mdx"), + "Ignored", + ); + writeFileSync( + join(contentDirectory, "docs/manual/api-extra.mdx"), + "# API extra", + ); + const options = { + contentDirectory, + apiDirectory: join(contentDirectory, "docs/api"), + }; + const before = loadRoutes(options); + const beforePaths = concretePaths(before.routes); + assert.ok(beforePaths.includes("/blog/archived/old-post")); + assert.ok(beforePaths.includes("/docs/manual/api-extra")); + assert.ok(beforePaths.includes("/docs/manual/api/dom/example")); + assert.ok(!beforePaths.includes("/blog/ignored")); + assert.ok(!beforePaths.includes("/docs/manual/api/ignored")); + assert.deepEqual(before.stdlibPaths, ["docs/manual/api/stdlib/example"]); + + rmSync(join(contentDirectory, "blog/archived/old-post.mdx")); + writeFileSync(join(contentDirectory, "blog/new-post.mdx"), "# New post"); + writeFileSync( + join(contentDirectory, "docs/api/stdlib.json"), + JSON.stringify({ stdlib: {}, "stdlib/newmodule": {} }), + ); + const after = concretePaths(loadRoutes(options).routes); + assert.ok(!after.includes("/blog/archived/old-post")); + assert.ok(after.includes("/blog/new-post")); + assert.ok(!after.includes("/docs/manual/api/stdlib/example")); + assert.ok(after.includes("/docs/manual/api/stdlib/newmodule")); +}); + +test("check mode detects missing and stale files without writing them", (t) => { + const destination = join(temporaryDirectory(t), "Path.res"); + const routeConfig = [{ index: true }, { path: "new-route" }]; + const options = { routes: routeConfig, destination }; + assert.equal(generateRouteTypes({ ...options, check: true }), false); + assert.throws(() => readFileSync(destination), { code: "ENOENT" }); + assert.equal(generateRouteTypes(options), true); + assert.equal(generateRouteTypes({ ...options, check: true }), true); + const source = readFileSync(destination, "utf8"); + assert.match(source, /#"\/new-route"/); + writeFileSync(destination, source + "// stale\n"); + assert.equal(generateRouteTypes({ ...options, check: true }), false); + assert.equal(readFileSync(destination, "utf8"), source + "// stale\n"); + assert.equal(generateRouteTypes(options), true); + assert.equal(readFileSync(destination, "utf8"), source); +}); + +test("generation is deterministic and quotes path literals", () => { + const first = [ + { path: "z" }, + { path: 'a"quoted' }, + { index: true }, + { path: "z" }, + ]; + assert.equal(renderPathType(first), renderPathType([...first].reverse())); + assert.ok(renderPathType(first).includes('#"/a\\"quoted"')); + assert.throws(() => renderPathType([{ path: "*" }]), /No concrete routes/); +}); + +test("the standalone check runs from an unrelated working directory", (t) => { + const before = readFileSync(outputPath, "utf8"); + const generator = fileURLToPath( + new URL("../../generate-route-types.mjs", import.meta.url), + ); + execFileSync(process.execPath, [generator, "--check"], { + cwd: temporaryDirectory(t), + }); + assert.equal(readFileSync(outputPath, "utf8"), before); +}); diff --git a/apps/docs/src/markdown/MdxFile.res b/apps/docs/src/markdown/MdxFile.res index 0ad68358c..227b1f715 100644 --- a/apps/docs/src/markdown/MdxFile.res +++ b/apps/docs/src/markdown/MdxFile.res @@ -83,12 +83,6 @@ let rec scanDir = (baseDir, currentDir) => { }) } -let scanPaths = (~dir, ~alias) => { - scanDir(dir, dir)->Array.map(relativePath => { - alias ++ "/" ++ relativePath - }) -} - // Convert frontmatter JSON dict to Mdx.attributes // This is the same unsafe approach as react-router-mdx — frontmatter YAML // becomes a JS object that we type as Mdx.attributes. Fields not present diff --git a/apps/docs/src/markdown/MdxFile.resi b/apps/docs/src/markdown/MdxFile.resi index 60b5b815e..308a6cf9c 100644 --- a/apps/docs/src/markdown/MdxFile.resi +++ b/apps/docs/src/markdown/MdxFile.resi @@ -15,12 +15,6 @@ let resolveFilePath: (string, ~dir: string, ~alias: string) => string /** Read a file from disk and parse its frontmatter using MarkdownParser. */ let loadFile: string => promise -/** Scan a directory recursively for .mdx files and return URL paths. - * e.g. scanPaths(~dir="markdown-pages/blog", ~alias="blog") - * → ["blog/release-12-0-0", "blog/archived/some-post", ...] - */ -let scanPaths: (~dir: string, ~alias: string) => array - /** Compile raw MDX content into a function-body string using @mdx-js/mdx. */ let compileMdx: ( string, diff --git a/apps/guide/package.json b/apps/guide/package.json index eda0193b4..24ce6ffa7 100644 --- a/apps/guide/package.json +++ b/apps/guide/package.json @@ -5,7 +5,7 @@ "license": "MIT", "type": "module", "scripts": { - "build:res": "rescript build --warn-error +3+8+11+12+26+27+31+32+33+34+35+39+44+45+110", + "build:res": "node ../docs/generate-route-types.mjs && rescript build --warn-error +3+8+11+12+26+27+31+32+33+34+35+39+44+45+110", "build:vite": "react-router build", "build": "yarn build:res && yarn build:vite", "ci:test": "yarn build:res && yarn vitest --run --browser.headless", diff --git a/package.json b/package.json index b54140105..6ce11cbc9 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,9 @@ "build:scripts": "yarn workspace @rescript-lang/docs build:scripts", "build:generate-llms": "yarn workspace @rescript-lang/docs build:generate-llms", "build:guide": "yarn workspace @rescript-lang/guide build", - "build:res": "rescript build --warn-error +3+8+11+12+26+27+31+32+33+34+35+39+44+45+110", + "build:routes": "node apps/docs/generate-route-types.mjs", + "check:routes": "node apps/docs/generate-route-types.mjs --check", + "build:res": "yarn build:routes && rescript build --warn-error +3+8+11+12+26+27+31+32+33+34+35+39+44+45+110", "build:sync-bundles": "yarn workspace @rescript-lang/docs build:sync-bundles", "build:update-index": "yarn workspace @rescript-lang/docs build:update-index", "build:vite": "yarn workspace @rescript-lang/docs build:vite", @@ -25,7 +27,7 @@ "ci:test:visual": "yarn workspace @rescript-lang/docs ci:test:visual", "clean:res": "rescript clean", "convert-images": "yarn workspace @rescript-lang/docs convert-images", - "dev:res": "rescript watch", + "dev:res": "yarn build:routes && rescript watch", "dev:guide": "yarn workspace @rescript-lang/guide dev:vite", "dev:vite": "yarn workspace @rescript-lang/docs dev:vite", "dev:wrangler": "yarn workspace @rescript-lang/docs dev:wrangler", diff --git a/packages/shared/src/Path.res b/packages/shared/src/Path.res index cd26075e6..45d4197e6 100644 --- a/packages/shared/src/Path.res +++ b/packages/shared/src/Path.res @@ -1,453 +1,453 @@ +// Generated by apps/docs/generate-route-types.mjs. Do not edit. +// Run yarn build:routes after changing routes, MDX pages, or API data. type t = [ | #"/" - | #"/undefined" - | #"/packages" - | #"/try" - | #"/brand" - | #"/syntax-lookup" | #"/blog" | #"/blog/archived" + | #"/blog/archived/a-note-on-bucklescripts-future-commitments" + | #"/blog/archived/a-small-step-for-bucklescript" + | #"/blog/archived/a-story-of-exception-encoding" + | #"/blog/archived/a-story-of-lazy-encoding" + | #"/blog/archived/another-encoding" + | #"/blog/archived/arity-zero" + | #"/blog/archived/bucklescript-8-1-new-syntax" + | #"/blog/archived/bucklescript-release-1-0" + | #"/blog/archived/bucklescript-release-1-4-2" + | #"/blog/archived/bucklescript-release-1-4-3" + | #"/blog/archived/bucklescript-release-1-5-0" + | #"/blog/archived/bucklescript-release-1-5-1" + | #"/blog/archived/bucklescript-release-1-5-2" + | #"/blog/archived/bucklescript-release-1-7-0" + | #"/blog/archived/bucklescript-release-1-7-4" + | #"/blog/archived/bucklescript-release-1-7-5" + | #"/blog/archived/bucklescript-release-3-0-0" + | #"/blog/archived/bucklescript-release-3-1-0" + | #"/blog/archived/bucklescript-release-3-1-4" + | #"/blog/archived/bucklescript-release-4-0-0-pt1" + | #"/blog/archived/bucklescript-release-4-0-0-pt2" + | #"/blog/archived/bucklescript-release-4-0-17" + | #"/blog/archived/bucklescript-release-4-0-8" + | #"/blog/archived/bucklescript-release-5-0" + | #"/blog/archived/bucklescript-release-5-0-1" + | #"/blog/archived/bucklescript-release-5-0-4" + | #"/blog/archived/bucklescript-release-5-0-5" + | #"/blog/archived/bucklescript-release-5-1-0" + | #"/blog/archived/bucklescript-release-5-2-0" + | #"/blog/archived/bucklescript-release-6-0" + | #"/blog/archived/bucklescript-release-7-0-2" + | #"/blog/archived/bucklescript-release-7-1-0" + | #"/blog/archived/bucklescript-release-7-2" + | #"/blog/archived/bucklescript-release-7-3" + | #"/blog/archived/bucklescript-release-8-1-1" + | #"/blog/archived/bucklescript-release-8-2" + | #"/blog/archived/bucklescript-roadmap-q3-4-2018" + | #"/blog/archived/feature-preview-variadic" + | #"/blog/archived/ffi-overview" + | #"/blog/archived/generalize-uncurry" + | #"/blog/archived/loading-stdlib-in-memory" + | #"/blog/archived/overview-of-new_encoding" + | #"/blog/archived/scalable" + | #"/blog/archived/state-of-reasonml-org-2020-q2-pt1" + | #"/blog/archived/state-of-reasonml-org-2020-q2-pt2" + | #"/blog/archived/state-of-reasonml-org-2020-q2-pt3" + | #"/blog/archived/state-of-reasonml-org-2020-q2-pt4" + | #"/blog/archived/string-literal-types-in-reason" + | #"/blog/archived/union-types-in-bucklescript" + | #"/blog/archived/whats-new-in-7-pt1" + | #"/blog/archived/whats-new-in-7-pt2" + | #"/blog/bucklescript-is-rebranding" + | #"/blog/editor-support-custom-operators-and-more" + | #"/blog/editor-support-release-1-0" + | #"/blog/enhanced-ergonomics-for-record-types" + | #"/blog/first-class-dynamic-import-support" + | #"/blog/improving-interop" + | #"/blog/introducing-unified-operators" + | #"/blog/new-rescript-logo" + | #"/blog/reactive-analysis" + | #"/blog/reforging-build-system" + | #"/blog/release-10-0-0" + | #"/blog/release-10-1" + | #"/blog/release-11-0-0" + | #"/blog/release-11-1-0" + | #"/blog/release-12-0-0" + | #"/blog/release-8-3" + | #"/blog/release-8-3-2" + | #"/blog/release-8-4" + | #"/blog/release-9-0" + | #"/blog/release-9-1" + | #"/blog/rescript-association-rebranding" + | #"/blog/retreat-recap-2026" + | #"/blog/retreats" + | #"/blog/roadmap-2021-and-new-landing-page" + | #"/blog/uncurried-mode" + | #"/blog/what-can-i-do-with-rescript" + | #"/brand" + | #"/community/code-of-conduct" + | #"/community/content" + | #"/community/overview" + | #"/community/roadmap" + | #"/community/translations" | #"/docs" - | #"/docs/manual/api/stdlib" - | #"/docs/manual/api/introduction" + | #"/docs/guides/overview" + | #"/docs/guides/publishing-packages" + | #"/docs/manual/api" | #"/docs/manual/api/belt" - | #"/docs/manual/api/dom" - | #"/docs/manual/api/stdlib/intl/numberformat/grouping" - | #"/docs/manual/api/stdlib/biguint64array/constants" - | #"/docs/manual/api/stdlib/bigint64array/constants" - | #"/docs/manual/api/stdlib/uint8clampedarray/constants" - | #"/docs/manual/api/stdlib/uint32array/constants" - | #"/docs/manual/api/stdlib/uint16array/constants" - | #"/docs/manual/api/stdlib/uint8array/constants" - | #"/docs/manual/api/stdlib/int32array/constants" - | #"/docs/manual/api/stdlib/int16array/constants" - | #"/docs/manual/api/stdlib/int8array/constants" - | #"/docs/manual/api/stdlib/float64array/constants" - | #"/docs/manual/api/stdlib/float32array/constants" - | #"/docs/manual/api/stdlib/type/classify" - | #"/docs/manual/api/stdlib/regexp/result" - | #"/docs/manual/api/stdlib/math/int" - | #"/docs/manual/api/stdlib/math/constants" - | #"/docs/manual/api/stdlib/json/decode" - | #"/docs/manual/api/stdlib/json/encode" - | #"/docs/manual/api/stdlib/json/classify" - | #"/docs/manual/api/stdlib/jserror/urierror" - | #"/docs/manual/api/stdlib/jserror/typeerror" - | #"/docs/manual/api/stdlib/jserror/syntaxerror" - | #"/docs/manual/api/stdlib/jserror/referenceerror" - | #"/docs/manual/api/stdlib/jserror/rangeerror" - | #"/docs/manual/api/stdlib/jserror/evalerror" - | #"/docs/manual/api/stdlib/intl/segments" - | #"/docs/manual/api/stdlib/intl/segmenter" - | #"/docs/manual/api/stdlib/intl/relativetimeformat" - | #"/docs/manual/api/stdlib/intl/pluralrules" - | #"/docs/manual/api/stdlib/intl/numberformat" - | #"/docs/manual/api/stdlib/intl/locale" - | #"/docs/manual/api/stdlib/intl/listformat" - | #"/docs/manual/api/stdlib/intl/datetimeformat" - | #"/docs/manual/api/stdlib/intl/collator" - | #"/docs/manual/api/stdlib/intl/common" - | #"/docs/manual/api/stdlib/int/ref" - | #"/docs/manual/api/stdlib/int/bitwise" - | #"/docs/manual/api/stdlib/int/constants" - | #"/docs/manual/api/stdlib/float/constants" - | #"/docs/manual/api/stdlib/error/urierror" - | #"/docs/manual/api/stdlib/error/typeerror" - | #"/docs/manual/api/stdlib/error/syntaxerror" - | #"/docs/manual/api/stdlib/error/referenceerror" - | #"/docs/manual/api/stdlib/error/rangeerror" - | #"/docs/manual/api/stdlib/error/evalerror" - | #"/docs/manual/api/stdlib/date/utc" - | #"/docs/manual/api/stdlib/biguint64array" - | #"/docs/manual/api/stdlib/bigint64array" - | #"/docs/manual/api/stdlib/uint8clampedarray" - | #"/docs/manual/api/stdlib/uint32array" - | #"/docs/manual/api/stdlib/uint16array" - | #"/docs/manual/api/stdlib/uint8array" - | #"/docs/manual/api/stdlib/int32array" - | #"/docs/manual/api/stdlib/int16array" - | #"/docs/manual/api/stdlib/int8array" - | #"/docs/manual/api/stdlib/float64array" - | #"/docs/manual/api/stdlib/float32array" - | #"/docs/manual/api/stdlib/typedarray" - | #"/docs/manual/api/stdlib/arraybuffer" - | #"/docs/manual/api/stdlib/weakset" - | #"/docs/manual/api/stdlib/set" - | #"/docs/manual/api/stdlib/weakmap" - | #"/docs/manual/api/stdlib/map" - | #"/docs/manual/api/stdlib/asynciterator" - | #"/docs/manual/api/stdlib/iterator" - | #"/docs/manual/api/stdlib/type" - | #"/docs/manual/api/stdlib/symbol" - | #"/docs/manual/api/stdlib/string" - | #"/docs/manual/api/stdlib/result" - | #"/docs/manual/api/stdlib/regexp" - | #"/docs/manual/api/stdlib/promise" - | #"/docs/manual/api/stdlib/pair" - | #"/docs/manual/api/stdlib/ordering" - | #"/docs/manual/api/stdlib/option" - | #"/docs/manual/api/stdlib/object" - | #"/docs/manual/api/stdlib/nullable" - | #"/docs/manual/api/stdlib/null" - | #"/docs/manual/api/stdlib/math" - | #"/docs/manual/api/stdlib/list" - | #"/docs/manual/api/stdlib/lazy" - | #"/docs/manual/api/stdlib/json" - | #"/docs/manual/api/stdlib/jsexn" - | #"/docs/manual/api/stdlib/jserror" - | #"/docs/manual/api/stdlib/intl" - | #"/docs/manual/api/stdlib/int" - | #"/docs/manual/api/stdlib/float" - | #"/docs/manual/api/stdlib/error" - | #"/docs/manual/api/stdlib/exn" - | #"/docs/manual/api/stdlib/dict" - | #"/docs/manual/api/stdlib/date" - | #"/docs/manual/api/stdlib/dataview" - | #"/docs/manual/api/stdlib/console" - | #"/docs/manual/api/stdlib/bool" - | #"/docs/manual/api/stdlib/bigint" - | #"/docs/manual/api/stdlib/array" - | #"/docs/manual/api/stdlib/intervalid" - | #"/docs/manual/api/stdlib/timeoutid" - | #"/docs/manual/api/belt/hashmap/string" + | #"/docs/manual/api/belt/array" + | #"/docs/manual/api/belt/float" + | #"/docs/manual/api/belt/hashmap" | #"/docs/manual/api/belt/hashmap/int" - | #"/docs/manual/api/belt/hashset/string" + | #"/docs/manual/api/belt/hashmap/string" + | #"/docs/manual/api/belt/hashset" | #"/docs/manual/api/belt/hashset/int" - | #"/docs/manual/api/belt/mutablemap/string" - | #"/docs/manual/api/belt/mutablemap/int" - | #"/docs/manual/api/belt/mutableset/string" - | #"/docs/manual/api/belt/mutableset/int" - | #"/docs/manual/api/belt/map/dict" - | #"/docs/manual/api/belt/map/string" - | #"/docs/manual/api/belt/map/int" - | #"/docs/manual/api/belt/set/dict" - | #"/docs/manual/api/belt/set/string" - | #"/docs/manual/api/belt/set/int" - | #"/docs/manual/api/belt/sortarray/string" - | #"/docs/manual/api/belt/sortarray/int" - | #"/docs/manual/api/belt/id/makehashable" - | #"/docs/manual/api/belt/id/makehashableu" + | #"/docs/manual/api/belt/hashset/string" + | #"/docs/manual/api/belt/id" | #"/docs/manual/api/belt/id/makecomparable" | #"/docs/manual/api/belt/id/makecomparableu" - | #"/docs/manual/api/belt/float" + | #"/docs/manual/api/belt/id/makehashable" + | #"/docs/manual/api/belt/id/makehashableu" | #"/docs/manual/api/belt/int" - | #"/docs/manual/api/belt/result" - | #"/docs/manual/api/belt/option" - | #"/docs/manual/api/belt/hashmap" - | #"/docs/manual/api/belt/hashset" + | #"/docs/manual/api/belt/list" + | #"/docs/manual/api/belt/map" + | #"/docs/manual/api/belt/map/dict" + | #"/docs/manual/api/belt/map/int" + | #"/docs/manual/api/belt/map/string" | #"/docs/manual/api/belt/mutablemap" + | #"/docs/manual/api/belt/mutablemap/int" + | #"/docs/manual/api/belt/mutablemap/string" + | #"/docs/manual/api/belt/mutablequeue" | #"/docs/manual/api/belt/mutableset" - | #"/docs/manual/api/belt/map" - | #"/docs/manual/api/belt/set" - | #"/docs/manual/api/belt/range" - | #"/docs/manual/api/belt/list" + | #"/docs/manual/api/belt/mutableset/int" + | #"/docs/manual/api/belt/mutableset/string" | #"/docs/manual/api/belt/mutablestack" - | #"/docs/manual/api/belt/mutablequeue" + | #"/docs/manual/api/belt/option" + | #"/docs/manual/api/belt/range" + | #"/docs/manual/api/belt/result" + | #"/docs/manual/api/belt/set" + | #"/docs/manual/api/belt/set/dict" + | #"/docs/manual/api/belt/set/int" + | #"/docs/manual/api/belt/set/string" | #"/docs/manual/api/belt/sortarray" - | #"/docs/manual/api/belt/array" - | #"/docs/manual/api/belt/id" - | #"/blog/what-can-i-do-with-rescript" - | #"/blog/uncurried-mode" - | #"/blog/roadmap-2021-and-new-landing-page" - | #"/blog/retreats" - | #"/blog/rescript-association-rebranding" - | #"/blog/release-9-1" - | #"/blog/release-9-0" - | #"/blog/release-8-4" - | #"/blog/release-8-3" - | #"/blog/release-8-3-2" - | #"/blog/release-12-0-0" - | #"/blog/release-11-1-0" - | #"/blog/release-11-0-0" - | #"/blog/release-10-1" - | #"/blog/release-10-0-0" - | #"/blog/reforging-build-system" - | #"/blog/new-rescript-logo" - | #"/blog/introducing-unified-operators" - | #"/blog/improving-interop" - | #"/blog/first-class-dynamic-import-support" - | #"/blog/enhanced-ergonomics-for-record-types" - | #"/blog/editor-support-release-1-0" - | #"/blog/editor-support-custom-operators-and-more" - | #"/blog/bucklescript-is-rebranding" - | #"/blog/archived/whats-new-in-7-pt2" - | #"/blog/archived/whats-new-in-7-pt1" - | #"/blog/archived/union-types-in-bucklescript" - | #"/blog/archived/string-literal-types-in-reason" - | #"/blog/archived/state-of-reasonml-org-2020-q2-pt4" - | #"/blog/archived/state-of-reasonml-org-2020-q2-pt3" - | #"/blog/archived/state-of-reasonml-org-2020-q2-pt2" - | #"/blog/archived/state-of-reasonml-org-2020-q2-pt1" - | #"/blog/archived/scalable" - | #"/blog/archived/overview-of-new_encoding" - | #"/blog/archived/loading-stdlib-in-memory" - | #"/blog/archived/generalize-uncurry" - | #"/blog/archived/ffi-overview" - | #"/blog/archived/feature-preview-variadic" - | #"/blog/archived/bucklescript-roadmap-q3-4-2018" - | #"/blog/archived/bucklescript-release-8-2" - | #"/blog/archived/bucklescript-release-8-1-1" - | #"/blog/archived/bucklescript-release-7-3" - | #"/blog/archived/bucklescript-release-7-2" - | #"/blog/archived/bucklescript-release-7-1-0" - | #"/blog/archived/bucklescript-release-7-0-2" - | #"/blog/archived/bucklescript-release-6-0" - | #"/blog/archived/bucklescript-release-5-2-0" - | #"/blog/archived/bucklescript-release-5-1-0" - | #"/blog/archived/bucklescript-release-5-0" - | #"/blog/archived/bucklescript-release-5-0-5" - | #"/blog/archived/bucklescript-release-5-0-4" - | #"/blog/archived/bucklescript-release-5-0-1" - | #"/blog/archived/bucklescript-release-4-0-8" - | #"/blog/archived/bucklescript-release-4-0-17" - | #"/blog/archived/bucklescript-release-4-0-0-pt2" - | #"/blog/archived/bucklescript-release-4-0-0-pt1" - | #"/blog/archived/bucklescript-release-3-1-4" - | #"/blog/archived/bucklescript-release-3-1-0" - | #"/blog/archived/bucklescript-release-3-0-0" - | #"/blog/archived/bucklescript-release-1-7-5" - | #"/blog/archived/bucklescript-release-1-7-4" - | #"/blog/archived/bucklescript-release-1-7-0" - | #"/blog/archived/bucklescript-release-1-5-2" - | #"/blog/archived/bucklescript-release-1-5-1" - | #"/blog/archived/bucklescript-release-1-5-0" - | #"/blog/archived/bucklescript-release-1-4-3" - | #"/blog/archived/bucklescript-release-1-4-2" - | #"/blog/archived/bucklescript-release-1-0" - | #"/blog/archived/bucklescript-8-1-new-syntax" - | #"/blog/archived/arity-zero" - | #"/blog/archived/another-encoding" - | #"/blog/archived/a-story-of-lazy-encoding" - | #"/blog/archived/a-story-of-exception-encoding" - | #"/blog/archived/a-small-step-for-bucklescript" - | #"/blog/archived/a-note-on-bucklescripts-future-commitments" - | #"/docs/react/styling" - | #"/docs/react/server-components" - | #"/docs/react/router" - | #"/docs/react/rendering-elements" - | #"/docs/react/refs-and-the-dom" - | #"/docs/react/memo" - | #"/docs/react/llms" - | #"/docs/react/lazy-components" - | #"/docs/react/introduction" - | #"/docs/react/installation" - | #"/docs/react/import-export-reactjs" - | #"/docs/react/hooks-state" - | #"/docs/react/hooks-ref" - | #"/docs/react/hooks-reducer" - | #"/docs/react/hooks-overview" - | #"/docs/react/hooks-effect" - | #"/docs/react/hooks-custom" - | #"/docs/react/hooks-context" - | #"/docs/react/forwarding-refs" - | #"/docs/react/extensions-of-props" - | #"/docs/react/events" - | #"/docs/react/elements-and-jsx" - | #"/docs/react/context" - | #"/docs/react/components-and-props" - | #"/docs/react/beyond-jsx" - | #"/docs/react/arrays-and-keys" - | #"/docs/manual/warning-numbers" - | #"/docs/manual/variant" - | #"/docs/manual/use-illegal-identifier-names" - | #"/docs/manual/typescript-integration" - | #"/docs/manual/type" - | #"/docs/manual/tuple" - | #"/docs/manual/try" - | #"/docs/manual/tagged-templates" - | #"/docs/manual/shared-data-types" - | #"/docs/manual/scoped-polymorphic-types" - | #"/docs/manual/reserved-keywords" - | #"/docs/manual/record" - | #"/docs/manual/promise" - | #"/docs/manual/project-structure" - | #"/docs/manual/primitive-types" - | #"/docs/manual/polymorphic-variant" - | #"/docs/manual/pipe" - | #"/docs/manual/pattern-matching-destructuring" - | #"/docs/manual/overview" - | #"/docs/manual/object" - | #"/docs/manual/null-undefined-option" - | #"/docs/manual/mutation" - | #"/docs/manual/module" - | #"/docs/manual/module-functions" - | #"/docs/manual/migrate-to-v11" - | #"/docs/manual/llms" - | #"/docs/manual/libraries" - | #"/docs/manual/let-binding" - | #"/docs/manual/lazy-values" - | #"/docs/manual/jsx" - | #"/docs/manual/json" - | #"/docs/manual/introduction" - | #"/docs/manual/interop-with-js-build-systems" - | #"/docs/manual/interop-cheatsheet" - | #"/docs/manual/installation" - | #"/docs/manual/inlining-constants" - | #"/docs/manual/import-from-export-to-js" - | #"/docs/manual/import-export" - | #"/docs/manual/generate-converters-accessors" - | #"/docs/manual/generalized-algebraic-data-types" - | #"/docs/manual/function" - | #"/docs/manual/external" - | #"/docs/manual/extensible-variant" - | #"/docs/manual/exception" - | #"/docs/manual/equality-comparison" - | #"/docs/manual/embed-raw-javascript" - | #"/docs/manual/editor-plugins" - | #"/docs/manual/editor-code-analysis" - | #"/docs/manual/dict" - | #"/docs/manual/converting-from-js" - | #"/docs/manual/control-flow" - | #"/docs/manual/build-performance" - | #"/docs/manual/build-overview" - | #"/docs/manual/build-monorepo-setup" + | #"/docs/manual/api/belt/sortarray/int" + | #"/docs/manual/api/belt/sortarray/string" + | #"/docs/manual/api/dom" + | #"/docs/manual/api/dom/storage" + | #"/docs/manual/api/dom/storage2" + | #"/docs/manual/api/introduction" + | #"/docs/manual/api/stdlib" + | #"/docs/manual/api/stdlib/array" + | #"/docs/manual/api/stdlib/arraybuffer" + | #"/docs/manual/api/stdlib/asynciterator" + | #"/docs/manual/api/stdlib/bigint" + | #"/docs/manual/api/stdlib/bigint64array" + | #"/docs/manual/api/stdlib/bigint64array/constants" + | #"/docs/manual/api/stdlib/biguint64array" + | #"/docs/manual/api/stdlib/biguint64array/constants" + | #"/docs/manual/api/stdlib/bool" + | #"/docs/manual/api/stdlib/console" + | #"/docs/manual/api/stdlib/dataview" + | #"/docs/manual/api/stdlib/date" + | #"/docs/manual/api/stdlib/date/utc" + | #"/docs/manual/api/stdlib/dict" + | #"/docs/manual/api/stdlib/error" + | #"/docs/manual/api/stdlib/error/evalerror" + | #"/docs/manual/api/stdlib/error/rangeerror" + | #"/docs/manual/api/stdlib/error/referenceerror" + | #"/docs/manual/api/stdlib/error/syntaxerror" + | #"/docs/manual/api/stdlib/error/typeerror" + | #"/docs/manual/api/stdlib/error/urierror" + | #"/docs/manual/api/stdlib/exn" + | #"/docs/manual/api/stdlib/float" + | #"/docs/manual/api/stdlib/float/constants" + | #"/docs/manual/api/stdlib/float32array" + | #"/docs/manual/api/stdlib/float32array/constants" + | #"/docs/manual/api/stdlib/float64array" + | #"/docs/manual/api/stdlib/float64array/constants" + | #"/docs/manual/api/stdlib/int" + | #"/docs/manual/api/stdlib/int/bitwise" + | #"/docs/manual/api/stdlib/int/constants" + | #"/docs/manual/api/stdlib/int/ref" + | #"/docs/manual/api/stdlib/int16array" + | #"/docs/manual/api/stdlib/int16array/constants" + | #"/docs/manual/api/stdlib/int32array" + | #"/docs/manual/api/stdlib/int32array/constants" + | #"/docs/manual/api/stdlib/int8array" + | #"/docs/manual/api/stdlib/int8array/constants" + | #"/docs/manual/api/stdlib/intervalid" + | #"/docs/manual/api/stdlib/intl" + | #"/docs/manual/api/stdlib/intl/collator" + | #"/docs/manual/api/stdlib/intl/common" + | #"/docs/manual/api/stdlib/intl/datetimeformat" + | #"/docs/manual/api/stdlib/intl/listformat" + | #"/docs/manual/api/stdlib/intl/locale" + | #"/docs/manual/api/stdlib/intl/numberformat" + | #"/docs/manual/api/stdlib/intl/numberformat/grouping" + | #"/docs/manual/api/stdlib/intl/pluralrules" + | #"/docs/manual/api/stdlib/intl/relativetimeformat" + | #"/docs/manual/api/stdlib/intl/segmenter" + | #"/docs/manual/api/stdlib/intl/segments" + | #"/docs/manual/api/stdlib/iterator" + | #"/docs/manual/api/stdlib/jserror" + | #"/docs/manual/api/stdlib/jserror/evalerror" + | #"/docs/manual/api/stdlib/jserror/rangeerror" + | #"/docs/manual/api/stdlib/jserror/referenceerror" + | #"/docs/manual/api/stdlib/jserror/syntaxerror" + | #"/docs/manual/api/stdlib/jserror/typeerror" + | #"/docs/manual/api/stdlib/jserror/urierror" + | #"/docs/manual/api/stdlib/jsexn" + | #"/docs/manual/api/stdlib/json" + | #"/docs/manual/api/stdlib/json/classify" + | #"/docs/manual/api/stdlib/json/decode" + | #"/docs/manual/api/stdlib/json/encode" + | #"/docs/manual/api/stdlib/lazy" + | #"/docs/manual/api/stdlib/list" + | #"/docs/manual/api/stdlib/map" + | #"/docs/manual/api/stdlib/math" + | #"/docs/manual/api/stdlib/math/constants" + | #"/docs/manual/api/stdlib/math/int" + | #"/docs/manual/api/stdlib/null" + | #"/docs/manual/api/stdlib/nullable" + | #"/docs/manual/api/stdlib/object" + | #"/docs/manual/api/stdlib/option" + | #"/docs/manual/api/stdlib/ordering" + | #"/docs/manual/api/stdlib/pair" + | #"/docs/manual/api/stdlib/promise" + | #"/docs/manual/api/stdlib/regexp" + | #"/docs/manual/api/stdlib/regexp/result" + | #"/docs/manual/api/stdlib/result" + | #"/docs/manual/api/stdlib/set" + | #"/docs/manual/api/stdlib/string" + | #"/docs/manual/api/stdlib/symbol" + | #"/docs/manual/api/stdlib/timeoutid" + | #"/docs/manual/api/stdlib/type" + | #"/docs/manual/api/stdlib/type/classify" + | #"/docs/manual/api/stdlib/typedarray" + | #"/docs/manual/api/stdlib/uint16array" + | #"/docs/manual/api/stdlib/uint16array/constants" + | #"/docs/manual/api/stdlib/uint32array" + | #"/docs/manual/api/stdlib/uint32array/constants" + | #"/docs/manual/api/stdlib/uint8array" + | #"/docs/manual/api/stdlib/uint8array/constants" + | #"/docs/manual/api/stdlib/uint8clampedarray" + | #"/docs/manual/api/stdlib/uint8clampedarray/constants" + | #"/docs/manual/api/stdlib/weakmap" + | #"/docs/manual/api/stdlib/weakset" + | #"/docs/manual/array-and-list" + | #"/docs/manual/async-await" + | #"/docs/manual/attribute" + | #"/docs/manual/bind-to-global-js-values" + | #"/docs/manual/bind-to-js-function" + | #"/docs/manual/bind-to-js-object" | #"/docs/manual/build-configuration" | #"/docs/manual/build-configuration-schema" - | #"/docs/manual/bind-to-js-object" - | #"/docs/manual/bind-to-js-function" - | #"/docs/manual/bind-to-global-js-values" - | #"/docs/manual/attribute" - | #"/docs/manual/async-await" - | #"/docs/manual/array-and-list" - | #"/docs/manual/api" - | #"/docs/guides/overview" - | #"/docs/guides/publishing-packages" - | #"/community/translations" - | #"/community/roadmap" - | #"/community/overview" - | #"/community/content" - | #"/community/code-of-conduct" - | #"/syntax-lookup/specialvalues_pos_of" - | #"/syntax-lookup/specialvalues_pos" - | #"/syntax-lookup/specialvalues_module" - | #"/syntax-lookup/specialvalues_loc_of" - | #"/syntax-lookup/specialvalues_loc" - | #"/syntax-lookup/specialvalues_line_of" - | #"/syntax-lookup/specialvalues_line" - | #"/syntax-lookup/specialvalues_file" - | #"/syntax-lookup/operators_type_coercion" - | #"/syntax-lookup/operators_shift_right_unsigned" - | #"/syntax-lookup/operators_shift_right" - | #"/syntax-lookup/operators_shift_left" - | #"/syntax-lookup/operators_triangle_pipe" - | #"/syntax-lookup/operators_string_concatenation" - | #"/syntax-lookup/operators_pipe" - | #"/syntax-lookup/operators_mod" - | #"/syntax-lookup/operators_exponentiation" - | #"/syntax-lookup/operators_boolean_or" - | #"/syntax-lookup/operators_boolean_not" - | #"/syntax-lookup/operators_boolean_and" - | #"/syntax-lookup/operators_bitwise_xor" - | #"/syntax-lookup/operators_bitwise_or" - | #"/syntax-lookup/operators_bitwise_not" - | #"/syntax-lookup/operators_bitwise_and" - | #"/syntax-lookup/operators_integer_subtraction" - | #"/syntax-lookup/operators_integer_multiplication" - | #"/syntax-lookup/operators_integer_division" - | #"/syntax-lookup/operators_integer_addition" - | #"/syntax-lookup/operators_float_subtraction" - | #"/syntax-lookup/operators_float_multiplication" - | #"/syntax-lookup/operators_float_division" - | #"/syntax-lookup/operators_float_addition" - | #"/syntax-lookup/operator_ref_value_assignment" - | #"/syntax-lookup/language_while" - | #"/syntax-lookup/language_uncurried_function" - | #"/syntax-lookup/language_ref" - | #"/syntax-lookup/language_record_type" - | #"/syntax-lookup/language_type_parameter" - | #"/syntax-lookup/language_type" - | #"/syntax-lookup/language_type_rec" - | #"/syntax-lookup/language_ternary" - | #"/syntax-lookup/language_switch" - | #"/syntax-lookup/language_or_pattern" - | #"/syntax-lookup/language_string_literal" - | #"/syntax-lookup/language_string_interpolation" - | #"/syntax-lookup/language_spreads" - | #"/syntax-lookup/language_scoped_polymorphic_type" - | #"/syntax-lookup/language_regular_expression" - | #"/syntax-lookup/language_polyvar" - | #"/syntax-lookup/language_open_object_type" - | #"/syntax-lookup/language_object_type" - | #"/syntax-lookup/language_optional_record_field" - | #"/syntax-lookup/language_placeholder" - | #"/syntax-lookup/language_optional_labeled_argument" - | #"/syntax-lookup/language_open" - | #"/syntax-lookup/language_module" - | #"/syntax-lookup/language_module_function" - | #"/syntax-lookup/language_module_type" - | #"/syntax-lookup/language_module_type_of" + | #"/docs/manual/build-monorepo-setup" + | #"/docs/manual/build-overview" + | #"/docs/manual/build-performance" + | #"/docs/manual/control-flow" + | #"/docs/manual/converting-from-js" + | #"/docs/manual/dict" + | #"/docs/manual/editor-code-analysis" + | #"/docs/manual/editor-plugins" + | #"/docs/manual/embed-raw-javascript" + | #"/docs/manual/equality-comparison" + | #"/docs/manual/exception" + | #"/docs/manual/extensible-variant" + | #"/docs/manual/external" + | #"/docs/manual/function" + | #"/docs/manual/generalized-algebraic-data-types" + | #"/docs/manual/generate-converters-accessors" + | #"/docs/manual/import-export" + | #"/docs/manual/import-from-export-to-js" + | #"/docs/manual/inlining-constants" + | #"/docs/manual/installation" + | #"/docs/manual/interop-cheatsheet" + | #"/docs/manual/interop-with-js-build-systems" + | #"/docs/manual/introduction" + | #"/docs/manual/json" + | #"/docs/manual/jsx" + | #"/docs/manual/lazy-values" + | #"/docs/manual/let-binding" + | #"/docs/manual/libraries" + | #"/docs/manual/llms" + | #"/docs/manual/migrate-to-v11" + | #"/docs/manual/migrate-to-v12" + | #"/docs/manual/module" + | #"/docs/manual/module-functions" + | #"/docs/manual/mutation" + | #"/docs/manual/null-undefined-option" + | #"/docs/manual/object" + | #"/docs/manual/overview" + | #"/docs/manual/pattern-matching-destructuring" + | #"/docs/manual/pipe" + | #"/docs/manual/polymorphic-variant" + | #"/docs/manual/primitive-types" + | #"/docs/manual/project-structure" + | #"/docs/manual/promise" + | #"/docs/manual/record" + | #"/docs/manual/rescript-for-javascript-developers" + | #"/docs/manual/reserved-keywords" + | #"/docs/manual/scoped-polymorphic-types" + | #"/docs/manual/shared-data-types" + | #"/docs/manual/tagged-templates" + | #"/docs/manual/try" + | #"/docs/manual/tuple" + | #"/docs/manual/type" + | #"/docs/manual/typescript-integration" + | #"/docs/manual/use-illegal-identifier-names" + | #"/docs/manual/variant" + | #"/docs/manual/warning-numbers" + | #"/docs/react/arrays-and-keys" + | #"/docs/react/beyond-jsx" + | #"/docs/react/components-and-props" + | #"/docs/react/context" + | #"/docs/react/elements-and-jsx" + | #"/docs/react/events" + | #"/docs/react/extensions-of-props" + | #"/docs/react/forwarding-refs" + | #"/docs/react/hooks-context" + | #"/docs/react/hooks-custom" + | #"/docs/react/hooks-effect" + | #"/docs/react/hooks-overview" + | #"/docs/react/hooks-reducer" + | #"/docs/react/hooks-ref" + | #"/docs/react/hooks-state" + | #"/docs/react/import-export-reactjs" + | #"/docs/react/installation" + | #"/docs/react/introduction" + | #"/docs/react/lazy-components" + | #"/docs/react/llms" + | #"/docs/react/memo" + | #"/docs/react/refs-and-the-dom" + | #"/docs/react/rendering-elements" + | #"/docs/react/router" + | #"/docs/react/server-components" + | #"/docs/react/styling" + | #"/packages" + | #"/syntax-lookup" + | #"/syntax-lookup/decorator_as" + | #"/syntax-lookup/decorator_dead" + | #"/syntax-lookup/decorator_deriving" + | #"/syntax-lookup/decorator_directive" + | #"/syntax-lookup/decorator_does_not_raise" + | #"/syntax-lookup/decorator_does_not_throw" + | #"/syntax-lookup/decorator_expression_deprecated" + | #"/syntax-lookup/decorator_expression_warning" + | #"/syntax-lookup/decorator_gentype" + | #"/syntax-lookup/decorator_get" + | #"/syntax-lookup/decorator_get_index" + | #"/syntax-lookup/decorator_ignore" + | #"/syntax-lookup/decorator_inline" + | #"/syntax-lookup/decorator_int" + | #"/syntax-lookup/decorator_jsx_component" + | #"/syntax-lookup/decorator_live" + | #"/syntax-lookup/decorator_meth" + | #"/syntax-lookup/decorator_module" + | #"/syntax-lookup/decorator_module_deprecated" + | #"/syntax-lookup/decorator_module_warning" + | #"/syntax-lookup/decorator_new" + | #"/syntax-lookup/decorator_not_undefined" + | #"/syntax-lookup/decorator_raises" + | #"/syntax-lookup/decorator_react_component" + | #"/syntax-lookup/decorator_react_component_with_props" + | #"/syntax-lookup/decorator_return" + | #"/syntax-lookup/decorator_scope" + | #"/syntax-lookup/decorator_send" + | #"/syntax-lookup/decorator_send_pipe" + | #"/syntax-lookup/decorator_set" + | #"/syntax-lookup/decorator_set_index" + | #"/syntax-lookup/decorator_string" + | #"/syntax-lookup/decorator_tag" + | #"/syntax-lookup/decorator_taggedTemplate" + | #"/syntax-lookup/decorator_this" + | #"/syntax-lookup/decorator_throws" + | #"/syntax-lookup/decorator_unboxed" + | #"/syntax-lookup/decorator_unwrap" + | #"/syntax-lookup/decorator_val" + | #"/syntax-lookup/decorator_variadic" + | #"/syntax-lookup/extension_debugger" + | #"/syntax-lookup/extension_identity" + | #"/syntax-lookup/extension_private_let" + | #"/syntax-lookup/extension_raw_expression" + | #"/syntax-lookup/extension_raw_top_level_expression" + | #"/syntax-lookup/extension_regular_expression" + | #"/syntax-lookup/extension_todo" + | #"/syntax-lookup/language_and" + | #"/syntax-lookup/language_async" + | #"/syntax-lookup/language_attached_doc_comment" + | #"/syntax-lookup/language_await" + | #"/syntax-lookup/language_block_comment" + | #"/syntax-lookup/language_char_literal" + | #"/syntax-lookup/language_covariant_type_parameter" + | #"/syntax-lookup/language_dict" + | #"/syntax-lookup/language_doc_comment" + | #"/syntax-lookup/language_empty_object_type" + | #"/syntax-lookup/language_exception" + | #"/syntax-lookup/language_external" + | #"/syntax-lookup/language_for" + | #"/syntax-lookup/language_function" + | #"/syntax-lookup/language_if_else" + | #"/syntax-lookup/language_include" | #"/syntax-lookup/language_jsx_component" + | #"/syntax-lookup/language_labeled_argument" | #"/syntax-lookup/language_let" | #"/syntax-lookup/language_let_rec" - | #"/syntax-lookup/language_labeled_argument" - | #"/syntax-lookup/language_include" - | #"/syntax-lookup/language_if_else" - | #"/syntax-lookup/language_function" - | #"/syntax-lookup/language_for" - | #"/syntax-lookup/language_external" - | #"/syntax-lookup/language_exception" - | #"/syntax-lookup/language_empty_object_type" - | #"/syntax-lookup/language_doc_comment" - | #"/syntax-lookup/language_covariant_type_parameter" - | #"/syntax-lookup/language_attached_doc_comment" - | #"/syntax-lookup/language_block_comment" | #"/syntax-lookup/language_line_comment" - | #"/syntax-lookup/language_dict" - | #"/syntax-lookup/language_char_literal" - | #"/syntax-lookup/language_await" - | #"/syntax-lookup/language_async" - | #"/syntax-lookup/language_and" + | #"/syntax-lookup/language_module" + | #"/syntax-lookup/language_module_function" + | #"/syntax-lookup/language_module_type" + | #"/syntax-lookup/language_module_type_of" + | #"/syntax-lookup/language_object_type" + | #"/syntax-lookup/language_open" + | #"/syntax-lookup/language_open_object_type" + | #"/syntax-lookup/language_optional_labeled_argument" + | #"/syntax-lookup/language_optional_record_field" + | #"/syntax-lookup/language_or_pattern" + | #"/syntax-lookup/language_placeholder" + | #"/syntax-lookup/language_polyvar" + | #"/syntax-lookup/language_record_type" + | #"/syntax-lookup/language_ref" + | #"/syntax-lookup/language_regular_expression" + | #"/syntax-lookup/language_scoped_polymorphic_type" + | #"/syntax-lookup/language_spreads" + | #"/syntax-lookup/language_string_interpolation" + | #"/syntax-lookup/language_string_literal" + | #"/syntax-lookup/language_switch" + | #"/syntax-lookup/language_ternary" + | #"/syntax-lookup/language_type" + | #"/syntax-lookup/language_type_parameter" + | #"/syntax-lookup/language_type_rec" | #"/syntax-lookup/language_variant_type" - | #"/syntax-lookup/extension_todo" - | #"/syntax-lookup/extension_regular_expression" - | #"/syntax-lookup/extension_raw_top_level_expression" - | #"/syntax-lookup/extension_raw_expression" - | #"/syntax-lookup/extension_private_let" - | #"/syntax-lookup/extension_identity" - | #"/syntax-lookup/extension_debugger" - | #"/syntax-lookup/decorator_variadic" - | #"/syntax-lookup/decorator_val" - | #"/syntax-lookup/decorator_unwrap" - | #"/syntax-lookup/decorator_uncurry" - | #"/syntax-lookup/decorator_uncurried" - | #"/syntax-lookup/decorator_unboxed" - | #"/syntax-lookup/decorator_throws" - | #"/syntax-lookup/decorator_this" - | #"/syntax-lookup/decorator_taggedTemplate" - | #"/syntax-lookup/decorator_tag" - | #"/syntax-lookup/decorator_string" - | #"/syntax-lookup/decorator_set_index" - | #"/syntax-lookup/decorator_set" - | #"/syntax-lookup/decorator_send_pipe" - | #"/syntax-lookup/decorator_send" - | #"/syntax-lookup/decorator_scope" - | #"/syntax-lookup/decorator_return" - | #"/syntax-lookup/decorator_react_component_with_props" - | #"/syntax-lookup/decorator_react_component" - | #"/syntax-lookup/decorator_raises" - | #"/syntax-lookup/decorator_obj" - | #"/syntax-lookup/decorator_not_undefined" - | #"/syntax-lookup/decorator_new" - | #"/syntax-lookup/decorator_module_warning" - | #"/syntax-lookup/decorator_module_deprecated" - | #"/syntax-lookup/decorator_module" - | #"/syntax-lookup/decorator_meth" - | #"/syntax-lookup/decorator_live" - | #"/syntax-lookup/decorator_jsx_component" - | #"/syntax-lookup/decorator_int" - | #"/syntax-lookup/decorator_inline" - | #"/syntax-lookup/decorator_ignore" - | #"/syntax-lookup/decorator_get_index" - | #"/syntax-lookup/decorator_get" - | #"/syntax-lookup/decorator_gentype" - | #"/syntax-lookup/decorator_expression_warning" - | #"/syntax-lookup/decorator_expression_deprecated" - | #"/syntax-lookup/decorator_does_not_throw" - | #"/syntax-lookup/decorator_does_not_raise" - | #"/syntax-lookup/decorator_directive" - | #"/syntax-lookup/decorator_deriving" - | #"/syntax-lookup/decorator_dead" - | #"/syntax-lookup/decorator_as" - | #"/syntax-lookup/builtinfunctions_ignore" - | #"/*" + | #"/syntax-lookup/language_while" + | #"/syntax-lookup/operator_ref_value_assignment" + | #"/syntax-lookup/operators_bitwise_and" + | #"/syntax-lookup/operators_bitwise_not" + | #"/syntax-lookup/operators_bitwise_or" + | #"/syntax-lookup/operators_bitwise_xor" + | #"/syntax-lookup/operators_boolean_and" + | #"/syntax-lookup/operators_boolean_not" + | #"/syntax-lookup/operators_boolean_or" + | #"/syntax-lookup/operators_exponentiation" + | #"/syntax-lookup/operators_float_addition" + | #"/syntax-lookup/operators_float_division" + | #"/syntax-lookup/operators_float_multiplication" + | #"/syntax-lookup/operators_float_subtraction" + | #"/syntax-lookup/operators_integer_addition" + | #"/syntax-lookup/operators_integer_division" + | #"/syntax-lookup/operators_integer_multiplication" + | #"/syntax-lookup/operators_integer_subtraction" + | #"/syntax-lookup/operators_mod" + | #"/syntax-lookup/operators_pipe" + | #"/syntax-lookup/operators_shift_left" + | #"/syntax-lookup/operators_shift_right" + | #"/syntax-lookup/operators_shift_right_unsigned" + | #"/syntax-lookup/operators_string_concatenation" + | #"/syntax-lookup/operators_type_coercion" + | #"/syntax-lookup/specialvalues_file" + | #"/syntax-lookup/specialvalues_line" + | #"/syntax-lookup/specialvalues_line_of" + | #"/syntax-lookup/specialvalues_loc" + | #"/syntax-lookup/specialvalues_loc_of" + | #"/syntax-lookup/specialvalues_module" + | #"/syntax-lookup/specialvalues_pos" + | #"/syntax-lookup/specialvalues_pos_of" + | #"/try" ] diff --git a/packages/shared/src/ReactRouter.res b/packages/shared/src/ReactRouter.res index a6d4ade12..c465edd8c 100644 --- a/packages/shared/src/ReactRouter.res +++ b/packages/shared/src/ReactRouter.res @@ -11,7 +11,7 @@ external useSearchParams: unit => (WebAPI.URLAPI.urlSearchParams, {..} => unit) @module("react-router") external useLoaderData: unit => 'a = "useLoaderData" -/* The types for this are auto-generated from the react-router.config.mjs file */ +/* Path.t is generated from apps/docs/route-config.mjs by yarn build:routes. */ type path = {pathname: Path.t, search?: string, hash?: string} module Loader = { From c704e16b4d82aa627242c5d4d27b165ac376e069 Mon Sep 17 00:00:00 2001 From: Florian Hammerschmidt Date: Sun, 11 Oct 2026 23:15:06 +0200 Subject: [PATCH 02/10] fix: align local test commands with their supported workflows Remove local screenshot-update aliases and document the feature-branch CI workflow that owns visual baselines. Point the full-site Cypress suite at the Vite dev server, explain server startup and URL overrides, and distinguish the managed production homepage suite. Fixes #1415 --- README.md | 61 +++++++++++++++++++++++++++--------- apps/docs/cypress.config.mjs | 2 +- apps/docs/package.json | 3 +- package.json | 3 +- 4 files changed, 49 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 2630705a2..66af0a28a 100644 --- a/README.md +++ b/README.md @@ -87,18 +87,20 @@ Tailwind is configured in [`styles/main.css`](styles/main.css). There is no `tai ## Common Commands -| Command | Purpose | -| -------------------- | ----------------------------------------------------------------- | -| `yarn dev` | Prepare generated files and run the local development environment | -| `yarn build` | Run the full production build | -| `yarn preview` | Build and serve the generated static client locally | -| `yarn build:res` | Compile ReScript only | -| `yarn dev:res` | Run the ReScript compiler in watch mode | -| `yarn format` | Run Prettier and the ReScript formatter | -| `yarn test` | Run markdown example and href validation | -| `yarn ci:test` | Run Vitest browser tests headlessly | -| `yarn vitest` | Run Vitest directly | -| `yarn vitest:update` | Update screenshot baselines headlessly | +| Command | Purpose | +| ---------------- | ----------------------------------------------------------------- | +| `yarn dev` | Prepare generated files and run the local development environment | +| `yarn build` | Run the full production build | +| `yarn preview` | Build and serve the generated static client locally | +| `yarn build:res` | Compile ReScript only | +| `yarn dev:res` | Run the ReScript compiler in watch mode | +| `yarn format` | Run Prettier and the ReScript formatter | +| `yarn test` | Run markdown example and href validation | +| `yarn ci:test` | Run Vitest browser tests headlessly | +| `yarn vitest` | Run Vitest directly | +| `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 @@ -107,6 +109,9 @@ Tailwind is configured in [`styles/main.css`](styles/main.css). There is no `tai We use [Vitest](https://vitest.dev/) in browser mode with Playwright for component-level tests. Test files live in `__tests__/` and are written in ReScript. ```sh +# Compile the ReScript tests first +yarn build:res + # Watch mode yarn vitest @@ -114,13 +119,39 @@ yarn vitest yarn ci:test ``` -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://127.0.0.1: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://127.0.0.1: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..a231b6d56 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://127.0.0.1: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 e7350a978..b519d4356 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -39,8 +39,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/package.json b/package.json index b54140105..01455f85f 100644 --- a/package.json +++ b/package.json @@ -41,8 +41,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", From 63e925408e8fcaa419f309aedf2864e65b0318db Mon Sep 17 00:00:00 2001 From: Florian Hammerschmidt Date: Mon, 12 Oct 2026 00:20:32 +0200 Subject: [PATCH 03/10] fix: retain localhost in local Cypress URLs Keep localhost as the full-site Cypress hostname while correcting the default port to 5173. Align the README default URL and alternate-port example with the config. --- README.md | 4 ++-- apps/docs/cypress.config.mjs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 66af0a28a..7582050c3 100644 --- a/README.md +++ b/README.md @@ -145,10 +145,10 @@ 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://127.0.0.1:5173`. If Vite reports another port, or you want to test a deployed site, pass its URL explicitly: +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 cy:e2e --config baseUrl=http://127.0.0.1:5174 +yarn cy:e2e --config baseUrl=http://localhost:5174 ``` 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`. diff --git a/apps/docs/cypress.config.mjs b/apps/docs/cypress.config.mjs index a231b6d56..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://127.0.0.1:5173", + baseUrl: "http://localhost:5173", specPattern: "e2e/**/*.cy.jsx", excludeSpecPattern: "e2e/homepage/**", supportFile: "cypress/support/e2e.js", From 4ea200d921f6b89bb888a2bb065aac81382d0757 Mon Sep 17 00:00:00 2001 From: Florian Hammerschmidt Date: Mon, 12 Oct 2026 01:01:03 +0200 Subject: [PATCH 04/10] docs: align contributor guidance with the current monorepo Document the workspace paths, docs SSR and guide deployment, current MDX pipeline, compiler sources, staged formatting hooks, and generated output policy. Clarify link-check working directories and CI behavior, recommend Oxc in VS Code, and ignore bundled Yarn releases by pattern. Refs #1417 and #1421. --- .oxfmtrc.json | 2 +- .vscode/extensions.json | 2 +- AGENTS.md | 107 ++++++++++++++++++++++------------------ CONTRIBUTING.md | 22 +++++++-- README.md | 64 +++++++++++++++--------- 5 files changed, 119 insertions(+), 78 deletions(-) diff --git a/.oxfmtrc.json b/.oxfmtrc.json index f4e51e821..a6a57ab7a 100644 --- a/.oxfmtrc.json +++ b/.oxfmtrc.json @@ -39,7 +39,7 @@ "apps/docs/public/llms/react/**/llm*.txt", "apps/docs/markdown-pages/docs/**/**/llms.mdx", "apps/docs/markdown-pages/docs/manual/installation.mdx", - ".yarn/releases/yarn-4.12.0.cjs", + ".yarn/releases/*.cjs", "apps/docs/data/api/*" ] } diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 241f2c935..9cf683216 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,3 +1,3 @@ { - "recommendations": ["esbenp.prettier-vscode", "chenglou92.rescript-vscode"] + "recommendations": ["oxc.oxc-vscode", "chenglou92.rescript-vscode"] } diff --git a/AGENTS.md b/AGENTS.md index 20a6c4e85..ed1ebd5bd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,35 +2,49 @@ ## Project Overview -This is the official documentation website for the [ReScript](https://rescript-lang.org) programming language. It is primarily pre-rendered at build time, with runtime server-side rendering for `/try` through Cloudflare Pages Functions. It is built with **ReScript v12 + React 19 + React Router v8 + Vite 8 + Tailwind CSS v4**, deployed to **Cloudflare Pages**. +This monorepo contains the official documentation website for the [ReScript](https://rescript-lang.org) programming language and the interactive guide. Both use **ReScript v12 + React 19 + React Router v8 + Vite 8**. The docs site uses Tailwind CSS v4 and is primarily pre-rendered at build time, with runtime server-side rendering for `/try` through Cloudflare Pages Functions. The guide pre-renders its entry page and runs its interactive lessons in the browser, with assets served by a Cloudflare Worker. ## System Requirements - Node.js ≥ 26 -- Yarn 4.12.0 (via Corepack) +- Yarn 4.13.0 (via Corepack, pinned in `package.json` and `.yarnrc.yml`) ## Project Structure ``` -app/ → React Router app shell (root layout, route definitions, route modules) - routes/ → Route components with loaders (e.g. BlogRoute.res, MdxRoute.res) -src/ → Core ReScript source code - bindings/ → Zero-cost bindings to JS libraries (@module externals) - common/ → Shared utilities, hooks, helpers (non-component modules) - components/ → Reusable React components (e.g. SidebarNav, Breadcrumbs) - ffi/ → Plain JS interop files (prefer %raw over adding new files here) - layouts/ → Page layout components (DocsLayout, SidebarPageLayout, etc.) -markdown-pages/ → MDX content (blog posts, docs, community pages, syntax-lookup) -data/ → Hand-curated data (API docs JSON, sidebar ordering) -styles/ → CSS files (Tailwind v4 config in main.css, custom utilities) -scripts/ → Build/codegen scripts (ReScript + JS) -functions/ → Cloudflare Pages Functions (e.g. OG image generation) -compilers/ → Bundled ReScript compiler versions (for the playground) -plugins/ → HighlightJS & CodeMirror plugins -public/ → Static assets (images, fonts, favicons) -__tests__/ → Vitest browser-mode tests (Playwright) +apps/docs/ → Documentation website workspace (@rescript-lang/docs) + app/ → React Router shell, layouts, and route modules + routes/ → Page components and loaders (e.g. BlogRoute.res) + src/ → Website ReScript code + bindings/ → App-specific bindings to JS libraries + common/ → Shared website utilities and hooks + components/ → Reusable React components + ffi/ → Legacy plain JS interop + layouts/ → Page layout components + markdown/ → MDX loading, compilation, and frontmatter parsing + markdown-pages/ → Blog, docs, community, and syntax-lookup MDX + data/ → Sidebar ordering and content metadata + styles/ → Tailwind v4 entry points, theme, and custom CSS + scripts/ → Build, code generation, and validation scripts + functions/ → Pages Functions for /try, /try.data, and OG images + server/ → React Router request handler for Pages Functions + public/ → Static assets (images, fonts, favicons) + __tests__/ → Vitest browser and visual tests + e2e/ → Cypress browser tests +apps/guide/ → Interactive guide workspace (@rescript-lang/guide) +packages/playground/ → Shared browser playground (@rescript-lang/playground) +packages/shared/ → Shared bindings and utilities (@rescript-lang/shared) + src/ → ReactRouter, Vitest, CodeMirror, highlighting, etc. + plugins/ → CodeMirror 6 ReScript/Reason grammar +data/api/ → Compiler-published API JSON snapshots +.github/ → CI, deployment, and screenshot workflows ``` +Paths in these guidelines are relative to the repository root. Run the commands +below from there; use `yarn workspace ` for workspace commands. +The playground loads published browser compiler bundles. Markdown example checks +use the installed `rescript` package, not a separate compiler installation. + ## Key Commands | Command | Purpose | @@ -39,8 +53,8 @@ __tests__/ → Vitest browser-mode tests (Playwright) | `yarn build` | Full production build (ReScript → scripts → Vite/React Router) | | `yarn build:res` | ReScript compilation only | | `yarn dev:res` | ReScript watch mode only | -| `yarn format` | Run Prettier + ReScript formatter | -| `yarn test` | Run example and href validation scripts | +| `yarn format` | Run Oxfmt + ReScript formatter | +| `yarn test` | Run example, href, and redirect validation scripts | | `yarn vitest` | Run Vitest browser tests with Playwright | | `make` | Build (install deps + ReScript compile + update index) | @@ -52,11 +66,11 @@ __tests__/ → Vitest browser-mode tests (Playwright) - `.res` files must always be capitalized (PascalCase), matching ReScript module conventions. - 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`. -- Never directly edit an `.jsx` files, you must edit the corresponding `.res` file. The `.jsx` files are generated by the ReScript compiler and will be overwritten on the next compile. +- 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 -- You use **ReScript v12** (latest). Ensure all suggestions match this version. +- This branch uses **ReScript v12**. Ensure all suggestions match this version. - Ensure any produced JSX matches ReScript JSX v4 syntax (configured with `"preserve": true`). - **Never use the `Belt` or `Js` modules** — these are legacy. Use the modern `Stdlib` / core modules instead. - Always use the `JSON.t` type for JSON values. @@ -78,7 +92,7 @@ __tests__/ → Vitest browser-mode tests (Playwright) The project uses several patterns for JavaScript interop. Follow the existing conventions: -- **`@module` externals** for binding to npm packages (see `src/bindings/` for examples): +- **`@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" ``` @@ -87,21 +101,17 @@ The project uses several patterns for JavaScript interop. Follow the existing co @module("react-router") @react.component external make: (~children: React.element) => React.element = "Outlet" ``` -- **`%raw()`** for inline JS when no clean binding is possible: - ``` - let copyToClipboard: string => bool = %raw(`function(str) { ... }`) - ``` -- **`%%raw()`** (double percent) for top-level side-effectful JS imports. +- `%raw` and `%%raw` embed JavaScript; use them only when specifically requested. - **`@scope`** for binding to object properties like `process.env`. - **`external ... = "%identity"`** for zero-cost type coercions. -- Put new bindings in `src/bindings/` as a dedicated module (e.g. `Fuse.res`, `DocSearch.res`). +- Put app-specific bindings in `apps/docs/src/bindings/` as a dedicated module (e.g. `Fuse.res`, `DocSearch.res`). Bindings shared with the guide or playground belong in `packages/shared/src/`. ## ReScript React - This project uses **React 19** and **React Router v8** (framework mode). - 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. -- Route modules live in `app/routes/` and export a `loader` and a `default` component. +- 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. @@ -153,21 +163,21 @@ let default: unit => React.element ## Styling -- **Tailwind CSS v4** configured via the Vite plugin (`@tailwindcss/vite`). There is no `tailwind.config.js`. -- Tailwind design tokens and custom utilities live in `styles/_theme.css`. It is imported by `styles/main.css` and referenced by route-specific stylesheets. +- The docs site uses **Tailwind CSS v4**, configured via the Vite plugin (`@tailwindcss/vite`). Design tokens and custom utilities live in `apps/docs/styles/_theme.css`, imported by `apps/docs/styles/main.css` and referenced by route-specific stylesheets. +- The guide uses its own plain CSS in `apps/guide/styles/main.css`. - **LightningCSS** is used as the CSS transformer. -- The project defines custom design tokens in `styles/_theme.css`: +- The project defines custom design tokens in `apps/docs/styles/_theme.css`: - Custom colors: `gray-*`, `fire-*`, `sky-*`, `berry-*`, `water`, `turtle`, `orange-*` - Custom font sizes: `text-11` through `text-68` - Custom utility classes: `hl-title`, `hl-1`–`hl-5`, `body-lg`, `body-md`, `body-sm`, `captions` - Fonts: Inter (sans), Roboto Mono (mono) -- Use existing custom utilities and design tokens. Check `styles/_theme.css` before creating new ones. +- Use existing custom utilities and design tokens. Check `apps/docs/styles/_theme.css` before creating new ones. ## Testing - Tests use **Vitest 5** in browser mode with **Playwright** (Chromium). -- Test files live in `__tests__/` and are named `ComponentName_.test.res` (compiled to `.test.jsx`). -- Tests use custom ReScript bindings in `src/bindings/Vitest.res`. +- Browser tests live in `apps/docs/__tests__/` and `apps/guide/__tests__/`. Docs visual tests live in `apps/docs/__tests__/visual/`. Test modules are named `ComponentName_.test.res` (compiled to `.test.jsx`). +- Tests use custom ReScript bindings in `packages/shared/src/Vitest.res`. - Tests are visual/integration tests that render components and assert visibility, interactions, and screenshots. - Test pattern: @@ -185,22 +195,24 @@ let default: unit => React.element - Components requiring React Router context must be wrapped in ``. - Do not use `` in tests. -- Run tests with `yarn vitest --browser.headless --run`. -- Do not update snapshots without asking for confirmation. +- Run `yarn build:res` before `yarn vitest --browser.headless --run` so the tests use current compiler output. +- 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. ## MDX Content -- Documentation content is in `markdown-pages/` organized by section (blog, docs, community, syntax-lookup). -- MDX is processed by `react-router-mdx` with remark/rehype plugins. -- Custom MDX components are mapped in `app/routes/MdxRoute.res` (e.g. ``, ``, ``, `