Conversation
…ange event Stacked on #1952. ## Summary A file map plugin often knows things about a batch of changes that nothing else can work out. The motivating case is Haste - #1952 records the Haste names a resolution looks up, and to invalidate from those a consumer needs to know which names were rebound by a change. It can't derive that from the `ChangeEvent`, which carries paths and modified times, and by the time it sees the event a removed file's name is already gone from the map. `HastePlugin` has it to hand - its `onChanged` is given each removed file's old name and each added file's new one. This gives plugins a way to say so. `FileMapPlugin.onChanged` may now return a summary, and the file map publishes what each plugin returned as `ChangeEvent.pluginChanges`, by plugin name. A plugin that returns nothing is absent, so existing plugins are unaffected - the summary is a third type parameter on `FileMapPlugin` that defaults to `void`. `getPluginChanges(event, plugin)` reads an entry with the plugin's own summary type, so a consumer never writes the name or casts. The summary describes exactly the batch in `changes`. Plugins are already updated synchronously, immediately before the emit, so there's no ordering for a consumer to get wrong - which is the problem with the alternative of asking the plugin afterwards. There are two emit sites: the batched watch path updates plugins inline, and a recrawl updates them inside `#applyFileDelta`, which now returns what they reported along with the changes. The initial build goes through `#applyFileDelta` too but emits nothing, so its summaries go unused. `HastePlugin` is the first user. It reports `changedNames`, the names bound or unbound for any platform by the files added and removed - a bare name, matching what #1952 records. Plugin names now have to be unique across all of a file map's plugins. That was only checked for plugins with a worker, but persisted plugin state is keyed by name for every plugin, so two that shared a name would already have overwritten each other's state. Two things worth knowing: - On a cold start `onChanged` sees every file as added, so `HastePlugin` builds a set of every name for a summary nobody reads. That's nothing where Haste is inert, which is Metro's default, and transient otherwise. If it matters, the alternative is an optional method the file map calls only for a batch it's about to emit. - A modified file reaches plugins in `modifiedFiles` with its new data only, and `HastePlugin.onChanged` doesn't read that list. So an edit that changes a file's Haste name (a docblock, or a global package's `name`) is neither applied to the map nor reported here. That predates this, and can't happen with a path-based `hasteImpl`. `FileSystemChangeAggregator` keeps each file's pre-batch metadata, so giving plugins the old value is feasible, separately. Nothing consumes this yet. ## Test plan ``` yarn jest packages/metro-file-map packages/metro-resolver packages/metro/src/node-haste packages/metro/src/DeltaBundler packages/metro/src/integration_tests packages/metro/src/__tests__ packages/metro/src/Server yarn flow check yarn typecheck-ts yarn verify-api-snapshots ``` - `HastePlugin-test.js`: `onChanged` reports the names bound and unbound, and nothing when no file with a name was added or removed. - `index-test.js`: in watch mode an added and a removed file put both names on the event, read back through `getPluginChanges`, and plugins with nothing to say are absent. A recrawl publishes what plugins reported too. Two plugins sharing a name throw, with or without a worker. Changelog: Internal
robhogan
added this pull request to stack #1947
September 18, 2026 21:05
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #1952.
Summary
A file map plugin often knows things about a batch of changes that nothing else can work out. The motivating case is Haste - #1952 records the Haste names a resolution looks up, and to invalidate from those a consumer needs to know which names were rebound by a change. It can't derive that from the
ChangeEvent, which carries paths and modified times, and by the time it sees the event a removed file's name is already gone from the map.HastePluginhas it to hand - itsonChangedis given each removed file's old name and each added file's new one.This gives plugins a way to say so.
FileMapPlugin.onChangedmay now return a summary, and the file map publishes what each plugin returned asChangeEvent.pluginChanges, by plugin name. A plugin that returns nothing is absent, so existing plugins are unaffected - the summary is a third type parameter onFileMapPluginthat defaults tovoid.getPluginChanges(event, plugin)reads an entry with the plugin's own summary type, so a consumer never writes the name or casts.The summary describes exactly the batch in
changes. Plugins are already updated synchronously, immediately before the emit, so there's no ordering for a consumer to get wrong - which is the problem with the alternative of asking the plugin afterwards. There are two emit sites: the batched watch path updates plugins inline, and a recrawl updates them inside#applyFileDelta, which now returns what they reported along with the changes. The initial build goes through#applyFileDeltatoo but emits nothing, so its summaries go unused.HastePluginis the first user. It reportschangedNames, the names bound or unbound for any platform by the files added and removed - a bare name, matching what #1952 records.Plugin names now have to be unique across all of a file map's plugins. That was only checked for plugins with a worker, but persisted plugin state is keyed by name for every plugin, so two that shared a name would already have overwritten each other's state.
Two things worth knowing:
onChangedsees every file as added, soHastePluginbuilds a set of every name for a summary nobody reads. That's nothing where Haste is inert, which is Metro's default, and transient otherwise. If it matters, the alternative is an optional method the file map calls only for a batch it's about to emit.modifiedFileswith its new data only, andHastePlugin.onChangeddoesn't read that list. So an edit that changes a file's Haste name (a docblock, or a global package'sname) is neither applied to the map nor reported here. That predates this, and can't happen with a path-basedhasteImpl.FileSystemChangeAggregatorkeeps each file's pre-batch metadata, so giving plugins the old value is feasible, separately.Nothing consumes this yet.
Test plan
HastePlugin-test.js:onChangedreports the names bound and unbound, and nothing when no file with a name was added or removed.index-test.js: in watch mode an added and a removed file put both names on the event, read back throughgetPluginChanges, and plugins with nothing to say are absent. A recrawl publishes what plugins reported too. Two plugins sharing a name throw, with or without a worker.Changelog: Internal