@knighted/duel 2.1.7 → 3.0.0

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
@@ -16,7 +16,7 @@ Tool for building a Node.js [dual package](https://nodejs.org/api/packages.html#
16
16
 
17
17
  ## Requirements
18
18
 
19
- - Node >= 20.11.0
19
+ - Node >= 22.21.1 (<23) or >= 24 (<25)
20
20
 
21
21
  ## Example
22
22
 
@@ -73,6 +73,13 @@ Assuming an `outDir` of `dist`, running the above will create `dist/esm` and `di
73
73
 
74
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 unexpected behavior. You can use the `--modules` option to have the [differences between ES modules and CommonJS](https://nodejs.org/api/esm.html#differences-between-es-modules-and-commonjs) transformed by `duel` prior to running compilation with `tsc` so that there are no compilation or runtime errors.
75
75
 
76
+ `duel` infers the primary vs dual build orientation from your `package.json` `type`:
77
+
78
+ - `"type": "module"` → primary ESM, dual CJS
79
+ - `"type": "commonjs"` → primary CJS, dual ESM
80
+
81
+ The `--dirs` flag nests outputs under `outDir/esm` and `outDir/cjs` accordingly.
82
+
76
83
  Note, there is a slight performance penalty since your project needs to be copied first to run the transforms before compiling with `tsc`.
77
84
 
78
85
  ```json
package/dist/cjs/duel.cjs CHANGED
@@ -10,7 +10,6 @@ const node_crypto_1 = require("node:crypto");
10
10
  const node_perf_hooks_1 = require("node:perf_hooks");
11
11
  const glob_1 = require("glob");
12
12
  const find_up_1 = require("find-up");
13
- const specifier_1 = require("@knighted/specifier");
14
13
  const module_1 = require("@knighted/module");
15
14
  const init_js_1 = require("./init.cjs");
16
15
  const util_js_1 = require("./util.cjs");
@@ -50,7 +49,11 @@ const duel = async (args) => {
50
49
  const absoluteOutDir = (0, node_path_1.resolve)(projectDir, outDir);
51
50
  const originalType = pkg.packageJson.type ?? 'commonjs';
52
51
  const isCjsBuild = originalType !== 'commonjs';
53
- const targetExt = isCjsBuild ? '.cjs' : '.mjs';
52
+ const primaryOutDir = dirs
53
+ ? isCjsBuild
54
+ ? (0, node_path_1.join)(absoluteOutDir, 'esm')
55
+ : (0, node_path_1.join)(absoluteOutDir, 'cjs')
56
+ : absoluteOutDir;
54
57
  const hex = (0, node_crypto_1.randomBytes)(4).toString('hex');
55
58
  const getOverrideTsConfig = () => {
56
59
  return {
@@ -63,29 +66,39 @@ const duel = async (args) => {
63
66
  };
64
67
  };
65
68
  const runPrimaryBuild = () => {
66
- return runBuild(configPath, dirs
67
- ? isCjsBuild
68
- ? (0, node_path_1.join)(absoluteOutDir, 'esm')
69
- : (0, node_path_1.join)(absoluteOutDir, 'cjs')
70
- : absoluteOutDir);
69
+ return runBuild(configPath, primaryOutDir);
71
70
  };
72
- const updateSpecifiersAndFileExtensions = async (filenames) => {
71
+ const updateSpecifiersAndFileExtensions = async (filenames, target, ext) => {
73
72
  for (const filename of filenames) {
74
73
  const dts = /(\.d\.ts)$/;
75
- const outFilename = dts.test(filename)
76
- ? filename.replace(dts, isCjsBuild ? '.d.cts' : '.d.mts')
77
- : filename.replace(/\.js$/, targetExt);
78
- const update = await specifier_1.specifier.update(filename, ({ value }) => {
79
- // Collapse any BinaryExpression or NewExpression to test for a relative specifier
74
+ const isDts = dts.test(filename);
75
+ const outFilename = isDts
76
+ ? filename.replace(dts, target === 'commonjs' ? '.d.cts' : '.d.mts')
77
+ : filename.replace(/\.js$/, ext);
78
+ if (isDts) {
79
+ const source = await (0, promises_1.readFile)(filename, 'utf8');
80
+ const rewritten = source.replace(/(?<=['"])(\.\.?(?:\/[\w.-]+)*)\.js(?=['"])/g, `$1${ext}`);
81
+ await (0, promises_1.writeFile)(outFilename, rewritten);
82
+ if (outFilename !== filename) {
83
+ await (0, promises_1.rm)(filename, { force: true });
84
+ }
85
+ continue;
86
+ }
87
+ const rewriteSpecifier = (value = '') => {
80
88
  const collapsed = value.replace(/['"`+)\s]|new String\(/g, '');
81
- const relative = /^(?:\.|\.\.)\//;
82
- if (relative.test(collapsed)) {
83
- // $2 is for any closing quotation/parens around BE or NE
84
- return value.replace(/(.+)\.js([)'"`]*)?$/, `$1${targetExt}$2`);
89
+ if (/^(?:\.|\.\.)\//.test(collapsed)) {
90
+ return value.replace(/(.+)\.js([)"'`]*)?$/, `$1${ext}$2`);
85
91
  }
86
- });
87
- await (0, promises_1.writeFile)(outFilename, update);
88
- await (0, promises_1.rm)(filename, { force: true });
92
+ };
93
+ const writeOptions = {
94
+ target,
95
+ rewriteSpecifier,
96
+ ...(outFilename === filename ? { inPlace: true } : { out: outFilename }),
97
+ };
98
+ await (0, module_1.transform)(filename, writeOptions);
99
+ if (outFilename !== filename) {
100
+ await (0, promises_1.rm)(filename, { force: true });
101
+ }
89
102
  }
90
103
  };
91
104
  const logSuccess = start => {
@@ -112,7 +125,11 @@ const duel = async (args) => {
112
125
  const compileFiles = (0, util_js_1.getCompileFiles)(tsc, projectDir);
113
126
  dualConfigPath = (0, node_path_1.join)(subDir, `tsconfig.${hex}.json`);
114
127
  await (0, promises_1.mkdir)(subDir);
115
- await Promise.all(compileFiles.map(file => (0, promises_1.cp)(file, (0, node_path_1.join)(subDir, (0, node_path_1.relative)(projectDir, file).replace(/^(\.\.\/)*/, '')))));
128
+ await Promise.all(compileFiles.map(async (file) => {
129
+ const dest = (0, node_path_1.join)(subDir, (0, node_path_1.relative)(projectDir, file).replace(/^(\.\.\/)+/, ''));
130
+ await (0, promises_1.mkdir)((0, node_path_1.dirname)(dest), { recursive: true });
131
+ await (0, promises_1.cp)(file, dest);
132
+ }));
116
133
  /**
117
134
  * Transform ambiguous modules for the target dual build.
118
135
  * @see https://github.com/microsoft/TypeScript/issues/58658
@@ -129,7 +146,11 @@ const duel = async (args) => {
129
146
  *
130
147
  * @see https://github.com/microsoft/TypeScript/issues/58658
131
148
  */
132
- await (0, module_1.transform)(file, { out: file, type: isCjsBuild ? 'commonjs' : 'module' });
149
+ await (0, module_1.transform)(file, {
150
+ out: file,
151
+ target: isCjsBuild ? 'commonjs' : 'module',
152
+ transformSyntax: false,
153
+ });
133
154
  }
134
155
  }
135
156
  /**
@@ -161,10 +182,16 @@ const duel = async (args) => {
161
182
  }
162
183
  }
163
184
  if (success) {
185
+ const dualTarget = isCjsBuild ? 'commonjs' : 'module';
186
+ const dualTargetExt = isCjsBuild ? '.cjs' : dirs ? '.js' : '.mjs';
164
187
  const filenames = await (0, glob_1.glob)(`${absoluteDualOutDir.replace(/\\/g, '/')}/**/*{.js,.d.ts}`, {
165
188
  ignore: 'node_modules/**',
166
189
  });
167
- await updateSpecifiersAndFileExtensions(filenames);
190
+ await updateSpecifiersAndFileExtensions(filenames, dualTarget, dualTargetExt);
191
+ if (dirs && originalType === 'commonjs') {
192
+ const primaryFiles = await (0, glob_1.glob)(`${primaryOutDir.replace(/\\/g, '/')}/**/*{.js,.d.ts}`, { ignore: 'node_modules/**' });
193
+ await updateSpecifiersAndFileExtensions(primaryFiles, 'commonjs', '.cjs');
194
+ }
168
195
  logSuccess(startTime);
169
196
  }
170
197
  }
@@ -173,7 +200,7 @@ const duel = async (args) => {
173
200
  exports.duel = duel;
174
201
  (async () => {
175
202
  const realFileUrlArgv1 = await (0, util_js_1.getRealPathAsFileUrl)(node_process_1.argv[1] ?? '');
176
- if (require("node:url").pathToFileURL(__filename).toString() === realFileUrlArgv1) {
203
+ if (require("node:url").pathToFileURL(__filename).href === realFileUrlArgv1) {
177
204
  await duel();
178
205
  }
179
206
  })();
package/dist/esm/duel.js CHANGED
@@ -2,12 +2,11 @@
2
2
  import { argv, platform } from 'node:process';
3
3
  import { join, dirname, resolve, relative } from 'node:path';
4
4
  import { spawn } from 'node:child_process';
5
- import { writeFile, rm, rename, mkdir, cp, access } from 'node:fs/promises';
5
+ import { writeFile, rm, rename, mkdir, cp, access, readFile } from 'node:fs/promises';
6
6
  import { randomBytes } from 'node:crypto';
7
7
  import { performance } from 'node:perf_hooks';
8
8
  import { glob } from 'glob';
9
9
  import { findUp } from 'find-up';
10
- import { specifier } from '@knighted/specifier';
11
10
  import { transform } from '@knighted/module';
12
11
  import { init } from './init.js';
13
12
  import { getRealPathAsFileUrl, getCompileFiles, logError, log } from './util.js';
@@ -47,7 +46,11 @@ const duel = async (args) => {
47
46
  const absoluteOutDir = resolve(projectDir, outDir);
48
47
  const originalType = pkg.packageJson.type ?? 'commonjs';
49
48
  const isCjsBuild = originalType !== 'commonjs';
50
- const targetExt = isCjsBuild ? '.cjs' : '.mjs';
49
+ const primaryOutDir = dirs
50
+ ? isCjsBuild
51
+ ? join(absoluteOutDir, 'esm')
52
+ : join(absoluteOutDir, 'cjs')
53
+ : absoluteOutDir;
51
54
  const hex = randomBytes(4).toString('hex');
52
55
  const getOverrideTsConfig = () => {
53
56
  return {
@@ -60,29 +63,39 @@ const duel = async (args) => {
60
63
  };
61
64
  };
62
65
  const runPrimaryBuild = () => {
63
- return runBuild(configPath, dirs
64
- ? isCjsBuild
65
- ? join(absoluteOutDir, 'esm')
66
- : join(absoluteOutDir, 'cjs')
67
- : absoluteOutDir);
66
+ return runBuild(configPath, primaryOutDir);
68
67
  };
69
- const updateSpecifiersAndFileExtensions = async (filenames) => {
68
+ const updateSpecifiersAndFileExtensions = async (filenames, target, ext) => {
70
69
  for (const filename of filenames) {
71
70
  const dts = /(\.d\.ts)$/;
72
- const outFilename = dts.test(filename)
73
- ? filename.replace(dts, isCjsBuild ? '.d.cts' : '.d.mts')
74
- : filename.replace(/\.js$/, targetExt);
75
- const update = await specifier.update(filename, ({ value }) => {
76
- // Collapse any BinaryExpression or NewExpression to test for a relative specifier
71
+ const isDts = dts.test(filename);
72
+ const outFilename = isDts
73
+ ? filename.replace(dts, target === 'commonjs' ? '.d.cts' : '.d.mts')
74
+ : filename.replace(/\.js$/, ext);
75
+ if (isDts) {
76
+ const source = await readFile(filename, 'utf8');
77
+ const rewritten = source.replace(/(?<=['"])(\.\.?(?:\/[\w.-]+)*)\.js(?=['"])/g, `$1${ext}`);
78
+ await writeFile(outFilename, rewritten);
79
+ if (outFilename !== filename) {
80
+ await rm(filename, { force: true });
81
+ }
82
+ continue;
83
+ }
84
+ const rewriteSpecifier = (value = '') => {
77
85
  const collapsed = value.replace(/['"`+)\s]|new String\(/g, '');
78
- const relative = /^(?:\.|\.\.)\//;
79
- if (relative.test(collapsed)) {
80
- // $2 is for any closing quotation/parens around BE or NE
81
- return value.replace(/(.+)\.js([)'"`]*)?$/, `$1${targetExt}$2`);
86
+ if (/^(?:\.|\.\.)\//.test(collapsed)) {
87
+ return value.replace(/(.+)\.js([)"'`]*)?$/, `$1${ext}$2`);
82
88
  }
83
- });
84
- await writeFile(outFilename, update);
85
- await rm(filename, { force: true });
89
+ };
90
+ const writeOptions = {
91
+ target,
92
+ rewriteSpecifier,
93
+ ...(outFilename === filename ? { inPlace: true } : { out: outFilename }),
94
+ };
95
+ await transform(filename, writeOptions);
96
+ if (outFilename !== filename) {
97
+ await rm(filename, { force: true });
98
+ }
86
99
  }
87
100
  };
88
101
  const logSuccess = start => {
@@ -109,7 +122,11 @@ const duel = async (args) => {
109
122
  const compileFiles = getCompileFiles(tsc, projectDir);
110
123
  dualConfigPath = join(subDir, `tsconfig.${hex}.json`);
111
124
  await mkdir(subDir);
112
- await Promise.all(compileFiles.map(file => cp(file, join(subDir, relative(projectDir, file).replace(/^(\.\.\/)*/, '')))));
125
+ await Promise.all(compileFiles.map(async (file) => {
126
+ const dest = join(subDir, relative(projectDir, file).replace(/^(\.\.\/)+/, ''));
127
+ await mkdir(dirname(dest), { recursive: true });
128
+ await cp(file, dest);
129
+ }));
113
130
  /**
114
131
  * Transform ambiguous modules for the target dual build.
115
132
  * @see https://github.com/microsoft/TypeScript/issues/58658
@@ -126,7 +143,11 @@ const duel = async (args) => {
126
143
  *
127
144
  * @see https://github.com/microsoft/TypeScript/issues/58658
128
145
  */
129
- await transform(file, { out: file, type: isCjsBuild ? 'commonjs' : 'module' });
146
+ await transform(file, {
147
+ out: file,
148
+ target: isCjsBuild ? 'commonjs' : 'module',
149
+ transformSyntax: false,
150
+ });
130
151
  }
131
152
  }
132
153
  /**
@@ -158,10 +179,16 @@ const duel = async (args) => {
158
179
  }
159
180
  }
160
181
  if (success) {
182
+ const dualTarget = isCjsBuild ? 'commonjs' : 'module';
183
+ const dualTargetExt = isCjsBuild ? '.cjs' : dirs ? '.js' : '.mjs';
161
184
  const filenames = await glob(`${absoluteDualOutDir.replace(/\\/g, '/')}/**/*{.js,.d.ts}`, {
162
185
  ignore: 'node_modules/**',
163
186
  });
164
- await updateSpecifiersAndFileExtensions(filenames);
187
+ await updateSpecifiersAndFileExtensions(filenames, dualTarget, dualTargetExt);
188
+ if (dirs && originalType === 'commonjs') {
189
+ const primaryFiles = await glob(`${primaryOutDir.replace(/\\/g, '/')}/**/*{.js,.d.ts}`, { ignore: 'node_modules/**' });
190
+ await updateSpecifiersAndFileExtensions(primaryFiles, 'commonjs', '.cjs');
191
+ }
165
192
  logSuccess(startTime);
166
193
  }
167
194
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@knighted/duel",
3
- "version": "2.1.7",
3
+ "version": "3.0.0",
4
4
  "description": "TypeScript dual packages.",
5
5
  "type": "module",
6
6
  "main": "dist/esm/duel.js",
@@ -16,7 +16,7 @@
16
16
  "./package.json": "./package.json"
17
17
  },
18
18
  "engines": {
19
- "node": ">=20.11.0"
19
+ "node": ">=22.21.1 <23 || >=24 <25"
20
20
  },
21
21
  "engineStrict": true,
22
22
  "scripts": {
@@ -67,6 +67,7 @@
67
67
  },
68
68
  "devDependencies": {
69
69
  "@eslint/js": "^9.39.1",
70
+ "@knighted/module": "^1.0.0-rc.2",
70
71
  "@tsconfig/recommended": "^1.0.10",
71
72
  "@types/node": "^24.10.1",
72
73
  "c8": "^10.1.3",
@@ -82,8 +83,6 @@
82
83
  "vite": "^7.2.4"
83
84
  },
84
85
  "dependencies": {
85
- "@knighted/module": "^1.0.0-alpha.10",
86
- "@knighted/specifier": "^2.0.9",
87
86
  "find-up": "^8.0.0",
88
87
  "get-tsconfig": "^4.13.0",
89
88
  "glob": "^13.0.0",