Files
pnpm/resolving/default-resolver/test/customResolver.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

488 lines
16 KiB
TypeScript

/// <reference path="../../../__typings__/index.d.ts"/>
import { expect, jest, test } from '@jest/globals'
import type { CustomResolver, WantedDependency } from '@pnpm/hooks.types'
import { createResolver } from '@pnpm/resolving.default-resolver'
test('custom resolver intercepts matching packages', async () => {
const customResolver: CustomResolver = {
canResolve: (wantedDependency: WantedDependency) => {
return wantedDependency.alias === 'test-package'
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
resolve: async (wantedDependency: WantedDependency, _opts: any) => {
return {
id: `custom:${wantedDependency.alias}@${wantedDependency.bareSpecifier}`,
resolution: {
type: 'directory',
directory: '/test/path',
},
}
},
}
const fetchFromRegistry = async (): Promise<Response> => new Response('')
const getAuthHeader = () => undefined
const { resolve } = createResolver(fetchFromRegistry, getAuthHeader, {
customResolvers: [customResolver],
cacheDir: '/tmp/test-cache',
offline: false,
preferOffline: false,
retry: {},
timeout: 60000,
storeDir: '.store',
registries: { default: 'https://registry.npmjs.org/' },
})
const result = await resolve(
{ alias: 'test-package', bareSpecifier: '1.0.0' },
{
lockfileDir: '/test',
projectDir: '/test',
preferredVersions: {},
}
)
expect(result.id).toBe('custom:test-package@1.0.0')
expect(result.resolvedVia).toBe('custom-resolver')
})
test('custom resolver with synchronous methods', async () => {
const customResolver: CustomResolver = {
// Synchronous support check
canResolve: (wantedDependency: WantedDependency) => {
return wantedDependency.alias!.startsWith('@sync/')
},
// Synchronous resolution
resolve: (wantedDependency: WantedDependency) => {
return {
id: `sync:${wantedDependency.alias}@${wantedDependency.bareSpecifier}`,
resolution: {
tarball: `file://${wantedDependency.alias}-${wantedDependency.bareSpecifier}.tgz`,
integrity: 'sha512-test',
},
}
},
}
const fetchFromRegistry = async (): Promise<Response> => new Response('')
const getAuthHeader = () => undefined
const { resolve } = createResolver(fetchFromRegistry, getAuthHeader, {
customResolvers: [customResolver],
cacheDir: '/tmp/test-cache',
offline: false,
preferOffline: false,
retry: {},
timeout: 60000,
storeDir: '.store',
registries: { default: 'https://registry.npmjs.org/' },
})
const result = await resolve(
{ alias: '@sync/test', bareSpecifier: '2.0.0' },
{
lockfileDir: '/test',
projectDir: '/test',
preferredVersions: {},
}
)
expect(result.id).toBe('sync:@sync/test@2.0.0')
expect(result.resolvedVia).toBe('custom-resolver')
})
test('multiple custom resolvers - first matching wins', async () => {
const resolver1: CustomResolver = {
canResolve: (wantedDependency) => wantedDependency.alias === 'shared-package',
resolve: () => ({
id: 'resolver-1:shared-package',
resolution: { tarball: 'file://resolver1.tgz', integrity: 'sha512-1' },
}),
}
const resolver2: CustomResolver = {
canResolve: (wantedDependency) => wantedDependency.alias === 'shared-package',
resolve: () => ({
id: 'resolver-2:shared-package',
resolution: { tarball: 'file://resolver2.tgz', integrity: 'sha512-2' },
}),
}
const fetchFromRegistry = async (): Promise<Response> => new Response('')
const getAuthHeader = () => undefined
const { resolve } = createResolver(fetchFromRegistry, getAuthHeader, {
customResolvers: [resolver1, resolver2], // Order matters
cacheDir: '/tmp/test-cache',
offline: false,
preferOffline: false,
retry: {},
timeout: 60000,
storeDir: '.store',
registries: { default: 'https://registry.npmjs.org/' },
})
const result = await resolve(
{ alias: 'shared-package', bareSpecifier: '1.0.0' },
{
lockfileDir: '/test',
projectDir: '/test',
preferredVersions: {},
}
)
// First custom resolver should win
expect(result.id).toBe('resolver-1:shared-package')
expect(result.resolvedVia).toBe('custom-resolver')
})
test('custom resolver error handling', async () => {
const customResolver: CustomResolver = {
canResolve: () => true,
resolve: () => {
throw new Error('Custom resolver failed')
},
}
const { resolve } = createResolver(async () => new Response(''), () => undefined, {
customResolvers: [customResolver],
cacheDir: '/tmp/test-cache',
offline: false,
preferOffline: false,
retry: {},
timeout: 60000,
storeDir: '.store',
registries: { default: 'https://registry.npmjs.org/' },
})
await expect(resolve({ alias: 'any', bareSpecifier: '1.0.0' }, { lockfileDir: '/test', projectDir: '/test', preferredVersions: {} })).rejects.toThrow('Custom resolver failed')
})
test('preferredVersions are passed to custom resolver', async () => {
const resolve = jest.fn<NonNullable<CustomResolver['resolve']>>(() => ({
id: 'test@1.0.0',
resolution: { tarball: 'file://test.tgz', integrity: 'sha512-test' },
}))
const customResolver: CustomResolver = {
canResolve: () => true,
resolve,
}
const { resolve: resolvePackage } = createResolver(async () => new Response(''), () => undefined, {
customResolvers: [customResolver],
cacheDir: '/tmp/test-cache',
offline: false,
preferOffline: false,
retry: {},
timeout: 60000,
storeDir: '.store',
registries: { default: 'https://registry.npmjs.org/' },
})
await resolvePackage(
{ alias: 'any', bareSpecifier: '1.0.0' },
{ lockfileDir: '/test', projectDir: '/test', preferredVersions: { any: { '1.0.0': 'version' } } as unknown as Record<string, Record<string, 'version' | 'range' | 'tag'>> }
)
expect(resolve).toHaveBeenCalledWith(
{ alias: 'any', bareSpecifier: '1.0.0' },
expect.objectContaining({
lockfileDir: '/test',
projectDir: '/test',
preferredVersions: { any: { '1.0.0': 'version' } },
})
)
})
test('custom resolver can intercept any protocol', async () => {
const customResolver: CustomResolver = {
canResolve: (wantedDependency: WantedDependency) => {
return wantedDependency.alias!.startsWith('custom-')
},
resolve: (wantedDependency: WantedDependency) => ({
id: `custom-handled:${wantedDependency.alias}@${wantedDependency.bareSpecifier}`,
resolution: {
type: 'custom:test',
directory: `/custom/${wantedDependency.alias}`,
},
}),
}
const { resolve } = createResolver(async () => new Response(''), () => undefined, {
customResolvers: [customResolver],
cacheDir: '/tmp/test-cache',
offline: false,
preferOffline: false,
retry: {},
timeout: 60000,
storeDir: '.store',
registries: { default: 'https://registry.npmjs.org/' },
})
const result = await resolve(
{ alias: 'custom-package', bareSpecifier: 'file:../some-path' },
{ lockfileDir: '/test', projectDir: '/test', preferredVersions: {} }
)
expect(result.resolvedVia).toBe('custom-resolver')
expect(result.id).toBe('custom-handled:custom-package@file:../some-path')
})
test('custom resolver falls through when not supported', async () => {
const customResolver: CustomResolver = {
canResolve: (wantedDependency: WantedDependency) => {
return wantedDependency.alias!.startsWith('custom-')
},
resolve: (wantedDependency: WantedDependency) => ({
id: `custom:${wantedDependency.alias}@${wantedDependency.bareSpecifier}`,
resolution: { type: 'custom:test', directory: '/custom' },
}),
}
const { resolve } = createResolver(async () => new Response(''), () => undefined, {
customResolvers: [customResolver],
cacheDir: '/tmp/test-cache',
offline: false,
preferOffline: false,
retry: {},
timeout: 60000,
storeDir: '.store',
registries: { default: 'https://registry.npmjs.org/' },
})
await expect(
resolve(
{ alias: 'regular-package', bareSpecifier: 'file:../nonexistent' },
{ lockfileDir: '/test', projectDir: '/test', preferredVersions: {} }
)
).rejects.toThrow()
})
test('custom resolver can override npm registry resolution', async () => {
const npmStyleResolver: CustomResolver = {
canResolve: (wantedDependency) => {
return !wantedDependency.bareSpecifier!.includes(':')
},
resolve: (wantedDependency) => ({
id: `custom-registry:${wantedDependency.alias}@${wantedDependency.bareSpecifier}`,
resolution: {
tarball: `https://custom-registry.com/${wantedDependency.alias}/-/${wantedDependency.alias}-${wantedDependency.bareSpecifier}.tgz`,
integrity: 'sha512-custom',
},
}),
}
const { resolve } = createResolver(async () => new Response(''), () => undefined, {
customResolvers: [npmStyleResolver],
cacheDir: '/tmp/test-cache',
offline: false,
preferOffline: false,
retry: {},
timeout: 60000,
storeDir: '.store',
registries: { default: 'https://registry.npmjs.org/' },
})
const result = await resolve(
{ alias: 'express', bareSpecifier: '^4.0.0' },
{ lockfileDir: '/test', projectDir: '/test', preferredVersions: {} }
)
expect(result.resolvedVia).toBe('custom-resolver')
expect('tarball' in result.resolution && result.resolution.tarball).toContain('custom-registry.com')
})
// Fetch phase custom fetcher tests - showing complete fetcher replacements
test('custom custom fetcher: reuse local tarball fetcher', async () => {
// This demonstrates how a custom resolver can reuse pnpm's local tarball fetcher
// for a custom protocol like "company-local:package-name"
const localTarballResolver: CustomResolver = {
canResolve: (wantedDependency) => wantedDependency.alias!.startsWith('company-local:'),
resolve: (wantedDependency) => {
const actualName = wantedDependency.alias!.replace('company-local:', '')
return {
id: wantedDependency.alias!,
resolution: {
type: 'custom:local',
localPath: `/company/tarballs/${actualName}-${wantedDependency.bareSpecifier}.tgz`,
},
}
},
}
const { resolve } = createResolver(async () => new Response(''), () => undefined, {
customResolvers: [localTarballResolver],
cacheDir: '/tmp/test-cache',
offline: false,
preferOffline: false,
retry: {},
timeout: 60000,
registries: { default: 'https://registry.npmjs.org/' },
storeDir: '.store',
})
const result = await resolve(
{ alias: 'company-local:my-package', bareSpecifier: '1.0.0' },
{ lockfileDir: '/test', projectDir: '/test', preferredVersions: {} }
)
expect(result.resolvedVia).toBe('custom-resolver')
expect(result.resolution).toHaveProperty('type', 'custom:local')
})
test('custom custom fetcher: reuse remote tarball downloader', async () => {
// This demonstrates fetching from a custom CDN using pnpm's download utilities
// for a custom protocol like "cdn:package-name"
const cdnResolver: CustomResolver = {
canResolve: (wantedDependency) => wantedDependency.alias!.startsWith('cdn:'),
resolve: (wantedDependency) => {
const actualName = wantedDependency.alias!.replace('cdn:', '')
return {
id: wantedDependency.alias!,
resolution: {
type: 'custom:cdn',
cdnUrl: `https://cdn.example.com/packages/${actualName}/${wantedDependency.bareSpecifier}/${actualName}-${wantedDependency.bareSpecifier}.tgz`,
},
}
},
}
const { resolve } = createResolver(async () => new Response(''), () => undefined, {
customResolvers: [cdnResolver],
cacheDir: '/tmp/test-cache',
offline: false,
preferOffline: false,
retry: {},
timeout: 60000,
registries: { default: 'https://registry.npmjs.org/' },
storeDir: '.store',
})
const result = await resolve(
{ alias: 'cdn:awesome-lib', bareSpecifier: '2.0.0' },
{ lockfileDir: '/test', projectDir: '/test', preferredVersions: {} }
)
expect(result.resolvedVia).toBe('custom-resolver')
expect(result.resolution).toHaveProperty('type', 'custom:cdn')
})
test('custom custom fetcher: wrap npm registry with custom logic', async () => {
// This demonstrates wrapping/enhancing standard npm registry resolution and fetching
// for a protocol like "private-npm:package-name" that uses private registry
const privateNpmResolver: CustomResolver = {
canResolve: (wantedDependency) => wantedDependency.alias!.startsWith('private-npm:'),
resolve: async (wantedDependency, _opts) => {
const actualName = wantedDependency.alias!.replace('private-npm:', '')
// In a real implementation, you'd fetch from your private registry here
// For this test, we mock the registry response
return {
id: `private-npm:${actualName}@${wantedDependency.bareSpecifier}`,
resolution: {
tarball: `https://private-registry.company.com/${actualName}/-/${actualName}-${wantedDependency.bareSpecifier}.tgz`,
integrity: 'sha512-mock-integrity',
registry: 'https://private-registry.company.com/',
},
}
},
}
const { resolve } = createResolver(async () => new Response(''), () => undefined, {
customResolvers: [privateNpmResolver],
cacheDir: '/tmp/test-cache',
offline: false,
preferOffline: false,
retry: {},
timeout: 60000,
registries: { default: 'https://registry.npmjs.org/' },
storeDir: '.store',
})
const result = await resolve(
{ alias: 'private-npm:company-utils', bareSpecifier: '3.0.0' },
{ lockfileDir: '/test', projectDir: '/test', preferredVersions: {} }
)
expect(result.resolvedVia).toBe('custom-resolver')
expect('tarball' in result.resolution && result.resolution.tarball).toContain('private-registry.company.com')
})
test('custom resolver receives currentPkg when provided', async () => {
let receivedCurrentPkg: unknown = null
const customResolver: CustomResolver = {
canResolve: (wantedDependency: WantedDependency) => {
return wantedDependency.alias === 'test-package'
},
resolve: async (wantedDependency: WantedDependency, opts) => {
receivedCurrentPkg = opts.currentPkg
// If currentPkg is provided, return existing resolution
if (opts.currentPkg) {
return {
id: opts.currentPkg.id,
resolution: opts.currentPkg.resolution,
}
}
return {
id: `custom:${wantedDependency.alias}@1.0.0`,
resolution: {
type: 'directory',
directory: '/test/path',
},
}
},
}
const fetchFromRegistry = async (): Promise<Response> => new Response('')
const getAuthHeader = () => undefined
const { resolve } = createResolver(fetchFromRegistry, getAuthHeader, {
customResolvers: [customResolver],
cacheDir: '/tmp/test-cache',
offline: false,
preferOffline: false,
retry: {},
timeout: 60000,
registries: { default: 'https://registry.npmjs.org/' },
})
// First call without currentPkg
const result1 = await resolve(
{ alias: 'test-package', bareSpecifier: '1.0.0' },
{
lockfileDir: '/test',
projectDir: '/test',
preferredVersions: {},
}
)
expect(result1.id).toBe('custom:test-package@1.0.0')
expect(receivedCurrentPkg).toBeUndefined()
// Second call with currentPkg
const existingResolution = {
type: 'directory' as const,
directory: '/existing/path',
}
const result2 = await resolve(
{ alias: 'test-package', bareSpecifier: '1.0.0' },
{
lockfileDir: '/test',
projectDir: '/test',
preferredVersions: {},
currentPkg: {
id: 'existing:test-package@1.0.0' as any, // eslint-disable-line @typescript-eslint/no-explicit-any
resolution: existingResolution,
},
}
)
expect(receivedCurrentPkg).toBeTruthy()
expect((receivedCurrentPkg as any).id).toBe('existing:test-package@1.0.0') // eslint-disable-line @typescript-eslint/no-explicit-any
expect(result2.id).toBe('existing:test-package@1.0.0')
expect(result2.resolution).toBe(existingResolution)
})