Skip to content

Resolver feature: schemeResolvers - #1804

Open
robhogan wants to merge 1 commit into
mainfrom
export-D113034376
Open

Resolver feature: schemeResolvers#1804
robhogan wants to merge 1 commit into
mainfrom
export-D113034376

Conversation

@robhogan

@robhogan robhogan commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

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 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 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

meta-codesync Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

@robhogan has exported this pull request. If you are a Meta employee, you can view the originating Diff in D113034376.

@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 Aug 2, 2026
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
meta-codesync Bot force-pushed the export-D113034376 branch from 87b5ef5 to d8386f5 Compare August 2, 2026 14:24
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. meta-exported

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant