diff --git a/README.md b/README.md index 23347a7..27e1897 100644 --- a/README.md +++ b/README.md @@ -12,16 +12,17 @@ Early stages of development. Inspired by https://github.com/microsoft/TypeScript * Node >= 16.19.0. * TypeScript, `npm i typescript`. +* A `tsconfig.json` with `outDir` defined. ## Example -First, install the package to create the `duel` executable inside your `node_modules/.bin` directory. +First, install this package to create the `duel` executable inside your `node_modules/.bin` directory. ```console user@comp ~ $ npm i @knighted/duel ``` -Then, given a `package.json` that defines `"type": "module"` and a `tsconfig.json` file that looks like the following: +Then, given a `package.json` that defines `"type": "module"` and a `tsconfig.json` file that looks something like the following: ```json { @@ -51,9 +52,9 @@ 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` in package.json to match the build output. +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` would define `--module` and `--moduleResolution` differently, and you'd want to pass `-x .mjs`. +It should work similarly for a CJS first project. Except, your `tsconfig.json` would define `--module` and `--moduleResolution` differently, and you'd want to pass `--target-extension .mjs`. See the available [options](#options). @@ -71,13 +72,15 @@ You can run `duel --help` to get more info. Below is the output of that: 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] ---help, -h Print this message. +--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] +--help, -h Print this message. ``` ## Gotchas -* Unfortunately, TypeScript doesn't really understand dual packages very well. For instance, it will **always** create CJS exports when `--module commonjs` is used, even on files with an `.mts` extension. One reference issue is https://github.com/microsoft/TypeScript/issues/54573. If you use `.mts` extensions to enforce an ESM module system, this might break in the corresponding dual CJS build. -* If targeting a dual CJS build, and you are using [top level `await`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await#top_level_await), you will most likely encounter the compilation error `error TS1378: Top-level 'await' expressions are only allowed when the 'module' option is set to 'es2022', 'esnext', 'system', 'node16', or 'nodenext', and the 'target' option is set to 'es2017' or higher.` during the CJS build. This is because `duel` creates a temporary `tsconfig.json` from your original and overwrites the `--module` and `--moduleResolution` based on the provided `--target-ext`. +* 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, it will **always** create CJS exports when `--module commonjs` is used, _even on files with an `.mts` extension_, which is contrary to [how Node determines module systems](https://nodejs.org/api/packages.html#determining-module-system). The `tsc` compiler is fundamentally broken in this regard. One reference issue is https://github.com/microsoft/TypeScript/issues/54573. If you use `.mts` extensions to enforce an ESM module system, this will break in the corresponding dual CJS build. There is no way to fix this until TypeScript fixes their compiler. + +* If targeting a dual CJS build, and you are using [top level `await`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await#top_level_await), you will most likely encounter the compilation error `error TS1378: Top-level 'await' expressions are only allowed when the 'module' option is set to 'es2022', 'esnext', 'system', 'node16', or 'nodenext', and the 'target' option is set to 'es2017' or higher.` during the CJS build. This is because `duel` creates a temporary `tsconfig.json` from your original and necessarily overwrites the `--module` and `--moduleResolution` based on the provided `--target-ext`. There is no workaround other than to **not** use top level await if you want a dual build. + * 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`. diff --git a/package-lock.json b/package-lock.json index 3deed60..5a8c555 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@knighted/duel", - "version": "1.0.0-alpha.2", + "version": "1.0.0-alpha.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@knighted/duel", - "version": "1.0.0-alpha.2", + "version": "1.0.0-alpha.3", "license": "MIT", "dependencies": { "@knighted/specifier": "^1.0.0-alpha.5", diff --git a/package.json b/package.json index 36affdc..d226a1f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@knighted/duel", - "version": "1.0.0-alpha.2", + "version": "1.0.0-alpha.3", "description": "TypeScript dual packages.", "type": "module", "main": "dist", diff --git a/src/init.js b/src/init.js index acfebd5..9e4855a 100644 --- a/src/init.js +++ b/src/init.js @@ -41,12 +41,12 @@ const init = async args => { log('Usage: duel [options]\n') log('Options:') log( - "--project, -p \t\t\t Compile the project given the path to its configuration file, or to a folder with a 'tsconfig.json'.", + "--project, -p \t\t Compile the project given the path to its configuration file, or to a folder with a 'tsconfig.json'.", ) log( - '--target-extension, -x \t\t Sets the file extension for the dual build. [.cjs,.mjs]', + '--target-extension, -x \t Sets the file extension for the dual build. [.cjs,.mjs]', ) - log('--help, -h \t\t\t Print this message.') + log('--help, -h \t\t Print this message.') } else { const { project, 'target-extension': targetExt } = parsed let configPath = resolve(project) diff --git a/test/integration.js b/test/integration.js index c0df522..59a4727 100644 --- a/test/integration.js +++ b/test/integration.js @@ -3,6 +3,7 @@ import assert from 'node:assert/strict' import { fileURLToPath } from 'node:url' import { dirname, resolve } from 'node:path' import { rm } from 'node:fs/promises' +import { existsSync } from 'node:fs' import { duel } from '../src/duel.js' @@ -87,6 +88,13 @@ describe('duel', () => { assert.ok( spy.mock.calls[2].arguments[0].startsWith('Successfully created a dual CJS build'), ) + // Check that the expected files and extensions are there + assert.ok(existsSync(resolve(dist, 'index.js'))) + assert.ok(existsSync(resolve(dist, 'index.d.ts'))) + assert.ok(existsSync(resolve(dist, 'cjs.cjs'))) + assert.ok(existsSync(resolve(dist, 'cjs/index.cjs'))) + assert.ok(existsSync(resolve(dist, 'cjs/index.d.cts'))) + assert.ok(existsSync(resolve(dist, 'cjs/esm.mjs'))) }) it('reports compilation errors during a build', async t => {