Skip to content

ci(docs): #102-docs-strategy generate API reference with TypeDoc - #129

Open
elycruz wants to merge 1 commit into
mainfrom
102-docs-strategy
Open

ci(docs): #102-docs-strategy generate API reference with TypeDoc#129
elycruz wants to merge 1 commit into
mainfrom
102-docs-strategy

Conversation

@elycruz

@elycruz elycruz commented Jul 31, 2026

Copy link
Copy Markdown
Member

Summary

Chooses TypeDoc as the documentation rendering strategy for the monorepo, implements it, and adds a release-triggered GitHub Pages publish workflow.

All three candidates were run against this working tree, not evaluated from their docs. The result overturns the issue's initial recommendation.

Closes #102

Work unit: 102-docs-strategy


The finding that decided it

The repo already has an untracked docs/ directory at root, dated 2024-05-28, matched by the **/docs line in .gitignore. Its file signature (all_symbols.html, fuse.js, search_index.js, styles.css) identifies it as deno doc --html output — so the Deno path in this issue was not just proposed, it was tried.

It documents nothing:

// docs/search_index.js  (2024-05-28)
(function () {
  window.DENO_DOC_SEARCH_INDEX = {"nodes":[]};
})()
<!-- docs/all_symbols.html -->
<main><div class="space-y-7" id=""></div></main>

Reproduced today with Deno 2.4.2 — the tool exits successfully and emits a complete-looking 13-file site with zero symbols:

$ deno doc --html --name=fjl --output=./deno-out packages/fjl/src/index.ts
Written 13 files to "./deno-out"

$ grep -o '"name":"[^"]*"' ./deno-out/search_index.js | wc -l
0

Cause: fjl uses extensionless relative imports (import {reduce} from "../list/utils") throughout, which Deno's resolver rejects — silently, for deno doc. The maintainer's own note on origin/feat/#55/deno_support (node_scripts/tasks/fjl-deno.json.mjs, commit ad4076b5) confirms this was the known blocker; it's a stub whose whole body is a comment describing an unwritten script to synthesize deno.json.imports so "all source imports work as they are (without file extensions)".

Options, as actually run

deno doc

--unstable-sloppy-imports fixes the single-package case:

$ deno doc --unstable-sloppy-imports --html --name=fjl \
    --output=./deno-sloppy packages/fjl/src/index.ts
Written 465 files to "./deno-sloppy"

$ grep -o '"name":"[^"]*"' ./deno-sloppy/search_index.js | wc -l
452

The multi-package case fails outright:

$ deno doc --unstable-sloppy-imports --html --name=fjl --output=./deno-multi \
    packages/fjl/src/index.ts packages/fjl-validator/src/index.ts \
    packages/fjl-inputfilter/src/index.ts \
    packages/fjl-validator-recaptcha/src/index.ts

Warning Relative import path "querystring" not prefixed with / or ./ or ../
  hint: If you want to use a built-in Node module, add a "node:" prefix.
    at .../packages/fjl-validator-recaptcha/src/index.ts:10:25
Warning Relative import path "https" not prefixed with / or ./ or ../
    at .../packages/fjl-validator-recaptcha/src/index.ts:9:19
error: Failed resolving 'https' from '.../fjl-validator-recaptcha/src/index.ts'

No output. fjl-validator-recaptcha imports the Node builtins https and querystring without the node: prefix Deno requires. deno doc also has no concept of packages — multiple entry points flatten into one namespace.

api-extractor + api-documenter

Run for real against packages/fjl/dist/esm/index.d.ts after a full pnpm build:

$ pnpm dlx @microsoft/api-extractor@7 run --local --config ./api-extractor.probe.json
Warning: dist/esm/types/arity.d.ts:9:1 - (ae-missing-release-tag) "UnitNary" is
  part of the package's API, but it is missing a release tag (@alpha, @beta,
  @public, or @internal)
  ... 415 more ...
API Extractor completed successfully

$ ls -la ./ae-out/
-rw-r--r-- 832137 fjl.api.json

416 ae-missing-release-tag warnings for fjl alone; requires a build (and fjl-validator-recaptcha is commented out of rollup.config.mjs, so it can't be documented at all); produces JSON, not a site — rendering needs api-documenter (Markdown) plus an SSG.

TypeDoc

Clean on all four published packages, first attempt, from sources, no build, zero warnings. See evidence below.

Comparison

deno doc TypeDoc api-extractor + api-documenter
Documents all 4 published packages no — hard error on fjl-validator-recaptcha yes needs 1 config/package; fjl-validator-recaptcha impossible (not built)
Symbols extracted (single run) 452 (fjl only, w/ unstable flag) 489 across 4 packages 416+ (fjl only, as JSON)
Reads sources directly yes yes no — requires pnpm build first
Renders a browsable site yes yes no — JSON → Markdown → SSG
Multi-package structure flat, single namespace one module per package one isolated JSON per package
Extra toolchain in CI Deno runtime none — a devDependency none, but 3 tools
Unstable flags required --unstable-sloppy-imports none none
Warnings on this codebase resolution warnings 0 416
Wall time sub-second (fjl only) ~2s (all 4) ~30s + full build
Source changes required first node: prefixes; import map none 416 release-tag annotations

Decision: TypeDoc

  1. Only option that documents all four published packages. deno doc errors on fjl-validator-recaptcha; api-extractor can't see it because rollup doesn't build it.
  2. Needs no source changes to work today. The other two both require editing library sources before producing anything.
  3. Reads sources, not build outputpnpm run docs is independent of pnpm build, so it can't publish docs for a stale dist/.
  4. No new runtime in CI — a devDependency installed by the pnpm i the release workflow already runs, using the same TypeScript 5.4 as build and tests.
  5. Models the monorepo — four cross-linked modules/ entries.

Full reasoning: md/adr/0001-documentation-generation-strategy.md.

Known limitation, shared by every candidate

Neither TypeDoc nor deno doc picks up a doc comment placed inside a multi-declarator export const list:

export const

  /**
   * Filters given slice ... using given predicate (`pred`).
   */
  filter = <T>(pred: TernaryPred<T, number, T[]>, xs: T[]): T[] => { /* ... */ },

115 files use this shape and their prose is dropped by both tools equally — verified: fjl.filter renders without a description under TypeDoc and under deno doc, while fjl.mapObj (classic /** */ export const placement) renders correctly under both. It's a source-comment-placement problem, not a tool-selection one. Filed as follow-up 2 below.

Changes

  • typedoc.json — 4 entry points (one per published package), output docs-dist/, root README.md as the landing page, GitHub/npm nav links.
  • tsconfig.docs.json — docs-only tsconfig; paths maps fjl / fjl-validator / fjl-inputfilter / fjl-validator-recaptcha to their src/index.ts, so cross-package imports resolve without a build.
  • package.json — adds typedoc@^0.25.13 devDependency and a docs script. Additive only, to stay out of the way of the concurrent scripts edit.
  • pnpm-lock.yaml — regenerated for the new devDependency.
  • .gitignore / .eslintignore — ignore docs-dist/. Without the eslint entry, 502 generated files would be linted.
  • .github/workflows/docs.yml — new. release: [created] (matching publish.yml) + workflow_dispatch; Node 20 / pnpm 8 / HUSKY: 0, consistent with the existing workflows; pages: write + id-token: write OIDC permissions; concurrency: pages; a guard step that fails loudly if docs-dist/index.html is missing; upload-pages-artifactdeploy-pages.
  • md/adr/0001-documentation-generation-strategy.md — the ADR. New md/adr/ directory alongside the existing md/issues/.

Gotcha worth knowing

pnpm docs does not work — use pnpm run docs. pnpm forwards unrecognized commands to npm, and npm docs is a real command (opens a package homepage), so bare pnpm docs exits 0 and generates nothing:

$ pnpm docs
npm warn Unknown env config "manage-package-manager-versions". ...
$ ls docs-dist
ls: cannot access 'docs-dist': No such file or directory

The workflow uses pnpm run docs, and it's noted in the ADR and in a workflow comment.

Evidence the generator actually ran

$ rm -rf docs-dist && pnpm run docs

> fjl-monorepo@0.0.0 docs /home/edlc/workspace/functional-jslib/fjl
> typedoc

[info] Documentation generated at ./docs-dist
EXIT=0

$ find docs-dist -type f | wc -l
502
$ du -sh docs-dist
4.9M	docs-dist

$ ls docs-dist
assets  classes  enums  functions  hierarchy.html  index.html
interfaces  modules  types  variables

$ ls docs-dist/modules
fjl.html  fjlInputFilter.html  fjlValidator.html  fjlValidatorReCaptcha.html

$ for d in functions types interfaces variables classes enums; do \
    echo "  $d: $(ls docs-dist/$d | wc -l)"; done
  functions: 386
  types: 70
  interfaces: 18
  variables: 11
  classes: 2
  enums: 2

489 symbol pages across 4 modules. Rendered content spot-check (tags stripped):

### docs-dist/index.html
fjl | GitHub npm | fjl-mono-repo — Mono repo for libraries in the
functional-jslib org. Development / Building — Build commands are defined
on repo root. Testing — Test commands are defined on repo root.

### docs-dist/modules/fjlValidator.html
fjlValidator | fjl ... Module fjlValidator — Defined in
packages/fjl-validator/src/index.ts:1 — Index: Interfaces: LenValidatorMessage
Templates, LenValidatorOptions, MessageTemplates, RegexValidatorOptions,
ValidatorOptions, ValidatorResult — Type Aliases: MessageGetter,
MessageTemplateRValue, Validator — Functions: $alnumValidator, $digitValidator...

$-prefixed curried variants render under their real names ($filter, $map); only the filename is sanitized to _filter.html.

Verification

check result
pnpm run docs exit 0, 502 files, 0 warnings
pnpm test pass (via pre-push hook)
pnpm build exit 0 (via pre-push hook)
pnpm lint 0 errors, 62 warnings — unchanged from baseline; verified with docs-dist/ present, confirming the .eslintignore entry works
git status with docs-dist/ present clean — correctly gitignored
workflow YAML parses; jobs: [build-docs, deploy-docs], on: [release, workflow_dispatch]

No commit or push hooks were bypassed — commit-msg, pre-commit, and pre-push all ran and passed.

Note on the lint baseline: the 21 pre-existing errors reported on main come from docs/fuse.js, docs/script.js, and docs/search.js. That directory is matched by **/docs in .gitignore and is therefore untracked — it exists only on checkouts where it was generated locally in 2024, which is why a fresh worktree reports 0 errors. This PR doesn't change that either way.

Manual follow-up required

GitHub Pages must be enabled before docs.yml can deploy. Deliberately not done here — it's an outward-facing settings mutation on a public repo.

Click-path: Settings → Pages → Build and deployment → Source → "GitHub Actions"

Or, for a maintainer with admin scope:

gh api -X POST repos/functional-jslib/fjl/pages \
  -f 'build_type=workflow'

(Use -X PUT instead of -X POST if Pages is already enabled with a different source.)

Until that's done, build-docs succeeds and uploads the artifact while deploy-docs fails with a clear Pages-not-enabled error. The workflow is added, not triggered — nothing has been published as part of this PR.

Recommended follow-up issues

  1. Delete the legacy docs/ directory. It's deno doc output from 2024-05-28 containing zero symbols, and its vendored fuse.js / script.js / search.js are the source of most of the repo's eslint errors on checkouts where it exists. Left untouched here on purpose; the new generator writes to docs-dist/ specifically to avoid the collision.
  2. Normalize doc-comment placement across the 115 files that put /** */ inside a multi-declarator export const, so the prose reaches the rendered site. This is the single highest-value change for docs quality and it's renderer-independent.
  3. Add node: prefixes to https / querystring in packages/fjl-validator-recaptcha/src/index.ts — correct regardless of the docs decision, and it unblocks Deno consumers.
  4. Re-enable fjl-validator-recaptcha in rollup.config.mjs — it's published to npm but not built.
  5. Reconsider api-extractor for API-surface review, separately from docs: a reviewed .api.md report would catch accidental breaking changes at release time. That's its real strength, and a legitimate future need — just not the problem CI/CD: Choose documentation generating strategy #102 poses.

Chooses TypeDoc as the documentation rendering strategy, wires it up, and
adds a release-triggered GitHub Pages publish workflow.

All three candidates were run against this working tree:

- `deno doc` (the issue's initial suggestion) hard-fails on
  `fjl-validator-recaptcha`, which imports the Node builtins `https` and
  `querystring` without the `node:` prefix Deno requires. On `fjl` alone it
  needs `--unstable-sloppy-imports` because the sources use extensionless
  relative imports; without that flag it exits 0 and emits an empty site.
  That is exactly what the untracked `docs/` directory at repo root contains
  - `deno doc --html` output from 2024-05-28 with `{"nodes":[]}`.
- api-extractor works but reads `.d.ts`, so it needs a build first, and
  `fjl-validator-recaptcha` is commented out of `rollup.config.mjs`. It also
  emits 416 `ae-missing-release-tag` warnings for `fjl` alone and produces
  JSON, not a site.
- TypeDoc ran clean on all four published packages, from sources, with no
  build and zero warnings: 502 files, 489 symbol pages, 4 modules, ~2s.

Changes:

- add `typedoc` devDependency, `typedoc.json`, `tsconfig.docs.json`
- add root `docs` script (invoke as `pnpm run docs`; bare `pnpm docs` is
  swallowed by npm's built-in `docs` command)
- output to `docs-dist/`, gitignored and eslint-ignored, so it does not
  collide with the legacy `docs/` directory
- add `.github/workflows/docs.yml`, triggered on `release: [created]` to
  match `publish.yml`, plus `workflow_dispatch`
- record the comparison and decision in
  `md/adr/0001-documentation-generation-strategy.md`

The legacy `docs/` directory is deliberately left alone; its removal is
recorded as a follow-up in the ADR.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CI/CD: Choose documentation generating strategy

1 participant