Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .yarnrc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ compressionLevel: 0

dynamicPackageExtensions: ./scripts/dependencies.config.js

dynamicPackageOverrides:
- id: nx

enableGlobalCache: false

enableScripts: false
Expand Down Expand Up @@ -72,6 +75,7 @@ plugins:
- path: .yarn/plugins/@yarnpkg/plugin-compat.cjs
spec: "@yarnpkg/plugin-compat"
- path: incubator/yarn-plugin-dynamic-extensions/index.js
- path: incubator/yarn-plugin-dynamic-overrides/index.js
- path: incubator/yarn-plugin-external-workspaces/dist/external-workspaces.cjs
- path: incubator/yarn-plugin-ignore/dist/yarn-plugin-ignore.cjs
- path: incubator/yarn-plugin-install-to/dist/yarn-plugin-install-to.cjs
Expand Down
61 changes: 61 additions & 0 deletions incubator/yarn-plugin-dynamic-overrides/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# @rnx-kit/yarn-plugin-dynamic-overrides

[![Build](https://github.com/microsoft/rnx-kit/actions/workflows/build.yml/badge.svg)](https://github.com/microsoft/rnx-kit/actions/workflows/build.yml)
[![npm version](https://img.shields.io/npm/v/@rnx-kit/yarn-plugin-dynamic-overrides)](https://www.npmjs.com/package/@rnx-kit/yarn-plugin-dynamic-overrides)

🚧🚧🚧🚧🚧🚧🚧🚧🚧🚧🚧

### THIS TOOL IS EXPERIMENTAL — USE WITH CAUTION

🚧🚧🚧🚧🚧🚧🚧🚧🚧🚧🚧

This is a Yarn plugin that lets you override a package's dependencies to allow
greater version ranges than the package declares.

## Motivation

A number of packages use explicit version numbers instead of version ranges,
forcing package managers to install multiple copies of a dependency even though
a more recent version is still compatible.

This plugin attempts to address to by replacing the explicit version with a
version range. This is useful for deduping and allows for updating dependencies
to a more recent version.

## Installation

```sh
yarn plugin import https://raw.githubusercontent.com/microsoft/rnx-kit/main/incubator/yarn-plugin-dynamic-overrides/index.js
```

## Usage

List the packages that you want to override in `.yarnrc.yml`. For example, to
override dependencies of `@appium/base-driver`:

```yml
dynamicPackageOverrides:
- id: "@appium/base-driver"
```

Next time you run `yarn install`, its dependencies will be overridden:

```
"@appium/support": "npm:7.2.5"
"@appium/types": "npm:1.5.1"
async-lock: "npm:1.4.1" -> "npm:^1.4.1"
asyncbox: "npm:6.3.0" -> "npm:^6.3.0"
axios: "npm:1.18.0" -> "npm:^1.18.0"
```

Note that `@appium` packages are untouched. The plugin will try to detect
inter-dependencies and leave them alone.

If you want to use a different specifier than `^`, you can configure it like
below:

```yml
dynamicPackageOverrides:
- id: "@appium/base-driver"
specifier: "~"
Comment thread
tido64 marked this conversation as resolved.
```
118 changes: 118 additions & 0 deletions incubator/yarn-plugin-dynamic-overrides/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// @ts-check

/**
* @import { Descriptor, Hooks, Ident, Plugin } from "@yarnpkg/core";
*
* @typedef {Ident & { specifier: string; }} ExtendedIdent
*/

const DEFAULT_SPECIFIER = "^";
const DYNAMIC_OVERRIDES_KEY = "dynamicPackageOverrides";

/**
* @param {Descriptor} dependency
* @param {Ident=} locator
* @returns {locator is Ident}
*/
function matches(dependency, locator) {
if (!locator) {
return false;
}

// appium -> @appium/base-plugin
if (locator.name === dependency.scope) {
return false;
}

// @appium/base-plugin -> @appium/base-driver
if (locator.scope && locator.scope === dependency.scope) {
return false;
}

return true;
}

// This module *must* be CommonJS because `actions/setup-node` (and probably
// other GitHub actions) does not support ESM. Yarn itself does.
exports.name = "@rnx-kit/yarn-plugin-dynamic-overrides";

/** @type {(require: NodeJS.Require) => Plugin<Hooks>} */
exports.factory = (require) => {
const { SettingsType, structUtils } = require("@yarnpkg/core");

/**
* @param {Map<string, string>} entry
* @returns {[string, ExtendedIdent]}
*/
function toKeyValuePair(entry) {
const id = entry.get("id");
if (!id) {
throw new Error(`"${DYNAMIC_OVERRIDES_KEY}" expects an id`);
}
const ident = structUtils.parseIdent(id);
return [
ident.identHash,
{
...ident,
specifier: entry.get("specifier") ?? DEFAULT_SPECIFIER,
},
];
}

/** @type {Plugin<Hooks>["configuration"] & Record<string, unknown>} */
const configuration = {};
configuration[DYNAMIC_OVERRIDES_KEY] = {
description: "Packages whose dependencies are overridden",
type: SettingsType.SHAPE,
isArray: true,
properties: {
id: {
type: SettingsType.STRING,
},
specifier: {
type: SettingsType.STRING,
default: DEFAULT_SPECIFIER,
},
},
};

/** @type {Map<string, ExtendedIdent>} */
let overrides;

return {
configuration,
hooks: {
reduceDependency: async (
dependency,
project,
locator,
_initialDependency,
_extra
) => {
if (!overrides) {
const config = project.configuration.get(DYNAMIC_OVERRIDES_KEY);
if (!Array.isArray(config)) {
throw new Error(
`Expected "${DYNAMIC_OVERRIDES_KEY}" to be an array`
);
}
overrides = new Map(config.map(toKeyValuePair));
}

const ident = overrides.get(locator.identHash);
if (matches(dependency, ident)) {
const { protocol, selector } = structUtils.parseRange(
dependency.range
);
if (protocol === "npm:" && /^\d/.test(selector)) {
return structUtils.makeDescriptor(
dependency,
`${protocol}${ident.specifier}${selector}`
);
}
}
return dependency;
},
},
};
};
40 changes: 40 additions & 0 deletions incubator/yarn-plugin-dynamic-overrides/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"name": "@rnx-kit/yarn-plugin-dynamic-overrides",
"version": "0.0.1",
"private": true,
"description": "EXPERIMENTAL - USE WITH CAUTION - yarn-plugin-dynamic-overrides",
"homepage": "https://github.com/microsoft/rnx-kit/tree/main/incubator/yarn-plugin-dynamic-overrides#readme",
"license": "MIT",
"author": {
"name": "Microsoft Open Source",
"email": "microsoftopensource@users.noreply.github.com"
},
"repository": {
"type": "git",
"url": "https://github.com/microsoft/rnx-kit",
"directory": "incubator/yarn-plugin-dynamic-overrides"
},
"files": [
"index.js"
],
"main": "index.js",
"exports": {
".": "./index.js"
},
"scripts": {
"build": "rnx-kit-scripts build",
"format": "rnx-kit-scripts format",
"lint": "rnx-kit-scripts lint",
"test": "rnx-kit-scripts test"
},
"devDependencies": {
"@rnx-kit/scripts": "*",
"@rnx-kit/tsconfig": "*",
"@yarnpkg/core": "^4.0.0"
},
"engines": {
"node": ">=20.18",
"yarn": ">=4.0"
},
"experimental": true
}
7 changes: 7 additions & 0 deletions incubator/yarn-plugin-dynamic-overrides/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"extends": "@rnx-kit/tsconfig/tsconfig.nodenext.json",
"compilerOptions": {
"noEmit": true
},
"include": ["index.js"]
}
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@
"@react-native-windows/telemetry/@xmldom/xmldom": "^0.8.13",
"@rnx-kit/react-native-host": "workspace:*",
"node-gyp": "ignore:",
"nx/minimatch": "^10.2.4",
"react-native-windows/@react-native-community/cli": "^20.0.0",
"react-native-windows/@react-native-community/cli-platform-android": "^20.0.0",
"react-native-windows/@react-native-community/cli-platform-ios": "^20.0.0"
Expand Down
Loading
Loading