fix: route <alias>:@scope/pkg to the named-registry resolver (#11598)

## Summary

- The local resolver's path-shape match 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.
- Split the local resolver into two exports: `resolveFromLocalScheme` (handles `file:`/`link:`/`workspace:`/`path:`) and `resolveFromLocalPath` (path-shape match — tarball extension, `path.sep`, `isFilespec`). `resolveFromLocal` is removed.
- Re-order the default-resolver chain so the scheme pass runs *before* `resolveFromNamedRegistry` and the path pass runs *after*. Explicit local protocols still win even when a user configures a colliding `namedRegistries` alias; named-registry aliases reach their configured URL.

Repro before the fix:

```
$ cat pnpm-workspace.yaml
namedRegistries:
  bit: https://node-registry.bit.cloud/

$ pnpm add bit:@teambit/bit
[WARN] Installing a dependency from a non-existent directory: /private/tmp/.../bit:@teambit/bit

dependencies:
+ bit 0.0.0 <- bit:@teambit/bit
```

After the fix, the same command resolves `@teambit/bit 1.13.173` from `https://node-registry.bit.cloud/` and writes `"@teambit/bit": "bit:^1.13.173"` to `package.json`.
This commit is contained in:
Zoltan Kochan
2026-05-12 12:50:17 +02:00
committed by GitHub
parent ca0ba1bf6f
commit 3ab403a1fa
10 changed files with 241 additions and 55 deletions

View File

@@ -0,0 +1,6 @@
---
"@pnpm/resolving.default-resolver": patch
"pnpm": patch
---
Fixed `pnpm add <alias>:@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.

View File

@@ -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).

9
pnpm-lock.yaml generated
View File

@@ -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:

View File

@@ -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"

View File

@@ -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 `<alias>:@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 !== '') {

View File

@@ -0,0 +1,108 @@
/// <reference path="../../../__typings__/index.d.ts"/>
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<any>(
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 <alias>:@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')
})

View File

@@ -42,6 +42,9 @@
{
"path": "../../store/cafs-types"
},
{
"path": "../../testing/mock-agent"
},
{
"path": "../git-resolver"
},

View File

@@ -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<LocalResolveResult | null> {
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<LocalResolveResult | null> {
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<LocalResolveResult | null> {
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') {

View File

@@ -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,

View File

@@ -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()
})