From 6a0dc3a9bc0981594def73fd25c457e5537707c4 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 27 Apr 2026 14:08:54 +0100 Subject: [PATCH] fix: cache plugin discovery realpaths --- CHANGELOG.md | 1 + src/plugins/bundle-manifest.ts | 4 ++ src/plugins/discovery.test.ts | 85 +++++++++++++++++++++++++ src/plugins/discovery.ts | 84 +++++++++++++++++++----- src/plugins/manifest.ts | 2 + src/plugins/package-entry-resolution.ts | 33 ++++++++++ src/plugins/path-safety.ts | 1 + 7 files changed, 195 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb88db2482a..ba50079a58d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Google Meet: route local Chrome joins through OpenClaw browser control instead of raw default Chrome, so agents use the configured OpenClaw browser profile when opening Meet. Thanks @openclaw. +- Plugins/startup: reuse canonical realpath lookups throughout each plugin discovery pass, including package and manifest boundary checks, so Windows npm-global startups no longer repeat expensive path resolution for the same plugin roots. Fixes #65733. Thanks @welfo-beo. - Reply/link understanding: keep media and link preprocessing on stable runtime entrypoints and continue with raw message content if optional enrichment fails, so URL-bearing messages are no longer dropped after stale runtime chunk upgrades. Fixes #68466. Thanks @songshikang0111. - Discord: persist routed model-picker overrides when the hidden `/model` dispatch succeeds but the bound thread session store is still stale, including LM Studio suffixed model ids. Carries forward #61473. Thanks @Nanako0129. - Nodes/CLI: add `openclaw nodes remove --node ` and `node.pair.remove` so stale gateway-owned node pairing records can be cleaned without hand-editing state files. Thanks @openclaw. diff --git a/src/plugins/bundle-manifest.ts b/src/plugins/bundle-manifest.ts index 6cde0080eba..5e56c6dc353 100644 --- a/src/plugins/bundle-manifest.ts +++ b/src/plugins/bundle-manifest.ts @@ -92,6 +92,7 @@ function slugifyPluginId(raw: string | undefined, rootDir: string): string { function loadBundleManifestFile(params: { rootDir: string; + rootRealPath?: string; manifestRelativePath: string; rejectHardlinks: boolean; allowMissing?: boolean; @@ -100,6 +101,7 @@ function loadBundleManifestFile(params: { const opened = openBoundaryFileSync({ absolutePath: manifestPath, rootPath: params.rootDir, + ...(params.rootRealPath !== undefined ? { rootRealPath: params.rootRealPath } : {}), boundaryLabel: "plugin root", rejectHardlinks: params.rejectHardlinks, }); @@ -327,6 +329,7 @@ function buildCursorCapabilities(raw: Record, rootDir: string): export function loadBundleManifest(params: { rootDir: string; + rootRealPath?: string; bundleFormat: PluginBundleFormat; rejectHardlinks?: boolean; }): BundleManifestLoadResult { @@ -339,6 +342,7 @@ export function loadBundleManifest(params: { : CLAUDE_BUNDLE_MANIFEST_RELATIVE_PATH; const loaded = loadBundleManifestFile({ rootDir: params.rootDir, + ...(params.rootRealPath !== undefined ? { rootRealPath: params.rootRealPath } : {}), manifestRelativePath, rejectHardlinks, allowMissing: params.bundleFormat === "claude", diff --git a/src/plugins/discovery.test.ts b/src/plugins/discovery.test.ts index d5e2eacb6cc..38929dd3f99 100644 --- a/src/plugins/discovery.test.ts +++ b/src/plugins/discovery.test.ts @@ -1,4 +1,5 @@ import fs from "node:fs"; +import os from "node:os"; import path from "node:path"; import { afterEach, describe, expect, it, vi } from "vitest"; import { bundledDistPluginFile } from "../../test/helpers/bundled-plugin-paths.js"; @@ -17,6 +18,21 @@ function makeTempDir() { const mkdirSafe = mkdirSafeDir; +const canCreateDirectorySymlinks = (() => { + const probeDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-symlink-probe-")); + const targetDir = path.join(probeDir, "target"); + const linkDir = path.join(probeDir, "link"); + try { + fs.mkdirSync(targetDir); + fs.symlinkSync(targetDir, linkDir, process.platform === "win32" ? "junction" : "dir"); + return true; + } catch { + return false; + } finally { + fs.rmSync(probeDir, { recursive: true, force: true }); + } +})(); + function normalizePathForAssertion(value: string | undefined): string | undefined { if (!value) { return value; @@ -573,6 +589,75 @@ describe("discoverOpenClawPlugins", () => { expectCandidateIds(candidates, { includes: ["pack/one", "pack/two"] }); }); + it("reuses one filesystem realpath lookup per package root within a discovery run", () => { + const stateDir = makeTempDir(); + const packageDir = path.join(stateDir, "extensions", "pack"); + mkdirSafe(path.join(packageDir, "src")); + + writePluginPackageManifest({ + packageDir, + packageName: "pack", + extensions: ["./src/one.ts", "./src/two.ts"], + }); + writePluginEntry(path.join(packageDir, "src", "one.ts")); + writePluginEntry(path.join(packageDir, "src", "two.ts")); + + const realpathSync = vi.spyOn(fs, "realpathSync"); + const { candidates } = discoverOpenClawPlugins({ + env: buildDiscoveryEnv(stateDir), + cache: false, + }); + + expectCandidateIds(candidates, { includes: ["pack/one", "pack/two"] }); + expect( + realpathSync.mock.calls.filter( + ([targetPath]) => path.resolve(String(targetPath)) === path.resolve(packageDir), + ), + ).toHaveLength(1); + }); + + it.skipIf(!canCreateDirectorySymlinks)( + "reuses the canonical realpath cache entry for symlinked package roots", + () => { + const stateDir = makeTempDir(); + const realPackageDir = path.join(stateDir, "real-pack"); + mkdirSafe(path.join(realPackageDir, "src")); + + writePluginPackageManifest({ + packageDir: realPackageDir, + packageName: "pack", + extensions: ["./src/index.ts"], + }); + writePluginEntry(path.join(realPackageDir, "src", "index.ts")); + + const linkedPackageDir = path.join(stateDir, "linked-pack"); + fs.symlinkSync( + realPackageDir, + linkedPackageDir, + process.platform === "win32" ? "junction" : "dir", + ); + const canonicalPackageDir = fs.realpathSync(realPackageDir); + + const realpathSync = vi.spyOn(fs, "realpathSync"); + const { candidates } = discoverOpenClawPlugins({ + extraPaths: [linkedPackageDir, canonicalPackageDir], + env: buildDiscoveryEnv(stateDir), + cache: false, + }); + + expectCandidateIds(candidates, { includes: ["pack"] }); + expect( + realpathSync.mock.calls.filter(([targetPath]) => { + const resolved = path.resolve(String(targetPath)); + return ( + resolved === path.resolve(linkedPackageDir) || + resolved === path.resolve(canonicalPackageDir) + ); + }), + ).toHaveLength(1); + }, + ); + it("uses explicit runtime extension entries for installed package plugins", async () => { const stateDir = makeTempDir(); const pluginDir = path.join(stateDir, "extensions", "runtime-pack"); diff --git a/src/plugins/discovery.ts b/src/plugins/discovery.ts index f0c77201e5b..96fe87a44e8 100644 --- a/src/plugins/discovery.ts +++ b/src/plugins/discovery.ts @@ -158,9 +158,10 @@ type CandidateBlockIssue = { function checkSourceEscapesRoot(params: { source: string; rootDir: string; + realpathCache: Map; }): CandidateBlockIssue | null { - const sourceRealPath = safeRealpathSync(params.source); - const rootRealPath = safeRealpathSync(params.rootDir); + const sourceRealPath = safeRealpathSync(params.source, params.realpathCache); + const rootRealPath = safeRealpathSync(params.rootDir, params.realpathCache); if (!sourceRealPath || !rootRealPath) { return null; } @@ -259,10 +260,12 @@ function findCandidateBlockIssue(params: { rootDir: string; origin: PluginOrigin; ownershipUid?: number | null; + realpathCache: Map; }): CandidateBlockIssue | null { const escaped = checkSourceEscapesRoot({ source: params.source, rootDir: params.rootDir, + realpathCache: params.realpathCache, }); if (escaped) { return escaped; @@ -294,12 +297,14 @@ function isUnsafePluginCandidate(params: { origin: PluginOrigin; diagnostics: PluginDiagnostic[]; ownershipUid?: number | null; + realpathCache: Map; }): boolean { const issue = findCandidateBlockIssue({ source: params.source, rootDir: params.rootDir, origin: params.origin, ownershipUid: params.ownershipUid, + realpathCache: params.realpathCache, }); if (!issue) { return false; @@ -348,12 +353,16 @@ function shouldIgnoreScannedDirectory(dirName: string): boolean { return false; } -function resolvesToSameDirectory(left?: string, right?: string): boolean { +function resolvesToSameDirectory( + left: string | undefined, + right: string | undefined, + realpathCache: Map, +): boolean { if (!left || !right) { return false; } - const leftRealPath = safeRealpathSync(left); - const rightRealPath = safeRealpathSync(right); + const leftRealPath = safeRealpathSync(left, realpathCache); + const rightRealPath = safeRealpathSync(right, realpathCache); if (leftRealPath && rightRealPath) { return leftRealPath === rightRealPath; } @@ -403,11 +412,16 @@ function getCachedDiscoveryResult(params: { return result; } -function readPackageManifest(dir: string, rejectHardlinks = true): PackageManifest | null { +function readPackageManifest( + dir: string, + rejectHardlinks = true, + rootRealPath?: string, +): PackageManifest | null { const manifestPath = path.join(dir, "package.json"); const opened = openBoundaryFileSync({ absolutePath: manifestPath, rootPath: dir, + ...(rootRealPath !== undefined ? { rootRealPath } : {}), boundaryLabel: "plugin package directory", rejectHardlinks, }); @@ -456,8 +470,12 @@ function deriveIdHint(params: { return `${normalizedPackageId}/${base}`; } -function resolveIdHintManifestId(rootDir: string, rejectHardlinks: boolean): string | undefined { - const manifest = loadPluginManifest(rootDir, rejectHardlinks); +function resolveIdHintManifestId( + rootDir: string, + rejectHardlinks: boolean, + rootRealPath?: string, +): string | undefined { + const manifest = loadPluginManifest(rootDir, rejectHardlinks, rootRealPath); return manifest.ok ? manifest.manifest.id : undefined; } @@ -478,12 +496,14 @@ function addCandidate(params: { packageDir?: string; bundledManifest?: PluginManifest; bundledManifestPath?: string; + realpathCache: Map; }) { const resolved = path.resolve(params.source); if (params.seen.has(resolved)) { return; } - const resolvedRoot = safeRealpathSync(params.rootDir) ?? path.resolve(params.rootDir); + const resolvedRoot = + safeRealpathSync(params.rootDir, params.realpathCache) ?? path.resolve(params.rootDir); if ( isUnsafePluginCandidate({ source: resolved, @@ -491,6 +511,7 @@ function addCandidate(params: { origin: params.origin, diagnostics: params.diagnostics, ownershipUid: params.ownershipUid, + realpathCache: params.realpathCache, }) ) { return; @@ -524,13 +545,16 @@ function discoverBundleInRoot(params: { candidates: PluginCandidate[]; diagnostics: PluginDiagnostic[]; seen: Set; + realpathCache: Map; }): "added" | "invalid" | "none" { const bundleFormat = detectBundleManifestFormat(params.rootDir); if (!bundleFormat) { return "none"; } + const rootRealPath = safeRealpathSync(params.rootDir, params.realpathCache) ?? undefined; const bundleManifest = loadBundleManifest({ rootDir: params.rootDir, + ...(rootRealPath !== undefined ? { rootRealPath } : {}), bundleFormat, rejectHardlinks: params.origin !== "bundled", }); @@ -554,6 +578,7 @@ function discoverBundleInRoot(params: { bundleFormat, ownershipUid: params.ownershipUid, workspaceDir: params.workspaceDir, + realpathCache: params.realpathCache, }); return "added"; } @@ -566,6 +591,7 @@ function discoverInDirectory(params: { candidates: PluginCandidate[]; diagnostics: PluginDiagnostic[]; seen: Set; + realpathCache: Map; recurseDirectories?: boolean; skipDirectories?: Set; visitedDirectories?: Set; @@ -573,7 +599,8 @@ function discoverInDirectory(params: { if (!fs.existsSync(params.dir)) { return; } - const resolvedDir = safeRealpathSync(params.dir) ?? path.resolve(params.dir); + const resolvedDir = + safeRealpathSync(params.dir, params.realpathCache) ?? path.resolve(params.dir); if (params.recurseDirectories) { if (params.visitedDirectories?.has(resolvedDir)) { return; @@ -608,6 +635,7 @@ function discoverInDirectory(params: { origin: params.origin, ownershipUid: params.ownershipUid, workspaceDir: params.workspaceDir, + realpathCache: params.realpathCache, }); } if (!entry.isDirectory()) { @@ -621,12 +649,14 @@ function discoverInDirectory(params: { } const rejectHardlinks = params.origin !== "bundled"; - const manifest = readPackageManifest(fullPath, rejectHardlinks); + const fullPathRealPath = safeRealpathSync(fullPath, params.realpathCache) ?? undefined; + const manifest = readPackageManifest(fullPath, rejectHardlinks, fullPathRealPath); const extensionResolution = resolvePackageExtensionEntries(manifest ?? undefined); const extensions = extensionResolution.status === "ok" ? extensionResolution.entries : []; - const manifestId = resolveIdHintManifestId(fullPath, rejectHardlinks); + const manifestId = resolveIdHintManifestId(fullPath, rejectHardlinks, fullPathRealPath); const setupSource = resolvePackageSetupSource({ packageDir: fullPath, + ...(fullPathRealPath !== undefined ? { packageRootRealPath: fullPathRealPath } : {}), manifest, origin: params.origin, sourceLabel: fullPath, @@ -637,6 +667,7 @@ function discoverInDirectory(params: { if (extensions.length > 0) { const resolvedRuntimeSources = resolvePackageRuntimeExtensionSources({ packageDir: fullPath, + ...(fullPathRealPath !== undefined ? { packageRootRealPath: fullPathRealPath } : {}), manifest, extensions, origin: params.origin, @@ -663,6 +694,7 @@ function discoverInDirectory(params: { workspaceDir: params.workspaceDir, manifest, packageDir: fullPath, + realpathCache: params.realpathCache, }); } continue; @@ -676,6 +708,7 @@ function discoverInDirectory(params: { candidates: params.candidates, diagnostics: params.diagnostics, seen: params.seen, + realpathCache: params.realpathCache, }); if (bundleDiscovery === "added") { continue; @@ -698,6 +731,7 @@ function discoverInDirectory(params: { workspaceDir: params.workspaceDir, manifest, packageDir: fullPath, + realpathCache: params.realpathCache, }); continue; } @@ -720,6 +754,7 @@ function discoverFromPath(params: { candidates: PluginCandidate[]; diagnostics: PluginDiagnostic[]; seen: Set; + realpathCache: Map; }) { const resolved = resolveUserPath(params.rawPath, params.env); if (!fs.existsSync(resolved)) { @@ -751,18 +786,21 @@ function discoverFromPath(params: { origin: params.origin, ownershipUid: params.ownershipUid, workspaceDir: params.workspaceDir, + realpathCache: params.realpathCache, }); return; } if (stat.isDirectory()) { const rejectHardlinks = params.origin !== "bundled"; - const manifest = readPackageManifest(resolved, rejectHardlinks); + const resolvedRealPath = safeRealpathSync(resolved, params.realpathCache) ?? undefined; + const manifest = readPackageManifest(resolved, rejectHardlinks, resolvedRealPath); const extensionResolution = resolvePackageExtensionEntries(manifest ?? undefined); const extensions = extensionResolution.status === "ok" ? extensionResolution.entries : []; - const manifestId = resolveIdHintManifestId(resolved, rejectHardlinks); + const manifestId = resolveIdHintManifestId(resolved, rejectHardlinks, resolvedRealPath); const setupSource = resolvePackageSetupSource({ packageDir: resolved, + ...(resolvedRealPath !== undefined ? { packageRootRealPath: resolvedRealPath } : {}), manifest, origin: params.origin, sourceLabel: resolved, @@ -773,6 +811,7 @@ function discoverFromPath(params: { if (extensions.length > 0) { const resolvedRuntimeSources = resolvePackageRuntimeExtensionSources({ packageDir: resolved, + ...(resolvedRealPath !== undefined ? { packageRootRealPath: resolvedRealPath } : {}), manifest, extensions, origin: params.origin, @@ -799,6 +838,7 @@ function discoverFromPath(params: { workspaceDir: params.workspaceDir, manifest, packageDir: resolved, + realpathCache: params.realpathCache, }); } return; @@ -812,6 +852,7 @@ function discoverFromPath(params: { candidates: params.candidates, diagnostics: params.diagnostics, seen: params.seen, + realpathCache: params.realpathCache, }); if (bundleDiscovery === "added") { return; @@ -835,6 +876,7 @@ function discoverFromPath(params: { workspaceDir: params.workspaceDir, manifest, packageDir: resolved, + realpathCache: params.realpathCache, }); return; } @@ -847,6 +889,7 @@ function discoverFromPath(params: { candidates: params.candidates, diagnostics: params.diagnostics, seen: params.seen, + realpathCache: params.realpathCache, }); return; } @@ -876,6 +919,7 @@ export function discoverOpenClawPlugins(params: { load: () => { const result = createDiscoveryResult(); const seen = new Set(); + const realpathCache = new Map(); const extra = params.extraPaths ?? []; for (const extraPath of extra) { if (typeof extraPath !== "string") { @@ -906,9 +950,14 @@ export function discoverOpenClawPlugins(params: { candidates: result.candidates, diagnostics: result.diagnostics, seen, + realpathCache, }); } - const workspaceMatchesBundledRoot = resolvesToSameDirectory(workspaceRoot, roots.stock); + const workspaceMatchesBundledRoot = resolvesToSameDirectory( + workspaceRoot, + roots.stock, + realpathCache, + ); if (roots.workspace && workspaceRoot && !workspaceMatchesBundledRoot) { // Keep workspace auto-discovery constrained to the OpenClaw extensions root. // Recursively scanning the full workspace treats arbitrary project folders as @@ -921,6 +970,7 @@ export function discoverOpenClawPlugins(params: { candidates: result.candidates, diagnostics: result.diagnostics, seen, + realpathCache, }); } return result; @@ -936,6 +986,7 @@ export function discoverOpenClawPlugins(params: { load: () => { const result = createDiscoveryResult(); const seen = new Set(); + const realpathCache = new Map(); for (const sourceOverlayDir of listBundledSourceOverlayDirs({ bundledRoot: roots.stock, env, @@ -949,6 +1000,7 @@ export function discoverOpenClawPlugins(params: { candidates: result.candidates, diagnostics: result.diagnostics, seen, + realpathCache, }); result.diagnostics.push({ level: "warn", @@ -965,6 +1017,7 @@ export function discoverOpenClawPlugins(params: { candidates: result.candidates, diagnostics: result.diagnostics, seen, + realpathCache, }); } // Keep auto-discovered global extensions behind bundled plugins. @@ -976,6 +1029,7 @@ export function discoverOpenClawPlugins(params: { candidates: result.candidates, diagnostics: result.diagnostics, seen, + realpathCache, }); return result; }, diff --git a/src/plugins/manifest.ts b/src/plugins/manifest.ts index b3afdbad3c0..62c0a520400 100644 --- a/src/plugins/manifest.ts +++ b/src/plugins/manifest.ts @@ -1151,11 +1151,13 @@ function parsePluginKind(raw: unknown): PluginKind | PluginKind[] | undefined { export function loadPluginManifest( rootDir: string, rejectHardlinks = true, + rootRealPath?: string, ): PluginManifestLoadResult { const manifestPath = resolvePluginManifestPath(rootDir); const opened = openBoundaryFileSync({ absolutePath: manifestPath, rootPath: rootDir, + ...(rootRealPath !== undefined ? { rootRealPath } : {}), boundaryLabel: "plugin root", maxBytes: MAX_PLUGIN_MANIFEST_BYTES, rejectHardlinks, diff --git a/src/plugins/package-entry-resolution.ts b/src/plugins/package-entry-resolution.ts index e532dd3bf0a..c5e1d9e06d5 100644 --- a/src/plugins/package-entry-resolution.ts +++ b/src/plugins/package-entry-resolution.ts @@ -173,6 +173,7 @@ export async function validatePackageExtensionEntriesForInstall(params: { function resolvePackageEntrySource(params: { packageDir: string; + packageRootRealPath?: string; entryPath: string; sourceLabel: string; diagnostics: PluginDiagnostic[]; @@ -185,6 +186,9 @@ function resolvePackageEntrySource(params: { const opened = openBoundaryFileSync({ absolutePath, rootPath: params.packageDir, + ...(params.packageRootRealPath !== undefined + ? { rootRealPath: params.packageRootRealPath } + : {}), boundaryLabel: "plugin package directory", rejectHardlinks, }); @@ -236,6 +240,7 @@ function shouldInferBuiltRuntimeEntry(origin: PluginOrigin): boolean { function resolveSafePackageEntry(params: { packageDir: string; + packageRootRealPath?: string; entryPath: string; sourceLabel: string; diagnostics: PluginDiagnostic[]; @@ -245,6 +250,9 @@ function resolveSafePackageEntry(params: { if (fs.existsSync(absolutePath)) { const existingSource = resolvePackageEntrySource({ packageDir: params.packageDir, + ...(params.packageRootRealPath !== undefined + ? { packageRootRealPath: params.packageRootRealPath } + : {}), entryPath: params.entryPath, sourceLabel: params.sourceLabel, diagnostics: params.diagnostics, @@ -263,6 +271,9 @@ function resolveSafePackageEntry(params: { resolveBoundaryPathSync({ absolutePath, rootPath: params.packageDir, + ...(params.packageRootRealPath !== undefined + ? { rootCanonicalPath: params.packageRootRealPath } + : {}), boundaryLabel: "plugin package directory", }); } catch { @@ -278,6 +289,7 @@ function resolveSafePackageEntry(params: { function resolveExistingPackageEntrySource(params: { packageDir: string; + packageRootRealPath?: string; entryPath: string; sourceLabel: string; diagnostics: PluginDiagnostic[]; @@ -292,6 +304,7 @@ function resolveExistingPackageEntrySource(params: { function resolvePackageRuntimeEntrySource(params: { packageDir: string; + packageRootRealPath?: string; entryPath: string; runtimeEntryPath?: string; origin: PluginOrigin; @@ -301,6 +314,9 @@ function resolvePackageRuntimeEntrySource(params: { }): string | null { const safeEntry = resolveSafePackageEntry({ packageDir: params.packageDir, + ...(params.packageRootRealPath !== undefined + ? { packageRootRealPath: params.packageRootRealPath } + : {}), entryPath: params.entryPath, sourceLabel: params.sourceLabel, diagnostics: params.diagnostics, @@ -313,6 +329,9 @@ function resolvePackageRuntimeEntrySource(params: { if (params.runtimeEntryPath) { const runtimeSource = resolvePackageEntrySource({ packageDir: params.packageDir, + ...(params.packageRootRealPath !== undefined + ? { packageRootRealPath: params.packageRootRealPath } + : {}), entryPath: params.runtimeEntryPath, sourceLabel: params.sourceLabel, diagnostics: params.diagnostics, @@ -327,6 +346,9 @@ function resolvePackageRuntimeEntrySource(params: { for (const candidate of listBuiltRuntimeEntryCandidates(safeEntry.relativePath)) { const runtimeSource = resolveExistingPackageEntrySource({ packageDir: params.packageDir, + ...(params.packageRootRealPath !== undefined + ? { packageRootRealPath: params.packageRootRealPath } + : {}), entryPath: candidate, sourceLabel: params.sourceLabel, diagnostics: params.diagnostics, @@ -344,6 +366,9 @@ function resolvePackageRuntimeEntrySource(params: { return resolvePackageEntrySource({ packageDir: params.packageDir, + ...(params.packageRootRealPath !== undefined + ? { packageRootRealPath: params.packageRootRealPath } + : {}), entryPath: params.entryPath, sourceLabel: params.sourceLabel, diagnostics: params.diagnostics, @@ -353,6 +378,7 @@ function resolvePackageRuntimeEntrySource(params: { export function resolvePackageSetupSource(params: { packageDir: string; + packageRootRealPath?: string; manifest: PackageManifest | null; origin: PluginOrigin; sourceLabel: string; @@ -366,6 +392,9 @@ export function resolvePackageSetupSource(params: { } return resolvePackageRuntimeEntrySource({ packageDir: params.packageDir, + ...(params.packageRootRealPath !== undefined + ? { packageRootRealPath: params.packageRootRealPath } + : {}), entryPath: setupEntryPath, runtimeEntryPath: normalizeOptionalString(packageManifest?.runtimeSetupEntry), origin: params.origin, @@ -377,6 +406,7 @@ export function resolvePackageSetupSource(params: { export function resolvePackageRuntimeExtensionSources(params: { packageDir: string; + packageRootRealPath?: string; manifest: PackageManifest | null; extensions: readonly string[]; origin: PluginOrigin; @@ -400,6 +430,9 @@ export function resolvePackageRuntimeExtensionSources(params: { return params.extensions.flatMap((entryPath, index) => { const source = resolvePackageRuntimeEntrySource({ packageDir: params.packageDir, + ...(params.packageRootRealPath !== undefined + ? { packageRootRealPath: params.packageRootRealPath } + : {}), entryPath, runtimeEntryPath: runtimeResolution.runtimeExtensions[index], origin: params.origin, diff --git a/src/plugins/path-safety.ts b/src/plugins/path-safety.ts index 7935312cbe4..bcc637fbc61 100644 --- a/src/plugins/path-safety.ts +++ b/src/plugins/path-safety.ts @@ -13,6 +13,7 @@ export function safeRealpathSync(targetPath: string, cache?: Map try { const resolved = fs.realpathSync(targetPath); cache?.set(targetPath, resolved); + cache?.set(resolved, resolved); return resolved; } catch { return null;