From a848cd5ce4625b73fcbb26f2924a675ef26727e9 Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Sat, 11 Apr 2026 01:30:09 +0100 Subject: [PATCH 1/6] Prenormalize symlink target to normal and remove weakmap cache The WeakMap is likely more expensive and this can leave unnormalized absolute paths in the file map cache. NOTE: This bumps the `CACHE_BREAKER` value! --- .../src/crawlers/watchman/index.js | 6 ++++ packages/metro-file-map/src/index.js | 7 +++-- .../metro-file-map/src/lib/RootPathUtils.js | 24 +++++++++++++++ packages/metro-file-map/src/lib/TreeFS.js | 29 ++++--------------- 4 files changed, 40 insertions(+), 26 deletions(-) diff --git a/packages/metro-file-map/src/crawlers/watchman/index.js b/packages/metro-file-map/src/crawlers/watchman/index.js index 2e45eba592..41c7f05841 100644 --- a/packages/metro-file-map/src/crawlers/watchman/index.js +++ b/packages/metro-file-map/src/crawlers/watchman/index.js @@ -327,6 +327,12 @@ export default async function watchmanCrawl({ if (fileData.type === 'l') { symlinkInfo = fileData['symlink_target'] ?? 1; } + if (typeof symlinkInfo === 'string') { + symlinkInfo = pathUtils.resolveSymlinkToNormal( + relativeFilePath, + symlinkInfo, + ); + } const nextData: FileMetadata = [ mtime, diff --git a/packages/metro-file-map/src/index.js b/packages/metro-file-map/src/index.js index 113f85be1d..53d5febbf2 100644 --- a/packages/metro-file-map/src/index.js +++ b/packages/metro-file-map/src/index.js @@ -155,7 +155,7 @@ export type { // This should be bumped whenever a code change to `metro-file-map` itself // would cause a change to the cache data structure and/or content (for a given // filesystem state and build parameters). -const CACHE_BREAKER = '11'; +const CACHE_BREAKER = '12'; const CHANGE_INTERVAL = 30; @@ -565,7 +565,10 @@ export default class FileMap extends EventEmitter { .readlink(this.#pathUtils.normalToAbsolute(normalPath)) .then(symlinkTarget => { fileMetadata[H.VISITED] = 1; - fileMetadata[H.SYMLINK] = symlinkTarget; + fileMetadata[H.SYMLINK] = this.#pathUtils.resolveSymlinkToNormal( + normalPath, + symlinkTarget, + ); }); } return null; diff --git a/packages/metro-file-map/src/lib/RootPathUtils.js b/packages/metro-file-map/src/lib/RootPathUtils.js index 5720d60ee9..53a321111a 100644 --- a/packages/metro-file-map/src/lib/RootPathUtils.js +++ b/packages/metro-file-map/src/lib/RootPathUtils.js @@ -8,6 +8,7 @@ * @format */ +import normalizePathSeparatorsToSystem from './normalizePathSeparatorsToSystem'; import invariant from 'invariant'; import * as path from 'path'; @@ -166,6 +167,29 @@ export class RootPathUtils { ); } + resolveSymlinkToNormal( + symlinkNormalPath: string, + readlinkResult: string, + ): string { + let target = normalizePathSeparatorsToSystem(readlinkResult); + // WARN: This only applies to Windows + Node 20 case, where the value is completely + // unnormalized and a trailing slash may be returned + if (target[target.length - 1] === path.sep) { + target = target.slice(0, -1); + } + if (path.isAbsolute(target)) { + return this.absoluteToNormal(target); + } + // Resolve relative to the symlink's containing directory, expressed as + // a root-relative (possibly non-normal) path, then normalize + const sepIdx = symlinkNormalPath.lastIndexOf(path.sep); + const rootRelativeTarget = + sepIdx === -1 + ? target + : symlinkNormalPath.slice(0, sepIdx) + path.sep + target; + return this.relativeToNormal(rootRelativeTarget); + } + // If a path is a direct ancestor of the project root (or the root itself), // return a number with the degrees of separation, e.g. root=0, parent=1,.. // or null otherwise. diff --git a/packages/metro-file-map/src/lib/TreeFS.js b/packages/metro-file-map/src/lib/TreeFS.js index 7b1691ae2c..05f06f36c4 100644 --- a/packages/metro-file-map/src/lib/TreeFS.js +++ b/packages/metro-file-map/src/lib/TreeFS.js @@ -124,8 +124,6 @@ type MetadataIteratorOptions = Readonly<{ * a trailing slash */ export default class TreeFS implements MutableFileSystem { - +#cachedNormalSymlinkTargets: WeakMap = - new WeakMap(); +#pathUtils: RootPathUtils; +#processFile: ProcessFileFunction; +#rootDir: Path; @@ -1214,33 +1212,16 @@ export default class TreeFS implements MutableFileSystem { symlinkNode: FileMetadata, canonicalPathOfSymlink: Path, ): NormalizedSymlinkTarget { - const cachedResult = this.#cachedNormalSymlinkTargets.get(symlinkNode); - if (cachedResult != null) { - return cachedResult; - } - - const literalSymlinkTarget = symlinkNode[H.SYMLINK]; + const symlinkTarget = symlinkNode[H.SYMLINK]; invariant( - typeof literalSymlinkTarget === 'string', + typeof symlinkTarget === 'string', 'Expected symlink target to be populated.', ); - const absoluteSymlinkTarget = path.resolve( - this.#rootDir, - canonicalPathOfSymlink, - '..', // Symlink target is relative to its containing directory. - literalSymlinkTarget, // May be absolute, in which case the above are ignored - ); - const normalSymlinkTarget = path.relative( - this.#rootDir, - absoluteSymlinkTarget, - ); const result = { - ancestorOfRootIdx: - this.#pathUtils.getAncestorOfRootIdx(normalSymlinkTarget), - normalPath: normalSymlinkTarget, - startOfBasenameIdx: normalSymlinkTarget.lastIndexOf(path.sep) + 1, + ancestorOfRootIdx: this.#pathUtils.getAncestorOfRootIdx(symlinkTarget), + normalPath: symlinkTarget, + startOfBasenameIdx: symlinkTarget.lastIndexOf(path.sep) + 1, }; - this.#cachedNormalSymlinkTargets.set(symlinkNode, result); return result; } From d3fd493016add66694e88803b581ceba0f7a072b Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Sat, 11 Apr 2026 01:34:36 +0100 Subject: [PATCH 2/6] Normalize stored value to posix This keeps the metro-file-map value system-stable. Before they could store absolute paths, and system paths, however, this is now normalized to posix normal paths. --- packages/metro-file-map/src/crawlers/watchman/index.js | 5 ++--- packages/metro-file-map/src/index.js | 5 ++--- packages/metro-file-map/src/lib/TreeFS.js | 4 +++- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/metro-file-map/src/crawlers/watchman/index.js b/packages/metro-file-map/src/crawlers/watchman/index.js index 41c7f05841..dbfbab37ca 100644 --- a/packages/metro-file-map/src/crawlers/watchman/index.js +++ b/packages/metro-file-map/src/crawlers/watchman/index.js @@ -328,9 +328,8 @@ export default async function watchmanCrawl({ symlinkInfo = fileData['symlink_target'] ?? 1; } if (typeof symlinkInfo === 'string') { - symlinkInfo = pathUtils.resolveSymlinkToNormal( - relativeFilePath, - symlinkInfo, + symlinkInfo = normalizePathSeparatorsToPosix( + pathUtils.resolveSymlinkToNormal(relativeFilePath, symlinkInfo), ); } diff --git a/packages/metro-file-map/src/index.js b/packages/metro-file-map/src/index.js index 53d5febbf2..5eda1e0e0a 100644 --- a/packages/metro-file-map/src/index.js +++ b/packages/metro-file-map/src/index.js @@ -565,9 +565,8 @@ export default class FileMap extends EventEmitter { .readlink(this.#pathUtils.normalToAbsolute(normalPath)) .then(symlinkTarget => { fileMetadata[H.VISITED] = 1; - fileMetadata[H.SYMLINK] = this.#pathUtils.resolveSymlinkToNormal( - normalPath, - symlinkTarget, + fileMetadata[H.SYMLINK] = normalizePathSeparatorsToPosix( + this.#pathUtils.resolveSymlinkToNormal(normalPath, symlinkTarget), ); }); } diff --git a/packages/metro-file-map/src/lib/TreeFS.js b/packages/metro-file-map/src/lib/TreeFS.js index 05f06f36c4..9cabab0d41 100644 --- a/packages/metro-file-map/src/lib/TreeFS.js +++ b/packages/metro-file-map/src/lib/TreeFS.js @@ -21,6 +21,7 @@ import type { } from '../flow-types'; import H from '../constants'; +import normalizePathSeparatorsToSystem from './normalizePathSeparatorsToSystem'; import {RootPathUtils} from './RootPathUtils'; import invariant from 'invariant'; import path from 'path'; @@ -1212,11 +1213,12 @@ export default class TreeFS implements MutableFileSystem { symlinkNode: FileMetadata, canonicalPathOfSymlink: Path, ): NormalizedSymlinkTarget { - const symlinkTarget = symlinkNode[H.SYMLINK]; + let symlinkTarget = symlinkNode[H.SYMLINK]; invariant( typeof symlinkTarget === 'string', 'Expected symlink target to be populated.', ); + symlinkTarget = normalizePathSeparatorsToSystem(symlinkTarget); const result = { ancestorOfRootIdx: this.#pathUtils.getAncestorOfRootIdx(symlinkTarget), normalPath: symlinkTarget, From e8a016c1e2f94b3105215f0d28fdb500ebc74eb4 Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Sat, 11 Apr 2026 01:43:01 +0100 Subject: [PATCH 3/6] Add/update unit tests --- .../src/lib/__tests__/RootPathUtils-test.js | 41 ++++++++++++ .../src/lib/__tests__/TreeFS-test.js | 66 ++++++++----------- 2 files changed, 69 insertions(+), 38 deletions(-) diff --git a/packages/metro-file-map/src/lib/__tests__/RootPathUtils-test.js b/packages/metro-file-map/src/lib/__tests__/RootPathUtils-test.js index 8fb693c498..d71f3abd36 100644 --- a/packages/metro-file-map/src/lib/__tests__/RootPathUtils-test.js +++ b/packages/metro-file-map/src/lib/__tests__/RootPathUtils-test.js @@ -153,4 +153,45 @@ describe.each([['win32'], ['posix']])('RootPathUtils on %s', platform => { ])('getAncestorOfRootIdx (%s => %s)', (input, expected) => { expect(pathUtils.getAncestorOfRootIdx(input)).toEqual(expected); }); + + describe('resolveSymlinkToNormal', () => { + beforeEach(() => { + pathUtils = new RootPathUtils(p('/project/root')); + }); + + test.each([ + ['foo/link', './target.js', p('foo/target.js')], + ['foo/link', '../bar.js', 'bar.js'], + ['link', 'target.js', 'target.js'], + [p('a/b/link'), p('../../c.js'), 'c.js'], + [p('a/b/link'), p('../../../outside/f.js'), p('../outside/f.js')], + ])( + 'resolves relative target (%s -> %s) to %s', + (symlinkPath, readlinkResult, expected) => { + expect( + pathUtils.resolveSymlinkToNormal(p(symlinkPath), readlinkResult), + ).toEqual(expected); + }, + ); + + test.each([ + ['link', p('/project/root/target.js'), 'target.js'], + ['link', p('/project/root/a/b.js'), p('a/b.js')], + ['link', p('/outside/foo.js'), p('../../outside/foo.js')], + [p('a/link'), p('/project/root'), ''], + ])( + 'resolves absolute target (%s -> %s) to %s', + (symlinkPath, readlinkResult, expected) => { + expect( + pathUtils.resolveSymlinkToNormal(p(symlinkPath), readlinkResult), + ).toEqual(expected); + }, + ); + + test('strips trailing separator from target', () => { + expect( + pathUtils.resolveSymlinkToNormal('link', p('/project/root/dir/')), + ).toEqual('dir'); + }); + }); }); diff --git a/packages/metro-file-map/src/lib/__tests__/TreeFS-test.js b/packages/metro-file-map/src/lib/__tests__/TreeFS-test.js index a65eab4293..5bdc3a0869 100644 --- a/packages/metro-file-map/src/lib/__tests__/TreeFS-test.js +++ b/packages/metro-file-map/src/lib/__tests__/TreeFS-test.js @@ -41,18 +41,18 @@ describe.each([['win32'], ['posix']])('TreeFS on %s', platform => { rootDir: p('/project'), files: new Map([ [p('foo/another.js'), [123, 2, 0, null, 0, 'another']], - [p('foo/owndir'), [0, 0, 0, null, '.', null]], - [p('foo/link-to-bar.js'), [0, 0, 0, null, p('../bar.js'), null]], - [p('foo/link-to-another.js'), [0, 0, 0, null, p('another.js'), null]], + [p('foo/owndir'), [0, 0, 0, null, 'foo', null]], + [p('foo/link-to-bar.js'), [0, 0, 0, null, 'bar.js', null]], + [p('foo/link-to-another.js'), [0, 0, 0, null, 'foo/another.js', null]], [p('../outside/external.js'), [0, 0, 0, null, 0, null]], [p('bar.js'), [234, 3, 0, null, 0, 'bar']], - [p('link-to-foo'), [456, 0, 0, null, p('./../project/foo'), null]], - [p('abs-link-out'), [456, 0, 0, null, p('/outside/./baz/..'), null]], + [p('link-to-foo'), [456, 0, 0, null, 'foo', null]], + [p('abs-link-out'), [456, 0, 0, null, '../outside', null]], [p('root'), [0, 0, 0, null, '..', null]], - [p('link-to-nowhere'), [123, 0, 0, null, p('./nowhere'), null]], - [p('link-to-self'), [123, 0, 0, null, p('./link-to-self'), null]], - [p('link-cycle-1'), [123, 0, 0, null, p('./link-cycle-2'), null]], - [p('link-cycle-2'), [123, 0, 0, null, p('./link-cycle-1'), null]], + [p('link-to-nowhere'), [123, 0, 0, null, 'nowhere', null]], + [p('link-to-self'), [123, 0, 0, null, 'link-to-self', null]], + [p('link-cycle-1'), [123, 0, 0, null, 'link-cycle-2', null]], + [p('link-cycle-2'), [123, 0, 0, null, 'link-cycle-1', null]], [p('node_modules/pkg/a.js'), [123, 0, 0, null, 0, 'a']], [p('node_modules/pkg/package.json'), [123, 0, 0, null, 0, 'pkg']], ]), @@ -194,7 +194,7 @@ describe.each([['win32'], ['posix']])('TreeFS on %s', platform => { rootDir: p('/deep/project/root'), files: new Map([ [p('foo/index.js'), [123, 0, 0, null, 0, null]], - [p('link-up'), [123, 0, 0, null, p('..'), null]], + [p('link-up'), [123, 0, 0, null, '..', null]], ]), processFile: () => { throw new Error('Not implemented'); @@ -221,7 +221,7 @@ describe.each([['win32'], ['posix']])('TreeFS on %s', platform => { describe('symlinks to an ancestor of the project root', () => { beforeEach(() => { - tfs.addOrModify(p('foo/link-up-2'), [0, 0, 0, null, p('../..'), null]); + tfs.addOrModify(p('foo/link-up-2'), [0, 0, 0, null, '..', null]); }); test.each([ @@ -277,14 +277,14 @@ describe.each([['win32'], ['posix']])('TreeFS on %s', platform => { test('returns changed (inc. new) and removed files in given FileData', () => { const newFiles: FileData = new Map([ [p('new-file'), [789, 0, 0, null, 0, null]], - [p('link-to-foo'), [456, 0, 0, null, p('./foo'), null]], + [p('link-to-foo'), [456, 0, 0, null, 'foo', null]], // Different modified time, expect new mtime in changedFiles [p('foo/another.js'), [124, 0, 0, null, 0, null]], - [p('link-cycle-1'), [123, 0, 0, null, p('./link-cycle-2'), null]], - [p('link-cycle-2'), [123, 0, 0, null, p('./link-cycle-1'), null]], + [p('link-cycle-1'), [123, 0, 0, null, 'link-cycle-2', null]], + [p('link-cycle-2'), [123, 0, 0, null, 'link-cycle-1', null]], // Was a symlink, now a regular file [p('link-to-self'), [123, 0, 0, null, 0, null]], - [p('link-to-nowhere'), [123, 0, 0, null, p('./nowhere'), null]], + [p('link-to-nowhere'), [123, 0, 0, null, 'nowhere', null]], [p('node_modules/pkg/a.js'), [123, 0, 0, null, 0, 'a']], [p('node_modules/pkg/package.json'), [123, 0, 0, null, 0, 'pkg']], ]); @@ -393,24 +393,18 @@ describe.each([['win32'], ['posix']])('TreeFS on %s', platform => { [ [ p('a/1/package.json'), - [0, 0, 0, null, './real-package.json', null], + [0, 0, 0, null, 'a/1/real-package.json', null], ], [ p('a/2/package.json'), - [0, 0, 0, null, './notexist-package.json', null], - ], - [p('a/b/c/d/link-to-C'), [0, 0, 0, null, p('../../../..'), null]], - [ - p('a/b/c/d/link-to-B'), - [0, 0, 0, null, p('../../../../..'), null], - ], - [ - p('a/b/c/d/link-to-A'), - [0, 0, 0, null, p('../../../../../..'), null], + [0, 0, 0, null, 'a/2/notexist-package.json', null], ], + [p('a/b/c/d/link-to-C'), [0, 0, 0, null, '', null]], + [p('a/b/c/d/link-to-B'), [0, 0, 0, null, '..', null]], + [p('a/b/c/d/link-to-A'), [0, 0, 0, null, '../..', null]], [ p('n_m/workspace/link-to-pkg'), - [0, 0, 0, null, p('../../../workspace-pkg'), null], + [0, 0, 0, null, '../workspace-pkg', null], ], ] as Array<[CanonicalPath, FileMetadata]> ).concat( @@ -817,7 +811,7 @@ describe.each([['win32'], ['posix']])('TreeFS on %s', platform => { new Map([ [ p('newdir/link-to-link-to-bar.js'), - [0, 0, 0, null, p('../foo/link-to-bar.js'), null], + [0, 0, 0, null, 'foo/link-to-bar.js', null], ], [p('foo/baz.js'), [0, 0, 0, null, 0, null]], [p('bar.js'), [999, 1, 0, null, 0, null]], @@ -967,7 +961,7 @@ describe.each([['win32'], ['posix']])('TreeFS on %s', platform => { { baseName: 'link-to-bar.js', canonicalPath: p('foo/link-to-bar.js'), - metadata: [0, 0, 0, null, p('../bar.js'), null], + metadata: [0, 0, 0, null, 'bar.js', null], }, ]), ); @@ -983,7 +977,7 @@ describe.each([['win32'], ['posix']])('TreeFS on %s', platform => { files: new Map([ [p('foo.js'), [123, 0, 0, 'def456', 0, null]], [p('bar.js'), [123, 0, 0, null, 0, null]], - [p('link-to-bar'), [456, 0, 0, null, p('./bar.js'), null]], + [p('link-to-bar'), [456, 0, 0, null, 'bar.js', null]], ]), processFile: mockProcessFile, }); @@ -1084,7 +1078,7 @@ describe.each([['win32'], ['posix']])('TreeFS on %s', platform => { files: new Map([ [p('existing.js'), [123, 0, 0, '', 0]], [p('dir/nested.js'), [456, 0, 0, '', 0]], - [p('mylink'), [0, 0, 0, '', p('./dir')]], + [p('mylink'), [0, 0, 0, '', 'dir']], ]), processFile: () => { throw new Error('Not implemented'); @@ -1214,23 +1208,19 @@ describe.each([['win32'], ['posix']])('TreeFS on %s', platform => { test('tracks added files when adding a symlink', () => { simpleTfs.addOrModify( p('link-to-existing'), - [0, 0, 0, '', p('./existing.js')], + [0, 0, 0, '', 'existing.js'], listener, ); expect(logChange.mock.calls).toEqual([ - [ - 'fileAdded', - p('link-to-existing'), - [0, 0, 0, '', p('./existing.js')], - ], + ['fileAdded', p('link-to-existing'), [0, 0, 0, '', 'existing.js']], ]); }); test('tracks removed symlinks with their metadata', () => { simpleTfs.remove(p('mylink'), listener); expect(logChange.mock.calls).toEqual([ - ['fileRemoved', p('mylink'), [0, 0, 0, '', p('./dir')]], + ['fileRemoved', p('mylink'), [0, 0, 0, '', 'dir']], ]); }); }); From e14f40a7ef8e197aa366ce7185bde078086576a7 Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Sat, 11 Apr 2026 01:44:24 +0100 Subject: [PATCH 4/6] Rebuild ts-defs --- packages/metro-file-map/types/lib/RootPathUtils.d.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/metro-file-map/types/lib/RootPathUtils.d.ts b/packages/metro-file-map/types/lib/RootPathUtils.d.ts index 79d65e0262..ff91c9b41d 100644 --- a/packages/metro-file-map/types/lib/RootPathUtils.d.ts +++ b/packages/metro-file-map/types/lib/RootPathUtils.d.ts @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. * * @noformat - * @generated SignedSource<<5ecdb559fce5f5c6ed50df6e4eaebf20>> + * @generated SignedSource<> * * This file was translated from Flow by scripts/generateTypeScriptDefinitions.js * Original file: packages/metro-file-map/src/lib/RootPathUtils.js @@ -21,6 +21,10 @@ export declare class RootPathUtils { absoluteToNormal(absolutePath: string): string; normalToAbsolute(normalPath: string): string; relativeToNormal(relativePath: string): string; + resolveSymlinkToNormal( + symlinkNormalPath: string, + readlinkResult: string, + ): string; getAncestorOfRootIdx(normalPath: string): null | undefined | number; joinNormalToRelative( normalPath: string, From 0a6435120887217ecc560ff35f7a5272cee6a9b8 Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Wed, 22 Apr 2026 09:01:54 +0100 Subject: [PATCH 5/6] Drop intermediate data structure in resolveSymlinkTargetToNormalPath --- packages/metro-file-map/src/lib/TreeFS.js | 29 +++++++---------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/packages/metro-file-map/src/lib/TreeFS.js b/packages/metro-file-map/src/lib/TreeFS.js index 9cabab0d41..d8bd57f8ca 100644 --- a/packages/metro-file-map/src/lib/TreeFS.js +++ b/packages/metro-file-map/src/lib/TreeFS.js @@ -38,12 +38,6 @@ function isRegularFile(node: FileNode): boolean { return node[H.SYMLINK] === 0; } -type NormalizedSymlinkTarget = { - ancestorOfRootIdx: ?number, - normalPath: string, - startOfBasenameIdx: number, -}; - type DeserializedSnapshotInput = { rootDir: string, fileSystemData: DirectoryNode, @@ -728,7 +722,7 @@ export default class TreeFS implements MutableFileSystem { // Append any subsequent path segments to the symlink target, and reset // with our new target. const joinedResult = this.#pathUtils.joinNormalToRelative( - normalSymlinkTarget.normalPath, + normalSymlinkTarget, remainingTargetPath, ); @@ -744,12 +738,13 @@ export default class TreeFS implements MutableFileSystem { // with the remaining path results in collapsing segments, e.g: // '../..' + 'parentofroot/root/foo.js' = 'foo.js', then we must add // parentofroot and root as ancestors. + ancestorOfRootIdx = + this.#pathUtils.getAncestorOfRootIdx(normalSymlinkTarget); if ( collectAncestors && !isLastSegment && // No-op optimisation to bail out the common case of nothing to do. - (normalSymlinkTarget.ancestorOfRootIdx === 0 || - joinedResult.collapsedSegments > 0) + (ancestorOfRootIdx === 0 || joinedResult.collapsedSegments > 0) ) { let node: MixedNode = this.#rootNode; let collapsedPath = ''; @@ -764,7 +759,7 @@ export default class TreeFS implements MutableFileSystem { // Add the root only if the target is the root or we have // collapsed segments. i > 0 || - normalSymlinkTarget.ancestorOfRootIdx === 0 || + ancestorOfRootIdx === 0 || joinedResult.collapsedSegments > 0 ) { reverseAncestors.push({ @@ -787,7 +782,7 @@ export default class TreeFS implements MutableFileSystem { // the symlink target, and start collecting ancestors only // from the target itself (ie, the basename of the normal target path) // onwards. - unseenPathFromIdx = normalSymlinkTarget.startOfBasenameIdx; + unseenPathFromIdx = normalSymlinkTarget.lastIndexOf(path.sep) + 1; if (seen == null) { // Optimisation: set this lazily only when we've encountered a symlink @@ -1212,19 +1207,13 @@ export default class TreeFS implements MutableFileSystem { #resolveSymlinkTargetToNormalPath( symlinkNode: FileMetadata, canonicalPathOfSymlink: Path, - ): NormalizedSymlinkTarget { - let symlinkTarget = symlinkNode[H.SYMLINK]; + ): Path { + const symlinkTarget = symlinkNode[H.SYMLINK]; invariant( typeof symlinkTarget === 'string', 'Expected symlink target to be populated.', ); - symlinkTarget = normalizePathSeparatorsToSystem(symlinkTarget); - const result = { - ancestorOfRootIdx: this.#pathUtils.getAncestorOfRootIdx(symlinkTarget), - normalPath: symlinkTarget, - startOfBasenameIdx: symlinkTarget.lastIndexOf(path.sep) + 1, - }; - return result; + return normalizePathSeparatorsToSystem(symlinkTarget); } #getFileData( From 32f7bd45da59535cfdf70c653654ecfad25d9882 Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Wed, 22 Apr 2026 09:07:11 +0100 Subject: [PATCH 6/6] Lazily compute ancestor of root idx --- packages/metro-file-map/src/lib/TreeFS.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/metro-file-map/src/lib/TreeFS.js b/packages/metro-file-map/src/lib/TreeFS.js index d8bd57f8ca..84523295ab 100644 --- a/packages/metro-file-map/src/lib/TreeFS.js +++ b/packages/metro-file-map/src/lib/TreeFS.js @@ -738,13 +738,13 @@ export default class TreeFS implements MutableFileSystem { // with the remaining path results in collapsing segments, e.g: // '../..' + 'parentofroot/root/foo.js' = 'foo.js', then we must add // parentofroot and root as ancestors. - ancestorOfRootIdx = - this.#pathUtils.getAncestorOfRootIdx(normalSymlinkTarget); if ( collectAncestors && !isLastSegment && // No-op optimisation to bail out the common case of nothing to do. - (ancestorOfRootIdx === 0 || joinedResult.collapsedSegments > 0) + ((ancestorOfRootIdx = + this.#pathUtils.getAncestorOfRootIdx(normalSymlinkTarget)) === 0 || + joinedResult.collapsedSegments > 0) ) { let node: MixedNode = this.#rootNode; let collapsedPath = '';