From 88bef84e1355f1fe7af2fed539a83b418108664d Mon Sep 17 00:00:00 2001 From: Florian Hammerschmidt Date: Tue, 20 Oct 2026 18:24:10 +0200 Subject: [PATCH] fix: select published v13 API documentation Build on the shared website sync to select v13 publications, preview labels, search metadata, and generated manual indexes from DocsVersion.current. Add ReScript tests for publication selection, route loaders, data URLs, sidebars, and link validation. Remove the now-unused major-version helper. Fixes #1433 --- apps/docs/__tests__/Meta_.test.res | 2 +- apps/docs/__tests__/VersionSelect_.test.res | 7 +- apps/docs/docs-version.json | 2 +- .../scripts/__tests__/ApiDataFixtures.res | 27 ++++ .../docs/scripts/__tests__/ApiLinks_.test.res | 58 ++++++++ .../scripts/__tests__/ApiRoutes_.test.res | 139 ++++++++++++++++++ apps/docs/scripts/generate_llms.res | 2 +- apps/docs/src/bindings/ApiDataSource.res | 4 + apps/docs/src/bindings/DocsRouteConfig.res | 11 +- apps/docs/src/common/Constants.res | 4 +- apps/docs/src/components/VersionSelect.res | 3 +- apps/docs/vitest.scripts.config.mjs | 1 + data/api/README.md | 10 +- packages/shared/src/Path.res | 11 +- packages/shared/src/Semver.res | 6 - packages/shared/src/Semver.resi | 2 - 16 files changed, 267 insertions(+), 22 deletions(-) create mode 100644 apps/docs/scripts/__tests__/ApiDataFixtures.res create mode 100644 apps/docs/scripts/__tests__/ApiLinks_.test.res create mode 100644 apps/docs/scripts/__tests__/ApiRoutes_.test.res diff --git a/apps/docs/__tests__/Meta_.test.res b/apps/docs/__tests__/Meta_.test.res index f6a6c87b1..d439517ab 100644 --- a/apps/docs/__tests__/Meta_.test.res +++ b/apps/docs/__tests__/Meta_.test.res @@ -43,7 +43,7 @@ test("renders DocSearch crawler meta tags", async () => { ) expect(getMetaContent("docsearch:language"))->toBe("en") - expect(getMetaContent("docsearch:version"))->toBe("v12,latest") + expect(getMetaContent("docsearch:version"))->toBe("v13,next") }) test("generates Open Graph image URLs from the current page URL", async () => { diff --git a/apps/docs/__tests__/VersionSelect_.test.res b/apps/docs/__tests__/VersionSelect_.test.res index 54943bdae..5a5d13b87 100644 --- a/apps/docs/__tests__/VersionSelect_.test.res +++ b/apps/docs/__tests__/VersionSelect_.test.res @@ -6,7 +6,7 @@ test("renders current version label", async () => { let el = await screen->getByTestId("version-select") await element(el)->toBeVisible - let label = await screen->getByText("v12 (latest)") + let label = await screen->getByText("v13 (preview)") await element(label)->toBeVisible }) @@ -21,7 +21,10 @@ test("clicking button shows older versions", async () => { let button = await screen->getByRole(#button) await button->click - // Older versions should now be visible + // Released documentation should now be visible + let latest = await screen->getByText("v12 (latest)") + await element(latest)->toBeVisible + let v11After = await screen->getByText("v11") await element(v11After)->toBeVisible diff --git a/apps/docs/docs-version.json b/apps/docs/docs-version.json index 4543ea595..474222fa9 100644 --- a/apps/docs/docs-version.json +++ b/apps/docs/docs-version.json @@ -1,3 +1,3 @@ { - "current": "v12" + "current": "v13" } diff --git a/apps/docs/scripts/__tests__/ApiDataFixtures.res b/apps/docs/scripts/__tests__/ApiDataFixtures.res new file mode 100644 index 000000000..4176a2b32 --- /dev/null +++ b/apps/docs/scripts/__tests__/ApiDataFixtures.res @@ -0,0 +1,27 @@ +open ScriptTest + +let apiModule = (id, name, version) => JSON.Object( + Dict.fromArray([ + ("id", JSON.String(id)), + ("name", JSON.String(name)), + ("docstrings", JSON.Array([JSON.String(`Docs from ${version}`)])), + ("items", JSON.Array([])), + ]), +) + +let snapshot = async (directory, version, moduleName) => { + let target = join([directory, version]) + await mkdir(target, {recursive: true}) + let writes = ["stdlib", "belt", "dom"]->Array.map(async library => { + let modulePath = `${library}/${moduleName->String.toLowerCase}` + let data = JSON.Object( + Dict.fromArray([ + (library, apiModule(library->String.capitalize, library->String.capitalize, version)), + (modulePath, apiModule(`${library->String.capitalize}.${moduleName}`, moduleName, version)), + ]), + ) + await write(join([target, `${library}.json`]), JSON.stringify(data)) + }) + let results = await Promise.all(writes) + ignore(results) +} diff --git a/apps/docs/scripts/__tests__/ApiLinks_.test.res b/apps/docs/scripts/__tests__/ApiLinks_.test.res new file mode 100644 index 000000000..8530529ed --- /dev/null +++ b/apps/docs/scripts/__tests__/ApiLinks_.test.res @@ -0,0 +1,58 @@ +open ScriptTest + +let check = async markdown => { + let directory = await temporaryDirectory("v13-api-links-") + let options = {cwd: directory, env: environment, encoding: "utf8"} + expectSuccess(spawn("git", ["init", "--quiet"], options)) + expectSuccess( + spawn( + "git", + ["remote", "add", "origin", "https://github.com/rescript-lang/rescript-lang.org.git"], + options, + ), + ) + let docs = join([directory, "apps/docs"]) + let manual = join([docs, "markdown-pages/docs/manual"]) + await mkdir(manual, {recursive: true}) + let apiDirectory = join([directory, "data/api"]) + await ApiDataFixtures.snapshot(apiDirectory, "v12.99.0", "MaintenanceModule") + await ApiDataFixtures.snapshot(apiDirectory, "v13.3.1", "Example") + await write(join([manual, "api.mdx"]), markdown) + spawn( + executable, + [join([cwd(), "scripts/test-hrefs.mjs"])], + { + cwd: docs, + env: withEnvironment(environment, [("CI", "1")]), + encoding: "utf8", + }, + ) +} + +test("link validation accepts routes from the selected v13 publication", async () => { + let result = await check(` +[Stdlib](/docs/manual/api/stdlib) +[Dom](/docs/manual/api/dom) +[Nested module](/docs/manual/api/belt/example/) +[API entry](/docs/manual/api/stdlib/example#value-get) +`) + expectSuccess(result) + expect(result.stdout)->toContain("0 issues found") +}) + +for_([ + "/docs/manual/api/stdlibdom", + "/docs/manual/api/stdlib/typo", + "/docs/manual/api/stdlib/maintenancemodule", + "/docs/manual/missing-page", + "./nested/docs/manual/api/dom", +])("link validation rejects missing or wrong-major API targets: %s", async href => { + let result = await check( + ` +[Valid API](/docs/manual/api/dom) +[Missing page](${href}) +`, + ) + expectStatus(result, 1) + expect(result.stdout)->toContain("Cannot find file") +}) diff --git a/apps/docs/scripts/__tests__/ApiRoutes_.test.res b/apps/docs/scripts/__tests__/ApiRoutes_.test.res new file mode 100644 index 000000000..1842db7ed --- /dev/null +++ b/apps/docs/scripts/__tests__/ApiRoutes_.test.res @@ -0,0 +1,139 @@ +open ScriptTest +open ApiDataFixtures + +let contentDirectory = async directory => { + let directory = join([directory, "markdown-pages"]) + let writes = [ + "blog", + "community", + "docs/manual", + "docs/guides", + "docs/react", + "syntax-lookup", + ]->Array.map(async name => { + await mkdir(join([directory, name]), {recursive: true}) + }) + ignore(await Promise.all(writes)) + await write(join([directory, "docs/manual/api.mdx"]), "# API") + await write(join([directory, "docs/manual/new-page.mdx"]), "# New page") + directory +} + +test( + "v13 routes and content select the same publication without crossing compiler majors", + async () => { + expect(DocsVersion.current)->toBe("v13") + let directory = await temporaryDirectory("v13-api-routes-") + let contentDirectory = await contentDirectory(directory) + let apiDirectory = join([directory, "data/api"]) + await snapshot(apiDirectory, "v12.99.0", "MaintenanceModule") + await snapshot(apiDirectory, "v13.9.0", "OldModule") + await snapshot(apiDirectory, "v13.10.0", "NewModule") + await snapshot(apiDirectory, "v14.0.0", "FutureModule") + let selected = ApiDataSource.directory(~options={directory: apiDirectory}) + let data = ApiData.load(~directory=apiDirectory, ~major=DocsVersion.current) + expect(selected)->toBe(join([apiDirectory, data.version])) + let {routes} = DocsRouteConfig.loadRoutes(~options={contentDirectory, apiDirectory: selected}) + let paths = DocsRouteConfig.concretePaths(routes) + expect(paths->Array.includes("/docs/manual/new-page"))->toBe(true) + ["stdlib", "belt", "dom"]->Array.forEach(library => { + expect(paths->Array.includes(`/docs/manual/api/${library}`))->toBe(true) + expect(paths->Array.includes(`/docs/manual/api/${library}/newmodule`))->toBe(true) + expect(paths->Array.includes(`/docs/manual/api/${library}/oldmodule`))->toBe(false) + expect(paths->Array.includes(`/docs/manual/api/${library}/maintenancemodule`))->toBe(false) + expect(paths->Array.includes(`/docs/manual/api/${library}/futuremodule`))->toBe(false) + }) + }, +) + +test("route types include nested index routes and omit URL patterns", async () => { + let routes: array = [ + { + path: "docs", + children: [{index: true}, {path: "guide"}, {path: ":slug"}, {path: "*"}, {path: "/try"}], + }, + {path: "optional?"}, + ] + expect(DocsRouteConfig.concretePaths(routes))->toStrictEqual(["/docs", "/docs/guide", "/try"]) +}) + +test("route generation detects stale types without overwriting them in check mode", async () => { + let directory = await temporaryDirectory("v13-route-types-") + let destination = join([directory, "Path.res"]) + let routes: array = [{path: "/new-api-module"}] + await write(destination, "stale") + expect(DocsRouteConfig.generate(~options={routes, destination, check: true}))->toBe(false) + expect(await read(destination))->toBe("stale") + expect(DocsRouteConfig.generate(~options={routes, destination}))->toBe(true) + expect(await read(destination))->toBe(DocsRouteConfig.renderPathType(routes)) + expect(DocsRouteConfig.generate(~options={routes, destination, check: true}))->toBe(true) +}) + +test( + "route generation checks the live v13 configuration from another working directory", + async () => { + let directory = await temporaryDirectory("v13-route-cwd-") + let script = join([cwd(), "generate-route-types.mjs"]) + let result = spawn( + executable, + [script, "--check"], + { + cwd: directory, + env: environment, + encoding: "utf8", + }, + ) + expectSuccess(result) + }, +) + +for_( + [ + "stdlib", + "stdlib/array", + "belt", + "belt/array", + "dom", + "dom/storage", + ]->Array.flatMap(modulePath => + ["", ".data", "/_.data", "/"]->Array.map(suffix => (modulePath, suffix)) + ), +)("API loader serves the published v13 %s%s module and its sidebar", async (( + modulePath, + suffix, +)) => { + let request = WebAPI.Request.fromURL( + `https://v13.rescript-lang.org/docs/manual/api/${modulePath}${suffix}`, + ) + let result = await ApiRoute.loader({request: request}) + switch result { + | Error(message) => expect(message)->toBe("expected a valid published API module") + | Ok({module_, toctree}) => + let library = modulePath->String.split("/")->Array.getUnsafe(0) + let published = ApiData.current()->ApiData.library(library)->Dict.getUnsafe(modulePath) + let published = published->JSON.Decode.object->Option.getOrThrow + expect(module_.id)->toBe(published->Dict.getUnsafe("id")->JSON.Decode.string->Option.getOrThrow) + expect(module_.docstrings)->toStrictEqual( + published + ->Dict.getUnsafe("docstrings") + ->JSON.Decode.array + ->Option.getOrThrow + ->Array.map(value => value->JSON.Decode.string->Option.getOrThrow), + ) + expect(toctree.name)->toBe(library->String.capitalize) + expect(toctree.children->Array.length > 0)->toBe(true) + } +}) + +for_(["", ".data", "/_.data", "/"])( + "MDX loaders resolve document and data URLs consistently: %s", + async suffix => { + expect( + MdxFile.resolveFilePath( + `/docs/manual/introduction${suffix}`, + ~dir="markdown-pages/docs/manual", + ~alias="docs/manual", + ), + )->toBe("markdown-pages/docs/manual/introduction.mdx") + }, +) diff --git a/apps/docs/scripts/generate_llms.res b/apps/docs/scripts/generate_llms.res index 7e162be51..24beac348 100644 --- a/apps/docs/scripts/generate_llms.res +++ b/apps/docs/scripts/generate_llms.res @@ -312,7 +312,7 @@ let generateFile = ( createSectionLlmFiles(~llmsDirectory, ~sectionFiles, ~documents) } -let currentManualVersion = "v12" +let currentManualVersion = DocsVersion.current let currentReactVersion = "v0.14.2" let currentReactRuntimeVersion = "v19.2.4" diff --git a/apps/docs/src/bindings/ApiDataSource.res b/apps/docs/src/bindings/ApiDataSource.res index 033a07f40..a8359e0ce 100644 --- a/apps/docs/src/bindings/ApiDataSource.res +++ b/apps/docs/src/bindings/ApiDataSource.res @@ -1,2 +1,6 @@ @module("../../scripts/api-data.mjs") external latestVersion: (~major: string, array) => string = "latestApiVersion" + +type options = {directory: string, major?: string} +@module("../../scripts/api-data.mjs") +external directory: (~options: options) => string = "apiDataDirectory" diff --git a/apps/docs/src/bindings/DocsRouteConfig.res b/apps/docs/src/bindings/DocsRouteConfig.res index 797a4eda8..e7e0f4e45 100644 --- a/apps/docs/src/bindings/DocsRouteConfig.res +++ b/apps/docs/src/bindings/DocsRouteConfig.res @@ -1,9 +1,16 @@ -type route +type rec route = {path?: string, index?: bool, children?: array} type config = {routes: array} -type options = {contentDirectory?: string} +type options = {contentDirectory?: string, apiDirectory?: string} @module("../../route-config.mjs") external loadRoutes: (~options: options=?) => config = "loadRoutes" @module("../../generate-route-types.mjs") external concretePaths: array => array = "concretePaths" + +@module("../../generate-route-types.mjs") +external renderPathType: array => string = "renderPathType" + +type generationOptions = {routes: array, destination: string, check?: bool} +@module("../../generate-route-types.mjs") +external generate: (~options: generationOptions) => bool = "generateRouteTypes" diff --git a/apps/docs/src/common/Constants.res b/apps/docs/src/common/Constants.res index 79d3a924c..dcc6a04d3 100644 --- a/apps/docs/src/common/Constants.res +++ b/apps/docs/src/common/Constants.res @@ -1,6 +1,4 @@ -external latestVersion: string = "import.meta.env.VITE_VERSION_LATEST" - -let docSearchVersionTokens = [latestVersion->Semver.tryGetMajorString, "latest"] +let docSearchVersionTokens = [DocsVersion.current, "next"] // Used for the DocsOverview let languageManual = [ diff --git a/apps/docs/src/components/VersionSelect.res b/apps/docs/src/components/VersionSelect.res index df8b957c4..88d90eb5b 100644 --- a/apps/docs/src/components/VersionSelect.res +++ b/apps/docs/src/components/VersionSelect.res @@ -3,9 +3,10 @@ type version = { link: string, } -let currentVersion = {label: DocsVersion.current ++ " (latest)", link: "#"} +let currentVersion = {label: DocsVersion.current ++ " (preview)", link: "#"} let olderVersions = [ + {label: "v12 (latest)", link: "https://rescript-lang.org/docs/manual/introduction"}, {label: "v11", link: "https://v11.rescript-lang.org/docs/manual/v11.0.0/introduction"}, {label: "v9.1 - v10.1", link: "https://v11.rescript-lang.org/docs/manual/v10.0.0/introduction"}, {label: "v8.2 - v9.0", link: "https://v11.rescript-lang.org/docs/manual/v9.0.0/introduction"}, diff --git a/apps/docs/vitest.scripts.config.mjs b/apps/docs/vitest.scripts.config.mjs index adecde336..7b160a8ab 100644 --- a/apps/docs/vitest.scripts.config.mjs +++ b/apps/docs/vitest.scripts.config.mjs @@ -1,6 +1,7 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ + envDir: "../..", test: { environment: "node", include: ["scripts/__tests__/*.test.jsx"], diff --git a/data/api/README.md b/data/api/README.md index 5ed1ac612..46d726318 100644 --- a/data/api/README.md +++ b/data/api/README.md @@ -14,8 +14,14 @@ module must have nonempty `id` and `name` strings, a `docstrings` array of strin and an `items` array before the snapshot can be cached or used to generate routes. Route generation, sidebar navigation, and page content use the same snapshot. -`master` selects v12; when porting this loader to the v13 website branch, update -`apps/docs/docs-version.json` to `v13` alongside the displayed documentation version. +`master` selects v12; this website branch selects v13 through +`apps/docs/docs-version.json`. The displayed documentation version and generated +manual indexes use the same setting. + +Pushes to `v13`, including compiler API-data publications, trigger +`.github/workflows/deploy.yml` and deploy the website at +`https://v13.rescript-lang.org`. Compiler publication must target this branch +only after this loader is deployed (compiler issue #8753). Keep the compiler artifact intact, including the generated `js.json` and `toc_tree.json` files. The website currently builds its sidebar from the module diff --git a/packages/shared/src/Path.res b/packages/shared/src/Path.res index c64ab8921..df922d9cf 100644 --- a/packages/shared/src/Path.res +++ b/packages/shared/src/Path.res @@ -135,12 +135,16 @@ type t = [ | #"/docs/manual/api/stdlib" | #"/docs/manual/api/stdlib/array" | #"/docs/manual/api/stdlib/arraybuffer" + | #"/docs/manual/api/stdlib/asyncgenerator" + | #"/docs/manual/api/stdlib/asynciterable" + | #"/docs/manual/api/stdlib/asynciterableiterator" | #"/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/blob" | #"/docs/manual/api/stdlib/bool" | #"/docs/manual/api/stdlib/console" | #"/docs/manual/api/stdlib/dataview" @@ -155,16 +159,17 @@ type t = [ | #"/docs/manual/api/stdlib/error/typeerror" | #"/docs/manual/api/stdlib/error/urierror" | #"/docs/manual/api/stdlib/exn" + | #"/docs/manual/api/stdlib/file" | #"/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/generator" | #"/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" @@ -184,7 +189,10 @@ type t = [ | #"/docs/manual/api/stdlib/intl/relativetimeformat" | #"/docs/manual/api/stdlib/intl/segmenter" | #"/docs/manual/api/stdlib/intl/segments" + | #"/docs/manual/api/stdlib/iterable" + | #"/docs/manual/api/stdlib/iterableiterator" | #"/docs/manual/api/stdlib/iterator" + | #"/docs/manual/api/stdlib/iteratorobject" | #"/docs/manual/api/stdlib/jserror" | #"/docs/manual/api/stdlib/jserror/evalerror" | #"/docs/manual/api/stdlib/jserror/rangeerror" @@ -216,6 +224,7 @@ type t = [ | #"/docs/manual/api/stdlib/set" | #"/docs/manual/api/stdlib/string" | #"/docs/manual/api/stdlib/symbol" + | #"/docs/manual/api/stdlib/taggedtemplate" | #"/docs/manual/api/stdlib/timeoutid" | #"/docs/manual/api/stdlib/type" | #"/docs/manual/api/stdlib/type/classify" diff --git a/packages/shared/src/Semver.res b/packages/shared/src/Semver.res index c0a6420f5..85cf63f52 100644 --- a/packages/shared/src/Semver.res +++ b/packages/shared/src/Semver.res @@ -67,9 +67,3 @@ let toString = ({major, minor, patch, preRelease}) => { `${mainVersion}-${identifier}` } } - -let tryGetMajorString = (versionStr: string) => - switch versionStr->parse { - | None => versionStr // fallback to given version if it cannot be parsed - | Some({major}) => "v" ++ major->Int.toString - } diff --git a/packages/shared/src/Semver.resi b/packages/shared/src/Semver.resi index f49e4c0b6..15ee058f4 100644 --- a/packages/shared/src/Semver.resi +++ b/packages/shared/src/Semver.resi @@ -14,5 +14,3 @@ type t = { let parse: string => option let toString: t => string - -let tryGetMajorString: string => string