From 9ca691ae7fd1fc93290f540286b20e720529b504 Mon Sep 17 00:00:00 2001 From: Liam Rella Date: Fri, 14 Feb 2025 14:51:03 +1030 Subject: [PATCH] feat: upgrade command builder --- package.json | 8 +- pnpm-lock.yaml | 854 ++++++++++++++++++++++++++++++++++- scripts/build.ts | 17 +- src/command-builder.ts | 997 ++++++++++++++++++++++++----------------- src/execa-utils.ts | 12 + src/file-utils.ts | 15 - src/json-utils.ts | 13 +- src/node-utils.ts | 35 ++ src/path-utils.ts | 116 +++++ src/prettier-utils.ts | 30 ++ src/stream-utils.ts | 65 +++ src/string-utils.ts | 13 + 12 files changed, 1737 insertions(+), 438 deletions(-) create mode 100644 src/execa-utils.ts delete mode 100644 src/file-utils.ts create mode 100644 src/node-utils.ts create mode 100644 src/path-utils.ts create mode 100644 src/prettier-utils.ts create mode 100644 src/stream-utils.ts create mode 100644 src/string-utils.ts diff --git a/package.json b/package.json index fa3f4c4..0d5ee81 100644 --- a/package.json +++ b/package.json @@ -63,18 +63,24 @@ "@pinojs/json-colorizer": "^4.0.0", "@types/fs-extra": "^11.0.4", "@types/node": "^22.13.2", + "@types/yargs": "^17.0.33", "bumpp": "^10.0.3", "chalk": "^5.4.1", "cosmiconfig": "^9.0.0", + "decircular": "^1.0.0", "deepmerge": "^4.3.1", "esbuild": "^0.25.0", "execa": "^9.5.2", + "find-workspaces": "^0.3.1", "fs-extra": "^11.3.0", "json-stringify-pretty-compact": "^4.0.0", "mdat": "^0.10.0", "meow": "^13.2.0", "package-up": "^5.0.0", + "prettier": "^3.5.0", "tsx": "^4.19.2", - "typescript": "~5.7.3" + "typescript": "~5.7.3", + "vitest": "^3.0.5", + "yargs": "^17.7.2" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9801b38..7ffd133 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,6 +41,9 @@ importers: '@types/node': specifier: ^22.13.2 version: 22.13.2 + '@types/yargs': + specifier: ^17.0.33 + version: 17.0.33 bumpp: specifier: ^10.0.3 version: 10.0.3 @@ -50,6 +53,9 @@ importers: cosmiconfig: specifier: ^9.0.0 version: 9.0.0(typescript@5.7.3) + decircular: + specifier: ^1.0.0 + version: 1.0.0 deepmerge: specifier: ^4.3.1 version: 4.3.1 @@ -59,6 +65,9 @@ importers: execa: specifier: ^9.5.2 version: 9.5.2 + find-workspaces: + specifier: ^0.3.1 + version: 0.3.1 fs-extra: specifier: ^11.3.0 version: 11.3.0 @@ -74,12 +83,21 @@ importers: package-up: specifier: ^5.0.0 version: 5.0.0 + prettier: + specifier: ^3.5.0 + version: 3.5.0 tsx: specifier: ^4.19.2 version: 4.19.2 typescript: specifier: ~5.7.3 version: 5.7.3 + vitest: + specifier: ^3.0.5 + version: 3.0.5(@types/debug@4.1.12)(@types/node@22.13.2)(jiti@2.4.2)(tsx@4.19.2)(yaml@2.7.0) + yargs: + specifier: ^17.7.2 + version: 17.7.2 packages/browserslist-config: dependencies: @@ -98,21 +116,36 @@ importers: packages/cspell-config: dependencies: + '@cspell/cspell-types': + specifier: ^8.17.3 + version: 8.17.3 '@pinojs/json-colorizer': specifier: ^4.0.0 version: 4.0.0 + case-police: + specifier: ^1.0.0 + version: 1.0.0 cosmiconfig: specifier: ^9.0.0 version: 9.0.0(typescript@5.7.3) cspell: specifier: ^8.8.4 version: 8.17.3 + cspell-lib: + specifier: ^8.17.3 + version: 8.17.3 execa: specifier: ^9.5.2 version: 9.5.2 + find-workspaces: + specifier: ^0.3.1 + version: 0.3.1 fs-extra: specifier: ^11.2.0 version: 11.3.0 + prettier: + specifier: ^3.5.0 + version: 3.5.0 packages/eslint-config: dependencies: @@ -263,13 +296,16 @@ importers: execa: specifier: ^9.5.2 version: 9.5.2 + find-workspaces: + specifier: ^0.3.1 + version: 0.3.1 fs-extra: specifier: ^11.2.0 version: 11.3.0 + prettier: + specifier: ^3.5.0 + version: 3.5.0 devDependencies: - camelcase: - specifier: ^8.0.0 - version: 8.0.0 chalk: specifier: ^5.3.0 version: 5.4.1 @@ -304,6 +340,30 @@ importers: specifier: ^13.1.0 version: 13.1.0(postcss@8.5.2)(stylelint@16.14.1(typescript@5.7.3)) + packages/typescript-config: + dependencies: + '@pinojs/json-colorizer': + specifier: ^4.0.0 + version: 4.0.0 + cosmiconfig: + specifier: ^9.0.0 + version: 9.0.0(typescript@5.7.3) + execa: + specifier: ^9.5.2 + version: 9.5.2 + find-workspaces: + specifier: ^0.3.1 + version: 0.3.1 + fs-extra: + specifier: ^11.2.0 + version: 11.3.0 + prettier: + specifier: ^3.5.0 + version: 3.5.0 + typescript: + specifier: ~5.7.3 + version: 5.7.3 + packages: '@ampproject/remapping@2.3.0': @@ -600,6 +660,12 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.24.2': + resolution: {integrity: sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/aix-ppc64@0.25.0': resolution: {integrity: sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==} engines: {node: '>=18'} @@ -612,6 +678,12 @@ packages: cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.24.2': + resolution: {integrity: sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm64@0.25.0': resolution: {integrity: sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==} engines: {node: '>=18'} @@ -624,6 +696,12 @@ packages: cpu: [arm] os: [android] + '@esbuild/android-arm@0.24.2': + resolution: {integrity: sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-arm@0.25.0': resolution: {integrity: sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==} engines: {node: '>=18'} @@ -636,6 +714,12 @@ packages: cpu: [x64] os: [android] + '@esbuild/android-x64@0.24.2': + resolution: {integrity: sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/android-x64@0.25.0': resolution: {integrity: sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==} engines: {node: '>=18'} @@ -648,6 +732,12 @@ packages: cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.24.2': + resolution: {integrity: sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-arm64@0.25.0': resolution: {integrity: sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==} engines: {node: '>=18'} @@ -660,6 +750,12 @@ packages: cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.24.2': + resolution: {integrity: sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/darwin-x64@0.25.0': resolution: {integrity: sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==} engines: {node: '>=18'} @@ -672,6 +768,12 @@ packages: cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.24.2': + resolution: {integrity: sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-arm64@0.25.0': resolution: {integrity: sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==} engines: {node: '>=18'} @@ -684,6 +786,12 @@ packages: cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.24.2': + resolution: {integrity: sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/freebsd-x64@0.25.0': resolution: {integrity: sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==} engines: {node: '>=18'} @@ -696,6 +804,12 @@ packages: cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.24.2': + resolution: {integrity: sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm64@0.25.0': resolution: {integrity: sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==} engines: {node: '>=18'} @@ -708,6 +822,12 @@ packages: cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.24.2': + resolution: {integrity: sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-arm@0.25.0': resolution: {integrity: sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==} engines: {node: '>=18'} @@ -720,6 +840,12 @@ packages: cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.24.2': + resolution: {integrity: sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-ia32@0.25.0': resolution: {integrity: sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==} engines: {node: '>=18'} @@ -732,6 +858,12 @@ packages: cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.24.2': + resolution: {integrity: sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-loong64@0.25.0': resolution: {integrity: sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==} engines: {node: '>=18'} @@ -744,6 +876,12 @@ packages: cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.24.2': + resolution: {integrity: sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-mips64el@0.25.0': resolution: {integrity: sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==} engines: {node: '>=18'} @@ -756,6 +894,12 @@ packages: cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.24.2': + resolution: {integrity: sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-ppc64@0.25.0': resolution: {integrity: sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==} engines: {node: '>=18'} @@ -768,6 +912,12 @@ packages: cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.24.2': + resolution: {integrity: sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-riscv64@0.25.0': resolution: {integrity: sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==} engines: {node: '>=18'} @@ -780,6 +930,12 @@ packages: cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.24.2': + resolution: {integrity: sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-s390x@0.25.0': resolution: {integrity: sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==} engines: {node: '>=18'} @@ -792,12 +948,24 @@ packages: cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.24.2': + resolution: {integrity: sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/linux-x64@0.25.0': resolution: {integrity: sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==} engines: {node: '>=18'} cpu: [x64] os: [linux] + '@esbuild/netbsd-arm64@0.24.2': + resolution: {integrity: sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-arm64@0.25.0': resolution: {integrity: sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==} engines: {node: '>=18'} @@ -810,6 +978,12 @@ packages: cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.24.2': + resolution: {integrity: sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/netbsd-x64@0.25.0': resolution: {integrity: sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==} engines: {node: '>=18'} @@ -822,6 +996,12 @@ packages: cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-arm64@0.24.2': + resolution: {integrity: sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-arm64@0.25.0': resolution: {integrity: sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==} engines: {node: '>=18'} @@ -834,6 +1014,12 @@ packages: cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.24.2': + resolution: {integrity: sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/openbsd-x64@0.25.0': resolution: {integrity: sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==} engines: {node: '>=18'} @@ -846,6 +1032,12 @@ packages: cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.24.2': + resolution: {integrity: sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/sunos-x64@0.25.0': resolution: {integrity: sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==} engines: {node: '>=18'} @@ -858,6 +1050,12 @@ packages: cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.24.2': + resolution: {integrity: sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-arm64@0.25.0': resolution: {integrity: sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==} engines: {node: '>=18'} @@ -870,6 +1068,12 @@ packages: cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.24.2': + resolution: {integrity: sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-ia32@0.25.0': resolution: {integrity: sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==} engines: {node: '>=18'} @@ -882,6 +1086,12 @@ packages: cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.24.2': + resolution: {integrity: sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@esbuild/win32-x64@0.25.0': resolution: {integrity: sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==} engines: {node: '>=18'} @@ -1050,6 +1260,101 @@ packages: engines: {node: '>=18'} hasBin: true + '@rollup/rollup-android-arm-eabi@4.34.6': + resolution: {integrity: sha512-+GcCXtOQoWuC7hhX1P00LqjjIiS/iOouHXhMdiDSnq/1DGTox4SpUvO52Xm+div6+106r+TcvOeo/cxvyEyTgg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.34.6': + resolution: {integrity: sha512-E8+2qCIjciYUnCa1AiVF1BkRgqIGW9KzJeesQqVfyRITGQN+dFuoivO0hnro1DjT74wXLRZ7QF8MIbz+luGaJA==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.34.6': + resolution: {integrity: sha512-z9Ib+OzqN3DZEjX7PDQMHEhtF+t6Mi2z/ueChQPLS/qUMKY7Ybn5A2ggFoKRNRh1q1T03YTQfBTQCJZiepESAg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.34.6': + resolution: {integrity: sha512-PShKVY4u0FDAR7jskyFIYVyHEPCPnIQY8s5OcXkdU8mz3Y7eXDJPdyM/ZWjkYdR2m0izD9HHWA8sGcXn+Qrsyg==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.34.6': + resolution: {integrity: sha512-YSwyOqlDAdKqs0iKuqvRHLN4SrD2TiswfoLfvYXseKbL47ht1grQpq46MSiQAx6rQEN8o8URtpXARCpqabqxGQ==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.34.6': + resolution: {integrity: sha512-HEP4CgPAY1RxXwwL5sPFv6BBM3tVeLnshF03HMhJYCNc6kvSqBgTMmsEjb72RkZBAWIqiPUyF1JpEBv5XT9wKQ==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.34.6': + resolution: {integrity: sha512-88fSzjC5xeH9S2Vg3rPgXJULkHcLYMkh8faix8DX4h4TIAL65ekwuQMA/g2CXq8W+NJC43V6fUpYZNjaX3+IIg==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.34.6': + resolution: {integrity: sha512-wM4ztnutBqYFyvNeR7Av+reWI/enK9tDOTKNF+6Kk2Q96k9bwhDDOlnCUNRPvromlVXo04riSliMBs/Z7RteEg==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.34.6': + resolution: {integrity: sha512-9RyprECbRa9zEjXLtvvshhw4CMrRa3K+0wcp3KME0zmBe1ILmvcVHnypZ/aIDXpRyfhSYSuN4EPdCCj5Du8FIA==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.34.6': + resolution: {integrity: sha512-qTmklhCTyaJSB05S+iSovfo++EwnIEZxHkzv5dep4qoszUMX5Ca4WM4zAVUMbfdviLgCSQOu5oU8YoGk1s6M9Q==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loongarch64-gnu@4.34.6': + resolution: {integrity: sha512-4Qmkaps9yqmpjY5pvpkfOerYgKNUGzQpFxV6rnS7c/JfYbDSU0y6WpbbredB5cCpLFGJEqYX40WUmxMkwhWCjw==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-powerpc64le-gnu@4.34.6': + resolution: {integrity: sha512-Zsrtux3PuaxuBTX/zHdLaFmcofWGzaWW1scwLU3ZbW/X+hSsFbz9wDIp6XvnT7pzYRl9MezWqEqKy7ssmDEnuQ==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.34.6': + resolution: {integrity: sha512-aK+Zp+CRM55iPrlyKiU3/zyhgzWBxLVrw2mwiQSYJRobCURb781+XstzvA8Gkjg/hbdQFuDw44aUOxVQFycrAg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.34.6': + resolution: {integrity: sha512-WoKLVrY9ogmaYPXwTH326+ErlCIgMmsoRSx6bO+l68YgJnlOXhygDYSZe/qbUJCSiCiZAQ+tKm88NcWuUXqOzw==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.34.6': + resolution: {integrity: sha512-Sht4aFvmA4ToHd2vFzwMFaQCiYm2lDFho5rPcvPBT5pCdC+GwHG6CMch4GQfmWTQ1SwRKS0dhDYb54khSrjDWw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.34.6': + resolution: {integrity: sha512-zmmpOQh8vXc2QITsnCiODCDGXFC8LMi64+/oPpPx5qz3pqv0s6x46ps4xoycfUiVZps5PFn1gksZzo4RGTKT+A==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-win32-arm64-msvc@4.34.6': + resolution: {integrity: sha512-3/q1qUsO/tLqGBaD4uXsB6coVGB3usxw3qyeVb59aArCgedSF66MPdgRStUd7vbZOsko/CgVaY5fo2vkvPLWiA==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.34.6': + resolution: {integrity: sha512-oLHxuyywc6efdKVTxvc0135zPrRdtYVjtVD5GUm55I3ODxhU/PwkQFD97z16Xzxa1Fz0AEe4W/2hzRtd+IfpOA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.34.6': + resolution: {integrity: sha512-0PVwmgzZ8+TZ9oGBmdZoQVXflbvuwzN/HRclujpl4N/q3i+y0lqLw8n1bXA8ru3sApDjlmONaNAuYr38y1Kr9w==} + cpu: [x64] + os: [win32] + '@sec-ant/readable-stream@0.4.1': resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} @@ -1165,6 +1470,35 @@ packages: resolution: {integrity: sha512-kArLq83QxGLbuHrTMoOEWO+l2MwsNS2TGISEdx8xgqpkbytB07XmlQyQdNDrCc1ecSqx0cnmhGvpX+VBwqqSkg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@vitest/expect@3.0.5': + resolution: {integrity: sha512-nNIOqupgZ4v5jWuQx2DSlHLEs7Q4Oh/7AYwNyE+k0UQzG7tSmjPXShUikn1mpNGzYEN2jJbTvLejwShMitovBA==} + + '@vitest/mocker@3.0.5': + resolution: {integrity: sha512-CLPNBFBIE7x6aEGbIjaQAX03ZZlBMaWwAjBdMkIf/cAn6xzLTiM3zYqO/WAbieEjsAZir6tO71mzeHZoodThvw==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@3.0.5': + resolution: {integrity: sha512-CjUtdmpOcm4RVtB+up8r2vVDLR16Mgm/bYdkGFe3Yj/scRfCpbSi2W/BDSDcFK7ohw8UXvjMbOp9H4fByd/cOA==} + + '@vitest/runner@3.0.5': + resolution: {integrity: sha512-BAiZFityFexZQi2yN4OX3OkJC6scwRo8EhRB0Z5HIGGgd2q+Nq29LgHU/+ovCtd0fOfXj5ZI6pwdlUmC5bpi8A==} + + '@vitest/snapshot@3.0.5': + resolution: {integrity: sha512-GJPZYcd7v8QNUJ7vRvLDmRwl+a1fGg4T/54lZXe+UOGy47F9yUfE18hRCtXL5aHN/AONu29NGzIXSVFh9K0feA==} + + '@vitest/spy@3.0.5': + resolution: {integrity: sha512-5fOzHj0WbUNqPK6blI/8VzZdkBlQLnT25knX0r4dbZI9qoZDf3qAdjoMmDcLG5A83W6oUUFJgUd0EYBc2P5xqg==} + + '@vitest/utils@3.0.5': + resolution: {integrity: sha512-N9AX0NUoUtVwKwy21JtwzaqR5L5R5A99GAbrHfCCXK1lp593i/3AZAXhSP43wRQuxYsflrdzEfXZFo1reR1Nkg==} + '@xml-tools/parser@1.0.11': resolution: {integrity: sha512-aKqQ077XnR+oQtHJlrAflaZaL7qZsulWc/i/ZEooar5JiWj1eLt0+Wg28cpa+XLney107wXqneC+oG1IZvxkTA==} @@ -1241,6 +1575,10 @@ packages: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + ast-types@0.13.4: resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} engines: {node: '>=4'} @@ -1365,13 +1703,17 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} - camelcase@8.0.0: - resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==} - engines: {node: '>=16'} - caniuse-lite@1.0.30001699: resolution: {integrity: sha512-b+uH5BakXZ9Do9iK+CkDmctUSEqZl+SP056vc5usa0PL+ev5OHw003rZXcnjNDv3L8P5j6rwT6C0BPKSikW08w==} + case-police@1.0.0: + resolution: {integrity: sha512-THHWd3TZmtB5sSrd8bt5t0shPQ/MVBXWChIULc9Fome4NN4SagoxNlc/V+lpl5uA4pNcCsBv5Z3wMmCqlYilxw==} + hasBin: true + + chai@5.1.2: + resolution: {integrity: sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==} + engines: {node: '>=12'} + chalk-template@1.1.0: resolution: {integrity: sha512-T2VJbcDuZQ0Tb2EWwSotMPJjgpy1/tGee1BTpUNsGZ/qgNjV2t7Mvu+d4600U564nbLesN1x2dPL+xii174Ekg==} engines: {node: '>=14.16'} @@ -1384,6 +1726,10 @@ packages: resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + check-error@2.1.1: + resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} + engines: {node: '>= 16'} + chevrotain@7.1.1: resolution: {integrity: sha512-wy3mC1x4ye+O+QkEinVJkPf5u2vsrDIYW9G7ZuwFl6v/Yu0LwUuT2POsb+NUWApebyxfkQq6+yDfRExbnI5rcw==} @@ -1581,6 +1927,14 @@ packages: supports-color: optional: true + decircular@1.0.0: + resolution: {integrity: sha512-YhCtYW0jQs9+gzL2vDLxanRhMHeKa55kw5z2oheI6D+MQA7KafrqtiGKhhpKCLZQurm2a9h0LkP9T5z5gy+A0A==} + engines: {node: '>=18'} + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -1679,6 +2033,9 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} + es-module-lexer@1.6.0: + resolution: {integrity: sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==} + es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} @@ -1688,6 +2045,11 @@ packages: engines: {node: '>=18'} hasBin: true + esbuild@0.24.2: + resolution: {integrity: sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==} + engines: {node: '>=18'} + hasBin: true + esbuild@0.25.0: resolution: {integrity: sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==} engines: {node: '>=18'} @@ -1812,6 +2174,9 @@ packages: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} @@ -1824,6 +2189,10 @@ packages: resolution: {integrity: sha512-EHlpxMCpHWSAh1dgS6bVeoLAXGnJNdR93aabr4QCGbzOM73o5XmRfM/e5FUqsw3aagP8S8XEWUWFAxnRBnAF0Q==} engines: {node: ^18.19.0 || >=20.5.0} + expect-type@1.1.0: + resolution: {integrity: sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==} + engines: {node: '>=12.0.0'} + express@4.21.2: resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} engines: {node: '>= 0.10.0'} @@ -1915,6 +2284,9 @@ packages: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} + find-workspaces@0.3.1: + resolution: {integrity: sha512-UDkGILGJSA1LN5Aa7McxCid4sqW3/e+UYsVwyxki3dDT0F8+ym0rAfnCkEfkL0rO7M+8/mvkim4t/s3IPHmg+w==} + flat-cache@4.0.1: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} @@ -2331,6 +2703,9 @@ packages: lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + loupe@3.1.3: + resolution: {integrity: sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==} + lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -2627,6 +3002,10 @@ packages: pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + pathval@2.0.0: + resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} + engines: {node: '>= 14.16'} + pend@1.2.0: resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} @@ -2944,6 +3323,11 @@ packages: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rollup@4.34.6: + resolution: {integrity: sha512-wc2cBWqJgkU3Iz5oztRkQbfVkbxoz5EhnCGOrnJvnLnQ7O0WhQUYyv18qQI79O8L7DdHrrlJNeCHd4VGpnaXKQ==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -3001,6 +3385,9 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} @@ -3055,10 +3442,16 @@ packages: sprintf-js@1.1.3: resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + statuses@2.0.1: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} + std-env@3.8.0: + resolution: {integrity: sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==} + streamx@2.22.0: resolution: {integrity: sha512-sLh1evHOzBy/iWRiR6d1zRcLao4gGZr3C1kzNz4fopCOKJb6xD9ub8Mpi9Mr1R6id5o43S+d93fI48UC5uM9aw==} @@ -3203,6 +3596,9 @@ packages: text-decoder@1.2.3: resolution: {integrity: sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} @@ -3210,6 +3606,18 @@ packages: resolution: {integrity: sha512-Zc+8eJlFMvgatPZTl6A9L/yht8QqdmUNtURHaKZLmKBE12hNPSrqNkUp2cs3M/UKmNVVAMFQYSjYIVHDjW5zew==} engines: {node: '>=12.0.0'} + tinypool@1.0.2: + resolution: {integrity: sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyspy@3.0.2: + resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} + engines: {node: '>=14.0.0'} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -3360,6 +3768,79 @@ packages: vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + vite-node@3.0.5: + resolution: {integrity: sha512-02JEJl7SbtwSDJdYS537nU6l+ktdvcREfLksk/NDAqtdKWGqHl+joXzEubHROmS3E6pip+Xgu2tFezMu75jH7A==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + + vite@6.1.0: + resolution: {integrity: sha512-RjjMipCKVoR4hVfPY6GQTgveinjNuyLw+qruksLDvA5ktI1150VmcMBKmQaEWJhg/j6Uaf6dNCNA0AfdzUb/hQ==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@3.0.5: + resolution: {integrity: sha512-4dof+HvqONw9bvsYxtkfUp2uHsTN9bV2CZIi1pWgoFpL1Lld8LA1ka9q/ONSsoScAKG7NVGf2stJTI7XRkXb2Q==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.0.5 + '@vitest/ui': 3.0.5 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + vscode-languageserver-textdocument@1.0.12: resolution: {integrity: sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==} @@ -3388,6 +3869,11 @@ packages: engines: {node: ^18.17.0 || >=20.5.0} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} @@ -3746,147 +4232,222 @@ snapshots: '@esbuild/aix-ppc64@0.23.1': optional: true + '@esbuild/aix-ppc64@0.24.2': + optional: true + '@esbuild/aix-ppc64@0.25.0': optional: true '@esbuild/android-arm64@0.23.1': optional: true + '@esbuild/android-arm64@0.24.2': + optional: true + '@esbuild/android-arm64@0.25.0': optional: true '@esbuild/android-arm@0.23.1': optional: true + '@esbuild/android-arm@0.24.2': + optional: true + '@esbuild/android-arm@0.25.0': optional: true '@esbuild/android-x64@0.23.1': optional: true + '@esbuild/android-x64@0.24.2': + optional: true + '@esbuild/android-x64@0.25.0': optional: true '@esbuild/darwin-arm64@0.23.1': optional: true + '@esbuild/darwin-arm64@0.24.2': + optional: true + '@esbuild/darwin-arm64@0.25.0': optional: true '@esbuild/darwin-x64@0.23.1': optional: true + '@esbuild/darwin-x64@0.24.2': + optional: true + '@esbuild/darwin-x64@0.25.0': optional: true '@esbuild/freebsd-arm64@0.23.1': optional: true + '@esbuild/freebsd-arm64@0.24.2': + optional: true + '@esbuild/freebsd-arm64@0.25.0': optional: true '@esbuild/freebsd-x64@0.23.1': optional: true + '@esbuild/freebsd-x64@0.24.2': + optional: true + '@esbuild/freebsd-x64@0.25.0': optional: true '@esbuild/linux-arm64@0.23.1': optional: true + '@esbuild/linux-arm64@0.24.2': + optional: true + '@esbuild/linux-arm64@0.25.0': optional: true '@esbuild/linux-arm@0.23.1': optional: true + '@esbuild/linux-arm@0.24.2': + optional: true + '@esbuild/linux-arm@0.25.0': optional: true '@esbuild/linux-ia32@0.23.1': optional: true + '@esbuild/linux-ia32@0.24.2': + optional: true + '@esbuild/linux-ia32@0.25.0': optional: true '@esbuild/linux-loong64@0.23.1': optional: true + '@esbuild/linux-loong64@0.24.2': + optional: true + '@esbuild/linux-loong64@0.25.0': optional: true '@esbuild/linux-mips64el@0.23.1': optional: true + '@esbuild/linux-mips64el@0.24.2': + optional: true + '@esbuild/linux-mips64el@0.25.0': optional: true '@esbuild/linux-ppc64@0.23.1': optional: true + '@esbuild/linux-ppc64@0.24.2': + optional: true + '@esbuild/linux-ppc64@0.25.0': optional: true '@esbuild/linux-riscv64@0.23.1': optional: true + '@esbuild/linux-riscv64@0.24.2': + optional: true + '@esbuild/linux-riscv64@0.25.0': optional: true '@esbuild/linux-s390x@0.23.1': optional: true + '@esbuild/linux-s390x@0.24.2': + optional: true + '@esbuild/linux-s390x@0.25.0': optional: true '@esbuild/linux-x64@0.23.1': optional: true + '@esbuild/linux-x64@0.24.2': + optional: true + '@esbuild/linux-x64@0.25.0': optional: true + '@esbuild/netbsd-arm64@0.24.2': + optional: true + '@esbuild/netbsd-arm64@0.25.0': optional: true '@esbuild/netbsd-x64@0.23.1': optional: true + '@esbuild/netbsd-x64@0.24.2': + optional: true + '@esbuild/netbsd-x64@0.25.0': optional: true '@esbuild/openbsd-arm64@0.23.1': optional: true + '@esbuild/openbsd-arm64@0.24.2': + optional: true + '@esbuild/openbsd-arm64@0.25.0': optional: true '@esbuild/openbsd-x64@0.23.1': optional: true + '@esbuild/openbsd-x64@0.24.2': + optional: true + '@esbuild/openbsd-x64@0.25.0': optional: true '@esbuild/sunos-x64@0.23.1': optional: true + '@esbuild/sunos-x64@0.24.2': + optional: true + '@esbuild/sunos-x64@0.25.0': optional: true '@esbuild/win32-arm64@0.23.1': optional: true + '@esbuild/win32-arm64@0.24.2': + optional: true + '@esbuild/win32-arm64@0.25.0': optional: true '@esbuild/win32-ia32@0.23.1': optional: true + '@esbuild/win32-ia32@0.24.2': + optional: true + '@esbuild/win32-ia32@0.25.0': optional: true '@esbuild/win32-x64@0.23.1': optional: true + '@esbuild/win32-x64@0.24.2': + optional: true + '@esbuild/win32-x64@0.25.0': optional: true @@ -4096,6 +4657,63 @@ snapshots: - bare-buffer - supports-color + '@rollup/rollup-android-arm-eabi@4.34.6': + optional: true + + '@rollup/rollup-android-arm64@4.34.6': + optional: true + + '@rollup/rollup-darwin-arm64@4.34.6': + optional: true + + '@rollup/rollup-darwin-x64@4.34.6': + optional: true + + '@rollup/rollup-freebsd-arm64@4.34.6': + optional: true + + '@rollup/rollup-freebsd-x64@4.34.6': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.34.6': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.34.6': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.34.6': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.34.6': + optional: true + + '@rollup/rollup-linux-loongarch64-gnu@4.34.6': + optional: true + + '@rollup/rollup-linux-powerpc64le-gnu@4.34.6': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.34.6': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.34.6': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.34.6': + optional: true + + '@rollup/rollup-linux-x64-musl@4.34.6': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.34.6': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.34.6': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.34.6': + optional: true + '@sec-ant/readable-stream@0.4.1': {} '@sindresorhus/merge-streams@2.3.0': {} @@ -4237,6 +4855,46 @@ snapshots: '@typescript-eslint/types': 8.24.0 eslint-visitor-keys: 4.2.0 + '@vitest/expect@3.0.5': + dependencies: + '@vitest/spy': 3.0.5 + '@vitest/utils': 3.0.5 + chai: 5.1.2 + tinyrainbow: 2.0.0 + + '@vitest/mocker@3.0.5(vite@6.1.0(@types/node@22.13.2)(jiti@2.4.2)(tsx@4.19.2)(yaml@2.7.0))': + dependencies: + '@vitest/spy': 3.0.5 + estree-walker: 3.0.3 + magic-string: 0.30.17 + optionalDependencies: + vite: 6.1.0(@types/node@22.13.2)(jiti@2.4.2)(tsx@4.19.2)(yaml@2.7.0) + + '@vitest/pretty-format@3.0.5': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/runner@3.0.5': + dependencies: + '@vitest/utils': 3.0.5 + pathe: 2.0.3 + + '@vitest/snapshot@3.0.5': + dependencies: + '@vitest/pretty-format': 3.0.5 + magic-string: 0.30.17 + pathe: 2.0.3 + + '@vitest/spy@3.0.5': + dependencies: + tinyspy: 3.0.2 + + '@vitest/utils@3.0.5': + dependencies: + '@vitest/pretty-format': 3.0.5 + loupe: 3.1.3 + tinyrainbow: 2.0.0 + '@xml-tools/parser@1.0.11': dependencies: chevrotain: 7.1.1 @@ -4304,6 +4962,8 @@ snapshots: array-union@2.1.0: {} + assertion-error@2.0.1: {} + ast-types@0.13.4: dependencies: tslib: 2.8.1 @@ -4451,10 +5111,18 @@ snapshots: callsites@3.1.0: {} - camelcase@8.0.0: {} - caniuse-lite@1.0.30001699: {} + case-police@1.0.0: {} + + chai@5.1.2: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.1 + deep-eql: 5.0.2 + loupe: 3.1.3 + pathval: 2.0.0 + chalk-template@1.1.0: dependencies: chalk: 5.4.1 @@ -4466,6 +5134,8 @@ snapshots: chalk@5.4.1: {} + check-error@2.1.1: {} + chevrotain@7.1.1: dependencies: regexp-to-ast: 0.5.0 @@ -4691,6 +5361,10 @@ snapshots: dependencies: ms: 2.1.3 + decircular@1.0.0: {} + + deep-eql@5.0.2: {} + deep-is@0.1.4: {} deepmerge@4.3.1: {} @@ -4763,6 +5437,8 @@ snapshots: es-errors@1.3.0: {} + es-module-lexer@1.6.0: {} + es-object-atoms@1.1.1: dependencies: es-errors: 1.3.0 @@ -4794,6 +5470,34 @@ snapshots: '@esbuild/win32-ia32': 0.23.1 '@esbuild/win32-x64': 0.23.1 + esbuild@0.24.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.24.2 + '@esbuild/android-arm': 0.24.2 + '@esbuild/android-arm64': 0.24.2 + '@esbuild/android-x64': 0.24.2 + '@esbuild/darwin-arm64': 0.24.2 + '@esbuild/darwin-x64': 0.24.2 + '@esbuild/freebsd-arm64': 0.24.2 + '@esbuild/freebsd-x64': 0.24.2 + '@esbuild/linux-arm': 0.24.2 + '@esbuild/linux-arm64': 0.24.2 + '@esbuild/linux-ia32': 0.24.2 + '@esbuild/linux-loong64': 0.24.2 + '@esbuild/linux-mips64el': 0.24.2 + '@esbuild/linux-ppc64': 0.24.2 + '@esbuild/linux-riscv64': 0.24.2 + '@esbuild/linux-s390x': 0.24.2 + '@esbuild/linux-x64': 0.24.2 + '@esbuild/netbsd-arm64': 0.24.2 + '@esbuild/netbsd-x64': 0.24.2 + '@esbuild/openbsd-arm64': 0.24.2 + '@esbuild/openbsd-x64': 0.24.2 + '@esbuild/sunos-x64': 0.24.2 + '@esbuild/win32-arm64': 0.24.2 + '@esbuild/win32-ia32': 0.24.2 + '@esbuild/win32-x64': 0.24.2 + esbuild@0.25.0: optionalDependencies: '@esbuild/aix-ppc64': 0.25.0 @@ -4980,6 +5684,10 @@ snapshots: estraverse@5.3.0: {} + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.6 + esutils@2.0.3: {} etag@1.8.1: {} @@ -4999,6 +5707,8 @@ snapshots: strip-final-newline: 4.0.0 yoctocolors: 2.1.1 + expect-type@1.1.0: {} + express@4.21.2: dependencies: accepts: 1.3.8 @@ -5127,6 +5837,12 @@ snapshots: locate-path: 6.0.0 path-exists: 4.0.0 + find-workspaces@0.3.1: + dependencies: + fast-glob: 3.3.3 + pkg-types: 1.3.1 + yaml: 2.7.0 + flat-cache@4.0.1: dependencies: flatted: 3.3.2 @@ -5504,6 +6220,8 @@ snapshots: lodash@4.17.21: {} + loupe@3.1.3: {} + lru-cache@10.4.3: {} lru-cache@7.18.3: {} @@ -5815,6 +6533,8 @@ snapshots: pathe@2.0.3: {} + pathval@2.0.0: {} + pend@1.2.0: {} perfect-debounce@1.0.0: {} @@ -6080,6 +6800,31 @@ snapshots: reusify@1.0.4: {} + rollup@4.34.6: + dependencies: + '@types/estree': 1.0.6 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.34.6 + '@rollup/rollup-android-arm64': 4.34.6 + '@rollup/rollup-darwin-arm64': 4.34.6 + '@rollup/rollup-darwin-x64': 4.34.6 + '@rollup/rollup-freebsd-arm64': 4.34.6 + '@rollup/rollup-freebsd-x64': 4.34.6 + '@rollup/rollup-linux-arm-gnueabihf': 4.34.6 + '@rollup/rollup-linux-arm-musleabihf': 4.34.6 + '@rollup/rollup-linux-arm64-gnu': 4.34.6 + '@rollup/rollup-linux-arm64-musl': 4.34.6 + '@rollup/rollup-linux-loongarch64-gnu': 4.34.6 + '@rollup/rollup-linux-powerpc64le-gnu': 4.34.6 + '@rollup/rollup-linux-riscv64-gnu': 4.34.6 + '@rollup/rollup-linux-s390x-gnu': 4.34.6 + '@rollup/rollup-linux-x64-gnu': 4.34.6 + '@rollup/rollup-linux-x64-musl': 4.34.6 + '@rollup/rollup-win32-arm64-msvc': 4.34.6 + '@rollup/rollup-win32-ia32-msvc': 4.34.6 + '@rollup/rollup-win32-x64-msvc': 4.34.6 + fsevents: 2.3.3 + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -6159,6 +6904,8 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} + signal-exit@4.1.0: {} sisteransi@1.0.5: {} @@ -6209,8 +6956,12 @@ snapshots: sprintf-js@1.1.3: {} + stackback@0.0.2: {} + statuses@2.0.1: {} + std-env@3.8.0: {} + streamx@2.22.0: dependencies: fast-fifo: 1.3.2 @@ -6433,6 +7184,8 @@ snapshots: dependencies: b4a: 1.6.7 + tinybench@2.9.0: {} + tinyexec@0.3.2: {} tinyglobby@0.2.10: @@ -6440,6 +7193,12 @@ snapshots: fdir: 6.4.3(picomatch@4.0.2) picomatch: 4.0.2 + tinypool@1.0.2: {} + + tinyrainbow@2.0.0: {} + + tinyspy@3.0.2: {} + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -6609,6 +7368,78 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.2 + vite-node@3.0.5(@types/node@22.13.2)(jiti@2.4.2)(tsx@4.19.2)(yaml@2.7.0): + dependencies: + cac: 6.7.14 + debug: 4.4.0 + es-module-lexer: 1.6.0 + pathe: 2.0.3 + vite: 6.1.0(@types/node@22.13.2)(jiti@2.4.2)(tsx@4.19.2)(yaml@2.7.0) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vite@6.1.0(@types/node@22.13.2)(jiti@2.4.2)(tsx@4.19.2)(yaml@2.7.0): + dependencies: + esbuild: 0.24.2 + postcss: 8.5.2 + rollup: 4.34.6 + optionalDependencies: + '@types/node': 22.13.2 + fsevents: 2.3.3 + jiti: 2.4.2 + tsx: 4.19.2 + yaml: 2.7.0 + + vitest@3.0.5(@types/debug@4.1.12)(@types/node@22.13.2)(jiti@2.4.2)(tsx@4.19.2)(yaml@2.7.0): + dependencies: + '@vitest/expect': 3.0.5 + '@vitest/mocker': 3.0.5(vite@6.1.0(@types/node@22.13.2)(jiti@2.4.2)(tsx@4.19.2)(yaml@2.7.0)) + '@vitest/pretty-format': 3.0.5 + '@vitest/runner': 3.0.5 + '@vitest/snapshot': 3.0.5 + '@vitest/spy': 3.0.5 + '@vitest/utils': 3.0.5 + chai: 5.1.2 + debug: 4.4.0 + expect-type: 1.1.0 + magic-string: 0.30.17 + pathe: 2.0.3 + std-env: 3.8.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinypool: 1.0.2 + tinyrainbow: 2.0.0 + vite: 6.1.0(@types/node@22.13.2)(jiti@2.4.2)(tsx@4.19.2)(yaml@2.7.0) + vite-node: 3.0.5(@types/node@22.13.2)(jiti@2.4.2)(tsx@4.19.2)(yaml@2.7.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/debug': 4.1.12 + '@types/node': 22.13.2 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + vscode-languageserver-textdocument@1.0.12: {} vscode-uri@3.1.0: {} @@ -6631,6 +7462,11 @@ snapshots: dependencies: isexe: 3.1.1 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + word-wrap@1.2.5: {} wrap-ansi@7.0.0: diff --git a/scripts/build.ts b/scripts/build.ts index a1950a3..2ac234a 100755 --- a/scripts/build.ts +++ b/scripts/build.ts @@ -4,10 +4,23 @@ import { build } from 'esbuild'; await build({ bundle: true, entryPoints: ['src/cli.ts'], - external: ['execa', '@pinojs/json-colorizer', 'cosmiconfig', 'fs-extra'], + external: [ + 'execa', + '@pinojs/json-colorizer', + 'cosmiconfig', + 'fs-extra', + 'find-workspaces', + 'cspell', + 'knip', + 'cspell-lib', + 'eslint', + 'mdat', + 'prettier', + 'stylelint', + ], format: 'esm', minify: true, outfile: 'bin/cli.js', platform: 'node', - target: 'node18', + target: 'node22', }); diff --git a/src/command-builder.ts b/src/command-builder.ts index 21b82db..5c7616e 100644 --- a/src/command-builder.ts +++ b/src/command-builder.ts @@ -1,498 +1,683 @@ #!/usr/bin/env node -// Creates cli bin files for each package -// based on the shared-config field in their package.js - -import type { Flag } from 'meow'; - -import prettierConfig from '$root/prettier.config.js'; +import type internal from 'node:stream'; // eslint-disable-next-line unicorn/import-style import chalk, { type foregroundColorNames } from 'chalk'; -import { cosmiconfig } from 'cosmiconfig'; -import { execa, type ExecaError } from 'execa'; +import { cosmiconfig, type CosmiconfigResult } from 'cosmiconfig'; +import { execa } from 'execa'; + import fse from 'fs-extra'; -import meow from 'meow'; +import fs from 'node:fs'; import path from 'node:path'; -import { PassThrough, Transform, type Stream } from 'node:stream'; +import { PassThrough } from 'node:stream'; import { fileURLToPath } from 'node:url'; import { packageUp } from 'package-up'; -import prettier from 'prettier'; +import yargs from 'yargs'; +import { hideBin } from 'yargs/helpers'; +import { version } from '../package.json'; +import { isErrorExecaError } from './execa-utils.js'; import { merge, stringify } from './json-utils.ts'; - -// TODO get these from meow? -type StringFlag = Flag<'string', string> | Flag<'string', string[], true>; -type BooleanFlag = Flag<'boolean', boolean> | Flag<'boolean', boolean[], true>; -type NumberFlag = Flag<'number', number> | Flag<'number', number[], true>; -type AnyFlag = BooleanFlag | NumberFlag | StringFlag; -type AnyFlags = Record; +import { type CwdOverrideOptions, getCwdOverride } from './path-utils.js'; +import { formatFileInPlace } from './prettier-utils.js'; +import { createStreamTransform, streamToString } from './stream-utils.ts'; +import { pluralize } from './string-utils.js'; type ChalkColor = (typeof foregroundColorNames)[number]; -interface OptionCommand { - /** Either a string to run a command, or a function to do something custom. If undefined, a default behavior is used. */ - command?: - | (( - /** Useful if you're logging in the function, ensures output is prefixed */ - _logStream: NodeJS.WritableStream, - _args: string[], - _options: string[], - ) => Promise) - | string; - /** Arguments to be passed to the command in the absence of user-provided arguments */ - defaultArguments?: string[]; - /** Options to be passed to the command. The argument is handled in command-builder.ts */ - options?: string[]; +interface CommandCommon { + /** Customizes color of log prefix string. Default color used if undefined. */ + logColor?: ChalkColor; + /** Enables a string prefix in the log output. False if undefined */ + logPrefix?: string; + /** CLI command name to execute, or function name to be used in logs */ + name: string; } -// Supported options -type OptionCommands = Partial>; - -function createStreamTransform(logPrefix: string | undefined, logColor: ChalkColor): Transform { - return new Transform({ - transform(chunk: string | Uint8Array, _: BufferEncoding, callback) { - // Convert the chunk to a string and prepend the string to each line - const lines: string[] = chunk - .toString() - .split(/\r?\n/) - .filter((line) => line.trim().length > 0); - - const transformed = - lines.map((line) => `${logPrefix ? chalk[logColor](logPrefix) : ''} ${line}`).join('\n') + - '\n'; - - // Pass the transformed data to the next stage in the stream - this.push(transformed); - callback(); - }, - }); +type CommandFunction = CommandCommon & { + execute: ( + _logStream: NodeJS.WritableStream, + _positionalArguments: string[], // Passed by default, but can be ignored in implementation + _optionFlags: string[], // Passed by default, but can be ignored in implementation + ) => Promise; +}; + +export type CommandCli = CommandCommon & { + /** Optionally change the context where the command is executed. Defaults to `process.cwd()` if undefined. */ + cwdOverride?: CwdOverrideOptions; + /** Command-local fixed option flags. */ + optionFlags?: string[]; + /** Command-local fixed positional arguments. */ + positionalArguments?: string[]; + /** Formats and colorizes output if JSON. False if undefined */ + prettyJsonOutput?: boolean; + /** If true, option flags are passed from the parent command. False if undefined. */ + receiveOptionFlags?: boolean; + /** If true, positional arguments are passed in from the parent command. False if undefined. */ + receivePositionalArguments?: boolean; + /** Comes immediately after the command */ + subcommands?: string[]; +}; + +export type Command = CommandCli | CommandFunction; + +// Init +// Optionally takes --location option flag +interface InitCommand { + /** Optional additional commands to run */ + commands?: Command[]; + /** Specific config file */ + configFile?: string; + configPackageJson?: Record; + /** Optional, just used for top-level shared-config command */ + description?: string; + locationOptionFlag: boolean; } -function generateHelpText(command: string, options: OptionCommands): string { - let usageText = `$ ${command} [ ...]`; +// Lint +// Optionally takes files (plural) positional arguments (array of strings, possibly expanded from glob?) +interface LintCommand { + commands: Command[]; + description: string; + positionalArgumentDefault?: string; // Only applies if arguments mode is not 'none' + positionalArgumentMode: 'none' | 'optional' | 'required'; +} - if (command === 'browserslist-config') { - usageText = `$ ${command} --init`; - } +// Fix +// Same as lint for now +type FixCommand = LintCommand; - let helpText = `Usage - ${usageText} +// Print Config +// Same as lint for now, Optionally takes file (singular) positional argument +type PrintConfigCommand = LintCommand; - Options`; +export interface Commands { + fix?: FixCommand; + init?: InitCommand; + lint?: LintCommand; + printConfig?: PrintConfigCommand; +} - if (Object.keys(options).length > 0) { - for (const name of Object.keys(options)) { - switch (name) { - case 'init': { - helpText += - command === 'browserslist-config' - ? '\n --init, -i Add browserslist key to `package.json`.' - : '\n --init, -i Initialize by copying starter config files to your project root.'; - break; - } +// Exported for aggregation later +export interface CommandDefinition { + commands: Commands; + description: string; + logColor: ChalkColor; + logPrefix: string | undefined; + name: string; + order: number; + showSummary?: boolean; + verbose?: boolean; +} - case 'check': { - helpText += `\n --check, -c Check for and report issues. Same as \`${command}\`.`; - break; - } +async function executeFunctionCommand( + logStream: NodeJS.WritableStream, + positionalArguments: string[], + optionFlags: string[], + command: CommandFunction, + verbose?: boolean, +): Promise { + let exitCode = 1; // Assume failure - case 'fix': { - helpText += - '\n --fix, -f Fix all auto-fixable issues, and report the un-fixable.'; - break; - } + // Add to the log stream if desired + let targetStream: NodeJS.WritableStream; - case 'printConfig': { - helpText += - '\n --print-config, -p Print the effective configuration at a certain path.'; - break; - } + if (command.logPrefix === undefined) { + targetStream = logStream; + } else { + const subStream = createStreamTransform(command.logPrefix, command.logColor); + subStream.pipe(logStream); + targetStream = subStream; + } - case 'help': { - break; - } + if (verbose) { + targetStream.write( + chalk.bold( + `Running: "${command.name}() with Positional arguments: ${String(positionalArguments)} and Option flags: ${String(optionFlags)}"`, + ), + ); + } - case 'version': { - break; - } + try { + exitCode = await command.execute(targetStream, positionalArguments, optionFlags); + } catch (error) { + console.error(String(error)); + exitCode = 1; + } - default: { - console.error(`Unknown command name in generateHelpText: ${name}`); - } - } - } + return exitCode; +} + +async function executeCliCommand( + logStream: NodeJS.WritableStream, + positionalArguments: string[], + optionFlags: string[], + command: CommandCli, + verbose?: boolean, +): Promise { + let exitCode = 1; // Assume failure + + // Add the log stream if desired + let targetStream: NodeJS.WritableStream; + + if (command.logPrefix === undefined) { + targetStream = logStream; + } else { + const subStream = createStreamTransform(command.logPrefix, command.logColor); + subStream.pipe(logStream); + targetStream = subStream; } - // Note some spooky behavior around these affecting how options are parsed - helpText += '\n --help, -h Print this help info.'; - helpText += '\n --version, -v Print the package version.\n'; + const resolvedSubcommands = command.subcommands ?? []; - return helpText; -} + const resolvedPositionalArguments = [ + ...(command.receivePositionalArguments ? positionalArguments : []), + ...(command.positionalArguments ?? []), + ]; + const resolvedOptionFlags = [ + ...(command.receiveOptionFlags ? optionFlags : []), + ...(command.optionFlags ?? []), + ]; -function generateFlags(options: OptionCommands): AnyFlags { - return Object.keys(options).reduce((accumulator, name) => { - let flagOptions: AnyFlag = {}; + const resolvedArguments = [ + ...resolvedSubcommands, + ...resolvedOptionFlags, + ...resolvedPositionalArguments, + ]; - switch (name) { - case 'init': { - flagOptions = { - shortFlag: 'i', - type: 'boolean', - }; - break; - } + // Manage current working directory + const cwd = getCwdOverride(command.cwdOverride); - case 'check': { - flagOptions = { - aliases: ['lint', ''], - shortFlag: 'l', - type: 'boolean', - }; - break; - } + if (verbose) { + targetStream.write(`Running: "${command.name} ${resolvedArguments.join(' ')}"`); + } - case 'fix': { - flagOptions = { - shortFlag: 'f', - type: 'boolean', - }; - break; - } + const cliTargetStream: NodeJS.WritableStream = command.prettyJsonOutput + ? new PassThrough() + : targetStream; - case 'printConfig': { - flagOptions = { - shortFlag: 'p', - type: 'boolean', - }; - break; - } + try { + const subprocess = execa(command.name, resolvedArguments, { + cwd, + env: { + // Use colorful output unless NO_COLOR is set + ...(process.env.NO_COLOR === undefined ? { FORCE_COLOR: 'true' } : {}), + // Quiet node for when processing *.config.js files in Node 22 + // Suppress experimental type stripping warning with --no-warnings + NODE_OPTIONS: '--experimental-strip-types --disable-warning=ExperimentalWarning', + }, + preferLocal: true, + reject: false, // Prevents throwing on non-zero exit code + stdin: 'inherit', + }); - case 'help': { - flagOptions = { - type: 'boolean', - }; - break; - } + // End false is required here, otherwise the stream will close before the process is done + subprocess.stdout.pipe(cliTargetStream, { end: false }); + subprocess.stderr.pipe(cliTargetStream, { end: false }); - case 'version': { - flagOptions = { - type: 'boolean', - }; - break; - } + await subprocess; - default: { - console.error(`Unknown command name: ${name}`); + if (command.prettyJsonOutput) { + cliTargetStream.end(); + // TODO is this a bad cast? + const jsonString = await streamToString(cliTargetStream as unknown as internal.Stream); + const prettyAndColorfulJsonLines = stringify(JSON.parse(jsonString)).split('\n'); + for (const line of prettyAndColorfulJsonLines) { + targetStream.write(`${line}\n`); } } - accumulator[name] = flagOptions; - return accumulator; - }, {}); + exitCode = subprocess.exitCode ?? 1; + } catch (error) { + // Extra debugging... + console.error(`${command.name} failed with error:`); + console.error(error); + if (isErrorExecaError(error)) { + exitCode = typeof error.exitCode === 'number' ? error.exitCode : 1; + } + } + + return exitCode; } -async function streamToString(stream: Stream): Promise { - const chunks: Uint8Array[] = []; - return new Promise((resolve, reject) => { - stream.on('data', (chunk: Uint8Array) => chunks.push(Buffer.from(chunk))); - stream.on('error', (error) => { - reject(error as Error); - }); - stream.on('end', () => { - resolve(Buffer.concat(chunks).toString('utf8')); - }); - }); +// Type guard for CommandCli vs CommandFunction +function isCommandFunction(command: Command): command is CommandFunction { + return 'execute' in command; } -async function executeJsonOutput( +/** + * Execute multiple commands (either functions or command line) in serial + */ +export async function executeCommands( logStream: NodeJS.WritableStream, - optionCommand: OptionCommand, - input: string[] = [], + positionalArguments: string[], + optionFlags: string[], + commands: Command[], + verbose?: boolean, + showSummary?: boolean, ): Promise { - // Capture the output of execution, and then format is nicely - const pass = new PassThrough(); - const exitCode = await execute(pass, optionCommand, input); - pass.end(); - - if (exitCode !== 0) { - logStream.write('Error printing config.\n'); - return exitCode; - } + const exitCodes: { exitCode: number; name: string }[] = []; - try { - const configString = await streamToString(pass); + for (const command of commands) { + const exitCode = await (isCommandFunction(command) + ? executeFunctionCommand(logStream, positionalArguments, optionFlags, command, verbose) + : executeCliCommand(logStream, positionalArguments, optionFlags, command, verbose)); - logStream.write(stringify(JSON.parse(configString))); - logStream.write('\n'); - return 0; - } catch (error) { - logStream.write(`Error: ${String(error)}\n`); - return 1; + exitCodes.push({ exitCode, name: command.name }); } -} -async function execute( - logStream: NodeJS.WritableStream, - optionCommand: OptionCommand, - input: string[] = [], -): Promise { - if (optionCommand.command !== undefined && typeof optionCommand.command === 'string') { - let exitCode = 1; - - try { - const subprocess = execa( - optionCommand.command, - [...(optionCommand.options ?? []), ...input], - { - env: { - FORCE_COLOR: 'true', - }, - stdin: 'inherit', // For input, todo anything weird here? - }, + if (showSummary) { + const successfulCommands = exitCodes + .filter(({ exitCode }) => exitCode === 0) + .map(({ name }) => name); + const failedCommands = exitCodes + .filter(({ exitCode }) => exitCode !== 0) + .map(({ name }) => name); + const totalCommands = exitCodes.length; + + if (successfulCommands.length > 0) { + logStream.write( + `✅ ${chalk.green.bold( + `${successfulCommands.length} / ${totalCommands} ${pluralize('Command', successfulCommands.length)} Succeeded: `, + )} ${chalk.green(successfulCommands.join(', '))}\n`, ); - - // End false is required here, otherwise the stream will close before the subprocess is done - subprocess.stdout?.pipe(logStream, { end: false }); - subprocess.stderr?.pipe(logStream, { end: false }); - await subprocess; - exitCode = subprocess.exitCode ?? 1; - } catch (error) { - // Console.error(`${optionCommand.command} failed with error "${error.shortMessage}"`); - if (isErrorExecaError(error)) { - exitCode = typeof error.exitCode === 'number' ? error.exitCode : 1; - } } - return exitCode; + if (failedCommands.length > 0) { + logStream.write( + `❌ ${chalk.red.bold( + `${failedCommands.length} / ${totalCommands} ${pluralize('Command', failedCommands.length)} Failed: `, + )} ${chalk.red(failedCommands.join(', '))}\n`, + ); + } } - logStream.write(`Error: Invalid optionCommand: ${JSON.stringify(optionCommand, undefined, 2)}`); - return 1; + // Return zero if all zero, otherwise return 1 + return exitCodes.every(({ exitCode }) => exitCode === 0) ? 0 : 1; } -function checkArguments( - input: string[], - optionCommand: OptionCommand, +async function copyAndMergeInitFiles( logStream: NodeJS.WritableStream, -): void { - // Warn if no default arguments are provided, don't be too clever - if (input.length === 0 && !optionCommand.defaultArguments) { - logStream.write('Error: This command must be used with a file argument\n'); - process.exit(1); - } -} + location: string | undefined, + configFile: string | undefined, + configPackageJson: Record | undefined, +): Promise { + // By default, copies files in the script package's /init directory to the root of the package it's called from + // For files in .vscode, if both the source and destination files are json, then merge them instead of overwriting -async function buildCommands( - command: string, - logPrefix: string | undefined, - logColor: ChalkColor, - options: OptionCommands, -) { - const cli = meow({ - allowUnknownFlags: false, - booleanDefault: undefined, - flags: generateFlags(options), - help: generateHelpText(command, options), - importMeta: import.meta, - }); + // Copy files + const destinationPackage = await packageUp(); + if (destinationPackage === undefined) { + throw new Error('The `init` command must be used in a directory with a package.json file'); + } - const { flags, input } = cli; + // TODO do we actually need import.meta.resolve() here? + const sourcePackage = await packageUp({ cwd: fileURLToPath(import.meta.url) }); + if (sourcePackage === undefined) { + logStream.write('Error: The script being called was not in a package, weird.\n'); + return 1; + } - const commandsToRun = Object.keys(options).reduce( - (accumulator, command: string) => { - if (flags[command]) { - accumulator[command as keyof OptionCommands] = options[command as keyof OptionCommands]; - } + const source = path.join(path.dirname(sourcePackage), 'init'); + const destination = path.dirname(destinationPackage); - return accumulator; - }, - {}, - ); + const hasConfigLocationOption = + (location === 'file' || location === 'package') && + configFile !== undefined && + configPackageJson !== undefined; - // Set up log stream - const logStream = createStreamTransform(logPrefix, logColor); - logStream.pipe(process.stdout); + try { + if (hasConfigLocationOption) { + const configKey = Object.keys(configPackageJson)[0]; + + if (location === 'package') { + const destinationPackageJson = fse.readJSONSync(destinationPackage) as Record< + string, + unknown + >; + + // Merge json into package.json + logStream.write( + `Merging: \nPackage config key "${configKey} → "${destination}" (Because --location is set to "package")\n`, + ); + const mergedPackageJson = merge(destinationPackageJson, configPackageJson); + fse.writeJSONSync(destinationPackage, mergedPackageJson, { spaces: 2 }); + await formatFileInPlace(destinationPackage); + } + } - // Make 'check' the default behavior if no flags are specified - if (Object.keys(commandsToRun).length === 0) { - if (options.check === undefined) { - logStream.write(`This command requires options. Run ${command} --help for valid commands.\n`); - } else { - commandsToRun.check = options.check; + // Make sure there's stuff to copy from init before proceeding + const sourceExists = await fse.pathExists(source); + if (!sourceExists) { + return 0; } - } - // Debug - // console.log(`commandsToRun: ${JSON.stringify(commandsToRun, undefined, 2)}`); + const sourceFiles = await fse.readdir(source); + if (sourceFiles.length === 0) { + logStream.write(`Source directory "${source}" is empty.\n`); + return 0; + } - let aggregateExitCode = 0; + logStream.write(`Adding initial configuration files from:\n"${source}" → "${destination}"\n`); + + await fse.copy(source, destination, { + async filter(source, destination) { + const isFile = fs.statSync(source).isFile(); + const destinationExists = fs.existsSync(destination); + + if (isFile) { + // Special case to skip copying config files to root if --location is set to package + if (hasConfigLocationOption && location === 'package' && source.includes(configFile)) { + if (destinationExists) { + logStream.write( + `Deleting: \n"${source}" → "${destination}" (Because --location is set to "package")\n`, + ); + + fse.removeSync(destination); + } else { + logStream.write( + `Skipping: \n"${source}" → "${destination}" (Because --location is set to "package")\n`, + ); + } + + return false; + } - for (const [name, optionCommand] of Object.entries(commandsToRun)) { - if (typeof optionCommand.command === 'function') { - checkArguments(input, optionCommand, logStream); + // Special case to merge package.json and .vscode json settings files + if ( + destinationExists && + (destination.includes('.vscode/') || destination.includes('package.json')) && + path.extname(destination) === '.json' + ) { + // Merge + logStream.write(`Merging: \n"${source}" → "${destination}"\n`); - const args = input.length === 0 ? (optionCommand.defaultArguments ?? []) : input; - const options = optionCommand.options ?? []; + const sourceJson = fse.readJSONSync(source) as Record; + const destinationJson = fse.readJSONSync(destination) as Record; + const mergedJson = merge(destinationJson, sourceJson); - // Custom function execution is always the same - aggregateExitCode += await optionCommand.command(logStream, args, options); - } else if (typeof optionCommand.command === 'string') { - // Warn if no default arguments are provided, don't be too clever - checkArguments(input, optionCommand, logStream); + fse.writeJSONSync(destination, mergedJson, { spaces: 2 }); + await formatFileInPlace(destination); - aggregateExitCode += await execute( - logStream, - optionCommand, - input.length === 0 ? optionCommand.defaultArguments : input, - ); - } else { - // Handle default behaviors (e.g. {}) - switch (name) { - case 'init': { - // By default, copies files in script package's /init directory to the root of the package it's called from - // For files in .vscode, if both the source and destination files are json, then merge them instead of overwriting - - // Copy files - const destinationPackage = await packageUp(); - if (destinationPackage === undefined) { - logStream.write( - 'Error: The `--init` flag must be used in a directory with a package.json file\n', - ); - aggregateExitCode += 1; - break; + return false; } - const sourcePackage = await packageUp({ cwd: fileURLToPath(import.meta.url) }); - if (sourcePackage === undefined) { - logStream.write('Error: The script being called was not in a package, weird.\n'); - aggregateExitCode += 1; - break; + if (destinationExists) { + logStream.write(`Overwriting: \n"${source}" → "${destination}"\n`); + await formatFileInPlace(destination); + return true; } - const source = path.join(path.dirname(sourcePackage), 'init/'); - const destination = path.dirname(destinationPackage); - - logStream.write( - `Adding initial configuration files from:\n"${source}" → "${destination}"\n`, - ); - - try { - await fse.copy(source, destination, { - async filter(source, destination) { - const isFile = fse.statSync(source).isFile(); - const destinationExists = fse.existsSync(destination); - - if (isFile) { - if ( - destinationExists && - destination.includes('.vscode/') && - path.extname(destination) === '.json' - ) { - // Merge - logStream.write(`Merging: \n"${source}" → "${destination}"\n`); - - const sourceJson = fse.readJSONSync(source) as Record; - const destinationJson = fse.readJSONSync(destination) as Record< - string, - unknown - >; - const mergedJson = merge(destinationJson, sourceJson); - const prettifiedJson = await prettier.format( - JSON.stringify(mergedJson), - prettierConfig, - ); - - fse.writeFileSync(destination, prettifiedJson); - - return false; - } - - if (destinationExists) { - logStream.write(`Overwriting: \n"${source}" → "${destination}"\n`); - return true; - } - - logStream.write(`Copying: \n"${source}" → "${destination}"\n`); - return true; - } - - // Don't log directory copy - return true; - }, - overwrite: true, - }); - } catch { - // Intentionally blank - } + logStream.write(`Copying: \n"${source}" → "${destination}"\n`); + await formatFileInPlace(destination); + return true; + } - // TODO - aggregateExitCode += 0; + // Don't log directory copy + return true; + }, + overwrite: true, + }); + } catch (error) { + console.error(String(error)); + return 1; + } - break; - } + return 0; +} - case 'check': { - console.error( - 'There is no default implementation for check. The [tool]-config package must define a command.', - ); - aggregateExitCode += 1; - break; - } +/** + * Create a simple command line interface for a package. + */ +export async function buildCommands(commandDefinition: CommandDefinition) { + const { + commands: { fix, init, lint, printConfig }, + description, + logColor, + logPrefix, + name, + showSummary, + verbose, + } = commandDefinition; - case 'fix': { - console.error( - 'There is no default implementation for fix. The [tool]-config package must define a command.', - ); - aggregateExitCode += 1; - break; - } + // Set up log stream + const logStream = createStreamTransform(logPrefix, logColor); + logStream.pipe(process.stdout); + + const yargsInstance = yargs(hideBin(process.argv)) + .scriptName(name) + .usage('$0 ', description); + + if (init !== undefined) { + yargsInstance.command({ + builder(yargs) { + return init.locationOptionFlag + ? yargs.option('location', { + choices: ['file', 'package'], + default: 'file', + description: 'TK', + type: 'string', + }) + : yargs; + }, + command: 'init', + // Command: init.locationOptionFlag ? 'init [--location]' : init + describe: + init.description ?? + `Initialize by copying starter config files to your project root${init.locationOptionFlag ? ' or to your package.json file.' : '.'}`, + async handler(argv) { + // Copy files + const location = init.locationOptionFlag + ? (argv.location as string | undefined) + : undefined; + + // Grab context by closure + const copyAndMergeInitFilesCommand: CommandFunction = { + async execute(logStream, _, optionFlags) { + return copyAndMergeInitFiles( + logStream, + optionFlags.at(1), + init.configFile, + init.configPackageJson, + ); + }, + name: 'copyAndMergeInitFiles', + }; - case 'printConfig': { - const args = input.length === 0 ? (optionCommand.defaultArguments ?? ['.']) : input; - const filePath = args?.at(0); + // Run commands + const exitCode = await executeCommands( + logStream, + [], + location === undefined ? [] : ['--location', location], + [copyAndMergeInitFilesCommand, ...(init.commands ?? [])], + ); - // Brittle, could pass config name to commandBuilder() instead - const configName = command.split('-').at(0); + process.exit(exitCode); + }, + }); + } - if (configName === undefined) { - logStream.write(`Error: Could not find or parse config file for ${command}.\n`); - aggregateExitCode += 1; - break; - } + if (lint !== undefined) { + yargsInstance.command({ + builder(yargs) { + return lint.positionalArgumentMode === 'none' + ? yargs + : yargs.positional('files', { + array: true, + ...(lint.positionalArgumentDefault === undefined + ? {} + : { default: lint.positionalArgumentDefault }), + describe: 'Files or glob pattern to lint.', + type: 'string', + }); + }, + command: lint.positionalArgumentMode === 'none' ? 'lint [files..]' : 'lint ', + describe: lint.description, + async handler(argv) { + const positionalArguments = (argv.files as string[] | undefined) ?? []; + const exitCode = await executeCommands( + logStream, + positionalArguments, + [], + lint.commands, + verbose, + showSummary, + ); + process.exit(exitCode); + }, + }); + } - const configSearch = await cosmiconfig(configName).search(filePath); + // Duplicate of above, but whatever + if (fix !== undefined) { + yargsInstance.command({ + builder(yargs) { + return fix.positionalArgumentMode === 'none' + ? yargs + : yargs.positional('files', { + array: true, + ...(fix.positionalArgumentDefault === undefined + ? {} + : { default: fix.positionalArgumentDefault }), + describe: 'Files or glob pattern to fix.', + type: 'string', + }); + }, + command: fix.positionalArgumentMode === 'none' ? 'fix [files..]' : 'fix ', + describe: fix.description, + async handler(argv) { + const positionalArguments = (argv.files as string[] | undefined) ?? []; + const exitCode = await executeCommands( + logStream, + positionalArguments, + [], + fix.commands, + verbose, + showSummary, + ); + process.exit(exitCode); + }, + }); + } - if (!configSearch?.config) { - logStream.write(`Error: Could not find or parse config file for ${configName}.\n`); - aggregateExitCode += 1; - break; - } + if (printConfig !== undefined) { + yargsInstance.command({ + builder(yargs) { + return printConfig.positionalArgumentMode === 'none' + ? yargs + : yargs.positional('file', { + ...(printConfig.positionalArgumentDefault === undefined + ? {} + : { default: printConfig.positionalArgumentDefault }), + describe: 'File or glob pattern to TK', + type: 'string', + }); + }, + command: + printConfig.positionalArgumentMode === 'none' + ? 'print-config' + : printConfig.positionalArgumentMode === 'optional' + ? 'print-config [file]' + : 'print-config ', + describe: printConfig.description, + async handler(argv) { + const fileArgument = (argv.file as string | undefined) ?? undefined; + const positionalArguments = fileArgument === undefined ? [] : [fileArgument]; + + const exitCode = await executeCommands( + logStream, + positionalArguments, + [], + printConfig.commands, + verbose, + showSummary, + ); + process.exit(exitCode); + }, + }); + } - logStream.write(`${logPrefix} config path: "${configSearch?.filepath}"\n`); - logStream.write(stringify(configSearch.config)); - logStream.write('\n'); - break; - } + // Parse and execute + yargsInstance.alias('h', 'help'); + yargsInstance.version(version); + yargsInstance.alias('v', 'version'); + yargsInstance.help(); + yargsInstance.wrap(process.stdout.isTTY ? Math.min(120, yargsInstance.terminalWidth()) : 0); - default: { - console.error(`Unknown command name: ${name}`); - aggregateExitCode += 1; - break; - } + await yargsInstance.parseAsync(); +} +/** + * TK + */ +export function getCosmicconfigCommand(configName: string): CommandFunction { + return { + async execute(logStream) { + const result = await getCosmicconfigResult(configName); + + if (result === undefined) { + return 1; } - } - } - process.exit(aggregateExitCode > 0 ? 1 : 0); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const { config, filepath: configFilePath, isEmpty } = result; + + logStream.write(`Found ${configName} configuration at "${configFilePath}"\n`); + + if (isEmpty) { + logStream.write('Configuration is empty.\n'); + return 0; + } + + const prettyAndColorfulJsonLines = stringify(config).split('\n'); + for (const line of prettyAndColorfulJsonLines) { + logStream.write(`${line}\n`); + } + return 0; + }, + name: `Cosmicconfig ${configName}`, + }; } -function isErrorExecaError(error: unknown): error is ExecaError { - return ( - error instanceof Error && - 'exitCode' in error && - typeof (error as ExecaError).exitCode === 'number' - ); +type NullToUndefined = T extends null ? undefined : T; + +/** + * Convenience wrapper to safely fetch a cosmicconfig result. + */ +export async function getCosmicconfigResult( + configName: string, +): Promise> { + const explorer = cosmiconfig(configName, { + searchStrategy: 'project', + // Alt approach? + // searchStrategy: 'global', + // stopDir: getCwdOverride('workspace-root') + }); + + try { + const result = await explorer.search(); + + if (result === null) { + console.error(`No ${configName} configuration found.`); + return undefined; + } + } catch (error) { + console.error(`Error while searching for ${configName} configuration:`, error); + return undefined; + } } -export { buildCommands, execute, executeJsonOutput }; -export type { OptionCommands }; +/** + * Commonly reused CLI help description strings. Some duplication is intentional for future flexibility. + */ +export const DESCRIPTION = { + fileRun: 'Matches files below the current working directory by default.', + monorepoRun: + 'In a monorepo, it will also run in all packages below the current working directory.', + monorepoSearch: 'Searches up to the root of a monorepo if necessary.', + multiArgumentCaveat: + 'Will use file arguments / globs where possible if provided, but some of the invoked tools only operate at the package-scope.', + multiOptionCaveat: + 'Will use option flags where possible if provided, but some of the invoked tools will ignore them.', + optionalFileRun: 'Package-scoped by default, file-scoped if a file argument is provided.', + packageRun: 'Package-scoped', + packageSearch: 'Package-scoped', +}; diff --git a/src/execa-utils.ts b/src/execa-utils.ts new file mode 100644 index 0000000..8e69f86 --- /dev/null +++ b/src/execa-utils.ts @@ -0,0 +1,12 @@ +import { type ExecaError } from 'execa'; + +/** + * Returns true if the error is an ExecaError. + */ +export function isErrorExecaError(error: unknown): error is ExecaError { + return ( + error instanceof Error && + 'exitCode' in error && + typeof (error as ExecaError).exitCode === 'number' + ); +} diff --git a/src/file-utils.ts b/src/file-utils.ts deleted file mode 100644 index 51d4706..0000000 --- a/src/file-utils.ts +++ /dev/null @@ -1,15 +0,0 @@ -import fs from 'node:fs/promises'; - -export async function checkFileExists(file: string): Promise { - try { - await fs.stat(file); - return true; // File exists - } catch (error) { - if ((error as NodeJS.ErrnoException).code === 'ENOENT') { - return false; // File does not exist - } - - // Re-throw the error if it's not a 'File does not exist' error - throw error; - } -} diff --git a/src/json-utils.ts b/src/json-utils.ts index 1f16427..09d910b 100644 --- a/src/json-utils.ts +++ b/src/json-utils.ts @@ -7,7 +7,10 @@ import type { ArrayMergeOptions, Options } from 'deepmerge'; import deepmerge from 'deepmerge'; import jsonStringifyPrettyCompact from 'json-stringify-pretty-compact'; -function stringify(object: unknown): string { +/** + * TK + */ +export function stringify(object: any): string { return jsonColorizer( jsonStringifyPrettyCompact(object, { indent: 2, @@ -45,13 +48,13 @@ const combineMerge = (target: any[], source: any[], options: ArrayMergeOptions): return destination; }; -function merge( +/** + * TK + */ +export function merge( destination: any, source: any, - options: Options = { arrayMerge: combineMerge }, ): any[] { return deepmerge(destination, source, options); } - -export { merge, stringify }; diff --git a/src/node-utils.ts b/src/node-utils.ts new file mode 100644 index 0000000..88b3cda --- /dev/null +++ b/src/node-utils.ts @@ -0,0 +1,35 @@ +import process from 'node:process'; + +let originalEmit: typeof process.emit | undefined; + +/** + * Silences the experimental feature warnings. Call `restoreNodeWarnings` after to restore the original behavior. + */ +export function suppressNodeWarnings(): void { + // eslint-disable-next-line @typescript-eslint/unbound-method + originalEmit ??= process.emit; + + process.emit = function (...args) { + if (args[1] instanceof Error && args[1].name === 'ExperimentalWarning') { + return false; + } + + if (originalEmit === undefined) { + throw new Error( + "'suppressNodeWarnings' must be called before the first experimental warning is emitted", + ); + } + + return originalEmit.call(this, ...args); + } as typeof process.emit; +} + +/** + * Restores the original experimental feature warning behavior of `process.emit`. + */ +export function restoreNodeWarnings(): void { + if (originalEmit) { + process.emit = originalEmit; + originalEmit = undefined; // Clear the stored reference + } +} diff --git a/src/path-utils.ts b/src/path-utils.ts new file mode 100644 index 0000000..9b4d391 --- /dev/null +++ b/src/path-utils.ts @@ -0,0 +1,116 @@ +import { findWorkspaces, findWorkspacesRoot } from 'find-workspaces'; +import fse from 'fs-extra'; +import path from 'node:path'; +import { packageUpSync } from 'package-up'; + +function isDirectoryBelow(directory: string, parent: string): boolean { + return directory.startsWith(parent + path.sep); +} + +/** + * Returns list of package directories at or below the current working directory. Useful for monorepos. If not a monorepo, returns the closest package directory. + * @example + * Calling from `/Users/lrella/Code/shared-config` will yield: + * ```ts + * [ + * '/Users/lrella/Code/shared-config', + * '/Users/lrella/Code/shared-config/packages/browsers-config', + * '/Users/lrella/Code/shared-config/packages/cspell-config', + * '/Users/lrella/Code/shared-config/packages/eslint-config', + * '/Users/lrella/Code/shared-config/packages/knip-config', + * '/Users/lrella/Code/shared-config/packages/mdat-config', + * '/Users/lrella/Code/shared-config/packages/prettier-config', + * '/Users/lrella/Code/shared-config/packages/remark-config', + * '/Users/lrella/Code/shared-config/packages/repo-config', + * '/Users/lrella/Code/shared-config/packages/shared-config', + * '/Users/lrella/Code/shared-config/packages/stylelint-config', + * '/Users/lrella/Code/shared-config/packages/typescript-config', + * ] + * ``` + */ +export function findWorkspacePackageDirectories(): string[] { + const packageDirectory = getPackageDirectory(); + + const directories = new Set(); + directories.add(packageDirectory); + + // Find all workspaces + const workspaces = findWorkspaces(); + if (workspaces !== null) { + for (const workspace of workspaces) { + if (isDirectoryBelow(workspace.location, packageDirectory)) { + directories.add(workspace.location); + } + } + } + + return [...directories]; +} + +/** + * Returns the closest package directory to the current working directory. Throws an error if no package.json is found. + */ +export function getPackageDirectory(): string { + const closestPackage = packageUpSync(); + if (closestPackage === undefined) { + throw new Error('No package.json found.'); + } + return path.dirname(closestPackage); +} + +/** + * True if the project is a monorepo. + */ +export function isMonorepo(): boolean { + return findWorkspacesRoot() !== null; +} + +/** + * Returns the root of the monorepo if you're in one, or the closest package directory if you're not in a monorepo. + */ +export function getWorkspaceRoot(): string { + const workspaceRoot = findWorkspacesRoot(); + if (workspaceRoot !== null) { + return workspaceRoot.location; + } + + return getPackageDirectory(); +} + +/** + * Useful for searching for ignored files in the root of the workspace, + */ +export function getFilePathAtProjectRoot(fileName: string): string | undefined { + const filePath = path.join(getWorkspaceRoot(), fileName); + + if (fse.existsSync(filePath)) { + return filePath; + } + + return undefined; +} + +export type CwdOverrideOptions = 'package-dir' | 'workspace-root' | (string & {}); + +/** + * Tries to get a specific cwd override, and safely falls back depending on monorepo etc. + */ +export function getCwdOverride(option?: CwdOverrideOptions): string { + if (option === 'workspace-root') { + // Falls back to package if not in a monorepo + return getWorkspaceRoot(); + } + + if (option === 'package-dir') { + return getPackageDirectory(); + } + + if (typeof option === 'string') { + if (!fse.pathExistsSync(option)) { + throw new Error(`Custom cwd directory does not exist: ${option}`); + } + return option; + } + + return process.cwd(); +} diff --git a/src/prettier-utils.ts b/src/prettier-utils.ts new file mode 100644 index 0000000..fafa8a2 --- /dev/null +++ b/src/prettier-utils.ts @@ -0,0 +1,30 @@ +import fs from 'node:fs/promises'; + +/** + * Formats and saves the file content. + * @param filePath - Pth to the file. + * @param content - Content to format and save. + */ +export async function formatTextAndSaveFile(filePath: string, content: string): Promise { + try { + const { default: prettier } = await import('prettier'); + const prettierConfig = await prettier.resolveConfig(filePath); + const formattedContent = await prettier.format(content, { + filepath: filePath, + ...prettierConfig, + }); + await fs.writeFile(filePath, formattedContent, 'utf8'); + } catch { + console.warn(`Skipped formatting ${filePath} since Prettier is not installed.`); + // Do nothing on error + } +} + +export async function formatFileInPlace(filePath: string): Promise { + try { + const content = await fs.readFile(filePath, 'utf8'); + await formatTextAndSaveFile(filePath, content); + } catch { + // Do nothing on error + } +} diff --git a/src/stream-utils.ts b/src/stream-utils.ts new file mode 100644 index 0000000..30e6c1c --- /dev/null +++ b/src/stream-utils.ts @@ -0,0 +1,65 @@ +import { type Stream, Transform } from 'node:stream'; +// eslint-disable-next-line unicorn/import-style +import chalk, { type foregroundColorNames } from 'chalk'; + +type ChalkColor = (typeof foregroundColorNames)[number]; + +/** + * Creates a transform stream that filters out lines that match the given matcher + */ +export function createStreamFilter(matcher: (_text: string) => boolean): Transform { + return new Transform({ + transform(chunk: string | Uint8Array, _: BufferEncoding, callback) { + const filtered = chunk + .toString() + .split(/\r?\n/) + .filter((line) => line.trim() !== '' && !matcher(line)) + .join('\n'); + this.push(filtered + '\n'); + callback(); + }, + }); +} + +/** + * Creates a transform stream that prepends a log prefix to each line + */ +export function createStreamTransform( + logPrefix: string | undefined, + logColor: ChalkColor, +): Transform { + return new Transform({ + transform(chunk: string | Uint8Array, _: BufferEncoding, callback) { + const lines: string[] = chunk + .toString() + .split(/\r?\n/) + .filter((line) => line.trim().length > 0); + + const transformed = lines + .map( + (line) => + `${logPrefix ? (logColor === undefined ? logPrefix : chalk[logColor](logPrefix)) : ''} ${line}\n`, + ) + .join(''); + + this.push(transformed); + callback(); + }, + }); +} + +/** + * Converts a stream to a string + */ +export async function streamToString(stream: Stream): Promise { + const chunks: Uint8Array[] = []; + return new Promise((resolve, reject) => { + stream.on('data', (chunk: Uint8Array) => chunks.push(chunk)); + stream.on('error', (error) => { + reject(error as Error); + }); + stream.on('end', () => { + resolve(Buffer.concat(chunks).toString('utf8')); + }); + }); +} diff --git a/src/string-utils.ts b/src/string-utils.ts new file mode 100644 index 0000000..d0dfffc --- /dev/null +++ b/src/string-utils.ts @@ -0,0 +1,13 @@ +/** + * Converts a camelCase string to kebab-case. + */ +export function kebabCase(text: string): string { + return text.replaceAll(/[A-Z\u00C0-\u00D6\u00D8-\u00DE]/g, (match) => '-' + match.toLowerCase()); +} + +/** + * Natively pluralizes a word based on a quantity. + */ +export function pluralize(text: string, quantity: number): string { + return quantity === 1 ? text : text + 's'; +}