@knighted/duel 3.0.0 → 3.1.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 +11 -1
- package/dist/cjs/duel.cjs +182 -3
- package/dist/cjs/init.cjs +19 -8
- package/dist/cjs/init.d.cts +1 -0
- package/dist/cjs/util.cjs +30 -4
- package/dist/cjs/util.d.cts +4 -2
- package/dist/esm/duel.js +184 -5
- package/dist/esm/init.d.ts +1 -0
- package/dist/esm/init.js +19 -8
- package/dist/esm/util.d.ts +4 -2
- package/dist/esm/util.js +28 -4
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -90,6 +90,10 @@ Note, there is a slight performance penalty since your project needs to be copie
|
|
|
90
90
|
|
|
91
91
|
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)).
|
|
92
92
|
|
|
93
|
+
#### Pre-`tsc` transform (TypeScript 58658)
|
|
94
|
+
|
|
95
|
+
When you pass `--modules`, `duel` copies your sources and runs [`@knighted/module`](https://github.com/knightedcodemonkey/module) **before** `tsc` so the transformed files no longer trigger TypeScript’s asymmetrical module-global errors (see [TypeScript#58658](https://github.com/microsoft/TypeScript/issues/58658)). No extra setup is needed: `--modules` is the pre-`tsc` mitigation.
|
|
96
|
+
|
|
93
97
|
## Options
|
|
94
98
|
|
|
95
99
|
The available options are limited, because you should define most of them inside your project's `tsconfig.json` file.
|
|
@@ -98,6 +102,10 @@ The available options are limited, because you should define most of them inside
|
|
|
98
102
|
- `--pkg-dir, -k` The directory to start looking for a package.json file. Defaults to `--project` dir.
|
|
99
103
|
- `--modules, -m` Transform module globals for dual build target. Defaults to false.
|
|
100
104
|
- `--dirs, -d` Outputs both builds to directories inside of `outDir`. Defaults to `false`.
|
|
105
|
+
- `--exports, -e` Generate `package.json` `exports` from build output. Values: `wildcard` | `dir` | `name`.
|
|
106
|
+
|
|
107
|
+
> [!NOTE]
|
|
108
|
+
> Exports keys are extensionless by design; the target `import`/`require`/`types` entries keep explicit file extensions so Node resolution remains deterministic.
|
|
101
109
|
|
|
102
110
|
You can run `duel --help` to get the same info. Below is the output of that:
|
|
103
111
|
|
|
@@ -118,7 +126,7 @@ These are definitely edge cases, and would only really come up if your project m
|
|
|
118
126
|
|
|
119
127
|
- 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.
|
|
120
128
|
|
|
121
|
-
- Unfortunately, `tsc` doesn't support [dual packages](https://nodejs.org/api/packages.html#dual-commonjses-module-packages) completely.
|
|
129
|
+
- Unfortunately, `tsc` doesn't support [dual packages](https://nodejs.org/api/packages.html#dual-commonjses-module-packages) completely. For mitigation details, see the pre-`tsc` transform note above (`--modules`).
|
|
122
130
|
|
|
123
131
|
- If running `duel` with your project's package.json file open in your editor, you may temporarily see the content replaced. This is because `duel` dynamically creates a new package.json using the `type` necessary for the dual build. Your original package.json will be restored after the build completes.
|
|
124
132
|
|
|
@@ -131,3 +139,5 @@ Fortunately, Node.js has added `--experimental-require-module` so that you can [
|
|
|
131
139
|
## Documentation
|
|
132
140
|
|
|
133
141
|
- [docs/faq.md](docs/faq.md)
|
|
142
|
+
- [docs/exports.md](docs/exports.md)
|
|
143
|
+
- [docs/migrate-v2-v3.md](docs/migrate-v2-v3.md)
|
package/dist/cjs/duel.cjs
CHANGED
|
@@ -13,15 +13,178 @@ const find_up_1 = require("find-up");
|
|
|
13
13
|
const module_1 = require("@knighted/module");
|
|
14
14
|
const init_js_1 = require("./init.cjs");
|
|
15
15
|
const util_js_1 = require("./util.cjs");
|
|
16
|
+
const stripKnownExt = path => {
|
|
17
|
+
return path.replace(/(\.d\.(?:ts|mts|cts)|\.(?:mjs|cjs|js))$/, '');
|
|
18
|
+
};
|
|
19
|
+
const ensureDotSlash = path => {
|
|
20
|
+
return path.startsWith('./') ? path : `./${path}`;
|
|
21
|
+
};
|
|
22
|
+
const getSubpath = (mode, relFromRoot) => {
|
|
23
|
+
const parsed = (0, node_path_1.parse)(relFromRoot);
|
|
24
|
+
const segments = parsed.dir.split('/').filter(Boolean);
|
|
25
|
+
if (mode === 'name') {
|
|
26
|
+
return parsed.name ? `./${parsed.name}` : null;
|
|
27
|
+
}
|
|
28
|
+
if (mode === 'dir') {
|
|
29
|
+
const last = segments.at(-1);
|
|
30
|
+
return last ? `./${last}` : null;
|
|
31
|
+
}
|
|
32
|
+
if (mode === 'wildcard') {
|
|
33
|
+
const first = segments[0];
|
|
34
|
+
return first ? `./${first}/*` : null;
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
};
|
|
16
38
|
const handleErrorAndExit = message => {
|
|
17
39
|
const exitCode = Number(message);
|
|
18
40
|
(0, util_js_1.logError)('Compilation errors found.');
|
|
19
41
|
process.exit(exitCode);
|
|
20
42
|
};
|
|
43
|
+
const generateExports = async (options) => {
|
|
44
|
+
const { mode, pkg, pkgDir, esmRoot, cjsRoot, mainDefaultKind, mainPath } = options;
|
|
45
|
+
const toPosix = path => path.replace(/\\/g, '/');
|
|
46
|
+
const esmRootPosix = toPosix(esmRoot);
|
|
47
|
+
const cjsRootPosix = toPosix(cjsRoot);
|
|
48
|
+
const esmIgnore = ['node_modules/**'];
|
|
49
|
+
const cjsIgnore = ['node_modules/**'];
|
|
50
|
+
const baseMap = new Map();
|
|
51
|
+
const subpathMap = new Map();
|
|
52
|
+
const baseToSubpath = new Map();
|
|
53
|
+
if (cjsRootPosix.startsWith(`${esmRootPosix}/`)) {
|
|
54
|
+
esmIgnore.push(`${cjsRootPosix}/**`);
|
|
55
|
+
}
|
|
56
|
+
if (esmRootPosix.startsWith(`${cjsRootPosix}/`)) {
|
|
57
|
+
cjsIgnore.push(`${esmRootPosix}/**`);
|
|
58
|
+
}
|
|
59
|
+
const recordPath = (kind, filePath, root) => {
|
|
60
|
+
const relPkg = toPosix((0, node_path_1.relative)(pkgDir, filePath));
|
|
61
|
+
const relFromRoot = toPosix((0, node_path_1.relative)(root, filePath));
|
|
62
|
+
const withDot = ensureDotSlash(relPkg);
|
|
63
|
+
const baseKey = stripKnownExt(relPkg);
|
|
64
|
+
const baseEntry = baseMap.get(baseKey) ?? {};
|
|
65
|
+
baseEntry[kind] = withDot;
|
|
66
|
+
baseMap.set(baseKey, baseEntry);
|
|
67
|
+
const subpath = getSubpath(mode, relFromRoot);
|
|
68
|
+
if (kind === 'types') {
|
|
69
|
+
const mappedSubpath = baseToSubpath.get(baseKey);
|
|
70
|
+
if (mappedSubpath) {
|
|
71
|
+
const subEntry = subpathMap.get(mappedSubpath) ?? {};
|
|
72
|
+
subEntry.types = withDot;
|
|
73
|
+
subpathMap.set(mappedSubpath, subEntry);
|
|
74
|
+
}
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (subpath && subpath !== '.') {
|
|
78
|
+
const subEntry = subpathMap.get(subpath) ?? {};
|
|
79
|
+
subEntry[kind] = withDot;
|
|
80
|
+
subpathMap.set(subpath, subEntry);
|
|
81
|
+
baseToSubpath.set(baseKey, subpath);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
const esmFiles = await (0, glob_1.glob)(`${esmRootPosix}/**/*.{js,mjs,d.ts,d.mts}`, {
|
|
85
|
+
ignore: esmIgnore,
|
|
86
|
+
});
|
|
87
|
+
for (const file of esmFiles) {
|
|
88
|
+
if (/\.d\.(ts|mts)$/.test(file)) {
|
|
89
|
+
recordPath('types', file, esmRoot);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
recordPath('import', file, esmRoot);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const cjsFiles = await (0, glob_1.glob)(`${cjsRootPosix}/**/*.{js,cjs,d.ts,d.cts}`, {
|
|
96
|
+
ignore: cjsIgnore,
|
|
97
|
+
});
|
|
98
|
+
for (const file of cjsFiles) {
|
|
99
|
+
if (/\.d\.(ts|cts)$/.test(file)) {
|
|
100
|
+
recordPath('types', file, cjsRoot);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
recordPath('require', file, cjsRoot);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const exportsMap = {};
|
|
107
|
+
const mainBase = mainPath ? stripKnownExt(mainPath.replace(/^\.\//, '')) : null;
|
|
108
|
+
const mainEntry = mainBase ? (baseMap.get(mainBase) ?? {}) : {};
|
|
109
|
+
if (mainPath) {
|
|
110
|
+
const rootEntry = {};
|
|
111
|
+
if (mainEntry.types) {
|
|
112
|
+
rootEntry.types = mainEntry.types;
|
|
113
|
+
}
|
|
114
|
+
if (mainDefaultKind === 'import') {
|
|
115
|
+
rootEntry.import = mainEntry.import ?? ensureDotSlash(mainPath);
|
|
116
|
+
if (mainEntry.require) {
|
|
117
|
+
rootEntry.require = mainEntry.require;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
rootEntry.require = mainEntry.require ?? ensureDotSlash(mainPath);
|
|
122
|
+
if (mainEntry.import) {
|
|
123
|
+
rootEntry.import = mainEntry.import;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
rootEntry.default = ensureDotSlash(mainPath);
|
|
127
|
+
exportsMap['.'] = rootEntry;
|
|
128
|
+
}
|
|
129
|
+
const defaultKind = mainDefaultKind ?? 'import';
|
|
130
|
+
for (const [subpath, entry] of subpathMap.entries()) {
|
|
131
|
+
const out = {};
|
|
132
|
+
if (entry.types) {
|
|
133
|
+
out.types = entry.types;
|
|
134
|
+
}
|
|
135
|
+
if (entry.import) {
|
|
136
|
+
out.import = entry.import;
|
|
137
|
+
}
|
|
138
|
+
if (entry.require) {
|
|
139
|
+
out.require = entry.require;
|
|
140
|
+
}
|
|
141
|
+
const def = defaultKind === 'import'
|
|
142
|
+
? (entry.import ?? entry.require)
|
|
143
|
+
: (entry.require ?? entry.import);
|
|
144
|
+
if (def) {
|
|
145
|
+
out.default = def;
|
|
146
|
+
}
|
|
147
|
+
if (Object.keys(out).length) {
|
|
148
|
+
exportsMap[subpath] = out;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (!exportsMap['.'] && baseMap.size) {
|
|
152
|
+
const [subpath, entry] = subpathMap.entries().next().value ?? [];
|
|
153
|
+
if (entry) {
|
|
154
|
+
const out = {};
|
|
155
|
+
if (entry.types) {
|
|
156
|
+
out.types = entry.types;
|
|
157
|
+
}
|
|
158
|
+
if (entry.import) {
|
|
159
|
+
out.import = entry.import;
|
|
160
|
+
}
|
|
161
|
+
if (entry.require) {
|
|
162
|
+
out.require = entry.require;
|
|
163
|
+
}
|
|
164
|
+
const def = defaultKind === 'import'
|
|
165
|
+
? (entry.import ?? entry.require)
|
|
166
|
+
: (entry.require ?? entry.import);
|
|
167
|
+
if (def) {
|
|
168
|
+
out.default = def;
|
|
169
|
+
}
|
|
170
|
+
if (Object.keys(out).length) {
|
|
171
|
+
exportsMap['.'] = out;
|
|
172
|
+
exportsMap[subpath] ??= out;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (Object.keys(exportsMap).length) {
|
|
177
|
+
const pkgJson = {
|
|
178
|
+
...pkg.packageJson,
|
|
179
|
+
exports: exportsMap,
|
|
180
|
+
};
|
|
181
|
+
await (0, promises_1.writeFile)(pkg.path, `${JSON.stringify(pkgJson, null, 2)}\n`);
|
|
182
|
+
}
|
|
183
|
+
};
|
|
21
184
|
const duel = async (args) => {
|
|
22
185
|
const ctx = await (0, init_js_1.init)(args);
|
|
23
186
|
if (ctx) {
|
|
24
|
-
const { projectDir, tsconfig, configPath, modules, dirs, pkg } = ctx;
|
|
187
|
+
const { projectDir, tsconfig, configPath, modules, dirs, pkg, exports: exportsOpt, } = ctx;
|
|
25
188
|
const tsc = await (0, find_up_1.findUp)(async (dir) => {
|
|
26
189
|
const tscBin = (0, node_path_1.join)(dir, 'node_modules', '.bin', 'tsc');
|
|
27
190
|
try {
|
|
@@ -45,6 +208,8 @@ const duel = async (args) => {
|
|
|
45
208
|
});
|
|
46
209
|
};
|
|
47
210
|
const pkgDir = (0, node_path_1.dirname)(pkg.path);
|
|
211
|
+
const mainPath = pkg.packageJson.main;
|
|
212
|
+
const mainDefaultKind = mainPath?.endsWith('.cjs') ? 'require' : 'import';
|
|
48
213
|
const outDir = tsconfig.compilerOptions?.outDir ?? 'dist';
|
|
49
214
|
const absoluteOutDir = (0, node_path_1.resolve)(projectDir, outDir);
|
|
50
215
|
const originalType = pkg.packageJson.type ?? 'commonjs';
|
|
@@ -93,6 +258,7 @@ const duel = async (args) => {
|
|
|
93
258
|
const writeOptions = {
|
|
94
259
|
target,
|
|
95
260
|
rewriteSpecifier,
|
|
261
|
+
transformSyntax: 'globals-only',
|
|
96
262
|
...(outFilename === filename ? { inPlace: true } : { out: outFilename }),
|
|
97
263
|
};
|
|
98
264
|
await (0, module_1.transform)(filename, writeOptions);
|
|
@@ -102,7 +268,7 @@ const duel = async (args) => {
|
|
|
102
268
|
}
|
|
103
269
|
};
|
|
104
270
|
const logSuccess = start => {
|
|
105
|
-
(0, util_js_1.
|
|
271
|
+
(0, util_js_1.logSuccess)(`Successfully created a dual ${isCjsBuild ? 'CJS' : 'ESM'} build in ${Math.round(node_perf_hooks_1.performance.now() - start)}ms.`);
|
|
106
272
|
};
|
|
107
273
|
(0, util_js_1.log)('Starting primary build...');
|
|
108
274
|
let success = false;
|
|
@@ -149,7 +315,7 @@ const duel = async (args) => {
|
|
|
149
315
|
await (0, module_1.transform)(file, {
|
|
150
316
|
out: file,
|
|
151
317
|
target: isCjsBuild ? 'commonjs' : 'module',
|
|
152
|
-
transformSyntax:
|
|
318
|
+
transformSyntax: 'globals-only',
|
|
153
319
|
});
|
|
154
320
|
}
|
|
155
321
|
}
|
|
@@ -192,6 +358,19 @@ const duel = async (args) => {
|
|
|
192
358
|
const primaryFiles = await (0, glob_1.glob)(`${primaryOutDir.replace(/\\/g, '/')}/**/*{.js,.d.ts}`, { ignore: 'node_modules/**' });
|
|
193
359
|
await updateSpecifiersAndFileExtensions(primaryFiles, 'commonjs', '.cjs');
|
|
194
360
|
}
|
|
361
|
+
if (exportsOpt) {
|
|
362
|
+
const esmRoot = isCjsBuild ? primaryOutDir : absoluteDualOutDir;
|
|
363
|
+
const cjsRoot = isCjsBuild ? absoluteDualOutDir : primaryOutDir;
|
|
364
|
+
await generateExports({
|
|
365
|
+
mode: exportsOpt,
|
|
366
|
+
pkg,
|
|
367
|
+
pkgDir,
|
|
368
|
+
esmRoot,
|
|
369
|
+
cjsRoot,
|
|
370
|
+
mainDefaultKind,
|
|
371
|
+
mainPath,
|
|
372
|
+
});
|
|
373
|
+
}
|
|
195
374
|
logSuccess(startTime);
|
|
196
375
|
}
|
|
197
376
|
}
|
package/dist/cjs/init.cjs
CHANGED
|
@@ -37,6 +37,10 @@ const init = async (args) => {
|
|
|
37
37
|
short: 'd',
|
|
38
38
|
default: false,
|
|
39
39
|
},
|
|
40
|
+
exports: {
|
|
41
|
+
type: 'string',
|
|
42
|
+
short: 'e',
|
|
43
|
+
},
|
|
40
44
|
help: {
|
|
41
45
|
type: 'boolean',
|
|
42
46
|
short: 'h',
|
|
@@ -51,16 +55,18 @@ const init = async (args) => {
|
|
|
51
55
|
return false;
|
|
52
56
|
}
|
|
53
57
|
if (parsed.help) {
|
|
54
|
-
|
|
55
|
-
(0, util_js_1.log)('
|
|
56
|
-
(0, util_js_1.log)(
|
|
57
|
-
(0, util_js_1.log)(
|
|
58
|
-
(0, util_js_1.log)('--
|
|
59
|
-
(0, util_js_1.log)('--
|
|
60
|
-
(0, util_js_1.log)('--
|
|
58
|
+
const bare = { bare: true };
|
|
59
|
+
(0, util_js_1.log)('Usage: duel [options]\n', 'info', bare);
|
|
60
|
+
(0, util_js_1.log)('Options:', 'info', bare);
|
|
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'.", 'info', bare);
|
|
62
|
+
(0, util_js_1.log)('--pkg-dir, -k [path] \t The directory to start looking for a package.json file. Defaults to --project directory.', 'info', bare);
|
|
63
|
+
(0, util_js_1.log)('--modules, -m \t\t Transform module globals for dual build target. Defaults to false.', 'info', bare);
|
|
64
|
+
(0, util_js_1.log)('--dirs, -d \t\t Output both builds to directories inside of outDir. [esm, cjs].', 'info', bare);
|
|
65
|
+
(0, util_js_1.log)('--exports, -e \t Generate package.json exports. Values: wildcard | dir | name.', 'info', bare);
|
|
66
|
+
(0, util_js_1.log)('--help, -h \t\t Print this message.', 'info', bare);
|
|
61
67
|
}
|
|
62
68
|
else {
|
|
63
|
-
const { project, 'target-extension': targetExt, 'pkg-dir': pkgDir, modules, dirs, } = parsed;
|
|
69
|
+
const { project, 'target-extension': targetExt, 'pkg-dir': pkgDir, modules, dirs, exports: exportsOpt, } = parsed;
|
|
64
70
|
let configPath = (0, node_path_1.resolve)(project);
|
|
65
71
|
let stats = null;
|
|
66
72
|
let pkg = null;
|
|
@@ -96,10 +102,15 @@ const init = async (args) => {
|
|
|
96
102
|
if (!tsconfig.compilerOptions?.outDir) {
|
|
97
103
|
(0, util_js_1.log)('No outDir defined in tsconfig.json. Build output will be in "dist".');
|
|
98
104
|
}
|
|
105
|
+
if (exportsOpt && !['wildcard', 'dir', 'name'].includes(exportsOpt)) {
|
|
106
|
+
(0, util_js_1.logError)('--exports expects one of: wildcard | dir | name');
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
99
109
|
return {
|
|
100
110
|
pkg,
|
|
101
111
|
dirs,
|
|
102
112
|
modules,
|
|
113
|
+
exports: exportsOpt,
|
|
103
114
|
tsconfig,
|
|
104
115
|
projectDir,
|
|
105
116
|
configPath,
|
package/dist/cjs/init.d.cts
CHANGED
|
@@ -2,6 +2,7 @@ export function init(args: any): Promise<false | {
|
|
|
2
2
|
pkg: import("read-package-up", { with: { "resolution-mode": "import" } }).NormalizedReadResult;
|
|
3
3
|
dirs: boolean;
|
|
4
4
|
modules: boolean;
|
|
5
|
+
exports: string | undefined;
|
|
5
6
|
tsconfig: {
|
|
6
7
|
compilerOptions?: import("get-tsconfig").TsConfigJson.CompilerOptions | undefined;
|
|
7
8
|
watchOptions?: import("get-tsconfig").TsConfigJson.WatchOptions | undefined;
|
package/dist/cjs/util.cjs
CHANGED
|
@@ -1,17 +1,43 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.getCompileFiles = exports.getRealPathAsFileUrl = exports.logError = exports.log = void 0;
|
|
3
|
+
exports.getCompileFiles = exports.getRealPathAsFileUrl = exports.logWarn = exports.logSuccess = 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
6
|
const node_child_process_1 = require("node:child_process");
|
|
7
7
|
const node_process_1 = require("node:process");
|
|
8
8
|
const node_os_1 = require("node:os");
|
|
9
|
-
const
|
|
9
|
+
const COLORS = {
|
|
10
|
+
reset: '\x1b[0m',
|
|
11
|
+
info: '\x1b[36m',
|
|
12
|
+
success: '\x1b[32m',
|
|
13
|
+
warn: '\x1b[33m',
|
|
14
|
+
error: '\x1b[31m',
|
|
15
|
+
};
|
|
16
|
+
const log = (msg = '', level = 'info', opts = {}) => {
|
|
17
|
+
const { bare = false } = opts;
|
|
18
|
+
const palette = {
|
|
19
|
+
info: COLORS.info,
|
|
20
|
+
success: COLORS.success,
|
|
21
|
+
warn: COLORS.warn,
|
|
22
|
+
error: COLORS.error,
|
|
23
|
+
};
|
|
24
|
+
const badge = {
|
|
25
|
+
success: '[✓]',
|
|
26
|
+
warn: '[!]',
|
|
27
|
+
error: '[x]',
|
|
28
|
+
info: '[i]',
|
|
29
|
+
}[level];
|
|
30
|
+
const color = palette[level] ?? COLORS.info;
|
|
31
|
+
const prefix = !bare && badge ? `${badge} ` : '';
|
|
10
32
|
// eslint-disable-next-line no-console
|
|
11
|
-
console.log(`${color}%s
|
|
33
|
+
console.log(`${color}${prefix}%s${COLORS.reset}`, msg);
|
|
12
34
|
};
|
|
13
35
|
exports.log = log;
|
|
14
|
-
const
|
|
36
|
+
const logSuccess = msg => log(msg, 'success');
|
|
37
|
+
exports.logSuccess = logSuccess;
|
|
38
|
+
const logWarn = msg => log(msg, 'warn');
|
|
39
|
+
exports.logWarn = logWarn;
|
|
40
|
+
const logError = msg => log(msg, 'error');
|
|
15
41
|
exports.logError = logError;
|
|
16
42
|
const getRealPathAsFileUrl = async (path) => {
|
|
17
43
|
const realPath = await (0, promises_1.realpath)(path);
|
package/dist/cjs/util.d.cts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
export function log(
|
|
2
|
-
export
|
|
1
|
+
export function log(msg?: string, level?: string, opts?: {}): void;
|
|
2
|
+
export function logError(msg: any): void;
|
|
3
|
+
export function logSuccess(msg: any): void;
|
|
4
|
+
export function logWarn(msg: any): void;
|
|
3
5
|
export function getRealPathAsFileUrl(path: any): Promise<string>;
|
|
4
6
|
export function getCompileFiles(tscBinPath: any, wd?: string): string[];
|
package/dist/esm/duel.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { argv, platform } from 'node:process';
|
|
3
|
-
import { join, dirname, resolve, relative } from 'node:path';
|
|
3
|
+
import { join, dirname, resolve, relative, parse as parsePath } from 'node:path';
|
|
4
4
|
import { spawn } from 'node:child_process';
|
|
5
5
|
import { writeFile, rm, rename, mkdir, cp, access, readFile } from 'node:fs/promises';
|
|
6
6
|
import { randomBytes } from 'node:crypto';
|
|
@@ -9,16 +9,179 @@ import { glob } from 'glob';
|
|
|
9
9
|
import { findUp } from 'find-up';
|
|
10
10
|
import { transform } from '@knighted/module';
|
|
11
11
|
import { init } from './init.js';
|
|
12
|
-
import { getRealPathAsFileUrl, getCompileFiles, logError,
|
|
12
|
+
import { getRealPathAsFileUrl, getCompileFiles, log, logError, logSuccess as logSuccessBadge, } from './util.js';
|
|
13
|
+
const stripKnownExt = path => {
|
|
14
|
+
return path.replace(/(\.d\.(?:ts|mts|cts)|\.(?:mjs|cjs|js))$/, '');
|
|
15
|
+
};
|
|
16
|
+
const ensureDotSlash = path => {
|
|
17
|
+
return path.startsWith('./') ? path : `./${path}`;
|
|
18
|
+
};
|
|
19
|
+
const getSubpath = (mode, relFromRoot) => {
|
|
20
|
+
const parsed = parsePath(relFromRoot);
|
|
21
|
+
const segments = parsed.dir.split('/').filter(Boolean);
|
|
22
|
+
if (mode === 'name') {
|
|
23
|
+
return parsed.name ? `./${parsed.name}` : null;
|
|
24
|
+
}
|
|
25
|
+
if (mode === 'dir') {
|
|
26
|
+
const last = segments.at(-1);
|
|
27
|
+
return last ? `./${last}` : null;
|
|
28
|
+
}
|
|
29
|
+
if (mode === 'wildcard') {
|
|
30
|
+
const first = segments[0];
|
|
31
|
+
return first ? `./${first}/*` : null;
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
};
|
|
13
35
|
const handleErrorAndExit = message => {
|
|
14
36
|
const exitCode = Number(message);
|
|
15
37
|
logError('Compilation errors found.');
|
|
16
38
|
process.exit(exitCode);
|
|
17
39
|
};
|
|
40
|
+
const generateExports = async (options) => {
|
|
41
|
+
const { mode, pkg, pkgDir, esmRoot, cjsRoot, mainDefaultKind, mainPath } = options;
|
|
42
|
+
const toPosix = path => path.replace(/\\/g, '/');
|
|
43
|
+
const esmRootPosix = toPosix(esmRoot);
|
|
44
|
+
const cjsRootPosix = toPosix(cjsRoot);
|
|
45
|
+
const esmIgnore = ['node_modules/**'];
|
|
46
|
+
const cjsIgnore = ['node_modules/**'];
|
|
47
|
+
const baseMap = new Map();
|
|
48
|
+
const subpathMap = new Map();
|
|
49
|
+
const baseToSubpath = new Map();
|
|
50
|
+
if (cjsRootPosix.startsWith(`${esmRootPosix}/`)) {
|
|
51
|
+
esmIgnore.push(`${cjsRootPosix}/**`);
|
|
52
|
+
}
|
|
53
|
+
if (esmRootPosix.startsWith(`${cjsRootPosix}/`)) {
|
|
54
|
+
cjsIgnore.push(`${esmRootPosix}/**`);
|
|
55
|
+
}
|
|
56
|
+
const recordPath = (kind, filePath, root) => {
|
|
57
|
+
const relPkg = toPosix(relative(pkgDir, filePath));
|
|
58
|
+
const relFromRoot = toPosix(relative(root, filePath));
|
|
59
|
+
const withDot = ensureDotSlash(relPkg);
|
|
60
|
+
const baseKey = stripKnownExt(relPkg);
|
|
61
|
+
const baseEntry = baseMap.get(baseKey) ?? {};
|
|
62
|
+
baseEntry[kind] = withDot;
|
|
63
|
+
baseMap.set(baseKey, baseEntry);
|
|
64
|
+
const subpath = getSubpath(mode, relFromRoot);
|
|
65
|
+
if (kind === 'types') {
|
|
66
|
+
const mappedSubpath = baseToSubpath.get(baseKey);
|
|
67
|
+
if (mappedSubpath) {
|
|
68
|
+
const subEntry = subpathMap.get(mappedSubpath) ?? {};
|
|
69
|
+
subEntry.types = withDot;
|
|
70
|
+
subpathMap.set(mappedSubpath, subEntry);
|
|
71
|
+
}
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (subpath && subpath !== '.') {
|
|
75
|
+
const subEntry = subpathMap.get(subpath) ?? {};
|
|
76
|
+
subEntry[kind] = withDot;
|
|
77
|
+
subpathMap.set(subpath, subEntry);
|
|
78
|
+
baseToSubpath.set(baseKey, subpath);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
const esmFiles = await glob(`${esmRootPosix}/**/*.{js,mjs,d.ts,d.mts}`, {
|
|
82
|
+
ignore: esmIgnore,
|
|
83
|
+
});
|
|
84
|
+
for (const file of esmFiles) {
|
|
85
|
+
if (/\.d\.(ts|mts)$/.test(file)) {
|
|
86
|
+
recordPath('types', file, esmRoot);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
recordPath('import', file, esmRoot);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const cjsFiles = await glob(`${cjsRootPosix}/**/*.{js,cjs,d.ts,d.cts}`, {
|
|
93
|
+
ignore: cjsIgnore,
|
|
94
|
+
});
|
|
95
|
+
for (const file of cjsFiles) {
|
|
96
|
+
if (/\.d\.(ts|cts)$/.test(file)) {
|
|
97
|
+
recordPath('types', file, cjsRoot);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
recordPath('require', file, cjsRoot);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const exportsMap = {};
|
|
104
|
+
const mainBase = mainPath ? stripKnownExt(mainPath.replace(/^\.\//, '')) : null;
|
|
105
|
+
const mainEntry = mainBase ? (baseMap.get(mainBase) ?? {}) : {};
|
|
106
|
+
if (mainPath) {
|
|
107
|
+
const rootEntry = {};
|
|
108
|
+
if (mainEntry.types) {
|
|
109
|
+
rootEntry.types = mainEntry.types;
|
|
110
|
+
}
|
|
111
|
+
if (mainDefaultKind === 'import') {
|
|
112
|
+
rootEntry.import = mainEntry.import ?? ensureDotSlash(mainPath);
|
|
113
|
+
if (mainEntry.require) {
|
|
114
|
+
rootEntry.require = mainEntry.require;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
rootEntry.require = mainEntry.require ?? ensureDotSlash(mainPath);
|
|
119
|
+
if (mainEntry.import) {
|
|
120
|
+
rootEntry.import = mainEntry.import;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
rootEntry.default = ensureDotSlash(mainPath);
|
|
124
|
+
exportsMap['.'] = rootEntry;
|
|
125
|
+
}
|
|
126
|
+
const defaultKind = mainDefaultKind ?? 'import';
|
|
127
|
+
for (const [subpath, entry] of subpathMap.entries()) {
|
|
128
|
+
const out = {};
|
|
129
|
+
if (entry.types) {
|
|
130
|
+
out.types = entry.types;
|
|
131
|
+
}
|
|
132
|
+
if (entry.import) {
|
|
133
|
+
out.import = entry.import;
|
|
134
|
+
}
|
|
135
|
+
if (entry.require) {
|
|
136
|
+
out.require = entry.require;
|
|
137
|
+
}
|
|
138
|
+
const def = defaultKind === 'import'
|
|
139
|
+
? (entry.import ?? entry.require)
|
|
140
|
+
: (entry.require ?? entry.import);
|
|
141
|
+
if (def) {
|
|
142
|
+
out.default = def;
|
|
143
|
+
}
|
|
144
|
+
if (Object.keys(out).length) {
|
|
145
|
+
exportsMap[subpath] = out;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (!exportsMap['.'] && baseMap.size) {
|
|
149
|
+
const [subpath, entry] = subpathMap.entries().next().value ?? [];
|
|
150
|
+
if (entry) {
|
|
151
|
+
const out = {};
|
|
152
|
+
if (entry.types) {
|
|
153
|
+
out.types = entry.types;
|
|
154
|
+
}
|
|
155
|
+
if (entry.import) {
|
|
156
|
+
out.import = entry.import;
|
|
157
|
+
}
|
|
158
|
+
if (entry.require) {
|
|
159
|
+
out.require = entry.require;
|
|
160
|
+
}
|
|
161
|
+
const def = defaultKind === 'import'
|
|
162
|
+
? (entry.import ?? entry.require)
|
|
163
|
+
: (entry.require ?? entry.import);
|
|
164
|
+
if (def) {
|
|
165
|
+
out.default = def;
|
|
166
|
+
}
|
|
167
|
+
if (Object.keys(out).length) {
|
|
168
|
+
exportsMap['.'] = out;
|
|
169
|
+
exportsMap[subpath] ??= out;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (Object.keys(exportsMap).length) {
|
|
174
|
+
const pkgJson = {
|
|
175
|
+
...pkg.packageJson,
|
|
176
|
+
exports: exportsMap,
|
|
177
|
+
};
|
|
178
|
+
await writeFile(pkg.path, `${JSON.stringify(pkgJson, null, 2)}\n`);
|
|
179
|
+
}
|
|
180
|
+
};
|
|
18
181
|
const duel = async (args) => {
|
|
19
182
|
const ctx = await init(args);
|
|
20
183
|
if (ctx) {
|
|
21
|
-
const { projectDir, tsconfig, configPath, modules, dirs, pkg } = ctx;
|
|
184
|
+
const { projectDir, tsconfig, configPath, modules, dirs, pkg, exports: exportsOpt, } = ctx;
|
|
22
185
|
const tsc = await findUp(async (dir) => {
|
|
23
186
|
const tscBin = join(dir, 'node_modules', '.bin', 'tsc');
|
|
24
187
|
try {
|
|
@@ -42,6 +205,8 @@ const duel = async (args) => {
|
|
|
42
205
|
});
|
|
43
206
|
};
|
|
44
207
|
const pkgDir = dirname(pkg.path);
|
|
208
|
+
const mainPath = pkg.packageJson.main;
|
|
209
|
+
const mainDefaultKind = mainPath?.endsWith('.cjs') ? 'require' : 'import';
|
|
45
210
|
const outDir = tsconfig.compilerOptions?.outDir ?? 'dist';
|
|
46
211
|
const absoluteOutDir = resolve(projectDir, outDir);
|
|
47
212
|
const originalType = pkg.packageJson.type ?? 'commonjs';
|
|
@@ -90,6 +255,7 @@ const duel = async (args) => {
|
|
|
90
255
|
const writeOptions = {
|
|
91
256
|
target,
|
|
92
257
|
rewriteSpecifier,
|
|
258
|
+
transformSyntax: 'globals-only',
|
|
93
259
|
...(outFilename === filename ? { inPlace: true } : { out: outFilename }),
|
|
94
260
|
};
|
|
95
261
|
await transform(filename, writeOptions);
|
|
@@ -99,7 +265,7 @@ const duel = async (args) => {
|
|
|
99
265
|
}
|
|
100
266
|
};
|
|
101
267
|
const logSuccess = start => {
|
|
102
|
-
|
|
268
|
+
logSuccessBadge(`Successfully created a dual ${isCjsBuild ? 'CJS' : 'ESM'} build in ${Math.round(performance.now() - start)}ms.`);
|
|
103
269
|
};
|
|
104
270
|
log('Starting primary build...');
|
|
105
271
|
let success = false;
|
|
@@ -146,7 +312,7 @@ const duel = async (args) => {
|
|
|
146
312
|
await transform(file, {
|
|
147
313
|
out: file,
|
|
148
314
|
target: isCjsBuild ? 'commonjs' : 'module',
|
|
149
|
-
transformSyntax:
|
|
315
|
+
transformSyntax: 'globals-only',
|
|
150
316
|
});
|
|
151
317
|
}
|
|
152
318
|
}
|
|
@@ -189,6 +355,19 @@ const duel = async (args) => {
|
|
|
189
355
|
const primaryFiles = await glob(`${primaryOutDir.replace(/\\/g, '/')}/**/*{.js,.d.ts}`, { ignore: 'node_modules/**' });
|
|
190
356
|
await updateSpecifiersAndFileExtensions(primaryFiles, 'commonjs', '.cjs');
|
|
191
357
|
}
|
|
358
|
+
if (exportsOpt) {
|
|
359
|
+
const esmRoot = isCjsBuild ? primaryOutDir : absoluteDualOutDir;
|
|
360
|
+
const cjsRoot = isCjsBuild ? absoluteDualOutDir : primaryOutDir;
|
|
361
|
+
await generateExports({
|
|
362
|
+
mode: exportsOpt,
|
|
363
|
+
pkg,
|
|
364
|
+
pkgDir,
|
|
365
|
+
esmRoot,
|
|
366
|
+
cjsRoot,
|
|
367
|
+
mainDefaultKind,
|
|
368
|
+
mainPath,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
192
371
|
logSuccess(startTime);
|
|
193
372
|
}
|
|
194
373
|
}
|
package/dist/esm/init.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export function init(args: any): Promise<false | {
|
|
|
2
2
|
pkg: import("read-package-up").NormalizedReadResult;
|
|
3
3
|
dirs: boolean;
|
|
4
4
|
modules: boolean;
|
|
5
|
+
exports: string | undefined;
|
|
5
6
|
tsconfig: {
|
|
6
7
|
compilerOptions?: import("get-tsconfig").TsConfigJson.CompilerOptions | undefined;
|
|
7
8
|
watchOptions?: import("get-tsconfig").TsConfigJson.WatchOptions | undefined;
|
package/dist/esm/init.js
CHANGED
|
@@ -34,6 +34,10 @@ const init = async (args) => {
|
|
|
34
34
|
short: 'd',
|
|
35
35
|
default: false,
|
|
36
36
|
},
|
|
37
|
+
exports: {
|
|
38
|
+
type: 'string',
|
|
39
|
+
short: 'e',
|
|
40
|
+
},
|
|
37
41
|
help: {
|
|
38
42
|
type: 'boolean',
|
|
39
43
|
short: 'h',
|
|
@@ -48,16 +52,18 @@ const init = async (args) => {
|
|
|
48
52
|
return false;
|
|
49
53
|
}
|
|
50
54
|
if (parsed.help) {
|
|
51
|
-
|
|
52
|
-
log('
|
|
53
|
-
log(
|
|
54
|
-
log(
|
|
55
|
-
log('--
|
|
56
|
-
log('--
|
|
57
|
-
log('--
|
|
55
|
+
const bare = { bare: true };
|
|
56
|
+
log('Usage: duel [options]\n', 'info', bare);
|
|
57
|
+
log('Options:', 'info', bare);
|
|
58
|
+
log("--project, -p [path] \t Compile the project given the path to its configuration file, or to a folder with a 'tsconfig.json'.", 'info', bare);
|
|
59
|
+
log('--pkg-dir, -k [path] \t The directory to start looking for a package.json file. Defaults to --project directory.', 'info', bare);
|
|
60
|
+
log('--modules, -m \t\t Transform module globals for dual build target. Defaults to false.', 'info', bare);
|
|
61
|
+
log('--dirs, -d \t\t Output both builds to directories inside of outDir. [esm, cjs].', 'info', bare);
|
|
62
|
+
log('--exports, -e \t Generate package.json exports. Values: wildcard | dir | name.', 'info', bare);
|
|
63
|
+
log('--help, -h \t\t Print this message.', 'info', bare);
|
|
58
64
|
}
|
|
59
65
|
else {
|
|
60
|
-
const { project, 'target-extension': targetExt, 'pkg-dir': pkgDir, modules, dirs, } = parsed;
|
|
66
|
+
const { project, 'target-extension': targetExt, 'pkg-dir': pkgDir, modules, dirs, exports: exportsOpt, } = parsed;
|
|
61
67
|
let configPath = resolve(project);
|
|
62
68
|
let stats = null;
|
|
63
69
|
let pkg = null;
|
|
@@ -93,10 +99,15 @@ const init = async (args) => {
|
|
|
93
99
|
if (!tsconfig.compilerOptions?.outDir) {
|
|
94
100
|
log('No outDir defined in tsconfig.json. Build output will be in "dist".');
|
|
95
101
|
}
|
|
102
|
+
if (exportsOpt && !['wildcard', 'dir', 'name'].includes(exportsOpt)) {
|
|
103
|
+
logError('--exports expects one of: wildcard | dir | name');
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
96
106
|
return {
|
|
97
107
|
pkg,
|
|
98
108
|
dirs,
|
|
99
109
|
modules,
|
|
110
|
+
exports: exportsOpt,
|
|
100
111
|
tsconfig,
|
|
101
112
|
projectDir,
|
|
102
113
|
configPath,
|
package/dist/esm/util.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
export function log(
|
|
2
|
-
export
|
|
1
|
+
export function log(msg?: string, level?: string, opts?: {}): void;
|
|
2
|
+
export function logError(msg: any): void;
|
|
3
|
+
export function logSuccess(msg: any): void;
|
|
4
|
+
export function logWarn(msg: any): void;
|
|
3
5
|
export function getRealPathAsFileUrl(path: any): Promise<string>;
|
|
4
6
|
export function getCompileFiles(tscBinPath: any, wd?: string): string[];
|
package/dist/esm/util.js
CHANGED
|
@@ -3,11 +3,35 @@ import { realpath } from 'node:fs/promises';
|
|
|
3
3
|
import { spawnSync } from 'node:child_process';
|
|
4
4
|
import { cwd, platform } from 'node:process';
|
|
5
5
|
import { EOL } from 'node:os';
|
|
6
|
-
const
|
|
6
|
+
const COLORS = {
|
|
7
|
+
reset: '\x1b[0m',
|
|
8
|
+
info: '\x1b[36m',
|
|
9
|
+
success: '\x1b[32m',
|
|
10
|
+
warn: '\x1b[33m',
|
|
11
|
+
error: '\x1b[31m',
|
|
12
|
+
};
|
|
13
|
+
const log = (msg = '', level = 'info', opts = {}) => {
|
|
14
|
+
const { bare = false } = opts;
|
|
15
|
+
const palette = {
|
|
16
|
+
info: COLORS.info,
|
|
17
|
+
success: COLORS.success,
|
|
18
|
+
warn: COLORS.warn,
|
|
19
|
+
error: COLORS.error,
|
|
20
|
+
};
|
|
21
|
+
const badge = {
|
|
22
|
+
success: '[✓]',
|
|
23
|
+
warn: '[!]',
|
|
24
|
+
error: '[x]',
|
|
25
|
+
info: '[i]',
|
|
26
|
+
}[level];
|
|
27
|
+
const color = palette[level] ?? COLORS.info;
|
|
28
|
+
const prefix = !bare && badge ? `${badge} ` : '';
|
|
7
29
|
// eslint-disable-next-line no-console
|
|
8
|
-
console.log(`${color}%s
|
|
30
|
+
console.log(`${color}${prefix}%s${COLORS.reset}`, msg);
|
|
9
31
|
};
|
|
10
|
-
const
|
|
32
|
+
const logSuccess = msg => log(msg, 'success');
|
|
33
|
+
const logWarn = msg => log(msg, 'warn');
|
|
34
|
+
const logError = msg => log(msg, 'error');
|
|
11
35
|
const getRealPathAsFileUrl = async (path) => {
|
|
12
36
|
const realPath = await realpath(path);
|
|
13
37
|
const asFileUrl = pathToFileURL(realPath).href;
|
|
@@ -24,4 +48,4 @@ const getCompileFiles = (tscBinPath, wd = cwd()) => {
|
|
|
24
48
|
.split(EOL)
|
|
25
49
|
.filter(path => !/node_modules|^$/.test(path));
|
|
26
50
|
};
|
|
27
|
-
export { log, logError, getRealPathAsFileUrl, getCompileFiles };
|
|
51
|
+
export { log, logError, logSuccess, logWarn, getRealPathAsFileUrl, getCompileFiles };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@knighted/duel",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.1",
|
|
4
4
|
"description": "TypeScript dual packages.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/esm/duel.js",
|
|
@@ -67,7 +67,6 @@
|
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
69
69
|
"@eslint/js": "^9.39.1",
|
|
70
|
-
"@knighted/module": "^1.0.0-rc.2",
|
|
71
70
|
"@tsconfig/recommended": "^1.0.10",
|
|
72
71
|
"@types/node": "^24.10.1",
|
|
73
72
|
"c8": "^10.1.3",
|
|
@@ -83,6 +82,7 @@
|
|
|
83
82
|
"vite": "^7.2.4"
|
|
84
83
|
},
|
|
85
84
|
"dependencies": {
|
|
85
|
+
"@knighted/module": "^1.0.0-rc.6",
|
|
86
86
|
"find-up": "^8.0.0",
|
|
87
87
|
"get-tsconfig": "^4.13.0",
|
|
88
88
|
"glob": "^13.0.0",
|