mirror of
https://github.com/pnpm/pnpm.git
synced 2026-03-28 12:01:37 -04:00
* fix(deploy): omit inject workspace packages setting in deploy lockfiles When the deploy command creates a new lockfile, create the deployment lockfile without the setting to inject workspace packages, because it has already been applied when creating the lockfile and the deployment is not, itself, a workspace. * docs: add changesets --------- Co-authored-by: Zoltan Kochan <z@kochan.io>
550 lines
15 KiB
TypeScript
550 lines
15 KiB
TypeScript
import fs from 'fs'
|
|
import path from 'path'
|
|
import { assertProject } from '@pnpm/assert-project'
|
|
import { preparePackages } from '@pnpm/prepare'
|
|
import { filterPackagesFromDir } from '@pnpm/workspace.filter-packages-from-dir'
|
|
import { jest } from '@jest/globals'
|
|
import { DEFAULT_OPTS } from './utils/index.js'
|
|
import { install } from '@pnpm/plugin-commands-installation'
|
|
|
|
const original = await import('@pnpm/logger')
|
|
const warn = jest.fn()
|
|
jest.unstable_mockModule('@pnpm/logger', () => {
|
|
const logger = {
|
|
...original.logger,
|
|
warn,
|
|
}
|
|
return {
|
|
...original,
|
|
globalWarn: jest.fn(),
|
|
logger: Object.assign(() => logger, logger),
|
|
}
|
|
})
|
|
const { globalWarn } = await import('@pnpm/logger')
|
|
const { deploy } = await import('@pnpm/plugin-commands-deploy')
|
|
|
|
beforeEach(async () => {
|
|
jest.mocked(globalWarn).mockClear()
|
|
})
|
|
|
|
afterEach(() => {
|
|
jest.restoreAllMocks()
|
|
})
|
|
|
|
test('deploy without existing lockfile', async () => {
|
|
preparePackages([
|
|
{
|
|
name: 'project-1',
|
|
version: '1.0.0',
|
|
files: ['index.js'],
|
|
dependencies: {
|
|
'project-2': 'workspace:*',
|
|
'is-positive': '1.0.0',
|
|
},
|
|
devDependencies: {
|
|
'project-3': 'workspace:*',
|
|
'is-negative': '1.0.0',
|
|
},
|
|
},
|
|
{
|
|
name: 'project-2',
|
|
version: '2.0.0',
|
|
files: ['index.js'],
|
|
dependencies: {
|
|
'project-3': 'workspace:*',
|
|
'is-odd': '1.0.0',
|
|
},
|
|
},
|
|
{
|
|
name: 'project-3',
|
|
version: '2.0.0',
|
|
files: ['index.js'],
|
|
dependencies: {
|
|
'project-3': 'workspace:*',
|
|
'is-odd': '1.0.0',
|
|
},
|
|
},
|
|
])
|
|
|
|
for (const name of ['project-1', 'project-2', 'project-3']) {
|
|
fs.writeFileSync(`${name}/test.js`, '', 'utf8')
|
|
fs.writeFileSync(`${name}/index.js`, '', 'utf8')
|
|
}
|
|
|
|
const { allProjects, selectedProjectsGraph } = await filterPackagesFromDir(process.cwd(), [{ namePattern: 'project-1' }])
|
|
|
|
await deploy.handler({
|
|
...DEFAULT_OPTS,
|
|
allProjects,
|
|
dir: process.cwd(),
|
|
dev: false,
|
|
production: true,
|
|
recursive: true,
|
|
selectedProjectsGraph,
|
|
sharedWorkspaceLockfile: true,
|
|
lockfileDir: process.cwd(),
|
|
workspaceDir: process.cwd(),
|
|
}, ['deploy'])
|
|
|
|
expect(globalWarn).toHaveBeenCalledWith('Shared lockfile not found. Falling back to installing without a lockfile.')
|
|
|
|
const project = assertProject(path.resolve('deploy'))
|
|
project.has('project-2')
|
|
project.has('is-positive')
|
|
project.hasNot('project-3')
|
|
project.hasNot('is-negative')
|
|
expect(fs.existsSync('deploy/index.js')).toBeTruthy()
|
|
expect(fs.existsSync('deploy/test.js')).toBeFalsy()
|
|
expect(fs.existsSync('deploy/node_modules/.modules.yaml')).toBeTruthy()
|
|
expect(fs.existsSync('deploy/node_modules/.pnpm/project-2@file+project-2/node_modules/project-2/index.js')).toBeTruthy()
|
|
expect(fs.existsSync('deploy/node_modules/.pnpm/project-2@file+project-2/node_modules/project-2/test.js')).toBeFalsy()
|
|
expect(fs.existsSync('deploy/node_modules/.pnpm/project-3@file+project-3/node_modules/project-3/index.js')).toBeTruthy()
|
|
expect(fs.existsSync('deploy/node_modules/.pnpm/project-3@file+project-3/node_modules/project-3/test.js')).toBeFalsy()
|
|
expect(fs.existsSync('pnpm-lock.yaml')).toBeFalsy() // no changes to the lockfile are written
|
|
})
|
|
|
|
test('deploy in workspace with shared-workspace-lockfile=false', async () => {
|
|
preparePackages([
|
|
{
|
|
name: 'project-1',
|
|
version: '1.0.0',
|
|
files: ['index.js'],
|
|
dependencies: {
|
|
'project-2': 'workspace:*',
|
|
'is-positive': '1.0.0',
|
|
},
|
|
devDependencies: {
|
|
'project-3': 'workspace:*',
|
|
'is-negative': '1.0.0',
|
|
},
|
|
},
|
|
{
|
|
name: 'project-2',
|
|
version: '2.0.0',
|
|
files: ['index.js'],
|
|
dependencies: {
|
|
'project-3': 'workspace:*',
|
|
'is-odd': '1.0.0',
|
|
},
|
|
},
|
|
{
|
|
name: 'project-3',
|
|
version: '2.0.0',
|
|
files: ['index.js'],
|
|
dependencies: {
|
|
'project-3': 'workspace:*',
|
|
'is-odd': '1.0.0',
|
|
},
|
|
},
|
|
])
|
|
|
|
for (const name of ['project-1', 'project-2', 'project-3']) {
|
|
fs.writeFileSync(`${name}/test.js`, '', 'utf8')
|
|
fs.writeFileSync(`${name}/index.js`, '', 'utf8')
|
|
}
|
|
|
|
const { allProjects, selectedProjectsGraph } = await filterPackagesFromDir(process.cwd(), [{ namePattern: 'project-1' }])
|
|
|
|
await deploy.handler({
|
|
...DEFAULT_OPTS,
|
|
allProjects,
|
|
dir: process.cwd(),
|
|
dev: false,
|
|
production: true,
|
|
recursive: true,
|
|
selectedProjectsGraph,
|
|
sharedWorkspaceLockfile: false,
|
|
workspaceDir: process.cwd(),
|
|
}, ['deploy'])
|
|
|
|
const project = assertProject(path.resolve('deploy'))
|
|
project.has('project-2')
|
|
project.has('is-positive')
|
|
project.hasNot('project-3')
|
|
project.hasNot('is-negative')
|
|
expect(fs.existsSync('deploy/index.js')).toBeTruthy()
|
|
expect(fs.existsSync('deploy/test.js')).toBeFalsy()
|
|
expect(fs.existsSync('deploy/node_modules/.modules.yaml')).toBeTruthy()
|
|
expect(fs.existsSync('deploy/node_modules/.pnpm/project-2@file+..+project-2/node_modules/project-2/index.js')).toBeTruthy()
|
|
expect(fs.existsSync('deploy/node_modules/.pnpm/project-2@file+..+project-2/node_modules/project-2/test.js')).toBeFalsy()
|
|
expect(fs.existsSync('deploy/node_modules/.pnpm/project-3@file+..+project-3/node_modules/project-3/index.js')).toBeTruthy()
|
|
expect(fs.existsSync('deploy/node_modules/.pnpm/project-3@file+..+project-3/node_modules/project-3/test.js')).toBeFalsy()
|
|
expect(fs.existsSync('pnpm-lock.yaml')).toBeFalsy() // no changes to the lockfile are written
|
|
})
|
|
|
|
test('deploy with node-linker=hoisted', async () => {
|
|
preparePackages([
|
|
{
|
|
location: '.',
|
|
package: {
|
|
name: 'root',
|
|
},
|
|
},
|
|
{
|
|
name: 'project-1',
|
|
version: '1.0.0',
|
|
files: ['index.js'],
|
|
dependencies: {
|
|
'project-2': 'workspace:*',
|
|
'is-positive': '1.0.0',
|
|
},
|
|
devDependencies: {
|
|
'project-3': 'workspace:*',
|
|
'is-negative': '1.0.0',
|
|
},
|
|
},
|
|
{
|
|
name: 'project-2',
|
|
version: '2.0.0',
|
|
files: ['index.js'],
|
|
dependencies: {
|
|
'project-3': 'workspace:*',
|
|
'is-odd': '1.0.0',
|
|
},
|
|
},
|
|
{
|
|
name: 'project-3',
|
|
version: '2.0.0',
|
|
files: ['index.js'],
|
|
dependencies: {
|
|
'project-3': 'workspace:*',
|
|
'is-odd': '1.0.0',
|
|
},
|
|
},
|
|
])
|
|
|
|
; ['project-1', 'project-2', 'project-3'].forEach(name => {
|
|
fs.writeFileSync(`${name}/test.js`, '', 'utf8')
|
|
fs.writeFileSync(`${name}/index.js`, '', 'utf8')
|
|
})
|
|
|
|
const { allProjects, selectedProjectsGraph } = await filterPackagesFromDir(process.cwd(), [{ namePattern: 'project-1' }])
|
|
|
|
await deploy.handler({
|
|
...DEFAULT_OPTS,
|
|
allProjects,
|
|
dir: process.cwd(),
|
|
dev: false,
|
|
production: true,
|
|
recursive: true,
|
|
selectedProjectsGraph,
|
|
nodeLinker: 'hoisted',
|
|
sharedWorkspaceLockfile: true,
|
|
lockfileDir: process.cwd(),
|
|
workspaceDir: process.cwd(),
|
|
}, ['dist'])
|
|
|
|
const project = assertProject(path.resolve('dist'))
|
|
project.has('project-2')
|
|
project.has('is-positive')
|
|
project.has('project-3')
|
|
project.hasNot('is-negative')
|
|
expect(fs.existsSync('dist/index.js')).toBeTruthy()
|
|
expect(fs.existsSync('dist/test.js')).toBeFalsy()
|
|
expect(fs.existsSync('dist/node_modules/.modules.yaml')).toBeTruthy()
|
|
expect(fs.existsSync('dist/node_modules/project-2/index.js')).toBeTruthy()
|
|
expect(fs.existsSync('dist/node_modules/project-2/test.js')).toBeFalsy()
|
|
expect(fs.existsSync('dist/node_modules/project-3/index.js')).toBeTruthy()
|
|
expect(fs.existsSync('dist/node_modules/project-3/test.js')).toBeFalsy()
|
|
expect(fs.existsSync('pnpm-lock.yaml')).toBeFalsy() // no changes to the lockfile are written
|
|
})
|
|
|
|
// Similar to the test above making sure pnpm deploy works with
|
|
// node-linker=hoisted, but we should also make sure not to link projects not in
|
|
// the dependency graph of the deployed package.
|
|
//
|
|
// Let's check node-linker=isolated as well for good measure.
|
|
test.each(['isolated', 'hoisted'] as const)(
|
|
'deploy does not link unnecessary workspace packages when node-linker=%p',
|
|
async (nodeLinker) => {
|
|
preparePackages([
|
|
{
|
|
location: '.',
|
|
package: {
|
|
name: 'root',
|
|
},
|
|
},
|
|
{
|
|
name: 'project-1',
|
|
version: '1.0.0',
|
|
dependencies: {
|
|
'project-2': 'workspace:*',
|
|
'is-positive': '1.0.0',
|
|
},
|
|
},
|
|
{
|
|
name: 'project-2',
|
|
version: '2.0.0',
|
|
},
|
|
{
|
|
name: 'project-3',
|
|
version: '2.0.0',
|
|
dependencies: {
|
|
'is-odd': '1.0.0',
|
|
},
|
|
},
|
|
])
|
|
|
|
const { allProjects, selectedProjectsGraph } = await filterPackagesFromDir(process.cwd(), [{ namePattern: 'project-1' }])
|
|
|
|
await deploy.handler({
|
|
...DEFAULT_OPTS,
|
|
allProjects,
|
|
dir: process.cwd(),
|
|
dev: false,
|
|
production: true,
|
|
recursive: true,
|
|
selectedProjectsGraph,
|
|
nodeLinker,
|
|
sharedWorkspaceLockfile: true,
|
|
lockfileDir: process.cwd(),
|
|
workspaceDir: process.cwd(),
|
|
}, ['dist'])
|
|
|
|
const project = assertProject(path.resolve('dist'))
|
|
|
|
project.has('project-2')
|
|
project.has('is-positive')
|
|
|
|
// project-3 should not be deployed since it's not in the dependency graph of
|
|
// project-1. "is-odd" should not be deployed either since it's only a
|
|
// dependency of project-3.
|
|
project.hasNot('project-3')
|
|
project.hasNot('is-odd')
|
|
}
|
|
)
|
|
|
|
test('deploy fails when the destination directory exists and is not empty', async () => {
|
|
preparePackages([
|
|
{
|
|
name: 'project',
|
|
version: '1.0.0',
|
|
files: ['index.js'],
|
|
dependencies: {},
|
|
devDependencies: {},
|
|
},
|
|
])
|
|
fs.writeFileSync('project/index.js', '', 'utf8')
|
|
|
|
const deployPath = 'deploy'
|
|
fs.writeFileSync(deployPath, 'aaa', 'utf8')
|
|
const deployFullPath = path.resolve(deployPath)
|
|
|
|
const { allProjects, selectedProjectsGraph } = await filterPackagesFromDir(process.cwd(), [{ namePattern: 'project' }])
|
|
|
|
await expect(() =>
|
|
deploy.handler({
|
|
...DEFAULT_OPTS,
|
|
allProjects,
|
|
dir: process.cwd(),
|
|
dev: false,
|
|
production: true,
|
|
recursive: true,
|
|
selectedProjectsGraph,
|
|
sharedWorkspaceLockfile: true,
|
|
lockfileDir: process.cwd(),
|
|
workspaceDir: process.cwd(),
|
|
}, [deployPath])).rejects.toThrow(`Deploy path ${deployFullPath} is not empty`)
|
|
|
|
expect(fs.existsSync(`${deployPath}/index.js`)).toBeFalsy() // no changes to the deploy path are made
|
|
expect(fs.existsSync('pnpm-lock.yaml')).toBeFalsy() // no changes to the lockfile are written
|
|
})
|
|
|
|
test('forced deploy succeeds with a warning when destination directory exists and is not empty', async () => {
|
|
preparePackages([
|
|
{
|
|
name: 'project',
|
|
version: '1.0.0',
|
|
files: ['index.js'],
|
|
dependencies: {
|
|
'is-positive': '1.0.0',
|
|
},
|
|
devDependencies: {
|
|
'is-negative': '1.0.0',
|
|
},
|
|
},
|
|
])
|
|
fs.writeFileSync('project/index.js', '', 'utf8')
|
|
|
|
const deployPath = 'deploy'
|
|
fs.writeFileSync(deployPath, 'aaa', 'utf8')
|
|
const deployFullPath = path.resolve(deployPath)
|
|
|
|
const { allProjects, selectedProjectsGraph } = await filterPackagesFromDir(process.cwd(), [{ namePattern: 'project' }])
|
|
|
|
await deploy.handler({
|
|
...DEFAULT_OPTS,
|
|
allProjects,
|
|
dir: process.cwd(),
|
|
dev: false,
|
|
production: true,
|
|
recursive: true,
|
|
force: true,
|
|
selectedProjectsGraph,
|
|
sharedWorkspaceLockfile: true,
|
|
lockfileDir: process.cwd(),
|
|
workspaceDir: process.cwd(),
|
|
}, [deployPath])
|
|
|
|
expect(warn).toHaveBeenCalledWith({
|
|
message: expect.stringMatching(/^using --force, deleting deploy pat/),
|
|
prefix: deployFullPath,
|
|
})
|
|
|
|
// deployed successfully
|
|
const project = assertProject(deployFullPath)
|
|
project.has('is-positive')
|
|
project.hasNot('is-negative')
|
|
expect(fs.existsSync('deploy/index.js')).toBeTruthy()
|
|
expect(fs.existsSync('pnpm-lock.yaml')).toBeFalsy() // no changes to the lockfile are written
|
|
|
|
warn.mockRestore()
|
|
})
|
|
|
|
test('deploy with dedupePeerDependents=true ignores the value of dedupePeerDependents', async () => {
|
|
preparePackages([
|
|
{
|
|
name: 'project-1',
|
|
version: '1.0.0',
|
|
dependencies: {
|
|
'is-positive': '1.0.0',
|
|
},
|
|
},
|
|
{
|
|
location: './sub-dir/project-2',
|
|
package: {
|
|
name: 'project-2',
|
|
version: '2.0.0',
|
|
dependencies: {
|
|
'is-odd': '1.0.0',
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: 'project-3',
|
|
version: '2.0.0',
|
|
dependencies: {
|
|
'is-number': '1.0.0',
|
|
},
|
|
},
|
|
])
|
|
|
|
const { allProjects, selectedProjectsGraph, allProjectsGraph } = await filterPackagesFromDir(process.cwd(), [{ namePattern: 'project-1' }])
|
|
|
|
await deploy.handler({
|
|
...DEFAULT_OPTS,
|
|
allProjects,
|
|
allProjectsGraph,
|
|
dir: process.cwd(),
|
|
dev: false,
|
|
production: true,
|
|
recursive: true,
|
|
selectedProjectsGraph,
|
|
sharedWorkspaceLockfile: true,
|
|
lockfileDir: process.cwd(),
|
|
workspaceDir: process.cwd(),
|
|
dedupePeerDependents: true, // This is ignored by deploy
|
|
}, ['deploy'])
|
|
const project = assertProject(path.resolve('deploy'))
|
|
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()
|
|
})
|
|
|
|
test('deploy does not preserve the inject workspace packages settings in the lockfile', async () => {
|
|
preparePackages([
|
|
{
|
|
location: '.',
|
|
package: {
|
|
name: 'root',
|
|
version: '1.0.0',
|
|
private: true,
|
|
},
|
|
},
|
|
{
|
|
name: 'project',
|
|
version: '1.0.0',
|
|
},
|
|
])
|
|
|
|
const { allProjects, selectedProjectsGraph } = await filterPackagesFromDir(process.cwd(), [{ namePattern: 'project' }])
|
|
|
|
await install.handler({
|
|
...DEFAULT_OPTS,
|
|
allProjects,
|
|
dir: process.cwd(),
|
|
dev: true,
|
|
production: true,
|
|
lockfileOnly: true,
|
|
sharedWorkspaceLockfile: true,
|
|
lockfileDir: process.cwd(),
|
|
workspaceDir: process.cwd(),
|
|
})
|
|
|
|
await deploy.handler({
|
|
...DEFAULT_OPTS,
|
|
allProjects,
|
|
dir: process.cwd(),
|
|
dev: false,
|
|
production: true,
|
|
recursive: true,
|
|
selectedProjectsGraph,
|
|
sharedWorkspaceLockfile: true,
|
|
lockfileDir: process.cwd(),
|
|
workspaceDir: process.cwd(),
|
|
}, ['dist'])
|
|
|
|
const project = assertProject(path.resolve('dist'))
|
|
const lockfile = project.readLockfile()
|
|
expect(lockfile.settings).not.toHaveProperty('injectWorkspacePackages')
|
|
})
|