Skip to content

Commit 0ea2c00

Browse files
authored
automatically sync API docs on rescript-lang.org (#7555)
* automatically sync API docs on rescript-lang.org * use GITHUB_TOKEN * rename package to get rid of constraints * build website on PR, push on release * format * try to sync API docs on PR * fix workspace name * add apiDocs folder * use deploy key * remove unused command * try pushing to website master * push only on release * add changelog * rename api docs job
1 parent f4c6214 commit 0ea2c00

File tree

13 files changed

+489
-5
lines changed

13 files changed

+489
-5
lines changed

.github/workflows/ci.yml

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ env:
2121

2222
jobs:
2323
build-compiler:
24+
outputs:
25+
api-docs-artifact-id: ${{ steps.upload-api-docs.outputs.artifact-id }}
2426
strategy:
2527
fail-fast: false
2628
matrix:
@@ -36,6 +38,7 @@ jobs:
3638
upload_binaries: true
3739
# Build the playground compiler and run the benchmarks on the fastest runner
3840
build_playground: true
41+
generate_api_docs: true
3942
benchmarks: true
4043
node-target: linux-arm64
4144
rust-target: aarch64-unknown-linux-musl
@@ -438,6 +441,18 @@ jobs:
438441
name: lib-ocaml
439442
path: lib/ocaml
440443

444+
- name: Generate API Docs
445+
if: ${{ matrix.generate_api_docs }}
446+
run: yarn apidocs:generate
447+
448+
- name: "Upload artifacts: scripts/res/apiDocs"
449+
id: upload-api-docs
450+
if: ${{ matrix.generate_api_docs }}
451+
uses: actions/upload-artifact@v4
452+
with:
453+
name: api-docs
454+
path: scripts/res/apiDocs/
455+
441456
pkg-pr-new:
442457
needs:
443458
- build-compiler
@@ -465,6 +480,48 @@ jobs:
465480
run: |
466481
yarn dlx pkg-pr-new publish "." "./packages/@rescript/*"
467482
483+
api-docs:
484+
needs:
485+
- build-compiler
486+
runs-on: ubuntu-24.04-arm
487+
steps:
488+
- name: Checkout rescript-lang.org
489+
uses: actions/checkout@v4
490+
with:
491+
repository: rescript-lang/rescript-lang.org
492+
ssh-key: ${{ secrets.RESCRIPT_LANG_ORG_DEPLOY_KEY }}
493+
494+
- name: Download artifacts
495+
uses: actions/download-artifact@v4
496+
with:
497+
artifact-ids: ${{ needs.build-compiler.outputs.api-docs-artifact-id }}
498+
path: data/api
499+
500+
- name: Check if repo is clean
501+
id: diffcheck
502+
run: |
503+
if [ -z "$(git status --porcelain)" ]; then
504+
echo "clean=true" >> $GITHUB_OUTPUT
505+
else
506+
echo "clean=false" >> $GITHUB_OUTPUT
507+
fi
508+
509+
- name: Build website
510+
if: steps.diffcheck.outputs.clean == 'false'
511+
run: |
512+
npm ci
513+
npx rescript
514+
npm run build
515+
516+
- name: Commit and push
517+
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
518+
run: |
519+
git config --global user.name "github-actions[bot]"
520+
git config --global user.email "github-actions@rescript-lang.org"
521+
git add data/api
522+
git commit -m "Update API docs for ${{ github.ref_name }}"
523+
git push
524+
468525
test-integration:
469526
needs:
470527
- pkg-pr-new

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
- 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
5656
- sync Reanalyze with the new APIs around exception. https://github.com/rescript-lang/rescript/pull/7536
5757
- Improve array pattern spread error message. https://github.com/rescript-lang/rescript/pull/7549
58+
- Sync API docs with rescript-lang.org on release. https://github.com/rescript-lang/rescript/pull/7555
5859

5960
#### :house: Internal
6061

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

6566
#### :boom: Breaking Change
67+
6668
- `Iterator.forEach` now emits `Iterator.prototype.forEach` call. https://github.com/rescript-lang/rescript/pull/7506
6769

6870
# 12.0.0-alpha.13

package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@
5252
"check:all": "biome check .",
5353
"format": "biome check --changed --no-errors-on-unmatched . --fix",
5454
"coverage": "nyc --timeout=3000 --reporter=html mocha tests/tests/src/*_test.js && open ./coverage/index.html",
55-
"typecheck": "tsc"
55+
"typecheck": "tsc",
56+
"apidocs:generate": "yarn workspace @utils/scripts apidocs:generate"
5657
},
5758
"files": [
5859
"CHANGELOG.md",
@@ -96,8 +97,10 @@
9697
"packages/@rescript/*",
9798
"tests/dependencies/**",
9899
"tests/analysis_tests/**",
100+
"tests/docstring_tests",
99101
"tests/gentype_tests/**",
100-
"tests/tools_tests"
102+
"tests/tools_tests",
103+
"scripts/res"
101104
],
102105
"packageManager": "yarn@4.9.1",
103106
"preferUnplugged": true

scripts/res/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
*.res.js
2+
lib
3+
apiDocs/**/*
4+
!.gitkeep

scripts/res/GenApiDocs.res

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
/***
2+
Generate API docs from ReScript Compiler
3+
4+
## Run
5+
6+
```bash
7+
node scripts/res/GenApiDocs.res.js
8+
```
9+
*/
10+
open Node
11+
module Docgen = RescriptTools.Docgen
12+
13+
let packagePath = Path.join([Node.dirname, "..", "..", "package.json"])
14+
let version = switch Fs.readFileSync(packagePath, ~encoding="utf8")->JSON.parseOrThrow {
15+
| Object(dict{"version": JSON.String(version)}) => version
16+
| _ => JsError.panic("Invalid package.json format")
17+
}
18+
let version = Semver.parse(version)->Option.getExn
19+
let version = Semver.toString({...version, preRelease: None}) // Remove pre-release identifiers for API docs
20+
let dirVersion = Path.join([Node.dirname, "apiDocs", version])
21+
if !Fs.existsSync(dirVersion) {
22+
Fs.mkdirSync(dirVersion)
23+
}
24+
25+
26+
let entryPointFiles = ["Belt.res", "Dom.res", "Js.res", "Stdlib.res"]
27+
28+
let hiddenModules = ["Js.Internal", "Js.MapperRt"]
29+
30+
type module_ = {
31+
id: string,
32+
docstrings: array<string>,
33+
name: string,
34+
items: array<Docgen.item>,
35+
}
36+
37+
type section = {
38+
name: string,
39+
docstrings: array<string>,
40+
deprecated: option<string>,
41+
topLevelItems: array<Docgen.item>,
42+
submodules: array<module_>,
43+
}
44+
45+
let env = Process.env
46+
47+
let docsDecoded = entryPointFiles->Array.map(libFile =>
48+
try {
49+
let entryPointFile = Path.join([Node.dirname, "..", "..", "runtime", libFile])
50+
51+
let rescriptToolsPath = Path.join([Node.dirname, "..", "..", "cli", "rescript-tools.js"])
52+
let output = ChildProcess.execSync(
53+
`${rescriptToolsPath} doc ${entryPointFile}`,
54+
~options={
55+
maxBuffer: 30_000_000.,
56+
},
57+
)->Buffer.toString
58+
59+
let docs = output
60+
->JSON.parseOrThrow
61+
->Docgen.decodeFromJson
62+
Console.log(`Generated docs from ${libFile}`)
63+
docs
64+
} catch {
65+
| JsExn(exn) =>
66+
Console.error(
67+
`Error while generating docs from ${libFile}: ${exn
68+
->JsExn.message
69+
->Option.getOr("[no message]")}`,
70+
)
71+
JsExn.throw(exn)
72+
}
73+
)
74+
75+
let removeStdlibOrPrimitive = s => s->String.replaceAllRegExp(/Stdlib_|Primitive_js_extern\./g, "")
76+
77+
let docs = docsDecoded->Array.map(doc => {
78+
let topLevelItems = doc.items->Array.filterMap(item =>
79+
switch item {
80+
| Value(_) as item | Type(_) as item => item->Some
81+
| _ => None
82+
}
83+
)
84+
85+
let rec getModules = (lst: list<Docgen.item>, moduleNames: list<module_>) =>
86+
switch lst {
87+
| list{
88+
Module({id, items, name, docstrings})
89+
| ModuleAlias({id, items, name, docstrings})
90+
| ModuleType({id, items, name, docstrings}),
91+
...rest,
92+
} =>
93+
if Array.includes(hiddenModules, id) {
94+
getModules(rest, moduleNames)
95+
} else {
96+
getModules(
97+
list{...rest, ...List.fromArray(items)},
98+
list{{id, items, name, docstrings}, ...moduleNames},
99+
)
100+
}
101+
| list{Type(_) | Value(_), ...rest} => getModules(rest, moduleNames)
102+
| list{} => moduleNames
103+
}
104+
105+
let id = doc.name
106+
107+
let top = {id, name: id, docstrings: doc.docstrings, items: topLevelItems}
108+
let submodules = getModules(doc.items->List.fromArray, list{})->List.toArray
109+
let result = [top]->Array.concat(submodules)
110+
111+
(id, result)
112+
})
113+
114+
let allModules = {
115+
open JSON
116+
let encodeItem = (docItem: Docgen.item) => {
117+
switch docItem {
118+
| Value({id, name, docstrings, signature, ?deprecated}) => {
119+
let dict = Dict.fromArray(
120+
[
121+
("id", id->String),
122+
("kind", "value"->String),
123+
("name", name->String),
124+
(
125+
"docstrings",
126+
docstrings
127+
->Array.map(s => s->removeStdlibOrPrimitive->String)
128+
->Array,
129+
),
130+
(
131+
"signature",
132+
signature
133+
->removeStdlibOrPrimitive
134+
->String,
135+
),
136+
]->Array.concat(
137+
switch deprecated {
138+
| Some(v) => [("deprecated", v->String)]
139+
| None => []
140+
},
141+
),
142+
)
143+
dict->Object->Some
144+
}
145+
146+
| Type({id, name, docstrings, signature, ?deprecated}) =>
147+
let dict = Dict.fromArray(
148+
[
149+
("id", id->String),
150+
("kind", "type"->String),
151+
("name", name->String),
152+
("docstrings", docstrings->Array.map(s => s->removeStdlibOrPrimitive->String)->Array),
153+
("signature", signature->removeStdlibOrPrimitive->String),
154+
]->Array.concat(
155+
switch deprecated {
156+
| Some(v) => [("deprecated", v->String)]
157+
| None => []
158+
},
159+
),
160+
)
161+
Object(dict)->Some
162+
163+
| _ => None
164+
}
165+
}
166+
167+
docs->Array.map(((topLevelName, modules)) => {
168+
let submodules =
169+
modules
170+
->Array.map(mod => {
171+
let items =
172+
mod.items
173+
->Array.filterMap(item => encodeItem(item))
174+
->Array
175+
176+
let rest = Dict.fromArray([
177+
("id", mod.id->String),
178+
("name", mod.name->String),
179+
("docstrings", mod.docstrings->Array.map(s => s->String)->Array),
180+
("items", items),
181+
])
182+
(
183+
mod.id
184+
->String.split(".")
185+
->Array.join("/")
186+
->String.toLowerCase,
187+
rest->Object,
188+
)
189+
})
190+
->Dict.fromArray
191+
192+
(topLevelName, submodules)
193+
})
194+
}
195+
196+
let () = {
197+
allModules->Array.forEach(((topLevelName, mod)) => {
198+
let json = JSON.Object(mod)
199+
200+
Fs.writeFileSync(
201+
Path.join([dirVersion, `${topLevelName->String.toLowerCase}.json`]),
202+
json->JSON.stringify(~space=2),
203+
)
204+
})
205+
}
206+
207+
type rec node = {
208+
name: string,
209+
path: array<string>,
210+
children: array<node>,
211+
}
212+
213+
// Generate TOC modules
214+
let () = {
215+
let joinPath = (~path: array<string>, ~name: string) => {
216+
Array.concat(path, [name])->Array.map(path => path->String.toLowerCase)
217+
}
218+
let rec getModules = (lst: list<Docgen.item>, moduleNames, path) => {
219+
switch lst {
220+
| list{
221+
Module({id, items, name}) | ModuleAlias({id, items, name}) | ModuleType({id, items, name}),
222+
...rest,
223+
} =>
224+
if Array.includes(hiddenModules, id) {
225+
getModules(rest, moduleNames, path)
226+
} else {
227+
let itemsList = items->List.fromArray
228+
let children = getModules(itemsList, [], joinPath(~path, ~name))
229+
230+
getModules(
231+
rest,
232+
Array.concat([{name, path: joinPath(~path, ~name), children}], moduleNames),
233+
path,
234+
)
235+
}
236+
| list{Type(_) | Value(_), ...rest} => getModules(rest, moduleNames, path)
237+
| list{} => moduleNames
238+
}
239+
}
240+
241+
let tocTree = docsDecoded->Array.map(({name, items}) => {
242+
let path = name->String.toLowerCase
243+
(
244+
path,
245+
{
246+
name,
247+
path: [path],
248+
children: items
249+
->List.fromArray
250+
->getModules([], [path]),
251+
},
252+
)
253+
})
254+
255+
Fs.writeFileSync(
256+
Path.join([dirVersion, "toc_tree.json"]),
257+
tocTree
258+
->Dict.fromArray
259+
->JSON.stringifyAny
260+
->Option.getExn,
261+
)
262+
Console.log("Generated toc_tree.json")
263+
Console.log(`API docs generated successfully in ${dirVersion}`)
264+
}

0 commit comments

Comments
 (0)