⚛️ 🧲 Intercept and mutate runtime dependency resolution.
metro-plugin-anisotropic-transform
is a transform plugin for React Native's Metro Bundler. It is designed to inspect the relationships that exist between dependencies; specifically, those in the node_modules/
directory which make a cyclic dependence to the root project.
This transform is designed to fulfill the following functionality:
- Suppress cyclic dependencies on the root project, which can lead to drastically increased installation time.
- Derisk the possibility of library dependencies relying upon runtime functionality exported by the root project.
- Prevent dependencies from squatting on critical functionality exported by other
node_modules
.
Applications built using React Native are forced to resolve all module dependencies at bundle time. This is because unlike the Node.js ecosystem, the entire dependency map of the compiled application must be resolved prior to app distrbution in order translate into a fixed application bundle that can be transported.
This makes the following impact on the compilation process:
- Dynamic
require
s are not possible in React Native wheninlineRequires
is set tofalse
. All attempts toimport
andrequire
, even those which have been deferred until execution time, must be resolved during the bundle phase.- Note that it is possible to dynamically
require
in React Native, so in order to protect the transform from runtime misuse,inlineRequires
must be set tofalse
.
- Note that it is possible to dynamically
- The entire scope of an application's module resolution map can be determined and interrogated at bundle time.
metro-plugin-anisotropic-transform
utilizes these restrictions in library resolution to compare and handle relationships between the core application and children of the node_modules
directory, and in these cases, resolve appropriately.
Using Yarn:
yarn add --dev metro-plugin-anisotropic-transform
We'll create our own custom Metro transform to invoke the anisotropic transform.
const deepmerge = require("deepmerge");
const { transform: anisotropic } = require("metro-plugin-anisotropic-transform");
module.exports.transform = function ({
src,
filename,
options,
}) {
const opts = deepmerge(options, {
customTransformOptions: {
["metro-plugin-anisotropic-transform"]: {
cyclicDependents: /.+\/node_modules\/expo\/AppEntry\.js$/,
globalScopeFilter: {
'react-native-animated-charts': {
exceptions: ['my-package'], // optional
},
},
},
},
});
return anisotropic({ src, filename, options: opts });
};
Note: Here we use
deepmerge
to safely propagate received transform options from the preceding step in the bundler chain.
Inside customTransformOptions
, we declare a child object under the key metro-plugin-anisotropic-transform
which can be used to specify configuration arguments. In this example, we've defined a simple RegExp
to permit a cyclic dependency on /node_modules/expo/AppEntry.js
, which is required for Expo projects. In this instance, any other dependencies in the node_modules
directory which does not match this pattern will cause the bundler to fail.
Note: In production environments, it is imported to declare the full system path to the resolved dependency. This is because bad actors could exploit a simple directory structure to create a technically allowable path, i.e.
node_modules/evil-dangerous-package/node_modules/expo/AppEntry.js
.
Additionally, we define the globalScopeFilter
property. This is used to escape any library dependencies from asserting a dependence upon another library in your node_modules
directory. In this example, the metro bundler will terminate bundling if an included dependency asserts a dependence upon react-native-animated-charts
.
Finally, you'll need to update your metro.config.js
to invoke the metro.transform.js
during the bundle phase:
module.exports = {
+ transformer: {
+ babelTransformerPath: require.resolve("./metro.transform.js"),
+ getTransformOptions: () => ({ inlineRequires: false, allowOptionalDependencies: false }),
+ },
};
If you're using Expo, you'll also need to update your app.json
in addition to updating your metro.config.js
:
{
"expo": {
"packagerOpts": {
+ "transformer": "./metro.transform.js"
}
}
}
And that's everything! Now whenever you rebundle your application, your application source will be passed through the anisotropic transform and any cyclic dependencies in your project will be detected.
⚠️ Important! Whenever you apply any changes to your bundler configuration, you must clear the cache by callingreact-native start --reset-cache
.
babel-plugin-anisotropic-transform
defaults to the following configuration:
{
madge: {
includeNpm: true,
fileExtensions: ["js", "jsx", "ts", "tsx"],
detectiveOptions: {
es6: {
mixedImports: true
}
},
},
cyclicDependents: /a^/, /* by default, do not permit anything */
globalScopeFilter: {}, /* no filtering applied */
resolve: ({ type, referrer, ...extras }) => {
if (type === 'cyclicDependents') {
const {target} = extras;
throw new Error(`${name}: Detected a cyclic dependency. (${referrer} => ${target})`);
} else if (type === 'globalScopeFilter') {
const {globalScope} = extras;
throw new Error(`${name}: Detected disallowed dependence upon ${globalScope.map(e => `"${e}"`).join(',')}. (${referrer})`);
}
throw new Error(`Encountered unimplemented type, "${type}".`);
},
}
Controls invocation of the madge
tool, which is used to interrogate module relationships. See Madge Configuration.
A RegExp
which determines which cyclic dependencies are permitted to exist. By default, none are allowed.
An object whose keys map to dependencies in your node_modules
directory which are not permitted to be included by other dependencies. This is useful for preventing libraries from executing potentially priviledged functionality exported by another module.
- At this time, only the keys of this property are consumed by the transform. For future proofing, consider using an empty object
{}
.
A function called when the anisotropic platform detects a sensitive relationship. By default, this is configured to throw
and prevent the bundler from continuing.