@knighted/duel 1.0.8 → 2.0.0-rc.1

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
@@ -10,11 +10,14 @@ Tool for building a Node.js [dual package](https://nodejs.org/api/packages.html#
10
10
 
11
11
  - Bidirectional ESM ↔️ CJS dual builds inferred from the package.json `type`.
12
12
  - Correctly preserves module systems for `.mts` and `.cts` file extensions.
13
- - Use only one package.json and tsconfig.json.
13
+ - No extra configuration files needed, uses `package.json` and `tsconfig.json` files.
14
+ - Transforms the [differences between ES modules and CommonJS](https://nodejs.org/api/esm.html#differences-between-es-modules-and-commonjs).
15
+ - Works with monorepos.
16
+
14
17
 
15
18
  ## Requirements
16
19
 
17
- - Node >= 16.19.0.
20
+ - Node >= 20.11.0
18
21
 
19
22
  ## Example
20
23
 
@@ -67,17 +70,19 @@ If you prefer to have both builds in directories inside of your defined `outDir`
67
70
 
68
71
  Assuming an `outDir` of `dist`, running the above will create `dist/esm` and `dist/cjs` directories.
69
72
 
70
- ### Parallel builds
73
+ ### Module transforms
74
+
75
+ 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.
71
76
 
72
- This is experimental, as your mileage may vary based on the size of your `node_modules` directory.
77
+ Note, there is a slight performance penalty since your project needs to be copied first to run the transforms before compiling with `tsc`.
73
78
 
74
79
  ```json
75
80
  "scripts": {
76
- "build": "duel --parallel"
81
+ "build": "duel --modules"
77
82
  }
78
83
  ```
79
84
 
80
- You _might_ reduce your build times, but only if your project has minimal dependencies. This requires first copying your project to a parent directory of `--project` if it exists as a writable folder. Common [gitignored directories for Node.js projects](https://github.com/github/gitignore/blob/main/Node.gitignore) are not copied, with the exception of `node_modules`. See the [notes](#notes) as to why this can't be improved much further. In most cases, you're better off with serial builds.
85
+ This feature is still a work in progress regarding transforming `exports` when targeting an ES module build (relies on [`@knighted/module`](https://github.com/knightedcodemonkey/module)).
81
86
 
82
87
  ## Options
83
88
 
@@ -85,8 +90,8 @@ The available options are limited, because you should define most of them inside
85
90
 
86
91
  - `--project, -p` The path to the project's configuration file. Defaults to `tsconfig.json`.
87
92
  - `--pkg-dir, -k` The directory to start looking for a package.json file. Defaults to the cwd.
93
+ - `--modules, -m` Transform module globals for dual build target. Defaults to false.
88
94
  - `--dirs, -d` Outputs both builds to directories inside of `outDir`. Defaults to `false`.
89
- - `--parallel, -l` Run the builds in parallel. Defaults to `false`.
90
95
 
91
96
  You can run `duel --help` to get the same info. Below is the output of that:
92
97
 
@@ -96,8 +101,8 @@ Usage: duel [options]
96
101
  Options:
97
102
  --project, -p [path] Compile the project given the path to its configuration file, or to a folder with a 'tsconfig.json'.
98
103
  --pkg-dir, -k [path] The directory to start looking for a package.json file. Defaults to cwd.
104
+ --modules, -m Transform module globals for dual build target. Defaults to false.
99
105
  --dirs, -d Output both builds to directories inside of outDir. [esm, cjs].
100
- --parallel, -l Run the builds in parallel.
101
106
  --help, -h Print this message.
102
107
  ```
103
108
 
@@ -107,7 +112,7 @@ These are definitely edge cases, and would only really come up if your project m
107
112
 
108
113
  - 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.
109
114
 
110
- - 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 requiring **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 file's extension, so that it works with [how Node.js determines module systems](https://nodejs.org/api/packages.html#determining-module-system).
115
+ - Unfortunately, TypeScript doesn't really build [dual packages](https://nodejs.org/api/packages.html#dual-commonjses-module-packages) very well. One instance of unexpected behavior is when the compiler throws errors for ES module globals when running a dual CJS build, but not for the inverse case, despite both causing runtime errors in Node.js. See the [open issue](https://github.com/microsoft/TypeScript/issues/58658). You can circumvent this with `duel` by using the `--modules` option if your project uses module globals such as `import.meta` properties or `__dirname`, `__filename`, etc. in a CommonJS project.
111
116
 
112
117
  - 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`.
113
118
 
@@ -115,5 +120,6 @@ These are definitely edge cases, and would only really come up if your project m
115
120
 
116
121
  ## Notes
117
122
 
118
- As far as I can tell, `duel` is one (if not the only) way to get a correct dual package build using `tsc` with only **one package.json and tsconfig.json file**, _and also preserving module system by file extension_. Basically, how you expect things to work. The Microsoft backed TypeScript team [keep](https://github.com/microsoft/TypeScript/pull/54546) [talking](https://github.com/microsoft/TypeScript/issues/54593) about dual build support, but their philosophy is mainly one of self-preservation, rather than collaboration. For instance, they continue to [refuse to rewrite specifiers](https://github.com/microsoft/TypeScript/issues/16577). The downside of their decisions, and the fact that `npm` does not support using alternative names for the package.json file, is that `duel` must copy your project
119
- directory before attempting to run the builds in parallel.
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 Microsoft backed 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).
124
+
125
+ Fortunately, 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.
package/dist/cjs/duel.cjs CHANGED
@@ -11,29 +11,9 @@ 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
13
  const specifier_1 = require("@knighted/specifier");
14
+ const module_1 = require("@knighted/module");
14
15
  const init_js_1 = require("./init.cjs");
15
16
  const util_js_1 = require("./util.cjs");
16
- const tsc = await (0, find_up_1.findUp)(async (dir) => {
17
- const tscBin = (0, node_path_1.join)(dir, 'node_modules', '.bin', 'tsc');
18
- if (await (0, find_up_1.pathExists)(tscBin)) {
19
- return tscBin;
20
- }
21
- });
22
- const runBuild = (project, outDir) => {
23
- return new Promise((resolve, reject) => {
24
- const args = outDir ? ['-p', project, '--outDir', outDir] : ['-p', project];
25
- const build = (0, node_child_process_1.spawn)(tsc, args, { stdio: 'inherit' });
26
- build.on('error', err => {
27
- reject(new Error(`Failed to compile: ${err.message}`));
28
- });
29
- build.on('exit', code => {
30
- if (code > 0) {
31
- return reject(new Error(code));
32
- }
33
- resolve(code);
34
- });
35
- });
36
- };
37
17
  const handleErrorAndExit = message => {
38
18
  const exitCode = Number(message);
39
19
  if (isNaN(exitCode)) {
@@ -48,7 +28,28 @@ const handleErrorAndExit = message => {
48
28
  const duel = async (args) => {
49
29
  const ctx = await (0, init_js_1.init)(args);
50
30
  if (ctx) {
51
- const { projectDir, tsconfig, configPath, parallel, dirs, pkg } = ctx;
31
+ const { projectDir, tsconfig, configPath, modules, dirs, pkg } = ctx;
32
+ const tsc = await (0, find_up_1.findUp)(async (dir) => {
33
+ const tscBin = (0, node_path_1.join)(dir, 'node_modules', '.bin', 'tsc');
34
+ if (await (0, find_up_1.pathExists)(tscBin)) {
35
+ return tscBin;
36
+ }
37
+ }, { cwd: projectDir });
38
+ const runBuild = (project, outDir) => {
39
+ return new Promise((resolve, reject) => {
40
+ const args = outDir ? ['-p', project, '--outDir', outDir] : ['-p', project];
41
+ const build = (0, node_child_process_1.spawn)(tsc, args, { stdio: 'inherit' });
42
+ build.on('error', err => {
43
+ reject(new Error(`Failed to compile: ${err.message}`));
44
+ });
45
+ build.on('exit', code => {
46
+ if (code > 0) {
47
+ return reject(new Error(code));
48
+ }
49
+ resolve(code);
50
+ });
51
+ });
52
+ };
52
53
  const pkgDir = (0, node_path_1.dirname)(pkg.path);
53
54
  const outDir = tsconfig.compilerOptions?.outDir ?? 'dist';
54
55
  const absoluteOutDir = (0, node_path_1.resolve)(projectDir, outDir);
@@ -97,129 +98,83 @@ const duel = async (args) => {
97
98
  const logSuccess = start => {
98
99
  (0, util_js_1.log)(`Successfully created a dual ${isCjsBuild ? 'CJS' : 'ESM'} build in ${Math.round(node_perf_hooks_1.performance.now() - start)}ms.`);
99
100
  };
100
- if (parallel) {
101
- const paraName = `_${hex}_`;
102
- const paraParent = (0, node_path_1.join)(projectDir, '..');
103
- const paraTempDir = (0, node_path_1.join)(paraParent, paraName);
104
- let isDirWritable = true;
105
- try {
106
- const stats = await (0, promises_1.stat)(paraParent);
107
- if (stats.isDirectory()) {
108
- await (0, promises_1.access)(paraParent, promises_1.constants.W_OK);
109
- }
110
- else {
111
- isDirWritable = false;
112
- }
113
- }
114
- catch {
115
- isDirWritable = false;
116
- }
117
- if (!isDirWritable) {
118
- (0, util_js_1.logError)('No writable directory to prepare parallel builds. Exiting.');
119
- return;
120
- }
121
- (0, util_js_1.log)('Preparing parallel build...');
122
- const prepStart = node_perf_hooks_1.performance.now();
123
- await (0, promises_1.cp)(projectDir, paraTempDir, {
124
- recursive: true,
101
+ (0, util_js_1.log)('Starting primary build...');
102
+ let success = false;
103
+ const startTime = node_perf_hooks_1.performance.now();
104
+ try {
105
+ await runPrimaryBuild();
106
+ success = true;
107
+ }
108
+ catch ({ message }) {
109
+ handleErrorAndExit(message);
110
+ }
111
+ if (success) {
112
+ const subDir = (0, node_path_1.join)(projectDir, `_${hex}_`);
113
+ const absoluteDualOutDir = (0, node_path_1.join)(projectDir, isCjsBuild ? (0, node_path_1.join)(outDir, 'cjs') : (0, node_path_1.join)(outDir, 'esm'));
114
+ const tsconfigDual = getOverrideTsConfig();
115
+ const pkgRename = 'package.json.bak';
116
+ let dualConfigPath = (0, node_path_1.join)(projectDir, `tsconfig.${hex}.json`);
117
+ let errorMsg = '';
118
+ if (modules) {
119
+ const compileFiles = (0, util_js_1.getCompileFiles)(tsc, projectDir);
120
+ dualConfigPath = (0, node_path_1.join)(subDir, `tsconfig.${hex}.json`);
121
+ await (0, promises_1.mkdir)(subDir);
122
+ 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(/^(\.\.\/)*/, '')))));
125
123
  /**
126
- * Ignore common .gitignored directories in Node.js projects.
127
- * Except node_modules.
128
- *
129
- * @see https://github.com/github/gitignore/blob/main/Node.gitignore
124
+ * Transform ambiguous modules for the target dual build.
125
+ * @see https://github.com/microsoft/TypeScript/issues/58658
130
126
  */
131
- filter: src => !/logs|pids|lib-cov|coverage|bower_components|build|dist|jspm_packages|web_modules|out|\.next|\.tsbuildinfo|\.npm|\.node_repl_history|\.tgz|\.yarn|\.pnp|\.nyc_output|\.grunt|\.DS_Store/i.test(src),
132
- });
133
- const dualConfigPath = (0, node_path_1.join)(paraTempDir, 'tsconfig.json');
134
- const absoluteDualOutDir = (0, node_path_1.join)(paraTempDir, isCjsBuild ? (0, node_path_1.join)(outDir, 'cjs') : (0, node_path_1.join)(outDir, 'esm'));
135
- const tsconfigDual = getOverrideTsConfig();
136
- await (0, promises_1.writeFile)(dualConfigPath, JSON.stringify(tsconfigDual));
137
- await (0, promises_1.writeFile)((0, node_path_1.join)(paraTempDir, 'package.json'), JSON.stringify({
127
+ const toTransform = await (0, glob_1.glob)(`${subDir}/**/*{.js,.jsx,.ts,.tsx}`, {
128
+ ignore: 'node_modules/**',
129
+ });
130
+ for (const file of toTransform) {
131
+ /**
132
+ * Maybe include the option to transform modules implicitly
133
+ * (modules: true) so that `exports` are correctly converted
134
+ * when targeting a CJS dual build. Depends on @knighted/module
135
+ * supporting he `modules` option.
136
+ *
137
+ * @see https://github.com/microsoft/TypeScript/issues/58658
138
+ */
139
+ await (0, module_1.transform)(file, { out: file, type: isCjsBuild ? 'commonjs' : 'module' });
140
+ }
141
+ }
142
+ /**
143
+ * Create a new package.json with updated `type` field.
144
+ * Create a new tsconfig.json.
145
+ */
146
+ await (0, promises_1.rename)(pkg.path, (0, node_path_1.join)(pkgDir, pkgRename));
147
+ await (0, promises_1.writeFile)(pkg.path, JSON.stringify({
138
148
  type: isCjsBuild ? 'commonjs' : 'module',
139
149
  }));
140
- (0, util_js_1.log)(`Prepared in ${Math.round(node_perf_hooks_1.performance.now() - prepStart)}ms.`);
141
- (0, util_js_1.log)('Starting parallel dual builds...');
142
- let success = false;
143
- const startTime = node_perf_hooks_1.performance.now();
150
+ await (0, promises_1.writeFile)(dualConfigPath, JSON.stringify(tsconfigDual));
151
+ // Build dual
152
+ (0, util_js_1.log)('Starting dual build...');
144
153
  try {
145
- await Promise.all([
146
- runPrimaryBuild(),
147
- runBuild(dualConfigPath, absoluteDualOutDir),
148
- ]);
149
- success = true;
154
+ await runBuild(dualConfigPath, absoluteDualOutDir);
150
155
  }
151
156
  catch ({ message }) {
152
- handleErrorAndExit(message);
157
+ success = false;
158
+ errorMsg = message;
159
+ }
160
+ finally {
161
+ // Cleanup and restore
162
+ await (0, promises_1.rm)(dualConfigPath, { force: true });
163
+ await (0, promises_1.rm)(pkg.path, { force: true });
164
+ await (0, promises_1.rm)(subDir, { force: true, recursive: true });
165
+ await (0, promises_1.rename)((0, node_path_1.join)(pkgDir, pkgRename), pkg.path);
166
+ if (errorMsg) {
167
+ handleErrorAndExit(errorMsg);
168
+ }
153
169
  }
154
170
  if (success) {
155
171
  const filenames = await (0, glob_1.glob)(`${absoluteDualOutDir}/**/*{.js,.d.ts}`, {
156
172
  ignore: 'node_modules/**',
157
173
  });
158
174
  await updateSpecifiersAndFileExtensions(filenames);
159
- // Copy over and cleanup
160
- await (0, promises_1.cp)(absoluteDualOutDir, (0, node_path_1.join)(absoluteOutDir, isCjsBuild ? 'cjs' : 'esm'), {
161
- recursive: true,
162
- });
163
- await (0, promises_1.rm)(paraTempDir, { force: true, recursive: true });
164
175
  logSuccess(startTime);
165
176
  }
166
177
  }
167
- else {
168
- (0, util_js_1.log)('Starting primary build...');
169
- let success = false;
170
- const startTime = node_perf_hooks_1.performance.now();
171
- try {
172
- await runPrimaryBuild();
173
- success = true;
174
- }
175
- catch ({ message }) {
176
- handleErrorAndExit(message);
177
- }
178
- if (success) {
179
- const dualConfigPath = (0, node_path_1.join)(projectDir, `tsconfig.${hex}.json`);
180
- const absoluteDualOutDir = (0, node_path_1.join)(projectDir, isCjsBuild ? (0, node_path_1.join)(outDir, 'cjs') : (0, node_path_1.join)(outDir, 'esm'));
181
- const tsconfigDual = getOverrideTsConfig();
182
- const pkgRename = 'package.json.bak';
183
- let errorMsg = '';
184
- /**
185
- * Create a new package.json with updated `type` field.
186
- * Create a new tsconfig.json.
187
- *
188
- * The need to create a new package.json makes doing
189
- * the builds in parallel difficult.
190
- */
191
- await (0, promises_1.rename)(pkg.path, (0, node_path_1.join)(pkgDir, pkgRename));
192
- await (0, promises_1.writeFile)(pkg.path, JSON.stringify({
193
- type: isCjsBuild ? 'commonjs' : 'module',
194
- }));
195
- await (0, promises_1.writeFile)(dualConfigPath, JSON.stringify(tsconfigDual));
196
- // Build dual
197
- (0, util_js_1.log)('Starting dual build...');
198
- try {
199
- await runBuild(dualConfigPath, absoluteDualOutDir);
200
- }
201
- catch ({ message }) {
202
- success = false;
203
- errorMsg = message;
204
- }
205
- finally {
206
- // Cleanup and restore
207
- await (0, promises_1.rm)(dualConfigPath, { force: true });
208
- await (0, promises_1.rm)(pkg.path, { force: true });
209
- await (0, promises_1.rename)((0, node_path_1.join)(pkgDir, pkgRename), pkg.path);
210
- if (errorMsg) {
211
- handleErrorAndExit(errorMsg);
212
- }
213
- }
214
- if (success) {
215
- const filenames = await (0, glob_1.glob)(`${absoluteDualOutDir}/**/*{.js,.d.ts}`, {
216
- ignore: 'node_modules/**',
217
- });
218
- await updateSpecifiersAndFileExtensions(filenames);
219
- logSuccess(startTime);
220
- }
221
- }
222
- }
223
178
  }
224
179
  };
225
180
  exports.duel = duel;
package/dist/cjs/init.cjs CHANGED
@@ -32,9 +32,9 @@ const init = async (args) => {
32
32
  short: 'k',
33
33
  default: (0, node_process_1.cwd)(),
34
34
  },
35
- parallel: {
35
+ modules: {
36
36
  type: 'boolean',
37
- short: 'l',
37
+ short: 'm',
38
38
  default: false,
39
39
  },
40
40
  dirs: {
@@ -60,12 +60,12 @@ const init = async (args) => {
60
60
  (0, util_js_1.log)('Options:');
61
61
  (0, util_js_1.log)("--project, -p [path] \t Compile the project given the path to its configuration file, or to a folder with a 'tsconfig.json'.");
62
62
  (0, util_js_1.log)('--pkg-dir, -k [path] \t The directory to start looking for a package.json file. Defaults to cwd.');
63
+ (0, util_js_1.log)('--modules, -m \t\t Transform module globals for dual build target. Defaults to false.');
63
64
  (0, util_js_1.log)('--dirs, -d \t\t Output both builds to directories inside of outDir. [esm, cjs].');
64
- (0, util_js_1.log)('--parallel, -l \t\t Run the builds in parallel.');
65
65
  (0, util_js_1.log)('--help, -h \t\t Print this message.');
66
66
  }
67
67
  else {
68
- const { project, 'target-extension': targetExt, 'pkg-dir': pkgDir, parallel, dirs, } = parsed;
68
+ const { project, 'target-extension': targetExt, 'pkg-dir': pkgDir, modules, dirs, } = parsed;
69
69
  let configPath = (0, node_path_1.resolve)(project);
70
70
  let stats = null;
71
71
  let pkg = null;
@@ -114,7 +114,7 @@ const init = async (args) => {
114
114
  return {
115
115
  pkg,
116
116
  dirs,
117
- parallel,
117
+ modules,
118
118
  tsconfig,
119
119
  projectDir,
120
120
  configPath,
@@ -1,7 +1,7 @@
1
1
  export function init(args: any): Promise<false | {
2
2
  pkg: import("read-package-up", { with: { "resolution-mode": "import" } }).NormalizedReadResult;
3
3
  dirs: boolean | undefined;
4
- parallel: boolean | undefined;
4
+ modules: boolean | undefined;
5
5
  tsconfig: any;
6
6
  projectDir: string;
7
7
  configPath: string;
package/dist/cjs/util.cjs CHANGED
@@ -1,8 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getRealPathAsFileUrl = exports.logError = exports.log = void 0;
3
+ exports.getCompileFiles = exports.getRealPathAsFileUrl = exports.logError = exports.log = void 0;
4
4
  const node_url_1 = require("node:url");
5
5
  const promises_1 = require("node:fs/promises");
6
+ const node_child_process_1 = require("node:child_process");
7
+ const node_process_1 = require("node:process");
6
8
  const log = (color = '\x1b[30m', msg = '') => {
7
9
  // eslint-disable-next-line no-console
8
10
  console.log(`${color}%s\x1b[0m`, msg);
@@ -16,3 +18,12 @@ const getRealPathAsFileUrl = async (path) => {
16
18
  return asFileUrl;
17
19
  };
18
20
  exports.getRealPathAsFileUrl = getRealPathAsFileUrl;
21
+ const getCompileFiles = (tscBinPath, wd = (0, node_process_1.cwd)()) => {
22
+ const { stdout } = (0, node_child_process_1.spawnSync)(tscBinPath, ['--listFilesOnly'], { cwd: wd });
23
+ // Exclude node_modules and empty strings.
24
+ return stdout
25
+ .toString()
26
+ .split('\n')
27
+ .filter(path => !/node_modules|^$/.test(path));
28
+ };
29
+ exports.getCompileFiles = getCompileFiles;
@@ -1,3 +1,4 @@
1
1
  export function log(color?: string, msg?: string): void;
2
2
  export const logError: (msg?: string | undefined) => void;
3
3
  export function getRealPathAsFileUrl(path: any): Promise<string>;
4
+ export function getCompileFiles(tscBinPath: any, wd?: string): string[];
package/dist/esm/duel.js CHANGED
@@ -1,36 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  import { argv } from 'node:process';
3
- import { join, dirname, resolve } from 'node:path';
3
+ import { join, dirname, resolve, relative } from 'node:path';
4
4
  import { spawn } from 'node:child_process';
5
- import { writeFile, rm, cp, rename, stat, access, constants } from 'node:fs/promises';
5
+ import { writeFile, rm, rename, cp, mkdir } 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, pathExists } from 'find-up';
10
10
  import { specifier } from '@knighted/specifier';
11
+ import { transform } from '@knighted/module';
11
12
  import { init } from './init.js';
12
- import { getRealPathAsFileUrl, logError, log } from './util.js';
13
- const tsc = await findUp(async (dir) => {
14
- const tscBin = join(dir, 'node_modules', '.bin', 'tsc');
15
- if (await pathExists(tscBin)) {
16
- return tscBin;
17
- }
18
- });
19
- const runBuild = (project, outDir) => {
20
- return new Promise((resolve, reject) => {
21
- const args = outDir ? ['-p', project, '--outDir', outDir] : ['-p', project];
22
- const build = spawn(tsc, args, { stdio: 'inherit' });
23
- build.on('error', err => {
24
- reject(new Error(`Failed to compile: ${err.message}`));
25
- });
26
- build.on('exit', code => {
27
- if (code > 0) {
28
- return reject(new Error(code));
29
- }
30
- resolve(code);
31
- });
32
- });
33
- };
13
+ import { getRealPathAsFileUrl, getCompileFiles, logError, log } from './util.js';
34
14
  const handleErrorAndExit = message => {
35
15
  const exitCode = Number(message);
36
16
  if (isNaN(exitCode)) {
@@ -45,7 +25,28 @@ const handleErrorAndExit = message => {
45
25
  const duel = async (args) => {
46
26
  const ctx = await init(args);
47
27
  if (ctx) {
48
- const { projectDir, tsconfig, configPath, parallel, dirs, pkg } = ctx;
28
+ const { projectDir, tsconfig, configPath, modules, dirs, pkg } = ctx;
29
+ const tsc = await findUp(async (dir) => {
30
+ const tscBin = join(dir, 'node_modules', '.bin', 'tsc');
31
+ if (await pathExists(tscBin)) {
32
+ return tscBin;
33
+ }
34
+ }, { cwd: projectDir });
35
+ const runBuild = (project, outDir) => {
36
+ return new Promise((resolve, reject) => {
37
+ const args = outDir ? ['-p', project, '--outDir', outDir] : ['-p', project];
38
+ const build = spawn(tsc, args, { stdio: 'inherit' });
39
+ build.on('error', err => {
40
+ reject(new Error(`Failed to compile: ${err.message}`));
41
+ });
42
+ build.on('exit', code => {
43
+ if (code > 0) {
44
+ return reject(new Error(code));
45
+ }
46
+ resolve(code);
47
+ });
48
+ });
49
+ };
49
50
  const pkgDir = dirname(pkg.path);
50
51
  const outDir = tsconfig.compilerOptions?.outDir ?? 'dist';
51
52
  const absoluteOutDir = resolve(projectDir, outDir);
@@ -94,129 +95,83 @@ const duel = async (args) => {
94
95
  const logSuccess = start => {
95
96
  log(`Successfully created a dual ${isCjsBuild ? 'CJS' : 'ESM'} build in ${Math.round(performance.now() - start)}ms.`);
96
97
  };
97
- if (parallel) {
98
- const paraName = `_${hex}_`;
99
- const paraParent = join(projectDir, '..');
100
- const paraTempDir = join(paraParent, paraName);
101
- let isDirWritable = true;
102
- try {
103
- const stats = await stat(paraParent);
104
- if (stats.isDirectory()) {
105
- await access(paraParent, constants.W_OK);
106
- }
107
- else {
108
- isDirWritable = false;
109
- }
110
- }
111
- catch {
112
- isDirWritable = false;
113
- }
114
- if (!isDirWritable) {
115
- logError('No writable directory to prepare parallel builds. Exiting.');
116
- return;
117
- }
118
- log('Preparing parallel build...');
119
- const prepStart = performance.now();
120
- await cp(projectDir, paraTempDir, {
121
- recursive: true,
98
+ log('Starting primary build...');
99
+ let success = false;
100
+ const startTime = performance.now();
101
+ try {
102
+ await runPrimaryBuild();
103
+ success = true;
104
+ }
105
+ catch ({ message }) {
106
+ handleErrorAndExit(message);
107
+ }
108
+ if (success) {
109
+ const subDir = join(projectDir, `_${hex}_`);
110
+ const absoluteDualOutDir = join(projectDir, isCjsBuild ? join(outDir, 'cjs') : join(outDir, 'esm'));
111
+ const tsconfigDual = getOverrideTsConfig();
112
+ const pkgRename = 'package.json.bak';
113
+ let dualConfigPath = join(projectDir, `tsconfig.${hex}.json`);
114
+ let errorMsg = '';
115
+ if (modules) {
116
+ const compileFiles = getCompileFiles(tsc, projectDir);
117
+ dualConfigPath = join(subDir, `tsconfig.${hex}.json`);
118
+ await mkdir(subDir);
119
+ await Promise.all(compileFiles.map(file => cp(file, join(subDir, relative(projectDir, file).replace(/^(\.\.\/)*/, '')))));
122
120
  /**
123
- * Ignore common .gitignored directories in Node.js projects.
124
- * Except node_modules.
125
- *
126
- * @see https://github.com/github/gitignore/blob/main/Node.gitignore
121
+ * Transform ambiguous modules for the target dual build.
122
+ * @see https://github.com/microsoft/TypeScript/issues/58658
127
123
  */
128
- filter: src => !/logs|pids|lib-cov|coverage|bower_components|build|dist|jspm_packages|web_modules|out|\.next|\.tsbuildinfo|\.npm|\.node_repl_history|\.tgz|\.yarn|\.pnp|\.nyc_output|\.grunt|\.DS_Store/i.test(src),
129
- });
130
- const dualConfigPath = join(paraTempDir, 'tsconfig.json');
131
- const absoluteDualOutDir = join(paraTempDir, isCjsBuild ? join(outDir, 'cjs') : join(outDir, 'esm'));
132
- const tsconfigDual = getOverrideTsConfig();
133
- await writeFile(dualConfigPath, JSON.stringify(tsconfigDual));
134
- await writeFile(join(paraTempDir, 'package.json'), JSON.stringify({
124
+ const toTransform = await glob(`${subDir}/**/*{.js,.jsx,.ts,.tsx}`, {
125
+ ignore: 'node_modules/**',
126
+ });
127
+ for (const file of toTransform) {
128
+ /**
129
+ * Maybe include the option to transform modules implicitly
130
+ * (modules: true) so that `exports` are correctly converted
131
+ * when targeting a CJS dual build. Depends on @knighted/module
132
+ * supporting he `modules` option.
133
+ *
134
+ * @see https://github.com/microsoft/TypeScript/issues/58658
135
+ */
136
+ await transform(file, { out: file, type: isCjsBuild ? 'commonjs' : 'module' });
137
+ }
138
+ }
139
+ /**
140
+ * Create a new package.json with updated `type` field.
141
+ * Create a new tsconfig.json.
142
+ */
143
+ await rename(pkg.path, join(pkgDir, pkgRename));
144
+ await writeFile(pkg.path, JSON.stringify({
135
145
  type: isCjsBuild ? 'commonjs' : 'module',
136
146
  }));
137
- log(`Prepared in ${Math.round(performance.now() - prepStart)}ms.`);
138
- log('Starting parallel dual builds...');
139
- let success = false;
140
- const startTime = performance.now();
147
+ await writeFile(dualConfigPath, JSON.stringify(tsconfigDual));
148
+ // Build dual
149
+ log('Starting dual build...');
141
150
  try {
142
- await Promise.all([
143
- runPrimaryBuild(),
144
- runBuild(dualConfigPath, absoluteDualOutDir),
145
- ]);
146
- success = true;
151
+ await runBuild(dualConfigPath, absoluteDualOutDir);
147
152
  }
148
153
  catch ({ message }) {
149
- handleErrorAndExit(message);
154
+ success = false;
155
+ errorMsg = message;
156
+ }
157
+ finally {
158
+ // Cleanup and restore
159
+ await rm(dualConfigPath, { force: true });
160
+ await rm(pkg.path, { force: true });
161
+ await rm(subDir, { force: true, recursive: true });
162
+ await rename(join(pkgDir, pkgRename), pkg.path);
163
+ if (errorMsg) {
164
+ handleErrorAndExit(errorMsg);
165
+ }
150
166
  }
151
167
  if (success) {
152
168
  const filenames = await glob(`${absoluteDualOutDir}/**/*{.js,.d.ts}`, {
153
169
  ignore: 'node_modules/**',
154
170
  });
155
171
  await updateSpecifiersAndFileExtensions(filenames);
156
- // Copy over and cleanup
157
- await cp(absoluteDualOutDir, join(absoluteOutDir, isCjsBuild ? 'cjs' : 'esm'), {
158
- recursive: true,
159
- });
160
- await rm(paraTempDir, { force: true, recursive: true });
161
172
  logSuccess(startTime);
162
173
  }
163
174
  }
164
- else {
165
- log('Starting primary build...');
166
- let success = false;
167
- const startTime = performance.now();
168
- try {
169
- await runPrimaryBuild();
170
- success = true;
171
- }
172
- catch ({ message }) {
173
- handleErrorAndExit(message);
174
- }
175
- if (success) {
176
- const dualConfigPath = join(projectDir, `tsconfig.${hex}.json`);
177
- const absoluteDualOutDir = join(projectDir, isCjsBuild ? join(outDir, 'cjs') : join(outDir, 'esm'));
178
- const tsconfigDual = getOverrideTsConfig();
179
- const pkgRename = 'package.json.bak';
180
- let errorMsg = '';
181
- /**
182
- * Create a new package.json with updated `type` field.
183
- * Create a new tsconfig.json.
184
- *
185
- * The need to create a new package.json makes doing
186
- * the builds in parallel difficult.
187
- */
188
- await rename(pkg.path, join(pkgDir, pkgRename));
189
- await writeFile(pkg.path, JSON.stringify({
190
- type: isCjsBuild ? 'commonjs' : 'module',
191
- }));
192
- await writeFile(dualConfigPath, JSON.stringify(tsconfigDual));
193
- // Build dual
194
- log('Starting dual build...');
195
- try {
196
- await runBuild(dualConfigPath, absoluteDualOutDir);
197
- }
198
- catch ({ message }) {
199
- success = false;
200
- errorMsg = message;
201
- }
202
- finally {
203
- // Cleanup and restore
204
- await rm(dualConfigPath, { force: true });
205
- await rm(pkg.path, { force: true });
206
- await rename(join(pkgDir, pkgRename), pkg.path);
207
- if (errorMsg) {
208
- handleErrorAndExit(errorMsg);
209
- }
210
- }
211
- if (success) {
212
- const filenames = await glob(`${absoluteDualOutDir}/**/*{.js,.d.ts}`, {
213
- ignore: 'node_modules/**',
214
- });
215
- await updateSpecifiersAndFileExtensions(filenames);
216
- logSuccess(startTime);
217
- }
218
- }
219
- }
220
175
  }
221
176
  };
222
177
  const realFileUrlArgv1 = await getRealPathAsFileUrl(argv[1]);
@@ -1,7 +1,7 @@
1
1
  export function init(args: any): Promise<false | {
2
2
  pkg: import("read-package-up").NormalizedReadResult;
3
3
  dirs: boolean | undefined;
4
- parallel: boolean | undefined;
4
+ modules: boolean | undefined;
5
5
  tsconfig: any;
6
6
  projectDir: string;
7
7
  configPath: string;
package/dist/esm/init.js CHANGED
@@ -26,9 +26,9 @@ const init = async (args) => {
26
26
  short: 'k',
27
27
  default: cwd(),
28
28
  },
29
- parallel: {
29
+ modules: {
30
30
  type: 'boolean',
31
- short: 'l',
31
+ short: 'm',
32
32
  default: false,
33
33
  },
34
34
  dirs: {
@@ -54,12 +54,12 @@ const init = async (args) => {
54
54
  log('Options:');
55
55
  log("--project, -p [path] \t Compile the project given the path to its configuration file, or to a folder with a 'tsconfig.json'.");
56
56
  log('--pkg-dir, -k [path] \t The directory to start looking for a package.json file. Defaults to cwd.');
57
+ log('--modules, -m \t\t Transform module globals for dual build target. Defaults to false.');
57
58
  log('--dirs, -d \t\t Output both builds to directories inside of outDir. [esm, cjs].');
58
- log('--parallel, -l \t\t Run the builds in parallel.');
59
59
  log('--help, -h \t\t Print this message.');
60
60
  }
61
61
  else {
62
- const { project, 'target-extension': targetExt, 'pkg-dir': pkgDir, parallel, dirs, } = parsed;
62
+ const { project, 'target-extension': targetExt, 'pkg-dir': pkgDir, modules, dirs, } = parsed;
63
63
  let configPath = resolve(project);
64
64
  let stats = null;
65
65
  let pkg = null;
@@ -108,7 +108,7 @@ const init = async (args) => {
108
108
  return {
109
109
  pkg,
110
110
  dirs,
111
- parallel,
111
+ modules,
112
112
  tsconfig,
113
113
  projectDir,
114
114
  configPath,
@@ -1,3 +1,4 @@
1
1
  export function log(color?: string, msg?: string): void;
2
2
  export const logError: (msg?: string | undefined) => void;
3
3
  export function getRealPathAsFileUrl(path: any): Promise<string>;
4
+ export function getCompileFiles(tscBinPath: any, wd?: string): string[];
package/dist/esm/util.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import { pathToFileURL } from 'node:url';
2
2
  import { realpath } from 'node:fs/promises';
3
+ import { spawnSync } from 'node:child_process';
4
+ import { cwd } from 'node:process';
3
5
  const log = (color = '\x1b[30m', msg = '') => {
4
6
  // eslint-disable-next-line no-console
5
7
  console.log(`${color}%s\x1b[0m`, msg);
@@ -10,4 +12,12 @@ const getRealPathAsFileUrl = async (path) => {
10
12
  const asFileUrl = pathToFileURL(realPath).href;
11
13
  return asFileUrl;
12
14
  };
13
- export { log, logError, getRealPathAsFileUrl };
15
+ const getCompileFiles = (tscBinPath, wd = cwd()) => {
16
+ const { stdout } = spawnSync(tscBinPath, ['--listFilesOnly'], { cwd: wd });
17
+ // Exclude node_modules and empty strings.
18
+ return stdout
19
+ .toString()
20
+ .split('\n')
21
+ .filter(path => !/node_modules|^$/.test(path));
22
+ };
23
+ export { log, logError, getRealPathAsFileUrl, getCompileFiles };
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@knighted/duel",
3
- "version": "1.0.8",
3
+ "version": "2.0.0-rc.1",
4
4
  "description": "TypeScript dual packages.",
5
5
  "type": "module",
6
- "main": "dist",
6
+ "main": "dist/esm/duel.js",
7
7
  "bin": "dist/esm/duel.js",
8
8
  "exports": {
9
9
  ".": {
@@ -14,12 +14,14 @@
14
14
  "./package.json": "./package.json"
15
15
  },
16
16
  "engines": {
17
- "node": ">=16.19.0"
17
+ "node": ">=20.11.0"
18
18
  },
19
19
  "engineStrict": true,
20
20
  "scripts": {
21
21
  "prettier": "prettier -w src/*.js test/*.js",
22
22
  "lint": "eslint src/*.js test/*.js",
23
+ "test:integration": "node --test --test-reporter=spec test/integration.js",
24
+ "test:monorepos": "node --test --test-reporter=spec test/monorepos.js",
23
25
  "test": "c8 --reporter=text --reporter=text-summary --reporter=lcov node --test --test-reporter=spec test/*.js",
24
26
  "build": "node src/duel.js --dirs",
25
27
  "prepack": "npm run build"
@@ -54,16 +56,18 @@
54
56
  "typescript": ">=4.0.0 || >=4.9.0-dev || >=5.3.0-dev || >=5.4.0-dev || >=5.5.0-dev || next"
55
57
  },
56
58
  "devDependencies": {
57
- "@types/node": "^20.4.6",
59
+ "@types/node": "^20.11.0",
58
60
  "c8": "^8.0.1",
59
61
  "eslint": "^8.45.0",
60
62
  "eslint-plugin-n": "^16.0.1",
61
63
  "prettier": "^3.2.4",
62
- "typescript": "^5.5.0-dev.20240228",
64
+ "tsx": "^4.11.2",
65
+ "typescript": "^5.5.0-dev.20240525",
63
66
  "vite": "^5.2.8"
64
67
  },
65
68
  "dependencies": {
66
- "@knighted/specifier": "^1.0.1",
69
+ "@knighted/module": "^1.0.0-alpha.4",
70
+ "@knighted/specifier": "^2.0.0-rc.1",
67
71
  "find-up": "^6.3.0",
68
72
  "glob": "^10.3.3",
69
73
  "jsonc-parser": "^3.2.0",