@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 +17 -11
- package/dist/cjs/duel.cjs +84 -129
- package/dist/cjs/init.cjs +5 -5
- package/dist/cjs/{init.d.ts → init.d.cts} +1 -1
- package/dist/cjs/util.cjs +12 -1
- package/dist/cjs/util.d.cts +1 -0
- package/dist/esm/duel.js +87 -132
- package/dist/esm/init.d.ts +1 -1
- package/dist/esm/init.js +5 -5
- package/dist/esm/util.d.ts +1 -0
- package/dist/esm/util.js +11 -1
- package/package.json +10 -6
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
|
-
-
|
|
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 >=
|
|
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
|
-
###
|
|
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
|
-
|
|
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 --
|
|
81
|
+
"build": "duel --modules"
|
|
77
82
|
}
|
|
78
83
|
```
|
|
79
84
|
|
|
80
|
-
|
|
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
|
|
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`
|
|
119
|
-
|
|
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,
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if (
|
|
118
|
-
(0, util_js_1.
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
*
|
|
127
|
-
*
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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,
|
|
141
|
-
|
|
142
|
-
|
|
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
|
|
146
|
-
runPrimaryBuild(),
|
|
147
|
-
runBuild(dualConfigPath, absoluteDualOutDir),
|
|
148
|
-
]);
|
|
149
|
-
success = true;
|
|
154
|
+
await runBuild(dualConfigPath, absoluteDualOutDir);
|
|
150
155
|
}
|
|
151
156
|
catch ({ message }) {
|
|
152
|
-
|
|
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
|
-
|
|
35
|
+
modules: {
|
|
36
36
|
type: 'boolean',
|
|
37
|
-
short: '
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
package/dist/cjs/util.d.cts
CHANGED
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,
|
|
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,
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
if (
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
*
|
|
124
|
-
*
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
|
143
|
-
runPrimaryBuild(),
|
|
144
|
-
runBuild(dualConfigPath, absoluteDualOutDir),
|
|
145
|
-
]);
|
|
146
|
-
success = true;
|
|
151
|
+
await runBuild(dualConfigPath, absoluteDualOutDir);
|
|
147
152
|
}
|
|
148
153
|
catch ({ message }) {
|
|
149
|
-
|
|
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]);
|
package/dist/esm/init.d.ts
CHANGED
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
|
-
|
|
29
|
+
modules: {
|
|
30
30
|
type: 'boolean',
|
|
31
|
-
short: '
|
|
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,
|
|
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
|
-
|
|
111
|
+
modules,
|
|
112
112
|
tsconfig,
|
|
113
113
|
projectDir,
|
|
114
114
|
configPath,
|
package/dist/esm/util.d.ts
CHANGED
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
|
-
|
|
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": "
|
|
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": ">=
|
|
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.
|
|
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
|
-
"
|
|
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/
|
|
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",
|