Compare commits

..
Author SHA1 Message Date
Zoltan KochanandGitHub ea17c68df8 feat: support pnpm v12 (#288)
* feat: support pnpm v12

Approve the pinned @pnpm/exe install script for npm 12, add pnpm 12 coverage across supported runner platforms, and clarify that action-setup remains supported alongside pnpm/setup.

* refactor: use pnpm 12 native bootstrap

Install the plain pnpm v12 package as the single native bootstrap, remove the legacy @pnpm/exe lockfile and runtime path, and retain the standalone input as a no-op for workflow compatibility.

* fix: keep bootstrap updates on pnpm 12

Handle npm's array-shaped view output and resolve only pnpm 12 distribution tags.

* fix: use native bootstrap only for pnpm 12

Route pnpm 12-only versions and ranges through the plain pnpm native package while preserving the existing Node and @pnpm/exe bootstrap paths for older releases.

* style: remove updater narrative comments
2026-09-05 01:42:41 +02:00
16 changed files with 485 additions and 542 deletions
+33 -63
View File
@@ -27,6 +27,12 @@ jobs:
- name: 'ubuntu / v10.33.0'
os: ubuntu-latest
version: '10.33.0'
- name: 'ubuntu / v11.25.0'
os: ubuntu-latest
version: '11.25.0'
- name: 'ubuntu / v12.3.4'
os: ubuntu-latest
version: '12.3.4'
- name: 'ubuntu / v9.15.5 / custom-dest'
os: ubuntu-latest
version: '9.15.5'
@@ -34,9 +40,15 @@ jobs:
- name: 'macos / v9.15.5'
os: macos-latest
version: '9.15.5'
- name: 'macos / v12.3.4'
os: macos-latest
version: '12.3.4'
- name: 'windows / v9.15.5'
os: windows-latest
version: '9.15.5'
- name: 'windows / v12.3.4'
os: windows-latest
version: '12.3.4'
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
@@ -166,26 +178,42 @@ jobs:
shell: bash
standalone:
name: Standalone mode
name: 'Native pnpm bootstrap (npm 12)'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
persist-credentials: false
- name: Run the action
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
with:
node-version: 26
- name: Use npm 12
run: |
npm install --global npm@12.0.2
test "$(npm --version)" = "12.0.2"
- id: pnpm
name: Run the action
uses: ./
with:
version: 9.15.0
version: 12
standalone: true
- name: 'Test: pnpm works'
env:
PNPM_DEST: ${{ steps.pnpm.outputs.dest }}
run: |
set -e
test -x "$PNPM_DEST/node_modules/pnpm/pnpm"
test ! -d "$PNPM_DEST/node_modules/@pnpm/exe"
which pnpm
actual="$(pnpm --version)"
if [ "${actual}" != "9.15.0" ]; then
echo "Expected 9.15.0, got ${actual}"
if [ "${actual}" != "12.3.4" ]; then
echo "Expected 12.3.4, got ${actual}"
exit 1
fi
mkdir /tmp/test-standalone
@@ -329,61 +357,3 @@ jobs:
exit 1
fi
shell: bash
cache_lockfile_verification:
# The action caches pnpm's lockfile verification log, which lives in
# `cacheDir` — a directory pnpm resolves per platform and does not print.
# Guard the action's copy of that default against pnpm's own.
name: 'Lockfile verification cache (${{ matrix.os }}, cache=${{ matrix.cache }})'
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
# The log is cached independently of the store, so the store-less
# configuration has to reach it too.
- os: ubuntu-latest
cache: false
- os: ubuntu-latest
cache: true
- os: macos-latest
cache: true
- os: windows-latest
cache: true
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Set up a project with a supply-chain policy
# A one-minute floor activates the verification without holding back
# any version the install resolves.
run: |
echo '{"dependencies":{"is-odd":"3.0.1"}}' > package.json
printf 'packages:\n - .\nminimumReleaseAge: 1\n' > pnpm-workspace.yaml
shell: bash
- uses: ./
with:
version: '12.0.0-rc.4'
cache: ${{ matrix.cache }}
run_install: |
- args: [--no-frozen-lockfile]
- name: 'Test: pnpm wrote the verification log where the action looks for it'
run: |
set -e
case "$RUNNER_OS" in
Linux) cacheDir="${XDG_CACHE_HOME:-$HOME/.cache}/pnpm" ;;
macOS) cacheDir="$HOME/Library/Caches/pnpm" ;;
Windows) cacheDir="$(cygpath -u "$LOCALAPPDATA")/pnpm-cache" ;;
*) echo "Unexpected RUNNER_OS: $RUNNER_OS"; exit 1 ;;
esac
echo "Expecting the verification log in ${cacheDir}"
if [ ! -f "${cacheDir}/lockfile-verified.jsonl" ]; then
echo "No lockfile-verified.jsonl there; the action would cache nothing"
ls -la "${cacheDir}" || true
exit 1
fi
shell: bash
+10 -27
View File
@@ -1,9 +1,11 @@
> [!IMPORTANT]
> **This action has a successor: [`pnpm/setup`](https://github.com/pnpm/setup).**
> **This action supports pnpm v12 and earlier.**
>
> For pnpm v11 and newer, use [`pnpm/setup`](https://github.com/pnpm/setup) instead. It downloads pnpm's self-contained release binary (no Node.js or npm required) and can install a JavaScript runtime (Node.js, Bun, or Deno) in the same step, replacing `actions/setup-node`.
> For pnpm v11 and newer, [`pnpm/setup`](https://github.com/pnpm/setup) is also available. It downloads pnpm's self-contained release binary (no Node.js or npm required) and can install a JavaScript runtime (Node.js, Bun, or Deno) in the same step, replacing `actions/setup-node` when its feature set fits your workflow.
>
> `pnpm/action-setup` remains the action to use for installing pnpm v10 and older. See [Migrating to pnpm/setup](#migrating-to-pnpmsetup) below.
> You can continue using `pnpm/action-setup` with `actions/setup-node`, including for pnpm v11 and v12. See [Using pnpm/setup instead](#using-pnpmsetup-instead) below if you want a single action to install pnpm and a JavaScript runtime.
>
> `pnpm/setup` cannot install pnpm v11 on Intel macOS (`darwin-x64`), where no standalone pnpm v11 binary is published. On that platform, run `actions/setup-node` with Node.js 22.13 or newer before `pnpm/action-setup`, or upgrade to pnpm v12.
# Setup pnpm
@@ -13,7 +15,7 @@ Install pnpm package manager.
>
> The v2 version of this action [has stopped working](https://github.com/pnpm/action-setup/issues/135) with newer Node.js versions. Please, upgrade to the latest version to fix any issues.
## Migrating to pnpm/setup
## Using pnpm/setup instead
[`pnpm/setup`](https://github.com/pnpm/setup) installs pnpm v11+ as a native standalone executable and can install Node.js, Bun, or Deno in the same step, so a typical workflow no longer needs `actions/setup-node` or an explicit `pnpm install` step:
@@ -94,7 +96,7 @@ If `run_install` is a YAML string representation of either an object or an array
### `cache`
**Optional** (_type:_ `boolean`, _default:_ `false`) Whether to cache the pnpm store directory, keyed on the lockfile's content hash. On pnpm v11 and newer, the results of pnpm's lockfile verification are cached regardless of this input — see [Lockfile verification cache](#lockfile-verification-cache).
**Optional** (_type:_ `boolean`, _default:_ `false`) Whether to cache the pnpm store directory.
### `cache_dependency_path`
@@ -106,9 +108,9 @@ If `run_install` is a YAML string representation of either an object or an array
### `standalone`
**Optional** (_type:_ `boolean`, _default:_ `false`) When set to true, [@pnpm/exe](https://www.npmjs.com/package/@pnpm/exe), which is a Node.js bundled package, will be installed, enabling using `pnpm` without Node.js.
**Optional** (_type:_ `boolean`, _default:_ `false`) For pnpm v11 and earlier, install [@pnpm/exe](https://www.npmjs.com/package/@pnpm/exe), enabling pnpm to run without Node.js.
This is useful when you want to use a incompatible pair of Node.js and pnpm.
For pnpm v12, this input has no effect because the plain `pnpm` package already installs a standalone native executable.
## Outputs
@@ -208,25 +210,6 @@ jobs:
**Note:** You don't need to run `pnpm store prune` at the end; post-action has already taken care of that.
### Lockfile verification cache
pnpm v11 and newer check every lockfile entry before installing it — that each entry pins an integrity hash, that a pinned tarball URL matches the registry's own metadata, and, where configured, your `minimumReleaseAge` and `trustPolicy` policies. The verdict is memoized in a sub-kilobyte file, so an unchanged lockfile is not re-checked against the registry.
The action restores and saves that file on every run, independently of the `cache` input, because a job that starts without it pays for the check every time. On a repository with ~2000 lockfile entries and a warm store:
| | without the log | with it |
| --- | --- | --- |
| `minimumReleaseAge` + `trustPolicy` | 13.5s | 1.5s |
| no policies configured | 6.7s | 1.6s |
Reusing a verdict is not a weaker check: pnpm re-verifies whenever the lockfile content changes, and whenever the recorded policy is looser than the one now configured.
The log is uploaded as soon as the install that produced it finishes, not at the end of the job, so nothing the job runs afterwards — its tests, its build, any later step — can alter what other jobs restore. Dependency lifecycle scripts are the exception, since they run inside the install itself, ahead of the upload: pnpm refuses to run them unless the repository allow-lists the package through `allowBuilds`, and a package on that list can already run code in the job.
Before uploading, the action checks that the log grew the way an install grows it: every record that predated the install still there, and no more new records than installs it ran. A dependency's script that slips an extra record in is caught by that, and the log is not cached — the next job re-verifies, which costs seconds and nothing else.
A job that installs in a step of its own rather than through this action is saved at the end of the job instead, since that is the first moment the log is known to be complete. The record count cannot be bounded there, so only the "nothing disappeared" half of the check applies.
### Cache dependencies from multiple lockfiles
```yaml
@@ -256,7 +239,7 @@ jobs:
## Notes
This action does not set up Node.js. Use [actions/setup-node](https://github.com/actions/setup-node) yourself. If you are on pnpm v11 or newer, [`pnpm/setup`](https://github.com/pnpm/setup) can install pnpm and Node.js in a single step.
This action does not set up Node.js. Use [actions/setup-node](https://github.com/actions/setup-node) yourself. As an alternative for pnpm v11 or newer, [`pnpm/setup`](https://github.com/pnpm/setup) can install pnpm and Node.js in a single step.
## License
+2 -5
View File
@@ -16,10 +16,7 @@ inputs:
required: false
default: 'null'
cache:
description: |
Whether to cache the pnpm store directory, keyed on the lockfile's
content hash. On pnpm v11 and newer, the results of pnpm's lockfile
verification are cached either way — see the README.
description: Whether to cache the pnpm store directory
required: false
default: 'false'
cache_dependency_path:
@@ -31,7 +28,7 @@ inputs:
required: false
default: 'package.json'
standalone:
description: When set to true, @pnpm/exe, which is a Node.js bundled package, will be installed, enabling using pnpm without Node.js.
description: When set to true for pnpm v11 and earlier, install @pnpm/exe to use pnpm without Node.js. pnpm v12 uses the plain pnpm package's native executable.
required: false
default: 'false'
outputs:
+156 -157
View File
File diff suppressed because one or more lines are too long
+2
View File
@@ -14,10 +14,12 @@
"@types/expand-tilde": "^2.0.2",
"@types/node": "^22.0.0",
"expand-tilde": "^2.0.2",
"semver": "^7.8.5",
"yaml": "^2.3.4",
"zod": "^3.22.4"
},
"devDependencies": {
"@types/semver": "^7.8.0",
"esbuild": "^0.27.4",
"typescript": "^5.3.3"
}
+18
View File
@@ -29,6 +29,9 @@ importers:
expand-tilde:
specifier: ^2.0.2
version: 2.0.2
semver:
specifier: ^7.8.5
version: 7.8.5
yaml:
specifier: ^2.3.4
version: 2.7.0
@@ -36,6 +39,9 @@ importers:
specifier: ^3.22.4
version: 3.24.1
devDependencies:
'@types/semver':
specifier: ^7.8.0
version: 7.8.0
esbuild:
specifier: ^0.27.4
version: 0.27.4
@@ -293,6 +299,9 @@ packages:
'@types/node@22.19.11':
resolution: {integrity: sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==}
'@types/semver@7.8.0':
resolution: {integrity: sha512-1mAINjtQCXXeLkJ9ehXkwOcBpqtLxiVtKhpUf83DdRNdQKV0iXZpaHYqRr7nj+wvxuJzoAmAwXI+sCNMv1CzLQ==}
'@typespec/ts-http-runtime@0.3.0':
resolution: {integrity: sha512-sOx1PKSuFwnIl7z4RN0Ls7N9AQawmR9r66eI5rFCzLDIs8HTIYrIpH9QjYWoX0lkgGrkLxXhi4QnK7MizPRrIg==}
engines: {node: '>=20.0.0'}
@@ -463,6 +472,11 @@ packages:
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
hasBin: true
semver@7.8.5:
resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==}
engines: {node: '>=10'}
hasBin: true
strnum@2.1.1:
resolution: {integrity: sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==}
@@ -770,6 +784,8 @@ snapshots:
dependencies:
undici-types: 6.21.0
'@types/semver@7.8.0': {}
'@typespec/ts-http-runtime@0.3.0':
dependencies:
http-proxy-agent: 7.0.2
@@ -957,6 +973,8 @@ snapshots:
semver@6.3.1: {}
semver@7.8.5: {}
strnum@2.1.1: {}
tr46@0.0.3: {}
+14 -14
View File
@@ -1,9 +1,5 @@
#!/usr/bin/env node
// Usage: node scripts/update-bootstrap.mjs [version]
// If version is omitted, fetches the latest next-11 tag from npm.
// Regenerates the bootstrap lockfiles used by action-setup to install pnpm via npm.
import { execSync } from 'child_process'
import { mkdtempSync, rmSync, readFileSync, writeFileSync } from 'fs'
import { join } from 'path'
@@ -11,21 +7,24 @@ import { tmpdir } from 'os'
const BOOTSTRAP_DIR = new URL('../src/install-pnpm/bootstrap/', import.meta.url).pathname
const version = process.argv[2] || resolveLatestVersion()
const legacyVersion = process.argv[2] || resolveLatestVersion(11)
const nativeVersion = process.argv[3] || resolveLatestVersion(12)
console.log(`Updating bootstrap lockfiles to pnpm@${version} ...`)
console.log(`Updating bootstrap lockfiles to pnpm@${legacyVersion} and pnpm@${nativeVersion} ...`)
generateLock('pnpm-lock.json', { pnpm: version }, 'bootstrap-pnpm')
generateLock('exe-lock.json', { '@pnpm/exe': version }, 'bootstrap-exe')
generateLock('pnpm-lock.json', { pnpm: legacyVersion }, 'bootstrap-pnpm')
generateLock('exe-lock.json', { '@pnpm/exe': legacyVersion }, 'bootstrap-exe')
generateLock('native-lock.json', { pnpm: nativeVersion }, 'bootstrap-native-pnpm')
console.log('Done!')
function resolveLatestVersion() {
const json = execSync('npm view @pnpm/exe dist-tags --json', { encoding: 'utf8' })
const tags = JSON.parse(json)
const version = tags['next-11'] || tags['latest']
function resolveLatestVersion(major) {
const json = execSync('npm view pnpm dist-tags --json', { encoding: 'utf8' })
const parsed = JSON.parse(json)
const tags = Array.isArray(parsed) ? parsed[0] : parsed
const version = tags[`next-${major}`] || tags[`latest-${major}`]
if (!version) {
console.error('Could not determine latest pnpm version from npm dist-tags')
console.error(`Could not determine latest pnpm v${major} version from npm dist-tags`)
process.exit(1)
}
return version
@@ -40,7 +39,8 @@ function generateLock(filename, dependencies, name) {
const parsed = JSON.parse(lock)
parsed.name = name
writeFileSync(join(BOOTSTRAP_DIR, filename), JSON.stringify(parsed, null, 2) + '\n')
console.log(` ${filename} -> ${Object.values(dependencies)[0]}@${version}`)
const [packageName, packageVersion] = Object.entries(dependencies)[0]
console.log(` ${filename} -> ${packageName}@${packageVersion}`)
} finally {
rmSync(tmp, { recursive: true, force: true })
}
+3 -3
View File
@@ -4,10 +4,10 @@ import { Inputs } from '../inputs'
import { runRestoreCache } from './run'
export async function restoreCache(inputs: Inputs) {
if (!inputs.cache) return
if (!isFeatureAvailable()) {
if (inputs.cache) {
warning('Cache is not available, skipping cache restoration')
}
warning('Cache is not available, skipping cache restoration')
return
}
+6 -25
View File
@@ -4,35 +4,16 @@ import { getExecOutput } from '@actions/exec'
import { hashFiles } from '@actions/glob'
import os from 'os'
import { Inputs } from '../inputs'
import { restoreVerificationCache } from '../lockfile-verification-cache'
import { removeWindowsExtendedPathPrefix } from '../windows-path'
export async function runRestoreCache(inputs: Inputs) {
const fileHash = await hashFiles(inputs.cacheDependencyPath)
if (!fileHash) {
// Both caches are keyed on the lockfile, so neither can be restored
// without one. Only the store cache was asked for by name.
if (inputs.cache) {
throw new Error('Some specified paths were not resolved, unable to cache dependencies.')
}
return
}
// Restored whether or not the store is cached: the log is a fraction of a
// kilobyte, and without it pnpm re-checks every lockfile entry against the
// registry on each run — seconds even on a repository that configures no
// supply-chain policies.
await restoreVerificationCache(fileHash)
if (inputs.cache) {
await runRestoreStoreCache(fileHash)
}
}
async function runRestoreStoreCache(fileHash: string) {
const cachePath = await getCacheDirectory()
saveState('cache_path', cachePath)
const fileHash = await hashFiles(inputs.cacheDependencyPath)
if (!fileHash) {
throw new Error('Some specified paths were not resolved, unable to cache dependencies.')
}
const primaryKey = `pnpm-cache-${process.env.RUNNER_OS}-${os.arch()}-${fileHash}`
debug(`Primary key is ${primaryKey}`)
saveState('cache_primary_key', primaryKey)
@@ -61,7 +42,7 @@ async function runRestoreStoreCache(fileHash: string) {
async function getCacheDirectory() {
const { stdout } = await getExecOutput('pnpm store path --silent')
const cacheFolderPath = removeWindowsExtendedPathPrefix(stdout.trim())
const cacheFolderPath = stdout.trim()
debug(`Cache folder is set to "${cacheFolderPath}"`)
return cacheFolderPath
}
-6
View File
@@ -3,7 +3,6 @@ import restoreCache from './cache-restore'
import saveCache from './cache-save'
import getInputs, { Inputs } from './inputs'
import installPnpm from './install-pnpm'
import { saveVerificationCache } from './lockfile-verification-cache'
import setOutputs from './outputs'
import pnpmInstall from './pnpm-install'
import pruneStore from './pnpm-store-prune'
@@ -29,15 +28,10 @@ async function runMain() {
await restoreCache(inputs)
pnpmInstall(inputs)
await saveVerificationCache(inputs.runInstall.length)
}
async function runPost() {
const inputs = JSON.parse(getState('inputs')) as Inputs
// Covers a job that installs in a later step of its own; when this action
// installed, the log was already saved then. Runs before the prune because
// pnpm versions before pnpm/pnpm#13893 delete the log during one.
await saveVerificationCache()
pruneStore(inputs)
await saveCache(inputs)
}
+32 -32
View File
@@ -5,13 +5,13 @@
"packages": {
"": {
"dependencies": {
"@pnpm/exe": "11.19.0"
"@pnpm/exe": "11.25.0"
}
},
"node_modules/@pnpm/exe": {
"version": "11.19.0",
"resolved": "https://registry.npmjs.org/@pnpm/exe/-/exe-11.19.0.tgz",
"integrity": "sha512-P1mw8BZaNkEpttlyzKsxTj7PVs94bMB4cQ8pJhgbrCTSBP7xCzKS3VlOwIInS7aS5by6KAbfO5Vs9yK/ekugrg==",
"version": "11.25.0",
"resolved": "https://registry.npmjs.org/@pnpm/exe/-/exe-11.25.0.tgz",
"integrity": "sha512-X19R2uC+VAJ4UJQE9c/PCdOXbDaWnZtad7WUrTHBFQuMVaK9MAHbyO/WgahQCuRtTmJkLbxcWKKCk+JeTYFm/g==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
@@ -28,19 +28,19 @@
"url": "https://opencollective.com/pnpm"
},
"optionalDependencies": {
"@pnpm/linux-arm64": "11.19.0",
"@pnpm/linux-x64": "11.19.0",
"@pnpm/linuxstatic-arm64": "11.19.0",
"@pnpm/linuxstatic-x64": "11.19.0",
"@pnpm/macos-arm64": "11.19.0",
"@pnpm/win-arm64": "11.19.0",
"@pnpm/win-x64": "11.19.0"
"@pnpm/linux-arm64": "11.25.0",
"@pnpm/linux-x64": "11.25.0",
"@pnpm/linuxstatic-arm64": "11.25.0",
"@pnpm/linuxstatic-x64": "11.25.0",
"@pnpm/macos-arm64": "11.25.0",
"@pnpm/win-arm64": "11.25.0",
"@pnpm/win-x64": "11.25.0"
}
},
"node_modules/@pnpm/linux-arm64": {
"version": "11.19.0",
"resolved": "https://registry.npmjs.org/@pnpm/linux-arm64/-/linux-arm64-11.19.0.tgz",
"integrity": "sha512-c5AJqnsj0BMqMCOtctOzsCqyfY+afFe1kdM9F2FrNhYwtnQRMHoJy+51qfee4yuTPOVRyk3Yh1dwvyAadiznvA==",
"version": "11.25.0",
"resolved": "https://registry.npmjs.org/@pnpm/linux-arm64/-/linux-arm64-11.25.0.tgz",
"integrity": "sha512-ra8akqhzsbcOhKSJ9fFV8H+Oc9uGQAcp/XmIBEdT+8hPPoZCit3+RNCHmg8bLoNvpSLtRvZE0WFCMj7WyzxeBA==",
"cpu": [
"arm64"
],
@@ -54,9 +54,9 @@
}
},
"node_modules/@pnpm/linux-x64": {
"version": "11.19.0",
"resolved": "https://registry.npmjs.org/@pnpm/linux-x64/-/linux-x64-11.19.0.tgz",
"integrity": "sha512-KOKpA9o75SvmRQgWO+EqEpQzJg1b9uHk5y61PAx90sSpdYtKDPua2eBXBglzv8YK1xM3DsYXFEf7Scs+3HeDsA==",
"version": "11.25.0",
"resolved": "https://registry.npmjs.org/@pnpm/linux-x64/-/linux-x64-11.25.0.tgz",
"integrity": "sha512-pQl/L10diKQCbF73viRrtVU8qVWMTudUCSG1uZ+EWBNKtU9Sbxe6Q378diXDb1uTwSgUVMQoypxlC1MTzh7qvQ==",
"cpu": [
"x64"
],
@@ -70,9 +70,9 @@
}
},
"node_modules/@pnpm/linuxstatic-arm64": {
"version": "11.19.0",
"resolved": "https://registry.npmjs.org/@pnpm/linuxstatic-arm64/-/linuxstatic-arm64-11.19.0.tgz",
"integrity": "sha512-Ci9WxgCInZc3F43R6BfaHevi+dYqaI5CLi6421egFsK649eHvYPytvu+zABhTNlYGNJbD8j9w+AUYInz/TPsyQ==",
"version": "11.25.0",
"resolved": "https://registry.npmjs.org/@pnpm/linuxstatic-arm64/-/linuxstatic-arm64-11.25.0.tgz",
"integrity": "sha512-cYcrbB/xN1N6/5VUlFuHblb1gNyJgZv1CF5Pk1T0sHJxQMY4DFJV3OqHVAgahxyUfSTaQcVLvH1H2JFvd/+sgw==",
"cpu": [
"arm64"
],
@@ -89,9 +89,9 @@
}
},
"node_modules/@pnpm/linuxstatic-x64": {
"version": "11.19.0",
"resolved": "https://registry.npmjs.org/@pnpm/linuxstatic-x64/-/linuxstatic-x64-11.19.0.tgz",
"integrity": "sha512-GNMk96vNJ4uEWigekarjHofXNDFsTYRL0Mw2YlDkv/W+u0cn/UAVPHfd5aAfsf6Arel2ZQfxPUTXlpunFujWnA==",
"version": "11.25.0",
"resolved": "https://registry.npmjs.org/@pnpm/linuxstatic-x64/-/linuxstatic-x64-11.25.0.tgz",
"integrity": "sha512-HXDtaeQod16DDMUUcVLIMxvEc1jKn7IYsNPwbwAPs/Je/d3umRPJi3Ejjdn6e9qpXN6Oon+yvSsJr7B9oDt6KA==",
"cpu": [
"x64"
],
@@ -108,9 +108,9 @@
}
},
"node_modules/@pnpm/macos-arm64": {
"version": "11.19.0",
"resolved": "https://registry.npmjs.org/@pnpm/macos-arm64/-/macos-arm64-11.19.0.tgz",
"integrity": "sha512-I3Ee/GQlPxEOeBSzqxBNwcTHxVebjMiN2xLdTBS6OK2TAPzbWMIExjU5brxhr8rAk73p7mbv/a/2tFC/3gl6FA==",
"version": "11.25.0",
"resolved": "https://registry.npmjs.org/@pnpm/macos-arm64/-/macos-arm64-11.25.0.tgz",
"integrity": "sha512-m/eAgEqKhiSexGxWPHNXNnSRI0hudymg6K7LbrU2EoDs9IgJ28OKEz9mGxMzhkYBweEquR4k5teMNes/aToy9w==",
"cpu": [
"arm64"
],
@@ -124,9 +124,9 @@
}
},
"node_modules/@pnpm/win-arm64": {
"version": "11.19.0",
"resolved": "https://registry.npmjs.org/@pnpm/win-arm64/-/win-arm64-11.19.0.tgz",
"integrity": "sha512-rOiWuJ9wjaFY9ZDVPGVjpvjIQHZNB72rioxvuyzDKJ7dqKQ97VF27a4rmOtjhMB5u83cdDGEJ+OY+H1+TP+frw==",
"version": "11.25.0",
"resolved": "https://registry.npmjs.org/@pnpm/win-arm64/-/win-arm64-11.25.0.tgz",
"integrity": "sha512-oAeECbtZ+eJziaBmPUkwJM8Dx7KVQ5FO367AXTjD2HGfB5ob1Bcu5hoUF5RBTgDBiP9fRQ1u13uAEpqar/6NLA==",
"cpu": [
"arm64"
],
@@ -140,9 +140,9 @@
}
},
"node_modules/@pnpm/win-x64": {
"version": "11.19.0",
"resolved": "https://registry.npmjs.org/@pnpm/win-x64/-/win-x64-11.19.0.tgz",
"integrity": "sha512-l26XeTxoGxfU+mVcZ5jFdP9hNe5yrOlLPSkAlwwpXPI1iyyU4JUSxm6Jdyxu1UAU1FerD/BvLK2vrlR6d9ogag==",
"version": "11.25.0",
"resolved": "https://registry.npmjs.org/@pnpm/win-x64/-/win-x64-11.25.0.tgz",
"integrity": "sha512-8/n+wCc0a8TRYTMFqti93Vh0cscdJ25xfMyYSDdXrLUh2ccxSFuS+SE7cNPjd/nOXoFAJvdW0kwfbyRPLa16/w==",
"cpu": [
"x64"
],
+154
View File
@@ -0,0 +1,154 @@
{
"name": "bootstrap-native-pnpm",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"pnpm": "12.3.4"
}
},
"node_modules/@pnpm/exe.darwin-arm64": {
"version": "12.3.4",
"resolved": "https://registry.npmjs.org/@pnpm/exe.darwin-arm64/-/exe.darwin-arm64-12.3.4.tgz",
"integrity": "sha512-PAyUol8T1+/+ViOiXAt51ECA+QnfXCqz6foL4bW+LsoX0NcVd5XVEM2mRQu+LV4oc7uRz9zf9U0P+XFfuQeDAw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@pnpm/exe.darwin-x64": {
"version": "12.3.4",
"resolved": "https://registry.npmjs.org/@pnpm/exe.darwin-x64/-/exe.darwin-x64-12.3.4.tgz",
"integrity": "sha512-fxP9JCk0Cdye+ePuj+GJJLMUMTqHGWRdb1dtv4How876uQ2ehxvenpgiYAir/ceO9PsYUZkFTtyZdx+rRu5QOA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@pnpm/exe.linux-arm64": {
"version": "12.3.4",
"resolved": "https://registry.npmjs.org/@pnpm/exe.linux-arm64/-/exe.linux-arm64-12.3.4.tgz",
"integrity": "sha512-t71AVA7LRqiKTyZ5xMYaZc2n5DfdpMbfokZuiIOXHBOM03ECnF0t4iYwaBDqJgVjlKYUOwaF/bRQajGNA4cJ4w==",
"cpu": [
"arm64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@pnpm/exe.linux-arm64-musl": {
"version": "12.3.4",
"resolved": "https://registry.npmjs.org/@pnpm/exe.linux-arm64-musl/-/exe.linux-arm64-musl-12.3.4.tgz",
"integrity": "sha512-FBOt0/7ye6O6q4AllVV5QMviB6qE6fqkeczV/+MDWQsmo+QJrlfsh6X7CpH/tClVpBZEyIbjpUoT8bNhCYBxEg==",
"cpu": [
"arm64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@pnpm/exe.linux-x64": {
"version": "12.3.4",
"resolved": "https://registry.npmjs.org/@pnpm/exe.linux-x64/-/exe.linux-x64-12.3.4.tgz",
"integrity": "sha512-2ZqOlSPkfwX1h5cR+FPiWf8+F+2hZT/3TvhUK5sigHqwaQCIiq8R7CGxhndKs63JtcLi2a1Qpo+wX/EoyfjyJQ==",
"cpu": [
"x64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@pnpm/exe.linux-x64-musl": {
"version": "12.3.4",
"resolved": "https://registry.npmjs.org/@pnpm/exe.linux-x64-musl/-/exe.linux-x64-musl-12.3.4.tgz",
"integrity": "sha512-RPmk7Jb/aYaFvL2iyDN/AtMY+hUEsue732WmXpcuQ9tBpMnGyA5py7Z3+e+qmQaJ0zY/4ni9jJiyPBQHujmv6w==",
"cpu": [
"x64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@pnpm/exe.win32-arm64": {
"version": "12.3.4",
"resolved": "https://registry.npmjs.org/@pnpm/exe.win32-arm64/-/exe.win32-arm64-12.3.4.tgz",
"integrity": "sha512-ANyrHqyqco6SXBysUTRF74itDyyraea7IbFsKFdNXTjcFnfycTDx37EwuhdpPYFNSIh2JhUG4fByclsRfiHX7w==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@pnpm/exe.win32-x64": {
"version": "12.3.4",
"resolved": "https://registry.npmjs.org/@pnpm/exe.win32-x64/-/exe.win32-x64-12.3.4.tgz",
"integrity": "sha512-WH/KqBPY/hq2Tb7SgQltEZytimcjgKRaCRL/aM9CI0c67iKc5TVmHUhIiL3Ux9FB4bWn36i6XewUcScQI+zG8w==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/pnpm": {
"version": "12.3.4",
"resolved": "https://registry.npmjs.org/pnpm/-/pnpm-12.3.4.tgz",
"integrity": "sha512-lhqkH7B32joEpEHZ+OFevAyW2o73ELLrZ7+e58sGEOq9SPH9hfUc/+c4RnhfoPh8VqOocqHYk/hEZ0G1zORUVw==",
"hasInstallScript": true,
"license": "MIT",
"bin": {
"pn": "pn",
"pnpm": "pnpm",
"pnpx": "pnpx",
"pnx": "pnx"
},
"engines": {
"node": ">=18.*"
},
"optionalDependencies": {
"@pnpm/exe.darwin-arm64": "12.3.4",
"@pnpm/exe.darwin-x64": "12.3.4",
"@pnpm/exe.linux-arm64": "12.3.4",
"@pnpm/exe.linux-arm64-musl": "12.3.4",
"@pnpm/exe.linux-x64": "12.3.4",
"@pnpm/exe.linux-x64-musl": "12.3.4",
"@pnpm/exe.win32-arm64": "12.3.4",
"@pnpm/exe.win32-x64": "12.3.4"
}
}
}
}
+4 -4
View File
@@ -5,13 +5,13 @@
"packages": {
"": {
"dependencies": {
"pnpm": "11.19.0"
"pnpm": "11.25.0"
}
},
"node_modules/pnpm": {
"version": "11.19.0",
"resolved": "https://registry.npmjs.org/pnpm/-/pnpm-11.19.0.tgz",
"integrity": "sha512-eIHz7VkNRyxKlV4riLISF5ERYGbcyIy8o4SeybYPG7qm0syyIfqR2k4cZb7yvL43k2Wup6xTnHv4be3DobItzg==",
"version": "11.25.0",
"resolved": "https://registry.npmjs.org/pnpm/-/pnpm-11.25.0.tgz",
"integrity": "sha512-XN6SW08HX3Jetx+64YpC/+eEUkeJ8ZthxzHLhyHsKKruFg4BqNWvT+2ypCzb8wDv4j2zVrDUoXtNY+EfirfJVg==",
"license": "MIT",
"bin": {
"pn": "bin/pnpm.mjs",
+51 -23
View File
@@ -6,11 +6,28 @@ import path from 'path'
import util from 'util'
import { Inputs } from '../inputs'
import { parse as parseYaml } from 'yaml'
import { satisfies, subset, validRange } from 'semver'
import pnpmLock from './bootstrap/pnpm-lock.json'
import exeLock from './bootstrap/exe-lock.json'
import nativeLock from './bootstrap/native-lock.json'
const BOOTSTRAP_PNPM_PACKAGE_JSON = JSON.stringify({ private: true, dependencies: { pnpm: pnpmLock.packages['node_modules/pnpm'].version } })
const BOOTSTRAP_EXE_PACKAGE_JSON = JSON.stringify({ private: true, dependencies: { '@pnpm/exe': exeLock.packages['node_modules/@pnpm/exe'].version } })
const bootstrapPnpmVersion = pnpmLock.packages['node_modules/pnpm'].version
const bootstrapExeVersion = exeLock.packages['node_modules/@pnpm/exe'].version
const bootstrapNativeVersion = nativeLock.packages['node_modules/pnpm'].version
const BOOTSTRAP_PNPM_PACKAGE_JSON = JSON.stringify({
private: true,
dependencies: { pnpm: bootstrapPnpmVersion },
})
const BOOTSTRAP_EXE_PACKAGE_JSON = JSON.stringify({
private: true,
dependencies: { '@pnpm/exe': bootstrapExeVersion },
allowScripts: { [`@pnpm/exe@${bootstrapExeVersion}`]: true },
})
const BOOTSTRAP_NATIVE_PACKAGE_JSON = JSON.stringify({
private: true,
dependencies: { pnpm: bootstrapNativeVersion },
allowScripts: { [`pnpm@${bootstrapNativeVersion}`]: true },
})
export interface SelfInstallerResult {
exitCode: number
@@ -19,18 +36,20 @@ export interface SelfInstallerResult {
export async function runSelfInstaller(inputs: Inputs): Promise<SelfInstallerResult> {
const { version, dest, packageJsonFile } = inputs
// pnpm v11 requires Node >= 22.13; use standalone (exe) bootstrap which
// bundles its own Node.js when the system Node is too old
const systemNode = await getSystemNodeVersion()
const standalone = inputs.standalone || systemNode.major < 22 || (systemNode.major === 22 && systemNode.minor < 13)
const targetVersion = readTargetVersion({ version, packageJsonFile })
const native = targetsPnpm12(targetVersion)
let standalone = false
if (!native) {
const systemNode = await getSystemNodeVersion()
standalone = inputs.standalone || systemNode.major < 22 || (systemNode.major === 22 && systemNode.minor < 13)
}
// Install bootstrap pnpm via npm (integrity verified by committed lockfile)
await rm(dest, { recursive: true, force: true })
await mkdir(dest, { recursive: true })
const lockfile = standalone ? exeLock : pnpmLock
const packageJson = standalone ? BOOTSTRAP_EXE_PACKAGE_JSON : BOOTSTRAP_PNPM_PACKAGE_JSON
const lockfile = native ? nativeLock : standalone ? exeLock : pnpmLock
const packageJson = native ? BOOTSTRAP_NATIVE_PACKAGE_JSON : standalone ? BOOTSTRAP_EXE_PACKAGE_JSON : BOOTSTRAP_PNPM_PACKAGE_JSON
await writeFile(path.join(dest, 'package.json'), packageJson)
await writeFile(path.join(dest, 'package-lock.json'), JSON.stringify(lockfile))
@@ -53,9 +72,6 @@ export async function runSelfInstaller(inputs: Inputs): Promise<SelfInstallerRes
return { exitCode: npmExitCode, binDest: path.join(dest, 'node_modules', '.bin') }
}
// On Windows with standalone mode, npm's .bin shims can't properly
// execute the extensionless @pnpm/exe native binaries. Add the
// @pnpm/exe directory directly to PATH so pnpm.exe is found natively.
const pnpmHome = standalone && process.platform === 'win32'
? path.join(dest, 'node_modules', '@pnpm', 'exe')
: path.join(dest, 'node_modules', '.bin')
@@ -73,22 +89,28 @@ export async function runSelfInstaller(inputs: Inputs): Promise<SelfInstallerRes
const pnpmBinLink = path.join(dest, 'node_modules', '.bin', 'pnpm')
if (!existsSync(pnpmBinLink)) {
await mkdir(path.join(dest, 'node_modules', '.bin'), { recursive: true })
const target = standalone
? path.join('..', '@pnpm', 'exe', 'pnpm')
: path.join('..', 'pnpm', 'bin', 'pnpm.mjs')
const target = native
? path.join('..', 'pnpm', 'pnpm')
: standalone
? path.join('..', '@pnpm', 'exe', 'pnpm')
: path.join('..', 'pnpm', 'bin', 'pnpm.mjs')
await symlink(target, pnpmBinLink)
}
}
const bootstrapPnpm = standalone
? path.join(dest, 'node_modules', '@pnpm', 'exe', process.platform === 'win32' ? 'pnpm.exe' : 'pnpm')
: path.join(dest, 'node_modules', 'pnpm', 'bin', 'pnpm.mjs')
const bootstrapPnpm = native
? path.join(dest, 'node_modules', 'pnpm', process.platform === 'win32' ? 'pnpm.exe' : 'pnpm')
: standalone
? path.join(dest, 'node_modules', '@pnpm', 'exe', process.platform === 'win32' ? 'pnpm.exe' : 'pnpm')
: path.join(dest, 'node_modules', 'pnpm', 'bin', 'pnpm.mjs')
const bootstrapVersion = native ? bootstrapNativeVersion : standalone ? bootstrapExeVersion : bootstrapPnpmVersion
// Self-update the bootstrap to the requested pnpm version. readTargetVersion
// either returns a value or throws, so this always runs.
const targetVersion = readTargetVersion({ version, packageJsonFile })
const cmd = standalone ? bootstrapPnpm : process.execPath
const args = standalone ? ['self-update', targetVersion] : [bootstrapPnpm, 'self-update', targetVersion]
if (targetVersion === bootstrapVersion || (native && satisfies(bootstrapVersion, targetVersion))) {
return { exitCode: 0, binDest: pnpmHome }
}
const cmd = native || standalone ? bootstrapPnpm : process.execPath
const args = native || standalone ? ['self-update', targetVersion] : [bootstrapPnpm, 'self-update', targetVersion]
const exitCode = await runCommand(cmd, args, { cwd: dest })
if (exitCode !== 0) {
return { exitCode, binDest: pnpmHome }
@@ -179,6 +201,12 @@ Please specify it by one of the following ways:
- in the package.json with the key "devEngines.packageManager"`)
}
function targetsPnpm12(version: string): boolean {
if (version === 'latest-12' || version === 'next-12') return true
const range = validRange(version)
return range !== null && subset(range, '>=12.0.0 <13.0.0')
}
function getSystemNodeVersion(): Promise<{ major: number; minor: number }> {
return new Promise((resolve) => {
const cp = spawn('node', ['--version'], { stdio: ['pipe', 'pipe', 'pipe'], shell: process.platform === 'win32' })
-164
View File
@@ -1,164 +0,0 @@
import { restoreCache, saveCache } from '@actions/cache'
import { debug, getState, info, saveState, warning } from '@actions/core'
import { getExecOutput } from '@actions/exec'
import { existsSync, readFileSync } from 'fs'
import os from 'os'
import path from 'path'
import { removeWindowsExtendedPathPrefix } from '../windows-path'
/**
* Where pnpm v11+ memoizes which lockfile passed which supply-chain policies.
* A job without it re-checks every lockfile entry against the registry, which
* on a large repository costs more than the install.
*/
const VERIFICATION_CACHE_FILE = 'lockfile-verified.jsonl'
const PATH_STATE = 'lockfile_verification_cache_path'
const KEY_STATE = 'lockfile_verification_cache_key'
const STORED_STATE = 'lockfile_verification_cache_stored'
/**
* Where the log lives and under which key it belongs in the cache. Held in
* memory as well as in the action's state because the main and post steps run
* as separate processes, and state written by one is only readable by the
* other.
*/
let target: { cacheFilePath: string, key: string } | undefined
/** Whether this process already restored or saved the log. */
let stored = false
/** The log's records as they stood before the install ran. */
let recordsBeforeInstall: string[] | undefined
/**
* The verdict is only valid for the exact lockfile content it was recorded
* for, so this cache is keyed on the same lockfile hash as the store cache
* but restored without prefix fallback: an older entry could never be used.
*/
export async function restoreVerificationCache(lockfileHash: string): Promise<void> {
try {
const cacheFilePath = path.join(await getPnpmCacheDirectory(), VERIFICATION_CACHE_FILE)
const key = `pnpm-lockfile-verified-${process.env.RUNNER_OS}-${os.arch()}-${lockfileHash}`
target = { cacheFilePath, key }
saveState(PATH_STATE, cacheFilePath)
saveState(KEY_STATE, key)
debug(`Lockfile verification cache path is ${cacheFilePath}, key is ${key}`)
const restoredKey = await restoreCache([cacheFilePath], key)
recordsBeforeInstall = readRecords(cacheFilePath)
if (!restoredKey) {
info('Lockfile verification cache is not found')
return
}
stored = true
saveState(STORED_STATE, 'true')
info(`Lockfile verification cache restored from key: ${restoredKey}`)
} catch (error) {
// The gate only costs time, never correctness — a job that cannot reuse
// a past verdict re-verifies and moves on.
warning(`Failed to restore the lockfile verification cache: ${(error as Error).message}`)
}
}
/**
* Uploaded as soon as the install that produced the log finishes, rather than
* at the end of the job: whatever a job runs after installing can rewrite the
* log on disk, and the job's own cache write would then publish that for later
* jobs to trust. Lifecycle scripts of the installed packages stay inside the
* window — they run during the install — but pnpm only runs those the
* repository has allow-listed, and `expectedNewRecords` catches what they
* append.
*
* Safe to call more than once; the second call is a no-op.
*/
export async function saveVerificationCache(expectedNewRecords = Infinity): Promise<void> {
if (stored || getState(STORED_STATE) === 'true') return
const cacheFilePath = target?.cacheFilePath ?? getState(PATH_STATE)
const key = target?.key ?? getState(KEY_STATE)
if (!cacheFilePath || !key || !existsSync(cacheFilePath)) return
if (!onlyGrewAsExpected(cacheFilePath, expectedNewRecords)) return
try {
const cacheId = await saveCache([cacheFilePath], key)
if (cacheId === -1) return
stored = true
saveState(STORED_STATE, 'true')
info(`Lockfile verification cache saved with the key: ${key}`)
} catch (error) {
warning(`Failed to save the lockfile verification cache: ${(error as Error).message}`)
}
}
/**
* An install appends its own verdict and leaves every earlier record in place.
* Anything else — a record the install did not write, or an earlier one gone —
* means something other than pnpm's verification wrote to the log, and
* uploading it would hand that to every later job. pnpm compacting the log
* (past a thousand records) lands here too, at the cost of one re-verification.
*/
function onlyGrewAsExpected(cacheFilePath: string, expectedNewRecords: number): boolean {
const before = recordsBeforeInstall
if (before === undefined) return true
const after = readRecords(cacheFilePath)
if (after === undefined) return false
if (!before.every((record, index) => after[index] === record)) {
warning(
'Records that predate the install are missing from the lockfile verification log; not caching it.'
)
return false
}
const added = after.length - before.length
if (added > expectedNewRecords) {
warning(
`The lockfile verification log gained ${added} records during the install, expected at most ${expectedNewRecords}; not caching it.`
)
return false
}
return true
}
function readRecords(cacheFilePath: string): string[] | undefined {
try {
return readFileSync(cacheFilePath, 'utf8').split('\n').filter(Boolean)
} catch {
return undefined
}
}
async function getPnpmCacheDirectory(): Promise<string> {
const { stdout } = await getExecOutput('pnpm config get cacheDir', undefined, {
silent: true,
ignoreReturnCode: true,
})
const configured = stdout.trim()
// `pnpm config get` reports settings, not defaults: an unset `cacheDir`
// prints `undefined` and the default has to be derived here.
if (configured && configured !== 'undefined') {
return removeWindowsExtendedPathPrefix(configured)
}
return defaultPnpmCacheDirectory()
}
/** Mirrors pnpm's own `cacheDir` default. */
function defaultPnpmCacheDirectory(): string {
const { XDG_CACHE_HOME, LOCALAPPDATA } = process.env
if (XDG_CACHE_HOME) return path.join(XDG_CACHE_HOME, 'pnpm')
const homeDir = os.homedir()
switch (process.platform) {
case 'darwin':
return path.join(homeDir, 'Library', 'Caches', 'pnpm')
case 'win32':
return LOCALAPPDATA ? path.join(LOCALAPPDATA, 'pnpm-cache') : path.join(homeDir, '.pnpm-cache')
default:
return path.join(homeDir, '.cache', 'pnpm')
}
}
-19
View File
@@ -1,19 +0,0 @@
/**
* pnpm may report an extended-length path on Windows. The `?` in that prefix
* is interpreted as a wildcard by `@actions/cache`, which rejects it as a glob
* in the root segment. Cache APIs do not need the extended-length form, so
* convert it back to a regular drive or UNC path.
*/
export function removeWindowsExtendedPathPrefix(cachePath: string): string {
const extendedPathPrefix = '\\\\?\\'
if (!cachePath.startsWith(extendedPathPrefix)) return cachePath
const pathWithoutPrefix = cachePath.slice(extendedPathPrefix.length)
const uncPrefix = 'UNC\\'
if (pathWithoutPrefix.toUpperCase().startsWith(uncPrefix)) {
return `\\\\${pathWithoutPrefix.slice(uncPrefix.length)}`
}
return pathWithoutPrefix
}
export default removeWindowsExtendedPathPrefix