From 52c0e0724da9627e8ea98bfcc376744be783d475 Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Mon, 11 May 2026 17:38:55 +0100 Subject: [PATCH 01/10] Add failing tests --- .../src/lib/__tests__/RootPathUtils-test.js | 137 ++++++++++++++---- .../src/lib/__tests__/TreeFS-test.js | 63 ++++++++ 2 files changed, 170 insertions(+), 30 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..3a91a85fec 100644 --- a/packages/metro-file-map/src/lib/__tests__/RootPathUtils-test.js +++ b/packages/metro-file-map/src/lib/__tests__/RootPathUtils-test.js @@ -91,37 +91,47 @@ describe.each([['win32'], ['posix']])('RootPathUtils on %s', platform => { expect(pathRelative).toHaveBeenCalled(); }); - test.each([ - p('..'), - p('../..'), - p('../../'), - p('normal/path'), - p('normal/path/'), - p('../normal/path'), - p('../normal/path/'), - p('../../normal/path'), - p('../../../normal/path'), - ])(`normalToAbsolute('%s') matches path.resolve`, normalPath => { - let expected = mockPathModule.resolve(rootDir, normalPath); - // Unlike path.resolve, we expect to preserve trailing separators. - if (normalPath.endsWith(sep) && !expected.endsWith(sep)) { - expected += sep; - } - expect(pathUtils.normalToAbsolute(normalPath)).toEqual(expected); - }); + const normalToAbsoluteInputs = + rootDir === p('/project/root') + ? [ + p('..'), + p('../..'), + p('../../'), + p('normal/path'), + p('normal/path/'), + p('../normal/path'), + p('../normal/path/'), + p('../../normal/path'), + ] + : [p('..'), p('../..'), p('../../'), p('normal/path'), p('normal/path/')]; - test.each([ - p('..'), - p('../root'), - p('../root/path'), - p('../project'), - p('../project/'), - p('../../project/root'), - p('../../project/root/'), - p('../../../normal/path'), - p('../../../normal/path/'), - p('../../..'), - ])( + test.each(normalToAbsoluteInputs)( + `normalToAbsolute('%s') matches path.resolve`, + normalPath => { + let expected = mockPathModule.resolve(rootDir, normalPath); + // Unlike path.resolve, we expect to preserve trailing separators. + if (normalPath.endsWith(sep) && !expected.endsWith(sep)) { + expected += sep; + } + expect(pathUtils.normalToAbsolute(normalPath)).toEqual(expected); + }, + ); + + const relativeToNormalInputs = + rootDir === p('/project/root') + ? [ + p('..'), + p('../root'), + p('../root/path'), + p('../project'), + p('../project/'), + p('../../project/root'), + p('../../project/root/'), + p('../../..'), + ] + : [p('..')]; + + test.each(relativeToNormalInputs)( `relativeToNormal('%s') matches path.resolve + path.relative`, relativePath => { let expected = mockPathModule.relative( @@ -153,4 +163,71 @@ describe.each([['win32'], ['posix']])('RootPathUtils on %s', platform => { ])('getAncestorOfRootIdx (%s => %s)', (input, expected) => { expect(pathUtils.getAncestorOfRootIdx(input)).toEqual(expected); }); + + if (platform === 'win32') { + describe('cross-drive absolute paths (Windows)', () => { + test.each([['C:\\project\\root'], ['C:\\']])( + 'path.relative returns cross-drive target as-is from rootDir=%s', + rootDir => { + expect(mockPathModule.relative(rootDir, 'D:\\some\\file.js')).toEqual( + 'D:\\some\\file.js', + ); + }, + ); + + test.each([ + ['C:\\project\\root', 'D:\\some\\file.js', '..\\..\\..\\D:\\some\\file.js'], + ['C:\\project\\root', 'D:\\some\\', '..\\..\\..\\D:\\some\\'], + ['C:\\project\\root', 'D:\\', '..\\..\\..\\D:\\'], + ['C:\\', 'D:\\some\\file.js', '..\\D:\\some\\file.js'], + ['C:\\', 'D:\\', '..\\D:\\'], + ['D:\\project\\root', 'C:\\file.js', '..\\..\\..\\C:\\file.js'], + ])( + 'absoluteToNormal emits a ..-chain (rootDir=%s, X=%s -> %s)', + (rootDir, absolutePath, expectedNormal) => { + pathUtils = new RootPathUtils(rootDir); + expect(pathUtils.absoluteToNormal(absolutePath)).toEqual( + expectedNormal, + ); + }, + ); + + test.each([ + ['C:\\project\\root', 'D:\\some\\file.js'], + ['C:\\project\\root', 'D:\\some\\'], + ['C:\\project\\root', 'D:\\'], + ['C:\\', 'D:\\some\\file.js'], + ['C:\\', 'D:\\some\\'], + ['C:\\', 'D:\\'], + ['D:\\project\\root', 'C:\\file.js'], + ['D:\\project\\root', 'C:\\'], + ])( + 'normalToAbsolute(absoluteToNormal(X)) === X for rootDir=%s, X=%s', + (rootDir, absolutePath) => { + pathUtils = new RootPathUtils(rootDir); + const normal = pathUtils.absoluteToNormal(absolutePath); + expect(pathUtils.normalToAbsolute(normal)).toEqual(absolutePath); + }, + ); + + test.each([ + ['C:\\project\\root', 'D:\\dir\\sub', 'extra\\file.js'], + ['C:\\project\\root', 'D:\\', 'foo.js'], + ['C:\\', 'D:\\dir', 'sub\\file.js'], + ])( + 'joinNormalToRelative round-trips cross-drive (rootDir=%s, base=%s, rel=%s)', + (rootDir, baseAbsolute, relativePath) => { + pathUtils = new RootPathUtils(rootDir); + const baseNormal = pathUtils.absoluteToNormal(baseAbsolute); + const {normalPath} = pathUtils.joinNormalToRelative( + baseNormal, + relativePath, + ); + expect(pathUtils.normalToAbsolute(normalPath)).toEqual( + mockPathModule.join(baseAbsolute, relativePath), + ); + }, + ); + }); + } }); 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..41c4493172 100644 --- a/packages/metro-file-map/src/lib/__tests__/TreeFS-test.js +++ b/packages/metro-file-map/src/lib/__tests__/TreeFS-test.js @@ -1235,4 +1235,67 @@ describe.each([['win32'], ['posix']])('TreeFS on %s', platform => { }); }); }); + + if (platform === 'win32') { + describe('cross-drive paths (Windows)', () => { + let tfsCD: TreeFSType; + const externalMeta: FileMetadata = [123, 4, 0, null, 0, 'external']; + + beforeEach(() => { + tfsCD = new TreeFS({ + rootDir: 'C:\\project', + files: new Map([ + ['bar.js', [234, 3, 0, null, 0, 'bar']], + ['..\\..\\D:\\external\\file.js', externalMeta], + ]), + processFile: () => { + throw new Error('Not implemented'); + }, + }); + }); + + test('exists() finds a seeded cross-drive file', () => { + expect(tfsCD.exists('D:\\external\\file.js')).toBe(true); + }); + + test('lookup() returns the absolute drive-prefixed path as realPath', () => { + expect(tfsCD.lookup('D:\\external\\file.js')).toMatchObject({ + exists: true, + type: 'f', + realPath: 'D:\\external\\file.js', + }); + }); + + test('getAllFiles() enumerates cross-drive and in-tree files side by side', () => { + expect(tfsCD.getAllFiles().sort()).toEqual([ + 'C:\\project\\bar.js', + 'D:\\external\\file.js', + ]); + }); + + test('addOrModify() accepts a new cross-drive absolute path', () => { + tfsCD.addOrModify('D:\\added\\later.js', [1, 1, 0, null, 0, 'later']); + expect(tfsCD.exists('D:\\added\\later.js')).toBe(true); + expect(tfsCD.lookup('D:\\added\\later.js')).toMatchObject({ + exists: true, + type: 'f', + realPath: 'D:\\added\\later.js', + }); + }); + + test('remove() deletes a cross-drive entry and prunes empty ancestor dirs', () => { + tfsCD.remove('D:\\external\\file.js'); + expect(tfsCD.exists('D:\\external\\file.js')).toBe(false); + expect(tfsCD.lookup('D:\\external').exists).toBe(false); + expect(tfsCD.exists('C:\\project\\bar.js')).toBe(true); + }); + + test('lookup() reports missing for non-existent cross-drive path', () => { + expect(tfsCD.lookup('D:\\external\\missing.js')).toMatchObject({ + exists: false, + }); + expect(tfsCD.exists('E:\\anywhere.js')).toBe(false); + }); + }); + } }); From 0ba41e3f1724603998d46c392fb7db1369a5474c Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Mon, 11 May 2026 17:52:07 +0100 Subject: [PATCH 02/10] Fix RootPathUtils.normalToAbsolute not cutting off cross-device absolute paths --- packages/metro-file-map/src/lib/RootPathUtils.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/metro-file-map/src/lib/RootPathUtils.js b/packages/metro-file-map/src/lib/RootPathUtils.js index 5720d60ee9..7efcb07e8d 100644 --- a/packages/metro-file-map/src/lib/RootPathUtils.js +++ b/packages/metro-file-map/src/lib/RootPathUtils.js @@ -149,6 +149,11 @@ export class RootPathUtils { const right = pos === 0 ? normalPath : normalPath.slice(pos); if (right.length === 0) { return left; + } else if (pos > this.#rootDepth * UP_FRAGMENT_SEP_LENGTH) { + // When we walk above the filesystem root, emit the remaining path as + // is. This is important on Windows, since we canonicalize cross-device + // paths as relative paths from rootDir. + return right; } // left may already end in a path separator only if it is a filesystem root, // '/' or 'X:\'. From 315f832e4538cf595037d9112b7dc4bbc9b46636 Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Mon, 11 May 2026 17:52:50 +0100 Subject: [PATCH 03/10] Prevent double-slash on Windows 'C:\' root path being appended with relative --- packages/metro-file-map/src/lib/RootPathUtils.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/metro-file-map/src/lib/RootPathUtils.js b/packages/metro-file-map/src/lib/RootPathUtils.js index 7efcb07e8d..5ab70a6415 100644 --- a/packages/metro-file-map/src/lib/RootPathUtils.js +++ b/packages/metro-file-map/src/lib/RootPathUtils.js @@ -203,7 +203,9 @@ export class RootPathUtils { if (relativePath === '') { return {collapsedSegments: 0, normalPath}; } - const left = normalPath + path.sep; + const left = normalPath.endsWith(path.sep) + ? normalPath + : normalPath + path.sep; const rawPath = left + relativePath; if (normalPath === '..' || normalPath.endsWith(SEP_UP_FRAGMENT)) { const collapsed = this.#tryCollapseIndirectionsInSuffix(rawPath, 0, 0); From 1ed61d1052f96c89db2ac467321ebe319fde774a Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Mon, 11 May 2026 17:53:13 +0100 Subject: [PATCH 04/10] Remove re-normalization on recursive remove to stop relative caps --- packages/metro-file-map/src/lib/TreeFS.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/metro-file-map/src/lib/TreeFS.js b/packages/metro-file-map/src/lib/TreeFS.js index 7b1691ae2c..38b0b0a502 100644 --- a/packages/metro-file-map/src/lib/TreeFS.js +++ b/packages/metro-file-map/src/lib/TreeFS.js @@ -493,6 +493,13 @@ export default class TreeFS implements MutableFileSystem { remove(mixedPath: Path, changeListener?: FileSystemListener): void { const normalPath = this.#normalizePath(mixedPath); + this.#removeNormalPath(normalPath, changeListener); + } + + #removeNormalPath( + normalPath: string, + changeListener?: FileSystemListener, + ): void { const result = this.#lookupByNormalPath(normalPath, {followLeaf: false}); if (!result.exists) { return; @@ -501,7 +508,10 @@ export default class TreeFS implements MutableFileSystem { if (isDirectory(node) && node.size > 0) { for (const basename of node.keys()) { - this.remove(canonicalPath + path.sep + basename, changeListener); + this.#removeNormalPath( + canonicalPath + path.sep + basename, + changeListener, + ); } // Removing the last file will delete this directory return; @@ -521,7 +531,7 @@ export default class TreeFS implements MutableFileSystem { // that's not expected to be a case common enough to justify // implementation complexity, or slowing down more common uses of // _lookupByNormalPath. - this.remove(path.dirname(canonicalPath), changeListener); + this.#removeNormalPath(path.dirname(canonicalPath), changeListener); } } } From 7e5aee3ff663dfdbb609d83be050c1e0039e61fd Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Mon, 11 May 2026 17:53:40 +0100 Subject: [PATCH 05/10] Adjust relative root maximum for Windows --- packages/metro-file-map/src/lib/RootPathUtils.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/metro-file-map/src/lib/RootPathUtils.js b/packages/metro-file-map/src/lib/RootPathUtils.js index 5ab70a6415..6ce7ec581e 100644 --- a/packages/metro-file-map/src/lib/RootPathUtils.js +++ b/packages/metro-file-map/src/lib/RootPathUtils.js @@ -46,6 +46,9 @@ const SEP_UP_FRAGMENT = path.sep + '..'; const UP_FRAGMENT_SEP_LENGTH = UP_FRAGMENT_SEP.length; const CURRENT_FRAGMENT = '.' + path.sep; +const IS_WIN32 = path.sep === '\\'; +const ROOT_BASE_IDX = IS_WIN32 ? 0 : 1; + export class RootPathUtils { #rootDir: string; #rootDirnames: ReadonlyArray; @@ -306,9 +309,10 @@ export class RootPathUtils { }; } - // Cap the number of indirections at the total number of root segments. - // File systems treat '..' at the root as '.'. - if (totalUpIndirections < this.#rootParts.length - 1) { + // Cap the number of indirections at the total number of root parts. + // File systems treat '..' at the root as '.'. For Windows, cross-device + // paths need to survive this. + if (totalUpIndirections < this.#rootParts.length - ROOT_BASE_IDX) { totalUpIndirections++; } From 274c1af41ff4bc1219f526d677916fc1c3b26774 Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Mon, 11 May 2026 17:54:07 +0100 Subject: [PATCH 06/10] Normalize absolute symlink targets to normals --- packages/metro-file-map/src/lib/TreeFS.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/metro-file-map/src/lib/TreeFS.js b/packages/metro-file-map/src/lib/TreeFS.js index 38b0b0a502..da85967217 100644 --- a/packages/metro-file-map/src/lib/TreeFS.js +++ b/packages/metro-file-map/src/lib/TreeFS.js @@ -1240,10 +1240,18 @@ export default class TreeFS implements MutableFileSystem { '..', // Symlink target is relative to its containing directory. literalSymlinkTarget, // May be absolute, in which case the above are ignored ); - const normalSymlinkTarget = path.relative( + let normalSymlinkTarget = path.relative( this.#rootDir, absoluteSymlinkTarget, ); + // path.relative leaves cross-device targets absolute on Windows (e.g. + // 'D:\\file' from a 'C:\\' root). Canonicalise those via RootPathUtils so + // they round-trip as a '..'-chain from rootDir. + if (path.isAbsolute(normalSymlinkTarget)) { + normalSymlinkTarget = this.#pathUtils.absoluteToNormal( + absoluteSymlinkTarget, + ); + } const result = { ancestorOfRootIdx: this.#pathUtils.getAncestorOfRootIdx(normalSymlinkTarget), From 27e7cb4297eda4a87601b5eede0a79a0f8384efd Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Mon, 11 May 2026 17:54:26 +0100 Subject: [PATCH 07/10] Add tests for cross-device symlink targets --- .../src/lib/__tests__/RootPathUtils-test.js | 14 ++++++++++++-- .../src/lib/__tests__/TreeFS-test.js | 18 ++++++++++++++++++ 2 files changed, 30 insertions(+), 2 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 3a91a85fec..b71c002eb4 100644 --- a/packages/metro-file-map/src/lib/__tests__/RootPathUtils-test.js +++ b/packages/metro-file-map/src/lib/__tests__/RootPathUtils-test.js @@ -103,7 +103,13 @@ describe.each([['win32'], ['posix']])('RootPathUtils on %s', platform => { p('../normal/path/'), p('../../normal/path'), ] - : [p('..'), p('../..'), p('../../'), p('normal/path'), p('normal/path/')]; + : [ + p('..'), + p('../..'), + p('../../'), + p('normal/path'), + p('normal/path/'), + ]; test.each(normalToAbsoluteInputs)( `normalToAbsolute('%s') matches path.resolve`, @@ -176,7 +182,11 @@ describe.each([['win32'], ['posix']])('RootPathUtils on %s', platform => { ); test.each([ - ['C:\\project\\root', 'D:\\some\\file.js', '..\\..\\..\\D:\\some\\file.js'], + [ + 'C:\\project\\root', + 'D:\\some\\file.js', + '..\\..\\..\\D:\\some\\file.js', + ], ['C:\\project\\root', 'D:\\some\\', '..\\..\\..\\D:\\some\\'], ['C:\\project\\root', 'D:\\', '..\\..\\..\\D:\\'], ['C:\\', 'D:\\some\\file.js', '..\\D:\\some\\file.js'], 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 41c4493172..bc5923cece 100644 --- a/packages/metro-file-map/src/lib/__tests__/TreeFS-test.js +++ b/packages/metro-file-map/src/lib/__tests__/TreeFS-test.js @@ -1296,6 +1296,24 @@ describe.each([['win32'], ['posix']])('TreeFS on %s', platform => { }); expect(tfsCD.exists('E:\\anywhere.js')).toBe(false); }); + + test('lookup() follows a symlink whose target is a cross-drive path', () => { + const tfsLink = new TreeFS({ + rootDir: 'C:\\project', + files: new Map([ + ['..\\..\\D:\\external\\file.js', externalMeta], + ['link', [0, 0, 0, null, 'D:\\external\\file.js', null]], + ]), + processFile: () => { + throw new Error('Not implemented'); + }, + }); + expect(tfsLink.lookup('C:\\project\\link')).toMatchObject({ + exists: true, + type: 'f', + realPath: 'D:\\external\\file.js', + }); + }); }); } }); From d95b7a676fb1c1dfa4d107f938daee1eecd6c622 Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Tue, 12 May 2026 15:08:00 +0100 Subject: [PATCH 08/10] Re-add relative at root test and guard cross-drive escaping to Windows --- packages/metro-file-map/src/lib/RootPathUtils.js | 7 +++---- .../metro-file-map/src/lib/__tests__/RootPathUtils-test.js | 6 ++++++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/metro-file-map/src/lib/RootPathUtils.js b/packages/metro-file-map/src/lib/RootPathUtils.js index 6ce7ec581e..ae528e27d2 100644 --- a/packages/metro-file-map/src/lib/RootPathUtils.js +++ b/packages/metro-file-map/src/lib/RootPathUtils.js @@ -152,10 +152,9 @@ export class RootPathUtils { const right = pos === 0 ? normalPath : normalPath.slice(pos); if (right.length === 0) { return left; - } else if (pos > this.#rootDepth * UP_FRAGMENT_SEP_LENGTH) { - // When we walk above the filesystem root, emit the remaining path as - // is. This is important on Windows, since we canonicalize cross-device - // paths as relative paths from rootDir. + } else if (IS_WIN32 && pos > this.#rootDepth * UP_FRAGMENT_SEP_LENGTH) { + // On Windows, up-fragments at the root encode target on anoher drive + // (e.g. '..\..\D:\file'), but on POSIX '..' re-enters the root return right; } // left may already end in a path separator only if it is a filesystem root, 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 b71c002eb4..493f9b3eb4 100644 --- a/packages/metro-file-map/src/lib/__tests__/RootPathUtils-test.js +++ b/packages/metro-file-map/src/lib/__tests__/RootPathUtils-test.js @@ -102,6 +102,8 @@ describe.each([['win32'], ['posix']])('RootPathUtils on %s', platform => { p('../normal/path'), p('../normal/path/'), p('../../normal/path'), + // On POSIX, `..` at the root re-enters the root + ...(platform === 'posix' ? [p('../../../normal/path')] : []), ] : [ p('..'), @@ -134,6 +136,10 @@ describe.each([['win32'], ['posix']])('RootPathUtils on %s', platform => { p('../../project/root'), p('../../project/root/'), p('../../..'), + // On POSIX, `..` at the root re-enters the root + ...(platform === 'posix' + ? [p('../../../normal/path'), p('../../../normal/path/')] + : []), ] : [p('..')]; From 9884f2ac776692ae865de3f2207de2ecc39d1f14 Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Tue, 12 May 2026 16:04:20 +0100 Subject: [PATCH 09/10] Drop `path.relative` and replace with `endsWith(path.sep` slice Co-authored-by: Rob Hogan <2590098+robhogan@users.noreply.github.com> --- packages/metro-file-map/src/lib/RootPathUtils.js | 6 ++++-- packages/metro-file-map/src/lib/TreeFS.js | 15 ++++----------- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/packages/metro-file-map/src/lib/RootPathUtils.js b/packages/metro-file-map/src/lib/RootPathUtils.js index ae528e27d2..bb3312108e 100644 --- a/packages/metro-file-map/src/lib/RootPathUtils.js +++ b/packages/metro-file-map/src/lib/RootPathUtils.js @@ -153,8 +153,10 @@ export class RootPathUtils { if (right.length === 0) { return left; } else if (IS_WIN32 && pos > this.#rootDepth * UP_FRAGMENT_SEP_LENGTH) { - // On Windows, up-fragments at the root encode target on anoher drive - // (e.g. '..\..\D:\file'), but on POSIX '..' re-enters the root + // On a real file system, navigating to `..` at the top level (posix `/` + // or Windows drive) is a no-op, but we can't respect that on Windows + // because Metro uses e.g. `..\..\D:\foo` to represent cross-drive + // relative paths. return right; } // left may already end in a path separator only if it is a filesystem root, diff --git a/packages/metro-file-map/src/lib/TreeFS.js b/packages/metro-file-map/src/lib/TreeFS.js index da85967217..198c854d2d 100644 --- a/packages/metro-file-map/src/lib/TreeFS.js +++ b/packages/metro-file-map/src/lib/TreeFS.js @@ -1240,17 +1240,10 @@ export default class TreeFS implements MutableFileSystem { '..', // Symlink target is relative to its containing directory. literalSymlinkTarget, // May be absolute, in which case the above are ignored ); - let normalSymlinkTarget = path.relative( - this.#rootDir, - absoluteSymlinkTarget, - ); - // path.relative leaves cross-device targets absolute on Windows (e.g. - // 'D:\\file' from a 'C:\\' root). Canonicalise those via RootPathUtils so - // they round-trip as a '..'-chain from rootDir. - if (path.isAbsolute(normalSymlinkTarget)) { - normalSymlinkTarget = this.#pathUtils.absoluteToNormal( - absoluteSymlinkTarget, - ); + let normalSymlinkTarget = + this.#pathUtils.absoluteToNormal(absoluteSymlinkTarget); + if (normalSymlinkTarget.endsWith(path.sep)) { + normalSymlinkTarget = normalSymlinkTarget.slice(0, -1); } const result = { ancestorOfRootIdx: From 72380c7ad61fe367fb6875a9be93f72881fb245f Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Tue, 12 May 2026 16:45:43 +0100 Subject: [PATCH 10/10] Apply lints --- packages/metro-file-map/src/lib/TreeFS.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/metro-file-map/src/lib/TreeFS.js b/packages/metro-file-map/src/lib/TreeFS.js index 198c854d2d..2790ce44ae 100644 --- a/packages/metro-file-map/src/lib/TreeFS.js +++ b/packages/metro-file-map/src/lib/TreeFS.js @@ -1234,17 +1234,19 @@ export default class TreeFS implements MutableFileSystem { typeof literalSymlinkTarget === 'string', 'Expected symlink target to be populated.', ); - const absoluteSymlinkTarget = path.resolve( + let 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 ); - let normalSymlinkTarget = - this.#pathUtils.absoluteToNormal(absoluteSymlinkTarget); - if (normalSymlinkTarget.endsWith(path.sep)) { - normalSymlinkTarget = normalSymlinkTarget.slice(0, -1); + if (absoluteSymlinkTarget.endsWith(path.sep)) { + absoluteSymlinkTarget = absoluteSymlinkTarget.slice(0, -1); } + const normalSymlinkTarget = this.#pathUtils.absoluteToNormal( + absoluteSymlinkTarget, + ); + const result = { ancestorOfRootIdx: this.#pathUtils.getAncestorOfRootIdx(normalSymlinkTarget),