@knighted/duel 3.2.2 → 3.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -53,48 +53,41 @@ And then running it:
53
53
  npm run build
54
54
  ```
55
55
 
56
- 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.
56
+ If 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 <mode>` to generate them automatically (see [docs/exports.md](docs/exports.md)).
57
57
 
58
58
  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`.
59
59
 
60
- ### Output directories
61
-
62
- If you prefer to have both builds in directories inside of your defined `outDir`, you can use the `--dirs` option.
60
+ > [!IMPORTANT]
61
+ > 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.
63
62
 
64
- ```json
65
- "scripts": {
66
- "build": "duel --dirs"
67
- }
68
- ```
69
-
70
- Assuming an `outDir` of `dist`, running the above will create `dist/esm` and `dist/cjs` directories.
71
-
72
- ### Module transforms
63
+ > [!NOTE]
64
+ > While `duel` runs it briefly swaps in a temporary package.json with the needed `type`; your original file is restored when the build finishes.
73
65
 
74
- 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](https://github.com/microsoft/TypeScript/issues/58658) regarding this asymmetry. Prefer the single-switch `--mode` interface: `--mode globals` (equivalent to `--modules`) or `--mode full` (equivalent to `--modules --transform-syntax`) to have the [ESM vs CJS differences](https://nodejs.org/api/esm.html#differences-between-es-modules-and-commonjs) transformed by `duel` prior to running `tsc` so you avoid compilation or runtime errors. The legacy `--modules`/`--transform-syntax` flags remain supported.
66
+ ### Build orientation
75
67
 
76
68
  `duel` infers the primary vs dual build orientation from your `package.json` `type`:
77
69
 
78
70
  - `"type": "module"` → primary ESM, dual CJS
79
71
  - `"type": "commonjs"` → primary CJS, dual ESM
80
72
 
81
- The `--dirs` flag nests outputs under `outDir/esm` and `outDir/cjs` accordingly. There is a small performance cost because sources are copied to run the transform before `tsc`.
73
+ ### Output directories
74
+
75
+ If you prefer to have both builds in directories inside of your defined `outDir`, you can use the `--dirs` option.
82
76
 
83
77
  ```json
84
78
  "scripts": {
85
- "build": "duel --modules"
79
+ "build": "duel --dirs"
86
80
  }
87
81
  ```
88
82
 
89
- For projects that need full syntax lowering, opt in explicitly:
83
+ Assuming an `outDir` of `dist`, running the above will create `dist/esm` and `dist/cjs` directories.
90
84
 
91
- ```json
92
- "scripts": {
93
- "build": "duel --modules --transform-syntax"
94
- }
95
- ```
85
+ ### Module transforms
96
86
 
97
- Using the single switch:
87
+ `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:
88
+
89
+ - `--mode globals` [rewrites module globals](https://github.com/knightedcodemonkey/module/blob/main/docs/globals-only.md#rewrites-at-a-glance).
90
+ - `--mode full` adds syntax lowering _in addition to_ the globals rewrite.
98
91
 
99
92
  ```json
100
93
  "scripts": {
@@ -108,9 +101,7 @@ Using the single switch:
108
101
  }
109
102
  ```
110
103
 
111
- #### Pre-`tsc` transform (TypeScript 58658)
112
-
113
- When you enable module transforms (`--mode globals`, `--mode full`, or `--modules`), `duel` copies your sources and runs [`@knighted/module`](https://github.com/knightedcodemonkey/module) **before** `tsc` so the transformed files no longer trigger TypeScript’s asymmetrical module-global errors (see [TypeScript#58658](https://github.com/microsoft/TypeScript/issues/58658)). No extra setup is needed: module transforms are the pre-`tsc` mitigation. If you also select full lowering (`--mode full` or `--transform-syntax`), that pre-`tsc` step performs full lowering instead of globals-only.
104
+ When `--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`.
114
105
 
115
106
  ## Options
116
107
 
@@ -118,9 +109,7 @@ The available options are limited, because you should define most of them inside
118
109
 
119
110
  - `--project, -p` The path to the project's configuration file. Defaults to `tsconfig.json`.
120
111
  - `--pkg-dir, -k` The directory to start looking for a package.json file. Defaults to `--project` dir.
121
- - `--mode` Optional shorthand for the module transform mode: `none` (default), `globals` (modules + globals-only), `full` (modules + full syntax lowering). Recommended.
122
- - `--modules, -m` Transform module globals for dual build target. Defaults to false.
123
- - `--transform-syntax, -s` Opt in to full syntax lowering via `@knighted/module` (default is globals-only). Implies `--modules`.
112
+ - `--mode` Optional shorthand for the module transform mode: `none` (default), `globals` (globals-only), `full` (globals + full syntax lowering).
124
113
  - `--dirs, -d` Outputs both builds to directories inside of `outDir`. Defaults to `false`.
125
114
  - `--exports, -e` Generate `package.json` `exports` from build output. Values: `wildcard` | `dir` | `name`.
126
115
 
@@ -129,16 +118,6 @@ The available options are limited, because you should define most of them inside
129
118
 
130
119
  You can run `duel --help` to get the same info.
131
120
 
132
- ## Gotchas
133
-
134
- 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.
135
-
136
- - 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](https://nodejs.org/api/esm.html#import-specifiers). This package makes no attempt to rewrite bare specifiers, or remap any relative specifiers to a directory index.
137
-
138
- - Unfortunately, `tsc` doesn't support [dual packages](https://nodejs.org/api/packages.html#dual-commonjses-module-packages) completely. For mitigation details, see the pre-`tsc` transform note above (`--modules`).
139
-
140
- - 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.
141
-
142
121
  ## Notes
143
122
 
144
123
  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 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).
package/dist/cjs/duel.cjs CHANGED
@@ -27,7 +27,7 @@ const getSubpath = (mode, relFromRoot) => {
27
27
  }
28
28
  if (mode === 'dir') {
29
29
  const last = segments.at(-1);
30
- return last ? `./${last}` : null;
30
+ return last ? `./${last}/*` : null;
31
31
  }
32
32
  if (mode === 'wildcard') {
33
33
  const first = segments[0];
@@ -56,6 +56,17 @@ const generateExports = async (options) => {
56
56
  if (esmRootPosix.startsWith(`${cjsRootPosix}/`)) {
57
57
  cjsIgnore.push(`${esmRootPosix}/**`);
58
58
  }
59
+ const toWildcardValue = value => {
60
+ const dir = node_path_1.posix.dirname(value);
61
+ const file = node_path_1.posix.basename(value);
62
+ const dtsMatch = file.match(/(\.d\.(?:ts|mts|cts))$/i);
63
+ if (dtsMatch) {
64
+ const ext = dtsMatch[1];
65
+ return dir === '.' ? `./*${ext}` : `${dir}/*${ext}`;
66
+ }
67
+ const ext = node_path_1.posix.extname(file);
68
+ return dir === '.' ? `./*${ext}` : `${dir}/*${ext}`;
69
+ };
59
70
  const recordPath = (kind, filePath, root) => {
60
71
  const relPkg = toPosix((0, node_path_1.relative)(pkgDir, filePath));
61
72
  const relFromRoot = toPosix((0, node_path_1.relative)(root, filePath));
@@ -65,18 +76,19 @@ const generateExports = async (options) => {
65
76
  baseEntry[kind] = withDot;
66
77
  baseMap.set(baseKey, baseEntry);
67
78
  const subpath = getSubpath(mode, relFromRoot);
79
+ const useWildcard = subpath?.includes('*');
68
80
  if (kind === 'types') {
69
81
  const mappedSubpath = baseToSubpath.get(baseKey);
70
82
  if (mappedSubpath) {
71
83
  const subEntry = subpathMap.get(mappedSubpath) ?? {};
72
- subEntry.types = withDot;
84
+ subEntry.types = useWildcard ? toWildcardValue(withDot) : withDot;
73
85
  subpathMap.set(mappedSubpath, subEntry);
74
86
  }
75
87
  return;
76
88
  }
77
89
  if (subpath && subpath !== '.') {
78
90
  const subEntry = subpathMap.get(subpath) ?? {};
79
- subEntry[kind] = withDot;
91
+ subEntry[kind] = useWildcard ? toWildcardValue(withDot) : withDot;
80
92
  subpathMap.set(subpath, subEntry);
81
93
  baseToSubpath.set(baseKey, subpath);
82
94
  }
@@ -148,9 +160,10 @@ const generateExports = async (options) => {
148
160
  exportsMap[subpath] = out;
149
161
  }
150
162
  }
151
- if (!exportsMap['.'] && baseMap.size) {
152
- const [subpath, entry] = subpathMap.entries().next().value ?? [];
153
- if (entry) {
163
+ if (!exportsMap['.']) {
164
+ const firstNonWildcard = [...subpathMap.entries()].find(([key]) => !key.includes('*'));
165
+ if (firstNonWildcard) {
166
+ const [subpath, entry] = firstNonWildcard;
154
167
  const out = {};
155
168
  if (entry.types) {
156
169
  out.types = entry.types;
@@ -169,7 +182,9 @@ const generateExports = async (options) => {
169
182
  }
170
183
  if (Object.keys(out).length) {
171
184
  exportsMap['.'] = out;
172
- exportsMap[subpath] ??= out;
185
+ if (!exportsMap[subpath]) {
186
+ exportsMap[subpath] = out;
187
+ }
173
188
  }
174
189
  }
175
190
  }
package/dist/cjs/init.cjs CHANGED
@@ -68,15 +68,21 @@ const init = async (args) => {
68
68
  (0, util_js_1.log)('Options:', 'info', bare);
69
69
  (0, util_js_1.log)("--project, -p [path] \t\t Compile the project given the path to its configuration file, or to a folder with a 'tsconfig.json'.", 'info', bare);
70
70
  (0, util_js_1.log)('--pkg-dir, -k [path] \t\t The directory to start looking for a package.json file. Defaults to --project directory.', 'info', bare);
71
- (0, util_js_1.log)('--modules, -m \t\t\t Transform module globals for dual build target. Defaults to false.', 'info', bare);
71
+ (0, util_js_1.log)('--modules, -m \t\t\t Transform module globals for dual build target. Defaults to false. (deprecated; use --mode globals/full).', 'info', bare);
72
72
  (0, util_js_1.log)('--dirs, -d \t\t\t Output both builds to directories inside of outDir. [esm, cjs].', 'info', bare);
73
73
  (0, util_js_1.log)('--exports, -e \t\t\t Generate package.json exports. Values: wildcard | dir | name.', 'info', bare);
74
- (0, util_js_1.log)('--transform-syntax, -s \t\t Opt in to full syntax lowering via @knighted/module (default is globals-only).', 'info', bare);
74
+ (0, util_js_1.log)('--transform-syntax, -s \t\t Opt in to full syntax lowering via @knighted/module (default is globals-only). (deprecated; use --mode full).', 'info', bare);
75
75
  (0, util_js_1.log)('--mode [none|globals|full] \t Optional shorthand for module transforms and syntax lowering.', 'info', bare);
76
76
  (0, util_js_1.log)('--help, -h \t\t\t Print this message.', 'info', bare);
77
77
  }
78
78
  else {
79
79
  const { project, 'target-extension': targetExt, 'pkg-dir': pkgDir, modules, dirs, exports: exportsOpt, 'transform-syntax': transformSyntax, mode, } = parsed;
80
+ if (modules) {
81
+ (0, util_js_1.logWarn)('--modules is deprecated; prefer --mode globals or --mode full.');
82
+ }
83
+ if (transformSyntax) {
84
+ (0, util_js_1.logWarn)('--transform-syntax is deprecated; prefer --mode full.');
85
+ }
80
86
  let configPath = (0, node_path_1.resolve)(project);
81
87
  let stats = null;
82
88
  let pkg = null;
package/dist/esm/duel.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { argv, platform } from 'node:process';
3
- import { join, dirname, resolve, relative, parse as parsePath } from 'node:path';
3
+ import { join, dirname, resolve, relative, parse as parsePath, posix } from 'node:path';
4
4
  import { spawn } from 'node:child_process';
5
5
  import { writeFile, rm, rename, mkdir, cp, access, readFile } from 'node:fs/promises';
6
6
  import { randomBytes } from 'node:crypto';
@@ -24,7 +24,7 @@ const getSubpath = (mode, relFromRoot) => {
24
24
  }
25
25
  if (mode === 'dir') {
26
26
  const last = segments.at(-1);
27
- return last ? `./${last}` : null;
27
+ return last ? `./${last}/*` : null;
28
28
  }
29
29
  if (mode === 'wildcard') {
30
30
  const first = segments[0];
@@ -53,6 +53,17 @@ const generateExports = async (options) => {
53
53
  if (esmRootPosix.startsWith(`${cjsRootPosix}/`)) {
54
54
  cjsIgnore.push(`${esmRootPosix}/**`);
55
55
  }
56
+ const toWildcardValue = value => {
57
+ const dir = posix.dirname(value);
58
+ const file = posix.basename(value);
59
+ const dtsMatch = file.match(/(\.d\.(?:ts|mts|cts))$/i);
60
+ if (dtsMatch) {
61
+ const ext = dtsMatch[1];
62
+ return dir === '.' ? `./*${ext}` : `${dir}/*${ext}`;
63
+ }
64
+ const ext = posix.extname(file);
65
+ return dir === '.' ? `./*${ext}` : `${dir}/*${ext}`;
66
+ };
56
67
  const recordPath = (kind, filePath, root) => {
57
68
  const relPkg = toPosix(relative(pkgDir, filePath));
58
69
  const relFromRoot = toPosix(relative(root, filePath));
@@ -62,18 +73,19 @@ const generateExports = async (options) => {
62
73
  baseEntry[kind] = withDot;
63
74
  baseMap.set(baseKey, baseEntry);
64
75
  const subpath = getSubpath(mode, relFromRoot);
76
+ const useWildcard = subpath?.includes('*');
65
77
  if (kind === 'types') {
66
78
  const mappedSubpath = baseToSubpath.get(baseKey);
67
79
  if (mappedSubpath) {
68
80
  const subEntry = subpathMap.get(mappedSubpath) ?? {};
69
- subEntry.types = withDot;
81
+ subEntry.types = useWildcard ? toWildcardValue(withDot) : withDot;
70
82
  subpathMap.set(mappedSubpath, subEntry);
71
83
  }
72
84
  return;
73
85
  }
74
86
  if (subpath && subpath !== '.') {
75
87
  const subEntry = subpathMap.get(subpath) ?? {};
76
- subEntry[kind] = withDot;
88
+ subEntry[kind] = useWildcard ? toWildcardValue(withDot) : withDot;
77
89
  subpathMap.set(subpath, subEntry);
78
90
  baseToSubpath.set(baseKey, subpath);
79
91
  }
@@ -145,9 +157,10 @@ const generateExports = async (options) => {
145
157
  exportsMap[subpath] = out;
146
158
  }
147
159
  }
148
- if (!exportsMap['.'] && baseMap.size) {
149
- const [subpath, entry] = subpathMap.entries().next().value ?? [];
150
- if (entry) {
160
+ if (!exportsMap['.']) {
161
+ const firstNonWildcard = [...subpathMap.entries()].find(([key]) => !key.includes('*'));
162
+ if (firstNonWildcard) {
163
+ const [subpath, entry] = firstNonWildcard;
151
164
  const out = {};
152
165
  if (entry.types) {
153
166
  out.types = entry.types;
@@ -166,7 +179,9 @@ const generateExports = async (options) => {
166
179
  }
167
180
  if (Object.keys(out).length) {
168
181
  exportsMap['.'] = out;
169
- exportsMap[subpath] ??= out;
182
+ if (!exportsMap[subpath]) {
183
+ exportsMap[subpath] = out;
184
+ }
170
185
  }
171
186
  }
172
187
  }
package/dist/esm/init.js CHANGED
@@ -3,7 +3,7 @@ import { resolve, join, dirname } from 'node:path';
3
3
  import { stat } from 'node:fs/promises';
4
4
  import { parseTsconfig } from 'get-tsconfig';
5
5
  import { readPackageUp } from 'read-package-up';
6
- import { logError, log } from './util.js';
6
+ import { logError, log, logWarn } from './util.js';
7
7
  const init = async (args) => {
8
8
  let parsed = null;
9
9
  try {
@@ -65,15 +65,21 @@ const init = async (args) => {
65
65
  log('Options:', 'info', bare);
66
66
  log("--project, -p [path] \t\t Compile the project given the path to its configuration file, or to a folder with a 'tsconfig.json'.", 'info', bare);
67
67
  log('--pkg-dir, -k [path] \t\t The directory to start looking for a package.json file. Defaults to --project directory.', 'info', bare);
68
- log('--modules, -m \t\t\t Transform module globals for dual build target. Defaults to false.', 'info', bare);
68
+ log('--modules, -m \t\t\t Transform module globals for dual build target. Defaults to false. (deprecated; use --mode globals/full).', 'info', bare);
69
69
  log('--dirs, -d \t\t\t Output both builds to directories inside of outDir. [esm, cjs].', 'info', bare);
70
70
  log('--exports, -e \t\t\t Generate package.json exports. Values: wildcard | dir | name.', 'info', bare);
71
- log('--transform-syntax, -s \t\t Opt in to full syntax lowering via @knighted/module (default is globals-only).', 'info', bare);
71
+ log('--transform-syntax, -s \t\t Opt in to full syntax lowering via @knighted/module (default is globals-only). (deprecated; use --mode full).', 'info', bare);
72
72
  log('--mode [none|globals|full] \t Optional shorthand for module transforms and syntax lowering.', 'info', bare);
73
73
  log('--help, -h \t\t\t Print this message.', 'info', bare);
74
74
  }
75
75
  else {
76
76
  const { project, 'target-extension': targetExt, 'pkg-dir': pkgDir, modules, dirs, exports: exportsOpt, 'transform-syntax': transformSyntax, mode, } = parsed;
77
+ if (modules) {
78
+ logWarn('--modules is deprecated; prefer --mode globals or --mode full.');
79
+ }
80
+ if (transformSyntax) {
81
+ logWarn('--transform-syntax is deprecated; prefer --mode full.');
82
+ }
77
83
  let configPath = resolve(project);
78
84
  let stats = null;
79
85
  let pkg = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@knighted/duel",
3
- "version": "3.2.2",
3
+ "version": "3.2.4",
4
4
  "description": "TypeScript dual packages.",
5
5
  "type": "module",
6
6
  "main": "dist/esm/duel.js",
@@ -26,7 +26,7 @@
26
26
  "test:integration": "node --test --test-reporter=spec test/integration.js",
27
27
  "test:monorepos": "node --test --test-reporter=spec test/monorepos.js",
28
28
  "test": "c8 --reporter=text --reporter=text-summary --reporter=lcov node --test --test-reporter=spec test/integration.js test/monorepos.js",
29
- "build": "node src/duel.js --dirs --modules",
29
+ "build": "node src/duel.js --dirs --mode globals",
30
30
  "prepack": "npm run build",
31
31
  "prepare": "husky"
32
32
  },
@@ -82,7 +82,7 @@
82
82
  "vite": "^7.2.4"
83
83
  },
84
84
  "dependencies": {
85
- "@knighted/module": "^1.3.0",
85
+ "@knighted/module": "^1.3.1",
86
86
  "find-up": "^8.0.0",
87
87
  "get-tsconfig": "^4.13.0",
88
88
  "glob": "^13.0.0",