pnpm reads settings only from the pnpm-workspace.yaml at the workspace root. When an outer workspace's packages glob matches a directory that is itself a workspace, the inner file's patchedDependencies, overrides, and other settings were dropped without a word, and the next install from the outer root rewrote the inner lockfile without its patches. Both CLIs now print one warning per installed project that carries a pnpm-workspace.yaml, naming the file and the two ways out: move the settings to the root, or exclude the project from the root's packages. The TypeScript CLI emits it from installDeps next to the workspace cycle warning. The Rust CLI emits it from the install-family selection, where the scope log is reported once for every plan shape, so a workspace with a lockfile per project still warns once. Closes pnpm/pnpm#11724 --------- Co-authored-by: Zoltan Kochan <z@kochan.io>
105 lines
3.7 KiB
TypeScript
105 lines
3.7 KiB
TypeScript
import fs from 'node:fs'
|
|
import path from 'node:path'
|
|
|
|
import { afterEach, expect, jest, test } from '@jest/globals'
|
|
import { preparePackages } from '@pnpm/prepare'
|
|
import { filterProjectsBySelectorObjectsFromDir } from '@pnpm/workspace.projects-filter'
|
|
|
|
import { DEFAULT_OPTS } from './utils/index.js'
|
|
|
|
const warn = jest.fn()
|
|
const info = jest.fn()
|
|
const debug = jest.fn()
|
|
const original = await import('@pnpm/logger')
|
|
jest.unstable_mockModule('@pnpm/logger', () => ({
|
|
...original,
|
|
logger: Object.assign(() => ({ warn, info, debug }), { warn, info, debug }),
|
|
}))
|
|
const { install } = await import('@pnpm/installing.commands')
|
|
|
|
afterEach(() => {
|
|
jest.mocked(warn).mockClear()
|
|
jest.mocked(debug).mockClear()
|
|
})
|
|
|
|
async function installWorkspace (selectors: Array<{ namePattern: string }> = []): Promise<void> {
|
|
const { allProjects, selectedProjectsGraph } = await filterProjectsBySelectorObjectsFromDir(process.cwd(), selectors)
|
|
await install.handler({
|
|
...DEFAULT_OPTS,
|
|
allProjects,
|
|
dir: process.cwd(),
|
|
recursive: true,
|
|
selectedProjectsGraph,
|
|
workspaceDir: process.cwd(),
|
|
})
|
|
}
|
|
|
|
test('warns that the pnpm-workspace.yaml of a workspace project does not apply', async () => {
|
|
preparePackages([
|
|
{ name: 'project-1', version: '1.0.0' },
|
|
{ name: 'project-2', version: '1.0.0' },
|
|
])
|
|
fs.writeFileSync(path.join('project-2', 'pnpm-workspace.yaml'), 'overrides:\n foo: 1.0.0\n')
|
|
|
|
await installWorkspace()
|
|
|
|
expect(warn).toHaveBeenCalledTimes(1)
|
|
expect(warn).toHaveBeenCalledWith({
|
|
message: 'The settings in project-2/pnpm-workspace.yaml do not apply, because project-2 is a project of this workspace. ' +
|
|
'pnpm reads settings only from the pnpm-workspace.yaml at the workspace root. ' +
|
|
'Move the settings there, or add "!project-2" to the root\'s "packages" to keep that project a separate workspace.',
|
|
prefix: process.cwd(),
|
|
})
|
|
})
|
|
|
|
test('does not warn about the pnpm-workspace.yaml of the workspace root', async () => {
|
|
preparePackages([
|
|
{ name: 'project-1', version: '1.0.0' },
|
|
{ name: 'project-2', version: '1.0.0' },
|
|
])
|
|
fs.writeFileSync('pnpm-workspace.yaml', 'packages:\n - "*"\n')
|
|
fs.writeFileSync('package.json', JSON.stringify({ name: 'root', version: '1.0.0' }))
|
|
|
|
await installWorkspace()
|
|
|
|
expect(warn).toHaveBeenCalledTimes(0)
|
|
})
|
|
|
|
test('does not warn when the install leaves out the project with its own pnpm-workspace.yaml', async () => {
|
|
preparePackages([
|
|
{ name: 'project-1', version: '1.0.0' },
|
|
{ name: 'project-2', version: '1.0.0' },
|
|
])
|
|
fs.writeFileSync(path.join('project-2', 'pnpm-workspace.yaml'), 'overrides:\n foo: 1.0.0\n')
|
|
|
|
await installWorkspace([{ namePattern: 'project-1' }])
|
|
|
|
expect(warn).toHaveBeenCalledTimes(0)
|
|
})
|
|
|
|
test('does not fail when checking a nested workspace manifest encounters a filesystem error', async () => {
|
|
preparePackages([
|
|
{ name: 'project-1', version: '1.0.0' },
|
|
{ name: 'project-2', version: '1.0.0' },
|
|
])
|
|
const targetManifest = path.join(process.cwd(), 'project-2', 'pnpm-workspace.yaml')
|
|
const originalStat = fs.promises.stat.bind(fs.promises)
|
|
const statSpy = jest.spyOn(fs.promises, 'stat').mockImplementation(async (filePath, opts) => {
|
|
if (path.resolve(String(filePath)) === targetManifest) {
|
|
throw Object.assign(new Error('Permission denied'), { code: 'EACCES' })
|
|
}
|
|
return originalStat(filePath, opts)
|
|
})
|
|
try {
|
|
await installWorkspace()
|
|
expect(warn).toHaveBeenCalledTimes(0)
|
|
expect(debug).toHaveBeenCalledWith(expect.objectContaining({
|
|
error: expect.objectContaining({ code: 'EACCES' }),
|
|
message: `Could not stat nested workspace manifest at "${targetManifest}"`,
|
|
}))
|
|
} finally {
|
|
statSpy.mockRestore()
|
|
}
|
|
})
|
|
|