UNPKG

symlink-dir

Version:

Cross-platform directory symlinking

265 lines 10.4 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); function resolveSrcOnWinJunction(target, path) { return `${pathLib.isAbsolute(target) ? target : pathLib.join(pathLib.dirname(path), target)}\\`; } function symlinkDir(target, path, opts) { path = betterPathResolve(path); if (betterPathResolve(target) === path) throw new Error(`Symlink path is the same as the target path (${target})`); return forceSymlink(target, path, opts); } function isExistingSymlinkUpToDate(wantedTarget, path, linkString) { if (wantedTarget === linkString) return true; // 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); const wantedTargetAbsolute = pathLib.isAbsolute(wantedTarget) ? wantedTarget : pathLib.join(pathLib.dirname(path), wantedTarget); return pathLib.relative(wantedTargetAbsolute, 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_1.promises.symlink(target, path); } function createTrueSymlinkSync(target, path) { (0, fs_1.symlinkSync)(target, path); } function createJunctionAsync(target, path) { return fs_1.promises.symlink(resolveSrcOnWinJunction(target, path), path, 'junction'); } function createJunctionSync(target, path) { (0, fs_1.symlinkSync)(resolveSrcOnWinJunction(target, path), path, 'junction'); } async function forceSymlink(target, path, opts) { let initialErr; try { if ((opts === null || opts === void 0 ? void 0 : 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_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 (isExistingSymlinkUpToDate(target, path, 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); if (betterPathResolve(target) === path) throw new Error(`Symlink path is the same as the target path (${target})`); return forceSymlinkSync(target, path, opts); } symlinkDir.sync = sync; })(symlinkDir || (symlinkDir = {})); function forceSymlinkSync(target, path, opts) { let initialErr; try { if ((opts === null || opts === void 0 ? void 0 : opts.noJunction) === true) { createTrueSymlinkSync(target, path); } else { createSymlinkSync(target, path); } 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 (isExistingSymlinkUpToDate(target, path, 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