npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@atombyte/duel

v2.0.0-rc.1

Published

TypeScript dual packages.

Downloads

15

Readme

@knighted/duel

CI codecov NPM version

Tool for building a Node.js dual package with TypeScript. Supports CommonJS and ES module projects.

Features

  • Bidirectional ESM ↔️ CJS dual builds inferred from the package.json type.
  • Correctly preserves module systems for .mts and .cts file extensions.
  • No extra configuration files needed, uses package.json and tsconfig.json files.
  • Transforms the differences between ES modules and CommonJS.
  • Works with monorepos.

Requirements

  • Node >= 20.11.0

Example

First, install this package to create the duel executable inside your node_modules/.bin directory.

user@comp ~ $ npm i @knighted/duel --save-dev

Then, given a package.json that defines "type": "module" and a tsconfig.json file that looks something like the following:

{
  "compilerOptions": {
    "declaration": true,
    "module": "NodeNext",
    "outDir": "dist"
  },
  "include": ["src"]
}

You can create an ES module build for the project defined by the above configuration, and also a dual CJS build by defining the following npm run script in your package.json:

"scripts": {
  "build": "duel"
}

And then running it:

user@comp ~ $ npm run build

If everything worked, you should have an ESM build inside of dist and a CJS build inside of dist/cjs. Now you can update your exports to match the build output.

It should work similarly for a CJS-first project. Except, your package.json file would use "type": "commonjs" and the dual build directory is in dist/esm.

Output directories

If you prefer to have both builds in directories inside of your defined outDir, you can use the --dirs option.

"scripts": {
  "build": "duel --dirs"
}

Assuming an outDir of dist, running the above will create dist/esm and dist/cjs directories.

Module transforms

TypeScript will throw compiler errors when using import.meta globals while targeting a CommonJS dual build, but will not throw compiler errors when the inverse is true, i.e. using CommonJS globals (__filename, __dirname, etc.) while targeting an ES module dual build. There is an open issue regarding this unexpected behavior. You can use the --modules option to have the differences between ES modules and CommonJS transformed by duel prior to running compilation with tsc so that there are no compilation or runtime errors.

Note, there is a slight performance penalty since your project needs to be copied first to run the transforms before compiling with tsc.

"scripts": {
  "build": "duel --modules"
}

This feature is still a work in progress regarding transforming exports when targeting an ES module build (relies on @knighted/module).

Options

The available options are limited, because you should define most of them inside your project's tsconfig.json file.

  • --project, -p The path to the project's configuration file. Defaults to tsconfig.json.
  • --pkg-dir, -k The directory to start looking for a package.json file. Defaults to the cwd.
  • --modules, -m Transform module globals for dual build target. Defaults to false.
  • --dirs, -d Outputs both builds to directories inside of outDir. Defaults to false.

You can run duel --help to get the same info. Below is the output of that:

Usage: duel [options]

Options:
--project, -p [path] 	 Compile the project given the path to its configuration file, or to a folder with a 'tsconfig.json'.
--pkg-dir, -k [path] 	 The directory to start looking for a package.json file. Defaults to cwd.
--modules, -m 		 Transform module globals for dual build target. Defaults to false.
--dirs, -d 		 Output both builds to directories inside of outDir. [esm, cjs].
--help, -h 		 Print this message.

Gotchas

These are definitely edge cases, and would only really come up if your project mixes file extensions. For example, if you have .ts files combined with .mts, and/or .cts. For most projects, things should just work as expected.

  • This is going to work best if your CJS-first project uses file extensions in relative specifiers. This is completely acceptable in CJS projects, and required in ESM projects. This package makes no attempt to rewrite bare specifiers, or remap any relative specifiers to a directory index.

  • Unfortunately, TypeScript doesn't really build dual packages very well. One instance of unexpected behavior is when the compiler throws errors for ES module globals when running a dual CJS build, but not for the inverse case, despite both causing runtime errors in Node.js. See the open issue. You can circumvent this with duel by using the --modules option if your project uses module globals such as import.meta properties or __dirname, __filename, etc. in a CommonJS project.

  • If doing an import type across module systems, i.e. from .mts into .cts, or vice versa, you might encounter the compilation error error TS1452: 'resolution-mode' assertions are only supported when `moduleResolution` is `node16` or `nodenext`.. This is a known issue and TypeScript currently suggests installing the nightly build, i.e. npm i typescript@next.

  • If running duel with your project's package.json file open in your editor, you may temporarily see the content replaced. This is because duel dynamically creates a new package.json using the type necessary for the dual build. Your original package.json will be restored after the build completes.

Notes

As far as I can tell, duel is one (if not the only) way to get a correct dual package build using tsc without requiring multiple tsconfig.json files or extra configuration. The Microsoft backed TypeScript team keep talking about dual build support, but they continue to refuse to rewrite specifiers.

Fortunately, Node.js has added --experimental-require-module so that you can require() ES modules if they don't use top level await, which sets the stage for possibly no longer requiring dual builds.