Files
pnpm/resolving/default-resolver/test/customResolver.ts
Zoltan Kochan 4a36b9a110 refactor: rename internal packages to @pnpm/<domain>.<leaf> convention (#10997)
## Summary

Rename all internal packages so their npm names follow the `@pnpm/<domain>.<leaf>` convention, matching their directory structure. Also rename directories to remove redundancy and improve clarity.

### Bulk rename (94 packages)

All `@pnpm/` packages now derive their name from their directory path using dot-separated segments. Exceptions: `packages/`, `__utils__/`, and `pnpm/artifacts/` keep leaf names only.

### Directory renames (removing redundant prefixes)

- `cli/cli-meta` → `cli/meta`, `cli/cli-utils` → `cli/utils`
- `config/config` → `config/reader`, `config/config-writer` → `config/writer`
- `fetching/fetching-types` → `fetching/types`
- `lockfile/lockfile-to-pnp` → `lockfile/to-pnp`
- `store/store-connection-manager` → `store/connection-manager`
- `store/store-controller-types` → `store/controller-types`
- `store/store-path` → `store/path`

### Targeted renames (clarity improvements)

- `deps/dependency-path` → `deps/path` (`@pnpm/deps.path`)
- `deps/calc-dep-state` → `deps/graph-hasher` (`@pnpm/deps.graph-hasher`)
- `deps/inspection/dependencies-hierarchy` → `deps/inspection/tree-builder` (`@pnpm/deps.inspection.tree-builder`)
- `bins/link-bins` → `bins/linker`, `bins/remove-bins` → `bins/remover`, `bins/package-bins` → `bins/resolver`
- `installing/get-context` → `installing/context`
- `store/package-store` → `store/controller`
- `pkg-manifest/manifest-utils` → `pkg-manifest/utils`

### Manifest reader/writer renames

- `workspace/read-project-manifest` → `workspace/project-manifest-reader` (`@pnpm/workspace.project-manifest-reader`)
- `workspace/write-project-manifest` → `workspace/project-manifest-writer` (`@pnpm/workspace.project-manifest-writer`)
- `workspace/read-manifest` → `workspace/workspace-manifest-reader` (`@pnpm/workspace.workspace-manifest-reader`)
- `workspace/manifest-writer` → `workspace/workspace-manifest-writer` (`@pnpm/workspace.workspace-manifest-writer`)

### Workspace package renames

- `workspace/find-packages` → `workspace/projects-reader`
- `workspace/find-workspace-dir` → `workspace/root-finder`
- `workspace/resolve-workspace-range` → `workspace/range-resolver`
- `workspace/filter-packages-from-dir` merged into `workspace/filter-workspace-packages` → `workspace/projects-filter`

### Domain moves

- `pkg-manifest/read-project-manifest` → `workspace/project-manifest-reader`
- `pkg-manifest/write-project-manifest` → `workspace/project-manifest-writer`
- `pkg-manifest/exportable-manifest` → `releasing/exportable-manifest`

### Scope

- 1206 files changed
- Updated: package.json names/deps, TypeScript imports, tsconfig references, changeset files, renovate.json, test fixtures, import ordering
2026-03-17 21:50:40 +01:00

494 lines
16 KiB
TypeScript

/// <reference path="../../../__typings__/index.d.ts"/>
import { jest } from '@jest/globals'
import type { CustomResolver, WantedDependency } from '@pnpm/hooks.types'
import { createResolver } from '@pnpm/resolving.default-resolver'
import { Response } from 'node-fetch'
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],
rawConfig: {},
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],
rawConfig: {},
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
rawConfig: {},
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],
rawConfig: {},
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(() => ({
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],
rawConfig: {},
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' }, { 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],
rawConfig: {},
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],
rawConfig: {},
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],
rawConfig: {},
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],
rawConfig: {},
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],
rawConfig: {},
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],
rawConfig: {},
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],
rawConfig: {},
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)
})