Skip to content

Commit 19c136e

Browse files
authored
Tweaks the plugin pipeline (#683)
* Tweaks the plugin pipeline * Exposes the hello world plugin
1 parent 0af851f commit 19c136e

File tree

10 files changed

+235
-98
lines changed

10 files changed

+235
-98
lines changed

.yarn/versions/042aef38.yml

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
releases:
2+
"@yarnpkg/[email protected]": prerelease
3+
"@yarnpkg/[email protected]": prerelease
4+
"@yarnpkg/[email protected]": prerelease
5+
"@yarnpkg/[email protected]": prerelease
6+
"@yarnpkg/[email protected]": prerelease
7+
8+
declined:
9+
- "@yarnpkg/[email protected]"
10+
- "@yarnpkg/[email protected]"
11+
- "@yarnpkg/[email protected]"
12+
- "@yarnpkg/[email protected]"
13+
- "@yarnpkg/[email protected]"
14+
- "@yarnpkg/[email protected]"
15+
- "@yarnpkg/[email protected]"
16+
- "@yarnpkg/[email protected]"
17+
- "@yarnpkg/[email protected]"
18+
- "@yarnpkg/[email protected]"
19+
- "@yarnpkg/[email protected]"
20+
- "@yarnpkg/[email protected]"
21+
- "@yarnpkg/[email protected]"
22+
- "@yarnpkg/[email protected]"
23+
- "@yarnpkg/[email protected]"
24+
- "@yarnpkg/[email protected]"
25+
- "@yarnpkg/[email protected]"
26+
- "@yarnpkg/[email protected]"
27+
- "@yarnpkg/[email protected]"
28+
- "@yarnpkg/[email protected]"
29+
- "@yarnpkg/[email protected]"
30+
- "@yarnpkg/[email protected]"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
describe(`Commands`, () => {
2+
describe(`plugin import`, () => {
3+
test(
4+
`it should support adding a plugin via its path`,
5+
makeTemporaryEnv({}, async ({path, run, source}) => {
6+
await run(`plugin`, `import`, require.resolve(`@yarnpkg/monorepo/scripts/plugin-hello-world.js`));
7+
await run(`hello`, `--email`, `[email protected]`);
8+
}),
9+
);
10+
});
11+
});

packages/gatsby/content/advanced/plugin-tutorial.md

+19-4
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ path: /advanced/plugin-tutorial
44
title: "Plugin Tutorial"
55
---
66

7-
Starting from the v2, 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](/features/plugins). We'll talk here about the exact steps needed to write one. It's quite simple, really!
7+
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](/features/plugins). We'll talk here about the exact steps needed to write one. It's quite simple, really!
88

99
## What does a plugin look like?
1010

@@ -20,6 +20,17 @@ Open in a text editor a new file called `plugin-hello-world.js`, and type the fo
2020
module.exports = {
2121
name: `plugin-hello-world`,
2222
factory: require => ({
23+
// What is this `require` function, you ask? It's a `require`
24+
// implementation provided by Yarn core that allows you to
25+
// access various packages (such as @yarnpkg/core) without
26+
// having to list them in your own dependencies - hence
27+
// lowering your plugin bundle size, and making sure that
28+
// you'll use the exact same core modules as the rest of the
29+
// application.
30+
//
31+
// Of course, the regular `require` implementation remains
32+
// available, so feel free to use the `require` you need for
33+
// your use case!
2334
})
2435
};
2536
```
@@ -70,7 +81,7 @@ module.exports = {
7081
}
7182
}
7283

73-
Command.Path(`hello`)(HelloWorldCommand.prototype);
84+
HelloWorldCommand.addPath(`hello`);
7485

7586
return {
7687
commands: [
@@ -99,7 +110,11 @@ module.exports = {
99110
// Note: This curious syntax is because @Command.String is actually
100111
// a decorator! But since they aren't supported in native JS at the
101112
// moment, we need to call them manually.
102-
Command.String(`--email`)(HelloWorldCommand.prototype, `email`);
113+
HelloWorldCommand.addOption(`email`, Command.String(`--email`));
114+
115+
// Similarly we would be able to use a decorator here too, but since
116+
// we're writing our code in JS-only we need to go through "addPath".
117+
HelloWorldCommand.addPath(`hello`);
103118

104119
// Similarly, native JS doesn't support member variable as of today,
105120
// hence the awkward writing.
@@ -124,6 +139,6 @@ module.exports = {
124139
HelloWorldCommand,
125140
],
126141
};
127-
}
142+
},
128143
};
129144
```

packages/gatsby/content/advanced/questions-and-answers.md

+5-3
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ Lockfiles should **always** be kept within the repository. Continuous integratio
5050

5151
- `.yarn/unplugged` and `.yarn/build-state.yml` should likely always be ignored since they typically hold machine-specific build artifacts. Ignoring them might however prevent [Zero-Installs](https://next.yarnpkg.com/features/zero-installs) from working (to prevent this, set [`enableScripts`](/configuration/yarnrc#enableScripts) to `false`).
5252

53+
- `.yarn/versions` is used by the [version plugin](/features/release-workflow) to store the package release definitions. You will want to keep it within your repository.
54+
5355
- `.yarn/cache` and `.pnp.*` may be safely ignored, but you'll need to run `yarn install` to regenerate them between each branch switch - which would be optional otherwise, cf [Zero-Installs](/features/zero-installs).
5456

5557
- `yarn.lock` should always be stored within your repository ([even if you develop a library](#should-lockfiles-be-committed-to-the-repository)).
@@ -68,9 +70,9 @@ So to summarize:
6870
**If you're not using Zero-Installs:**
6971

7072
```gitignore
71-
.yarn/*
72-
!.yarn/releases
73-
!.yarn/plugins
73+
.yarn/cache
74+
.yarn/unplugged
75+
.yarn/build-state.yml
7476
.pnp.*
7577
```
7678

packages/gatsby/content/features/plugins.md

+12-27
Original file line numberDiff line numberDiff line change
@@ -8,41 +8,26 @@ Ever since Yarn was created, our very essence has been about experimenting, evol
88

99
As you can guess, this philosophy (coupled with the high number of external contribution we receive) requires us to iterate fast in order to accomodate with the various experiments that we brew. In a major step forward, Yarn got redesigned in the v2 in order to leverage a new modular API that can be extended through plugins. Nowadays, most of our features are implemented through those plugins - even `yarn add` and `yarn install` are preinstalled plugins!
1010

11-
## What can plugins do?
12-
13-
- **Plugins can add new resolvers.** Resolvers are the components tasked from converting dependency ranges (for example `^1.2.0`) into fully-qualified package references (for example `npm:1.2.0`). By implementing a resolver, you can tell Yarn which versions are valid candidates to a specific range.
14-
15-
- **Plugins can add new fetchers.** Fetchers are the components that take the fully-qualified package references we mentioned in the previous step (for example `npm:1.2.0`) and know how to obtain the data of the package they belong to. Fetchers can work with remote packages (for example the npm registry), but can also find the packages directly from their location on the disk (or any other data source).
16-
17-
- **Plugins can add new linkers.** Once all the packages have been located and are ready for installation, Yarn will call the linkers to generate the files needed for the install targets to work properly. As an example, the PnP linker would generate the `.pnp.js` manifest, and a Python linker would instead generate the virtualenv files needed.
18-
19-
- **Plugins can add new commands.** Each plugin can ship as many commands as they see fit, which will be injected into our CLI (also making them available through `yarn --help`). Because the Yarn plugins are dynamically linked with the running Yarn process, they can be very small and guaranteed to share the exact same behavior as your package manager (which wouldn't be the case if you were to reimplement the workspace detection, for example).
11+
## Where to find plugins?
2012

21-
- **Plugins can be integrated with each other.** Each plugin has the ability to trigger special actions called hooks, and to register themselves to any defined hook. So for example, you could make a plugin that would execute an action each time a package is added as dependency of one of your workspaces!
22-
23-
## How to use plugins?
24-
25-
The Yarn plugins are single-file JS scripts. They are easy to use:
13+
Just type [`yarn plugin list`](/cli/plugin/list), or consult the repository: [plugins.yml](https://github.com/yarnpkg/berry/blob/master/plugins.yml).
2614

27-
### Automatic setup
15+
Note that the plugins exposed through `yarn plugin list` are only the official ones. Since plugins are single-file JS scripts, anyone can author them. By using [`yarn plugin import`](/cli/plugin/import) with an URL or a filesystem path, you'll be able to download (and execute, be careful!) code provided by anyone.
2816

29-
The official plugins (the ones whose development happen on the Yarn repository) can be installed using the following commands:
17+
## How to write plugins?
3018

31-
- `yarn plugin list` will print the name of all available [official plugins](https://github.com/yarnpkg/berry/tree/plugins.yml).
19+
We have a tutorial for this! Head over to [Plugin Tutorial](/advanced/plugin-tutorial).
3220

33-
- `yarn plugin import <plugin-name>` will download one of the plugins from the list, store it within the `.yarn/plugins` directory, and modify your local `.yarnrc.yml` file to reference it.
21+
## What can plugins do?
3422

35-
- `yarn plugin import <url>` will do the same thing, but because it uses an URL it will also work with any plugin regardless of where the plugin is actually hosted.
23+
- **Plugins can add new resolvers.** Resolvers are the components tasked from converting dependency ranges (for example `^1.2.0`) into fully-qualified package references (for example `npm:1.2.0`). By implementing a resolver, you can tell Yarn which versions are valid candidates to a specific range.
3624

37-
### Manual setup
25+
- **Plugins can add new fetchers.** Fetchers are the components that take the fully-qualified package references we mentioned in the previous step (for example `npm:1.2.0`) and know how to obtain the data of the package they belong to. Fetchers can work with remote packages (for example the npm registry), but can also find the packages directly from their location on the disk (or any other data source).
3826

39-
The `yarn plugin import` command is useful, but in case you prefer to setup your project yourself:
27+
- **Plugins can add new linkers.** Once all the packages have been located and are ready for installation, Yarn will call the linkers to generate the files needed for the install targets to work properly. As an example, the PnP linker would generate the `.pnp.js` manifest, and a Python linker would instead generate the virtualenv files needed.
4028

41-
- Download the plugin you want to use and put it somewhere
29+
- **Plugins can add new commands.** Each plugin can ship as many commands as they see fit, which will be injected into our CLI (also making them available through `yarn --help`). Because the Yarn plugins are dynamically linked with the running Yarn process, they can be very small and guaranteed to share the exact same behavior as your package manager (which wouldn't be the case if you were to reimplement the workspace detection, for example).
4230

43-
- Update your project-level `.yarnrc.yml` file by adding the following property:
31+
- **Plugins can register to some events.** Yarn has a concept known as "hooks", where events are periodically triggered during the lifecycle of the package manager. Plugins can register to those hooks in order to add their own logic depending on what the core allows. For example, the `afterAllInstalled` hook will be called each time the `Project#install` method ends - typically after each `yarn install`.
4432

45-
```yaml
46-
plugins:
47-
- "./my-plugin.js"
48-
```
33+
- **Plugins can be integrated with each other.** Each plugin has the ability to trigger special actions called hooks, and to register themselves to any defined hook. So for example, you could make a plugin that would execute an action each time a package is added as dependency of one of your workspaces!

packages/plugin-essentials/sources/commands/plugin/import.ts

+66-31
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import {BaseCommand} from '@yarnpkg/cli';
2-
import {Configuration, MessageName, Project, ReportError, StreamReport} from '@yarnpkg/core';
3-
import {httpUtils, structUtils} from '@yarnpkg/core';
4-
import {PortablePath, npath, ppath, xfs} from '@yarnpkg/fslib';
5-
import {Command} from 'clipanion';
6-
import {runInNewContext} from 'vm';
1+
import {BaseCommand} from '@yarnpkg/cli';
2+
import {Configuration, MessageName, Project, ReportError, StreamReport, miscUtils} from '@yarnpkg/core';
3+
import {httpUtils, structUtils} from '@yarnpkg/core';
4+
import {PortablePath, npath, ppath, xfs} from '@yarnpkg/fslib';
5+
import {Command} from 'clipanion';
6+
import {runInNewContext} from 'vm';
77

8-
import {getAvailablePlugins} from './list';
8+
import {getAvailablePlugins} from './list';
99

1010
// eslint-disable-next-line arca/no-default-export
1111
export default class PluginDlCommand extends BaseCommand {
@@ -29,9 +29,15 @@ export default class PluginDlCommand extends BaseCommand {
2929
examples: [[
3030
`Download and activate the "@yarnpkg/plugin-exec" plugin`,
3131
`$0 plugin import @yarnpkg/plugin-exec`,
32+
], [
33+
`Download and activate the "@yarnpkg/plugin-exec" plugin (shorthand)`,
34+
`$0 plugin import exec`,
3235
], [
3336
`Download and activate a community plugin`,
3437
`$0 plugin import https://example.org/path/to/plugin.js`,
38+
], [
39+
`Activate a local plugin`,
40+
`$0 plugin import ./path/to/plugin.js`,
3541
]],
3642
});
3743

@@ -45,35 +51,37 @@ export default class PluginDlCommand extends BaseCommand {
4551
}, async report => {
4652
const {project} = await Project.find(configuration, this.context.cwd);
4753

48-
const name = this.name!;
49-
const candidatePath = ppath.resolve(this.context.cwd, npath.toPortablePath(name));
50-
51-
let pluginBuffer;
54+
let pluginSpec: string;
55+
let pluginBuffer: Buffer;
56+
if (this.name.match(/^\.{0,2}[\\\/]/) || npath.isAbsolute(this.name)) {
57+
const candidatePath = ppath.resolve(this.context.cwd, npath.toPortablePath(this.name));
5258

53-
if (await xfs.existsPromise(candidatePath)) {
5459
report.reportInfo(MessageName.UNNAMED, `Reading ${configuration.format(candidatePath, `green`)}`);
60+
61+
pluginSpec = ppath.relative(project.cwd, candidatePath);
5562
pluginBuffer = await xfs.readFilePromise(candidatePath);
5663
} else {
57-
const ident = structUtils.tryParseIdent(name);
58-
5964
let pluginUrl;
60-
if (ident) {
61-
const key = structUtils.stringifyIdent(ident);
62-
const data = await getAvailablePlugins(configuration);
63-
64-
if (!Object.prototype.hasOwnProperty.call(data, key))
65-
throw new ReportError(MessageName.PLUGIN_NAME_NOT_FOUND, `Couldn't find a plugin named "${key}" on the remote registry. Note that only the plugins referenced on our website (https://github.com/yarnpkg/berry/blob/master/plugins.yml) can be referenced by their name; any other plugin will have to be referenced through its public url (for example https://github.com/yarnpkg/berry/raw/master/packages/plugin-typescript/bin/%40yarnpkg/plugin-typescript.js).`);
66-
67-
pluginUrl = data[key].url;
68-
} else {
65+
if (this.name.match(/^https?:/)) {
6966
try {
7067
// @ts-ignore We don't want to add the dom to the TS env just for this line
71-
new URL(name);
68+
new URL(this.name);
7269
} catch {
73-
throw new ReportError(MessageName.INVALID_PLUGIN_REFERENCE, `Plugin specifier "${name}" is neither a plugin name nor a valid url`);
70+
throw new ReportError(MessageName.INVALID_PLUGIN_REFERENCE, `Plugin specifier "${this.name}" is neither a plugin name nor a valid url`);
7471
}
7572

73+
pluginSpec = this.name;
7674
pluginUrl = name;
75+
} else {
76+
const ident = structUtils.parseIdent(this.name.replace(/^((@yarnpkg\/)?|(plugin-)?)/, `@yarnpkg/plugin-`));
77+
const identStr = structUtils.stringifyIdent(ident);
78+
const data = await getAvailablePlugins(configuration);
79+
80+
if (!Object.prototype.hasOwnProperty.call(data, identStr))
81+
throw new ReportError(MessageName.PLUGIN_NAME_NOT_FOUND, `Couldn't find a plugin named "${identStr}" on the remote registry. Note that only the plugins referenced on our website (https://github.com/yarnpkg/berry/blob/master/plugins.yml) can be referenced by their name; any other plugin will have to be referenced through its public url (for example https://github.com/yarnpkg/berry/raw/master/packages/plugin-typescript/bin/%40yarnpkg/plugin-typescript.js).`);
82+
83+
pluginSpec = identStr;
84+
pluginUrl = data[identStr].url;
7785
}
7886

7987
report.reportInfo(MessageName.UNNAMED, `Downloading ${configuration.format(pluginUrl, `green`)}`);
@@ -88,18 +96,45 @@ export default class PluginDlCommand extends BaseCommand {
8896
exports: vmExports,
8997
});
9098

91-
const relativePath = `.yarn/plugins/${vmModule.exports.name}.js` as PortablePath;
99+
const pluginName = vmModule.exports.name;
100+
101+
const relativePath = `.yarn/plugins/${pluginName}.js` as PortablePath;
92102
const absolutePath = ppath.resolve(project.cwd, relativePath);
93103

94104
report.reportInfo(MessageName.UNNAMED, `Saving the new plugin in ${configuration.format(relativePath, `magenta`)}`);
95105
await xfs.mkdirpPromise(ppath.dirname(absolutePath));
96106
await xfs.writeFilePromise(absolutePath, pluginBuffer);
97107

98-
await Configuration.updateConfiguration(project.cwd, (current: any) => ({
99-
plugins: (current.plugins || []).concat([
100-
relativePath,
101-
]),
102-
}));
108+
const pluginMeta = {
109+
path: relativePath,
110+
spec: pluginSpec,
111+
};
112+
113+
await Configuration.updateConfiguration(project.cwd, (current: any) => {
114+
const plugins = [];
115+
let hasBeenReplaced = false;
116+
117+
for (const entry of current.plugins || []) {
118+
const userProvidedPath = typeof entry !== `string`
119+
? entry.path
120+
: entry;
121+
122+
const pluginPath = ppath.resolve(project.cwd, npath.toPortablePath(userProvidedPath));
123+
const {name} = miscUtils.dynamicRequire(npath.fromPortablePath(pluginPath));
124+
125+
if (name !== pluginName) {
126+
plugins.push(entry);
127+
} else {
128+
plugins.push(pluginMeta);
129+
hasBeenReplaced = true;
130+
}
131+
}
132+
133+
if (!hasBeenReplaced)
134+
plugins.push(pluginMeta);
135+
136+
return {plugins};
137+
});
103138
});
104139

105140
return report.exitCode();

packages/yarnpkg-core/sources/Configuration.ts

+11-3
Original file line numberDiff line numberDiff line change
@@ -574,9 +574,13 @@ export class Configuration {
574574
if (!Array.isArray(data.plugins))
575575
continue;
576576

577-
for (const userProvidedPath of data.plugins) {
577+
for (const userPluginEntry of data.plugins) {
578+
const userProvidedPath = typeof userPluginEntry !== `string`
579+
? userPluginEntry.path
580+
: userPluginEntry;
581+
578582
const pluginPath = ppath.resolve(cwd, npath.toPortablePath(userProvidedPath));
579-
const {factory, name} = nodeUtils.dynamicRequire(npath.fromPortablePath(pluginPath));
583+
const {factory, name} = miscUtils.dynamicRequire(npath.fromPortablePath(pluginPath));
580584

581585
// Prevent plugin redefinition so that the ones declared deeper in the
582586
// filesystem always have precedence over the ones below.
@@ -592,8 +596,12 @@ export class Configuration {
592596
}
593597
};
594598

599+
const getDefault = (object: any) => {
600+
return object.default || object;
601+
};
602+
595603
const plugin = miscUtils.prettifySyncErrors(() => {
596-
return factory(pluginRequire).default;
604+
return getDefault(factory(pluginRequire));
597605
}, message => {
598606
return `${message} (when initializing ${name}, defined in ${path})`;
599607
});

0 commit comments

Comments
 (0)