Skip to main content

插件教程

从 Yarn 2 开始,Yarn 现在支持插件。有关它们是什么以及在哪种情况下要使用它们的更多信息,请参阅 专用页面。我们将在这里讨论编写一个所需的确切步骤。它真的很简单!

¥Starting from the Yarn 2, Yarn now supports plugins. For more information about what they are and in which case you'd want to use them, consult the dedicated page. We'll talk here about the exact steps needed to write one. It's quite simple, really!

插件是什么样子的?

¥What does a plugin look like?

插件是 Yarn 在运行时加载的脚本,可以向其中注入新的行为。它们还可以需要 Yarn 本身提供的一些包,例如 @yarnpkg/core。这允许你使用与当前正在使用的 Yarn 二进制文件完全相同的核心 API,有点像对等依赖!

¥Plugins are scripts that get loaded at runtime by Yarn, and that can inject new behaviors into it. They also can require some packages provided by Yarn itself, such as @yarnpkg/core. This allows you to use the exact same core API as the Yarn binary currently in use, kinda like if it was a peer dependency!

信息

由于插件是在 Yarn 启动之前加载的(因此在你进行第一次安装之前),强烈建议以这样的方式编写插件,使其可以在没有依赖的情况下工作。如果这变得困难,请知道我们提供了一个强大的工具 (@yarnpkg/builder),它可以将你的插件打包到一个 Javascript 文件中,随时可以发布。

¥Since plugins are loaded before Yarn starts (and thus before you make your first install), it's strongly advised to write your plugins in such a way that they work without dependencies. If that becomes difficult, know that we provide a powerful tool (@yarnpkg/builder that can bundle your plugins into a single Javascript file, ready to be published.

编写我们的第一个插件

¥Writing our first plugin

在文本编辑器中打开一个名为 plugin-hello-world.js 的新文件,然后输入以下代码:

¥Open in a text editor a new file called plugin-hello-world.js, and type the following code:

module.exports = {
name: `plugin-hello-world`,
factory: require => ({
// What is this `require` function, you ask? It's a `require`
// implementation provided by Yarn core that allows you to
// access various packages (such as @yarnpkg/core) without
// having to list them in your own dependencies - hence
// lowering your plugin bundle size, and making sure that
// you'll use the exact same core modules as the rest of the
// application.
//
// Of course, the regular `require` implementation remains
// available, so feel free to use the `require` you need for
// your use case!
})
};

我们有插件,但现在我们需要注册它,以便 Yarn 知道在哪里找到它。为此,我们只需在存储库根目录的 .yarnrc.yml 文件中添加一个条目:

¥We have our plugin, but now we need to register it so that Yarn knows where to find it. To do this, we'll just add an entry within the .yarnrc.yml file at the root of the repository:

plugins:
- ./plugin-hello-world.js

就是这样!恭喜,你有了第一个插件!当然,它并没有做太多事情(或者实际上什么都没有做),但我们将看到如何扩展它以使其更强大。

¥That's it! You have your first plugin, congratulations! Of course it doesn't do much (or anything at all, really), but we'll see how to extend it to make it more powerful.

一体化插件构建器

¥All-in-one plugin builder

正如我们所见,插件应该是独立的 JavaScript 源文件。手动编写它们是完全可能的,特别是如果你只需要一个小命令,但是一旦你开始添加多个命令,它就会变得有点复杂。为了简化此过程,我们维护了一个名为 @yarnpkg/builder 的包。此构建器之于 Yarn 相当于 Next.js 之于 Web 开发 - 它是一种旨在帮助创建、构建和管理用 TypeScript 编写的复杂插件的工具。

¥As we saw, plugins are meant to be standalone JavaScript source files. It's very possible to author them by hand, especially if you only need a small one, but once you start adding multiple commands it can become a bit more complicated. To make this process easier, we maintain a package called @yarnpkg/builder. This builder is to Yarn what Next.js is to web development - it's a tool designed to help creating, building, and managing complex plugins written in TypeScript.

可以在 专用页面 上找到它的文档,但请记住你不需要使用它。有时好的旧脚本就很好!

¥Its documentation can be found on the dedicated page, but remember that you're not required to use it. Sometimes good old scripts are just fine!

添加命令

¥Adding commands

插件还可以注册自己的命令。为此,我们只需使用 clipanion 库编写它们 - 我们甚至不必将它添加到我们的依赖中!让我们看一个例子:

¥Plugins can also register their own commands. To do this, we just have to write them using the clipanion library - and we don't even have to add it to our dependencies! Let's see an example:

module.exports = {
name: `plugin-hello-world`,
factory: require => {
const {BaseCommand} = require(`@yarnpkg/cli`);

class HelloWorldCommand extends BaseCommand {
static paths = [[`hello`]];

async execute() {
this.context.stdout.write(`This is my very own plugin 😎\n`);
}
}

return {
commands: [
HelloWorldCommand,
],
};
}
};

现在,尝试运行 yarn hello。你将看到你的消息出现!请注意,你可以使用 clipanion 提供的全套功能,包括短选项、长选项、可变参数列表等你甚至可以使用我们提供的 typanion 库验证你的选项。这是一个我们只接受数字作为参数的示例:

¥Now, try to run yarn hello. You'll see your message appear! Note that you can use the full set of features provided by clipanion, including short options, long options, variadic argument lists, ... You can even validate your options using the typanion library, which we provide. Here's an example where we only accept numbers as parameter:

module.exports = {
name: `plugin-addition`,
factory: require => {
const {BaseCommand} = require(`@yarnpkg/cli`);
const {Command, Option} = require(`clipanion`);
const t = require(`typanion`);

class AdditionCommand extends BaseCommand {
static paths = [[`addition`]];

// Show descriptive usage for a --help argument passed to this command
static usage = Command.Usage({
description: `hello world!`,
details: `
This command will print a nice message.
`,
examples: [[
`Add two numbers together`,
`yarn addition 42 10`,
]],
});

a = Option.String({validator: t.isNumber()});
b = Option.String({validator: t.isNumber()});

async execute() {
this.context.stdout.write(`${this.a}+${this.b}=${this.a + this.b}\n`);
}
}

return {
commands: [
AdditionCommand,
],
};
},
};

使用钩子

¥Using hooks

插件可以在 Yarn 生命周期内注册各种事件,并为它们提供额外的信息来改变它们的行为。为此,你只需在插件中声明一个新的 hooks 属性并为你想要监听的每个钩子添加成员:

¥Plugins can register to various events in the Yarn lifetime, and provide them additional information to alter their behavior. To do this, you just need to declare a new hooks property in your plugin and add members for each hook you want to listen to:

module.exports = {
name: `plugin-hello-world`,
factory: require => ({
hooks: {
setupScriptEnvironment(project, scriptEnv) {
scriptEnv.HELLO_WORLD = `my first plugin!`;
},
},
})
};

在此示例中,我们注册到 setupScriptEnvironment 钩子并使用它来向环境中注入参数。现在,每次运行脚本时,你都会看到你的环境将包含一个名为 HELLO_WORLD 的新值!

¥In this example, we registered to the setupScriptEnvironment hook and used it to inject an argument into the environment. Now, each time you'll run a script, you'll see that your env will contain a new value called HELLO_WORLD!

钩子很多,我们仍在研究它们。根据你的反馈,可能会添加、删除或更改一些。因此,如果你想做一些钩子还不允许你做的事情,请告诉我们!

¥Hooks are numerous, and we're still working on them. Some might be added, removed, or changed, based on your feedback. So if you'd like to do something hooks don't allow you to do yet, come tell us!

使用 Yarn API

¥Using the Yarn API

Yarn 的大多数钩子都使用各种参数调用,这些参数会告诉你有关调用钩子的上下文的更多信息。每个钩子的确切参数列表不同,但一般来说,它们是 @yarnpkg/core 库中定义的类型。

¥Most of Yarn's hooks are called with various arguments that tell you more about the context under which the hook is being called. The exact argument list is different for each hook, but in general they are of the types defined in the @yarnpkg/core library.

在此示例中,我们将与 afterAllInstalled 钩子集成,以便在每次安装后打印有关依赖树的一些基本信息。此钩子通过一个附加参数调用,该参数是公共 Project 实例,其中包含 Yarn 收集的有关该项目的大部分信息:依赖、包清单、工作区信息等。

¥In this example, we will integrate with the afterAllInstalled hook in order to print some basic information about the dependency tree after each install. This hook gets invoked with an additional parameter that is the public Project instance where lie most of the information Yarn has collected about the project: dependencies, package manifests, workspace information, and so on.

const fs = require(`fs`);
const util = require(`util`);

module.exports = {
name: `plugin-project-info`,
factory: require => {
const {structUtils} = require(`@yarnpkg/core`);

return {
default: {
hooks: {
afterAllInstalled(project) {
let descriptorCount = 0;
for (const descriptor of project.storedDescriptors.values())
if (!structUtils.isVirtualDescriptor(descriptor))
descriptorCount += 1;

let packageCount = 0;
for (const pkg of project.storedPackages.values())
if (!structUtils.isVirtualLocator(pkg))
packageCount += 1;

console.log(`This project contains ${descriptorCount} different descriptors that resolve to ${packageCount} packages`);
}
}
}
};
}
};

这变得有趣了。如你所见,我们从项目实例中访问了 storedDescriptorsstoredPackages 字段,并对它们进行迭代以获取非虚拟项目的数量(虚拟包在 此处 中有更详细的描述)。这是一个非常简单的用例,但我们可以做更多的事情:项目根目录位于 cwd 属性中,工作区公开为 workspaces,描述符和包之间的链接可以通过 storedResolutions 建立,... 等等。

¥This is getting interesting. As you can see, we accessed the storedDescriptors and storedPackages fields from our project instance, and iterated over them to obtain the number of non-virtual items (virtual packages are described in more details here). This is a very simple use case, but we could have done many more things: the project root is located in the cwd property, the workspaces are exposed as workspaces, the link between descriptors and packages can be made via storedResolutions, ... etc.

请注意,我们只触及了 Project 类实例的表面!Yarn 核心提供了许多其他类(和钩子),允许你使用缓存、下载包、触发 http 请求等。还有更多。下次你想编写插件时,请看一看,那里几乎肯定有一个实用程序可以让你避免重新实现轮子。

¥Note that we've only scratched the surface of the Project class instance! The Yarn core provides many other classes (and hooks) that allow you to work with the cache, download packages, trigger http requests, ... and much more. Next time you want to write a plugin, give it a look, there's almost certainly an utility there that will allow you to avoid having to reimplement the wheel.

使用 YARN_PLUGINS 环境变量动态加载插件

¥Dynamically loading plugins using the YARN_PLUGINS environment variable

虽然插件通常在 .yarnrc.yml 配置文件中声明,但这些插件代表面向用户的配置,第三方工具不应在未经用户许可的情况下修改这些配置。

¥While plugins are usually declared inside .yarnrc.yml configuration files, those represent the user-facing configuration that third-party tools shouldn't modify without the user's permission.

YARN_PLUGINS 环境变量是一个以分号分隔的插件路径列表,Yarn 将在调用时动态加载这些路径。路径是相对于调用 startingCwd Yarn 来解析的。

¥The YARN_PLUGINS environment variable is a semicolon-separated list of plugin paths that Yarn will dynamically load when called. Paths are resolved relative to the startingCwd Yarn is called in.

包可以使用此机制动态注册插件并使用命令查询 Yarn API,而无需明确依赖 Yarn 包并处理潜在的版本不匹配。

¥Packages can use this mechanism to dynamically register plugins and query the Yarn API using commands without having to explicitly depend on the Yarn packages and deal with potential version mismatches.

官方钩子

¥Official hooks

我们的新网站尚不支持生成钩子列表;抱歉 :(

¥Our new website doesn't support generating the hook list yet; sorry :(