UNPKG

5.49 kBPlain TextView Raw
1import debug from "debug";
2import * as path from "path";
3import * as semver from "semver";
4
5import { BuidlerContext } from "../context";
6
7import { BuidlerError } from "./errors";
8import { ERRORS } from "./errors-list";
9import { ExecutionMode, getExecutionMode } from "./execution-mode";
10
11const log = debug("buidler:core:plugins");
12
13interface PackageJson {
14 name: string;
15 version: string;
16 peerDependencies: {
17 [name: string]: string;
18 };
19}
20
21/**
22 * Validates a plugin dependencies and loads it.
23 * @param pluginName - The plugin name
24 * @param buidlerContext - The BuidlerContext
25 * @param from - Where to resolve plugins and dependencies from. Only for
26 * testing purposes.
27 */
28export function usePlugin(
29 buidlerContext: BuidlerContext,
30 pluginName: string,
31 from?: string
32) {
33 log("Loading plugin %s", pluginName);
34
35 const executionMode = getExecutionMode();
36
37 if (from === undefined) {
38 // We have two different ways to search for plugins.
39 //
40 // If Buidler is installed globally, we want to force the plugins to also be
41 // installed globally, otherwise we can end up in a very chaotic situation.
42 // The way we enforce this is by setting `from` to something inside Buidler
43 // itself, as it will be placed in the global node_modules.
44 //
45 // If Buidler is not installed globally, we want the plugins to be
46 // accessible from the project's root, not from the Buidler installation.
47 // The reason for this is that yarn workspaces can easily hoist Buidler and
48 // not the plugins, leaving you with something like this:
49 //
50 // root/
51 // node_modules/
52 // buidler
53 // subpackage1/
54 // node_modules/
55 // plugin@v1/
56 // buidler.config.js
57 // subpackage2/
58 // node_modules/
59 // plugin@v2/
60 // buidler.config.js
61 //
62 // If we were to load the plugins from the Buidler installation in this
63 // situation, they wouldn't be found. Instead, we should load them from the
64 // project's root.
65 //
66 // To make things slightly more complicated, we don't really know the
67 // project's root, as we are still loading the config. What we do know
68 // though, is the config file's path, which must be inside the project, so
69 // we use that instead.
70 if (executionMode === ExecutionMode.EXECUTION_MODE_GLOBAL_INSTALLATION) {
71 from = __dirname;
72 } else {
73 from = buidlerContext.getConfigPath();
74 }
75 }
76
77 let globalFlag = "";
78 let globalWarning = "";
79 if (executionMode === ExecutionMode.EXECUTION_MODE_GLOBAL_INSTALLATION) {
80 globalFlag = " --global";
81 globalWarning =
82 "You are using a global installation of Buidler. Plugins and their dependencies must also be global.\n";
83 }
84
85 const pluginPackageJson = readPackageJson(pluginName, from);
86
87 if (pluginPackageJson === undefined) {
88 const installExtraFlags = globalFlag;
89
90 throw new BuidlerError(ERRORS.PLUGINS.NOT_INSTALLED, {
91 plugin: pluginName,
92 extraMessage: globalWarning,
93 extraFlags: installExtraFlags,
94 });
95 }
96
97 // We use the package.json's version of the name, as it is normalized.
98 pluginName = pluginPackageJson.name;
99
100 if (buidlerContext.loadedPlugins.includes(pluginName)) {
101 return;
102 }
103
104 if (pluginPackageJson.peerDependencies !== undefined) {
105 for (const [dependencyName, versionSpec] of Object.entries(
106 pluginPackageJson.peerDependencies
107 )) {
108 const dependencyPackageJson = readPackageJson(dependencyName, from);
109
110 let installExtraFlags = globalFlag;
111
112 if (versionSpec.match(/^[0-9]/) !== null) {
113 installExtraFlags += " --save-exact";
114 }
115
116 if (dependencyPackageJson === undefined) {
117 throw new BuidlerError(ERRORS.PLUGINS.MISSING_DEPENDENCY, {
118 plugin: pluginName,
119 dependency: dependencyName,
120 extraMessage: globalWarning,
121 extraFlags: installExtraFlags,
122 versionSpec,
123 });
124 }
125
126 const installedVersion = dependencyPackageJson.version;
127
128 if (
129 !semver.satisfies(installedVersion, versionSpec, {
130 includePrerelease: true,
131 })
132 ) {
133 throw new BuidlerError(ERRORS.PLUGINS.DEPENDENCY_VERSION_MISMATCH, {
134 plugin: pluginName,
135 dependency: dependencyName,
136 extraMessage: globalWarning,
137 extraFlags: installExtraFlags,
138 versionSpec,
139 installedVersion,
140 });
141 }
142 }
143 }
144
145 const options = from !== undefined ? { paths: [from] } : undefined;
146 const pluginPath = require.resolve(pluginName, options);
147 loadPluginFile(pluginPath);
148
149 buidlerContext.setPluginAsLoaded(pluginName);
150}
151
152export function loadPluginFile(absolutePluginFilePath: string) {
153 log("Loading plugin file %s", absolutePluginFilePath);
154 const imported = require(absolutePluginFilePath);
155 const plugin = imported.default !== undefined ? imported.default : imported;
156 if (typeof plugin === "function") {
157 plugin();
158 }
159}
160
161export function readPackageJson(
162 packageName: string,
163 from?: string
164): PackageJson | undefined {
165 try {
166 const options = from !== undefined ? { paths: [from] } : undefined;
167 const packageJsonPath = require.resolve(
168 path.join(packageName, "package.json"),
169 options
170 );
171
172 return require(packageJsonPath);
173 } catch (error) {
174 return undefined;
175 }
176}
177
178export function ensurePluginLoadedWithUsePlugin() {
179 // No-op. Only here for backwards compatibility
180}