<file_sep>import isPlainObject from 'lodash/isPlainObject';
import { of as of$ } from 'rxjs/observable/of';
import { merge as merge$ } from 'rxjs/observable/merge';
import { concatMap as concatMap$ } from 'rxjs/operator/concatMap';
import { scan as scan$ } from 'rxjs/operator/scan';

import isObservable from './isObservable';

class Streamer {
  constructor(defaults = {}) {
    this._observables = [
      of$(defaults),
    ];
  }

  _push(observable) {
    this._observables.push(observable);
  }

  set(value, ...args) {
    // (key, value)
    if (typeof value === 'string') {
      return this.setKey(value, args[0]);
    }

    // (plainObject)
    if (isPlainObject(value)) {
      return this.setPlainObject(value);
    }

    // (observable$, ...mapperFns)
    if (isObservable(value)) {
      return this.setObservable(value, ...args);
    }

    return this;
  }

  setKey(key, value) {
    this._push(of$({
      [key]: value
    }));

    return this;
  }

  setPlainObject(object) {
    this._push(of$(object));

    return this;
  }

  setObservable(object$, ...mappers) {
    let mappedObject$ = object$;

    mappers.forEach((mapperFn) => {
      mappedObject$ = mappedObject$
        ::concatMap$((object) => {
          const result = mapperFn(object);

          if (isObservable(result)) {
            return result;
          }

          return of$(result);
        });
    });

    this._push(mappedObject$);

    return this;
  }

  setDispatch(actions, store) {
    const object = {};

    Object.keys(actions)
      .forEach((propKey) => {
        const actionFn = actions[propKey];

        object[propKey] = (...args) => {
          return store.dispatch(actionFn(...args));
        };
      });

    this._push(of$(object));

    return this;
  }

  get$() {
    return merge$(...this._observables)
      ::scan$((props, emitted) => {
        return {
          ...props,
          ...emitted,
        };
      });
  }
}

export default function streamProps(defaultProps = {}) {
  return new Streamer(defaultProps);
}
<file_sep>/* eslint-disable import/no-extraneous-dependencies, func-names, no-unused-expressions */
/* global describe, it, before, resetDOM */
import { expect } from 'chai';
import React from 'react';
import { shallow } from 'enzyme';
import { createApp } from 'frint';
import { MemoryRouterService } from 'frint-router';

import Route from './Route';

function createContext() {
  const App = createApp({
    name: 'TestRouterApp',
    providers: [
      {
        name: 'router',
        useFactory: function () {
          return new MemoryRouterService();
        },
        cascade: true,
      },
    ],
  });

  return {
    app: new App()
  };
}

function getAppInstance(wrapper) {
  return wrapper.instance()._handler._appInstance;
}

describe('frint-route-react › Route', function () {
  before(function () {
    resetDOM();
  });

  it('renders nothing (null) when no app or component prop is passed', function () {
    // exact and path
    const wrapperPathExact = shallow(
      <Route exact path="/about" />,
      { context: createContext() }
    );

    expect(wrapperPathExact.type()).to.be.null;

    // only path
    const wrapperPath = shallow(
      <Route path="/about" />,
      { context: createContext() }
    );

    expect(wrapperPath.type()).to.be.null;

    // only computedMatch
    const wrapperComputedMatch = shallow(
      <Route computedMatch={{ url: '/about' }} />,
      { context: createContext() }
    );

    expect(wrapperComputedMatch.type()).to.be.null;
  });

  it('renders component when route is matched not exactly', function () {
    const Component = () => <div>Matched</div>;

    const context = createContext();
    const wrapper = shallow(
      <Route component={Component} path="/about" />,
      { context }
    );

    context.app.get('router').push('/');
    expect(wrapper.type()).to.be.null;

    context.app.get('router').push('/about');
    expect(wrapper.type()).to.equal(Component);

    context.app.get('router').push('/service');
    expect(wrapper.type()).to.be.null;

    context.app.get('router').push('/about/contact');
    expect(wrapper.type()).to.equal(Component);
  });

  it('renders stateless component, when route is matched not exactly', function () {
    const context = createContext();
    const wrapper = shallow(
      <Route
        path="/about"
        render={function ({ match }) {
          return (
            <div>
              <div className="url">{match.url}</div>
            </div>
          );
        }}
      />,
      { context }
    );

    context.app.get('router').push('/');
    expect(wrapper.type()).to.be.null;

    context.app.get('router').push('/about');
    expect(wrapper.find('.url').text()).to.equal('/about');

    context.app.get('router').push('/service');
    expect(wrapper.type()).to.be.null;

    context.app.get('router').push('/about/contact');
    expect(wrapper.find('.url').text()).to.equal('/about');
  });

  it('renders component when exact prop passed and route is matched exactly', function () {
    const Component = () => <div>Matched</div>;

    const context = createContext();
    const wrapper = shallow(
      <Route component={Component} exact path="/about" />,
      { context }
    );

    context.app.get('router').push('/');
    expect(wrapper.type()).to.be.null;

    context.app.get('router').push('/about');
    expect(wrapper.type()).to.equal(Component);

    context.app.get('router').push('/service');
    expect(wrapper.type()).to.be.null;

    context.app.get('router').push('/about/contact');
    expect(wrapper.type()).to.be.null;
  });

  it('passes match from router to the component when it matches path', function () {
    const Component = () => <div>Matched</div>;

    const context = createContext();
    const wrapper = shallow(
      <Route component={Component} path="/about" />,
      { context }
    );

    context.app.get('router').push('/');
    expect(wrapper.type()).to.be.null;

    context.app.get('router').push('/about');
    expect(wrapper.type()).to.equal(Component);
    expect(wrapper.prop('match')).to.include({ url: '/about', isExact: true });

    context.app.get('router').push('/about/contact');
    expect(wrapper.type()).to.equal(Component);
    expect(wrapper.prop('match')).to.include({ url: '/about', isExact: false });
  });

  it('renders component only when computedMatch is passed and passes computedMatch as prop.match to child', function () {
    const Component = () => <div>Matched</div>;

    const context = createContext();
    const wrapper = shallow(
      <Route component={Component} computedMatch={{ url: '/about' }} />,
      { context }
    );

    expect(wrapper.type()).to.equal(Component);
    expect(wrapper.prop('match')).to.deep.equal({ url: '/about' });

    wrapper.setProps({ computedMatch: undefined });
    expect(wrapper.type()).to.be.null;
  });

  it('gives priority to computedMatch over path matching router url', function () {
    const Component = () => <div>Matched</div>;

    const context = createContext();
    const wrapper = shallow(
      <Route component={Component} computedMatch={{ url: '/computed' }} path="/about" />,
      { context }
    );

    context.app.get('router').push('/about');

    expect(wrapper.prop('match')).to.include({ url: '/computed' });

    wrapper.setProps({ computedMatch: undefined });
    expect(wrapper.prop('match')).to.include({ url: '/about' });

    wrapper.setProps({ computedMatch: { url: '/whatever' } });
    expect(wrapper.prop('match')).to.include({ url: '/whatever' });
  });

  describe('registers app, renders its component and then destroys it', function () {
    let beforeDestroyCallCount = 0;

    const AboutApp = createApp({
      name: 'AboutApp',
      providers: [
        {
          name: 'component',
          useValue: () => <article>About</article>,
        },
      ],
      beforeDestroy: () => {
        beforeDestroyCallCount += 1;
      }
    });

    const context = createContext();
    const wrapper = shallow(
      <Route app={AboutApp} path="/about" />,
      { context }
    );

    it('registers app with parent app from context', function () {
      const aboutApp = getAppInstance(wrapper);
      expect(aboutApp.getParentApp()).to.equal(context.app);
    });

    it('doesn\'t get rendered when path doesn\'t match', function () {
      expect(wrapper.type()).to.be.null;
    });

    it('gets rendered when path matches', function () {
      context.app.get('router').push('/about');
      expect(wrapper.html()).to.equal('<article>About</article>');
      expect(wrapper.prop('match')).to.include({ url: '/about' });
    });

    it('beforeDestroy is called on unmount', function () {
      expect(beforeDestroyCallCount).to.equal(0);
      wrapper.unmount();
      expect(beforeDestroyCallCount).to.equal(1);
    });
  });

  describe('renders components, apps, registers and destroys them during lifecycle when they are changes in props', function () {
    const HomeComponent = () => <header>Home</header>;

    let beforeDestroyAboutCallCount = 0;

    const AboutApp = createApp({
      name: 'AboutApp',
      providers: [
        {
          name: 'component',
          useValue: () => <article>About</article>,
        },
      ],
      beforeDestroy: () => {
        beforeDestroyAboutCallCount += 1;
      }
    });

    let beforeDestroyServicesCallCount = 0;

    const ServicesApp = createApp({
      name: 'ServicesApp',
      providers: [
        {
          name: 'component',
          useValue: () => <section>Services</section>,
        },
      ],
      beforeDestroy: () => {
        beforeDestroyServicesCallCount += 1;
      }
    });

    const context = createContext();
    const wrapper = shallow(
      <Route component={HomeComponent} path="/about" />,
      { context }
    );

    it('renders nothing then path doesn\'t match', function () {
      expect(wrapper.type()).to.be.null;
    });

    it('renders HomeComponent when path matches', function () {
      context.app.get('router').push('/about');
      expect(wrapper.html()).to.equal('<header>Home</header>');
    });

    let aboutApp;

    it('instantiates AboutApp and registers it with parent app from context', function () {
      wrapper.setProps({ app: AboutApp, component: undefined });

      aboutApp = getAppInstance(wrapper);
      expect(aboutApp.getParentApp()).to.equal(context.app);

      expect(wrapper.html()).to.equal('<article>About</article>');
    });

    it('doesn\'t destroy the app and doesn\'t reinitialise it when it\'s the same app', function () {
      wrapper.setProps({ app: AboutApp });
      expect(beforeDestroyAboutCallCount).to.equal(0);
      expect(getAppInstance(wrapper)).to.equal(aboutApp);
    });

    it('calls beforeDestroy for AboutApp when app is changed', function () {
      wrapper.setProps({ app: ServicesApp });
      expect(beforeDestroyAboutCallCount).to.equal(1);
    });

    it('instantiates servicesApp and registers it with parent app from context', function () {
      const servicesApp = getAppInstance(wrapper);
      expect(servicesApp.getParentApp()).to.equal(context.app);
      expect(servicesApp).to.not.equal(aboutApp);
    });

    it('renders ServicesApp', function () {
      expect(wrapper.html()).to.equal('<section>Services</section>');
    });

    it('destroys ServicesApp when switching to component', function () {
      expect(beforeDestroyServicesCallCount).to.equal(0);

      wrapper.setProps({ app: undefined, component: HomeComponent });
      expect(beforeDestroyServicesCallCount).to.equal(1);
      expect(getAppInstance(wrapper)).to.be.null;
    });

    it('renders HomeComponent', function () {
      expect(wrapper.html()).to.equal('<header>Home</header>');
    });

    it('unmounts nicely', function () {
      wrapper.unmount();
    });
  });
});
<file_sep>---
title: Changelog
importContentFromRoot: CHANGELOG.md
sidebarPartial: docsSidebar
---
<file_sep>---
title: Maintainers guide
importContentFromRoot: MAINTAINERS.md
sidebarPartial: docsSidebar
---
<file_sep>---
title: frint-store
importContentFromPackage: frint-store
sidebarPartial: docsSidebar
---
<file_sep>/* eslint-disable no-console */
import isPlainObject from 'lodash/isPlainObject';
import padStart from 'lodash/padStart';
import { Subject } from 'rxjs/Subject';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { map as map$ } from 'rxjs/operator/map';
import { switchMap as switchMap$ } from 'rxjs/operator/switchMap';
import { scan as scan$ } from 'rxjs/operator/scan';
import ActionsObservable from './ActionsObservable';

function Store(options = {}) {
  this.options = {
    initialState: undefined,
    deps: null,
    appendAction: false,
    reducer: state => state,
    epic: null,
    enableLogger: true,
    console: console,
    ...options,
  };

  this.internalState$ = new BehaviorSubject(this.options.initialState)
    ::scan$((previousState, action) => {
      let updatedState;
      const d = new Date();
      const prettyDate = [
        padStart(d.getHours(), 2, 0),
        ':',
        padStart(d.getMinutes(), 2, 0),
        ':',
        padStart(d.getSeconds(), 2, 0),
        '.',
        padStart(d.getMilliseconds(), 3, 0)
      ].join('');

      try {
        updatedState = this.options.reducer(previousState, action);
      } catch (error) {
        if (action && action.type) {
          this.options.console.error(`Error processing @ ${prettyDate} ${action.type}:`);
        }
        this.options.console.error(error);

        return previousState;
      }

      // logger in non-production mode only
      if (process.env.NODE_ENV !== 'production') {
        if (this.options.enableLogger === true) {
          const groupName = `action @ ${prettyDate} ${action.type}`;

          if (typeof this.options.console.group === 'function') {
            this.options.console.group(groupName);
          }

          this.options.console.log('%cprevious state', 'color: #9e9e9e; font-weight: bold;', previousState);
          this.options.console.log('%caction', 'color: #33c3f0; font-weight: bold;', action);
          this.options.console.log('%ccurrent state', 'color: #4cAf50; font-weight: bold;', updatedState);

          if (typeof this.options.console.groupEnd === 'function') {
            this.options.console.groupEnd();
          }
        }
      }

      return updatedState;
    });
  this.exposedState$ = new BehaviorSubject();

  this.cachedState = Object.assign({}, this.options.initialState);
  this.subscription = this.internalState$
    .subscribe((state) => {
      this.cachedState = state;
      this.exposedState$.next(state);
    });

  this.getState = this.getState.bind(this);
  this.dispatch = this.dispatch.bind(this);

  // for epic
  this._input$ = null;
  this._action$ = null;
  this._epic$ = null;
  this._epicSubscription = null;

  if (this.options.epic) {
    this._input$ = new Subject();
    this._action$ = new ActionsObservable(this._input$);
    this._epic$ = new Subject();

    this._epicSubscription = this._epic$
      ::map$(epic => epic(this._action$, this, this.options.deps))
      ::switchMap$(output$ => output$)
      .subscribe(this.dispatch);

    this._epic$.next(this.options.epic);
  }

  this.dispatch({ type: '__FRINT_INIT__' });
}

Store.prototype.getState$ = function getState$() {
  return this.exposedState$;
};

Store.prototype.getState = function getState() {
  return this.cachedState;
};

Store.prototype.dispatch = function dispatch(action) {
  if (typeof action === 'function') {
    return action(
      this.dispatch,
      this.getState,
      this.options.deps
    );
  }

  const payload = (
    this.options.appendAction &&
    isPlainObject(this.options.appendAction)
  )
    ? { ...this.options.appendAction, ...action }
    : action;
  const result = this.internalState$.next(payload);

  if (this.options.epic) {
    this._input$.next(payload);
  }

  return result;
};

Store.prototype.destroy = function destroy() {
  if (this.subscription) {
    this.subscription.unsubscribe();
  }

  if (this._epicSubscription) {
    this._epicSubscription.unsubscribe();
  }
};

export default Store;
<file_sep>---
title: REPL
layout: repl
---

Content here
<file_sep>import { jsdom } from 'jsdom';

export function resetDOM() {
  global.document = jsdom('<html><body><div id="root"></div></body></html>');
  global.window = global.document.defaultView;
  global.location = global.window.location;
  global.navigator = { userAgent: 'node.js' };

  /*
  Temporary fix for chai's expect(plainObject1).to.include(plainObject2) to work.
  Until these are solved:
  - https://github.com/chaijs/type-detect/pull/91
  - https://github.com/chaijs/type-detect/issues/98
  */
  global.HTMLElement = global.window.HTMLElement;
}

export function takeOverConsole(console) {
  let hijackedFns = {};

  function intercept(method, fn) {
    const original = console[method];
    console[method] = function () { // eslint-disable-line
      fn((...args) => {
        const [f] = args;
        if (typeof f !== 'undefined') {
          original.apply(console, args);
        }
      }, arguments); // eslint-disable-line
    };
    return original;
  }

  ['log', 'warn', 'error'].forEach((method) => {
    hijackedFns[method] = intercept(method, (through, [firstArg, ...rest]) => {
      if (typeof firstArg === 'string' && firstArg.startsWith('[DEPRECATED]')) {
        return;
      }
      through(firstArg, ...rest);
    });
  });

  return function reset() {
    Object.keys(hijackedFns).forEach((method) => {
      console[method] = hijackedFns[method];
    });
  };
}
<file_sep>import { composeHandlers } from 'frint-component-utils';

const RouteHandler = {
  _routerSubscription: null,
  _appInstance: null,

  getInitialData() {
    return {
      component: null,
      matched: null,
    };
  },

  beforeMount() {
    const props = this.getProps();
    this._calculateMatchedState(props);
    this._calculateComponentState(props);
  },

  propsChange(nextProps, pathChanged, exactChanged, appChanged) {
    this._calculateMatchedState(nextProps, pathChanged, exactChanged);
    this._calculateComponentState(nextProps, appChanged);
  },

  beforeDestroy() {
    this._unsubscribeFromRouter();
    this._destroyRouteApp();
  },

  _getRouter() {
    return this.app.get('router');
  },

  _calculateMatchedState(nextProps, pathChanged = false, exactChanged = false) {
    if (nextProps.computedMatch) {
      // in case it was subscribed before
      this._unsubscribeFromRouter();
    } else if (nextProps.path) {
      if (!this._routerSubscription || pathChanged || exactChanged) {
        this._unsubscribeFromRouter();

        this._routerSubscription = this._getRouter()
          .getMatch$(nextProps.path, {
            exact: nextProps.exact,
          })
          .subscribe((matched) => {
            this.setData('matched', matched);
          });
      }
    }
  },

  _calculateComponentState(nextProps, appChanged = false) {
    if (nextProps.component) {
      // component
      this._destroyRouteApp();

      this.setData('component', nextProps.component);
    } else if (nextProps.app && (this._appInstance === null || appChanged)) {
      // app
      this._destroyRouteApp();

      const RouteApp = nextProps.app;

      this._appInstance = new RouteApp({
        parentApp: this.app,
      });

      this.setData('component', this.getMountableComponent(this._appInstance));
    }
  },

  _unsubscribeFromRouter() {
    if (this._routerSubscription) {
      this._routerSubscription.unsubscribe();
      this._routerSubscription = null;
    }
  },

  _destroyRouteApp() {
    if (this._appInstance) {
      this._appInstance.beforeDestroy();
      this._appInstance = null;
    }
  }
};

export default function createRouteHandler(ComponentHandler, app, component) {
  if (typeof ComponentHandler.getMountableComponent !== 'function') {
    throw new Error('ComponentHandler must provide getMountableComponent() method');
  }

  return composeHandlers(
    RouteHandler,
    ComponentHandler,
    {
      app,
      component
    },
  );
}

<file_sep>---
title: frint-test-utils
importContentFromPackage: frint-test-utils
sidebarPartial: docsSidebar
---
<file_sep>---
title: frint-compat
importContentFromPackage: frint-compat
sidebarPartial: docsSidebar
---
<file_sep>/* eslint-disable import/no-extraneous-dependencies, func-names, no-unused-expressions */
/* global describe, it */
import { expect } from 'chai';

import { MemoryRouterService } from 'frint-router';
import createRouteHandler from './createRouteHandler';
import { ComponentHandler, createTestAppInstance, createTestApp, createComponent } from './testHelpers';

describe('frint-router-component-handlers › createRouteHandler', function () {
  it('creates a handler with getInitialData, beforeMount, propsChange, beforeDestroy methods', function () {
    const handler = createRouteHandler(
      ComponentHandler,
      createTestAppInstance(new MemoryRouterService()),
      createComponent()
    );

    expect(handler).to.include.all.keys('getInitialData', 'beforeMount', 'propsChange', 'beforeDestroy');
  });

  it('throws exception when ComponentHandler doesn\'t have getMountableComponent() method', function () {
    const invalidCreation = () => {
      createRouteHandler(
        {},
        createTestAppInstance(new MemoryRouterService()),
        createComponent()
      );
    };

    expect(invalidCreation).to.throw(Error, 'ComponentHandler must provide getMountableComponent() method');
  });

  describe('RouteHandler functions properly throughout lifecycle', function () {
    const router = new MemoryRouterService();

    const component = createComponent();
    const componentToRender = createComponent();
    component.props = {
      path: '/products',
      exact: true,
      component: componentToRender
    };

    const handler = createRouteHandler(
      ComponentHandler,
      createTestAppInstance(router),
      component
    );

    it('initially component and matched data are set to null', function () {
      component.data = handler.getInitialData();

      expect(component.data.component).to.be.null;
      expect(component.data.matched).to.be.null;
    });

    it('when beforeMount() is called it subscribes to router and changes matched and component accordingly', function () {
      handler.beforeMount();

      expect(component.data.matched).to.be.null;
      expect(component.data.component).to.equal(componentToRender);

      router.push('/products');

      expect(component.data.matched).to.be.an('object');
    });

    it('when component and app props changed and propsChange() is called, component data is set to app\'s component', function () {
      const appComponent = createComponent();
      component.props.component = undefined;

      component.props.app = createTestApp(undefined, appComponent);

      handler.propsChange(component.props, false, false, true);

      expect(component.data.component).to.equal(appComponent);
    });

    it('when path prop change and propsChange() is called, matched data is changed', function () {
      expect(component.data.matched).to.be.an('object');

      component.props.path = '/contact';
      handler.propsChange(component.props, true, false, false);

      expect(component.data.matched).to.be.null;
    });

    it('when beforeDestroy() is called, it unsubscribes from router and matched no longer changes', function () {
      expect(component.data.matched).to.be.null;

      handler.beforeDestroy();
      router.push('/contact');

      expect(component.data.matched).to.be.null;
    });
  });

  describe('RouteHandler functions properly with computedMatch throughout lifecycle', function () {
    const router = new MemoryRouterService();

    const component = createComponent();
    const componentToRender = createComponent();
    const computedMatch = {};

    component.props = {
      path: '/products',
      exact: true,
      computedMatch,
      component: componentToRender
    };

    const handler = createRouteHandler(
      ComponentHandler,
      createTestAppInstance(router),
      component
    );

    router.push('/');

    it('initially matched data is set to null', function () {
      component.data = handler.getInitialData();

      expect(component.data.matched).to.be.null;
    });

    it('when beforeMount() is called it doesn\'t subscribe to router and therefore matched data is not affected', function () {
      expect(component.data.matched).to.be.null;

      handler.beforeMount();

      router.push('/products');

      expect(component.data.matched).to.be.null;
    });

    it('when computedMatch prop is removed and propChanged() is called it subscribes to router and therefore matched data is affected', function () {
      component.props.computedMatch = null;
      handler.propsChange(component.props, false, false, false);

      expect(component.data.matched).to.be.an('object');
    });
  });
});
<file_sep>---
title: frint-config
importContentFromPackage: frint-config
sidebarPartial: docsSidebar
---
<file_sep>/* eslint-disable import/no-extraneous-dependencies, func-names */
/* global describe, it */
const expect = require('chai').expect;
const App = require('frint').App;

const createRootApp = require('../root/index.mock');
const CommandApp = require('./help');

describe('frint-cli › commands › help', function () {
  it('is a Frint App', function () {
    const RootApp = createRootApp();
    const rootApp = new RootApp();
    rootApp.registerApp(CommandApp);
    const commandApp = rootApp.getAppInstance('help');

    expect(commandApp).to.be.an.instanceOf(App);
  });

  it('prints error when run without any command name', function () {
    const RootApp = createRootApp();
    const rootApp = new RootApp();
    rootApp.registerApp(CommandApp);
    const commandApp = rootApp.getAppInstance('help');
    const fakeConsole = rootApp.get('console');

    commandApp.get('execute')();

    expect(fakeConsole.errors.length).to.equal(1);
    expect(fakeConsole.errors[0]).to.contain('Must provide a command name');
  });

  it('prints help text when run with a command name', function () {
    const RootApp = createRootApp({
      providers: [
        {
          name: 'command',
          useValue: 'help',
          cascade: true,
        },
        {
          name: 'params',
          useValue: {
            _: ['help'],
          },
          cascade: true,
        },
      ],
    });
    const rootApp = new RootApp();
    rootApp.registerApp(CommandApp);
    const commandApp = rootApp.getAppInstance('help');
    const fakeConsole = rootApp.get('console');

    commandApp.get('execute')();

    expect(fakeConsole.logs.length).to.equal(1);
  });
});
<file_sep>/* eslint-disable import/no-extraneous-dependencies, func-names */
/* global describe, it */
import { expect } from 'chai';
import { of as of$ } from 'rxjs/observable/of';
import { last as last$ } from 'rxjs/operator/last';

import streamProps from './streamProps';

describe('frint-react › streamProps', function () {
  it('is a function', function () {
    expect(streamProps).to.be.a('function');
  });

  it('streams with default object', function (done) {
    const streamer = streamProps({
      key: 'value',
    });

    streamer.get$()
      .subscribe(function (props) {
        expect(props).to.deep.equal({
          key: 'value',
        });

        done();
      });
  });

  it('streams plain object, by merging with default', function (done) {
    const streamer = streamProps({
      key: 'value',
      key2: 'value2',
    });

    streamer.set({
      key2: 'value2 overridden',
      key3: 'value3'
    });

    streamer.get$()
      ::last$()
      .subscribe(function (props) {
        expect(props).to.deep.equal({
          key: 'value',
          key2: 'value2 overridden',
          key3: 'value3',
        });

        done();
      });
  });

  it('streams key/value pairs, by merging with default', function (done) {
    const streamer = streamProps({
      key: 'value',
    });

    streamer.set('key2', 'value2 overridden');
    streamer.set('key3', 'value3');

    streamer.get$()
      ::last$()
      .subscribe(function (props) {
        expect(props).to.deep.equal({
          key: 'value',
          key2: 'value2 overridden',
          key3: 'value3',
        });

        done();
      });
  });

  it('streams multiple observables with mappings, by merging with default', function (done) {
    const streamer = streamProps({
      key: 'value',
    });

    const names$ = of$(
      'Fahad',
      'Ricardo',
      'Mark',
      'Jean',
      'Alex' // last one wins
    );
    const numbers$ = of$(
      1,
      2,
      3 // last one wins
    );

    streamer.set(
      names$,
      name => ({ name }) // final plain object
    );

    streamer.set(
      numbers$,
      number => number * 2, // direct mapped values
      number => of$(number), // even mapped observables
      number => ({ number }) // final plain object
    );

    streamer.get$()
      ::last$()
      .subscribe(function (props) {
        expect(props).to.deep.equal({
          key: 'value',
          name: 'Alex',
          number: 6, // 3 * 2
        });

        done();
      });
  });

  it('steams dispatchable actions against store', function (done) {
    const streamer = streamProps();
    let dispatchedPayload;

    const fakeStore = {
      dispatch(payload) {
        dispatchedPayload = payload;
      }
    };

    function myActionCreator(value) {
      return {
        type: 'MY_ACTION_TYPE',
        value,
      };
    }

    streamer.setDispatch(
      { myAction: myActionCreator },
      fakeStore
    );

    streamer.get$()
      ::last$()
      .subscribe(function (props) {
        props.myAction('someValue');

        expect(dispatchedPayload).to.deep.equal({
          type: 'MY_ACTION_TYPE',
          value: 'someValue',
        });

        done();
      });
  });

  it('has no impact if unexpected values are set', function (done) {
    const streamer = streamProps({
      key: 'value',
    });

    streamer.set(() => 'blah');

    streamer.get$()
      .subscribe(function (props) {
        expect(props).to.deep.equal({
          key: 'value',
        });

        done();
      });
  });
});
<file_sep>const createApp = require('frint').createApp;

const providers = require('./providers.mock');

module.exports = function createMockedRootApp() {
  const options = arguments[0] || {}; // eslint-disable-line
  const overrideProviders = options.providers || [];

  return createApp({
    name: 'FrintCLI Mocked',

    providers: overrideProviders.reduce((acc, provider) => {
      const index = providers.findIndex(item => item.name === provider.name);

      if (index > -1) {
        acc[index] = provider;
      }

      return acc;
    }, providers)
  });
};
<file_sep>---
title: FrintJS - The Modular JavaScript Framework
layout: home
---

Content here
<file_sep>/* eslint-disable import/no-extraneous-dependencies */
const MemoryFs = require('memory-fs');

const fs = new MemoryFs();

module.exports = [
  {
    name: 'fs',
    useValue: fs,
    cascade: true,
  },
  {
    name: 'pwd',
    useValue: process.env.PWD,
    cascade: true,
  },
  {
    name: 'command',
    useValue: null,
    cascade: true,
  },
  {
    name: 'params',
    useValue: {
      _: [],
    },
    cascade: true,
  },
  {
    name: 'config',
    useValue: {
      plugins: [],
    },
    cascade: true,
  },
  {
    name: 'console',
    useFactory: function useFactory() {
      const fakeConsole = {
        logs: [],
        errors: [],

        log: function log(message) {
          this.logs.push(message);
        },

        error: function error(message) {
          this.errors.push(message);
        },
      };

      return fakeConsole;
    },
    cascade: true,
  },
];
<file_sep>/* eslint-disable import/no-extraneous-dependencies */
import React from 'react';
import ReactDOM from 'react-dom';

import FrintReact from './';

export default function render(app, node) {
  const MountableComponent = FrintReact.getMountableComponent(app);

  return ReactDOM.render(<MountableComponent />, node);
}
<file_sep>const createApp = require('frint').createApp;

const providers = require('./providers');

const App = createApp({
  name: 'FrintCLI',

  providers: providers,
});

module.exports = App;
<file_sep>/* eslint-disable import/no-extraneous-dependencies, func-names */
/* global describe, it */
import { expect } from 'chai';

import Types from './Types';
import createModel from './createModel';
import createCollection from './createCollection';

describe('frint-data › Types', function () {
  describe('Types :: any', function () {
    it('accepts any values', function () {
      const type = Types.any;

      expect(type(1)).to.equal(1);
      expect(type('hello')).to.equal('hello');
      expect(type([])).to.deep.equal([]);
      expect(type({})).to.deep.equal({});
    });

    it('checks for required values', function () {
      const type = Types.any.isRequired;

      expect(() => type()).to.throw(/value is not defined/);
    });

    it('allows empty values when default is set', function () {
      const type = Types.any.defaults(123);

      expect(type()).to.equal(123);
      expect(type(234)).to.equal(234);
    });
  });

  describe('Types :: array', function () {
    it('accepts undefined unless required', function () {
      const type = Types.array;

      expect(type()).to.be.an('undefined');
      expect(() => type.isRequired()).to.throw('value is not defined');
    });

    it('accepts array values', function () {
      const type = Types.array;

      expect(type([])).to.deep.equal([]);
      expect(type([1, 2, 3])).to.deep.equal([1, 2, 3]);
    });

    it('rejects non-array values', function () {
      const type = Types.array;

      expect(() => type(0)).to.throw(/value is not an array/);
      expect(() => type(null)).to.throw(/value is not an array/);
      expect(() => type(true)).to.throw(/value is not an array/);
      expect(() => type({})).to.throw(/value is not an array/);
      expect(() => type(() => {})).to.throw(/value is not an array/);
    });

    it('checks for required values', function () {
      const type = Types.array.isRequired;

      expect(() => type()).to.throw(/value is not defined/);
    });

    it('allows empty values when default is set', function () {
      const type = Types.array.defaults(['hi', 'there']);

      expect(type()).to.deep.equal(['hi', 'there']);
      expect(type([1, 2, 3])).to.deep.equal([1, 2, 3]);
      expect(() => type(123)).to.throw(/value is not an array/);
    });
  });

  describe('Types :: bool', function () {
    it('accepts undefined unless required', function () {
      const type = Types.bool;

      expect(type()).to.be.an('undefined');
      expect(() => type.isRequired()).to.throw('value is not defined');
    });

    it('accepts boolean values', function () {
      const type = Types.bool;

      expect(type(true)).to.equal(true);
      expect(type(false)).to.equal(false);
    });

    it('rejects non-boolean values', function () {
      const type = Types.bool;

      expect(() => type(0)).to.throw(/value is not a boolean/);
      expect(() => type(null)).to.throw(/value is not a boolean/);
      expect(() => type('hello world')).to.throw(/value is not a boolean/);
      expect(() => type(() => {})).to.throw(/value is not a boolean/);
    });

    it('checks for required values', function () {
      const type = Types.bool.isRequired;

      expect(() => type()).to.throw(/value is not defined/);
    });

    it('allows empty values when default is set', function () {
      const type = Types.bool.defaults(true);

      expect(type()).to.equal(true);
      expect(() => type(123)).to.throw(/value is not a boolean/);
    });
  });

  describe('Types :: collection', function () {
    it('accepts undefined unless required', function () {
      const type = Types.collection;

      expect(type()).to.be.an('undefined');
      expect(() => type.isRequired()).to.throw('value is not defined');
    });

    it('accepts collection instances', function () {
      const type = Types.collection;

      const Person = createModel({
        schema: {
          name: Types.string.isRequired,
        },
      });
      const People = createCollection({
        model: Person,
      });

      const people = new People([{ name: 'Frint' }]);

      expect(type(people)).to.equal(people);
    });

    it('rejects non-collection values', function () {
      const type = Types.collection;

      expect(() => type('hello')).to.throw(/value is not a Collection/);
      expect(() => type(null)).to.throw(/value is not a Collection/);
      expect(() => type(true)).to.throw(/value is not a Collection/);
      expect(() => type({})).to.throw(/value is not a Collection/);
      expect(() => type(() => {})).to.throw(/value is not a Collection/);
    });

    it('checks for required values', function () {
      const type = Types.collection.isRequired;

      expect(() => type()).to.throw(/value is not defined/);
    });

    it('allows empty values when default is set', function () {
      const Person = createModel({
        schema: {
          name: Types.string.isRequired,
        },
      });
      const People = createCollection({
        model: Person,
      });

      const people = new People([{ name: 'Frint' }]);
      const type = Types.collection.defaults(people);

      expect(type()).to.equal(people);

      const otherPeople = new People([{ name: '<NAME>' }]);
      expect(type(otherPeople)).to.equal(otherPeople);
      expect(() => type('hello world')).to.throw(/value is not a Collection/);
    });

    it('accepts collection instances of certain Colelction', function () {
      const Person = createModel({
        schema: {
          name: Types.string.isRequired,
        },
      });
      const People = createCollection({
        model: Person,
      });

      const type = Types.collection.of(People);
      const people = new People([{ name: 'Frint' }]);

      expect(type(people)).to.equal(people);
      expect(type([{ name: 'Name here' }])).to.be.instanceof(People);

      const Post = createModel({
        schema: {
          title: Types.string.isRequired,
        },
      });
      const Posts = createCollection({
        model: Post,
      });

      const posts = new Posts([{ title: 'Hello World' }]);
      expect(() => type(posts)).to.throw(/value is not instance of expected Collection/);
    });
  });

  describe('Types :: enum', function () {
    it('accepts undefined unless required', function () {
      const type = Types.enum([1, 2, 3]);

      expect(type()).to.be.an('undefined');
      expect(() => type.isRequired()).to.throw('value is not defined');
    });

    it('accepts enum values', function () {
      const type = Types.enum([1, 2, 3]);

      expect(type(1)).to.equal(1);
      expect(type(2)).to.equal(2);
      expect(type(3)).to.equal(3);
    });

    it('rejects non-enum values', function () {
      const type = Types.enum([1, 2, 3]);

      expect(() => type(0)).to.throw(/value is none of the provided enums/);
      expect(() => type(null)).to.throw(/value is none of the provided enums/);
      expect(() => type(true)).to.throw(/value is none of the provided enums/);
      expect(() => type(() => {})).to.throw(/value is none of the provided enums/);
      expect(() => type('hello')).to.throw(/value is none of the provided enums/);
    });

    it('checks for required values', function () {
      const type = Types.enum([1, 2, 3]).isRequired;

      expect(() => type()).to.throw(/value is not defined/);
      expect(type(1)).to.equal(1);
    });

    it('allows empty values when default is set', function () {
      const type = Types.enum([1, 2, 3]).defaults(2);

      expect(type()).to.equal(2);
      expect(type(3)).to.equal(3);
      expect(() => type('hello world')).to.throw(/value is none of the provided enums/);
    });

    it('accepts enum of types', function () {
      const type = Types.enum.of([
        Types.string,
        Types.number
      ]).isRequired;

      expect(type(1)).to.equal(1);
      expect(type(2)).to.equal(2);
      expect(type('hi')).to.equal('hi');
      expect(() => type({})).to.throw('value is none of the provided enum types');
    });
  });

  describe('Types :: func', function () {
    it('accepts undefined unless required', function () {
      const type = Types.func;

      expect(type()).to.be.an('undefined');
      expect(() => type.isRequired()).to.throw('value is not defined');
    });

    it('accepts function values', function () {
      const type = Types.func;

      const fn = () => {};
      expect(type(fn)).to.equal(fn);
    });

    it('rejects non-function values', function () {
      const type = Types.func;

      expect(() => type(0)).to.throw(/value is not a function/);
      expect(() => type(null)).to.throw(/value is not a function/);
      expect(() => type('hello world')).to.throw(/value is not a function/);
      expect(() => type({})).to.throw(/value is not a function/);
      expect(() => type([])).to.throw(/value is not a function/);
    });

    it('checks for required values', function () {
      const type = Types.func.isRequired;

      expect(() => type()).to.throw(/value is not defined/);
    });

    it('allows empty values when default is set', function () {
      const defaultFunc = () => {};
      const type = Types.func.defaults(defaultFunc);

      expect(type()).to.equal(defaultFunc);
      expect(() => type(123)).to.throw(/value is not a function/);
    });
  });

  describe('Types :: model', function () {
    it('accepts undefined unless required', function () {
      const type = Types.model;

      expect(type()).to.be.an('undefined');
      expect(() => type.isRequired()).to.throw('value is not defined');
    });

    it('accepts model instances', function () {
      const type = Types.model;
      const Person = createModel({
        schema: {
          name: Types.string.isRequired,
        },
      });

      const person = new Person({ name: 'Frint' });

      expect(type(person)).to.equal(person);
    });

    it('rejects non-model values', function () {
      const type = Types.model;

      expect(() => type('hello')).to.throw(/value is not a Model/);
      expect(() => type(null)).to.throw(/value is not a Model/);
      expect(() => type(true)).to.throw(/value is not a Model/);
      expect(() => type({})).to.throw(/value is not a Model/);
      expect(() => type(() => {})).to.throw(/value is not a Model/);
    });

    it('checks for required values', function () {
      const type = Types.model.isRequired;

      expect(() => type()).to.throw(/value is not defined/);
    });

    it('allows empty values when default is set', function () {
      const Person = createModel({
        schema: {
          name: Types.string.isRequired,
        },
      });

      const person = new Person({ name: 'Frint' });
      const type = Types.model.defaults(person);

      expect(type()).to.equal(person);

      const anotherPerson = new Person({ name: '<NAME>' });
      expect(type(anotherPerson)).to.equal(anotherPerson);
      expect(() => type('hello world')).to.throw(/value is not a Model/);
    });

    it('accepts model instances of certain Model', function () {
      const Person = createModel({
        schema: {
          name: Types.string.isRequired,
        },
      });
      const Post = createModel({
        schema: {
          title: Types.string.isRequired,
        },
      });

      const type = Types.model.of(Person);

      const person = new Person({ name: 'Frint' });
      expect(type(person)).to.equal(person);
      expect(type({ name: 'Name here' })).to.be.instanceof(Person);

      const post = new Post({ title: 'Hello World' });
      expect(() => type(post)).to.throw(/value is not instance of expected Model/);
    });
  });

  describe('Types :: number', function () {
    it('accepts undefined unless required', function () {
      const type = Types.number;

      expect(type()).to.be.an('undefined');
      expect(() => type.isRequired()).to.throw('value is not defined');
    });

    it('accepts number values', function () {
      const type = Types.number;

      expect(type(123)).to.equal(123);
      expect(type(0)).to.equal(0);
    });

    it('rejects non-number values', function () {
      const type = Types.number;

      expect(() => type('hello')).to.throw(/value is not a number/);
      expect(() => type(null)).to.throw(/value is not a number/);
      expect(() => type(true)).to.throw(/value is not a number/);
      expect(() => type(() => {})).to.throw(/value is not a number/);
    });

    it('checks for required values', function () {
      const type = Types.number.isRequired;

      expect(() => type()).to.throw(/value is not defined/);
    });

    it('allows empty values when default is set', function () {
      const type = Types.number.defaults(123);

      expect(type()).to.equal(123);
      expect(type(345)).to.equal(345);
      expect(() => type('hello world')).to.throw(/value is not a number/);
    });
  });

  describe('Types :: object', function () {
    it('accepts undefined unless required', function () {
      const type = Types.object;

      expect(type()).to.be.an('undefined');
      expect(() => type.isRequired()).to.throw('value is not defined');
    });

    it('accepts object values', function () {
      const type = Types.object;

      expect(type({})).to.deep.equal({});
      expect(type({ a: 1, b: 2 })).to.deep.equal({ a: 1, b: 2 });
    });

    it('rejects non-object values', function () {
      const type = Types.object;

      expect(() => type(0)).to.throw(/value is not an object/);
      expect(() => type(null)).to.throw(/value is not an object/);
      expect(() => type(true)).to.throw(/value is not an object/);
      expect(() => type([])).to.throw(/value is not an object/);
      expect(() => type(() => {})).to.throw(/value is not an object/);
    });

    it('checks for required values', function () {
      const type = Types.object.isRequired;

      expect(() => type()).to.throw(/value is not defined/);
    });

    it('allows empty values when default is set', function () {
      const type = Types.object.defaults({ hi: 'there' });

      expect(type()).to.deep.equal({ hi: 'there' });
      expect(type({ hello: 'world' })).to.deep.equal({ hello: 'world' });
      expect(() => type(123)).to.throw(/value is not an object/);
    });

    it('checks for nested types', function () {
      const type = Types.object.of({
        street: Types.string,
        city: Types.string.isRequired,
        postalCode: Types.number.isRequired,
        country: Types.string.defaults('Netherlands')
      });

      expect(type({
        street: 'Amsterdam',
        city: 'Amsterdam',
        postalCode: 123
      })).to.deep.equal({
        street: 'Amsterdam',
        city: 'Amsterdam',
        postalCode: 123,
        country: 'Netherlands'
      });

      expect(() => type({
        street: 'Amsterdam',
        city: 'Amsterdam',
        postalCode: '123'
      })).to.throw(/schema failed for key 'postalCode', value is not a number/);
    });

    it('checks for deep-nested types', function () {
      const type = Types.object.of({
        name: Types.string.isRequired,
        address: Types.object.of({
          street: Types.string,
          city: Types.string.isRequired,
          postalCode: Types.number.isRequired,
          country: Types.string.defaults('Netherlands')
        }).isRequired
      });

      expect(type({
        name: 'Frint',
        address: {
          street: 'Amsterdam',
          city: 'Amsterdam',
          postalCode: 123
        }
      })).to.deep.equal({
        name: 'Frint',
        address: {
          street: 'Amsterdam',
          city: 'Amsterdam',
          postalCode: 123,
          country: 'Netherlands'
        }
      });

      expect(() => type({
        name: 'Frint',
        address: {
          street: 'Amsterdam',
          city: 'Amsterdam',
          postalCode: '123'
        }
      })).to.throw(/schema failed for key 'postalCode', value is not a number/);
    });
  });

  describe('Types :: string', function () {
    it('accepts undefined unless required', function () {
      const type = Types.string;

      expect(type()).to.be.an('undefined');
      expect(() => type.isRequired()).to.throw('value is not defined');
    });

    it('accepts string values', function () {
      const type = Types.string;

      expect(type('hello')).to.equal('hello');
      expect(type('hello world')).to.equal('hello world');
      expect(type('')).to.equal('');
    });

    it('rejects non-string values', function () {
      const type = Types.string;

      expect(() => type(0)).to.throw(/value is not a string/);
      expect(() => type(null)).to.throw(/value is not a string/);
      expect(() => type(true)).to.throw(/value is not a string/);
      expect(() => type(() => {})).to.throw(/value is not a string/);
    });

    it('checks for required values', function () {
      const type = Types.string.isRequired;

      expect(() => type()).to.throw(/value is not defined/);
    });

    it('allows empty values when default is set', function () {
      const type = Types.string.defaults('hello');

      expect(type()).to.equal('hello');
      expect(type('something')).to.equal('something');
      expect(() => type(123)).to.throw(/value is not a string/);
    });
  });

  describe('Types :: uuid', function () {
    it('accepts undefined unless required', function () {
      const type = Types.uuid;

      expect(type()).to.be.an('undefined');
      expect(() => type.isRequired()).to.throw('value is not defined');
    });

    it('accepts UUIDs values', function () {
      const type = Types.uuid;

      expect(type('27961a0e-f4e8-4eb3-bf95-c5203e1d87b9')).to.equal('27961a0e-f4e8-4eb3-bf95-c5203e1d87b9');
      expect(type('90691cbc-b5ea-5826-ae98-951e30fc3b2d')).to.equal('90691cbc-b5ea-5826-ae98-951e30fc3b2d');
    });

    it('rejects non-UUID values', function () {
      const type = Types.uuid;

      expect(() => type('hello')).to.throw(/value is not a valid UUID/);
      expect(() => type(null)).to.throw(/value is not a valid UUID/);
      expect(() => type(true)).to.throw(/value is not a valid UUID/);
      expect(() => type(() => {})).to.throw(/value is not a valid UUID/);
    });

    it('checks for required values', function () {
      const type = Types.uuid.isRequired;

      expect(() => type()).to.throw(/value is not defined/);
    });

    it('allows empty values when default is set', function () {
      const type = Types.uuid.defaults('90691cbc-b5ea-5826-ae98-951e30fc3b2d');

      expect(type()).to.equal('90691cbc-b5ea-5826-ae98-951e30fc3b2d');
      expect(type('27961a0e-f4e8-4eb3-bf95-c5203e1d87b9')).to.equal('27961a0e-f4e8-4eb3-bf95-c5203e1d87b9');
      expect(() => type('hello world')).to.throw(/value is not a valid UUID/);
    });
  });
});
<file_sep>---
title: frint-react-server
importContentFromPackage: frint-react-server
sidebarPartial: docsSidebar
---
<file_sep>import App from './App';
import createApp from './createApp';

const Frint = {
  App,
  createApp,
};

export default Frint;
<file_sep>---
title: frint
importContentFromPackage: frint
sidebarPartial: docsSidebar
---
<file_sep>---
title: frint-component-handlers
importContentFromPackage: frint-component-handlers
sidebarPartial: docsSidebar
---
<file_sep>---
title: Guides
sidebarPartial: guidesSidebar
---

# Guides

All the guides assume, you have:

* [Node.js](https://nodejs.org/) v6.9+
* [npm](https://www.npmjs.com/) v3+
<file_sep>/* eslint-disable import/no-extraneous-dependencies, func-names */
/* global describe, it */
import { expect } from 'chai';

import App from './App';
import createApp from './createApp';

describe('frint › createApp', function () {
  it('is a function', function () {
    expect(createApp).to.be.a('function');
  });

  it('returns App class', function () {
    const MyApp = createApp({
      name: 'MyAppNameFromClass',
    });

    const app = new MyApp({
      name: 'MyAppNameFromInstance',
    });

    expect(app).to.be.instanceOf(App);
    expect(app.getName()).to.equal('MyAppNameFromInstance');
  });
});
<file_sep>---
title: frint-react
importContentFromPackage: frint-react
sidebarPartial: docsSidebar
---
<file_sep>---
title: frint-router-component-handlers
importContentFromPackage: frint-router-component-handlers
sidebarPartial: docsSidebar
---
<file_sep>/* eslint-disable import/no-extraneous-dependencies */
import React from 'react';
import PropTypes from 'prop-types';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';

import composeHandlers from 'frint-component-utils/lib/composeHandlers';
import ObserveHandler from 'frint-component-handlers/lib/ObserveHandler';

import ReactHandler from '../handlers/ReactHandler';

export default function observe(fn) {
  return (Component) => {
    class WrappedComponent extends React.Component {
      static displayName = (typeof Component.displayName !== 'undefined')
        ? `observe(${Component.displayName})`
        : 'observe';

      static contextTypes = {
        app: PropTypes.object.isRequired
      };

      constructor(...args) {
        super(...args);

        this._handler = composeHandlers(
          ReactHandler,
          ObserveHandler,
          {
            component: this,
            getProps$: typeof fn === 'function'
              ? app => fn(app, this._props$)
              : fn,
          }
        );

        this.state = this._handler.getInitialData();
        this._props$ = new BehaviorSubject(this.props);
      }

      componentWillMount() {
        this._handler.app = this.context.app;
        this._handler.beforeMount();
      }

      componentWillReceiveProps(newProps) {
        this._props$.next(newProps);
      }

      componentWillUnmount() {
        this._handler.beforeDestroy();
      }

      render() {
        const { computedProps } = this.state;

        return <Component {...computedProps} {...this.props} />;
      }
    }

    return WrappedComponent;
  };
}
<file_sep>---
title: frint-data
importContentFromPackage: frint-data
sidebarPartial: docsSidebar
---
<file_sep>---
title: frint-cli
importContentFromPackage: frint-cli
sidebarPartial: docsSidebar
---
<file_sep>---
title: Higher-Order Components
sidebarPartial: guidesSidebar
---

# Higher Order Components (HOCs)

<!-- MarkdownTOC depth=2 autolink=true bracket=round -->

- [What is an HOC?](#what-is-an-hoc)
- [How does it work with Frint?](#how-does-it-work-with-frint)
  - [Dependencies](#dependencies)
  - [Base component](#base-component)
  - [App](#app)
  - [Observed component](#observed-component)
  - [Render](#render)

<!-- /MarkdownTOC -->

## What is an HOC?

[React](https://facebook.github.io/react/) has a very detailed documentation on this topic [here](https://facebook.github.io/react/docs/higher-order-components.html).

In short:

> A higher-order component is a function that takes a component and returns a new component.

## How does it work with Frint?

In our `frint-react` package, we expose an `observe` function which gives us an HOC.

### Dependencies

Install the dependencies first:

```
$ npm install --save rxjs react react-dom frint frint-react
```

### Base component

Now let's say, we have a basic React component:

```js
// components/Root.js
import React from 'react';

function MyComponent(props) {
  return (
    <p>Name: {props.name}</p>
  );
}

export default MyComponent;
```

The component just renders a name, which is given to it as props.

### App

And the App definition happens to looks like this:

```js
// app/index.js
import { createApp } from 'frint';

import Root from '../components/Root';

export default createApp({
  name: 'MyAppNameHere',
  providers: [
    {
      name: 'component',
      useValue: Root
    }
  ]
})
```

### Observed component

But we want to inject the App's name to the component, and we could easily do that using `observe`:

```js
// components/Root.js
import { Observable } from 'rxjs';
import { observe } from 'frint-react';

function MyComponent(props) {
  return (
    <p>Name: {props.name}</p>
  );
}

export default observe(function (app) {
  // `app` is our App's instance
  const props = {
    name: app.getName() // `MyAppNameHere`
  };

  // this function must always return an Observable of props
  return Observable.of(props);
})(MyComponent);
```

### Render

Now when your App gets rendered, your Root component would show your App's name:

```js
// index.js
import { render } from 'frint-react';

import App from './app';

const app = new App();
render(app, document.getElementById('root'));
```
<file_sep>---
title: frint-router
importContentFromPackage: frint-router
sidebarPartial: docsSidebar
---
<file_sep>const fs = require('fs');
const path = require('path');

const clone = require('lodash/clone');
const argv = require('yargs').argv;

module.exports = [
  {
    name: 'fs',
    useValue: fs,
    cascade: true,
  },
  {
    name: 'pwd',
    useValue: process.env.PWD,
    cascade: true,
  },
  {
    name: 'command',
    useFactory: function useFactory() {
      if (argv._[0] !== undefined) {
        return argv._[0];
      }

      return null;
    },
    cascade: true,
  },
  {
    name: 'params',
    useFactory: function useFactory() {
      const clonedArgv = clone(argv);
      clonedArgv._.shift();

      return clonedArgv;
    },
    cascade: true,
  },
  {
    name: 'config',
    useFactory: function useFactory(deps) {
      let config = {};
      const pwd = deps.pwd;

      try {
        config = JSON.parse(deps.fs.readFileSync(`${pwd}/.frintrc`, 'utf8'));
      } catch (e) {
        // do nothing
      }

      if (typeof config.plugins === 'undefined') {
        config.plugins = [];
      }

      config.plugins = config.plugins.map((plugin) => {
        if (plugin.startsWith('.')) {
          return path.join(pwd, plugin);
        }

        return plugin;
      });

      return config;
    },
    deps: [
      'pwd',
      'fs',
    ],
    cascade: true,
  },
  {
    name: 'console',
    useValue: console,
    cascade: true,
  },
];
<file_sep>---
title: frint-router-react
importContentFromPackage: frint-router-react
sidebarPartial: docsSidebar
---
<file_sep>/* eslint-disable import/no-extraneous-dependencies, func-names, no-new, class-methods-use-this */
/* global describe, it */
import { expect } from 'chai';

import App from './App';
import createApp from './createApp';

describe('frint  › App', function () {
  it('throws error when creating new instance without name', function () {
    expect(() => {
      new App();
    }).to.throw(/Must provide `name` in options/);
  });

  it('gets option value', function () {
    const app = new App({
      name: 'MyApp',
    });

    expect(app.getName()).to.equal('MyApp');
  });

  it('gets parent and root app', function () {
    const rootApp = new App({
      name: 'RootApp',
    });

    const childApp = new App({
      name: 'ChildApp',
      parentApp: rootApp,
    });

    const grandchildApp = new App({
      name: 'GrandchildApp',
      parentApp: childApp,
    });

    expect(rootApp.getName()).to.equal('RootApp');
    expect(childApp.getName()).to.equal('ChildApp');
    expect(rootApp.getParentApps()).to.deep.equal([]);

    expect(childApp.getParentApp()).to.deep.equal(rootApp);
    expect(childApp.getRootApp()).to.deep.equal(rootApp);
    expect(
      childApp
        .getParentApps()
        .map(x => x.options.name)
    ).to.deep.equal([
      'RootApp'
    ]);

    expect(grandchildApp.getParentApp()).to.deep.equal(childApp);
    expect(grandchildApp.getRootApp()).to.deep.equal(rootApp);
    expect(
      grandchildApp
        .getParentApps()
        .map(x => x.options.name)
    ).to.deep.equal([
      'ChildApp',
      'RootApp',
    ]);
  });

  it('registers providers with direct values', function () {
    const app = new App({
      name: 'MyApp',
      providers: [
        { name: 'foo', useValue: 'fooValue' },
      ],
    });

    expect(app.get('foo')).to.equal('fooValue');
  });

  it('registers providers with factory values', function () {
    const app = new App({
      name: 'MyApp',
      providers: [
        { name: 'foo', useFactory: () => 'fooValue' },
      ],
    });

    expect(app.get('foo')).to.equal('fooValue');
  });

  it('registers providers with class values', function () {
    class Foo {
      getValue() {
        return 'fooValue';
      }
    }

    const app = new App({
      name: 'MyApp',
      providers: [
        { name: 'foo', useClass: Foo },
      ],
    });

    expect(app.get('foo').getValue()).to.equal('fooValue');
  });

  it('registers providers with dependencies', function () {
    class Baz {
      constructor({ foo, bar }) {
        this.foo = foo;
        this.bar = bar;
      }

      getValue() {
        return `bazValue, ${this.foo}, ${this.bar}`;
      }
    }


    const app = new App({
      name: 'MyApp',
      providers: [
        {
          name: 'foo',
          useValue: 'fooValue'
        },
        {
          name: 'bar',
          useFactory: ({ foo }) => {
            return `barValue, ${foo}`;
          },
          deps: ['foo'],
        },
        {
          name: 'baz',
          useClass: Baz,
          deps: ['foo', 'bar'],
        }
      ],
    });

    expect(app.get('foo')).to.equal('fooValue');
    expect(app.get('bar')).to.equal('barValue, fooValue');
    expect(app.get('baz').getValue()).to.equal('bazValue, fooValue, barValue, fooValue');
  });

  it('returns services from Root that are cascaded', function () {
    class ServiceC {
      getValue() {
        return 'serviceC';
      }
    }

    const Root = createApp({
      name: 'MyApp',
      providers: [
        {
          name: 'serviceA',
          useValue: 'serviceA',
          scoped: true,
          cascade: true,
        },
        {
          name: 'serviceB',
          useFactory: () => 'serviceB',
          scoped: true,
          cascade: true,
        },
        {
          name: 'serviceC',
          useClass: ServiceC,
          cascade: true,
        },
        {
          name: 'serviceCScoped',
          useClass: ServiceC,
          cascade: true,
          scoped: true,
        },
        {
          name: 'serviceD',
          useValue: 'serviceD',
          cascade: false,
        },
        {
          name: 'serviceE',
          useValue: 'serviceE',
          cascade: true,
          scoped: false,
        }
      ],
    });

    const App1 = createApp({ name: 'App1' });

    const root = new Root();
    root.registerApp(App1);

    const app = root.getAppInstance('App1');
    expect(app.get('serviceA')).to.equal('serviceA');
    expect(app.get('serviceB')).to.equal('serviceB');
    expect(app.get('serviceC').getValue()).to.equal('serviceC');
    expect(app.get('serviceD')).to.equal(null);
    expect(app.get('serviceE')).to.equal('serviceE');

    expect(app.get('serviceF')).to.equal(null);
    expect(root.get('serviceF')).to.equal(null);
  });

  it('returns null when service is non-existent in both Child App and Root', function () {
    const Root = createApp({ name: 'MyApp' });
    const App1 = createApp({ name: 'App1' });

    const app = new Root();
    app.registerApp(App1);

    const serviceA = app
      .getAppInstance('App1')
      .get('serviceA');

    expect(serviceA).to.equal(null);
  });

  it('gets container', function () {
    const app = new App({
      name: 'MyApp'
    });

    expect(app.getContainer()).to.deep.equal(app.container);
  });

  it('gets providers definition list', function () {
    const app = new App({
      name: 'MyApp',
      providers: [
        { name: 'foo', useValue: 'fooValue' },
      ],
    });

    expect(app.getProviders()).to.deep.equal([
      { name: 'foo', useValue: 'fooValue' },
    ]);
  });

  it('gets individual provider definition', function () {
    const app = new App({
      name: 'MyApp',
      providers: [
        { name: 'foo', useValue: 'fooValue' },
      ],
    });

    expect(app.getProvider('foo')).to.deep.equal({ name: 'foo', useValue: 'fooValue' });
  });

  it('calls initialize during construction, as passed in options', function () {
    let called = false;

    const app = new App({
      name: 'MyApp',
      initialize() {
        called = true;
      }
    });

    expect(app.getName()).to.equal('MyApp');
    expect(called).to.equal(true);
  });

  it('calls beforeDestroy, as passed in options', function () {
    let called = false;

    const app = new App({
      name: 'MyApp',
      beforeDestroy() {
        called = true;
      }
    });
    app.beforeDestroy();

    expect(app.getName()).to.equal('MyApp');
    expect(called).to.equal(true);
  });

  it('registers apps', function () {
    const Root = createApp({ name: 'MyApp' });
    const App1 = createApp({ name: 'App1' });

    const app = new Root();

    app.registerApp(App1, {
      regions: ['sidebar'],
    });

    expect(app.hasAppInstance('App1')).to.equal(true);
    expect(app.getAppInstance('App1').getOption('name')).to.equal('App1');
  });

  it('registers apps, by overriding options', function () {
    const Root = createApp({ name: 'MyApp' });
    const App1 = createApp({ name: 'App1' });

    const app = new Root();

    app.registerApp(App1, {
      name: 'AppOne',
      regions: ['sidebar'],
    });

    expect(app.hasAppInstance('AppOne')).to.equal(true);
    expect(app.getAppInstance('AppOne').getOption('name')).to.equal('AppOne');
  });

  it('registers apps', function () {
    const Root = createApp({ name: 'MyApp' });
    const App1 = createApp({ name: 'App1' });

    const app = new Root();

    app.registerApp(App1, {
      regions: ['sidebar'],
    });

    expect(app.hasAppInstance('App1')).to.equal(true);
    expect(app.getAppInstance('App1').getOption('name')).to.equal('App1');
  });

  it('streams registered apps as a collection', function (done) {
    const Root = createApp({ name: 'MyApp' });
    const App1 = createApp({ name: 'App1' });

    const app = new Root();

    app.registerApp(App1, {
      regions: ['sidebar'],
    });
    const apps$ = app.getApps$();

    apps$.subscribe(function (apps) {
      expect(Array.isArray(apps)).to.equal(true);
      expect(apps.length).to.equal(1);
      expect(apps[0].name).to.equal('App1');

      done();
    });
  });

  it('streams registered apps as a collection, with region filtering', function (done) {
    const Root = createApp({ name: 'MyApp' });
    const App1 = createApp({ name: 'App1' });

    const app = new Root();

    app.registerApp(App1, {
      regions: ['sidebar'],
    });
    const apps$ = app.getApps$('sidebar');

    apps$.subscribe(function (apps) {
      expect(Array.isArray(apps)).to.equal(true);
      expect(apps.length).to.equal(1);
      expect(apps[0].name).to.equal('App1');

      done();
    });
  });

  it('gets app once available (that will be available in future)', function (done) {
    const Root = createApp({ name: 'MyApp' });
    const App1 = createApp({ name: 'App1' });

    const root = new Root();

    root.getAppOnceAvailable$('App1')
      .subscribe(function (app) {
        expect(app.getName()).to.equal('App1');

        done();
      });

    root.registerApp(App1);
  });

  it('gets app once available (that is already available)', function (done) {
    const Root = createApp({ name: 'MyApp' });
    const App1 = createApp({ name: 'App1' });

    const root = new Root();
    root.registerApp(App1);

    root.getAppOnceAvailable$('App1')
      .subscribe(function (app) {
        expect(app.getName()).to.equal('App1');

        done();
      });
  });

  it('gets app scoped by region', function () {
    const Root = createApp({ name: 'MyApp' });
    const App1 = createApp({ name: 'App1' });
    const App2 = createApp({ name: 'App2' });

    const app = new Root();
    app.registerApp(App1, {
      regions: ['sidebar'],
    });
    app.registerApp(App2, {
      regions: ['footer'],
      multi: true,
    });

    expect(app.getAppInstance('App1')).to.be.an('object');
    expect(app.getAppInstance('App1', 'sidebar')).to.be.an('object');

    expect(app.getAppInstance('App2')).to.equal(null);
    expect(app.getAppInstance('App2', 'footer')).to.equal(null);

    app.instantiateApp('App2', 'footer', 'footer-123');
    expect(app.getAppInstance('App2', 'footer', 'footer-123')).to.be.an('object');
  });

  it('throws error when registering same App twice', function () {
    const Root = createApp({ name: 'MyApp' });
    const App1 = createApp({ name: 'App1' });

    const app = new Root();
    app.registerApp(App1);

    expect(() => {
      app.registerApp(App1);
    }).to.throw(/App 'App1' has been already registered before/);
  });

  it('checks for app instance availability', function () {
    const Root = createApp({ name: 'MyApp' });
    const App1 = createApp({ name: 'App1' });

    const app = new Root();
    expect(app.hasAppInstance('blah')).to.equal(false);

    app.registerApp(App1);
    expect(app.hasAppInstance('App1')).to.equal(true);
  });

  it('throws error when trying to instantiate non-existent App', function () {
    const Root = createApp({ name: 'MyApp' });
    const app = new Root();

    expect(() => {
      app.instantiateApp('blah');
    }).to.throw(/No app found with name 'blah'/);
  });
});
<file_sep>const mkdirp = require('mkdirp');
const request = require('request');
const tar = require('tar');

const createApp = require('frint').createApp;

const descriptionText = `
Usage:

  $ frint init
  $ frint init --example exampleName

Example:

  $ mkdir my-new-directory
  $ cd my-new-directory

  $ frint init --example kitchensink

You can find list of all available examples here:
https://github.com/Travix-International/frint/tree/master/examples
`.trim();

module.exports = createApp({
  name: 'init',
  providers: [
    {
      name: 'summary',
      useValue: 'Scaffolds a new Frint app in current working directory',
    },
    {
      name: 'description',
      useValue: descriptionText,
    },
    {
      name: 'execute',
      useFactory: function useFactory(deps) {
        return function execute() {
          const example = deps.params.example || 'counter';
          const dir = deps.pwd;

          function streamFrintExampleToDir() {
            request('https://codeload.github.com/Travix-International/frint/tar.gz/master')
              .on('error', deps.console.error)
              .pipe(tar.x({
                filter: path => path.indexOf(`frint-master/examples/${example}/`) === 0,
                strip: 3,
                C: dir
              }))
              .on('error', deps.console.error)
              .on('finish', () => deps.console.log('Done!'));
          }

          deps.console.log('Initializing...');

          mkdirp(dir, function mkdirpCallback(error) {
            if (error) {
              deps.console.error(error);
              return;
            }
            streamFrintExampleToDir();
          });
        };
      },
      deps: [
        'console',
        'params',
        'pwd',
      ],
    }
  ],
});
<file_sep>/* eslint-disable import/no-extraneous-dependencies, func-names */
/* global describe, it */
import { expect } from 'chai';
import { filter as filter$ } from 'rxjs/operator/filter';
import { delay as delay$ } from 'rxjs/operator/delay';
import { map as map$ } from 'rxjs/operator/map';
import { take as take$ } from 'rxjs/operator/take';
import { last as last$ } from 'rxjs/operator/last';
import { scan as scan$ } from 'rxjs/operator/scan';

import createStore from './createStore';
import combineReducers from './combineReducers';
import combineEpics from './combineEpics';

describe('frint-store › createStore', function () {
  it('returns function', function () {
    const Store = createStore();
    expect(Store).to.be.a('function');
  });

  it('returns initial state upon subscription', function () {
    const Store = createStore();
    const store = new Store({
      enableLogger: false,
      initialState: {
        ok: true,
      }
    });

    const subscription = store.getState$()
      .subscribe(function (state) {
        expect(state).to.deep.equal({
          ok: true,
        });
      });

    subscription.unsubscribe();
  });

  it('dispatches actions, that update state', function () {
    const Store = createStore({
      enableLogger: false,
      initialState: {
        counter: 0,
      },
      reducer: function (state, action) {
        switch (action.type) {
          case 'INCREMENT_COUNTER':
            return Object.assign({}, {
              counter: state.counter + 1
            });
          case 'DECREMENT_COUNTER':
            return Object.assign({}, {
              counter: state.counter - 1
            });
          default:
            return state;
        }
      }
    });
    const store = new Store();

    const states = [];
    const subscription = store.getState$()
      .subscribe(function (state) {
        states.push(state);
      });

    store.dispatch({ type: 'INCREMENT_COUNTER' });
    store.dispatch({ type: 'INCREMENT_COUNTER' });
    store.dispatch({ type: 'DECREMENT_COUNTER' });

    expect(states.length).to.equal(4); // 1 initial + 3 dispatches

    const lastState = states[states.length - 1];
    expect(lastState).to.deep.equal({
      counter: 1
    });

    const synchronousState = store.getState();
    expect(synchronousState).to.deep.equal({
      counter: 1
    });

    subscription.unsubscribe();
  });

  it('appends to action payload', function () {
    const actions = [];
    const Store = createStore({
      enableLogger: false,
      appendAction: {
        appName: 'Blah',
      },
      initialState: {
        counter: 0,
      },
      reducer: function (state, action) {
        actions.push(action);

        return state;
      }
    });

    const store = new Store();

    const states = [];
    const subscription = store.getState$()
      .subscribe(function (state) {
        states.push(state);
      });

    store.dispatch({ type: 'INCREMENT_COUNTER' });

    expect(actions).to.deep.equal([
      { appName: 'Blah', type: '__FRINT_INIT__' },
      { appName: 'Blah', type: 'INCREMENT_COUNTER' },
    ]);

    subscription.unsubscribe();
  });

  it('dispatches async actions, with deps argument', function () {
    const actions = [];
    const Store = createStore({
      enableLogger: false,
      deps: { foo: 'bar' },
      initialState: {
        counter: 0,
      },
      reducer: function (state, action) {
        actions.push(action);

        switch (action.type) {
          case 'INCREMENT_COUNTER':
            return Object.assign({}, {
              counter: state.counter + 1
            });
          case 'DECREMENT_COUNTER':
            return Object.assign({}, {
              counter: state.counter - 1
            });
          default:
            return state;
        }
      }
    });
    const store = new Store();

    const states = [];
    const subscription = store.getState$()
      .subscribe(function (state) {
        states.push(state);
      });

    store.dispatch({ type: 'INCREMENT_COUNTER' });
    store.dispatch(function (dispatch, getState, deps) {
      dispatch({
        type: 'INCREMENT_COUNTER',
        deps
      });
    });
    store.dispatch({ type: 'DECREMENT_COUNTER' });

    expect(actions).to.deep.equal([
      { type: '__FRINT_INIT__' },
      { type: 'INCREMENT_COUNTER' },
      { type: 'INCREMENT_COUNTER', deps: { foo: 'bar' } },
      { type: 'DECREMENT_COUNTER' },
    ]);

    expect(states.length).to.equal(4);
    expect(states).to.deep.equal([
      { counter: 0 },
      { counter: 1 },
      { counter: 2 },
      { counter: 1 },
    ]);

    subscription.unsubscribe();
  });

  it('destroys internal subscription', function () {
    const Store = createStore({
      enableLogger: false,
      epic: function (action$) {
        return action$
          ::filter$(action => action.type === 'PING');
      },
      initialState: {
        counter: 0
      }
    });
    const store = new Store();

    let changesCount = 0;
    const subscription = store.getState$()
      .subscribe(function () {
        changesCount += 1;
      });

    store.dispatch({ type: 'DO_SOMETHING' });
    expect(changesCount).to.equal(2); // 1 initial + 1 dispatch

    store.destroy();

    store.dispatch({ type: 'DO_SOMETHING_IGNORED' });
    expect(changesCount).to.equal(2); // will stop at 2

    subscription.unsubscribe();
  });

  it('logs state changes', function () {
    const consoleCalls = [];
    const fakeConsole = {
      group() { },
      groupEnd() { },
      log(...args) {
        consoleCalls.push({ method: 'log', args });
      },
    };

    const Store = createStore({
      enableLogger: true,
      console: fakeConsole,
      initialState: {
        counter: 0,
      },
      reducer: function (state, action) {
        switch (action.type) {
          case 'INCREMENT_COUNTER':
            return Object.assign({}, {
              counter: state.counter + 1
            });
          case 'DECREMENT_COUNTER':
            return Object.assign({}, {
              counter: state.counter - 1
            });
          default:
            return state;
        }
      }
    });
    const store = new Store();

    const states = [];
    const subscription = store.getState$()
      .subscribe(function (state) {
        states.push(state);
      });

    store.dispatch({ type: 'INCREMENT_COUNTER' });
    store.dispatch({ type: 'INCREMENT_COUNTER' });
    store.dispatch({ type: 'DECREMENT_COUNTER' });

    expect(states.length).to.equal(4); // 1 initial + 3 dispatches
    expect(states).to.deep.equal([
      { counter: 0 },
      { counter: 1 },
      { counter: 2 },
      { counter: 1 },
    ]);

    expect(consoleCalls.length).to.equal(12); // (1 init + 3 actions) * 3 logs (prev + action + current)
    expect(consoleCalls[3].args[2]).to.deep.equal({ counter: 0 }); // prev
    expect(consoleCalls[4].args[2]).to.deep.equal({ type: 'INCREMENT_COUNTER' }); // action
    expect(consoleCalls[5].args[2]).to.deep.equal({ counter: 1 }); // action

    subscription.unsubscribe();
  });

  it('logs errors from reducers', function () {
    const consoleCalls = [];
    const fakeConsole = {
      group() { },
      groupEnd() { },
      log(...args) {
        consoleCalls.push({ method: 'log', args });
      },
      error(...args) {
        consoleCalls.push({ method: 'error', args });
      }
    };

    const Store = createStore({
      enableLogger: true,
      console: fakeConsole,
      initialState: {
        counter: 0,
      },
      reducer: function (state, action) {
        switch (action.type) {
          case 'DO_SOMETHING':
            throw new Error('Something went wrong...');
          default:
            return state;
        }
      }
    });
    const store = new Store();

    const subscription = store.getState$()
      .subscribe(() => {});

    store.dispatch({ type: 'DO_SOMETHING' });

    expect(consoleCalls.length).to.equal(5); // 3 init + 2 errors

    expect(consoleCalls[3].method).to.equal('error');
    expect(consoleCalls[3].args[0]).to.exist
      .and.to.contain('Error processing @')
      .and.to.contain('DO_SOMETHING');

    expect(consoleCalls[4].method).to.equal('error');
    expect(consoleCalls[4].args[0]).to.exist
      .and.be.instanceof(Error)
      .and.have.property('message', 'Something went wrong...');

    subscription.unsubscribe();
  });

  describe('handles combined reducers', function () {
    function counterReducer(state = { value: 0 }, action) {
      switch (action.type) {
        case 'INCREMENT_COUNTER':
          return Object.assign({}, {
            value: state.value + 1
          });
        case 'DECREMENT_COUNTER':
          return Object.assign({}, {
            value: state.value - 1
          });
        default:
          return state;
      }
    }

    function colorReducer(state = { value: 'blue' }, action) {
      switch (action.type) {
        case 'SET_COLOR':
          return Object.assign({}, {
            value: action.color
          });
        default:
          return state;
      }
    }

    const rootReducer = combineReducers({
      counter: counterReducer,
      color: colorReducer,
    });

    it('with given initial state', function () {
      const Store = createStore({
        enableLogger: false,
        initialState: {
          counter: {
            value: 100,
          },
          color: {
            value: 'red'
          }
        },
        reducer: rootReducer,
      });
      const store = new Store();

      const states = [];
      const subscription = store.getState$()
        .subscribe((state) => {
          states.push(state);
        });

      store.dispatch({ type: 'INCREMENT_COUNTER' });
      store.dispatch({ type: 'INCREMENT_COUNTER' });
      store.dispatch({ type: 'DECREMENT_COUNTER' });
      store.dispatch({ type: 'SET_COLOR', color: 'green' });

      expect(states).to.deep.equal([
        { counter: { value: 100 }, color: { value: 'red' } }, // initial
        { counter: { value: 101 }, color: { value: 'red' } }, // INCREMENT_COUNTER
        { counter: { value: 102 }, color: { value: 'red' } }, // INCREMENT_COUNTER
        { counter: { value: 101 }, color: { value: 'red' } }, // DECREMENT_COUNTER
        { counter: { value: 101 }, color: { value: 'green' } } // SET_COLOR
      ]);

      subscription.unsubscribe();
    });

    it('with no given initial state', function () {
      const Store = createStore({
        enableLogger: false,
        reducer: rootReducer,
      });
      const store = new Store();

      const states = [];
      const subscription = store.getState$()
        .subscribe((state) => {
          states.push(state);
        });

      store.dispatch({ type: 'INCREMENT_COUNTER' });
      store.dispatch({ type: 'INCREMENT_COUNTER' });
      store.dispatch({ type: 'DECREMENT_COUNTER' });
      store.dispatch({ type: 'SET_COLOR', color: 'green' });

      expect(states).to.deep.equal([
        { counter: { value: 0 }, color: { value: 'blue' } }, // initial
        { counter: { value: 1 }, color: { value: 'blue' } }, // INCREMENT_COUNTER
        { counter: { value: 2 }, color: { value: 'blue' } }, // INCREMENT_COUNTER
        { counter: { value: 1 }, color: { value: 'blue' } }, // DECREMENT_COUNTER
        { counter: { value: 1 }, color: { value: 'green' } } // SET_COLOR
      ]);

      subscription.unsubscribe();
    });

    it('with partially given initial state for certain reducers', function () {
      const Store = createStore({
        enableLogger: false,
        initialState: {
          counter: {
            value: 100,
          },
        },
        reducer: rootReducer,
      });
      const store = new Store();

      const states = [];
      const subscription = store.getState$()
        .subscribe((state) => {
          states.push(state);
        });

      store.dispatch({ type: 'INCREMENT_COUNTER' });
      store.dispatch({ type: 'INCREMENT_COUNTER' });
      store.dispatch({ type: 'DECREMENT_COUNTER' });
      store.dispatch({ type: 'SET_COLOR', color: 'green' });

      expect(states).to.deep.equal([
        { counter: { value: 100 }, color: { value: 'blue' } }, // initial
        { counter: { value: 101 }, color: { value: 'blue' } }, // INCREMENT_COUNTER
        { counter: { value: 102 }, color: { value: 'blue' } }, // INCREMENT_COUNTER
        { counter: { value: 101 }, color: { value: 'blue' } }, // DECREMENT_COUNTER
        { counter: { value: 101 }, color: { value: 'green' } } // SET_COLOR
      ]);

      subscription.unsubscribe();
    });
  });

  it('creates Store with epics', function (done) {
    // constants
    const PING = 'PING';
    const PONG = 'PONG';

    const INITIAL_STATE = {
      isPinging: false,
    };

    // reducers
    function pingReducer(state = INITIAL_STATE, action) {
      switch (action.type) {
        case PING:
          return {
            isPinging: true,
          };

        case PONG:
          return {
            isPinging: false,
          };

        default:
          return state;
      }
    }

    const rootReducer = combineReducers({
      ping: pingReducer,
    });

    // epics
    function pingEpic$(action$) {
      return action$
        ::filter$(action => action.type === PING)
        ::delay$(10)
        ::map$(() => ({ type: PONG }));
    }

    const rootEpic$ = combineEpics(pingEpic$);

    // Store
    const Store = createStore({
      enableLogger: false,
      reducer: rootReducer,
      epic: rootEpic$,
    });

    const store = new Store();

    expect(store.getState().ping.isPinging).to.equal(false);

    store.getState$()
      ::take$(3)
      ::scan$(
        function (acc, curr) {
          acc.push({ isPinging: curr.ping.isPinging });

          return acc;
        },
        []
      )
      ::last$()
      .subscribe(function (pingStates) {
        expect(pingStates).to.deep.equal([
          { isPinging: false }, // initial state
          { isPinging: true }, // after PING
          { isPinging: false }, // after PING has dispatched PONG
        ]);

        done();
      });

    store.dispatch({ type: PING });
  });
});
<file_sep>/* eslint-disable global-require, import/no-dynamic-require */
const createApp = require('frint').createApp;

const descriptionText = `
Usage:

  $ frint help <commandName>

Example:

  $ frint help init
`.trim();

module.exports = createApp({
  name: 'help',

  providers: [
    {
      name: 'summary',
      useValue: 'Shows help text for commands',
    },
    {
      name: 'description',
      useValue: descriptionText,
    },
    {
      name: 'execute',
      useFactory: function useFactory(deps) {
        return function execute() {
          const commandName = deps.params._[0];

          if (typeof commandName === 'undefined') {
            return deps.console.error('Must provide a command name as argument.');
          }

          const commandApp = deps.rootApp.getAppInstance(commandName);

          if (!commandApp) {
            return deps.console.error(`No command found with name: ${commandName}`);
          }

          const output = [
            commandApp.get('summary'),
            commandApp.get('description'),
          ]
            .filter(text => text)
            .join('\n\n');

          return deps.console.log(output);
        };
      },
      deps: [
        'console',
        'params',
        'rootApp',
      ],
    }
  ],
});
<file_sep>/** Declaration file generated by dts-gen */

import { Container, GeneratedClass } from 'travix-di';
import { BehaviorSubject, Observable } from 'rxjs';

export interface ProviderNames {
  component: string;
  container: Container;
  store: string;
  app: string;
  parentApp: string;
  rootApp: string;
  region: string;
}

export interface AppOptions {
  name: string;
  parentApp: App;
  providers: Container[];
  providerNames: ProviderNames;
  initialize?: () => void;
  beforeDestroy?: () => void;
}

export interface RegisterAppOptions {
  regions: string[];
  weight: number;
  multi: boolean;
}

export class RegisteredApp extends App implements RegisterAppOptions {
  App: RegisteredApp;
  name: string;
  regions: any[];
  instances: { [name: string] : App };
  weight: number;
  multi: boolean;
}

type Constructor<T extends App> = new(...args: any[]) => T;

export class App {
    constructor(args: AppOptions);

    container: Container;

    beforeDestroy(): any;

    destroyApp(name: string, region?: string, regionKey?: string): boolean;

    destroyWidget(name: string, region?: string, regionKey?: string): boolean;

    get(providerName: string): any;

    getAppInstance(name: string, region?: string, regionKey?: string): any;

    getAppOnceAvailable$(name: string, region?: string, regionKey?: string): Observable<App>;

    getApps$<T extends RegisteredApp>(regionName: string): Observable<T>;

    getContainer(): Container;

    getOption(key: string): any;

    getParentApp(): App;

    getParentApps(): App[];

    getProvider(name: string): any;

    getProviders(): any[];

    getRootApp(): App;

    getWidgetInstance(name: string, region?: string, regionKey?: string): App;

    getWidgetOnceAvailable$(name: string, region?: string, regionKey?: string): Observable<App>;

    getWidgets$<T extends RegisteredApp>(regionName: string): Observable<T>;

    hasAppInstance(name: string, region?: string, regionKey?: string): boolean;

    hasWidgetInstance(name: string, region?: string, regionKey?: string): boolean;

    instantiateApp(name: string, region?: string, regionKey?: string): RegisteredApp;

    registerApp<T extends Constructor<App>>(AppClass: T, options?: RegisterAppOptions): void;

    registerWidget<T extends Constructor<App>>(AppClass: T, options?: RegisterAppOptions): void;

    frintAppName: string;

}

export function createApp<T extends Constructor<App>>(...args: any[]): T;
<file_sep><!DOCTYPE html>
<html>
  <head>
    <title><%= title %></title>
  </head>

  <body>
    <section class="hero is-primary is-medium">
      <!-- Hero header: will stick at the top -->
      <div class="hero-head">
        <header class="nav">
          <div class="container">
            <%= renderPartial('navLinks') %>
          </div>
          <a href="https://github.com/Travix-International/frint" class="github-corner" aria-label="View source on Github">
            <svg width="80" height="80" viewBox="0 0 250 250" style="fill:#151513; color:#fff; position: absolute; top: 0; border: 0; right: 0;" aria-hidden="true">
              <path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path>
              <path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path>
              <path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path>
            </svg>
          </a>
        </header>
      </div>

      <!-- Hero content: will be in the middle -->
      <div class="hero-body">
        <div class="container has-text-centered">
          <h1 class="title">
            Modular <span>JavaScript</span> framework<br />
            for building <span>Scalable</span> &amp; <span>Reactive</span> applications.
          </h1>

          <div>
            <a href="/docs" class="button is-primary is-medium">Learn more</a>
            <a href="http://github.com/Travix-International/frint" class="button is-transparent is-medium">GitHub</a>
          </div>
        </div>
      </div>
    </section>

    <section class="section about">
      <div class="container">
        <div class="columns">
          <div class="column">
            <img alt="Modular" src="/img/js-logo.png" />

            <h3>Modular & Extensible</h3>

            <p>
              The whole framework is split into multiple independent packages.
              You get to compose your Apps with only the packages your need.
            </p>
          </div>

          <div class="column">
            <img alt="Component" src="/img/react-logo.svg" />

            <h3>Component-Based</h3>

            <p>
              You can roll your own Component based view libraries with Frint.
              We have official support for React.
            </p>
          </div>

          <div class="column">
            <img alt="Reactive" src="/img/rxjs-logo.png" />

            <h3>Reactive Development</h3>

            <p>
              We embraced the power of reactive programming everywhere, including app-to-app communication for ease of development.
            </p>
          </div>
        </div>
      </div>
    </section>

    <section class="section apps">
      <div class="container">
        <h2>Split your application across multiple apps</h2>

        <img alt="apps" src="/img/frint-apps.png" />

        <p>
          Learn how to write <a href="/guides/apps">Root Apps &amp; Child Apps</a>
          targeting different <a href="/guides/regions">Regions</a>.
        </p>
      </div>
    </section>

    <section class="section regions">
      <div class="container">
        <h2>Load apps coming in from separate bundles</h2>

        <img alt="apps" src="/img/frint-code-splitting.png" />

        <p>
          Learn more about
          <a href="/guides/apps">Apps</a> and
          <a href="/guides/regions">Regions</a>, and
          <a href="/guides/code-splitting">Code Splitting</a>.
        </p>
      </div>
    </section>

    <!--
    <section class="section">
      <div class="container">
        <p>Content here...</p>

        <%= contents %>
      </div>
    </section>
    -->

    <%= renderPartial('footer') %>
    <%= renderPartial('assets') %>
  </body>
</html>
<file_sep>---
title: frint-model
importContentFromPackage: frint-model
sidebarPartial: docsSidebar
---
<file_sep>/* eslint-disable import/no-extraneous-dependencies, func-names, no-unused-expressions */
/* global describe, it */
import { expect } from 'chai';
import React from 'react';
import PropTypes from 'prop-types';
import { shallow, mount } from 'enzyme';
import { createApp } from 'frint';
import { MemoryRouterService } from 'frint-router';

import Route from './Route';
import Switch from './Switch';

function createContext() {
  const App = createApp({
    name: 'TestRouterApp',
    providers: [
      {
        name: 'router',
        useFactory: function () {
          return new MemoryRouterService();
        },
        cascade: true,
      },
    ],
  });

  return {
    app: new App()
  };
}

describe('frint-router-react › Switch', function () {
  it('switches routes according to router and passes computedMatch', function () {
    const HomeComponent = () => <header>HomeComponent</header>;
    const AboutComponent = () => <article>AboutComponent</article>;
    const NotFound = () => <div>Not found</div>;

    const context = createContext();
    const router = context.app.get('router');

    const wrapper = shallow(
      <Switch>
        <Route component={HomeComponent} exact path="/"/>
        <Route component={AboutComponent} path="/about" />
        <Route component={NotFound} />
      </Switch>,
      { context }
    );


    it('renders Route when it\'s path matches exactly', function () {
      router.push('/');

      expect(wrapper.type()).to.equal(Route);

      expect(wrapper.props()).to.deep.equal({
        computedMatch: { url: '/', isExact: true, params: {} },
        component: HomeComponent,
        exact: true,
        path: '/'
      });
    });

    it('renders Route when it\'s path matches inexactly, skipping first one which requires exact match', function () {
      router.push('/about/our-sport-team');

      expect(wrapper.props()).to.deep.equal({
        computedMatch: { url: '/about', isExact: false, params: {} },
        component: AboutComponent,
        path: '/about'
      });
    });

    it('renders default Route when nothing matches and doesn\'t pass computedMatch to it', function () {
      router.push('/not-the-page-youre-looking-for');

      expect(wrapper.props()).to.deep.equal({
        component: NotFound,
        computedMatch: { url: '/not-the-page-youre-looking-for', isExact: false, params: {} }
      });
    });
  });

  it('renders only first match', function () {
    const FirstComponent = () => <h1>FirstComponent</h1>;
    const SecondComponent = () => <h2>SecondComponent</h2>;

    const context = createContext();
    const router = context.app.get('router');

    const wrapper = shallow(
      <Switch>
        <Route component={FirstComponent} path="/about" />
        <Route component={SecondComponent} path="/about" />
      </Switch>,
      { context }
    );

    router.push('/about');

    expect(wrapper.type()).to.equal(Route);

    expect(wrapper.props()).to.deep.equal({
      computedMatch: { url: '/about', isExact: true, params: {} },
      component: FirstComponent,
      path: '/about'
    });
  });

  it('renders only first match, also if it\'s default', function () {
    const DefaultComponent = () => <h1>DefaultComponent</h1>;
    const SecondComponent = () => <h2>SecondComponent</h2>;

    const context = createContext();
    const router = context.app.get('router');

    const wrapper = shallow(
      <Switch>
        <Route component={DefaultComponent} />
        <Route component={SecondComponent} path="/services" />
      </Switch>,
      { context }
    );

    router.push('/services');

    expect(wrapper.type()).to.equal(Route);
    expect(wrapper.props()).to.deep.equal({
      component: DefaultComponent,
      computedMatch: { url: '/services', isExact: false, params: {} }
    });
  });

  it('renders nothing if there\'s no match', function () {
    const ServicesComponent = () => <h2>ServicesComponent</h2>;

    const context = createContext();
    const router = context.app.get('router');

    const wrapper = shallow(
      <Switch>
        <Route component={ServicesComponent} path="/services" />
      </Switch>,
      { context }
    );

    router.push('/about');

    expect(wrapper.type()).to.equal(null);
  });

  it('renders nothing if there\'s no match', function () {
    const ServicesComponent = () => <h2>ServicesComponent</h2>;

    const context = createContext();
    const router = context.app.get('router');

    const wrapper = shallow(
      <Switch>
        <Route component={ServicesComponent} path="/services" />
      </Switch>,
      { context }
    );

    router.push('/about');

    expect(wrapper.type()).to.equal(null);
  });

  it('unsubscribes from router on unmount', function () {
    let subscribeCount = 0;
    let unsubscribeCount = 0;

    const subscription = {
      unsubscribe() {
        unsubscribeCount += 1;
      }
    };

    const router = {
      getHistory$() {
        return {
          subscribe() {
            subscribeCount += 1;
            return subscription;
          }
        };
      }
    };

    const context = {
      app: {
        // eslint-disable-next-line consistent-return
        get(key) {
          if (key === 'router') {
            return router;
          }
        }
      }
    };

    const wrapper = shallow(
      <Switch />,
      { context }
    );

    expect(subscribeCount).to.equal(1);
    expect(unsubscribeCount).to.equal(0);

    wrapper.unmount();
    expect(subscribeCount).to.equal(1);
    expect(unsubscribeCount).to.equal(1);
  });

  describe('switches routes correctly when child Routes are changing', function () {
    let beforeDestroyCallCount = 0;

    const HomeApp = createApp({
      name: 'HomeApp',
      providers: [
        {
          name: 'component',
          useValue: () => <article>HomeApp</article>,
        },
      ],
      beforeDestroy: () => {
        beforeDestroyCallCount += 1;
      }
    });

    const WrapperComponent = ({ routeSet }) => {
      const changingRoutes = [];

      if (routeSet === 1) {
        changingRoutes.push(
          <Route component={() => <header>HomeComponent</header>} exact key="1h" path="/home" />,
          <Route component={() => <article>ServicesComponent</article>} key="1s" path="/services" />,
        );
      } else if (routeSet === 2) {
        changingRoutes.push(
          <Route app={HomeApp} exact key="2h" path="/home" />
        );
      }

      return (
        <Switch>
          {changingRoutes}
          <Route component={() => <div>Not found</div>} key="0n" />
        </Switch>
      );
    };

    WrapperComponent.propTypes = {
      routeSet: PropTypes.number
    };

    const context = createContext();
    const router = context.app.get('router');

    const wrapper = mount(
      <WrapperComponent routeSet={1} />,
      { context, childContextTypes: { app: PropTypes.object } }
    );

    it('renders matching Route and component from the first set', function () {
      router.push('/services');
      expect(wrapper.html()).to.equal('<article>ServicesComponent</article>');
    });

    it('falls back to default when matching Route disappears', function () {
      wrapper.setProps({ routeSet: 2 });
      expect(wrapper.html()).to.equal('<div>Not found</div>');
    });

    it('renders matching HomeApp when URL matches', function () {
      router.push("/home");
      expect(wrapper.html()).to.equal('<article>HomeApp</article>');
      expect(beforeDestroyCallCount).to.equal(0);
    });

    it('renders another component matching the same route, destorying previous app', function () {
      wrapper.setProps({ routeSet: 1 });
      expect(beforeDestroyCallCount).to.equal(1);
      expect(wrapper.html()).to.equal('<header>HomeComponent</header>');
    });
  });

  it('doesn\'t crash when not React elements are passed as children', function () {
    const DefaultComponent = () => <h2>DefaultComponent</h2>;

    const context = createContext();

    const wrapper = shallow(
      <Switch>
        {0}
        {'string'}
        <Route component={DefaultComponent} />
      </Switch>,
      { context }
    );

    expect(wrapper.type()).to.equal(Route);
    expect(wrapper.props()).to.deep.equal({
      component: DefaultComponent,
      computedMatch: { url: '/', isExact: false, params: {} }
    });
  });
});
<file_sep>---
title: About
sidebarPartial: aboutSidebar
processTemplate: true
---

# About

FrintJS was built at [Travix International](https://travix.com) for helping us with building our front-end web applications in a faster, modular and more scalable way.

## Contributors

<div class="columns is-multiline contributors">
  <% data.contributors.forEach(function (contributor) { %>
  <div class="column is-one-quarter">
    <div class="contributor">
      <a target="_blank" href="<%= contributor.html_url %>">
        <img alt="<%= contributor.name %>" src="<%= contributor.avatar_url %>" />

        <h4 class="no-permalink"><%= contributor.name %></h4>
      </a>
    </div>
  </div>
  <% }) %>
</div>

More on [GitHub](https://github.com/Travix-International/frint/graphs/contributors).
<file_sep>---
title: frint-component-utils
importContentFromPackage: frint-component-utils
sidebarPartial: docsSidebar
---
<file_sep>// @TODO: can we do it without jQuery? :D
(function () {
  function endUrlWithSlash(url) {
    if (url.endsWith('/')) {
      return url;
    }

    return url + '/';
  }

  const currentPath = endUrlWithSlash(window.location.pathname);

  // active top nav links
  const $topNavLinks = $('nav .nav-left a');
  $topNavLinks.each(function () {
    const url = endUrlWithSlash($(this).attr('href'));

    if (currentPath.indexOf(url) > -1) {
      $topNavLinks.removeClass('is-active');
      $(this).addClass('is-active');
    }
  });

  // active sidebar links
  const $asideLinks = $('aside.menu.docs > ul > li > a');
  $asideLinks.each(function () {
    const url = endUrlWithSlash($(this).attr('href'));

    if (currentPath.indexOf(url) > -1) {
      $asideLinks.removeClass('is-active');
      $(this).addClass('is-active');

      // mount TOC in sidebar from content area
      const $lists = $('.content ul');

      if ($lists.length === 0) {
        // is not TOC
        return;
      }

      if (!$($lists[0]).find('li:first-child').html().startsWith('<a')) {
        // is not TOC
        return;
      }

      const $toc = $($lists[0]);
      $toc.hide();
      $(this).after('<ul>' + $toc.html() + '</ul>');
    }
  });

  // hashed links
  $('a[href^="#"]').click(function () {
    const href = $(this).attr('href');

    if (history.pushState) {
      history.pushState(null, null, href);
    }

    $('html, body').animate({
      scrollTop: $(href).offset().top - 65
    }, 300);

    return false;
  });

  // hyperlink headers
  $('.content h1, .content h2, .content h3, .content h4').each(function () {
    const $h = $(this);

    if ($h.hasClass('no-permalink')) {
      return;
    }

    const id = $h.attr('id');
    const title = $h.text();
    const html = $h.html();

    if (html.indexOf('<a') > -1) {
      return;
    }

    const newHtml = $('<a>' + title + '</a>')
      .attr('href', '#' + id)
      .addClass('permalink')
      .click(function (e) {
        if (history.pushState) {
          history.pushState(null, null, '#' + id);
        }

        $('html, body').animate({
          scrollTop: $h.offset().top - 65
        }, 300);

        return false;
      });

    $(this).html(newHtml);
  });
})();
<file_sep>import lodashGet from 'lodash/get';
import lodashSet from 'lodash/set';
import cloneDeep from 'lodash/cloneDeep';

import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { map as map$ } from 'rxjs/operator/map';

function Model(attributes) {
  this.attributes = Object.assign({}, attributes);
  this.$ = null;
}

function getFromAttributes(attributes, key) {
  if (typeof key === 'undefined') {
    return attributes;
  }

  if (typeof key !== 'string') {
    return undefined;
  }

  return lodashGet(attributes, key);
}

Model.prototype.get = function get(key) {
  return getFromAttributes(this.attributes, key);
};

Model.prototype.set = function set(key, value) {
  lodashSet(this.attributes, key, value);

  if (this.$) {
    this.$.next(this.attributes);
  }
};

Model.prototype.get$ = function get$(key) {
  if (!this.$) {
    this.$ = new BehaviorSubject(this.attributes);
  }

  return this.$
    ::map$((attributes) => {
      return getFromAttributes(attributes, key);
    });
};

Model.prototype.toJS = function toJS() {
  return cloneDeep(this.attributes);
};

export default Model;
<file_sep>/* eslint-disable import/no-extraneous-dependencies, func-names */
/* global describe, it */
import { expect } from 'chai';
import { Subject } from 'rxjs/Subject';
import { filter as filter$ } from 'rxjs/operator/filter';
import { map as map$ } from 'rxjs/operator/map';

import combineEpics from './combineEpics';
import ActionsObservable from './ActionsObservable';

describe('frint-store › combineEpics', function () {
  it('triggers epics correct response', function () {
    const pingEpic = function (action$) {
      return action$
        ::filter$(action => action.type === 'PING')
        ::map$(() => ({ type: 'PONG' }));
    };

    const rootEpic = combineEpics(pingEpic);

    const subject$ = new Subject();
    const actions$ = new ActionsObservable(subject$);
    const result$ = rootEpic(actions$);
    const emittedActions = [];

    const subscription = result$.subscribe((emittedAction) => {
      emittedActions.push(emittedAction);
    });
    subject$.next({ type: 'PING' });

    expect(emittedActions).to.deep.equal([
      { type: 'PONG' }
    ]);

    subscription.unsubscribe();
  });
});
<file_sep>module.exports = {
  // only show these contributors in about page
  contributorsOptedIn: [
    'AlexDudar',
    'alexmiranda',
    'asci',
    'fahad19',
    'gtzio',
    'jackTheRipper',
    'markvincze',
    'mAiNiNfEcTiOn',
    'spzm',
  ],
};
<file_sep>/* eslint-disable global-require, import/no-dynamic-require */
const path = require('path');
const createApp = require('frint').createApp;

module.exports = createApp({
  name: 'version',

  providers: [
    {
      name: 'summary',
      useValue: 'Shows current version number of frint-cli',
    },
    {
      name: 'execute',
      useFactory: function useFactory(deps) {
        return function execute() {
          const pkg = JSON.parse(
            deps.fs.readFileSync(
              path.resolve(`${__dirname}/../package.json`)
            )
          );

          deps.console.log(`v${pkg.version}`);
        };
      },
      deps: [
        'console',
        'fs',
      ],
    }
  ],
});
