@typhonjs-build-test/esm-d-ts

@typhonjs-build-test/esm-d-ts

NPM Code Style License Coverage API Docs Discord Twitch

Provides a modern battle tested near zero configuration tool for ESM / ES Module / Javascript / Typescript developers to generate bundled Typescript declarations from TS or ESM source code utilizing typed JSDoc. This tooling can be employed to build types for a primary export and one or more sub-path exports creating independent ESM oriented / module based declarations utilizing import / export semantics. This tooling can be employed by any project, but is particularly useful for library authors as there are many additional options covering advanced use cases that library authors may encounter. Some of these optional advanced features include support for re-exporting / re-bundling packages w/ TS declarations and thorough support for utilizing imports / import conditions in a variety of flexible ways.

API documentation

It is recommended to install esm-d-ts as a developer dependency in package.json as follows:

{
"devDependencies": {
"@typhonjs-build-test/esm-d-ts": "^0.3.0"
}
}

esm-d-ts 0.3.x supports Typescript 5.5 - 5.9.x. A forthcoming 0.4.0 release will support Typescript 6.x.

Presently the CLI and esm-d-ts can not be installed or used globally; this will be addressed in a future update.

  • 100% test coverage / all functionality verified in detail.

  • Full support for any file format that Typescript supports including React. You may now leverage esm-d-ts to create bundled declarations for Typescript source code.

  • Added plugin support for alternate file formats that support ES Modules. The first plugin available adds support for Svelte 4 components (.svelte files). For more information on Svelte component support please see: @typhonjs-build-test/esm-d-ts-plugin-svelte. Eventually, additional 1st party support may be added for alternate file formats / frameworks that can be transpiled to ESM. Presently, 1st party plugins simply need to be installed as additional developer dependencies and load automatically. You may also provide custom 3rd party plugins via new plugins configuration option.

  • Added bundleDTS convenience function and new bundle command from the CLI which allows easy bundling of existing well formatted module based Typescript declarations.

  • Added emitCTS option to output additional '.d.cts' file for strict Typescript adherence for packages that are dual ESM / CJS. When referencing require as an export condition in an dual ESM / CJS package the types referenced must be .d.cts for the require condition for strict Typescript adherence.

  • Added importsLocal support from @typhonjs-build-test/rollup-plugin-pkg-imports allowing #import sub-paths to be replaced / remapped to actual sub-path package paths.

There is a lot to unpack regarding how to set up a modern ESM Node package for efficient distribution that includes TS declarations. At this time I'll point to the Typescript JSDoc informational resources and the handbook description on how to set up package.json exports with the types condition. In time, I will expand the documentation and resources available about esm-d-ts covering new patterns unlocked from modern use cases combining JSDoc / TS capabilities. If you have questions please open a discussion in the issue tracker. You may also stop by the wiki and the TyphonJS Discord server for discussion & support.

A design goal behind esm-d-ts is to provide flexibility and near-zero configuration, so that you may adapt and use esm-d-ts for a variety of build and usage scenarios. There are four main ways to configure esm-d-ts:

  • CLI immediate mode.
  • CLI w/ configuration file.
  • As a rollup plugin (100% zero configuration)
  • Programmatically.

The Rollup plugin can be used w/ 100% zero configuration, but the other ways to set up esm-d-ts require at minimum an input source file that should be the entry point for the given main or sub-path export. By default, when only providing the input entry point the bundled declaration file will be generated next to the input source file with the same name and .d.ts extension. To generate the bundled declaration file at a specific location provide an output file path w/ extension. All the ways to configure esm-d-ts accept the same configuration object. Except for the Rollup plugin every way to configure esm-d-ts accepts a list of configuration objects allowing you to completely build all sub-path exports in one invocation of esm-d-ts.

The following examples demonstrate essential usage patterns. Each example will take into consideration a hypothetical package that has a primary export and one sub-path export. The resulting package.json exports field looks like this:

{
"exports": {
".": {
"types": "./src/main/index.d.ts",
"import": "./src/main/index.js"
},
"./sub": {
"types": "./src/sub/index.d.ts",
"import": "./src/sub/index.js"
}
}
}

Note: Typescript requires the types condition to always be the first entry in a conditional block in exports.

You may use the CLI via the command line or define a NPM script that invokes it. The CLI has two commands check and generate. generate has two aliases gen & g. The generate command creates bundled declaration files. The check command is a convenient way to log diagnostics from the Typescript compiler checkJs output that by default is filtered to only display messages limited to the scope of the source files referenced from the entry point specified.

To receive help about the CLI use esm-d-ts --help. Please use it to learn about additional CLI options available.

All examples will demonstrate NPM script usage.

There are two ways to use the CLI. The first is "immediate mode" where you directly supply an input / entry point. Presently, only one source file may be specified in "immediate mode".

{
"scripts": {
"types": "esm-d-ts gen src/main/index.js && esm-d-ts gen src/sub/index.js"
}
}

A more convenient way to define a project is through defining a configuration file. You may specify the --config or alias -c to load a default config defined as ./esm-d-ts.config.js or ./esm-d-ts.config.mjs. You may also provide a specific file path to a config after the --config option.

{
"scripts": {
"types": "esm-d-ts gen --config"
}
}

The config file should be in ESM format and have a default export that provides one or a list of GenerateConfig objects.

/**
* @type {import('@typhonjs-build-test/esm-d-ts').GenerateConfig[]}
*/
const config = [
{ input: './src/main/index.js' },
{ input: './src/sub/index.js' },
];

export default config;

You may directly import checkDTS or generateDTS. These are asynchronous functions that can be invoked with top level await.

import { checkDTS, generateDTS } from '@typhonjs-build-test/esm-d-ts';

// Generates TS declaration bundles.
await generateDTS([
{ input: './src/main/index.js' },
{ input: './src/sub/index.js' },
]);

// Log `checkJs` diagnostics.
await checkDTS([
{ input: './src/main/index.js' },
{ input: './src/sub/index.js' },
]);

A Rollup plugin is accessible via generateDTS.plugin() and takes the same configuration object as generateDTS. When using Rollup you don't have to specify the input or output parameters as it will use the Rollup options for input and file option for output. An example use case in a Rollup configuration object follows:

import { generateDTS }   from '@typhonjs-build-test/esm-d-ts';

// Rollup configuration object which will generate the `dist/index.d.ts` declaration file.
export default [
{
input: 'src/main/index.js',
plugins: [generateDTS.plugin()],
output: {
format: 'es',
file: 'dist/main/index.js'
}
},
{
input: 'src/sub/index.js',
plugins: [generateDTS.plugin()],
output: {
format: 'es',
file: 'dist/sub/index.js'
}
}
]

esm-d-ts will generate respective bundled declarations next to the output file:

  • dist/main/index.d.ts
  • dist/sub/index.d.ts

Presently esm-d-ts only handles a single input entry point. A future update may expand this to handle multiple entry points. If you need this functionality please open an issue.

There is no checkDTS Rollup plugin.

There are several more advanced configuration options and usage scenarios that are not discussed in this README. You may view a description of all options available in the documentation for GenerateConfig / GeneratePluginConfig

esm-d-ts allows some rather advanced usage scenarios for library authors as well from handling imports in package.json to further modification of the TS declarations generated through processing the intermediate AST / Abstract Syntax Tree data.

One thing that is super useful is that you can use Typescript .ts files and export named types (aliases / interfaces) and anything that is too cumbersome to manage with JSDoc. Any .ts files that are located at the entry point and subdirectories are included in compilation of the declarations. You may use import types to reference them just like other symbols across your project. With the new support for @implements you can now properly represent classes in ESM that implement an interface. Note: .d.ts files are not included in the declaration generation. However, it is useful to use .d.ts files exporting types that are considered package private.

There is not a well-defined resource that pulls together all the concepts employed or available for using JSDoc to generate Typescript declarations. esm-d-ts has been in development since November 2021. It is completely working and used in production for TyphonJS packages and releases. A good recent article to review that covers JSDoc + Typescript is Boost Your JavaScript with JSDoc Typing.

That being said presently esm-d-ts does require a very particular way of linking all types in JSDoc across a project requiring explicit use of import types for all symbols linked. Even symbols from the local project. This likely is a foreign concept to most ESM / JS developers used to IDE tooling that analyzes a project and allows local symbols to be referenced directly in @param JSDoc tags. This will be solved by adding an analysis stage to esm-d-ts in the future allowing local symbols to be used without import types.

The background on the current need for import types is that with Typescript you must explicitly import all symbols referenced in documentation or source code. Typescript performs "import / export elision" when transpiling TS to JS source code removing imports only used in documentation. JSDoc when used in IDEs for ESM / JS development handles any project analysis and documentation generation tooling also analyzes a project for local symbols.

An additional caveat to be aware of is that presently esm-d-ts during the generation process creates intermediate TS declaration files and by default they are located in the ./.dts folder. It is recommended to add an exclusion rule in a .gitignore file for /.dts. This also is on the roadmap to provide a completely in-memory generation process.

Providing type declarations for your package is a great way to make your package easier to use and consume with modern tooling. What about automatically generating API documentation from the generated types? @typhonjs-typedoc/typedoc-pkg provides a zero configuration CLI frontend to generate API documentation with TypeDoc from a well configured package.json with Typescript declarations.

  • Implement declaration source maps for unbundled package distributions. When shipping direct unbundled ESM source code provide a configuration option to create declaration source maps.

  • Provide a way to manage the generation process entirely in memory. Presently the intermediate individual TS declarations created in execution are stored in the ./.dts folder. Add this folder to your .gitignore. This is a limitation of rollup-plugin-dts & the TS compiler API utilized that uses the file system for bundling. I will be looking into submitting a PR to rollup-plugin-dts to handle virtual bundling.

I would like to bring awareness to the awesome underlying packages that make esm-d-ts possible: