Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 52 additions & 24 deletions apps/docs/app/routes/ApiDocs.res
Original file line number Diff line number Diff line change
Expand Up @@ -339,40 +339,69 @@ let make = (props: props) => {
module Data = {
type t = {
mainModule: Dict.t<JSON.t>,
tree: Dict.t<JSON.t>,
}

let dir = try {
Node.Path.resolve("data", "api")
} catch {
| _ => ""
}

let getVersion = (~moduleName: string) => {
open Node
open Node
let dir = Path.resolve("data", "api")

let moduleContent =
Fs.readFileSync(`markdown-pages/docs/api/${moduleName}.json`)->JSON.parseOrThrow
let versions = Fs.readdirSync(dir)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Defer filesystem enumeration to the loader

When a browser loads an API route, ApiRoute.default imports this module to render <ApiDocs>, so this top-level readdirSync is evaluated outside the loader. In this ssr: false app the browser has no Node fs; unlike the previous implementation, which deferred the filesystem read until getVersion and guarded its top-level path lookup, this can make the API bundle throw before rendering. Enumerate the versions inside the loader-only path instead.

AGENTS.md reference: AGENTS.md:L101-L104

Useful? React with 👍 / 👎.


let content = switch moduleContent {
| Object(dict) => dict->Some
| _ => None
let normalizeVersion = version =>
switch version {
| "latest" => Constants.versions.latest
| "next" => Constants.versions.next
| version => version
}

switch content {
| Some(content) => Some({mainModule: content, tree: Dict.make()})
| _ => None
let getVersion = (~version: string, ~moduleName: string) => {
let version = version->normalizeVersion
let selectedVersion = Semver.parse(version)->Option.getOrThrow

let latest =
versions
->Array.filterMap(v =>
switch Semver.parse(v) {
| Some(v) if v.major == selectedVersion.major => Some(v.raw)
| _ => None
}
)
->Array.toSorted((a, b) => Semver.rcompare(b, a)->Int.toFloat)
->Array.last
->Option.getOr(version)

let moduleFilePath = Path.join([dir, latest, `${moduleName}.json`])

if !Fs.existsSync(moduleFilePath) {
None
} else {
let moduleContent = Fs.readFileSync2(moduleFilePath, "utf-8")->JSON.parseOrThrow

let content = switch moduleContent {
| Object(dict) => dict->Some
| _ => None
}

switch content {
| Some(content) => Some({mainModule: content})
| _ => None
}
}
}
}

let processStaticProps = (~slug: array<string>) => {
let moduleName = slug->Belt.Array.getExn(0)
let processStaticProps = (~version: string, ~slug: array<string>) => {
let moduleName = switch slug->Array.get(0) {
| Some("belt") => "belt"
| Some("dom") => "dom"
| Some("js") => "js"
| Some("stdlib") => "stdlib"
| _ => "stdlib"
}
let modulePath = slug->Array.join("/")

let content =
// TODO post RR7: rename this to getByModuleName
Data.getVersion(~moduleName)
Data.getVersion(~version, ~moduleName)
->Option.map(data => data.mainModule)
->Option.flatMap(Dict.get(_, modulePath))

Expand Down Expand Up @@ -458,12 +487,11 @@ let processStaticProps = (~slug: array<string>) => {

Ok({module_, toctree: Obj.magic({name: "root", path: [], children: []})})

| None => Error(`Failed to get API Data for module ${moduleName}`)
| None => Error(`Failed to get API Data for module ${moduleName} in version ${version}`)
}
}

let getStaticProps = async slug => {
let result = processStaticProps(~slug)

let getStaticProps = async (~version, slug) => {
let result = processStaticProps(~version, ~slug)
{"props": result}
}
64 changes: 52 additions & 12 deletions apps/docs/app/routes/ApiRoute.res
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,51 @@ let rec rawApiItemToNode = (apiItem: apiItem): ApiDocs.node => {
@scope("JSON") @val
external parseApi: string => Dict.t<apiItem> = "parse"

type apiRequestPath = {
version: string,
slug: array<string>,
}

let normalizeVersion = version =>
switch version {
| "latest" => Constants.versions.latest
| "next" => Constants.versions.next
| version => version
}

let isVersionSegment = segment =>
segment === "latest" || segment === "next" || segment->Semver.parse->Option.isSome

let getApiRequestPath = (pathname: string): apiRequestPath => {
let segments = pathname->String.split("/")->Array.filter(segment => segment !== "")
let apiIndex = segments->Array.findIndex(segment => segment === "api")

let version = switch apiIndex {
| -1 => Constants.versions.latest
| index =>
switch segments->Array.get(index - 1) {
| Some(segment) if segment->isVersionSegment => segment->normalizeVersion
| _ => Constants.versions.latest
}
}

let slug = switch apiIndex {
| -1 => []
| index => segments->Array.slice(~start=index + 1)
}

{version, slug}
}

let getApiModuleName = slug =>
switch slug->Array.get(0) {
| Some("belt") => "belt"
| Some("dom") => "dom"
| Some("js") => "js"
| Some("stdlib") => "stdlib"
| _ => "stdlib"
}

let groupItems = apiDocs => {
let parsedItems =
apiDocs
Expand Down Expand Up @@ -122,26 +167,21 @@ let makeBreadcrumbs = (~prefix: Url.breadcrumb, route: Path.t): list<Url.breadcr
}

let loader: ReactRouter.Loader.t<loaderData> = async args => {
let path =
WebAPI.URL.make(~url=args.request.url).pathname
->String.replace("/docs/manual/api/", "")
->String.split("/")
let {pathname} = WebAPI.URL.make(~url=args.request.url)
let apiRequestPath = getApiRequestPath((pathname :> string))
let version = apiRequestPath.version
let path = apiRequestPath.slug
let basePath = path->getApiModuleName

let basePath = path[0]->Option.getUnsafe

let apiDocs = switch basePath {
| "belt" => parseApi(await Node.Fs.readFile("./markdown-pages/docs/api/belt.json", "utf-8"))
| "dom" => parseApi(await Node.Fs.readFile("./markdown-pages/docs/api/dom.json", "utf-8"))
| _ => parseApi(await Node.Fs.readFile("./markdown-pages/docs/api/stdlib.json", "utf-8"))
}
let apiDocs = parseApi(await Node.Fs.readFile(`data/api/${version}/${basePath}.json`, "utf-8"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Register routes from the versioned API data

When v13 becomes the selected documentation version, new modules in this dataset remain unreachable: this commit adds stdlib/taggedtemplate to data/api/v13.0.0/stdlib.json, but DocsRoutes.res lines 3–11 still generates the static route table from markdown-pages/docs/api/stdlib.json, where that key is absent. Consequently /docs/manual/api/stdlib/taggedtemplate falls through to the wildcard route instead of invoking this loader; generate API routes from the same versioned data source.

AGENTS.md reference: AGENTS.md:L5-L5

Useful? React with 👍 / 👎.


let toctree = groupItems(apiDocs)

let data = {
// TODO POST RR7: refactor this function to only return the module and not the toctree
// or move the toc logic to this function
try {
await ApiDocs.getStaticProps(path)
await ApiDocs.getStaticProps(~version, path)
} catch {
| err => {"props": Error(JSON.stringifyAny(err)->Option.getOr("Error loading API data"))}
}
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loading