Skip to content

Commit fa47256

Browse files
authored
feat: add Yarn plugin enabling dynamic package extensions (#3510)
1 parent a6934cf commit fa47256

File tree

60 files changed

+380
-403
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+380
-403
lines changed

.changeset/slow-poets-itch.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@rnx-kit/yarn-plugin-dynamic-extensions": minor
3+
---
4+
5+
Added experimental Yarn plugin to enable dynamic package extensions

.yarnrc.yml

+2
Original file line numberDiff line numberDiff line change
@@ -41,5 +41,7 @@ packageExtensions:
4141
plugins:
4242
- path: .yarn/plugins/@yarnpkg/plugin-compat.cjs
4343
spec: "@yarnpkg/plugin-compat"
44+
- path: incubator/yarn-plugin-dynamic-extensions/index.js
45+
dynamicPackageExtensions: ./scripts/dependencies.config.js
4446
tsEnableAutoTypes: false
4547
yarnPath: .yarn/releases/yarn-4.6.0.cjs

docsite/.yarnrc.yml

+1
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,6 @@ logFilters:
1414
level: discard
1515
nodeLinker: node-modules
1616
npmRegistryServer: "https://registry.npmjs.org"
17+
dynamicPackageExtensions: false
1718
tsEnableAutoTypes: false
1819
yarnPath: ../.yarn/releases/yarn-4.6.0.cjs

incubator/@react-native-webapis/battery-status/package.json

+1-4
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,8 @@
4444
"@rnx-kit/scripts": "*",
4545
"@rnx-kit/tsconfig": "*",
4646
"@types/node": "^20.0.0",
47-
"eslint": "^9.0.0",
48-
"prettier": "^3.0.0",
4947
"react": "18.3.1",
50-
"react-native": "^0.76.0",
51-
"typescript": "^5.0.0"
48+
"react-native": "^0.76.0"
5249
},
5350
"engines": {
5451
"node": ">=16.17"

incubator/@react-native-webapis/web-storage/package.json

+1-5
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,8 @@
5858
"@rnx-kit/eslint-config": "*",
5959
"@rnx-kit/scripts": "*",
6060
"@rnx-kit/tsconfig": "*",
61-
"eslint": "^9.0.0",
62-
"jest": "^29.2.1",
63-
"prettier": "^3.0.0",
6461
"react": "18.3.1",
65-
"react-native": "^0.76.0",
66-
"typescript": "^5.0.0"
62+
"react-native": "^0.76.0"
6763
},
6864
"engines": {
6965
"node": ">=16.17"

incubator/build-plugin-firebase/package.json

+1-4
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,7 @@
3939
"@rnx-kit/build": "*",
4040
"@rnx-kit/eslint-config": "*",
4141
"@rnx-kit/scripts": "*",
42-
"@rnx-kit/tsconfig": "*",
43-
"eslint": "^9.0.0",
44-
"prettier": "^3.0.0",
45-
"typescript": "^5.0.0"
42+
"@rnx-kit/tsconfig": "*"
4643
},
4744
"engines": {
4845
"node": ">=18.12"

incubator/build/package.json

+1-4
Original file line numberDiff line numberDiff line change
@@ -97,10 +97,7 @@
9797
"@types/git-url-parse": "^9.0.0",
9898
"@types/node": "^20.0.0",
9999
"@types/qrcode": "^1.4.2",
100-
"@types/yargs": "^16.0.0",
101-
"eslint": "^9.0.0",
102-
"prettier": "^3.0.0",
103-
"typescript": "^5.0.0"
100+
"@types/yargs": "^16.0.0"
104101
},
105102
"engines": {
106103
"node": ">=18.12"

incubator/commitlint-lite/package.json

+1-4
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,7 @@
3636
"@rnx-kit/eslint-config": "*",
3737
"@rnx-kit/scripts": "*",
3838
"@rnx-kit/tsconfig": "*",
39-
"@types/node": "^20.0.0",
40-
"eslint": "^9.0.0",
41-
"prettier": "^3.0.0",
42-
"typescript": "^5.0.0"
39+
"@types/node": "^20.0.0"
4340
},
4441
"engines": {
4542
"node": ">=16.17"

incubator/esbuild-bundle-analyzer/package.json

+1-6
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,8 @@
4242
"@rnx-kit/jest-preset": "*",
4343
"@rnx-kit/scripts": "*",
4444
"@rnx-kit/tsconfig": "*",
45-
"@types/jest": "^29.2.1",
4645
"@types/node": "^20.0.0",
47-
"@types/yargs": "^16.0.0",
48-
"eslint": "^9.0.0",
49-
"jest": "^29.2.1",
50-
"prettier": "^3.0.0",
51-
"typescript": "^5.0.0"
46+
"@types/yargs": "^16.0.0"
5247
},
5348
"engines": {
5449
"node": ">=16.17"

incubator/patcher-rnmacos/package.json

+1-5
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,7 @@
3535
"@rnx-kit/tsconfig": "*",
3636
"@types/fs-extra": "^9.0.0",
3737
"@types/istextorbinary": "^2.3.0",
38-
"@types/node": "^20.0.0",
39-
"eslint": "^9.0.0",
40-
"jest": "^29.2.1",
41-
"prettier": "^3.0.0",
42-
"typescript": "^5.0.0"
38+
"@types/node": "^20.0.0"
4339
},
4440
"dependencies": {
4541
"commander": "^4.1.1",

incubator/polyfills/package.json

+1-4
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,7 @@
4848
"@types/babel__helper-plugin-utils": "^7.0.0",
4949
"@types/babel__template": "^7.0.0",
5050
"@types/node": "^20.0.0",
51-
"eslint": "^9.0.0",
52-
"metro-config": "^0.81.0",
53-
"prettier": "^3.0.0",
54-
"typescript": "^5.0.0"
51+
"metro-config": "^0.81.0"
5552
},
5653
"engines": {
5754
"node": ">=16.17"

incubator/react-native-error-trace-decorator/package.json

+1-5
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,7 @@
3232
"@rnx-kit/scripts": "*",
3333
"@rnx-kit/tsconfig": "*",
3434
"@types/node": "^20.0.0",
35-
"@types/yargs": "^16.0.0",
36-
"eslint": "^9.0.0",
37-
"jest": "^29.2.1",
38-
"prettier": "^3.0.0",
39-
"typescript": "^5.0.0"
35+
"@types/yargs": "^16.0.0"
4036
},
4137
"dependencies": {
4238
"@rnx-kit/console": "^2.0.0",

incubator/rn-changelog-generator/package.json

-4
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,8 @@
3838
"@types/node": "^20.0.0",
3939
"chalk": "^4.1.0",
4040
"deepmerge": "^4.2.2",
41-
"eslint": "^9.0.0",
4241
"fast-levenshtein": "^3.0.0",
43-
"jest": "^29.2.1",
4442
"p-limit": "^3.1.0",
45-
"prettier": "^3.0.0",
46-
"typescript": "^5.0.0",
4743
"yargs": "^16.0.0"
4844
},
4945
"engines": {

incubator/tools-typescript/package.json

+1-5
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,7 @@
3939
"@rnx-kit/eslint-config": "*",
4040
"@rnx-kit/jest-preset": "*",
4141
"@rnx-kit/scripts": "*",
42-
"@rnx-kit/tsconfig": "*",
43-
"eslint": "^9.0.0",
44-
"jest": "^29.2.1",
45-
"prettier": "^3.0.0",
46-
"typescript": "^5.0.0"
42+
"@rnx-kit/tsconfig": "*"
4743
},
4844
"peerDependencies": {
4945
"typescript": ">=4.7.0"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# @rnx-kit/yarn-plugin-dynamic-extensions
2+
3+
[![Build](https://github.com/microsoft/rnx-kit/actions/workflows/build.yml/badge.svg)](https://github.com/microsoft/rnx-kit/actions/workflows/build.yml)
4+
[![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)
5+
6+
🚧🚧🚧🚧🚧🚧🚧🚧🚧🚧🚧
7+
8+
### THIS TOOL IS EXPERIMENTAL — USE WITH CAUTION
9+
10+
🚧🚧🚧🚧🚧🚧🚧🚧🚧🚧🚧
11+
12+
This is a Yarn plugin that lets you extend the package definitions of your
13+
dependencies, similar to [`packageExtensions`][], but dynamically.
14+
15+
## Motivation
16+
17+
Making sure a large number of packages are using the same version of
18+
dependencies, like `eslint` or `typescript`, can involve a lot of manual work.
19+
It is easy to make mistakes, especially if these packages span across multiple
20+
repositories.
21+
22+
This plugin allows you to manage all dependencies across multiple repositories
23+
from a central location.
24+
25+
## Installation
26+
27+
```sh
28+
yarn plugin import https://raw.githubusercontent.com/microsoft/rnx-kit/main/incubator/yarn-plugin-dynamic-extensions/index.js
29+
```
30+
31+
## Usage
32+
33+
Create a module that will return package extensions. In the following example,
34+
we create a module that adds `typescript` to all packages:
35+
36+
```js
37+
/**
38+
* @param {Object} workspace The package currently being processed
39+
* @param {string} workspace.cwd Path of the current package
40+
* @param {Object} workspace.manifest The content of `package.json`
41+
* @returns {{
42+
* dependencies?: Record<string, string>;
43+
* peerDependencies?: Record<string, string>;
44+
* peerDependenciesMeta?: Record<string, { optional?: boolean }>;
45+
* }}
46+
*/
47+
export default function ({ cwd, manifest }) {
48+
return {
49+
dependencies: {
50+
typescript: "^5.0.0",
51+
},
52+
};
53+
}
54+
```
55+
56+
The function will receive context on the currently processed package, and is
57+
expected to return a map similar to the one for [`packageExtensions`][].
58+
59+
For a more complete example, take a look at how we use it in
60+
[`rnx-kit`](https://github.com/microsoft/rnx-kit/blob/main/scripts/dependencies.config.js).
61+
62+
Add the configuration in your `.yarnrc.yml`:
63+
64+
```yaml
65+
dynamicPackageExtensions: ./my-dependencies.config.js
66+
```
67+
68+
If you run `yarn install` now, Yarn will install `typescript` in all your
69+
packages. To verify, try running `tsc`:
70+
71+
```
72+
% yarn tsc --version
73+
Version 5.7.3
74+
```
75+
76+
Other Yarn commands will also work as if you had installed dependencies
77+
explicitly as you normally would. For example, `yarn why`:
78+
79+
```
80+
% yarn why typescript
81+
└─ @rnx-kit/yarn-plugin-dynamic-extensions@workspace:incubator/yarn-plugin-dynamic-extensions
82+
└─ typescript@npm:5.7.3 (via npm:^5.0.0)
83+
```
84+
85+
<!-- References -->
86+
87+
[`packageExtensions`]:
88+
https://yarnpkg.com/configuration/yarnrc#packageExtensions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = require("@rnx-kit/eslint-config");
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// @ts-check
2+
3+
/**
4+
* @import { Configuration, Hooks, Manifest, PackageExtensionData, Plugin } from "@yarnpkg/core";
5+
* @typedef {{ cwd: string; manifest: Manifest["raw"]; }} Workspace;
6+
*/
7+
8+
const DYNAMIC_PACKAGE_EXTENSIONS_KEY = "dynamicPackageExtensions";
9+
10+
// This module *must* be CommonJS because `actions/setup-node` (and probably
11+
// other GitHub actions) does not support ESM. Yarn itself does.
12+
exports.name = "@rnx-kit/yarn-plugin-dynamic-extensions";
13+
14+
/** @type {(require: NodeJS.Require) => Plugin<Hooks>} */
15+
exports.factory = (require) => {
16+
const { Project, SettingsType, structUtils } = require("@yarnpkg/core");
17+
18+
/**
19+
* @param {Configuration} configuration
20+
* @param {string} projectRoot
21+
* @returns {Promise<((ws: Workspace) => PackageExtensionData | undefined) | void>}
22+
*/
23+
async function loadUserExtensions(configuration, projectRoot) {
24+
const packageExtensions = configuration.get(DYNAMIC_PACKAGE_EXTENSIONS_KEY);
25+
if (
26+
typeof packageExtensions !== "string" ||
27+
packageExtensions === "false"
28+
) {
29+
return;
30+
}
31+
32+
const path = require("node:path");
33+
const { pathToFileURL } = require("node:url");
34+
35+
// On Windows, import paths must include the `file:` protocol.
36+
const url = pathToFileURL(path.resolve(projectRoot, packageExtensions));
37+
const external = await import(url.toString());
38+
return external?.default ?? external;
39+
}
40+
41+
/** @type {Plugin<Hooks>["configuration"] & Record<string, unknown>} */
42+
const configuration = {};
43+
configuration[DYNAMIC_PACKAGE_EXTENSIONS_KEY] = {
44+
description: "Path to module providing package extensions",
45+
type: SettingsType.STRING,
46+
};
47+
48+
return {
49+
configuration,
50+
hooks: {
51+
registerPackageExtensions: async (
52+
configuration,
53+
registerPackageExtension
54+
) => {
55+
const { projectCwd } = configuration;
56+
if (!projectCwd) {
57+
return;
58+
}
59+
60+
const { workspace } = await Project.find(configuration, projectCwd);
61+
if (!workspace) {
62+
return;
63+
}
64+
65+
// @ts-expect-error Cannot find module or its corresponding type declarations
66+
const { npath } = require("@yarnpkg/fslib");
67+
68+
const root = npath.fromPortablePath(projectCwd);
69+
const getUserExtensions = await loadUserExtensions(configuration, root);
70+
if (!getUserExtensions) {
71+
return;
72+
}
73+
74+
workspace.project.workspacesByCwd.forEach(({ cwd, manifest }) => {
75+
const { name, version, raw } = manifest;
76+
if (!name || !version) {
77+
return;
78+
}
79+
80+
/** @type {Workspace} */
81+
const workspace = { cwd: npath.fromPortablePath(cwd), manifest: raw };
82+
const data = getUserExtensions(workspace);
83+
if (!data) {
84+
return;
85+
}
86+
87+
const descriptor = structUtils.makeDescriptor(name, version);
88+
registerPackageExtension(descriptor, data);
89+
});
90+
},
91+
},
92+
};
93+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"name": "@rnx-kit/yarn-plugin-dynamic-extensions",
3+
"version": "0.0.1",
4+
"description": "EXPERIMENTAL - USE WITH CAUTION - yarn-plugin-dynamic-extensions",
5+
"homepage": "https://github.com/microsoft/rnx-kit/tree/main/incubator/yarn-plugin-dynamic-extensions#readme",
6+
"license": "MIT",
7+
"author": {
8+
"name": "Microsoft Open Source",
9+
"email": "[email protected]"
10+
},
11+
"files": [
12+
"index.js"
13+
],
14+
"main": "index.js",
15+
"exports": {
16+
".": "./index.js"
17+
},
18+
"repository": {
19+
"type": "git",
20+
"url": "https://github.com/microsoft/rnx-kit",
21+
"directory": "incubator/yarn-plugin-dynamic-extensions"
22+
},
23+
"engines": {
24+
"node": ">=18.12",
25+
"yarn": ">=4.0"
26+
},
27+
"scripts": {
28+
"build": "rnx-kit-scripts build",
29+
"format": "rnx-kit-scripts format",
30+
"lint": "rnx-kit-scripts lint",
31+
"test": "rnx-kit-scripts test"
32+
},
33+
"devDependencies": {
34+
"@rnx-kit/eslint-config": "*",
35+
"@rnx-kit/scripts": "*",
36+
"@rnx-kit/tsconfig": "*",
37+
"@yarnpkg/core": "^4.0.0"
38+
},
39+
"experimental": true
40+
}

0 commit comments

Comments
 (0)