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