@dish/rollup-plugin-flat-dts

.d.ts files flattener and Rollup plugin

Usage no npm install needed!

<script type="module">
  import dishRollupPluginFlatDts from 'https://cdn.skypack.dev/@dish/rollup-plugin-flat-dts';
</script>

README

Flatten .d.ts Files

NPM Build Status GitHub Project API Documentation

Example Configuration

Add the following rollup.config.js:

import commonjs from '@rollup/plugin-commonjs';
import nodeResolve from '@rollup/plugin-node-resolve';
import flatDts from 'rollup-plugin-flat-dts';
import sourcemaps from 'rollup-plugin-sourcemaps';
import ts from 'rollup-plugin-typescript2';

export default {
  input: './src/index.ts',
  plugins: [
    commonjs(),
    ts(),
    nodeResolve(),
    sourcemaps(),
  ],
  output: {
    format: 'esm',
    sourcemap: true,
    file: 'dist/index.js',
    plugins: [
      flatDts(),
    ],
  },
};

Then the command rollup --config ./rollup.config.js would transpile TypeScript files from src/ directory to dist/index.js, and generate dist/index.d.ts with flattened type definitions.

Limitations

This plugin flattens type definitions instead of merging them. This applies severe limitations to the source code:

  1. Every export in every TypeScript file considered exported from the package (i.e. part of public API).

    Mark internal exports (internal API) with @internal javadoc tag to prevent this, or declare internal modules with internal option.

  2. Default exports supported only at the top level and in entry points (see below).

  3. Exported symbols should be unique across the code base.

  4. Exports should not be renamed when re-exporting them.

    Aliasing is still possible.

Project Structure

To adhere to these limitations the project structure could be like this:

  1. An index file (index.ts) is present in each source directory with exported symbols.

    Index file re-exports all publicly available symbols from the same directory with statements like export * from './file';.

    Index file also re-export all symbols from nested directories with statements like export * from './dir';

  2. All exported symbols that are not re-exported by index files considered part of internal API.

    Every such symbols has a javadoc block containing @internal tag.

    Alternatively, the internal modules follow some naming convention. The internal option reflects this convention. E.g. internal: ['**/impl/**', '**/*.impl] would treat all .impl.ts source files and files in impl/ directories as part of internal API.

  3. Rollup entry points are index files.

Configuration Options

flatDts({}) accepts configuration object with the following properties:

  • tsconfig - tsconfig.json file location relative to working directory.

    tsconfig.json by default.

  • compilerOptions - TypeScript compiler options to apply.

    Override the options from tsconfig.

  • file - Output .d.ts file name relative to output directory.

    index.d.ts by default.

  • moduleName - The module name to replace flattened module declarations with.

    Defaults to package name extracted from package.json.

  • entries - Module entries.

    A map of entry name declarations (see below).

  • lib Whether to add triple-slash directives to refer the libraries used.

    Allowed values:

    • true to add an entry for each referred library from lib compiler option,
    • false (the default) to not add any library references,
    • an explicit list of libraries to refer.
  • external - External module names.

    An array of external module names and their glob patterns. These names won't be changed during flattening process.

    This is useful for external module augmentation.

  • internal - Internal module names.

    An array of internal module names and their glob patterns. Internal module type definitions are excluded from generated .d.ts files.

Multiple Entries

By default, the generated .d.ts file contains declare module statements with the same moduleName.

If your package has additional entry points then you probably want to reflect this in type definition. This can be achieved with entries option.

Here is an example configuration:

import commonjs from '@rollup/plugin-commonjs';
import nodeResolve from '@rollup/plugin-node-resolve';
import flatDts from 'rollup-plugin-flat-dts';
import sourcemaps from 'rollup-plugin-sourcemaps';
import ts from 'rollup-plugin-typescript2';

export default {
  input: {
    main: './src/index.ts',        // Main entry point
    node: './src/node/index.ts',   // Node.js-specific API
    web: './src/browser/index.ts', // Browser-specific API
  },
  plugins: [
    commonjs(),
    ts(),
    nodeResolve(),
    sourcemaps(),
  ],
  output: {
    format: 'esm',
    sourcemap: true,
    dir: 'dist',                    // Place the output files to `dist` directory.
    entryFileNames: '[name].js',    // Entry file names have `.js` extension.
    chunkFileNames: '_[name].js',   // Chunks have underscore prefix.
    plugins: [
      flatDts({
        moduleName: 'my-package',   // By default, exports belong to `my-package` module.
        entries: {
          node: {},                 // All exports from `src/node` directory
                                    // belong to `my-package/node` sub-module.
          browser: {                // All exports from `src/browser` directory
            as: 'web',              // belong to `my-package/web` sub-module.
                                    // (Would be `my-package/browser` if omitted)
            lib: 'DOM',             // Add `DOM` library reference.
            file: 'web/index.d.ts', // Write type definitions to separate `.d.ts` file.
                                    // (Would be written to main `index.d.ts` if omitted) 
          },
        },
      }),
    ],
  },
};

The package.json would contain the following then:

{
  "name": "my-package",
  "type": "module",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": "./dist/main.js",
    "./node": "./dist/node.js",
    "./web": "./dist/web.js"
  }
}

Standalone Usage

The API is available standalone, without using Rollup:

import { emitFlatDts } from 'rollup-plugin-flat-dts/api';

emitFlatDts({
  /* Type definition options */
}).then(flatDts => {
  if (flatDts.diagnostics.length) {
    console.error(flatDts.formatDiagnostics());
  }  
  return flatDts.writeOut();
}).catch(error => {
  console.error('Failed to generate type definitions', error);
  process.exit(1);
});

Algorithm Explained

The plugin algorithm is not very sophisticated, but simple and straightforward. This made it actually usable on my projects, where other tools failed to generate valid type definitions.

The simplicity comes at a price. So, this plugin applies limitations on code base rather trying to resolve all non-trivial cases.

So, here what this plugin is doing:

  1. Generates single-file type definition by overriding original tsconfig.json options with the following:

    {
      // Avoid extra work
      checkJs: false,
      // Ensure ".d.ts" modules are generated
      declaration: true,
      // Prevent output to declaration directory
      declarationDir: null,
      // Source maps unsupported
      declarationMap: false,
      // Skip ".js" generation
      emitDeclarationOnly: true,
      // Single file emission is impossible with this flag set
      isolatedModules: false,
      // Generate single file
      module: "None",
      // Always emit
      noEmit: false,
      // Skip code generation when error occurs
      noEmitOnError: true,
      // Ensure TS2742 errors are visible
      preserveSymlinks: true,
      // Ignore errors in library type definitions
      skipLibCheck: true,
      // Always strip internal exports
      stripInternal: true,
    }
    
  2. The generated file consists of declare module "path/to/file" { ... } statements. One such statement per each source file.

    The plugin replaces all "path/to/file" references with corresponding module name. I.e. either with ${packageName}, or ${packageName}/${entry.as} for matching entry point.

  3. Updates all import and export statements and adjusts module references.

    Some imports and exports removed along the way. E.g. there is no point to import to the module from itself, unless the named import or export assigns an alias to the imported symbol.

  4. Updates inner module declarations.

    Just like 2., but also expands declarations if inner module receives the same name as enclosing one.

  5. Removes module declarations that became (or was originally) empty.

Other Tools

See more here.