UNPKG

symlink-dir

Version:

Cross-platform directory symlinking

206 lines 8.16 kB
"use strict"; const betterPathResolve = require("better-path-resolve"); const fs_1 = require("fs"); const util = require("util"); const pathLib = require("path"); const 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 // lack permission to create them const symlinkType = IS_WINDOWS ? 'junction' : 'dir'; const resolveSrc = IS_WINDOWS ? resolveSrcOnWin : resolveSrcOnNonWin; function resolveSrcOnWin(src, dest) { return `${src}\\`; } function resolveSrcOnNonWin(src, dest) { return pathLib.relative(pathLib.dirname(dest), src); } 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})`); target = resolveSrc(target, path); return forceSymlink(target, path, opts); } /** * Creates a symlink. Re-link if a symlink already exists at the supplied * srcPath. API compatible with [`fs#symlink`](https://nodejs.org/api/fs.html#fs_fs_symlink_srcpath_dstpath_type_callback). */ async function forceSymlink(target, path, opts) { let initialErr; try { await fs_1.promises.symlink(target, path, symlinkType); return { reused: false }; } catch (err) { switch (err.code) { case 'ENOENT': try { await fs_1.promises.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_1.promises.readlink(path); } catch (err) { if ((opts === null || opts === void 0 ? void 0 : opts.overwrite) === false) { throw initialErr; } // path is not a link const parentDir = pathLib.dirname(path); let warn; if (opts === null || opts === void 0 ? void 0 : 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_1.promises.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 (util.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 (pathLib.relative(target, linkString) === '') { return { reused: true }; } if ((opts === null || opts === void 0 ? void 0 : opts.overwrite) === false) { throw initialErr; } try { await fs_1.promises.unlink(path); } catch (error) { if (!util.types.isNativeError(error) || !('code' in error) || error.code !== 'ENOENT') { throw error; } } return await forceSymlink(target, path, opts); } // for backward compatibility symlinkDir['default'] = symlinkDir; (function (symlinkDir) { function sync(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})`); target = resolveSrc(target, path); return forceSymlinkSync(target, path, opts); } symlinkDir.sync = sync; })(symlinkDir || (symlinkDir = {})); function forceSymlinkSync(target, path, opts) { let initialErr; try { (0, fs_1.symlinkSync)(target, path, symlinkType); return { reused: false }; } catch (err) { initialErr = err; switch (err.code) { case 'ENOENT': try { (0, fs_1.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 = (0, fs_1.readlinkSync)(path); } catch (err) { if ((opts === null || opts === void 0 ? void 0 : opts.overwrite) === false) { throw initialErr; } // path is not a link const parentDir = pathLib.dirname(path); let warn; if (opts === null || opts === void 0 ? void 0 : 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 (0, fs_1.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 { renameOverwrite.sync(path, pathLib.join(parentDir, ignore)); } catch (error) { if (util.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 (pathLib.relative(target, linkString) === '') { return { reused: true }; } if ((opts === null || opts === void 0 ? void 0 : opts.overwrite) === false) { throw initialErr; } try { (0, fs_1.unlinkSync)(path); } catch (error) { if (!util.types.isNativeError(error) || !('code' in error) || error.code !== 'ENOENT') { throw error; } } return forceSymlinkSync(target, path, opts); } module.exports = symlinkDir; //# sourceMappingURL=index.js.map