@knighted/duel 1.0.0-alpha.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 ADDED
@@ -0,0 +1,43 @@
1
+ # [`@knighted/duel`](https://www.npmjs.com/package/@knighted/duel)
2
+
3
+ ![CI](https://github.com/knightedcodemonkey/duel/actions/workflows/ci.yml/badge.svg)
4
+ [![codecov](https://codecov.io/gh/knightedcodemonkey/duel/branch/main/graph/badge.svg?token=7K74BRLHFy)](https://codecov.io/gh/knightedcodemonkey/duel)
5
+ [![NPM version](https://img.shields.io/npm/v/@knighted/duel.svg)](https://www.npmjs.com/package/@knighted/duel)
6
+
7
+ Node.js tool for creating a TypeScript dual package.
8
+
9
+ Early stages of development. Inspired by https://github.com/microsoft/TypeScript/issues/49462.
10
+
11
+ ## Example
12
+
13
+ Consider a project that is ESM-first, i.e. `"type": "module"` in package.json, that also wants to create a separate CJS build. It might have a tsconfig.json file that looks like the following.
14
+
15
+ **tsconfig.json**
16
+
17
+ ```json
18
+ {
19
+ "compilerOptions": {
20
+ "target": "ESNext",
21
+ "module": "NodeNext",
22
+ "moduleResolution": "NodeNext",
23
+ "declaration": true,
24
+ "strict": true,
25
+ "outDir": "dist"
26
+ },
27
+ "include": ["src/*.ts"]
28
+ }
29
+ ```
30
+
31
+ Running the following will use the tsconfig.json defined above and create a separate CJS build in `dist/cjs`.
32
+
33
+ ```console
34
+ user@comp ~ $ duel -p tsconfig.json -x .cjs
35
+ ```
36
+
37
+ Now you can update your `exports` in package.json to match the build output.
38
+
39
+ It should work similarly for a CJS first project. Except, your tsconfig.json would be slightly different and you'd want to pass `-x .mjs`.
40
+
41
+ ## Gotchas
42
+
43
+ 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.
package/dist/duel.cjs ADDED
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ Object.defineProperty(exports, "__esModule", {
5
+ value: true
6
+ });
7
+ exports.duel = void 0;
8
+ var _nodeProcess = require("node:process");
9
+ var _nodeUrl = require("node:url");
10
+ var _nodePath = require("node:path");
11
+ var _nodeChild_process = require("node:child_process");
12
+ var _promises = require("node:fs/promises");
13
+ var _nodeCrypto = require("node:crypto");
14
+ var _nodePerf_hooks = require("node:perf_hooks");
15
+ var _glob = require("glob");
16
+ var _specifier = require("@knighted/specifier");
17
+ var _init = require("./init.cjs");
18
+ var _util = require("./util.cjs");
19
+ const _filename = (0, _nodeUrl.fileURLToPath)(import.meta.url);
20
+ const _dirname = (0, _nodePath.dirname)(_filename);
21
+ const root = (0, _nodePath.join)(_dirname, '..');
22
+ const tsc = (0, _nodePath.join)(root, 'node_modules', '.bin', 'tsc');
23
+ const runBuild = project => {
24
+ const {
25
+ status,
26
+ error
27
+ } = (0, _nodeChild_process.spawnSync)(tsc, ['-p', project], {
28
+ stdio: 'inherit'
29
+ });
30
+ if (error) {
31
+ (0, _util.logError)(`Failed to compile: ${error.message}`);
32
+ return false;
33
+ }
34
+ if (status === null) {
35
+ (0, _util.logError)(`Failed to compile. The process was terminated.`);
36
+ return false;
37
+ }
38
+ if (status > 0) {
39
+ (0, _util.logError)('Compilation errors found.');
40
+ return false;
41
+ }
42
+ return true;
43
+ };
44
+ const duel = async args => {
45
+ const ctx = await (0, _init.init)(args);
46
+ if (ctx) {
47
+ const {
48
+ projectDir,
49
+ tsconfig,
50
+ targetExt,
51
+ configPath
52
+ } = ctx;
53
+ const startTime = _nodePerf_hooks.performance.now();
54
+ (0, _util.log)('Starting primary build...\n');
55
+ let success = runBuild(configPath);
56
+ if (success) {
57
+ const isCjsBuild = targetExt === '.cjs';
58
+ const hex = (0, _nodeCrypto.randomBytes)(4).toString('hex');
59
+ const {
60
+ outDir
61
+ } = tsconfig.compilerOptions;
62
+ const dualConfigPath = (0, _nodePath.join)(projectDir, `tsconfig.${hex}.json`);
63
+ const dualOutDir = isCjsBuild ? (0, _nodePath.join)(outDir, 'cjs') : (0, _nodePath.join)(outDir, 'mjs');
64
+ const tsconfigDual = {
65
+ ...tsconfig,
66
+ compilerOptions: {
67
+ ...tsconfig.compilerOptions,
68
+ outDir: dualOutDir,
69
+ module: isCjsBuild ? 'CommonJS' : 'NodeNext',
70
+ moduleResolution: isCjsBuild ? 'Node' : 'NodeNext'
71
+ }
72
+ };
73
+ await (0, _promises.writeFile)(dualConfigPath, JSON.stringify(tsconfigDual, null, 2));
74
+ (0, _util.log)('Starting dual build...\n');
75
+ success = runBuild(dualConfigPath);
76
+ await (0, _promises.rm)(dualConfigPath, {
77
+ force: true
78
+ });
79
+ if (success) {
80
+ const filenames = await (0, _glob.glob)(`${(0, _nodePath.join)(projectDir, dualOutDir)}/**/*{.js,.d.ts}`, {
81
+ ignore: 'node_modules/**'
82
+ });
83
+ for (const filename of filenames) {
84
+ const dts = /(\.d\.ts)$/;
85
+ const outFilename = dts.test(filename) ? filename.replace(dts, isCjsBuild ? '.d.cts' : '.d.mts') : filename.replace(/\.js$/, targetExt);
86
+ const code = await _specifier.specifier.update(filename, ({
87
+ value
88
+ }) => {
89
+ // Collapse any BinaryExpression or NewExpression to test for a relative specifier
90
+ const collapsed = value.replace(/['"`+)\s]|new String\(/g, '');
91
+ const relative = /^(?:\.|\.\.)\//i;
92
+ if (relative.test(collapsed)) {
93
+ // $2 is for any closing quotation/parens around BE or NE
94
+ return value.replace(/(.+)\.js([)'"`]*)?$/, `$1${targetExt}$2`);
95
+ }
96
+ });
97
+ await (0, _promises.writeFile)(outFilename, code);
98
+ await (0, _promises.rm)(filename, {
99
+ force: true
100
+ });
101
+ }
102
+ (0, _util.log)(`Successfully created a dual ${targetExt.replace('.', '').toUpperCase()} build in ${Math.round(_nodePerf_hooks.performance.now() - startTime)}ms.`);
103
+ }
104
+ }
105
+ }
106
+ };
107
+ exports.duel = duel;
108
+ const realFileUrlArgv1 = await (0, _util.getRealPathAsFileUrl)(_nodeProcess.argv[1]);
109
+ if (import.meta.url === realFileUrlArgv1) {
110
+ await duel();
111
+ }
package/dist/duel.js ADDED
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env node
2
+ import { argv } from 'node:process';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { dirname, join } from 'node:path';
5
+ import { spawnSync } from 'node:child_process';
6
+ import { writeFile, rm } from 'node:fs/promises';
7
+ import { randomBytes } from 'node:crypto';
8
+ import { performance } from 'node:perf_hooks';
9
+ import { glob } from 'glob';
10
+ import { specifier } from '@knighted/specifier';
11
+ import { init } from './init.js';
12
+ import { getRealPathAsFileUrl, logError, log } from './util.js';
13
+ const __filename = fileURLToPath(import.meta.url);
14
+ const __dirname = dirname(__filename);
15
+ const root = join(__dirname, '..');
16
+ const tsc = join(root, 'node_modules', '.bin', 'tsc');
17
+ const runBuild = project => {
18
+ const {
19
+ status,
20
+ error
21
+ } = spawnSync(tsc, ['-p', project], {
22
+ stdio: 'inherit'
23
+ });
24
+ if (error) {
25
+ logError(`Failed to compile: ${error.message}`);
26
+ return false;
27
+ }
28
+ if (status === null) {
29
+ logError(`Failed to compile. The process was terminated.`);
30
+ return false;
31
+ }
32
+ if (status > 0) {
33
+ logError('Compilation errors found.');
34
+ return false;
35
+ }
36
+ return true;
37
+ };
38
+ const duel = async args => {
39
+ const ctx = await init(args);
40
+ if (ctx) {
41
+ const {
42
+ projectDir,
43
+ tsconfig,
44
+ targetExt,
45
+ configPath
46
+ } = ctx;
47
+ const startTime = performance.now();
48
+ log('Starting primary build...\n');
49
+ let success = runBuild(configPath);
50
+ if (success) {
51
+ const isCjsBuild = targetExt === '.cjs';
52
+ const hex = randomBytes(4).toString('hex');
53
+ const {
54
+ outDir
55
+ } = tsconfig.compilerOptions;
56
+ const dualConfigPath = join(projectDir, `tsconfig.${hex}.json`);
57
+ const dualOutDir = isCjsBuild ? join(outDir, 'cjs') : join(outDir, 'mjs');
58
+ const tsconfigDual = {
59
+ ...tsconfig,
60
+ compilerOptions: {
61
+ ...tsconfig.compilerOptions,
62
+ outDir: dualOutDir,
63
+ module: isCjsBuild ? 'CommonJS' : 'NodeNext',
64
+ moduleResolution: isCjsBuild ? 'Node' : 'NodeNext'
65
+ }
66
+ };
67
+ await writeFile(dualConfigPath, JSON.stringify(tsconfigDual, null, 2));
68
+ log('Starting dual build...\n');
69
+ success = runBuild(dualConfigPath);
70
+ await rm(dualConfigPath, {
71
+ force: true
72
+ });
73
+ if (success) {
74
+ const filenames = await glob(`${join(projectDir, dualOutDir)}/**/*{.js,.d.ts}`, {
75
+ ignore: 'node_modules/**'
76
+ });
77
+ for (const filename of filenames) {
78
+ const dts = /(\.d\.ts)$/;
79
+ const outFilename = dts.test(filename) ? filename.replace(dts, isCjsBuild ? '.d.cts' : '.d.mts') : filename.replace(/\.js$/, targetExt);
80
+ const code = await specifier.update(filename, ({
81
+ value
82
+ }) => {
83
+ // Collapse any BinaryExpression or NewExpression to test for a relative specifier
84
+ const collapsed = value.replace(/['"`+)\s]|new String\(/g, '');
85
+ const relative = /^(?:\.|\.\.)\//i;
86
+ if (relative.test(collapsed)) {
87
+ // $2 is for any closing quotation/parens around BE or NE
88
+ return value.replace(/(.+)\.js([)'"`]*)?$/, `$1${targetExt}$2`);
89
+ }
90
+ });
91
+ await writeFile(outFilename, code);
92
+ await rm(filename, {
93
+ force: true
94
+ });
95
+ }
96
+ log(`Successfully created a dual ${targetExt.replace('.', '').toUpperCase()} build in ${Math.round(performance.now() - startTime)}ms.`);
97
+ }
98
+ }
99
+ }
100
+ };
101
+ const realFileUrlArgv1 = await getRealPathAsFileUrl(argv[1]);
102
+ if (import.meta.url === realFileUrlArgv1) {
103
+ await duel();
104
+ }
105
+ export { duel };
package/dist/init.cjs ADDED
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.init = void 0;
7
+ var _nodeUtil = require("node:util");
8
+ var _nodePath = require("node:path");
9
+ var _promises = require("node:fs/promises");
10
+ var _util = require("./util.cjs");
11
+ const init = async args => {
12
+ const validTargetExts = ['.cjs', '.mjs'];
13
+ let parsed = null;
14
+ try {
15
+ const {
16
+ values
17
+ } = (0, _nodeUtil.parseArgs)({
18
+ args,
19
+ options: {
20
+ project: {
21
+ type: 'string',
22
+ short: 'p',
23
+ default: 'tsconfig.json'
24
+ },
25
+ 'target-extension': {
26
+ type: 'string',
27
+ short: 'x',
28
+ default: '.cjs'
29
+ },
30
+ help: {
31
+ type: 'boolean',
32
+ short: 'h',
33
+ default: false
34
+ }
35
+ }
36
+ });
37
+ parsed = values;
38
+ } catch (err) {
39
+ (0, _util.logError)(err.message);
40
+ return false;
41
+ }
42
+ if (parsed.help) {
43
+ (0, _util.log)('Usage: duel [options]\n');
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.');
48
+ } else {
49
+ const {
50
+ project,
51
+ 'target-extension': targetExt
52
+ } = parsed;
53
+ let configPath = (0, _nodePath.resolve)(project);
54
+ let stats = null;
55
+ if (!validTargetExts.includes(targetExt)) {
56
+ (0, _util.logError)(`Invalid arg '${targetExt}' for --target-extension. Must be one of ${validTargetExts.toString()}`);
57
+ return false;
58
+ }
59
+ try {
60
+ stats = await (0, _promises.stat)(configPath);
61
+ } catch {
62
+ (0, _util.logError)(`Provided --project '${project}' resolves to ${configPath} which is not a file or directory.`);
63
+ return false;
64
+ }
65
+ if (stats.isDirectory()) {
66
+ configPath = (0, _nodePath.join)(configPath, 'tsconfig.json');
67
+ try {
68
+ stats = await (0, _promises.stat)(configPath);
69
+ } catch {
70
+ (0, _util.logError)(`Provided --project '${project}' resolves to a directory ${(0, _nodePath.dirname)(configPath)} with no tsconfig.json.`);
71
+ return false;
72
+ }
73
+ }
74
+ if (stats.isFile()) {
75
+ let tsconfig = null;
76
+ try {
77
+ tsconfig = JSON.parse((await (0, _promises.readFile)(configPath)).toString());
78
+ } catch (err) {
79
+ (0, _util.logError)(`The config file found at ${configPath} is not parsable as JSON.`);
80
+ return false;
81
+ }
82
+ if (!tsconfig.compilerOptions?.outDir) {
83
+ (0, _util.logError)('You must define an `outDir` in your project config.');
84
+ return false;
85
+ }
86
+ const projectDir = (0, _nodePath.dirname)(configPath);
87
+ return {
88
+ tsconfig,
89
+ targetExt,
90
+ projectDir,
91
+ configPath,
92
+ absoluteOutDir: (0, _nodePath.resolve)(projectDir, tsconfig.compilerOptions.outDir)
93
+ };
94
+ }
95
+ }
96
+ return false;
97
+ };
98
+ exports.init = init;
package/dist/init.js ADDED
@@ -0,0 +1,92 @@
1
+ import { parseArgs } from 'node:util';
2
+ import { resolve, join, dirname } from 'node:path';
3
+ import { stat, readFile } from 'node:fs/promises';
4
+ import { logError, log } from './util.js';
5
+ const init = async args => {
6
+ const validTargetExts = ['.cjs', '.mjs'];
7
+ let parsed = null;
8
+ try {
9
+ const {
10
+ values
11
+ } = parseArgs({
12
+ args,
13
+ options: {
14
+ project: {
15
+ type: 'string',
16
+ short: 'p',
17
+ default: 'tsconfig.json'
18
+ },
19
+ 'target-extension': {
20
+ type: 'string',
21
+ short: 'x',
22
+ default: '.cjs'
23
+ },
24
+ help: {
25
+ type: 'boolean',
26
+ short: 'h',
27
+ default: false
28
+ }
29
+ }
30
+ });
31
+ parsed = values;
32
+ } catch (err) {
33
+ logError(err.message);
34
+ return false;
35
+ }
36
+ if (parsed.help) {
37
+ log('Usage: duel [options]\n');
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.');
42
+ } else {
43
+ const {
44
+ project,
45
+ 'target-extension': targetExt
46
+ } = parsed;
47
+ let configPath = resolve(project);
48
+ let stats = null;
49
+ if (!validTargetExts.includes(targetExt)) {
50
+ logError(`Invalid arg '${targetExt}' for --target-extension. Must be one of ${validTargetExts.toString()}`);
51
+ return false;
52
+ }
53
+ try {
54
+ stats = await stat(configPath);
55
+ } catch {
56
+ logError(`Provided --project '${project}' resolves to ${configPath} which is not a file or directory.`);
57
+ return false;
58
+ }
59
+ if (stats.isDirectory()) {
60
+ configPath = join(configPath, 'tsconfig.json');
61
+ try {
62
+ stats = await stat(configPath);
63
+ } catch {
64
+ logError(`Provided --project '${project}' resolves to a directory ${dirname(configPath)} with no tsconfig.json.`);
65
+ return false;
66
+ }
67
+ }
68
+ if (stats.isFile()) {
69
+ let tsconfig = null;
70
+ try {
71
+ tsconfig = JSON.parse((await readFile(configPath)).toString());
72
+ } catch (err) {
73
+ logError(`The config file found at ${configPath} is not parsable as JSON.`);
74
+ return false;
75
+ }
76
+ if (!tsconfig.compilerOptions?.outDir) {
77
+ logError('You must define an `outDir` in your project config.');
78
+ return false;
79
+ }
80
+ const projectDir = dirname(configPath);
81
+ return {
82
+ tsconfig,
83
+ targetExt,
84
+ projectDir,
85
+ configPath,
86
+ absoluteOutDir: resolve(projectDir, tsconfig.compilerOptions.outDir)
87
+ };
88
+ }
89
+ }
90
+ return false;
91
+ };
92
+ export { init };
package/dist/util.cjs ADDED
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.logError = exports.log = exports.getRealPathAsFileUrl = void 0;
7
+ var _nodeUrl = require("node:url");
8
+ var _promises = require("node:fs/promises");
9
+ const log = (color = '\x1b[30m', msg = '') => {
10
+ // eslint-disable-next-line no-console
11
+ console.log(`${color}%s\x1b[0m`, msg);
12
+ };
13
+ exports.log = log;
14
+ const logError = log.bind(null, '\x1b[31m');
15
+ exports.logError = logError;
16
+ const getRealPathAsFileUrl = async path => {
17
+ const realPath = await (0, _promises.realpath)(path);
18
+ const asFileUrl = (0, _nodeUrl.pathToFileURL)(realPath).href;
19
+ return asFileUrl;
20
+ };
21
+ exports.getRealPathAsFileUrl = getRealPathAsFileUrl;
package/dist/util.js ADDED
@@ -0,0 +1,13 @@
1
+ import { pathToFileURL } from 'node:url';
2
+ import { realpath } from 'node:fs/promises';
3
+ const log = (color = '\x1b[30m', msg = '') => {
4
+ // eslint-disable-next-line no-console
5
+ console.log(`${color}%s\x1b[0m`, msg);
6
+ };
7
+ const logError = log.bind(null, '\x1b[31m');
8
+ const getRealPathAsFileUrl = async path => {
9
+ const realPath = await realpath(path);
10
+ const asFileUrl = pathToFileURL(realPath).href;
11
+ return asFileUrl;
12
+ };
13
+ export { log, logError, getRealPathAsFileUrl };
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "@knighted/duel",
3
+ "version": "1.0.0-alpha.0",
4
+ "description": "TypeScript dual packages.",
5
+ "type": "module",
6
+ "main": "dist",
7
+ "bin": "dist/duel.js",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/duel.js",
11
+ "require": "./dist/duel.cjs",
12
+ "default": "./dist/duel.js"
13
+ },
14
+ "./package.json": "./package.json"
15
+ },
16
+ "engines": {
17
+ "node": ">=16.19.0"
18
+ },
19
+ "engineStrict": true,
20
+ "scripts": {
21
+ "prettier": "prettier -w src/*.js test/*.js",
22
+ "lint": "eslint src/*.js test/*.js",
23
+ "test": "c8 --reporter=text --reporter=text-summary --reporter=lcov node --test --test-reporter=spec test/*.js",
24
+ "build": "babel-dual-package --no-cjs-dir src",
25
+ "prepack": "npm run build"
26
+ },
27
+ "keywords": [
28
+ "tsc",
29
+ "typescript",
30
+ "dual package",
31
+ "cjs",
32
+ "mjs"
33
+ ],
34
+ "files": [
35
+ "dist"
36
+ ],
37
+ "author": "KCM <knightedcodemonkey@gmail.com>",
38
+ "license": "MIT",
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "https://github.com/knightedcodemonkey/duel.git"
42
+ },
43
+ "bugs": {
44
+ "url": "https://github.com/knightedcodemonkey/duel/issues"
45
+ },
46
+ "peerDependencies": {
47
+ "typescript": "^5.0.0"
48
+ },
49
+ "devDependencies": {
50
+ "babel-dual-package": "^1.0.0-rc.5",
51
+ "c8": "^8.0.1",
52
+ "eslint": "^8.45.0",
53
+ "eslint-plugin-n": "^16.0.1",
54
+ "prettier": "^3.0.0",
55
+ "typescript": "^5.2.0-dev.20230727"
56
+ },
57
+ "dependencies": {
58
+ "@knighted/specifier": "^1.0.0-alpha.5",
59
+ "glob": "^10.3.3",
60
+ "magic-string": "^0.30.1"
61
+ },
62
+ "prettier": {
63
+ "arrowParens": "avoid",
64
+ "printWidth": 90,
65
+ "semi": false,
66
+ "singleQuote": true
67
+ }
68
+ }