@knighted/duel 1.0.0-alpha.2 → 1.0.0-alpha.4

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,22 +6,23 @@
6
6
 
7
7
  Node.js tool for creating a TypeScript dual package.
8
8
 
9
- Early stages of development. Inspired by https://github.com/microsoft/TypeScript/issues/49462.
9
+ Inspired by https://github.com/microsoft/TypeScript/issues/49462.
10
10
 
11
11
  ## Requirements
12
12
 
13
13
  * Node >= 16.19.0.
14
14
  * TypeScript, `npm i typescript`.
15
+ * A `tsconfig.json` with `outDir` defined.
15
16
 
16
17
  ## Example
17
18
 
18
- First, install the package to create the `duel` executable inside your `node_modules/.bin` directory.
19
+ First, install this package to create the `duel` executable inside your `node_modules/.bin` directory.
19
20
 
20
21
  ```console
21
22
  user@comp ~ $ npm i @knighted/duel
22
23
  ```
23
24
 
24
- Then, given a `package.json` that defines `"type": "module"` and a `tsconfig.json` file that looks like the following:
25
+ Then, given a `package.json` that defines `"type": "module"` and a `tsconfig.json` file that looks something like the following:
25
26
 
26
27
  ```json
27
28
  {
@@ -51,9 +52,9 @@ And then running it:
51
52
  user@comp ~ $ npm run build
52
53
  ```
53
54
 
54
- If everything worked, you should have an ESM build inside of `dist` and a CJS build inside of `dist/cjs`. Now you can update your `exports` in package.json to match the build output.
55
+ If everything worked, you should have an ESM build inside of `dist` and a CJS build inside of `dist/cjs`. Now you can update your [`exports`](https://nodejs.org/api/packages.html#exports) to match the build output.
55
56
 
56
- It should work similarly for a CJS first project. Except, your `tsconfig.json` would define `--module` and `--moduleResolution` differently, and you'd want to pass `-x .mjs`.
57
+ It should work similarly for a CJS first project. Except, your `tsconfig.json` would define `--module` and `--moduleResolution` differently, and you'd want to pass `--target-extension .mjs`.
57
58
 
58
59
  See the available [options](#options).
59
60
 
@@ -71,13 +72,15 @@ You can run `duel --help` to get more info. Below is the output of that:
71
72
  Usage: duel [options]
72
73
 
73
74
  Options:
74
- --project, -p Compile the project given the path to its configuration file, or to a folder with a 'tsconfig.json'.
75
- --target-extension, -x Sets the file extension for the dual build. [.cjs,.mjs]
76
- --help, -h Print this message.
75
+ --project, -p Compile the project given the path to its configuration file, or to a folder with a 'tsconfig.json'.
76
+ --target-extension, -x Sets the file extension for the dual build. [.cjs,.mjs]
77
+ --help, -h Print this message.
77
78
  ```
78
79
 
79
80
  ## Gotchas
80
81
 
81
- * Unfortunately, TypeScript doesn't really understand dual packages very well. For instance, it will **always** create CJS exports when `--module commonjs` is used, even on files with an `.mts` extension. One reference issue is https://github.com/microsoft/TypeScript/issues/54573. If you use `.mts` extensions to enforce an ESM module system, this might break in the corresponding dual CJS build.
82
- * If targeting a dual CJS build, and you are using [top level `await`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await#top_level_await), you will most likely encounter the compilation error `error TS1378: Top-level 'await' expressions are only allowed when the 'module' option is set to 'es2022', 'esnext', 'system', 'node16', or 'nodenext', and the 'target' option is set to 'es2017' or higher.` during the CJS build. This is because `duel` creates a temporary `tsconfig.json` from your original and overwrites the `--module` and `--moduleResolution` based on the provided `--target-ext`.
82
+ These are definitely edge cases, and would only really come up if your project mixes file extensions. For example, if you have `.ts` files combined with `.mts`, and/or `.cts`. For most project, things should just work as expected.
83
+
84
+ * Unfortunately, TypeScript doesn't really build [dual packages](https://nodejs.org/api/packages.html#dual-commonjses-module-packages) very well in regards to preserving module system by file extension. For instance, there doesn't appear to be a way to convert an arbitrary `.ts` file into another module system, _while also preserving the module system of `.mts` and `.cts` files_. In my opinion, the `tsc` compiler is fundamentally broken in this regard, and at best is enforcing usage patterns it shouldn't. If you want to see one of my extended rants on this, check out this [comment](https://github.com/microsoft/TypeScript/pull/50985#issuecomment-1656991606). This is only mentioned for transparency, `duel` will correct for this and produce files with the module system you would expect based on the files extension, so that it works with [how Node.js determines module systems](https://nodejs.org/api/packages.html#determining-module-system).
85
+
83
86
  * If doing an `import type` across module systems, i.e. from `.mts` into `.cts`, or vice versa, you might encounter the compilation error ``error TS1452: 'resolution-mode' assertions are only supported when `moduleResolution` is `node16` or `nodenext`.``. This is a [known issue](https://github.com/microsoft/TypeScript/issues/49055) and TypeScript currently suggests installing the nightly build, i.e. `npm i typescript@next`.
package/dist/duel.cjs CHANGED
@@ -15,7 +15,6 @@ var _glob = require("glob");
15
15
  var _specifier = require("@knighted/specifier");
16
16
  var _init = require("./init.cjs");
17
17
  var _util = require("./util.cjs");
18
- // TypeScript is defined as a peer dependency.
19
18
  const tsc = (0, _nodePath.join)((0, _nodeProcess.cwd)(), 'node_modules', '.bin', 'tsc');
20
19
  const runBuild = project => {
21
20
  const {
@@ -45,7 +44,8 @@ const duel = async args => {
45
44
  projectDir,
46
45
  tsconfig,
47
46
  targetExt,
48
- configPath
47
+ configPath,
48
+ absoluteOutDir
49
49
  } = ctx;
50
50
  const startTime = _nodePerf_hooks.performance.now();
51
51
  (0, _util.log)('Starting primary build...\n');
@@ -58,13 +58,15 @@ const duel = async args => {
58
58
  } = tsconfig.compilerOptions;
59
59
  const dualConfigPath = (0, _nodePath.join)(projectDir, `tsconfig.${hex}.json`);
60
60
  const dualOutDir = isCjsBuild ? (0, _nodePath.join)(outDir, 'cjs') : (0, _nodePath.join)(outDir, 'mjs');
61
+ // Using structuredClone() would require node >= 17.0.0
61
62
  const tsconfigDual = {
62
63
  ...tsconfig,
63
64
  compilerOptions: {
64
65
  ...tsconfig.compilerOptions,
65
66
  outDir: dualOutDir,
66
- module: isCjsBuild ? 'CommonJS' : 'NodeNext',
67
- moduleResolution: isCjsBuild ? 'Node' : 'NodeNext'
67
+ module: isCjsBuild ? 'CommonJS' : 'ESNext',
68
+ // Best way to make this work given how tsc works
69
+ moduleResolution: 'Node'
68
70
  }
69
71
  };
70
72
  await (0, _promises.writeFile)(dualConfigPath, JSON.stringify(tsconfigDual, null, 2));
@@ -74,7 +76,8 @@ const duel = async args => {
74
76
  force: true
75
77
  });
76
78
  if (success) {
77
- const filenames = await (0, _glob.glob)(`${(0, _nodePath.join)(projectDir, dualOutDir)}/**/*{.js,.d.ts}`, {
79
+ const absoluteDualOutDir = (0, _nodePath.join)(projectDir, dualOutDir);
80
+ const filenames = await (0, _glob.glob)(`${absoluteDualOutDir}/**/*{.js,.d.ts}`, {
78
81
  ignore: 'node_modules/**'
79
82
  });
80
83
  for (const filename of filenames) {
@@ -96,6 +99,49 @@ const duel = async args => {
96
99
  force: true
97
100
  });
98
101
  }
102
+
103
+ /**
104
+ * This is a fix for tsc compiler which doesn't seem to support
105
+ * converting an arbitrary `.ts` file, into another module system,
106
+ * while also preserving the module systems of `.mts` and `.cts` files.
107
+ *
108
+ * Hopefully it can be removed when TS updates their supported options,
109
+ * or at least how the combination of `--module` and `--moduleResolution`
110
+ * currently work.
111
+ *
112
+ * @see https://github.com/microsoft/TypeScript/pull/50985#issuecomment-1656991606
113
+ */
114
+ if (isCjsBuild) {
115
+ const mjsFiles = await (0, _glob.glob)(`${absoluteOutDir}/**/*.mjs`, {
116
+ ignore: ['node_modules/**', `${absoluteDualOutDir}/**`]
117
+ });
118
+ for (const filename of mjsFiles) {
119
+ const relativeFn = (0, _nodePath.relative)(absoluteOutDir, filename);
120
+ await (0, _promises.copyFile)(filename, (0, _nodePath.join)(absoluteDualOutDir, relativeFn));
121
+ }
122
+ } else {
123
+ const cjsFiles = await (0, _glob.glob)(`${absoluteOutDir}/**/*.cjs`, {
124
+ ignore: ['node_modules/**', `${absoluteDualOutDir}/**`]
125
+ });
126
+ for (const filename of cjsFiles) {
127
+ const relativeFn = (0, _nodePath.relative)(absoluteOutDir, filename);
128
+ await (0, _promises.copyFile)(filename, (0, _nodePath.join)(absoluteDualOutDir, relativeFn));
129
+ }
130
+
131
+ /**
132
+ * Now copy the good .mjs files from the dual out dir
133
+ * to the original out dir, but build the file path
134
+ * from the original out dir to distinguish from the
135
+ * dual build .mjs files.
136
+ */
137
+ const mjsFiles = await (0, _glob.glob)(`${absoluteOutDir}/**/*.mjs`, {
138
+ ignore: ['node_modules/**', `${absoluteDualOutDir}/**`]
139
+ });
140
+ for (const filename of mjsFiles) {
141
+ const relativeFn = (0, _nodePath.relative)(absoluteOutDir, filename);
142
+ await (0, _promises.copyFile)((0, _nodePath.join)(absoluteDualOutDir, relativeFn), filename);
143
+ }
144
+ }
99
145
  (0, _util.log)(`Successfully created a dual ${targetExt.replace('.', '').toUpperCase()} build in ${Math.round(_nodePerf_hooks.performance.now() - startTime)}ms.`);
100
146
  }
101
147
  }
package/dist/duel.js CHANGED
@@ -1,16 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import { argv, cwd } from 'node:process';
3
- import { join } from 'node:path';
3
+ import { join, relative } from 'node:path';
4
4
  import { spawnSync } from 'node:child_process';
5
- import { writeFile, rm } from 'node:fs/promises';
5
+ import { writeFile, copyFile, rm } from 'node:fs/promises';
6
6
  import { randomBytes } from 'node:crypto';
7
7
  import { performance } from 'node:perf_hooks';
8
8
  import { glob } from 'glob';
9
9
  import { specifier } from '@knighted/specifier';
10
10
  import { init } from './init.js';
11
11
  import { getRealPathAsFileUrl, logError, log } from './util.js';
12
-
13
- // TypeScript is defined as a peer dependency.
14
12
  const tsc = join(cwd(), 'node_modules', '.bin', 'tsc');
15
13
  const runBuild = project => {
16
14
  const {
@@ -40,7 +38,8 @@ const duel = async args => {
40
38
  projectDir,
41
39
  tsconfig,
42
40
  targetExt,
43
- configPath
41
+ configPath,
42
+ absoluteOutDir
44
43
  } = ctx;
45
44
  const startTime = performance.now();
46
45
  log('Starting primary build...\n');
@@ -53,13 +52,15 @@ const duel = async args => {
53
52
  } = tsconfig.compilerOptions;
54
53
  const dualConfigPath = join(projectDir, `tsconfig.${hex}.json`);
55
54
  const dualOutDir = isCjsBuild ? join(outDir, 'cjs') : join(outDir, 'mjs');
55
+ // Using structuredClone() would require node >= 17.0.0
56
56
  const tsconfigDual = {
57
57
  ...tsconfig,
58
58
  compilerOptions: {
59
59
  ...tsconfig.compilerOptions,
60
60
  outDir: dualOutDir,
61
- module: isCjsBuild ? 'CommonJS' : 'NodeNext',
62
- moduleResolution: isCjsBuild ? 'Node' : 'NodeNext'
61
+ module: isCjsBuild ? 'CommonJS' : 'ESNext',
62
+ // Best way to make this work given how tsc works
63
+ moduleResolution: 'Node'
63
64
  }
64
65
  };
65
66
  await writeFile(dualConfigPath, JSON.stringify(tsconfigDual, null, 2));
@@ -69,7 +70,8 @@ const duel = async args => {
69
70
  force: true
70
71
  });
71
72
  if (success) {
72
- const filenames = await glob(`${join(projectDir, dualOutDir)}/**/*{.js,.d.ts}`, {
73
+ const absoluteDualOutDir = join(projectDir, dualOutDir);
74
+ const filenames = await glob(`${absoluteDualOutDir}/**/*{.js,.d.ts}`, {
73
75
  ignore: 'node_modules/**'
74
76
  });
75
77
  for (const filename of filenames) {
@@ -91,6 +93,49 @@ const duel = async args => {
91
93
  force: true
92
94
  });
93
95
  }
96
+
97
+ /**
98
+ * This is a fix for tsc compiler which doesn't seem to support
99
+ * converting an arbitrary `.ts` file, into another module system,
100
+ * while also preserving the module systems of `.mts` and `.cts` files.
101
+ *
102
+ * Hopefully it can be removed when TS updates their supported options,
103
+ * or at least how the combination of `--module` and `--moduleResolution`
104
+ * currently work.
105
+ *
106
+ * @see https://github.com/microsoft/TypeScript/pull/50985#issuecomment-1656991606
107
+ */
108
+ if (isCjsBuild) {
109
+ const mjsFiles = await glob(`${absoluteOutDir}/**/*.mjs`, {
110
+ ignore: ['node_modules/**', `${absoluteDualOutDir}/**`]
111
+ });
112
+ for (const filename of mjsFiles) {
113
+ const relativeFn = relative(absoluteOutDir, filename);
114
+ await copyFile(filename, join(absoluteDualOutDir, relativeFn));
115
+ }
116
+ } else {
117
+ const cjsFiles = await glob(`${absoluteOutDir}/**/*.cjs`, {
118
+ ignore: ['node_modules/**', `${absoluteDualOutDir}/**`]
119
+ });
120
+ for (const filename of cjsFiles) {
121
+ const relativeFn = relative(absoluteOutDir, filename);
122
+ await copyFile(filename, join(absoluteDualOutDir, relativeFn));
123
+ }
124
+
125
+ /**
126
+ * Now copy the good .mjs files from the dual out dir
127
+ * to the original out dir, but build the file path
128
+ * from the original out dir to distinguish from the
129
+ * dual build .mjs files.
130
+ */
131
+ const mjsFiles = await glob(`${absoluteOutDir}/**/*.mjs`, {
132
+ ignore: ['node_modules/**', `${absoluteDualOutDir}/**`]
133
+ });
134
+ for (const filename of mjsFiles) {
135
+ const relativeFn = relative(absoluteOutDir, filename);
136
+ await copyFile(join(absoluteDualOutDir, relativeFn), filename);
137
+ }
138
+ }
94
139
  log(`Successfully created a dual ${targetExt.replace('.', '').toUpperCase()} build in ${Math.round(performance.now() - startTime)}ms.`);
95
140
  }
96
141
  }
package/dist/init.cjs CHANGED
@@ -42,9 +42,9 @@ const init = async args => {
42
42
  if (parsed.help) {
43
43
  (0, _util.log)('Usage: duel [options]\n');
44
44
  (0, _util.log)('Options:');
45
- (0, _util.log)("--project, -p \t\t\t Compile the project given the path to its configuration file, or to a folder with a 'tsconfig.json'.");
46
- (0, _util.log)('--target-extension, -x \t\t Sets the file extension for the dual build. [.cjs,.mjs]');
47
- (0, _util.log)('--help, -h \t\t\t Print this message.');
45
+ (0, _util.log)("--project, -p \t\t Compile the project given the path to its configuration file, or to a folder with a 'tsconfig.json'.");
46
+ (0, _util.log)('--target-extension, -x \t Sets the file extension for the dual build. [.cjs,.mjs]');
47
+ (0, _util.log)('--help, -h \t\t Print this message.');
48
48
  } else {
49
49
  const {
50
50
  project,
package/dist/init.js CHANGED
@@ -36,9 +36,9 @@ const init = async args => {
36
36
  if (parsed.help) {
37
37
  log('Usage: duel [options]\n');
38
38
  log('Options:');
39
- log("--project, -p \t\t\t Compile the project given the path to its configuration file, or to a folder with a 'tsconfig.json'.");
40
- log('--target-extension, -x \t\t Sets the file extension for the dual build. [.cjs,.mjs]');
41
- log('--help, -h \t\t\t Print this message.');
39
+ log("--project, -p \t\t Compile the project given the path to its configuration file, or to a folder with a 'tsconfig.json'.");
40
+ log('--target-extension, -x \t Sets the file extension for the dual build. [.cjs,.mjs]');
41
+ log('--help, -h \t\t Print this message.');
42
42
  } else {
43
43
  const {
44
44
  project,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@knighted/duel",
3
- "version": "1.0.0-alpha.2",
3
+ "version": "1.0.0-alpha.4",
4
4
  "description": "TypeScript dual packages.",
5
5
  "type": "module",
6
6
  "main": "dist",
@@ -44,7 +44,7 @@
44
44
  "url": "https://github.com/knightedcodemonkey/duel/issues"
45
45
  },
46
46
  "peerDependencies": {
47
- "typescript": "^5.0.0"
47
+ "typescript": ">=4.0.0"
48
48
  },
49
49
  "devDependencies": {
50
50
  "babel-dual-package": "^1.0.0-rc.5",