diff --git a/common/changes/@rushstack/npm-check-fork/atomic-style-claude_2026-01-24-18-36.json b/common/changes/@rushstack/npm-check-fork/atomic-style-claude_2026-01-24-18-36.json new file mode 100644 index 00000000000..43c79c40984 --- /dev/null +++ b/common/changes/@rushstack/npm-check-fork/atomic-style-claude_2026-01-24-18-36.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/npm-check-fork", + "comment": "Remove dependencies on throat and package-json", + "type": "patch" + } + ], + "packageName": "@rushstack/npm-check-fork" +} \ No newline at end of file diff --git a/common/config/subspaces/build-tests-subspace/pnpm-lock.yaml b/common/config/subspaces/build-tests-subspace/pnpm-lock.yaml index 6de7624d794..cc3b25b61ed 100644 --- a/common/config/subspaces/build-tests-subspace/pnpm-lock.yaml +++ b/common/config/subspaces/build-tests-subspace/pnpm-lock.yaml @@ -960,20 +960,12 @@ packages: '@sinclair/typebox@0.34.48': resolution: {integrity: sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==} - '@sindresorhus/is@4.6.0': - resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} - engines: {node: '>=10'} - '@sinonjs/commons@3.0.1': resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} '@sinonjs/fake-timers@10.3.0': resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} - '@szmarczak/http-timer@4.0.6': - resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} - engines: {node: '>=10'} - '@types/argparse@1.0.38': resolution: {integrity: sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==} @@ -989,9 +981,6 @@ packages: '@types/babel__traverse@7.28.0': resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} - '@types/cacheable-request@6.0.3': - resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==} - '@types/eslint-scope@3.7.7': resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} @@ -1010,9 +999,6 @@ packages: '@types/html-minifier-terser@6.1.0': resolution: {integrity: sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==} - '@types/http-cache-semantics@4.0.4': - resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} - '@types/istanbul-lib-coverage@2.0.4': resolution: {integrity: sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==} @@ -1034,18 +1020,12 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} - '@types/keyv@3.1.4': - resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} - '@types/node@20.17.19': resolution: {integrity: sha512-LEwC7o1ifqg/6r2gn9Dns0f1rhK+fPFDoMiceTJ6kWmVk6bgXBI/9IOWfVan4WiAavK9pIVWdX0/e3J+eEUh5A==} '@types/prettier@2.7.3': resolution: {integrity: sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==} - '@types/responselike@1.0.3': - resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} - '@types/stack-utils@2.0.3': resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} @@ -1422,14 +1402,6 @@ packages: builtins@1.0.3: resolution: {integrity: sha512-uYBjakWipfaO/bXI7E8rq6kpwHRZK5cNYrUv2OzZSI/FvmdMyXJ2tG9dKcjEC5YHmHpUAwsargWIZNWdxb/bnQ==} - cacheable-lookup@5.0.4: - resolution: {integrity: sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==} - engines: {node: '>=10.6.0'} - - cacheable-request@7.0.4: - resolution: {integrity: sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==} - engines: {node: '>=8'} - call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -1514,9 +1486,6 @@ packages: resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==} engines: {node: '>= 10'} - clone-response@1.0.3: - resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==} - clone@1.0.4: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} @@ -1614,10 +1583,6 @@ packages: resolution: {integrity: sha512-syBZ+rnAK3EgMsH2aYEOLUW7mZSY9Gb+0wUMCFsZvcmiz+HigA0LOcq/HoQqVuGG+EKykunc7QG2bzrponfaSw==} deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. - decompress-response@6.0.0: - resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} - engines: {node: '>=10'} - dedent@1.7.1: resolution: {integrity: sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==} peerDependencies: @@ -1626,10 +1591,6 @@ packages: babel-plugin-macros: optional: true - deep-extend@0.6.0: - resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} - engines: {node: '>=4.0.0'} - deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -1640,10 +1601,6 @@ packages: defaults@1.0.4: resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} - defer-to-connect@2.0.1: - resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} - engines: {node: '>=10'} - define-data-property@1.1.4: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} @@ -1724,9 +1681,6 @@ packages: resolution: {integrity: sha512-6qOwkl1g0fv0DN3Y3ggr2EaZXN71aoAqPp3p/pVaWSBSIo+YjLOWN61Fva43oVyQNPf7kgm8lkudzlzojwE2jw==} engines: {node: '>=10'} - end-of-stream@1.4.5: - resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} - enhanced-resolve@5.18.4: resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==} engines: {node: '>=10.13.0'} @@ -2031,10 +1985,6 @@ packages: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} - get-stream@5.2.0: - resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} - engines: {node: '>=8'} - get-stream@6.0.1: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} @@ -2087,10 +2037,6 @@ packages: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} - got@11.8.6: - resolution: {integrity: sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==} - engines: {node: '>=10.19.0'} - graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -2159,13 +2105,6 @@ packages: htmlparser2@6.1.0: resolution: {integrity: sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==} - http-cache-semantics@4.2.0: - resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} - - http2-wrapper@1.0.3: - resolution: {integrity: sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==} - engines: {node: '>=10.19.0'} - https-proxy-agent@5.0.1: resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} engines: {node: '>= 6'} @@ -2222,9 +2161,6 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - ini@1.3.8: - resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} - inquirer@8.2.7: resolution: {integrity: sha512-UjOaSel/iddGZJ5xP/Eixh6dY1XghiBw4XK13rCCIJcJfyhhoul/7KhLLUGtebEj6GDYM6Vnx/mVsjx2L/mFIA==} engines: {node: '>=12.0.0'} @@ -2685,10 +2621,6 @@ packages: lower-case@2.0.2: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} - lowercase-keys@2.0.0: - resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} - engines: {node: '>=8'} - lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -2742,14 +2674,6 @@ packages: resolution: {integrity: sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==} engines: {node: '>=8'} - mimic-response@1.0.1: - resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} - engines: {node: '>=4'} - - mimic-response@3.1.0: - resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} - engines: {node: '>=10'} - minimatch@10.0.3: resolution: {integrity: sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==} engines: {node: 20 || >=22} @@ -2829,10 +2753,6 @@ packages: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} - normalize-url@6.1.0: - resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} - engines: {node: '>=10'} - npm-bundled@2.0.1: resolution: {integrity: sha512-gZLxXdjEzE/+mOstGDqR6b0EkhJ+kM6fxM6vUuckuctuVPh80Q6pw/rSZj9s4Gex9GxWtIicO1pc8DB9KZWudw==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} @@ -2926,10 +2846,6 @@ packages: resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} engines: {node: '>= 0.4'} - p-cancelable@2.1.1: - resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} - engines: {node: '>=8'} - p-defer@1.0.0: resolution: {integrity: sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw==} engines: {node: '>=4'} @@ -2962,10 +2878,6 @@ packages: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} - package-json@7.0.0: - resolution: {integrity: sha512-CHJqc94AA8YfSLHGQT3DbvSIuE12NLFekpM4n7LRrAd3dOJtA911+4xe9q6nC3/jcKraq7nNS9VxgtT0KC+diA==} - engines: {node: '>=12'} - pako@1.0.11: resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} @@ -3050,9 +2962,6 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} - pump@3.0.3: - resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} - punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -3063,20 +2972,12 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - quick-lru@5.1.1: - resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} - engines: {node: '>=10'} - ramda@0.27.2: resolution: {integrity: sha512-SbiLPU40JuJniHexQSAgad32hfwd+DRUdwF2PlVuI5RZD0/vahUco7R8vD86J/tcEKKF9vZrUVwgtmGCqlCKyA==} randombytes@2.1.0: resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} - rc@1.2.8: - resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} - hasBin: true - react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -3114,14 +3015,6 @@ packages: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} - registry-auth-token@4.2.2: - resolution: {integrity: sha512-PC5ZysNb42zpFME6D/XlIgtNGdTl8bBOCw90xQLVMpzuuubJKYDWFAEuUNc+Cn8Z8724tg2SDhDRrkVEsqfDMg==} - engines: {node: '>=6.0.0'} - - registry-url@5.1.0: - resolution: {integrity: sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==} - engines: {node: '>=8'} - relateurl@0.2.7: resolution: {integrity: sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==} engines: {node: '>= 0.10'} @@ -3133,9 +3026,6 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} - resolve-alpn@1.2.1: - resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} - resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -3157,9 +3047,6 @@ packages: resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==} hasBin: true - responselike@2.0.1: - resolution: {integrity: sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==} - restore-cursor@3.1.0: resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} engines: {node: '>=8'} @@ -3407,10 +3294,6 @@ packages: resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} engines: {node: '>=6'} - strip-json-comments@2.0.1: - resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} - engines: {node: '>=0.10.0'} - strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -3479,9 +3362,6 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} - throat@6.0.2: - resolution: {integrity: sha512-WKexMoJj3vEuK0yFEapj8y64V0A6xcuPuK9Gt1d0R+dzCSJc0lHqQytAbSB4cDAK0dWh4T0E2ETkoLE2WZ41OQ==} - through2@4.0.2: resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==} @@ -4305,7 +4185,7 @@ snapshots: '@rushstack/heft-config-file': file:../../../libraries/heft-config-file(@types/node@20.17.19) '@rushstack/lookup-by-path': file:../../../libraries/lookup-by-path(@types/node@20.17.19) '@rushstack/node-core-library': file:../../../libraries/node-core-library(@types/node@20.17.19) - '@rushstack/npm-check-fork': file:../../../libraries/npm-check-fork + '@rushstack/npm-check-fork': file:../../../libraries/npm-check-fork(@types/node@20.17.19) '@rushstack/package-deps-hash': file:../../../libraries/package-deps-hash(@types/node@20.17.19) '@rushstack/package-extractor': file:../../../libraries/package-extractor(@types/node@20.17.19) '@rushstack/rig-package': file:../../../libraries/rig-package @@ -4855,13 +4735,14 @@ snapshots: optionalDependencies: '@types/node': 20.17.19 - '@rushstack/npm-check-fork@file:../../../libraries/npm-check-fork': + '@rushstack/npm-check-fork@file:../../../libraries/npm-check-fork(@types/node@20.17.19)': dependencies: + '@rushstack/node-core-library': file:../../../libraries/node-core-library(@types/node@20.17.19) giturl: 2.0.0 lodash: 4.17.23 - package-json: 7.0.0 semver: 7.5.4 - throat: 6.0.2 + transitivePeerDependencies: + - '@types/node' '@rushstack/operation-graph@file:../../../libraries/operation-graph(@types/node@20.17.19)': dependencies: @@ -4959,8 +4840,6 @@ snapshots: '@sinclair/typebox@0.34.48': {} - '@sindresorhus/is@4.6.0': {} - '@sinonjs/commons@3.0.1': dependencies: type-detect: 4.0.8 @@ -4969,10 +4848,6 @@ snapshots: dependencies: '@sinonjs/commons': 3.0.1 - '@szmarczak/http-timer@4.0.6': - dependencies: - defer-to-connect: 2.0.1 - '@types/argparse@1.0.38': {} '@types/babel__core@7.20.5': @@ -4996,13 +4871,6 @@ snapshots: dependencies: '@babel/types': 7.28.6 - '@types/cacheable-request@6.0.3': - dependencies: - '@types/http-cache-semantics': 4.0.4 - '@types/keyv': 3.1.4 - '@types/node': 20.17.19 - '@types/responselike': 1.0.3 - '@types/eslint-scope@3.7.7': dependencies: '@types/eslint': 9.6.1 @@ -5025,8 +4893,6 @@ snapshots: '@types/html-minifier-terser@6.1.0': {} - '@types/http-cache-semantics@4.0.4': {} - '@types/istanbul-lib-coverage@2.0.4': {} '@types/istanbul-lib-coverage@2.0.6': {} @@ -5048,20 +4914,12 @@ snapshots: '@types/json5@0.0.29': {} - '@types/keyv@3.1.4': - dependencies: - '@types/node': 20.17.19 - '@types/node@20.17.19': dependencies: undici-types: 6.19.8 '@types/prettier@2.7.3': {} - '@types/responselike@1.0.3': - dependencies: - '@types/node': 20.17.19 - '@types/stack-utils@2.0.3': {} '@types/tapable@1.0.6': {} @@ -5665,18 +5523,6 @@ snapshots: builtins@1.0.3: {} - cacheable-lookup@5.0.4: {} - - cacheable-request@7.0.4: - dependencies: - clone-response: 1.0.3 - get-stream: 5.2.0 - http-cache-semantics: 4.2.0 - keyv: 4.5.4 - lowercase-keys: 2.0.0 - normalize-url: 6.1.0 - responselike: 2.0.1 - call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -5748,10 +5594,6 @@ snapshots: cli-width@3.0.0: {} - clone-response@1.0.3: - dependencies: - mimic-response: 1.0.1 - clone@1.0.4: {} cmd-extension@1.0.2: {} @@ -5834,14 +5676,8 @@ snapshots: debuglog@1.0.1: {} - decompress-response@6.0.0: - dependencies: - mimic-response: 3.1.0 - dedent@1.7.1: {} - deep-extend@0.6.0: {} - deep-is@0.1.4: {} deepmerge@4.3.1: {} @@ -5850,8 +5686,6 @@ snapshots: dependencies: clone: 1.0.4 - defer-to-connect@2.0.1: {} - define-data-property@1.1.4: dependencies: es-define-property: 1.0.1 @@ -5935,10 +5769,6 @@ snapshots: dependencies: mem: 8.1.1 - end-of-stream@1.4.5: - dependencies: - once: 1.4.0 - enhanced-resolve@5.18.4: dependencies: graceful-fs: 4.2.11 @@ -6456,10 +6286,6 @@ snapshots: dunder-proto: 1.0.1 es-object-atoms: 1.1.1 - get-stream@5.2.0: - dependencies: - pump: 3.0.3 - get-stream@6.0.1: {} get-symbol-description@1.1.0: @@ -6510,20 +6336,6 @@ snapshots: gopd@1.2.0: {} - got@11.8.6: - dependencies: - '@sindresorhus/is': 4.6.0 - '@szmarczak/http-timer': 4.0.6 - '@types/cacheable-request': 6.0.3 - '@types/responselike': 1.0.3 - cacheable-lookup: 5.0.4 - cacheable-request: 7.0.4 - decompress-response: 6.0.0 - http2-wrapper: 1.0.3 - lowercase-keys: 2.0.0 - p-cancelable: 2.1.1 - responselike: 2.0.1 - graceful-fs@4.2.11: {} graceful-fs@4.2.4: {} @@ -6590,13 +6402,6 @@ snapshots: domutils: 2.8.0 entities: 2.2.0 - http-cache-semantics@4.2.0: {} - - http2-wrapper@1.0.3: - dependencies: - quick-lru: 5.1.1 - resolve-alpn: 1.2.1 - https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 @@ -6642,8 +6447,6 @@ snapshots: inherits@2.0.4: {} - ini@1.3.8: {} - inquirer@8.2.7(@types/node@20.17.19): dependencies: '@inquirer/external-editor': 1.0.3(@types/node@20.17.19) @@ -7384,8 +7187,6 @@ snapshots: dependencies: tslib: 2.8.1 - lowercase-keys@2.0.0: {} - lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -7432,10 +7233,6 @@ snapshots: mimic-fn@3.1.0: {} - mimic-response@1.0.1: {} - - mimic-response@3.1.0: {} - minimatch@10.0.3: dependencies: '@isaacs/brace-expansion': 5.0.0 @@ -7517,8 +7314,6 @@ snapshots: normalize-path@3.0.0: {} - normalize-url@6.1.0: {} - npm-bundled@2.0.1: dependencies: npm-normalize-package-bin: 2.0.0 @@ -7637,8 +7432,6 @@ snapshots: object-keys: 1.1.1 safe-push-apply: 1.0.0 - p-cancelable@2.1.1: {} - p-defer@1.0.0: {} p-limit@2.3.0: @@ -7666,13 +7459,6 @@ snapshots: p-try@2.2.0: {} - package-json@7.0.0: - dependencies: - got: 11.8.6 - registry-auth-token: 4.2.2 - registry-url: 5.1.0 - semver: 7.7.3 - pako@1.0.11: {} param-case@3.0.4: @@ -7758,32 +7544,18 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 - pump@3.0.3: - dependencies: - end-of-stream: 1.4.5 - once: 1.4.0 - punycode@2.3.1: {} pure-rand@6.1.0: {} queue-microtask@1.2.3: {} - quick-lru@5.1.1: {} - ramda@0.27.2: {} randombytes@2.1.0: dependencies: safe-buffer: 5.2.1 - rc@1.2.8: - dependencies: - deep-extend: 0.6.0 - ini: 1.3.8 - minimist: 1.2.8 - strip-json-comments: 2.0.1 - react-is@16.13.1: {} react-is@18.3.1: {} @@ -7851,14 +7623,6 @@ snapshots: gopd: 1.2.0 set-function-name: 2.0.2 - registry-auth-token@4.2.2: - dependencies: - rc: 1.2.8 - - registry-url@5.1.0: - dependencies: - rc: 1.2.8 - relateurl@0.2.7: {} renderkid@3.0.0: @@ -7871,8 +7635,6 @@ snapshots: require-from-string@2.0.2: {} - resolve-alpn@1.2.1: {} - resolve-from@4.0.0: {} resolve-from@5.0.0: {} @@ -7891,10 +7653,6 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - responselike@2.0.1: - dependencies: - lowercase-keys: 2.0.0 - restore-cursor@3.1.0: dependencies: onetime: 5.1.2 @@ -8179,8 +7937,6 @@ snapshots: strip-final-newline@2.0.0: {} - strip-json-comments@2.0.1: {} - strip-json-comments@3.1.1: {} supports-color@5.5.0: @@ -8241,8 +7997,6 @@ snapshots: dependencies: any-promise: 1.3.0 - throat@6.0.2: {} - through2@4.0.2: dependencies: readable-stream: 3.6.2 diff --git a/common/config/subspaces/build-tests-subspace/repo-state.json b/common/config/subspaces/build-tests-subspace/repo-state.json index 6c5fab479be..1f3d58abea6 100644 --- a/common/config/subspaces/build-tests-subspace/repo-state.json +++ b/common/config/subspaces/build-tests-subspace/repo-state.json @@ -1,6 +1,6 @@ // DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush. { - "pnpmShrinkwrapHash": "da80c128380df78244e113861884b5e6a38ad7c5", + "pnpmShrinkwrapHash": "5d9bc6eee5c9d99eb1d47a58664f6c7488199d42", "preferredVersionsHash": "550b4cee0bef4e97db6c6aad726df5149d20e7d9", - "packageJsonInjectedDependenciesHash": "1c312688ef85bfdb64079cc271a46b18d816d411" + "packageJsonInjectedDependenciesHash": "88585d29209a34b4908b0928fd68566f978c2bca" } diff --git a/common/config/subspaces/default/pnpm-lock.yaml b/common/config/subspaces/default/pnpm-lock.yaml index 38034be545c..1f548223a28 100644 --- a/common/config/subspaces/default/pnpm-lock.yaml +++ b/common/config/subspaces/default/pnpm-lock.yaml @@ -3857,21 +3857,18 @@ importers: ../../../libraries/npm-check-fork: dependencies: + '@rushstack/node-core-library': + specifier: workspace:* + version: link:../node-core-library giturl: specifier: ^2.0.0 version: 2.0.0 lodash: specifier: ~4.17.15 version: 4.17.23 - package-json: - specifier: ^7 - version: 7.0.0 semver: specifier: ~7.5.4 version: 7.5.4 - throat: - specifier: ^6.0.2 - version: 6.0.2 devDependencies: '@rushstack/heft': specifier: workspace:* @@ -17721,9 +17718,6 @@ packages: peerDependencies: tslib: ^2 - throat@6.0.2: - resolution: {integrity: sha512-WKexMoJj3vEuK0yFEapj8y64V0A6xcuPuK9Gt1d0R+dzCSJc0lHqQytAbSB4cDAK0dWh4T0E2ETkoLE2WZ41OQ==} - throttle-debounce@3.0.1: resolution: {integrity: sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==} engines: {node: '>=10'} @@ -36285,8 +36279,6 @@ snapshots: dependencies: tslib: 2.8.1 - throat@6.0.2: {} - throttle-debounce@3.0.1: {} through2@2.0.5: diff --git a/common/config/subspaces/default/repo-state.json b/common/config/subspaces/default/repo-state.json index 6f7b9aabb5c..485952c12d6 100644 --- a/common/config/subspaces/default/repo-state.json +++ b/common/config/subspaces/default/repo-state.json @@ -1,5 +1,5 @@ // DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush. { - "pnpmShrinkwrapHash": "aebea9e28d4e012d8a37fc57c92bd28eb386df5b", + "pnpmShrinkwrapHash": "b0dbc9dd6df6e790055edb199b9d85eb3a0f200f", "preferredVersionsHash": "a9b67c38568259823f9cfb8270b31bf6d8470b27" } diff --git a/libraries/npm-check-fork/package.json b/libraries/npm-check-fork/package.json index 04dd5c34888..b3204169c8b 100644 --- a/libraries/npm-check-fork/package.json +++ b/libraries/npm-check-fork/package.json @@ -20,9 +20,8 @@ "dependencies": { "giturl": "^2.0.0", "lodash": "~4.17.15", - "package-json": "^10.0.1", "semver": "~7.5.4", - "throat": "^6.0.2" + "@rushstack/node-core-library": "workspace:*" }, "devDependencies": { "@rushstack/heft": "workspace:*", diff --git a/libraries/npm-check-fork/src/GetLatestFromRegistry.ts b/libraries/npm-check-fork/src/GetLatestFromRegistry.ts index d7dd2e97bf2..48a16d41bcb 100644 --- a/libraries/npm-check-fork/src/GetLatestFromRegistry.ts +++ b/libraries/npm-check-fork/src/GetLatestFromRegistry.ts @@ -1,45 +1,97 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + import os from 'node:os'; import _ from 'lodash'; import semver from 'semver'; -import packageJson from 'package-json'; -import throat from 'throat'; + +import { Async } from '@rushstack/node-core-library'; import bestGuessHomepage from './BestGuessHomepage'; -import type { INpmRegistryInfo } from './interfaces/INpmCheckRegistry'; +import { NpmRegistryClient, type INpmRegistryClientResult } from './NpmRegistryClient'; +import type { + INpmRegistryInfo, + INpmCheckRegistryData, + INpmRegistryPackageResponse +} from './interfaces/INpmCheckRegistry'; -const cpuCount: number = os.cpus().length; +// Module-level registry client instance (lazy initialized) +let _registryClient: NpmRegistryClient | undefined; + +/** + * Gets or creates the shared registry client instance. + */ +function getRegistryClient(): NpmRegistryClient { + if (!_registryClient) { + _registryClient = new NpmRegistryClient(); + } + return _registryClient; +} +/** + * Fetches package information from the npm registry. + * + * @param packageName - The name of the package to fetch + * @returns A promise that resolves to the package registry info + */ export default async function getNpmInfo(packageName: string): Promise { - const limit: () => Promise = throat(cpuCount, () => - packageJson(packageName, { fullMetadata: true, allVersions: true }) + const client: NpmRegistryClient = getRegistryClient(); + const result: INpmRegistryClientResult = await client.fetchPackageMetadataAsync(packageName); + + if (result.error) { + return { + error: `Registry error ${result.error}` + }; + } + + const rawData: INpmRegistryPackageResponse = result.data!; + const CRAZY_HIGH_SEMVER: string = '8000.0.0'; + const sortedVersions: string[] = _(rawData.versions) + .keys() + .remove(_.partial(semver.gt, CRAZY_HIGH_SEMVER)) + .sort(semver.compare) + .valueOf(); + + const latest: string = rawData['dist-tags'].latest; + const next: string = rawData['dist-tags'].next; + const latestStableRelease: string | undefined = semver.satisfies(latest, '*') + ? latest + : semver.maxSatisfying(sortedVersions, '*') || ''; + + // Cast to INpmCheckRegistryData for bestGuessHomepage compatibility + // INpmRegistryPackageResponse is a superset of INpmCheckRegistryData + const registryData: INpmCheckRegistryData = rawData as unknown as INpmCheckRegistryData; + + return { + latest: latestStableRelease, + next: next, + versions: sortedVersions, + homepage: bestGuessHomepage(registryData) || '' + }; +} + +/** + * Fetches package information for multiple packages concurrently. + * + * @param packageNames - Array of package names to fetch + * @param concurrency - Maximum number of concurrent requests (defaults to CPU count) + * @returns A promise that resolves to a Map of package name to registry info + */ +export async function getNpmInfoBatch( + packageNames: string[], + concurrency: number = os.cpus().length +): Promise> { + const results: Map = new Map(); + + // TODO: Refactor createPackageSummary to use this batch function to reduce registry requests + await Async.forEachAsync( + packageNames, + async (packageName: string) => { + results.set(packageName, await getNpmInfo(packageName)); + }, + { concurrency } ); - return limit() - .then((rawData: packageJson.FullMetadata) => { - const CRAZY_HIGH_SEMVER: string = '8000.0.0'; - const sortedVersions: string[] = _(rawData.versions) - .keys() - .remove(_.partial(semver.gt, CRAZY_HIGH_SEMVER)) - .sort(semver.compare) - .valueOf(); - - const latest: string = rawData['dist-tags'].latest; - const next: string = rawData['dist-tags'].next; - const latestStableRelease: string | undefined = semver.satisfies(latest, '*') - ? latest - : semver.maxSatisfying(sortedVersions, '*') || ''; - - return { - latest: latestStableRelease, - next: next, - versions: sortedVersions, - homepage: bestGuessHomepage(rawData) || '' - }; - }) - .catch((error) => { - const errorMessage: string = `Registry error ${error.message}`; - return { - error: errorMessage - }; - }); + + return results; } diff --git a/libraries/npm-check-fork/src/NpmRegistryClient.ts b/libraries/npm-check-fork/src/NpmRegistryClient.ts new file mode 100644 index 00000000000..b8a000b2e1f --- /dev/null +++ b/libraries/npm-check-fork/src/NpmRegistryClient.ts @@ -0,0 +1,200 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import * as https from 'node:https'; +import * as http from 'node:http'; +import * as os from 'node:os'; +import * as process from 'node:process'; +import * as zlib from 'node:zlib'; + +import type { INpmRegistryPackageResponse } from './interfaces/INpmCheckRegistry'; + +/** + * Options for configuring the NpmRegistryClient. + * @public + */ +export interface INpmRegistryClientOptions { + /** + * The base URL of the npm registry. + * @defaultValue 'https://registry.npmjs.org' + */ + registryUrl?: string; + + /** + * The User-Agent header to send with requests. + * @defaultValue A string containing npm-check-fork version and platform info + */ + userAgent?: string; + + /** + * Request timeout in milliseconds. + * @defaultValue 30000 + */ + timeoutMs?: number; +} + +/** + * Result from fetching package metadata from the npm registry. + * @public + */ +export interface INpmRegistryClientResult { + /** + * The package metadata if the request was successful. + */ + data?: INpmRegistryPackageResponse; + + /** + * Error message if the request failed. + */ + error?: string; +} + +const DEFAULT_REGISTRY_URL: string = 'https://registry.npmjs.org'; +const DEFAULT_TIMEOUT_MS: number = 30000; + +/** + * A client for fetching package metadata from the npm registry. + * + * @remarks + * This client provides a simple interface for fetching package metadata + * without external dependencies like `package-json`. + * + * @public + */ +export class NpmRegistryClient { + private readonly _registryUrl: string; + private readonly _userAgent: string; + private readonly _timeoutMs: number; + + public constructor(options?: INpmRegistryClientOptions) { + // trim trailing slash if one was provided + this._registryUrl = (options?.registryUrl ?? DEFAULT_REGISTRY_URL).replace(/\/$/, ''); + this._userAgent = + options?.userAgent ?? `npm-check-fork node/${process.version} ${os.platform()} ${os.arch()}`; + this._timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS; + } + + /** + * Builds the URL for fetching package metadata. + * + * @remarks + * Handles scoped packages by URL-encoding the package name. + * For example, `@scope/name` becomes `@scope%2Fname`. + * + * @param packageName - The name of the package + * @returns The full URL for fetching the package metadata + */ + private _buildPackageUrl(packageName: string): string { + // Scoped packages need the slash encoded + // @scope/name -> @scope%2Fname + const encodedName: string = packageName.replace(/\//g, '%2F'); + return `${this._registryUrl}/${encodedName}`; + } + + /** + * Fetches package metadata from the npm registry. + * + * @param packageName - The name of the package to fetch + * @returns A promise that resolves to the result containing either data or an error + * + * @example + * ```ts + * const client = new NpmRegistryClient(); + * const result = await client.fetchPackageMetadataAsync('lodash'); + * if (result.error) { + * console.error(result.error); + * } else { + * console.log(result.data?.['dist-tags'].latest); + * } + * ``` + */ + public async fetchPackageMetadataAsync(packageName: string): Promise { + const url: string = this._buildPackageUrl(packageName); + + return new Promise((resolve) => { + const parsedUrl: URL = new URL(url); + const isHttps: boolean = parsedUrl.protocol === 'https:'; + const requestModule: typeof https | typeof http = isHttps ? https : http; + + const requestOptions: https.RequestOptions = { + hostname: parsedUrl.hostname, + port: parsedUrl.port || (isHttps ? 443 : 80), + path: parsedUrl.pathname + parsedUrl.search, + method: 'GET', + timeout: this._timeoutMs, + headers: { + Accept: 'application/json', + 'Accept-Encoding': 'gzip, deflate', + 'User-Agent': this._userAgent + } + }; + + // TODO: Extract WebClient from rush-lib so that we can use it here + // instead of this reimplementation of HTTP request logic. + const request: http.ClientRequest = requestModule.request( + requestOptions, + (response: http.IncomingMessage) => { + const chunks: Buffer[] = []; + + response.on('data', (chunk: Buffer) => { + chunks.push(chunk); + }); + + response.on('end', () => { + const statusCode: number = response.statusCode ?? 0; + + // Handle 404 - Package not found + if (statusCode === 404) { + resolve({ error: 'Package not found' }); + return; + } + + // Handle other HTTP errors + if (statusCode < 200 || statusCode >= 300) { + resolve({ error: `HTTP error ${statusCode}: ${response.statusMessage}` }); + return; + } + + try { + let buffer: Buffer = Buffer.concat(chunks); + + // Decompress if needed + const contentEncoding: string | undefined = response.headers['content-encoding']; + if (contentEncoding === 'gzip') { + buffer = zlib.gunzipSync(buffer); + } else if (contentEncoding === 'deflate') { + buffer = zlib.inflateSync(buffer); + } + + const data: INpmRegistryPackageResponse = JSON.parse(buffer.toString('utf8')); + + // Successfully retrieved and parsed data + resolve({ data }); + } catch (parseError) { + resolve({ + error: `Failed to parse response: ${ + parseError instanceof Error ? parseError.message : String(parseError) + }` + }); + } + }); + + response.on('error', (error: Error) => { + resolve({ error: `Response error: ${error.message}` }); + }); + } + ); + + request.on('error', (error: Error) => { + resolve({ error: `Network error: ${error.message}` }); + }); + + request.on('timeout', () => { + request.destroy(); + resolve({ error: `Request timed out after ${this._timeoutMs}ms` }); + }); + + request.end(); + }); + } +} diff --git a/libraries/npm-check-fork/src/index.ts b/libraries/npm-check-fork/src/index.ts index 43444bb3484..df216bda88a 100644 --- a/libraries/npm-check-fork/src/index.ts +++ b/libraries/npm-check-fork/src/index.ts @@ -1,3 +1,14 @@ export { default as NpmCheck } from './NpmCheck'; export type { INpmCheckPackageSummary } from './interfaces/INpmCheckPackageSummary'; export type { INpmCheckState } from './interfaces/INpmCheck'; +export { + NpmRegistryClient, + type INpmRegistryClientOptions, + type INpmRegistryClientResult +} from './NpmRegistryClient'; +export type { + INpmRegistryInfo, + INpmRegistryPackageResponse, + INpmRegistryVersionMetadata +} from './interfaces/INpmCheckRegistry'; +export { getNpmInfoBatch } from './GetLatestFromRegistry'; diff --git a/libraries/npm-check-fork/src/interfaces/INpmCheckRegistry.ts b/libraries/npm-check-fork/src/interfaces/INpmCheckRegistry.ts index 63f4e63e0dc..67c5e7a9639 100644 --- a/libraries/npm-check-fork/src/interfaces/INpmCheckRegistry.ts +++ b/libraries/npm-check-fork/src/interfaces/INpmCheckRegistry.ts @@ -1,3 +1,6 @@ +/** + * The result returned by getNpmInfo for a single package. + */ export interface INpmRegistryInfo { latest?: string; next?: string; @@ -21,3 +24,44 @@ export interface INpmCheckRegistryData { versions: Record; ['dist-tags']: { latest: string }; } + +/** + * Metadata for a specific package version from the npm registry. + * + * @remarks + * This interface extends the existing INpmCheckPackageVersion with additional + * fields that are present in the npm registry response. + * + * @see https://github.com/npm/registry/blob/main/docs/responses/package-metadata.md + */ +export interface INpmRegistryVersionMetadata extends INpmCheckPackageVersion { + /** Package name */ + name: string; + + /** Version string */ + version: string; +} + +/** + * Response structure from npm registry API for full metadata. + * + * @remarks + * This interface represents the full response from the npm registry when + * fetching package metadata. It is structurally compatible with INpmCheckRegistryData + * to maintain compatibility with existing code like bestGuessHomepage. + * + * @see https://github.com/npm/registry/blob/main/docs/responses/package-metadata.md + */ +export interface INpmRegistryPackageResponse { + /** Package name */ + name: string; + + /** Distribution tags (latest, next, etc.) */ + 'dist-tags': Record; + + /** All published versions with their metadata */ + versions: Record; + + /** Modification timestamps for each version */ + time?: Record; +} diff --git a/libraries/npm-check-fork/src/tests/GetLatestFromRegistry.test.ts b/libraries/npm-check-fork/src/tests/GetLatestFromRegistry.test.ts index 9490c34b926..48e1ce72d5c 100644 --- a/libraries/npm-check-fork/src/tests/GetLatestFromRegistry.test.ts +++ b/libraries/npm-check-fork/src/tests/GetLatestFromRegistry.test.ts @@ -1,28 +1,49 @@ -jest.mock('package-json'); +// Mock the NpmRegistryClient before imports +jest.mock('../NpmRegistryClient'); -import getNpmInfo from '../GetLatestFromRegistry'; -import packageJson from 'package-json'; -import type { INpmRegistryInfo } from '../interfaces/INpmCheckRegistry'; - -const mockPackageJson = packageJson as jest.MockedFunction; +import type { INpmRegistryInfo, INpmRegistryPackageResponse } from '../interfaces/INpmCheckRegistry'; describe('getNpmInfo', () => { + let getNpmInfo: (packageName: string) => Promise; + let mockFetchPackageMetadataAsync: jest.Mock; + beforeEach(() => { + jest.resetModules(); jest.clearAllMocks(); + + // Re-require to get fresh module instances + mockFetchPackageMetadataAsync = jest.fn(); + + // Set up the mock implementation before importing getNpmInfo + const mockNpmRegistryClient = jest.requireMock('../NpmRegistryClient'); + mockNpmRegistryClient.NpmRegistryClient.mockImplementation(() => ({ + fetchPackageMetadataAsync: mockFetchPackageMetadataAsync + })); + + // Import the module under test + const module = jest.requireActual('../GetLatestFromRegistry'); + getNpmInfo = module.default; }); it('returns registry info with homepage', async () => { - mockPackageJson.mockResolvedValue({ + const mockData: INpmRegistryPackageResponse = { + name: 'test-package', versions: { '1.0.0': { + name: 'test-package', + version: '1.0.0', homepage: 'https://homepage.com' }, '2.0.0': { + name: 'test-package', + version: '2.0.0', bugs: { url: 'https://bugs.com' } } }, 'dist-tags': { latest: '1.0.0', next: '2.0.0' } - } as unknown as packageJson.FullMetadata); + }; + mockFetchPackageMetadataAsync.mockResolvedValue({ data: mockData }); + const result: INpmRegistryInfo = await getNpmInfo('test-package'); expect(result).toHaveProperty('latest', '1.0.0'); expect(result).toHaveProperty('next', '2.0.0'); @@ -30,21 +51,31 @@ describe('getNpmInfo', () => { expect(result).toHaveProperty('homepage', 'https://homepage.com'); }); - it('returns error if packageJson throws', async () => { - mockPackageJson.mockRejectedValue(new Error('Registry down')); + it('returns error if fetch fails', async () => { + mockFetchPackageMetadataAsync.mockResolvedValue({ error: 'Registry down' }); + const result: INpmRegistryInfo = await getNpmInfo('test-package'); expect(result).toHaveProperty('error'); expect(result.error).toBe('Registry error Registry down'); }); it('returns "" homepage if not present', async () => { - mockPackageJson.mockResolvedValue({ + const mockData: INpmRegistryPackageResponse = { + name: 'test-package', versions: { - '1.0.0': {}, - '2.0.0': {} + '1.0.0': { + name: 'test-package', + version: '1.0.0' + }, + '2.0.0': { + name: 'test-package', + version: '2.0.0' + } }, 'dist-tags': { latest: '1.0.0', next: '2.0.0' } - } as unknown as packageJson.FullMetadata); + }; + mockFetchPackageMetadataAsync.mockResolvedValue({ data: mockData }); + const result: INpmRegistryInfo = await getNpmInfo('test-package'); expect(result).toHaveProperty('homepage', ''); }); diff --git a/libraries/npm-check-fork/src/tests/NpmRegistryClient.test.ts b/libraries/npm-check-fork/src/tests/NpmRegistryClient.test.ts new file mode 100644 index 00000000000..00ab716f72f --- /dev/null +++ b/libraries/npm-check-fork/src/tests/NpmRegistryClient.test.ts @@ -0,0 +1,455 @@ +// Mock modules +jest.mock('node:https'); +jest.mock('node:http'); + +import type * as http from 'node:http'; +import type * as https from 'node:https'; +import { EventEmitter } from 'node:events'; +import * as zlib from 'node:zlib'; + +import { NpmRegistryClient, type INpmRegistryClientOptions } from '../NpmRegistryClient'; +import type { INpmRegistryPackageResponse } from '../interfaces/INpmCheckRegistry'; + +describe('NpmRegistryClient', () => { + let mockHttpsRequest: jest.Mock; + let mockHttpRequest: jest.Mock; + + beforeEach(() => { + jest.clearAllMocks(); + + // Get the mocked modules + const httpsModule = jest.requireMock('node:https'); + const httpModule = jest.requireMock('node:http'); + + mockHttpsRequest = httpsModule.request = jest.fn(); + mockHttpRequest = httpModule.request = jest.fn(); + }); + + describe('constructor', () => { + it('uses default registry URL when not provided', () => { + const client = new NpmRegistryClient(); + expect(client).toBeDefined(); + }); + + it('accepts custom options', () => { + const options: INpmRegistryClientOptions = { + registryUrl: 'https://custom.registry.com', + userAgent: 'custom-agent', + timeoutMs: 10000 + }; + const client = new NpmRegistryClient(options); + expect(client).toBeDefined(); + }); + + it('removes trailing slash from registry URL', () => { + const options: INpmRegistryClientOptions = { + registryUrl: 'https://registry.example.com/' + }; + const client = new NpmRegistryClient(options); + expect(client).toBeDefined(); + }); + }); + + describe('fetchPackageMetadataAsync', () => { + interface IMockRequest extends EventEmitter { + destroy: jest.Mock; + end: jest.Mock; + } + + interface IMockResponse extends EventEmitter { + statusCode?: number; + statusMessage?: string; + headers: Record; + } + + function createMockRequest(): { + request: IMockRequest; + response: IMockResponse; + } { + const request = new EventEmitter() as IMockRequest; + const response = new EventEmitter() as IMockResponse; + + request.destroy = jest.fn(); + request.end = jest.fn(); + response.headers = {}; + + return { request, response }; + } + + it('successfully fetches package metadata with https', async () => { + const client = new NpmRegistryClient(); + const { request, response } = createMockRequest(); + + const mockData: INpmRegistryPackageResponse = { + name: 'test-package', + versions: { + '1.0.0': { + name: 'test-package', + version: '1.0.0' + } + }, + 'dist-tags': { latest: '1.0.0' } + }; + + mockHttpsRequest.mockImplementation( + (options: https.RequestOptions, callback: (res: http.IncomingMessage) => void) => { + // Verify request options + expect(options.hostname).toBe('registry.npmjs.org'); + expect(options.method).toBe('GET'); + expect(options.headers).toMatchObject({ + Accept: 'application/json', + 'Accept-Encoding': 'gzip, deflate', + 'User-Agent': expect.stringContaining('npm-check-fork') + }); + + // Trigger callback with response + setImmediate(() => callback(response as http.IncomingMessage)); + return request; + } + ); + + const fetchPromise = client.fetchPackageMetadataAsync('test-package'); + + // Simulate response + response.statusCode = 200; + response.statusMessage = 'OK'; + setImmediate(() => { + response.emit('data', Buffer.from(JSON.stringify(mockData))); + response.emit('end'); + }); + + const result = await fetchPromise; + expect(result.data).toEqual(mockData); + expect(result.error).toBeUndefined(); + }); + + it('builds correct URL for scoped packages', async () => { + const client = new NpmRegistryClient(); + const { request, response } = createMockRequest(); + + mockHttpsRequest.mockImplementation( + (options: https.RequestOptions, callback: (res: http.IncomingMessage) => void) => { + // Verify that scoped package name is URL-encoded + expect(options.path).toBe('/@scope%2Fpackage-name'); + + setImmediate(() => callback(response as http.IncomingMessage)); + return request; + } + ); + + const fetchPromise = client.fetchPackageMetadataAsync('@scope/package-name'); + + response.statusCode = 200; + setImmediate(() => { + response.emit( + 'data', + Buffer.from(JSON.stringify({ name: '@scope/package-name', versions: {}, 'dist-tags': {} })) + ); + response.emit('end'); + }); + + await fetchPromise; + }); + + it('uses custom registry URL', async () => { + const client = new NpmRegistryClient({ registryUrl: 'https://custom.registry.com' }); + const { request, response } = createMockRequest(); + + mockHttpsRequest.mockImplementation( + (options: https.RequestOptions, callback: (res: http.IncomingMessage) => void) => { + expect(options.hostname).toBe('custom.registry.com'); + + setImmediate(() => callback(response as http.IncomingMessage)); + return request; + } + ); + + const fetchPromise = client.fetchPackageMetadataAsync('test-package'); + + response.statusCode = 200; + setImmediate(() => { + response.emit('data', Buffer.from(JSON.stringify({ name: 'test', versions: {}, 'dist-tags': {} }))); + response.emit('end'); + }); + + await fetchPromise; + }); + + it('uses http for http URLs', async () => { + const client = new NpmRegistryClient({ registryUrl: 'http://custom.registry.com' }); + const { request, response } = createMockRequest(); + + mockHttpRequest.mockImplementation( + (options: http.RequestOptions, callback: (res: http.IncomingMessage) => void) => { + expect(options.hostname).toBe('custom.registry.com'); + expect(options.port).toBe(80); + + setImmediate(() => callback(response as http.IncomingMessage)); + return request; + } + ); + + const fetchPromise = client.fetchPackageMetadataAsync('test-package'); + + response.statusCode = 200; + setImmediate(() => { + response.emit('data', Buffer.from(JSON.stringify({ name: 'test', versions: {}, 'dist-tags': {} }))); + response.emit('end'); + }); + + await fetchPromise; + expect(mockHttpRequest).toHaveBeenCalled(); + expect(mockHttpsRequest).not.toHaveBeenCalled(); + }); + + it('handles gzip-encoded responses', async () => { + const client = new NpmRegistryClient(); + const { request, response } = createMockRequest(); + + const mockData: INpmRegistryPackageResponse = { + name: 'test-package', + versions: {}, + 'dist-tags': { latest: '1.0.0' } + }; + + mockHttpsRequest.mockImplementation( + (options: https.RequestOptions, callback: (res: http.IncomingMessage) => void) => { + setImmediate(() => callback(response as http.IncomingMessage)); + return request; + } + ); + + const fetchPromise = client.fetchPackageMetadataAsync('test-package'); + + response.statusCode = 200; + response.headers['content-encoding'] = 'gzip'; + + setImmediate(() => { + const compressed = zlib.gzipSync(Buffer.from(JSON.stringify(mockData))); + response.emit('data', compressed); + response.emit('end'); + }); + + const result = await fetchPromise; + expect(result.data).toEqual(mockData); + expect(result.error).toBeUndefined(); + }); + + it('handles deflate-encoded responses', async () => { + const client = new NpmRegistryClient(); + const { request, response } = createMockRequest(); + + const mockData: INpmRegistryPackageResponse = { + name: 'test-package', + versions: {}, + 'dist-tags': { latest: '1.0.0' } + }; + + mockHttpsRequest.mockImplementation( + (options: https.RequestOptions, callback: (res: http.IncomingMessage) => void) => { + setImmediate(() => callback(response as http.IncomingMessage)); + return request; + } + ); + + const fetchPromise = client.fetchPackageMetadataAsync('test-package'); + + response.statusCode = 200; + response.headers['content-encoding'] = 'deflate'; + + setImmediate(() => { + const compressed = zlib.deflateSync(Buffer.from(JSON.stringify(mockData))); + response.emit('data', compressed); + response.emit('end'); + }); + + const result = await fetchPromise; + expect(result.data).toEqual(mockData); + expect(result.error).toBeUndefined(); + }); + + it('handles 404 status code', async () => { + const client = new NpmRegistryClient(); + const { request, response } = createMockRequest(); + + mockHttpsRequest.mockImplementation( + (options: https.RequestOptions, callback: (res: http.IncomingMessage) => void) => { + setImmediate(() => callback(response as http.IncomingMessage)); + return request; + } + ); + + const fetchPromise = client.fetchPackageMetadataAsync('nonexistent-package'); + + response.statusCode = 404; + response.statusMessage = 'Not Found'; + setImmediate(() => { + response.emit('data', Buffer.from('Not found')); + response.emit('end'); + }); + + const result = await fetchPromise; + expect(result.data).toBeUndefined(); + expect(result.error).toBe('Package not found'); + }); + + it('handles non-2xx status codes', async () => { + const client = new NpmRegistryClient(); + const { request, response } = createMockRequest(); + + mockHttpsRequest.mockImplementation( + (options: https.RequestOptions, callback: (res: http.IncomingMessage) => void) => { + setImmediate(() => callback(response as http.IncomingMessage)); + return request; + } + ); + + const fetchPromise = client.fetchPackageMetadataAsync('test-package'); + + response.statusCode = 500; + response.statusMessage = 'Internal Server Error'; + setImmediate(() => { + response.emit('data', Buffer.from('Error')); + response.emit('end'); + }); + + const result = await fetchPromise; + expect(result.data).toBeUndefined(); + expect(result.error).toBe('HTTP error 500: Internal Server Error'); + }); + + it('handles network errors', async () => { + const client = new NpmRegistryClient(); + const { request } = createMockRequest(); + + mockHttpsRequest.mockImplementation(() => { + return request; + }); + + const fetchPromise = client.fetchPackageMetadataAsync('test-package'); + + setImmediate(() => { + request.emit('error', new Error('Network connection failed')); + }); + + const result = await fetchPromise; + expect(result.data).toBeUndefined(); + expect(result.error).toBe('Network error: Network connection failed'); + }); + + it('handles response errors', async () => { + const client = new NpmRegistryClient(); + const { request, response } = createMockRequest(); + + mockHttpsRequest.mockImplementation( + (options: https.RequestOptions, callback: (res: http.IncomingMessage) => void) => { + setImmediate(() => callback(response as http.IncomingMessage)); + return request; + } + ); + + const fetchPromise = client.fetchPackageMetadataAsync('test-package'); + + response.statusCode = 200; + setImmediate(() => { + response.emit('error', new Error('Stream error')); + }); + + const result = await fetchPromise; + expect(result.data).toBeUndefined(); + expect(result.error).toBe('Response error: Stream error'); + }); + + it('handles timeout', async () => { + const client = new NpmRegistryClient({ timeoutMs: 1000 }); + const { request } = createMockRequest(); + + mockHttpsRequest.mockImplementation(() => { + return request; + }); + + const fetchPromise = client.fetchPackageMetadataAsync('test-package'); + + setImmediate(() => { + request.emit('timeout'); + }); + + const result = await fetchPromise; + expect(result.data).toBeUndefined(); + expect(result.error).toBe('Request timed out after 1000ms'); + expect(request.destroy).toHaveBeenCalled(); + }); + + it('handles JSON parse errors', async () => { + const client = new NpmRegistryClient(); + const { request, response } = createMockRequest(); + + mockHttpsRequest.mockImplementation( + (options: https.RequestOptions, callback: (res: http.IncomingMessage) => void) => { + setImmediate(() => callback(response as http.IncomingMessage)); + return request; + } + ); + + const fetchPromise = client.fetchPackageMetadataAsync('test-package'); + + response.statusCode = 200; + setImmediate(() => { + response.emit('data', Buffer.from('invalid json')); + response.emit('end'); + }); + + const result = await fetchPromise; + expect(result.data).toBeUndefined(); + expect(result.error).toContain('Failed to parse response'); + }); + + it('uses custom User-Agent header', async () => { + const client = new NpmRegistryClient({ userAgent: 'custom-agent/1.0' }); + const { request, response } = createMockRequest(); + + mockHttpsRequest.mockImplementation( + (options: https.RequestOptions, callback: (res: http.IncomingMessage) => void) => { + expect(options.headers?.['User-Agent']).toBe('custom-agent/1.0'); + + setImmediate(() => callback(response as http.IncomingMessage)); + return request; + } + ); + + const fetchPromise = client.fetchPackageMetadataAsync('test-package'); + + response.statusCode = 200; + setImmediate(() => { + response.emit('data', Buffer.from(JSON.stringify({ name: 'test', versions: {}, 'dist-tags': {} }))); + response.emit('end'); + }); + + await fetchPromise; + }); + + it('uses custom timeout value', async () => { + const client = new NpmRegistryClient({ timeoutMs: 5000 }); + const { request, response } = createMockRequest(); + + mockHttpsRequest.mockImplementation( + (options: https.RequestOptions, callback: (res: http.IncomingMessage) => void) => { + expect(options.timeout).toBe(5000); + + setImmediate(() => callback(response as http.IncomingMessage)); + return request; + } + ); + + const fetchPromise = client.fetchPackageMetadataAsync('test-package'); + + response.statusCode = 200; + setImmediate(() => { + response.emit('data', Buffer.from(JSON.stringify({ name: 'test', versions: {}, 'dist-tags': {} }))); + response.emit('end'); + }); + + await fetchPromise; + }); + }); +});