Resolver feature: schemeResolvers - #1804
Open
robhogan wants to merge 1 commit into
Open
Conversation
Contributor
|
@robhogan has exported this pull request. If you are a Meta employee, you can view the originating Diff in D113034376. |
Summary:
Adds a pluggable mechanism for resolving URI-scheme-prefixed import specifiers (e.g. `metro:foo`) in metro-resolver.
- Adds optional field `schemeResolvers?: Readonly<{[scheme: string]: CustomResolver}>` to `ResolutionContext`, keyed by scheme. The scheme parsed from a specifier is lowercased before lookup, so keys must be lowercase (both `Foo:` and `foo:` match the `'foo'` key). Lookup uses an own-property check (`Object.hasOwn`), so a specifier whose scheme collides with an `Object.prototype` key (e.g. `constructor:`) is never dispatched to an inherited value.
- Exposed as `config.resolver.schemeResolvers` (default `{}`). `mergeConfig` deep-merges it per scheme, so presets and user configs combine key-by-key rather than replacing the whole map.
- `resolve()` dispatches a scheme-prefixed specifier to its registered resolver as part of specifier classification: after (mutually exclusive) relative/absolute and subpath-import handling, but before the remaining strategies (browser-field redirection, Haste, node_modules, extraNodeModules). A user `resolveRequest` still takes precedence, since it runs first and can delegate back into default resolution, where scheme dispatch then applies.
- Because absolute-path handling runs first, Windows drive-absolute specifiers (`C:\...`, `C:/...`) are resolved as paths and never treated as schemes. A scheme with no registered resolver falls through to normal resolution and, only if that also fails, raises a scheme-specific error.
## Why?
### Example - `babel/runtime`
Concretely, an example of a problem this solves is with using `babel/plugin-transform-runtime`. Currently, Metro's transform pipeline injects imports of `babel/runtime`, and resolves it as any other runtime dependency, using the source location as the resolver origin. The problem is, even though we've injected this dependency, we have no guarantee that it will resolve as we expect - because it's indistinguishable from an ordinary user-authored import, we resolve hierarchically, which may fail or resolve to an unexpected version.
`babel/plugin-transform-runtime` has the [`absoluteRuntime`](https://babeljs.io/docs/babel-plugin-transform-runtime#absoluteruntime) option, which overcomes the issue above, but at the cost of making the transform cache non-portable by injecting absolute file paths into ASTs. This totally breaks remote caching, and is a non-starter in Metro's architecture.
`babel/plugin-transform-runtime` *also* has a (newer) [`moduleName`](https://babeljs.io/docs/babel-plugin-transform-runtime#modulename) option, which allows us to replace `babel/runtime` with a string of our choosing. We can use that to inject, say `metro:babel-runtime`, and with `schemeResolvers`, Metro core can configure where that resolves. *And* we can collect those dependencies to determine which helpers are actually used (FB: see footnote)
Note `babel/runtime` is not a core concern of resolution generically, so special handling like this belongs in `metro`, not `metro-resolver`. That's why I think a pluggable `metro:` protocol makes sense - Metro can clearly dictate the behaviour of its own namespace, without the indirection and cost of wrapping the whole resolver via custom `resolveRequest`.
### And beyond: `data:`, `virtual:`, `react-native:`, `expo:`
This is an ergonomic extension point for Metro, integrators and library authors to provide custom resolution behaviour for injected or virtual imports. Currently, this requires wrapping `resolveRequest` repeatedly.
```
- **[Feature]** Add `schemeResolvers` to `ResolutionContext`, configurable via `config.resolver.schemeResolvers`, to resolve custom URI schemes
```
Reviewed By: huntie
Differential Revision: D113034376
meta-codesync
Bot
force-pushed
the
export-D113034376
branch
from
August 2, 2026 14:24
87b5ef5 to
d8386f5
Compare
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.
Summary:
Adds a pluggable mechanism for resolving URI-scheme-prefixed import specifiers (e.g.
metro:foo) in metro-resolver.schemeResolvers?: Readonly<{[scheme: string]: CustomResolver}>toResolutionContext, keyed by scheme. The scheme parsed from a specifier is lowercased before lookup, so keys must be lowercase (bothFoo:andfoo:match the'foo'key). Lookup uses an own-property check (Object.hasOwn), so a specifier whose scheme collides with anObject.prototypekey (e.g.constructor:) is never dispatched to an inherited value.config.resolver.schemeResolvers(default{}).mergeConfigdeep-merges it per scheme, so presets and user configs combine key-by-key rather than replacing the whole map.resolve()dispatches a scheme-prefixed specifier to its registered resolver as part of specifier classification: after (mutually exclusive) relative/absolute and subpath-import handling, but before the remaining strategies (browser-field redirection, Haste, node_modules, extraNodeModules). A userresolveRequeststill takes precedence, since it runs first and can delegate back into default resolution, where scheme dispatch then applies.C:\...,C:/...) are resolved as paths and never treated as schemes. A scheme with no registered resolver falls through to normal resolution and, only if that also fails, raises a scheme-specific error.Why?
Example -
babel/runtimeConcretely, an example of a problem this solves is with using
babel/plugin-transform-runtime. Currently, Metro's transform pipeline injects imports ofbabel/runtime, and resolves it as any other runtime dependency, using the source location as the resolver origin. The problem is, even though we've injected this dependency, we have no guarantee that it will resolve as we expect - because it's indistinguishable from an ordinary user-authored import, we resolve hierarchically, which may fail or resolve to an unexpected version.babel/plugin-transform-runtimehas theabsoluteRuntimeoption, which overcomes the issue above, but at the cost of making the transform cache non-portable by injecting absolute file paths into ASTs. This totally breaks remote caching, and is a non-starter in Metro's architecture.babel/plugin-transform-runtimealso has a (newer)moduleNameoption, which allows us to replacebabel/runtimewith a string of our choosing. We can use that to inject, saymetro:babel-runtime, and withschemeResolvers, Metro core can configure where that resolves. And we can collect those dependencies to determine which helpers are actually used (FB: see footnote)Note
babel/runtimeis not a core concern of resolution generically, so special handling like this belongs inmetro, notmetro-resolver. That's why I think a pluggablemetro:protocol makes sense - Metro can clearly dictate the behaviour of its own namespace, without the indirection and cost of wrapping the whole resolver via customresolveRequest.And beyond:
data:,virtual:,react-native:,expo:This is an ergonomic extension point for Metro, integrators and library authors to provide custom resolution behaviour for injected or virtual imports. Currently, this requires wrapping
resolveRequestrepeatedly.Reviewed By: huntie
Differential Revision: D113034376