Files
pnpm/fs/indexed-pkg-importer/test/createImportPackage.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

408 lines
16 KiB
TypeScript

import fs, { type BigIntStats } from 'node:fs'
import path from 'node:path'
import { afterAll, beforeEach, expect, jest, test } from '@jest/globals'
const testOnLinuxOnly = (process.platform === 'darwin' || process.platform === 'win32') ? test.skip : test
jest.unstable_mockModule('@pnpm/fs.graceful-fs', () => {
const { access } = jest.requireActual<typeof fs>('fs')
const fsMock = {
access,
copyFileSync: jest.fn(),
readFileSync: jest.fn(),
readdirSync: jest.fn(),
linkSync: jest.fn(),
mkdirSync: jest.fn(),
renameSync: jest.fn(),
writeFileSync: jest.fn(),
statSync: jest.fn(),
}
return {
__esModule: true,
default: fsMock,
...fsMock,
}
})
jest.unstable_mockModule('path-temp', () => ({ fastPathTemp: (file: string) => `${file}_tmp` }))
jest.unstable_mockModule('rename-overwrite', () => ({ renameOverwrite: jest.fn(), renameOverwriteSync: jest.fn() }))
jest.unstable_mockModule('fs-extra', () => ({
default: {
copySync: jest.fn(),
},
}))
jest.unstable_mockModule('@pnpm/logger', () => ({
logger: jest.fn(() => ({ debug: jest.fn() })),
globalWarn: jest.fn(),
globalInfo: jest.fn(),
}))
const { default: gfs } = await import('@pnpm/fs.graceful-fs')
const { createIndexedPkgImporter } = await import('@pnpm/fs.indexed-pkg-importer')
const { globalInfo } = await import('@pnpm/logger')
const { renameOverwriteSync } = await import('rename-overwrite')
beforeEach(() => {
// Clean up real directories created by the importer (not mocked) so each
// test starts fresh — otherwise the fast path sees leftover dirs and writes
// over them, causing different behavior than a fresh import.
fs.rmSync('project', { recursive: true, force: true })
fs.rmSync('project2', { recursive: true, force: true })
jest.mocked(gfs.copyFileSync).mockClear()
jest.mocked(gfs.readFileSync as jest.Mock).mockClear()
jest.mocked(gfs.writeFileSync).mockClear()
jest.mocked(gfs.linkSync).mockClear()
jest.mocked(gfs.mkdirSync).mockClear()
jest.mocked(gfs.renameSync).mockClear()
jest.mocked(gfs.statSync as jest.Mock).mockReset()
jest.mocked(globalInfo).mockReset()
jest.mocked(renameOverwriteSync).mockClear()
})
afterAll(() => {
fs.rmSync('project', { recursive: true, force: true })
fs.rmSync('project2', { recursive: true, force: true })
})
testOnLinuxOnly('packageImportMethod=auto: clone files by default', () => {
const importPackage = createIndexedPkgImporter('auto')
expect(importPackage('project/package', {
filesMap: new Map([
['index.js', 'hash2'],
['package.json', 'hash1'],
]),
force: false,
resolvedFrom: 'remote',
})).toBe('clone')
expect(gfs.copyFileSync).toHaveBeenCalledWith(
path.join('hash1'),
path.join('project', 'package', 'package.json'),
fs.constants.COPYFILE_FICLONE_FORCE
)
expect(gfs.copyFileSync).toHaveBeenCalledWith(
path.join('hash2'),
path.join('project', 'package', 'index.js'),
fs.constants.COPYFILE_FICLONE_FORCE
)
})
testOnLinuxOnly('packageImportMethod=auto: link files if cloning fails', () => {
const importPackage = createIndexedPkgImporter('auto')
jest.mocked(gfs.copyFileSync).mockImplementation(() => {
throw new Error('This file system does not support cloning')
})
expect(importPackage('project/package', {
filesMap: new Map([
['index.js', 'hash2'],
['package.json', 'hash1'],
]),
force: false,
resolvedFrom: 'remote',
})).toBe('hardlink')
expect(gfs.linkSync).toHaveBeenCalledWith(path.join('hash1'), path.join('project', 'package', 'package.json'))
expect(gfs.linkSync).toHaveBeenCalledWith(path.join('hash2'), path.join('project', 'package', 'index.js'))
expect(gfs.copyFileSync).toHaveBeenCalled()
jest.mocked(gfs.copyFileSync).mockClear()
// The copy function will not be called again
expect(importPackage('project2/package', {
filesMap: new Map([
['index.js', 'hash2'],
['package.json', 'hash1'],
]),
force: false,
resolvedFrom: 'remote',
})).toBe('hardlink')
expect(gfs.copyFileSync).not.toHaveBeenCalled()
expect(gfs.linkSync).toHaveBeenCalledWith(path.join('hash1'), path.join('project2', 'package', 'package.json'))
expect(gfs.linkSync).toHaveBeenCalledWith(path.join('hash2'), path.join('project2', 'package', 'index.js'))
})
testOnLinuxOnly('packageImportMethod=auto: link files if cloning fails and even hard linking fails but not with EXDEV error', () => {
const importPackage = createIndexedPkgImporter('auto')
jest.mocked(gfs.copyFileSync).mockImplementation(() => {
throw new Error('This file system does not support cloning')
})
let linkFirstCall = true
jest.mocked(gfs.linkSync).mockImplementation(() => {
if (linkFirstCall) {
linkFirstCall = false
throw new Error()
}
})
expect(importPackage('project/package', {
filesMap: new Map([
['index.js', 'hash2'],
]),
force: false,
resolvedFrom: 'remote',
})).toBe('hardlink')
expect(gfs.linkSync).toHaveBeenCalledWith(path.join('hash2'), path.join('project', 'package', 'index.js'))
expect(gfs.linkSync).toHaveBeenCalledTimes(2)
// copyFileSync is called twice: the clone attempt fails in both the fast
// path and the staging fallback before initialAuto moves on to hardlink.
expect(gfs.copyFileSync).toHaveBeenCalledTimes(2)
})
testOnLinuxOnly('packageImportMethod=auto: chooses copying if cloning and hard linking is not possible', () => {
const importPackage = createIndexedPkgImporter('auto')
jest.mocked(gfs.copyFileSync).mockImplementation((src, dest, flags?: number) => {
if (flags === fs.constants.COPYFILE_FICLONE_FORCE) {
throw new Error('This file system does not support cloning')
}
})
jest.mocked(gfs.linkSync).mockImplementation(() => {
throw new Error('EXDEV: cross-device link not permitted')
})
expect(importPackage('project/package', {
filesMap: new Map([
['index.js', 'hash2'],
]),
force: false,
resolvedFrom: 'remote',
})).toBe('copy')
expect(gfs.copyFileSync).toHaveBeenCalledWith(path.join('hash2'), path.join('project', 'package', 'index.js'))
// 3 calls: clone fails twice (fast path + staging fallback), then copy succeeds.
expect(gfs.copyFileSync).toHaveBeenCalledTimes(3)
})
testOnLinuxOnly('packageImportMethod=hardlink: fall back to copying if hardlinking fails', () => {
const importPackage = createIndexedPkgImporter('hardlink')
jest.mocked(gfs.linkSync).mockImplementation((src, dest) => {
if (dest.toString().endsWith('license')) {
throw Object.assign(new Error(''), { code: 'EEXIST' })
}
throw new Error('This file system does not support hard linking')
})
expect(importPackage('project/package', {
filesMap: new Map([
['index.js', 'hash2'],
['package.json', 'hash1'],
['license', 'hash3'],
]),
force: false,
resolvedFrom: 'remote',
})).toBe('hardlink')
expect(gfs.linkSync).toHaveBeenCalledTimes(3)
expect(gfs.copyFileSync).toHaveBeenCalledTimes(2) // One time the target already exists, so it won't be copied
expect(gfs.copyFileSync).toHaveBeenCalledWith(path.join('hash1'), path.join('project', 'package', 'package.json'))
expect(gfs.copyFileSync).toHaveBeenCalledWith(path.join('hash2'), path.join('project', 'package', 'index.js'))
})
test('packageImportMethod=hardlink does not relink package from store if package.json is linked from the store', () => {
const importPackage = createIndexedPkgImporter('hardlink')
jest.mocked(gfs.statSync).mockReturnValue({ ino: BigInt(1) } as fs.BigIntStats)
expect(importPackage('project/package', {
filesMap: new Map([
['index.js', 'hash2'],
['package.json', 'hash1'],
]),
force: false,
resolvedFrom: 'store',
})).toBeUndefined()
})
test('packageImportMethod=hardlink relinks package from store if package.json is not linked from the store', () => {
const importPackage = createIndexedPkgImporter('hardlink')
let ino = 0
jest.mocked(gfs.statSync as jest.Mock).mockImplementation(() => ({ ino: ++ino }))
expect(importPackage('project/package', {
filesMap: new Map([
['index.js', 'hash2'],
['package.json', 'hash1'],
]),
force: false,
resolvedFrom: 'store',
})).toBe('hardlink')
expect(globalInfo).toHaveBeenCalledWith('Relinking project/package from the store')
})
test('packageImportMethod=hardlink does not relink package from store if package.json is not present in the store', () => {
const importPackage = createIndexedPkgImporter('hardlink')
jest.mocked(gfs.statSync).mockImplementation(((file: string) => {
expect(typeof file).toBe('string')
return { ino: BigInt(1) } as BigIntStats
}) as unknown as typeof gfs.statSync)
expect(importPackage('project/package', {
filesMap: new Map([
['index.js', 'hash2'],
]),
force: false,
resolvedFrom: 'store',
})).toBeUndefined()
})
test('packageImportMethod=hardlink links packages when they are not found', () => {
const importPackage = createIndexedPkgImporter('hardlink')
jest.mocked(gfs.statSync).mockImplementation(((file: string) => {
if (file === path.join('project/package', 'package.json')) {
throw Object.assign(new Error(), { code: 'ENOENT' })
}
return { ino: BigInt(0) } as BigIntStats
}) as unknown as typeof gfs.statSync)
expect(importPackage('project/package', {
filesMap: new Map([
['index.js', 'hash2'],
['package.json', 'hash1'],
]),
force: false,
resolvedFrom: 'store',
})).toBe('hardlink')
expect(globalInfo).not.toHaveBeenCalledWith('Relinking project/package from the store')
})
testOnLinuxOnly('packageImportMethod=hardlink: falls back to read+write when copyFileSync throws ENOTSUP', () => {
const importPackage = createIndexedPkgImporter('hardlink')
jest.mocked(gfs.linkSync).mockImplementation(() => {
throw new Error('hard link failed')
})
jest.mocked(gfs.copyFileSync).mockImplementation(() => {
throw Object.assign(new Error('ENOTSUP: operation not supported on socket'), { code: 'ENOTSUP' })
})
jest.mocked(gfs.statSync as jest.Mock).mockReturnValue({ mode: 0o644 })
jest.mocked(gfs.readFileSync as jest.Mock).mockReturnValue(Buffer.from('file content'))
expect(importPackage('project/package', {
filesMap: new Map([
['index.js', 'hash2'],
['package.json', 'hash1'],
]),
force: false,
resolvedFrom: 'remote',
})).toBe('hardlink')
expect(gfs.readFileSync).toHaveBeenCalled()
expect(gfs.writeFileSync).toHaveBeenCalledWith(
path.join('project', 'package', 'index.js'),
Buffer.from('file content'),
{ mode: 0o644 }
)
})
testOnLinuxOnly('packageImportMethod=copy: falls back to read+write when copyFileSync throws ENOTSUP', () => {
const importPackage = createIndexedPkgImporter('copy')
jest.mocked(gfs.copyFileSync).mockImplementation(() => {
throw Object.assign(new Error('ENOTSUP: operation not supported on socket'), { code: 'ENOTSUP' })
})
jest.mocked(gfs.statSync as jest.Mock).mockReturnValue({ mode: 0o755 })
jest.mocked(gfs.readFileSync as jest.Mock).mockReturnValue(Buffer.from('file content'))
expect(importPackage('project/package', {
filesMap: new Map([
['index.js', 'hash2'],
['package.json', 'hash1'],
]),
force: false,
resolvedFrom: 'remote',
})).toBe('copy')
expect(gfs.readFileSync).toHaveBeenCalled()
expect(gfs.writeFileSync).toHaveBeenCalledWith(
path.join('project', 'package', 'index.js'),
Buffer.from('file content'),
{ mode: 0o755 }
)
})
testOnLinuxOnly('packageImportMethod=auto: ENOTSUP on clone falls through to hardlinks', () => {
const importPackage = createIndexedPkgImporter('auto')
jest.mocked(gfs.copyFileSync).mockImplementation((_src, _dest, flags?: number) => {
if (flags === fs.constants.COPYFILE_FICLONE_FORCE) {
throw Object.assign(new Error('ENOTSUP: operation not supported on socket'), { code: 'ENOTSUP' })
}
})
expect(importPackage('project/package', {
filesMap: new Map([
['index.js', 'hash2'],
['package.json', 'hash1'],
]),
force: false,
resolvedFrom: 'remote',
})).toBe('hardlink')
expect(gfs.linkSync).toHaveBeenCalledWith(path.join('hash1'), path.join('project', 'package', 'package.json'))
expect(gfs.linkSync).toHaveBeenCalledWith(path.join('hash2'), path.join('project', 'package', 'index.js'))
})
testOnLinuxOnly('packageImportMethod=auto: ENOTSUP on clone uses hardlinks for all subsequent packages too', () => {
// Regression test: the ENOTSUP fallback in createClonePkg() used to silently
// convert clone failures to copies, so the auto-importer thought cloning
// worked and selected it for all packages. On ext4 (no reflink support),
// this caused every file to be copied instead of hardlinked — a multi-second
// regression on large projects.
jest.mocked(gfs.linkSync).mockReset()
const importPackage = createIndexedPkgImporter('auto')
jest.mocked(gfs.copyFileSync).mockImplementation((_src, _dest, flags?: number) => {
if (flags === fs.constants.COPYFILE_FICLONE_FORCE) {
throw Object.assign(new Error('ENOTSUP: operation not supported on socket'), { code: 'ENOTSUP' })
}
})
expect(importPackage('project/package', {
filesMap: new Map([
['index.js', 'hash2'],
['package.json', 'hash1'],
]),
force: false,
resolvedFrom: 'remote',
})).toBe('hardlink')
jest.mocked(gfs.copyFileSync).mockClear()
jest.mocked(gfs.linkSync).mockClear()
// Second package must also use hardlinks — not copy
expect(importPackage('project2/package', {
filesMap: new Map([
['index.js', 'hash2'],
['package.json', 'hash1'],
]),
force: false,
resolvedFrom: 'remote',
})).toBe('hardlink')
expect(gfs.linkSync).toHaveBeenCalledWith(path.join('hash1'), path.join('project2', 'package', 'package.json'))
expect(gfs.linkSync).toHaveBeenCalledWith(path.join('hash2'), path.join('project2', 'package', 'index.js'))
// copyFileSync should not have been called (no clone, no copy fallback)
expect(gfs.copyFileSync).not.toHaveBeenCalled()
})
testOnLinuxOnly('packageImportMethod=clone: falls back to copy on ENOTSUP, using atomic write for package.json', () => {
const importPackage = createIndexedPkgImporter('clone')
jest.mocked(gfs.copyFileSync).mockImplementation((_src, _dest, flags?: number) => {
if (flags === fs.constants.COPYFILE_FICLONE_FORCE) {
throw Object.assign(new Error('ENOTSUP: operation not supported on socket'), { code: 'ENOTSUP' })
}
})
jest.mocked(gfs.statSync as jest.Mock).mockReturnValue({ mode: 0o644 })
jest.mocked(gfs.readFileSync as jest.Mock).mockReturnValue(Buffer.from('file content'))
expect(importPackage('project/package', {
filesMap: new Map([
['index.js', 'hash2'],
['package.json', 'hash1'],
]),
force: false,
resolvedFrom: 'remote',
})).toBe('clone')
// Regular file: falls back to plain copyFileSync (without reflink flag)
expect(gfs.copyFileSync).toHaveBeenCalledWith(
path.join('hash2'),
path.join('project', 'package', 'index.js')
)
// package.json: falls back to atomic temp+rename
expect(renameOverwriteSync).toHaveBeenCalledWith(
path.join('project', 'package', 'package.json') + '_tmp',
path.join('project', 'package', 'package.json')
)
})
testOnLinuxOnly('packageImportMethod=hardlink: rethrows non-ENOTSUP errors from copyFileSync', () => {
const importPackage = createIndexedPkgImporter('hardlink')
jest.mocked(gfs.linkSync).mockImplementation(() => {
throw new Error('hard link failed')
})
jest.mocked(gfs.copyFileSync).mockImplementation(() => {
throw Object.assign(new Error('EACCES: permission denied'), { code: 'EACCES' })
})
expect(() => importPackage('project/package', {
filesMap: new Map([
['index.js', 'hash2'],
]),
force: false,
resolvedFrom: 'remote',
})).toThrow('EACCES: permission denied')
})