fix: fall back to junctions on windows

This commit is contained in:
saberzero1
2026-05-26 21:15:20 +02:00
parent 667d487a0f
commit 882d3b3f50
3 changed files with 63 additions and 11 deletions

View File

@@ -22,6 +22,7 @@ import {
gitPull,
popContentFolder,
stashContentFolder,
symlinkOrCopy,
} from "./helpers.js"
import {
handlePluginRestore,
@@ -210,7 +211,7 @@ export async function handleCreate(argv) {
preserveTimestamps: true,
})
} else if (setupStrategy === "symlink") {
await fs.promises.symlink(originalFolder, contentFolder, "dir")
await symlinkOrCopy(originalFolder, contentFolder)
}
} else if (setupStrategy === "new") {
await fs.promises.writeFile(

View File

@@ -3,6 +3,7 @@ import { styleText } from "util"
import { contentCacheFolder } from "./constants.js"
import { spawnSync } from "child_process"
import fs from "fs"
import path from "path"
export function escapePath(fp) {
return fp
@@ -52,3 +53,57 @@ export async function popContentFolder(contentFolder) {
})
await fs.promises.rm(contentCacheFolder, { force: true, recursive: true })
}
/**
* Create a directory symlink with Windows fallback.
*
* On Windows, creating symlinks requires Developer Mode or admin privileges.
* When that fails (EPERM), we try a junction first (no elevation needed),
* then fall back to a recursive copy as a last resort.
*
* @param {string} target Symlink target (may be relative)
* @param {string} linkPath Path where the link is created
*/
export function symlinkOrCopySync(target, linkPath) {
try {
fs.symlinkSync(target, linkPath, "dir")
} catch (err) {
if (err.code === "EEXIST") return
if (err.code === "EPERM" && process.platform === "win32") {
try {
fs.symlinkSync(target, linkPath, "junction")
return
} catch {
const resolvedTarget = path.resolve(path.dirname(linkPath), target)
fs.cpSync(resolvedTarget, linkPath, { recursive: true })
return
}
}
throw err
}
}
/**
* Async version of {@link symlinkOrCopySync}.
*
* @param {string} target Symlink target (may be relative)
* @param {string} linkPath Path where the link is created
*/
export async function symlinkOrCopy(target, linkPath) {
try {
await fs.promises.symlink(target, linkPath, "dir")
} catch (err) {
if (err.code === "EEXIST") return
if (err.code === "EPERM" && process.platform === "win32") {
try {
await fs.promises.symlink(target, linkPath, "junction")
return
} catch {
const resolvedTarget = path.resolve(path.dirname(linkPath), target)
await fs.promises.cp(resolvedTarget, linkPath, { recursive: true })
return
}
}
throw err
}
}

View File

@@ -20,6 +20,7 @@ import {
resolveLockfileName,
getNameOverrides,
} from "./plugin-data.js"
import { symlinkOrCopySync } from "./helpers.js"
const INTERNAL_EXPORTS = new Set(["manifest", "default"])
@@ -128,12 +129,7 @@ function needsBuild(pluginDir) {
* share a single copy of packages like unified, vfile, rehype-raw, etc.
*/
function trySymlink(target, linkPath) {
try {
fs.symlinkSync(target, linkPath, "dir")
} catch (err) {
if (err.code === "EEXIST") return
throw err
}
symlinkOrCopySync(target, linkPath)
}
function linkPeerPlugins(pluginDir) {
@@ -621,7 +617,7 @@ export async function handlePluginInstallUnified({
}
console.log(styleText("cyan", `→ Linking ${name} from ${resolvedPath}...`))
fs.mkdirSync(path.dirname(pluginDir), { recursive: true })
fs.symlinkSync(resolvedPath, pluginDir, "dir")
symlinkOrCopySync(resolvedPath, pluginDir)
lockfile.plugins[name] = {
source: entry.source,
resolved: resolvedPath,
@@ -810,7 +806,7 @@ export async function handlePluginInstallUnified({
continue
}
fs.mkdirSync(path.dirname(pluginDir), { recursive: true })
fs.symlinkSync(entry.resolved, pluginDir, "dir")
symlinkOrCopySync(entry.resolved, pluginDir)
console.log(styleText("green", `${name} restored (local symlink)`))
restoredPlugins.push({ name, pluginDir })
installed++
@@ -1045,7 +1041,7 @@ export async function handlePluginInstallUnified({
continue
}
fs.mkdirSync(path.dirname(pluginDir), { recursive: true })
fs.symlinkSync(entry.resolved, pluginDir, "dir")
symlinkOrCopySync(entry.resolved, pluginDir)
console.log(styleText("green", `${name} (local) linked`))
pluginsToBuild.push({ name, pluginDir })
installed++
@@ -1222,7 +1218,7 @@ export async function handlePluginAdd(
}
console.log(styleText("cyan", `→ Adding ${name} from local path ${resolvedPath}...`))
fs.mkdirSync(path.dirname(pluginDir), { recursive: true })
fs.symlinkSync(resolvedPath, pluginDir, "dir")
symlinkOrCopySync(resolvedPath, pluginDir)
lockfile.plugins[name] = {
source,
resolved: resolvedPath,