@knighted/duel 3.1.1 → 3.2.0

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
@@ -71,16 +71,14 @@ Assuming an `outDir` of `dist`, running the above will create `dist/esm` and `di
71
71
 
72
72
  ### Module transforms
73
73
 
74
- TypeScript will throw compiler errors when using `import.meta` globals while targeting a CommonJS dual build, but _will not_ throw compiler errors when the inverse is true, i.e. using CommonJS globals (`__filename`, `__dirname`, etc.) while targeting an ES module dual build. There is an [open issue](https://github.com/microsoft/TypeScript/issues/58658) regarding this unexpected behavior. You can use the `--modules` option to have the [differences between ES modules and CommonJS](https://nodejs.org/api/esm.html#differences-between-es-modules-and-commonjs) transformed by `duel` prior to running compilation with `tsc` so that there are no compilation or runtime errors.
74
+ TypeScript will throw compiler errors when using `import.meta` globals while targeting a CommonJS dual build, but _will not_ throw compiler errors when the inverse is true, i.e. using CommonJS globals (`__filename`, `__dirname`, etc.) while targeting an ES module dual build. There is an [open issue](https://github.com/microsoft/TypeScript/issues/58658) regarding this asymmetry. Prefer the single-switch `--mode` interface: `--mode globals` (equivalent to `--modules`) or `--mode full` (equivalent to `--modules --transform-syntax`) to have the [ESM vs CJS differences](https://nodejs.org/api/esm.html#differences-between-es-modules-and-commonjs) transformed by `duel` prior to running `tsc` so you avoid compilation or runtime errors. The legacy `--modules`/`--transform-syntax` flags remain supported.
75
75
 
76
76
  `duel` infers the primary vs dual build orientation from your `package.json` `type`:
77
77
 
78
78
  - `"type": "module"` → primary ESM, dual CJS
79
79
  - `"type": "commonjs"` → primary CJS, dual ESM
80
80
 
81
- The `--dirs` flag nests outputs under `outDir/esm` and `outDir/cjs` accordingly.
82
-
83
- Note, there is a slight performance penalty since your project needs to be copied first to run the transforms before compiling with `tsc`.
81
+ The `--dirs` flag nests outputs under `outDir/esm` and `outDir/cjs` accordingly. There is a small performance cost because sources are copied to run the transform before `tsc`.
84
82
 
85
83
  ```json
86
84
  "scripts": {
@@ -88,11 +86,31 @@ Note, there is a slight performance penalty since your project needs to be copie
88
86
  }
89
87
  ```
90
88
 
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)).
89
+ For projects that need full syntax lowering, opt in explicitly:
90
+
91
+ ```json
92
+ "scripts": {
93
+ "build": "duel --modules --transform-syntax"
94
+ }
95
+ ```
96
+
97
+ Using the single switch:
98
+
99
+ ```json
100
+ "scripts": {
101
+ "build": "duel --mode globals"
102
+ }
103
+ ```
104
+
105
+ ```json
106
+ "scripts": {
107
+ "build": "duel --mode full"
108
+ }
109
+ ```
92
110
 
93
111
  #### Pre-`tsc` transform (TypeScript 58658)
94
112
 
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.
113
+ When you enable module transforms (`--mode globals`, `--mode full`, or `--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: module transforms are the pre-`tsc` mitigation. If you also select full lowering (`--mode full` or `--transform-syntax`), that pre-`tsc` step performs full lowering instead of globals-only.
96
114
 
97
115
  ## Options
98
116
 
@@ -100,25 +118,16 @@ The available options are limited, because you should define most of them inside
100
118
 
101
119
  - `--project, -p` The path to the project's configuration file. Defaults to `tsconfig.json`.
102
120
  - `--pkg-dir, -k` The directory to start looking for a package.json file. Defaults to `--project` dir.
121
+ - `--mode` Optional shorthand for the module transform mode: `none` (default), `globals` (modules + globals-only), `full` (modules + full syntax lowering). Recommended.
103
122
  - `--modules, -m` Transform module globals for dual build target. Defaults to false.
123
+ - `--transform-syntax, -s` Opt in to full syntax lowering via `@knighted/module` (default is globals-only). Implies `--modules`.
104
124
  - `--dirs, -d` Outputs both builds to directories inside of `outDir`. Defaults to `false`.
105
125
  - `--exports, -e` Generate `package.json` `exports` from build output. Values: `wildcard` | `dir` | `name`.
106
126
 
107
127
  > [!NOTE]
108
128
  > Exports keys are extensionless by design; the target `import`/`require`/`types` entries keep explicit file extensions so Node resolution remains deterministic.
109
129
 
110
- You can run `duel --help` to get the same info. Below is the output of that:
111
-
112
- ```console
113
- Usage: duel [options]
114
-
115
- Options:
116
- --project, -p [path] Compile the project given the path to its configuration file, or to a folder with a 'tsconfig.json'.
117
- --pkg-dir, -k [path] The directory to start looking for a package.json file. Defaults to --project directory.
118
- --modules, -m Transform module globals for dual build target. Defaults to false.
119
- --dirs, -d Output both builds to directories inside of outDir. [esm, cjs].
120
- --help, -h Print this message.
121
- ```
130
+ You can run `duel --help` to get the same info.
122
131
 
123
132
  ## Gotchas
124
133
 
package/dist/cjs/duel.cjs CHANGED
@@ -184,7 +184,7 @@ const generateExports = async (options) => {
184
184
  const duel = async (args) => {
185
185
  const ctx = await (0, init_js_1.init)(args);
186
186
  if (ctx) {
187
- const { projectDir, tsconfig, configPath, modules, dirs, pkg, exports: exportsOpt, } = ctx;
187
+ const { projectDir, tsconfig, configPath, modules, dirs, transformSyntax, pkg, exports: exportsOpt, } = ctx;
188
188
  const tsc = await (0, find_up_1.findUp)(async (dir) => {
189
189
  const tscBin = (0, node_path_1.join)(dir, 'node_modules', '.bin', 'tsc');
190
190
  try {
@@ -233,6 +233,7 @@ const duel = async (args) => {
233
233
  const runPrimaryBuild = () => {
234
234
  return runBuild(configPath, primaryOutDir);
235
235
  };
236
+ const syntaxMode = transformSyntax ? true : 'globals-only';
236
237
  const updateSpecifiersAndFileExtensions = async (filenames, target, ext) => {
237
238
  for (const filename of filenames) {
238
239
  const dts = /(\.d\.ts)$/;
@@ -258,7 +259,7 @@ const duel = async (args) => {
258
259
  const writeOptions = {
259
260
  target,
260
261
  rewriteSpecifier,
261
- transformSyntax: 'globals-only',
262
+ transformSyntax: syntaxMode,
262
263
  ...(outFilename === filename ? { inPlace: true } : { out: outFilename }),
263
264
  };
264
265
  await (0, module_1.transform)(filename, writeOptions);
@@ -304,18 +305,12 @@ const duel = async (args) => {
304
305
  ignore: 'node_modules/**',
305
306
  });
306
307
  for (const file of toTransform) {
307
- /**
308
- * Maybe include the option to transform modules implicitly
309
- * (modules: true) so that `exports` are correctly converted
310
- * when targeting a CJS dual build. Depends on @knighted/module
311
- * supporting he `modules` option.
312
- *
313
- * @see https://github.com/microsoft/TypeScript/issues/58658
314
- */
308
+ const isTsLike = /\.[cm]?tsx?$/.test(file);
309
+ const transformSyntaxMode = syntaxMode === true && isTsLike ? 'globals-only' : syntaxMode;
315
310
  await (0, module_1.transform)(file, {
316
311
  out: file,
317
312
  target: isCjsBuild ? 'commonjs' : 'module',
318
- transformSyntax: 'globals-only',
313
+ transformSyntax: transformSyntaxMode,
319
314
  });
320
315
  }
321
316
  }
package/dist/cjs/init.cjs CHANGED
@@ -41,6 +41,14 @@ const init = async (args) => {
41
41
  type: 'string',
42
42
  short: 'e',
43
43
  },
44
+ 'transform-syntax': {
45
+ type: 'boolean',
46
+ short: 's',
47
+ default: false,
48
+ },
49
+ mode: {
50
+ type: 'string',
51
+ },
44
52
  help: {
45
53
  type: 'boolean',
46
54
  short: 'h',
@@ -58,18 +66,24 @@ const init = async (args) => {
58
66
  const bare = { bare: true };
59
67
  (0, util_js_1.log)('Usage: duel [options]\n', 'info', bare);
60
68
  (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);
69
+ (0, util_js_1.log)("--project, -p [path] \t\t Compile the project given the path to its configuration file, or to a folder with a 'tsconfig.json'.", 'info', bare);
70
+ (0, util_js_1.log)('--pkg-dir, -k [path] \t\t The directory to start looking for a package.json file. Defaults to --project directory.', 'info', bare);
71
+ (0, util_js_1.log)('--modules, -m \t\t\t Transform module globals for dual build target. Defaults to false.', 'info', bare);
72
+ (0, util_js_1.log)('--dirs, -d \t\t\t Output both builds to directories inside of outDir. [esm, cjs].', 'info', bare);
73
+ (0, util_js_1.log)('--exports, -e \t\t\t Generate package.json exports. Values: wildcard | dir | name.', 'info', bare);
74
+ (0, util_js_1.log)('--transform-syntax, -s \t\t Opt in to full syntax lowering via @knighted/module (default is globals-only).', 'info', bare);
75
+ (0, util_js_1.log)('--mode [none|globals|full] \t Optional shorthand for module transforms and syntax lowering.', 'info', bare);
76
+ (0, util_js_1.log)('--help, -h \t\t\t Print this message.', 'info', bare);
67
77
  }
68
78
  else {
69
- const { project, 'target-extension': targetExt, 'pkg-dir': pkgDir, modules, dirs, exports: exportsOpt, } = parsed;
79
+ const { project, 'target-extension': targetExt, 'pkg-dir': pkgDir, modules, dirs, exports: exportsOpt, 'transform-syntax': transformSyntax, mode, } = parsed;
70
80
  let configPath = (0, node_path_1.resolve)(project);
71
81
  let stats = null;
72
82
  let pkg = null;
83
+ if (mode && !['none', 'globals', 'full'].includes(mode)) {
84
+ (0, util_js_1.logError)('--mode expects one of: none | globals | full');
85
+ return false;
86
+ }
73
87
  if (targetExt) {
74
88
  (0, util_js_1.logError)('--target-extension is deprecated. Define "type" in your package.json instead and the dual build will be inferred from that.');
75
89
  return false;
@@ -106,10 +120,30 @@ const init = async (args) => {
106
120
  (0, util_js_1.logError)('--exports expects one of: wildcard | dir | name');
107
121
  return false;
108
122
  }
123
+ let modulesFinal = modules;
124
+ let transformSyntaxFinal = transformSyntax;
125
+ if (mode) {
126
+ if (mode === 'none') {
127
+ modulesFinal = false;
128
+ transformSyntaxFinal = false;
129
+ }
130
+ else if (mode === 'globals') {
131
+ modulesFinal = true;
132
+ transformSyntaxFinal = false;
133
+ }
134
+ else if (mode === 'full') {
135
+ modulesFinal = true;
136
+ transformSyntaxFinal = true;
137
+ }
138
+ }
139
+ else if (transformSyntax && !modules) {
140
+ modulesFinal = true;
141
+ }
109
142
  return {
110
143
  pkg,
111
144
  dirs,
112
- modules,
145
+ modules: modulesFinal,
146
+ transformSyntax: transformSyntaxFinal,
113
147
  exports: exportsOpt,
114
148
  tsconfig,
115
149
  projectDir,
@@ -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
+ transformSyntax: boolean;
5
6
  exports: string | undefined;
6
7
  tsconfig: {
7
8
  compilerOptions?: import("get-tsconfig").TsConfigJson.CompilerOptions | undefined;
package/dist/esm/duel.js CHANGED
@@ -181,7 +181,7 @@ const generateExports = async (options) => {
181
181
  const duel = async (args) => {
182
182
  const ctx = await init(args);
183
183
  if (ctx) {
184
- const { projectDir, tsconfig, configPath, modules, dirs, pkg, exports: exportsOpt, } = ctx;
184
+ const { projectDir, tsconfig, configPath, modules, dirs, transformSyntax, pkg, exports: exportsOpt, } = ctx;
185
185
  const tsc = await findUp(async (dir) => {
186
186
  const tscBin = join(dir, 'node_modules', '.bin', 'tsc');
187
187
  try {
@@ -230,6 +230,7 @@ const duel = async (args) => {
230
230
  const runPrimaryBuild = () => {
231
231
  return runBuild(configPath, primaryOutDir);
232
232
  };
233
+ const syntaxMode = transformSyntax ? true : 'globals-only';
233
234
  const updateSpecifiersAndFileExtensions = async (filenames, target, ext) => {
234
235
  for (const filename of filenames) {
235
236
  const dts = /(\.d\.ts)$/;
@@ -255,7 +256,7 @@ const duel = async (args) => {
255
256
  const writeOptions = {
256
257
  target,
257
258
  rewriteSpecifier,
258
- transformSyntax: 'globals-only',
259
+ transformSyntax: syntaxMode,
259
260
  ...(outFilename === filename ? { inPlace: true } : { out: outFilename }),
260
261
  };
261
262
  await transform(filename, writeOptions);
@@ -301,18 +302,12 @@ const duel = async (args) => {
301
302
  ignore: 'node_modules/**',
302
303
  });
303
304
  for (const file of toTransform) {
304
- /**
305
- * Maybe include the option to transform modules implicitly
306
- * (modules: true) so that `exports` are correctly converted
307
- * when targeting a CJS dual build. Depends on @knighted/module
308
- * supporting he `modules` option.
309
- *
310
- * @see https://github.com/microsoft/TypeScript/issues/58658
311
- */
305
+ const isTsLike = /\.[cm]?tsx?$/.test(file);
306
+ const transformSyntaxMode = syntaxMode === true && isTsLike ? 'globals-only' : syntaxMode;
312
307
  await transform(file, {
313
308
  out: file,
314
309
  target: isCjsBuild ? 'commonjs' : 'module',
315
- transformSyntax: 'globals-only',
310
+ transformSyntax: transformSyntaxMode,
316
311
  });
317
312
  }
318
313
  }
@@ -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
+ transformSyntax: boolean;
5
6
  exports: string | undefined;
6
7
  tsconfig: {
7
8
  compilerOptions?: import("get-tsconfig").TsConfigJson.CompilerOptions | undefined;
package/dist/esm/init.js CHANGED
@@ -38,6 +38,14 @@ const init = async (args) => {
38
38
  type: 'string',
39
39
  short: 'e',
40
40
  },
41
+ 'transform-syntax': {
42
+ type: 'boolean',
43
+ short: 's',
44
+ default: false,
45
+ },
46
+ mode: {
47
+ type: 'string',
48
+ },
41
49
  help: {
42
50
  type: 'boolean',
43
51
  short: 'h',
@@ -55,18 +63,24 @@ const init = async (args) => {
55
63
  const bare = { bare: true };
56
64
  log('Usage: duel [options]\n', 'info', bare);
57
65
  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);
66
+ log("--project, -p [path] \t\t Compile the project given the path to its configuration file, or to a folder with a 'tsconfig.json'.", 'info', bare);
67
+ log('--pkg-dir, -k [path] \t\t The directory to start looking for a package.json file. Defaults to --project directory.', 'info', bare);
68
+ log('--modules, -m \t\t\t Transform module globals for dual build target. Defaults to false.', 'info', bare);
69
+ log('--dirs, -d \t\t\t Output both builds to directories inside of outDir. [esm, cjs].', 'info', bare);
70
+ log('--exports, -e \t\t\t Generate package.json exports. Values: wildcard | dir | name.', 'info', bare);
71
+ log('--transform-syntax, -s \t\t Opt in to full syntax lowering via @knighted/module (default is globals-only).', 'info', bare);
72
+ log('--mode [none|globals|full] \t Optional shorthand for module transforms and syntax lowering.', 'info', bare);
73
+ log('--help, -h \t\t\t Print this message.', 'info', bare);
64
74
  }
65
75
  else {
66
- const { project, 'target-extension': targetExt, 'pkg-dir': pkgDir, modules, dirs, exports: exportsOpt, } = parsed;
76
+ const { project, 'target-extension': targetExt, 'pkg-dir': pkgDir, modules, dirs, exports: exportsOpt, 'transform-syntax': transformSyntax, mode, } = parsed;
67
77
  let configPath = resolve(project);
68
78
  let stats = null;
69
79
  let pkg = null;
80
+ if (mode && !['none', 'globals', 'full'].includes(mode)) {
81
+ logError('--mode expects one of: none | globals | full');
82
+ return false;
83
+ }
70
84
  if (targetExt) {
71
85
  logError('--target-extension is deprecated. Define "type" in your package.json instead and the dual build will be inferred from that.');
72
86
  return false;
@@ -103,10 +117,30 @@ const init = async (args) => {
103
117
  logError('--exports expects one of: wildcard | dir | name');
104
118
  return false;
105
119
  }
120
+ let modulesFinal = modules;
121
+ let transformSyntaxFinal = transformSyntax;
122
+ if (mode) {
123
+ if (mode === 'none') {
124
+ modulesFinal = false;
125
+ transformSyntaxFinal = false;
126
+ }
127
+ else if (mode === 'globals') {
128
+ modulesFinal = true;
129
+ transformSyntaxFinal = false;
130
+ }
131
+ else if (mode === 'full') {
132
+ modulesFinal = true;
133
+ transformSyntaxFinal = true;
134
+ }
135
+ }
136
+ else if (transformSyntax && !modules) {
137
+ modulesFinal = true;
138
+ }
106
139
  return {
107
140
  pkg,
108
141
  dirs,
109
- modules,
142
+ modules: modulesFinal,
143
+ transformSyntax: transformSyntaxFinal,
110
144
  exports: exportsOpt,
111
145
  tsconfig,
112
146
  projectDir,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@knighted/duel",
3
- "version": "3.1.1",
3
+ "version": "3.2.0",
4
4
  "description": "TypeScript dual packages.",
5
5
  "type": "module",
6
6
  "main": "dist/esm/duel.js",