Skip to content

Publish JSDoc-derived type declarations (icebird-style, no copy) - #184

Closed
platypii wants to merge 2 commits into
dts-phase2-drop-function-declsfrom
publish-types-emit
Closed

Publish JSDoc-derived type declarations (icebird-style, no copy)#184
platypii wants to merge 2 commits into
dts-phase2-drop-function-declsfrom
publish-types-emit

Conversation

@platypii

Copy link
Copy Markdown
Contributor

Publish real types to consumers

Today consumers get real types only for the two entries with a hand-written sibling .d.ts (././core, ./integration); the other four (./core/observability, ./core/sinks, ./core/query, ./tui) resolve to implicit any, because TypeScript does not read JSDoc out of an installed package's .js and there is no declaration build.

Approach (icebird-style, no copy)

  • tsconfig.build.json emits .d.ts (+ maps) from JSDoc with rootDir: srcoutDir: types, run via build:types and wired to prepare.
  • types/ is gitignored and shipped through the files allowlist.
  • exports gains a types condition per entry: generated types/... for JSDoc-only entries, the hand-written src/...d.ts for the barrel entries.
  • Every type-import specifier is anchored to a repo-root .js path (e.g. ../../src/core/types.js) so the same import resolves identically from both src/<P>/x.js and the generated types/<P>/x.d.ts. Documented as the project convention in AGENTS.md.

Targeted fixes (the only hand-written-.d.ts → JSDoc-module couplings the split can't bridge)

  • cli/types.d.ts: use the named KernelRuntime instead of ReturnType<typeof createKernelRuntime>.
  • observability/types.d.ts: declare ObservabilityHandle as an explicit interface (precise provider types still reach consumers via the generated index.d.ts).
  • observability/tracer.js: @import { Span } so the emitted declaration resolves Span from its sibling generated runtime.d.ts.
  • One dynamic hypaware-core import in core_commands.js is indirected through a variable so the build (rootDir: src) does not pull it under src's emit root (TS6059).

Verification

External-consumer probe (strict, nodenext) importing all six entry points: types resolve for every entry; negative control errors as expected. typecheck, lint (423 files), and npm test (1473 pass) all green.

Notes

Consumers of the package got real types only for the two entry points that
happened to have a hand-written sibling .d.ts (`.`/`./core` and `./integration`);
the other four (`./core/observability`, `./core/sinks`, `./core/query`, `./tui`)
resolved to implicit `any`, because TypeScript does not read JSDoc out of an
installed package's `.js` and there was no declaration build.
Add a declaration build modeled on icebird: `tsconfig.build.json` emits `.d.ts`
(+ maps) from JSDoc with `rootDir: src` -> `outDir: types`, run via
`build:types` and wired to `prepare` so it runs before pack/publish. `types/` is
gitignored and shipped through the `files` allowlist; `exports` gains a `types`
condition per entry (generated `types/...` for JSDoc-only entries, the
hand-written `src/...d.ts` for the barrel entries).
The icebird mechanism needs every type-import specifier to resolve identically
from both `src/<P>/x.js` and the parallel generated `types/<P>/x.d.ts`. Anchor
all 159 such specifiers to repo-root `.js` paths (e.g. `../../src/core/types.js`)
so tsc keeps them verbatim and they resolve from both trees. Documented as the
project convention in AGENTS.md.
Three targeted fixes remove the only hand-written-.d.ts -> JSDoc-module
couplings that the two-tree split cannot bridge:
- cli/types.d.ts: use the named `KernelRuntime` instead of
`ReturnType<typeof createKernelRuntime>` (drops the activation.js dependency).
- observability/types.d.ts: declare `ObservabilityHandle` as an explicit
interface instead of deriving it from `typeof installObservability`; the
precise provider types still reach consumers via the generated `index.d.ts`.
- observability/tracer.js: `@import { Span }` so the emitted tracer declaration
resolves `Span` from its sibling generated `runtime.d.ts`.
One dynamic `hypaware-core` import in core_commands.js is indirected through a
variable so the declaration build (rootDir: src) does not pull that module under
src's emit root (TS6059).
Verified with an external-consumer probe (strict, nodenext) importing all six
entry points: types resolve for every entry (negative control errors as
expected). typecheck, lint (423 files), and npm test (1473 pass) all green.
@platypii

Copy link
Copy Markdown
ContributorAuthor

Validation against the real packed tarball

Built the tarball via npm pack (which runs preparebuild:types), then installed it into a clean consumer project (with hypaware's real dependencies) and validated both type resolution and runtime importability for all six entry points.

Tarball ships the types: 226 generated .d.ts under types/, plus the hand-written barrel declarations; every exportstypes target is present (src/core/index.d.ts, src/core/cli/integration.d.ts, types/core/{observability,sinks,query,cli/tui}/index.d.ts).

Types resolve as real (not any) — strict + nodenext consumer, tsc exit 0. Each entry imports a value and/or named type and uses it; deliberate misuse is guarded with @ts-expect-error, so if any type had collapsed to any the unused directive would fail the build:

  • hypaware/corePluginName, JsonObject (object type enforced)
  • hypaware/integrationrun/attach return Promise<CommandResult>/Promise<ClientResult>; HypAwareCommandError.code typed number in a catch
  • hypaware/core/observabilityinstallObservability() handle has typed shutdown, rejects unknown members
  • hypaware/core/sinks, hypaware/core/query, hypaware/tui — exported functions typed (not number); SelectSpec enforces its real shape

Runtime importabilitynode dynamic-imports all six specifiers; 6/6 expose live exports:

PASS hypaware/core (Attr, CAP_BLOB_STORE, CAP_ENCODER, ...)
PASS hypaware/integration (HypAwareCommandError, attach, detach, join, ...)
PASS hypaware/core/observability (installObservability, ...)
PASS hypaware/core/sinks (clusterColumnsForDataset, encodePartition)
PASS hypaware/core/query (executeQuerySql, parquetDataSource, whereToParquetFilter)
PASS hypaware/tui (PromptCancelledError, confirm, multiselect, select, ...)

Minor pre-existing note (not introduced here):HypAwareCommandError exposes no constructor in its public type (it inherits Error's), so consumers can't new it with the {code,stdout,stderr,json} detail under types — they catch it and read the typed fields, which works. The declaration lives in the hand-written cli/integration.d.ts from the phase-1 work; worth a follow-up if construction is part of the intended public API.

The hand-written cli/integration.d.ts exposed the instance fields but no
constructor, so consumers (resolving the public ./integration entry) inherited
Error's (message, options) signature and could not construct the error with its
{code, stdout, stderr, json} detail under types. Declare the real constructor so
the public type matches integration.js. Validated against the packed tarball: a
clean consumer can now construct it with typed detail and mistyped detail is
rejected.
@platypii

Copy link
Copy Markdown
ContributorAuthor

Follow-up: declared the HypAwareCommandError constructor in cli/integration.d.ts so its {code, stdout, stderr, json} detail is part of the public type (it previously inherited Error's signature). Re-validated against a freshly packed + reinstalled tarball: a clean consumer now constructs it with typed detail, and mistyped detail is rejected. dev typecheck, lint (423 files), and tests (1473 pass) all green.

@platypii
platypii deleted the branch dts-phase2-drop-function-declsJune 28, 2026 22:26
Sign up for freeto 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.

1 participant

@platypii