diff --git a/.changeset/great-jeans-teach.md b/.changeset/great-jeans-teach.md new file mode 100644 index 00000000000..f5cb36f82ae --- /dev/null +++ b/.changeset/great-jeans-teach.md @@ -0,0 +1,5 @@ +--- +"@pnpm/directory-fetcher": major +--- + +@pnpm/logger added to peer dependencies. diff --git a/.changeset/young-toes-talk.md b/.changeset/young-toes-talk.md new file mode 100644 index 00000000000..f7178d9a6e3 --- /dev/null +++ b/.changeset/young-toes-talk.md @@ -0,0 +1,6 @@ +--- +"@pnpm/directory-fetcher": patch +"pnpm": patch +--- + +Installation shouldn't fail when the injected dependency has broken symlinks. The broken symlinks should be just skipped [#5598](https://github.com/pnpm/pnpm/issues/5598). diff --git a/packages/directory-fetcher/package.json b/packages/directory-fetcher/package.json index 25a20905c9b..f8bbf4fbeb3 100644 --- a/packages/directory-fetcher/package.json +++ b/packages/directory-fetcher/package.json @@ -30,6 +30,9 @@ "url": "https://github.com/pnpm/pnpm/issues" }, "homepage": "https://github.com/pnpm/pnpm/blob/main/packages/directory-fetcher#readme", + "peerDependencies": { + "@pnpm/logger": "^5.0.0" + }, "dependencies": { "@pnpm/fetcher-base": "workspace:*", "@pnpm/read-project-manifest": "workspace:*", diff --git a/packages/directory-fetcher/src/index.ts b/packages/directory-fetcher/src/index.ts index 5a702a35e8c..8e7938b563e 100644 --- a/packages/directory-fetcher/src/index.ts +++ b/packages/directory-fetcher/src/index.ts @@ -1,10 +1,13 @@ -import { promises as fs } from 'fs' +import { promises as fs, Stats } from 'fs' import path from 'path' import type { DirectoryFetcher, DirectoryFetcherOptions } from '@pnpm/fetcher-base' +import { logger } from '@pnpm/logger' import { safeReadProjectManifestOnly } from '@pnpm/read-project-manifest' import fromPairs from 'ramda/src/fromPairs' import packlist from 'npm-packlist' +const directoryFetcherLogger = logger('directory-fetcher') + export interface CreateDirectoryFetcherOptions { includeOnlyPackageFiles?: boolean } @@ -65,7 +68,17 @@ async function _fetchAllFilesFromDir ( .filter((file) => file !== 'node_modules') .map(async (file) => { const filePath = path.join(dir, file) - const stat = await fs.stat(filePath) + let stat: Stats + try { + stat = await fs.stat(filePath) + } catch (err: any) { // eslint-disable-line @typescript-eslint/no-explicit-any + // Broken symlinks are skipped + if (err.code === 'ENOENT') { + directoryFetcherLogger.debug({ brokenSymlink: filePath }) + return + } + throw err + } const relativeSubdir = `${relativeDir}${relativeDir ? '/' : ''}${file}` if (stat.isDirectory()) { const subFilesIndex = await _fetchAllFilesFromDir(filePath, relativeSubdir) diff --git a/packages/directory-fetcher/test/fixtures/pkg-with-broken-symlink/index.js b/packages/directory-fetcher/test/fixtures/pkg-with-broken-symlink/index.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/directory-fetcher/test/fixtures/pkg-with-broken-symlink/not-exists b/packages/directory-fetcher/test/fixtures/pkg-with-broken-symlink/not-exists new file mode 120000 index 00000000000..a11a2d2d855 --- /dev/null +++ b/packages/directory-fetcher/test/fixtures/pkg-with-broken-symlink/not-exists @@ -0,0 +1 @@ +broken-symlink \ No newline at end of file diff --git a/packages/directory-fetcher/test/fixtures/pkg-with-broken-symlink/package.json b/packages/directory-fetcher/test/fixtures/pkg-with-broken-symlink/package.json new file mode 100644 index 00000000000..6fea5f21a37 --- /dev/null +++ b/packages/directory-fetcher/test/fixtures/pkg-with-broken-symlink/package.json @@ -0,0 +1,4 @@ +{ + "name": "simple-pkg", + "version": "0.0.0" +} diff --git a/packages/directory-fetcher/test/index.ts b/packages/directory-fetcher/test/index.ts index 998c50c2843..2cc3ccb5176 100644 --- a/packages/directory-fetcher/test/index.ts +++ b/packages/directory-fetcher/test/index.ts @@ -1,9 +1,15 @@ /// import path from 'path' import { createDirectoryFetcher } from '@pnpm/directory-fetcher' +// @ts-expect-error +import { debug } from '@pnpm/logger' import { fixtures } from '@pnpm/test-fixtures' const f = fixtures(__dirname) +jest.mock('@pnpm/logger', () => { + const debug = jest.fn() + return ({ debug, logger: () => ({ debug }) }) +}) test('fetch including only package files', async () => { process.chdir(f.find('simple-pkg')) @@ -79,3 +85,28 @@ test('fetch a directory that has no package.json', async () => { 'index.js', ]) }) + +test('fetch does not fail on package with broken symlink', async () => { + debug.mockClear() + process.chdir(f.find('pkg-with-broken-symlink')) + const fetcher = createDirectoryFetcher() + + // eslint-disable-next-line + const fetchResult = await fetcher.directory({} as any, { + directory: '.', + type: 'directory', + }, { + lockfileDir: process.cwd(), + }) + + expect(fetchResult.local).toBe(true) + expect(fetchResult.packageImportMethod).toBe('hardlink') + expect(fetchResult.filesIndex['package.json']).toBe(path.resolve('package.json')) + + // Only those files are included which would get published + expect(Object.keys(fetchResult.filesIndex).sort()).toStrictEqual([ + 'index.js', + 'package.json', + ]) + expect(debug).toHaveBeenCalledWith({ brokenSymlink: path.resolve('not-exists') }) +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 134b4215712..c1f22bd3290 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1047,6 +1047,9 @@ importers: '@pnpm/fetcher-base': specifier: workspace:* version: link:../fetcher-base + '@pnpm/logger': + specifier: ^5.0.0 + version: 5.0.0 '@pnpm/read-project-manifest': specifier: workspace:* version: link:../read-project-manifest