{"id":17491825,"url":"https://github.com/knightedcodemonkey/duel","last_synced_at":"2026-01-03T19:16:03.986Z","repository":{"id":184512281,"uuid":"672019875","full_name":"knightedcodemonkey/duel","owner":"knightedcodemonkey","description":"TypeScript dual packages.","archived":false,"fork":false,"pushed_at":"2025-04-16T03:58:33.000Z","size":294,"stargazers_count":27,"open_issues_count":2,"forks_count":3,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-04-22T20:15:58.127Z","etag":null,"topics":["commonjs","cts","dualpackage","esm","mts","nodejs","tsc","typescript","windows"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/knightedcodemonkey.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2023-07-28T17:49:22.000Z","updated_at":"2025-04-16T03:56:15.000Z","dependencies_parsed_at":"2025-04-22T20:16:00.598Z","dependency_job_id":"1009ef95-2130-4242-b3d4-35c2c175bd31","html_url":"https://github.com/knightedcodemonkey/duel","commit_stats":null,"previous_names":["knightedcodemonkey/duel"],"tags_count":37,"template":false,"template_full_name":null,"purl":"pkg:github/knightedcodemonkey/duel","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/knightedcodemonkey%2Fduel","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/knightedcodemonkey%2Fduel/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/knightedcodemonkey%2Fduel/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/knightedcodemonkey%2Fduel/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/knightedcodemonkey","download_url":"https://codeload.github.com/knightedcodemonkey/duel/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/knightedcodemonkey%2Fduel/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":262163953,"owners_count":23268779,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["commonjs","cts","dualpackage","esm","mts","nodejs","tsc","typescript","windows"],"created_at":"2024-10-19T08:05:19.019Z","updated_at":"2026-01-03T19:16:03.964Z","avatar_url":"https://github.com/knightedcodemonkey.png","language":"JavaScript","funding_links":[],"categories":["JavaScript"],"sub_categories":[],"readme":"# [`@knighted/duel`](https://www.npmjs.com/package/@knighted/duel)\n\n![CI](https://github.com/knightedcodemonkey/duel/actions/workflows/ci.yml/badge.svg)\n[![codecov](https://codecov.io/gh/knightedcodemonkey/duel/branch/main/graph/badge.svg?token=7K74BRLHFy)](https://codecov.io/gh/knightedcodemonkey/duel)\n[![NPM version](https://img.shields.io/npm/v/@knighted/duel.svg)](https://www.npmjs.com/package/@knighted/duel)\n\nTool for building a Node.js [dual package](https://nodejs.org/api/packages.html#dual-commonjses-module-packages) with TypeScript. Supports CommonJS and ES module projects.\n\n\u003e [!NOTE]\n\u003e I wish this tool were unnecessary, but dual emit was declared out of scope by the TypeScript team, so `duel` exists to fill that gap.\n\n## Features\n\n- Bidirectional ESM ↔️ CJS dual builds inferred from the package.json `type`.\n- Correctly preserves module systems for `.mts` and `.cts` file extensions.\n- No extra configuration files needed, uses `package.json` and `tsconfig.json` files.\n- Transforms the [differences between ES modules and CommonJS](https://nodejs.org/api/esm.html#differences-between-es-modules-and-commonjs).\n- Works with monorepos.\n\n## Requirements\n\n- Node \u003e= 22.21.1 (\u003c23) or \u003e= 24 (\u003c25)\n\n## Example\n\nFirst, install this package to create the `duel` executable inside your `node_modules/.bin` directory.\n\n```console\nnpm i @knighted/duel --save-dev\n```\n\nThen, given a `package.json` that defines `\"type\": \"module\"` and a `tsconfig.json` file that looks something like the following:\n\n```json\n{\n  \"compilerOptions\": {\n    \"declaration\": true,\n    \"module\": \"NodeNext\",\n    \"outDir\": \"dist\"\n  },\n  \"include\": [\"src\"]\n}\n```\n\nYou 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`:\n\n```json\n\"scripts\": {\n  \"build\": \"duel\"\n}\n```\n\nAnd then running it:\n\n```console\nnpm run build\n```\n\nIf everything worked, you should have an ESM build inside of `dist` and a CJS build inside of `dist/cjs`. You can manually update your [`exports`](https://nodejs.org/api/packages.html#exports) to match the build output, or run `duel --exports \u003cmode\u003e` to generate them automatically (see [docs/exports.md](docs/exports.md)).\n\nIt 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`.\n\n\u003e [!IMPORTANT]\n\u003e This works best if your CJS-first project uses file extensions in _relative_ specifiers. That is acceptable in CJS and [required in ESM](https://nodejs.org/api/esm.html#import-specifiers). `duel` does not rewrite bare specifiers or remap relative specifiers to directory indexes.\n\n\u003e [!TIP]\n\u003e `duel` creates a hash-named temp workspace (`.duel-cache/_duel_\u003chash\u003e_`) inside your project during a build. The `_duel_\u003chash\u003e_` temp directory is removed on success/failure unless `DUEL_KEEP_TEMP=1` is set. The `.duel-cache/` folder itself (which also holds incremental caches) is not automatically deleted—add it to your `.gitignore`. If a temp folder is ever left behind (e.g., abrupt kill), it is safe to delete.\n\n### Build orientation\n\n`duel` infers the primary vs dual build orientation from your `package.json` `type`:\n\n- `\"type\": \"module\"` → primary ESM, dual CJS\n- `\"type\": \"commonjs\"` → primary CJS, dual ESM\n\n### Output directories\n\nIf you prefer to have both builds in directories inside of your defined `outDir`, you can use the `--dirs` option.\n\n```json\n\"scripts\": {\n  \"build\": \"duel --dirs\"\n}\n```\n\nAssuming an `outDir` of `dist`, running the above will create `dist/esm` and `dist/cjs` directories.\n\n### Module transforms\n\n`tsc` is asymmetric: `import.meta` globals fail in a CJS-targeted build, but CommonJS globals like `__filename`/`__dirname` pass when targeting ESM, causing runtime errors in the compiled output. See [TypeScript#58658](https://github.com/microsoft/TypeScript/issues/58658). Use `--mode` to mitigate:\n\n- `--mode globals` [rewrites module globals](https://github.com/knightedcodemonkey/module/blob/main/docs/globals-only.md#rewrites-at-a-glance).\n- `--mode full` adds syntax lowering _in addition to_ the globals rewrite. TS sources keep a pre-`tsc` guard (`transformSyntax: \"globals-only\"`) so TypeScript controls declaration emit; JS/JSX and the dual CJS rewrite path are fully lowered. See the [mode matrix](docs/mode-matrix.md) for details.\n\n```json\n\"scripts\": {\n  \"build\": \"duel --mode globals\"\n}\n```\n\n```json\n\"scripts\": {\n  \"build\": \"duel --mode full\"\n}\n```\n\nWhen `--mode` is enabled, `duel` copies sources and runs [`@knighted/module`](https://github.com/knightedcodemonkey/module) **before** `tsc`, so TypeScript sees already-mitigated sources. That pre-`tsc` step is globals-only for `--mode globals` and full lowering for `--mode full`.\n\n### Dual package hazards\n\nMixed `import`/`require` of the same dual package (especially when conditional exports differ) can create two module instances. `duel` exposes the detector from `@knighted/module`:\n\n- `--detect-dual-package-hazard [off|warn|error]` (default `warn`): emit diagnostics; `error` exits non-zero.\n- `--dual-package-hazard-scope [file|project]` (default `file`): per-file checks or a project-wide pre-pass that aggregates package usage across all compiled sources before building.\n\nProject scope is helpful in monorepos or hoisted installs where hazards surface only when looking across files.\n\n## Options\n\nThese are the CLI options `duel` supports to work alongside your project's `tsconfig.json` settings.\n\n- `--project, -p` The path to the project's configuration file. Defaults to `tsconfig.json`.\n- `--pkg-dir, -k` The directory to start looking for a package.json file. Defaults to `--project` dir.\n- `--mode` Optional shorthand for the module transform mode: `none` (default), `globals` (globals-only), `full` (globals + full syntax lowering).\n- `--dirs, -d` Outputs both builds to directories inside of `outDir`. Defaults to `false`.\n- `--exports, -e` Generate `package.json` `exports` from build output. Values: `wildcard` | `dir` | `name`.\n- `--exports-config` Provide a JSON file with `{ \"entries\": [\"./dist/index.js\", ...], \"main\": \"./dist/index.js\" }` to limit which outputs become exports.\n- `--exports-validate` Dry-run exports generation/validation without writing package.json; combine with `--exports` or `--exports-config` to emit after validation.\n- `--rewrite-policy [safe|warn|skip]` Control how specifier rewrites behave when a matching target is missing (`safe` warns and skips, `warn` rewrites and warns, `skip` leaves specifiers untouched).\n- `--validate-specifiers` Validate that rewritten specifiers resolve to outputs; defaults to `true` when `--rewrite-policy` is `safe`.\n- `--detect-dual-package-hazard [off|warn|error]` Flag mixed import/require usage of dual packages; `error` exits non-zero.\n- `--dual-package-hazard-scope [file|project]` Run hazard checks per file (default) or aggregate across the project.\n- `--copy-mode [sources|full]` Temp copy strategy. `sources` (default) copies only files participating in the build (plus configs); `full` mirrors the previous whole-project copy.\n- `--verbose, -V` Verbose logging.\n- `--help, -h` Print the help text.\n\n\u003e [!NOTE]\n\u003e Exports keys are extensionless by design; the target `import`/`require`/`types` entries keep explicit file extensions so Node resolution remains deterministic.\n\nYou can run `duel --help` to get the same info.\n\n## Notes\n\nAs 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 TypeScript team [keep](https://github.com/microsoft/TypeScript/pull/54546) [talking](https://github.com/microsoft/TypeScript/issues/54593) about dual build support, but they continue to [refuse to rewrite specifiers](https://github.com/microsoft/TypeScript/issues/16577).\n\nFortunately, Node.js has added `--experimental-require-module` so that you can [`require()` ES modules](https://nodejs.org/api/esm.html#require) if they don't use top level await, which sets the stage for possibly no longer requiring dual builds.\n\n## Documentation\n\n- [docs/faq.md](docs/faq.md)\n- [docs/exports.md](docs/exports.md)\n- [docs/migrate-v2-v3.md](docs/migrate-v2-v3.md)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fknightedcodemonkey%2Fduel","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fknightedcodemonkey%2Fduel","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fknightedcodemonkey%2Fduel/lists"}