Files
pnpm/fetching/pick-fetcher/test/customFetch.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

565 lines
19 KiB
TypeScript

import path from 'node:path'
import { jest } from '@jest/globals'
import type { Fetchers, FetchFunction, FetchOptions } from '@pnpm/fetching.fetcher-base'
import { pickFetcher } from '@pnpm/fetching.pick-fetcher'
import { createTarballFetcher } from '@pnpm/fetching.tarball-fetcher'
import type { CustomFetcher } from '@pnpm/hooks.types'
import { createFetchFromRegistry } from '@pnpm/network.fetch'
import type { AtomicResolution } from '@pnpm/resolving.resolver-base'
import type { Cafs } from '@pnpm/store.cafs-types'
import { createCafsStore } from '@pnpm/store.create-cafs-store'
import { StoreIndex } from '@pnpm/store.index'
import { fixtures } from '@pnpm/test-fixtures'
import nock from 'nock'
import { temporaryDirectory } from 'tempy'
const f = fixtures(import.meta.dirname)
const storeIndex = new StoreIndex(temporaryDirectory())
afterAll(() => {
storeIndex.close()
})
// Test helpers to reduce type casting
function createMockFetchers (partial: Partial<Fetchers> = {}): Fetchers {
const noop = jest.fn() as FetchFunction
return {
localTarball: noop,
remoteTarball: noop,
gitHostedTarball: noop,
directory: noop as any, // eslint-disable-line @typescript-eslint/no-explicit-any
git: noop as any, // eslint-disable-line @typescript-eslint/no-explicit-any
binary: noop as any, // eslint-disable-line @typescript-eslint/no-explicit-any
...partial,
}
}
function createMockCafs (partial: Partial<Cafs> = {}): Cafs {
return {
addFilesFromDir: jest.fn(),
addFilesFromTarball: jest.fn() as any, // eslint-disable-line @typescript-eslint/no-explicit-any
...partial,
} as Cafs
}
function createMockResolution (resolution: Partial<AtomicResolution> & Record<string, any>): any { // eslint-disable-line @typescript-eslint/no-explicit-any
return resolution
}
function createMockFetchOptions (opts: Partial<FetchOptions> = {}): any { // eslint-disable-line @typescript-eslint/no-explicit-any
return opts
}
function createMockCustomFetcher (
canFetch: CustomFetcher['canFetch'],
fetch: CustomFetcher['fetch']
): CustomFetcher {
return { canFetch, fetch }
}
/**
* These tests demonstrate realistic custom fetcher implementations and verify
* that the custom fetcher API works correctly for common use cases.
*/
describe('custom fetcher implementation examples', () => {
describe('basic custom fetcher contract', () => {
test('should successfully return FetchResult with manifest and filesIndex', async () => {
const mockManifest = { name: 'test-package', version: '1.0.0' }
const mockFilesMap = new Map([['package.json', '/path/to/store/package.json']])
const customFetcher = createMockCustomFetcher(
() => true,
async () => ({
filesMap: mockFilesMap,
manifest: mockManifest,
requiresBuild: false,
})
)
const fetcher = await pickFetcher(
createMockFetchers(),
createMockResolution({ tarball: 'http://example.com/package.tgz' }),
{ customFetchers: [customFetcher], packageId: 'test-package@1.0.0' }
)
const result = await fetcher(
createMockCafs(),
createMockResolution({ tarball: 'http://example.com/package.tgz' }),
createMockFetchOptions()
)
expect(result.manifest).toEqual(mockManifest)
expect(result.filesMap).toEqual(mockFilesMap)
expect(result.requiresBuild).toBe(false)
})
test('should handle requiresBuild flag correctly', async () => {
const customFetcher = createMockCustomFetcher(
() => true,
async () => ({
filesMap: new Map(),
manifest: { name: 'pkg', version: '1.0.0', scripts: { install: 'node install.js' } },
requiresBuild: true,
})
)
const fetcher = await pickFetcher(
createMockFetchers(),
createMockResolution({ tarball: 'http://example.com/package.tgz' }),
{ customFetchers: [customFetcher], packageId: 'pkg@1.0.0' }
)
const result = await fetcher(
createMockCafs(),
createMockResolution({ tarball: 'http://example.com/package.tgz' }),
createMockFetchOptions()
)
expect(result.requiresBuild).toBe(true)
})
test('should propagate errors from custom fetcher', async () => {
const customFetcher = createMockCustomFetcher(
() => true,
async () => {
throw new Error('Network error during fetch')
}
)
const fetcher = await pickFetcher(
createMockFetchers(),
createMockResolution({ tarball: 'http://example.com/package.tgz' }),
{ customFetchers: [customFetcher], packageId: 'pkg@1.0.0' }
)
await expect(
fetcher(
createMockCafs(),
createMockResolution({ tarball: 'http://example.com/package.tgz' }),
createMockFetchOptions()
)
).rejects.toThrow('Network error during fetch')
})
test('should pass CAFS to custom fetcher for file operations', async () => {
let receivedCafs: Cafs | null = null
const customFetcher = createMockCustomFetcher(
() => true,
async (cafs) => {
receivedCafs = cafs
return {
filesMap: new Map(),
manifest: { name: 'pkg', version: '1.0.0' },
requiresBuild: false,
}
}
)
const fetcher = await pickFetcher(
createMockFetchers(),
createMockResolution({ tarball: 'http://example.com/package.tgz' }),
{ customFetchers: [customFetcher], packageId: 'pkg@1.0.0' }
)
const mockCafs = createMockCafs({ addFilesFromTarball: jest.fn() as any }) // eslint-disable-line @typescript-eslint/no-explicit-any
await fetcher(
mockCafs,
createMockResolution({ tarball: 'http://example.com/package.tgz' }),
createMockFetchOptions()
)
expect(receivedCafs).toBe(mockCafs)
})
test('should pass progress callbacks to custom fetcher', async () => {
const onStartFn = jest.fn()
const onProgressFn = jest.fn()
const customFetcher = createMockCustomFetcher(
() => true,
async (_cafs, _resolution, opts) => {
// Custom fetcher can call progress callbacks
opts.onStart?.(100, 1)
;(opts.onProgress as any)?.({ done: 50, total: 100 }) // eslint-disable-line @typescript-eslint/no-explicit-any
return {
filesMap: new Map(),
manifest: { name: 'pkg', version: '1.0.0' },
requiresBuild: false,
}
}
)
const fetcher = await pickFetcher(
createMockFetchers(),
createMockResolution({ tarball: 'http://example.com/package.tgz' }),
{ customFetchers: [customFetcher], packageId: 'pkg@1.0.0' }
)
await fetcher(
createMockCafs(),
createMockResolution({ tarball: 'http://example.com/package.tgz' }),
createMockFetchOptions({ onStart: onStartFn, onProgress: onProgressFn })
)
expect(onStartFn).toHaveBeenCalledWith(100, 1)
expect(onProgressFn).toHaveBeenCalledWith({ done: 50, total: 100 })
})
test('should work with custom resolution types', async () => {
const customResolution = createMockResolution({
type: 'custom:cdn',
cdnUrl: 'https://cdn.example.com/pkg.tgz',
})
const customFetcher = createMockCustomFetcher(
(_pkgId, resolution) => resolution.type === 'custom:cdn',
async (_cafs, resolution) => {
// Custom fetcher can access custom resolution fields
expect(resolution.type).toBe('custom:cdn')
expect((resolution as any).cdnUrl).toBe('https://cdn.example.com/pkg.tgz') // eslint-disable-line @typescript-eslint/no-explicit-any
return {
filesMap: new Map(),
manifest: { name: 'pkg', version: '1.0.0' },
requiresBuild: false,
}
}
)
const fetcher = await pickFetcher(
createMockFetchers(),
customResolution,
{ customFetchers: [customFetcher], packageId: 'pkg@1.0.0' }
)
await fetcher(createMockCafs(), customResolution, createMockFetchOptions())
})
test('should allow custom fetcher.fetch to return partial manifest', async () => {
const customFetcher = createMockCustomFetcher(
() => true,
async () => ({
filesMap: new Map(),
requiresBuild: false,
// Manifest is optional in FetchResult
})
)
const fetcher = await pickFetcher(
createMockFetchers(),
createMockResolution({ tarball: 'http://example.com/package.tgz' }),
{ customFetchers: [customFetcher], packageId: 'pkg@1.0.0' }
)
const result = await fetcher(
createMockCafs(),
createMockResolution({ tarball: 'http://example.com/package.tgz' }),
createMockFetchOptions()
)
expect(result.manifest).toBeUndefined()
expect(result.filesMap).toBeDefined()
})
})
describe('delegating to tarball fetcher', () => {
const registry = 'http://localhost:4873/'
const tarballPath = f.find('babel-helper-hoist-variables-6.24.1.tgz')
const tarballIntegrity = 'sha1-HssnaJydJVE+rbyZFKc/VAi+enY='
test('custom fetcher can delegate to remoteTarball fetcher', async () => {
const scope = nock(registry)
.get('/custom-pkg.tgz')
.replyWithFile(200, tarballPath, {
'Content-Length': '1279',
})
const storeDir = temporaryDirectory()
const cafs = createCafsStore(storeDir)
const filesIndexFile = path.join(storeDir, 'index.json')
// Create standard fetchers to pass to custom fetcher
const fetchFromRegistry = createFetchFromRegistry({})
const tarballFetchers = createTarballFetcher(
fetchFromRegistry,
() => undefined,
{ rawConfig: {}, storeIndex }
)
// Custom fetcher that maps custom URLs to tarballs
const customFetcher = createMockCustomFetcher(
(_pkgId, resolution) => resolution.type === 'custom:url' && Boolean((resolution as any).customUrl), // eslint-disable-line @typescript-eslint/no-explicit-any
async (cafs, resolution, opts, fetchers) => {
// Map custom resolution to tarball resolution
const tarballResolution = {
tarball: (resolution as any).customUrl, // eslint-disable-line @typescript-eslint/no-explicit-any
integrity: tarballIntegrity,
}
// Delegate to standard tarball fetcher (passed via fetchers parameter)
return fetchers.remoteTarball(cafs, tarballResolution, opts)
}
)
const customResolution = createMockResolution({
type: 'custom:url',
customUrl: `${registry}custom-pkg.tgz`,
})
const fetcher = await pickFetcher(
tarballFetchers as Fetchers,
customResolution,
{ customFetchers: [customFetcher], packageId: 'custom-pkg@1.0.0' }
)
const result = await fetcher(
cafs,
customResolution,
createMockFetchOptions({ filesIndexFile, lockfileDir: process.cwd() })
)
expect(result.filesMap.get('package.json')).toBeTruthy()
expect(scope.isDone()).toBeTruthy()
})
test('custom fetcher can delegate to localTarball fetcher', async () => {
const storeDir = temporaryDirectory()
const cafs = createCafsStore(storeDir)
const filesIndexFile = path.join(storeDir, 'index.json')
const fetchFromRegistry = createFetchFromRegistry({})
const tarballFetchers = createTarballFetcher(
fetchFromRegistry,
() => undefined,
{ rawConfig: {}, storeIndex }
)
// Custom fetcher that maps custom local paths to tarballs
const customFetcher = createMockCustomFetcher(
(_pkgId, resolution) => resolution.type === 'custom:local' && Boolean((resolution as any).localPath), // eslint-disable-line @typescript-eslint/no-explicit-any
async (cafs, resolution, opts, fetchers) => {
const tarballResolution = {
tarball: `file:${(resolution as any).localPath}`, // eslint-disable-line @typescript-eslint/no-explicit-any
integrity: tarballIntegrity,
}
return fetchers.localTarball(cafs, tarballResolution, opts)
}
)
const customResolution = createMockResolution({
type: 'custom:local',
localPath: tarballPath,
})
const fetcher = await pickFetcher(
tarballFetchers as Fetchers,
customResolution,
{ customFetchers: [customFetcher], packageId: 'local-pkg@1.0.0' }
)
const result = await fetcher(
cafs,
customResolution,
createMockFetchOptions({ filesIndexFile, lockfileDir: process.cwd() })
)
expect(result.filesMap.get('package.json')).toBeTruthy()
})
test('custom fetcher can transform resolution before delegating to tarball fetcher', async () => {
const scope = nock(registry)
.get('/transformed-pkg.tgz')
.replyWithFile(200, tarballPath, {
'Content-Length': '1279',
})
const storeDir = temporaryDirectory()
const cafs = createCafsStore(storeDir)
const filesIndexFile = path.join(storeDir, 'index.json')
const fetchFromRegistry = createFetchFromRegistry({})
const tarballFetchers = createTarballFetcher(
fetchFromRegistry,
() => undefined,
{ rawConfig: {}, storeIndex }
)
// Custom fetcher that transforms custom resolution to tarball URL
const customFetcher = createMockCustomFetcher(
(_pkgId, resolution) => resolution.type === 'custom:registry',
async (cafs, resolution, opts, fetchers) => {
// Transform custom registry format to standard tarball URL
const tarballUrl = `${registry}${(resolution as any).packageName}.tgz` // eslint-disable-line @typescript-eslint/no-explicit-any
const tarballResolution = {
tarball: tarballUrl,
integrity: tarballIntegrity,
}
return fetchers.remoteTarball(cafs, tarballResolution, opts)
}
)
const customResolution = createMockResolution({
type: 'custom:registry',
packageName: 'transformed-pkg',
})
const fetcher = await pickFetcher(
tarballFetchers as Fetchers,
customResolution,
{ customFetchers: [customFetcher], packageId: 'transformed-pkg@1.0.0' }
)
const result = await fetcher(
cafs,
customResolution,
createMockFetchOptions({ filesIndexFile, lockfileDir: process.cwd() })
)
expect(result.filesMap.get('package.json')).toBeTruthy()
expect(scope.isDone()).toBeTruthy()
})
test('custom fetcher can use gitHostedTarball fetcher for custom git URLs', async () => {
const storeDir = temporaryDirectory()
const cafs = createCafsStore(storeDir)
const filesIndexFile = path.join(storeDir, 'index.json')
const fetchFromRegistry = createFetchFromRegistry({})
const tarballFetchers = createTarballFetcher(
fetchFromRegistry,
() => undefined,
{ rawConfig: {}, storeIndex, ignoreScripts: true }
)
// Custom fetcher that maps custom git resolution to git-hosted tarball
const customFetcher = createMockCustomFetcher(
(_pkgId, resolution) => resolution.type === 'custom:git',
async (cafs, resolution, opts, fetchers) => {
// Map custom git resolution to GitHub codeload URL
const tarballResolution = {
tarball: `https://codeload.github.com/${(resolution as any).repo}/tar.gz/${(resolution as any).commit}`, // eslint-disable-line @typescript-eslint/no-explicit-any
}
return fetchers.gitHostedTarball(cafs, tarballResolution, opts)
}
)
const customResolution = createMockResolution({
type: 'custom:git',
repo: 'sveltejs/action-deploy-docs',
commit: 'a65fbf5a90f53c9d72fed4daaca59da50f074355',
})
const fetcher = await pickFetcher(
tarballFetchers as Fetchers,
customResolution,
{ customFetchers: [customFetcher], packageId: 'git-pkg@1.0.0' }
)
const result = await fetcher(
cafs,
customResolution,
createMockFetchOptions({ filesIndexFile, lockfileDir: process.cwd() })
)
expect(result.filesMap).toBeTruthy()
})
})
describe('custom fetch implementations', () => {
test('custom fetcher can implement custom caching logic', async () => {
const fetchCalls: number[] = []
const cache = new Map<string, any>() // eslint-disable-line @typescript-eslint/no-explicit-any
const customFetcher = createMockCustomFetcher(
(_pkgId, resolution) => resolution.type === 'custom:cached',
async (_cafs, resolution) => {
fetchCalls.push(Date.now())
// Check cache first
const cacheKey = `${(resolution as any).url}@${(resolution as any).version}` // eslint-disable-line @typescript-eslint/no-explicit-any
if (cache.has(cacheKey)) {
return cache.get(cacheKey)
}
// Simulate fetch
const result = {
filesMap: new Map([['package.json', '/store/pkg.json']]),
manifest: { name: 'cached-pkg', version: (resolution as any).version }, // eslint-disable-line @typescript-eslint/no-explicit-any
}
cache.set(cacheKey, result)
return result
}
)
const customResolution = createMockResolution({
type: 'custom:cached',
url: 'https://cache.example.com/pkg',
version: '1.0.0',
})
const fetcher = await pickFetcher(
createMockFetchers(),
customResolution,
{ customFetchers: [customFetcher], packageId: 'cached-pkg@1.0.0' }
)
// First fetch - should hit the fetch logic
const result1 = await fetcher(createMockCafs(), customResolution, createMockFetchOptions())
// Second fetch - should use cache
const result2 = await fetcher(createMockCafs(), customResolution, createMockFetchOptions())
expect(result1).toBe(result2)
expect(fetchCalls).toHaveLength(2) // Fetcher called twice, but cache hit on second call
})
test('custom fetcher can implement authentication and token refresh', async () => {
let authToken = 'initial-token'
const authCalls: string[] = []
const customFetcher = createMockCustomFetcher(
(_pkgId, resolution) => resolution.type === 'custom:auth',
async () => {
authCalls.push(authToken)
// Simulate token refresh on 401
if (authToken === 'initial-token') {
authToken = 'refreshed-token'
}
return {
filesMap: new Map(),
manifest: { name: 'auth-pkg', version: '1.0.0' },
requiresBuild: false,
authToken, // Could store for future use
}
}
)
const customResolution = createMockResolution({
type: 'custom:auth',
url: 'https://secure.example.com/pkg',
})
const fetcher = await pickFetcher(
createMockFetchers(),
customResolution,
{ customFetchers: [customFetcher], packageId: 'auth-pkg@1.0.0' }
)
const result = await fetcher(createMockCafs(), customResolution, createMockFetchOptions())
expect(authCalls).toEqual(['initial-token'])
expect((result as any).authToken).toBe('refreshed-token') // eslint-disable-line @typescript-eslint/no-explicit-any
})
})
})