@knighted/duel 1.0.8 → 2.0.0-rc.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
@@ -10,11 +10,12 @@ 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
+ - Resolves the [differences between ES modules and CommonJS](https://nodejs.org/api/esm.html#differences-between-es-modules-and-commonjs).
13
14
  - Use only one package.json and tsconfig.json.
14
15
 
15
16
  ## Requirements
16
17
 
17
- - Node >= 16.19.0.
18
+ - Node >= 20.11.0
18
19
 
19
20
  ## Example
20
21
 
@@ -67,18 +68,6 @@ If you prefer to have both builds in directories inside of your defined `outDir`
67
68
 
68
69
  Assuming an `outDir` of `dist`, running the above will create `dist/esm` and `dist/cjs` directories.
69
70
 
70
- ### Parallel builds
71
-
72
- This is experimental, as your mileage may vary based on the size of your `node_modules` directory.
73
-
74
- ```json
75
- "scripts": {
76
- "build": "duel --parallel"
77
- }
78
- ```
79
-
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.
81
-
82
71
  ## Options
83
72
 
84
73
  The available options are limited, because you should define most of them inside your project's `tsconfig.json` file.
@@ -86,7 +75,6 @@ The available options are limited, because you should define most of them inside
86
75
  - `--project, -p` The path to the project's configuration file. Defaults to `tsconfig.json`.
87
76
  - `--pkg-dir, -k` The directory to start looking for a package.json file. Defaults to the cwd.
88
77
  - `--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
78
 
91
79
  You can run `duel --help` to get the same info. Below is the output of that:
92
80
 
@@ -97,7 +85,6 @@ Options:
97
85
  --project, -p [path] Compile the project given the path to its configuration file, or to a folder with a 'tsconfig.json'.
98
86
  --pkg-dir, -k [path] The directory to start looking for a package.json file. Defaults to cwd.
99
87
  --dirs, -d Output both builds to directories inside of outDir. [esm, cjs].
100
- --parallel, -l Run the builds in parallel.
101
88
  --help, -h Print this message.
102
89
  ```
103
90
 
@@ -115,5 +102,4 @@ These are definitely edge cases, and would only really come up if your project m
115
102
 
116
103
  ## Notes
117
104
 
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.
105
+ 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**, _while 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 they continue to [refuse to rewrite specifiers](https://github.com/microsoft/TypeScript/issues/16577).
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, 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,133 +98,77 @@ 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,
125
- /**
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
130
- */
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'));
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 compileFiles = (0, util_js_1.getCompileFiles)(tsc, projectDir);
113
+ const subDir = (0, node_path_1.join)(projectDir, `_${hex}_`);
114
+ const dualConfigPath = (0, node_path_1.join)(subDir, `tsconfig.${hex}.json`);
115
+ const absoluteDualOutDir = (0, node_path_1.join)(projectDir, isCjsBuild ? (0, node_path_1.join)(outDir, 'cjs') : (0, node_path_1.join)(outDir, 'esm'));
135
116
  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({
117
+ const pkgRename = 'package.json.bak';
118
+ let errorMsg = '';
119
+ // Copy project directory as a subdirectory
120
+ await (0, promises_1.mkdir)(subDir);
121
+ await Promise.all(compileFiles.map(file => (0, promises_1.cp)(file, (0, node_path_1.resolve)(subDir, (0, node_path_1.relative)(projectDir, file)))));
122
+ /**
123
+ * Create a new package.json with updated `type` field.
124
+ * Create a new tsconfig.json.
125
+ */
126
+ await (0, promises_1.rename)(pkg.path, (0, node_path_1.join)(pkgDir, pkgRename));
127
+ await (0, promises_1.writeFile)(pkg.path, JSON.stringify({
138
128
  type: isCjsBuild ? 'commonjs' : 'module',
139
129
  }));
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();
130
+ await (0, promises_1.writeFile)(dualConfigPath, JSON.stringify(tsconfigDual));
131
+ /**
132
+ * Transform ambiguous modules for the target dual build.
133
+ * @see https://github.com/microsoft/TypeScript/issues/58658
134
+ */
135
+ const toTransform = await (0, glob_1.glob)(`${subDir}/**/*{.js,.jsx,.ts,.tsx}`, {
136
+ ignore: 'node_modules/**',
137
+ });
138
+ for (const file of toTransform) {
139
+ await (0, module_1.transform)(file, { out: file, type: isCjsBuild ? 'commonjs' : 'module' });
140
+ }
141
+ // Build dual
142
+ (0, util_js_1.log)('Starting dual build...');
144
143
  try {
145
- await Promise.all([
146
- runPrimaryBuild(),
147
- runBuild(dualConfigPath, absoluteDualOutDir),
148
- ]);
149
- success = true;
144
+ await runBuild(dualConfigPath, absoluteDualOutDir);
150
145
  }
151
146
  catch ({ message }) {
152
- handleErrorAndExit(message);
147
+ success = false;
148
+ errorMsg = message;
149
+ }
150
+ finally {
151
+ // Cleanup and restore
152
+ await (0, promises_1.rm)(dualConfigPath, { force: true });
153
+ await (0, promises_1.rm)(pkg.path, { force: true });
154
+ await (0, promises_1.rm)(subDir, { force: true, recursive: true });
155
+ await (0, promises_1.rename)((0, node_path_1.join)(pkgDir, pkgRename), pkg.path);
156
+ if (errorMsg) {
157
+ handleErrorAndExit(errorMsg);
158
+ }
153
159
  }
154
160
  if (success) {
155
161
  const filenames = await (0, glob_1.glob)(`${absoluteDualOutDir}/**/*{.js,.d.ts}`, {
156
162
  ignore: 'node_modules/**',
157
163
  });
158
164
  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
165
  logSuccess(startTime);
165
166
  }
166
167
  }
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
168
  }
224
169
  };
225
170
  exports.duel = duel;
226
171
  const realFileUrlArgv1 = await (0, util_js_1.getRealPathAsFileUrl)(node_process_1.argv[1]);
227
- if (import.meta.url === realFileUrlArgv1) {
172
+ if (require("node:url").pathToFileURL(__filename).toString() === realFileUrlArgv1) {
228
173
  await duel();
229
174
  }
package/dist/cjs/init.cjs CHANGED
@@ -32,11 +32,6 @@ const init = async (args) => {
32
32
  short: 'k',
33
33
  default: (0, node_process_1.cwd)(),
34
34
  },
35
- parallel: {
36
- type: 'boolean',
37
- short: 'l',
38
- default: false,
39
- },
40
35
  dirs: {
41
36
  type: 'boolean',
42
37
  short: 'd',
@@ -61,11 +56,10 @@ const init = async (args) => {
61
56
  (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
57
  (0, util_js_1.log)('--pkg-dir, -k [path] \t The directory to start looking for a package.json file. Defaults to cwd.');
63
58
  (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
59
  (0, util_js_1.log)('--help, -h \t\t Print this message.');
66
60
  }
67
61
  else {
68
- const { project, 'target-extension': targetExt, 'pkg-dir': pkgDir, parallel, dirs, } = parsed;
62
+ const { project, 'target-extension': targetExt, 'pkg-dir': pkgDir, dirs } = parsed;
69
63
  let configPath = (0, node_path_1.resolve)(project);
70
64
  let stats = null;
71
65
  let pkg = null;
@@ -114,7 +108,6 @@ const init = async (args) => {
114
108
  return {
115
109
  pkg,
116
110
  dirs,
117
- parallel,
118
111
  tsconfig,
119
112
  projectDir,
120
113
  configPath,
@@ -1,7 +1,6 @@
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;
5
4
  tsconfig: any;
6
5
  projectDir: string;
7
6
  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, 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,73 @@ 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,
122
- /**
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
127
- */
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'));
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 compileFiles = getCompileFiles(tsc, projectDir);
110
+ const subDir = join(projectDir, `_${hex}_`);
111
+ const dualConfigPath = join(subDir, `tsconfig.${hex}.json`);
112
+ const absoluteDualOutDir = join(projectDir, isCjsBuild ? join(outDir, 'cjs') : join(outDir, 'esm'));
132
113
  const tsconfigDual = getOverrideTsConfig();
133
- await writeFile(dualConfigPath, JSON.stringify(tsconfigDual));
134
- await writeFile(join(paraTempDir, 'package.json'), JSON.stringify({
114
+ const pkgRename = 'package.json.bak';
115
+ let errorMsg = '';
116
+ // Copy project directory as a subdirectory
117
+ await mkdir(subDir);
118
+ await Promise.all(compileFiles.map(file => cp(file, resolve(subDir, relative(projectDir, file)))));
119
+ /**
120
+ * Create a new package.json with updated `type` field.
121
+ * Create a new tsconfig.json.
122
+ */
123
+ await rename(pkg.path, join(pkgDir, pkgRename));
124
+ await writeFile(pkg.path, JSON.stringify({
135
125
  type: isCjsBuild ? 'commonjs' : 'module',
136
126
  }));
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();
127
+ await writeFile(dualConfigPath, JSON.stringify(tsconfigDual));
128
+ /**
129
+ * Transform ambiguous modules for the target dual build.
130
+ * @see https://github.com/microsoft/TypeScript/issues/58658
131
+ */
132
+ const toTransform = await glob(`${subDir}/**/*{.js,.jsx,.ts,.tsx}`, {
133
+ ignore: 'node_modules/**',
134
+ });
135
+ for (const file of toTransform) {
136
+ await transform(file, { out: file, type: isCjsBuild ? 'commonjs' : 'module' });
137
+ }
138
+ // Build dual
139
+ log('Starting dual build...');
141
140
  try {
142
- await Promise.all([
143
- runPrimaryBuild(),
144
- runBuild(dualConfigPath, absoluteDualOutDir),
145
- ]);
146
- success = true;
141
+ await runBuild(dualConfigPath, absoluteDualOutDir);
147
142
  }
148
143
  catch ({ message }) {
149
- handleErrorAndExit(message);
144
+ success = false;
145
+ errorMsg = message;
146
+ }
147
+ finally {
148
+ // Cleanup and restore
149
+ await rm(dualConfigPath, { force: true });
150
+ await rm(pkg.path, { force: true });
151
+ await rm(subDir, { force: true, recursive: true });
152
+ await rename(join(pkgDir, pkgRename), pkg.path);
153
+ if (errorMsg) {
154
+ handleErrorAndExit(errorMsg);
155
+ }
150
156
  }
151
157
  if (success) {
152
158
  const filenames = await glob(`${absoluteDualOutDir}/**/*{.js,.d.ts}`, {
153
159
  ignore: 'node_modules/**',
154
160
  });
155
161
  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
162
  logSuccess(startTime);
162
163
  }
163
164
  }
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
165
  }
221
166
  };
222
167
  const realFileUrlArgv1 = await getRealPathAsFileUrl(argv[1]);
@@ -1,7 +1,6 @@
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;
5
4
  tsconfig: any;
6
5
  projectDir: string;
7
6
  configPath: string;
package/dist/esm/init.js CHANGED
@@ -26,11 +26,6 @@ const init = async (args) => {
26
26
  short: 'k',
27
27
  default: cwd(),
28
28
  },
29
- parallel: {
30
- type: 'boolean',
31
- short: 'l',
32
- default: false,
33
- },
34
29
  dirs: {
35
30
  type: 'boolean',
36
31
  short: 'd',
@@ -55,11 +50,10 @@ const init = async (args) => {
55
50
  log("--project, -p [path] \t Compile the project given the path to its configuration file, or to a folder with a 'tsconfig.json'.");
56
51
  log('--pkg-dir, -k [path] \t The directory to start looking for a package.json file. Defaults to cwd.');
57
52
  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
53
  log('--help, -h \t\t Print this message.');
60
54
  }
61
55
  else {
62
- const { project, 'target-extension': targetExt, 'pkg-dir': pkgDir, parallel, dirs, } = parsed;
56
+ const { project, 'target-extension': targetExt, 'pkg-dir': pkgDir, dirs } = parsed;
63
57
  let configPath = resolve(project);
64
58
  let stats = null;
65
59
  let pkg = null;
@@ -108,7 +102,6 @@ const init = async (args) => {
108
102
  return {
109
103
  pkg,
110
104
  dirs,
111
- parallel,
112
105
  tsconfig,
113
106
  projectDir,
114
107
  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.0",
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,7 +14,7 @@
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": {
@@ -54,16 +54,17 @@
54
54
  "typescript": ">=4.0.0 || >=4.9.0-dev || >=5.3.0-dev || >=5.4.0-dev || >=5.5.0-dev || next"
55
55
  },
56
56
  "devDependencies": {
57
- "@types/node": "^20.4.6",
57
+ "@types/node": "^20.11.0",
58
58
  "c8": "^8.0.1",
59
59
  "eslint": "^8.45.0",
60
60
  "eslint-plugin-n": "^16.0.1",
61
61
  "prettier": "^3.2.4",
62
- "typescript": "^5.5.0-dev.20240228",
62
+ "typescript": "^5.5.0-dev.20240525",
63
63
  "vite": "^5.2.8"
64
64
  },
65
65
  "dependencies": {
66
- "@knighted/specifier": "^1.0.1",
66
+ "@knighted/module": "^1.0.0-alpha.3",
67
+ "@knighted/specifier": "^2.0.0-rc.1",
67
68
  "find-up": "^6.3.0",
68
69
  "glob": "^10.3.3",
69
70
  "jsonc-parser": "^3.2.0",