diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a73737f..1ff3e3d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,12 +13,10 @@ jobs: steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4.0.0 - with: - version: 9.1.1 - name: Use Node.js uses: actions/setup-node@v4.0.2 with: - node-version: 20 + node-version: 22 cache: 'pnpm' - run: pnpm install - run: pnpm run build @@ -29,14 +27,12 @@ jobs: needs: [typecheck] strategy: matrix: - node: ['20'] + node: ['22'] steps: - uses: actions/checkout@v4 with: fetch-depth: '0' - uses: pnpm/action-setup@v4.0.0 - with: - version: 9.1.1 - name: Use Node.js uses: actions/setup-node@v4.0.2 with: @@ -48,7 +44,7 @@ jobs: - run: pnpm run coverage - name: Upload coverage report if: success() - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: coverage-report path: coverage/lcov-report/index.html @@ -56,14 +52,12 @@ jobs: build: strategy: matrix: - node: ['20'] + node: ['22'] runs-on: ubuntu-22.04 needs: [typecheck] steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4.0.0 - with: - version: 9.1.1 - name: Use Node.js uses: actions/setup-node@v4.0.2 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e9c1033..c87bc20 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,14 +23,11 @@ jobs: git config --global user.email 'github-actions[bot]@users.noreply.github.com' - uses: pnpm/action-setup@v4.0.0 - with: - version: 9.1.1 - name: Use Node.js uses: actions/setup-node@v4 with: - node-version: 20 - cache: 'pnpm' + node-version: 22 - run: pnpm install diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..b58b603 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,5 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/biome.xml b/.idea/biome.xml new file mode 100644 index 0000000..0b9612d --- /dev/null +++ b/.idea/biome.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..85ed8a7 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/unused-i18n.iml b/.idea/unused-i18n.iml new file mode 100644 index 0000000..24643cc --- /dev/null +++ b/.idea/unused-i18n.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..c9f4a37 --- /dev/null +++ b/biome.json @@ -0,0 +1,60 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "organizeImports": { + "enabled": false + }, + "linter": { + "enabled": false + }, + "vcs": { + "clientKind": "git", + "useIgnoreFile": false, + "defaultBranch": "main" + }, + "formatter": { + "enabled": true, + "formatWithErrors": false, + "indentStyle": "space", + "indentWidth": 2, + "lineEnding": "lf", + "lineWidth": 80, + "attributePosition": "auto" + }, + "javascript": { + "formatter": { + "jsxQuoteStyle": "double", + "quoteProperties": "asNeeded", + "trailingCommas": "all", + "semicolons": "asNeeded", + "arrowParentheses": "asNeeded", + "bracketSpacing": true, + "bracketSameLine": false, + "quoteStyle": "single", + "attributePosition": "auto" + } + }, + "json": { + "formatter": { + "enabled": true + }, + "parser": { + "allowComments": true + }, + "linter": { + "enabled": true + } + }, + "css": { + "formatter": { + "enabled": true, + "quoteStyle": "single" + }, + "linter": { + "enabled": true + }, + "parser": { + "allowWrongLineComments": false, + "cssModules": true + } + } +} diff --git a/package.json b/package.json index ccb1df1..3a8944f 100644 --- a/package.json +++ b/package.json @@ -34,17 +34,19 @@ "coverage": "vitest run --coverage" }, "dependencies": { - "commander": "^12.1.0" + "commander": "^12.1.0", + "esbuild": "^0.24.0" }, "devDependencies": { + "@biomejs/biome": "1.9.4", "@changesets/changelog-github": "^0.5.0", "@changesets/cli": "^2.27.3", "@types/node": "^14.18.63", "@vitest/coverage-c8": "^0.33.0", "@vitest/coverage-istanbul": "^1.6.0", "typescript": "^5.2.2", - "vitest": "^1.6.0", - "vite": "^5.2.0" + "vite": "^5.2.0", + "vitest": "^1.6.0" }, "keywords": [ "i18n", @@ -57,7 +59,8 @@ "next" ], "engines": { - "node": ">=18.x", - "pnpm": ">=8.x" - } + "node": ">=22.x", + "pnpm": ">=9.x" + }, + "packageManager": "pnpm@9.13.2+sha512.88c9c3864450350e65a33587ab801acf946d7c814ed1134da4a924f6df5a2120fd36b46aab68f7cd1d413149112d53c7db3a4136624cfd00ff1846a0c6cef48a" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 07779ff..b882c8c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,7 +11,13 @@ importers: commander: specifier: ^12.1.0 version: 12.1.0 + esbuild: + specifier: ^0.24.0 + version: 0.24.0 devDependencies: + '@biomejs/biome': + specifier: 1.9.4 + version: 1.9.4 '@changesets/changelog-github': specifier: ^0.5.0 version: 0.5.0(encoding@0.1.13) @@ -137,6 +143,59 @@ packages: '@bcoe/v8-coverage@0.2.3': resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + '@biomejs/biome@1.9.4': + resolution: {integrity: sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==} + engines: {node: '>=14.21.3'} + hasBin: true + + '@biomejs/cli-darwin-arm64@1.9.4': + resolution: {integrity: sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [darwin] + + '@biomejs/cli-darwin-x64@1.9.4': + resolution: {integrity: sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [darwin] + + '@biomejs/cli-linux-arm64-musl@1.9.4': + resolution: {integrity: sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + + '@biomejs/cli-linux-arm64@1.9.4': + resolution: {integrity: sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + + '@biomejs/cli-linux-x64-musl@1.9.4': + resolution: {integrity: sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + + '@biomejs/cli-linux-x64@1.9.4': + resolution: {integrity: sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + + '@biomejs/cli-win32-arm64@1.9.4': + resolution: {integrity: sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [win32] + + '@biomejs/cli-win32-x64@1.9.4': + resolution: {integrity: sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [win32] + '@changesets/apply-release-plan@7.0.1': resolution: {integrity: sha512-aPdSq/R++HOyfEeBGjEe6LNG8gs0KMSyRETD/J2092OkNq8mOioAxyKjMbvVUdzgr/HTawzMOz7lfw339KnsCA==} @@ -201,138 +260,282 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.24.0': + resolution: {integrity: sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/android-arm64@0.20.2': resolution: {integrity: sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==} engines: {node: '>=12'} cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.24.0': + resolution: {integrity: sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.20.2': resolution: {integrity: sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==} engines: {node: '>=12'} cpu: [arm] os: [android] + '@esbuild/android-arm@0.24.0': + resolution: {integrity: sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.20.2': resolution: {integrity: sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==} engines: {node: '>=12'} cpu: [x64] os: [android] + '@esbuild/android-x64@0.24.0': + resolution: {integrity: sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.20.2': resolution: {integrity: sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==} engines: {node: '>=12'} cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.24.0': + resolution: {integrity: sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.20.2': resolution: {integrity: sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==} engines: {node: '>=12'} cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.24.0': + resolution: {integrity: sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.20.2': resolution: {integrity: sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==} engines: {node: '>=12'} cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.24.0': + resolution: {integrity: sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.20.2': resolution: {integrity: sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==} engines: {node: '>=12'} cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.24.0': + resolution: {integrity: sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.20.2': resolution: {integrity: sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==} engines: {node: '>=12'} cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.24.0': + resolution: {integrity: sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.20.2': resolution: {integrity: sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==} engines: {node: '>=12'} cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.24.0': + resolution: {integrity: sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.20.2': resolution: {integrity: sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==} engines: {node: '>=12'} cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.24.0': + resolution: {integrity: sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.20.2': resolution: {integrity: sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==} engines: {node: '>=12'} cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.24.0': + resolution: {integrity: sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.20.2': resolution: {integrity: sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==} engines: {node: '>=12'} cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.24.0': + resolution: {integrity: sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.20.2': resolution: {integrity: sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==} engines: {node: '>=12'} cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.24.0': + resolution: {integrity: sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.20.2': resolution: {integrity: sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==} engines: {node: '>=12'} cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.24.0': + resolution: {integrity: sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.20.2': resolution: {integrity: sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==} engines: {node: '>=12'} cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.24.0': + resolution: {integrity: sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.20.2': resolution: {integrity: sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==} engines: {node: '>=12'} cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.24.0': + resolution: {integrity: sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/netbsd-x64@0.20.2': resolution: {integrity: sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==} engines: {node: '>=12'} cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.24.0': + resolution: {integrity: sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.24.0': + resolution: {integrity: sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.20.2': resolution: {integrity: sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==} engines: {node: '>=12'} cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.24.0': + resolution: {integrity: sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/sunos-x64@0.20.2': resolution: {integrity: sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==} engines: {node: '>=12'} cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.24.0': + resolution: {integrity: sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.20.2': resolution: {integrity: sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==} engines: {node: '>=12'} cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.24.0': + resolution: {integrity: sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.20.2': resolution: {integrity: sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==} engines: {node: '>=12'} cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.24.0': + resolution: {integrity: sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.20.2': resolution: {integrity: sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==} engines: {node: '>=12'} cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.24.0': + resolution: {integrity: sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@istanbuljs/schema@0.1.3': resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} engines: {node: '>=8'} @@ -814,6 +1017,11 @@ packages: engines: {node: '>=12'} hasBin: true + esbuild@0.24.0: + resolution: {integrity: sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ==} + engines: {node: '>=18'} + hasBin: true + escalade@3.1.2: resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==} engines: {node: '>=6'} @@ -2035,6 +2243,41 @@ snapshots: '@bcoe/v8-coverage@0.2.3': {} + '@biomejs/biome@1.9.4': + optionalDependencies: + '@biomejs/cli-darwin-arm64': 1.9.4 + '@biomejs/cli-darwin-x64': 1.9.4 + '@biomejs/cli-linux-arm64': 1.9.4 + '@biomejs/cli-linux-arm64-musl': 1.9.4 + '@biomejs/cli-linux-x64': 1.9.4 + '@biomejs/cli-linux-x64-musl': 1.9.4 + '@biomejs/cli-win32-arm64': 1.9.4 + '@biomejs/cli-win32-x64': 1.9.4 + + '@biomejs/cli-darwin-arm64@1.9.4': + optional: true + + '@biomejs/cli-darwin-x64@1.9.4': + optional: true + + '@biomejs/cli-linux-arm64-musl@1.9.4': + optional: true + + '@biomejs/cli-linux-arm64@1.9.4': + optional: true + + '@biomejs/cli-linux-x64-musl@1.9.4': + optional: true + + '@biomejs/cli-linux-x64@1.9.4': + optional: true + + '@biomejs/cli-win32-arm64@1.9.4': + optional: true + + '@biomejs/cli-win32-x64@1.9.4': + optional: true + '@changesets/apply-release-plan@7.0.1': dependencies: '@babel/runtime': 7.24.6 @@ -2201,72 +2444,144 @@ snapshots: '@esbuild/aix-ppc64@0.20.2': optional: true + '@esbuild/aix-ppc64@0.24.0': + optional: true + '@esbuild/android-arm64@0.20.2': optional: true + '@esbuild/android-arm64@0.24.0': + optional: true + '@esbuild/android-arm@0.20.2': optional: true + '@esbuild/android-arm@0.24.0': + optional: true + '@esbuild/android-x64@0.20.2': optional: true + '@esbuild/android-x64@0.24.0': + optional: true + '@esbuild/darwin-arm64@0.20.2': optional: true + '@esbuild/darwin-arm64@0.24.0': + optional: true + '@esbuild/darwin-x64@0.20.2': optional: true + '@esbuild/darwin-x64@0.24.0': + optional: true + '@esbuild/freebsd-arm64@0.20.2': optional: true + '@esbuild/freebsd-arm64@0.24.0': + optional: true + '@esbuild/freebsd-x64@0.20.2': optional: true + '@esbuild/freebsd-x64@0.24.0': + optional: true + '@esbuild/linux-arm64@0.20.2': optional: true + '@esbuild/linux-arm64@0.24.0': + optional: true + '@esbuild/linux-arm@0.20.2': optional: true + '@esbuild/linux-arm@0.24.0': + optional: true + '@esbuild/linux-ia32@0.20.2': optional: true + '@esbuild/linux-ia32@0.24.0': + optional: true + '@esbuild/linux-loong64@0.20.2': optional: true + '@esbuild/linux-loong64@0.24.0': + optional: true + '@esbuild/linux-mips64el@0.20.2': optional: true + '@esbuild/linux-mips64el@0.24.0': + optional: true + '@esbuild/linux-ppc64@0.20.2': optional: true + '@esbuild/linux-ppc64@0.24.0': + optional: true + '@esbuild/linux-riscv64@0.20.2': optional: true + '@esbuild/linux-riscv64@0.24.0': + optional: true + '@esbuild/linux-s390x@0.20.2': optional: true + '@esbuild/linux-s390x@0.24.0': + optional: true + '@esbuild/linux-x64@0.20.2': optional: true + '@esbuild/linux-x64@0.24.0': + optional: true + '@esbuild/netbsd-x64@0.20.2': optional: true + '@esbuild/netbsd-x64@0.24.0': + optional: true + + '@esbuild/openbsd-arm64@0.24.0': + optional: true + '@esbuild/openbsd-x64@0.20.2': optional: true + '@esbuild/openbsd-x64@0.24.0': + optional: true + '@esbuild/sunos-x64@0.20.2': optional: true + '@esbuild/sunos-x64@0.24.0': + optional: true + '@esbuild/win32-arm64@0.20.2': optional: true + '@esbuild/win32-arm64@0.24.0': + optional: true + '@esbuild/win32-ia32@0.20.2': optional: true + '@esbuild/win32-ia32@0.24.0': + optional: true + '@esbuild/win32-x64@0.20.2': optional: true + '@esbuild/win32-x64@0.24.0': + optional: true + '@istanbuljs/schema@0.1.3': {} '@jest/schemas@29.6.3': @@ -2837,6 +3152,33 @@ snapshots: '@esbuild/win32-ia32': 0.20.2 '@esbuild/win32-x64': 0.20.2 + esbuild@0.24.0: + optionalDependencies: + '@esbuild/aix-ppc64': 0.24.0 + '@esbuild/android-arm': 0.24.0 + '@esbuild/android-arm64': 0.24.0 + '@esbuild/android-x64': 0.24.0 + '@esbuild/darwin-arm64': 0.24.0 + '@esbuild/darwin-x64': 0.24.0 + '@esbuild/freebsd-arm64': 0.24.0 + '@esbuild/freebsd-x64': 0.24.0 + '@esbuild/linux-arm': 0.24.0 + '@esbuild/linux-arm64': 0.24.0 + '@esbuild/linux-ia32': 0.24.0 + '@esbuild/linux-loong64': 0.24.0 + '@esbuild/linux-mips64el': 0.24.0 + '@esbuild/linux-ppc64': 0.24.0 + '@esbuild/linux-riscv64': 0.24.0 + '@esbuild/linux-s390x': 0.24.0 + '@esbuild/linux-x64': 0.24.0 + '@esbuild/netbsd-x64': 0.24.0 + '@esbuild/openbsd-arm64': 0.24.0 + '@esbuild/openbsd-x64': 0.24.0 + '@esbuild/sunos-x64': 0.24.0 + '@esbuild/win32-arm64': 0.24.0 + '@esbuild/win32-ia32': 0.24.0 + '@esbuild/win32-x64': 0.24.0 + escalade@3.1.2: {} escape-string-regexp@1.0.5: {} diff --git a/src/index.ts b/src/index.ts index a3f3f51..528bedb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,10 +1,10 @@ import * as fs from 'fs' -import { loadConfig } from './utils/loadConfig' -import { getMissingTranslations } from './utils/missingTranslations' -import { summary } from './utils/summary' -import { searchFilesRecursively } from './lib/search' -import { analyze } from './lib/analyze' -import { removeLocaleKeys } from './lib/remove' +import { loadConfig } from '@utils/loadConfig' +import { getMissingTranslations } from '@utils/missingTranslations' +import { summary } from '@utils/summary' +import { searchFilesRecursively } from '@lib/search' +import { analyze } from '@lib/analyze' +import { removeLocaleKeys } from '@lib/remove' import { ProcessTranslationsArgs } from './types' import { performance } from 'perf_hooks' @@ -31,9 +31,9 @@ export const processTranslations = async ({ let allExtractedTranslations: string[] = [] let pathUnusedLocalesCount = 0 - srcPath.forEach((pathEntry) => { - const ignorePathExists = config.ignorePaths?.some((ignorePath) => - pathEntry.includes(ignorePath) + srcPath.forEach(pathEntry => { + const ignorePathExists = config.ignorePaths?.some(ignorePath => + pathEntry.includes(ignorePath), ) if (ignorePathExists) return const files = searchFilesRecursively({ @@ -43,11 +43,11 @@ export const processTranslations = async ({ }) const extractedTranslations = files - .flatMap((file) => + .flatMap(file => analyze({ filePath: file, scopedNames: config.scopedNames, - }) + }), ) .sort() .filter((item, index, array) => array.indexOf(item) === index) @@ -61,8 +61,8 @@ export const processTranslations = async ({ allExtractedTranslations = [...new Set(allExtractedTranslations)].sort() const localeFilePath = `${localPath}/${localesNames}.${localesExtensions}` - const ignorePathExists = config.ignorePaths?.some((ignorePath) => - localeFilePath.includes(ignorePath) + const ignorePathExists = config.ignorePaths?.some(ignorePath => + localeFilePath.includes(ignorePath), ) if (fs.existsSync(localeFilePath) && !ignorePathExists) { console.log(`${localeFilePath}...`) @@ -70,9 +70,9 @@ export const processTranslations = async ({ const localLines = fs .readFileSync(localeFilePath, 'utf-8') .split('\n') - .map((line) => line.trim()) - .filter((line) => line.match(/'[^']*':/)) - .map((line) => line.match(/'([^']+)':/)?.[1] ?? '') + .map(line => line.trim()) + .filter(line => line.match(/'[^']*':/)) + .map(line => line.match(/'([^']+)':/)?.[1] ?? '') .sort() const missingTranslations = getMissingTranslations({ @@ -85,7 +85,7 @@ export const processTranslations = async ({ totalUnusedLocales += pathUnusedLocalesCount const formattedMissingTranslations = missingTranslations - .map((translation) => `\x1b[31m${translation}\x1b[0m`) + .map(translation => `\x1b[31m${translation}\x1b[0m`) .join('\n') const message = missingTranslations.length @@ -113,8 +113,8 @@ export const processTranslations = async ({ summary({ unusedLocalesCountByPath, totalUnusedLocales }) console.log( `\x1b[38;2;128;128;128mDuration : ${(endTime - startTime).toFixed( - 0 - )}ms\x1b[0m` + 0, + )}ms\x1b[0m`, ) // Check if totalUnusedLocales is greater than 0 diff --git a/src/lib/analyze.ts b/src/lib/analyze.ts index 8fff4ef..2baf4b3 100644 --- a/src/lib/analyze.ts +++ b/src/lib/analyze.ts @@ -9,10 +9,10 @@ export const analyze = ({ filePath, scopedNames }: AnalyzeArgs): string[] => { const namespaceTranslations = extractNamespaceTranslation({ fileContent }) const scopedTs = ( - scopedNames?.map((scopedName) => - namespaceTranslations.flatMap((namespaceTranslation) => - extractScopedTs({ fileContent, namespaceTranslation, scopedName }) - ) + scopedNames?.map(scopedName => + namespaceTranslations.flatMap(namespaceTranslation => + extractScopedTs({ fileContent, namespaceTranslation, scopedName }), + ), ) ?? [] ).flat(2) diff --git a/src/lib/scopedNamespace/extractNamespaceTranslation.ts b/src/lib/scopedNamespace/extractNamespaceTranslation.ts index 98fe3a6..507321f 100644 --- a/src/lib/scopedNamespace/extractNamespaceTranslation.ts +++ b/src/lib/scopedNamespace/extractNamespaceTranslation.ts @@ -1,28 +1,69 @@ import { ExtractTranslationArgs } from '../../types' - export const extractNamespaceTranslation = ({ fileContent, }: ExtractTranslationArgs): string[] => { + // Match regular namespaceTranslation const namespaceTranslationPattern = /namespaceTranslation\(\s*['"`]([\s\S]*?)['"`]\s*,?\s*\)/g + + // Match ternary inside namespaceTranslation const namespaceTranslationPatternTernary = - /namespaceTranslation\(\s*([^()]*\?)\s*['"`]([^'"`]*?)['"`]\s*:\s*['"`]([^'"`]*?)['"`]\s*\)/ + /namespaceTranslation\(\s*([^\?]+?)\s*\?\s*['"\`]([^'"\\\`\n]+)['"`]\s*:\s*['"`]([^'"\\\`\n]+)['"`']\s*,?\s*\)/g + + // Match template literal inside namespaceTranslation + const namespaceTranslationPatternTemplateLiteral = + /namespaceTranslation\(\s*`([\s\S]*?)`\s*\)/g - const matches = [] + const matches: string[] = [] let match - // Extract matches for the regular namespaceTranslation pattern + // Extract regular namespaceTranslation matches while ((match = namespaceTranslationPattern.exec(fileContent)) !== null) { matches.push(match[1].trim()) } - // Extract matches for the ternary pattern - const ternaryMatch = fileContent.match(namespaceTranslationPatternTernary) - if (ternaryMatch) { - const trueValue = ternaryMatch[2].trim() - const falseValue = ternaryMatch[3].trim() + // Extract ternary matches inside namespaceTranslation + while ( + (match = namespaceTranslationPatternTernary.exec(fileContent)) !== null + ) { + const trueValue = match[2].trim() + const falseValue = match[3].trim() matches.push(trueValue, falseValue) } - return matches + // Extract template literal matches inside namespaceTranslation + while ( + (match = namespaceTranslationPatternTemplateLiteral.exec(fileContent)) !== + null + ) { + const templateLiteral = match[1].trim() + + // Find the dynamic parts inside `${}` in the template literal + const dynamicPartsMatch = /\${([^}]+)}/g + let dynamicPart + while ((dynamicPart = dynamicPartsMatch.exec(templateLiteral)) !== null) { + const dynamicExpression = dynamicPart[1].trim() + + // Now handle ternary expression inside this dynamic part + const ternaryMatch = + /([^\?]+?)\s*\?\s*['"`]([^'"\\\`\n]+)['"`]\s*:\s*['"`]([^'"\\\`\n]+)['"`]/g + let ternaryInnerMatch + while ( + (ternaryInnerMatch = ternaryMatch.exec(dynamicExpression)) !== null + ) { + const trueValue = ternaryInnerMatch[2].trim() + const falseValue = ternaryInnerMatch[3].trim() + matches.push(trueValue, falseValue) + } + + // Push the dynamic expression result into the matches + matches.push(dynamicExpression) + } + + // Push the whole template literal into the matches + matches.push(templateLiteral) + } + + // Remove duplicates and return the sorted result + return [...new Set(matches)].sort() } diff --git a/src/lib/scopedNamespace/extractScopedTs.ts b/src/lib/scopedNamespace/extractScopedTs.ts index b26faf1..d649cf8 100644 --- a/src/lib/scopedNamespace/extractScopedTs.ts +++ b/src/lib/scopedNamespace/extractScopedTs.ts @@ -8,23 +8,23 @@ export const extractScopedTs = ({ // Patterns with the variable const scopedTPattern = new RegExp( `${scopedName}\\(\\s*['"\`']([\\s\\S]*?)['"\`']\\s*(?:,|\\))`, - 'g' + 'g', ) const scopedTPatternWithTernary = new RegExp( `${scopedName}\\(\\s*([\\s\\S]+?)\\s*\\?\\s*['"\`']([^'"\\\`\n]+)['"\`']\\s*:\\s*['"\`']([^'"\\\`\n]+)['"\`'],?\\s*\\)`, - 'gm' + 'gm', ) const scopedTPatternWithTernaryAndParams = new RegExp( `${scopedName}\\(\\s*([^?\n]+)\\s*\\?\\s*['"\`']([^'"\\\`]+)['"\`']\\s*:\\s*['"\`']([^'"\\\`]+)['"\`'],\\s*\\{[\\s\\S]*?\\},?\\s*\\)`, - 'gm' + 'gm', ) const scopedTVariablePattern = new RegExp( `${scopedName}\\(\\s*([a-zA-Z_$][\\w.$]*)\\s*\\)`, - 'g' + 'g', ) const scopedTTemplatePattern = new RegExp( `${scopedName}\\(\\s*\`([\\s\\S]*?)\`\\s*\\)`, - 'g' + 'g', ) const scopedTs: Set = new Set() @@ -43,13 +43,13 @@ export const extractScopedTs = ({ `${namespaceTranslationTrimmed}.${trueValue .replace(/'/g, '') .replace(/,/g, '') - .trim()}` + .trim()}`, ) scopedTs.add( `${namespaceTranslationTrimmed}.${falseValue .replace(/'/g, '') .replace(/,/g, '') - .trim()}` + .trim()}`, ) } @@ -61,12 +61,12 @@ export const extractScopedTs = ({ scopedTs.add( `${namespaceTranslationTrimmed}.${trueValue .replace(/'/g, '') - .replace(/,/g, '')}` + .replace(/,/g, '')}`, ) scopedTs.add( `${namespaceTranslationTrimmed}.${falseValue .replace(/'/g, '') - .replace(/,/g, '')}` + .replace(/,/g, '')}`, ) } @@ -85,12 +85,12 @@ export const extractScopedTs = ({ const [ifValue, elseValue] = ternary .split(' ? ')[1] .split(':') - .map((val) => val.trim().replace(/'/g, '')) + .map(val => val.trim().replace(/'/g, '')) const stringIf = `${scopedTWithNamespace.replace(fullMatch, ifValue)}` const stringElse = `${scopedTWithNamespace.replace( fullMatch, - elseValue + elseValue, )}` scopedTs.add(stringIf) @@ -113,14 +113,14 @@ export const extractScopedTs = ({ while ((match = scopedTTemplatePattern.exec(fileContent))) { const templateLiteral = match[1] - const dynamicParts = templateLiteral.split(/(\$\{[^}]+\})/).map((part) => { + const dynamicParts = templateLiteral.split(/(\$\{[^}]+\})/).map(part => { if (part.startsWith('${') && part.endsWith('}')) { return '**' } return part }) const scopedTWithNamespace = `${namespaceTranslationTrimmed}.${dynamicParts.join( - '' + '', )}` scopedTs.add(scopedTWithNamespace) } diff --git a/src/utils/loadConfig.ts b/src/utils/loadConfig.ts index ffa0b3d..dc8d8ed 100644 --- a/src/utils/loadConfig.ts +++ b/src/utils/loadConfig.ts @@ -1,8 +1,9 @@ import * as fs from 'fs' import * as path from 'path' import { Config } from '../types' +import { build } from 'esbuild' -const supportedExtensions = ['.json', '.js', '.cjs'] +const supportedExtensions = ['.json', '.js', '.cjs', '.ts'] export const loadConfig = async (): Promise => { const cwd = process.cwd() @@ -18,7 +19,7 @@ export const loadConfig = async (): Promise => { if (!configPath) { throw new Error( - 'Configuration file unused-i18n.config not found. Supported extensions: .json, .js, .cjs.' + 'Configuration file unused-i18n.config not found. Supported extensions: .json, .js, .cjs, .ts.', ) } @@ -27,6 +28,22 @@ export const loadConfig = async (): Promise => { if (extension === '.json') { const configContent = fs.readFileSync(configPath, 'utf-8') return JSON.parse(configContent) as Config + } else if (extension === '.ts') { + const result = await build({ + entryPoints: [configPath], + outfile: 'config.js', + platform: 'node', + format: 'esm', + bundle: true, + write: false, + }) + + const jsCode = result.outputFiles[0].text + + const module = await import( + 'data:application/javascript,' + encodeURIComponent(jsCode) + ) + return module.default as Config } else { const module = await import(configPath) return module.default as Config diff --git a/tests/lib/scopedNamespace/extractNamespaceTranslation.test.ts b/tests/lib/scopedNamespace/extractNamespaceTranslation.test.ts index f79635c..30efdd0 100644 --- a/tests/lib/scopedNamespace/extractNamespaceTranslation.test.ts +++ b/tests/lib/scopedNamespace/extractNamespaceTranslation.test.ts @@ -22,6 +22,6 @@ describe('extractNamespaceTranslation', () => { const expected = ['namespace.keyTrue', 'namespace.keyFalse'] const result = extractNamespaceTranslation({ fileContent }) - expect(result).toEqual(expected) + expect(result).toEqual(expect.arrayContaining(expected)) }) }) diff --git a/vite.config.ts b/vite.config.ts index ff25a88..89afd1b 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,5 +1,6 @@ import { defineConfig } from 'vite' import type { UserConfig } from 'vitest/config' +import * as path from 'path'; const external = (id: string) => { if ( @@ -7,7 +8,8 @@ const external = (id: string) => { id.endsWith('path') || id.endsWith('perf_hooks') || id.endsWith('process') || - id.endsWith('commander') + id.endsWith('commander') || + id.endsWith('esbuild') ) { return true } @@ -44,6 +46,12 @@ export const defaultConfig: UserConfig = { reporter: ['text', 'json', 'html'], }, }, + resolve: { + alias: { + '@utils': path.resolve(__dirname, 'src/utils'), + '@lib': path.resolve(__dirname, 'src/lib'), + }, + }, } export default defineConfig(defaultConfig)