diff --git a/.changeset/fix-named-registry-vs-local-resolver.md b/.changeset/fix-named-registry-vs-local-resolver.md new file mode 100644 index 0000000000..111d705c55 --- /dev/null +++ b/.changeset/fix-named-registry-vs-local-resolver.md @@ -0,0 +1,6 @@ +--- +"@pnpm/resolving.default-resolver": patch +"pnpm": patch +--- + +Fixed `pnpm add :@scope/pkg` for [named registries](https://github.com/pnpm/pnpm/pull/11324). The local resolver was claiming any specifier containing `/` as a local directory, so `pnpm add bit:@teambit/bit` (with `bit` configured under `namedRegistries`) installed a bogus link to `bit:@teambit/bit/` instead of resolving from the configured registry. The local resolver now runs after the named-registry resolver in the resolution chain. diff --git a/.changeset/split-local-resolver.md b/.changeset/split-local-resolver.md new file mode 100644 index 0000000000..7840c0242b --- /dev/null +++ b/.changeset/split-local-resolver.md @@ -0,0 +1,5 @@ +--- +"@pnpm/resolving.local-resolver": major +--- + +Replaced the `resolveFromLocal` export with two narrower exports: `resolveFromLocalScheme` (handles `file:`/`link:`/`workspace:`/`path:`) and `resolveFromLocalPath` (path-shape match by tarball extension or filesystem characters). diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7596bbb074..b1f4659b64 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8422,6 +8422,15 @@ importers: '@pnpm/store.cafs-types': specifier: workspace:* version: link:../../store/cafs-types + '@pnpm/testing.mock-agent': + specifier: workspace:* + version: link:../../testing/mock-agent + load-json-file: + specifier: 'catalog:' + version: 7.0.1 + tempy: + specifier: 'catalog:' + version: 3.0.0 resolving/git-resolver: dependencies: diff --git a/resolving/default-resolver/package.json b/resolving/default-resolver/package.json index 9b1979c11b..3817de3608 100644 --- a/resolving/default-resolver/package.json +++ b/resolving/default-resolver/package.json @@ -52,7 +52,10 @@ "@pnpm/fetching.tarball-fetcher": "workspace:*", "@pnpm/network.fetch": "workspace:*", "@pnpm/resolving.default-resolver": "workspace:*", - "@pnpm/store.cafs-types": "workspace:*" + "@pnpm/store.cafs-types": "workspace:*", + "@pnpm/testing.mock-agent": "workspace:*", + "load-json-file": "catalog:", + "tempy": "catalog:" }, "engines": { "node": ">=22.13" diff --git a/resolving/default-resolver/src/index.ts b/resolving/default-resolver/src/index.ts index a305aa2673..0dfe134164 100644 --- a/resolving/default-resolver/src/index.ts +++ b/resolving/default-resolver/src/index.ts @@ -5,7 +5,7 @@ import { PnpmError } from '@pnpm/error' import type { FetchFromRegistry, GetAuthHeader } from '@pnpm/fetching.types' import { checkCustomResolverCanResolve, type CustomResolver } from '@pnpm/hooks.types' import { createGitResolver, type GitResolveResult } from '@pnpm/resolving.git-resolver' -import { type LocalResolveResult, resolveFromLocal } from '@pnpm/resolving.local-resolver' +import { type LocalResolveResult, resolveFromLocalPath, resolveFromLocalScheme } from '@pnpm/resolving.local-resolver' import { createNpmResolver, type JsrResolveResult, @@ -95,9 +95,9 @@ export function createResolver ( ): { resolve: DefaultResolver, clearCache: () => void } { const { resolveFromNpm, resolveFromJsr, resolveFromNamedRegistry, clearCache } = createNpmResolver(fetchFromRegistry, getAuthHeader, pnpmOpts) const resolveFromGit = createGitResolver(pnpmOpts) - const _resolveFromLocal = resolveFromLocal.bind(null, { - preserveAbsolutePaths: pnpmOpts.preserveAbsolutePaths, - }) + const localCtx = { preserveAbsolutePaths: pnpmOpts.preserveAbsolutePaths } + const _resolveFromLocalScheme = resolveFromLocalScheme.bind(null, localCtx) + const _resolveFromLocalPath = resolveFromLocalPath.bind(null, localCtx) const _resolveNodeRuntime = resolveNodeRuntime.bind(null, { fetchFromRegistry, offline: pnpmOpts.offline, nodeDownloadMirrors: pnpmOpts.nodeDownloadMirrors }) const _resolveDenoRuntime = resolveDenoRuntime.bind(null, { fetchFromRegistry, offline: pnpmOpts.offline, resolveFromNpm }) const _resolveBunRuntime = resolveBunRuntime.bind(null, { fetchFromRegistry, offline: pnpmOpts.offline, resolveFromNpm }) @@ -112,16 +112,19 @@ export function createResolver ( (wantedDependency.bareSpecifier && ( await resolveFromGit(wantedDependency as { bareSpecifier: string }, opts) ?? await resolveFromTarball(fetchFromRegistry, wantedDependency as { bareSpecifier: string }) ?? - await _resolveFromLocal(wantedDependency as { bareSpecifier: string }, opts) + await _resolveFromLocalScheme(wantedDependency as { bareSpecifier: string }, opts) )) ?? await _resolveNodeRuntime(wantedDependency, opts) ?? await _resolveDenoRuntime(wantedDependency, opts) ?? await _resolveBunRuntime(wantedDependency, opts) ?? - // Named-registry resolution runs last so that built-in schemes - // (`npm:`, `jsr:`, `git:`/`github:`/`gitlab:`/…, `file:`, `link:`, - // tarball URLs, etc.) are always claimed by their dedicated resolver - // before a user-configured alias gets a chance to shadow them. - await resolveFromNamedRegistry(wantedDependency, opts as ResolveFromNpmOptions) + // Named-registry runs between the explicit local schemes above and the + // path-shape match below, so `:@scope/pkg` reaches the configured + // registry while a colliding `file:`/`link:`/`workspace:` alias cannot + // hijack the built-in protocols. + await resolveFromNamedRegistry(wantedDependency, opts as ResolveFromNpmOptions) ?? + (wantedDependency.bareSpecifier + ? await _resolveFromLocalPath(wantedDependency as { bareSpecifier: string }, opts) + : null) if (!resolution) { let specifier = `${wantedDependency.alias ? wantedDependency.alias + '@' : ''}${wantedDependency.bareSpecifier ?? ''}` if (specifier !== '') { diff --git a/resolving/default-resolver/test/namedRegistry.ts b/resolving/default-resolver/test/namedRegistry.ts new file mode 100644 index 0000000000..ea79a2efeb --- /dev/null +++ b/resolving/default-resolver/test/namedRegistry.ts @@ -0,0 +1,108 @@ +/// +import fs from 'node:fs' +import path from 'node:path' + +import { afterEach, beforeEach, expect, test } from '@jest/globals' +import { createFetchFromRegistry } from '@pnpm/network.fetch' +import { createResolver } from '@pnpm/resolving.default-resolver' +import { getMockAgent, setupMockAgent, teardownMockAgent } from '@pnpm/testing.mock-agent' +import { loadJsonFileSync } from 'load-json-file' +import { temporaryDirectory } from 'tempy' + +/* eslint-disable @typescript-eslint/no-explicit-any */ +const ghAcmePrivateMeta = loadJsonFileSync( + path.join(import.meta.dirname, '../../npm-resolver/test/fixtures/gh-acme-private.json') +) +/* eslint-enable @typescript-eslint/no-explicit-any */ + +const GH_REGISTRY = 'https://npm.pkg.github.com/' +const ENTERPRISE_REGISTRY = 'https://npm.enterprise.example.com/' + +const registries = { + default: 'https://registry.npmjs.org/', + '@jsr': 'https://npm.jsr.io/', +} + +const fetch = createFetchFromRegistry({}) + +beforeEach(async () => { + await setupMockAgent() +}) + +afterEach(async () => { + await teardownMockAgent() +}) + +function interceptAcmePrivate (registry: string): void { + const slash = '%2F' + const pool = getMockAgent().get(registry.replace(/\/$/, '')) + pool.intercept({ path: `/@acme${slash}private`, method: 'GET' }).reply(200, ghAcmePrivateMeta) +} + +test('createResolver() routes :@scope/pkg through the named-registry resolver instead of the local resolver', async () => { + interceptAcmePrivate(GH_REGISTRY) + + const { resolve } = createResolver(fetch, () => undefined, { + cacheDir: temporaryDirectory(), + storeDir: temporaryDirectory(), + registries, + }) + + const result = await resolve( + { bareSpecifier: 'gh:@acme/private' }, + { lockfileDir: '/test', projectDir: '/test', preferredVersions: {} } + ) + + expect(result.resolvedVia).toBe('named-registry') + expect(result.id).toBe('@acme/private@2.1.0') +}) + +test('createResolver() routes a user-configured named registry alias through the named-registry resolver', async () => { + interceptAcmePrivate(ENTERPRISE_REGISTRY) + + const { resolve } = createResolver(fetch, () => undefined, { + cacheDir: temporaryDirectory(), + storeDir: temporaryDirectory(), + registries, + namedRegistries: { + work: ENTERPRISE_REGISTRY, + }, + }) + + const result = await resolve( + { bareSpecifier: 'work:@acme/private' }, + { lockfileDir: '/test', projectDir: '/test', preferredVersions: {} } + ) + + expect(result.resolvedVia).toBe('named-registry') + expect(result.id).toBe('@acme/private@2.1.0') +}) + +test.each([ + ['link:./pkg', 'link'], + ['workspace:./pkg', 'workspace'], + ['file:./pkg', 'file'], +])('createResolver() lets the explicit local protocol %s win over a colliding named-registry alias', async (bareSpecifier, alias) => { + const projectDir = temporaryDirectory() + fs.mkdirSync(path.join(projectDir, 'pkg')) + fs.writeFileSync( + path.join(projectDir, 'pkg', 'package.json'), + JSON.stringify({ name: 'pkg', version: '1.0.0' }) + ) + + const { resolve } = createResolver(fetch, () => undefined, { + cacheDir: temporaryDirectory(), + storeDir: temporaryDirectory(), + registries, + namedRegistries: { + [alias]: ENTERPRISE_REGISTRY, + }, + }) + + const result = await resolve( + { bareSpecifier }, + { lockfileDir: projectDir, projectDir, preferredVersions: {} } + ) + + expect(result.resolvedVia).toBe('local-filesystem') +}) diff --git a/resolving/default-resolver/tsconfig.json b/resolving/default-resolver/tsconfig.json index 6113ad5c00..56bb82cfb7 100644 --- a/resolving/default-resolver/tsconfig.json +++ b/resolving/default-resolver/tsconfig.json @@ -42,6 +42,9 @@ { "path": "../../store/cafs-types" }, + { + "path": "../../testing/mock-agent" + }, { "path": "../git-resolver" }, diff --git a/resolving/local-resolver/src/index.ts b/resolving/local-resolver/src/index.ts index 7a71f47a0b..5497d6139f 100644 --- a/resolving/local-resolver/src/index.ts +++ b/resolving/local-resolver/src/index.ts @@ -8,7 +8,7 @@ import type { DirectoryResolution, Resolution, ResolveResult, TarballResolution import type { DependencyManifest, PkgResolutionId } from '@pnpm/types' import { readProjectManifestOnly } from '@pnpm/workspace.project-manifest-reader' -import { parseBareSpecifier, type WantedLocalDependency } from './parseBareSpecifier.js' +import { type LocalPackageSpec, parseLocalPath, parseLocalScheme, type WantedLocalDependency } from './parseBareSpecifier.js' export { type WantedLocalDependency } @@ -19,26 +19,55 @@ export interface LocalResolveResult extends ResolveResult { resolvedVia: 'local-filesystem' } -/** - * Resolves a package hosted on the local filesystem - */ -export async function resolveFromLocal ( - ctx: { - preserveAbsolutePaths?: boolean - }, - wantedDependency: WantedLocalDependency, - opts: { - lockfileDir?: string - projectDir: string - currentPkg?: { - id: PkgResolutionId - resolution: DirectoryResolution | TarballResolution | Resolution - } - update?: false | 'compatible' | 'latest' +export interface LocalResolverContext { + preserveAbsolutePaths?: boolean +} + +export interface LocalResolverOptions { + lockfileDir?: string + projectDir: string + currentPkg?: { + id: PkgResolutionId + resolution: DirectoryResolution | TarballResolution | Resolution } + update?: false | 'compatible' | 'latest' +} + +/** + * Resolves a dependency declared with an explicit local scheme: + * `link:`, `workspace:`, `file:`, or (rejected) `path:`. + */ +export async function resolveFromLocalScheme ( + ctx: LocalResolverContext, + wantedDependency: WantedLocalDependency, + opts: LocalResolverOptions +): Promise { + const spec = parseLocalScheme(wantedDependency, opts.projectDir, opts.lockfileDir ?? opts.projectDir, { + preserveAbsolutePaths: ctx.preserveAbsolutePaths ?? false, + }) + return resolveSpec(spec, opts) +} + +/** + * Resolves a dependency by path shape — a relative/absolute path or a tarball + * filename. Does not look at scheme prefixes; callers that want scheme support + * should call {@link resolveFromLocalScheme} first. + */ +export async function resolveFromLocalPath ( + ctx: LocalResolverContext, + wantedDependency: WantedLocalDependency, + opts: LocalResolverOptions +): Promise { + const spec = parseLocalPath(wantedDependency, opts.projectDir, opts.lockfileDir ?? opts.projectDir, { + preserveAbsolutePaths: ctx.preserveAbsolutePaths ?? false, + }) + return resolveSpec(spec, opts) +} + +async function resolveSpec ( + spec: LocalPackageSpec | null, + opts: LocalResolverOptions ): Promise { - const preserveAbsolutePaths = ctx.preserveAbsolutePaths ?? false - const spec = parseBareSpecifier(wantedDependency, opts.projectDir, opts.lockfileDir ?? opts.projectDir, { preserveAbsolutePaths }) if (spec == null) return null if (spec.type === 'file') { diff --git a/resolving/local-resolver/src/parseBareSpecifier.ts b/resolving/local-resolver/src/parseBareSpecifier.ts index 469a83652e..37826190d6 100644 --- a/resolving/local-resolver/src/parseBareSpecifier.ts +++ b/resolving/local-resolver/src/parseBareSpecifier.ts @@ -35,7 +35,7 @@ class PathIsUnsupportedProtocolError extends PnpmError { } } -export function parseBareSpecifier ( +export function parseLocalScheme ( wd: WantedLocalDependency, projectDir: string, lockfileDir: string, @@ -44,13 +44,7 @@ export function parseBareSpecifier ( if (wd.bareSpecifier.startsWith('link:') || wd.bareSpecifier.startsWith('workspace:')) { return fromLocal(wd, projectDir, lockfileDir, 'directory', opts) } - if (wd.bareSpecifier.endsWith('.tgz') || - wd.bareSpecifier.endsWith('.tar.gz') || - wd.bareSpecifier.endsWith('.tar') || - wd.bareSpecifier.includes(path.sep) || - wd.bareSpecifier.startsWith('file:') || - isFilespec.test(wd.bareSpecifier) - ) { + if (wd.bareSpecifier.startsWith('file:')) { const type = isFilename.test(wd.bareSpecifier) ? 'file' : 'directory' return fromLocal(wd, projectDir, lockfileDir, type, opts) } @@ -60,6 +54,24 @@ export function parseBareSpecifier ( return null } +export function parseLocalPath ( + wd: WantedLocalDependency, + projectDir: string, + lockfileDir: string, + opts: { preserveAbsolutePaths: boolean } +): LocalPackageSpec | null { + if (wd.bareSpecifier.endsWith('.tgz') || + wd.bareSpecifier.endsWith('.tar.gz') || + wd.bareSpecifier.endsWith('.tar') || + wd.bareSpecifier.includes(path.sep) || + isFilespec.test(wd.bareSpecifier) + ) { + const type = isFilename.test(wd.bareSpecifier) ? 'file' : 'directory' + return fromLocal(wd, projectDir, lockfileDir, type, opts) + } + return null +} + function fromLocal ( { bareSpecifier, injected }: WantedLocalDependency, projectDir: string, diff --git a/resolving/local-resolver/test/index.ts b/resolving/local-resolver/test/index.ts index 8c578f30f4..debc9ebefe 100644 --- a/resolving/local-resolver/test/index.ts +++ b/resolving/local-resolver/test/index.ts @@ -4,7 +4,7 @@ import path from 'node:path' import { expect, jest, test } from '@jest/globals' import { logger } from '@pnpm/logger' -import { resolveFromLocal } from '@pnpm/resolving.local-resolver' +import { resolveFromLocalPath, resolveFromLocalScheme } from '@pnpm/resolving.local-resolver' import type { DirectoryResolution } from '@pnpm/resolving.resolver-base' import normalize from 'normalize-path' @@ -12,7 +12,7 @@ const require = createRequire(import.meta.dirname) const TEST_DIR = path.dirname(require.resolve('@pnpm/tgz-fixtures/tgz/pnpm-local-resolver-0.1.1.tgz')) test('resolve directory', async () => { - const resolveResult = await resolveFromLocal({}, { bareSpecifier: '..' }, { projectDir: import.meta.dirname }) + const resolveResult = await resolveFromLocalPath({}, { bareSpecifier: '..' }, { projectDir: import.meta.dirname }) expect(resolveResult!.id).toBe('link:..') expect(resolveResult!.normalizedBareSpecifier).toBe('link:..') expect(resolveResult!['manifest']!.name).toBe('@pnpm/resolving.local-resolver') @@ -23,7 +23,7 @@ test('resolve directory', async () => { test('resolve directory specified using absolute path', async () => { const linkedDir = path.join(import.meta.dirname, '..') const normalizedLinkedDir = normalize(linkedDir) - const resolveResult = await resolveFromLocal({}, { bareSpecifier: `link:${linkedDir}` }, { projectDir: import.meta.dirname }) + const resolveResult = await resolveFromLocalScheme({}, { bareSpecifier: `link:${linkedDir}` }, { projectDir: import.meta.dirname }) expect(resolveResult!.id).toBe('link:..') expect(resolveResult!.normalizedBareSpecifier).toBe(`link:${normalizedLinkedDir}`) expect(resolveResult!['manifest']!.name).toBe('@pnpm/resolving.local-resolver') @@ -34,7 +34,7 @@ test('resolve directory specified using absolute path', async () => { test('resolve directory specified using absolute path with preserveAbsolutePaths', async () => { const linkedDir = path.join(import.meta.dirname, '..') const normalizedLinkedDir = normalize(linkedDir) - const resolveResult = await resolveFromLocal({ preserveAbsolutePaths: true }, { bareSpecifier: `link:${linkedDir}` }, { projectDir: import.meta.dirname }) + const resolveResult = await resolveFromLocalScheme({ preserveAbsolutePaths: true }, { bareSpecifier: `link:${linkedDir}` }, { projectDir: import.meta.dirname }) expect(resolveResult!.id).toBe(`link:${normalizedLinkedDir}`) expect(resolveResult!.normalizedBareSpecifier).toBe(`link:${normalizedLinkedDir}`) expect(resolveResult!['manifest']!.name).toBe('@pnpm/resolving.local-resolver') @@ -45,7 +45,7 @@ test('resolve directory specified using absolute path with preserveAbsolutePaths test('resolve directory specified using absolute path with preserveAbsolutePaths and file: scheme', async () => { const linkedDir = path.join(import.meta.dirname, '..') const normalizedLinkedDir = normalize(linkedDir) - const resolveResult = await resolveFromLocal( + const resolveResult = await resolveFromLocalScheme( { preserveAbsolutePaths: true }, { bareSpecifier: `file:${linkedDir}` }, { projectDir: import.meta.dirname } @@ -58,7 +58,7 @@ test('resolve directory specified using absolute path with preserveAbsolutePaths }) test('resolve injected directory', async () => { - const resolveResult = await resolveFromLocal({}, { injected: true, bareSpecifier: '..' }, { projectDir: import.meta.dirname }) + const resolveResult = await resolveFromLocalPath({}, { injected: true, bareSpecifier: '..' }, { projectDir: import.meta.dirname }) expect(resolveResult!.id).toBe('file:..') expect(resolveResult!.normalizedBareSpecifier).toBe('file:..') expect(resolveResult!['manifest']!.name).toBe('@pnpm/resolving.local-resolver') @@ -67,7 +67,7 @@ test('resolve injected directory', async () => { }) test('resolve workspace directory', async () => { - const resolveResult = await resolveFromLocal({}, { bareSpecifier: 'workspace:..' }, { projectDir: import.meta.dirname }) + const resolveResult = await resolveFromLocalScheme({}, { bareSpecifier: 'workspace:..' }, { projectDir: import.meta.dirname }) expect(resolveResult!.id).toBe('link:..') expect(resolveResult!.normalizedBareSpecifier).toBe('link:..') expect(resolveResult!['manifest']!.name).toBe('@pnpm/resolving.local-resolver') @@ -76,7 +76,7 @@ test('resolve workspace directory', async () => { }) test('resolve directory specified using the file: protocol', async () => { - const resolveResult = await resolveFromLocal({}, { bareSpecifier: 'file:..' }, { projectDir: import.meta.dirname }) + const resolveResult = await resolveFromLocalScheme({}, { bareSpecifier: 'file:..' }, { projectDir: import.meta.dirname }) expect(resolveResult!.id).toBe('file:..') expect(resolveResult!.normalizedBareSpecifier).toBe('file:..') expect(resolveResult!['manifest']!.name).toBe('@pnpm/resolving.local-resolver') @@ -85,7 +85,7 @@ test('resolve directory specified using the file: protocol', async () => { }) test('resolve directory specified using the link: protocol', async () => { - const resolveResult = await resolveFromLocal({}, { bareSpecifier: 'link:..' }, { projectDir: import.meta.dirname }) + const resolveResult = await resolveFromLocalScheme({}, { bareSpecifier: 'link:..' }, { projectDir: import.meta.dirname }) expect(resolveResult!.id).toBe('link:..') expect(resolveResult!.normalizedBareSpecifier).toBe('link:..') expect(resolveResult!['manifest']!.name).toBe('@pnpm/resolving.local-resolver') @@ -95,7 +95,7 @@ test('resolve directory specified using the link: protocol', async () => { test('resolve file', async () => { const wantedDependency = { bareSpecifier: './pnpm-local-resolver-0.1.1.tgz' } - const resolveResult = await resolveFromLocal({}, wantedDependency, { projectDir: TEST_DIR }) + const resolveResult = await resolveFromLocalPath({}, wantedDependency, { projectDir: TEST_DIR }) expect(resolveResult).toEqual({ id: 'file:pnpm-local-resolver-0.1.1.tgz', @@ -110,7 +110,7 @@ test('resolve file', async () => { test("resolve file when lockfile directory differs from the package's dir", async () => { const wantedDependency = { bareSpecifier: './pnpm-local-resolver-0.1.1.tgz' } - const resolveResult = await resolveFromLocal({}, wantedDependency, { + const resolveResult = await resolveFromLocalPath({}, wantedDependency, { lockfileDir: path.join(TEST_DIR, '..'), projectDir: TEST_DIR, }) @@ -128,7 +128,7 @@ test("resolve file when lockfile directory differs from the package's dir", asyn test('resolve tarball specified with file: protocol', async () => { const wantedDependency = { bareSpecifier: 'file:./pnpm-local-resolver-0.1.1.tgz' } - const resolveResult = await resolveFromLocal({}, wantedDependency, { projectDir: TEST_DIR }) + const resolveResult = await resolveFromLocalScheme({}, wantedDependency, { projectDir: TEST_DIR }) expect(resolveResult).toEqual({ id: 'file:pnpm-local-resolver-0.1.1.tgz', @@ -143,7 +143,7 @@ test('resolve tarball specified with file: protocol', async () => { test('resolve file with different integrity (forceFetch)', async () => { const wantedDependency = { bareSpecifier: 'file:./pnpm-local-resolver-0.1.1.tgz' } - const resolveResult = await resolveFromLocal({}, wantedDependency, { + const resolveResult = await resolveFromLocalScheme({}, wantedDependency, { projectDir: TEST_DIR, currentPkg: { id: 'file:pnpm-local-resolver-0.1.1.tgz' as any, // eslint-disable-line @@ -168,7 +168,7 @@ test('resolve file with different integrity (forceFetch)', async () => { test('fail when resolving tarball specified with the link: protocol', async () => { const wantedDependency = { bareSpecifier: 'link:./pnpm-local-resolver-0.1.1.tgz' } await expect( - resolveFromLocal({}, wantedDependency, { projectDir: TEST_DIR }) + resolveFromLocalScheme({}, wantedDependency, { projectDir: TEST_DIR }) ).rejects.toMatchObject({ code: 'ERR_PNPM_NOT_PACKAGE_DIRECTORY' }) }) @@ -176,14 +176,14 @@ test('fail when resolving from not existing directory an injected dependency', a const wantedDependency = { bareSpecifier: 'file:./dir-does-not-exist' } const projectDir = import.meta.dirname await expect( - resolveFromLocal({}, wantedDependency, { projectDir }) + resolveFromLocalScheme({}, wantedDependency, { projectDir }) ).rejects.toThrow(`Could not install from "${path.join(projectDir, 'dir-does-not-exist')}" as it does not exist.`) }) test('do not fail when resolving from not existing directory', async () => { jest.spyOn(logger, 'warn') const wantedDependency = { bareSpecifier: 'link:./dir-does-not-exist' } - const resolveResult = await resolveFromLocal({}, wantedDependency, { projectDir: import.meta.dirname }) + const resolveResult = await resolveFromLocalScheme({}, wantedDependency, { projectDir: import.meta.dirname }) expect(resolveResult?.manifest).toStrictEqual({ name: 'dir-does-not-exist', version: '0.0.0', @@ -197,10 +197,18 @@ test('do not fail when resolving from not existing directory', async () => { test('throw error when the path: protocol is used', async () => { await expect( - resolveFromLocal({}, { bareSpecifier: 'path:..' }, { projectDir: import.meta.dirname }) + resolveFromLocalScheme({}, { bareSpecifier: 'path:..' }, { projectDir: import.meta.dirname }) ).rejects.toMatchObject({ code: 'ERR_PNPM_PATH_IS_UNSUPPORTED_PROTOCOL', bareSpecifier: 'path:..', protocol: 'path:', }) }) + +test('resolveFromLocalPath ignores explicit local schemes', async () => { + await expect(resolveFromLocalScheme({}, { bareSpecifier: 'foo' }, { projectDir: import.meta.dirname })).resolves.toBeNull() + await expect(resolveFromLocalPath({}, { bareSpecifier: 'link:..' }, { projectDir: import.meta.dirname })).resolves.toBeNull() + await expect(resolveFromLocalPath({}, { bareSpecifier: 'workspace:..' }, { projectDir: import.meta.dirname })).resolves.toBeNull() + await expect(resolveFromLocalPath({}, { bareSpecifier: 'file:..' }, { projectDir: import.meta.dirname })).resolves.toBeNull() + await expect(resolveFromLocalPath({}, { bareSpecifier: 'path:..' }, { projectDir: import.meta.dirname })).resolves.toBeNull() +})