diff --git a/.changeset/gold-colts-brush.md b/.changeset/gold-colts-brush.md new file mode 100644 index 0000000000..089a4545ce --- /dev/null +++ b/.changeset/gold-colts-brush.md @@ -0,0 +1,6 @@ +--- +"@pnpm/link-bins": patch +"pnpm": patch +--- + +Fixed EISDIR error when bin field points to a directory [#9441](https://github.com/pnpm/pnpm/issues/9441). diff --git a/cspell.json b/cspell.json index 6e9cee2729..4d37bfb416 100644 --- a/cspell.json +++ b/cspell.json @@ -57,6 +57,7 @@ "ebusy", "ehrkoext", "eintegrity", + "eisdir", "elifecycle", "elit", "emfile", diff --git a/pkg-manager/link-bins/src/index.ts b/pkg-manager/link-bins/src/index.ts index e4da2e902b..36114f5e86 100644 --- a/pkg-manager/link-bins/src/index.ts +++ b/pkg-manager/link-bins/src/index.ts @@ -284,7 +284,7 @@ async function linkBin (cmd: CommandInfo, binsDir: string, opts?: LinkBinOptions await symlinkDir(cmd.path, externalBinPath) await fixBin(cmd.path, 0o755) } catch (err: any) { // eslint-disable-line - if (err.code !== 'ENOENT') { + if (err.code !== 'ENOENT' && err.code !== 'EISDIR') { throw err } globalWarn(`Failed to create bin at ${externalBinPath}. ${err.message as string}`) @@ -308,7 +308,7 @@ async function linkBin (cmd: CommandInfo, binsDir: string, opts?: LinkBinOptions nodeExecPath: cmd.nodeExecPath, }) } catch (err: any) { // eslint-disable-line - if (err.code !== 'ENOENT') { + if (err.code !== 'ENOENT' && err.code !== 'EISDIR') { throw err } globalWarn(`Failed to create bin at ${externalBinPath}. ${err.message as string}`) diff --git a/pkg-manager/link-bins/test/fixtures/bin-is-directory/.gitignore b/pkg-manager/link-bins/test/fixtures/bin-is-directory/.gitignore new file mode 100644 index 0000000000..5867a0493e --- /dev/null +++ b/pkg-manager/link-bins/test/fixtures/bin-is-directory/.gitignore @@ -0,0 +1,2 @@ +!**/node_modules/**/* +!/node_modules/ diff --git a/pkg-manager/link-bins/test/fixtures/bin-is-directory/node_modules/invalid-bin/dist/readme.txt b/pkg-manager/link-bins/test/fixtures/bin-is-directory/node_modules/invalid-bin/dist/readme.txt new file mode 100644 index 0000000000..d2de8bf8d9 --- /dev/null +++ b/pkg-manager/link-bins/test/fixtures/bin-is-directory/node_modules/invalid-bin/dist/readme.txt @@ -0,0 +1 @@ +This is a directory, not a file. diff --git a/pkg-manager/link-bins/test/fixtures/bin-is-directory/node_modules/invalid-bin/package.json b/pkg-manager/link-bins/test/fixtures/bin-is-directory/node_modules/invalid-bin/package.json new file mode 100644 index 0000000000..174040dbfa --- /dev/null +++ b/pkg-manager/link-bins/test/fixtures/bin-is-directory/node_modules/invalid-bin/package.json @@ -0,0 +1,5 @@ +{ + "name": "invalid-bin", + "version": "1.0.0", + "bin": "./dist" +} diff --git a/pkg-manager/link-bins/test/index.ts b/pkg-manager/link-bins/test/index.ts index 9192808ee9..0449f3de99 100644 --- a/pkg-manager/link-bins/test/index.ts +++ b/pkg-manager/link-bins/test/index.ts @@ -32,6 +32,7 @@ const f = fixtures(__dirname) beforeEach(() => { jest.mocked(binsConflictLogger.debug).mockClear() + jest.mocked(globalWarn).mockClear() }) const POWER_SHELL_IS_SUPPORTED = isWindows() @@ -535,6 +536,17 @@ testOnWindows('linkBins() should remove an existing .exe file from the target di expect(fs.readdirSync(binTarget)).toEqual(getExpectedBins(['simple'])) }) +test('linkBins() should handle bin field pointing to a directory gracefully', async () => { + const binTarget = tempy.directory() + const binIsDirFixture = f.prepare('bin-is-directory') + const warn = jest.fn() + + await linkBins(path.join(binIsDirFixture, 'node_modules'), binTarget, { warn }) + + expect(fs.readdirSync(binTarget)).toEqual([]) + expect(globalWarn).toHaveBeenCalled() +}) + describe('enable prefer-symlinked-executables', () => { test('linkBins()', async () => { const binTarget = tempy.directory()