diff --git a/.changeset/slow-poets-itch.md b/.changeset/slow-poets-itch.md new file mode 100644 index 000000000..a29a23b97 --- /dev/null +++ b/.changeset/slow-poets-itch.md @@ -0,0 +1,5 @@ +--- +"@rnx-kit/yarn-plugin-align-deps": minor +--- + +Added experimental Yarn plugin to simplify dependency alignment diff --git a/incubator/yarn-plugin-align-deps/README.md b/incubator/yarn-plugin-align-deps/README.md new file mode 100644 index 000000000..778e95ce9 --- /dev/null +++ b/incubator/yarn-plugin-align-deps/README.md @@ -0,0 +1,33 @@ +# @rnx-kit/yarn-plugin-align-deps + +[![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-align-deps)](https://www.npmjs.com/package/@rnx-kit/yarn-plugin-align-deps) + +🚧🚧🚧🚧🚧🚧🚧🚧🚧🚧🚧 + +### THIS TOOL IS EXPERIMENTAL — USE WITH CAUTION + +🚧🚧🚧🚧🚧🚧🚧🚧🚧🚧🚧 + +This is a sample folder to use as base for generating new packages for +`rnx-kit`. + +## Motivation + +We want new packages to follow an existing set of patterns and guidelines; via +this package, we can enforce easily allow new folders to stick to at least a +common starting point. + +## Installation + +```sh +yarn add @rnx-kit/yarn-plugin-align-deps --dev +``` + +or if you're using npm + +```sh +npm add --save-dev @rnx-kit/yarn-plugin-align-deps +``` + +## Usage diff --git a/incubator/yarn-plugin-align-deps/eslint.config.js b/incubator/yarn-plugin-align-deps/eslint.config.js new file mode 100644 index 000000000..89ed77c6f --- /dev/null +++ b/incubator/yarn-plugin-align-deps/eslint.config.js @@ -0,0 +1 @@ +module.exports = require("@rnx-kit/eslint-config"); diff --git a/incubator/yarn-plugin-align-deps/index.js b/incubator/yarn-plugin-align-deps/index.js new file mode 100644 index 000000000..cb86fd63c --- /dev/null +++ b/incubator/yarn-plugin-align-deps/index.js @@ -0,0 +1,131 @@ +// @ts-check + +/** + * @import { Hooks, Manifest, PackageExtensionData, Plugin } from "@yarnpkg/core"; + * @typedef {{ [key: string]: string | number | boolean | JSONObject | JSONObject[] | null }} JSONObject; + * @typedef {Manifest["raw"]} RawManifest; + */ + +const PLUGIN_NAME = "@rnx-kit/yarn-plugin-align-deps"; + +/** + * @param {RawManifest} manifest + * @returns {JSONObject | undefined} + */ +function getKitConfig({ "rnx-kit": rnxconfig }) { + if (!rnxconfig || typeof rnxconfig !== "object") { + return undefined; + } + + return /** @type {JSONObject} */ (rnxconfig); +} + +/** + * @param {RawManifest} manifest + * @param {string} projectRoot + * @param {NodeJS.Require} require + * @returns {Promise<((cwd: string, manifest: RawManifest) => PackageExtensionData | undefined) | void>} + */ +async function loadUserProfiles( + manifest, + projectRoot, + // @ts-expect-error `tsc` thinks `require` is unused + require +) { + const rnxconfig = getKitConfig(manifest); + if (!rnxconfig || !("profiles" in rnxconfig)) { + return; + } + + const { profiles } = rnxconfig; + if (!profiles) { + return; + } + + if (typeof profiles === "string") { + const path = require("node:path"); + + // On Windows, import paths must include the `file:` protocol and we must + // manually prefix it. `URL.pathToFileURL` incorrectly adds the disk station + // so the URL becomes `file://C:/C:/...`. + const profilesPath = path.posix.resolve(projectRoot, profiles); + const external = await import(`file://${encodeURI(profilesPath)}`); + return external?.default ?? external; + } + + if (typeof profiles === "object" && !Array.isArray(profiles)) { + return (_cwd, manifest) => { + const rnxconfig = getKitConfig(manifest); + if (!rnxconfig || !("profile" in rnxconfig)) { + return; + } + + const { profile } = rnxconfig; + if (typeof profile !== "string") { + return; + } + + const p = profiles[profile]; + if (!p || typeof p !== "object" || Array.isArray(p)) { + return; + } + + return /** @type {PackageExtensionData} */ (p); + }; + } + + console.warn( + `${PLUGIN_NAME}: invalid configuration: 'profiles' must be an object or a string` + ); +} + +// This module *must* be CommonJS because `actions/setup-node` (and probably +// others) does not support ESM. Yarn itself does. +exports.name = PLUGIN_NAME; + +/** @type {(require: NodeJS.Require) => Plugin} */ +exports.factory = (require) => ({ + hooks: { + registerPackageExtensions: async ( + configuration, + registerPackageExtension + ) => { + const { projectCwd } = configuration; + if (!projectCwd) { + return; + } + + const { Project, structUtils } = require("@yarnpkg/core"); + + const { workspace } = await Project.find(configuration, projectCwd); + if (!workspace) { + return; + } + + const { manifest, project } = workspace; + const getUserProfile = await loadUserProfiles( + manifest.raw, + projectCwd, + require + ); + if (!getUserProfile) { + return; + } + + project.workspacesByCwd.forEach(({ cwd, manifest }) => { + const { name, version, raw } = manifest; + if (!name || !version) { + return; + } + + const profile = getUserProfile(cwd, raw); + if (!profile) { + return; + } + + const descriptor = structUtils.makeDescriptor(name, version); + registerPackageExtension(descriptor, profile); + }); + }, + }, +}); diff --git a/incubator/yarn-plugin-align-deps/package.json b/incubator/yarn-plugin-align-deps/package.json new file mode 100644 index 000000000..4ba07878f --- /dev/null +++ b/incubator/yarn-plugin-align-deps/package.json @@ -0,0 +1,43 @@ +{ + "private": true, + "name": "@rnx-kit/yarn-plugin-align-deps", + "version": "0.0.1", + "description": "EXPERIMENTAL - USE WITH CAUTION - yarn-plugin-align-deps", + "homepage": "https://github.com/microsoft/rnx-kit/tree/main/incubator/yarn-plugin-align-deps#readme", + "license": "MIT", + "author": { + "name": "Microsoft Open Source", + "email": "microsoftopensource@users.noreply.github.com" + }, + "files": [ + "index.js" + ], + "main": "index.js", + "exports": { + ".": "./index.js" + }, + "repository": { + "type": "git", + "url": "https://github.com/microsoft/rnx-kit", + "directory": "incubator/yarn-plugin-align-deps" + }, + "engines": { + "node": ">=18.12" + }, + "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 +} diff --git a/incubator/yarn-plugin-align-deps/tsconfig.json b/incubator/yarn-plugin-align-deps/tsconfig.json new file mode 100644 index 000000000..8b08be4b8 --- /dev/null +++ b/incubator/yarn-plugin-align-deps/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@rnx-kit/tsconfig/tsconfig.json", + "compilerOptions": { + "noEmit": true + }, + "include": ["index.js"] +} diff --git a/packages/template/package.json b/packages/template/package.json index b10b30438..e7a1c921c 100644 --- a/packages/template/package.json +++ b/packages/template/package.json @@ -21,7 +21,7 @@ "directory": "packages/template" }, "engines": { - "node": ">=16.17" + "node": ">=18.12" }, "scripts": { "build": "rnx-kit-scripts build", diff --git a/yarn.lock b/yarn.lock index f543588fd..f06da3ea5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4838,6 +4838,20 @@ __metadata: languageName: unknown linkType: soft +"@rnx-kit/yarn-plugin-align-deps@workspace:incubator/yarn-plugin-align-deps": + version: 0.0.0-use.local + resolution: "@rnx-kit/yarn-plugin-align-deps@workspace:incubator/yarn-plugin-align-deps" + 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" @@ -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: