@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 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. 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.
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.log)(`Successfully created a dual ${isCjsBuild ? 'CJS' : 'ESM'} build in ${Math.round(node_perf_hooks_1.performance.now() - start)}ms.`);
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: false,
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
- (0, util_js_1.log)('Usage: duel [options]\n');
55
- (0, util_js_1.log)('Options:');
56
- (0, util_js_1.log)("--project, -p [path] \t Compile the project given the path to its configuration file, or to a folder with a 'tsconfig.json'.");
57
- (0, util_js_1.log)('--pkg-dir, -k [path] \t The directory to start looking for a package.json file. Defaults to --project directory.');
58
- (0, util_js_1.log)('--modules, -m \t\t Transform module globals for dual build target. Defaults to false.');
59
- (0, util_js_1.log)('--dirs, -d \t\t Output both builds to directories inside of outDir. [esm, cjs].');
60
- (0, util_js_1.log)('--help, -h \t\t Print this message.');
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,
@@ -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 log = (color = '\x1b[30m', msg = '') => {
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\x1b[0m`, msg);
33
+ console.log(`${color}${prefix}%s${COLORS.reset}`, msg);
12
34
  };
13
35
  exports.log = log;
14
- const logError = log.bind(null, '\x1b[31m');
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);
@@ -1,4 +1,6 @@
1
- export function log(color?: string, msg?: string): void;
2
- export const logError: (msg?: string | undefined) => void;
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, log } from './util.js';
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
- log(`Successfully created a dual ${isCjsBuild ? 'CJS' : 'ESM'} build in ${Math.round(performance.now() - start)}ms.`);
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: false,
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
  }
@@ -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
- log('Usage: duel [options]\n');
52
- log('Options:');
53
- log("--project, -p [path] \t Compile the project given the path to its configuration file, or to a folder with a 'tsconfig.json'.");
54
- log('--pkg-dir, -k [path] \t The directory to start looking for a package.json file. Defaults to --project directory.');
55
- log('--modules, -m \t\t Transform module globals for dual build target. Defaults to false.');
56
- log('--dirs, -d \t\t Output both builds to directories inside of outDir. [esm, cjs].');
57
- log('--help, -h \t\t Print this message.');
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,
@@ -1,4 +1,6 @@
1
- export function log(color?: string, msg?: string): void;
2
- export const logError: (msg?: string | undefined) => void;
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 log = (color = '\x1b[30m', msg = '') => {
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\x1b[0m`, msg);
30
+ console.log(`${color}${prefix}%s${COLORS.reset}`, msg);
9
31
  };
10
- const logError = log.bind(null, '\x1b[31m');
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.0.0",
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",