hardhat
Version:
Hardhat is an extensible developer tool that helps smart contract developers increase productivity by reliably bringing together the tools they want.
248 lines (213 loc) • 8.38 kB
text/typescript
import util, { InspectOptions } from "util";
import { HardhatError } from "../core/errors";
import { ERRORS } from "../core/errors-list";
const inspect = Symbol.for("nodejs.util.inspect.custom");
/**
* This module provides function to implement proxy-based object, functions, and
* classes (they are functions). They receive an initializer function that it's
* not used until someone interacts with the lazy element.
*
* This functions can also be used like a lazy `require`, creating a proxy that
* doesn't require the module until needed.
*
* The disadvantage of using this technique is that the type information is
* lost wrt `import`, as `require` returns an `any. If done with enough care,
* this can be manually fixed.
*
* TypeScript doesn't emit `require` calls for modules that are imported only
* because of their types. So if one uses lazyObject or lazyFunction along with
* a normal ESM import you can pass the module's type to this function.
*
* An example of this can be:
*
* import findUpT from "find-up";
* export const findUp = lazyFunction<typeof findUpT>(() => require("find-up"));
*
* You can also use it with named exports:
*
* import { EthT } from "web3x/eth";
* const Eth = lazyFunction<typeof EthT>(() => require("web3x/eth").Eth);
*/
export function lazyObject<T extends object>(objectCreator: () => T): T {
return createLazyProxy(
objectCreator,
(getRealTarget) => ({
[inspect](
depth: number,
options: InspectOptions,
inspectFn: (
object: any,
options: InspectOptions
) => string = util.inspect
) {
const realTarget = getRealTarget();
const newOptions = { ...options, depth };
return inspectFn(realTarget, newOptions);
},
}),
(object) => {
if (object instanceof Function) {
throw new HardhatError(ERRORS.GENERAL.UNSUPPORTED_OPERATION, {
operation: "Creating lazy functions or classes with lazyObject",
});
}
if (typeof object !== "object" || object === null) {
throw new HardhatError(ERRORS.GENERAL.UNSUPPORTED_OPERATION, {
operation: "Using lazyObject with anything other than objects",
});
}
}
);
}
// eslint-disable-next-line @typescript-eslint/ban-types
export function lazyFunction<T extends Function>(functionCreator: () => T): T {
return createLazyProxy(
functionCreator,
(getRealTarget) => {
function dummyTarget() {}
(dummyTarget as any)[inspect] = function (
depth: number,
options: InspectOptions,
inspectFn: (
object: any,
options: InspectOptions
) => string = util.inspect
) {
const realTarget = getRealTarget();
const newOptions = { ...options, depth };
return inspectFn(realTarget, newOptions);
};
return dummyTarget;
},
(object) => {
if (!(object instanceof Function)) {
throw new HardhatError(ERRORS.GENERAL.UNSUPPORTED_OPERATION, {
operation:
"Using lazyFunction with anything other than functions or classes",
});
}
}
);
}
function createLazyProxy<ActualT extends GuardT, GuardT extends object>(
targetCreator: () => ActualT,
dummyTargetCreator: (getRealTarget: () => ActualT) => GuardT,
validator: (target: any) => void
): ActualT {
let realTarget: ActualT | undefined;
const dummyTarget: ActualT = dummyTargetCreator(getRealTarget) as any;
function getRealTarget(): ActualT {
if (realTarget === undefined) {
const target = targetCreator();
validator(target);
// We copy all properties. We won't use them, but help us avoid Proxy
// invariant violations
const properties = Object.getOwnPropertyNames(target);
for (const property of properties) {
const descriptor = Object.getOwnPropertyDescriptor(target, property)!;
Object.defineProperty(dummyTarget, property, descriptor);
}
Object.setPrototypeOf(dummyTarget, Object.getPrototypeOf(target));
// Using a null prototype seems to tirgger a V8 bug, so we forbid it
// See: https://github.com/nodejs/node/issues/29730
if (Object.getPrototypeOf(target) === null) {
throw new HardhatError(ERRORS.GENERAL.UNSUPPORTED_OPERATION, {
operation:
"Using lazyFunction or lazyObject to construct objects/functions with prototype null",
});
}
if (!Object.isExtensible(target)) {
Object.preventExtensions(dummyTarget);
}
realTarget = target;
}
return realTarget;
}
const handler: ProxyHandler<ActualT> = {
defineProperty(target, property, descriptor) {
Reflect.defineProperty(dummyTarget, property, descriptor);
return Reflect.defineProperty(getRealTarget(), property, descriptor);
},
deleteProperty(target, property) {
Reflect.deleteProperty(dummyTarget, property);
return Reflect.deleteProperty(getRealTarget(), property);
},
get(target, property, receiver) {
// We have this short-circuit logic here to avoid a cyclic require when
// loading Web3.js.
//
// If a lazy object is somehow accessed while its real target is being
// created, it would trigger an endless loop of recreation, which node
// detects and resolve to an empty object.
//
// This happens with Web3.js because we a lazyObject that loads it,
// and expose it as `global.web3`. This Web3.js file accesses
// `global.web3` when it's being loaded, triggering the loop we mentioned
// before: https://github.com/ethereum/web3.js/blob/8574bd3bf11a2e9cf4bcf8850cab13e1db56653f/packages/web3-core-requestmanager/src/givenProvider.js#L41
//
// We just return `undefined` in that case, to not enter into the loop.
//
// **SUPER IMPORTANT NOTE:** Removing this is very tempting, I know. This
// is a horrible hack. The most obvious approach for doing so is to
// remove the `global` elements that trigger this crazy behavior right
// before doing our `require("web3")`, and restore them afterwards.
// **THIS IS NOT ENOUGH** Users, and libraries (!!!!), will have their own
// `require`s that we can't control and will trigger the same bug.
const stack = new Error().stack;
if (
stack !== undefined &&
stack.includes("givenProvider.js") &&
realTarget === undefined
) {
return undefined;
}
return Reflect.get(getRealTarget(), property, receiver);
},
getOwnPropertyDescriptor(target, property) {
const descriptor = Reflect.getOwnPropertyDescriptor(
getRealTarget(),
property
);
if (descriptor !== undefined) {
Object.defineProperty(dummyTarget, property, descriptor);
}
return descriptor;
},
getPrototypeOf(_target) {
return Reflect.getPrototypeOf(getRealTarget());
},
has(target, property) {
return Reflect.has(getRealTarget(), property);
},
isExtensible(_target) {
return Reflect.isExtensible(getRealTarget());
},
ownKeys(_target) {
return Reflect.ownKeys(getRealTarget());
},
preventExtensions(_target) {
Object.preventExtensions(dummyTarget);
return Reflect.preventExtensions(getRealTarget());
},
set(target, property, value, receiver) {
Reflect.set(dummyTarget, property, value, receiver);
return Reflect.set(getRealTarget(), property, value, receiver);
},
setPrototypeOf(target, prototype) {
Reflect.setPrototypeOf(dummyTarget, prototype);
return Reflect.setPrototypeOf(getRealTarget(), prototype);
},
};
if (dummyTarget instanceof Function) {
// If dummy target is a function, the actual target must be a function too.
handler.apply = (target, thisArg: any, argArray?: any) => {
// eslint-disable-next-line @typescript-eslint/ban-types
return Reflect.apply(getRealTarget() as Function, thisArg, argArray);
};
handler.construct = (target, argArray: any, _newTarget?: any) => {
// eslint-disable-next-line @typescript-eslint/ban-types
return Reflect.construct(getRealTarget() as Function, argArray);
};
}
return new Proxy(dummyTarget, handler);
}