mirror of
https://github.com/pnpm/pnpm.git
synced 2026-03-31 13:32:18 -04:00
fix: changes local-resolver to support absolute paths (#9761)
* feat: changes local-resolver to support absolute paths Previously absolute paths were being turned to relative paths, but if the file: specifier is 'file:/path/to/file', and the users are using a shared network storage, this relative path requires that the users all use the same local folder structure. Instead, using an absolute path as the specifier allows them to have the source code anywhere, and the absolute path will be resolved consistently. Enabled via the `preserveAbsolutePaths` option. * chore: changeset * feat: add preserve absolute paths option * docs: add changesets * fix: also update the 'dependencyPath', add test for that case --------- Co-authored-by: Zoltan Kochan <z@kochan.io>
This commit is contained in:
5
.changeset/fine-waves-stay.md
Normal file
5
.changeset/fine-waves-stay.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@pnpm/local-resolver": minor
|
||||
---
|
||||
|
||||
Added `preserveAbsolutePaths` option to `resolveFromLocal`. When using `file:/path/to/package`, the absolute path will be preserved instead of being turned into a relative path.
|
||||
@@ -225,6 +225,7 @@ export interface Config extends OptionsFromRootManifest {
|
||||
initType: 'commonjs' | 'module'
|
||||
dangerouslyAllowAllBuilds: boolean
|
||||
ci: boolean
|
||||
preserveAbsolutePaths?: boolean
|
||||
}
|
||||
|
||||
export interface ConfigWithDeprecatedSettings extends Config {
|
||||
|
||||
@@ -81,6 +81,7 @@ export const types = Object.assign({
|
||||
'prefer-offline': Boolean,
|
||||
'prefer-symlinked-executables': Boolean,
|
||||
'prefer-workspace-packages': Boolean,
|
||||
'preserve-absolute-paths': Boolean,
|
||||
production: [null, true],
|
||||
'public-hoist-pattern': Array,
|
||||
'publish-branch': String,
|
||||
|
||||
@@ -30,6 +30,7 @@ export type ClientOptions = {
|
||||
gitShallowHosts?: string[]
|
||||
resolveSymlinksInInjectedDirs?: boolean
|
||||
includeOnlyPackageFiles?: boolean
|
||||
preserveAbsolutePaths?: boolean
|
||||
} & ResolverFactoryOptions & AgentOptions
|
||||
|
||||
export interface Client {
|
||||
|
||||
@@ -47,6 +47,9 @@ export function createResolver (
|
||||
): { resolve: DefaultResolver, clearCache: () => void } {
|
||||
const { resolveFromNpm, resolveFromJsr, clearCache } = createNpmResolver(fetchFromRegistry, getAuthHeader, pnpmOpts)
|
||||
const resolveFromGit = createGitResolver(pnpmOpts)
|
||||
const _resolveFromLocal = resolveFromLocal.bind(null, {
|
||||
preserveAbsolutePaths: pnpmOpts.preserveAbsolutePaths,
|
||||
})
|
||||
const _resolveNodeRuntime = resolveNodeRuntime.bind(null, { fetchFromRegistry, offline: pnpmOpts.offline, rawConfig: pnpmOpts.rawConfig })
|
||||
return {
|
||||
resolve: async (wantedDependency, opts) => {
|
||||
@@ -55,7 +58,7 @@ export function createResolver (
|
||||
(wantedDependency.bareSpecifier && (
|
||||
await resolveFromTarball(fetchFromRegistry, wantedDependency as { bareSpecifier: string }) ??
|
||||
await resolveFromGit(wantedDependency as { bareSpecifier: string }) ??
|
||||
await resolveFromLocal(wantedDependency as { bareSpecifier: string }, opts)
|
||||
await _resolveFromLocal(wantedDependency as { bareSpecifier: string }, opts)
|
||||
)) ??
|
||||
await _resolveNodeRuntime(wantedDependency)
|
||||
if (!resolution) {
|
||||
|
||||
@@ -21,13 +21,17 @@ export interface LocalResolveResult extends ResolveResult {
|
||||
* Resolves a package hosted on the local filesystem
|
||||
*/
|
||||
export async function resolveFromLocal (
|
||||
ctx: {
|
||||
preserveAbsolutePaths?: boolean
|
||||
},
|
||||
wantedDependency: WantedLocalDependency,
|
||||
opts: {
|
||||
lockfileDir?: string
|
||||
projectDir: string
|
||||
}
|
||||
): Promise<LocalResolveResult | null> {
|
||||
const spec = parseBareSpecifier(wantedDependency, opts.projectDir, opts.lockfileDir ?? opts.projectDir)
|
||||
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') {
|
||||
return {
|
||||
|
||||
@@ -37,10 +37,11 @@ class PathIsUnsupportedProtocolError extends PnpmError {
|
||||
export function parseBareSpecifier (
|
||||
wd: WantedLocalDependency,
|
||||
projectDir: string,
|
||||
lockfileDir: string
|
||||
lockfileDir: string,
|
||||
opts: { preserveAbsolutePaths: boolean }
|
||||
): LocalPackageSpec | null {
|
||||
if (wd.bareSpecifier.startsWith('link:') || wd.bareSpecifier.startsWith('workspace:')) {
|
||||
return fromLocal(wd, projectDir, lockfileDir, 'directory')
|
||||
return fromLocal(wd, projectDir, lockfileDir, 'directory', opts)
|
||||
}
|
||||
if (wd.bareSpecifier.endsWith('.tgz') ||
|
||||
wd.bareSpecifier.endsWith('.tar.gz') ||
|
||||
@@ -50,7 +51,7 @@ export function parseBareSpecifier (
|
||||
isFilespec.test(wd.bareSpecifier)
|
||||
) {
|
||||
const type = isFilename.test(wd.bareSpecifier) ? 'file' : 'directory'
|
||||
return fromLocal(wd, projectDir, lockfileDir, type)
|
||||
return fromLocal(wd, projectDir, lockfileDir, type, opts)
|
||||
}
|
||||
if (wd.bareSpecifier.startsWith('path:')) {
|
||||
throw new PathIsUnsupportedProtocolError(wd.bareSpecifier, 'path:')
|
||||
@@ -62,7 +63,8 @@ function fromLocal (
|
||||
{ bareSpecifier, injected }: WantedLocalDependency,
|
||||
projectDir: string,
|
||||
lockfileDir: string,
|
||||
type: 'file' | 'directory'
|
||||
type: 'file' | 'directory',
|
||||
opts: { preserveAbsolutePaths: boolean }
|
||||
): LocalPackageSpec {
|
||||
const spec = bareSpecifier.replace(/\\/g, '/')
|
||||
.replace(/^(?:file|link|workspace):\/*([A-Z]:)/i, '$1') // drive name paths on windows
|
||||
@@ -91,14 +93,24 @@ function fromLocal (
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeRelativeOrAbsolute (relativeTo: string, fromPath: string) {
|
||||
let specPath
|
||||
if (opts.preserveAbsolutePaths && isAbsolute(spec)) {
|
||||
specPath = path.resolve(fromPath)
|
||||
} else {
|
||||
specPath = path.relative(relativeTo, fromPath)
|
||||
}
|
||||
return normalize(specPath)
|
||||
}
|
||||
|
||||
injected = protocol === 'file:'
|
||||
const dependencyPath = injected
|
||||
? normalize(path.relative(lockfileDir, fetchSpec))
|
||||
? normalizeRelativeOrAbsolute(lockfileDir, fetchSpec)
|
||||
: normalize(path.resolve(fetchSpec))
|
||||
const id = (
|
||||
!injected && (type === 'directory' || projectDir === lockfileDir)
|
||||
? `${protocol}${normalize(path.relative(projectDir, fetchSpec))}`
|
||||
: `${protocol}${normalize(path.relative(lockfileDir, fetchSpec))}`
|
||||
? `${protocol}${normalizeRelativeOrAbsolute(projectDir, fetchSpec)}`
|
||||
: `${protocol}${normalizeRelativeOrAbsolute(lockfileDir, fetchSpec)}`
|
||||
) as PkgResolutionId
|
||||
|
||||
return {
|
||||
|
||||
@@ -8,7 +8,7 @@ import { logger } from '@pnpm/logger'
|
||||
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: __dirname })
|
||||
const resolveResult = await resolveFromLocal({}, { bareSpecifier: '..' }, { projectDir: __dirname })
|
||||
expect(resolveResult!.id).toEqual('link:..')
|
||||
expect(resolveResult!.normalizedBareSpecifier).toEqual('link:..')
|
||||
expect(resolveResult!['manifest']!.name).toEqual('@pnpm/local-resolver')
|
||||
@@ -19,7 +19,7 @@ test('resolve directory', async () => {
|
||||
test('resolve directory specified using absolute path', async () => {
|
||||
const linkedDir = path.join(__dirname, '..')
|
||||
const normalizedLinkedDir = normalize(linkedDir)
|
||||
const resolveResult = await resolveFromLocal({ bareSpecifier: `link:${linkedDir}` }, { projectDir: __dirname })
|
||||
const resolveResult = await resolveFromLocal({}, { bareSpecifier: `link:${linkedDir}` }, { projectDir: __dirname })
|
||||
expect(resolveResult!.id).toEqual('link:..')
|
||||
expect(resolveResult!.normalizedBareSpecifier).toEqual(`link:${normalizedLinkedDir}`)
|
||||
expect(resolveResult!['manifest']!.name).toEqual('@pnpm/local-resolver')
|
||||
@@ -27,8 +27,34 @@ test('resolve directory specified using absolute path', async () => {
|
||||
expect((resolveResult!.resolution as DirectoryResolution).type).toEqual('directory')
|
||||
})
|
||||
|
||||
test('resolve directory specified using absolute path with preserveAbsolutePaths', async () => {
|
||||
const linkedDir = path.join(__dirname, '..')
|
||||
const normalizedLinkedDir = normalize(linkedDir)
|
||||
const resolveResult = await resolveFromLocal({ preserveAbsolutePaths: true }, { bareSpecifier: `link:${linkedDir}` }, { projectDir: __dirname })
|
||||
expect(resolveResult!.id).toEqual(`link:${normalizedLinkedDir}`)
|
||||
expect(resolveResult!.normalizedBareSpecifier).toEqual(`link:${normalizedLinkedDir}`)
|
||||
expect(resolveResult!['manifest']!.name).toEqual('@pnpm/local-resolver')
|
||||
expect((resolveResult!.resolution as DirectoryResolution).directory).toEqual(normalizedLinkedDir)
|
||||
expect((resolveResult!.resolution as DirectoryResolution).type).toEqual('directory')
|
||||
})
|
||||
|
||||
test('resolve directory specified using absolute path with preserveAbsolutePaths and file: scheme', async () => {
|
||||
const linkedDir = path.join(__dirname, '..')
|
||||
const normalizedLinkedDir = normalize(linkedDir)
|
||||
const resolveResult = await resolveFromLocal(
|
||||
{ preserveAbsolutePaths: true },
|
||||
{ bareSpecifier: `file:${linkedDir}` },
|
||||
{ projectDir: __dirname }
|
||||
)
|
||||
expect(resolveResult!.id).toEqual(`file:${normalizedLinkedDir}`)
|
||||
expect(resolveResult!.normalizedBareSpecifier).toEqual(`file:${normalizedLinkedDir}`)
|
||||
expect(resolveResult!['manifest']!.name).toEqual('@pnpm/local-resolver')
|
||||
expect((resolveResult!.resolution as DirectoryResolution).directory).toEqual(normalizedLinkedDir)
|
||||
expect((resolveResult!.resolution as DirectoryResolution).type).toEqual('directory')
|
||||
})
|
||||
|
||||
test('resolve injected directory', async () => {
|
||||
const resolveResult = await resolveFromLocal({ injected: true, bareSpecifier: '..' }, { projectDir: __dirname })
|
||||
const resolveResult = await resolveFromLocal({}, { injected: true, bareSpecifier: '..' }, { projectDir: __dirname })
|
||||
expect(resolveResult!.id).toEqual('file:..')
|
||||
expect(resolveResult!.normalizedBareSpecifier).toEqual('file:..')
|
||||
expect(resolveResult!['manifest']!.name).toEqual('@pnpm/local-resolver')
|
||||
@@ -37,7 +63,7 @@ test('resolve injected directory', async () => {
|
||||
})
|
||||
|
||||
test('resolve workspace directory', async () => {
|
||||
const resolveResult = await resolveFromLocal({ bareSpecifier: 'workspace:..' }, { projectDir: __dirname })
|
||||
const resolveResult = await resolveFromLocal({}, { bareSpecifier: 'workspace:..' }, { projectDir: __dirname })
|
||||
expect(resolveResult!.id).toEqual('link:..')
|
||||
expect(resolveResult!.normalizedBareSpecifier).toEqual('link:..')
|
||||
expect(resolveResult!['manifest']!.name).toEqual('@pnpm/local-resolver')
|
||||
@@ -46,7 +72,7 @@ test('resolve workspace directory', async () => {
|
||||
})
|
||||
|
||||
test('resolve directory specified using the file: protocol', async () => {
|
||||
const resolveResult = await resolveFromLocal({ bareSpecifier: 'file:..' }, { projectDir: __dirname })
|
||||
const resolveResult = await resolveFromLocal({}, { bareSpecifier: 'file:..' }, { projectDir: __dirname })
|
||||
expect(resolveResult!.id).toEqual('file:..')
|
||||
expect(resolveResult!.normalizedBareSpecifier).toEqual('file:..')
|
||||
expect(resolveResult!['manifest']!.name).toEqual('@pnpm/local-resolver')
|
||||
@@ -55,7 +81,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: __dirname })
|
||||
const resolveResult = await resolveFromLocal({}, { bareSpecifier: 'link:..' }, { projectDir: __dirname })
|
||||
expect(resolveResult!.id).toEqual('link:..')
|
||||
expect(resolveResult!.normalizedBareSpecifier).toEqual('link:..')
|
||||
expect(resolveResult!['manifest']!.name).toEqual('@pnpm/local-resolver')
|
||||
@@ -65,7 +91,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 resolveFromLocal({}, wantedDependency, { projectDir: TEST_DIR })
|
||||
|
||||
expect(resolveResult).toEqual({
|
||||
id: 'file:pnpm-local-resolver-0.1.1.tgz',
|
||||
@@ -80,7 +106,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 resolveFromLocal({}, wantedDependency, {
|
||||
lockfileDir: path.join(TEST_DIR, '..'),
|
||||
projectDir: TEST_DIR,
|
||||
})
|
||||
@@ -98,7 +124,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 resolveFromLocal({}, wantedDependency, { projectDir: TEST_DIR })
|
||||
|
||||
expect(resolveResult).toEqual({
|
||||
id: 'file:pnpm-local-resolver-0.1.1.tgz',
|
||||
@@ -114,7 +140,7 @@ test('resolve tarball specified with file: protocol', async () => {
|
||||
test('fail when resolving tarball specified with the link: protocol', async () => {
|
||||
try {
|
||||
const wantedDependency = { bareSpecifier: 'link:./pnpm-local-resolver-0.1.1.tgz' }
|
||||
await resolveFromLocal(wantedDependency, { projectDir: TEST_DIR })
|
||||
await resolveFromLocal({}, wantedDependency, { projectDir: TEST_DIR })
|
||||
fail()
|
||||
} catch (err: any) { // eslint-disable-line
|
||||
expect(err).toBeDefined()
|
||||
@@ -126,14 +152,14 @@ test('fail when resolving from not existing directory an injected dependency', a
|
||||
const wantedDependency = { bareSpecifier: 'file:./dir-does-not-exist' }
|
||||
const projectDir = __dirname
|
||||
await expect(
|
||||
resolveFromLocal(wantedDependency, { projectDir })
|
||||
resolveFromLocal({}, 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: __dirname })
|
||||
const resolveResult = await resolveFromLocal({}, wantedDependency, { projectDir: __dirname })
|
||||
expect(resolveResult?.manifest).toStrictEqual({
|
||||
name: 'dir-does-not-exist',
|
||||
version: '0.0.0',
|
||||
@@ -147,7 +173,7 @@ test('do not fail when resolving from not existing directory', async () => {
|
||||
|
||||
test('throw error when the path: protocol is used', async () => {
|
||||
try {
|
||||
await resolveFromLocal({ bareSpecifier: 'path:..' }, { projectDir: __dirname })
|
||||
await resolveFromLocal({}, { bareSpecifier: 'path:..' }, { projectDir: __dirname })
|
||||
fail()
|
||||
} catch (err: any) { // eslint-disable-line
|
||||
expect(err).toBeDefined()
|
||||
|
||||
@@ -74,6 +74,7 @@ export interface ResolverFactoryOptions {
|
||||
timeout?: number
|
||||
registries: Registries
|
||||
saveWorkspaceProtocol?: boolean | 'rolling'
|
||||
preserveAbsolutePaths?: boolean
|
||||
}
|
||||
|
||||
export interface NpmResolveResult extends ResolveResult {
|
||||
|
||||
@@ -34,6 +34,7 @@ export type CreateNewStoreControllerOptions = CreateResolverOptions & Pick<Confi
|
||||
| 'offline'
|
||||
| 'packageImportMethod'
|
||||
| 'preferOffline'
|
||||
| 'preserveAbsolutePaths'
|
||||
| 'registries'
|
||||
| 'registrySupportsTimeField'
|
||||
| 'resolutionMode'
|
||||
@@ -92,6 +93,7 @@ export async function createNewStoreController (
|
||||
resolveSymlinksInInjectedDirs: opts.resolveSymlinksInInjectedDirs,
|
||||
includeOnlyPackageFiles: !opts.deployAllFiles,
|
||||
saveWorkspaceProtocol: opts.saveWorkspaceProtocol,
|
||||
preserveAbsolutePaths: opts.preserveAbsolutePaths,
|
||||
})
|
||||
await fs.mkdir(opts.storeDir, { recursive: true })
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user