Skip to content

Latest commit

 

History

History
145 lines (110 loc) · 7.35 KB

File metadata and controls

145 lines (110 loc) · 7.35 KB

metro-plugin-anisotropic-transform

⚛️ 🧲 Intercept and mutate runtime dependency resolution.

💪 Motivation

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.

🤔 How does it work?

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 requires are not possible in React Native 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.
  • 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:

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

📝 2. Creating a metro.transform.js

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.

3. ♾️ Applying the Transform

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 calling react-native start --reset-cache.

⚙️ Options

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

madge

Controls invocation of the madge tool, which is used to interrogate module relationships. See Madge Configuration.

cyclicDependents

A 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