Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Config extension/resolution mechanism for plugins #325

Merged
merged 4 commits into from
Sep 11, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/buidler-core/src/internal/context.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BuidlerRuntimeEnvironment } from "../types";
import { BuidlerRuntimeEnvironment, ConfigExtender } from "../types";

import { ExtenderManager } from "./core/config/extenders";
import { BuidlerError, ERRORS } from "./core/errors";
Expand Down Expand Up @@ -39,6 +39,7 @@ export class BuidlerContext {
public readonly extendersManager = new ExtenderManager();
public environment?: BuidlerRuntimeEnvironment;
public readonly loadedPlugins: string[] = [];
public readonly configExtenders: ConfigExtender[] = [];

public setBuidlerRuntimeEnvironment(env: BuidlerRuntimeEnvironment) {
if (this.environment !== undefined) {
Expand Down
6 changes: 6 additions & 0 deletions packages/buidler-core/src/internal/core/config/config-env.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
ActionType,
ConfigExtender,
ConfigurableTaskDefinition,
EnvironmentExtender,
TaskArguments
Expand Down Expand Up @@ -83,6 +84,11 @@ export function extendEnvironment(extender: EnvironmentExtender) {
extenderManager.add(extender);
}

export function extendConfig(extender: ConfigExtender) {
const ctx = BuidlerContext.getBuidlerContext();
ctx.configExtenders.push(extender);
}

/**
* Loads a Buidler plugin
* @param pluginName The plugin name.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as path from "path";

import { ResolvedBuidlerConfig } from "../../../types";
import { BuidlerContext } from "../../context";
import { loadPluginFile } from "../plugins";
import { getUserConfigPath } from "../project-structure";

Expand Down Expand Up @@ -42,5 +43,10 @@ export function loadConfigAndTasks(configPath?: string): ResolvedBuidlerConfig {
// To avoid bad practices we remove the previously exported stuff
Object.keys(configEnv).forEach(key => (globalAsAny[key] = undefined));

return resolveConfig(configPath, defaultConfig, userConfig);
return resolveConfig(
configPath,
defaultConfig,
userConfig,
BuidlerContext.getBuidlerContext().configExtenders
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import path from "path";

import {
BuidlerConfig,
ConfigExtender,
ProjectPaths,
ResolvedBuidlerConfig
} from "../../../types";
import { fromEntries } from "../../util/lang";
import { BuidlerError, ERRORS } from "../errors";

function mergeUserAndDefaultConfigs(
defaultConfig: BuidlerConfig,
Expand All @@ -25,26 +27,36 @@ function mergeUserAndDefaultConfigs(
* @param userConfigPath the user config filepath
* @param defaultConfig the buidler's default config object
* @param userConfig the user config object
* @param configExtenders An array of ConfigExtenders
*
* @returns the resolved config
*/
export function resolveConfig(
userConfigPath: string,
defaultConfig: BuidlerConfig,
userConfig: BuidlerConfig
userConfig: BuidlerConfig,
configExtenders: ConfigExtender[]
): ResolvedBuidlerConfig {
userConfig = deepFreezeUserConfig(userConfig);

const config = mergeUserAndDefaultConfigs(defaultConfig, userConfig);

const paths = resolveProjectPaths(userConfigPath, userConfig.paths);

return {
const resolved = {
...config,
paths,
networks: config.networks!,
solc: config.solc!,
defaultNetwork: config.defaultNetwork!,
analytics: config.analytics!
};

for (const extender of configExtenders) {
extender(resolved, userConfig);
}

return resolved;
}

function resolvePathFrom(
Expand Down Expand Up @@ -95,3 +107,34 @@ export function resolveProjectPaths(
tests: resolvePathFrom(root, "test", userPaths.tests)
};
}

function deepFreezeUserConfig(
config: any,
propertyPath: Array<string | number | symbol> = []
) {
if (typeof config !== "object" || config === null) {
return config;
}

return new Proxy(config, {
get(target: any, property: string | number | symbol, receiver: any): any {
return deepFreezeUserConfig(Reflect.get(target, property, receiver), [
...propertyPath,
property
]);
},

set(
target: any,
property: string | number | symbol,
value: any,
receiver: any
): boolean {
throw new BuidlerError(ERRORS.GENERAL.USER_CONFIG_MODIFIED, {
path: [...propertyPath, property]
.map(pathPart => pathPart.toString())
.join(".")
});
}
});
}
5 changes: 5 additions & 0 deletions packages/buidler-core/src/internal/core/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,11 @@ To learn more about Buidler's configuration, please go to https://buidler.dev/do
number: 9,
message: `Error while loading Buidler's configuration.
You probably imported @nomiclabs/buidler instead of @nomiclabs/buidler/config`
},
USER_CONFIG_MODIFIED: {
number: 10,
message: `Error while loading Buidler's configuration.
You or one of your plugins is trying to modify the userConfig.%path% value from a config extender`
}
},
NETWORK: {
Expand Down
14 changes: 14 additions & 0 deletions packages/buidler-core/src/internal/reset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* manually with `unloadModule`.
*/
import { BuidlerContext } from "./context";
import { getUserConfigPath } from "./core/project-structure";
import { globSync } from "./util/glob";

export function resetBuidlerContext() {
Expand All @@ -17,6 +18,19 @@ export function resetBuidlerContext() {
}
// unload config file too.
unloadModule(ctx.environment.config.paths.configFile);
} else {
// We may get here if loading the config has thrown, so be unload it
let configPath: string | undefined;

try {
configPath = getUserConfigPath();
} catch (error) {
// We weren't in a buidler project
}

if (configPath !== undefined) {
unloadModule(configPath);
}
}
BuidlerContext.deleteBuidlerContext();
}
Expand Down
8 changes: 6 additions & 2 deletions packages/buidler-core/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { EventEmitter } from "events";
import { DeepPartial, Omit } from "ts-essentials";
import { DeepPartial, DeepReadonly, Omit } from "ts-essentials";

import { Analytics } from "./internal/cli/analytics";
import * as types from "./internal/core/params/argumentTypes";

// Begin config types
Expand Down Expand Up @@ -125,6 +124,11 @@ export interface SolcInput {
*/
export type EnvironmentExtender = (env: BuidlerRuntimeEnvironment) => void;

export type ConfigExtender = (
config: ResolvedBuidlerConfig,
userConfig: DeepReadonly<BuidlerConfig>
) => void;

export interface TasksMap {
[name: string]: TaskDefinition;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
extendConfig((config, userConfig) => {
config.values = [1];
});

extendConfig((config, userConfig) => {
config.values.push(2);
});

module.exports = {};
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
extendConfig((config, userConfig) => {
userConfig.networks.asd = 123;
});

module.exports = { networks: {} };
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// extendConfig must be available
// extendConfig shouldn't let me modify th user config
// config extenders must run in order
// config extensions must be visible

import { assert } from "chai";

import { BuidlerContext } from "../../../../src/internal/context";
import { loadConfigAndTasks } from "../../../../src/internal/core/config/config-loading";
import { ERRORS } from "../../../../src/internal/core/errors";
import { resetBuidlerContext } from "../../../../src/internal/reset";
import { useEnvironment } from "../../../helpers/environment";
import { expectBuidlerError } from "../../../helpers/errors";
import { useFixtureProject } from "../../../helpers/project";

describe("Config extensions", function() {
describe("Valid extenders", function() {
useFixtureProject("config-extensions");
useEnvironment();

it("Should expose the new values", function() {
const config: any = this.env.config;
assert.isDefined(config.values);
});

it("Should execute extenders in order", function() {
const config: any = this.env.config;
assert.deepEqual(config.values, [1, 2]);
});
});

describe("Invalid extensions", function() {
useFixtureProject("invalid-config-extension");

beforeEach(function() {
BuidlerContext.createBuidlerContext();
});

afterEach(function() {
resetBuidlerContext();
});

it("Should throw the right error when trying to modify the user config", function() {
expectBuidlerError(
() => loadConfigAndTasks(),
ERRORS.GENERAL.USER_CONFIG_MODIFIED
);
});

it("Should have the right property path", function() {
assert.throws(() => loadConfigAndTasks(), "userConfig.networks.asd");
});
});
});