An open API service indexing awesome lists of open source software.

https://github.com/rainbow-me/metro-plugin-anisotropic-transform

βš›οΈ 🧲 Intercept and mutate runtime dependency resolution.
https://github.com/rainbow-me/metro-plugin-anisotropic-transform

metro react-native

Last synced: 3 months ago
JSON representation

βš›οΈ 🧲 Intercept and mutate runtime dependency resolution.

Awesome Lists containing this project

README

          

# metro-plugin-anisotropic-transform
βš›οΈ 🧲 Intercept and mutate runtime dependency resolution.

## πŸ’ͺ Motivation
[`metro-plugin-anisotropic-transform`](.) is a transform plugin for [**React Native**](https://reactnative.dev)'s [**Metro Bundler**](https://github.com/facebook/metro). 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`.

### πŸ€” How does it work?
Applications built using [**React Native**](https://reactnative.dev) are forced to resolve **all** module dependencies at bundle time. This is because unlike the [**Node.js**](https://nodejs.org/en/) 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**](https://reactnative.dev) when `inlineRequires` is set to `false`. All attempts to `import` and `require`, 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](https://twitter.com/baconbrix/status/1357448848331988994?s=21), so in order to protect the transform from runtime misuse, `inlineRequires` must be set to `false`.
- 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.

## πŸ“š Guide

### πŸš€ 1. Installing

Using [**Yarn**](https://yarnpkg.com):

```sh
yarn add --dev metro-plugin-anisotropic-transform
```

### πŸ“ 2. Creating a `metro.transform.js`

We'll create our own custom [**Metro**](https://github.com/facebook/metro) transform to invoke the anisotropic transform.

```javascript
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`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp) to permit a cyclic dependency on `/node_modules/expo/AppEntry.js`, which is required for [**Expo**](https://expo.io) 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`](https://github.com/rainbow-me/react-native-animated-charts).

### 3. ♾️ Applying the Transform

Finally, you'll need to update your `metro.config.js` to invoke the `metro.transform.js` during the bundle phase:

```diff
module.exports = {
+ transformer: {
+ babelTransformerPath: require.resolve("./metro.transform.js"),
+ getTransformOptions: () => ({ inlineRequires: false, allowOptionalDependencies: false }),
+ },
};
```

If you're using [**Expo**](https://expo.io), you'll also need to update your `app.json` in addition to updating your `metro.config.js`:

```diff
{
"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 calling `react-native start --reset-cache`.

## βš™οΈ Options

[`babel-plugin-anisotropic-transform`](.) defaults to the following configuration:

```javascript
{
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}".`);
},
}
```

### `madge`
Controls invocation of the [`madge`](https://github.com/pahen/madge) tool, which is used to interrogate module relationships. See [**Madge Configuration**](https://github.com/pahen/madge#configuration).

### `cyclicDependents`
A [`RegExp`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp) which determines which cyclic dependencies are permitted to exist. By default, none are allowed.

### `globalScopeFilter`
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 `{}`.

### `resolve`
A function called when the anisotropic platform detects a sensitive relationship. By default, this is configured to `throw` and prevent the bundler from continuing.

## 🌈 License
[**MIT**](./LICENSE)