From 6013fba9265ad4b655220a1ddc8d3cea3ecc05de Mon Sep 17 00:00:00 2001 From: Andrew Stegmaier Date: Tue, 23 Jun 2026 16:39:56 -0500 Subject: [PATCH 01/15] Add basic-pnpm-6 and basic-pnpm-9 test fixtures. --- .../__fixtures__/basic-pnpm-6/package.json | 13 + .../__fixtures__/basic-pnpm-6/pnpm-lock.yaml | 712 +++++++++++++++ .../__fixtures__/basic-pnpm-9/package.json | 13 + .../__fixtures__/basic-pnpm-9/pnpm-lock.yaml | 818 ++++++++++++++++++ .../src/__tests__/lockfile/lockfile.test.ts | 20 + .../src/__tests__/setupFixture.ts | 2 + 6 files changed, 1578 insertions(+) create mode 100644 packages/workspace-tools/src/__fixtures__/basic-pnpm-6/package.json create mode 100644 packages/workspace-tools/src/__fixtures__/basic-pnpm-6/pnpm-lock.yaml create mode 100644 packages/workspace-tools/src/__fixtures__/basic-pnpm-9/package.json create mode 100644 packages/workspace-tools/src/__fixtures__/basic-pnpm-9/pnpm-lock.yaml diff --git a/packages/workspace-tools/src/__fixtures__/basic-pnpm-6/package.json b/packages/workspace-tools/src/__fixtures__/basic-pnpm-6/package.json new file mode 100644 index 000000000..977e4ef89 --- /dev/null +++ b/packages/workspace-tools/src/__fixtures__/basic-pnpm-6/package.json @@ -0,0 +1,13 @@ +{ + "name": "basic-pnpm-6", + "packageManager": "pnpm@8.15.9", + "version": "0.1.0", + "license": "MIT", + "private": true, + "description": "derived from sveltejs/kit", + "devDependencies": { + "@changesets/cli": "^2.14.1", + "prettier": "2.8.0", + "typescript": "^4.2.3" + } +} diff --git a/packages/workspace-tools/src/__fixtures__/basic-pnpm-6/pnpm-lock.yaml b/packages/workspace-tools/src/__fixtures__/basic-pnpm-6/pnpm-lock.yaml new file mode 100644 index 000000000..9ef48624c --- /dev/null +++ b/packages/workspace-tools/src/__fixtures__/basic-pnpm-6/pnpm-lock.yaml @@ -0,0 +1,712 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +devDependencies: + '@changesets/cli': + specifier: ^2.14.1 + version: 2.31.0 + prettier: + specifier: 2.8.0 + version: 2.8.0 + typescript: + specifier: ^4.2.3 + version: 4.9.5 + +packages: + + /@babel/runtime@7.29.7: + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} + engines: {node: '>=6.9.0'} + dev: true + + /@changesets/apply-release-plan@7.1.1: + resolution: {integrity: sha512-9qPCm/rLx/xoOFXIHGB229+4GOL76S4MC+7tyOuTsR6+1jYlfFDQORdvwR5hDA6y4FL2BPt3qpbcQIS+dW85LA==} + dependencies: + '@changesets/config': 3.1.4 + '@changesets/get-version-range-type': 0.4.0 + '@changesets/git': 3.0.4 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + detect-indent: 6.1.0 + fs-extra: 7.0.1 + lodash.startcase: 4.4.0 + outdent: 0.5.0 + prettier: 2.8.0 + resolve-from: 5.0.0 + semver: 7.8.5 + dev: true + + /@changesets/assemble-release-plan@6.0.10: + resolution: {integrity: sha512-rSDcqdJ9KbVyjpBIuCidhvZNIiVt1XaIYp73ycVQRIA5n/j6wQaEk0ChRLMUQ1vkxZe51PTQ9OIhbg6HQMW45A==} + dependencies: + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.4 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + semver: 7.8.5 + dev: true + + /@changesets/changelog-git@0.2.1: + resolution: {integrity: sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q==} + dependencies: + '@changesets/types': 6.1.0 + dev: true + + /@changesets/cli@2.31.0: + resolution: {integrity: sha512-AhI4enNTgHu2IZr6K4WZyf0EPch4XVMn1yOMFmCD9gsfBGqMYaHXls5HyDv6/CL5axVQABz68eG30eCtbr2wFg==} + hasBin: true + dependencies: + '@changesets/apply-release-plan': 7.1.1 + '@changesets/assemble-release-plan': 6.0.10 + '@changesets/changelog-git': 0.2.1 + '@changesets/config': 3.1.4 + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.4 + '@changesets/get-release-plan': 4.0.16 + '@changesets/git': 3.0.4 + '@changesets/logger': 0.1.1 + '@changesets/pre': 2.0.2 + '@changesets/read': 0.6.7 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@changesets/write': 0.4.0 + '@inquirer/external-editor': 1.0.3 + '@manypkg/get-packages': 1.1.3 + ansi-colors: 4.1.3 + enquirer: 2.4.1 + fs-extra: 7.0.1 + mri: 1.2.0 + package-manager-detector: 0.2.11 + picocolors: 1.1.1 + resolve-from: 5.0.0 + semver: 7.8.5 + spawndamnit: 3.0.1 + term-size: 2.2.1 + transitivePeerDependencies: + - '@types/node' + dev: true + + /@changesets/config@3.1.4: + resolution: {integrity: sha512-pf0bvD/v6WI2cRlZ6hzpjtZdSlXDXMAJ+Iz7xfFzV4ZxJ8OGGAON+1qYc99ZPrijnt4xp3VGG7eNvAOGS24V1Q==} + dependencies: + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.4 + '@changesets/logger': 0.1.1 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + fs-extra: 7.0.1 + micromatch: 4.0.8 + dev: true + + /@changesets/errors@0.2.0: + resolution: {integrity: sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow==} + dependencies: + extendable-error: 0.1.7 + dev: true + + /@changesets/get-dependents-graph@2.1.4: + resolution: {integrity: sha512-ZsS00x6WvmHq3sQv8oCMwL0f/z3wbXCVuSVTJwCnnmbC/iBdNJGFx1EcbMG4PC6sXRyH69liM4A2WKXzn/kRPg==} + dependencies: + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + picocolors: 1.1.1 + semver: 7.8.5 + dev: true + + /@changesets/get-release-plan@4.0.16: + resolution: {integrity: sha512-2K5Om6CrMPm45rtvckfzWo7e9jOVCKLCnXia5eUPaURH7/LWzri7pK1TycdzAuAtehLkW7VPbWLCSExTHmiI6g==} + dependencies: + '@changesets/assemble-release-plan': 6.0.10 + '@changesets/config': 3.1.4 + '@changesets/pre': 2.0.2 + '@changesets/read': 0.6.7 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + dev: true + + /@changesets/get-version-range-type@0.4.0: + resolution: {integrity: sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ==} + dev: true + + /@changesets/git@3.0.4: + resolution: {integrity: sha512-BXANzRFkX+XcC1q/d27NKvlJ1yf7PSAgi8JG6dt8EfbHFHi4neau7mufcSca5zRhwOL8j9s6EqsxmT+s+/E6Sw==} + dependencies: + '@changesets/errors': 0.2.0 + '@manypkg/get-packages': 1.1.3 + is-subdir: 1.2.0 + micromatch: 4.0.8 + spawndamnit: 3.0.1 + dev: true + + /@changesets/logger@0.1.1: + resolution: {integrity: sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg==} + dependencies: + picocolors: 1.1.1 + dev: true + + /@changesets/parse@0.4.3: + resolution: {integrity: sha512-ZDmNc53+dXdWEv7fqIUSgRQOLYoUom5Z40gmLgmATmYR9NbL6FJJHwakcCpzaeCy+1D0m0n7mT4jj2B/MQPl7A==} + dependencies: + '@changesets/types': 6.1.0 + js-yaml: 4.2.0 + dev: true + + /@changesets/pre@2.0.2: + resolution: {integrity: sha512-HaL/gEyFVvkf9KFg6484wR9s0qjAXlZ8qWPDkTyKF6+zqjBe/I2mygg3MbpZ++hdi0ToqNUF8cjj7fBy0dg8Ug==} + dependencies: + '@changesets/errors': 0.2.0 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + fs-extra: 7.0.1 + dev: true + + /@changesets/read@0.6.7: + resolution: {integrity: sha512-D1G4AUYGrBEk8vj8MGwf75k9GpN6XL3wg8i42P2jZZwFLXnlr2Pn7r9yuQNbaMCarP7ZQWNJbV6XLeysAIMhTA==} + dependencies: + '@changesets/git': 3.0.4 + '@changesets/logger': 0.1.1 + '@changesets/parse': 0.4.3 + '@changesets/types': 6.1.0 + fs-extra: 7.0.1 + p-filter: 2.1.0 + picocolors: 1.1.1 + dev: true + + /@changesets/should-skip-package@0.1.2: + resolution: {integrity: sha512-qAK/WrqWLNCP22UDdBTMPH5f41elVDlsNyat180A33dWxuUDyNpg6fPi/FyTZwRriVjg0L8gnjJn2F9XAoF0qw==} + dependencies: + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + dev: true + + /@changesets/types@4.1.0: + resolution: {integrity: sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw==} + dev: true + + /@changesets/types@6.1.0: + resolution: {integrity: sha512-rKQcJ+o1nKNgeoYRHKOS07tAMNd3YSN0uHaJOZYjBAgxfV7TUE7JE+z4BzZdQwb5hKaYbayKN5KrYV7ODb2rAA==} + dev: true + + /@changesets/write@0.4.0: + resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} + dependencies: + '@changesets/types': 6.1.0 + fs-extra: 7.0.1 + human-id: 4.2.0 + prettier: 2.8.0 + dev: true + + /@inquirer/external-editor@1.0.3: + resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + dependencies: + chardet: 2.2.0 + iconv-lite: 0.7.2 + dev: true + + /@manypkg/find-root@1.1.0: + resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} + dependencies: + '@babel/runtime': 7.29.7 + '@types/node': 12.20.55 + find-up: 4.1.0 + fs-extra: 8.1.0 + dev: true + + /@manypkg/get-packages@1.1.3: + resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==} + dependencies: + '@babel/runtime': 7.29.7 + '@changesets/types': 4.1.0 + '@manypkg/find-root': 1.1.0 + fs-extra: 8.1.0 + globby: 11.1.0 + read-yaml-file: 1.1.0 + dev: true + + /@nodelib/fs.scandir@2.1.5: + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + dev: true + + /@nodelib/fs.stat@2.0.5: + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + dev: true + + /@nodelib/fs.walk@1.2.8: + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + dev: true + + /@types/node@12.20.55: + resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} + dev: true + + /ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + dev: true + + /ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + dev: true + + /argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + dependencies: + sprintf-js: 1.0.3 + dev: true + + /argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + dev: true + + /array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + dev: true + + /better-path-resolve@1.0.0: + resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} + engines: {node: '>=4'} + dependencies: + is-windows: 1.0.2 + dev: true + + /braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + dependencies: + fill-range: 7.1.1 + dev: true + + /chardet@2.2.0: + resolution: {integrity: sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==} + dev: true + + /cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + dev: true + + /detect-indent@6.1.0: + resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} + engines: {node: '>=8'} + dev: true + + /dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + dependencies: + path-type: 4.0.0 + dev: true + + /enquirer@2.4.1: + resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} + engines: {node: '>=8.6'} + dependencies: + ansi-colors: 4.1.3 + strip-ansi: 6.0.1 + dev: true + + /esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + dev: true + + /extendable-error@0.1.7: + resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} + dev: true + + /fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + dev: true + + /fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + dependencies: + reusify: 1.1.0 + dev: true + + /fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + dependencies: + to-regex-range: 5.0.1 + dev: true + + /find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + dev: true + + /fs-extra@7.0.1: + resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} + engines: {node: '>=6 <7 || >=8'} + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + dev: true + + /fs-extra@8.1.0: + resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} + engines: {node: '>=6 <7 || >=8'} + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + dev: true + + /glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + dependencies: + is-glob: 4.0.3 + dev: true + + /globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + dev: true + + /graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + dev: true + + /human-id@4.2.0: + resolution: {integrity: sha512-K3GbkIWqyvvlpfhBPlbEvD97TtqBpAYA4kt+cn2lD2x2HuohzZCibcA2nOlnJT6exqvJLggoB5nv2dNf192nEA==} + hasBin: true + dev: true + + /iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + dependencies: + safer-buffer: 2.1.2 + dev: true + + /ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + dev: true + + /is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + dev: true + + /is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + dependencies: + is-extglob: 2.1.1 + dev: true + + /is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + dev: true + + /is-subdir@1.2.0: + resolution: {integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==} + engines: {node: '>=4'} + dependencies: + better-path-resolve: 1.0.0 + dev: true + + /is-windows@1.0.2: + resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} + engines: {node: '>=0.10.0'} + dev: true + + /isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + dev: true + + /js-yaml@3.14.2: + resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} + hasBin: true + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + dev: true + + /js-yaml@4.2.0: + resolution: {integrity: sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==} + hasBin: true + dependencies: + argparse: 2.0.1 + dev: true + + /jsonfile@4.0.0: + resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + optionalDependencies: + graceful-fs: 4.2.11 + dev: true + + /locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + dependencies: + p-locate: 4.1.0 + dev: true + + /lodash.startcase@4.4.0: + resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} + dev: true + + /merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + dev: true + + /micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + dev: true + + /mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + dev: true + + /outdent@0.5.0: + resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} + dev: true + + /p-filter@2.1.0: + resolution: {integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==} + engines: {node: '>=8'} + dependencies: + p-map: 2.1.0 + dev: true + + /p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + dependencies: + p-try: 2.2.0 + dev: true + + /p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + dependencies: + p-limit: 2.3.0 + dev: true + + /p-map@2.1.0: + resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} + engines: {node: '>=6'} + dev: true + + /p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + dev: true + + /package-manager-detector@0.2.11: + resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==} + dependencies: + quansync: 0.2.11 + dev: true + + /path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + dev: true + + /path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + dev: true + + /path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + dev: true + + /picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + dev: true + + /picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + dev: true + + /pify@4.0.1: + resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} + engines: {node: '>=6'} + dev: true + + /prettier@2.8.0: + resolution: {integrity: sha512-9Lmg8hTFZKG0Asr/kW9Bp8tJjRVluO8EJQVfY2T7FMw9T5jy4I/Uvx0Rca/XWf50QQ1/SS48+6IJWnrb+2yemA==} + engines: {node: '>=10.13.0'} + hasBin: true + dev: true + + /quansync@0.2.11: + resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + dev: true + + /queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + dev: true + + /read-yaml-file@1.1.0: + resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==} + engines: {node: '>=6'} + dependencies: + graceful-fs: 4.2.11 + js-yaml: 3.14.2 + pify: 4.0.1 + strip-bom: 3.0.0 + dev: true + + /resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + dev: true + + /reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + dev: true + + /run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + dependencies: + queue-microtask: 1.2.3 + dev: true + + /safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + dev: true + + /semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + dev: true + + /shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + dependencies: + shebang-regex: 3.0.0 + dev: true + + /shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + dev: true + + /signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + dev: true + + /slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + dev: true + + /spawndamnit@3.0.1: + resolution: {integrity: sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==} + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + dev: true + + /sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + dev: true + + /strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + dependencies: + ansi-regex: 5.0.1 + dev: true + + /strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + dev: true + + /term-size@2.2.1: + resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} + engines: {node: '>=8'} + dev: true + + /to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + dependencies: + is-number: 7.0.0 + dev: true + + /typescript@4.9.5: + resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==} + engines: {node: '>=4.2.0'} + hasBin: true + dev: true + + /universalify@0.1.2: + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} + engines: {node: '>= 4.0.0'} + dev: true + + /which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + dependencies: + isexe: 2.0.0 + dev: true diff --git a/packages/workspace-tools/src/__fixtures__/basic-pnpm-9/package.json b/packages/workspace-tools/src/__fixtures__/basic-pnpm-9/package.json new file mode 100644 index 000000000..96767ad6b --- /dev/null +++ b/packages/workspace-tools/src/__fixtures__/basic-pnpm-9/package.json @@ -0,0 +1,13 @@ +{ + "name": "basic-pnpm-9", + "packageManager": "pnpm@11.9.0", + "version": "0.1.0", + "license": "MIT", + "private": true, + "description": "derived from sveltejs/kit", + "devDependencies": { + "@changesets/cli": "^2.14.1", + "prettier": "2.8.0", + "typescript": "^4.2.3" + } +} diff --git a/packages/workspace-tools/src/__fixtures__/basic-pnpm-9/pnpm-lock.yaml b/packages/workspace-tools/src/__fixtures__/basic-pnpm-9/pnpm-lock.yaml new file mode 100644 index 000000000..de0c3e974 --- /dev/null +++ b/packages/workspace-tools/src/__fixtures__/basic-pnpm-9/pnpm-lock.yaml @@ -0,0 +1,818 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@changesets/cli': + specifier: ^2.14.1 + version: 2.31.0 + prettier: + specifier: 2.8.0 + version: 2.8.0 + typescript: + specifier: ^4.2.3 + version: 4.9.5 + +packages: + + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} + engines: {node: '>=6.9.0'} + + '@changesets/apply-release-plan@7.1.1': + resolution: {integrity: sha512-9qPCm/rLx/xoOFXIHGB229+4GOL76S4MC+7tyOuTsR6+1jYlfFDQORdvwR5hDA6y4FL2BPt3qpbcQIS+dW85LA==} + + '@changesets/assemble-release-plan@6.0.10': + resolution: {integrity: sha512-rSDcqdJ9KbVyjpBIuCidhvZNIiVt1XaIYp73ycVQRIA5n/j6wQaEk0ChRLMUQ1vkxZe51PTQ9OIhbg6HQMW45A==} + + '@changesets/changelog-git@0.2.1': + resolution: {integrity: sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q==} + + '@changesets/cli@2.31.0': + resolution: {integrity: sha512-AhI4enNTgHu2IZr6K4WZyf0EPch4XVMn1yOMFmCD9gsfBGqMYaHXls5HyDv6/CL5axVQABz68eG30eCtbr2wFg==} + hasBin: true + + '@changesets/config@3.1.4': + resolution: {integrity: sha512-pf0bvD/v6WI2cRlZ6hzpjtZdSlXDXMAJ+Iz7xfFzV4ZxJ8OGGAON+1qYc99ZPrijnt4xp3VGG7eNvAOGS24V1Q==} + + '@changesets/errors@0.2.0': + resolution: {integrity: sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow==} + + '@changesets/get-dependents-graph@2.1.4': + resolution: {integrity: sha512-ZsS00x6WvmHq3sQv8oCMwL0f/z3wbXCVuSVTJwCnnmbC/iBdNJGFx1EcbMG4PC6sXRyH69liM4A2WKXzn/kRPg==} + + '@changesets/get-release-plan@4.0.16': + resolution: {integrity: sha512-2K5Om6CrMPm45rtvckfzWo7e9jOVCKLCnXia5eUPaURH7/LWzri7pK1TycdzAuAtehLkW7VPbWLCSExTHmiI6g==} + + '@changesets/get-version-range-type@0.4.0': + resolution: {integrity: sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ==} + + '@changesets/git@3.0.4': + resolution: {integrity: sha512-BXANzRFkX+XcC1q/d27NKvlJ1yf7PSAgi8JG6dt8EfbHFHi4neau7mufcSca5zRhwOL8j9s6EqsxmT+s+/E6Sw==} + + '@changesets/logger@0.1.1': + resolution: {integrity: sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg==} + + '@changesets/parse@0.4.3': + resolution: {integrity: sha512-ZDmNc53+dXdWEv7fqIUSgRQOLYoUom5Z40gmLgmATmYR9NbL6FJJHwakcCpzaeCy+1D0m0n7mT4jj2B/MQPl7A==} + + '@changesets/pre@2.0.2': + resolution: {integrity: sha512-HaL/gEyFVvkf9KFg6484wR9s0qjAXlZ8qWPDkTyKF6+zqjBe/I2mygg3MbpZ++hdi0ToqNUF8cjj7fBy0dg8Ug==} + + '@changesets/read@0.6.7': + resolution: {integrity: sha512-D1G4AUYGrBEk8vj8MGwf75k9GpN6XL3wg8i42P2jZZwFLXnlr2Pn7r9yuQNbaMCarP7ZQWNJbV6XLeysAIMhTA==} + + '@changesets/should-skip-package@0.1.2': + resolution: {integrity: sha512-qAK/WrqWLNCP22UDdBTMPH5f41elVDlsNyat180A33dWxuUDyNpg6fPi/FyTZwRriVjg0L8gnjJn2F9XAoF0qw==} + + '@changesets/types@4.1.0': + resolution: {integrity: sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw==} + + '@changesets/types@6.1.0': + resolution: {integrity: sha512-rKQcJ+o1nKNgeoYRHKOS07tAMNd3YSN0uHaJOZYjBAgxfV7TUE7JE+z4BzZdQwb5hKaYbayKN5KrYV7ODb2rAA==} + + '@changesets/write@0.4.0': + resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} + + '@inquirer/external-editor@1.0.3': + resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@manypkg/find-root@1.1.0': + resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} + + '@manypkg/get-packages@1.1.3': + resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@types/node@12.20.55': + resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} + + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + better-path-resolve@1.0.0: + resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} + engines: {node: '>=4'} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + chardet@2.2.0: + resolution: {integrity: sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + detect-indent@6.1.0: + resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} + engines: {node: '>=8'} + + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + enquirer@2.4.1: + resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} + engines: {node: '>=8.6'} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + extendable-error@0.1.7: + resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + fs-extra@7.0.1: + resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} + engines: {node: '>=6 <7 || >=8'} + + fs-extra@8.1.0: + resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} + engines: {node: '>=6 <7 || >=8'} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + human-id@4.2.0: + resolution: {integrity: sha512-K3GbkIWqyvvlpfhBPlbEvD97TtqBpAYA4kt+cn2lD2x2HuohzZCibcA2nOlnJT6exqvJLggoB5nv2dNf192nEA==} + hasBin: true + + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-subdir@1.2.0: + resolution: {integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==} + engines: {node: '>=4'} + + is-windows@1.0.2: + resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} + engines: {node: '>=0.10.0'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + js-yaml@3.14.2: + resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} + hasBin: true + + js-yaml@4.2.0: + resolution: {integrity: sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==} + hasBin: true + + jsonfile@4.0.0: + resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + lodash.startcase@4.4.0: + resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + + outdent@0.5.0: + resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} + + p-filter@2.1.0: + resolution: {integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==} + engines: {node: '>=8'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-map@2.1.0: + resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} + engines: {node: '>=6'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + package-manager-detector@0.2.11: + resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + pify@4.0.1: + resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} + engines: {node: '>=6'} + + prettier@2.8.0: + resolution: {integrity: sha512-9Lmg8hTFZKG0Asr/kW9Bp8tJjRVluO8EJQVfY2T7FMw9T5jy4I/Uvx0Rca/XWf50QQ1/SS48+6IJWnrb+2yemA==} + engines: {node: '>=10.13.0'} + hasBin: true + + quansync@0.2.11: + resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + read-yaml-file@1.1.0: + resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==} + engines: {node: '>=6'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + spawndamnit@3.0.1: + resolution: {integrity: sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==} + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + term-size@2.2.1: + resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} + engines: {node: '>=8'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + typescript@4.9.5: + resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==} + engines: {node: '>=4.2.0'} + hasBin: true + + universalify@0.1.2: + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} + engines: {node: '>= 4.0.0'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + +snapshots: + + '@babel/runtime@7.29.7': {} + + '@changesets/apply-release-plan@7.1.1': + dependencies: + '@changesets/config': 3.1.4 + '@changesets/get-version-range-type': 0.4.0 + '@changesets/git': 3.0.4 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + detect-indent: 6.1.0 + fs-extra: 7.0.1 + lodash.startcase: 4.4.0 + outdent: 0.5.0 + prettier: 2.8.0 + resolve-from: 5.0.0 + semver: 7.8.5 + + '@changesets/assemble-release-plan@6.0.10': + dependencies: + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.4 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + semver: 7.8.5 + + '@changesets/changelog-git@0.2.1': + dependencies: + '@changesets/types': 6.1.0 + + '@changesets/cli@2.31.0': + dependencies: + '@changesets/apply-release-plan': 7.1.1 + '@changesets/assemble-release-plan': 6.0.10 + '@changesets/changelog-git': 0.2.1 + '@changesets/config': 3.1.4 + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.4 + '@changesets/get-release-plan': 4.0.16 + '@changesets/git': 3.0.4 + '@changesets/logger': 0.1.1 + '@changesets/pre': 2.0.2 + '@changesets/read': 0.6.7 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@changesets/write': 0.4.0 + '@inquirer/external-editor': 1.0.3 + '@manypkg/get-packages': 1.1.3 + ansi-colors: 4.1.3 + enquirer: 2.4.1 + fs-extra: 7.0.1 + mri: 1.2.0 + package-manager-detector: 0.2.11 + picocolors: 1.1.1 + resolve-from: 5.0.0 + semver: 7.8.5 + spawndamnit: 3.0.1 + term-size: 2.2.1 + transitivePeerDependencies: + - '@types/node' + + '@changesets/config@3.1.4': + dependencies: + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.4 + '@changesets/logger': 0.1.1 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + fs-extra: 7.0.1 + micromatch: 4.0.8 + + '@changesets/errors@0.2.0': + dependencies: + extendable-error: 0.1.7 + + '@changesets/get-dependents-graph@2.1.4': + dependencies: + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + picocolors: 1.1.1 + semver: 7.8.5 + + '@changesets/get-release-plan@4.0.16': + dependencies: + '@changesets/assemble-release-plan': 6.0.10 + '@changesets/config': 3.1.4 + '@changesets/pre': 2.0.2 + '@changesets/read': 0.6.7 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + + '@changesets/get-version-range-type@0.4.0': {} + + '@changesets/git@3.0.4': + dependencies: + '@changesets/errors': 0.2.0 + '@manypkg/get-packages': 1.1.3 + is-subdir: 1.2.0 + micromatch: 4.0.8 + spawndamnit: 3.0.1 + + '@changesets/logger@0.1.1': + dependencies: + picocolors: 1.1.1 + + '@changesets/parse@0.4.3': + dependencies: + '@changesets/types': 6.1.0 + js-yaml: 4.2.0 + + '@changesets/pre@2.0.2': + dependencies: + '@changesets/errors': 0.2.0 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + fs-extra: 7.0.1 + + '@changesets/read@0.6.7': + dependencies: + '@changesets/git': 3.0.4 + '@changesets/logger': 0.1.1 + '@changesets/parse': 0.4.3 + '@changesets/types': 6.1.0 + fs-extra: 7.0.1 + p-filter: 2.1.0 + picocolors: 1.1.1 + + '@changesets/should-skip-package@0.1.2': + dependencies: + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + + '@changesets/types@4.1.0': {} + + '@changesets/types@6.1.0': {} + + '@changesets/write@0.4.0': + dependencies: + '@changesets/types': 6.1.0 + fs-extra: 7.0.1 + human-id: 4.2.0 + prettier: 2.8.0 + + '@inquirer/external-editor@1.0.3': + dependencies: + chardet: 2.2.0 + iconv-lite: 0.7.2 + + '@manypkg/find-root@1.1.0': + dependencies: + '@babel/runtime': 7.29.7 + '@types/node': 12.20.55 + find-up: 4.1.0 + fs-extra: 8.1.0 + + '@manypkg/get-packages@1.1.3': + dependencies: + '@babel/runtime': 7.29.7 + '@changesets/types': 4.1.0 + '@manypkg/find-root': 1.1.0 + fs-extra: 8.1.0 + globby: 11.1.0 + read-yaml-file: 1.1.0 + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + '@types/node@12.20.55': {} + + ansi-colors@4.1.3: {} + + ansi-regex@5.0.1: {} + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + argparse@2.0.1: {} + + array-union@2.1.0: {} + + better-path-resolve@1.0.0: + dependencies: + is-windows: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + chardet@2.2.0: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + detect-indent@6.1.0: {} + + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + + enquirer@2.4.1: + dependencies: + ansi-colors: 4.1.3 + strip-ansi: 6.0.1 + + esprima@4.0.1: {} + + extendable-error@0.1.7: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + fs-extra@7.0.1: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + + fs-extra@8.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + + graceful-fs@4.2.11: {} + + human-id@4.2.0: {} + + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + + ignore@5.3.2: {} + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + is-subdir@1.2.0: + dependencies: + better-path-resolve: 1.0.0 + + is-windows@1.0.2: {} + + isexe@2.0.0: {} + + js-yaml@3.14.2: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + + js-yaml@4.2.0: + dependencies: + argparse: 2.0.1 + + jsonfile@4.0.0: + optionalDependencies: + graceful-fs: 4.2.11 + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + lodash.startcase@4.4.0: {} + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + + mri@1.2.0: {} + + outdent@0.5.0: {} + + p-filter@2.1.0: + dependencies: + p-map: 2.1.0 + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-map@2.1.0: {} + + p-try@2.2.0: {} + + package-manager-detector@0.2.11: + dependencies: + quansync: 0.2.11 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-type@4.0.0: {} + + picocolors@1.1.1: {} + + picomatch@2.3.2: {} + + pify@4.0.1: {} + + prettier@2.8.0: {} + + quansync@0.2.11: {} + + queue-microtask@1.2.3: {} + + read-yaml-file@1.1.0: + dependencies: + graceful-fs: 4.2.11 + js-yaml: 3.14.2 + pify: 4.0.1 + strip-bom: 3.0.0 + + resolve-from@5.0.0: {} + + reusify@1.1.0: {} + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safer-buffer@2.1.2: {} + + semver@7.8.5: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + signal-exit@4.1.0: {} + + slash@3.0.0: {} + + spawndamnit@3.0.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + sprintf-js@1.0.3: {} + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-bom@3.0.0: {} + + term-size@2.2.1: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + typescript@4.9.5: {} + + universalify@0.1.2: {} + + which@2.0.2: + dependencies: + isexe: 2.0.0 diff --git a/packages/workspace-tools/src/__tests__/lockfile/lockfile.test.ts b/packages/workspace-tools/src/__tests__/lockfile/lockfile.test.ts index c15e220f3..e7205ddc5 100644 --- a/packages/workspace-tools/src/__tests__/lockfile/lockfile.test.ts +++ b/packages/workspace-tools/src/__tests__/lockfile/lockfile.test.ts @@ -85,5 +85,25 @@ describe("parseLockFile()", () => { expect(which).toBeTruthy(); expect(parsedLockFile.object[which!].dependencies?.["isexe"]).toBeTruthy(); }); + + it("parses pnpm-lock.yaml file when it is found (lockfileVersion 6.0)", async () => { + const packageRoot = setupFixture("basic-pnpm-6"); + const parsedLockFile = await parseLockFile(packageRoot); + + // If the lock file is updated, you might need to switch to a different key and dependency + const which = Object.keys(parsedLockFile.object).find((key) => /^which@/.test(key)); + expect(which).toBeTruthy(); + expect(parsedLockFile.object[which!].dependencies?.["isexe"]).toBeTruthy(); + }); + + it("parses pnpm-lock.yaml file when it is found (lockfileVersion 9.0)", async () => { + const packageRoot = setupFixture("basic-pnpm-9"); + const parsedLockFile = await parseLockFile(packageRoot); + + // If the lock file is updated, you might need to switch to a different key and dependency + const which = Object.keys(parsedLockFile.object).find((key) => /^which@/.test(key)); + expect(which).toBeTruthy(); + expect(parsedLockFile.object[which!].dependencies?.["isexe"]).toBeTruthy(); + }); }); }); diff --git a/packages/workspace-tools/src/__tests__/setupFixture.ts b/packages/workspace-tools/src/__tests__/setupFixture.ts index 0016aa4d4..42e035aa5 100644 --- a/packages/workspace-tools/src/__tests__/setupFixture.ts +++ b/packages/workspace-tools/src/__tests__/setupFixture.ts @@ -10,6 +10,8 @@ let tempNumber = 0; /** Full fixture folders under `__fixtures__` */ type RealFixtureName = | "basic-pnpm" + | "basic-pnpm-6" + | "basic-pnpm-9" | "basic-without-lock-file" | "basic-yarn-1" | "basic-yarn-berry" From 29515734f8973fc57bbf3285faa8ee0d77337a43 Mon Sep 17 00:00:00 2001 From: Andrew Stegmaier Date: Tue, 23 Jun 2026 16:50:24 -0500 Subject: [PATCH 02/15] Enhance lockfile tests to capture failure to parse version --- .../src/__tests__/lockfile/lockfile.test.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/workspace-tools/src/__tests__/lockfile/lockfile.test.ts b/packages/workspace-tools/src/__tests__/lockfile/lockfile.test.ts index e7205ddc5..06b7db8bd 100644 --- a/packages/workspace-tools/src/__tests__/lockfile/lockfile.test.ts +++ b/packages/workspace-tools/src/__tests__/lockfile/lockfile.test.ts @@ -84,6 +84,10 @@ describe("parseLockFile()", () => { const which = Object.keys(parsedLockFile.object).find((key) => /^which@/.test(key)); expect(which).toBeTruthy(); expect(parsedLockFile.object[which!].dependencies?.["isexe"]).toBeTruthy(); + + // The parsed entry should preserve the resolved version. + expect(which).toBe("which@2.0.2"); + expect(parsedLockFile.object[which!].version).toBe("2.0.2"); }); it("parses pnpm-lock.yaml file when it is found (lockfileVersion 6.0)", async () => { @@ -94,6 +98,12 @@ describe("parseLockFile()", () => { const which = Object.keys(parsedLockFile.object).find((key) => /^which@/.test(key)); expect(which).toBeTruthy(); expect(parsedLockFile.object[which!].dependencies?.["isexe"]).toBeTruthy(); + + // The parsed entry should preserve the resolved version. This currently fails because the + // parser splits lockfileVersion 6.0 keys (e.g. "/which@2.0.2") on "/", so the version is + // silently dropped (key becomes "which@2.0.2@undefined" and `version` is undefined). + expect(which).toBe("which@2.0.2"); + expect(parsedLockFile.object[which!].version).toBe("2.0.2"); }); it("parses pnpm-lock.yaml file when it is found (lockfileVersion 9.0)", async () => { @@ -104,6 +114,12 @@ describe("parseLockFile()", () => { const which = Object.keys(parsedLockFile.object).find((key) => /^which@/.test(key)); expect(which).toBeTruthy(); expect(parsedLockFile.object[which!].dependencies?.["isexe"]).toBeTruthy(); + + // The parsed entry should preserve the resolved version. This currently fails because the + // parser splits package keys on "/" and only reads the `packages` section, so lockfileVersion + // 9.0 keys (e.g. "which@2.0.2", with dependency edges under `snapshots`) are not parsed at all. + expect(which).toBe("which@2.0.2"); + expect(parsedLockFile.object[which!].version).toBe("2.0.2"); }); }); }); From b54efc75fcac8fe81934c21822a400f0eac774a9 Mon Sep 17 00:00:00 2001 From: Andrew Stegmaier Date: Tue, 23 Jun 2026 18:01:27 -0500 Subject: [PATCH 03/15] Fix parsePnpmLock --- .../src/__tests__/lockfile/lockfile.test.ts | 8 +--- .../src/lockfile/parsePnpmLock.ts | 44 ++++++++++++++++--- .../workspace-tools/src/lockfile/types.ts | 5 ++- 3 files changed, 44 insertions(+), 13 deletions(-) diff --git a/packages/workspace-tools/src/__tests__/lockfile/lockfile.test.ts b/packages/workspace-tools/src/__tests__/lockfile/lockfile.test.ts index 06b7db8bd..d1ca9c2fb 100644 --- a/packages/workspace-tools/src/__tests__/lockfile/lockfile.test.ts +++ b/packages/workspace-tools/src/__tests__/lockfile/lockfile.test.ts @@ -99,9 +99,7 @@ describe("parseLockFile()", () => { expect(which).toBeTruthy(); expect(parsedLockFile.object[which!].dependencies?.["isexe"]).toBeTruthy(); - // The parsed entry should preserve the resolved version. This currently fails because the - // parser splits lockfileVersion 6.0 keys (e.g. "/which@2.0.2") on "/", so the version is - // silently dropped (key becomes "which@2.0.2@undefined" and `version` is undefined). + // The parsed entry should preserve the resolved version. expect(which).toBe("which@2.0.2"); expect(parsedLockFile.object[which!].version).toBe("2.0.2"); }); @@ -115,9 +113,7 @@ describe("parseLockFile()", () => { expect(which).toBeTruthy(); expect(parsedLockFile.object[which!].dependencies?.["isexe"]).toBeTruthy(); - // The parsed entry should preserve the resolved version. This currently fails because the - // parser splits package keys on "/" and only reads the `packages` section, so lockfileVersion - // 9.0 keys (e.g. "which@2.0.2", with dependency edges under `snapshots`) are not parsed at all. + // The parsed entry should preserve the resolved version. expect(which).toBe("which@2.0.2"); expect(parsedLockFile.object[which!].version).toBe("2.0.2"); }); diff --git a/packages/workspace-tools/src/lockfile/parsePnpmLock.ts b/packages/workspace-tools/src/lockfile/parsePnpmLock.ts index cd388118d..e7faa1c49 100644 --- a/packages/workspace-tools/src/lockfile/parsePnpmLock.ts +++ b/packages/workspace-tools/src/lockfile/parsePnpmLock.ts @@ -1,21 +1,53 @@ import { nameAtVersion } from "./nameAtVersion.js"; import { type LockDependency, type ParsedLock, type PnpmLockFile } from "./types.js"; +/** + * Parse a pnpm package/snapshot key into its name and version, handling every lockfile format: + * - lockfileVersion <= 5.x: slash-separated, e.g. `/which/2.0.2` or `/@babel/runtime/7.28.4` + * - lockfileVersion 6.0: leading slash with `@` separator, e.g. `/which@2.0.2` or `/@babel/runtime@7.28.4` + * - lockfileVersion >= 9.0: no leading slash, e.g. `which@2.0.2` or `@babel/runtime@7.28.4` + * + * Any peer-dependency suffix (e.g. `(react@19.2.4)`) is stripped. + */ +function parsePackageKey(key: string): { name: string; version: string } { + // Strip the leading "/" present in lockfileVersion <= 6.0 keys. + let base = key.startsWith("/") ? key.slice(1) : key; + + // Strip any peer-dependency suffix, e.g. `(react@19.2.4)`. + const peerIndex = base.indexOf("("); + if (peerIndex !== -1) { + base = base.slice(0, peerIndex); + } + + // An "@" past the first character separates the version in lockfileVersion >= 6.0 + // (the first character may be the "@" of a scoped package name). + const atIndex = base.lastIndexOf("@"); + if (atIndex > 0) { + return { name: base.slice(0, atIndex), version: base.slice(atIndex + 1) }; + } + + // lockfileVersion <= 5.x uses "/" to separate the version. + const slashIndex = base.lastIndexOf("/"); + return { name: base.slice(0, slashIndex), version: base.slice(slashIndex + 1) }; +} + export function parsePnpmLock(yaml: PnpmLockFile): ParsedLock { const object: { [key in string]: LockDependency; } = {}; - if (yaml && yaml.packages) { - for (const [pkgSpec, snapshot] of Object.entries(yaml.packages)) { + // lockfileVersion >= 9.0 moves dependency edges into a `snapshots` section, leaving `packages` + // with only resolution metadata. Earlier versions keep dependencies inline under `packages`. + const entries = yaml?.snapshots ?? yaml?.packages; + + if (entries) { + for (const [pkgSpec, snapshot] of Object.entries(entries)) { // TODO: handle file:foo.tgz syntax (rush uses this for internal package links) - const specParts = pkgSpec.split(/\//); - const name = specParts.length > 3 ? `${specParts[1]}/${specParts[2]}` : specParts[1]; - const version = specParts.length > 3 ? specParts[3] : specParts[2]; + const { name, version } = parsePackageKey(pkgSpec); object[nameAtVersion(name, version)] = { version, - dependencies: snapshot.dependencies, + dependencies: snapshot?.dependencies, }; } } diff --git a/packages/workspace-tools/src/lockfile/types.ts b/packages/workspace-tools/src/lockfile/types.ts index 452914462..df8a1a3e7 100644 --- a/packages/workspace-tools/src/lockfile/types.ts +++ b/packages/workspace-tools/src/lockfile/types.ts @@ -14,7 +14,10 @@ export type ParsedLock = { /** pnpm `pnpm-lock.yaml` format */ export interface PnpmLockFile { - packages: { [name: string]: any }; + /** Resolution metadata. In lockfileVersion <= 6.0 this also holds dependency edges. */ + packages?: { [name: string]: any }; + /** Dependency edges in lockfileVersion >= 9.0. */ + snapshots?: { [name: string]: { dependencies?: Dependencies } }; } export interface NpmWorkspacesInfo { From 5c1475450197522236e111733f882e2f333fc203 Mon Sep 17 00:00:00 2001 From: Andrew Stegmaier Date: Wed, 24 Jun 2026 14:13:03 -0500 Subject: [PATCH 04/15] Add more sophisticated tests and implementation. --- .../__fixtures__/basic-pnpm-6/package.json | 12 +- .../basic-pnpm-6/patches/is-odd@3.0.1.patch | 7 + .../__fixtures__/basic-pnpm-6/pnpm-lock.yaml | 165 ++++++++++++++ .../__fixtures__/basic-pnpm-9/package.json | 7 +- .../basic-pnpm-9/patches/is-odd@3.0.1.patch | 7 + .../__fixtures__/basic-pnpm-9/pnpm-lock.yaml | 181 +++++++++++++++ .../basic-pnpm-9/pnpm-workspace.yaml | 2 + .../src/__tests__/lockfile/lockfile.test.ts | 93 ++++++-- .../__tests__/lockfile/parsePnpmLock.test.ts | 208 ++++++++++++++++++ .../src/lockfile/parsePnpmLock.ts | 74 +++++-- 10 files changed, 716 insertions(+), 40 deletions(-) create mode 100644 packages/workspace-tools/src/__fixtures__/basic-pnpm-6/patches/is-odd@3.0.1.patch create mode 100644 packages/workspace-tools/src/__fixtures__/basic-pnpm-9/patches/is-odd@3.0.1.patch create mode 100644 packages/workspace-tools/src/__fixtures__/basic-pnpm-9/pnpm-workspace.yaml create mode 100644 packages/workspace-tools/src/__tests__/lockfile/parsePnpmLock.test.ts diff --git a/packages/workspace-tools/src/__fixtures__/basic-pnpm-6/package.json b/packages/workspace-tools/src/__fixtures__/basic-pnpm-6/package.json index 977e4ef89..07a8f94b3 100644 --- a/packages/workspace-tools/src/__fixtures__/basic-pnpm-6/package.json +++ b/packages/workspace-tools/src/__fixtures__/basic-pnpm-6/package.json @@ -4,10 +4,20 @@ "version": "0.1.0", "license": "MIT", "private": true, - "description": "derived from sveltejs/kit", + "description": "derived from sveltejs/kit; includes edge-case deps (peer suffixes, patched dep, git/non-semver dep) for lockfile snapshot parsing tests", "devDependencies": { "@changesets/cli": "^2.14.1", + "@testing-library/react": "16.0.1", + "is-odd": "3.0.1", + "is-positive": "github:kevva/is-positive#97edff6f525f192a3f83cea1944765f769ae2678", "prettier": "2.8.0", + "react": "18.3.1", + "react-dom": "18.3.1", "typescript": "^4.2.3" + }, + "pnpm": { + "patchedDependencies": { + "is-odd@3.0.1": "patches/is-odd@3.0.1.patch" + } } } diff --git a/packages/workspace-tools/src/__fixtures__/basic-pnpm-6/patches/is-odd@3.0.1.patch b/packages/workspace-tools/src/__fixtures__/basic-pnpm-6/patches/is-odd@3.0.1.patch new file mode 100644 index 000000000..18e30fd96 --- /dev/null +++ b/packages/workspace-tools/src/__fixtures__/basic-pnpm-6/patches/is-odd@3.0.1.patch @@ -0,0 +1,7 @@ +diff --git a/index.js b/index.js +index 0000000..1111111 100644 +--- a/index.js ++++ b/index.js +@@ -1,1 +1,2 @@ ++// patched for lockfile fixture + 'use strict'; diff --git a/packages/workspace-tools/src/__fixtures__/basic-pnpm-6/pnpm-lock.yaml b/packages/workspace-tools/src/__fixtures__/basic-pnpm-6/pnpm-lock.yaml index 9ef48624c..dcf60808a 100644 --- a/packages/workspace-tools/src/__fixtures__/basic-pnpm-6/pnpm-lock.yaml +++ b/packages/workspace-tools/src/__fixtures__/basic-pnpm-6/pnpm-lock.yaml @@ -4,19 +4,53 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +patchedDependencies: + is-odd@3.0.1: + hash: kdke7teubu7iqmu2b267pbsl5i + path: patches/is-odd@3.0.1.patch + devDependencies: '@changesets/cli': specifier: ^2.14.1 version: 2.31.0 + '@testing-library/react': + specifier: 16.0.1 + version: 16.0.1(@testing-library/dom@10.4.1)(react-dom@18.3.1)(react@18.3.1) + is-odd: + specifier: 3.0.1 + version: 3.0.1(patch_hash=kdke7teubu7iqmu2b267pbsl5i) + is-positive: + specifier: github:kevva/is-positive#97edff6f525f192a3f83cea1944765f769ae2678 + version: github.com/kevva/is-positive/97edff6f525f192a3f83cea1944765f769ae2678 prettier: specifier: 2.8.0 version: 2.8.0 + react: + specifier: 18.3.1 + version: 18.3.1 + react-dom: + specifier: 18.3.1 + version: 18.3.1(react@18.3.1) typescript: specifier: ^4.2.3 version: 4.9.5 packages: + /@babel/code-frame@7.29.7: + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + dev: true + + /@babel/helper-validator-identifier@7.29.7: + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + dev: true + /@babel/runtime@7.29.7: resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} engines: {node: '>=6.9.0'} @@ -256,6 +290,45 @@ packages: fastq: 1.20.1 dev: true + /@testing-library/dom@10.4.1: + resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} + engines: {node: '>=18'} + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/runtime': 7.29.7 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + picocolors: 1.1.1 + pretty-format: 27.5.1 + dev: true + + /@testing-library/react@16.0.1(@testing-library/dom@10.4.1)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-dSmwJVtJXmku+iocRhWOUFbrERC76TX2Mnf0ATODz8brzAZrMBbzLwQixlBSanZxR6LddK3eiwpSFZgDET1URg==} + engines: {node: '>=18'} + peerDependencies: + '@testing-library/dom': ^10.0.0 + '@types/react': ^18.0.0 + '@types/react-dom': ^18.0.0 + react: ^18.0.0 + react-dom: ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.29.7 + '@testing-library/dom': 10.4.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: true + + /@types/aria-query@5.0.4: + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + dev: true + /@types/node@12.20.55: resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} dev: true @@ -270,6 +343,11 @@ packages: engines: {node: '>=8'} dev: true + /ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + dev: true + /argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} dependencies: @@ -280,6 +358,12 @@ packages: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} dev: true + /aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + dependencies: + dequal: 2.0.3 + dev: true + /array-union@2.1.0: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} @@ -312,6 +396,11 @@ packages: which: 2.0.2 dev: true + /dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + dev: true + /detect-indent@6.1.0: resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} engines: {node: '>=8'} @@ -324,6 +413,10 @@ packages: path-type: 4.0.0 dev: true + /dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + dev: true + /enquirer@2.4.1: resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} engines: {node: '>=8.6'} @@ -444,11 +537,24 @@ packages: is-extglob: 2.1.1 dev: true + /is-number@6.0.0: + resolution: {integrity: sha512-Wu1VHeILBK8KAWJUAiSZQX94GmOE45Rg6/538fKwiloUu21KncEkYGPqob2oSZ5mUT73vLGrHQjKw3KMPwfDzg==} + engines: {node: '>=0.10.0'} + dev: true + /is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} dev: true + /is-odd@3.0.1(patch_hash=kdke7teubu7iqmu2b267pbsl5i): + resolution: {integrity: sha512-CQpnWPrDwmP1+SMHXZhtLtJv90yiyVfluGsX5iNCVkrhQtU3TQHsUWPG9wkdk9Lgd5yNpAg9jQEo90CBaXgWMA==} + engines: {node: '>=4'} + dependencies: + is-number: 6.0.0 + dev: true + patched: true + /is-subdir@1.2.0: resolution: {integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==} engines: {node: '>=4'} @@ -465,6 +571,10 @@ packages: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} dev: true + /js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + dev: true + /js-yaml@3.14.2: resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} hasBin: true @@ -497,6 +607,18 @@ packages: resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} dev: true + /loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + dependencies: + js-tokens: 4.0.0 + dev: true + + /lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + dev: true + /merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -591,6 +713,15 @@ packages: hasBin: true dev: true + /pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + dev: true + /quansync@0.2.11: resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} dev: true @@ -599,6 +730,27 @@ packages: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} dev: true + /react-dom@18.3.1(react@18.3.1): + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + dev: true + + /react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + dev: true + + /react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + dependencies: + loose-envify: 1.4.0 + dev: true + /read-yaml-file@1.1.0: resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==} engines: {node: '>=6'} @@ -629,6 +781,12 @@ packages: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} dev: true + /scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + dependencies: + loose-envify: 1.4.0 + dev: true + /semver@7.8.5: resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} engines: {node: '>=10'} @@ -710,3 +868,10 @@ packages: dependencies: isexe: 2.0.0 dev: true + + github.com/kevva/is-positive/97edff6f525f192a3f83cea1944765f769ae2678: + resolution: {tarball: https://codeload.github.com/kevva/is-positive/tar.gz/97edff6f525f192a3f83cea1944765f769ae2678} + name: is-positive + version: 3.1.0 + engines: {node: '>=0.10.0'} + dev: true diff --git a/packages/workspace-tools/src/__fixtures__/basic-pnpm-9/package.json b/packages/workspace-tools/src/__fixtures__/basic-pnpm-9/package.json index 96767ad6b..f480b9e57 100644 --- a/packages/workspace-tools/src/__fixtures__/basic-pnpm-9/package.json +++ b/packages/workspace-tools/src/__fixtures__/basic-pnpm-9/package.json @@ -4,10 +4,15 @@ "version": "0.1.0", "license": "MIT", "private": true, - "description": "derived from sveltejs/kit", + "description": "derived from sveltejs/kit; includes edge-case deps (peer/nested-peer suffixes, patched dep, git/non-semver dep) for lockfile snapshot parsing tests", "devDependencies": { "@changesets/cli": "^2.14.1", + "@testing-library/react": "16.0.1", + "is-odd": "3.0.1", + "is-positive": "github:kevva/is-positive#97edff6f525f192a3f83cea1944765f769ae2678", "prettier": "2.8.0", + "react": "18.3.1", + "react-dom": "18.3.1", "typescript": "^4.2.3" } } diff --git a/packages/workspace-tools/src/__fixtures__/basic-pnpm-9/patches/is-odd@3.0.1.patch b/packages/workspace-tools/src/__fixtures__/basic-pnpm-9/patches/is-odd@3.0.1.patch new file mode 100644 index 000000000..18e30fd96 --- /dev/null +++ b/packages/workspace-tools/src/__fixtures__/basic-pnpm-9/patches/is-odd@3.0.1.patch @@ -0,0 +1,7 @@ +diff --git a/index.js b/index.js +index 0000000..1111111 100644 +--- a/index.js ++++ b/index.js +@@ -1,1 +1,2 @@ ++// patched for lockfile fixture + 'use strict'; diff --git a/packages/workspace-tools/src/__fixtures__/basic-pnpm-9/pnpm-lock.yaml b/packages/workspace-tools/src/__fixtures__/basic-pnpm-9/pnpm-lock.yaml index de0c3e974..5f1668537 100644 --- a/packages/workspace-tools/src/__fixtures__/basic-pnpm-9/pnpm-lock.yaml +++ b/packages/workspace-tools/src/__fixtures__/basic-pnpm-9/pnpm-lock.yaml @@ -4,6 +4,9 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +patchedDependencies: + is-odd@3.0.1: 97f62440b2a3a0dc38f52ca3991c1b62862197d25932422549ece06c0a3a6222 + importers: .: @@ -11,15 +14,38 @@ importers: '@changesets/cli': specifier: ^2.14.1 version: 2.31.0 + '@testing-library/react': + specifier: 16.0.1 + version: 16.0.1(@testing-library/dom@10.4.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + is-odd: + specifier: 3.0.1 + version: 3.0.1(patch_hash=97f62440b2a3a0dc38f52ca3991c1b62862197d25932422549ece06c0a3a6222) + is-positive: + specifier: github:kevva/is-positive#97edff6f525f192a3f83cea1944765f769ae2678 + version: https://codeload.github.com/kevva/is-positive/tar.gz/97edff6f525f192a3f83cea1944765f769ae2678 prettier: specifier: 2.8.0 version: 2.8.0 + react: + specifier: 18.3.1 + version: 18.3.1 + react-dom: + specifier: 18.3.1 + version: 18.3.1(react@18.3.1) typescript: specifier: ^4.2.3 version: 4.9.5 packages: + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + '@babel/runtime@7.29.7': resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} engines: {node: '>=6.9.0'} @@ -106,6 +132,28 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@testing-library/dom@10.4.1': + resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} + engines: {node: '>=18'} + + '@testing-library/react@16.0.1': + resolution: {integrity: sha512-dSmwJVtJXmku+iocRhWOUFbrERC76TX2Mnf0ATODz8brzAZrMBbzLwQixlBSanZxR6LddK3eiwpSFZgDET1URg==} + engines: {node: '>=18'} + peerDependencies: + '@testing-library/dom': ^10.0.0 + '@types/react': ^18.0.0 + '@types/react-dom': ^18.0.0 + react: ^18.0.0 + react-dom: ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + '@types/node@12.20.55': resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} @@ -117,12 +165,19 @@ packages: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + array-union@2.1.0: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} @@ -142,6 +197,10 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + detect-indent@6.1.0: resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} engines: {node: '>=8'} @@ -150,6 +209,9 @@ packages: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + enquirer@2.4.1: resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} engines: {node: '>=8.6'} @@ -216,10 +278,23 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-number@6.0.0: + resolution: {integrity: sha512-Wu1VHeILBK8KAWJUAiSZQX94GmOE45Rg6/538fKwiloUu21KncEkYGPqob2oSZ5mUT73vLGrHQjKw3KMPwfDzg==} + engines: {node: '>=0.10.0'} + is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-odd@3.0.1: + resolution: {integrity: sha512-CQpnWPrDwmP1+SMHXZhtLtJv90yiyVfluGsX5iNCVkrhQtU3TQHsUWPG9wkdk9Lgd5yNpAg9jQEo90CBaXgWMA==} + engines: {node: '>=4'} + + is-positive@https://codeload.github.com/kevva/is-positive/tar.gz/97edff6f525f192a3f83cea1944765f769ae2678: + resolution: {gitHosted: true, tarball: https://codeload.github.com/kevva/is-positive/tar.gz/97edff6f525f192a3f83cea1944765f769ae2678} + version: 3.1.0 + engines: {node: '>=0.10.0'} + is-subdir@1.2.0: resolution: {integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==} engines: {node: '>=4'} @@ -231,6 +306,9 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + js-yaml@3.14.2: resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} hasBin: true @@ -249,6 +327,14 @@ packages: lodash.startcase@4.4.0: resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -315,12 +401,28 @@ packages: engines: {node: '>=10.13.0'} hasBin: true + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + quansync@0.2.11: resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + react-dom@18.3.1: + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + read-yaml-file@1.1.0: resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==} engines: {node: '>=6'} @@ -339,6 +441,9 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + semver@7.8.5: resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} engines: {node: '>=10'} @@ -398,6 +503,14 @@ packages: snapshots: + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/helper-validator-identifier@7.29.7': {} + '@babel/runtime@7.29.7': {} '@changesets/apply-release-plan@7.1.1': @@ -576,18 +689,44 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 + '@testing-library/dom@10.4.1': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/runtime': 7.29.7 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + picocolors: 1.1.1 + pretty-format: 27.5.1 + + '@testing-library/react@16.0.1(@testing-library/dom@10.4.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.29.7 + '@testing-library/dom': 10.4.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@types/aria-query@5.0.4': {} + '@types/node@12.20.55': {} ansi-colors@4.1.3: {} ansi-regex@5.0.1: {} + ansi-styles@5.2.0: {} + argparse@1.0.10: dependencies: sprintf-js: 1.0.3 argparse@2.0.1: {} + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + array-union@2.1.0: {} better-path-resolve@1.0.0: @@ -606,12 +745,16 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + dequal@2.0.3: {} + detect-indent@6.1.0: {} dir-glob@3.0.1: dependencies: path-type: 4.0.0 + dom-accessibility-api@0.5.16: {} + enquirer@2.4.1: dependencies: ansi-colors: 4.1.3 @@ -683,8 +826,16 @@ snapshots: dependencies: is-extglob: 2.1.1 + is-number@6.0.0: {} + is-number@7.0.0: {} + is-odd@3.0.1(patch_hash=97f62440b2a3a0dc38f52ca3991c1b62862197d25932422549ece06c0a3a6222): + dependencies: + is-number: 6.0.0 + + is-positive@https://codeload.github.com/kevva/is-positive/tar.gz/97edff6f525f192a3f83cea1944765f769ae2678: {} + is-subdir@1.2.0: dependencies: better-path-resolve: 1.0.0 @@ -693,6 +844,8 @@ snapshots: isexe@2.0.0: {} + js-tokens@4.0.0: {} + js-yaml@3.14.2: dependencies: argparse: 1.0.10 @@ -712,6 +865,12 @@ snapshots: lodash.startcase@4.4.0: {} + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lz-string@1.5.0: {} + merge2@1.4.1: {} micromatch@4.0.8: @@ -757,10 +916,28 @@ snapshots: prettier@2.8.0: {} + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + quansync@0.2.11: {} queue-microtask@1.2.3: {} + react-dom@18.3.1(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + + react-is@17.0.2: {} + + react@18.3.1: + dependencies: + loose-envify: 1.4.0 + read-yaml-file@1.1.0: dependencies: graceful-fs: 4.2.11 @@ -778,6 +955,10 @@ snapshots: safer-buffer@2.1.2: {} + scheduler@0.23.2: + dependencies: + loose-envify: 1.4.0 + semver@7.8.5: {} shebang-command@2.0.0: diff --git a/packages/workspace-tools/src/__fixtures__/basic-pnpm-9/pnpm-workspace.yaml b/packages/workspace-tools/src/__fixtures__/basic-pnpm-9/pnpm-workspace.yaml new file mode 100644 index 000000000..e228edd0d --- /dev/null +++ b/packages/workspace-tools/src/__fixtures__/basic-pnpm-9/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +patchedDependencies: + is-odd@3.0.1: patches/is-odd@3.0.1.patch diff --git a/packages/workspace-tools/src/__tests__/lockfile/lockfile.test.ts b/packages/workspace-tools/src/__tests__/lockfile/lockfile.test.ts index d1ca9c2fb..923349210 100644 --- a/packages/workspace-tools/src/__tests__/lockfile/lockfile.test.ts +++ b/packages/workspace-tools/src/__tests__/lockfile/lockfile.test.ts @@ -90,32 +90,83 @@ describe("parseLockFile()", () => { expect(parsedLockFile.object[which!].version).toBe("2.0.2"); }); - it("parses pnpm-lock.yaml file when it is found (lockfileVersion 6.0)", async () => { - const packageRoot = setupFixture("basic-pnpm-6"); - const parsedLockFile = await parseLockFile(packageRoot); - - // If the lock file is updated, you might need to switch to a different key and dependency - const which = Object.keys(parsedLockFile.object).find((key) => /^which@/.test(key)); - expect(which).toBeTruthy(); - expect(parsedLockFile.object[which!].dependencies?.["isexe"]).toBeTruthy(); - - // The parsed entry should preserve the resolved version. - expect(which).toBe("which@2.0.2"); - expect(parsedLockFile.object[which!].version).toBe("2.0.2"); + // The `basic-pnpm-6` (lockfileVersion 6.0) and `basic-pnpm-9` (lockfileVersion 9.0) fixtures + // contain the same edge-case dependencies so we can verify pnpm dependency-path parsing: + // - scoped names (`@scope/name@version`) + // - a single peer-dependency suffix (`react-dom@(react@)`) + // - multiple/nested peer suffixes (`@testing-library/react@(...)(...)(...)`) + // - a patched dependency (`is-odd@(patch_hash=...)`) + // - a non-semver (git/tarball) version + describe.each(["basic-pnpm-6", "basic-pnpm-9"] as const)("edge cases (%s)", (fixtureName) => { + it("keeps a plain transitive dependency and its version", async () => { + const packageRoot = setupFixture(fixtureName); + const { object } = await parseLockFile(packageRoot); + + const which = Object.keys(object).find((key) => /^which@/.test(key)); + expect(which).toBe("which@2.0.2"); + expect(object[which!].version).toBe("2.0.2"); + expect(object[which!].dependencies?.["isexe"]).toBe("2.0.0"); + }); + + it("strips a single peer-dependency suffix from the version", async () => { + const packageRoot = setupFixture(fixtureName); + const { object } = await parseLockFile(packageRoot); + + // Snapshot key is `react-dom@18.3.1(react@18.3.1)`; the peer suffix must not leak into + // the parsed name or version. + expect(object["react-dom@18.3.1"]).toBeTruthy(); + expect(object["react-dom@18.3.1"].version).toBe("18.3.1"); + expect(object["react-dom@18.3.1"].dependencies?.["react"]).toBe("18.3.1"); + + // No parsed key should still contain a raw peer suffix. + expect(Object.keys(object).some((key) => key.includes("("))).toBe(false); + }); + + it("strips multiple/nested peer suffixes from a scoped package", async () => { + const packageRoot = setupFixture(fixtureName); + const { object } = await parseLockFile(packageRoot); + + // Snapshot key has several peer groups (nested in 9.0): + // `@testing-library/react@16.0.1(@testing-library/dom@10.4.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)` + const key = "@testing-library/react@16.0.1"; + expect(object[key]).toBeTruthy(); + expect(object[key].version).toBe("16.0.1"); + // The peer suffix is stripped from the parsed KEY, but dependency *values* are kept + // verbatim from the snapshot (they may still carry their own peer suffix). + expect(object[key].dependencies?.["react-dom"]).toBe("18.3.1(react@18.3.1)"); + }); + + it("strips a patch_hash suffix from the version", async () => { + const packageRoot = setupFixture(fixtureName); + const { object } = await parseLockFile(packageRoot); + + // Snapshot key is `is-odd@3.0.1(patch_hash=...)`. + expect(object["is-odd@3.0.1"]).toBeTruthy(); + expect(object["is-odd@3.0.1"].version).toBe("3.0.1"); + expect(object["is-odd@3.0.1"].dependencies?.["is-number"]).toBe("6.0.0"); + expect(Object.keys(object).some((key) => key.includes("patch_hash"))).toBe(false); + }); }); - it("parses pnpm-lock.yaml file when it is found (lockfileVersion 9.0)", async () => { + it("keeps a non-semver (git tarball) version verbatim (lockfileVersion 9.0)", async () => { const packageRoot = setupFixture("basic-pnpm-9"); - const parsedLockFile = await parseLockFile(packageRoot); + const { object } = await parseLockFile(packageRoot); - // If the lock file is updated, you might need to switch to a different key and dependency - const which = Object.keys(parsedLockFile.object).find((key) => /^which@/.test(key)); - expect(which).toBeTruthy(); - expect(parsedLockFile.object[which!].dependencies?.["isexe"]).toBeTruthy(); + // lockfileVersion 9.0 encodes the github dependency as `is-positive@`. + const url = "https://codeload.github.com/kevva/is-positive/tar.gz/97edff6f525f192a3f83cea1944765f769ae2678"; + expect(object[`is-positive@${url}`]).toBeTruthy(); + expect(object[`is-positive@${url}`].version).toBe(url); + }); - // The parsed entry should preserve the resolved version. - expect(which).toBe("which@2.0.2"); - expect(parsedLockFile.object[which!].version).toBe("2.0.2"); + it("parses the lockfileVersion 6.0 git dependency encoding", async () => { + const packageRoot = setupFixture("basic-pnpm-6"); + const { object } = await parseLockFile(packageRoot); + + // lockfileVersion 6.0 encodes the github dependency as `github.com///` + // (no `@`), so the final path segment is treated as the version. + const ref = "97edff6f525f192a3f83cea1944765f769ae2678"; + expect(object[`github.com/kevva/is-positive@${ref}`]).toBeTruthy(); + expect(object[`github.com/kevva/is-positive@${ref}`].version).toBe(ref); }); }); }); diff --git a/packages/workspace-tools/src/__tests__/lockfile/parsePnpmLock.test.ts b/packages/workspace-tools/src/__tests__/lockfile/parsePnpmLock.test.ts new file mode 100644 index 000000000..14ff500f2 --- /dev/null +++ b/packages/workspace-tools/src/__tests__/lockfile/parsePnpmLock.test.ts @@ -0,0 +1,208 @@ +import { describe, expect, it } from "@jest/globals"; +import { parsePnpmLock } from "../../lockfile/parsePnpmLock.js"; +import type { PnpmLockFile } from "../../lockfile/types.js"; + +/** + * Direct unit tests for the pnpm dependency-path key parsing in `parsePnpmLock`. These use + * hand-crafted lockfile objects to exercise edge cases that are awkward to reproduce as real + * fixtures (e.g. `git+ssh://` URLs that require network/auth to resolve). + * + * The expected behavior mirrors pnpm's own `parse` / `indexOfDepPathSuffix` in `@pnpm/deps.path`: + * the name/version separator is the first `@` at index >= 1, and trailing peer-dependency and + * patch-hash suffixes are removed by balanced-parenthesis matching. + */ +describe("parsePnpmLock", () => { + describe("lockfileVersion >= 9.0 (snapshots section)", () => { + it("reads dependency edges from `snapshots`, not `packages`", () => { + const lock: PnpmLockFile = { + packages: { "which@2.0.2": { resolution: { integrity: "sha512-..." } } }, + snapshots: { "which@2.0.2": { dependencies: { isexe: "2.0.0" } } }, + }; + + const { object } = parsePnpmLock(lock); + expect(object["which@2.0.2"]).toEqual({ version: "2.0.2", dependencies: { isexe: "2.0.0" } }); + }); + + it("ignores the scoped leading `@` when splitting name and version", () => { + const lock: PnpmLockFile = { + packages: {}, + snapshots: { "@babel/runtime@7.28.4": { dependencies: { "regenerator-runtime": "0.14.1" } } }, + }; + + const { object } = parsePnpmLock(lock); + expect(object["@babel/runtime@7.28.4"]).toEqual({ + version: "7.28.4", + dependencies: { "regenerator-runtime": "0.14.1" }, + }); + }); + + it("strips a single peer-dependency suffix", () => { + const lock: PnpmLockFile = { + packages: {}, + snapshots: { "react-dom@18.3.1(react@18.3.1)": { dependencies: { react: "18.3.1" } } }, + }; + + const { object } = parsePnpmLock(lock); + expect(object["react-dom@18.3.1"]).toEqual({ version: "18.3.1", dependencies: { react: "18.3.1" } }); + }); + + it("strips multiple peer-dependency suffixes", () => { + const lock: PnpmLockFile = { + packages: {}, + snapshots: { "foo@1.0.0(react@18.0.0)(react-dom@18.0.0)": { dependencies: {} } }, + }; + + const { object } = parsePnpmLock(lock); + expect(Object.keys(object)).toEqual(["foo@1.0.0"]); + expect(object["foo@1.0.0"].version).toBe("1.0.0"); + }); + + it("strips nested peer-dependency suffixes", () => { + const lock: PnpmLockFile = { + packages: {}, + snapshots: { + "@testing-library/react@16.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)": { + dependencies: { "react-dom": "18.3.1" }, + }, + }, + }; + + const { object } = parsePnpmLock(lock); + expect(object["@testing-library/react@16.0.1"]).toEqual({ + version: "16.0.1", + dependencies: { "react-dom": "18.3.1" }, + }); + }); + + it("strips a patch_hash suffix", () => { + const lock: PnpmLockFile = { + packages: {}, + snapshots: { "is-odd@3.0.1(patch_hash=abc123)": { dependencies: { "is-number": "6.0.0" } } }, + }; + + const { object } = parsePnpmLock(lock); + expect(object["is-odd@3.0.1"]).toEqual({ version: "3.0.1", dependencies: { "is-number": "6.0.0" } }); + }); + + it("strips a combined patch_hash and peer suffix", () => { + const lock: PnpmLockFile = { + packages: {}, + snapshots: { "foo@1.0.0(patch_hash=abc123)(react@18.0.0)": { dependencies: {} } }, + }; + + const { object } = parsePnpmLock(lock); + expect(Object.keys(object)).toEqual(["foo@1.0.0"]); + expect(object["foo@1.0.0"].version).toBe("1.0.0"); + }); + + it("keeps a non-semver tarball URL version verbatim", () => { + const url = "https://codeload.github.com/kevva/is-positive/tar.gz/97edff6f"; + const lock: PnpmLockFile = { + packages: {}, + snapshots: { [`is-positive@${url}`]: {} }, + }; + + const { object } = parsePnpmLock(lock); + expect(object[`is-positive@${url}`]).toEqual({ version: url, dependencies: undefined }); + }); + + it("splits on the first `@` for a non-semver version that itself contains `@`", () => { + // A `git+ssh://git@github.com/...` URL contains an `@`; the separator must be the first `@` + // after the name, not the last (which would corrupt both the name and the version). + const version = "git+ssh://git@github.com/sindresorhus/is-plain-obj.git#abc123"; + const lock: PnpmLockFile = { + packages: {}, + snapshots: { [`is-plain-obj@${version}`]: { dependencies: {} } }, + }; + + const { object } = parsePnpmLock(lock); + expect(object[`is-plain-obj@${version}`]).toBeTruthy(); + expect(object[`is-plain-obj@${version}`].version).toBe(version); + }); + + it("does not strip parentheses that are not a trailing suffix", () => { + // Suffix stripping only applies to balanced groups at the very end of the version, so a + // non-semver version that merely contains `(` is kept intact (mirrors pnpm only acting when + // the version ends with `)`). + const version = "https://example.com/pkg(beta).tgz"; + const lock: PnpmLockFile = { + packages: {}, + snapshots: { [`pkg@${version}`]: {} }, + }; + + const { object } = parsePnpmLock(lock); + expect(object[`pkg@${version}`].version).toBe(version); + }); + }); + + describe("lockfileVersion 6.0 (leading slash, dependencies inline in packages)", () => { + it("strips the leading slash and splits on `@`", () => { + const lock: PnpmLockFile = { + packages: { "/which@2.0.2": { dependencies: { isexe: "2.0.0" } } }, + }; + + const { object } = parsePnpmLock(lock); + expect(object["which@2.0.2"]).toEqual({ version: "2.0.2", dependencies: { isexe: "2.0.0" } }); + }); + + it("handles a scoped name with a leading slash and peer suffix", () => { + const lock: PnpmLockFile = { + packages: { "/@testing-library/react@16.0.1(react@18.3.1)": { dependencies: { react: "18.3.1" } } }, + }; + + const { object } = parsePnpmLock(lock); + expect(object["@testing-library/react@16.0.1"]).toEqual({ + version: "16.0.1", + dependencies: { react: "18.3.1" }, + }); + }); + + it("parses the git dependency encoding `github.com/owner/repo/ref`", () => { + const lock: PnpmLockFile = { + packages: { "github.com/kevva/is-positive/97edff6f": {} }, + }; + + const { object } = parsePnpmLock(lock); + expect(object["github.com/kevva/is-positive@97edff6f"]).toEqual({ + version: "97edff6f", + dependencies: undefined, + }); + }); + }); + + describe("lockfileVersion <= 5.x (slash-separated keys)", () => { + it("splits an unscoped `/name/version` key", () => { + const lock: PnpmLockFile = { + packages: { "/which/2.0.2": { dependencies: { isexe: "2.0.0" } } }, + }; + + const { object } = parsePnpmLock(lock); + expect(object["which@2.0.2"]).toEqual({ version: "2.0.2", dependencies: { isexe: "2.0.0" } }); + }); + + it("splits a scoped `/@scope/name/version` key", () => { + const lock: PnpmLockFile = { + packages: { "/@babel/runtime/7.28.4": { dependencies: {} } }, + }; + + const { object } = parsePnpmLock(lock); + expect(object["@babel/runtime@7.28.4"]).toEqual({ version: "7.28.4", dependencies: {} }); + }); + }); + + describe("degenerate input", () => { + it("returns an empty object when there are no packages or snapshots", () => { + expect(parsePnpmLock({} as PnpmLockFile).object).toEqual({}); + }); + + it("prefers `snapshots` over `packages` when both are present", () => { + const lock: PnpmLockFile = { + packages: { "foo@1.0.0": { dependencies: { fromPackages: "1.0.0" } } }, + snapshots: { "foo@1.0.0": { dependencies: { fromSnapshots: "1.0.0" } } }, + }; + + const { object } = parsePnpmLock(lock); + expect(object["foo@1.0.0"].dependencies).toEqual({ fromSnapshots: "1.0.0" }); + }); + }); +}); diff --git a/packages/workspace-tools/src/lockfile/parsePnpmLock.ts b/packages/workspace-tools/src/lockfile/parsePnpmLock.ts index e7faa1c49..59303c7e1 100644 --- a/packages/workspace-tools/src/lockfile/parsePnpmLock.ts +++ b/packages/workspace-tools/src/lockfile/parsePnpmLock.ts @@ -1,34 +1,74 @@ import { nameAtVersion } from "./nameAtVersion.js"; import { type LockDependency, type ParsedLock, type PnpmLockFile } from "./types.js"; +/** + * Remove the trailing peer-dependency and/or patch-hash suffix(es) from a pnpm version string, + * e.g. `1.0.0(react@18.0.0)`, `1.0.0(patch_hash=abc)`, `1.0.0(patch_hash=abc)(react@18.0.0)`, or + * nested groups like `1.0.0(react-dom@18(react@18))`. + * + * Mirrors `indexOfDepPathSuffix` from pnpm's `@pnpm/deps.path`: it walks the string from the end, + * counting balanced parentheses, so suffix groups are stripped even when a peer suffix itself + * contains nested parentheses. Versions without a trailing suffix (the common case) are returned + * unchanged, including non-semver versions such as tarball or git URLs. + */ +function stripPeerAndPatchSuffix(version: string): string { + if (!version.endsWith(")")) { + return version; + } + + let open = 1; + for (let i = version.length - 2; i >= 0; i--) { + const char = version[i]; + if (char === "(") { + open--; + } else if (char === ")") { + open++; + } else if (open === 0) { + // First non-paren character (scanning from the right) that sits outside all suffix groups: + // everything after it is the peer/patch suffix. + return version.slice(0, i + 1); + } + } + + return version; +} + /** * Parse a pnpm package/snapshot key into its name and version, handling every lockfile format: * - lockfileVersion <= 5.x: slash-separated, e.g. `/which/2.0.2` or `/@babel/runtime/7.28.4` - * - lockfileVersion 6.0: leading slash with `@` separator, e.g. `/which@2.0.2` or `/@babel/runtime@7.28.4` - * - lockfileVersion >= 9.0: no leading slash, e.g. `which@2.0.2` or `@babel/runtime@7.28.4` + * - lockfileVersion 6.0: leading slash with `@` separator, e.g. `/which@2.0.2`, plus peer/patch + * suffixes like `/react-dom@18.3.1(react@18.3.1)` and `/is-odd@3.0.1(patch_hash=...)` + * - lockfileVersion >= 9.0: no leading slash, e.g. `which@2.0.2`, with the same suffixes and + * nested peer groups, e.g. `@testing-library/react@16.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)` * - * Any peer-dependency suffix (e.g. `(react@19.2.4)`) is stripped. + * The name/version separator is the first `@` at index >= 1 (mirroring pnpm's `parse`), so the + * leading `@` of a scoped package is ignored and an `@` inside a non-semver version (e.g. a + * `git+ssh://git@host/...` URL) is not mistaken for the separator. */ function parsePackageKey(key: string): { name: string; version: string } { // Strip the leading "/" present in lockfileVersion <= 6.0 keys. - let base = key.startsWith("/") ? key.slice(1) : key; + const base = key.startsWith("/") ? key.slice(1) : key; - // Strip any peer-dependency suffix, e.g. `(react@19.2.4)`. - const peerIndex = base.indexOf("("); - if (peerIndex !== -1) { - base = base.slice(0, peerIndex); + const separatorIndex = base.indexOf("@", 1); + if (separatorIndex !== -1) { + return { + name: base.slice(0, separatorIndex), + version: stripPeerAndPatchSuffix(base.slice(separatorIndex + 1)), + }; } - // An "@" past the first character separates the version in lockfileVersion >= 6.0 - // (the first character may be the "@" of a scoped package name). - const atIndex = base.lastIndexOf("@"); - if (atIndex > 0) { - return { name: base.slice(0, atIndex), version: base.slice(atIndex + 1) }; + // No "@" separator: either a lockfileVersion <= 5.x slash-separated key (`name/version`, + // `@scope/name/version`) or a 6.0-era git key (`github.com/owner/repo/ref`). Split on the + // last "/" so the final segment is treated as the version. + const stripped = stripPeerAndPatchSuffix(base); + const slashIndex = stripped.lastIndexOf("/"); + if (slashIndex === -1) { + return { name: stripped, version: "" }; } - - // lockfileVersion <= 5.x uses "/" to separate the version. - const slashIndex = base.lastIndexOf("/"); - return { name: base.slice(0, slashIndex), version: base.slice(slashIndex + 1) }; + return { + name: stripped.slice(0, slashIndex), + version: stripped.slice(slashIndex + 1), + }; } export function parsePnpmLock(yaml: PnpmLockFile): ParsedLock { From a4474d10523e9bf99349f2552f202fad26cf5974 Mon Sep 17 00:00:00 2001 From: Andrew Stegmaier Date: Wed, 24 Jun 2026 14:35:37 -0500 Subject: [PATCH 05/15] Simplify implementation to keep existing codepath unchanged for <6.0 lockfiles. --- .../__tests__/lockfile/parsePnpmLock.test.ts | 32 +++++---- .../src/lockfile/parsePnpmLock.ts | 67 +++++++++++-------- .../workspace-tools/src/lockfile/types.ts | 2 + 3 files changed, 60 insertions(+), 41 deletions(-) diff --git a/packages/workspace-tools/src/__tests__/lockfile/parsePnpmLock.test.ts b/packages/workspace-tools/src/__tests__/lockfile/parsePnpmLock.test.ts index 14ff500f2..f2e5b5601 100644 --- a/packages/workspace-tools/src/__tests__/lockfile/parsePnpmLock.test.ts +++ b/packages/workspace-tools/src/__tests__/lockfile/parsePnpmLock.test.ts @@ -7,14 +7,16 @@ import type { PnpmLockFile } from "../../lockfile/types.js"; * hand-crafted lockfile objects to exercise edge cases that are awkward to reproduce as real * fixtures (e.g. `git+ssh://` URLs that require network/auth to resolve). * - * The expected behavior mirrors pnpm's own `parse` / `indexOfDepPathSuffix` in `@pnpm/deps.path`: - * the name/version separator is the first `@` at index >= 1, and trailing peer-dependency and - * patch-hash suffixes are removed by balanced-parenthesis matching. + * The expected behavior for lockfileVersion 6.0 / 9.0 mirrors pnpm's own `parse` / + * `indexOfDepPathSuffix` in `@pnpm/deps.path`: the name/version separator is the first `@` at + * index >= 1, and trailing peer-dependency and patch-hash suffixes are removed by + * balanced-parenthesis matching. */ describe("parsePnpmLock", () => { describe("lockfileVersion >= 9.0 (snapshots section)", () => { it("reads dependency edges from `snapshots`, not `packages`", () => { const lock: PnpmLockFile = { + lockfileVersion: "9.0", packages: { "which@2.0.2": { resolution: { integrity: "sha512-..." } } }, snapshots: { "which@2.0.2": { dependencies: { isexe: "2.0.0" } } }, }; @@ -25,7 +27,7 @@ describe("parsePnpmLock", () => { it("ignores the scoped leading `@` when splitting name and version", () => { const lock: PnpmLockFile = { - packages: {}, + lockfileVersion: "9.0", snapshots: { "@babel/runtime@7.28.4": { dependencies: { "regenerator-runtime": "0.14.1" } } }, }; @@ -38,7 +40,7 @@ describe("parsePnpmLock", () => { it("strips a single peer-dependency suffix", () => { const lock: PnpmLockFile = { - packages: {}, + lockfileVersion: "9.0", snapshots: { "react-dom@18.3.1(react@18.3.1)": { dependencies: { react: "18.3.1" } } }, }; @@ -48,7 +50,7 @@ describe("parsePnpmLock", () => { it("strips multiple peer-dependency suffixes", () => { const lock: PnpmLockFile = { - packages: {}, + lockfileVersion: "9.0", snapshots: { "foo@1.0.0(react@18.0.0)(react-dom@18.0.0)": { dependencies: {} } }, }; @@ -59,7 +61,7 @@ describe("parsePnpmLock", () => { it("strips nested peer-dependency suffixes", () => { const lock: PnpmLockFile = { - packages: {}, + lockfileVersion: "9.0", snapshots: { "@testing-library/react@16.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)": { dependencies: { "react-dom": "18.3.1" }, @@ -76,7 +78,7 @@ describe("parsePnpmLock", () => { it("strips a patch_hash suffix", () => { const lock: PnpmLockFile = { - packages: {}, + lockfileVersion: "9.0", snapshots: { "is-odd@3.0.1(patch_hash=abc123)": { dependencies: { "is-number": "6.0.0" } } }, }; @@ -86,7 +88,7 @@ describe("parsePnpmLock", () => { it("strips a combined patch_hash and peer suffix", () => { const lock: PnpmLockFile = { - packages: {}, + lockfileVersion: "9.0", snapshots: { "foo@1.0.0(patch_hash=abc123)(react@18.0.0)": { dependencies: {} } }, }; @@ -98,7 +100,7 @@ describe("parsePnpmLock", () => { it("keeps a non-semver tarball URL version verbatim", () => { const url = "https://codeload.github.com/kevva/is-positive/tar.gz/97edff6f"; const lock: PnpmLockFile = { - packages: {}, + lockfileVersion: "9.0", snapshots: { [`is-positive@${url}`]: {} }, }; @@ -111,7 +113,7 @@ describe("parsePnpmLock", () => { // after the name, not the last (which would corrupt both the name and the version). const version = "git+ssh://git@github.com/sindresorhus/is-plain-obj.git#abc123"; const lock: PnpmLockFile = { - packages: {}, + lockfileVersion: "9.0", snapshots: { [`is-plain-obj@${version}`]: { dependencies: {} } }, }; @@ -126,7 +128,7 @@ describe("parsePnpmLock", () => { // the version ends with `)`). const version = "https://example.com/pkg(beta).tgz"; const lock: PnpmLockFile = { - packages: {}, + lockfileVersion: "9.0", snapshots: { [`pkg@${version}`]: {} }, }; @@ -138,6 +140,7 @@ describe("parsePnpmLock", () => { describe("lockfileVersion 6.0 (leading slash, dependencies inline in packages)", () => { it("strips the leading slash and splits on `@`", () => { const lock: PnpmLockFile = { + lockfileVersion: "6.0", packages: { "/which@2.0.2": { dependencies: { isexe: "2.0.0" } } }, }; @@ -147,6 +150,7 @@ describe("parsePnpmLock", () => { it("handles a scoped name with a leading slash and peer suffix", () => { const lock: PnpmLockFile = { + lockfileVersion: "6.0", packages: { "/@testing-library/react@16.0.1(react@18.3.1)": { dependencies: { react: "18.3.1" } } }, }; @@ -159,6 +163,7 @@ describe("parsePnpmLock", () => { it("parses the git dependency encoding `github.com/owner/repo/ref`", () => { const lock: PnpmLockFile = { + lockfileVersion: "6.0", packages: { "github.com/kevva/is-positive/97edff6f": {} }, }; @@ -173,6 +178,7 @@ describe("parsePnpmLock", () => { describe("lockfileVersion <= 5.x (slash-separated keys)", () => { it("splits an unscoped `/name/version` key", () => { const lock: PnpmLockFile = { + lockfileVersion: 5.4, packages: { "/which/2.0.2": { dependencies: { isexe: "2.0.0" } } }, }; @@ -182,6 +188,7 @@ describe("parsePnpmLock", () => { it("splits a scoped `/@scope/name/version` key", () => { const lock: PnpmLockFile = { + lockfileVersion: 5.4, packages: { "/@babel/runtime/7.28.4": { dependencies: {} } }, }; @@ -197,6 +204,7 @@ describe("parsePnpmLock", () => { it("prefers `snapshots` over `packages` when both are present", () => { const lock: PnpmLockFile = { + lockfileVersion: "9.0", packages: { "foo@1.0.0": { dependencies: { fromPackages: "1.0.0" } } }, snapshots: { "foo@1.0.0": { dependencies: { fromSnapshots: "1.0.0" } } }, }; diff --git a/packages/workspace-tools/src/lockfile/parsePnpmLock.ts b/packages/workspace-tools/src/lockfile/parsePnpmLock.ts index 59303c7e1..1dcd0a266 100644 --- a/packages/workspace-tools/src/lockfile/parsePnpmLock.ts +++ b/packages/workspace-tools/src/lockfile/parsePnpmLock.ts @@ -34,40 +34,35 @@ function stripPeerAndPatchSuffix(version: string): string { } /** - * Parse a pnpm package/snapshot key into its name and version, handling every lockfile format: - * - lockfileVersion <= 5.x: slash-separated, e.g. `/which/2.0.2` or `/@babel/runtime/7.28.4` - * - lockfileVersion 6.0: leading slash with `@` separator, e.g. `/which@2.0.2`, plus peer/patch - * suffixes like `/react-dom@18.3.1(react@18.3.1)` and `/is-odd@3.0.1(patch_hash=...)` - * - lockfileVersion >= 9.0: no leading slash, e.g. `which@2.0.2`, with the same suffixes and - * nested peer groups, e.g. `@testing-library/react@16.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)` + * Parse a lockfileVersion 6.0 / 9.0 package or snapshot key into its name and version. These keys + * look like `name@version` or `@scope/name@version`, optionally with a leading `/` (6.0) and + * trailing peer/patch suffixes, e.g.: + * - `/react-dom@18.3.1(react@18.3.1)` + * - `is-odd@3.0.1(patch_hash=...)` + * - `@testing-library/react@16.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)` * * The name/version separator is the first `@` at index >= 1 (mirroring pnpm's `parse`), so the * leading `@` of a scoped package is ignored and an `@` inside a non-semver version (e.g. a * `git+ssh://git@host/...` URL) is not mistaken for the separator. */ function parsePackageKey(key: string): { name: string; version: string } { - // Strip the leading "/" present in lockfileVersion <= 6.0 keys. + // 6.0 keys have a leading "/"; 9.0 keys do not. const base = key.startsWith("/") ? key.slice(1) : key; const separatorIndex = base.indexOf("@", 1); - if (separatorIndex !== -1) { - return { - name: base.slice(0, separatorIndex), - version: stripPeerAndPatchSuffix(base.slice(separatorIndex + 1)), - }; + if (separatorIndex === -1) { + // The only 6.0/9.0 keys without an `@` are git shorthand deps encoded as + // `github.com/owner/repo/`. Keep this narrow: treat the last path segment as the version. + const slashIndex = base.lastIndexOf("/"); + if (slashIndex === -1) { + return { name: base, version: "" }; + } + return { name: base.slice(0, slashIndex), version: base.slice(slashIndex + 1) }; } - // No "@" separator: either a lockfileVersion <= 5.x slash-separated key (`name/version`, - // `@scope/name/version`) or a 6.0-era git key (`github.com/owner/repo/ref`). Split on the - // last "/" so the final segment is treated as the version. - const stripped = stripPeerAndPatchSuffix(base); - const slashIndex = stripped.lastIndexOf("/"); - if (slashIndex === -1) { - return { name: stripped, version: "" }; - } return { - name: stripped.slice(0, slashIndex), - version: stripped.slice(slashIndex + 1), + name: base.slice(0, separatorIndex), + version: stripPeerAndPatchSuffix(base.slice(separatorIndex + 1)), }; } @@ -76,20 +71,34 @@ export function parsePnpmLock(yaml: PnpmLockFile): ParsedLock { [key in string]: LockDependency; } = {}; - // lockfileVersion >= 9.0 moves dependency edges into a `snapshots` section, leaving `packages` - // with only resolution metadata. Earlier versions keep dependencies inline under `packages`. - const entries = yaml?.snapshots ?? yaml?.packages; + // lockfileVersion 6.0 introduced the `name@version` dependency-path format (and 9.0 additionally + // moved dependency edges into a `snapshots` section). Older lockfiles (5.x and below) use the + // original slash-separated `name/version` format, which is parsed by the legacy branch below. + const lockfileVersion = Number(yaml?.lockfileVersion ?? 0); - if (entries) { - for (const [pkgSpec, snapshot] of Object.entries(entries)) { - // TODO: handle file:foo.tgz syntax (rush uses this for internal package links) + if (lockfileVersion >= 6) { + // lockfileVersion >= 9.0 stores dependency edges under `snapshots`; 6.0 keeps them inline in + // `packages`. Either way the keys share the same `name@version(suffixes)` format. + const entries = yaml.snapshots ?? yaml.packages; + for (const [pkgSpec, snapshot] of Object.entries(entries ?? {})) { const { name, version } = parsePackageKey(pkgSpec); - object[nameAtVersion(name, version)] = { version, dependencies: snapshot?.dependencies, }; } + } else if (yaml?.packages) { + for (const [pkgSpec, snapshot] of Object.entries(yaml.packages)) { + // TODO: handle file:foo.tgz syntax (rush uses this for internal package links) + const specParts = pkgSpec.split(/\//); + const name = specParts.length > 3 ? `${specParts[1]}/${specParts[2]}` : specParts[1]; + const version = specParts.length > 3 ? specParts[3] : specParts[2]; + + object[nameAtVersion(name, version)] = { + version, + dependencies: snapshot.dependencies, + }; + } } return { diff --git a/packages/workspace-tools/src/lockfile/types.ts b/packages/workspace-tools/src/lockfile/types.ts index df8a1a3e7..21f4adeb9 100644 --- a/packages/workspace-tools/src/lockfile/types.ts +++ b/packages/workspace-tools/src/lockfile/types.ts @@ -14,6 +14,8 @@ export type ParsedLock = { /** pnpm `pnpm-lock.yaml` format */ export interface PnpmLockFile { + /** Lockfile format version, e.g. `5.4`, `'6.0'` or `'9.0'`. */ + lockfileVersion?: number | string; /** Resolution metadata. In lockfileVersion <= 6.0 this also holds dependency edges. */ packages?: { [name: string]: any }; /** Dependency edges in lockfileVersion >= 9.0. */ From a0ecefc979b62047bc5a95a992e9f504c5f2f6c9 Mon Sep 17 00:00:00 2001 From: Andrew Stegmaier Date: Wed, 24 Jun 2026 14:37:32 -0500 Subject: [PATCH 06/15] Move parsePackageKy and stripPeerAndPatchSuffix to the bottom of the file. --- .../src/lockfile/parsePnpmLock.ts | 122 +++++++++--------- 1 file changed, 61 insertions(+), 61 deletions(-) diff --git a/packages/workspace-tools/src/lockfile/parsePnpmLock.ts b/packages/workspace-tools/src/lockfile/parsePnpmLock.ts index 1dcd0a266..5943feb1e 100644 --- a/packages/workspace-tools/src/lockfile/parsePnpmLock.ts +++ b/packages/workspace-tools/src/lockfile/parsePnpmLock.ts @@ -1,36 +1,45 @@ import { nameAtVersion } from "./nameAtVersion.js"; import { type LockDependency, type ParsedLock, type PnpmLockFile } from "./types.js"; -/** - * Remove the trailing peer-dependency and/or patch-hash suffix(es) from a pnpm version string, - * e.g. `1.0.0(react@18.0.0)`, `1.0.0(patch_hash=abc)`, `1.0.0(patch_hash=abc)(react@18.0.0)`, or - * nested groups like `1.0.0(react-dom@18(react@18))`. - * - * Mirrors `indexOfDepPathSuffix` from pnpm's `@pnpm/deps.path`: it walks the string from the end, - * counting balanced parentheses, so suffix groups are stripped even when a peer suffix itself - * contains nested parentheses. Versions without a trailing suffix (the common case) are returned - * unchanged, including non-semver versions such as tarball or git URLs. - */ -function stripPeerAndPatchSuffix(version: string): string { - if (!version.endsWith(")")) { - return version; - } +export function parsePnpmLock(yaml: PnpmLockFile): ParsedLock { + const object: { + [key in string]: LockDependency; + } = {}; - let open = 1; - for (let i = version.length - 2; i >= 0; i--) { - const char = version[i]; - if (char === "(") { - open--; - } else if (char === ")") { - open++; - } else if (open === 0) { - // First non-paren character (scanning from the right) that sits outside all suffix groups: - // everything after it is the peer/patch suffix. - return version.slice(0, i + 1); + // lockfileVersion 6.0 introduced the `name@version` dependency-path format (and 9.0 additionally + // moved dependency edges into a `snapshots` section). Older lockfiles (5.x and below) use the + // original slash-separated `name/version` format, which is parsed by the legacy branch below. + const lockfileVersion = Number(yaml?.lockfileVersion ?? 0); + + if (lockfileVersion >= 6) { + // lockfileVersion >= 9.0 stores dependency edges under `snapshots`; 6.0 keeps them inline in + // `packages`. Either way the keys share the same `name@version(suffixes)` format. + const entries = yaml.snapshots ?? yaml.packages; + for (const [pkgSpec, snapshot] of Object.entries(entries ?? {})) { + const { name, version } = parsePackageKey(pkgSpec); + object[nameAtVersion(name, version)] = { + version, + dependencies: snapshot?.dependencies, + }; + } + } else if (yaml?.packages) { + for (const [pkgSpec, snapshot] of Object.entries(yaml.packages)) { + // TODO: handle file:foo.tgz syntax (rush uses this for internal package links) + const specParts = pkgSpec.split(/\//); + const name = specParts.length > 3 ? `${specParts[1]}/${specParts[2]}` : specParts[1]; + const version = specParts.length > 3 ? specParts[3] : specParts[2]; + + object[nameAtVersion(name, version)] = { + version, + dependencies: snapshot.dependencies, + }; } } - return version; + return { + object, + type: "success", + }; } /** @@ -66,43 +75,34 @@ function parsePackageKey(key: string): { name: string; version: string } { }; } -export function parsePnpmLock(yaml: PnpmLockFile): ParsedLock { - const object: { - [key in string]: LockDependency; - } = {}; - - // lockfileVersion 6.0 introduced the `name@version` dependency-path format (and 9.0 additionally - // moved dependency edges into a `snapshots` section). Older lockfiles (5.x and below) use the - // original slash-separated `name/version` format, which is parsed by the legacy branch below. - const lockfileVersion = Number(yaml?.lockfileVersion ?? 0); - - if (lockfileVersion >= 6) { - // lockfileVersion >= 9.0 stores dependency edges under `snapshots`; 6.0 keeps them inline in - // `packages`. Either way the keys share the same `name@version(suffixes)` format. - const entries = yaml.snapshots ?? yaml.packages; - for (const [pkgSpec, snapshot] of Object.entries(entries ?? {})) { - const { name, version } = parsePackageKey(pkgSpec); - object[nameAtVersion(name, version)] = { - version, - dependencies: snapshot?.dependencies, - }; - } - } else if (yaml?.packages) { - for (const [pkgSpec, snapshot] of Object.entries(yaml.packages)) { - // TODO: handle file:foo.tgz syntax (rush uses this for internal package links) - const specParts = pkgSpec.split(/\//); - const name = specParts.length > 3 ? `${specParts[1]}/${specParts[2]}` : specParts[1]; - const version = specParts.length > 3 ? specParts[3] : specParts[2]; +/** + * Remove the trailing peer-dependency and/or patch-hash suffix(es) from a pnpm version string, + * e.g. `1.0.0(react@18.0.0)`, `1.0.0(patch_hash=abc)`, `1.0.0(patch_hash=abc)(react@18.0.0)`, or + * nested groups like `1.0.0(react-dom@18(react@18))`. + * + * Mirrors `indexOfDepPathSuffix` from pnpm's `@pnpm/deps.path`: it walks the string from the end, + * counting balanced parentheses, so suffix groups are stripped even when a peer suffix itself + * contains nested parentheses. Versions without a trailing suffix (the common case) are returned + * unchanged, including non-semver versions such as tarball or git URLs. + */ +function stripPeerAndPatchSuffix(version: string): string { + if (!version.endsWith(")")) { + return version; + } - object[nameAtVersion(name, version)] = { - version, - dependencies: snapshot.dependencies, - }; + let open = 1; + for (let i = version.length - 2; i >= 0; i--) { + const char = version[i]; + if (char === "(") { + open--; + } else if (char === ")") { + open++; + } else if (open === 0) { + // First non-paren character (scanning from the right) that sits outside all suffix groups: + // everything after it is the peer/patch suffix. + return version.slice(0, i + 1); } } - return { - object, - type: "success", - }; + return version; } From 0356285dd286e9800f1b2f4e35e45621a1bef8e6 Mon Sep 17 00:00:00 2001 From: Andrew Stegmaier Date: Wed, 24 Jun 2026 15:29:27 -0500 Subject: [PATCH 07/15] Simplify special-cased handling of git dependencies --- .../src/__tests__/lockfile/lockfile.test.ts | 35 +++++++++---------- .../__tests__/lockfile/parsePnpmLock.test.ts | 23 ++++++++---- .../src/lockfile/parsePnpmLock.ts | 31 ++++++++-------- 3 files changed, 49 insertions(+), 40 deletions(-) diff --git a/packages/workspace-tools/src/__tests__/lockfile/lockfile.test.ts b/packages/workspace-tools/src/__tests__/lockfile/lockfile.test.ts index 923349210..73f87f0d0 100644 --- a/packages/workspace-tools/src/__tests__/lockfile/lockfile.test.ts +++ b/packages/workspace-tools/src/__tests__/lockfile/lockfile.test.ts @@ -146,27 +146,24 @@ describe("parseLockFile()", () => { expect(object["is-odd@3.0.1"].dependencies?.["is-number"]).toBe("6.0.0"); expect(Object.keys(object).some((key) => key.includes("patch_hash"))).toBe(false); }); - }); - - it("keeps a non-semver (git tarball) version verbatim (lockfileVersion 9.0)", async () => { - const packageRoot = setupFixture("basic-pnpm-9"); - const { object } = await parseLockFile(packageRoot); - - // lockfileVersion 9.0 encodes the github dependency as `is-positive@`. - const url = "https://codeload.github.com/kevva/is-positive/tar.gz/97edff6f525f192a3f83cea1944765f769ae2678"; - expect(object[`is-positive@${url}`]).toBeTruthy(); - expect(object[`is-positive@${url}`].version).toBe(url); - }); - it("parses the lockfileVersion 6.0 git dependency encoding", async () => { - const packageRoot = setupFixture("basic-pnpm-6"); - const { object } = await parseLockFile(packageRoot); + it("keeps a non-semver git dependency version verbatim, keyed by package name", async () => { + const packageRoot = setupFixture(fixtureName); + const { object } = await parseLockFile(packageRoot); - // lockfileVersion 6.0 encodes the github dependency as `github.com///` - // (no `@`), so the final path segment is treated as the version. - const ref = "97edff6f525f192a3f83cea1944765f769ae2678"; - expect(object[`github.com/kevva/is-positive@${ref}`]).toBeTruthy(); - expect(object[`github.com/kevva/is-positive@${ref}`].version).toBe(ref); + // Both lockfile versions key the parsed entry by the real package name (`is-positive`) with + // the git resolution descriptor preserved verbatim as the version. In 9.0 the name comes + // from the `name@` key; in 6.0 the key has no `@` so the name comes from the entry's + // `name` field and the whole key is preserved as the version. + const versionByFixture = { + "basic-pnpm-9": "https://codeload.github.com/kevva/is-positive/tar.gz/97edff6f525f192a3f83cea1944765f769ae2678", + "basic-pnpm-6": "github.com/kevva/is-positive/97edff6f525f192a3f83cea1944765f769ae2678", + }; + const version = versionByFixture[fixtureName]; + + expect(object[`is-positive@${version}`]).toBeTruthy(); + expect(object[`is-positive@${version}`].version).toBe(version); + }); }); }); }); diff --git a/packages/workspace-tools/src/__tests__/lockfile/parsePnpmLock.test.ts b/packages/workspace-tools/src/__tests__/lockfile/parsePnpmLock.test.ts index f2e5b5601..aaad06520 100644 --- a/packages/workspace-tools/src/__tests__/lockfile/parsePnpmLock.test.ts +++ b/packages/workspace-tools/src/__tests__/lockfile/parsePnpmLock.test.ts @@ -161,17 +161,28 @@ describe("parsePnpmLock", () => { }); }); - it("parses the git dependency encoding `github.com/owner/repo/ref`", () => { + it("preserves a git key verbatim as the version and takes the name from the entry", () => { + // 6.0 git dependency keys are not in `name@version` form (`github.com/owner/repo/`), so + // the key is kept as the version and the name is read from the entry's `name` field. + const key = "github.com/kevva/is-positive/97edff6f"; const lock: PnpmLockFile = { lockfileVersion: "6.0", - packages: { "github.com/kevva/is-positive/97edff6f": {} }, + packages: { [key]: { name: "is-positive", version: "3.1.0" } }, }; const { object } = parsePnpmLock(lock); - expect(object["github.com/kevva/is-positive@97edff6f"]).toEqual({ - version: "97edff6f", - dependencies: undefined, - }); + expect(object[`is-positive@${key}`]).toEqual({ version: key, dependencies: undefined }); + }); + + it("falls back to the key as the name when a no-`@` entry has no name field", () => { + const key = "github.com/kevva/is-positive/97edff6f"; + const lock: PnpmLockFile = { + lockfileVersion: "6.0", + packages: { [key]: {} }, + }; + + const { object } = parsePnpmLock(lock); + expect(object[`${key}@${key}`]).toEqual({ version: key, dependencies: undefined }); }); }); diff --git a/packages/workspace-tools/src/lockfile/parsePnpmLock.ts b/packages/workspace-tools/src/lockfile/parsePnpmLock.ts index 5943feb1e..d4766b5fe 100644 --- a/packages/workspace-tools/src/lockfile/parsePnpmLock.ts +++ b/packages/workspace-tools/src/lockfile/parsePnpmLock.ts @@ -16,7 +16,7 @@ export function parsePnpmLock(yaml: PnpmLockFile): ParsedLock { // `packages`. Either way the keys share the same `name@version(suffixes)` format. const entries = yaml.snapshots ?? yaml.packages; for (const [pkgSpec, snapshot] of Object.entries(entries ?? {})) { - const { name, version } = parsePackageKey(pkgSpec); + const { name, version } = parsePackageKey(pkgSpec, snapshot ?? {}); object[nameAtVersion(name, version)] = { version, dependencies: snapshot?.dependencies, @@ -43,30 +43,31 @@ export function parsePnpmLock(yaml: PnpmLockFile): ParsedLock { } /** - * Parse a lockfileVersion 6.0 / 9.0 package or snapshot key into its name and version. These keys - * look like `name@version` or `@scope/name@version`, optionally with a leading `/` (6.0) and - * trailing peer/patch suffixes, e.g.: + * Parse a lockfileVersion 6.0 / 9.0 package or snapshot key into its name and version. Keys may + * have a leading `/` (6.0) and trailing peer/patch suffixes, e.g.: * - `/react-dom@18.3.1(react@18.3.1)` * - `is-odd@3.0.1(patch_hash=...)` * - `@testing-library/react@16.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)` + * - `github.com/owner/repo/` (a 6.0 git dependency, which has no `name@version` form) * - * The name/version separator is the first `@` at index >= 1 (mirroring pnpm's `parse`), so the - * leading `@` of a scoped package is ignored and an `@` inside a non-semver version (e.g. a - * `git+ssh://git@host/...` URL) is not mistaken for the separator. + * The name/version separator is the first `@` after the package name, so scoped package names and + * non-semver versions containing `@` (e.g. a `git+ssh://git@host/...` URL) are handled correctly. + * + * This intentionally does not handle pnpm <= 5.x `/name/version` keys; those are parsed by the + * legacy branch in `parsePnpmLock`. + * + * @param key A pnpm 6/9 `packages` or `snapshots` map key. + * @param entry The corresponding lockfile entry; `entry.name` is the fallback name for keys with no + * `name@version` form (pnpm writes `name` onto the entry because it cannot be derived from the key). */ -function parsePackageKey(key: string): { name: string; version: string } { +function parsePackageKey(key: string, entry: { name?: string }): { name: string; version: string } { // 6.0 keys have a leading "/"; 9.0 keys do not. const base = key.startsWith("/") ? key.slice(1) : key; const separatorIndex = base.indexOf("@", 1); if (separatorIndex === -1) { - // The only 6.0/9.0 keys without an `@` are git shorthand deps encoded as - // `github.com/owner/repo/`. Keep this narrow: treat the last path segment as the version. - const slashIndex = base.lastIndexOf("/"); - if (slashIndex === -1) { - return { name: base, version: "" }; - } - return { name: base.slice(0, slashIndex), version: base.slice(slashIndex + 1) }; + // No `name@version` form (e.g. a git dependency): the key itself is the version. + return { name: entry.name ?? base, version: base }; } return { From 52259b1156a0f077b56f0f6e2d0460854c27dc4d Mon Sep 17 00:00:00 2001 From: Andrew Stegmaier Date: Wed, 24 Jun 2026 16:37:42 -0500 Subject: [PATCH 08/15] Commit public API changes to workspace-tools.api.md --- packages/workspace-tools/etc/workspace-tools.api.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/workspace-tools/etc/workspace-tools.api.md b/packages/workspace-tools/etc/workspace-tools.api.md index dab612d8e..a7f21b61f 100644 --- a/packages/workspace-tools/etc/workspace-tools.api.md +++ b/packages/workspace-tools/etc/workspace-tools.api.md @@ -652,10 +652,15 @@ type ParseRemoteBranchOptions = GitCommonOptions & { // @public export interface PnpmLockFile { - // (undocumented) - packages: { + lockfileVersion?: number | string; + packages?: { [name: string]: any; }; + snapshots?: { + [name: string]: { + dependencies?: Dependencies; + }; + }; } // @public (undocumented) From a02ac05dbb98e804051e4e108e614a3721fb0f28 Mon Sep 17 00:00:00 2001 From: Andrew Stegmaier Date: Wed, 24 Jun 2026 17:23:17 -0500 Subject: [PATCH 09/15] Strip peer suffixes from dependency versions. --- .../src/__tests__/lockfile/lockfile.test.ts | 11 +++++--- .../src/lockfile/parsePnpmLock.ts | 25 +++++++++++++++++-- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/packages/workspace-tools/src/__tests__/lockfile/lockfile.test.ts b/packages/workspace-tools/src/__tests__/lockfile/lockfile.test.ts index 73f87f0d0..6ee258906 100644 --- a/packages/workspace-tools/src/__tests__/lockfile/lockfile.test.ts +++ b/packages/workspace-tools/src/__tests__/lockfile/lockfile.test.ts @@ -88,6 +88,7 @@ describe("parseLockFile()", () => { // The parsed entry should preserve the resolved version. expect(which).toBe("which@2.0.2"); expect(parsedLockFile.object[which!].version).toBe("2.0.2"); + expect(parsedLockFile.object[which!].dependencies).toEqual({ isexe: "2.0.0" }); }); // The `basic-pnpm-6` (lockfileVersion 6.0) and `basic-pnpm-9` (lockfileVersion 9.0) fixtures @@ -131,9 +132,10 @@ describe("parseLockFile()", () => { const key = "@testing-library/react@16.0.1"; expect(object[key]).toBeTruthy(); expect(object[key].version).toBe("16.0.1"); - // The peer suffix is stripped from the parsed KEY, but dependency *values* are kept - // verbatim from the snapshot (they may still carry their own peer suffix). - expect(object[key].dependencies?.["react-dom"]).toBe("18.3.1(react@18.3.1)"); + // The peer suffix is stripped from both the parsed KEY and the dependency *values*, so the + // resolved graph stays self-consistent (every value, combined with its name, references an + // existing entry in `object`). + expect(object[key].dependencies?.["react-dom"]).toBe("18.3.1"); }); it("strips a patch_hash suffix from the version", async () => { @@ -156,7 +158,8 @@ describe("parseLockFile()", () => { // from the `name@` key; in 6.0 the key has no `@` so the name comes from the entry's // `name` field and the whole key is preserved as the version. const versionByFixture = { - "basic-pnpm-9": "https://codeload.github.com/kevva/is-positive/tar.gz/97edff6f525f192a3f83cea1944765f769ae2678", + "basic-pnpm-9": + "https://codeload.github.com/kevva/is-positive/tar.gz/97edff6f525f192a3f83cea1944765f769ae2678", "basic-pnpm-6": "github.com/kevva/is-positive/97edff6f525f192a3f83cea1944765f769ae2678", }; const version = versionByFixture[fixtureName]; diff --git a/packages/workspace-tools/src/lockfile/parsePnpmLock.ts b/packages/workspace-tools/src/lockfile/parsePnpmLock.ts index d4766b5fe..1ed518746 100644 --- a/packages/workspace-tools/src/lockfile/parsePnpmLock.ts +++ b/packages/workspace-tools/src/lockfile/parsePnpmLock.ts @@ -1,5 +1,5 @@ import { nameAtVersion } from "./nameAtVersion.js"; -import { type LockDependency, type ParsedLock, type PnpmLockFile } from "./types.js"; +import { type Dependencies, type LockDependency, type ParsedLock, type PnpmLockFile } from "./types.js"; export function parsePnpmLock(yaml: PnpmLockFile): ParsedLock { const object: { @@ -19,7 +19,7 @@ export function parsePnpmLock(yaml: PnpmLockFile): ParsedLock { const { name, version } = parsePackageKey(pkgSpec, snapshot ?? {}); object[nameAtVersion(name, version)] = { version, - dependencies: snapshot?.dependencies, + dependencies: stripDependencySuffixes(snapshot?.dependencies), }; } } else if (yaml?.packages) { @@ -76,6 +76,27 @@ function parsePackageKey(key: string, entry: { name?: string }): { name: string; }; } +/** + * Normalize the dependency-edge values of a lockfileVersion 6/9 snapshot so they line up with the + * peer/patch-stripped keys produced by `parsePackageKey`. pnpm records each edge's resolved version + * with the same peer/patch suffix it uses in the `packages`/`snapshots` keys (e.g. + * `react-dom: 18.3.1(react@18.3.1)`), while `parsePnpmLock` stores entries under the bare + * `name@version` key. Stripping the suffix here keeps the parsed graph self-consistent: every + * dependency value, combined with its name, references an existing entry in `object`. Values without + * a trailing suffix (the common case, plus `link:`/aliased specs) are returned unchanged. + */ +function stripDependencySuffixes(dependencies?: Dependencies): Dependencies | undefined { + if (!dependencies) { + return undefined; + } + + const stripped: Dependencies = {}; + for (const [name, version] of Object.entries(dependencies)) { + stripped[name] = stripPeerAndPatchSuffix(version); + } + return stripped; +} + /** * Remove the trailing peer-dependency and/or patch-hash suffix(es) from a pnpm version string, * e.g. `1.0.0(react@18.0.0)`, `1.0.0(patch_hash=abc)`, `1.0.0(patch_hash=abc)(react@18.0.0)`, or From eab8dcadde379fd458ca8a20fe1895b8153ddc2f Mon Sep 17 00:00:00 2001 From: Andrew Stegmaier Date: Wed, 24 Jun 2026 22:39:12 -0500 Subject: [PATCH 10/15] Also collect `importers` into parsed lockfile. --- .../src/__tests__/lockfile/lockfile.test.ts | 51 +++++++++++++++++++ .../src/lockfile/parsePnpmLock.ts | 42 ++++++++++++++- .../workspace-tools/src/lockfile/types.ts | 21 ++++++++ 3 files changed, 113 insertions(+), 1 deletion(-) diff --git a/packages/workspace-tools/src/__tests__/lockfile/lockfile.test.ts b/packages/workspace-tools/src/__tests__/lockfile/lockfile.test.ts index 6ee258906..54deb7ec4 100644 --- a/packages/workspace-tools/src/__tests__/lockfile/lockfile.test.ts +++ b/packages/workspace-tools/src/__tests__/lockfile/lockfile.test.ts @@ -3,6 +3,7 @@ import fs from "fs"; import path from "path"; import { setupFixture } from "../setupFixture.js"; import { parseLockFile } from "../../lockfile/parseLockFile.js"; +import { parsePnpmLock } from "../../lockfile/parsePnpmLock.js"; import { getPackageInfo } from "../../getPackageInfo.js"; const ERROR_MESSAGES = { @@ -168,5 +169,55 @@ describe("parseLockFile()", () => { expect(object[`is-positive@${version}`].version).toBe(version); }); }); + + describe("importers (workspace packages)", () => { + it("stores each importer under its raw path, stripping suffixes but keeping link: verbatim", () => { + const { object } = parsePnpmLock({ + lockfileVersion: "9.0", + importers: { + ".": { + devDependencies: { typescript: { specifier: "^4.2.3", version: "4.9.5" } }, + }, + "packages/package-a": { + dependencies: { + // External deps carry a peer/patch suffix that must be stripped to line up with + // the `name@version` keys parsed from `snapshots`. + "react-dom": { specifier: "^18.0.0", version: "18.3.1(react@18.3.1)" }, + "is-odd": { specifier: "3.0.1", version: "3.0.1(patch_hash=abc)" }, + // A sibling workspace package, recorded as a `link:` path relative to this importer. + "@scope/package-b": { specifier: "workspace:^", version: "link:../package-b" }, + }, + }, + "packages/package-b": {}, + }, + }); + + // Each importer is keyed by its unmodified path, with the path preserved as the version. + expect(object["."]).toEqual({ version: ".", dependencies: { typescript: "4.9.5" } }); + expect(object["packages/package-b"]).toEqual({ + version: "packages/package-b", + dependencies: {}, + }); + + // Peer/patch suffixes are stripped; the `link:` reference to a sibling workspace package is + // preserved verbatim so a consumer can resolve it against the `packages/package-b` key. + expect(object["packages/package-a"].dependencies).toEqual({ + "react-dom": "18.3.1", + "is-odd": "3.0.1", + "@scope/package-b": "link:../package-b", + }); + }); + + it("parses workspace importers from a 9.0 monorepo lockfile, keyed by path", async () => { + const packageRoot = setupFixture("monorepo-basic-pnpm"); + const { object } = await parseLockFile(packageRoot); + + // The root (".") and each nested workspace package appear under their lockfile importer path. + expect(object["."]).toBeTruthy(); + expect(object["packages/package-a"]).toBeTruthy(); + // External edges have their peer suffix stripped (`react-dom@19.2.4(react@19.2.4)`). + expect(object["packages/package-a"].dependencies?.["react-dom"]).toBe("19.2.4"); + }); + }); }); }); diff --git a/packages/workspace-tools/src/lockfile/parsePnpmLock.ts b/packages/workspace-tools/src/lockfile/parsePnpmLock.ts index 1ed518746..da206225c 100644 --- a/packages/workspace-tools/src/lockfile/parsePnpmLock.ts +++ b/packages/workspace-tools/src/lockfile/parsePnpmLock.ts @@ -1,5 +1,11 @@ import { nameAtVersion } from "./nameAtVersion.js"; -import { type Dependencies, type LockDependency, type ParsedLock, type PnpmLockFile } from "./types.js"; +import { + type Dependencies, + type LockDependency, + type ParsedLock, + type PnpmImporter, + type PnpmLockFile, +} from "./types.js"; export function parsePnpmLock(yaml: PnpmLockFile): ParsedLock { const object: { @@ -36,12 +42,46 @@ export function parsePnpmLock(yaml: PnpmLockFile): ParsedLock { } } + // Workspace packages live under `importers`, keyed by their path relative to the lockfile root + // (e.g. "." or "packages/foo"). They have no published `name@version`, so they are stored under the + // importer path verbatim, which lets a consumer resolve `link:` dependency values + // (references to sibling workspace packages) back to these keys. + for (const [importerPath, importer] of Object.entries(yaml?.importers ?? {})) { + object[importerPath] = { + version: importerPath, + dependencies: collectImporterDependencies(importer), + }; + } + return { object, type: "success", }; } +/** + * Flatten an importer's `dependencies` and `devDependencies` into a `name -> version` map. The + * resolved `version` is run through `stripPeerAndPatchSuffix` so external deps line up with the + * `name@version` keys parsed from `snapshots`; `link:` references to sibling workspace packages + * have no suffix and pass through unchanged for the consumer to resolve against the importer keys. + * `optionalDependencies` are omitted, mirroring the snapshot edges which only expose `dependencies`. + */ +function collectImporterDependencies(importer: PnpmImporter | undefined): Dependencies { + const dependencies: Dependencies = {}; + + for (const section of [importer?.dependencies, importer?.devDependencies]) { + for (const [name, spec] of Object.entries(section ?? {})) { + const version = typeof spec === "string" ? spec : spec?.version; + if (version === undefined) { + continue; + } + dependencies[name] = stripPeerAndPatchSuffix(version); + } + } + + return dependencies; +} + /** * Parse a lockfileVersion 6.0 / 9.0 package or snapshot key into its name and version. Keys may * have a leading `/` (6.0) and trailing peer/patch suffixes, e.g.: diff --git a/packages/workspace-tools/src/lockfile/types.ts b/packages/workspace-tools/src/lockfile/types.ts index 21f4adeb9..587ff8381 100644 --- a/packages/workspace-tools/src/lockfile/types.ts +++ b/packages/workspace-tools/src/lockfile/types.ts @@ -20,8 +20,29 @@ export interface PnpmLockFile { packages?: { [name: string]: any }; /** Dependency edges in lockfileVersion >= 9.0. */ snapshots?: { [name: string]: { dependencies?: Dependencies } }; + /** + * Workspace packages, keyed by their path relative to the lockfile root (`.`, `packages/foo`). + * Present in monorepo (and all 9.0) lockfiles. Unlike `packages`/`snapshots` these have no + * published `name@version`; their dependency edges are recorded as `{ specifier, version }`. + */ + importers?: { [importerPath: string]: PnpmImporter }; } +/** A single `importers` entry (one workspace package) in a pnpm lockfile. */ +export interface PnpmImporter { + dependencies?: PnpmImporterDependencies; + devDependencies?: PnpmImporterDependencies; + optionalDependencies?: PnpmImporterDependencies; +} + +/** + * Importer dependency edges. In lockfileVersion 6.0/9.0 each value is a `{ specifier, version }` + * object; older lockfiles use a bare version string. + */ +export type PnpmImporterDependencies = { + [name: string]: { specifier?: string; version?: string } | string; +}; + export interface NpmWorkspacesInfo { version: string; workspaces: { packages: string[] }; From 41f3a6f0eda2457c3439bcd176646dc672bf6d95 Mon Sep 17 00:00:00 2001 From: Andrew Stegmaier Date: Wed, 24 Jun 2026 23:01:27 -0500 Subject: [PATCH 11/15] More concise tests for importers, leveraging monorepo-basic-pnpm fixture --- .../monorepo-basic-pnpm/README.md | 6 ++ .../monorepo-basic-pnpm/package.json | 2 +- .../packages/package-a/package.json | 3 +- .../monorepo-basic-pnpm/pnpm-lock.yaml | 3 + .../src/__tests__/lockfile/lockfile.test.ts | 77 +++++++++---------- 5 files changed, 48 insertions(+), 43 deletions(-) diff --git a/packages/workspace-tools/src/__fixtures__/monorepo-basic-pnpm/README.md b/packages/workspace-tools/src/__fixtures__/monorepo-basic-pnpm/README.md index b2c023259..faf7c1d95 100644 --- a/packages/workspace-tools/src/__fixtures__/monorepo-basic-pnpm/README.md +++ b/packages/workspace-tools/src/__fixtures__/monorepo-basic-pnpm/README.md @@ -4,6 +4,12 @@ This fixture is intended to match the other `monorepo-basic-*` fixtures: - Same basic dependencies at root - `package-a` depends on `react` and `react-dom` (to introduce a `peerDependency`) +It additionally has one pnpm-specific dependency that the sibling `monorepo-basic-*` fixtures omit: + +- `package-a` depends on `package-b` via `workspace:^` (to introduce a `link:../package-b` workspace + reference in the lockfile `importers`, which `parsePnpmLock` must preserve verbatim). `link:` is a + pnpm-lockfile concept, so this edge is only meaningful for the pnpm fixture. + It should use the latest version of `pnpm` (may require changing to a newer Node version if updating the lock file). There should only be one `monorepo-basic-pnpm` fixture unless the way workspaces are specified changes. Lock file changes for different versions of `pnpm` should have separate fixtures. `pnpm` version to `lockfileVersion` mapping: https://github.com/pnpm/spec/blob/master/lockfile/README.md diff --git a/packages/workspace-tools/src/__fixtures__/monorepo-basic-pnpm/package.json b/packages/workspace-tools/src/__fixtures__/monorepo-basic-pnpm/package.json index ee5cdc651..c946cba35 100644 --- a/packages/workspace-tools/src/__fixtures__/monorepo-basic-pnpm/package.json +++ b/packages/workspace-tools/src/__fixtures__/monorepo-basic-pnpm/package.json @@ -9,6 +9,6 @@ "typescript": "^4.0.0" }, "engines": { - "pnpm": "10.x" + "pnpm": "11.x" } } diff --git a/packages/workspace-tools/src/__fixtures__/monorepo-basic-pnpm/packages/package-a/package.json b/packages/workspace-tools/src/__fixtures__/monorepo-basic-pnpm/packages/package-a/package.json index 1c3fb64f0..cc269342e 100644 --- a/packages/workspace-tools/src/__fixtures__/monorepo-basic-pnpm/packages/package-a/package.json +++ b/packages/workspace-tools/src/__fixtures__/monorepo-basic-pnpm/packages/package-a/package.json @@ -4,6 +4,7 @@ "version": "0.1.0", "dependencies": { "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-dom": "^19.0.0", + "package-b": "workspace:^" } } diff --git a/packages/workspace-tools/src/__fixtures__/monorepo-basic-pnpm/pnpm-lock.yaml b/packages/workspace-tools/src/__fixtures__/monorepo-basic-pnpm/pnpm-lock.yaml index 72404139c..7c0137ca1 100644 --- a/packages/workspace-tools/src/__fixtures__/monorepo-basic-pnpm/pnpm-lock.yaml +++ b/packages/workspace-tools/src/__fixtures__/monorepo-basic-pnpm/pnpm-lock.yaml @@ -19,6 +19,9 @@ importers: packages/package-a: dependencies: + package-b: + specifier: workspace:^ + version: link:../package-b react: specifier: ^19.0.0 version: 19.2.4 diff --git a/packages/workspace-tools/src/__tests__/lockfile/lockfile.test.ts b/packages/workspace-tools/src/__tests__/lockfile/lockfile.test.ts index 54deb7ec4..3a9cf8dfe 100644 --- a/packages/workspace-tools/src/__tests__/lockfile/lockfile.test.ts +++ b/packages/workspace-tools/src/__tests__/lockfile/lockfile.test.ts @@ -3,7 +3,6 @@ import fs from "fs"; import path from "path"; import { setupFixture } from "../setupFixture.js"; import { parseLockFile } from "../../lockfile/parseLockFile.js"; -import { parsePnpmLock } from "../../lockfile/parsePnpmLock.js"; import { getPackageInfo } from "../../getPackageInfo.js"; const ERROR_MESSAGES = { @@ -171,52 +170,48 @@ describe("parseLockFile()", () => { }); describe("importers (workspace packages)", () => { - it("stores each importer under its raw path, stripping suffixes but keeping link: verbatim", () => { - const { object } = parsePnpmLock({ - lockfileVersion: "9.0", - importers: { - ".": { - devDependencies: { typescript: { specifier: "^4.2.3", version: "4.9.5" } }, - }, - "packages/package-a": { - dependencies: { - // External deps carry a peer/patch suffix that must be stripped to line up with - // the `name@version` keys parsed from `snapshots`. - "react-dom": { specifier: "^18.0.0", version: "18.3.1(react@18.3.1)" }, - "is-odd": { specifier: "3.0.1", version: "3.0.1(patch_hash=abc)" }, - // A sibling workspace package, recorded as a `link:` path relative to this importer. - "@scope/package-b": { specifier: "workspace:^", version: "link:../package-b" }, - }, - }, - "packages/package-b": {}, - }, - }); - - // Each importer is keyed by its unmodified path, with the path preserved as the version. - expect(object["."]).toEqual({ version: ".", dependencies: { typescript: "4.9.5" } }); - expect(object["packages/package-b"]).toEqual({ - version: "packages/package-b", - dependencies: {}, - }); + // The real lockfileVersion 9.0 `monorepo-basic-pnpm` fixture exercises path-keyed importers, + // peer-suffix stripping, and a `link:` reference to a sibling workspace package (package-a -> + // package-b via `workspace:^`). + it("stores each importer under its raw path, stripping peer suffixes but keeping link: verbatim", async () => { + const packageRoot = setupFixture("monorepo-basic-pnpm"); + const { object } = await parseLockFile(packageRoot); - // Peer/patch suffixes are stripped; the `link:` reference to a sibling workspace package is - // preserved verbatim so a consumer can resolve it against the `packages/package-b` key. - expect(object["packages/package-a"].dependencies).toEqual({ - "react-dom": "18.3.1", - "is-odd": "3.0.1", - "@scope/package-b": "link:../package-b", + // Each importer is keyed by its unmodified path, with that path preserved as the version. + // The root and dependency-less workspace packages parse to empty dependency maps. + expect(object["."].version).toBe("."); + expect(object["individual"]).toEqual({ version: "individual", dependencies: {} }); + expect(object["packages/package-b"]).toEqual({ version: "packages/package-b", dependencies: {} }); + + const packageA = object["packages/package-a"]; + expect(packageA.version).toBe("packages/package-a"); + // `react-dom@19.2.4(react@19.2.4)` -> the peer suffix is stripped so it lines up with the + // `name@version` key parsed from `snapshots`; `react` is plain; the `workspace:^` dependency + // on `package-b` is recorded as `link:../package-b` and preserved verbatim so a consumer can + // resolve it back to the `packages/package-b` importer key. + expect(packageA.dependencies).toEqual({ + react: "19.2.4", + "react-dom": "19.2.4", + "package-b": "link:../package-b", }); }); - it("parses workspace importers from a 9.0 monorepo lockfile, keyed by path", async () => { - const packageRoot = setupFixture("monorepo-basic-pnpm"); + // The single-package `basic-pnpm-9` fixture's `.` importer carries the remaining real + // importer-value shapes (a patch_hash suffix, a nested peer suffix, and a git/tarball spec). + it("strips patch_hash and nested peer suffixes from `.` importer deps, keeping git specs verbatim", async () => { + const packageRoot = setupFixture("basic-pnpm-9"); const { object } = await parseLockFile(packageRoot); - // The root (".") and each nested workspace package appear under their lockfile importer path. - expect(object["."]).toBeTruthy(); - expect(object["packages/package-a"]).toBeTruthy(); - // External edges have their peer suffix stripped (`react-dom@19.2.4(react@19.2.4)`). - expect(object["packages/package-a"].dependencies?.["react-dom"]).toBe("19.2.4"); + const rootDependencies = object["."].dependencies; + // `is-odd@3.0.1(patch_hash=...)` -> patch suffix stripped. + expect(rootDependencies?.["is-odd"]).toBe("3.0.1"); + // `@testing-library/react@16.0.1(...)(react-dom@18.3.1(react@18.3.1))(...)` -> all (nested) + // peer suffixes stripped. + expect(rootDependencies?.["@testing-library/react"]).toBe("16.0.1"); + // A git/tarball resolution has no peer/patch suffix, so it is kept verbatim. + expect(rootDependencies?.["is-positive"]).toBe( + "https://codeload.github.com/kevva/is-positive/tar.gz/97edff6f525f192a3f83cea1944765f769ae2678" + ); }); }); }); From ed59abe2947a89545018c4bc9005d4a786f9f187 Mon Sep 17 00:00:00 2001 From: Andrew Stegmaier Date: Wed, 24 Jun 2026 23:01:55 -0500 Subject: [PATCH 12/15] Scope importers parsing under >6.0 lockfile parser. --- .../src/lockfile/parsePnpmLock.ts | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/packages/workspace-tools/src/lockfile/parsePnpmLock.ts b/packages/workspace-tools/src/lockfile/parsePnpmLock.ts index da206225c..e5a8b36b3 100644 --- a/packages/workspace-tools/src/lockfile/parsePnpmLock.ts +++ b/packages/workspace-tools/src/lockfile/parsePnpmLock.ts @@ -28,6 +28,18 @@ export function parsePnpmLock(yaml: PnpmLockFile): ParsedLock { dependencies: stripDependencySuffixes(snapshot?.dependencies), }; } + + // Workspace packages live under `importers`, keyed by their path relative to the lockfile root + // (e.g. "." or "packages/foo"). They have no published `name@version`, so they are stored under + // the importer path verbatim, which lets a consumer resolve `link:` dependency + // values (references to sibling workspace packages) back to these keys. This is scoped to the + // 6.0+ codepath so the legacy `< 6` parsing below stays untouched. + for (const [importerPath, importer] of Object.entries(yaml?.importers ?? {})) { + object[importerPath] = { + version: importerPath, + dependencies: collectImporterDependencies(importer), + }; + } } else if (yaml?.packages) { for (const [pkgSpec, snapshot] of Object.entries(yaml.packages)) { // TODO: handle file:foo.tgz syntax (rush uses this for internal package links) @@ -42,17 +54,6 @@ export function parsePnpmLock(yaml: PnpmLockFile): ParsedLock { } } - // Workspace packages live under `importers`, keyed by their path relative to the lockfile root - // (e.g. "." or "packages/foo"). They have no published `name@version`, so they are stored under the - // importer path verbatim, which lets a consumer resolve `link:` dependency values - // (references to sibling workspace packages) back to these keys. - for (const [importerPath, importer] of Object.entries(yaml?.importers ?? {})) { - object[importerPath] = { - version: importerPath, - dependencies: collectImporterDependencies(importer), - }; - } - return { object, type: "success", From bd1e4979b6b422d151f4e4d67f6218bff9a57a8b Mon Sep 17 00:00:00 2001 From: Andrew Stegmaier Date: Thu, 25 Jun 2026 14:21:31 -0500 Subject: [PATCH 13/15] Collect optionalDependencies from snapshots. --- .../__fixtures__/basic-pnpm-9/package.json | 5 +- .../__fixtures__/basic-pnpm-9/pnpm-lock.yaml | 64 +++++++++++++++++++ .../src/__tests__/lockfile/lockfile.test.ts | 28 ++++++++ .../src/lockfile/parsePnpmLock.ts | 42 +++++++----- .../workspace-tools/src/lockfile/types.ts | 4 +- 5 files changed, 125 insertions(+), 18 deletions(-) diff --git a/packages/workspace-tools/src/__fixtures__/basic-pnpm-9/package.json b/packages/workspace-tools/src/__fixtures__/basic-pnpm-9/package.json index f480b9e57..15f15fe68 100644 --- a/packages/workspace-tools/src/__fixtures__/basic-pnpm-9/package.json +++ b/packages/workspace-tools/src/__fixtures__/basic-pnpm-9/package.json @@ -4,7 +4,7 @@ "version": "0.1.0", "license": "MIT", "private": true, - "description": "derived from sveltejs/kit; includes edge-case deps (peer/nested-peer suffixes, patched dep, git/non-semver dep) for lockfile snapshot parsing tests", + "description": "derived from sveltejs/kit; includes edge-case deps (peer/nested-peer suffixes, patched dep, git/non-semver dep, and chokidar for optionalDependencies->fsevents) for lockfile snapshot parsing tests", "devDependencies": { "@changesets/cli": "^2.14.1", "@testing-library/react": "16.0.1", @@ -13,6 +13,7 @@ "prettier": "2.8.0", "react": "18.3.1", "react-dom": "18.3.1", - "typescript": "^4.2.3" + "typescript": "^4.2.3", + "chokidar": "3.6.0" } } diff --git a/packages/workspace-tools/src/__fixtures__/basic-pnpm-9/pnpm-lock.yaml b/packages/workspace-tools/src/__fixtures__/basic-pnpm-9/pnpm-lock.yaml index 5f1668537..dd5e72252 100644 --- a/packages/workspace-tools/src/__fixtures__/basic-pnpm-9/pnpm-lock.yaml +++ b/packages/workspace-tools/src/__fixtures__/basic-pnpm-9/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: '@testing-library/react': specifier: 16.0.1 version: 16.0.1(@testing-library/dom@10.4.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + chokidar: + specifier: 3.6.0 + version: 3.6.0 is-odd: specifier: 3.0.1 version: 3.0.1(patch_hash=97f62440b2a3a0dc38f52ca3991c1b62862197d25932422549ece06c0a3a6222) @@ -169,6 +172,10 @@ packages: resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} engines: {node: '>=10'} + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} @@ -186,6 +193,10 @@ packages: resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} engines: {node: '>=4'} + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -193,6 +204,10 @@ packages: chardet@2.2.0: resolution: {integrity: sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==} + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -247,6 +262,11 @@ packages: resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} engines: {node: '>=6 <7 || >=8'} + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -270,6 +290,10 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -347,6 +371,10 @@ packages: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + outdent@0.5.0: resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} @@ -427,6 +455,10 @@ packages: resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==} engines: {node: '>=6'} + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + resolve-from@5.0.0: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} @@ -717,6 +749,11 @@ snapshots: ansi-styles@5.2.0: {} + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.2 + argparse@1.0.10: dependencies: sprintf-js: 1.0.3 @@ -733,12 +770,26 @@ snapshots: dependencies: is-windows: 1.0.2 + binary-extensions@2.3.0: {} + braces@3.0.3: dependencies: fill-range: 7.1.1 chardet@2.2.0: {} + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -797,6 +848,9 @@ snapshots: jsonfile: 4.0.0 universalify: 0.1.2 + fsevents@2.3.3: + optional: true + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -820,6 +874,10 @@ snapshots: ignore@5.3.2: {} + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + is-extglob@2.1.1: {} is-glob@4.0.3: @@ -880,6 +938,8 @@ snapshots: mri@1.2.0: {} + normalize-path@3.0.0: {} + outdent@0.5.0: {} p-filter@2.1.0: @@ -945,6 +1005,10 @@ snapshots: pify: 4.0.1 strip-bom: 3.0.0 + readdirp@3.6.0: + dependencies: + picomatch: 2.3.2 + resolve-from@5.0.0: {} reusify@1.1.0: {} diff --git a/packages/workspace-tools/src/__tests__/lockfile/lockfile.test.ts b/packages/workspace-tools/src/__tests__/lockfile/lockfile.test.ts index 3a9cf8dfe..6f791a118 100644 --- a/packages/workspace-tools/src/__tests__/lockfile/lockfile.test.ts +++ b/packages/workspace-tools/src/__tests__/lockfile/lockfile.test.ts @@ -167,6 +167,34 @@ describe("parseLockFile()", () => { expect(object[`is-positive@${version}`]).toBeTruthy(); expect(object[`is-positive@${version}`].version).toBe(version); }); + + it("includes a snapshot's optionalDependencies among its dependency edges", async () => { + const packageRoot = setupFixture(fixtureName); + const { object } = await parseLockFile(packageRoot); + + // `jsonfile@4.0.0` declares only an optional dependency on `graceful-fs` (no regular + // dependencies). The yarn lockfile parser merges optionalDependencies into a package's + // resolved edges, so the pnpm parser must include them too, in both 6.0 and 9.0. + expect(object["jsonfile@4.0.0"].dependencies?.["graceful-fs"]).toBe("4.2.11"); + }); + }); + + it("merges a snapshot's dependencies and optionalDependencies (chokidar -> fsevents)", async () => { + const packageRoot = setupFixture("basic-pnpm-9"); + const { object } = await parseLockFile(packageRoot); + + // chokidar@3.6.0 has seven regular dependencies plus an optional `fsevents`. All must appear + // as edges, mirroring how the yarn parser merges `{...dependencies, ...optionalDependencies}`. + expect(object["chokidar@3.6.0"].dependencies).toEqual({ + anymatch: "3.1.3", + braces: "3.0.3", + "glob-parent": "5.1.2", + "is-binary-path": "2.1.0", + "is-glob": "4.0.3", + "normalize-path": "3.0.0", + readdirp: "3.6.0", + fsevents: "2.3.3", + }); }); describe("importers (workspace packages)", () => { diff --git a/packages/workspace-tools/src/lockfile/parsePnpmLock.ts b/packages/workspace-tools/src/lockfile/parsePnpmLock.ts index e5a8b36b3..55bc9bf17 100644 --- a/packages/workspace-tools/src/lockfile/parsePnpmLock.ts +++ b/packages/workspace-tools/src/lockfile/parsePnpmLock.ts @@ -25,7 +25,7 @@ export function parsePnpmLock(yaml: PnpmLockFile): ParsedLock { const { name, version } = parsePackageKey(pkgSpec, snapshot ?? {}); object[nameAtVersion(name, version)] = { version, - dependencies: stripDependencySuffixes(snapshot?.dependencies), + dependencies: collectSnapshotDependencies(snapshot), }; } @@ -65,7 +65,10 @@ export function parsePnpmLock(yaml: PnpmLockFile): ParsedLock { * resolved `version` is run through `stripPeerAndPatchSuffix` so external deps line up with the * `name@version` keys parsed from `snapshots`; `link:` references to sibling workspace packages * have no suffix and pass through unchanged for the consumer to resolve against the importer keys. - * `optionalDependencies` are omitted, mirroring the snapshot edges which only expose `dependencies`. + * `optionalDependencies` are omitted here, matching the yarn lockfile parser's package.json handling + * (workspace packages expose `dependencies` + `devDependencies` only). Note this differs from the + * snapshot handling above, which *does* include `optionalDependencies` — mirroring how yarn merges + * them for non-workspace packages. */ function collectImporterDependencies(importer: PnpmImporter | undefined): Dependencies { const dependencies: Dependencies = {}; @@ -118,24 +121,33 @@ function parsePackageKey(key: string, entry: { name?: string }): { name: string; } /** - * Normalize the dependency-edge values of a lockfileVersion 6/9 snapshot so they line up with the - * peer/patch-stripped keys produced by `parsePackageKey`. pnpm records each edge's resolved version - * with the same peer/patch suffix it uses in the `packages`/`snapshots` keys (e.g. - * `react-dom: 18.3.1(react@18.3.1)`), while `parsePnpmLock` stores entries under the bare - * `name@version` key. Stripping the suffix here keeps the parsed graph self-consistent: every - * dependency value, combined with its name, references an existing entry in `object`. Values without - * a trailing suffix (the common case, plus `link:`/aliased specs) are returned unchanged. + * Collect a lockfileVersion 6/9 snapshot's resolved dependency edges into a single `name -> version` + * map. Both `dependencies` and `optionalDependencies` are included (optional last, so it wins on the + * rare key collision) to match the yarn lockfile parser, which merges `{...dependencies, + * ...optionalDependencies}` for non-workspace packages. Each value is run through + * `stripPeerAndPatchSuffix` so it lines up with the bare `name@version` keys this parser produces. + * + * pnpm records the resolved version with the same peer/patch suffix it uses in the + * `packages`/`snapshots` keys (e.g. `react-dom: 18.3.1(react@18.3.1)`), while `parsePnpmLock` stores + * entries under the bare `name@version` key. Stripping the suffix here keeps the parsed graph + * self-consistent: every dependency value, combined with its name, references an existing entry in + * `object`. Values without a trailing suffix (the common case, plus `link:`/aliased specs) are + * returned unchanged. Returns `undefined` when the snapshot has no dependency edges at all. */ -function stripDependencySuffixes(dependencies?: Dependencies): Dependencies | undefined { - if (!dependencies) { +function collectSnapshotDependencies( + snapshot: { dependencies?: Dependencies; optionalDependencies?: Dependencies } | undefined +): Dependencies | undefined { + if (!snapshot?.dependencies && !snapshot?.optionalDependencies) { return undefined; } - const stripped: Dependencies = {}; - for (const [name, version] of Object.entries(dependencies)) { - stripped[name] = stripPeerAndPatchSuffix(version); + const collected: Dependencies = {}; + for (const section of [snapshot.dependencies, snapshot.optionalDependencies]) { + for (const [name, version] of Object.entries(section ?? {})) { + collected[name] = stripPeerAndPatchSuffix(version); + } } - return stripped; + return collected; } /** diff --git a/packages/workspace-tools/src/lockfile/types.ts b/packages/workspace-tools/src/lockfile/types.ts index 587ff8381..3045e1d48 100644 --- a/packages/workspace-tools/src/lockfile/types.ts +++ b/packages/workspace-tools/src/lockfile/types.ts @@ -19,7 +19,9 @@ export interface PnpmLockFile { /** Resolution metadata. In lockfileVersion <= 6.0 this also holds dependency edges. */ packages?: { [name: string]: any }; /** Dependency edges in lockfileVersion >= 9.0. */ - snapshots?: { [name: string]: { dependencies?: Dependencies } }; + snapshots?: { + [name: string]: { name?: string; dependencies?: Dependencies; optionalDependencies?: Dependencies }; + }; /** * Workspace packages, keyed by their path relative to the lockfile root (`.`, `packages/foo`). * Present in monorepo (and all 9.0) lockfiles. Unlike `packages`/`snapshots` these have no From 04e386c85ff825ac15e0cdfc8b22c1cbae4c2df6 Mon Sep 17 00:00:00 2001 From: Andrew Stegmaier Date: Thu, 25 Jun 2026 14:53:10 -0500 Subject: [PATCH 14/15] Change files --- .../change-8fddb88b-f48b-464b-bc44-380c5c510421.json | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 change/change-8fddb88b-f48b-464b-bc44-380c5c510421.json diff --git a/change/change-8fddb88b-f48b-464b-bc44-380c5c510421.json b/change/change-8fddb88b-f48b-464b-bc44-380c5c510421.json new file mode 100644 index 000000000..4a4be3306 --- /dev/null +++ b/change/change-8fddb88b-f48b-464b-bc44-380c5c510421.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "type": "minor", + "comment": "Add support for modern (v8+) pnpm lockfiles to parseLockFile", + "packageName": "workspace-tools", + "email": "ansteg@microsoft.com", + "dependentChangeType": "patch" + } + ] +} \ No newline at end of file From 16f0da2d4d0c85529a71d1f6af5abd9f54674b6f Mon Sep 17 00:00:00 2001 From: Andrew Stegmaier Date: Thu, 25 Jun 2026 15:30:14 -0500 Subject: [PATCH 15/15] Refresh API report, export importer types, and refine docs - Regenerate workspace-tools API report (was stale: missing importers, optionalDependencies, name, and the new PnpmImporter types). - Export PnpmImporter and PnpmImporterDependencies from the entry point (referenced by the public PnpmLockFile) to clear an ae-forgotten-export warning, and add concise JSDoc to their members. - Reword two PnpmLockFile comments to avoid TSDoc HTML-parsing warnings. - Align the change-file wording with the lockfileVersion 6.0/9.0 framing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...-8fddb88b-f48b-464b-bc44-380c5c510421.json | 2 +- .../etc/workspace-tools.api.md | 20 +++++++++++++++++++ packages/workspace-tools/src/index.ts | 2 ++ .../workspace-tools/src/lockfile/types.ts | 7 +++++-- 4 files changed, 28 insertions(+), 3 deletions(-) diff --git a/change/change-8fddb88b-f48b-464b-bc44-380c5c510421.json b/change/change-8fddb88b-f48b-464b-bc44-380c5c510421.json index 4a4be3306..be02932f0 100644 --- a/change/change-8fddb88b-f48b-464b-bc44-380c5c510421.json +++ b/change/change-8fddb88b-f48b-464b-bc44-380c5c510421.json @@ -2,7 +2,7 @@ "changes": [ { "type": "minor", - "comment": "Add support for modern (v8+) pnpm lockfiles to parseLockFile", + "comment": "Add support for pnpm lockfileVersion 6.0 (pnpm@8) and 9.0 (pnpm@9+) to parseLockFile", "packageName": "workspace-tools", "email": "ansteg@microsoft.com", "dependentChangeType": "patch" diff --git a/packages/workspace-tools/etc/workspace-tools.api.md b/packages/workspace-tools/etc/workspace-tools.api.md index a7f21b61f..ffc2e3338 100644 --- a/packages/workspace-tools/etc/workspace-tools.api.md +++ b/packages/workspace-tools/etc/workspace-tools.api.md @@ -650,15 +650,35 @@ type ParseRemoteBranchOptions = GitCommonOptions & { knownRemotes?: string[]; }; +// @public +export interface PnpmImporter { + dependencies?: PnpmImporterDependencies; + devDependencies?: PnpmImporterDependencies; + optionalDependencies?: PnpmImporterDependencies; +} + +// @public +export type PnpmImporterDependencies = { + [name: string]: { + specifier?: string; + version?: string; + } | string; +}; + // @public export interface PnpmLockFile { + importers?: { + [importerPath: string]: PnpmImporter; + }; lockfileVersion?: number | string; packages?: { [name: string]: any; }; snapshots?: { [name: string]: { + name?: string; dependencies?: Dependencies; + optionalDependencies?: Dependencies; }; }; } diff --git a/packages/workspace-tools/src/index.ts b/packages/workspace-tools/src/index.ts index 45595e8e0..ebd6962e5 100644 --- a/packages/workspace-tools/src/index.ts +++ b/packages/workspace-tools/src/index.ts @@ -20,6 +20,8 @@ export type { NpmSymlinkInfo, NpmWorkspacesInfo, ParsedLock, + PnpmImporter, + PnpmImporterDependencies, PnpmLockFile, } from "./lockfile/types.js"; export { findGitRoot, findPackageRoot, findProjectRoot, isChildOf, searchUp } from "./paths.js"; diff --git a/packages/workspace-tools/src/lockfile/types.ts b/packages/workspace-tools/src/lockfile/types.ts index 3045e1d48..158f433c7 100644 --- a/packages/workspace-tools/src/lockfile/types.ts +++ b/packages/workspace-tools/src/lockfile/types.ts @@ -16,9 +16,9 @@ export type ParsedLock = { export interface PnpmLockFile { /** Lockfile format version, e.g. `5.4`, `'6.0'` or `'9.0'`. */ lockfileVersion?: number | string; - /** Resolution metadata. In lockfileVersion <= 6.0 this also holds dependency edges. */ + /** Resolution metadata. In lockfileVersion 6.0 and earlier this also holds dependency edges. */ packages?: { [name: string]: any }; - /** Dependency edges in lockfileVersion >= 9.0. */ + /** Dependency edges in lockfileVersion 9.0 and later. */ snapshots?: { [name: string]: { name?: string; dependencies?: Dependencies; optionalDependencies?: Dependencies }; }; @@ -32,8 +32,11 @@ export interface PnpmLockFile { /** A single `importers` entry (one workspace package) in a pnpm lockfile. */ export interface PnpmImporter { + /** The workspace package's `dependencies`. */ dependencies?: PnpmImporterDependencies; + /** The workspace package's `devDependencies`. */ devDependencies?: PnpmImporterDependencies; + /** The workspace package's `optionalDependencies`. */ optionalDependencies?: PnpmImporterDependencies; }