Skip to content

Commit 3e65fbe

Browse files
abhu85claude
andauthored
fix(ensureSymlink): resolve relative srcpath correctly when symlink exists (#1064)
When ensureSymlink is called with a relative srcpath and the destination symlink already exists, the code was incorrectly resolving the srcpath relative to cwd instead of relative to the dstpath directory. This caused ENOENT errors when calling ensureSymlink a second time with the same relative path, because fs.stat() evaluated the relative path from the wrong location. The fix checks if srcpath is relative, and if so, first tries to resolve it relative to dstpath's directory (standard symlink behavior), falling back to cwd if that doesn't exist. Fixes #1038 Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent e2615e5 commit 3e65fbe

File tree

2 files changed

+79
-5
lines changed

2 files changed

+79
-5
lines changed

lib/ensure/__tests__/symlink.test.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,4 +350,53 @@ describe('fse-ensure-symlink', () => {
350350
if (newBehavior === 'dir-error') dirErrorSync(args, fn)
351351
})
352352
})
353+
354+
// https://github.com/jprichardson/node-fs-extra/issues/1038
355+
describe('ensureSymlink() with relative path called twice (issue #1038)', () => {
356+
it('should succeed when calling ensureSymlink twice with a relative path', async () => {
357+
const targetDir = path.join(TEST_DIR, 'target-1038')
358+
const linkDir = path.join(TEST_DIR, 'link-1038')
359+
const linkPath = path.join(linkDir, 'link')
360+
const relativeTarget = path.relative(linkDir, targetDir)
361+
362+
// Create target directory with a file
363+
await fse.ensureDir(targetDir)
364+
fs.writeFileSync(path.join(targetDir, 'file.txt'), 'content')
365+
366+
// First ensureSymlink call with relative path - should succeed
367+
await ensureSymlink(relativeTarget, linkPath, 'dir')
368+
assert.strictEqual(fs.lstatSync(linkPath).isSymbolicLink(), true)
369+
370+
// Second ensureSymlink call with same relative path - should also succeed
371+
await ensureSymlink(relativeTarget, linkPath, 'dir')
372+
assert.strictEqual(fs.lstatSync(linkPath).isSymbolicLink(), true)
373+
374+
// Verify the symlink still works
375+
const content = fs.readFileSync(path.join(linkPath, 'file.txt'), 'utf8')
376+
assert.strictEqual(content, 'content')
377+
})
378+
379+
it('should succeed when calling ensureSymlinkSync twice with a relative path', () => {
380+
const targetDir = path.join(TEST_DIR, 'target-1038-sync')
381+
const linkDir = path.join(TEST_DIR, 'link-1038-sync')
382+
const linkPath = path.join(linkDir, 'link')
383+
const relativeTarget = path.relative(linkDir, targetDir)
384+
385+
// Create target directory with a file
386+
fse.ensureDirSync(targetDir)
387+
fs.writeFileSync(path.join(targetDir, 'file.txt'), 'content')
388+
389+
// First ensureSymlinkSync call with relative path - should succeed
390+
ensureSymlinkSync(relativeTarget, linkPath, 'dir')
391+
assert.strictEqual(fs.lstatSync(linkPath).isSymbolicLink(), true)
392+
393+
// Second ensureSymlinkSync call with same relative path - should also succeed
394+
ensureSymlinkSync(relativeTarget, linkPath, 'dir')
395+
assert.strictEqual(fs.lstatSync(linkPath).isSymbolicLink(), true)
396+
397+
// Verify the symlink still works
398+
const content = fs.readFileSync(path.join(linkPath, 'file.txt'), 'utf8')
399+
assert.strictEqual(content, 'content')
400+
})
401+
})
353402
})

lib/ensure/symlink.js

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,22 @@ async function createSymlink (srcpath, dstpath, type) {
2020
} catch { }
2121

2222
if (stats && stats.isSymbolicLink()) {
23-
const [srcStat, dstStat] = await Promise.all([
24-
fs.stat(srcpath),
25-
fs.stat(dstpath)
26-
])
23+
// When srcpath is relative, resolve it relative to dstpath's directory
24+
// (standard symlink behavior) or fall back to cwd if that doesn't exist
25+
let srcStat
26+
if (path.isAbsolute(srcpath)) {
27+
srcStat = await fs.stat(srcpath)
28+
} else {
29+
const dstdir = path.dirname(dstpath)
30+
const relativeToDst = path.join(dstdir, srcpath)
31+
try {
32+
srcStat = await fs.stat(relativeToDst)
33+
} catch {
34+
srcStat = await fs.stat(srcpath)
35+
}
36+
}
2737

38+
const dstStat = await fs.stat(dstpath)
2839
if (areIdentical(srcStat, dstStat)) return
2940
}
3041

@@ -46,7 +57,21 @@ function createSymlinkSync (srcpath, dstpath, type) {
4657
stats = fs.lstatSync(dstpath)
4758
} catch { }
4859
if (stats && stats.isSymbolicLink()) {
49-
const srcStat = fs.statSync(srcpath)
60+
// When srcpath is relative, resolve it relative to dstpath's directory
61+
// (standard symlink behavior) or fall back to cwd if that doesn't exist
62+
let srcStat
63+
if (path.isAbsolute(srcpath)) {
64+
srcStat = fs.statSync(srcpath)
65+
} else {
66+
const dstdir = path.dirname(dstpath)
67+
const relativeToDst = path.join(dstdir, srcpath)
68+
try {
69+
srcStat = fs.statSync(relativeToDst)
70+
} catch {
71+
srcStat = fs.statSync(srcpath)
72+
}
73+
}
74+
5075
const dstStat = fs.statSync(dstpath)
5176
if (areIdentical(srcStat, dstStat)) return
5277
}

0 commit comments

Comments
 (0)