fix: install failure on pnpm deploy when using catalogs (#8298)

* fix: install failure on pnpm deploy when using catalogs

* test: add test for pnpm deploy when using catalogs

* chore: changeset
This commit is contained in:
Brandon Cheng
2024-07-13 20:45:35 -04:00
committed by GitHub
parent 5aa98b6d6f
commit 1e4dd79163
9 changed files with 162 additions and 0 deletions

View File

@@ -0,0 +1,7 @@
---
"@pnpm/plugin-commands-installation": patch
"@pnpm/plugin-commands-deploy": patch
pnpm: patch
---
The `pnpm deploy` command now supports the [`catalog:` protocol](https://pnpm.io/catalogs).

View File

@@ -252,6 +252,7 @@ export type InstallCommandOptions = Pick<Config,
| 'autoInstallPeers'
| 'bail'
| 'bin'
| 'catalogs'
| 'cliOptions'
| 'dedupeDirectDeps'
| 'dedupePeerDependents'

6
pnpm-lock.yaml generated
View File

@@ -5617,6 +5617,12 @@ importers:
releasing/plugin-commands-deploy:
dependencies:
'@pnpm/catalogs.resolver':
specifier: workspace:*
version: link:../../catalogs/resolver
'@pnpm/catalogs.types':
specifier: workspace:*
version: link:../../catalogs/types
'@pnpm/cli-utils':
specifier: workspace:*
version: link:../../cli/cli-utils

View File

@@ -42,6 +42,8 @@
"@pnpm/workspace.filter-packages-from-dir": "workspace:*"
},
"dependencies": {
"@pnpm/catalogs.resolver": "workspace:*",
"@pnpm/catalogs.types": "workspace:*",
"@pnpm/cli-utils": "workspace:*",
"@pnpm/common-cli-options-help": "workspace:*",
"@pnpm/directory-fetcher": "workspace:*",

View File

@@ -11,6 +11,7 @@ import rimraf from '@zkochan/rimraf'
import renderHelp from 'render-help'
import { deployHook } from './deployHook'
import { logger } from '@pnpm/logger'
import { deployCatalogHook } from './deployCatalogHook'
export const shorthands = install.shorthands
@@ -101,6 +102,7 @@ export async function handler (
readPackage: [
...(opts.hooks?.readPackage ?? []),
deployHook,
deployCatalogHook.bind(null, opts.catalogs ?? {}),
],
},
frozenLockfile: false,

View File

@@ -0,0 +1,51 @@
import { matchCatalogResolveResult, resolveFromCatalog } from '@pnpm/catalogs.resolver'
import { type Catalogs } from '@pnpm/catalogs.types'
import { type ProjectManifest, DEPENDENCIES_FIELDS } from '@pnpm/types'
/**
* Teach the `pnpm deploy` command how to interpret the catalog: protocol.
*
* This is a hack to work around a design problem between pnpm deploy and
* catalogs.
*
* - The catalog protocol is intentionally only allowed to be used by
* importers. External dependencies cannot use the catalog: protocol by
* design.
* - When using pnpm deploy, dependency workspace packages aren't considered
* "importers".
*
* To work around the conflict of designs above, this readPackage hook exists to
* make catalogs usable by non-importers specifically on pnpm deploy.
*
* Unfortunately this introduces a correctness issue where the catalog: protocol
* is replaced for all packages (even external dependencies), not just packages
* within the pnpm workspace. This caveat is somewhat mitigated by the fact that
* a regular pnpm install would still fail before users could pnpm deploy a
* project.
*/
export function deployCatalogHook (catalogs: Catalogs, pkg: ProjectManifest): ProjectManifest {
for (const depField of DEPENDENCIES_FIELDS) {
const depsBlock = pkg[depField]
if (depsBlock == null) {
continue
}
for (const [alias, pref] of Object.entries(depsBlock)) {
const resolveResult = resolveFromCatalog(catalogs, { alias, pref })
matchCatalogResolveResult(resolveResult, {
unused: () => {},
misconfiguration: (result) => {
throw result.error
},
found: (result) => {
depsBlock[alias] = result.resolution.specifier
},
})
}
}
return pkg
}

View File

@@ -213,3 +213,53 @@ test('deploy with dedupePeerDependents=true ignores the value of dedupePeerDepen
project.has('is-positive')
expect(fs.existsSync('sub-dir/deploy')).toBe(false)
})
// Regression test for https://github.com/pnpm/pnpm/issues/8297 (pnpm deploy doesn't replace catalog: protocol)
test('deploy works when workspace packages use catalog protocol', async () => {
preparePackages([
{
name: 'project-1',
dependencies: {
'project-2': 'workspace:*',
'is-positive': 'catalog:',
},
},
{
name: 'project-2',
dependencies: {
'project-3': 'workspace:*',
'is-positive': 'catalog:',
},
},
{
name: 'project-3',
dependencies: {
'project-3': 'workspace:*',
'is-positive': 'catalog:',
},
},
])
const { allProjects, selectedProjectsGraph } = await filterPackagesFromDir(process.cwd(), [{ namePattern: 'project-1' }])
await deploy.handler({
...DEFAULT_OPTS,
allProjects,
catalogs: {
default: {
'is-positive': '1.0.0',
},
},
dir: process.cwd(),
dev: false,
production: true,
recursive: true,
selectedProjectsGraph,
sharedWorkspaceLockfile: true,
lockfileDir: process.cwd(),
workspaceDir: process.cwd(),
}, ['deploy'])
// Make sure the is-positive cataloged dependency was actually installed.
expect(fs.existsSync('deploy/node_modules/.pnpm/project-3@file+project-3/node_modules/is-positive')).toBeTruthy()
})

View File

@@ -0,0 +1,37 @@
import { deployCatalogHook } from '../src/deployCatalogHook'
test('deployCatalogHook()', () => {
const catalogs = {
default: {
a: '^1.0.0',
},
foo: {
b: '^2.0.0',
},
bar: {
c: '^3.0.0',
},
}
expect(deployCatalogHook(catalogs, {
dependencies: {
a: 'catalog:',
},
devDependencies: {
b: 'catalog:foo',
},
optionalDependencies: {
c: 'catalog:bar',
},
})).toStrictEqual({
dependencies: {
a: '^1.0.0',
},
devDependencies: {
b: '^2.0.0',
},
optionalDependencies: {
c: '^3.0.0',
},
})
})

View File

@@ -15,6 +15,12 @@
{
"path": "../../__utils__/prepare"
},
{
"path": "../../catalogs/resolver"
},
{
"path": "../../catalogs/types"
},
{
"path": "../../cli/cli-utils"
},