@knighted/duel 4.0.0-rc.0 → 4.0.0-rc.2

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
@@ -6,6 +6,9 @@
6
6
 
7
7
  Tool for building a Node.js [dual package](https://nodejs.org/api/packages.html#dual-commonjses-module-packages) with TypeScript. Supports CommonJS and ES module projects.
8
8
 
9
+ > [!NOTE]
10
+ > I wish this tool were unnecessary, but dual emit was declared out of scope by the TypeScript team, so `duel` exists to fill that gap.
11
+
9
12
  ## Features
10
13
 
11
14
  - Bidirectional ESM ↔️ CJS dual builds inferred from the package.json `type`.
@@ -60,6 +63,9 @@ It should work similarly for a CJS-first project. Except, your package.json file
60
63
  > [!IMPORTANT]
61
64
  > This works best if your CJS-first project uses file extensions in _relative_ specifiers. That is acceptable in CJS and [required in ESM](https://nodejs.org/api/esm.html#import-specifiers). `duel` does not rewrite bare specifiers or remap relative specifiers to directory indexes.
62
65
 
66
+ > [!TIP]
67
+ > `duel` creates a hash-named temp workspace (`.duel-cache/_duel_<hash>_`) inside your project during a build. The `_duel_<hash>_` temp directory is removed on success/failure unless `DUEL_KEEP_TEMP=1` is set. The `.duel-cache/` folder itself (which also holds incremental caches) is not automatically deleted—add it to your `.gitignore`. If a temp folder is ever left behind (e.g., abrupt kill), it is safe to delete.
68
+
63
69
  ### Build orientation
64
70
 
65
71
  `duel` infers the primary vs dual build orientation from your `package.json` `type`:
@@ -84,7 +90,7 @@ Assuming an `outDir` of `dist`, running the above will create `dist/esm` and `di
84
90
  `tsc` is asymmetric: `import.meta` globals fail in a CJS-targeted build, but CommonJS globals like `__filename`/`__dirname` pass when targeting ESM, causing runtime errors in the compiled output. See [TypeScript#58658](https://github.com/microsoft/TypeScript/issues/58658). Use `--mode` to mitigate:
85
91
 
86
92
  - `--mode globals` [rewrites module globals](https://github.com/knightedcodemonkey/module/blob/main/docs/globals-only.md#rewrites-at-a-glance).
87
- - `--mode full` adds syntax lowering _in addition to_ the globals rewrite.
93
+ - `--mode full` adds syntax lowering _in addition to_ the globals rewrite. TS sources keep a pre-`tsc` guard (`transformSyntax: "globals-only"`) so TypeScript controls declaration emit; JS/JSX and the dual CJS rewrite path are fully lowered. See the [mode matrix](docs/mode-matrix.md) for details.
88
94
 
89
95
  ```json
90
96
  "scripts": {
package/dist/cjs/duel.cjs CHANGED
@@ -3,6 +3,7 @@
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.duel = void 0;
5
5
  const node_process_1 = require("node:process");
6
+ const node_url_1 = require("node:url");
6
7
  const node_path_1 = require("node:path");
7
8
  const node_child_process_1 = require("node:child_process");
8
9
  const promises_1 = require("node:fs/promises");
@@ -53,7 +54,7 @@ const duel = async (args) => {
53
54
  /* continue */
54
55
  }
55
56
  }, { cwd: projectDir });
56
- const runBuild = (project, outDir) => {
57
+ const runBuild = (project, outDir, tsBuildInfoFile, cwdForBuild) => {
57
58
  return new Promise((fulfill, rejectBuild) => {
58
59
  const useBuildMode = hasReferences;
59
60
  const tsArgs = useBuildMode
@@ -61,7 +62,16 @@ const duel = async (args) => {
61
62
  : outDir
62
63
  ? [tsc, '-p', project, '--outDir', outDir]
63
64
  : [tsc, '-p', project];
64
- const build = (0, node_child_process_1.spawn)(process.execPath, tsArgs, { stdio: 'inherit' });
65
+ if (!useBuildMode) {
66
+ tsArgs.push('--incremental');
67
+ if (tsBuildInfoFile) {
68
+ tsArgs.push('--tsBuildInfoFile', tsBuildInfoFile);
69
+ }
70
+ }
71
+ const build = (0, node_child_process_1.spawn)(process.execPath, tsArgs, {
72
+ stdio: 'inherit',
73
+ cwd: cwdForBuild ?? process.cwd(),
74
+ });
65
75
  build.on('exit', code => {
66
76
  if (code > 0) {
67
77
  return rejectBuild(new Error(code));
@@ -77,27 +87,94 @@ const duel = async (args) => {
77
87
  const absoluteOutDir = (0, node_path_1.resolve)(projectDir, outDir);
78
88
  const originalType = pkg.packageJson.type ?? 'commonjs';
79
89
  const isCjsBuild = originalType !== 'commonjs';
90
+ const absoluteDualOutDir = (0, node_path_1.join)(projectDir, isCjsBuild ? (0, node_path_1.join)(outDir, 'cjs') : (0, node_path_1.join)(outDir, 'esm'));
91
+ const projectRoot = (0, node_path_1.dirname)(projectDir);
80
92
  const primaryOutDir = dirs
81
93
  ? isCjsBuild
82
94
  ? (0, node_path_1.join)(absoluteOutDir, 'esm')
83
95
  : (0, node_path_1.join)(absoluteOutDir, 'cjs')
84
96
  : absoluteOutDir;
85
- const hex = (0, node_crypto_1.randomBytes)(4).toString('hex');
97
+ const { type, exports, imports, main, module, types, typings, typesVersions, sideEffects, } = pkg.packageJson ?? {};
98
+ const pkgHashInputs = {
99
+ type,
100
+ exports,
101
+ imports,
102
+ main,
103
+ module,
104
+ types,
105
+ typings,
106
+ typesVersions,
107
+ sideEffects,
108
+ };
109
+ const hash = (0, node_crypto_1.createHash)('sha1')
110
+ .update(JSON.stringify({
111
+ configPath,
112
+ tsconfig,
113
+ packageJson: pkgHashInputs,
114
+ dualTarget: isCjsBuild ? 'cjs' : 'esm',
115
+ }))
116
+ .digest('hex')
117
+ .slice(0, 8);
118
+ const cacheDir = (0, node_path_1.join)(projectDir, '.duel-cache');
119
+ const primaryTsBuildInfoFile = (0, node_path_1.join)(cacheDir, `primary.${hash}.tsbuildinfo`);
120
+ const dualTsBuildInfoFile = (0, node_path_1.join)(cacheDir, `dual.${hash}.tsbuildinfo`);
121
+ const subDir = (0, node_path_1.join)(cacheDir, `_duel_${hash}_`);
122
+ const shadowDualOutDir = (0, node_path_1.join)(subDir, (0, node_path_1.relative)(projectRoot, absoluteDualOutDir));
86
123
  const hazardMode = detectDualPackageHazard ?? 'warn';
87
124
  const hazardScope = dualPackageHazardScope ?? 'file';
88
- const getOverrideTsConfig = () => {
125
+ function mapReferencesToShadow(references = [], options) {
126
+ const { resolveRefPath, toShadowPathFn, fromDir } = options;
127
+ return references.map(ref => {
128
+ if (!ref?.path)
129
+ return ref;
130
+ const refAbs = resolveRefPath(ref.path);
131
+ const shadowRef = toShadowPathFn(refAbs);
132
+ return {
133
+ ...ref,
134
+ path: (0, node_path_1.relative)(fromDir, shadowRef),
135
+ };
136
+ });
137
+ }
138
+ const getOverrideTsConfig = dualConfigDir => {
139
+ const shadowReferences = mapReferencesToShadow(tsconfig.references ?? [], {
140
+ resolveRefPath: refPath => (0, node_path_1.resolve)(projectDir, refPath),
141
+ toShadowPathFn: abs => (0, node_path_1.join)(subDir, (0, node_path_1.relative)(projectRoot, abs)),
142
+ fromDir: dualConfigDir,
143
+ });
89
144
  return {
90
145
  ...tsconfig,
146
+ references: shadowReferences,
91
147
  compilerOptions: {
92
- ...tsconfig.compilerOptions,
148
+ ...(tsconfig.compilerOptions ?? {}),
93
149
  module: 'NodeNext',
94
150
  moduleResolution: 'NodeNext',
151
+ target: tsconfig.compilerOptions?.target ?? 'ES2022',
152
+ // Emit dual build into the shadow workspace, then copy to real outDir
153
+ outDir: shadowDualOutDir,
154
+ incremental: true,
155
+ tsBuildInfoFile: dualTsBuildInfoFile,
95
156
  },
96
157
  };
97
158
  };
98
159
  const hasReferences = Array.isArray(tsconfig.references) && tsconfig.references.length > 0;
99
160
  const runPrimaryBuild = () => {
100
- return runBuild(configPath, hasReferences ? undefined : primaryOutDir);
161
+ return runBuild(configPath, hasReferences ? undefined : primaryOutDir, hasReferences ? undefined : primaryTsBuildInfoFile, projectDir);
162
+ };
163
+ const refreshDualBuildInfo = async () => {
164
+ try {
165
+ await (0, promises_1.access)(shadowDualOutDir);
166
+ }
167
+ catch {
168
+ await (0, promises_1.rm)(dualTsBuildInfoFile, { force: true });
169
+ }
170
+ };
171
+ const refreshPrimaryBuildInfo = async () => {
172
+ try {
173
+ await (0, promises_1.access)(primaryOutDir);
174
+ }
175
+ catch {
176
+ await (0, promises_1.rm)(primaryTsBuildInfoFile, { force: true });
177
+ }
101
178
  };
102
179
  const resolveReferenceConfigPath = (baseDir, refPath) => {
103
180
  const abs = (0, node_path_1.resolve)(baseDir, refPath);
@@ -233,6 +310,7 @@ const duel = async (args) => {
233
310
  let success = false;
234
311
  const startTime = node_perf_hooks_1.performance.now();
235
312
  try {
313
+ await refreshPrimaryBuildInfo();
236
314
  await runPrimaryBuild();
237
315
  success = true;
238
316
  }
@@ -240,15 +318,19 @@ const duel = async (args) => {
240
318
  handleErrorAndExit(message);
241
319
  }
242
320
  if (success) {
243
- const projectRoot = (0, node_path_1.dirname)(projectDir);
244
321
  const parentRoot = (0, node_path_1.dirname)(projectRoot);
245
- const subDir = (0, node_path_1.join)(projectRoot, `_${hex}_`);
246
- const absoluteDualOutDir = (0, node_path_1.join)(projectDir, isCjsBuild ? (0, node_path_1.join)(outDir, 'cjs') : (0, node_path_1.join)(outDir, 'esm'));
247
- const tsconfigDual = getOverrideTsConfig();
248
322
  const tsconfigRel = (0, node_path_1.relative)(projectRoot, configPath);
249
- const tsconfigDualRel = tsconfigRel.replace(/tsconfig\.json$/i, `tsconfig.${hex}.json`);
323
+ const tsconfigDualRel = tsconfigRel.replace(/tsconfig\.json$/i, `tsconfig.${hash}.json`);
250
324
  const dualConfigPath = (0, node_path_1.join)(subDir, tsconfigDualRel);
251
325
  const dualConfigDir = (0, node_path_1.dirname)(dualConfigPath);
326
+ const tsconfigDual = getOverrideTsConfig(dualConfigDir);
327
+ const keepTemp = process.env.DUEL_KEEP_TEMP === '1';
328
+ const { cleanupTemp, cleanupTempSync } = (0, util_js_1.createTempCleanup)({
329
+ subDir,
330
+ keepTemp,
331
+ logWarnFn: util_js_1.logWarn,
332
+ });
333
+ const unregisterCleanupHandlers = (0, util_js_1.registerCleanupHandlers)(cleanupTempSync);
252
334
  let errorMsg = '';
253
335
  let exportsConfigData = null;
254
336
  if (exportsConfig) {
@@ -285,11 +367,16 @@ const duel = async (args) => {
285
367
  process.exit(1);
286
368
  }
287
369
  }
288
- await (0, promises_1.mkdir)(subDir, { recursive: true });
289
- await (0, util_js_1.maybeLinkNodeModules)(projectRoot, subDir);
370
+ await Promise.all([
371
+ (0, promises_1.mkdir)(subDir, { recursive: true }),
372
+ (0, promises_1.mkdir)(cacheDir, { recursive: true }),
373
+ ]);
374
+ const linkNodeModulesPromise = (0, util_js_1.maybeLinkNodeModules)(projectDir, subDir);
290
375
  const projectRel = (0, node_path_1.relative)(projectRoot, projectDir);
291
376
  const projectCopyDest = (0, node_path_1.join)(subDir, projectRel);
292
377
  const makeCopyFilter = (rootDir, allowDist) => src => {
378
+ if (src.split(/[/\\]/).includes('.duel-cache'))
379
+ return false;
293
380
  if (src.split(/[/\\]/).includes('node_modules'))
294
381
  return false;
295
382
  if (allowDist)
@@ -300,92 +387,170 @@ const duel = async (args) => {
300
387
  const [segment] = rel.split(node_path_1.sep);
301
388
  return segment !== outDir;
302
389
  };
303
- const copyProjectTree = async (allowDist) => {
304
- await (0, promises_1.cp)(projectDir, projectCopyDest, {
305
- recursive: true,
306
- filter: makeCopyFilter(projectDir, allowDist),
307
- });
308
- if (hasReferences) {
309
- for (const ref of tsconfig.references ?? []) {
310
- if (!ref.path)
390
+ const copyFilesToTemp = async () => {
391
+ const copyDirContents = async (sourceDir, destDir, allowDist) => {
392
+ await (0, promises_1.mkdir)(destDir, { recursive: true });
393
+ const filter = makeCopyFilter(sourceDir, allowDist);
394
+ const entries = await (0, promises_1.readdir)(sourceDir, { withFileTypes: true });
395
+ for (const entry of entries) {
396
+ const srcPath = (0, node_path_1.join)(sourceDir, entry.name);
397
+ if (!filter(srcPath))
311
398
  continue;
312
- const refAbs = (0, node_path_1.resolve)(projectDir, ref.path);
313
- const refRel = (0, node_path_1.relative)(projectRoot, refAbs);
314
- const refDest = (0, node_path_1.join)(subDir, refRel);
315
- await (0, promises_1.cp)(refAbs, refDest, {
399
+ const dstPath = (0, node_path_1.join)(destDir, entry.name);
400
+ await (0, promises_1.cp)(srcPath, dstPath, {
316
401
  recursive: true,
317
- filter: makeCopyFilter(refAbs, allowDist),
402
+ filter,
318
403
  });
319
404
  }
405
+ };
406
+ if (copyMode === 'full') {
407
+ const allowDist = hasReferences;
408
+ await copyDirContents(projectDir, projectCopyDest, allowDist);
409
+ if (hasReferences) {
410
+ for (const ref of tsconfig.references ?? []) {
411
+ if (!ref.path)
412
+ continue;
413
+ const refAbs = (0, node_path_1.resolve)(projectDir, ref.path);
414
+ const refRel = (0, node_path_1.relative)(projectRoot, refAbs);
415
+ const refDest = (0, node_path_1.join)(subDir, refRel);
416
+ await copyDirContents(refAbs, refDest, allowDist);
417
+ }
418
+ }
320
419
  }
321
- };
322
- if (copyMode === 'full') {
323
- const allowDist = hasReferences;
324
- await copyProjectTree(allowDist);
325
- }
326
- else {
327
- const filesToCopy = new Set([...compileFiles, ...configFiles, ...packageJsons]);
328
- for (const file of filesToCopy) {
329
- let rel = (0, node_path_1.relative)(projectRoot, file);
330
- rel = (0, node_path_1.normalize)(rel);
331
- if (rel.startsWith('..')) {
332
- const altRel = hasReferences ? (0, node_path_1.normalize)((0, node_path_1.relative)(parentRoot, file)) : rel;
333
- if (!altRel.startsWith('..')) {
334
- rel = altRel;
420
+ else {
421
+ const filesToCopy = new Set([...compileFiles, ...configFiles, ...packageJsons]);
422
+ for (const file of filesToCopy) {
423
+ let rel = (0, node_path_1.relative)(projectRoot, file);
424
+ rel = (0, node_path_1.normalize)(rel);
425
+ if (rel.startsWith('..')) {
426
+ const altRel = hasReferences ? (0, node_path_1.normalize)((0, node_path_1.relative)(parentRoot, file)) : rel;
427
+ if (!altRel.startsWith('..')) {
428
+ rel = altRel;
429
+ }
430
+ else {
431
+ (0, util_js_1.logWarn)(`Skipping copy for ${file} outside of project root ${projectRoot}`);
432
+ continue;
433
+ }
335
434
  }
336
- else {
337
- (0, util_js_1.logWarn)(`Skipping copy for ${file} outside of project root ${projectRoot}`);
338
- continue;
435
+ const dest = (0, node_path_1.join)(subDir, rel);
436
+ await (0, promises_1.mkdir)((0, node_path_1.dirname)(dest), { recursive: true });
437
+ await (0, promises_1.cp)(file, dest);
438
+ }
439
+ const missingConfigs = [];
440
+ for (const configFile of configFiles) {
441
+ const dest = (0, node_path_1.join)(subDir, (0, node_path_1.relative)(projectRoot, configFile));
442
+ try {
443
+ await (0, promises_1.access)(dest);
444
+ }
445
+ catch {
446
+ missingConfigs.push({ src: configFile, dest });
447
+ }
448
+ }
449
+ if (missingConfigs.length) {
450
+ (0, util_js_1.logWarn)(`Copying ${missingConfigs.length} missing referenced config(s) into temp workspace: ${missingConfigs
451
+ .map(entry => entry.src)
452
+ .join(', ')}`);
453
+ for (const { src, dest } of missingConfigs) {
454
+ await (0, promises_1.mkdir)((0, node_path_1.dirname)(dest), { recursive: true });
455
+ await (0, promises_1.cp)(src, dest);
339
456
  }
340
457
  }
341
- const dest = (0, node_path_1.join)(subDir, rel);
342
- await (0, promises_1.mkdir)((0, node_path_1.dirname)(dest), { recursive: true });
343
- await (0, promises_1.cp)(file, dest);
344
458
  }
345
- const missingConfigs = [];
459
+ };
460
+ const toShadowPath = absPath => (0, node_path_1.join)(subDir, (0, node_path_1.relative)(projectRoot, absPath));
461
+ // Patch referenced tsconfig files in the shadow workspace to emit dual outputs
462
+ const patchReferencedConfigs = async () => {
346
463
  for (const configFile of configFiles) {
464
+ if (configFile === configPath)
465
+ continue;
347
466
  const dest = (0, node_path_1.join)(subDir, (0, node_path_1.relative)(projectRoot, configFile));
467
+ let parsed = null;
348
468
  try {
349
- await (0, promises_1.access)(dest);
350
- }
351
- catch {
352
- missingConfigs.push({ src: configFile, dest });
469
+ parsed = (0, get_tsconfig_1.parseTsconfig)(dest);
353
470
  }
354
- }
355
- if (missingConfigs.length) {
356
- (0, util_js_1.logWarn)(`Copying ${missingConfigs.length} missing referenced config(s) into temp workspace: ${missingConfigs
357
- .map(entry => entry.src)
358
- .join(', ')}`);
359
- for (const { src, dest } of missingConfigs) {
360
- await (0, promises_1.mkdir)((0, node_path_1.dirname)(dest), { recursive: true });
361
- await (0, promises_1.cp)(src, dest);
471
+ catch (err) {
472
+ (0, util_js_1.logWarn)(`Skipping referenced tsconfig at ${dest} (parse failed): ${err?.message ?? err}`);
473
+ continue;
362
474
  }
475
+ const cfg = parsed?.tsconfig ?? parsed;
476
+ if (!cfg || typeof cfg !== 'object')
477
+ continue;
478
+ const cfgDir = (0, node_path_1.dirname)(configFile);
479
+ const baseOut = cfg.compilerOptions?.outDir
480
+ ? (0, node_path_1.resolve)(cfgDir, cfg.compilerOptions.outDir)
481
+ : (0, node_path_1.resolve)(cfgDir, 'dist');
482
+ const dualOutReal = (0, node_path_1.join)(baseOut, isCjsBuild ? 'cjs' : 'esm');
483
+ const dualOut = toShadowPath(dualOutReal);
484
+ const tsbuildReal = cfg.compilerOptions?.tsBuildInfoFile
485
+ ? (0, node_path_1.resolve)(cfgDir, cfg.compilerOptions.tsBuildInfoFile)
486
+ : (0, node_path_1.join)(baseOut, 'tsconfig.tsbuildinfo');
487
+ const dualTsbuild = toShadowPath((0, node_path_1.join)((0, node_path_1.dirname)(tsbuildReal), 'tsconfig.dual.tsbuildinfo'));
488
+ const shadowReferences = mapReferencesToShadow(cfg.references ?? [], {
489
+ resolveRefPath: refPath => resolveReferenceConfigPath(cfgDir, refPath),
490
+ toShadowPathFn: toShadowPath,
491
+ fromDir: (0, node_path_1.dirname)(dest),
492
+ });
493
+ const patched = {
494
+ ...cfg,
495
+ references: shadowReferences,
496
+ compilerOptions: {
497
+ ...(cfg.compilerOptions ?? {}),
498
+ module: 'NodeNext',
499
+ moduleResolution: 'NodeNext',
500
+ outDir: dualOut,
501
+ incremental: cfg.compilerOptions?.incremental ?? true,
502
+ tsBuildInfoFile: dualTsbuild,
503
+ },
504
+ };
505
+ await (0, promises_1.writeFile)(dest, JSON.stringify(patched, null, 2));
363
506
  }
364
- }
507
+ };
365
508
  /**
366
509
  * Write dual package.json and tsconfig into temp dir; avoid mutating root package.json.
367
510
  */
368
- await (0, promises_1.writeFile)((0, node_path_1.join)(subDir, (0, node_path_1.relative)(projectRoot, pkg.path)), JSON.stringify({
369
- type: isCjsBuild ? 'commonjs' : 'module',
370
- }));
371
- await (0, promises_1.mkdir)(dualConfigDir, { recursive: true });
372
- await (0, promises_1.writeFile)(dualConfigPath, JSON.stringify({
373
- ...tsconfigDual,
374
- compilerOptions: {
375
- ...tsconfigDual.compilerOptions,
376
- outDir: absoluteDualOutDir,
377
- },
378
- }, null, 2));
511
+ await copyFilesToTemp();
512
+ await patchReferencedConfigs();
513
+ const writeDualPackage = async () => {
514
+ const pkgDest = (0, node_path_1.join)(subDir, (0, node_path_1.relative)(projectRoot, pkg.path));
515
+ await (0, promises_1.mkdir)((0, node_path_1.dirname)(pkgDest), { recursive: true });
516
+ await (0, promises_1.writeFile)(pkgDest, JSON.stringify({
517
+ name: pkg.packageJson?.name,
518
+ version: pkg.packageJson?.version,
519
+ type: isCjsBuild ? 'commonjs' : 'module',
520
+ exports: pkg.packageJson?.exports,
521
+ imports: pkg.packageJson?.imports,
522
+ main: pkg.packageJson?.main,
523
+ module: pkg.packageJson?.module,
524
+ types: pkg.packageJson?.types ?? pkg.packageJson?.typings,
525
+ typesVersions: pkg.packageJson?.typesVersions,
526
+ sideEffects: pkg.packageJson?.sideEffects,
527
+ }, null, 2));
528
+ };
529
+ const writeDualConfig = async () => {
530
+ await (0, promises_1.mkdir)(dualConfigDir, { recursive: true });
531
+ await (0, promises_1.writeFile)(dualConfigPath, JSON.stringify({
532
+ ...tsconfigDual,
533
+ compilerOptions: {
534
+ ...tsconfigDual.compilerOptions,
535
+ outDir: shadowDualOutDir,
536
+ incremental: true,
537
+ tsBuildInfoFile: dualTsBuildInfoFile,
538
+ },
539
+ }, null, 2));
540
+ };
541
+ await Promise.all([linkNodeModulesPromise, writeDualPackage(), writeDualConfig()]);
379
542
  if (modules) {
380
543
  /**
381
544
  * Transform ambiguous modules for the target dual build.
382
545
  * @see https://github.com/microsoft/TypeScript/issues/58658
383
546
  */
384
547
  const toTransform = await (0, glob_1.glob)(`${subDir.replace(/\\/g, '/')}/**/*{.js,.jsx,.ts,.tsx}`, {
385
- ignore: 'node_modules/**',
548
+ ignore: `${subDir.replace(/\\/g, '/')}/**/node_modules/**`,
386
549
  });
387
550
  let transformDiagnosticsError = false;
388
551
  for (const file of toTransform) {
552
+ if (file.split(/[/\\]/).includes('node_modules'))
553
+ continue;
389
554
  const isTsLike = /\.[cm]?tsx?$/.test(file);
390
555
  const transformSyntaxMode = syntaxMode === true && isTsLike ? 'globals-only' : syntaxMode;
391
556
  const diagnostics = [];
@@ -407,22 +572,16 @@ const duel = async (args) => {
407
572
  // Build dual
408
573
  (0, util_js_1.log)('Starting dual build...');
409
574
  try {
410
- await runBuild(dualConfigPath, hasReferences ? undefined : absoluteDualOutDir);
575
+ await refreshDualBuildInfo();
576
+ await runBuild(dualConfigPath, hasReferences ? undefined : shadowDualOutDir, hasReferences ? undefined : dualTsBuildInfoFile, subDir);
411
577
  }
412
578
  catch ({ message }) {
413
579
  success = false;
414
580
  errorMsg = message;
415
581
  }
416
- finally {
417
- const keepTemp = process.env.DUEL_KEEP_TEMP === '1';
418
- // Cleanup temp dir unless debugging is requested
419
- if (!keepTemp) {
420
- await (0, promises_1.rm)(dualConfigPath, { force: true });
421
- await (0, promises_1.rm)(subDir, { force: true, recursive: true });
422
- }
423
- else {
424
- (0, util_js_1.logWarn)(`DUEL_KEEP_TEMP=1 set; temp workspace preserved at ${subDir}`);
425
- }
582
+ if (!success) {
583
+ await cleanupTemp();
584
+ unregisterCleanupHandlers();
426
585
  if (errorMsg) {
427
586
  handleErrorAndExit(errorMsg);
428
587
  }
@@ -430,24 +589,41 @@ const duel = async (args) => {
430
589
  if (success) {
431
590
  const dualTarget = isCjsBuild ? 'commonjs' : 'module';
432
591
  const dualTargetExt = isCjsBuild ? '.cjs' : dirs ? '.js' : '.mjs';
433
- const filenames = await (0, glob_1.glob)(`${absoluteDualOutDir.replace(/\\/g, '/')}/**/*{.js,.d.ts}`, {
434
- ignore: 'node_modules/**',
592
+ await (0, promises_1.rm)(absoluteDualOutDir, { force: true, recursive: true });
593
+ await (0, promises_1.mkdir)((0, node_path_1.dirname)(absoluteDualOutDir), { recursive: true });
594
+ // Only copy if the shadow dual outDir was produced; absent indicates a failed emit
595
+ try {
596
+ await (0, promises_1.cp)(shadowDualOutDir, absoluteDualOutDir, { recursive: true });
597
+ }
598
+ catch (err) {
599
+ if (err?.code === 'ENOENT') {
600
+ throw new Error(`Dual build output not found at ${shadowDualOutDir}`);
601
+ }
602
+ throw err;
603
+ }
604
+ const dualGlob = dualTarget === 'commonjs' ? '**/*{.js,.cjs,.d.ts}' : '**/*{.js,.mjs,.d.ts}';
605
+ const filenames = await (0, glob_1.glob)(`${absoluteDualOutDir.replace(/\\/g, '/')}/${dualGlob}`, {
606
+ ignore: `${absoluteDualOutDir.replace(/\\/g, '/')}/**/node_modules/**`,
435
607
  });
608
+ const rewriteSyntaxMode = dualTarget === 'commonjs' ? true : syntaxMode;
436
609
  await (0, resolver_js_1.rewriteSpecifiersAndExtensions)(filenames, {
437
610
  target: dualTarget,
438
611
  ext: dualTargetExt,
439
- syntaxMode,
612
+ syntaxMode: rewriteSyntaxMode,
440
613
  rewritePolicy,
441
614
  validateSpecifiers,
442
615
  onWarn: message => (0, util_js_1.logWarn)(message),
443
616
  onRewrite: (from, to) => logVerbose(`Rewrote specifiers in ${from} -> ${to}`),
444
617
  });
445
618
  if (dirs && originalType === 'commonjs') {
446
- const primaryFiles = await (0, glob_1.glob)(`${primaryOutDir.replace(/\\/g, '/')}/**/*{.js,.d.ts}`, { ignore: 'node_modules/**' });
619
+ const primaryFiles = await (0, glob_1.glob)(`${primaryOutDir.replace(/\\/g, '/')}/**/*{.js,.cjs,.d.ts}`, {
620
+ ignore: `${primaryOutDir.replace(/\\/g, '/')}/**/node_modules/**`,
621
+ });
447
622
  await (0, resolver_js_1.rewriteSpecifiersAndExtensions)(primaryFiles, {
448
623
  target: 'commonjs',
449
624
  ext: '.cjs',
450
- syntaxMode,
625
+ // Always lower syntax for primary CJS output when dirs mode rewrites primary build.
626
+ syntaxMode: true,
451
627
  rewritePolicy,
452
628
  validateSpecifiers,
453
629
  onWarn: message => (0, util_js_1.logWarn)(message),
@@ -467,15 +643,33 @@ const duel = async (args) => {
467
643
  mainDefaultKind,
468
644
  mainPath,
469
645
  });
646
+ await cleanupTemp();
647
+ unregisterCleanupHandlers();
470
648
  logSuccess(startTime);
471
649
  }
472
650
  }
473
651
  }
474
652
  };
475
653
  exports.duel = duel;
476
- (async () => {
477
- const realFileUrlArgv1 = await (0, util_js_1.getRealPathAsFileUrl)(node_process_1.argv[1] ?? '');
478
- if (require("node:url").pathToFileURL(__filename).href === realFileUrlArgv1) {
479
- await duel();
654
+ const getCurrentHref = () => {
655
+ if (typeof module !== 'undefined' && require("node:url").pathToFileURL(__filename).href)
656
+ return require("node:url").pathToFileURL(__filename).href;
657
+ if (typeof module !== 'undefined' && module?.filename) {
658
+ return (0, node_url_1.pathToFileURL)(module.filename).href;
659
+ }
660
+ return null;
661
+ };
662
+ const runIfEntry = async () => {
663
+ try {
664
+ const realFileUrlArgv1 = await (0, util_js_1.getRealPathAsFileUrl)(node_process_1.argv[1] ?? '');
665
+ const currentHref = getCurrentHref();
666
+ if (currentHref && currentHref === realFileUrlArgv1) {
667
+ await duel();
668
+ }
480
669
  }
481
- })();
670
+ catch (err) {
671
+ (0, util_js_1.logError)(err?.message ?? err);
672
+ process.exit(1);
673
+ }
674
+ };
675
+ runIfEntry();
package/dist/cjs/init.cjs CHANGED
@@ -189,7 +189,8 @@ const init = async (args) => {
189
189
  (0, util_js_1.logError)(`Provided --project '${project}' resolves to ${configPath} which is not a file or directory.`);
190
190
  return false;
191
191
  }
192
- pkg = await (0, read_package_up_1.readPackageUp)({ cwd: pkgDir ?? configPath });
192
+ const pkgSearchCwd = pkgDir ?? (stats.isDirectory() ? configPath : (0, node_path_1.dirname)(configPath));
193
+ pkg = await (0, read_package_up_1.readPackageUp)({ cwd: pkgSearchCwd });
193
194
  if (!pkg) {
194
195
  (0, util_js_1.logError)('No package.json file found.');
195
196
  return false;
@@ -1,18 +1,27 @@
1
1
  export function init(args: any): Promise<false | {
2
- pkg: any;
3
- dirs: any;
2
+ pkg: import("read-package-up", { with: { "resolution-mode": "import" } }).NormalizedReadResult;
3
+ dirs: boolean;
4
4
  modules: boolean;
5
5
  transformSyntax: boolean;
6
- exports: any;
7
- exportsConfig: any;
8
- exportsValidate: any;
9
- rewritePolicy: any;
10
- validateSpecifiers: any;
11
- detectDualPackageHazard: any;
12
- dualPackageHazardScope: any;
13
- verbose: any;
14
- copyMode: any;
15
- tsconfig: any;
16
- projectDir: any;
17
- configPath: any;
6
+ exports: string | undefined;
7
+ exportsConfig: string | undefined;
8
+ exportsValidate: boolean;
9
+ rewritePolicy: string;
10
+ validateSpecifiers: boolean;
11
+ detectDualPackageHazard: string;
12
+ dualPackageHazardScope: string;
13
+ verbose: boolean;
14
+ copyMode: string;
15
+ tsconfig: {
16
+ compilerOptions?: import("get-tsconfig").TsConfigJson.CompilerOptions | undefined;
17
+ watchOptions?: import("get-tsconfig").TsConfigJson.WatchOptions | undefined;
18
+ typeAcquisition?: import("get-tsconfig").TsConfigJson.TypeAcquisition | undefined;
19
+ compileOnSave?: boolean | undefined;
20
+ files?: string[] | undefined;
21
+ exclude?: string[] | undefined;
22
+ include?: string[] | undefined;
23
+ references?: import("get-tsconfig").TsConfigJson.References[] | undefined;
24
+ };
25
+ projectDir: string;
26
+ configPath: string;
18
27
  }>;