symlink-dir
Version:
Cross-platform directory symlinking
265 lines • 10.4 kB
JavaScript
;
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