From 57aaa791d06414f1719def4d51a025420279b277 Mon Sep 17 00:00:00 2001 From: Tatsunori Uchino Date: Sun, 20 Oct 2024 23:45:17 +0900 Subject: [PATCH] Use symbolic link if possible in Windows --- README.md | 3 ++- src/index.ts | 66 ++++++++++++++++++++++++++++++++++++++++------------ 2 files changed, 53 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 60a0c35..092cf17 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,8 @@ [![npm version](https://img.shields.io/npm/v/symlink-dir.svg)](https://www.npmjs.com/package/symlink-dir) -* Always uses "junctions" on Windows. Even though support for "symbolic links" was added in Vista+, users by default lack permission to create them +* Uses "junctions" on Windows if "symbolic links" is disallowed. Even though support for "symbolic links" was added in Vista+, users by default lack permission to create them +* If you prefer symbolic links in Windows, turn on the developer mode * Any file or directory, that has the destination name, is renamed before creating the link ## Installation diff --git a/src/index.ts b/src/index.ts index 4a5dc93..14c01f5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,28 +5,34 @@ import renameOverwrite = require('rename-overwrite') const IS_WINDOWS = process.platform === 'win32' || /^(msys|cygwin)$/.test(process.env.OSTYPE) -// Always use "junctions" on Windows. Even though support for "symbolic links" was added in Vista+, users by default +// Falls back to "junctions" on Windows if "symbolic links" is disallowed. Even though support for "symbolic links" was added in Vista+, users by default // lack permission to create them -const symlinkType = IS_WINDOWS ? 'junction' : 'dir' +let symlinkType: 'dir' | 'junction' = 'dir' -const resolveSrc = IS_WINDOWS ? resolveSrcOnWin: resolveSrcOnNonWin +let symlinkPermissionCheckDone = !IS_WINDOWS -function resolveSrcOnWin (src: string, dest: string) { +let resolveSrc = resolveSrcOnTrueSymlink + +function resolveSrcOnWinJunction (src: string, dest: string) { return `${src}\\` } -function resolveSrcOnNonWin (src: string, dest: string) { +function resolveSrcOnTrueSymlink (src: string, dest: string) { return pathLib.relative(pathLib.dirname(dest), src) } +function resolveExistingLinkTarget (linkTarget: string, linkPath: string) { + if (!IS_WINDOWS) return linkTarget + // Can be absolute (junction or symlink) or relative (symlink) in Windows, so we need to unify to absolute path + return betterPathResolve(pathLib.isAbsolute(linkTarget) ? linkTarget : pathLib.resolve(pathLib.dirname(linkPath), linkTarget)) +} + function symlinkDir (target: string, path: string, opts?: { overwrite?: boolean }): Promise<{ reused: boolean, warn?: string }> { path = betterPathResolve(path) target = betterPathResolve(target) if (target === path) throw new Error(`Symlink path is the same as the target path (${target})`) - target = resolveSrc(target, path) - return forceSymlink(target, path, opts) } @@ -44,7 +50,23 @@ async function forceSymlink ( ): Promise<{ reused: boolean, warn?: string }> { let initialErr: Error try { - await fs.symlink(target, path, symlinkType) + if (symlinkPermissionCheckDone) { + await fs.symlink(resolveSrc(target, path), path, symlinkType) + } else { + try { + await fs.symlink(resolveSrc(target, path), path, symlinkType) + symlinkPermissionCheckDone = true + } catch (err) { + if ((err).code === 'EPERM') { + await fs.symlink(resolveSrcOnWinJunction(target, path), path, 'junction') + symlinkType = 'junction' + resolveSrc = resolveSrcOnWinJunction + symlinkPermissionCheckDone = true + } else { + throw err + } + } + } return { reused: false } } catch (err) { switch ((err).code) { @@ -52,7 +74,7 @@ async function forceSymlink ( try { await fs.mkdir(pathLib.dirname(path), { recursive: true }) } catch (mkdirError) { - mkdirError.message = `Error while trying to symlink "${target}" to "${path}". ` + + mkdirError.message = `Error while trying to symlink "${resolveSrc(target, path)}" to "${path}". ` + `The error happened while trying to create the parent directory for the symlink target. ` + `Details: ${mkdirError}` throw mkdirError @@ -97,7 +119,7 @@ async function forceSymlink ( } } - if (target === linkString) { + if (target === resolveExistingLinkTarget(linkString, path)) { return { reused: true } } if (opts?.overwrite === false) { @@ -119,8 +141,6 @@ namespace symlinkDir { if (target === path) throw new Error(`Symlink path is the same as the target path (${target})`) - target = resolveSrc(target, path) - return forceSymlinkSync(target, path, opts) } } @@ -135,7 +155,23 @@ function forceSymlinkSync ( ): { reused: boolean, warn?: string } { let initialErr: Error try { - symlinkSync(target, path, symlinkType) + if (symlinkPermissionCheckDone) { + symlinkSync(resolveSrc(target, path), path, symlinkType) + } else { + try { + symlinkSync(resolveSrc(target, path), path, symlinkType) + symlinkPermissionCheckDone = true + } catch (err) { + if ((err).code === 'EPERM') { + symlinkSync(resolveSrcOnWinJunction(target, path), path, 'junction') + symlinkType = 'junction' + resolveSrc = resolveSrcOnWinJunction + symlinkPermissionCheckDone = true + } else { + throw err + } + } + } return { reused: false } } catch (err) { initialErr = err @@ -144,7 +180,7 @@ function forceSymlinkSync ( try { mkdirSync(pathLib.dirname(path), { recursive: true }) } catch (mkdirError) { - mkdirError.message = `Error while trying to symlink "${target}" to "${path}". ` + + mkdirError.message = `Error while trying to symlink "${resolveSrc(target, path)}" to "${path}". ` + `The error happened while trying to create the parent directory for the symlink target. ` + `Details: ${mkdirError}` throw mkdirError @@ -188,7 +224,7 @@ function forceSymlinkSync ( } } - if (target === linkString) { + if (target === resolveExistingLinkTarget(linkString, path)) { return { reused: true } } if (opts?.overwrite === false) {