UNPKG

symlink-dir

Version:

Cross-platform directory symlinking

260 lines 9.69 kB
import { betterPathResolve } from 'better-path-resolve'; import { promises as fs, symlinkSync, mkdirSync, readlinkSync, unlinkSync } from 'fs'; import { types } from 'util'; import pathLib from 'path'; import { renameOverwrite, renameOverwriteSync } from 'rename-overwrite'; const IS_WINDOWS = process.platform === 'win32' || /^(msys|cygwin)$/.test(process.env.OSTYPE); function resolveSrcOnWinJunction(src) { return `${src}\\`; } function resolveSrcOnTrueSymlink(src, dest) { return pathLib.relative(pathLib.dirname(dest), src); } export function symlinkDir(target, path, opts) { path = betterPathResolve(path); target = betterPathResolve(target); if (target === path) throw new Error(`Symlink path is the same as the target path (${target})`); return forceSymlink(target, path, opts); } export function symlinkDirSync(target, path, opts) { path = betterPathResolve(path); target = betterPathResolve(target); if (target === path) throw new Error(`Symlink path is the same as the target path (${target})`); return forceSymlinkSync(target, path, opts); } function isExistingSymlinkUpToDate(wantedTarget, path, linkString) { // path is going to be that of the symlink, so never be a (drive) root, therefore dirname(path) is different from path const existingTarget = pathLib.isAbsolute(linkString) ? linkString : pathLib.join(pathLib.dirname(path), linkString); return pathLib.relative(wantedTarget, existingTarget) === ''; } let createSymlinkAsync; let createSymlinkSync; if (IS_WINDOWS) { // 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 createSymlinkAsync = async (target, path) => { try { await createTrueSymlinkAsync(target, path); createSymlinkSync = createTrueSymlinkSync; createSymlinkAsync = createTrueSymlinkAsync; } catch (err) { if (err.code === 'EPERM') { await createJunctionAsync(target, path); createSymlinkSync = createJunctionSync; createSymlinkAsync = createJunctionAsync; } else { throw err; } } }; createSymlinkSync = (target, path) => { try { createTrueSymlinkSync(target, path); createSymlinkSync = createTrueSymlinkSync; createSymlinkAsync = createTrueSymlinkAsync; } catch (err) { if (err.code === 'EPERM') { createJunctionSync(target, path); createSymlinkSync = createJunctionSync; createSymlinkAsync = createJunctionAsync; } else { throw err; } } }; } else { createSymlinkAsync = createTrueSymlinkAsync; createSymlinkSync = createTrueSymlinkSync; } function createTrueSymlinkAsync(target, path) { return fs.symlink(resolveSrcOnTrueSymlink(target, path), path, 'dir'); } function createTrueSymlinkSync(target, path) { symlinkSync(resolveSrcOnTrueSymlink(target, path), path, 'dir'); } function createJunctionAsync(target, path) { return fs.symlink(resolveSrcOnWinJunction(target), path, 'junction'); } function createJunctionSync(target, path) { symlinkSync(resolveSrcOnWinJunction(target), path, 'junction'); } async function forceSymlink(target, path, opts) { let initialErr; try { if (opts?.noJunction === true) { await createTrueSymlinkAsync(target, path); } else { await createSymlinkAsync(target, path); } return { reused: false }; } catch (err) { switch (err.code) { case 'ENOENT': try { await fs.mkdir(pathLib.dirname(path), { recursive: true }); } catch (mkdirError) { mkdirError.message = `Error while trying to symlink "${target}" to "${path}". ` + `The error happened while trying to create the parent directory for the symlink target. ` + `Details: ${mkdirError}`; throw mkdirError; } await forceSymlink(target, path, opts); return { reused: false }; case 'EEXIST': case 'EISDIR': initialErr = err; // If the target file already exists then we proceed. // Additional checks are done below. break; default: throw err; } } let linkString; try { linkString = await fs.readlink(path); } catch (err) { if (opts?.overwrite === false) { throw initialErr; } // path is not a link const parentDir = pathLib.dirname(path); let warn; if (opts?.renameTried) { // This is needed in order to fix a mysterious bug that sometimes happens on macOS. // It is hard to reproduce and is described here: https://github.com/pnpm/pnpm/issues/5909#issuecomment-1400066890 await fs.unlink(path); warn = `Symlink wanted name was occupied by directory or file. Old entity removed: "${parentDir}${pathLib.sep}{${pathLib.basename(path)}".`; } else { const ignore = `.ignored_${pathLib.basename(path)}`; try { await renameOverwrite(path, pathLib.join(parentDir, ignore)); } catch (error) { if (types.isNativeError(error) && 'code' in error && error.code === 'ENOENT') { throw initialErr; } throw error; } warn = `Symlink wanted name was occupied by directory or file. Old entity moved: "${parentDir}${pathLib.sep}{${pathLib.basename(path)} => ${ignore}".`; } return { ...await forceSymlink(target, path, { ...opts, renameTried: true }), warn, }; } if (isExistingSymlinkUpToDate(target, path, linkString)) { return { reused: true }; } if (opts?.overwrite === false) { throw initialErr; } try { await fs.unlink(path); } catch (error) { if (!types.isNativeError(error) || !('code' in error) || error.code !== 'ENOENT') { throw error; } } return await forceSymlink(target, path, opts); } function forceSymlinkSync(target, path, opts) { let initialErr; try { if (opts?.noJunction === true) { createTrueSymlinkSync(target, path); } else { createSymlinkSync(target, path); } return { reused: false }; } catch (err) { initialErr = err; switch (err.code) { case 'ENOENT': try { mkdirSync(pathLib.dirname(path), { recursive: true }); } catch (mkdirError) { mkdirError.message = `Error while trying to symlink "${target}" to "${path}". ` + `The error happened while trying to create the parent directory for the symlink target. ` + `Details: ${mkdirError}`; throw mkdirError; } forceSymlinkSync(target, path, opts); return { reused: false }; case 'EEXIST': case 'EISDIR': // If the target file already exists then we proceed. // Additional checks are done below. break; default: throw err; } } let linkString; try { linkString = readlinkSync(path); } catch (err) { if (opts?.overwrite === false) { throw initialErr; } // path is not a link const parentDir = pathLib.dirname(path); let warn; if (opts?.renameTried) { // This is needed in order to fix a mysterious bug that sometimes happens on macOS. // It is hard to reproduce and is described here: https://github.com/pnpm/pnpm/issues/5909#issuecomment-1400066890 unlinkSync(path); warn = `Symlink wanted name was occupied by directory or file. Old entity removed: "${parentDir}${pathLib.sep}{${pathLib.basename(path)}".`; } else { const ignore = `.ignored_${pathLib.basename(path)}`; try { renameOverwriteSync(path, pathLib.join(parentDir, ignore)); } catch (error) { if (types.isNativeError(error) && 'code' in error && error.code === 'ENOENT') { throw initialErr; } throw error; } warn = `Symlink wanted name was occupied by directory or file. Old entity moved: "${parentDir}${pathLib.sep}{${pathLib.basename(path)} => ${ignore}".`; } return { ...forceSymlinkSync(target, path, { ...opts, renameTried: true }), warn, }; } if (isExistingSymlinkUpToDate(target, path, linkString)) { return { reused: true }; } if (opts?.overwrite === false) { throw initialErr; } try { unlinkSync(path); } catch (error) { if (!types.isNativeError(error) || !('code' in error) || error.code !== 'ENOENT') { throw error; } } return forceSymlinkSync(target, path, opts); } //# sourceMappingURL=index.js.map