Files
pnpm/releasing/commands/test/deploy/deploy.test.ts
Zoltan Kochan 187049055f chore: upgrade @typescript/native-preview to 7.0.0-dev.20260421.2 (#11332)
* chore: upgrade @typescript/native-preview to 7.0.0-dev.20260421.2

- Add explicit `types: ["node"]` to the shared tsconfig because tsgo
  20260421 no longer auto-acquires `@types/*` from `node_modules`.
- Refactor test files to explicitly import jest globals (`describe`,
  `it`, `test`, `expect`, `beforeEach`, etc.) from `@jest/globals`
  instead of relying on `@types/jest` ambient declarations. Under the
  new tsgo build, `import { jest } from '@jest/globals'` shadows the
  ambient `jest` namespace, breaking `@types/jest`'s `declare var
  describe: jest.Describe;` globals.
- Add `@jest/globals` to each package's devDependencies where tests
  now import from it, and add `@types/node` to packages that need it
  but were relying on hoisted resolution.
- Replace `fail()` calls with `throw new Error(...)` since `fail` is
  no longer globally available.

* chore: fix remaining tsgo type-strictness errors

- Strip `as <PnpmType>` casts on objects passed to toMatchObject /
  toStrictEqual / toEqual; @jest/globals rejects the typed objects
  (which include AsymmetricMatchers) vs. the repo-specific type.
- Type `jest.fn<...>()` explicitly where the mock's signature matters
  for toHaveBeenCalledWith.
- Replace `beforeEach(() => X)` with `beforeEach(() => { X })` so the
  return value is void, as the stricter jest typing requires.
- Use `expect.objectContaining({...})` in one place where the full
  expected object triggered stricter type resolution.
- Cast `prompt.mock.calls` arg through `as unknown as Record<...>[]`
  for patch.test.ts's nested-array matchers.
- Fix off-by-one `<reference path>` in pnpm/test/getConfig.test.ts
  that only surfaced now.
- Move `@jest/globals` from devDependencies to dependencies in the
  two `__utils__` packages that import it from `src/`.
- Clean up unused imports from the @jest/globals migration.

* chore: address Copilot review on #11332

- Move misplaced `@jest/globals` imports to the top import block in
  checkEngine, run.ts, and workspace/root-finder tests where the
  script dropped them below executable code.
- Replace `try { await x(); throw new Error('should have thrown') } catch`
  in bins/linker, lockfile/fs, and resolving/local-resolver tests with
  `await expect(x()).rejects.toMatchObject({...})`. The old pattern
  swallowed an unrelated `throw` if the under-test call silently
  succeeded, which would fail on the catch-block assertion with a
  misleading message.
2026-04-21 22:50:40 +02:00

552 lines
16 KiB
TypeScript

import fs from 'node:fs'
import path from 'node:path'
import { afterEach, beforeEach, expect, jest, test } from '@jest/globals'
import { assertProject } from '@pnpm/assert-project'
import { install } from '@pnpm/installing.commands'
import { preparePackages } from '@pnpm/prepare'
import { filterProjectsBySelectorObjectsFromDir } from '@pnpm/workspace.projects-filter'
import { DEFAULT_OPTS } from './utils/index.js'
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/releasing.commands')
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 filterProjectsBySelectorObjectsFromDir(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 filterProjectsBySelectorObjectsFromDir(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 filterProjectsBySelectorObjectsFromDir(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 filterProjectsBySelectorObjectsFromDir(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 filterProjectsBySelectorObjectsFromDir(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 filterProjectsBySelectorObjectsFromDir(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 filterProjectsBySelectorObjectsFromDir(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 filterProjectsBySelectorObjectsFromDir(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 filterProjectsBySelectorObjectsFromDir(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')
})