Skip to content

automatically sync API docs on rescript-lang.org #7555

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Jun 17, 2025
Merged
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
57 changes: 57 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ env:

jobs:
build-compiler:
outputs:
api-docs-artifact-id: ${{ steps.upload-api-docs.outputs.artifact-id }}
strategy:
fail-fast: false
matrix:
Expand All @@ -36,6 +38,7 @@ jobs:
upload_binaries: true
# Build the playground compiler and run the benchmarks on the fastest runner
build_playground: true
generate_api_docs: true
benchmarks: true
node-target: linux-arm64
rust-target: aarch64-unknown-linux-musl
Expand Down Expand Up @@ -438,6 +441,18 @@ jobs:
name: lib-ocaml
path: lib/ocaml

- name: Generate API Docs
if: ${{ matrix.generate_api_docs }}
run: yarn apidocs:generate

- name: "Upload artifacts: scripts/res/apiDocs"
id: upload-api-docs
if: ${{ matrix.generate_api_docs }}
uses: actions/upload-artifact@v4
with:
name: api-docs
path: scripts/res/apiDocs/

pkg-pr-new:
needs:
- build-compiler
Expand Down Expand Up @@ -465,6 +480,48 @@ jobs:
run: |
yarn dlx pkg-pr-new publish "." "./packages/@rescript/*"

api-docs:
needs:
- build-compiler
runs-on: ubuntu-24.04-arm
steps:
- name: Checkout rescript-lang.org
uses: actions/checkout@v4
with:
repository: rescript-lang/rescript-lang.org
ssh-key: ${{ secrets.RESCRIPT_LANG_ORG_DEPLOY_KEY }}

- name: Download artifacts
uses: actions/download-artifact@v4
with:
artifact-ids: ${{ needs.build-compiler.outputs.api-docs-artifact-id }}
path: data/api

- name: Check if repo is clean
id: diffcheck
run: |
if [ -z "$(git status --porcelain)" ]; then
echo "clean=true" >> $GITHUB_OUTPUT
else
echo "clean=false" >> $GITHUB_OUTPUT
fi

- name: Build website
if: steps.diffcheck.outputs.clean == 'false'
run: |
npm ci
npx rescript
npm run build

- name: Commit and push
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "[email protected]"
git add data/api
git commit -m "Update API docs for ${{ github.ref_name }}"
git push

test-integration:
needs:
- pkg-pr-new
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
- Improve error messages around type mismatches for try/catch, if, for, while, and optional record fields + optional function arguments. https://github.com/rescript-lang/rescript/pull/7522
- sync Reanalyze with the new APIs around exception. https://github.com/rescript-lang/rescript/pull/7536
- Improve array pattern spread error message. https://github.com/rescript-lang/rescript/pull/7549
- Sync API docs with rescript-lang.org on release. https://github.com/rescript-lang/rescript/pull/7555

#### :house: Internal

Expand All @@ -63,6 +64,7 @@
- Add `-editor-mode` arg to `bsc` for doing special optimizations only relevant to the editor tooling. https://github.com/rescript-lang/rescript/pull/7541

#### :boom: Breaking Change

- `Iterator.forEach` now emits `Iterator.prototype.forEach` call. https://github.com/rescript-lang/rescript/pull/7506

# 12.0.0-alpha.13
Expand Down
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@
"check:all": "biome check .",
"format": "biome check --changed --no-errors-on-unmatched . --fix",
"coverage": "nyc --timeout=3000 --reporter=html mocha tests/tests/src/*_test.js && open ./coverage/index.html",
"typecheck": "tsc"
"typecheck": "tsc",
"apidocs:generate": "yarn workspace @utils/scripts apidocs:generate"
},
"files": [
"CHANGELOG.md",
Expand Down Expand Up @@ -96,8 +97,10 @@
"packages/@rescript/*",
"tests/dependencies/**",
"tests/analysis_tests/**",
"tests/docstring_tests",
"tests/gentype_tests/**",
"tests/tools_tests"
"tests/tools_tests",
"scripts/res"
],
"packageManager": "[email protected]",
"preferUnplugged": true
Expand Down
4 changes: 4 additions & 0 deletions scripts/res/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
*.res.js
lib
apiDocs/**/*
!.gitkeep
264 changes: 264 additions & 0 deletions scripts/res/GenApiDocs.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
/***
Generate API docs from ReScript Compiler

## Run

```bash
node scripts/res/GenApiDocs.res.js
```
*/
open Node
module Docgen = RescriptTools.Docgen

let packagePath = Path.join([Node.dirname, "..", "..", "package.json"])
let version = switch Fs.readFileSync(packagePath, ~encoding="utf8")->JSON.parseOrThrow {
| Object(dict{"version": JSON.String(version)}) => version
| _ => JsError.panic("Invalid package.json format")
}
let version = Semver.parse(version)->Option.getExn
let version = Semver.toString({...version, preRelease: None}) // Remove pre-release identifiers for API docs
let dirVersion = Path.join([Node.dirname, "apiDocs", version])
if !Fs.existsSync(dirVersion) {
Fs.mkdirSync(dirVersion)
}


let entryPointFiles = ["Belt.res", "Dom.res", "Js.res", "Stdlib.res"]

let hiddenModules = ["Js.Internal", "Js.MapperRt"]

type module_ = {
id: string,
docstrings: array<string>,
name: string,
items: array<Docgen.item>,
}

type section = {
name: string,
docstrings: array<string>,
deprecated: option<string>,
topLevelItems: array<Docgen.item>,
submodules: array<module_>,
}

let env = Process.env

let docsDecoded = entryPointFiles->Array.map(libFile =>
try {
let entryPointFile = Path.join([Node.dirname, "..", "..", "runtime", libFile])

let rescriptToolsPath = Path.join([Node.dirname, "..", "..", "cli", "rescript-tools.js"])
let output = ChildProcess.execSync(
`${rescriptToolsPath} doc ${entryPointFile}`,
~options={
maxBuffer: 30_000_000.,
},
)->Buffer.toString

let docs = output
->JSON.parseOrThrow
->Docgen.decodeFromJson
Console.log(`Generated docs from ${libFile}`)
docs
} catch {
| JsExn(exn) =>
Console.error(
`Error while generating docs from ${libFile}: ${exn
->JsExn.message
->Option.getOr("[no message]")}`,
)
JsExn.throw(exn)
}
)

let removeStdlibOrPrimitive = s => s->String.replaceAllRegExp(/Stdlib_|Primitive_js_extern\./g, "")

let docs = docsDecoded->Array.map(doc => {
let topLevelItems = doc.items->Array.filterMap(item =>
switch item {
| Value(_) as item | Type(_) as item => item->Some
| _ => None
}
)

let rec getModules = (lst: list<Docgen.item>, moduleNames: list<module_>) =>
switch lst {
| list{
Module({id, items, name, docstrings})
| ModuleAlias({id, items, name, docstrings})
| ModuleType({id, items, name, docstrings}),
...rest,
} =>
if Array.includes(hiddenModules, id) {
getModules(rest, moduleNames)
} else {
getModules(
list{...rest, ...List.fromArray(items)},
list{{id, items, name, docstrings}, ...moduleNames},
)
}
| list{Type(_) | Value(_), ...rest} => getModules(rest, moduleNames)
| list{} => moduleNames
}

let id = doc.name

let top = {id, name: id, docstrings: doc.docstrings, items: topLevelItems}
let submodules = getModules(doc.items->List.fromArray, list{})->List.toArray
let result = [top]->Array.concat(submodules)

(id, result)
})

let allModules = {
open JSON
let encodeItem = (docItem: Docgen.item) => {
switch docItem {
| Value({id, name, docstrings, signature, ?deprecated}) => {
let dict = Dict.fromArray(
[
("id", id->String),
("kind", "value"->String),
("name", name->String),
(
"docstrings",
docstrings
->Array.map(s => s->removeStdlibOrPrimitive->String)
->Array,
),
(
"signature",
signature
->removeStdlibOrPrimitive
->String,
),
]->Array.concat(
switch deprecated {
| Some(v) => [("deprecated", v->String)]
| None => []
},
),
)
dict->Object->Some
}

| Type({id, name, docstrings, signature, ?deprecated}) =>
let dict = Dict.fromArray(
[
("id", id->String),
("kind", "type"->String),
("name", name->String),
("docstrings", docstrings->Array.map(s => s->removeStdlibOrPrimitive->String)->Array),
("signature", signature->removeStdlibOrPrimitive->String),
]->Array.concat(
switch deprecated {
| Some(v) => [("deprecated", v->String)]
| None => []
},
),
)
Object(dict)->Some

| _ => None
}
}

docs->Array.map(((topLevelName, modules)) => {
let submodules =
modules
->Array.map(mod => {
let items =
mod.items
->Array.filterMap(item => encodeItem(item))
->Array

let rest = Dict.fromArray([
("id", mod.id->String),
("name", mod.name->String),
("docstrings", mod.docstrings->Array.map(s => s->String)->Array),
("items", items),
])
(
mod.id
->String.split(".")
->Array.join("/")
->String.toLowerCase,
rest->Object,
)
})
->Dict.fromArray

(topLevelName, submodules)
})
}

let () = {
allModules->Array.forEach(((topLevelName, mod)) => {
let json = JSON.Object(mod)

Fs.writeFileSync(
Path.join([dirVersion, `${topLevelName->String.toLowerCase}.json`]),
json->JSON.stringify(~space=2),
)
})
}

type rec node = {
name: string,
path: array<string>,
children: array<node>,
}

// Generate TOC modules
let () = {
let joinPath = (~path: array<string>, ~name: string) => {
Array.concat(path, [name])->Array.map(path => path->String.toLowerCase)
}
let rec getModules = (lst: list<Docgen.item>, moduleNames, path) => {
switch lst {
| list{
Module({id, items, name}) | ModuleAlias({id, items, name}) | ModuleType({id, items, name}),
...rest,
} =>
if Array.includes(hiddenModules, id) {
getModules(rest, moduleNames, path)
} else {
let itemsList = items->List.fromArray
let children = getModules(itemsList, [], joinPath(~path, ~name))

getModules(
rest,
Array.concat([{name, path: joinPath(~path, ~name), children}], moduleNames),
path,
)
}
| list{Type(_) | Value(_), ...rest} => getModules(rest, moduleNames, path)
| list{} => moduleNames
}
}

let tocTree = docsDecoded->Array.map(({name, items}) => {
let path = name->String.toLowerCase
(
path,
{
name,
path: [path],
children: items
->List.fromArray
->getModules([], [path]),
},
)
})

Fs.writeFileSync(
Path.join([dirVersion, "toc_tree.json"]),
tocTree
->Dict.fromArray
->JSON.stringifyAny
->Option.getExn,
)
Console.log("Generated toc_tree.json")
Console.log(`API docs generated successfully in ${dirVersion}`)
}
Loading