diff --git a/dev-scripts/enum-optimising-transformer.cjs b/dev-scripts/enum-optimising-transformer.cjs new file mode 100644 index 0000000..5726fd0 --- /dev/null +++ b/dev-scripts/enum-optimising-transformer.cjs @@ -0,0 +1,186 @@ +/** + * @param {import('typescript').Program} program + * @param {import('ts-patch').PluginConfig} pluginConfig + * @param {import('ts-patch').TransformerExtras} extras + */ +module.exports = (program, pluginConfig, { ts: tsInstance }) => { + /** @param {import('typescript').TransformationContext} context */ + return (context) => { + const { factory } = context + + /** @param {import('typescript').SourceFile} sourceFile */ + return (sourceFile) => { + /** + * @param {import('typescript').Node} node + * @returns {import('typescript').Node} + */ + const visitor = (node) => { + if ( + tsInstance.isEnumDeclaration(node) && + (!node.modifiers || node.modifiers.every(modifier => modifier.kind !== tsInstance.SyntaxKind.DeclareKeyword)) + ) { + let variableStatementModifiers + + if (node.modifiers?.some(modifier => modifier.kind === tsInstance.SyntaxKind.ExportKeyword)) { + variableStatementModifiers = [ + factory.createModifier(tsInstance.SyntaxKind.ExportKeyword) + ] + } + + const properties = [] + let currentValue = 0 + + for (const member of node.members) { + const name = member.name.text + let value + let isNumeric = true + let hasMinus = false + + if (!member.initializer) { + value = currentValue.toString() + currentValue++ + } else if (tsInstance.isStringLiteral(member.initializer)) { + value = member.initializer.text + isNumeric = false + } else if (tsInstance.isNumericLiteral(member.initializer)) { + value = member.initializer.text + currentValue = Number(value) + 1 + } else if ( + tsInstance.isPrefixUnaryExpression(member.initializer) && + member.initializer.operator === tsInstance.SyntaxKind.MinusToken + ) { + value = member.initializer.operand.text + hasMinus = true + currentValue = (-1 * Number(value)) + 1 + } else { + console.warn(`Unsupported enum member initializer "${tsInstance.SyntaxKind[member.initializer.kind]}" in "${node.name.text}", using original enum output.`) + return tsInstance.visitEachChild(node, visitor, context); + } + + if (isNumeric) { + if (hasMinus) { + const mapping = factory.createPropertyAssignment( + name, + factory.createPrefixUnaryExpression( + tsInstance.SyntaxKind.MinusToken, + factory.createNumericLiteral(value) + ) + ) + + tsInstance.setOriginalNode(mapping, member) + tsInstance.setTextRange(mapping, member) + + tsInstance.setOriginalNode(mapping.name, member.name) + tsInstance.setTextRange(mapping.name, member.name) + + if (member.initializer) { + tsInstance.setOriginalNode(mapping.initializer, member.initializer) + tsInstance.setTextRange(mapping.initializer, member.initializer) + } + + const reverseMapping = factory.createPropertyAssignment( + factory.createStringLiteral(`-${value}`), + factory.createStringLiteral(name) + ) + + tsInstance.setOriginalNode(reverseMapping, member) + tsInstance.setTextRange(reverseMapping, member) + + tsInstance.setOriginalNode(reverseMapping.initializer, member.name) + tsInstance.setTextRange(reverseMapping.initializer, member.name) + + if (member.initializer) { + tsInstance.setOriginalNode(reverseMapping.name, member.initializer) + tsInstance.setTextRange(reverseMapping.name, member.initializer) + } + + properties.push(mapping, reverseMapping) + } else { + const mapping = factory.createPropertyAssignment( + name, + factory.createNumericLiteral(value) + ) + + tsInstance.setOriginalNode(mapping, member) + tsInstance.setTextRange(mapping, member) + + tsInstance.setOriginalNode(mapping.name, member.name) + tsInstance.setTextRange(mapping.name, member.name) + + if (member.initializer) { + tsInstance.setOriginalNode(mapping.initializer, member.initializer) + tsInstance.setTextRange(mapping.initializer, member.initializer) + } + + const reverseMapping = factory.createPropertyAssignment( + value, + factory.createStringLiteral(name) + ) + + tsInstance.setOriginalNode(reverseMapping, member) + tsInstance.setTextRange(reverseMapping, member) + + tsInstance.setOriginalNode(reverseMapping.initializer, member.name) + tsInstance.setTextRange(reverseMapping.initializer, member.name) + + if (member.initializer) { + tsInstance.setOriginalNode(reverseMapping.name, member.initializer) + tsInstance.setTextRange(reverseMapping.name, member.initializer) + } + + properties.push(mapping, reverseMapping) + } + } else { + const mapping = factory.createPropertyAssignment( + name, + factory.createStringLiteral(value) + ) + + tsInstance.setOriginalNode(mapping, member) + tsInstance.setTextRange(mapping, member) + + tsInstance.setOriginalNode(mapping.name, member.name) + tsInstance.setTextRange(mapping.name, member.name) + + tsInstance.setOriginalNode(mapping.initializer, member.initializer) + tsInstance.setTextRange(mapping.initializer, member.initializer) + + properties.push(mapping) + } + } + + const convertedNameIdentifier = factory.createIdentifier(node.name.text) + tsInstance.setOriginalNode(convertedNameIdentifier, node.name) + tsInstance.setTextRange(convertedNameIdentifier, node.name) + + const convertedEnum = factory.createVariableStatement( + variableStatementModifiers, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + convertedNameIdentifier, + undefined, + undefined, + factory.createObjectLiteralExpression( + properties, + true + ) + ) + ], + tsInstance.NodeFlags.Const + ) + ) + + tsInstance.setOriginalNode(convertedEnum, node) + tsInstance.setTextRange(convertedEnum, node) + + return convertedEnum + } + + return tsInstance.visitEachChild(node, visitor, context); + }; + + return tsInstance.visitNode(sourceFile, visitor); + } + } +} diff --git a/package-lock.json b/package-lock.json index 5cf2c20..0465a73 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "@types/eslint__js": "^8.42.3", "eslint": "^9.9.0", "globals": "^15.9.0", + "ts-patch": "^3.3.0", "ts-proto": "^2.2.0", "typescript": "^5.5.4", "typescript-eslint": "^8.2.0" @@ -1024,6 +1025,15 @@ "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", "dev": true }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", @@ -1037,6 +1047,44 @@ "node": ">= 6" } }, + "node_modules/global-prefix": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-4.0.0.tgz", + "integrity": "sha512-w0Uf9Y9/nyHinEk5vMJKRie+wa4kR5hmDbEhGGds/kG1PwGLLHKRoNMeJOyCQjjBkANlnScqgzcFwGHgmgLkVA==", + "dev": true, + "dependencies": { + "ini": "^4.1.3", + "kind-of": "^6.0.3", + "which": "^4.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/global-prefix/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, + "engines": { + "node": ">=16" + } + }, + "node_modules/global-prefix/node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, "node_modules/globals": { "version": "15.11.0", "resolved": "https://registry.npmjs.org/globals/-/globals-15.11.0.tgz", @@ -1066,6 +1114,18 @@ "node": ">=8" } }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -1100,6 +1160,30 @@ "node": ">=0.8.19" } }, + "node_modules/ini": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.3.tgz", + "integrity": "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -1185,6 +1269,15 @@ "json-buffer": "3.0.1" } }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -1272,6 +1365,15 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -1362,6 +1464,12 @@ "node": ">=8" } }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, "node_modules/picomatch": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", @@ -1413,6 +1521,26 @@ } ] }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -1525,6 +1653,18 @@ "node": ">=8" } }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -1557,6 +1697,24 @@ "typescript": ">=4.2.0" } }, + "node_modules/ts-patch": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/ts-patch/-/ts-patch-3.3.0.tgz", + "integrity": "sha512-zAOzDnd5qsfEnjd9IGy1IRuvA7ygyyxxdxesbhMdutt8AHFjD8Vw8hU2rMF89HX1BKRWFYqKHrO8Q6lw0NeUZg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.2", + "global-prefix": "^4.0.0", + "minimist": "^1.2.8", + "resolve": "^1.22.2", + "semver": "^7.6.3", + "strip-ansi": "^6.0.1" + }, + "bin": { + "ts-patch": "bin/ts-patch.js", + "tspc": "bin/tspc.js" + } + }, "node_modules/ts-poet": { "version": "6.9.0", "resolved": "https://registry.npmjs.org/ts-poet/-/ts-poet-6.9.0.tgz", diff --git a/package.json b/package.json index f657968..7a5b181 100644 --- a/package.json +++ b/package.json @@ -7,12 +7,12 @@ "types": "./dist/src/index.d.ts", "module": "./dist/src/index.js", "scripts": { - "watch": "npx tsc --watch", + "watch": "npx tspc --watch", "lint": "npx eslint ./src/**/*.ts", "lint:fix": "npx eslint --fix ./src/**/*.ts", "clean": "npx rimraf ./dist ./protos/generated", "build": "npm run clean && npm run lint && npm run build:proto && npm run build:esm", - "build:esm": "npx tsc", + "build:esm": "npx tspc", "build:proto": "node ./dev-scripts/generate-proto.mjs", "prepare": "npm run build" }, @@ -42,6 +42,7 @@ "@types/eslint__js": "^8.42.3", "eslint": "^9.9.0", "globals": "^15.9.0", + "ts-patch": "^3.3.0", "ts-proto": "^2.2.0", "typescript": "^5.5.4", "typescript-eslint": "^8.2.0" diff --git a/tsconfig.json b/tsconfig.json index ecebc0f..ee56793 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -90,7 +90,10 @@ // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ /* Completeness */ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ - "skipLibCheck": true /* Skip type checking all .d.ts files. */ + "skipLibCheck": true, /* Skip type checking all .d.ts files. */ + "plugins": [ + { "transform": "./dev-scripts/enum-optimising-transformer.cjs" } + ] }, "include": [ "src/**/*"