From f2496901f0ffd75f791c353c9365d3951c83b271 Mon Sep 17 00:00:00 2001 From: Rob Hogan Date: Fri, 18 Sep 2026 10:12:54 +0100 Subject: [PATCH] metro-file-map: Unify TreeFS observation collection behind one optional parameter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Motivation Two things: ### Incremental resolution Selectively and correctly invalidating graph edges on file system events requires knowing which file system events could affect a result - this is the foundation of that. ### Reimplementing Expo's "On-demand file system" See https://github.com/expo/expo/pull/45391 The user benefits of this are clear (and it was a [headline feature](https://expo.dev/changelog/sdk-56#expo-cli) of SDK 56, so there's no going back), but Expo currently forks `metro-file-map` and synchronously crawls new bits of the file system within a resolution attempt, and has no watch support. My plan for implementing this in Metro is that on a failed resolution, Metro (probably `DependencyGraph`) catches the failure, compares file system observations on TreeFS vs real `fs`, and infers when the resolution failed because a directory isn't watched. That immediately improves error reporting in the first instance, but we can also build on that to dynamically add a watched location, inline in the (async) traversal, and replay the resolution, without interrupting the user. ## Summary We have a bunch of APIs here that I added and evolved over time to eventually support incremental resolution. None are used so far. This cleans them up into one consistent pattern, and improves performance a bit along the way. Currently: - `TreeFS.lookup` returns `links` and `missing` on every `LookupResult`, unconditionally. It allocates a `Set` per call and, on a miss, derives an absolute path for the first missing segment. No callers currently use either. - `#lookupByNormalPath` takes a nullable `collectLinkPaths` and does nothing when it's absent (this is the shape we generalise here to `observations`). - `hierarchicalLookup` takes a nullable `invalidatedBy`, but collapses two kinds of observation into one flat set: traversed symlinks, which a modification can invalidate, and probed candidate paths, which only an addition or removal can. This PR replaces all three with one `Observations` record - an `existence` set and a `content` set - passed as an optional trailing parameter to `FileSystem.lookup` and `FileSystem.hierarchicalLookup`. It's a parameter rather than a return value so that the caller can accumulate observations across multiple queries. Paths are canonical, which is cheap and matches the keys of `ChangeEvent.changes` for easy invalidation (not implemented yet). Changelog: Internal `metro-file-map` is marked experimental so none of its API is considered semver stable, but for completeness - neither Expo nor any OSS searchable on GitHub uses `links`, `missing` or `invalidatedBy`. ## Benchmark Because we're no longer allocating new sets or converting missing paths to absolute, and because failed lookups dominate resolution, this is a (small) perf win. Replaying resolutions under a default RN config with a synthetic app, 30% of source outside projectRoot (6,935 resolutions): | metric | main | this diff | |---|---|---| | first pass after startup (ms) | 125.9 | **122.3 (−2.9%)** | | warm pass (ms) | 60.3 | **57.6 (−4.5%)** | | after a source change (ms) | 66.5 | **64.5 (−3.1%)** | | after a `package.json` change (ms) | 71.6 | **69.2 (−3.3%)** | ## Test plan - Existing unit test coverage - Observations isn't passed by any caller in Metro, so there's nothing to test e2e. --- packages/metro-file-map/API.md | 4 +- packages/metro-file-map/src/flow-types.js | 47 ++-- packages/metro-file-map/src/lib/TreeFS.js | 81 ++++--- .../src/lib/__tests__/TreeFS-test.js | 203 +++++++++++------- .../metro/src/node-haste/DependencyGraph.js | 1 - 5 files changed, 203 insertions(+), 133 deletions(-) diff --git a/packages/metro-file-map/API.md b/packages/metro-file-map/API.md index d9583a0eae..095cbd210f 100644 --- a/packages/metro-file-map/API.md +++ b/packages/metro-file-map/API.md @@ -167,12 +167,12 @@ interface FileSystem_2 { subpath: string, opts: { breakOnSegment?: null | undefined | string; - invalidatedBy?: null | undefined | Set; subpathType: 'f' | 'd'; }, + observations?: null | undefined | Observations, ): null | undefined | {absolutePath: string; containerRelativePath: string}; linkStats(file: Path): null | undefined | FileStats; - lookup(mixedPath: Path): LookupResult; + lookup(mixedPath: Path, observations?: null | undefined | Observations): LookupResult; matchFiles(opts: { filter?: RegExp | null | undefined; filterCompareAbsolute?: boolean | undefined; diff --git a/packages/metro-file-map/src/flow-types.js b/packages/metro-file-map/src/flow-types.js index 89e88d488a..bfb42e02da 100644 --- a/packages/metro-file-map/src/flow-types.js +++ b/packages/metro-file-map/src/flow-types.js @@ -367,8 +367,7 @@ export interface FileSystem { * X = dirname(X) * while X !== dirname(X) * - * If opts.invalidatedBy is given, collects all absolute, real paths that if - * added or removed may invalidate this result. + * If `observations` is given, records the paths this result depends upon. * * Useful for finding the closest package scope (subpath: package.json, * type f, breakOnSegment: node_modules) or closest potential package root @@ -379,9 +378,9 @@ export interface FileSystem { subpath: string, opts: { breakOnSegment?: ?string, - invalidatedBy?: ?Set, subpathType: 'f' | 'd', }, + observations?: ?Observations, ): ?{ absolutePath: string, containerRelativePath: string, @@ -396,8 +395,12 @@ export interface FileSystem { /** * Return information about the given path, whether a directory or file. * Always follow symlinks, and return a real path if it exists. + * + * If `observations` is given, records the paths this result depends upon: + * any symlink traversed, and either the real path found or the first path + * segment that did not exist. */ - lookup(mixedPath: Path): LookupResult; + lookup(mixedPath: Path, observations?: ?Observations): LookupResult; matchFiles(opts: { /* Filter relative paths against a pattern. */ @@ -431,16 +434,9 @@ export type LookupResult = // could indicate an unwatched path, or a directory containing no watched // files). exists: false, - // The real, normal, absolute paths of any symlinks traversed. - links: ReadonlySet, - // The real, normal, absolute path of the first path segment - // encountered that does not exist, or cannot be navigated through. - missing: string, } | { exists: true, - // The real, normal, absolute paths of any symlinks traversed. - links: ReadonlySet, // The real, normal, absolute path of the directory. realPath: string, // Currently lookup always follows symlinks, so can only return @@ -449,8 +445,6 @@ export type LookupResult = } | { exists: true, - // The real, normal, absolute paths of any symlinks traversed. - links: ReadonlySet, // The real, normal, absolute path of the file. realPath: string, // Currently lookup always follows symlinks, so can only return @@ -460,6 +454,33 @@ export type LookupResult = metadata: FileMetadata, }; +/** + * A mutable record of what a query observed of the file system, sufficient to + * decide later whether its answer may have changed. + * + * The parameter is optional and may be omitted for performance. + */ +export interface Observations { + /** + * Paths that were observed to exist, or not to exist. Invalidated by added or + * removed files or directories. + * + * Non-existence is always recorded against the *topmost* missing path, not + * the path asked for, so an absent directory collapses every query beneath + * it to one entry: a missing `node_modules` is one path invalidating every + * package resolution under it, rather than one per package. + */ + readonly existence: Set; + + /** + * Files whose content was observed (including traversed symlinks, whose + * content is their target) and directories whose immediate entries were + * enumerated. Invalidated by modification, as well as by addition or + * removal. + */ + readonly content: Set; +} + export interface MockMap { getMockModule(name: string): ?Path; } diff --git a/packages/metro-file-map/src/lib/TreeFS.js b/packages/metro-file-map/src/lib/TreeFS.js index 1aaef659c1..b81a92d9c2 100644 --- a/packages/metro-file-map/src/lib/TreeFS.js +++ b/packages/metro-file-map/src/lib/TreeFS.js @@ -16,6 +16,7 @@ import type { FileSystemListener, LookupResult, MutableFileSystem, + Observations, Path, ProcessFileFunction, } from '../flow-types'; @@ -279,25 +280,25 @@ export default class TreeFS implements MutableFileSystem { return result != null; } - lookup(mixedPath: Path): LookupResult { + lookup(mixedPath: Path, observations?: ?Observations): LookupResult { const normalPath = this.#normalizePath(mixedPath); - const links = new Set(); const result = this.#lookupByNormalPath(normalPath, { - collectLinkPaths: links, + observations, followLeaf: true, }); if (!result.exists) { - const {canonicalMissingPath} = result; - return { - exists: false, - links, - missing: this.#pathUtils.normalToAbsolute(canonicalMissingPath), - }; + if (observations) { + observations.existence.add(result.canonicalMissingPath); + } + return {exists: false}; } const {canonicalPath, node} = result; + if (observations) { + observations.existence.add(canonicalPath); + } const realPath = this.#pathUtils.normalToAbsolute(canonicalPath); if (isDirectory(node)) { - return {exists: true, links, realPath, type: 'd'}; + return {exists: true, realPath, type: 'd'}; } invariant( isRegularFile(node), @@ -305,7 +306,7 @@ export default class TreeFS implements MutableFileSystem { mixedPath, canonicalPath, ); - return {exists: true, links, realPath, type: 'f', metadata: node}; + return {exists: true, realPath, type: 'f', metadata: node}; } getAllFiles(): Array { @@ -559,9 +560,9 @@ export default class TreeFS implements MutableFileSystem { normalPath: string, segmentName: string, }>, - // Mutable Set into which absolute real paths of traversed symlinks will - // be added. Omit for performance if not needed. - collectLinkPaths?: ?Set, + // Record into which the canonical paths of traversed symlinks will be + // added, as `content`. Omit for performance if not needed. + observations?: ?Observations, // Low-level callbacks called on mutations of TreeFS data. // Omit for performance if not needed. @@ -730,10 +731,8 @@ export default class TreeFS implements MutableFileSystem { ); const normalSymlinkTarget = normalizePathSeparatorsToSystem(storedSymlinkTarget); - if (opts.collectLinkPaths) { - opts.collectLinkPaths.add( - this.#pathUtils.normalToAbsolute(currentPath), - ); + if (opts.observations) { + opts.observations.content.add(currentPath); } const remainingTargetPath = isLastSegment @@ -839,8 +838,8 @@ export default class TreeFS implements MutableFileSystem { * X = dirname(X) * while X !== dirname(X) * - * If opts.invalidatedBy is given, collects all absolute, real paths that if - * added or removed may invalidate this result. + * If `observations` is given, records the canonical paths this result + * depends upon. * * Useful for finding the closest package scope (subpath: package.json, * type f, breakOnSegment: node_modules) or closest potential package root @@ -851,9 +850,9 @@ export default class TreeFS implements MutableFileSystem { subpath: string, opts: { breakOnSegment: ?string, - invalidatedBy: ?Set, subpathType: 'f' | 'd', }, + observations?: ?Observations, ): ?{ absolutePath: string, containerRelativePath: string, @@ -865,10 +864,9 @@ export default class TreeFS implements MutableFileSystem { segmentName: string, }> = []; const normalPath = this.#normalizePath(mixedStartPath); - const invalidatedBy = opts.invalidatedBy; const closestLookup = this.#lookupByNormalPath(normalPath, { collectAncestors: ancestorsOfInput, - collectLinkPaths: invalidatedBy, + observations, }); if (closestLookup.exists && isDirectory(closestLookup.node)) { @@ -876,7 +874,7 @@ export default class TreeFS implements MutableFileSystem { closestLookup.canonicalPath, subpath, opts.subpathType, - invalidatedBy, + observations, null, ); if (maybeAbsolutePathMatch != null) { @@ -887,15 +885,13 @@ export default class TreeFS implements MutableFileSystem { } } else { if ( - invalidatedBy && + observations && (!closestLookup.exists || !isDirectory(closestLookup.node)) ) { - invalidatedBy.add( - this.#pathUtils.normalToAbsolute( - closestLookup.exists - ? closestLookup.canonicalPath - : closestLookup.canonicalMissingPath, - ), + observations.existence.add( + closestLookup.exists + ? closestLookup.canonicalPath + : closestLookup.canonicalMissingPath, ); } if ( @@ -951,7 +947,7 @@ export default class TreeFS implements MutableFileSystem { candidate.normalPath, subpath, opts.subpathType, - invalidatedBy, + observations, { ancestorOfRootIdx: candidate.ancestorOfRootIdx, node: candidate.node, @@ -998,7 +994,7 @@ export default class TreeFS implements MutableFileSystem { candidateNormalPath, subpath, opts.subpathType, - invalidatedBy, + observations, null, ); if (maybeAbsolutePathMatch != null) { @@ -1032,7 +1028,7 @@ export default class TreeFS implements MutableFileSystem { normalCandidatePath: string, subpath: string, subpathType: 'f' | 'd', - invalidatedBy: ?Set, + observations: ?Observations, start: ?{ ancestorOfRootIdx: ?number, node: DirectoryNode, @@ -1043,7 +1039,7 @@ export default class TreeFS implements MutableFileSystem { this.#pathUtils.joinNormalToRelative(normalCandidatePath, subpath) .normalPath, { - collectLinkPaths: invalidatedBy, + observations, }, ); if ( @@ -1051,14 +1047,15 @@ export default class TreeFS implements MutableFileSystem { // Should be a Map iff subpathType is directory isDirectory(lookupResult.node) === (subpathType === 'd') ) { + if (observations) { + observations.existence.add(lookupResult.canonicalPath); + } return this.#pathUtils.normalToAbsolute(lookupResult.canonicalPath); - } else if (invalidatedBy) { - invalidatedBy.add( - this.#pathUtils.normalToAbsolute( - lookupResult.exists - ? lookupResult.canonicalPath - : lookupResult.canonicalMissingPath, - ), + } else if (observations) { + observations.existence.add( + lookupResult.exists + ? lookupResult.canonicalPath + : lookupResult.canonicalMissingPath, ); } return null; 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 19685f8485..0cdc62c4eb 100644 --- a/packages/metro-file-map/src/lib/__tests__/TreeFS-test.js +++ b/packages/metro-file-map/src/lib/__tests__/TreeFS-test.js @@ -20,6 +20,11 @@ import typeof * as PathModule from 'node:path'; import H from '../../constants'; +const emptyObservations = () => ({ + content: new Set(), + existence: new Set(), +}); + let mockPathModule; jest.mock('node:path', () => mockPathModule); @@ -34,6 +39,15 @@ describe.each([['win32'], ['posix']])('TreeFS on %s', platform => { let tfs: TreeFSType; let TreeFS: Class; + // Observations are recorded as canonical (root-relative) paths. Test tables + // below are written as absolute paths for readability, and converted here. + const canonicalTo = + (rootDir: string) => + (absolutePath: string): CanonicalPath => + mockPathModule.relative(rootDir, absolutePath); + const canonical = (absolutePath: string) => + canonicalTo(p('/project'))(absolutePath); + beforeEach(() => { jest.resetModules(); mockPathModule = jest.requireActual<{}>('path')[platform]; @@ -149,14 +163,19 @@ describe.each([['win32'], ['posix']])('TreeFS on %s', platform => { [p('root/project/bar.js'), p('/project/bar.js'), [p('/project/root')]], ])( '%s -> %s through expected symlinks', - (givenPath, expectedRealPath, expectedSymlinks) => - expect(tfs.lookup(givenPath)).toEqual({ + (givenPath, expectedRealPath, expectedSymlinks) => { + const observations = emptyObservations(); + expect(tfs.lookup(givenPath, observations)).toEqual({ exists: true, - links: new Set(expectedSymlinks), realPath: expectedRealPath, type: 'f', metadata: expect.any(Array), - }), + }); + expect(observations).toEqual({ + content: new Set(expectedSymlinks.map(canonical)), + existence: new Set([canonical(expectedRealPath)]), + }); + }, ); test.each([ @@ -174,24 +193,34 @@ describe.each([['win32'], ['posix']])('TreeFS on %s', platform => { [p('/project/foo/../../project/missing'), [], p('/project/missing')], ])( 'non-existence for bad paths, missing files or broken links %s', - (givenPath, expectedSymlinks, missingPath) => - expect(tfs.lookup(givenPath)).toEqual({ - exists: false, - links: new Set(expectedSymlinks), - missing: missingPath, - }), + (givenPath, expectedSymlinks, missingPath) => { + const observations = emptyObservations(); + expect(tfs.lookup(givenPath, observations)).toEqual({exists: false}); + expect(observations).toEqual({ + content: new Set(expectedSymlinks.map(canonical)), + existence: new Set([canonical(missingPath)]), + }); + }, ); test.each([ - [p('/project/foo'), p('/project/foo')], - [p('/project/foo/'), p('/project/foo')], - [p('/project/root/outside'), p('/outside')], - ])('returns type: d for %s', (givenPath, expectedRealPath) => - expect(tfs.lookup(givenPath)).toMatchObject({ - exists: true, - type: 'd', - realPath: expectedRealPath, - }), + [p('/project/foo'), p('/project/foo'), []], + [p('/project/foo/'), p('/project/foo'), []], + [p('/project/root/outside'), p('/outside'), [p('/project/root')]], + ])( + 'returns type: d for %s', + (givenPath, expectedRealPath, expectedSymlinks) => { + const observations = emptyObservations(); + expect(tfs.lookup(givenPath, observations)).toEqual({ + exists: true, + type: 'd', + realPath: expectedRealPath, + }); + expect(observations).toEqual({ + content: new Set(expectedSymlinks.map(canonical)), + existence: new Set([canonical(expectedRealPath)]), + }); + }, ); test('traversing the same symlink multiple times does not imply a cycle', () => { @@ -215,22 +244,21 @@ describe.each([['win32'], ['posix']])('TreeFS on %s', platform => { throw new Error('Not implemented'); }, }); - expect(tfs.lookup(p('/deep/missing/bar.js'))).toMatchObject({ - exists: false, - missing: p('/deep/missing'), - }); - expect(tfs.lookup(p('link-up/bar.js'))).toMatchObject({ - exists: false, - missing: p('/deep/project/bar.js'), - }); - expect(tfs.lookup(p('../../baz.js'))).toMatchObject({ - exists: false, - missing: p('/deep/baz.js'), - }); - expect(tfs.lookup(p('../../project/root/baz.js'))).toMatchObject({ - exists: false, - missing: p('/deep/project/root/baz.js'), - }); + const toCanonical = canonicalTo(p('/deep/project/root')); + const expectMissing = (givenPath: string, missingPath: string) => { + const observations = emptyObservations(); + expect(tfs.lookup(givenPath, observations)).toEqual({exists: false}); + expect(observations.existence).toEqual( + new Set([toCanonical(missingPath)]), + ); + }; + expectMissing(p('/deep/missing/bar.js'), p('/deep/missing')); + expectMissing(p('link-up/bar.js'), p('/deep/project/bar.js')); + expectMissing(p('../../baz.js'), p('/deep/baz.js')); + expectMissing( + p('../../project/root/baz.js'), + p('/deep/project/root/baz.js'), + ); }); }); @@ -258,13 +286,16 @@ describe.each([['win32'], ['posix']])('TreeFS on %s', platform => { ])( 'lookup can find files that go back towards the project root (%s)', (mixedPath, expectedRealPath, expectedSymlinks) => { - expect(tfs.lookup(mixedPath)).toEqual({ + const observations = emptyObservations(); + expect(tfs.lookup(mixedPath, observations)).toEqual({ exists: true, realPath: expectedRealPath, - links: new Set(expectedSymlinks), type: 'f', metadata: expect.any(Array), }); + expect(observations.content).toEqual( + new Set(expectedSymlinks.map(canonical)), + ); }, ); @@ -480,139 +511,151 @@ describe.each([['win32'], ['posix']])('TreeFS on %s', platform => { }); }); + // The fourth column is paths, besides the match itself, whose addition or + // removal changes the result, the fifth those whose modification does too - + // the symlinks traversed. test.each([ - ['/A/B/C/a', '/A/B/C/a/package.json', '', []], - ['/A/B/C/a/b', '/A/B/C/a/package.json', 'b', ['/A/B/C/a/b/package.json']], + ['/A/B/C/a', '/A/B/C/a/package.json', '', [], []], + [ + '/A/B/C/a/b', + '/A/B/C/a/package.json', + 'b', + ['/A/B/C/a/b/package.json'], + [], + ], [ '/A/B/C/a/package.json', '/A/B/C/a/package.json', 'package.json', ['/A/B/C/a/package.json'], + [], ], [ '/A/B/C/a/b/notexists', '/A/B/C/a/package.json', 'b/notexists', ['/A/B/C/a/b/notexists', '/A/B/C/a/b/package.json'], + [], ], - ['/A/B/C/a/b/c', '/A/B/C/a/b/c/package.json', '', []], + ['/A/B/C/a/b/c', '/A/B/C/a/b/c/package.json', '', [], []], [ '/A/B/C/other', '/A/package.json', 'B/C/other', ['/A/B/C/other', '/A/B/C/package.json', '/A/B/package.json'], + [], ], [ '/A/B/C', '/A/package.json', 'B/C', ['/A/B/C/package.json', '/A/B/package.json'], + [], ], - ['/A/B', '/A/package.json', 'B', ['/A/B/package.json']], + ['/A/B', '/A/package.json', 'B', ['/A/B/package.json'], []], [ '/A/B/foo', '/A/package.json', 'B/foo', - ['/A/B/foo', '/A/B/package.json'], + [], ], - ['/A/foo', '/A/package.json', 'foo', ['/A/foo']], - ['/foo', null, null, ['/foo', '/package.json']], + ['/A/foo', '/A/package.json', 'foo', ['/A/foo'], []], + ['/foo', null, null, ['/foo', '/package.json'], []], [ '/A/B/C/a/b/c/d/link-to-C/foo.js', '/A/B/C/a/b/c/package.json', 'd/link-to-C/foo.js', - [ - '/A/B/C/a/b/c/d/link-to-C', - '/A/B/C/a/b/c/d/package.json', - '/A/B/C/foo.js', - '/A/B/C/package.json', - ], + ['/A/B/C/a/b/c/d/package.json', '/A/B/C/foo.js', '/A/B/C/package.json'], + ['/A/B/C/a/b/c/d/link-to-C'], ], [ '/A/B/C/a/b/c/d/link-to-B/C/foo.js', '/A/B/C/a/b/c/package.json', 'd/link-to-B/C/foo.js', [ - '/A/B/C/a/b/c/d/link-to-B', '/A/B/C/a/b/c/d/package.json', '/A/B/C/foo.js', '/A/B/C/package.json', '/A/B/package.json', ], + ['/A/B/C/a/b/c/d/link-to-B'], ], [ '/A/B/C/a/b/c/d/link-to-A/B/C/foo.js', '/A/package.json', 'B/C/foo.js', - [ - '/A/B/C/a/b/c/d/link-to-A', - '/A/B/C/foo.js', - '/A/B/C/package.json', - '/A/B/package.json', - ], + ['/A/B/C/foo.js', '/A/B/C/package.json', '/A/B/package.json'], + ['/A/B/C/a/b/c/d/link-to-A'], ], [ '/A/B/C/a/1/foo.js', '/A/B/C/a/1/real-package.json', 'foo.js', - ['/A/B/C/a/1/foo.js', '/A/B/C/a/1/package.json'], + ['/A/B/C/a/1/foo.js'], + ['/A/B/C/a/1/package.json'], ], [ '/A/B/C/a/2/foo.js', '/A/B/C/a/package.json', '2/foo.js', - [ - '/A/B/C/a/2/foo.js', - '/A/B/C/a/2/notexist-package.json', - '/A/B/C/a/2/package.json', - ], + ['/A/B/C/a/2/foo.js', '/A/B/C/a/2/notexist-package.json'], + ['/A/B/C/a/2/package.json'], ], [ '/A/B/C/a/n_m/pkg/notexist.js', '/A/B/C/a/n_m/pkg/package.json', 'notexist.js', ['/A/B/C/a/n_m/pkg/notexist.js'], + [], ], [ '/A/B/C/a/n_m/pkg/subpath/notexist.js', '/A/B/C/a/n_m/pkg/subpath/package.json', 'notexist.js', ['/A/B/C/a/n_m/pkg/subpath/notexist.js'], + [], ], [ '/A/B/C/a/n_m/pkg/otherpath/notexist.js', '/A/B/C/a/n_m/pkg/package.json', 'otherpath/notexist.js', ['/A/B/C/a/n_m/pkg/otherpath'], + [], ], // pkg3 does not exist, doesn't look beyond the containing n_m - ['/A/B/C/a/n_m/pkg3/foo.js', null, null, ['/A/B/C/a/n_m/pkg3']], + ['/A/B/C/a/n_m/pkg3/foo.js', null, null, ['/A/B/C/a/n_m/pkg3'], []], // Does not look beyond n_m, if n_m does not exist - ['/A/B/C/a/b/n_m/pkg/foo', null, null, ['/A/B/C/a/b/n_m']], + ['/A/B/C/a/b/n_m/pkg/foo', null, null, ['/A/B/C/a/b/n_m'], []], [ '/A/B/C/n_m/workspace/link-to-pkg/subpath', '/A/B/workspace-pkg/package.json', 'subpath', - ['/A/B/C/n_m/workspace/link-to-pkg', '/A/B/workspace-pkg/subpath'], + ['/A/B/workspace-pkg/subpath'], + ['/A/B/C/n_m/workspace/link-to-pkg'], ], ])( - '%s => %s (relative %s, invalidatedBy %s)', + '%s => %s (relative %s, existence %s, content %s)', ( startPath, expectedPath, expectedRelativeSubpath, - expectedInvalidatedBy, + expectedExistence, + expectedContent, ) => { const pathMap = (normalPosixPath: string) => mockPathModule.resolve(p('/A/B/C'), p(normalPosixPath)); - const invalidatedBy = new Set(); + const observations = emptyObservations(); expect( - tfs.hierarchicalLookup(p(startPath), 'package.json', { - breakOnSegment: 'n_m', - invalidatedBy, - subpathType: 'f', - }), + tfs.hierarchicalLookup( + p(startPath), + 'package.json', + { + breakOnSegment: 'n_m', + subpathType: 'f', + }, + observations, + ), ).toEqual( expectedPath == null ? null @@ -621,7 +664,17 @@ describe.each([['win32'], ['posix']])('TreeFS on %s', platform => { containerRelativePath: p(expectedRelativeSubpath), }, ); - expect(invalidatedBy).toEqual(new Set(expectedInvalidatedBy.map(p))); + const toCanonical = (posixPath: string) => + canonicalTo(p('/A/B/C'))(pathMap(posixPath)); + expect(observations).toEqual({ + content: new Set(expectedContent.map(toCanonical)), + existence: new Set( + (expectedPath == null + ? expectedExistence + : [...expectedExistence, expectedPath] + ).map(toCanonical), + ), + }); }, ); }); diff --git a/packages/metro/src/node-haste/DependencyGraph.js b/packages/metro/src/node-haste/DependencyGraph.js index 2c7bbf1f55..6623a4c6e3 100644 --- a/packages/metro/src/node-haste/DependencyGraph.js +++ b/packages/metro/src/node-haste/DependencyGraph.js @@ -181,7 +181,6 @@ export default class DependencyGraph extends EventEmitter { 'package.json', { breakOnSegment: 'node_modules', - invalidatedBy: null, subpathType: 'f', }, );