diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1bab5a5..19b8a14 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -38,11 +38,13 @@ jobs: - name: Installing dependencies run: pnpm install --frozen-lockfile - - name: Verify registry signatures for installed packages - run: pnpm audit signatures + - name: Building + run: pnpm build + + - name: Install npm for Trusted Publishing + run: pnpm add --global npm@latest - name: Release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - NPM_CONFIG_PROVENANCE: true run: pnpm exec semantic-release diff --git a/package.json b/package.json index cf3c494..e440aa9 100644 --- a/package.json +++ b/package.json @@ -110,7 +110,7 @@ "commitlint": "^21.2.3", "commitlint-plugin-cspell": "^0.9.5", "conventional-changelog-conventionalcommits": "^9.3.1", - "cspell": "^10.3.4", + "cspell": "^10.3.5", "eslint": "^8.57.1", "eslint-config-prettier": "^10.1.8", "eslint-import-resolver-typescript": "^4.4.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b05a56f..590a94c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,152 +8,152 @@ importers: packageManagerDependencies: pnpm: specifier: '>=12.0.0 <13.0.0' - version: 12.6.0 + version: 12.8.1 packages: - '@pnpm/exe.android-arm64@12.6.0': - resolution: {integrity: sha512-kviIHft9h02q+7N2In7tSL9T/HRUSGwYmU74YPtsWU5ee7vgqPyC0JT6RRX6miW+NxX1CvHgQxg69qq1qTLJYQ==} + '@pnpm/exe.android-arm64@12.8.1': + resolution: {integrity: sha512-siDxcidfz5eM6L2tJfik+i2bbUEw8UC7MZ3Yl2HYdRmqbdhHGAOQACtOOu6QVW4Jul4/w5wVKm+wM8ACtgh1Pg==} cpu: [arm64] os: [android] - '@pnpm/exe.android-x64@12.6.0': - resolution: {integrity: sha512-CT8aJKLq2mtZFE71pr4E5Z2xHL8uGnRGr+NclDqsXlVF4SVcQ2QAs14mWi0C8gjT4hbmi+Q0lJRRIkMAWfjjng==} + '@pnpm/exe.android-x64@12.8.1': + resolution: {integrity: sha512-0F/uqPRc3aN4vLHsa0f+6fTtmsigFQ9/yxEU43fionF//pNHEd5e97b3w/nKIgqYMCUQCJphmBRMA9Q8/wlo9Q==} cpu: [x64] os: [android] - '@pnpm/exe.darwin-arm64@12.6.0': - resolution: {integrity: sha512-rafpVkjzugKBMxSvGQd5wK1x5eOTkD/+lcJIFHeQhvd+BJ8kDisOOvwhQDrpGd4vd2Fx+hhW0P2Ptt638OlhFg==} + '@pnpm/exe.darwin-arm64@12.8.1': + resolution: {integrity: sha512-/rwavvMQJsl2RIxOHBnDF+4Gk+fhQFAY4M3PQZoKskJOEnaJcygG9p1xby6wcQcbS4d9dIubv09CQhufTgfmpw==} cpu: [arm64] os: [darwin] - '@pnpm/exe.darwin-x64@12.6.0': - resolution: {integrity: sha512-72Jpuv1m24gI8zUNcaylYpBWEhBzz1VPhnDIU2GYM2eRP+KsOqVLwV78i+JrihI39zY2NNXmaN4eAKX1inpaDQ==} + '@pnpm/exe.darwin-x64@12.8.1': + resolution: {integrity: sha512-htYt2gN7zqJKLhC99Ft6EUiO7myjZAZXfmGCTJUL1v+o7vE9hZdE1+fAkk4Oj1ZHNRti6b2woOxEkRkAusi8OQ==} cpu: [x64] os: [darwin] - '@pnpm/exe.freebsd-x64@12.6.0': - resolution: {integrity: sha512-LON4QgNy1w/XF4uySZIyC/5hEB0oGdelXsY9tSKqstER+2j2X2oHz67skD11RRd/HWFRtMr64shQZ4I9ZoZ0zg==} + '@pnpm/exe.freebsd-x64@12.8.1': + resolution: {integrity: sha512-PB5BULNgytxvExoOXUUeseS/DcpPENs+cg3iA6xHHeaLT4ctEGKSGXEbuj7USeqvo8JBpAx039hivnbzGqDwnA==} cpu: [x64] os: [freebsd] - '@pnpm/exe.linux-arm64-musl@12.6.0': - resolution: {integrity: sha512-BdDpX+DeaMUc5x6WNcE6FkmFy36NwrBbBvwsZU737Zt3u1fTY+iKFPchjEQNCM4rT6q6xvF2oN3ZwExGsJCkxQ==} + '@pnpm/exe.linux-arm64-musl@12.8.1': + resolution: {integrity: sha512-kKeyC/ArjrgdciQRBrOL0j9HtOI4gDRgoyP+eJ1CQKyadpKlf/DhDDyspXe07R3B9KagTVLUF2nlt6Dr5quFIg==} cpu: [arm64] os: [linux] libc: [musl] - '@pnpm/exe.linux-arm64@12.6.0': - resolution: {integrity: sha512-8h2sNoIhHDpHDYqZZCh9PXr6krqW10btb0GHVGuw710muDHlFAHMtUG2ogmaHge6zJG1xgY4Y7mwX5g/LWvrYg==} + '@pnpm/exe.linux-arm64@12.8.1': + resolution: {integrity: sha512-q4s9a9X2O3N9aeUFooW24orNrxvu20+jyVUFR5RanPCqOKKPBrgs2iywMkBBx1aXkkzdWT54/mfz8Be8sJlWEw==} cpu: [arm64] os: [linux] libc: [glibc] - '@pnpm/exe.linux-ppc64@12.6.0': - resolution: {integrity: sha512-ld6xgcEhFCFrsVsJhbmdmR714PnsEspO/Ij8w0/D7OSiz2hYGu3fZd5tKRxOI5m6H6Gl0/RRFAPncTHbsgXVjQ==} + '@pnpm/exe.linux-ppc64@12.8.1': + resolution: {integrity: sha512-F7XSjKJthwCh/L6abZiMpAfo2Cm61SZIT3e4dfgiIjESndlWaXCnfLKngSgZ/SEMsdmHT6dud3+kWIB3tw0/xQ==} cpu: [ppc64] os: [linux] libc: [glibc] - '@pnpm/exe.linux-riscv64@12.6.0': - resolution: {integrity: sha512-HIbImydoC3J8NFt+8MXfhTCf5rO6NjCMyinSAHac0VmV8uCHw+kbQkyAzxV84rzz9ZOzmBOnj0b1N461UIYGsw==} + '@pnpm/exe.linux-riscv64@12.8.1': + resolution: {integrity: sha512-z1ecpIK/ZY08Unk69KWWE9867oaKUytGUjrHnBoGFAAhsK+q+0cfvBH+f5fWobJ7FX97leMlLO+gkJolddM45Q==} cpu: [riscv64] os: [linux] libc: [glibc] - '@pnpm/exe.linux-s390x@12.6.0': - resolution: {integrity: sha512-DjSqT7+BZ/lWcDHwNMuzsRVfyxUimh+D54Xw4tSWbdVVQhtWtNMUyI93LVnwCmsoEpar2gC3SFeAgywx37svKQ==} + '@pnpm/exe.linux-s390x@12.8.1': + resolution: {integrity: sha512-KU6tnrBfu4uKXTq6N5qJwSm8FSrs9Wc8b5AUEsHK9bRXwNdsZ1/lRxG1++X29SKRW9OSNuDSjxASv5mAvGo1gA==} cpu: [s390x] os: [linux] libc: [glibc] - '@pnpm/exe.linux-x64-musl@12.6.0': - resolution: {integrity: sha512-31lKeGPmRE6xfV6I3VjaEGVFRei14pl/zgoFDK4w+UkbJqOuNl2Ht3O1GuqNIg3Gl14qTEb4kZRAYFHXifDx9A==} + '@pnpm/exe.linux-x64-musl@12.8.1': + resolution: {integrity: sha512-eUE8BWJkDr2tJb/yrk0yVnszlmNqFarVxUlqiZ9BAkct28Nq/5g2H0tvMDmPA7fAQW3STNj+omJPPRVT+4s5sQ==} cpu: [x64] os: [linux] libc: [musl] - '@pnpm/exe.linux-x64@12.6.0': - resolution: {integrity: sha512-qFWBneHJAJ73W4whtbaFOL1M/7DBC6ILHXuxc7ZPtEhfPuT1zeZiGrmKHoMAfJA+mcm6xhOFljqVTUS+00Jabw==} + '@pnpm/exe.linux-x64@12.8.1': + resolution: {integrity: sha512-8bDZ0lZlCdi2rvnFul7EYgcC7zKeWSe76y8JV2oXobaS8p9i9d6PSf6LgLtTM0fI7R9Oh4eLCbveXyNDMmvZ+g==} cpu: [x64] os: [linux] libc: [glibc] - '@pnpm/exe.win32-arm64@12.6.0': - resolution: {integrity: sha512-OhfefXEEykZlslSUhR8PPRe6MVPV3Oink/dVfIecE5velpc7j80cZYsHlaDrtRDcm3TPwQHkaRW1SJ1efX9oFg==} + '@pnpm/exe.win32-arm64@12.8.1': + resolution: {integrity: sha512-MsT0dlrRxnCRCHCaosNbhWpEiAFnFuMEu3KuDJDU41BXgTwiINxU8goloUSUDejWZUqKaUqNC/qUTihl9nFtqg==} cpu: [arm64] os: [win32] - '@pnpm/exe.win32-x64@12.6.0': - resolution: {integrity: sha512-L2tuyrD2+Imgxs3VK/ST/2L3xD6vDP0ktJQxM0TaGw7TbAQEMq+RMc8iK6Ew7Id579uzjJe8jSfJID2ZGsOHCA==} + '@pnpm/exe.win32-x64@12.8.1': + resolution: {integrity: sha512-SWtPe0PsbTTk//gJND10tMdOfCYjrkq5+qV49hzVhY7xz2iJ339Rc+xwASlcvbwTfrvH39YNqpKYibD+CRGLwA==} cpu: [x64] os: [win32] - pnpm@12.6.0: - resolution: {integrity: sha512-PvaPlRyxEawgS0paFvCy3fDaVqluBBPoHYVdnwtV75JnFHCQKOHNAMQFwsX7e56OxNxGd3yAXQNzwvL/AP0g7A==} + pnpm@12.8.1: + resolution: {integrity: sha512-9kupB1B/XOr+BsjTjmBS0BeURFgOwSed3Vv8EctIqoomRLZlmOB+dh2oSHKp/FfV+QK4f6SdAkGY1VhhKqu+RQ==} engines: {node: '>=18.*'} hasBin: true snapshots: - '@pnpm/exe.android-arm64@12.6.0': + '@pnpm/exe.android-arm64@12.8.1': optional: true - '@pnpm/exe.android-x64@12.6.0': + '@pnpm/exe.android-x64@12.8.1': optional: true - '@pnpm/exe.darwin-arm64@12.6.0': + '@pnpm/exe.darwin-arm64@12.8.1': optional: true - '@pnpm/exe.darwin-x64@12.6.0': + '@pnpm/exe.darwin-x64@12.8.1': optional: true - '@pnpm/exe.freebsd-x64@12.6.0': + '@pnpm/exe.freebsd-x64@12.8.1': optional: true - '@pnpm/exe.linux-arm64-musl@12.6.0': + '@pnpm/exe.linux-arm64-musl@12.8.1': optional: true - '@pnpm/exe.linux-arm64@12.6.0': + '@pnpm/exe.linux-arm64@12.8.1': optional: true - '@pnpm/exe.linux-ppc64@12.6.0': + '@pnpm/exe.linux-ppc64@12.8.1': optional: true - '@pnpm/exe.linux-riscv64@12.6.0': + '@pnpm/exe.linux-riscv64@12.8.1': optional: true - '@pnpm/exe.linux-s390x@12.6.0': + '@pnpm/exe.linux-s390x@12.8.1': optional: true - '@pnpm/exe.linux-x64-musl@12.6.0': + '@pnpm/exe.linux-x64-musl@12.8.1': optional: true - '@pnpm/exe.linux-x64@12.6.0': + '@pnpm/exe.linux-x64@12.8.1': optional: true - '@pnpm/exe.win32-arm64@12.6.0': + '@pnpm/exe.win32-arm64@12.8.1': optional: true - '@pnpm/exe.win32-x64@12.6.0': + '@pnpm/exe.win32-x64@12.8.1': optional: true - pnpm@12.6.0: + pnpm@12.8.1: optionalDependencies: - '@pnpm/exe.android-arm64': 12.6.0 - '@pnpm/exe.android-x64': 12.6.0 - '@pnpm/exe.darwin-arm64': 12.6.0 - '@pnpm/exe.darwin-x64': 12.6.0 - '@pnpm/exe.freebsd-x64': 12.6.0 - '@pnpm/exe.linux-arm64': 12.6.0 - '@pnpm/exe.linux-arm64-musl': 12.6.0 - '@pnpm/exe.linux-ppc64': 12.6.0 - '@pnpm/exe.linux-riscv64': 12.6.0 - '@pnpm/exe.linux-s390x': 12.6.0 - '@pnpm/exe.linux-x64': 12.6.0 - '@pnpm/exe.linux-x64-musl': 12.6.0 - '@pnpm/exe.win32-arm64': 12.6.0 - '@pnpm/exe.win32-x64': 12.6.0 + '@pnpm/exe.android-arm64': 12.8.1 + '@pnpm/exe.android-x64': 12.8.1 + '@pnpm/exe.darwin-arm64': 12.8.1 + '@pnpm/exe.darwin-x64': 12.8.1 + '@pnpm/exe.freebsd-x64': 12.8.1 + '@pnpm/exe.linux-arm64': 12.8.1 + '@pnpm/exe.linux-arm64-musl': 12.8.1 + '@pnpm/exe.linux-ppc64': 12.8.1 + '@pnpm/exe.linux-riscv64': 12.8.1 + '@pnpm/exe.linux-s390x': 12.8.1 + '@pnpm/exe.linux-x64': 12.8.1 + '@pnpm/exe.linux-x64-musl': 12.8.1 + '@pnpm/exe.win32-arm64': 12.8.1 + '@pnpm/exe.win32-x64': 12.8.1 --- lockfileVersion: '9.0' @@ -203,8 +203,8 @@ importers: specifier: ^9.3.1 version: 9.3.1 cspell: - specifier: ^10.3.4 - version: 10.3.4 + specifier: ^10.3.5 + version: 10.3.5 eslint: specifier: ^8.57.1 version: 8.57.1(supports-color@7.2.0) @@ -391,36 +391,36 @@ packages: resolution: {integrity: sha512-aalGyl7dbB5PArRebDIX43ZvBlXrYm9uWzGJ26t+4SzJVPsOuvfILGGbw5X4yX7i50YEmJ8zvbiWnqH/AAnZqg==} engines: {node: '>=22'} - '@cspell/cspell-bundled-dicts@10.3.4': - resolution: {integrity: sha512-43nfJOuuiWohR4kpH+uMsko2GMlW8ns+SolGcKK5fdp56POS45UbGKtGlmOmC5bi+1H/1bZmyHI/H7EX+wQNgA==} + '@cspell/cspell-bundled-dicts@10.3.5': + resolution: {integrity: sha512-gmgpSYyPP1kAZrKVulmJ+lycQUEFG54n2dsMxLfPtBcTpOhvFZUHfyzLmpkukujU8JySOeCSjynEtqIPqzxvUg==} engines: {node: '>=22.18.0'} - '@cspell/cspell-json-reporter@10.3.4': - resolution: {integrity: sha512-ycVkt2bS9QTpo0tqlWRnTzAdk6VR9ZJSV5nNo4QqDt7rFZl4m0NdQxryJn8+hnqQ0hc7UJN8kcSbWIjVFmoCHA==} + '@cspell/cspell-json-reporter@10.3.5': + resolution: {integrity: sha512-eB7nQU4pyV9SgT78yfqiaC5IKQkbYPu/3474CqlzKc+uZQ5/J0ujz0JQQgkVlwZf6i0N03mSQr/LI/iLmMWg5g==} engines: {node: '>=22.18.0'} - '@cspell/cspell-performance-monitor@10.3.4': - resolution: {integrity: sha512-vJ1MIQRUTAbqY/VN1UiqBBeLiC159A2PtNE/Xn95ON6J6awiBCImp+7Qp4haNvzUXGFcsMhWuklxP4Bk8zNw6w==} + '@cspell/cspell-performance-monitor@10.3.5': + resolution: {integrity: sha512-ZcnwB607vx9I4Ap8NFn1gDwRZRHAmU41pBgRW0IO2Dceue4zyEE4Hn7uh8I2ODHY0cgqHkueVjNekG7eUPFQ3A==} engines: {node: '>=22.18.0'} - '@cspell/cspell-pipe@10.3.4': - resolution: {integrity: sha512-PcqO2pcnNad3uOP2wmq4XD7MVpN8jm3ippyl5OfQD9PflKlwHMawLG/bWQ1eCdtWlARRQIR63ghfgNFFpjfCeg==} + '@cspell/cspell-pipe@10.3.5': + resolution: {integrity: sha512-ZeJCIFmak+UQf7DaBVh0mUFZes7NzbVAUtL8Gyhqmn3QBsIlngs75vVKNtS/4Dqf0HvuzDejUTK3TUtx0pwFAg==} engines: {node: '>=22.18.0'} - '@cspell/cspell-resolver@10.3.4': - resolution: {integrity: sha512-7dizl4j0FWyUymxCBO6UFLmQhALxYCxgZ2bf3bNc/BUO8wf/MP9B2Os5DwDaX/jFpMtpNnZ2rM5RAxWW7UKizA==} + '@cspell/cspell-resolver@10.3.5': + resolution: {integrity: sha512-UR+wX7rEUMVpOzBo6jhvIB0r5+mg/ZevtqJlj/haxtZaphNDrfcHEt5RUcofKQ03YU2QhlJearMU9RWiPaR1Ww==} engines: {node: '>=22.18.0'} - '@cspell/cspell-service-bus@10.3.4': - resolution: {integrity: sha512-D7C0Pr8bNHGzSn3HjiKrPXTD22Nui2Wk26aSZ4HFk6FMpxKe3pOT+STp9TYEK8D4hKA3sHLMef+YP4lCAPhD1g==} + '@cspell/cspell-service-bus@10.3.5': + resolution: {integrity: sha512-MsVtZ1YoiajW3zObXqqr+6rWHQuk97XKdB2Jl+Wv1djGVmLm5zJQ0ME35iK+OwYicswnBzAO9AIMjn2pPamtUQ==} engines: {node: '>=22.18.0'} - '@cspell/cspell-types@10.3.4': - resolution: {integrity: sha512-BFUDEATv9lhIa/JPK+BYulWEQcKmGHS0x8XLNeN+OeZtXNKOPGbvpgbXBRjQ33XqNuuOGRHulcok8M2998xA3w==} + '@cspell/cspell-types@10.3.5': + resolution: {integrity: sha512-zNnZVIE3+BfSG7CN5kLNSNo4US8066BCqoebrVksqo+5cn1LGPhgEPvM93wZwJ+LwCJRCuObEW+5BWn8+sqAWw==} engines: {node: '>=22.18.0'} - '@cspell/cspell-worker@10.3.4': - resolution: {integrity: sha512-KtJupFqoXR8tUwxiVDX0JA7cj98nTAJSZvukl++g1qQNlJZiVyxqlJ15lCo72A+YnfzOi2rUSoiChRERt1f2yg==} + '@cspell/cspell-worker@10.3.5': + resolution: {integrity: sha512-3M1iFXpHj3gvcDLS+GDBBlGnW8mfsrgeoHNp9WLRTm5YmOlovyYUNEcmDBdYK5Ji7fvrw4Osxob0W89Ny6id4w==} engines: {node: '>=22.18.0'} '@cspell/dict-ada@4.1.1': @@ -605,24 +605,24 @@ packages: '@cspell/dict-zig@1.0.0': resolution: {integrity: sha512-XibBIxBlVosU06+M6uHWkFeT0/pW5WajDRYdXG2CgHnq85b0TI/Ks0FuBJykmsgi2CAD3Qtx8UHFEtl/DSFnAQ==} - '@cspell/dynamic-import@10.3.4': - resolution: {integrity: sha512-qhnQpKALDV1grrGVMGZsvX1YCgP9Yq6is94V3LUvyK0r3Vx61UK/J4rwm4DUqUMshHL3i/5nE0HLcpjotHfIFA==} + '@cspell/dynamic-import@10.3.5': + resolution: {integrity: sha512-qg+KtbuVMP1WPZO6hj9jux1+UuaCUUoQ+/Rqh0G7LIK+72T81lv8tmJMymiCr31Wjd9w3cs0nOYdPZJ9o0ypQQ==} engines: {node: '>=22.18.0'} - '@cspell/filetypes@10.3.4': - resolution: {integrity: sha512-rMX/t5LUo8KkonCMeiAV5TWAcMqrABC5JOjq2SIt6krnpsqGJOTsBZYnJt24xQwpb0/WVpYrhLORkJQ3I6t3jg==} + '@cspell/filetypes@10.3.5': + resolution: {integrity: sha512-sgRV+O8bROVE9Qn6sTRYERdQ5g0jL6v+pxBLkvvQpiCovk9me7ycIDbY6OkRUiggqQr0uBeo/7lu4GtXyMTn8Q==} engines: {node: '>=22.18.0'} - '@cspell/rpc@10.3.4': - resolution: {integrity: sha512-GfkT0IYgWKqFH8pEEwzAiVtSYDlp5hAZp6f8lxKMdshw+LEQ/g4OCIG4tr1eZao35xMO4oiImnuBJahhIdddlQ==} + '@cspell/rpc@10.3.5': + resolution: {integrity: sha512-+Yei/jfo8anBaRM6eAaV6+W3FvpQX+Ethe+llZRVM4BnSVc8FLoNkTQiZUTrFq85KPDnmAfyI1Q77Fea3rKekA==} engines: {node: '>=22.18.0'} - '@cspell/strong-weak-map@10.3.4': - resolution: {integrity: sha512-0zibARpg8VECScJKGO/Eqr4rdHJvqP+lfNHxUk8ftYvbSHS8AQ//EEDcieM3Oyp7ILraVlVvanI6YeXFmdU7JA==} + '@cspell/strong-weak-map@10.3.5': + resolution: {integrity: sha512-78zH/wgrEJnY4pHmuC2MONaGIp7nQq/eceflF5qIaopnTz9Xn93Tm83BX4Goy1qNXAkgSOuYQibn5hQwx54INw==} engines: {node: '>=22.18.0'} - '@cspell/url@10.3.4': - resolution: {integrity: sha512-QGl/AmrVg+91ua2oVDFQSpIdqP1hUJjVeihYnsBIiQNjOfR8wPzj7aA8I9lyuC2/dC+iPDmmuI19KE65UwhPHg==} + '@cspell/url@10.3.5': + resolution: {integrity: sha512-wC2T2Fex2NIAJqZkAZBDAgDrvG9jWdS2DtbcAI+pCu+C/DDZ/2v9BCbE/rz/jy5/KrUUxEDJR+/MW90s7dX7nQ==} engines: {node: '>=22.18.0'} '@csstools/color-helpers@6.1.2': @@ -1818,44 +1818,44 @@ packages: resolution: {integrity: sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==} engines: {node: '>=12'} - cspell-config-lib@10.3.4: - resolution: {integrity: sha512-/0YHlZq6jPwXZ4Jbwv6Zwv3OpJRwrO4uXcyZB7+RBcj2AdyLZHr6T1VFElESHxCSzWBdZ0rsBVlgUZaeQaSrTg==} + cspell-config-lib@10.3.5: + resolution: {integrity: sha512-eAbLtQL72FOL0I6NKqrLXY8aPsKgL0nLHAJNKBg7lhV1hyiJeF71+8lE4qE0Nipx+OsZIUa6MespTKUwmfXd5A==} engines: {node: '>=22.18.0'} - cspell-dictionary@10.3.4: - resolution: {integrity: sha512-20jH9pCYfx1IiscrUcEa+ZxKeHafexE8WY6NW/l3UdXd38y4PhzeMovLG1QQEFoqN++KYsLO5Scn/A6sAEFFig==} + cspell-dictionary@10.3.5: + resolution: {integrity: sha512-rm+HN3dAs0XulakSodra4s6afLAD8KA+DWx8gniakV4JZBjxdKImNqC3W2yDxQJ6Sn+pI3IjSn6lCeQmwtnfXg==} engines: {node: '>=22.18.0'} - cspell-gitignore@10.3.4: - resolution: {integrity: sha512-sXtjB8VagZ9+GbAATQr+2u+vk4HACpfG+uPNOxjAYcYSZTWhF/VTx2FW1M/lsqNda5yLizI0xCW1eyU2or3DkA==} + cspell-gitignore@10.3.5: + resolution: {integrity: sha512-qQLZw+nfa+Zy2okSm3L8YLbonTKkuESChvCa3MLc1rWNLdYF5Vb5wPmsxna2nm90qEAZJBe41ognN47bHQlgJg==} engines: {node: '>=22.18.0'} hasBin: true - cspell-glob@10.3.4: - resolution: {integrity: sha512-uCUL8rqw8cfL+WY9Av9WFxIqw84Ek/MGLZhx+ROkbvYAm0Vjv5anw3GlZrQQCdDtQi902KFRrcHanqBngAKKXQ==} + cspell-glob@10.3.5: + resolution: {integrity: sha512-2FcPCV3nHIZrrJxt6mfE38trf6TMt8cZWIn/w/fTD6KdX6gK0a7nKlgMcHPj/EByjNTorKBYxj+tG0G0DBcrgQ==} engines: {node: '>=22.18.0'} - cspell-grammar@10.3.4: - resolution: {integrity: sha512-RBvvCtrKSv+QRyywsKa5XQCcN3xFAXBFtBdhJqt5irmrtQJGELkZAd/AwbFw6jOeHKPKR2OEhfqwcx6lP83gkg==} + cspell-grammar@10.3.5: + resolution: {integrity: sha512-isBahk4y5BOw12U3Iltb+gejea7gTNhUDE4XOqH/F+38W3HskJL4hRrxA/C8RyT/b+jvu8jy4hKafGzjsuM7oA==} engines: {node: '>=22.18.0'} hasBin: true - cspell-io@10.3.4: - resolution: {integrity: sha512-/wl1ATfDNw4zTh0zy9I3y6uX2liZqrwF13uo2bon8+frEQOttg3NrsXS13Ox3G0cfMKY33aulRX+9uRSxMtqIA==} + cspell-io@10.3.5: + resolution: {integrity: sha512-3rt3Hen3dDixXQtAjzV8oak/HTjGPqkdYckG2tkwu3Lfe0DnasD+dEGasxRu3uaEWGFY0SDIOUzA9+hi7bxCHA==} engines: {node: '>=22.18.0'} - cspell-lib@10.3.4: - resolution: {integrity: sha512-1TVE9OExPc9cbD65HoNWcBovkb3uu4M6hjZ2+36+P2JBqXmE3IwsyDy2fk5gdL+YYY/GTmEYW48NePQd9/gPvg==} + cspell-lib@10.3.5: + resolution: {integrity: sha512-4o0vN7WoZheOWtuobmm39NcQ7sFpd/80o+d7W3rrc6TDa1hxGJfMcC35dZhLcsAQKbG45fBcIl1KM/W7MRXzDQ==} engines: {node: '>=22.18.0'} - cspell-trie-lib@10.3.4: - resolution: {integrity: sha512-JgdMuAfYEzUTfbi0yVId/Nu3uze119QqU4Ss3oSD8li3IrmgCcNIk/RcDtfx+6dl20bmnXacx+J2xfXj6gPfiA==} + cspell-trie-lib@10.3.5: + resolution: {integrity: sha512-lVeiLN9U8EVrfW05BCSkMlSRPNG9RtjEGQD1cnFrrEKk/Af6Z3CJ+Yi2a6y37RTPQv8o6Tl4tBo3uBh+U6MvLQ==} engines: {node: '>=22.18.0'} peerDependencies: - '@cspell/cspell-types': 10.3.4 + '@cspell/cspell-types': 10.3.5 - cspell@10.3.4: - resolution: {integrity: sha512-WuyGrlaJdZXW0AHqcTFTe5DQoyB36JcAZAbdZJb7i7cKyMX8oKyRWuGP8r+mw0QcsZNZpsnxRX0oMd/D8Wkccw==} + cspell@10.3.5: + resolution: {integrity: sha512-Ru3hh2mpZ1wRt8NR69rp+kMEks+pHfdskwAdFIw0FtV+G9JUJbXV3B0wCc8A5KbQlhnR3vBeEX81V8chgl5g6g==} engines: {node: '>=22.18.0'} hasBin: true @@ -2740,7 +2740,6 @@ packages: jsonparse@1.3.1: resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} - engines: {'0': node >= 0.2.0} just-diff-apply@5.5.0: resolution: {integrity: sha512-OYTthRfSh55WOItVqwpefPtNt2VdKsq5AnAK6apdtR6yCH8pr0CmSr710J0Mf+WdQy7K/OzMy7K2MgAfdQURDw==} @@ -3916,11 +3915,11 @@ packages: resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==} engines: {node: '>=14.0.0'} - tldts-core@7.4.15: - resolution: {integrity: sha512-ERuv0p98XgSzmlSLJr8vDNxX+uATGInlN97V3R+JpYWaSqn5IG3ewWgCwczk4bkOpgth/o8Nt5FOi4B4w0n/1A==} + tldts-core@7.4.16: + resolution: {integrity: sha512-MDolfaSJtlSK5Y0A1xl3277ekubZwobpBjugknDizI9O5Rm60a1m8k4ICK+MRsCDzPygT81mp3BBf5RKDlFRfA==} - tldts@7.4.15: - resolution: {integrity: sha512-SJVBeHOxDbNoq14CvpAoA2mLEWdbldGK8nR+yumpjY5nrlZxOflcA7p1Qj1gbTGmbu9DY9qLj/bENSWhBzjxRQ==} + tldts@7.4.16: + resolution: {integrity: sha512-QwBER5KMR86IIjpIiO7H/Z3IMJPsZ1A6RKPAqzTTgOyUQUSt9FdnKcqhTaJmkY6HVrgouZHZR0ncK5QxvmnQeg==} hasBin: true to-regex-range@5.0.1: @@ -4504,7 +4503,7 @@ snapshots: '@conventional-changelog/template@1.4.0': {} - '@cspell/cspell-bundled-dicts@10.3.4': + '@cspell/cspell-bundled-dicts@10.3.5': dependencies: '@cspell/dict-ada': 4.1.1 '@cspell/dict-al': 1.1.1 @@ -4566,25 +4565,25 @@ snapshots: '@cspell/dict-vue': 3.0.5 '@cspell/dict-zig': 1.0.0 - '@cspell/cspell-json-reporter@10.3.4': + '@cspell/cspell-json-reporter@10.3.5': dependencies: - '@cspell/cspell-types': 10.3.4 + '@cspell/cspell-types': 10.3.5 - '@cspell/cspell-performance-monitor@10.3.4': {} + '@cspell/cspell-performance-monitor@10.3.5': {} - '@cspell/cspell-pipe@10.3.4': {} + '@cspell/cspell-pipe@10.3.5': {} - '@cspell/cspell-resolver@10.3.4': + '@cspell/cspell-resolver@10.3.5': dependencies: global-directory: 5.0.0 - '@cspell/cspell-service-bus@10.3.4': {} + '@cspell/cspell-service-bus@10.3.5': {} - '@cspell/cspell-types@10.3.4': {} + '@cspell/cspell-types@10.3.5': {} - '@cspell/cspell-worker@10.3.4': + '@cspell/cspell-worker@10.3.5': dependencies: - cspell-lib: 10.3.4 + cspell-lib: 10.3.5 '@cspell/dict-ada@4.1.1': {} @@ -4713,18 +4712,18 @@ snapshots: '@cspell/dict-zig@1.0.0': {} - '@cspell/dynamic-import@10.3.4': + '@cspell/dynamic-import@10.3.5': dependencies: - '@cspell/url': 10.3.4 + '@cspell/url': 10.3.5 import-meta-resolve: 4.2.0 - '@cspell/filetypes@10.3.4': {} + '@cspell/filetypes@10.3.5': {} - '@cspell/rpc@10.3.4': {} + '@cspell/rpc@10.3.5': {} - '@cspell/strong-weak-map@10.3.4': {} + '@cspell/strong-weak-map@10.3.5': {} - '@cspell/url@10.3.4': {} + '@cspell/url@10.3.5': {} '@csstools/color-helpers@6.1.2': {} @@ -5849,7 +5848,7 @@ snapshots: dependencies: '@commitlint/lint': 21.2.3 '@commitlint/types': 21.2.3 - cspell-lib: 10.3.4 + cspell-lib: 10.3.5 commitlint@21.2.3(@types/node@24.19.0)(@typescript/typescript6@6.0.2)(conventional-commits-parser@7.1.2): dependencies: @@ -5947,60 +5946,60 @@ snapshots: dependencies: type-fest: 1.4.0 - cspell-config-lib@10.3.4: + cspell-config-lib@10.3.5: dependencies: - '@cspell/cspell-types': 10.3.4 + '@cspell/cspell-types': 10.3.5 comment-json: 5.0.0 smol-toml: 1.9.0 yaml: 2.9.1 - cspell-dictionary@10.3.4: + cspell-dictionary@10.3.5: dependencies: - '@cspell/cspell-performance-monitor': 10.3.4 - '@cspell/cspell-pipe': 10.3.4 - '@cspell/cspell-types': 10.3.4 - cspell-trie-lib: 10.3.4(@cspell/cspell-types@10.3.4) + '@cspell/cspell-performance-monitor': 10.3.5 + '@cspell/cspell-pipe': 10.3.5 + '@cspell/cspell-types': 10.3.5 + cspell-trie-lib: 10.3.5(@cspell/cspell-types@10.3.5) fast-equals: 6.0.4 - cspell-gitignore@10.3.4: + cspell-gitignore@10.3.5: dependencies: - '@cspell/url': 10.3.4 - cspell-glob: 10.3.4 - cspell-io: 10.3.4 + '@cspell/url': 10.3.5 + cspell-glob: 10.3.5 + cspell-io: 10.3.5 - cspell-glob@10.3.4: + cspell-glob@10.3.5: dependencies: - '@cspell/url': 10.3.4 + '@cspell/url': 10.3.5 picomatch: 4.0.7 - cspell-grammar@10.3.4: - dependencies: - '@cspell/cspell-pipe': 10.3.4 - '@cspell/cspell-types': 10.3.4 - - cspell-io@10.3.4: - dependencies: - '@cspell/cspell-service-bus': 10.3.4 - '@cspell/url': 10.3.4 - - cspell-lib@10.3.4: - dependencies: - '@cspell/cspell-bundled-dicts': 10.3.4 - '@cspell/cspell-performance-monitor': 10.3.4 - '@cspell/cspell-pipe': 10.3.4 - '@cspell/cspell-resolver': 10.3.4 - '@cspell/cspell-types': 10.3.4 - '@cspell/dynamic-import': 10.3.4 - '@cspell/filetypes': 10.3.4 - '@cspell/rpc': 10.3.4 - '@cspell/strong-weak-map': 10.3.4 - '@cspell/url': 10.3.4 - cspell-config-lib: 10.3.4 - cspell-dictionary: 10.3.4 - cspell-glob: 10.3.4 - cspell-grammar: 10.3.4 - cspell-io: 10.3.4 - cspell-trie-lib: 10.3.4(@cspell/cspell-types@10.3.4) + cspell-grammar@10.3.5: + dependencies: + '@cspell/cspell-pipe': 10.3.5 + '@cspell/cspell-types': 10.3.5 + + cspell-io@10.3.5: + dependencies: + '@cspell/cspell-service-bus': 10.3.5 + '@cspell/url': 10.3.5 + + cspell-lib@10.3.5: + dependencies: + '@cspell/cspell-bundled-dicts': 10.3.5 + '@cspell/cspell-performance-monitor': 10.3.5 + '@cspell/cspell-pipe': 10.3.5 + '@cspell/cspell-resolver': 10.3.5 + '@cspell/cspell-types': 10.3.5 + '@cspell/dynamic-import': 10.3.5 + '@cspell/filetypes': 10.3.5 + '@cspell/rpc': 10.3.5 + '@cspell/strong-weak-map': 10.3.5 + '@cspell/url': 10.3.5 + cspell-config-lib: 10.3.5 + cspell-dictionary: 10.3.5 + cspell-glob: 10.3.5 + cspell-grammar: 10.3.5 + cspell-io: 10.3.5 + cspell-trie-lib: 10.3.5(@cspell/cspell-types@10.3.5) env-paths: 4.0.0 gensequence: 8.0.8 import-fresh: 4.0.1 @@ -6009,29 +6008,29 @@ snapshots: vscode-uri: 3.2.0 xdg-basedir: 5.1.0 - cspell-trie-lib@10.3.4(@cspell/cspell-types@10.3.4): + cspell-trie-lib@10.3.5(@cspell/cspell-types@10.3.5): dependencies: - '@cspell/cspell-types': 10.3.4 + '@cspell/cspell-types': 10.3.5 - cspell@10.3.4: + cspell@10.3.5: dependencies: - '@cspell/cspell-json-reporter': 10.3.4 - '@cspell/cspell-performance-monitor': 10.3.4 - '@cspell/cspell-pipe': 10.3.4 - '@cspell/cspell-types': 10.3.4 - '@cspell/cspell-worker': 10.3.4 - '@cspell/dynamic-import': 10.3.4 - '@cspell/url': 10.3.4 + '@cspell/cspell-json-reporter': 10.3.5 + '@cspell/cspell-performance-monitor': 10.3.5 + '@cspell/cspell-pipe': 10.3.5 + '@cspell/cspell-types': 10.3.5 + '@cspell/cspell-worker': 10.3.5 + '@cspell/dynamic-import': 10.3.5 + '@cspell/url': 10.3.5 ansi-regex: 6.4.0 chalk: 6.0.1 chalk-template: 1.1.3 commander: 15.0.0 - cspell-config-lib: 10.3.4 - cspell-dictionary: 10.3.4 - cspell-gitignore: 10.3.4 - cspell-glob: 10.3.4 - cspell-io: 10.3.4 - cspell-lib: 10.3.4 + cspell-config-lib: 10.3.5 + cspell-dictionary: 10.3.5 + cspell-gitignore: 10.3.5 + cspell-glob: 10.3.5 + cspell-io: 10.3.5 + cspell-lib: 10.3.5 flatted: 3.4.4 semver: 7.8.5 tinyglobby: 0.2.17 @@ -8081,11 +8080,11 @@ snapshots: tinyrainbow@3.1.1: {} - tldts-core@7.4.15: {} + tldts-core@7.4.16: {} - tldts@7.4.15: + tldts@7.4.16: dependencies: - tldts-core: 7.4.15 + tldts-core: 7.4.16 to-regex-range@5.0.1: dependencies: @@ -8093,7 +8092,7 @@ snapshots: tough-cookie@6.0.2: dependencies: - tldts: 7.4.15 + tldts: 7.4.16 tr46@6.0.0: dependencies: diff --git a/src/get.ts b/src/get.ts index 00f3106..2086400 100644 --- a/src/get.ts +++ b/src/get.ts @@ -1,3 +1,4 @@ +import { getNode } from './internal/getNode.js'; import { pathParts } from './pathParts.js'; import type { Path, PathValue } from './types/objects.js'; @@ -23,20 +24,5 @@ export function get | Proper input: Input, path: InputPath, ): unknown { - if (path === '') { - return input; - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let current: any = input; - - for (const part of pathParts(path)) { - if (typeof current === 'object' && current !== null && part in current) { - current = current[part]; - } else { - return; - } - } - - return current; + return path === '' ? input : getNode(input, pathParts(path)).value; } diff --git a/src/has.ts b/src/has.ts index df7145a..0cc96d5 100644 --- a/src/has.ts +++ b/src/has.ts @@ -1,3 +1,4 @@ +import { getNode } from './internal/getNode.js'; import { pathParts } from './pathParts.js'; import { isAnyObject } from './predicate/isAnyObject.js'; @@ -31,24 +32,9 @@ export function has | string input: Input, path: InputPath, ): input is Input & FromPath, unknown, PathValue>> { - if (!isAnyObject(input)) { + if (!isAnyObject(input) || path === '') { return false; } - if (path === '') { - return false; - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let current: any = input; - - for (const part of pathParts(path)) { - if (typeof current === 'object' && current !== null && part in current) { - current = current[part]; - } else { - return false; - } - } - - return true; + return getNode(input, pathParts(path)).found; } diff --git a/src/internal/getNode.ts b/src/internal/getNode.ts new file mode 100644 index 0000000..713291f --- /dev/null +++ b/src/internal/getNode.ts @@ -0,0 +1,44 @@ +import { isAnyObjectWith } from '../predicate/isAnyObjectWith.js'; + +export type GetNodeResult = + | { + value: unknown; + found: true; + visitedNodes: PropertyKey[]; + } + | { + value: undefined; + found: false; + visitedNodes: PropertyKey[]; + }; + +/** + * Retrieves the value of a nested property from an object based on a sequence of keys. + * + * @internal + */ +export function getNode(input: Input, pathKeys: Iterable): GetNodeResult { + let current: unknown = input; + + const visitedNodes: PropertyKey[] = []; + + for (const key of pathKeys) { + visitedNodes.push(key); + + if (isAnyObjectWith(current, key)) { + current = current[key]; + } else { + return { + value: undefined, + found: false, + visitedNodes, + }; + } + } + + return { + value: current, + found: true, + visitedNodes, + }; +} diff --git a/src/predicate/factory/index.ts b/src/predicate/factory/index.ts index 2029e40..5b812b1 100644 --- a/src/predicate/factory/index.ts +++ b/src/predicate/factory/index.ts @@ -12,6 +12,7 @@ export * from './nullable.js'; export * from './nullish.js'; export * from './optional.js'; export * from './range.js'; +export * from './record.js'; export * from './shape.js'; export * from './simple.js'; export * from './stringLength.js'; diff --git a/src/predicate/factory/record.ts b/src/predicate/factory/record.ts new file mode 100644 index 0000000..4c213f7 --- /dev/null +++ b/src/predicate/factory/record.ts @@ -0,0 +1,13 @@ +import { isObject } from '../isObject.js'; + +import type { TypePredicateFn } from '../../types/functions.js'; + +export const record = ( + keyPredicate: TypePredicateFn, + valuePredicate: TypePredicateFn, +): TypePredicateFn> => { + return (input: unknown): input is Record => + isObject(input) + ? Reflect.ownKeys(input).every((key) => keyPredicate(key) && valuePredicate(Reflect.get(input, key))) + : false; +}; diff --git a/src/predicate/isAnyObjectWith.ts b/src/predicate/isAnyObjectWith.ts index 6facbfa..bd798d4 100644 --- a/src/predicate/isAnyObjectWith.ts +++ b/src/predicate/isAnyObjectWith.ts @@ -1,17 +1,25 @@ import { isAnyObject } from './isAnyObject.js'; /** - * Determine if `input` is an object and has the provided properties. + * Determine if `input` is an object and has the provided property or properties. * * @example * ```ts + * isAnyObjectWith({ a: 1 }, 'a'); // true + * isAnyObjectWith({ a: 1 }, 'b'); // false * isAnyObjectWith({ a: 1, b: 2 }, ['a', 'b']); // true * isAnyObjectWith({ a: 1 }, ['a', 'b']); // false + * isAnyObjectWith({}, 'toString'); // true (inherited properties are included) + * isAnyObjectWith(['a'], 0); // true (arrays are objects) + * isAnyObjectWith(null, 'a'); // false * ``` */ export const isAnyObjectWith = ( input: T, - properties: P[], + properties: P | P[], ): input is T & Record => { - return isAnyObject(input) && properties.every((property) => property in input); + return ( + isAnyObject(input) && + (Array.isArray(properties) ? properties.every((property) => property in input) : properties in input) + ); }; diff --git a/src/set.ts b/src/set.ts index 322ac85..71f0da8 100644 --- a/src/set.ts +++ b/src/set.ts @@ -1,3 +1,5 @@ +import { assertIsObject } from './assertion/assertIsObject.js'; +import { getNode } from './internal/getNode.js'; import { pathParts } from './pathParts.js'; import type { Path, PathValue } from './types/objects.js'; @@ -48,21 +50,13 @@ export function set | string throw new Error('Path must have at least one part'); } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let current: any = input; + const result = getNode(input, parts); - const useParts: PropertyKey[] = []; - - // get a reference to the last node that will be updated - for (const part of parts) { - useParts.push(part); - - if (typeof current === 'object' && current !== null && part in current) { - current = current[part]; - } else { - throw new Error(`Path "${useParts.join('.')}" does not exist in the input object`); - } + if (!result.found) { + throw new Error(`Path "${result.visitedNodes.join('.')}" does not exist in the input object`); } - return (current[lastPart] = value); + assertIsObject(result.value); + + return (result.value[lastPart] = value); } diff --git a/test/internal/getNode.test.ts b/test/internal/getNode.test.ts new file mode 100644 index 0000000..48b2b71 --- /dev/null +++ b/test/internal/getNode.test.ts @@ -0,0 +1,210 @@ +import { describe, it, expect } from 'vitest'; + +import { getNode } from '../../src/internal/getNode.js'; + +describe('getNode()', () => { + it('Gets a node from an object', () => { + const data = { + job: { title: 'tester' }, + }; + + expect(getNode(data, ['job', 'title'])).toEqual({ + value: 'tester', + found: true, + visitedNodes: ['job', 'title'], + }); + }); + + it('Returns the input when no node names are provided', () => { + const data = { name: 'test' }; + + const result = getNode(data, []); + + expect(result).toEqual({ + value: data, + found: true, + visitedNodes: [], + }); + + expect(result.value).toBe(data); + }); + + it('Returns a reference to nested objects', () => { + const job = { title: 'tester' }; + const data = { job }; + + expect(getNode(data, ['job']).value).toBe(job); + }); + + it('Gets nodes from arrays', () => { + const data = { + items: [{ id: 1 }, { id: 2 }], + }; + + expect(getNode(data, ['items', 1, 'id'])).toEqual({ + value: 2, + found: true, + visitedNodes: ['items', 1, 'id'], + }); + + expect(getNode(data, ['items', '0', 'id'])).toEqual({ + value: 1, + found: true, + visitedNodes: ['items', '0', 'id'], + }); + }); + + it('Gets nodes using symbol keys', () => { + const key = Symbol('key'); + const data = { + [key]: { value: true }, + }; + + expect(getNode(data, [key, 'value'])).toEqual({ + value: true, + found: true, + visitedNodes: [key, 'value'], + }); + }); + + it('Finds nodes whose value is undefined or null', () => { + const data = { + a: undefined, + b: null, + }; + + expect(getNode(data, ['a'])).toEqual({ + value: undefined, + found: true, + visitedNodes: ['a'], + }); + + expect(getNode(data, ['b'])).toEqual({ + value: null, + found: true, + visitedNodes: ['b'], + }); + }); + + it('Includes inherited properties', () => { + const data = Object.create({ inherited: 'value' }); + + expect(getNode(data, ['inherited'])).toEqual({ + value: 'value', + found: true, + visitedNodes: ['inherited'], + }); + }); + + it('Accepts any iterable of node names', () => { + const data = { + a: { b: { c: 'c' } }, + }; + + function* names(): Generator { + yield 'a'; + yield 'b'; + yield 'c'; + } + + expect(getNode(data, names())).toEqual({ + value: 'c', + found: true, + visitedNodes: ['a', 'b', 'c'], + }); + + expect(getNode(data, new Set(['a', 'b']))).toEqual({ + value: { c: 'c' }, + found: true, + visitedNodes: ['a', 'b'], + }); + }); + + describe('Not found', () => { + it('Returns not found when a property does not exist', () => { + const data = { + job: { title: 'tester' }, + }; + + expect(getNode(data, ['job', 'salary'])).toEqual({ + value: undefined, + found: false, + visitedNodes: ['job', 'salary'], + }); + }); + + it('Stops at the first missing node', () => { + const data = { + job: { title: 'tester' }, + }; + + expect(getNode(data, ['company', 'name', 'length'])).toEqual({ + value: undefined, + found: false, + visitedNodes: ['company'], + }); + }); + + it('Returns not found when traversing through null or undefined', () => { + const data = { + a: null, + b: undefined, + }; + + expect(getNode(data, ['a', 'b'])).toEqual({ + value: undefined, + found: false, + visitedNodes: ['a', 'b'], + }); + + expect(getNode(data, ['b', 'c'])).toEqual({ + value: undefined, + found: false, + visitedNodes: ['b', 'c'], + }); + }); + + it('Does not traverse into primitive values', () => { + const data = { + name: 'test', + count: 1, + }; + + expect(getNode(data, ['name', 'length'])).toEqual({ + value: undefined, + found: false, + visitedNodes: ['name', 'length'], + }); + + expect(getNode(data, ['count', 'toFixed'])).toEqual({ + value: undefined, + found: false, + visitedNodes: ['count', 'toFixed'], + }); + }); + + it('Does not traverse into functions', () => { + const data = { + fn: Object.assign(() => {}, { prop: true }), + }; + + expect(getNode(data, ['fn', 'prop'])).toEqual({ + value: undefined, + found: false, + visitedNodes: ['fn', 'prop'], + }); + }); + + it('Returns not found for out of bounds array indexes', () => { + const data = { + items: ['a'], + }; + + expect(getNode(data, ['items', 1])).toEqual({ + value: undefined, + found: false, + visitedNodes: ['items', 1], + }); + }); + }); +}); diff --git a/test/predicate/factory/record.test-d.ts b/test/predicate/factory/record.test-d.ts new file mode 100644 index 0000000..e20105f --- /dev/null +++ b/test/predicate/factory/record.test-d.ts @@ -0,0 +1,33 @@ +import { describe, expectTypeOf, it } from 'vitest'; + +import { literal } from '../../../src/predicate/factory/literal.js'; +import { record } from '../../../src/predicate/factory/record.js'; +import { isNumber } from '../../../src/predicate/isNumber.js'; +import { isString } from '../../../src/predicate/isString.js'; + +import type { TypePredicateFn } from '../../../src/types/functions.js'; + +describe('record()', () => { + it('Returns a type predicate function', () => { + const fn = record(isString, isNumber); + + expectTypeOf(fn).toBeFunction(); + expectTypeOf(fn).parameter(0).toEqualTypeOf(); + expectTypeOf(fn).toEqualTypeOf>>(); + }); + + it('Narrows input', () => { + const input: unknown = { a: 1 }; + + if (record(literal('a'), isNumber)(input)) { + expectTypeOf(input).toEqualTypeOf>(); + } + }); + + it('Only accepts string or symbol key predicates', () => { + expectTypeOf(record).parameter(0).toEqualTypeOf>(); + + // @ts-expect-error: number keys are not supported since object keys are strings or symbols. + record(isNumber, isNumber); + }); +}); diff --git a/test/predicate/factory/record.test.ts b/test/predicate/factory/record.test.ts new file mode 100644 index 0000000..127e087 --- /dev/null +++ b/test/predicate/factory/record.test.ts @@ -0,0 +1,77 @@ +import { describe, it, expect, vi } from 'vitest'; + +import { literal } from '../../../src/predicate/factory/literal.js'; +import { record } from '../../../src/predicate/factory/record.js'; +import { isNumber } from '../../../src/predicate/isNumber.js'; +import { isString } from '../../../src/predicate/isString.js'; +import { isSymbol } from '../../../src/predicate/isSymbol.js'; + +describe('record()', () => { + it('Returns a type predicate function', () => { + expect(record(isString, isNumber)).toBeInstanceOf(Function); + }); + + it('Returns true when all keys and values pass', () => { + const fn = record(isString, isNumber); + + expect(fn({})).toBeTruthy(); + expect(fn({ a: 1, b: 2 })).toBeTruthy(); + expect(fn(Object.create(null))).toBeTruthy(); + }); + + it('Returns false when a value fails', () => { + const fn = record(isString, isNumber); + + expect(fn({ a: 1, b: 'test' })).toBeFalsy(); + }); + + it('Returns false when a key fails', () => { + const fn = record(isString, isNumber); + + expect(fn({ a: 1, [Symbol('test')]: 2 })).toBeFalsy(); + expect(record(isSymbol, isNumber)({ [Symbol('test')]: 1 })).toBeTruthy(); + expect(record(literal('a'), isNumber)({ a: 1, b: 2 })).toBeFalsy(); + }); + + it('Treats numeric keys as strings', () => { + expect(record(isString, isNumber)({ 1: 1 })).toBeTruthy(); + // @ts-expect-error: number keys are not supported since object keys are strings or symbols. + expect(record(isNumber, isNumber)({ 1: 1 })).toBeFalsy(); + }); + + it('Checks non-enumerable own keys', () => { + const input = Object.defineProperty({ a: 1 }, 'hidden', { value: 'test', enumerable: false }); + + expect(record(isString, isNumber)(input)).toBeFalsy(); + }); + + it('Ignores inherited keys', () => { + const input = Object.create({ inherited: 'test' }); + + input.a = 1; + + expect(record(isString, isNumber)(input)).toBeTruthy(); + }); + + it('Returns false for non-objects', () => { + const fn = record(isString, isNumber); + + expect(fn(null)).toBeFalsy(); + expect(fn(undefined)).toBeFalsy(); + expect(fn([1, 2, 3])).toBeFalsy(); + expect(fn('test')).toBeFalsy(); + expect(fn(123)).toBeFalsy(); + expect(fn(() => 1)).toBeFalsy(); + }); + + it('Stops checking after the first failure', () => { + const keyPredicate = vi.fn(isString) as unknown as typeof isString; + const valuePredicate = vi.fn(isNumber) as unknown as typeof isNumber; + + const fn = record(keyPredicate, valuePredicate); + + expect(fn({ a: 'fail', b: 2, c: 3 })).toBeFalsy(); + expect(keyPredicate).toHaveBeenCalledTimes(1); + expect(valuePredicate).toHaveBeenCalledTimes(1); + }); +}); diff --git a/test/predicate/isAnyObjectWith.test.ts b/test/predicate/isAnyObjectWith.test.ts index dd61605..464f0cb 100644 --- a/test/predicate/isAnyObjectWith.test.ts +++ b/test/predicate/isAnyObjectWith.test.ts @@ -9,7 +9,41 @@ describe('isAnyObjectWith()', () => { expect(isAnyObjectWith(['item'], ['length'])).toBeTruthy(); }); - it.each([null, false, 'string', Math.PI, Symbol()])('Returns false for invalid inputs', (item) => { - expect(isAnyObjectWith(item, ['property'])).toBeFalsy(); + it('Accepts a single property', () => { + expect(isAnyObjectWith({ a: 1 }, 'a')).toBeTruthy(); + expect(isAnyObjectWith({ a: 1 }, 'b')).toBeFalsy(); + expect(isAnyObjectWith(['a'], 0)).toBeTruthy(); + expect(isAnyObjectWith(['a'], 1)).toBeFalsy(); + + const symbol = Symbol(); + + expect(isAnyObjectWith({ [symbol]: true }, symbol)).toBeTruthy(); + expect(isAnyObjectWith({}, symbol)).toBeFalsy(); + }); + + it('Requires every property when given an array', () => { + expect(isAnyObjectWith({ a: 1, b: 2 }, ['a', 'b'])).toBeTruthy(); + expect(isAnyObjectWith({ a: 1 }, ['a', 'b'])).toBeFalsy(); + }); + + it('Returns true for an empty array of properties', () => { + expect(isAnyObjectWith({}, [])).toBeTruthy(); }); + + it('Includes inherited properties', () => { + expect(isAnyObjectWith({}, 'toString')).toBeTruthy(); + expect(isAnyObjectWith(Object.setPrototypeOf({}, { property: true }), 'property')).toBeTruthy(); + }); + + it('Includes properties with undefined values', () => { + expect(isAnyObjectWith({ property: undefined }, 'property')).toBeTruthy(); + }); + + it.each([null, undefined, false, 'string', Math.PI, Symbol(), () => {}])( + 'Returns false for invalid inputs', + (item) => { + expect(isAnyObjectWith(item, ['property'])).toBeFalsy(); + expect(isAnyObjectWith(item, 'property')).toBeFalsy(); + }, + ); });