diff --git a/src/__tests__/compound-plugins.js b/src/__tests__/compound-plugins.js new file mode 100644 index 00000000..06e8053f --- /dev/null +++ b/src/__tests__/compound-plugins.js @@ -0,0 +1,163 @@ +/* @flow */ +import tape from 'tape-cup'; +import ClientAppFactory from '../client-app'; +import ServerAppFactory from '../server-app'; +import {createPlugin} from '../create-plugin'; +import {createToken, createArrayToken} from '../create-token'; +import type {FusionPlugin, Token, ArrayToken} from '../types.js'; + +const App = __BROWSER__ ? ClientAppFactory() : ServerAppFactory(); +type DepsType = { + dep: string, +}; +type AType = { + a: string, +}; + +const TokenA: ArrayToken = createArrayToken('TokenA'); +const TokenDep1: Token = createToken('TokenDep1'); +const TokenDep2: Token = createToken('TokenDep2'); + +tape('compound tokens support dependencies', t => { + const app = new App('el', el => el); + t.ok(app, 'creates an app'); + const counters = { + deps: 0, + a: 0, + b: 0, + }; + const PluginDeps: FusionPlugin<{}, DepsType> = createPlugin({ + provides: () => { + counters.deps++; + t.equal(counters.deps, 1, 'only instantiates once'); + return { + dep: 'PluginDep', + }; + }, + }); + + const PluginA: FusionPlugin = createPlugin({ + provides: () => { + counters.a++; + t.equal(counters.a, 1, 'only instantiates once'); + return { + a: 'PluginA', + }; + }, + }); + + type PluginBType = FusionPlugin<{dep: Token}, AType>; + const PluginB: PluginBType = createPlugin({ + deps: {dep: TokenDep1}, + provides: deps => { + counters.b++; + t.equal(deps.dep.dep, 'PluginDep'); + t.equal(counters.b, 1, 'only instantiates once'); + return { + a: 'PluginB', + }; + }, + }); + + app.register(TokenA, PluginA); + app.register(TokenA, PluginB); + // $FlowFixMe + app.register(TokenA, 'value'); + app.register(TokenDep1, PluginDeps); + app.register( + createPlugin({ + deps: {a: TokenA}, + provides: deps => { + t.equal(deps.a[0].a, 'PluginA'); + t.equal(deps.a[1].a, 'PluginB'); + t.equal(deps.a[2], 'value'); + }, + }) + ); + t.equal(counters.a, 0, 'does not instantiate until resolve is called'); + t.equal(counters.b, 0, 'does not instantiate until resolve is called'); + t.equal(counters.deps, 0, 'does not instantiate until resolve is called'); + app.resolve(); + t.equal(counters.a, 1, 'only instantiates once'); + t.equal(counters.b, 1, 'only instantiates once'); + t.equal(counters.deps, 1, 'only instantiates once'); + t.end(); +}); + +tape('dependency registration with aliases', t => { + const app = new App('el', el => el); + t.ok(app, 'creates an app'); + const counters = { + dep1: 0, + dep2: 0, + a: 0, + b: 0, + }; + const PluginDep1: FusionPlugin<{}, DepsType> = createPlugin({ + provides: () => { + counters.dep1++; + t.equal(counters.dep1, 1, 'only instantiates once'); + return { + dep: 'PluginDep1', + }; + }, + }); + const PluginDep2: FusionPlugin<{}, DepsType> = createPlugin({ + provides: () => { + counters.dep2++; + t.equal(counters.dep2, 1, 'only instantiates once'); + return { + dep: 'PluginDep2', + }; + }, + }); + + const PluginA: FusionPlugin<{dep: Token}, AType> = createPlugin({ + deps: {dep: TokenDep1}, + provides: deps => { + counters.a++; + t.equal(deps.dep.dep, 'PluginDep2'); + t.equal(counters.a, 1, 'only instantiates once'); + return { + a: 'PluginA', + }; + }, + }); + + type PluginBType = FusionPlugin<{dep: Token}, AType>; + const PluginB: PluginBType = createPlugin({ + deps: {dep: TokenDep1}, + provides: deps => { + counters.b++; + t.equal(deps.dep.dep, 'PluginDep2'); + t.equal(counters.b, 1, 'only instantiates once'); + return { + a: 'PluginB', + }; + }, + }); + + app.register(TokenA, PluginA); + app.register(TokenA, PluginB).alias(TokenDep1, TokenDep2); + app.register(TokenDep1, PluginDep1); + app.register(TokenDep2, PluginDep2); + app.register( + createPlugin({ + deps: {a: TokenA}, + provides: deps => { + t.equal(deps.a[0].a, 'PluginA'); + t.equal(deps.a[1].a, 'PluginB'); + }, + }) + ); + t.equal(counters.a, 0, 'does not instantiate until resolve is called'); + t.equal(counters.b, 0, 'does not instantiate until resolve is called'); + t.equal(counters.dep1, 0, 'does not instantiate until resolve is called'); + t.equal(counters.dep2, 0, 'does not instantiate until resolve is called'); + app.resolve(); + t.equal(counters.a, 1, 'only instantiates once'); + t.equal(counters.b, 1, 'only instantiates once'); + t.equal(counters.dep1, 1, 'only instantiates once'); + t.equal(counters.dep2, 1, 'only instantiates once'); + t.end(); +}); diff --git a/src/base-app.js b/src/base-app.js index 513a1d36..fd91a32d 100644 --- a/src/base-app.js +++ b/src/base-app.js @@ -68,12 +68,42 @@ class FusionApp { return this._register(token, value); } _register(token: Token, value: *) { - this.plugins.push(token); - const {aliases, enhancers} = this.registered.get(getTokenRef(token)) || { + const {value: registeredValue, aliases, enhancers} = this.registered.get( + getTokenRef(token) + ) || { + value: null, aliases: new Map(), enhancers: [], }; - this.registered.set(getTokenRef(token), {value, aliases, enhancers, token}); + if (token.isCompound) { + if (!registeredValue) { + // Initial value is set as an empty array + this.registered.set(getTokenRef(token), { + // $FlowFixMe + value: [], + aliases, + enhancers, + token, + }); + } + this.enhance(token, originalValue => { + return createPlugin({ + deps: {...value.deps}, + provides: (...args) => { + value = value.provides ? value.provides(...args) : value; + return [...originalValue, value]; + }, + }); + }); + } else { + this.registered.set(getTokenRef(token), { + value, + aliases, + enhancers, + token, + }); + } + this.plugins.push(token); function alias(sourceToken: *, destToken: *) { if (aliases) { aliases.set(sourceToken, destToken); @@ -101,7 +131,12 @@ class FusionApp { if (enhancers && Array.isArray(enhancers)) { enhancers.push(enhancer); } - this.registered.set(getTokenRef(token), {value, aliases, enhancers, token}); + this.registered.set(getTokenRef(token), { + value, + aliases, + enhancers, + token, + }); } cleanup() { return Promise.all(this.cleanups.map(fn => fn())); diff --git a/src/base-app.js.flow b/src/base-app.js.flow index b32d016b..edb3ad19 100644 --- a/src/base-app.js.flow +++ b/src/base-app.js.flow @@ -13,6 +13,7 @@ import type { FusionPlugin, Middleware, Token, + ArrayToken, } from './types.js'; declare class FusionApp { @@ -26,11 +27,16 @@ declare class FusionApp { register( Plugin: FusionPlugin ): aliaser>; + register( + token: ArrayToken, + Plugin: FusionPlugin + ): aliaser>; register( token: Token, Plugin: FusionPlugin ): aliaser>; register(token: Token, val: TVal): aliaser>; + register(token: ArrayToken, val: TVal): aliaser>; middleware( deps: TDeps, middleware: (Deps: $ObjMap) => Middleware diff --git a/src/create-token.js b/src/create-token.js index c7765b84..ff3c219c 100644 --- a/src/create-token.js +++ b/src/create-token.js @@ -6,7 +6,7 @@ * @flow */ -import type {Token} from './types.js'; +import type {Token, ArrayToken} from './types.js'; export const TokenType = { Required: 0, @@ -18,6 +18,7 @@ export class TokenImpl { ref: mixed; type: $Values; optional: ?TokenImpl; + isCompound = false; constructor(name: string, ref: mixed) { this.name = name; @@ -33,3 +34,12 @@ export function createToken(name: string): Token { // $FlowFixMe return new TokenImpl(name); } + +export function createArrayToken( + name: string +): ArrayToken { + const token = new TokenImpl(name); + token.isCompound = true; + // $FlowFixMe + return token; +} diff --git a/src/index.js b/src/index.js index 3ea5b429..e6fdccef 100644 --- a/src/index.js +++ b/src/index.js @@ -41,7 +41,7 @@ export { HttpServerToken, } from './tokens'; export {createPlugin} from './create-plugin'; -export {createToken} from './create-token'; +export {createToken, createArrayToken} from './create-token'; export {getEnv}; type FusionApp = typeof BaseApp; diff --git a/src/types.js b/src/types.js index 23c568c5..a85b2fa1 100644 --- a/src/types.js +++ b/src/types.js @@ -11,6 +11,14 @@ import type {Context as KoaContext} from 'koa'; export type Token = { (): T, optional: () => void | T, + isCompound: false, +}; + +export type ArrayToken = { + (): T, +} & { + ...Token>, + isCompound: true, }; type ExtendedKoaContext = KoaContext & {memoized: Map};