@guchho/dts
Bundle TypeScript declarations into one flat .d.ts next to your JavaScript output.
#TypeScript declaration bundler plugin for Guchho. Guchho bundles
JavaScript, but a published package also needs one tidy .d.ts. This plugin
reads your declaration entry with the TypeScript compiler API, inlines every
local module it reaches into a single flat declaration file, leaves installed
packages as imports, renames colliding declarations, and emits the result as a
build asset next to the JavaScript output.
- Package:
@guchho/dts - Source: guchho/plugins/packages/dts
- Plugin API: Writing plugins · Hooks · Context
Installation
#npm install -D @guchho/dts typescript
Requirements
#- Node.js 20 or newer.
- The TypeScript compiler API. The plugin builds a
ts.Programto read and rewrite declarations, which means it needs a real compiler API at run time:- TypeScript 4.5 through 6.x ship the API directly.
- TypeScript 7 ships no compiler API. On a TypeScript 7 project, also
install
@typescript/typescript6, which re-exports the TypeScript 6 API. The plugin looks fortypescriptfirst, then@typescript/typescript6, and reports an actionable error if it finds neither.
- A Guchho that hosts plugin hooks. The plugin is an object with
buildStartandgenerateBundleand needs the engine that runs the plugin bridge.
Basic configuration
#Point the plugin at the TypeScript source. It compiles the source to
declarations in memory and bundles them, so no separate
tsc --emitDeclarationOnly step is needed before a build:
import dts from "@guchho/dts";
export default {
build: {
entry: "src/index.ts",
outdir: "dist",
},
plugins: [
dts({
entry: "src/index.ts",
outfile: "dist/index.d.ts",
}),
],
};
Guchho compiles src/index.ts to dist/index.js; the plugin emits
dist/index.d.ts next to it. To bundle declarations you already have instead,
pass the .d.ts entry and the plugin bundles it as is:
dts({ entry: "dist/types/index.d.ts", outfile: "dist/index.d.ts" });
How it works
#dts(options) validates the options immediately and hands back a plugin object.
buildStartruns the whole build once: a TypeScript program over the entries, the project's diagnostics, and one bundled module per entry. A source entry is compiled to in-memory declarations first, so no separatetscstep is needed. A bad option, a missing file, a non-TypeScript input, or a compiler error fails the build with a plugin-named diagnostic viathis.error- the build stops, no output is written.generateBundleemits each bundle as an asset withthis.emitFile, so it lands whereveroutfile/dirsays alongside the engine's own outputs.
The bundler walks the local declaration graph, so a statement only has to be right once:
- Local modules are inlined. Every
import/export/import()that resolves inside the project is followed and its declarations folded into the output; the import itself is dropped. - Installed packages stay external by default. Their imports are left exactly as written, so the consumer resolves the same package.
- Colliding declarations are renamed. Two modules that both declare
ThingbecomeThingandThing_1, and every reference is rewritten to match. - Exports are flattened. All
exportandexport defaultmodifiers are stripped from the bodies and re-expressed once at the end asexport { ... }andexport default .... - Namespace and import-type references are flattened.
ns.Valueandimport("./ns").Valuebecome the flattenedValue.
Options
#| Option | Type | Default | Description |
|---|---|---|---|
entry |
string | string[] |
- | Required. One entry, or several. Resolved against the build's working directory. A .ts/.tsx/.mts/.cts source is compiled to declarations before bundling; a .d.ts is bundled as is. |
outfile |
string |
- | The single bundle path, e.g. "dist/index.d.ts". Only valid with one entry. Mutually exclusive with dir. |
dir |
string |
- | A directory to collect one bundle per entry in. Mutually exclusive with outfile. |
tsconfig |
string |
discovered | A tsconfig path, resolved against the build's working directory. When omitted, the nearest tsconfig.json at or above the working directory is used. |
compilerOptions |
object |
- | Compiler options layered over the tsconfig before the program is built. |
respectExternal |
boolean |
true |
Leave imports of installed packages as imports instead of inlining them. |
includeExternal |
string[] |
[] |
Package names to inline even though they live in node_modules. |
sourcemap |
boolean |
false |
Reserved. Declaration source maps are not emitted yet; setting it raises a warning. |
With neither outfile nor dir, a single entry is bundled beside itself
("src/index.d.ts" becomes "src/index.d.ts", and "src/index.ts" becomes
"src/index.d.ts"). More than one entry needs outfile or dir to place the
bundles.
Input
#An entry may be a source or a declaration:
- A source file (
.ts,.tsx,.mts,.cts) is compiled to declarations in memory with the TypeScript API, then bundled. The emitted declarations live in a temporary directory that is never written to disk and never mistaken for an installed package. - A declaration file (
.d.ts,.d.mts,.d.cts) is bundled directly.
A source and a declaration entry can be mixed in one build. Any other extension fails the build with a message that says so.
TypeScript 7 ships no compiler API, so on a TypeScript 7 project install
@typescript/typescript6 alongside this plugin. The plugin prefers typescript
and falls back to @typescript/typescript6 when the installed typescript
exposes no API.
In practice, the declaration bundler runs next to the JavaScript build. Adding it
to the same config object means the .d.ts lands in dist with the JavaScript,
with no second build:
export default [
{
entry: "src/index.ts",
outdir: "dist",
format: "esm",
plugins: [dts({ entry: "src/index.ts", outfile: "dist/index.d.ts" })],
},
];
Common errors
#Every failure is raised through this.error with the @guchho/dts prefix, so
the build stops before any declaration is written.
| Message | Cause | Fix |
|---|---|---|
no TypeScript compiler API found |
Neither typescript nor @typescript/typescript6 is installed. |
npm install -D typescript. |
cannot find declaration entry "…" |
The entry path does not exist. |
Check the path; it is resolved against the build's working directory. |
entry "…" is not a TypeScript file |
The entry is not .ts, .tsx, .mts, .cts or a declaration. |
Point entry at a TypeScript file. |
cannot find tsconfig "…" |
An explicit tsconfig path does not exist. |
Correct the path or remove the option. |
declaration errors: followed by error TS… |
TypeScript reported a syntax or type error in the project. | Read the file, line and code; fix the source. |
entries "…" and "…" both write "…" |
Two entries resolve to the same output file. | Give the entries distinct names, or use dir. |
could not emit declarations |
Declaration emit was skipped. | The accompanying TypeScript diagnostic names the file. |
Path aliases (paths / baseUrl, with extends) are resolved through the
declaration output, so an @lib/x import is bundled from @lib/x and never
pulls the implementation back in.
Limits
#- No declaration source maps. The option is accepted and warns; nothing is written.
export =is not bundled. Entry files that use the CommonJSexport =form are not supported yet.export * as ns from "./local"(NamespaceExport) is not folded in.- A default export expression that is not an identifier (
export default { ... }) is dropped.
Publish a single declaration
#npm pack --dry-run reads files, so the published package carries only
src/. Everything the plugin emits is a build output, not part of the package.
Notes
#- The plugin never copies code from
rollup-plugin-dts(which is LGPL-3.0); the bundler here is written against the TypeScript API directly. - The plugin host's
this.resolveandthis.getFileNameare not wired in Guchho yet. A declaration bundler only needsbuildStartandgenerateBundle, so it is unaffected.
License
#MIT. Copyright © CodeHemu.