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 ce7d418
Show file tree
Hide file tree
Showing 8 changed files with 257 additions and 2 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
87 changes: 87 additions & 0 deletions incubator/yarn-plugin-dynamic-extensions/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# @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);
});
},
},
};
};
43 changes: 43 additions & 0 deletions incubator/yarn-plugin-dynamic-extensions/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"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",
"eslint": "^9.0.0",
"prettier": "^3.0.0",
"typescript": "^5.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"]
}
2 changes: 1 addition & 1 deletion packages/template/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"directory": "packages/template"
},
"engines": {
"node": ">=16.17"
"node": ">=18.12"
},
"scripts": {
"build": "rnx-kit-scripts build",
Expand Down
16 changes: 15 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4838,6 +4838,20 @@ __metadata:
languageName: unknown
linkType: soft

"@rnx-kit/yarn-plugin-dynamic-extensions@workspace:incubator/yarn-plugin-dynamic-extensions":
version: 0.0.0-use.local
resolution: "@rnx-kit/yarn-plugin-dynamic-extensions@workspace:incubator/yarn-plugin-dynamic-extensions"
dependencies:
"@rnx-kit/eslint-config": "npm:*"
"@rnx-kit/scripts": "npm:*"
"@rnx-kit/tsconfig": "npm:*"
"@yarnpkg/core": "npm:^4.0.0"
eslint: "npm:^9.0.0"
prettier: "npm:^3.0.0"
typescript: "npm:^5.0.0"
languageName: unknown
linkType: soft

"@shikijs/engine-oniguruma@npm:^1.25.1":
version: 1.26.1
resolution: "@shikijs/engine-oniguruma@npm:1.26.1"
Expand Down Expand Up @@ -5564,7 +5578,7 @@ __metadata:
languageName: node
linkType: hard

"@yarnpkg/core@npm:^4.1.6, @yarnpkg/core@npm:^4.2.0":
"@yarnpkg/core@npm:^4.0.0, @yarnpkg/core@npm:^4.1.6, @yarnpkg/core@npm:^4.2.0":
version: 4.2.0
resolution: "@yarnpkg/core@npm:4.2.0"
dependencies:
Expand Down

0 comments on commit ce7d418

Please sign in to comment.