Node.js: extra methods for the fs object like copy(), remove(), mkdirs()
- **Operating System: macOS 14.2.1** - **Node.js version: v20.11.1** - **`fs-extra` version: 11.2.0** # Issue - create a target directory with a file in it - create a symbolic link from somewhere else to the target directory - do this a second time This works if the reference to the `target` is an absolute path, but fails if the reference is a _relative_ path. # Example This is a `mocha` test that shows the issue. The first test ensures the symbolic link a second time with an absolute path, which succeeds as expected. The second test ensures the symbolic link a second time with a _relative_ path, which is rejected with `ENOENT`, but should succeed too. ```javascript /* eslint-env mocha */ require('should') const { resolve, join, relative, dirname } = require('node:path') const { ensureFile, remove, pathExists, ensureSymlink } = require('fs-extra') const testBaseDirectory = resolve('fs-extra-test-base-directory') describe('fs-extra ensureSymlink fails when ensuring a symbolic link with a relative path if it already exists', function () { beforeEach(async function () { // a directory with a file, as `destination` or `target` this.targetDirectory = join(testBaseDirectory, 'target-directory') const targetFileName = 'target-file' this.targetDirectoryFile = join(this.targetDirectory, targetFileName) await ensureFile(this.targetDirectoryFile) // a directory to put the symbolic link in (the `source`) this.linkDirectory = join(testBaseDirectory, 'link-directory') this.symbolicLinkPath = join(this.linkDirectory, 'link') this.targetFileViaSymbolicLink = join(this.symbolicLinkPath, targetFileName) this.relativeSymbolicLinkReference = relative(dirname(this.symbolicLinkPath), this.targetDirectory) }) afterEach(async function () { return remove(testBaseDirectory) }) it('can ensure a symbolic link a second time with an absolute path', async function () { await pathExists(this.targetDirectoryFile).should.be.resolvedWith(true) // first time, setting up with a relative reference await ensureSymlink(this.relativeSymbolicLinkReference, this.symbolicLinkPath, 'dir').should.be.resolved() await pathExists(this.symbolicLinkPath).should.be.resolvedWith(true) await pathExists(this.targetFileViaSymbolicLink).should.be.resolvedWith(true) // second time, setting up with an absolute reference await ensureSymlink(this.targetDirectory, this.symbolicLinkPath, 'dir').should.be.resolved() await pathExists(this.symbolicLinkPath).should.be.resolvedWith(true) await pathExists(this.targetFileViaSymbolicLink).should.be.resolvedWith(true) }) it('can ensure a symbolic link a second time with a relative path', async function () { await pathExists(this.targetDirectoryFile).should.be.resolvedWith(true) // first time, setting up with a relative reference await ensureSymlink(this.relativeSymbolicLinkReference, this.symbolicLinkPath, 'dir').should.be.resolved() await pathExists(this.symbolicLinkPath).should.be.resolvedWith(true) await pathExists(this.targetFileViaSymbolicLink).should.be.resolvedWith(true) // second time, setting up with a relative reference SHOULD ALSO RESOLVE, BUT REJECTS const error = await ensureSymlink( this.relativeSymbolicLinkReference, this.symbolicLinkPath, 'dir' ).should.be.rejected() error.code.should.equal('ENOENT') // YET THE TARGET FILE EXISTS VIA THE ABSOLUTE PATH await pathExists(this.targetDirectory).should.be.resolvedWith(true) // AND THE RELATIVE PATH RESOLVES TO THE ABSOLUTE PATH join(dirname(this.symbolicLinkPath), this.relativeSymbolicLinkReference).should.equal(this.targetDirectory) }) }) ``` # Analysis The issue is clear in `fs-extra/lib/ensure/symlink.js`, line 24, versus line 32. When there is no symbolic link yet at `dstpath`, the `if` of line 22 is skipped ```javascript let stats try { stats = await fs.lstat(dstpath) } catch { } if (stats && stats.isSymbolicLink()) { … } ``` and we arrive at line 31—32 where work is done to deal with relative `srcpath`s: ```javascript const relative = await symlinkPaths(srcpath, dstpath) srcpath = relative.toDst ``` When there is a symbolic link at `dstpath`, the `if`–branch at line 22 is executed. Here, the status of the `srcpath` is requested _as is_: ```javascript fs.stat(srcpath), ``` This evaluates a relative `srcpath` relative to the `cwd`, not to the `dstpath`. At that location the source does not exist, which results in `ENOENT`.
This issue appears to be discussing a feature request or bug report related to the repository. Based on the content, it seems to be still under discussion. The issue was opened by jandockx and has received 5 comments.