feat(core)!: publish one subpath per root namespace (#95)
## Summary
A consumer that emits declarations names DomainKit types by their
declaring module. `exports` listed only `.`, `./client`, `./server`,
`./testing`, and `./package.json`, so TypeScript had no specifier for
`dist/types/Principal.d.ts` or `dist/types/DnsRecord.d.ts` and raised
TS2883 once per class (Samva's `@samva/contracts` hit it on an HttpApi
group tuple over `DnsRecord.Model` members).
Decision D43: every namespace `src/index.ts` re-exports is its own
subpath, Effect style, with a curated explicit list.
```json
".": { "types": "./dist/types/index.d.ts", "import": "./dist/index.mjs" },
"./client": { "types": "./dist/types/entry/client.d.ts", "import": "./dist/entry/client.mjs" },
"./server": { "types": "./dist/types/entry/server.d.ts", "import": "./dist/entry/server.mjs" },
"./testing": { "types": "./dist/types/entry/testing.d.ts", "import": "./dist/entry/testing.mjs" },
"./Principal": { "types": "./dist/types/Principal.d.ts", "import": "./dist/Principal.mjs" },
"./DnsRecord": { "types": "./dist/types/DnsRecord.d.ts", "import": "./dist/DnsRecord.mjs" },
...one entry per root namespace (19): Approval, Cleanup, Cloudflare, Connect, Custody, DnsRecord, DomainKit, DomainName, Plan, Principal, Provider, Providers, Provision, Reason, Receipt, Resolver, Storage, Vercel, Verify
"./package.json": "./package.json"
```
No `./*` pattern, nothing under `internal` or `entry`, nothing dropped
from the root.
## Build
- tsdown runs unbundled (`unbundle: true`, `root: "src"`): one ESM file
per source module (`dist/Principal.mjs`, `dist/DnsRecord.mjs`, ...),
entry points under `dist/entry/`, no hashed chunks. The root and each
subpath share module instances.
- Declarations stay per module from `tsc -p tsconfig.build.json` in
`dist/types/`, so each subpath's `types` and `import` resolve to the
same module.
- Unbundled ESM evaluates the DomainName, internal/error, Reason, Plan,
DnsRecord import cycle in real module order, which the bundled chunk had
been ordering for us. `DnsRecord` and `Plan` now read the field schemas
that cross that cycle through `Schema.suspend`, so any module in it can
be evaluated first (`import ... from "domainkit/DomainName"` before
anything else works).
## Proof
- `tests/artifact/exports.test.ts` derives the namespace list from
`export * as` in `src/index.ts` and pins `Object.keys(exports)` plus
each subpath's `{ types, import }` to it, so a new public module without
an export entry fails the gate.
- `tests/artifact/package-contents.test.ts` requires every `types` and
`import` target in the `npm pack` file list.
- `tests/artifact/packed-consumers.test.ts` imports
`domainkit/Principal` and `domainkit/DnsRecord` at runtime under Node
24, Bun, and workerd and checks `PrincipalModule.Service ===
Principal.Service` and `cname === DnsRecord.cname` against the root. It
also compiles the consumer with `declaration: true` against the tarball
and asserts the emitted file names
`import("domainkit/Principal").Service` and contains neither
`node_modules` nor `dist/`. Emitted lines:
```ts
export declare const first: (items: ReadonlyArray<DnsRecord.Model>) => DnsRecord.A | DnsRecord.AAAA | DnsRecord.CAA | DnsRecord.CNAME | DnsRecord.MX | DnsRecord.NS | DnsRecord.SRV | DnsRecord.TXT;
export declare const planned: Effect.Effect<Plan.Model, DomainKit.Error, import("domainkit/Principal").Service | Provision.Service>;
export declare const principal: import("domainkit/Principal").Interface;
```
The `Principal.Service` item left open in #92 is closed by this.
`@domainkit/react` and `@domainkit/capsuledb` bundle a single
`dist/index.d.mts` and are unchanged; both typecheck against the rebuilt
core.
## Validation
- Core gate (oxfmt, lint, typecheck, 133 tests), `bun run --filter
domainkit build`
- `bun run --filter domainkit release:check` (typecheck, tests, build,
examples typecheck, 10 artifact tests)
- Root `typecheck` (react and capsuledb against the rebuilt core) and
root `release:check`
- Live provider tests are user-owned and were not run
Known artifact: each unbundled module file also exports rolldown's
synthesized namespace object (`Principal_exports`) next to its members;
declarations do not mention it, so it is invisible to TypeScript
consumers.
https://claude.ai/code/session_017P54ng7iisz1dsk18u1w1x S
Saatvik Arya committed
6c6718b321c4f4eb352f5b0588fcd8c0adc7685b
Parent: 4072375
Committed by GitHub <noreply@github.com>
on 9/3/2026, 7:41:54 PM