Skip to content

metro-file-map: Let plugins report what changed in a batch, on the change event - #1953

Draft
robhogan wants to merge 1 commit into
robhogan/observe-haste-namesfrom
robhogan/plugin-change-summaries
Draft

robhogan wants to merge 1 commit into
robhogan/observe-haste-namesfrom
robhogan/plugin-change-summaries

Conversation

@robhogan

Copy link
Copy Markdown
Collaborator

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

…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
robhogan added this pull request to stack #1947 September 18, 2026 21:05
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Sep 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant