Skip to content

Commit

Permalink
Config extension/resolution mechanism for plugins (#325)
Browse files Browse the repository at this point in the history
* First iteration of config resolution extensions

* Use DeepReadOnly in ConfigExtender

* Add tests and improve freezing mechanisim

* Update resetBuidlerContext
  • Loading branch information
alcuadrado committed Sep 11, 2019
1 parent 719f110 commit 3d920db
Show file tree
Hide file tree
Showing 10 changed files with 153 additions and 6 deletions.
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");
});
});
});

0 comments on commit 3d920db

Please sign in to comment.