Skip to content

Commit

Permalink
feat: add Yarn plugin enabling dynamic package extensions
Browse files Browse the repository at this point in the history
  • Loading branch information
tido64 committed Feb 27, 2025
1 parent 0bc1d47 commit f5b21d1
Show file tree
Hide file tree
Showing 60 changed files with 362 additions and 383 deletions.
5 changes: 5 additions & 0 deletions .changeset/slow-poets-itch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@rnx-kit/yarn-plugin-dynamic-extensions": minor
---

Added experimental Yarn plugin to enable dynamic package extensions
2 changes: 2 additions & 0 deletions .yarnrc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,7 @@ packageExtensions:
plugins:
- path: .yarn/plugins/@yarnpkg/plugin-compat.cjs
spec: "@yarnpkg/plugin-compat"
- path: incubator/yarn-plugin-dynamic-extensions/index.js
dynamicPackageExtensions: ./scripts/dependencies.config.js
tsEnableAutoTypes: false
yarnPath: .yarn/releases/yarn-4.6.0.cjs
1 change: 1 addition & 0 deletions docsite/.yarnrc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@ logFilters:
level: discard
nodeLinker: node-modules
npmRegistryServer: "https://registry.npmjs.org"
dynamicPackageExtensions: false
tsEnableAutoTypes: false
yarnPath: ../.yarn/releases/yarn-4.6.0.cjs
5 changes: 1 addition & 4 deletions incubator/@react-native-webapis/battery-status/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,8 @@
"@rnx-kit/scripts": "*",
"@rnx-kit/tsconfig": "*",
"@types/node": "^20.0.0",
"eslint": "^9.0.0",
"prettier": "^3.0.0",
"react": "18.3.1",
"react-native": "^0.76.0",
"typescript": "^5.0.0"
"react-native": "^0.76.0"
},
"engines": {
"node": ">=16.17"
Expand Down
6 changes: 1 addition & 5 deletions incubator/@react-native-webapis/web-storage/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,8 @@
"@rnx-kit/eslint-config": "*",
"@rnx-kit/scripts": "*",
"@rnx-kit/tsconfig": "*",
"eslint": "^9.0.0",
"jest": "^29.2.1",
"prettier": "^3.0.0",
"react": "18.3.1",
"react-native": "^0.76.0",
"typescript": "^5.0.0"
"react-native": "^0.76.0"
},
"engines": {
"node": ">=16.17"
Expand Down
5 changes: 1 addition & 4 deletions incubator/build-plugin-firebase/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,7 @@
"@rnx-kit/build": "*",
"@rnx-kit/eslint-config": "*",
"@rnx-kit/scripts": "*",
"@rnx-kit/tsconfig": "*",
"eslint": "^9.0.0",
"prettier": "^3.0.0",
"typescript": "^5.0.0"
"@rnx-kit/tsconfig": "*"
},
"engines": {
"node": ">=18.12"
Expand Down
5 changes: 1 addition & 4 deletions incubator/build/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,7 @@
"@types/git-url-parse": "^9.0.0",
"@types/node": "^20.0.0",
"@types/qrcode": "^1.4.2",
"@types/yargs": "^16.0.0",
"eslint": "^9.0.0",
"prettier": "^3.0.0",
"typescript": "^5.0.0"
"@types/yargs": "^16.0.0"
},
"engines": {
"node": ">=18.12"
Expand Down
5 changes: 1 addition & 4 deletions incubator/commitlint-lite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,7 @@
"@rnx-kit/eslint-config": "*",
"@rnx-kit/scripts": "*",
"@rnx-kit/tsconfig": "*",
"@types/node": "^20.0.0",
"eslint": "^9.0.0",
"prettier": "^3.0.0",
"typescript": "^5.0.0"
"@types/node": "^20.0.0"
},
"engines": {
"node": ">=16.17"
Expand Down
6 changes: 1 addition & 5 deletions incubator/esbuild-bundle-analyzer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,7 @@
"@rnx-kit/tsconfig": "*",
"@types/jest": "^29.2.1",
"@types/node": "^20.0.0",
"@types/yargs": "^16.0.0",
"eslint": "^9.0.0",
"jest": "^29.2.1",
"prettier": "^3.0.0",
"typescript": "^5.0.0"
"@types/yargs": "^16.0.0"
},
"engines": {
"node": ">=16.17"
Expand Down
6 changes: 1 addition & 5 deletions incubator/patcher-rnmacos/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,7 @@
"@rnx-kit/tsconfig": "*",
"@types/fs-extra": "^9.0.0",
"@types/istextorbinary": "^2.3.0",
"@types/node": "^20.0.0",
"eslint": "^9.0.0",
"jest": "^29.2.1",
"prettier": "^3.0.0",
"typescript": "^5.0.0"
"@types/node": "^20.0.0"
},
"dependencies": {
"commander": "^4.1.1",
Expand Down
5 changes: 1 addition & 4 deletions incubator/polyfills/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,7 @@
"@types/babel__helper-plugin-utils": "^7.0.0",
"@types/babel__template": "^7.0.0",
"@types/node": "^20.0.0",
"eslint": "^9.0.0",
"metro-config": "^0.81.0",
"prettier": "^3.0.0",
"typescript": "^5.0.0"
"metro-config": "^0.81.0"
},
"engines": {
"node": ">=16.17"
Expand Down
6 changes: 1 addition & 5 deletions incubator/react-native-error-trace-decorator/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,7 @@
"@rnx-kit/scripts": "*",
"@rnx-kit/tsconfig": "*",
"@types/node": "^20.0.0",
"@types/yargs": "^16.0.0",
"eslint": "^9.0.0",
"jest": "^29.2.1",
"prettier": "^3.0.0",
"typescript": "^5.0.0"
"@types/yargs": "^16.0.0"
},
"dependencies": {
"@rnx-kit/console": "^2.0.0",
Expand Down
4 changes: 0 additions & 4 deletions incubator/rn-changelog-generator/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,8 @@
"@types/node": "^20.0.0",
"chalk": "^4.1.0",
"deepmerge": "^4.2.2",
"eslint": "^9.0.0",
"fast-levenshtein": "^3.0.0",
"jest": "^29.2.1",
"p-limit": "^3.1.0",
"prettier": "^3.0.0",
"typescript": "^5.0.0",
"yargs": "^16.0.0"
},
"engines": {
Expand Down
6 changes: 1 addition & 5 deletions incubator/tools-typescript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,7 @@
"@rnx-kit/eslint-config": "*",
"@rnx-kit/jest-preset": "*",
"@rnx-kit/scripts": "*",
"@rnx-kit/tsconfig": "*",
"eslint": "^9.0.0",
"jest": "^29.2.1",
"prettier": "^3.0.0",
"typescript": "^5.0.0"
"@rnx-kit/tsconfig": "*"
},
"peerDependencies": {
"typescript": ">=4.7.0"
Expand Down
88 changes: 88 additions & 0 deletions incubator/yarn-plugin-dynamic-extensions/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# @rnx-kit/yarn-plugin-dynamic-extensions

[![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-extensions)](https://www.npmjs.com/package/@rnx-kit/yarn-plugin-dynamic-extensions)

🚧🚧🚧🚧🚧🚧🚧🚧🚧🚧🚧

### THIS TOOL IS EXPERIMENTAL — USE WITH CAUTION

🚧🚧🚧🚧🚧🚧🚧🚧🚧🚧🚧

This is a Yarn plugin that lets you extend the package definitions of your
dependencies, similar to [`packageExtensions`][], but dynamically.

## Motivation

Making sure a large number of packages are using the same version of
dependencies, like `eslint` or `typescript`, can involve a lot of manual work.
It is easy to make mistakes, especially if these packages span across multiple
repositories.

This plugin allows you to manage all dependencies across multiple repositories
from a central location.

## Installation

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

## Usage

Create a module that will return package extensions. In the following example,
we create a module that adds `typescript` to all packages:

```js
/**
* @param {Object} workspace The package currently being processed
* @param {string} workspace.cwd Path of the current package
* @param {Object} workspace.manifest The content of `package.json`
* @returns {{
* dependencies?: Record<string, string>;
* peerDependencies?: Record<string, string>;
* peerDependenciesMeta?: Record<string, { optional?: boolean }>;
* }}
*/
export default function ({ cwd, manifest }) {
return {
dependencies: {
typescript: "^5.0.0",
},
};
}
```

The function will receive context on the currently processed package, and is
expected to return a map similar to the one for [`packageExtensions`][].

For a more complete example, take a look at how we use it in
[`rnx-kit`](https://github.com/microsoft/rnx-kit/blob/main/scripts/dependencies.config.js).

Add the configuration in your `.yarnrc.yml`:

```yaml
dynamicPackageExtensions: ./my-dependencies.config.js
```
If you run `yarn install` now, Yarn will install `typescript` in all your
packages. To verify, try running `tsc`:

```
% yarn tsc --version
Version 5.7.3
```

Other Yarn commands will also work as if you had installed dependencies
explicitly as you normally would. For example, `yarn why`:

```
% yarn why typescript
└─ @rnx-kit/yarn-plugin-dynamic-extensions@workspace:incubator/yarn-plugin-dynamic-extensions
└─ typescript@npm:5.7.3 (via npm:^5.0.0)
```

<!-- References -->

[`packageExtensions`]:
https://yarnpkg.com/configuration/yarnrc#packageExtensions
1 change: 1 addition & 0 deletions incubator/yarn-plugin-dynamic-extensions/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require("@rnx-kit/eslint-config");
93 changes: 93 additions & 0 deletions incubator/yarn-plugin-dynamic-extensions/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// @ts-check

/**
* @import { Configuration, Hooks, Manifest, PackageExtensionData, Plugin } from "@yarnpkg/core";
* @typedef {{ cwd: string; manifest: Manifest["raw"]; }} Workspace;
*/

const DYNAMIC_PACKAGE_EXTENSIONS_KEY = "dynamicPackageExtensions";

// 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-extensions";

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

/**
* @param {Configuration} configuration
* @param {string} projectRoot
* @returns {Promise<((ws: Workspace) => PackageExtensionData | undefined) | void>}
*/
async function loadUserExtensions(configuration, projectRoot) {
const packageExtensions = configuration.get(DYNAMIC_PACKAGE_EXTENSIONS_KEY);
if (
typeof packageExtensions !== "string" ||
packageExtensions === "false"
) {
return;
}

const path = require("node:path");
const { pathToFileURL } = require("node:url");

// On Windows, import paths must include the `file:` protocol.
const url = pathToFileURL(path.resolve(projectRoot, packageExtensions));
const external = await import(url.toString());
return external?.default ?? external;
}

/** @type {Plugin<Hooks>["configuration"] & Record<string, unknown>} */
const configuration = {};
configuration[DYNAMIC_PACKAGE_EXTENSIONS_KEY] = {
description: "Path to module providing package extensions",
type: SettingsType.STRING,
};

return {
configuration,
hooks: {
registerPackageExtensions: async (
configuration,
registerPackageExtension
) => {
const { projectCwd } = configuration;
if (!projectCwd) {
return;
}

const { workspace } = await Project.find(configuration, projectCwd);
if (!workspace) {
return;
}

// @ts-expect-error Cannot find module or its corresponding type declarations
const { npath } = require("@yarnpkg/fslib");

const root = npath.fromPortablePath(projectCwd);
const getUserExtensions = await loadUserExtensions(configuration, root);
if (!getUserExtensions) {
return;
}

workspace.project.workspacesByCwd.forEach(({ cwd, manifest }) => {
const { name, version, raw } = manifest;
if (!name || !version) {
return;
}

/** @type {Workspace} */
const workspace = { cwd: npath.fromPortablePath(cwd), manifest: raw };
const data = getUserExtensions(workspace);
if (!data) {
return;
}

const descriptor = structUtils.makeDescriptor(name, version);
registerPackageExtension(descriptor, data);
});
},
},
};
};
40 changes: 40 additions & 0 deletions incubator/yarn-plugin-dynamic-extensions/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"name": "@rnx-kit/yarn-plugin-dynamic-extensions",
"version": "0.0.1",
"description": "EXPERIMENTAL - USE WITH CAUTION - yarn-plugin-dynamic-extensions",
"homepage": "https://github.com/microsoft/rnx-kit/tree/main/incubator/yarn-plugin-dynamic-extensions#readme",
"license": "MIT",
"author": {
"name": "Microsoft Open Source",
"email": "[email protected]"
},
"files": [
"index.js"
],
"main": "index.js",
"exports": {
".": "./index.js"
},
"repository": {
"type": "git",
"url": "https://github.com/microsoft/rnx-kit",
"directory": "incubator/yarn-plugin-dynamic-extensions"
},
"engines": {
"node": ">=18.12",
"yarn": ">=4.0"
},
"scripts": {
"build": "rnx-kit-scripts build",
"format": "rnx-kit-scripts format",
"lint": "rnx-kit-scripts lint",
"test": "rnx-kit-scripts test"
},
"devDependencies": {
"@rnx-kit/eslint-config": "*",
"@rnx-kit/scripts": "*",
"@rnx-kit/tsconfig": "*",
"@yarnpkg/core": "^4.0.0"
},
"experimental": true
}
12 changes: 12 additions & 0 deletions incubator/yarn-plugin-dynamic-extensions/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"extends": "@rnx-kit/tsconfig/tsconfig.esm.json",
"compilerOptions": {
"target": "esnext",
"module": "nodenext",
"moduleResolution": "nodenext",
"allowImportingTsExtensions": true,
"rewriteRelativeImportExtensions": true,
"noEmit": true
},
"include": ["index.js"]
}
Loading

0 comments on commit f5b21d1

Please sign in to comment.