Skip to content

Commit

Permalink
feat: infer dual build type from package.json microsoft/TypeScript#54593
Browse files Browse the repository at this point in the history


BREAKING CHANGE: deprecates --target-extension
  • Loading branch information
knightedcodemonkey committed Aug 2, 2023
1 parent 12d0fe1 commit 6e85c22
Show file tree
Hide file tree
Showing 10 changed files with 146 additions and 156 deletions.
24 changes: 13 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,17 @@

Node.js tool for building a TypeScript dual package.

Inspired by https://github.com/microsoft/TypeScript/issues/49462.
## Features

* Bidirectional ESM <--> CJS dual builds inferred from the package.json `type`.
* Correctly preserves module systems for `.mts` and `.cts` file extensions.
* Only one package.json and tsconfig.json needed.

## Requirements

* Node >= 16.19.0.
* TypeScript, `npm i typescript`.
* A `tsconfig.json` with `outDir` defined.
* A package.json with `type` defined.
* A tsconfig.json with `outDir` defined.

## Example

Expand All @@ -28,9 +32,7 @@ Then, given a `package.json` that defines `"type": "module"` and a `tsconfig.js
{
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext",
"declaration": true,
"esModuleInterop": true,
"outDir": "dist",
"strict": true,
},
Expand All @@ -54,7 +56,7 @@ 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`](https://nodejs.org/api/packages.html#exports) to match the build output.

It should work similarly for a CJS-first project. Except, your `tsconfig.json` may define `--module` and `--moduleResolution` differently, your package.json file would use `"type": "commonjs"`, and you'd want to pass `--target-extension .mjs`.
It should work similarly for a CJS-first project. Except, your package.json file would use `"type": "commonjs"`.

See the available [options](#options).

Expand All @@ -64,25 +66,25 @@ See the available [options](#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`.
* `--target-extension, -x` The desired target extension which determines the type of dual build. Defaults to `.cjs`.
* `--pkg-dir, -k` The directory to start looking for a package.json file. Defaults to the cwd.

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

```console
Usage: duel [options]

Options:
--project, -p Compile the project given the path to its configuration file, or to a folder with a 'tsconfig.json'.
--target-extension, -x Sets the file extension for the dual build. [.cjs,.mjs]
--pkg-dir, -k The directory to start looking for a package.json file. Defaults to cwd.
--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.

As far as I can tell, `duel` is one (if not the only) way to get a correct dual package build using `tsc` while only using a **one package.json** file and **one tsconfig.json** file _and also_ preserving module system by file extension. The Microsoft backed TypeScript team [keep](https://github.com/microsoft/TypeScript/issues/54593) [talking](https://github.com/microsoft/TypeScript/pull/54546) about dual build support, but their philosophy is mainly one of self perseverance, rather than collaboration.
As far as I can tell, `duel` is one (if not the only) way to get a correct dual package build using only `tsc` while only using **one package.json** file and **one tsconfig.json** file _and also_ preserving module system by file extension. The Microsoft backed TypeScript team [keep](https://github.com/microsoft/TypeScript/issues/54593) [talking](https://github.com/microsoft/TypeScript/pull/54546) about dual build support, but their philosophy is mainly one of self perseverance, rather than collaboration. For instance, they continue to refuse to rewrite specifiers.

* Unfortunately, TypeScript doesn't really build [dual packages](https://nodejs.org/api/packages.html#dual-commonjses-module-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_. In my opinion, the `tsc` compiler is fundamentally broken in this regard, and at best is enforcing usage patterns it shouldn't. If you want to see one of my extended rants on this, check out this [comment](https://github.com/microsoft/TypeScript/pull/50985#issuecomment-1656991606). This is only mentioned for transparency, `duel` will correct for this and produce files with the module system you would expect based on the files extension, so that it works with [how Node.js determines module systems](https://nodejs.org/api/packages.html#determining-module-system).
* Unfortunately, TypeScript doesn't really build [dual packages](https://nodejs.org/api/packages.html#dual-commonjses-module-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 using **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 files extension, so that it works with [how Node.js determines module systems](https://nodejs.org/api/packages.html#determining-module-system).

* 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](https://github.com/microsoft/TypeScript/issues/49055) and TypeScript currently suggests installing the nightly build, i.e. `npm i typescript@next`.
48 changes: 10 additions & 38 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@knighted/duel",
"version": "1.0.0-rc.0",
"version": "1.0.0-rc.1",
"description": "TypeScript dual packages.",
"type": "module",
"main": "dist",
Expand Down Expand Up @@ -56,7 +56,8 @@
},
"dependencies": {
"@knighted/specifier": "^1.0.0-alpha.5",
"glob": "^10.3.3"
"glob": "^10.3.3",
"read-pkg-up": "^10.0.0"
},
"prettier": {
"arrowParens": "avoid",
Expand Down
Loading

0 comments on commit 6e85c22

Please sign in to comment.