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

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

NPM Code Style License API Docs Discord Twitch

Provides a modern battle tested near zero configuration tool for ESM / ES Module / Javascript developers to generate bundled Typescript declarations from 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

Installation:

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.2.0"
}
}

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

What's New:

(0.2.2):

  • Added a TS AST transformer to support import types in @implements JSDoc tags. This allows you to reference an interface from a class and have it properly converted to implements <INTERFACE> in the declarations generated.
  • Added transformer "meta-transformer" to reduce the boilerplate of creating custom TS AST transformers.

(0.2.1):

  • Added a new internal AST transformer that corrects the output of the TS compiler for setter accessor parameter names. The TS compiler for ESM will rename setter accessor parameter names to arg regardless of the value set in the source file. If there is a JSDoc comment associated with a setter the first @param tag name will be set to the AST node param name. Downstream tooling such as TypeDoc 0.25.7+ validates comment / @param name against the type declaration name; this change fixes that mismatch.

(0.2.0):

  • Optional postprocessing

    • The first built-in postprocessing function is support for @inheritDoc. This is an unsupported JSDoc tag for Typescript and when types are generated any methods or constructor functions that use @inheritDoc have parameters that are typed as any. It is also possible to create custom postprocessing functions. For more details on postprocessing and AST transformation please see the wiki.
  • Support for the JSDoc @module / @packageDocumentation comment pass-through to the generated DTS bundle. This is helpful when generating docs from the DTS bundle. This is only supported for the main entry point source file.

  • All dependencies updated along with peer dependency requirements of Rollup 3.3 - 4.x and Typescript 5.1+.

Overview:

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.

Example use cases:

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.

CLI

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;

Programmatic Usage

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' },
]);

Rollup Plugin

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.

Advanced Configuration

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.

Caveats

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.

Synergies

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.

Roadmap

  • Create an initial processing stage where esm-d-ts analyzes all exported symbols of the local code base allowing local symbols to be used without import types.

  • 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.

  • Generate source maps for the bundled TS declarations allowing IDEs to not just jump to the declarations, but also open linked source code.

Appreciation

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