Skip to content

TypeScript 7 support and stub-resolution hardening (build-tsdoc 0.3.1)#44

Open
amery wants to merge 5 commits into
mainfrom
pr-amery-ts7
Open

TypeScript 7 support and stub-resolution hardening (build-tsdoc 0.3.1)#44
amery wants to merge 5 commits into
mainfrom
pr-amery-ts7

Conversation

@amery

@amery amery commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Summary

@kagal/build-tsdoc 0.3.1 makes a TypeScript 7 project a supported
consumer and repairs two ways extraction could crash on a resolution
that succeeds but yields the wrong semantic kind: a version stub where
a compiler was expected, and raw source where declarations were
expected. Both are now validated before the analysis engine sees them,
then handled — fall back, derive, or refuse with an actionable error —
instead of exploding deep inside api-extractor.

Consumer impact

  • TypeScript 7 is a supported consumer. The typescript peer
    range widens to ^5.9.0 || ^6.0.0 || ^7.0.0. A TS7 consumer's
    extraction runs on the bundled analysis engine deliberately: TS7's
    main export is a version stub, not the classic compiler API, so
    there is no consumer engine left to adopt.
  • The 0.3.0 crash is fixed. 0.3.0 aliased whatever typescript
    the project resolved, ungated; a TS7 consumer got its version stub
    grafted into api-extractor and died on
    ts.parseJsonConfigFileContent is not a function. The swap is now
    gated on the classic-API range (>=5.9 <7, exposing createProgram
    and a version string); anything outside it is left to the bundled
    compiler.
  • A dependency left in its development-stub state no longer aborts
    extraction.
    When a bundled dependency's types re-exports raw
    source (as unbuild --stub writes), declarations are derived from
    that source into node_modules/.cache/kagal-build-tsdoc/ and the
    dependency is remapped onto them through a paths override
    extending the consumer's tsconfig — so its re-exported symbols are
    documented, TSDoc included, as if it were built. The one case that
    cannot be rescued — source the compiler cannot compile — raises the
    new public UnbuiltDependencyError, naming the dependency to build
    first.

Changes

Engine and extraction:

  • feat(build-tsdoc): support TypeScript 7 consumers — the widened
    peer range, the alias gate, and a permanent row asserting the TS7
    fallback against the ts7 example's real install.
  • feat(build-tsdoc): redirect stub dependencies onto derived
    declarations
    — resolution-based stub detection (the modern stub
    writes a .js-mapped source specifier, so only resolving each
    re-export reveals it), declaration derivation and remap, forced emit
    options, UnbuiltDependencyError, and rows that assert the rescue on
    both the bundled and the adopted consumer engine.
  • chore(build-tsdoc): release 0.3.1 — the version bump and CHANGELOG
    promotion.

Example consumers:

  • chore(examples): tidy up playground-ts6 — an explicit
    BuildConfig[] typing, a prepare smoke step, the @kagal scope
    dropped from the private name, an explicit rootDir clearing the
    TS 6.0 advisory, and a README.
  • feat(examples): add playground-ts7, a TypeScript 7.x consumer — a
    typescript ^7.0.0 pin built with obuild and the oxc declaration
    generator (Rust, never loads typescript), carrying no lint because
    typescript-eslint has no TS7-safe path, with a README recording the
    toolchain wall.

Why a patch

Under 0.x rules a minor is reserved for a breaking or significant
change; a widened peer range and two crash fixes are patch territory.
The CHANGELOG records both under 0.3.1.

Verification

The repo-wide gate is green on the rebased branch — frozen-lockfile
install, lint, type-check, build, and test across every workspace
project. build-tsdoc's own suite passes 73 tests, including the TS7
fallback row and the stub-rescue rows.

After merge

Tagging the merge commit for 0.3.1 triggers publish.yml, which
authenticates to npm via OIDC and publishes with Sigstore provenance.

Summary by CodeRabbit

  • New Features
    • Added TypeScript 7 support for API declaration extraction (including updated TypeScript peer compatibility).
    • Added redirection/derivation for development-stub dependencies during extraction.
    • Added runnable TypeScript 6 and 7 playground examples.
  • Bug Fixes
    • Prevented extraction failures caused by incompatible TypeScript compiler stubs.
    • Improved handling of stubbed re-exports and clearer failure reporting when a dependency must be built first.
  • Documentation
    • Expanded setup/workflow guidance for the TypeScript 6/7 playground scenarios, including troubleshooting for stub derivation behavior.

Type the defineBuildConfig result as BuildConfig[] and export the
named binding, giving the default export an explicit type.

Add a prepare step that runs the cross-test smoke over the built
entry and falls back to dev:prepare when it is missing, with the
@kagal/cross-test dependency it needs.

Drop the @Kagal scope from the package name — it is a private
example of build-tsdoc extraction, not a @Kagal package of its own.
The name is workspace-internal, so the lockfile is unaffected.

Set rootDir explicitly — src for the source build, the package root
for the tools config — clearing the TypeScript 6.0 advisory that
asks for it wherever outDir is set.

Add a README describing the example: the consumer that runs
extraction end to end on unbuild and the last JS-based compiler,
the engine swap it exercises, and a snapshot of the toolchain
versions.

Signed-off-by: Alejandro Mery <amery@apptly.co>
@amery amery added enhancement New feature or request build-tsdoc @kagal/build-tsdoc package dependencies Pull requests that update a dependency file release Version bump and release prep labels Jul 14, 2026
@amery amery self-assigned this Jul 14, 2026
@socket-security

socket-security Bot commented Jul 14, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedtypescript@​7.0.29410089100100

View full report

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds TypeScript 7 playground coverage, guards consumer compiler aliasing, derives declarations for stubbed bundled dependencies, adds UnbuiltDependencyError, and expands regression tests and package documentation for the new extraction behavior.

Changes

Build-tsdoc extraction flow

Layer / File(s) Summary
Stub dependency derivation
packages/@kagal-build-tsdoc/src/redirect.ts, src/errors.ts, src/index.ts
Stubbed bundled dependencies are mapped to cached derived declarations, and derivation failures use the exported UnbuiltDependencyError.
Consumer compiler selection and extraction wiring
packages/@kagal-build-tsdoc/src/extract.ts, package.json
Consumer TypeScript is aliased only when it exposes the supported classic compiler API; extraction incorporates derived-declaration path mappings and TypeScript 7 peer support.
Stub and TypeScript 7 regression coverage
packages/@kagal-build-tsdoc/src/__tests__/*
Fixtures and tests cover TypeScript 7 gating, multiple stub layouts, derived declarations, built dependencies, and failure reporting.
TypeScript 6 and 7 consumer fixtures
examples/playground-ts6/*, examples/playground-ts7/*
The examples define TypeScript 6 and TypeScript 7 build, type-check, declaration, and toolchain configurations with documented sample APIs.
Release metadata and package documentation
packages/@kagal-build-tsdoc/CHANGELOG.md, packages/@kagal-build-tsdoc/README.md
Version 0.3.1, TypeScript 7 support, stub remapping behavior, and the new error are documented.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Consumer
  participant extractEntryManifest
  participant TypeScript
  participant redirectStubDependencies
  participant api-extractor
  Consumer->>extractEntryManifest: request API manifest extraction
  extractEntryManifest->>TypeScript: resolve consumer compiler
  extractEntryManifest->>redirectStubDependencies: inspect bundled dependency declarations
  redirectStubDependencies-->>extractEntryManifest: return derived-declaration path mappings
  extractEntryManifest->>api-extractor: prepare compiler and bundled package configuration
  api-extractor-->>Consumer: produce API manifest
Loading

Possibly related PRs

Poem

A bunny found stubs in the code,
And cached fresh declarations abroad.
TS7 hopped through the gate,
While errors explained the wait.
“Build your dep first!” cried the hare—
Clean manifests everywhere! 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: TypeScript 7 support and hardened stub resolution in build-tsdoc 0.3.1.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch pr-amery-ts7

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/@kagal-build-tsdoc/src/extract.ts (2)

128-129: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate aeMain/bundledEntry resolution.

The same three-line lookup (requireHere.resolve('@microsoft/api-extractor')createRequire(aeMain).resolve('typescript')) is repeated verbatim in preferConsumerTypeScript and loadAnalysisTypeScript. Extracting a single helper (e.g. resolveBundledTypeScriptEntry()) removes the duplication and the risk of the two call sites silently drifting apart later.

♻️ Suggested helper extraction
+function resolveBundledTypeScriptEntry(): string {
+  const aeMain = requireHere.resolve('`@microsoft/api-extractor`');
+  return createRequire(aeMain).resolve('typescript');
+}
+
 function preferConsumerTypeScript(projectFolder: string): void {
   const consumerEntry = resolveFrom(projectFolder, 'typescript');
   if (consumerEntry === undefined) {
     return;
   }
-  const aeMain = requireHere.resolve('`@microsoft/api-extractor`');
-  const bundledEntry = createRequire(aeMain).resolve('typescript');
+  const bundledEntry = resolveBundledTypeScriptEntry();
   if (bundledEntry === consumerEntry) {
     return;
   }
   ...
 }
 
 function loadAnalysisTypeScript(): TypeScriptModule {
-  const aeMain = requireHere.resolve('`@microsoft/api-extractor`');
-  const bundledEntry = createRequire(aeMain).resolve('typescript');
+  const bundledEntry = resolveBundledTypeScriptEntry();
   return requireHere(bundledEntry) as TypeScriptModule;
 }

Also applies to: 166-169, 179-183

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/`@kagal-build-tsdoc/src/extract.ts around lines 128 - 129, Extract
the duplicated `@microsoft/api-extractor` and bundled TypeScript resolution into a
shared helper such as resolveBundledTypeScriptEntry(). Update
preferConsumerTypeScript and loadAnalysisTypeScript to call that helper,
removing their repeated aeMain/createRequire lookup while preserving the
existing resolution behavior.

123-157: 🩺 Stability & Availability | 🔵 Trivial

Sticky process-global alias can leak across consumers in one process.

requireHere.cache[bundledEntry] = consumerModule mutates a process-wide module cache slot with no way to reset it. If extractEntryManifest is ever invoked more than once in the same Node process for consumers pinned to different TypeScript engines (e.g. a monorepo script looping over packages), the first adoptable consumer's engine will stay aliased into api-extractor's typescript slot for every subsequent call — including one for a TS7 consumer that should fall back to the bundled compiler. The test suite works around exactly this ("own worker file: the engine is fixed at the first api-extractor load") but that only proves the hazard is real, not that production call sites are isolated the same way.

Worth confirming callers of extractEntryManifest always run in a fresh process per consumer engine, or otherwise documenting/guarding this constraint.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/`@kagal-build-tsdoc/src/extract.ts around lines 123 - 157, Update
the extractEntryManifest flow and preferConsumerTypeScript so the process-global
TypeScript cache alias cannot leak between consumers. Preserve the original
bundledEntry cache state before adopting a consumer compiler, and restore it
after extraction (including fallback when no adoption occurs), or otherwise
enforce/document a single-engine process constraint at the production entry
point. Ensure later consumers can independently select their adoptable engine or
the bundled compiler.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@examples/playground-ts7/README.md`:
- Around line 65-68: Update the fenced error-log block in the README to declare
the text language, preserving the existing log contents.

In `@packages/`@kagal-build-tsdoc/src/redirect.ts:
- Around line 339-351: Update the diagnostic collection in the program emit flow
to use ts.getPreEmitDiagnostics(program), ensuring semantic diagnostics are
included alongside existing pre-emit checks. Keep the existing error filtering
and UnbuiltDependencyError construction unchanged.

---

Outside diff comments:
In `@packages/`@kagal-build-tsdoc/src/extract.ts:
- Around line 128-129: Extract the duplicated `@microsoft/api-extractor` and
bundled TypeScript resolution into a shared helper such as
resolveBundledTypeScriptEntry(). Update preferConsumerTypeScript and
loadAnalysisTypeScript to call that helper, removing their repeated
aeMain/createRequire lookup while preserving the existing resolution behavior.
- Around line 123-157: Update the extractEntryManifest flow and
preferConsumerTypeScript so the process-global TypeScript cache alias cannot
leak between consumers. Preserve the original bundledEntry cache state before
adopting a consumer compiler, and restore it after extraction (including
fallback when no adoption occurs), or otherwise enforce/document a single-engine
process constraint at the production entry point. Ensure later consumers can
independently select their adoptable engine or the bundled compiler.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4568d20e-6559-4e64-94de-989f6632c8c9

📥 Commits

Reviewing files that changed from the base of the PR and between 3b7aa15 and f2e65d9.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (22)
  • examples/playground-ts6/README.md
  • examples/playground-ts6/build.config.ts
  • examples/playground-ts6/package.json
  • examples/playground-ts6/tsconfig.json
  • examples/playground-ts6/tsconfig.tools.json
  • examples/playground-ts7/README.md
  • examples/playground-ts7/build.config.ts
  • examples/playground-ts7/package.json
  • examples/playground-ts7/src/index.ts
  • examples/playground-ts7/tsconfig.json
  • examples/playground-ts7/tsconfig.tools.json
  • packages/@kagal-build-tsdoc/CHANGELOG.md
  • packages/@kagal-build-tsdoc/README.md
  • packages/@kagal-build-tsdoc/package.json
  • packages/@kagal-build-tsdoc/src/__tests__/consumer-typescript-ts7.test.ts
  • packages/@kagal-build-tsdoc/src/__tests__/fixtures/stub-re-export.ts
  • packages/@kagal-build-tsdoc/src/__tests__/stub-re-export-bundled.test.ts
  • packages/@kagal-build-tsdoc/src/__tests__/stub-re-export-consumer.test.ts
  • packages/@kagal-build-tsdoc/src/errors.ts
  • packages/@kagal-build-tsdoc/src/extract.ts
  • packages/@kagal-build-tsdoc/src/index.ts
  • packages/@kagal-build-tsdoc/src/redirect.ts

Comment thread examples/playground-ts7/README.md Outdated
Comment on lines +339 to +351
const program = ts.createProgram(targets, options);
const emitted = program.emit();
const failure = [
...program.getOptionsDiagnostics(),
...program.getSyntacticDiagnostics(),
...emitted.diagnostics,
].find((d) => d.category === ts.DiagnosticCategory.Error);
if (failure !== undefined) {
throw new UnbuiltDependencyError(
dependency,
ts.flattenDiagnosticMessageText(failure.messageText, ' '),
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does ts.getPreEmitDiagnostics include semantic diagnostics, and does Program.emit() diagnostics exclude them, in TypeScript 5.9?

💡 Result:

Yes, ts.getPreEmitDiagnostics includes semantic diagnostics, and Program.emit diagnostics generally exclude them. ts.getPreEmitDiagnostics is an authoritative function in the TypeScript Compiler API designed to collect and return a comprehensive list of all diagnostics for a program [1]. This includes: - Config file parsing diagnostics [1][2] - Compiler options diagnostics [1][2] - Syntactic diagnostics [1][2] - Global diagnostics [1][2] - Semantic diagnostics [1][2] - Declaration diagnostics (if enabled) [1][2] In contrast, the diagnostics returned as part of the EmitResult object from Program.emit are specifically those generated during the emit process itself [3]. They do not include the full suite of semantic and syntactic errors that would have been identified prior to emit [4]. To obtain a complete view of all errors in a program, common practice involves concatenating the result of ts.getPreEmitDiagnostics(program) with the diagnostics returned from program.emit [5][6][7].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the relevant section of the file with line numbers.
sed -n '320,370p' packages/@kagal-build-tsdoc/src/redirect.ts | cat -n

# Find the TypeScript version(s) referenced by this package/workspace.
printf '\n--- package.json references ---\n'
rg -n '"typescript"|typescript@' -g 'package.json' -g 'pnpm-lock.yaml' -g 'yarn.lock' -g 'package-lock.json' .

# Show the package manifest for the module under review if present.
printf '\n--- package manifest ---\n'
cat packages/@kagal-build-tsdoc/package.json 2>/dev/null || true

Repository: kagal-dev/tsdoc

Length of output: 16881


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect any local code paths that already use pre-emit diagnostics.
rg -n "getPreEmitDiagnostics|getSyntacticDiagnostics|getOptionsDiagnostics|emit\(\)\.diagnostics" packages/@kagal-build-tsdoc/src

# Map the file structure around the reviewed module.
fd -H -t f . packages/@kagal-build-tsdoc/src

Repository: kagal-dev/tsdoc

Length of output: 1523


Include semantic diagnostics in this failure check
getOptionsDiagnostics() + getSyntacticDiagnostics() miss unresolved imports, undefined identifiers, and other semantic errors, so a broken stub can still emit declarations and pass the existsSync check. Use ts.getPreEmitDiagnostics(program) here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/`@kagal-build-tsdoc/src/redirect.ts around lines 339 - 351, Update
the diagnostic collection in the program emit flow to use
ts.getPreEmitDiagnostics(program), ensuring semantic diagnostics are included
alongside existing pre-emit checks. Keep the existing error filtering and
UnbuiltDependencyError construction unchanged.

amery added 4 commits July 14, 2026 04:51
Add examples/playground-ts7, a private workspace package that pins
typescript ^7.0.0 over the same Point/distance/translate surface as
playground-ts6. No @kagal/build-tsdoc hook is wired in, so it builds,
stubs, and type-checks on its own.

Build with obuild rather than unbuild, and set isolatedDeclarations
to select rolldown-plugin-dts's oxc generator. TypeScript 7's main
export is a version stub, not the compiler, so anything that imports
typescript as a library crashes on it; the oxc generator is Rust and
never loads it, emitting the declaration bundle where unbuild's
rollup-plugin-dts dies.

Carry no lint scripts. typescript-eslint reads the compiler off the
same stub and has no TS7-safe path, so eslint cannot run here; the
root ESLint exclusion of examples/* already keeps the repo-wide gate
green.

Add a README recording the toolchain wall and a snapshot of the
pinned versions.

Signed-off-by: Alejandro Mery <amery@apptly.co>
Widen the typescript peer range to ^5.9.0 || ^6.0.0 || ^7.0.0, so a
TypeScript 7 consumer is a supported consumer whose extraction runs on
the bundled engine deliberately rather than by accident.

Gate the consumer-compiler swap before it aliases: read the resolved
typescript version and adopt only a compiler in the classic-API range
(>=5.9 <7) that exposes createProgram and a version string. Anything
else — including a TS7 version stub — is left to api-extractor's
bundled compiler. This fixes the 0.3.0 crash where the ungated alias
grafted a TS7 stub into api-extractor and died on
ts.parseJsonConfigFileContent is not a function.

Add a permanent row that resolves the ts7 example's typescript and
asserts extraction falls back to the bundled engine, with the
fixture's CONSUMER_TS7_ROOT / CONSUMER_TS7_VERSION beside the 6.x
pair.

Wire the extraction hook into examples/playground-ts7 so it exercises
that fallback end to end: the obuild build now runs the hooks over the
TS7 declarations and completes on the bundled engine.

Signed-off-by: Alejandro Mery <amery@apptly.co>
A bundled dependency left in its development-stub state ships a types
entry that re-exports raw TypeScript source (as unbuild --stub
writes). api-extractor can only analyse declarations, so following the
stub aborted deep in its analyser with "Unable to determine semantic
information". Before invoking api-extractor, derive real declarations
from the stubbed source with the analysis compiler into
node_modules/.cache/kagal-build-tsdoc/ and remap the dependency onto
them through a tsconfig paths override extending the consumer's config,
so the re-exported symbols are documented — TSDoc included — as if the
dependency were built.

Detect the stub by resolving what each re-export lands on, not by
matching specifier patterns: modern unbuild stubs write the source
specifier with a .js extension, so only resolution reveals it points
at source. This covers the jiti-era .ts specifier, the modern
.js-mapped specifier, and a types entry aimed straight at source.

The mirror must replicate the dependency's layout — a canonical
node_modules/<name>/ tail and a directory-targeted paths mapping — so
the compiler stamps the derived files with the dependency's packageId
and api-extractor attributes them to the package instead of dropping
them as foreign. Force the emit options that would otherwise divert
the declarations (declarationDir, outFile) or break under a sibling
tsconfig (composite, incremental, noEmit).

Add UnbuiltDependencyError — public and exported — for the one arm the
redirect cannot rescue: stubbed source the compiler cannot compile.
The message names the dependency to build first.

Turn the former stub-explosion characterisation tests, which asserted
the abort, into rows that assert the rescue on both the bundled and
the adopted consumer engine, alongside rows for the modern .js-mapped
stub, a types entry aimed at source, a dependency whose tsconfig sets
declarationDir, the untouched built-dependency pass path, and the
actionable error when the stubbed source does not parse.

Signed-off-by: Alejandro Mery <amery@apptly.co>
Bump @kagal/build-tsdoc to 0.3.1 and promote the existing
[Unreleased] changelog entries under the new version with a
2026-07-13 release date, keeping an empty [Unreleased]
placeholder on top.

0.3.1 is a patch: it adds TypeScript 7 to the supported peer
range and stops two resolution-stub states — a TS7 consumer's
version stub and a dependency's development stub — from
crashing extraction. The CHANGELOG covers both.

Once this PR is merged, tagging the resulting commit for 0.3.1
triggers publish.yml, which authenticates to npm via OIDC and
publishes with Sigstore provenance.

Signed-off-by: Alejandro Mery <amery@apptly.co>
@amery

amery commented Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

Force-pushed to fold the CodeRabbit review into the source commits — no
new commits, the branch is rebased in place on the same base
(Merge pull request #42). The four points are handled at the commit
that introduced each, so history stays clean:

  • redirect.ts — semantic diagnostics in the derivation failure
    check.
    Documented, deliberately not changed. The check excludes
    getSemanticDiagnostics/getPreEmitDiagnostics by design: derivation
    is best-effort, and a stub's source can reference types that do not
    fully resolve in the mirror context yet still emit usable
    declarations. Refusing on semantic errors would reject stubs the
    redirect can otherwise rescue. Only errors that mean no usable
    output
    raise UnbuiltDependencyError; the missing-declaration
    existsSync check below is the backstop. A comment now records the
    rationale. (folded into feat(build-tsdoc): redirect stub dependencies onto derived declarations)

  • extract.ts — process-global alias leaking across consumers.
    Documented as a constraint. preferConsumerTypeScript mutates a
    shared module-cache slot that is never restored, and api-extractor
    captures whichever compiler is aliased the first time its analyser
    loads — so one extraction process serves a single consumer engine.
    Callers run one build process per consumer, which holds in practice;
    the doc comment now states it as a constraint to preserve rather than
    a latent bug. (folded into feat(build-tsdoc): support TypeScript 7 consumers)

  • extract.ts — duplicate api-extractor→typescript resolution.
    Fixed. The repeated requireHere.resolve + createRequire lookup is
    extracted into resolveBundledTypeScriptEntry(); both call sites now
    use it. (folded into feat(build-tsdoc): support TypeScript 7 consumers)

  • playground-ts7/README.md — fenced block missing a language
    (MD040).
    Fixed: the error-log fence is now ```text. (folded
    into feat(examples): add playground-ts7, a TypeScript 7.x consumer)

Base unchanged, full pnpm precommit gate green after the autosquash.

@pkg-pr-new

pkg-pr-new Bot commented Jul 14, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@kagal/build-tsdoc@44
npm i https://pkg.pr.new/@kagal/model-tsdoc@44

commit: 47ae25e

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/`@kagal-build-tsdoc/src/extract.ts:
- Around line 190-235: Update parseConsumerTsconfig to inspect the error field
returned by ts.readConfigFile before parsing config. Return undefined when an
error is present, while preserving the existing parsing and pathsBase behavior
for successfully read tsconfig files.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: e9ea0f56-e2e0-497a-82f5-956430f65290

📥 Commits

Reviewing files that changed from the base of the PR and between f2e65d9 and 47ae25e.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (17)
  • examples/playground-ts7/README.md
  • examples/playground-ts7/build.config.ts
  • examples/playground-ts7/package.json
  • examples/playground-ts7/src/index.ts
  • examples/playground-ts7/tsconfig.json
  • examples/playground-ts7/tsconfig.tools.json
  • packages/@kagal-build-tsdoc/CHANGELOG.md
  • packages/@kagal-build-tsdoc/README.md
  • packages/@kagal-build-tsdoc/package.json
  • packages/@kagal-build-tsdoc/src/__tests__/consumer-typescript-ts7.test.ts
  • packages/@kagal-build-tsdoc/src/__tests__/fixtures/stub-re-export.ts
  • packages/@kagal-build-tsdoc/src/__tests__/stub-re-export-bundled.test.ts
  • packages/@kagal-build-tsdoc/src/__tests__/stub-re-export-consumer.test.ts
  • packages/@kagal-build-tsdoc/src/errors.ts
  • packages/@kagal-build-tsdoc/src/extract.ts
  • packages/@kagal-build-tsdoc/src/index.ts
  • packages/@kagal-build-tsdoc/src/redirect.ts

Comment on lines +190 to +235
/**
* The compiler namespace analysis runs on: api-extractor's own
* `typescript` resolution, read *after* {@link loadAPIExtractor}
* has primed the cache — the consumer's adopted engine when the
* alias applied, the bundled compiler otherwise. The stub redirect
* derives declarations with this same engine so what it derives is
* what the analysis parses.
*/
function loadAnalysisTypeScript(): TypeScriptModule {
const bundledEntry = resolveBundledTypeScriptEntry();
return requireHere(bundledEntry) as TypeScriptModule;
}

/** Consumer tsconfig, parsed to its effective compiler options. */
interface ParsedTsconfig {
/** Effective options, extends chain applied. */
options: TSModule.CompilerOptions
/** Base directory relative `paths` entries resolve against. */
pathsBase: string
}

/**
* Parse the consumer's tsconfig with the analysis engine, for the
* stub redirect: detection resolves modules under the consumer's
* effective options, and any pre-existing `paths` must survive the
* override merge. `undefined` when the file cannot be read —
* api-extractor reports that on its own terms.
*/
function parseConsumerTsconfig(
ts: TypeScriptModule,
tsconfigPath: string,
): ParsedTsconfig | undefined {
const { config } =
ts.readConfigFile(tsconfigPath, ts.sys.readFile) as
{ config?: unknown };
if (config === undefined) {
return undefined;
}
const basePath = path.dirname(tsconfigPath);
const parsed = ts.parseJsonConfigFileContent(config, ts.sys, basePath);
const pathsBase = parsed.options.pathsBasePath;
return {
options: parsed.options,
pathsBase: typeof pathsBase === 'string' ? pathsBase : basePath,
};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

set -euo pipefail

printf '\n## extract.ts excerpt\n'
sed -n '160,260p' packages/@kagal-build-tsdoc/src/extract.ts

printf '\n## TypeScript version references\n'
rg -n '"typescript"|typescript@|from '\''typescript'\''' -g 'package.json' -g 'pnpm-lock.yaml' -g 'package-lock.json' -g 'yarn.lock' .

printf '\n## local declaration/source for readConfigFile if present\n'
rg -n "readConfigFile\\(" node_modules typescript . -g '*.d.ts' -g '*.ts' -g '*.js' 2>/dev/null | head -n 40

Repository: kagal-dev/tsdoc

Length of output: 15912


🌐 Web query:

TypeScript readConfigFile unreadable file returns config error implementation parseConfigFileTextToJson

💡 Result:

In the TypeScript Compiler API, ts.readConfigFile and ts.parseConfigFileTextToJson handle file reading and parsing in distinct ways, and both require careful error checking to handle unreadable or malformed files [1][2]. ts.readConfigFile This function takes a file path and a readFile callback (typically ts.sys.readFile) [3][4]. If the file cannot be read or does not exist, the behavior depends on the provided callback, but generally, it returns an object containing an error property instead of throwing an exception [5][6]. The returned object has the signature: { config?: any; error?: Diagnostic } You must explicitly check for the presence of the error property before accessing the config property to avoid runtime errors [7][6]. ts.parseConfigFileTextToJson This function is a lower-level utility that parses raw JSON text into a TypeScript configuration object [1][2]. It does not perform any file I/O itself [2]. Its signature is: ts.parseConfigFileTextToJson(fileName: string, jsonText: string): { config?: any; error?: Diagnostic } Similar to readConfigFile, this function returns an object containing either the parsed config or a Diagnostic object if the JSON text was malformed [8]. It is commonly used when you have already retrieved the file content manually and need to convert it into a TypeScript-compatible format [2]. Best Practices for Error Handling 1. Always Check for Diagnostics: After calling either function, always inspect the error field [7]. TypeScript diagnostics provide detailed information about why a file was unreadable or why the JSON was malformed [6]. 2. Use Compiler Host: For robust file resolution, it is recommended to use ts.findConfigFile combined with ts.readConfigFile, followed by ts.parseJsonConfigFileContent, rather than relying on manual file reading [3][4]. The latter function processes extended configurations and compiler options, which is the standard way to prepare a configuration for use with ts.createProgram [4]. 3. Diagnostic Reporting: If an error occurs, you can use the TypeScript diagnostic formatter (if needed) or simply log the diagnostic's messageText to understand the failure [6][3]. If you receive an error like "readFile is not a function," it typically indicates that the second argument passed to readConfigFile is not a valid function or is missing entirely [9]. Ensure you are passing ts.sys.readFile as the second argument to satisfy the function requirements [3][4].

Citations:


🌐 Web query:

site:github.com/microsoft/TypeScript readConfigFile implementation config error Diagnostic return object

💡 Result:

When using the TypeScript Compiler API, readConfigFile is a low-level utility function that reads a configuration file from disk but does not parse it into compiler options [1]. Because it only performs the initial read, it does not return a Diagnostic object directly in its primary result [1]. Instead, readConfigFile returns an object with the following structure: { config?: any; // The parsed JSON object, if successful error?: Diagnostic; // A diagnostic object, if an error occurred during reading or parsing } Common implementation details and error patterns include: 1. Low-Level Functionality: readConfigFile only reads the file content and parses the JSON [1]. It does not resolve extended configurations, handle default options, or validate the specific schema of a tsconfig.json file [1]. 2. Parsing Diagnostics: If you need to handle configuration errors, the error property of the returned object will contain a Diagnostic object if the file could not be read or if the JSON is malformed [2][3]. 3. Recommended Usage Flow: To correctly use the compiler options defined in a configuration file, you must pass the result of readConfigFile to subsequent API functions [1]. A robust implementation typically follows this pattern: const configFileName = ts.findConfigFile("./", ts.sys.fileExists, "tsconfig.json"); const configFile = ts.readConfigFile(configFileName, ts.sys.readFile); if (configFile.error) { // Handle the error using the Diagnostic object console.error(configFile.error.messageText); } else { // Parse the configuration properly const parsed = ts.parseJsonConfigFileContent( configFile.config, ts.sys, "./"); // Use parsed.options and check parsed.errors if needed let program = ts.createProgram(fileNames, parsed.options); } If you encounter unexpected errors, ensure your JSON file does not contain comments (which JSON.parse will reject) or Byte Order Marks (BOM), as readConfigFile may be sensitive to file encoding formatting [4]. If you need detailed diagnostics regarding specific compiler options, those are provided by parseJsonConfigFileContent rather than readConfigFile [1][5].

Citations:


🌐 Web query:

TypeScript source readConfigFile returns config error raw tsconfig readConfigFile implementation

💡 Result:

In the TypeScript compiler API, the ts.readConfigFile function is designed to read and parse a tsconfig.json (or similar) file while handling TypeScript-specific JSON extensions, such as comments and trailing commas [1][2][3]. Implementation Details: The function signature is: ts.readConfigFile(fileName: string, readFile: (path: string) => string | undefined): { config?: any; error?: ts.Diagnostic } 1. Return Value: Unlike standard JSON parsing, it returns an object containing either the parsed config object or an error object (of type ts.Diagnostic) [3]. It does not throw an error if the file is missing or contains invalid JSON; instead, it populates the error field [3]. 2. The readFile Callback: The function requires a second argument, a readFile callback, which performs the actual file system access [1][3]. A common source of errors (e.g., "readFile is not a function") arises when the host or the provided callback is incorrectly passed, missing, or incompatible with the expected interface [4][5]. 3. Scope: ts.readConfigFile only performs the initial reading and parsing of the file content into a raw JavaScript object [1][3]. It does not automatically resolve the extends property or convert compiler options into the internal ts.CompilerOptions format [1]. To fully resolve a tsconfig.json for use in a programmatic context, developers typically use ts.parseJsonConfigFileContent after reading the file with ts.readConfigFile [1][3]. This secondary step handles the extends inheritance, expands glob patterns, and validates the configuration [1][3].

Citations:


🏁 Script executed:

set -euo pipefail

python3 - <<'PY'
import urllib.request
url = 'https://raw.githubusercontent.com/microsoft/TypeScript/v5.9.3/src/compiler/commandLineParser.ts'
try:
    with urllib.request.urlopen(url, timeout=20) as r:
        text = r.read().decode('utf-8', 'replace')
    for needle in ['function readConfigFile', 'readConfigFile(', 'parseConfigFileTextToJson']:
        idx = text.find(needle)
        print(f'## {needle}')
        if idx == -1:
            print('not found')
        else:
            start = max(0, idx - 400)
            end = min(len(text), idx + 1200)
            print(text[start:end])
            print()
except Exception as e:
    print('ERROR:', repr(e))
PY

Repository: kagal-dev/tsdoc

Length of output: 319


🏁 Script executed:

set -euo pipefail

python3 - <<'PY'
import ssl
import urllib.request

url = 'https://raw.githubusercontent.com/microsoft/TypeScript/v5.9.3/src/compiler/commandLineParser.ts'
ctx = ssl._create_unverified_context()
with urllib.request.urlopen(url, context=ctx, timeout=20) as r:
    text = r.read().decode('utf-8', 'replace')

for needle in ['function readConfigFile', 'function parseConfigFileTextToJson', 'readFileText']:
    idx = text.find(needle)
    print(f'## {needle}')
    if idx == -1:
        print('not found')
    else:
        start = max(0, idx - 300)
        end = min(len(text), idx + 1800)
        print(text[start:end])
        print()
PY

Repository: kagal-dev/tsdoc

Length of output: 4448


Handle readConfigFile failures via error ts.readConfigFile(...) returns { config: {}, error } when the tsconfig can’t be read, so config === undefined never catches that case. This should return undefined on error instead of continuing with near-default compiler options.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/`@kagal-build-tsdoc/src/extract.ts around lines 190 - 235, Update
parseConsumerTsconfig to inspect the error field returned by ts.readConfigFile
before parsing config. Return undefined when an error is present, while
preserving the existing parsing and pathsBase behavior for successfully read
tsconfig files.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

build-tsdoc @kagal/build-tsdoc package dependencies Pull requests that update a dependency file enhancement New feature or request release Version bump and release prep

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant