Skip to content

Commit 7307d4c

Browse files
committed
automatically sync API docs on rescript-lang.org
1 parent 35c8f0e commit 7307d4c

File tree

11 files changed

+491
-5
lines changed

11 files changed

+491
-5
lines changed

.github/workflows/ci.yml

Lines changed: 61 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 && startsWith(github.ref, 'refs/tags/v') }}
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,52 @@ jobs:
465480
run: |
466481
yarn dlx pkg-pr-new publish "." "./packages/@rescript/*"
467482
483+
sync-api-docs:
484+
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
485+
needs:
486+
- build-compiler
487+
runs-on: ubuntu-24.04-arm
488+
steps:
489+
- name: Checkout rescript-lang.org
490+
uses: actions/checkout@v4
491+
with:
492+
repository: rescript-lang/rescript-lang.org
493+
token: ${{ secrets.RESCRIPT_LANG_ORG_REPO_TOKEN }}
494+
495+
- name: Set up Git config
496+
run: |
497+
git config --global user.name "github-actions[bot]"
498+
git config --global user.email "github-actions[bot]@users.noreply.github.com"
499+
500+
- name: Authenticate GitHub CLI
501+
env:
502+
GH_TOKEN: ${{ secrets.RESCRIPT_LANG_ORG_REPO_TOKEN }}
503+
run: echo "$GH_TOKEN" | gh auth login --with-token
504+
505+
- name: create-new-branch
506+
run: git checkout -b sync-api-docs-${{ github.ref_name }}
507+
508+
- name: Download artifacts
509+
uses: actions/download-artifact@v4
510+
with:
511+
artifact-ids: ${{ needs.build-compiler.outputs.api-docs-artifact-id }}
512+
path: data/api
513+
514+
- name: Commit and push
515+
run: |
516+
git add data/api
517+
git commit -m "Update API docs for ${{ github.ref_name }}"
518+
git push --set-upstream origin sync-api-docs-${{ github.ref_name }}
519+
520+
- name: Create Pull Request
521+
run: |
522+
cd repo-b
523+
gh pr create \
524+
--title "Sync API docs for ${{ github.ref_name }}" \
525+
--body "Sync API docs for ${{ github.ref_name }}. This PR was created via GitHub Actions using gh CLI." \
526+
--head sync-api-docs-${{ github.ref_name }} \
527+
--base master
528+
468529
test-integration:
469530
needs:
470531
- pkg-pr-new

package.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,9 @@
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+
"scripts:build": "rescript",
57+
"apidocs:generate": "yarn workspace @rescript/scripts apidocs:generate"
5658
},
5759
"files": [
5860
"CHANGELOG.md",
@@ -96,8 +98,10 @@
9698
"packages/@rescript/*",
9799
"tests/dependencies/**",
98100
"tests/analysis_tests/**",
101+
"tests/docstring_tests",
99102
"tests/gentype_tests/**",
100-
"tests/tools_tests"
103+
"tests/tools_tests",
104+
"scripts/res"
101105
],
102106
"packageManager": "[email protected]",
103107
"preferUnplugged": true

scripts/res/.gitignore

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

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)