You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
At Meta, we build several React Native apps in a large monorepo with a single shared Metro config. There is a use case we're exploring where we want some resolution behaviour to vary per app within the same Metro instance. Think of this as a form of dependency injection: the dependencies of a single module can have different resolutions, based on context that's only known at the app level. (See more about the use case under Future, below).
We could use custom transform options in the URL to change the dependencies at the source, before they're even resolved - but that would fork the transform cache for every variant, which is undesirable. Instead, we want to model the dependency injection case more closely: the importing module should be transformed (and cached) only once, and only the resolutions should change.
Proposal
Currently, a custom resolver's output is only allowed to vary based on the values available through the resolution context, and resolutions are cached in-memory inside the DependencyGraph class based on the origin directory, current platform and dependency specifier.
I propose that we:
Add a customResolverOptions mechanism where the bundle URL can have arbitrary extra parameters of the form &resolve.foo=bar.
Add any custom resolver options to the graphId for each bundle in IncrementalBundler, and to the resolution cache key for each resolution request inside DependencyGraph. This will have the effect of forking the in-memory resolution cache, but not the transform cache, based on the resolver options.
During resolution, pass customResolverOptions to resolveRequest via the context parameter.
Document that it's legal for a custom resolver's output to depend on customResolverOptions, even for resolution requests that are otherwise identical.
Future
Internally, what we're looking to build on top of this is a require-style API that conditionally resolves to one of a set of options passed at the call site, while allowing dead code elimination at build time.
There may be room for this type of high-level API in Metro itself (or in React Native). For instance, the following code could resolve to one of three versions of a module based on the current "build flavor", which would be an app-defined or framework-defined variable.
// Imaginary API - this doesn't exist and is not part of this proposalconstrenderer=require.env('BUILD_FLAVOR',{profiling: './renderer.profiling.js',debug: './renderer.debug.js',production: './renderer.production.js',},/* default */'./renderer.production.js');
This would be conceptually similar to resolving based on an environment variable[1], hence the name require.env. We could then create a mechanism for setting these variables in Metro's config and/or via the bundle URL, similar to what's proposed here with customResolverOptions.
[1] As alluded to above, the reason not to use something like if (process.env.BUILD_FLAVOR === 'profiling') require('foo') else require('bar') is that we'd need to fork the transform cache (i.e. transform every module with every combination of possible environment variables) in order to perform dead-code elimination.
Such an API will need more careful design to be useful, and we would need to think about compatibility with other bundlers/runtimes before we introduce and recommend it in user code (and especially published packages). We therefore have no plans to do this right away (we'll only build the Meta-internal version of this for now). But we do expect that shipping the lower-level customResolverOptions primitive will allow us to prototype ideas for such an API and inform the eventual design.
Another existing design in this space is Node's subpath imports with user-defined conditions. It will be worth evaluating that as an alternative to shipping our own conditional require()-style API.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
-
Context
At Meta, we build several React Native apps in a large monorepo with a single shared Metro config. There is a use case we're exploring where we want some resolution behaviour to vary per app within the same Metro instance. Think of this as a form of dependency injection: the dependencies of a single module can have different resolutions, based on context that's only known at the app level. (See more about the use case under Future, below).
We could use custom transform options in the URL to change the dependencies at the source, before they're even resolved - but that would fork the transform cache for every variant, which is undesirable. Instead, we want to model the dependency injection case more closely: the importing module should be transformed (and cached) only once, and only the resolutions should change.
Proposal
Currently, a custom resolver's output is only allowed to vary based on the values available through the resolution context, and resolutions are cached in-memory inside the DependencyGraph class based on the origin directory, current platform and dependency specifier.
I propose that we:
customResolverOptions
mechanism where the bundle URL can have arbitrary extra parameters of the form&resolve.foo=bar
.graphId
for each bundle inIncrementalBundler
, and to the resolution cache key for each resolution request insideDependencyGraph
. This will have the effect of forking the in-memory resolution cache, but not the transform cache, based on the resolver options.customResolverOptions
toresolveRequest
via thecontext
parameter.customResolverOptions
, even for resolution requests that are otherwise identical.Future
Internally, what we're looking to build on top of this is a
require
-style API that conditionally resolves to one of a set of options passed at the call site, while allowing dead code elimination at build time.There may be room for this type of high-level API in Metro itself (or in React Native). For instance, the following code could resolve to one of three versions of a module based on the current "build flavor", which would be an app-defined or framework-defined variable.
This would be conceptually similar to resolving based on an environment variable[1], hence the name
require.env
. We could then create a mechanism for setting these variables in Metro's config and/or via the bundle URL, similar to what's proposed here withcustomResolverOptions
.[1] As alluded to above, the reason not to use something like
if (process.env.BUILD_FLAVOR === 'profiling') require('foo') else require('bar')
is that we'd need to fork the transform cache (i.e. transform every module with every combination of possible environment variables) in order to perform dead-code elimination.Such an API will need more careful design to be useful, and we would need to think about compatibility with other bundlers/runtimes before we introduce and recommend it in user code (and especially published packages). We therefore have no plans to do this right away (we'll only build the Meta-internal version of this for now). But we do expect that shipping the lower-level
customResolverOptions
primitive will allow us to prototype ideas for such an API and inform the eventual design.Another existing design in this space is Node's subpath imports with user-defined conditions. It will be worth evaluating that as an alternative to shipping our own conditional
require()
-style API.Beta Was this translation helpful? Give feedback.
All reactions