1.0.7 • Published 2 months ago

@knighted/duel v1.0.7

Weekly downloads
-
License
MIT
Repository
github
Last release
2 months ago

@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.
  • Use only one package.json and tsconfig.json.

Requirements

  • Node >= 16.19.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.

Parallel builds

This is experimental, as your mileage may vary based on the size of your node_modules directory.

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

You might reduce your build times, but only if your project has minimal dependencies. This requires first copying your project to a parent directory of --project if it exists as a writable folder. Common gitignored directories for Node.js projects are not copied, with the exception of node_modules. See the notes as to why this can't be improved much further. In most cases, you're better off with serial builds.

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.
  • --dirs, -d Outputs both builds to directories inside of outDir. Defaults to false.
  • --parallel, -l Run the builds in parallel. 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.
--dirs, -d 		 Output both builds to directories inside of outDir. [esm, cjs].
--parallel, -l 		 Run the builds in parallel.
--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 in regards to preserving module system by file extension. For instance, there doesn't appear to be a way to convert an arbitrary .ts file into another module system, while also preserving the module system of .mts and .cts files, without requiring multiple package.json files. In my opinion, the tsc compiler is fundamentally broken in this regard, and at best is enforcing usage patterns it shouldn't. This is only mentioned for transparency, duel will correct for this and produce files with the module system you would expect based on the file's extension, so that it works with how Node.js determines module systems.

  • 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 with only one package.json and tsconfig.json file, and also preserving module system by file extension. Basically, how you expect things to work. The Microsoft backed TypeScript team keep talking about dual build support, but their philosophy is mainly one of self-preservation, rather than collaboration. For instance, they continue to refuse to rewrite specifiers. The downside of their decisions, and the fact that npm does not support using alternative names for the package.json file, is that duel must copy your project directory before attempting to run the builds in parallel.

1.0.7

2 months ago

1.0.6

3 months ago

1.0.5

3 months ago

1.0.4

5 months ago

1.0.3

5 months ago

1.0.2

5 months ago

1.0.1

7 months ago

1.0.0

8 months ago

1.0.0-rc.13

8 months ago

1.0.0-rc.12

9 months ago

1.0.0-rc.11

9 months ago

1.0.0-rc.10

9 months ago

1.0.0-rc.9

9 months ago

1.0.0-rc.8

9 months ago

1.0.0-rc.7

9 months ago

1.0.0-rc.6

9 months ago

1.0.0-rc.5

9 months ago

1.0.0-rc.4

9 months ago

1.0.0-rc.3

9 months ago

1.0.0-rc.2

9 months ago

1.0.0-rc.1

9 months ago

1.0.0-rc.0

9 months ago

1.0.0-alpha.4

9 months ago

1.0.0-alpha.3

9 months ago

1.0.0-alpha.2

9 months ago

1.0.0-alpha.1

9 months ago

1.0.0-alpha.0

9 months ago