@knighted/duel 1.0.4 → 1.0.5
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/dist/cjs/duel.cjs +215 -0
- package/dist/cjs/duel.d.cts +2 -0
- package/dist/cjs/init.cjs +126 -0
- package/dist/cjs/init.d.ts +8 -0
- package/dist/cjs/util.cjs +18 -0
- package/dist/cjs/util.d.cts +3 -0
- package/dist/esm/duel.d.ts +2 -0
- package/dist/esm/duel.js +212 -0
- package/dist/esm/init.d.ts +8 -0
- package/dist/esm/init.js +120 -0
- package/dist/esm/util.d.ts +3 -0
- package/dist/esm/util.js +13 -0
- package/package.json +11 -11
- package/dist/duel.cjs +0 -214
- package/dist/duel.js +0 -208
- package/dist/init.cjs +0 -133
- package/dist/init.js +0 -126
- package/dist/util.cjs +0 -20
- package/dist/util.js +0 -12
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.duel = void 0;
|
|
5
|
+
const node_process_1 = require("node:process");
|
|
6
|
+
const node_path_1 = require("node:path");
|
|
7
|
+
const node_child_process_1 = require("node:child_process");
|
|
8
|
+
const promises_1 = require("node:fs/promises");
|
|
9
|
+
const node_crypto_1 = require("node:crypto");
|
|
10
|
+
const node_perf_hooks_1 = require("node:perf_hooks");
|
|
11
|
+
const glob_1 = require("glob");
|
|
12
|
+
const find_up_1 = require("find-up");
|
|
13
|
+
const specifier_1 = require("@knighted/specifier");
|
|
14
|
+
const init_js_1 = require("./init.cjs");
|
|
15
|
+
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('close', code => {
|
|
30
|
+
if (code === null) {
|
|
31
|
+
return reject(new Error('Failed to compile.'));
|
|
32
|
+
}
|
|
33
|
+
if (code > 0) {
|
|
34
|
+
return reject(new Error('Compilation errors found.'));
|
|
35
|
+
}
|
|
36
|
+
resolve(code);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
const duel = async (args) => {
|
|
41
|
+
const ctx = await (0, init_js_1.init)(args);
|
|
42
|
+
if (ctx) {
|
|
43
|
+
const { projectDir, tsconfig, configPath, parallel, dirs, pkg } = ctx;
|
|
44
|
+
const pkgDir = (0, node_path_1.dirname)(pkg.path);
|
|
45
|
+
const outDir = tsconfig.compilerOptions?.outDir ?? 'dist';
|
|
46
|
+
const absoluteOutDir = (0, node_path_1.resolve)(projectDir, outDir);
|
|
47
|
+
const originalType = pkg.packageJson.type ?? 'commonjs';
|
|
48
|
+
const isCjsBuild = originalType !== 'commonjs';
|
|
49
|
+
const targetExt = isCjsBuild ? '.cjs' : '.mjs';
|
|
50
|
+
const hex = (0, node_crypto_1.randomBytes)(4).toString('hex');
|
|
51
|
+
const getOverrideTsConfig = () => {
|
|
52
|
+
return {
|
|
53
|
+
...tsconfig,
|
|
54
|
+
compilerOptions: {
|
|
55
|
+
...tsconfig.compilerOptions,
|
|
56
|
+
module: 'NodeNext',
|
|
57
|
+
moduleResolution: 'NodeNext',
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
const runPrimaryBuild = () => {
|
|
62
|
+
return runBuild(configPath, dirs
|
|
63
|
+
? isCjsBuild
|
|
64
|
+
? (0, node_path_1.join)(absoluteOutDir, 'esm')
|
|
65
|
+
: (0, node_path_1.join)(absoluteOutDir, 'cjs')
|
|
66
|
+
: absoluteOutDir);
|
|
67
|
+
};
|
|
68
|
+
const updateSpecifiersAndFileExtensions = async (filenames) => {
|
|
69
|
+
for (const filename of filenames) {
|
|
70
|
+
const dts = /(\.d\.ts)$/;
|
|
71
|
+
const outFilename = dts.test(filename)
|
|
72
|
+
? filename.replace(dts, isCjsBuild ? '.d.cts' : '.d.mts')
|
|
73
|
+
: filename.replace(/\.js$/, targetExt);
|
|
74
|
+
const { code, error } = await specifier_1.specifier.update(filename, ({ value }) => {
|
|
75
|
+
// Collapse any BinaryExpression or NewExpression to test for a relative specifier
|
|
76
|
+
const collapsed = value.replace(/['"`+)\s]|new String\(/g, '');
|
|
77
|
+
const relative = /^(?:\.|\.\.)\//;
|
|
78
|
+
if (relative.test(collapsed)) {
|
|
79
|
+
// $2 is for any closing quotation/parens around BE or NE
|
|
80
|
+
return value.replace(/(.+)\.js([)'"`]*)?$/, `$1${targetExt}$2`);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
if (code && !error) {
|
|
84
|
+
await (0, promises_1.writeFile)(outFilename, code);
|
|
85
|
+
await (0, promises_1.rm)(filename, { force: true });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
const logSuccess = start => {
|
|
90
|
+
(0, util_js_1.log)(`Successfully created a dual ${isCjsBuild ? 'CJS' : 'ESM'} build in ${Math.round(node_perf_hooks_1.performance.now() - start)}ms.`);
|
|
91
|
+
};
|
|
92
|
+
if (parallel) {
|
|
93
|
+
const paraName = `_${hex}_`;
|
|
94
|
+
const paraParent = (0, node_path_1.join)(projectDir, '..');
|
|
95
|
+
const paraTempDir = (0, node_path_1.join)(paraParent, paraName);
|
|
96
|
+
let isDirWritable = true;
|
|
97
|
+
try {
|
|
98
|
+
const stats = await (0, promises_1.stat)(paraParent);
|
|
99
|
+
if (stats.isDirectory()) {
|
|
100
|
+
await (0, promises_1.access)(paraParent, promises_1.constants.W_OK);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
isDirWritable = false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
isDirWritable = false;
|
|
108
|
+
}
|
|
109
|
+
if (!isDirWritable) {
|
|
110
|
+
(0, util_js_1.logError)('No writable directory to prepare parallel builds. Exiting.');
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
(0, util_js_1.log)('Preparing parallel build...');
|
|
114
|
+
const prepStart = node_perf_hooks_1.performance.now();
|
|
115
|
+
await (0, promises_1.cp)(projectDir, paraTempDir, {
|
|
116
|
+
recursive: true,
|
|
117
|
+
/**
|
|
118
|
+
* Ignore common .gitignored directories in Node.js projects.
|
|
119
|
+
* Except node_modules.
|
|
120
|
+
*
|
|
121
|
+
* @see https://github.com/github/gitignore/blob/main/Node.gitignore
|
|
122
|
+
*/
|
|
123
|
+
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),
|
|
124
|
+
});
|
|
125
|
+
const dualConfigPath = (0, node_path_1.join)(paraTempDir, 'tsconfig.json');
|
|
126
|
+
const absoluteDualOutDir = (0, node_path_1.join)(paraTempDir, isCjsBuild ? (0, node_path_1.join)(outDir, 'cjs') : (0, node_path_1.join)(outDir, 'esm'));
|
|
127
|
+
const tsconfigDual = getOverrideTsConfig();
|
|
128
|
+
await (0, promises_1.writeFile)(dualConfigPath, JSON.stringify(tsconfigDual));
|
|
129
|
+
await (0, promises_1.writeFile)((0, node_path_1.join)(paraTempDir, 'package.json'), JSON.stringify({
|
|
130
|
+
type: isCjsBuild ? 'commonjs' : 'module',
|
|
131
|
+
}));
|
|
132
|
+
(0, util_js_1.log)(`Prepared in ${Math.round(node_perf_hooks_1.performance.now() - prepStart)}ms.`);
|
|
133
|
+
(0, util_js_1.log)('Starting parallel dual builds...');
|
|
134
|
+
let success = false;
|
|
135
|
+
const startTime = node_perf_hooks_1.performance.now();
|
|
136
|
+
try {
|
|
137
|
+
await Promise.all([
|
|
138
|
+
runPrimaryBuild(),
|
|
139
|
+
runBuild(dualConfigPath, absoluteDualOutDir),
|
|
140
|
+
]);
|
|
141
|
+
success = true;
|
|
142
|
+
}
|
|
143
|
+
catch ({ message }) {
|
|
144
|
+
(0, util_js_1.logError)(message);
|
|
145
|
+
}
|
|
146
|
+
if (success) {
|
|
147
|
+
const filenames = await (0, glob_1.glob)(`${absoluteDualOutDir}/**/*{.js,.d.ts}`, {
|
|
148
|
+
ignore: 'node_modules/**',
|
|
149
|
+
});
|
|
150
|
+
await updateSpecifiersAndFileExtensions(filenames);
|
|
151
|
+
// Copy over and cleanup
|
|
152
|
+
await (0, promises_1.cp)(absoluteDualOutDir, (0, node_path_1.join)(absoluteOutDir, isCjsBuild ? 'cjs' : 'esm'), {
|
|
153
|
+
recursive: true,
|
|
154
|
+
});
|
|
155
|
+
await (0, promises_1.rm)(paraTempDir, { force: true, recursive: true });
|
|
156
|
+
logSuccess(startTime);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
(0, util_js_1.log)('Starting primary build...');
|
|
161
|
+
let success = false;
|
|
162
|
+
const startTime = node_perf_hooks_1.performance.now();
|
|
163
|
+
try {
|
|
164
|
+
await runPrimaryBuild();
|
|
165
|
+
success = true;
|
|
166
|
+
}
|
|
167
|
+
catch ({ message }) {
|
|
168
|
+
(0, util_js_1.logError)(message);
|
|
169
|
+
}
|
|
170
|
+
if (success) {
|
|
171
|
+
const dualConfigPath = (0, node_path_1.join)(projectDir, `tsconfig.${hex}.json`);
|
|
172
|
+
const absoluteDualOutDir = (0, node_path_1.join)(projectDir, isCjsBuild ? (0, node_path_1.join)(outDir, 'cjs') : (0, node_path_1.join)(outDir, 'esm'));
|
|
173
|
+
const tsconfigDual = getOverrideTsConfig();
|
|
174
|
+
const pkgRename = 'package.json.bak';
|
|
175
|
+
/**
|
|
176
|
+
* Create a new package.json with updated `type` field.
|
|
177
|
+
* Create a new tsconfig.json.
|
|
178
|
+
*
|
|
179
|
+
* The need to create a new package.json makes doing
|
|
180
|
+
* the builds in parallel difficult.
|
|
181
|
+
*/
|
|
182
|
+
await (0, promises_1.rename)(pkg.path, (0, node_path_1.join)(pkgDir, pkgRename));
|
|
183
|
+
await (0, promises_1.writeFile)(pkg.path, JSON.stringify({
|
|
184
|
+
type: isCjsBuild ? 'commonjs' : 'module',
|
|
185
|
+
}));
|
|
186
|
+
await (0, promises_1.writeFile)(dualConfigPath, JSON.stringify(tsconfigDual));
|
|
187
|
+
// Build dual
|
|
188
|
+
(0, util_js_1.log)('Starting dual build...');
|
|
189
|
+
try {
|
|
190
|
+
await runBuild(dualConfigPath, absoluteDualOutDir);
|
|
191
|
+
}
|
|
192
|
+
catch ({ message }) {
|
|
193
|
+
success = false;
|
|
194
|
+
(0, util_js_1.logError)(message);
|
|
195
|
+
}
|
|
196
|
+
// Cleanup and restore
|
|
197
|
+
await (0, promises_1.rm)(dualConfigPath, { force: true });
|
|
198
|
+
await (0, promises_1.rm)(pkg.path, { force: true });
|
|
199
|
+
await (0, promises_1.rename)((0, node_path_1.join)(pkgDir, pkgRename), pkg.path);
|
|
200
|
+
if (success) {
|
|
201
|
+
const filenames = await (0, glob_1.glob)(`${absoluteDualOutDir}/**/*{.js,.d.ts}`, {
|
|
202
|
+
ignore: 'node_modules/**',
|
|
203
|
+
});
|
|
204
|
+
await updateSpecifiersAndFileExtensions(filenames);
|
|
205
|
+
logSuccess(startTime);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
exports.duel = duel;
|
|
212
|
+
const realFileUrlArgv1 = await (0, util_js_1.getRealPathAsFileUrl)(node_process_1.argv[1]);
|
|
213
|
+
if (import.meta.url === realFileUrlArgv1) {
|
|
214
|
+
await duel();
|
|
215
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.init = void 0;
|
|
7
|
+
const node_process_1 = require("node:process");
|
|
8
|
+
const node_util_1 = require("node:util");
|
|
9
|
+
const node_path_1 = require("node:path");
|
|
10
|
+
const promises_1 = require("node:fs/promises");
|
|
11
|
+
const read_pkg_up_1 = require("read-pkg-up");
|
|
12
|
+
const jsonc_parser_1 = __importDefault(require("jsonc-parser"));
|
|
13
|
+
const util_js_1 = require("./util.cjs");
|
|
14
|
+
const init = async (args) => {
|
|
15
|
+
let parsed = null;
|
|
16
|
+
try {
|
|
17
|
+
const { values } = (0, node_util_1.parseArgs)({
|
|
18
|
+
args,
|
|
19
|
+
options: {
|
|
20
|
+
project: {
|
|
21
|
+
type: 'string',
|
|
22
|
+
short: 'p',
|
|
23
|
+
default: 'tsconfig.json',
|
|
24
|
+
},
|
|
25
|
+
'target-extension': {
|
|
26
|
+
type: 'string',
|
|
27
|
+
short: 'x',
|
|
28
|
+
default: '',
|
|
29
|
+
},
|
|
30
|
+
'pkg-dir': {
|
|
31
|
+
type: 'string',
|
|
32
|
+
short: 'k',
|
|
33
|
+
default: (0, node_process_1.cwd)(),
|
|
34
|
+
},
|
|
35
|
+
parallel: {
|
|
36
|
+
type: 'boolean',
|
|
37
|
+
short: 'l',
|
|
38
|
+
default: false,
|
|
39
|
+
},
|
|
40
|
+
dirs: {
|
|
41
|
+
type: 'boolean',
|
|
42
|
+
short: 'd',
|
|
43
|
+
default: false,
|
|
44
|
+
},
|
|
45
|
+
help: {
|
|
46
|
+
type: 'boolean',
|
|
47
|
+
short: 'h',
|
|
48
|
+
default: false,
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
parsed = values;
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
(0, util_js_1.logError)(err.message);
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
if (parsed.help) {
|
|
59
|
+
(0, util_js_1.log)('Usage: duel [options]\n');
|
|
60
|
+
(0, util_js_1.log)('Options:');
|
|
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
|
+
(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)('--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
|
+
(0, util_js_1.log)('--help, -h \t\t Print this message.');
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
const { project, 'target-extension': targetExt, 'pkg-dir': pkgDir, parallel, dirs, } = parsed;
|
|
69
|
+
let configPath = (0, node_path_1.resolve)(project);
|
|
70
|
+
let stats = null;
|
|
71
|
+
let pkg = null;
|
|
72
|
+
if (targetExt) {
|
|
73
|
+
(0, util_js_1.logError)('--target-extension is deprecated. Define "type" in your package.json instead and the dual build will be inferred from that.');
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
pkg = await (0, read_pkg_up_1.readPackageUp)({ cwd: pkgDir });
|
|
77
|
+
if (!pkg) {
|
|
78
|
+
(0, util_js_1.logError)('No package.json file found.');
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
stats = await (0, promises_1.stat)(configPath);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
(0, util_js_1.logError)(`Provided --project '${project}' resolves to ${configPath} which is not a file or directory.`);
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
if (stats.isDirectory()) {
|
|
89
|
+
configPath = (0, node_path_1.join)(configPath, 'tsconfig.json');
|
|
90
|
+
try {
|
|
91
|
+
stats = await (0, promises_1.stat)(configPath);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
(0, util_js_1.logError)(`Provided --project '${project}' resolves to a directory ${(0, node_path_1.dirname)(configPath)} with no tsconfig.json.`);
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (stats.isFile()) {
|
|
99
|
+
let tsconfig = null;
|
|
100
|
+
const errors = [];
|
|
101
|
+
const jsonText = (await (0, promises_1.readFile)(configPath)).toString();
|
|
102
|
+
tsconfig = jsonc_parser_1.default.parse(jsonText, errors, {
|
|
103
|
+
disallowComments: false,
|
|
104
|
+
allowTrailingComma: true,
|
|
105
|
+
});
|
|
106
|
+
if (errors.length) {
|
|
107
|
+
(0, util_js_1.logError)(`The config file found at ${configPath} is not parsable as JSONC.`);
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
if (!tsconfig.compilerOptions?.outDir) {
|
|
111
|
+
(0, util_js_1.log)('No outDir defined in tsconfig.json. Build output will be in "dist".');
|
|
112
|
+
}
|
|
113
|
+
const projectDir = (0, node_path_1.dirname)(configPath);
|
|
114
|
+
return {
|
|
115
|
+
pkg,
|
|
116
|
+
dirs,
|
|
117
|
+
parallel,
|
|
118
|
+
tsconfig,
|
|
119
|
+
projectDir,
|
|
120
|
+
configPath,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return false;
|
|
125
|
+
};
|
|
126
|
+
exports.init = init;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export function init(args: any): Promise<false | {
|
|
2
|
+
pkg: import("read-pkg-up", { with: { "resolution-mode": "import" } }).NormalizedReadResult;
|
|
3
|
+
dirs: boolean | undefined;
|
|
4
|
+
parallel: boolean | undefined;
|
|
5
|
+
tsconfig: any;
|
|
6
|
+
projectDir: string;
|
|
7
|
+
configPath: string;
|
|
8
|
+
}>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getRealPathAsFileUrl = exports.logError = exports.log = void 0;
|
|
4
|
+
const node_url_1 = require("node:url");
|
|
5
|
+
const promises_1 = require("node:fs/promises");
|
|
6
|
+
const log = (color = '\x1b[30m', msg = '') => {
|
|
7
|
+
// eslint-disable-next-line no-console
|
|
8
|
+
console.log(`${color}%s\x1b[0m`, msg);
|
|
9
|
+
};
|
|
10
|
+
exports.log = log;
|
|
11
|
+
const logError = log.bind(null, '\x1b[31m');
|
|
12
|
+
exports.logError = logError;
|
|
13
|
+
const getRealPathAsFileUrl = async (path) => {
|
|
14
|
+
const realPath = await (0, promises_1.realpath)(path);
|
|
15
|
+
const asFileUrl = (0, node_url_1.pathToFileURL)(realPath).href;
|
|
16
|
+
return asFileUrl;
|
|
17
|
+
};
|
|
18
|
+
exports.getRealPathAsFileUrl = getRealPathAsFileUrl;
|
package/dist/esm/duel.js
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { argv } from 'node:process';
|
|
3
|
+
import { join, dirname, resolve } from 'node:path';
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
import { writeFile, rm, cp, rename, stat, access, constants } from 'node:fs/promises';
|
|
6
|
+
import { randomBytes } from 'node:crypto';
|
|
7
|
+
import { performance } from 'node:perf_hooks';
|
|
8
|
+
import { glob } from 'glob';
|
|
9
|
+
import { findUp, pathExists } from 'find-up';
|
|
10
|
+
import { specifier } from '@knighted/specifier';
|
|
11
|
+
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('close', code => {
|
|
27
|
+
if (code === null) {
|
|
28
|
+
return reject(new Error('Failed to compile.'));
|
|
29
|
+
}
|
|
30
|
+
if (code > 0) {
|
|
31
|
+
return reject(new Error('Compilation errors found.'));
|
|
32
|
+
}
|
|
33
|
+
resolve(code);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
const duel = async (args) => {
|
|
38
|
+
const ctx = await init(args);
|
|
39
|
+
if (ctx) {
|
|
40
|
+
const { projectDir, tsconfig, configPath, parallel, dirs, pkg } = ctx;
|
|
41
|
+
const pkgDir = dirname(pkg.path);
|
|
42
|
+
const outDir = tsconfig.compilerOptions?.outDir ?? 'dist';
|
|
43
|
+
const absoluteOutDir = resolve(projectDir, outDir);
|
|
44
|
+
const originalType = pkg.packageJson.type ?? 'commonjs';
|
|
45
|
+
const isCjsBuild = originalType !== 'commonjs';
|
|
46
|
+
const targetExt = isCjsBuild ? '.cjs' : '.mjs';
|
|
47
|
+
const hex = randomBytes(4).toString('hex');
|
|
48
|
+
const getOverrideTsConfig = () => {
|
|
49
|
+
return {
|
|
50
|
+
...tsconfig,
|
|
51
|
+
compilerOptions: {
|
|
52
|
+
...tsconfig.compilerOptions,
|
|
53
|
+
module: 'NodeNext',
|
|
54
|
+
moduleResolution: 'NodeNext',
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
const runPrimaryBuild = () => {
|
|
59
|
+
return runBuild(configPath, dirs
|
|
60
|
+
? isCjsBuild
|
|
61
|
+
? join(absoluteOutDir, 'esm')
|
|
62
|
+
: join(absoluteOutDir, 'cjs')
|
|
63
|
+
: absoluteOutDir);
|
|
64
|
+
};
|
|
65
|
+
const updateSpecifiersAndFileExtensions = async (filenames) => {
|
|
66
|
+
for (const filename of filenames) {
|
|
67
|
+
const dts = /(\.d\.ts)$/;
|
|
68
|
+
const outFilename = dts.test(filename)
|
|
69
|
+
? filename.replace(dts, isCjsBuild ? '.d.cts' : '.d.mts')
|
|
70
|
+
: filename.replace(/\.js$/, targetExt);
|
|
71
|
+
const { code, error } = await specifier.update(filename, ({ value }) => {
|
|
72
|
+
// Collapse any BinaryExpression or NewExpression to test for a relative specifier
|
|
73
|
+
const collapsed = value.replace(/['"`+)\s]|new String\(/g, '');
|
|
74
|
+
const relative = /^(?:\.|\.\.)\//;
|
|
75
|
+
if (relative.test(collapsed)) {
|
|
76
|
+
// $2 is for any closing quotation/parens around BE or NE
|
|
77
|
+
return value.replace(/(.+)\.js([)'"`]*)?$/, `$1${targetExt}$2`);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
if (code && !error) {
|
|
81
|
+
await writeFile(outFilename, code);
|
|
82
|
+
await rm(filename, { force: true });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
const logSuccess = start => {
|
|
87
|
+
log(`Successfully created a dual ${isCjsBuild ? 'CJS' : 'ESM'} build in ${Math.round(performance.now() - start)}ms.`);
|
|
88
|
+
};
|
|
89
|
+
if (parallel) {
|
|
90
|
+
const paraName = `_${hex}_`;
|
|
91
|
+
const paraParent = join(projectDir, '..');
|
|
92
|
+
const paraTempDir = join(paraParent, paraName);
|
|
93
|
+
let isDirWritable = true;
|
|
94
|
+
try {
|
|
95
|
+
const stats = await stat(paraParent);
|
|
96
|
+
if (stats.isDirectory()) {
|
|
97
|
+
await access(paraParent, constants.W_OK);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
isDirWritable = false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
isDirWritable = false;
|
|
105
|
+
}
|
|
106
|
+
if (!isDirWritable) {
|
|
107
|
+
logError('No writable directory to prepare parallel builds. Exiting.');
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
log('Preparing parallel build...');
|
|
111
|
+
const prepStart = performance.now();
|
|
112
|
+
await cp(projectDir, paraTempDir, {
|
|
113
|
+
recursive: true,
|
|
114
|
+
/**
|
|
115
|
+
* Ignore common .gitignored directories in Node.js projects.
|
|
116
|
+
* Except node_modules.
|
|
117
|
+
*
|
|
118
|
+
* @see https://github.com/github/gitignore/blob/main/Node.gitignore
|
|
119
|
+
*/
|
|
120
|
+
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),
|
|
121
|
+
});
|
|
122
|
+
const dualConfigPath = join(paraTempDir, 'tsconfig.json');
|
|
123
|
+
const absoluteDualOutDir = join(paraTempDir, isCjsBuild ? join(outDir, 'cjs') : join(outDir, 'esm'));
|
|
124
|
+
const tsconfigDual = getOverrideTsConfig();
|
|
125
|
+
await writeFile(dualConfigPath, JSON.stringify(tsconfigDual));
|
|
126
|
+
await writeFile(join(paraTempDir, 'package.json'), JSON.stringify({
|
|
127
|
+
type: isCjsBuild ? 'commonjs' : 'module',
|
|
128
|
+
}));
|
|
129
|
+
log(`Prepared in ${Math.round(performance.now() - prepStart)}ms.`);
|
|
130
|
+
log('Starting parallel dual builds...');
|
|
131
|
+
let success = false;
|
|
132
|
+
const startTime = performance.now();
|
|
133
|
+
try {
|
|
134
|
+
await Promise.all([
|
|
135
|
+
runPrimaryBuild(),
|
|
136
|
+
runBuild(dualConfigPath, absoluteDualOutDir),
|
|
137
|
+
]);
|
|
138
|
+
success = true;
|
|
139
|
+
}
|
|
140
|
+
catch ({ message }) {
|
|
141
|
+
logError(message);
|
|
142
|
+
}
|
|
143
|
+
if (success) {
|
|
144
|
+
const filenames = await glob(`${absoluteDualOutDir}/**/*{.js,.d.ts}`, {
|
|
145
|
+
ignore: 'node_modules/**',
|
|
146
|
+
});
|
|
147
|
+
await updateSpecifiersAndFileExtensions(filenames);
|
|
148
|
+
// Copy over and cleanup
|
|
149
|
+
await cp(absoluteDualOutDir, join(absoluteOutDir, isCjsBuild ? 'cjs' : 'esm'), {
|
|
150
|
+
recursive: true,
|
|
151
|
+
});
|
|
152
|
+
await rm(paraTempDir, { force: true, recursive: true });
|
|
153
|
+
logSuccess(startTime);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
log('Starting primary build...');
|
|
158
|
+
let success = false;
|
|
159
|
+
const startTime = performance.now();
|
|
160
|
+
try {
|
|
161
|
+
await runPrimaryBuild();
|
|
162
|
+
success = true;
|
|
163
|
+
}
|
|
164
|
+
catch ({ message }) {
|
|
165
|
+
logError(message);
|
|
166
|
+
}
|
|
167
|
+
if (success) {
|
|
168
|
+
const dualConfigPath = join(projectDir, `tsconfig.${hex}.json`);
|
|
169
|
+
const absoluteDualOutDir = join(projectDir, isCjsBuild ? join(outDir, 'cjs') : join(outDir, 'esm'));
|
|
170
|
+
const tsconfigDual = getOverrideTsConfig();
|
|
171
|
+
const pkgRename = 'package.json.bak';
|
|
172
|
+
/**
|
|
173
|
+
* Create a new package.json with updated `type` field.
|
|
174
|
+
* Create a new tsconfig.json.
|
|
175
|
+
*
|
|
176
|
+
* The need to create a new package.json makes doing
|
|
177
|
+
* the builds in parallel difficult.
|
|
178
|
+
*/
|
|
179
|
+
await rename(pkg.path, join(pkgDir, pkgRename));
|
|
180
|
+
await writeFile(pkg.path, JSON.stringify({
|
|
181
|
+
type: isCjsBuild ? 'commonjs' : 'module',
|
|
182
|
+
}));
|
|
183
|
+
await writeFile(dualConfigPath, JSON.stringify(tsconfigDual));
|
|
184
|
+
// Build dual
|
|
185
|
+
log('Starting dual build...');
|
|
186
|
+
try {
|
|
187
|
+
await runBuild(dualConfigPath, absoluteDualOutDir);
|
|
188
|
+
}
|
|
189
|
+
catch ({ message }) {
|
|
190
|
+
success = false;
|
|
191
|
+
logError(message);
|
|
192
|
+
}
|
|
193
|
+
// Cleanup and restore
|
|
194
|
+
await rm(dualConfigPath, { force: true });
|
|
195
|
+
await rm(pkg.path, { force: true });
|
|
196
|
+
await rename(join(pkgDir, pkgRename), pkg.path);
|
|
197
|
+
if (success) {
|
|
198
|
+
const filenames = await glob(`${absoluteDualOutDir}/**/*{.js,.d.ts}`, {
|
|
199
|
+
ignore: 'node_modules/**',
|
|
200
|
+
});
|
|
201
|
+
await updateSpecifiersAndFileExtensions(filenames);
|
|
202
|
+
logSuccess(startTime);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
const realFileUrlArgv1 = await getRealPathAsFileUrl(argv[1]);
|
|
209
|
+
if (import.meta.url === realFileUrlArgv1) {
|
|
210
|
+
await duel();
|
|
211
|
+
}
|
|
212
|
+
export { duel };
|