feat(build-tsdoc)!: pivot to api-extractor (0.2.0)#22
Conversation
This repo is TSDoc tooling, so identifiers carry "doc" or "docs" throughout, and "dir" has a single meaning here (directory). unicorn/prevent-abbreviations rewrites all three by default (doc→document, docs→documents, dir→directory) — whitelist them so the short forms pass unchanged. @poupe/eslint-config 0.9.2 exposes withAbbreviations(tokens), which re-emits its poupe/unicorn block with the given tokens added to allowList and pinned to false in replacements. The root eslint.config.ts calls it with ['dir', 'doc', 'docs']. ESLint flat config is nearest-wins, not merged, so a per-package config shadows the root rather than extending it. The package config therefore repeats the whitelist: packages/@kagal-build-tsdoc/eslint.config.mjs becomes eslint.config.ts so it can import withAbbreviations and apply the same tokens. Signed-off-by: Alejandro Mery <amery@apptly.co>
Drop the `v[0-9]*` umbrella tag pattern. The repo-wide
release model from "chore(ci): use npm-style per-package
release tags" is no longer used — each package now ships
on its own cadence under `@scope/name@version`, so there
is no scenario where a single tag should publish more
than one package.
Switch the concurrency group from a single global
`publish` to `publish-${{ github.ref_name }}`. With a
shared group, two simultaneous package tag pushes
serialised against each other; per-ref grouping lets
independent packages release in parallel while still
preventing a duplicate run on the same tag.
Replace `pnpm -r publish:maybe` with
`pnpm --fail-if-no-match --filter "${TAG%@*}"
publish:maybe`, where `${TAG%@*}` strips the trailing
`@<version>` from the tag (`@kagal/build-tsdoc@0.1.0`
→ `@kagal/build-tsdoc`). The runner now publishes
exactly the package whose tag fired the workflow,
instead of walking every package; `--fail-if-no-match`
turns a tag that resolves to no package into a failed
run rather than a silent no-op. The republish guard is
unchanged — each package's `publish:maybe` skips when
`npm view $name@$version` already lists the version.
Reflow AGENTS.md §Publishing to describe the
per-package tag form as the only pattern, the per-ref
concurrency behaviour, and the filtered publish step.
Signed-off-by: Alejandro Mery <amery@apptly.co>
Replace the `tsdoc-markdown` engine and the unbuild-shaped
hook with `@microsoft/api-extractor` and a single adapter
function on top of it.
`extractEntryManifest({ projectFolder, entryName?,
entryFile?, outDir?, outputPath?, tsconfigPath?,
packageFullPath? })`
runs api-extractor against one rolled declaration file and
writes `<entryName>.api.json` in `@microsoft/api-extractor-model`'s
wire format. Defaults derive from `projectFolder` and
`entryName` (`dist/<entryName>.d.mts` in,
`dist/<entryName>.api.json` out, root `tsconfig.json`,
root `package.json`); each override slots in independently
for non-standard layouts. Missing `entryFile` returns
`undefined` so callers can invoke unconditionally from a
build hook — stub builds skip it. api-extractor errors throw;
warnings come back in `warningCount`.
Runtime dependencies from the manifest are passed as
api-extractor's `bundledPackages`, so a symbol re-exported
from a dependency is inlined into the doc model as part of
this package instead of dropped as a foreign reference.
Output moves from `_docs/` to `dist/`, so manifests ship with
the package via the existing `files: ["dist"]`. Consumers load
with `ApiPackage.loadFromJsonFile()` instead of parsing a
custom wrapper.
Bundler coupling is gone. No `BuildContext` parameter, no
`unbuild` peer dep, no per-entry iteration inside the helper —
the caller wires its own post-build hook and loops over its
own entries. The package's own `build.config.ts` demonstrates
the pattern against unbuild and dogfoods the new export.
Knock-on removals:
- `newDocumentsHook()`, `DocumentsManifest`, `ExportManifest`,
`DocumentsHookOptions`, `DocEntry` types — superseded by
api-extractor's wire format.
- `DuplicateExportPathError`, `DuplicateOutputFileError` —
collision detection now belongs to the caller (only it
knows the entry-name to output-filename mapping).
- `DEFAULT_OUTPUT_DIRECTORY` — output is always under `dist/`.
- `_docs/` directory, unified `api.json` index, `api_<name>.json`
prefix scheme — replaced by per-entry `<entryName>.api.json`.
- `tsdoc-markdown` runtime dep; `unbuild` peer dep.
- `src/types.ts`, `src/write.ts`, `src/errors.ts` — folded
into a single `src/extract.ts` exporting the helper plus
`ExtractEntryOptions` / `ExtractEntryResult`.
- The legacy directory-walking fixture tree
(`src/__tests__/fixtures/sample.ts` and `sample-dir/`) — no
longer wired up.
Compat smoke test (`src/__tests__/compat.mjs`) and Vitest
suite shrink to the new surface: `extractEntryManifest` is
callable, `VERSION` matches `package.json`, removed exports
are absent. A fixture-driven api-extractor invocation belongs
in a follow-up — the current suite covers the public-surface
shape only.
CHANGELOG `[Unreleased]` documents the breaking pivot under
Changed / Removed / Added; `package.json` description,
keywords (`api-extractor`, `api-extractor-model`,
`build-hook`), and dependency block update to match.
`AGENTS.md`, the root `README.md`, and the package
`README.md` reflow the package summary, pipeline diagram,
source layout, and the build-time dogfood note.
Signed-off-by: Alejandro Mery <amery@apptly.co>
Loop unbuild's bundler-resolved entries and call `extractEntryManifest` per entry from a factory-built hook map. The map is keyed by hook name (`build:done`) so it spreads straight into `defineBuildConfig`'s `hooks`; to combine extraction with other post-build steps, callers keep the map in a variable and wrap its hook. Entries must be the bundler's own — they carry the data the hook detects from. unbuild resolves `name` and `outDir` before hooks fire, so the hook requires `name` and rejects hand-written name lists with `InvalidBuildEntryError`. Stub builds are skipped via `options.stub`; per-entry `outDir` is honoured. The shim lives in `unbuild.ts` with structural interfaces declaring only the fields the hook reads — unbuild is not imported at type or runtime. The discriminator cast (`asUnbuildContext`) verifies those reads at the boundary so a misroute surfaces as `UnrecognisedBuildContextError` instead of a cryptic `TypeError` deeper in the loop. Error classes sit in `errors.ts`, a leaf module. `DuplicateEntryNameError` fires when two entries resolve to the same entry name; single-entry `extractEntryManifest` retains no list-level checks — callers iterating directly own that path. The package's own `build.config.ts` dogfoods the factory via the spread. A vitest suite covers the discriminator, the hook's dispatch and skip/throw paths, and collision detection; the compat smoke test gains rows for the new exports. CHANGELOG `[Unreleased]`, README, and AGENTS.md reflect the new surface. Signed-off-by: Alejandro Mery <amery@apptly.co>
obuild's `end` hook context is only `{ pkg, pkgDir }` and the
resolved entries (absolute paths, effective `stub` flags) are
visible only to the `entries` hook, so extraction needs both:
`newOBuildHooks()` returns a pair keyed by hook name —
`entries` captures them in a closure, `end` extracts the
capture and throws `HooksNotWiredError` when `entries` never
fired. Like the unbuild map, the pair spreads straight into
`hooks`; wrapping `end` combines extraction with other
post-build steps.
obuild entries carry no `name`, so one derives per `input` the
way rolldown names chunks: basename minus the final extension.
Stub and `'transform'` entries are skipped (per-file
declarations, no rollup); per-entry `outDir` is honoured;
hand-written name lists and raw string entries are rejected
with `InvalidBuildEntryError`.
The shim mirrors `unbuild.ts`: structural interfaces in
`obuild.ts` declaring only the fields the hooks read — obuild
is not imported at type or runtime — with the `asOBuildContext`
discriminator verifying those reads at the boundary.
A vitest suite covers the discriminator, the hook pair's
capture/extract cycle, skip/throw paths, and collision
detection; the compat smoke test gains rows for the new
exports.
Adds `obuild` to package keywords. CHANGELOG `[Unreleased]`,
README, and AGENTS.md gain the obuild surface, including the
wrapped-`end` extended-use example.
Signed-off-by: Alejandro Mery <amery@apptly.co>
Bump @kagal/build-tsdoc to 0.2.0 and promote the existing [Unreleased] changelog entries under the new version with a 2026-06-09 release date, keeping an empty [Unreleased] placeholder on top. 0.2.0 is a breaking release: it replaces the tsdoc-markdown engine with @microsoft/api-extractor, drops the unbuild peer coupling, and ships api manifests in dist/ rather than _docs/. Runtime dependencies are bundled, so symbols re-exported from a dependency are documented as part of the package itself. After this PR merges, tagging the merge commit v0.2.0 triggers publish.yml, which authenticates to npm via OIDC and publishes with Sigstore provenance. Signed-off-by: Alejandro Mery <amery@apptly.co>
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (31)
💤 Files with no reviewable changes (10)
📝 WalkthroughWalkthroughThis PR replaces ChangesAPI-Extractor Adapter Migration
🎯 4 (Complex) | ⏱️ ~60 minutes Possibly Related PRs
Suggested Labels
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Summary
@kagal/build-tsdoc0.2.0 replaces thetsdoc-markdownenginewith
@microsoft/api-extractor. The package becomes a thin,bundler-agnostic adapter over api-extractor: it runs the
extractor against one rolled declaration file per entry and
writes
<entryName>.api.jsonin@microsoft/api-extractor-model'swire format, loadable directly with
ApiPackage.loadFromJsonFile().This is a breaking release. The previous
newDocumentsHook()/DocumentsManifestsurface and the custom_docs/JSON wrapperare gone, and the package no longer couples to a bundler.
Breaking changes & migration
old
DocumentsManifest. Load it withApiPackage.loadFromJsonFile()from
@microsoft/api-extractor-modelinstead of parsing a customwrapper.
_docs/→dist/, so manifests ship withthe package via the existing
files: ["dist"].newDocumentsHook(),DocumentsManifest,ExportManifest,DocumentsHookOptions,DocEntry,DuplicateExportPathError,DuplicateOutputFileError,DEFAULT_OUTPUT_DIRECTORY. Collision detection now belongs to thecaller (only it knows the entry-name → output-file mapping).
tsdoc-markdownruntime dependency dropped; nomore
unbuildpeer dependency.New surface
extractEntryManifest(options)— single-entry adapter.Options:
projectFolder(required), thenentryName?,entryFile?,outDir?,outputPath?,tsconfigPath?,packageFullPath?, each slotting in independently fornon-standard layouts. Defaults derive from
projectFolder/entryName(dist/<entryName>.d.mtsin,dist/<entryName>.api.jsonout, root
tsconfig.json+package.json). Returnsundefinedwhen the entry file is missing, so a build hook can call it
unconditionally — stub builds skip. Runtime dependencies are
passed as api-extractor
bundledPackages, so a symbol re-exportedfrom a dependency is inlined into the model as part of this
package rather than dropped as foreign.
emits every entry point with an empty import path, so per-entry
manifests would all collide on
@scope/pkg!when a consumermerges them. Non-
indexentries graft their name onto the entrypoint's import path, rebuilding member references as
@scope/pkg/<entry>!(the stored excerpt-token links arerewritten to match). The package name is untouched — consumers
key by the entry point.
or runtime; each shim declares only the fields its hooks read,
with a boundary discriminator (
asUnbuildContext/asOBuildContext) so a misroute surfaces asUnrecognisedBuildContextErrorrather than a deeperTypeError.newUnbuildHooks()— hook map keyed bybuild:done; spreadsstraight into unbuild's
hooks.newOBuildHooks()— hook pair (entries+end) for obuild,whose
endcontext carries no resolved entries;entriescaptures them in a closure, and
endthrowsHooksNotWiredErrorif
entriesnever fired.outDir, skip stub /'transform'entries, reject hand-written name lists with
InvalidBuildEntryError, and raiseDuplicateEntryNameErrorwhen two entries resolve to the same name. Single-entry
extractEntryManifestkeeps no list-level checks.Tooling & CI
v[0-9]*umbrellatag, filters publish to the package whose tag fired the workflow
(
pnpm --fail-if-no-match --filter "${TAG%@*}" publish:maybe), andswitches to
publish-${ github.ref_name }concurrency soindependent package releases run in parallel.
@poupe/eslint-configto^0.9.2and whitelistdir/doc/docsthrough its new first-classwithAbbreviationshelper, replacing a temporary local re-emit ofthe
poupe/unicornblock.Commits
Verification
Full-repo
pnpm precommitgreen: eslint + cspell, type-check(source / tools / tests), build, and 53 Vitest tests across 4
files. The suite shifts to the new surface —
extract.test.tsrewritten, new
unbuild.test.tsandobuild.test.tscovering thediscriminators, hook dispatch, skip/throw paths, and collision
detection; the legacy directory-walking fixture tree is removed.
Summary by CodeRabbit
Release Notes
New Features
0.2.0of@kagal/build-tsdocintroduces support for both Unbuild and OBuild bundlers.Breaking Changes
newDocumentsHookwithnewUnbuildHooksandnewOBuildHooksfactories.Documentation