Add TypeScript ports of the perfectionist rules that the Rust workspace enforces through dylint.toml to @pnpm/eslint-config, with the same limits, and fix every finding that needs no refactor. The size and shape rules (function length, cognitive complexity, nesting, local bindings, file length) are enforced for packages that already meet them. The packages that do not are listed in PENDING_SIZE_AND_SHAPE_REFACTOR in eslint.config.mjs and are refactored in follow-up changes. The custom rules mirror perfectionist's semantics rather than reusing sonarjs or max-lines-per-function, so that one limit means the same thing in both codebases: a closure is a level of the body that contains it, a nested function declaration is measured on its own, and a test-framework callback plays the role of a #[test] function. Bugs found and fixed along the way: - a registry error body cut at exactly 64 KiB lost its truncation marker - removing a dependency could leave dangling .bin shims - constructor/__proto__ names crashed the projects graph, were skipped by the injected-deps syncer, and were dropped from writeWantedLockfile's returned lockfile The CI chunk runner learns to run the node:test suite of the ESLint config package.
488 lines
16 KiB
TypeScript
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 -- the test resolver ignores its options
|
|
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',
|
|
registriesByScope: { 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',
|
|
registriesByScope: { 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',
|
|
registriesByScope: { 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',
|
|
registriesByScope: { 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',
|
|
registriesByScope: { 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',
|
|
registriesByScope: { 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',
|
|
registriesByScope: { 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',
|
|
registriesByScope: { 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,
|
|
registriesByScope: { 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,
|
|
registriesByScope: { 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,
|
|
registriesByScope: { 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,
|
|
registriesByScope: { 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 -- a plain string stands in for the branded package id
|
|
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 -- the captured value is untyped test state
|
|
expect(result2.id).toBe('existing:test-package@1.0.0')
|
|
expect(result2.resolution).toBe(existingResolution)
|
|
})
|