From 9fcc3f260c742e28e7fa6911bc0f597e6990aeee Mon Sep 17 00:00:00 2001 From: saberzero1 Date: Mon, 27 Apr 2026 22:34:31 +0200 Subject: [PATCH] fix: handle EEXIST race in peer dependency symlink creation --- quartz/cli/plugin-git-handlers.js | 18 +++++++++++------- quartz/plugins/loader/gitLoader.ts | 13 +++++++++++-- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/quartz/cli/plugin-git-handlers.js b/quartz/cli/plugin-git-handlers.js index 8a85721..6ddf8e8 100644 --- a/quartz/cli/plugin-git-handlers.js +++ b/quartz/cli/plugin-git-handlers.js @@ -127,6 +127,15 @@ function needsBuild(pluginDir) { * 2. All other peers → symlink to the host Quartz node_modules so plugins * 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 + } +} + function linkPeerPlugins(pluginDir) { const pkgPath = path.join(pluginDir, "package.json") if (!fs.existsSync(pkgPath)) return @@ -134,16 +143,13 @@ function linkPeerPlugins(pluginDir) { const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8")) const peers = pkg.peerDependencies ?? {} - // Locate the host Quartz node_modules (two levels up from .quartz/plugins/) const quartzRoot = path.resolve(pluginDir, "..", "..", "..") const hostNodeModules = path.join(quartzRoot, "node_modules") for (const peerName of Object.keys(peers)) { - // Check if this peer is already satisfied (e.g. installed as a regular dep) const peerNodeModulesPath = path.join(pluginDir, "node_modules", ...peerName.split("/")) if (fs.existsSync(peerNodeModulesPath)) continue - // Case 1: @quartz-community scoped packages → sibling plugin symlink if (peerName.startsWith("@quartz-community/")) { const siblingPlugin = findPluginByPackageName(peerName) if (!siblingPlugin) continue @@ -152,15 +158,13 @@ function linkPeerPlugins(pluginDir) { fs.mkdirSync(scopeDir, { recursive: true }) const target = path.relative(scopeDir, siblingPlugin) - fs.symlinkSync(target, peerNodeModulesPath, "dir") + trySymlink(target, peerNodeModulesPath) continue } - // Case 2: Other peers → resolve from host Quartz node_modules const hostPeerPath = path.join(hostNodeModules, ...peerName.split("/")) if (!fs.existsSync(hostPeerPath)) continue - // Ensure parent directory exists (for scoped packages like @napi-rs/simple-git) const parts = peerName.split("/") if (parts.length > 1) { const scopeDir = path.join(pluginDir, "node_modules", parts[0]) @@ -170,7 +174,7 @@ function linkPeerPlugins(pluginDir) { } const target = path.relative(path.dirname(peerNodeModulesPath), hostPeerPath) - fs.symlinkSync(target, peerNodeModulesPath, "dir") + trySymlink(target, peerNodeModulesPath) } } diff --git a/quartz/plugins/loader/gitLoader.ts b/quartz/plugins/loader/gitLoader.ts index 07563c4..3a511b1 100644 --- a/quartz/plugins/loader/gitLoader.ts +++ b/quartz/plugins/loader/gitLoader.ts @@ -306,6 +306,15 @@ function findPluginByPackageName(packageName: string): string | null { * share a single copy of packages like unified, vfile, preact, etc. * @quartz-community/* peers resolve to co-installed sibling plugins instead. */ +function trySymlink(target: string, linkPath: string): void { + try { + fs.symlinkSync(target, linkPath, "dir") + } catch (err: unknown) { + if ((err as NodeJS.ErrnoException).code === "EEXIST") return + throw err + } +} + function linkPeerDependencies(pluginDir: string): void { const pkgPath = path.join(pluginDir, "package.json") if (!fs.existsSync(pkgPath)) return @@ -328,7 +337,7 @@ function linkPeerDependencies(pluginDir: string): void { fs.mkdirSync(scopeDir, { recursive: true }) const target = path.relative(scopeDir, siblingPlugin) - fs.symlinkSync(target, peerNodeModulesPath, "dir") + trySymlink(target, peerNodeModulesPath) continue } @@ -344,7 +353,7 @@ function linkPeerDependencies(pluginDir: string): void { } const target = path.relative(path.dirname(peerNodeModulesPath), hostPeerPath) - fs.symlinkSync(target, peerNodeModulesPath, "dir") + trySymlink(target, peerNodeModulesPath) } }