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