mirror of
https://github.com/pnpm/pnpm.git
synced 2026-02-14 17:12:35 -05:00
fix: skip local file: protocol dependencies during pnpm fetch (#10514)
This fixes an issue where pnpm fetch would fail in Docker builds when local directory dependencies (file: protocol) were not available. The fix adds an ignoreLocalPackages option that is passed from the fetch command to skip local dependencies during graph building, since pnpm fetch only downloads packages from the registry and doesn't need local packages that won't be available in Docker builds. close #10460
This commit is contained in:
committed by
Zoltan Kochan
parent
f1cb40c4e1
commit
7adf26b017
9
.changeset/fix-pnpm-fetch-file-protocol.md
Normal file
9
.changeset/fix-pnpm-fetch-file-protocol.md
Normal file
@@ -0,0 +1,9 @@
|
||||
---
|
||||
"@pnpm/deps.graph-builder": patch
|
||||
"@pnpm/headless": patch
|
||||
"@pnpm/core": patch
|
||||
"@pnpm/plugin-commands-installation": patch
|
||||
"pnpm": patch
|
||||
---
|
||||
|
||||
Skip local `file:` protocol dependencies during `pnpm fetch`. This fixes an issue where `pnpm fetch` would fail in Docker builds when local directory dependencies were not available [#10460](https://github.com/pnpm/pnpm/issues/10460).
|
||||
14
deps/graph-builder/src/lockfileToDepGraph.ts
vendored
14
deps/graph-builder/src/lockfileToDepGraph.ts
vendored
@@ -70,6 +70,12 @@ export interface LockfileToDepGraphOptions {
|
||||
include: IncludedDependencies
|
||||
includeUnchangedDeps?: boolean
|
||||
ignoreScripts: boolean
|
||||
/**
|
||||
* When true, skip fetching local dependencies (file: protocol pointing to directories).
|
||||
* This is useful for `pnpm fetch` which only downloads packages from the registry
|
||||
* and doesn't need local packages that won't be available (e.g., in Docker builds).
|
||||
*/
|
||||
ignoreLocalPackages?: boolean
|
||||
lockfileDir: string
|
||||
nodeVersion: string
|
||||
pnpmVersion: string
|
||||
@@ -206,6 +212,14 @@ async function buildGraphFromPackages (
|
||||
}
|
||||
|
||||
const isDirectoryDep = 'directory' in pkgSnapshot.resolution && pkgSnapshot.resolution.directory != null
|
||||
if (isDirectoryDep && opts.ignoreLocalPackages) {
|
||||
logger.info({
|
||||
message: `Skipping local dependency ${pkgName}@${pkgVersion} (file: protocol)`,
|
||||
prefix: opts.lockfileDir,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const depIsPresent = !isDirectoryDep &&
|
||||
currentPackages[depPath] &&
|
||||
equals(currentPackages[depPath].dependencies, pkgSnapshot.dependencies)
|
||||
|
||||
@@ -51,6 +51,12 @@ export interface StrictInstallOptions {
|
||||
ignoreCompatibilityDb: boolean
|
||||
ignoreDepScripts: boolean
|
||||
ignorePackageManifest: boolean
|
||||
/**
|
||||
* When true, skip fetching local dependencies (file: protocol pointing to directories).
|
||||
* This is used by `pnpm fetch` which only downloads packages from the registry
|
||||
* and doesn't need local packages that won't be available (e.g., in Docker builds).
|
||||
*/
|
||||
ignoreLocalPackages: boolean
|
||||
preferFrozenLockfile: boolean
|
||||
saveWorkspaceProtocol: boolean | 'rolling'
|
||||
lockfileCheck?: (prev: LockfileObject, next: LockfileObject) => void
|
||||
@@ -224,6 +230,7 @@ const defaults = (opts: InstallOptions): StrictInstallOptions => {
|
||||
ownLifecycleHooksStdio: 'inherit',
|
||||
ignoreCompatibilityDb: false,
|
||||
ignorePackageManifest: false,
|
||||
ignoreLocalPackages: false,
|
||||
packageExtensions: {},
|
||||
ignoredOptionalDependencies: [] as string[],
|
||||
packageManager,
|
||||
|
||||
@@ -134,6 +134,12 @@ export interface HeadlessOptions {
|
||||
ignoreDepScripts: boolean
|
||||
ignoreScripts: boolean
|
||||
ignorePackageManifest?: boolean
|
||||
/**
|
||||
* When true, skip fetching local dependencies (file: protocol pointing to directories).
|
||||
* This is used by `pnpm fetch` which only downloads packages from the registry
|
||||
* and doesn't need local packages that won't be available (e.g., in Docker builds).
|
||||
*/
|
||||
ignoreLocalPackages?: boolean
|
||||
include: IncludedDependencies
|
||||
selectedProjectDirs: string[]
|
||||
allProjects: Record<string, Project>
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
packageIdFromSnapshot,
|
||||
pkgSnapshotToResolution,
|
||||
} from '@pnpm/lockfile.utils'
|
||||
import { logger } from '@pnpm/logger'
|
||||
import { type IncludedDependencies } from '@pnpm/modules-yaml'
|
||||
import { packageIsInstallable } from '@pnpm/package-is-installable'
|
||||
import { type PatchGroupRecord, getPatchInfo } from '@pnpm/patching.config'
|
||||
@@ -38,6 +39,12 @@ export interface LockfileToHoistedDepGraphOptions {
|
||||
importerIds: string[]
|
||||
include: IncludedDependencies
|
||||
ignoreScripts: boolean
|
||||
/**
|
||||
* When true, skip fetching local dependencies (file: protocol pointing to directories).
|
||||
* This is used by `pnpm fetch` which only downloads packages from the registry
|
||||
* and doesn't need local packages that won't be available (e.g., in Docker builds).
|
||||
*/
|
||||
ignoreLocalPackages?: boolean
|
||||
currentHoistedLocations?: Record<string, string[]>
|
||||
lockfileDir: string
|
||||
modulesDir?: string
|
||||
@@ -199,6 +206,16 @@ async function fetchDeps (
|
||||
opts.skipped.add(depPath)
|
||||
return
|
||||
}
|
||||
|
||||
const isDirectoryDep = 'directory' in pkgSnapshot.resolution && pkgSnapshot.resolution.directory != null
|
||||
if (isDirectoryDep && opts.ignoreLocalPackages) {
|
||||
logger.info({
|
||||
message: `Skipping local dependency ${pkgName}@${pkgVersion} (file: protocol)`,
|
||||
prefix: opts.lockfileDir,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const dir = path.join(modules, dep.name)
|
||||
const depLocation = path.relative(opts.lockfileDir, dir)
|
||||
const resolution = pkgSnapshotToResolution(depPath, pkgSnapshot, opts.registries)
|
||||
@@ -224,7 +241,7 @@ async function fetchDeps (
|
||||
ignoreScripts: opts.ignoreScripts,
|
||||
pkg: pkgResolution,
|
||||
})
|
||||
fetchResponse = { filesIndexFile } as any // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
fetchResponse = { filesIndexFile } as unknown as ReturnType<FetchPackageToStoreFunction>
|
||||
} else {
|
||||
try {
|
||||
fetchResponse = opts.storeController.fetchPackage({
|
||||
@@ -234,9 +251,9 @@ async function fetchDeps (
|
||||
ignoreScripts: opts.ignoreScripts,
|
||||
pkg: pkgResolution,
|
||||
supportedArchitectures: opts.supportedArchitectures,
|
||||
}) as any // eslint-disable-line
|
||||
}) as unknown as ReturnType<FetchPackageToStoreFunction>
|
||||
if (fetchResponse instanceof Promise) fetchResponse = await fetchResponse
|
||||
} catch (err: any) { // eslint-disable-line
|
||||
} catch (err: unknown) {
|
||||
if (pkgSnapshot.optional) return
|
||||
throw err
|
||||
}
|
||||
@@ -287,8 +304,8 @@ async function dirHasPackageJsonWithVersion (dir: string, expectedVersion?: stri
|
||||
try {
|
||||
const manifest = await safeReadPackageJsonFromDir(dir)
|
||||
return manifest?.version === expectedVersion
|
||||
} catch (err: any) { // eslint-disable-line
|
||||
if (err.code === 'ENOENT') {
|
||||
} catch (err: unknown) {
|
||||
if ((err as NodeJS.ErrnoException)?.code === 'ENOENT') {
|
||||
return pathExists(dir)
|
||||
}
|
||||
throw err
|
||||
|
||||
@@ -64,6 +64,7 @@ export async function handler (opts: FetchCommandOptions): Promise<void> {
|
||||
...opts,
|
||||
...getOptionsFromRootManifest(opts.rootProjectManifestDir, opts.rootProjectManifest ?? {}),
|
||||
ignorePackageManifest: true,
|
||||
ignoreLocalPackages: true,
|
||||
include,
|
||||
modulesCacheMaxAge: 0,
|
||||
pruneStore: true,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { install, fetch } from '@pnpm/plugin-commands-installation'
|
||||
import { prepare } from '@pnpm/prepare'
|
||||
@@ -132,3 +133,50 @@ test('fetch only dev dependencies', async () => {
|
||||
project.storeHas('is-negative')
|
||||
project.storeHasNot('is-positive')
|
||||
})
|
||||
|
||||
// Regression test for https://github.com/pnpm/pnpm/issues/10460
|
||||
// pnpm fetch should skip local file: protocol dependencies
|
||||
// because they won't be available in Docker builds
|
||||
test('fetch skips file: protocol dependencies that do not exist', async () => {
|
||||
const project = prepare({
|
||||
dependencies: {
|
||||
'is-positive': '1.0.0',
|
||||
'@local/pkg': 'file:./local-pkg',
|
||||
},
|
||||
})
|
||||
const storeDir = path.resolve('store')
|
||||
const localPkgDir = path.resolve(project.dir(), 'local-pkg')
|
||||
|
||||
// Create the local package for initial install to generate lockfile
|
||||
fs.mkdirSync(localPkgDir, { recursive: true })
|
||||
fs.writeFileSync(
|
||||
path.join(localPkgDir, 'package.json'),
|
||||
JSON.stringify({ name: '@local/pkg', version: '1.0.0' })
|
||||
)
|
||||
|
||||
// Create a lockfile with the file: dependency
|
||||
await install.handler({
|
||||
...DEFAULT_OPTIONS,
|
||||
cacheDir: path.resolve('cache'),
|
||||
dir: process.cwd(),
|
||||
linkWorkspacePackages: true,
|
||||
storeDir,
|
||||
})
|
||||
|
||||
rimraf(path.resolve(project.dir(), 'node_modules'))
|
||||
rimraf(path.resolve(project.dir(), './package.json'))
|
||||
// Remove the local package directory to simulate Docker build scenario
|
||||
rimraf(localPkgDir)
|
||||
|
||||
project.storeHasNot('is-positive')
|
||||
|
||||
// This should not throw an error even though the file: dependency doesn't exist
|
||||
await fetch.handler({
|
||||
...DEFAULT_OPTIONS,
|
||||
cacheDir: path.resolve('cache'),
|
||||
dir: process.cwd(),
|
||||
storeDir,
|
||||
})
|
||||
|
||||
project.storeHas('is-positive')
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user