react-ioc
Version:
Hierarchical Dependency Injection for React
584 lines (575 loc) • 17.3 kB
JavaScript
import "reflect-metadata";
import { __extends } from "tslib";
import hoistNonReactStatics from "hoist-non-react-statics";
import {
Component,
createContext,
createElement,
useContext,
useRef
} from "react";
function isFunction(arg) {
return typeof arg === "function";
}
function isObject(arg) {
return arg && typeof arg === "object";
}
function isString(arg) {
return typeof arg === "string";
}
function isSymbol(arg) {
return typeof arg === "symbol";
}
function isToken(arg) {
return isFunction(arg) || isObject(arg) || isString(arg) || isSymbol(arg);
}
function isReactComponent(prototype) {
return isObject(prototype) && isObject(prototype.isReactComponent);
}
function isValidMetadata(arg) {
return (
isFunction(arg) &&
[Object, Function, Number, String, Boolean].indexOf(arg) === -1
);
}
function getDebugName(value) {
if (isFunction(value)) {
return String(value.displayName || value.name);
}
if (isObject(value) && isFunction(value.constructor)) {
return String(value.constructor.name);
}
return String(value);
}
function logError(message) {
try {
throw new Error(message);
} catch (e) {
console.error(e);
}
}
function logIncorrectBinding(token, binding) {
var tokenName = getDebugName(token);
var bindingName = getDebugName(binding);
logError("Binding [" + tokenName + ", " + bindingName + "] is incorrect.");
}
function logNotFoundDependency(token) {
var name = getDebugName(token);
logError(
"Dependency " +
name +
" is not found.\nPlease register " +
name +
" in some Provider e.g.\n@provider([" +
name +
", " +
name +
"])\nclass App extends React.Component { /*...*/ }"
);
}
function logNotFoundProvider(target) {
if (isReactComponent(target)) {
var name_1 = getDebugName(target);
logError(
"Provider is not found.\n Please define Provider and set " +
name_1 +
".contextType = InjectorContext e.g.\n @provider([MyService, MyService])\n class App extends React.Component { /*...*/ }\n class " +
name_1 +
" extends React.Component {\n static contextType = InjectorContext;\n }"
);
} else {
logError(
"Provider is not found.\n Please define Provider e.g.\n @provider([MyService, MyService])\n class App extends React.Component { /*...*/ }"
);
}
}
function logInvalidMetadata(target, token) {
var tokenName = getDebugName(token);
var targetName = getDebugName(target);
logError(
tokenName +
" is not a valid dependency.\nPlease specify ES6 class as property type e.g.\nclass MyService {}\nclass " +
targetName +
" {\n @inject myService: MyService;\n}"
);
}
/** @typedef {import("./types").Token} Token */
/** React Context for Injector */
var InjectorContext = createContext(null);
/**
* Dependency injection container
* @internal
*/
var Injector = /** @class */ (function(_super) {
__extends(Injector, _super);
function Injector() {
return (_super !== null && _super.apply(this, arguments)) || this;
}
return Injector;
})(Component);
/**
* Find Injector for passed object and cache it inside this object
* @internal
* @param {Object} target The object in which we inject value
* @returns {Injector}
*/
function getInjector(target) {
var injector = target[INJECTOR];
if (injector) {
return injector;
}
injector = currentInjector || target.context;
if (injector instanceof Injector) {
target[INJECTOR] = injector;
return injector;
}
return null;
}
/** @type {Injector} */
var currentInjector = null;
/* istanbul ignore next */
var INJECTOR = typeof Symbol === "function" ? Symbol() : "__injector__";
/**
* Resolve a class instance that registered by some Provider in hierarchy.
* Instance is cached in Provider that registers it's class.
* @internal
* @param {Injector} injector Injector instance
* @param {Token} token Dependency injection token
* @returns {Object} Resolved class instance
*/
function getInstance(injector, token) {
if (registrationQueue.length > 0) {
registrationQueue.forEach(function(registration) {
registration();
});
registrationQueue.length = 0;
}
while (injector) {
var instance = injector._instanceMap.get(token);
if (instance !== undefined) {
return instance;
}
var binding = injector._bindingMap.get(token);
if (binding) {
var prevInjector = currentInjector;
currentInjector = injector;
try {
instance = binding(injector);
} finally {
currentInjector = prevInjector;
}
injector._instanceMap.set(token, instance);
return instance;
}
injector = injector._parent;
}
if (process.env.NODE_ENV !== "production") {
logNotFoundDependency(token);
}
return undefined;
}
/** @type {Function[]} */
var registrationQueue = [];
/** @typedef {import("./types").Token} Token */
/**
* Property decorator that resolves a class instance
* which registered by some Provider in hierarchy.
* Instance is cached in Provider that registers it's class.
* @param {Token | Object} [targetOrToken] Object or Class prototype or dependency injection token
* @param {string | symbol | Function} [keyOrToken] Property key or dependency injection token
*/
function inject(targetOrToken, keyOrToken) {
if (isFunction(keyOrToken)) {
return injectFunction(targetOrToken, keyOrToken);
}
/** @type {Token} */
var token;
if (!keyOrToken) {
token = targetOrToken;
return injectDecorator;
}
return injectDecorator(targetOrToken, keyOrToken);
function injectDecorator(prototype, key) {
if (process.env.NODE_ENV !== "production") {
defineContextType(prototype);
} else {
prototype.constructor.contextType = InjectorContext;
}
if (!token) {
token = Reflect.getMetadata("design:type", prototype, key);
if (process.env.NODE_ENV !== "production") {
if (!isValidMetadata(token)) {
logInvalidMetadata(targetOrToken, token);
}
}
}
var descriptor = {
configurable: true,
enumerable: true,
get: function() {
var instance = injectFunction(this, token);
Object.defineProperty(this, key, {
enumerable: true,
writable: true,
value: instance
});
return instance;
},
set: function(instance) {
Object.defineProperty(this, key, {
enumerable: true,
writable: true,
value: instance
});
}
};
Object.defineProperty(prototype, key, descriptor);
return descriptor;
}
}
/**
* Resolve a class instance that registered by some Provider in hierarchy.
* Instance is cached in Provider that registers it's class.
* @internal
* @param {Object} target The object in which we inject class instance
* @param {Token} token Dependency injection token
* @returns {Object} Resolved class instance
*/
function injectFunction(target, token) {
var injector = getInjector(target);
if (process.env.NODE_ENV !== "production") {
if (!injector) {
logNotFoundProvider(target);
}
}
return getInstance(injector, token);
}
/**
* Set Class.contextType = InjectorContext
* @internal
* @param {Object} prototype React Component prototype
*/
function defineContextType(prototype) {
if (isReactComponent(prototype)) {
var constructor = prototype.constructor;
var className_1 = getDebugName(constructor);
if (constructor.contextType !== InjectorContext) {
if (constructor.contextType) {
logError(
"Decorator tries to overwrite existing " +
className_1 +
".contextType"
);
} else {
Object.defineProperty(constructor, "contextType", {
get: function() {
return InjectorContext;
},
set: function() {
logError(
"You are trying to overwrite " +
className_1 +
".contextType = InjectorContext"
);
}
});
}
}
}
}
/** @typedef {import("./types").Definition} Definition */
/** @typedef {import("./types").Token} Token */
/**
* Bind type to specified class.
* @param {new (...args) => any} constructor
* @return {Function}
*/
function toClass(constructor) {
if (process.env.NODE_ENV !== "production") {
if (!isFunction(constructor)) {
logError(
"Class " + getDebugName(constructor) + " is not a valid dependency"
);
}
}
return asBinding(function(injector) {
var instance = new constructor();
if (!instance[INJECTOR]) {
instance[INJECTOR] = injector;
}
return instance;
});
}
/**
* Bind type to specified factory funciton.
* @param {any} depsOrFactory Dependencies or factory
* @param {Function} [factory] Factory
* @return {Function}
*/
function toFactory(depsOrFactory, factory) {
if (process.env.NODE_ENV !== "production") {
if (factory) {
if (!Array.isArray(depsOrFactory)) {
logError(
"Dependency array " + getDebugName(depsOrFactory) + " is invalid"
);
}
if (!isFunction(factory)) {
logError(
"Factory " + getDebugName(factory) + " is not a valid dependency"
);
}
} else if (!isFunction(depsOrFactory)) {
logError(
"Factory " + getDebugName(depsOrFactory) + " is not a valid dependency"
);
}
}
return asBinding(
factory
? function(injector) {
return factory.apply(
void 0,
depsOrFactory.map(function(token) {
return getInstance(injector, token);
})
);
}
: depsOrFactory
);
}
/**
* Bind type to specified value.
* @param {any} value
* @return {Function}
*/
function toValue(value) {
if (process.env.NODE_ENV !== "production") {
if (value === undefined) {
logError("Please specify some value");
}
}
return asBinding(function() {
return value;
});
}
/**
* Bind type to existing instance located by token.
* @param {Token} token
* @return {Function}
*/
function toExisting(token) {
if (process.env.NODE_ENV !== "production") {
if (!isFunction(token)) {
logError(
"Token " +
getDebugName(token) +
" is not a valid dependency injection token"
);
}
}
return asBinding(function(injector) {
return getInstance(injector, token);
});
}
/* istanbul ignore next */
var IS_BINDING = typeof Symbol === "function" ? Symbol() : "__binding__";
/**
* Mark function as binding function.
* @internal
* @param {Function} binding
* @returns {Function}
*/
function asBinding(binding) {
binding[IS_BINDING] = true;
return binding;
}
/**
* Add bindings to bindings Map
* @internal
* @param {Map<Token, Function>} bindingMap
* @param {Definition[]} definitions
*/
function addBindings(bindingMap, definitions) {
definitions.forEach(function(definition) {
var _a;
var token, binding;
if (Array.isArray(definition)) {
(token = definition[0]),
(_a = definition[1]),
(binding = _a === void 0 ? token : _a);
} else {
token = binding = definition;
}
if (process.env.NODE_ENV !== "production") {
if (!isToken(token) || !isFunction(binding)) {
logIncorrectBinding(token, binding);
}
}
// @ts-ignore
bindingMap.set(token, binding[IS_BINDING] ? binding : toClass(binding));
});
}
/** @typedef {import("./types").Definition} Definition */
/** @typedef {import("./types").Token} Token */
/**
* HOC that registers dependency injection bindings in scope of decorated component
* @param {...Definition} definitions Dependency injection configuration
*/
var provider = function() {
var definitions = [];
for (var _i = 0; _i < arguments.length; _i++) {
definitions[_i] = arguments[_i];
}
return function(Wrapped) {
/** @type {Map<Token, Function>} */
var bindingMap = new Map();
addBindings(bindingMap, definitions);
var Provider = /** @class */ (function(_super) {
__extends(Provider, _super);
function Provider() {
var _this = (_super !== null && _super.apply(this, arguments)) || this;
_this._parent = _this.context;
_this._bindingMap = bindingMap;
_this._instanceMap = new Map();
return _this;
}
Provider.prototype.componentWillUnmount = function() {
this._instanceMap.forEach(function(instance) {
if (isObject(instance) && isFunction(instance.dispose)) {
instance.dispose();
}
});
};
Provider.prototype.render = function() {
return createElement(
InjectorContext.Provider,
{ value: this },
createElement(Wrapped, this.props)
);
};
/**
* Register dependency injection bindings in scope of decorated class
* @param {...Definition} definitions Dependency injection configuration
*/
Provider.register = function() {
var definitions = [];
for (var _i = 0; _i < arguments.length; _i++) {
definitions[_i] = arguments[_i];
}
addBindings(bindingMap, definitions);
};
Provider.WrappedComponent = Wrapped;
return Provider;
})(Injector);
if (process.env.NODE_ENV !== "production") {
Provider.displayName =
"Provider(" + (Wrapped.displayName || Wrapped.name) + ")";
Object.defineProperty(Provider, "contextType", {
get: function() {
return InjectorContext;
},
set: function() {
logError(
"You are trying to overwrite " +
Provider.displayName +
".contextType = InjectorContext"
);
}
});
} else {
Provider.contextType = InjectorContext;
}
// static fields from component should be visible on the generated Consumer
return hoistNonReactStatics(Provider, Wrapped);
};
};
/**
* Register class in specified provider.
* @typedef {{ register(constructor: Function): void }} Provider
* @param {() => Provider} getProvider Function that returns some provider
* @param {Function} [binding] Dependency injection binding
*/
var registerIn = function(getProvider, binding) {
return function(constructor) {
registrationQueue.push(function() {
if (process.env.NODE_ENV !== "production") {
var provider_1 = getProvider();
if (
!isFunction(provider_1) ||
!(provider_1.prototype instanceof Injector)
) {
logError(
getDebugName(provider_1) +
" is not a valid Provider. Please use:\n" +
"@registerIn(() => MyProvider)\n" +
("class " + getDebugName(constructor) + " {}\n")
);
} else {
provider_1.register(binding ? [constructor, binding] : constructor);
}
} else {
getProvider().register(binding ? [constructor, binding] : constructor);
}
});
return constructor;
};
};
/** @typedef {import("./types").Token} Token */
/**
* React hook for resolving a class instance that registered by some Provider in hierarchy.
* Instance is cached in Provider that registers it's class.
* @param {Token} token Dependency injection token
* @returns {Object} Resolved class instance
*/
function useInstance(token) {
var ref = useRef(null);
var injector = useContext(InjectorContext);
if (process.env.NODE_ENV !== "production") {
if (!injector) {
logNotFoundProvider();
}
}
return ref.current || (ref.current = getInstance(injector, token));
}
/**
* React hook for resolving a class instances that registered by some Provider in hierarchy.
* Instances are cached in Provider that registers it's classes.
* @param {...Token} tokens Dependency injection tokens
* @returns {Object[]} Resolved class instances
*/
function useInstances() {
var tokens = [];
for (var _i = 0; _i < arguments.length; _i++) {
tokens[_i] = arguments[_i];
}
var ref = useRef(null);
var injector = useContext(InjectorContext);
if (process.env.NODE_ENV !== "production") {
if (!injector) {
logNotFoundProvider();
}
}
return (
ref.current ||
(ref.current = tokens.map(function(token) {
return getInstance(injector, token);
}))
);
}
export {
inject,
provider,
registerIn,
inject as Inject,
provider as Provider,
registerIn as RegisterIn,
InjectorContext,
toClass,
toFactory,
toExisting,
toValue,
useInstance,
useInstances
};
//# sourceMappingURL=index.esm.js.map