rnx-kit/packages/metro-plugin-duplicates-che...
renovate[bot] 57013d2191
fix(deps): update eslint (major) (#3394)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Tommy Nguyen <4123478+tido64@users.noreply.github.com>
2024-10-17 14:41:52 +00:00
..
src fix: bump minimum Node version to 16.17 (#3339) 2024-09-10 18:02:14 +00:00
test fix(cli): minor cleanups in preparation for bigger changes (#3263) 2024-08-07 09:31:38 +02:00
CHANGELOG.md RELEASING: Releasing 44 package(s) (#3342) 2024-09-11 08:59:50 +02:00
README.md docs(metro-plugin-duplicates-checker): add section about deduping (#2941) 2024-02-05 17:13:46 +01:00
eslint.config.js chore: migrate to ESLint flat config (#2782) 2023-11-03 14:02:18 +01:00
package.json fix(deps): update eslint (major) (#3394) 2024-10-17 14:41:52 +00:00
tsconfig.json feat(tsconfig): base TypeScript configs for working with Node (#2886) 2023-12-18 12:05:07 +01:00

README.md

@rnx-kit/metro-plugin-duplicates-checker

Build npm version

@rnx-kit/metro-plugin-duplicates-checker checks for duplicate packages in your bundle.

Usage

There are several ways to use this package.

The recommended way is to add it as a plugin in your metro.config.js using @rnx-kit/metro-serializer:

 const { makeMetroConfig } = require("@rnx-kit/metro-config");
+const {
+  DuplicateDependencies,
+} = require("@rnx-kit/metro-plugin-duplicates-checker");
+const { MetroSerializer } = require("@rnx-kit/metro-serializer");

 module.exports = makeMetroConfig({
   serializer: {
+    customSerializer: MetroSerializer([DuplicateDependencies()]),
   },
 });

You can also check for duplicate packages after a bundle is created:

const {
  checkForDuplicatePackagesInFile,
} = require("@rnx-kit/metro-plugin-duplicates-checker");

checkForDuplicatePackagesInFile(pathToSourceMapFile, {
  ignoredModules: [],
  bannedModules: [],
});

If you have a source map object, you can pass that directly to checkForDuplicatePackages():

const {
  checkForDuplicatePackages,
} = require("@rnx-kit/metro-plugin-duplicates-checker");

checkForDuplicatePackages(mySourceMap, {
  ignoredModules: [],
  bannedModules: [],
});

Options

Key Type Default Description
bannedModules string[] [] List of modules that are banned.
ignoredModules string[] [] List of modules that can be ignored.
throwOnError boolean true Whether to throw when duplicates are found.

Resolving Duplicates

So you have duplicates in your bundle, now what? Depending on your specific needs, we have several options.

Let's use a specific example:

error @fluentui-react-native/text (found 2 copies)
warn   0.21.14 /~/node_modules/@fluentui-react-native/text
warn   0.22.7 /~/node_modules/@fluentui-react-native/link/node_modules/@fluentui-react-native/text

This one occurs because @fluentui-react-native/link declares a dependency on @fluentui-react-native/text using a wide version range, >=0.21.14 <1.0.0.

Our project depends on @fluentui-react-native/text@^0.21.14, but package managers will typically still try to resolve >=0.21.14 <1.0.0 instead of using the existing resolution. This results in the duplicate error we see above.

From here, we have several options:

Manual Dedupe

This method is error-prone, especially if you have a lot of duplicates. It works if you have very few entries. It varies a lot depending on the package manager you're using.

The project in our example uses Yarn Classic: Open yarn.lock and look for @fluentui-react-native/text:

"@fluentui-react-native/text@0.21.14", "@fluentui-react-native/text@^0.21.14":
  version "0.21.14"
  resolved "https://registry.yarnpkg.com/@fluentui-react-native/text/-/text-0.21.14.tgz#04918a9558770ec551cbdac87ca1534bfccaeffb"
  integrity sha1-BJGKlVh3DsVRy9rIfKFTS/zK7/s=
  dependencies:
    "@fluentui-react-native/adapters" ">=0.11.3 <1.0.0"
    "@fluentui-react-native/framework" "0.11.10"
    "@fluentui-react-native/interactive-hooks" ">=0.24.12 <1.0.0"
    "@fluentui-react-native/theme-tokens" ">=0.25.4 <1.0.0"
    "@fluentui-react-native/tokens" ">=0.21.6 <1.0.0"
    "@uifabricshared/foundation-compose" "^1.14.12"
    tslib "^2.3.1"

"@fluentui-react-native/text@>=0.21.14 <1.0.0":
  version "0.22.7"
  resolved "https://registry.yarnpkg.com/@fluentui-react-native/text/-/text-0.22.7.tgz#bd11768d3cd69337ad2ec4be76ee88d6749ca24f"
  integrity sha1-vRF2jTzWkzetLsS+du6I1nScok8=
  dependencies:
    "@fluentui-react-native/adapters" ">=0.12.0 <1.0.0"
    "@fluentui-react-native/framework" "0.13.6"
    "@fluentui-react-native/interactive-hooks" ">=0.25.7 <1.0.0"
    "@fluentui-react-native/theme-tokens" ">=0.26.5 <1.0.0"
    "@fluentui-react-native/tokens" ">=0.22.5 <1.0.0"
    "@uifabricshared/foundation-compose" "^1.14.20"
    tslib "^2.3.1"

We can see there are two entries, one being resolved to 0.21.14 and the other to 0.22.7. In this case, since we want to keep 0.21.x and the version ranges just happens to be satisfied by this. We can merge the two entries like below:

"@fluentui-react-native/text@0.21.14", "@fluentui-react-native/text@>=0.21.14 <1.0.0", "@fluentui-react-native/text@^0.21.14":
  version "0.21.14"
  resolved "https://registry.yarnpkg.com/@fluentui-react-native/text/-/text-0.21.14.tgz#04918a9558770ec551cbdac87ca1534bfccaeffb"
  integrity sha1-BJGKlVh3DsVRy9rIfKFTS/zK7/s=
  dependencies:
    "@fluentui-react-native/adapters" ">=0.11.3 <1.0.0"
    "@fluentui-react-native/framework" "0.11.10"
    "@fluentui-react-native/interactive-hooks" ">=0.24.12 <1.0.0"
    "@fluentui-react-native/theme-tokens" ">=0.25.4 <1.0.0"
    "@fluentui-react-native/tokens" ">=0.21.6 <1.0.0"
    "@uifabricshared/foundation-compose" "^1.14.12"
    tslib "^2.3.1"

Now we should only have one copy of @fluentui-react-native/text.

Our example is relatively simple. Sometimes you have to go further up the dependency chain and dedupe dependees.

Fortunately, we can use tools in most cases.

Using Tools

If you're using Yarn Classic, there is a tool, yarn-deduplicate, for deduplicating everything in yarn.lock. You can run it like so:

npx yarn-deduplicate

By default, it will try to dedupe to the highest version. In our example, however, we want to keep using 0.21.x. We should also limit the number of packages that get deduped to make it easier to review later. yarn-deduplicate provides many options, but we'll be using the fewer strategy and the --scopes flag to only target @fluentui-react-native packages:

npx yarn-deduplicate --strategy fewer --scopes @fluentui-react-native

You can read more about yarn-deduplicate and available options here: https://github.com/scinos/yarn-deduplicate#readme

If you're using modern Yarn or other package managers, check out their built-in dedupe command:

Help Metro Resolve Correct Version

If, for some reason, you cannot dedupe the package, you can instead configure Metro to resolve the correct version by telling it which version to use (with resolver.extraNodeModules) and blocking every other copy (with resolver.blockList):

module.exports = {
  resolver: {
    extraNodeModules: {
      "@fluentui-react-native/text":
        "/~/my-project/node_modules/@fluentui-react-native/text",
    },
    blockList: [
      /(?<!\/~\/my-project)\/node_modules\/@fluentui-react-native\/text\/.*/,
    ],
  },
};

You don't have to write these entries manually. We have helper functions in @rnx-kit/metro-config for generating them dynamically.

Last Resort: Force Resolution

As a last resort, if everything else fails, you can manually override package resolutions. We don't recommend this solution because it forces a single version in all workspaces. For example, if you have two projects that have nothing to do with each other, they will both be forced to use the same version even though they could be using different versions. It also adds maintenance overhead because you will have to manually update the version as dependees update theirs.

If you've decided that you have no other options, you can find the appropriate documentation for your package manager below: