Skip to content

Commit

Permalink
Update pnpm version detection logic (#11445)
Browse files Browse the repository at this point in the history
The code we use to detect the version of `pnpm` based on lockfiles is hard to follow. It doesn't pick and executable, it instead of *sometimes* overrides the `PATH` for these utilities with a prepended alternate path. 

This means that there is no one single place where we definitively specify which version of `pnpm` that is used, so much as *hope* that the `PATH` has the correct version on it. I don't know that we *can* specify an executable outside of the build container, and it's unclear, since this is a package, whether this utility is used outside of a build container setting.

In this pull request I cleaned up the code and updated the logic to include the `pnpm 7` and `pnpm 8` changes requested, with no changes yet to implement `pnpm 9`.

It's possible that we want to rewrite this detection logic from scratch to avoid this roundabout way of "finding" the executable, but I haven't done that here.
  • Loading branch information
erikareads committed May 11, 2024
1 parent 6a8a849 commit 5014b1e
Show file tree
Hide file tree
Showing 4 changed files with 394 additions and 116 deletions.
6 changes: 6 additions & 0 deletions .changeset/cold-boxes-tan.md
@@ -0,0 +1,6 @@
---
'@vercel/build-utils': minor
---

Update pnpm version detection logic
Add support for pnpm 9
260 changes: 178 additions & 82 deletions packages/build-utils/src/fs/run-user-scripts.ts
Expand Up @@ -546,8 +546,7 @@ export function getEnvForPackageManager({
detectedLockfile,
detectedPackageManager,
path: newPath,
yarnNodeLinker,
} = getPathForPackageManager({
} = getPathOverrideForPackageManager({
cliType,
lockfileVersion,
nodeVersion,
Expand All @@ -558,36 +557,172 @@ export function getEnvForPackageManager({
...env,
};

if (newPath) {
const alreadyInPath = (newPath: string) => {
const oldPath = env.PATH ?? '';
return oldPath.split(path.delimiter).includes(newPath);
};

if (newPath && !alreadyInPath(newPath)) {
// Ensure that the binaries of the detected package manager are at the
// beginning of the `$PATH`.
const oldPath = env.PATH + '';
newEnv.PATH = `${newPath}${path.delimiter}${oldPath}`;
}

if (yarnNodeLinker) {
newEnv.YARN_NODE_LINKER = yarnNodeLinker;
}

if (detectedLockfile && detectedPackageManager) {
// For pnpm we also show the version of the lockfile we found
const versionString =
cliType === 'pnpm' ? `version ${lockfileVersion} ` : '';
if (detectedLockfile && detectedPackageManager) {
// For pnpm we also show the version of the lockfile we found
const versionString =
cliType === 'pnpm' ? `version ${lockfileVersion} ` : '';

console.log(
`Detected \`${detectedLockfile}\` ${versionString}generated by ${detectedPackageManager}`
);

if (cliType === 'bun') {
console.warn(
'Warning: Bun is used as a package manager at build time only, not at runtime with Functions'
console.log(
`Detected \`${detectedLockfile}\` ${versionString}generated by ${detectedPackageManager}`
);

if (cliType === 'bun') {
console.warn(
'Warning: Bun is used as a package manager at build time only, not at runtime with Functions'
);
}
}
}

if (cliType === 'yarn' && !env.YARN_NODE_LINKER) {
newEnv.YARN_NODE_LINKER = 'node-modules';
}

return newEnv;
}

type DetectedPnpmVersion =
| 'not found'
| 'pnpm 7'
| 'pnpm 8'
| 'pnpm 9'
| 'corepack_enabled';

function detectPnpmVersion(
lockfileVersion: number | undefined,
corepackEnabled: boolean
): DetectedPnpmVersion {
switch (true) {
case corepackEnabled:
return 'corepack_enabled';
case lockfileVersion === undefined:
return 'not found';
case lockfileVersion === 5.3 || lockfileVersion === 5.4:
return 'pnpm 7';
case lockfileVersion === 6.0 || lockfileVersion === 6.1:
return 'pnpm 8';
case lockfileVersion === 7.0 || lockfileVersion === 9.0:
return 'pnpm 9';
default:
return 'not found';
}
}

function shouldUseNpm7(
lockfileVersion: number | undefined,
nodeVersion: NodeVersion | undefined
): boolean {
if (lockfileVersion === undefined) return false;
return lockfileVersion >= 2 && (nodeVersion?.major || 0) < 16;
}

/**
* Helper to get the binary paths that link to the used package manager.
* Note: Make sure it doesn't contain any `console.log` calls.
*/
export function getPathOverrideForPackageManager({
cliType,
lockfileVersion,
nodeVersion,
env,
}: {
cliType: CliType;
lockfileVersion: number | undefined;
nodeVersion: NodeVersion | undefined;
env: { [x: string]: string | undefined };
}): {
/**
* Which lockfile was detected.
*/
detectedLockfile: string | undefined;
/**
* Detected package manager that generated the found lockfile.
*/
detectedPackageManager: string | undefined;
/**
* Value of $PATH that includes the binaries for the detected package manager.
* Undefined if no $PATH are necessary.
*/
path: string | undefined;
} {
const no_override = {
detectedLockfile: undefined,
detectedPackageManager: undefined,
path: undefined,
};

const corepackEnabled = env.ENABLE_EXPERIMENTAL_COREPACK === '1';

switch (cliType) {
case 'npm':
switch (true) {
case corepackEnabled:
return no_override;
case shouldUseNpm7(lockfileVersion, nodeVersion):
return {
path: '/node16/bin-npm7',
detectedLockfile: 'package-lock.json',
detectedPackageManager: 'npm 7+',
};
default:
return no_override;
}
case 'pnpm':
switch (detectPnpmVersion(lockfileVersion, corepackEnabled)) {
case 'corepack_enabled':
return no_override;
case 'pnpm 7':
// pnpm 7
return {
path: '/pnpm7/node_modules/.bin',
detectedLockfile: 'pnpm-lock.yaml',
detectedPackageManager: 'pnpm 7',
};
case 'pnpm 8':
// pnpm 8
return {
path: '/pnpm8/node_modules/.bin',
detectedLockfile: 'pnpm-lock.yaml',
detectedPackageManager: 'pnpm 8',
};
case 'pnpm 9':
// pnpm 9
return {
path: '/pnpm9/node_modules/.bin',
detectedLockfile: 'pnpm-lock.yaml',
detectedPackageManager: 'pnpm 9',
};
default:
return no_override;
}
case 'bun':
switch (true) {
case corepackEnabled:
return no_override;
default:
// Bun 1
return {
path: '/bun1',
detectedLockfile: 'bun.lockb',
detectedPackageManager: 'Bun',
};
}
case 'yarn':
return no_override;
}
}

/**
* Helper to get the binary paths that link to the used package manager.
* Note: Make sure it doesn't contain any `console.log` calls.
Expand Down Expand Up @@ -622,70 +757,31 @@ export function getPathForPackageManager({
*/
yarnNodeLinker: string | undefined;
} {
let detectedLockfile: string | undefined;
let detectedPackageManager: string | undefined;
let pathValue: string | undefined;
let yarnNodeLinker: string | undefined;
const oldPath = env.PATH + '';
const npm7 = '/node16/bin-npm7';
const pnpm7 = '/pnpm7/node_modules/.bin';
const pnpm8 = '/pnpm8/node_modules/.bin';
const bun1 = '/bun1';
const corepackEnabled = env.ENABLE_EXPERIMENTAL_COREPACK === '1';
if (cliType === 'npm') {
if (
typeof lockfileVersion === 'number' &&
lockfileVersion >= 2 &&
(nodeVersion?.major || 0) < 16 &&
!oldPath.includes(npm7) &&
!corepackEnabled
) {
// npm 7
pathValue = npm7;
detectedLockfile = 'package-lock.json';
detectedPackageManager = 'npm 7+';
}
} else if (cliType === 'pnpm') {
if (
typeof lockfileVersion === 'number' &&
lockfileVersion === 5.4 &&
!oldPath.includes(pnpm7) &&
!corepackEnabled
) {
// pnpm 7
pathValue = pnpm7;
detectedLockfile = 'pnpm-lock.yaml';
detectedPackageManager = 'pnpm 7';
} else if (
typeof lockfileVersion === 'number' &&
lockfileVersion >= 6 &&
!oldPath.includes(pnpm8) &&
!corepackEnabled
) {
// pnpm 8
pathValue = pnpm8;
detectedLockfile = 'pnpm-lock.yaml';
detectedPackageManager = 'pnpm 8';
}
} else if (cliType === 'bun') {
if (!oldPath.includes(bun1) && !corepackEnabled) {
// Bun 1
pathValue = bun1;
detectedLockfile = 'bun.lockb';
detectedPackageManager = 'Bun';
}
} else {
// Yarn v2 PnP mode may be activated, so force "node-modules" linker style
if (!env.YARN_NODE_LINKER) {
yarnNodeLinker = 'node-modules';
}
}
return {
detectedLockfile,
detectedPackageManager,
path: pathValue,
yarnNodeLinker,
const overrides = getPathOverrideForPackageManager({
cliType,
lockfileVersion,
nodeVersion,
env,
});

const alreadyInPath = (newPath: string) => {
const oldPath = env.PATH ?? '';
return oldPath.split(path.delimiter).includes(newPath);
};

switch (true) {
case cliType === 'yarn' && !env.YARN_NODE_LINKER:
return { ...overrides, yarnNodeLinker: 'node-modules' };
case alreadyInPath(overrides.path ?? ''):
return {
detectedLockfile: undefined,
detectedPackageManager: undefined,
path: undefined,
yarnNodeLinker: undefined,
};
default:
return { ...overrides, yarnNodeLinker: undefined };
}
}

export async function runCustomInstallCommand({
Expand Down
2 changes: 1 addition & 1 deletion packages/build-utils/test/fixtures/22-pnpm/vercel.json
Expand Up @@ -4,7 +4,7 @@
"probes": [
{
"path": "/",
"mustContain": "pnpm version: 6",
"mustContain": "pnpm version: 7",
"logMustContain": "pnpm run build"
}
]
Expand Down

0 comments on commit 5014b1e

Please sign in to comment.