UNPKG

reactant-di

Version:

A dependency injection lib for Reactant

749 lines (723 loc) 26.2 kB
'use strict'; require('reflect-metadata'); var inversify = require('inversify'); /****************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ /* global Reflect, Promise, SuppressedError, Symbol, Iterator */ var extendStatics = function(d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; return extendStatics(d, b); }; function __extends(d, b) { if (typeof b !== "function" && b !== null) throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); } var __assign = function() { __assign = Object.assign || function __assign(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; function __values(o) { var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0; if (m) return m.call(o); if (o && typeof o.length === "number") return { next: function () { if (o && i >= o.length) o = void 0; return { value: o && o[i++], done: !o }; } }; throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); } function __read(o, n) { var m = typeof Symbol === "function" && o[Symbol.iterator]; if (!m) return o; var i = m.call(o), r, ar = [], e; try { while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); } catch (error) { e = { error: error }; } finally { try { if (r && !r.done && (m = i["return"])) m.call(i); } finally { if (e) throw e.error; } } return ar; } function __spreadArray(to, from, pack) { if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { if (ar || !(i in from)) { if (!ar) ar = Array.prototype.slice.call(from, 0, i); ar[i] = from[i]; } } return to.concat(ar || Array.prototype.slice.call(from)); } typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { var e = new Error(message); return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; }; var defaultUndefinedValue = Symbol('defaultUndefined'); /** * > NOTE: does not support Changing dependencies without `@inject`. */ var Optional = /** @class */ (function () { function Optional(identifier) { this.identifier = identifier; } Object.defineProperty(Optional.prototype, "key", { get: function () { return defaultUndefinedValue; }, enumerable: false, configurable: true }); return Optional; }()); var getMetadata = function (metaKey) { return Reflect.getMetadata(metaKey, Reflect) || new Map(); }; var setMetadata = function (metaKey, target, serviceIdentifier) { if (serviceIdentifier === void 0) { serviceIdentifier = target; } var providesMeta = getMetadata(metaKey); providesMeta.set(serviceIdentifier || target, target); Reflect.defineMetadata(metaKey, providesMeta, Reflect); }; var modulesDeps; var setModulesDeps = function (deps) { modulesDeps = deps; }; var lookupServiceIdentifier = function (target, original, index) { var e_1, _a; if (typeof index === 'undefined') return original; try { for (var modulesDeps_1 = __values(modulesDeps), modulesDeps_1_1 = modulesDeps_1.next(); !modulesDeps_1_1.done; modulesDeps_1_1 = modulesDeps_1.next()) { var modulesDep = modulesDeps_1_1.value; if (typeof modulesDep === 'object') { var deps = modulesDep.deps; if ((modulesDep.provide === target || modulesDep.useClass === target) && Array.isArray(deps) && typeof deps[index] !== 'undefined') { if (deps[index] instanceof Optional) { return deps[index].key; } return deps[index]; } } } } catch (e_1_1) { e_1 = { error: e_1_1 }; } finally { try { if (modulesDeps_1_1 && !modulesDeps_1_1.done && (_a = modulesDeps_1.return)) _a.call(modulesDeps_1); } finally { if (e_1) throw e_1.error; } } return original; }; var lookupOptionalIdentifier = function (serviceIdentifier) { var e_2, _a; try { for (var modulesDeps_2 = __values(modulesDeps), modulesDeps_2_1 = modulesDeps_2.next(); !modulesDeps_2_1.done; modulesDeps_2_1 = modulesDeps_2.next()) { var modulesDep = modulesDeps_2_1.value; var deps = modulesDep.deps; if (typeof modulesDep === 'object' && Array.isArray(deps) && (modulesDep.useClass || Object.keys(modulesDep).length === 2)) { return !!deps.filter(function (dep) { return dep instanceof Optional && dep.identifier === serviceIdentifier; }).length; } } } catch (e_2_1) { e_2 = { error: e_2_1 }; } finally { try { if (modulesDeps_2_1 && !modulesDeps_2_1.done && (_a = modulesDeps_2.return)) _a.call(modulesDeps_2); } finally { if (e_2) throw e_2.error; } } return false; }; function lookupServiceIdentifiers(request, ServiceIdentifiers) { // It is used to perform the required dependency collection when dynamically importing modules. if (!ServiceIdentifiers.has(request.serviceIdentifier)) { ServiceIdentifiers.set(request.serviceIdentifier, []); } var depServiceIdentifier = request.childRequests.map(function (childRequest) { return childRequest.serviceIdentifier; }); ServiceIdentifiers.set(request.serviceIdentifier, depServiceIdentifier); if (request.childRequests.length === 0) return; request.childRequests.forEach(function (childRequest) { lookupServiceIdentifiers(childRequest, ServiceIdentifiers); }); } function createCollector(ServiceIdentifiers) { return function (planAndResolve) { return function (args) { var nextContextInterceptor = args.contextInterceptor; var contextInterceptor = function (context) { lookupServiceIdentifiers(context.plan.rootRequest, ServiceIdentifiers); return nextContextInterceptor(context); }; var result = planAndResolve(__assign(__assign({}, args), { contextInterceptor: contextInterceptor })); return result; }; }; } var METADATA_KEY = { optional: 'reactant:optional', provide: 'reactant:provide', multiple: 'reactant:multiple', lazy: 'reactant:lazy', paramtypes: 'design:paramtypes', inversifyParamtypes: 'inversify:paramtypes', inversifyTagged: 'inversify:tagged', }; var forwardRef = function (callback) { return new inversify.LazyServiceIdentifer(callback); }; /** * ## Description * * You can use `@inject()` to perform the required dependency injection module to decorate in the constructor of an injectable class. * * If the default is a dependency injection of the class itself as a type, e.g. `@inject(Foo) foo: Foo`, then it is exactly the same as `foo: Foo`. * * ## Example * * ```ts * @injectable() * class Bar { * getValue() { * return 'bar'; * } * } * * @injectable() * class Foo { * getValue() { * return 'foo'; * } * } * * @injectable() * class FooBar { * constructor(@inject() public bar: Bar, @inject('foo') public foo: Foo) {} * } * * const fooBar = testBed({ * modules: [ * Bar, * { provide: 'foo', useClass: Foo }, * ], * main: FooBar, * }); * * expect(fooBar.instance.foo.getValue()).toBe('foo'); * ``` */ function inject(serviceIdentifierOrFunc) { return function (target, key, index) { var self = Reflect.getMetadata(METADATA_KEY.paramtypes, target)[index]; var serviceIdentifier; if (serviceIdentifierOrFunc instanceof inversify.LazyServiceIdentifer) { serviceIdentifier = serviceIdentifierOrFunc; } else if (typeof serviceIdentifierOrFunc === 'undefined') { serviceIdentifier = forwardRef(function () { return lookupServiceIdentifier(target, self, index); }); } else { serviceIdentifier = forwardRef(function () { return lookupServiceIdentifier(target, serviceIdentifierOrFunc, index); }); } inversify.decorate(inversify.inject(serviceIdentifier), target, index); }; } /** * ## Description * * You can use `@optional()` to decorate an optionally injected module. * * If other modules have no relevant dependency injection or are also optionally injected, the module will not be injected by default unless the injected module is imported in the `modules` parameter. * * ## Example * * ```ts * @injectable() * class Bar { * getValue() { * return 'bar'; * } * } * * @injectable() * class Foo { * getValue() { * return 'foo'; * } * } * * @injectable() * class FooBar { * constructor(@optional() public bar: Bar, @optional('foo') public foo: Foo) {} * } * * const fooBar = testBed({ * modules: [ * { provide: 'foo', useClass: Foo }, * ], * main: FooBar, * }); * * expect(fooBar.instance.foo.getValue()).toBe('foo'); * expect(fooBar.fooBar.bar).toBeUndefined(); * ``` */ function optional(serviceIdentifier) { return function (target, key, index) { var paramtypes = Reflect.getMetadata(METADATA_KEY.paramtypes, target); setMetadata(METADATA_KEY.optional, paramtypes[index], serviceIdentifier); inversify.decorate(inject(serviceIdentifier), target, index); inversify.decorate(inversify.optional(), target, index); }; } function multiInject(serviceIdentifier) { return function (target, key, index) { var paramtypes = Reflect.getMetadata(METADATA_KEY.paramtypes, target); setMetadata(METADATA_KEY.multiple, paramtypes === null || paramtypes === void 0 ? void 0 : paramtypes[index], serviceIdentifier); inversify.decorate(inversify.multiInject(serviceIdentifier), target, index); }; } function multiOptional(serviceIdentifier) { return function (target, key, index) { var paramtypes = Reflect.getMetadata(METADATA_KEY.paramtypes, target); setMetadata(METADATA_KEY.optional, paramtypes === null || paramtypes === void 0 ? void 0 : paramtypes[index], serviceIdentifier); setMetadata(METADATA_KEY.multiple, paramtypes === null || paramtypes === void 0 ? void 0 : paramtypes[index], serviceIdentifier); inversify.decorate(multiInject(serviceIdentifier), target, index); inversify.decorate(inversify.optional(), target, index); }; } /** * ## Description * * You can use `@injectable()` to decorate an injectable module, which will also allow `emitDecoratorMetadata` to take effect in the decorated class, so the corresponding `@inject()` is optional. * * But if you don't want to use `@injectable()`, then the dependency injection decorator for constructors such as `@inject()` is required, and it must be imported in the corresponding `modules` startup configuration. Therefore, in most cases, it is recommended to use `@injectable()`. * * ## Example * * ```ts * @injectable() * class Bar { * getValue() { * return 'bar'; * } * } * * class Foo { * constructor(@inject() public bar: Bar) {} * } * * @injectable() * class FooBar { * constructor(public bar: Bar, public foo: Foo) {} * } * * const fooBar = testBed({ * modules: [ * Foo // `Foo` is required, but `Bar` will be injected automatically. * ], * main: FooBar, * }); * * expect(fooBar.instance.foo.getValue()).toBe('foo'); * ``` * * If you use JavaScript, then you can only use `@injectable()` to define the full dependency metadata. * * ```js * @injectable() * class Bar { * getValue() { * return 'bar'; * } * } * * @injectable() * class Foo { * getValue() { * return 'foo'; * } * } * * @injectable({ * deps: [Bar, { provide: 'foo' }], * }) * class FooBar { * constructor(bar, foo) { * this.bar = bar; * this.foo = foo; * } * } * * const fooBar = testBed({ * modules: [ * Bar, * { provide: 'foo', useClass: Foo }, * ], * main: FooBar, * }); * * expect(fooBar.instance.foo.getValue()).toBe('foo'); * ``` */ function injectable(options) { if (options === void 0) { options = {}; } return function (target) { var _a = options.deps, deps = _a === void 0 ? [] : _a; deps.forEach(function (option, index) { if (typeof option === 'function') { inversify.decorate(inject(option), target, index); } else if (toString.call(option) === '[object Object]') { if (typeof option.provide === 'undefined' || option.provide === null) { throw new Error("@injectable ".concat(option ? JSON.stringify(option) : option, " option error, please set a valid value for 'provide'")); } if (option.optional && !option.multi) { inversify.decorate(optional(option.provide), target, index); } else if (option.multi && !option.optional) { inversify.decorate(multiInject(option.provide), target, index); } else if (option.multi && option.optional) { inversify.decorate(multiOptional(option.provide), target, index); } else { inversify.decorate(inject(option.provide), target, index); } } else { throw new Error("@injectable ".concat(option === null || option === void 0 ? void 0 : option.toString(), " option error")); } }); // it has to use `Reflect.getMetadata` with metadata, it just get all injectable deps. // so add the services set for `injectable` services. setMetadata(METADATA_KEY.provide, target, target); inversify.decorate(inversify.injectable(), target); return target; }; } /** * ## Description * * You can get a decorator `@lazy(serviceIdentifier)` with `getLazyDecorator((serviceIdentifier) => container.get(serviceIdentifier))`, * and use it on any one dependency property that you need to lazily get. * * ## Example * * ```ts * let container: Container; * const lazy = getLazyDecorator((serviceIdentifier) => * container.get(serviceIdentifier) * ); * * @injectable() * class Foo { * public get test() { * return 'test'; * } * } * * @injectable() * class Bar { * @lazy('foo') * foo?: Foo; * } * * container = createContainer({ * ServiceIdentifiers: new Map(), * }); * * const bar = container.get(Bar); * * container.bind('foo').to(Foo); * expect(bar.foo?.test).toBe('test'); * ``` */ var getLazyDecorator = function (getService) { return function (serviceIdentifier, enableCache) { if (enableCache === void 0) { enableCache = true; } return function (target, key) { function getter() { if (enableCache && !Reflect.hasMetadata(METADATA_KEY.lazy, this, key)) { var service = getService(serviceIdentifier, this); if (service !== null) { Reflect.defineMetadata(METADATA_KEY.lazy, service, this, key); } } if (Reflect.hasMetadata(METADATA_KEY.lazy, this, key)) { return Reflect.getMetadata(METADATA_KEY.lazy, this, key); } return getService(serviceIdentifier, this); } function setter(newVal) { if (enableCache) { Reflect.defineMetadata(METADATA_KEY.lazy, newVal, this, key); } else { console.warn("\n Disable cache and the property ".concat(key.toString(), " in class \"").concat(this.constructor.name, "\" instance failed to set value.\n ")); } } // It should be compatible with the TS decorator and the babel decorator return { configurable: true, enumerable: true, get: getter, set: setter, }; }; }; }; /** * > Make sure that `Container` type for getting module. */ var ModuleRef = /** @class */ (function (_super) { __extends(ModuleRef, _super); function ModuleRef() { return _super !== null && _super.apply(this, arguments) || this; } return ModuleRef; }(inversify.Container)); var CustomMetadataReader = /** @class */ (function (_super) { __extends(CustomMetadataReader, _super); function CustomMetadataReader() { return _super !== null && _super.apply(this, arguments) || this; } CustomMetadataReader.prototype.getConstructorMetadata = function (constructorFunc) { var constructorMetadata = _super.prototype.getConstructorMetadata.call(this, constructorFunc); // TODO: hook return constructorMetadata; }; return CustomMetadataReader; }(inversify.MetadataReader)); function autoBindModules() { return new inversify.ContainerModule(function (bind, unbind, isBound) { var e_1, _a; var provideMeta = getMetadata(METADATA_KEY.provide); var optionalMeta = getMetadata(METADATA_KEY.optional); try { for (var provideMeta_1 = __values(provideMeta), provideMeta_1_1 = provideMeta_1.next(); !provideMeta_1_1.done; provideMeta_1_1 = provideMeta_1.next()) { var _b = __read(provideMeta_1_1.value, 2), identifier = _b[0], provide = _b[1]; // default injection without optional module. if ((!optionalMeta.has(identifier) || lookupOptionalIdentifier(identifier)) && !isBound(identifier)) { bind(identifier).to(provide); } } } catch (e_1_1) { e_1 = { error: e_1_1 }; } finally { try { if (provideMeta_1_1 && !provideMeta_1_1.done && (_a = provideMeta_1.return)) _a.call(provideMeta_1); } finally { if (e_1) throw e_1.error; } } }); } function isClassProvider(module) { return typeof module.useClass === 'function'; } function isFactoryProvider(module) { return typeof module.useFactory === 'function'; } /** * It ensures that the parameters of all modules from the configuration are decorated. * * class Foo { * constructor(bar: Bar) {} * } * * Equate to: * * class Foo { * constructor(@inject() bar: Bar) {} * } * * @param target {object} decorated target. */ function autoDecorateParams(target) { var metadata = Reflect.getMetadata(METADATA_KEY.inversifyParamtypes, target); metadata.forEach(function (_, index) { if (!Reflect.getMetadata(METADATA_KEY.inversifyTagged, target) || !Reflect.getMetadata(METADATA_KEY.inversifyTagged, target)[index]) { inject()(target, undefined, index); } }); } var Container = /** @class */ (function (_super) { __extends(Container, _super); function Container(options, _serviceIdentifiers) { var _this = _super.call(this, options) || this; _this._serviceIdentifiers = _serviceIdentifiers; return _this; } /** * get the loaded module */ Container.prototype.got = function (serviceIdentifier) { try { return this._serviceIdentifiers.has(serviceIdentifier) ? this.get(serviceIdentifier) : undefined; } catch (error) { if (process.env.NODE_ENV !== 'production') { console.warn(error); } return undefined; } }; /** * get loaded modules */ Container.prototype.gotAll = function (serviceIdentifier) { try { return this._serviceIdentifiers.has(serviceIdentifier) ? this.getAll(serviceIdentifier) : undefined; } catch (error) { if (process.env.NODE_ENV !== 'production') { console.warn(error); } return undefined; } }; return Container; }(inversify.Container)); function bindModules(container, modules) { var e_2, _a; var provideMeta = getMetadata(METADATA_KEY.provide); var _loop_1 = function (module_1) { if (typeof module_1 === 'function') { // auto decorate `@injectable` for module. if (!provideMeta.has(module_1)) inversify.decorate(injectable(), module_1); autoDecorateParams(module_1); container.bind(module_1).toSelf(); } else if (typeof module_1 === 'object') { if (isClassProvider(module_1)) { // auto decorate `@injectable` for module.useClass if (!provideMeta.has(module_1.useClass)) inversify.decorate(injectable(), module_1.useClass); autoDecorateParams(module_1.useClass); container.bind(module_1.provide).to(module_1.useClass); } else if (Object.hasOwnProperty.call(module_1, 'useValue')) { container .bind(module_1.provide) .toConstantValue(module_1.useValue); } else if (isFactoryProvider(module_1)) { container .bind(module_1.provide) .toFactory(function (context) { var deps = module_1.deps || []; var depInstances = deps.map(function (identifier) { // TODO: refactor with `is` assertion var provide = identifier.provide || identifier; if (identifier.optional && !context.container.isBound(identifier.provide)) { return undefined; } return context.container.get(provide); }); return module_1.useFactory.apply(module_1, __spreadArray([], __read(depInstances), false)); }); } else if (typeof module_1.provide === 'function') { // auto decorate `@injectable` for module.provide if (!provideMeta.has(module_1.provide)) inversify.decorate(injectable(), module_1.provide); autoDecorateParams(module_1.provide); container.bind(module_1.provide).toSelf(); } else { throw new Error("".concat(module_1, " option error")); } } else { throw new Error("".concat(module_1, " option error")); } }; try { for (var modules_1 = __values(modules), modules_1_1 = modules_1.next(); !modules_1_1.done; modules_1_1 = modules_1.next()) { var module_1 = modules_1_1.value; _loop_1(module_1); } } catch (e_2_1) { e_2 = { error: e_2_1 }; } finally { try { if (modules_1_1 && !modules_1_1.done && (_a = modules_1.return)) _a.call(modules_1); } finally { if (e_2) throw e_2.error; } } // load modules with `@injectable` decoration, but without `@optional` decoration. container.load(autoBindModules()); } function createContainer(_a) { var ServiceIdentifiers = _a.ServiceIdentifiers, _b = _a.modules, modules = _b === void 0 ? [] : _b, options = _a.options; setModulesDeps(modules); var container = new Container(options, ServiceIdentifiers); container.applyCustomMetadataReader(new CustomMetadataReader()); bindModules(container, modules); container.applyMiddleware(createCollector(ServiceIdentifiers)); if (container.isBound(ModuleRef)) { container.unbind(ModuleRef); } container.bind(ModuleRef).toConstantValue(container); container.bind(defaultUndefinedValue).toConstantValue(undefined); return container; } exports.METADATA_KEY = METADATA_KEY; exports.ModuleRef = ModuleRef; exports.Optional = Optional; exports.bindModules = bindModules; exports.createContainer = createContainer; exports.forwardRef = forwardRef; exports.getLazyDecorator = getLazyDecorator; exports.getMetadata = getMetadata; exports.inject = inject; exports.injectable = injectable; exports.multiInject = multiInject; exports.multiOptional = multiOptional; exports.optional = optional;