mirror of
https://github.com/pnpm/pnpm.git
synced 2026-03-27 11:31:45 -04:00
## 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
494 lines
16 KiB
TypeScript
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)
|
|
})
|