From 0b26dc2f43485c145ec177e032d7ad3f7053fb99 Mon Sep 17 00:00:00 2001 From: Robin Date: Fri, 14 Mar 2025 02:15:58 -0400 Subject: [PATCH] Create a plugin for linking other packages --- .gitignore | 1 + .yarn/plugins/linker.cjs | 87 ++++++++++++++++++++++++++++++++++++++++ .yarnrc.yml | 2 + 3 files changed, 90 insertions(+) create mode 100644 .yarn/plugins/linker.cjs diff --git a/.gitignore b/.gitignore index 2cb726c0..ff40eccc 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ yarn-error.log !/.yarn/releases !/.yarn/sdks !/.yarn/versions +/.links.yaml diff --git a/.yarn/plugins/linker.cjs b/.yarn/plugins/linker.cjs new file mode 100644 index 00000000..9172a206 --- /dev/null +++ b/.yarn/plugins/linker.cjs @@ -0,0 +1,87 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE in the repository root for full details. +*/ + +module.exports = { + name: "linker", + factory: (require) => ({ + hooks: { + registerPackageExtensions: async (config, registerPackageExtension) => { + const { structUtils } = require("@yarnpkg/core"); + const { parseSyml } = require("@yarnpkg/parsers"); + const path = require("path"); + const fs = require("fs"); + const process = require("process"); + + // Create a descriptor that we can use to target our direct dependencies + const projectPath = config.projectCwd + .replace(/\\/g, "/") + .replace("/C:/", "C:/"); + const manifestPath = path.join(projectPath, "package.json"); + const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); + const selfDescriptor = structUtils.parseDescriptor( + `${manifest.name}@*`, + true, + ); + + // Load the list of linked packages + const linksPath = path.join(projectPath, ".links.yaml"); + let linksFile; + try { + linksFile = fs.readFileSync(linksPath, "utf8"); + } catch (e) { + return; // File doesn't exist, there's nothing to link + } + let links; + try { + links = parseSyml(linksFile); + } catch (e) { + console.error(".links.yaml has invalid syntax", e); + process.exit(1); + } + + // Resolve paths and turn them into a Yarn package extension + const overrides = Object.fromEntries( + Object.entries(links).map(([name, link]) => [ + name, + `portal:${path.resolve(config.projectCwd, link)}`, + ]), + ); + const overrideIdentHashes = new Set(); + for (const name of Object.keys(overrides)) + overrideIdentHashes.add( + structUtils.parseDescriptor(`${name}@*`, true).identHash, + ); + + registerPackageExtension(selfDescriptor, { dependencies: overrides }); + + // Filter out the original dependencies from the package spec so Yarn + // knows to override them + const filterDependencies = (original) => { + const pkg = structUtils.copyPackage(original); + pkg.dependencies = new Map( + Array.from(pkg.dependencies.entries()).filter( + ([, value]) => !overrideIdentHashes.has(value.identHash), + ), + ); + return pkg; + }; + + // Patch Yarn's own normalizePackage method with the above filter + const originalNormalizePackage = config.normalizePackage; + config.normalizePackage = function (pkg, extensions) { + return originalNormalizePackage.call( + this, + pkg.identHash === selfDescriptor.identHash + ? filterDependencies(pkg) + : pkg, + extensions, + ); + }; + }, + }, + }), +}; diff --git a/.yarnrc.yml b/.yarnrc.yml index 3186f3f0..538de0e7 100644 --- a/.yarnrc.yml +++ b/.yarnrc.yml @@ -1 +1,3 @@ nodeLinker: node-modules +plugins: + - .yarn/plugins/linker.cjs