├── .npmignore
├── .gitignore
├── manual
├── asset
│ ├── logo.png
│ ├── context0.png
│ ├── context1.png
│ ├── patchSet.png
│ ├── render.png
│ ├── architecture.png
│ ├── logo.svg
│ └── architecture.svg
└── overview.md
├── esdoc.json
├── examples
├── src
│ ├── index.js
│ ├── vulp.js
│ ├── todos
│ │ ├── components
│ │ │ ├── Todo.js
│ │ │ ├── StateView.js
│ │ │ ├── Checkbox.js
│ │ │ ├── Header.js
│ │ │ ├── List.js
│ │ │ ├── Footer.js
│ │ │ └── App.js
│ │ ├── index.html
│ │ └── index.js
│ ├── readme
│ │ ├── index.js
│ │ └── index.html
│ └── counter
│ │ ├── index.html
│ │ └── index.js
├── .babelrc
└── package.json
├── .babelrc
├── tools
├── gobble-eslint.js
├── gobble-replace.js
├── gobble-esdoc-shell.js
├── gobble-mocha-shell.js
├── release.js
└── gobble-browserify.js
├── src
├── utils
│ ├── curry.js
│ ├── cycle.js
│ ├── choke.js
│ ├── patch.js
│ └── checkType.js
├── decorators
│ ├── name.js
│ ├── component.js
│ ├── index.js
│ ├── memoize.js
│ ├── debug.js
│ ├── checkContextType.js
│ ├── log.js
│ ├── styler.js
│ ├── mount.js
│ ├── controller.js
│ ├── dispatchChangeSets.js
│ └── utils.js
├── scopes
│ ├── value.js
│ ├── index.js
│ ├── fragment.js
│ └── combiner.js
├── views
│ ├── index.js
│ └── dom.js
├── Patch.js
├── state
│ └── index.js
├── tests
│ ├── state.js
│ ├── Context.js
│ └── Transform.js
├── index.js
├── Context.js
├── Transform.js
└── JSONPointer
│ └── index.js
├── LICENSE
├── gobblefile.js
├── package.json
├── README.md
└── .eslintrc
/.npmignore:
--------------------------------------------------------------------------------
1 | img
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | docs
4 | .gobble*
5 |
--------------------------------------------------------------------------------
/manual/asset/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freemountain/vulp/HEAD/manual/asset/logo.png
--------------------------------------------------------------------------------
/manual/overview.md:
--------------------------------------------------------------------------------
1 | # Overview
2 |
3 | ## Architecture
4 | 
5 |
--------------------------------------------------------------------------------
/manual/asset/context0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freemountain/vulp/HEAD/manual/asset/context0.png
--------------------------------------------------------------------------------
/manual/asset/context1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freemountain/vulp/HEAD/manual/asset/context1.png
--------------------------------------------------------------------------------
/manual/asset/patchSet.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freemountain/vulp/HEAD/manual/asset/patchSet.png
--------------------------------------------------------------------------------
/manual/asset/render.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freemountain/vulp/HEAD/manual/asset/render.png
--------------------------------------------------------------------------------
/manual/asset/architecture.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freemountain/vulp/HEAD/manual/asset/architecture.png
--------------------------------------------------------------------------------
/esdoc.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "vulp",
3 | "manual": {
4 | "asset": "./manual/asset",
5 | "overview": ["./manual/overview.md"]
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/examples/src/index.js:
--------------------------------------------------------------------------------
1 | import todos from './todos';
2 | import counter from './counter';
3 | import readme from './readme';
4 |
5 | export {
6 | todos,
7 | counter,
8 | readme
9 | };
10 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "ignore": [
3 | "node_modules",
4 | "dist"
5 | ],
6 | "presets": ["es2015", "stage-2"],
7 | "plugins": [
8 | ["transform-react-jsx", {"pragma": "element"}]
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/examples/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "ignore": [
3 | "node_modules",
4 | "dist"
5 | ],
6 | "presets": ["es2015", "stage-2"],
7 | "plugins": [
8 | ["transform-react-jsx", {"pragma": "element"}]
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/examples/src/vulp.js:
--------------------------------------------------------------------------------
1 | import { scopes, decorators, views, cycle, element } from './../src';
2 |
3 | /*
4 | include shim
5 | if you're using the starter package, import 'vulp' inside your files
6 | */
7 |
8 | export { scopes, decorators, views, cycle, element };
9 |
--------------------------------------------------------------------------------
/tools/gobble-eslint.js:
--------------------------------------------------------------------------------
1 | const rewire = require("rewire");
2 | const eslint = rewire('gobble-eslint');
3 |
4 | eslint.__set__('linter', require('eslint').linter);
5 |
6 | module.exports = function esdocShell(inputdir, options) {
7 | return eslint.call(this, inputdir, {});
8 | };
9 |
--------------------------------------------------------------------------------
/tools/gobble-replace.js:
--------------------------------------------------------------------------------
1 | const regxp = /from \'\.\/(\.\.\/)*vulp\'/;
2 | const subst = 'from \'vulp\'';
3 |
4 | function replace(input) {
5 | return input.replace(regxp, subst);
6 | }
7 |
8 | replace.defaults = {
9 | accept: ['.js']
10 | };
11 |
12 | module.exports = replace;
13 |
--------------------------------------------------------------------------------
/src/utils/curry.js:
--------------------------------------------------------------------------------
1 | export default function(f, arity) {
2 | const length = Number.isInteger(arity) ? arity : f.length;
3 |
4 | function call(...args) {
5 | if(args.length >= length) return f.apply(this, args);
6 | return (...newArgs) => call(...args.concat(newArgs));
7 | }
8 |
9 | return call;
10 | }
11 |
--------------------------------------------------------------------------------
/src/decorators/name.js:
--------------------------------------------------------------------------------
1 | import { normalize } from './utils';
2 |
3 | /**
4 | * add name to component - for debug messages
5 | *
6 | * @param {string} name - component name
7 | * @return {HOC}
8 | */
9 | export default function(name) {
10 | return rawComponent => Object.assign({}, normalize(rawComponent), { name });
11 | }
12 |
--------------------------------------------------------------------------------
/examples/src/todos/components/Todo.js:
--------------------------------------------------------------------------------
1 | import { element, decorators } from './../../vulp';
2 | import _Checkbox from './Checkbox';
3 |
4 | const { memoize, component, name } = decorators;
5 | const Checkbox = decorators.mount({ checked: '/completed' })(_Checkbox);
6 |
7 | export default component(
8 | memoize(),
9 | name('Todo')
10 | )(function({ context }) {
11 |
12 | return (
13 |
14 | {context.get('/title')}
15 |
16 | );
17 | });
18 |
--------------------------------------------------------------------------------
/examples/src/todos/components/StateView.js:
--------------------------------------------------------------------------------
1 | import { element, decorators } from './../../vulp';
2 |
3 | const { component, memoize, styler } = decorators;
4 |
5 | function render({ context }) {
6 | const text = JSON.stringify(context.get('', true), null, ' ');
7 |
8 | return (
9 |
10 |
13 |
14 | );
15 | }
16 |
17 | export default component(
18 | memoize(),
19 | styler()
20 | )(render);
21 |
--------------------------------------------------------------------------------
/tools/gobble-esdoc-shell.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const esdoc = require('esdoc');
3 | const publisher = require('esdoc/out/src/Publisher/publish');
4 |
5 | module.exports = function esdocShell(inputdir, outputdir, options, cb) {
6 | const source = path.resolve(inputdir, options.source || '');
7 | const destination = path.resolve(outputdir, options.source || '');
8 | const esdocOptions = Object.assign({}, options, { source, destination });
9 |
10 | esdoc.generate(Object.assign({}, esdocOptions), publisher);
11 | cb();
12 | };
13 |
--------------------------------------------------------------------------------
/examples/src/readme/index.js:
--------------------------------------------------------------------------------
1 | import { element, cycle, views, scopes, decorators } from './../vulp';
2 |
3 | const { component, controller, dispatchChangeSets } = decorators;
4 | const App = component(
5 | dispatchChangeSets(),
6 | controller({
7 | inc: ['/count', count => count + 1]
8 | }),
9 | )(function({ context }) {
10 | return (
11 |
12 | {context.get('/count')}
13 |
);
14 | });
15 |
16 | const view = views.dom(document.body, App);
17 | const scope = scopes.value({ count: 0 });
18 |
19 | export default () => cycle(view, scope);
20 |
--------------------------------------------------------------------------------
/examples/src/todos/components/Checkbox.js:
--------------------------------------------------------------------------------
1 | import { element, decorators } from './../../vulp';
2 |
3 | const { memoize, controller, component, dispatchChangeSets, name } = decorators;
4 |
5 | export default component(
6 | memoize(),
7 | dispatchChangeSets(),
8 | controller({
9 | uncheck: ['/checked', false],
10 | check: ['/checked', true]
11 | }),
12 | name('Checkbox'),
13 | )(({ context }) => {
14 | const checked = context.get('/checked');
15 |
16 | return ();
21 | });
22 |
--------------------------------------------------------------------------------
/src/decorators/component.js:
--------------------------------------------------------------------------------
1 | import { normalize } from './utils/';
2 |
3 | /**
4 | * apply multiple decorators on component
5 | *
6 | * ```javascript
7 | * component(
8 | * someDeoraotor(),
9 | * someOther()
10 | * )(model => { ... })
11 | * ```
12 | *
13 | * @param {...HOC} decoration - list of component decorators (hocs)
14 | * @return {HOC}
15 | */
16 |
17 | export default function component(...decoration) {
18 | return rawComponent => {
19 | const comp = normalize(rawComponent);
20 |
21 | return decoration
22 | .slice()
23 | .reverse()
24 | .reduce((target, decorator) => decorator(target), comp);
25 | };
26 | }
27 |
--------------------------------------------------------------------------------
/src/utils/cycle.js:
--------------------------------------------------------------------------------
1 | import flyd from 'flyd';
2 |
3 | /**
4 | * pipes a to b and b to a -> cycle a and b
5 | * @param {BoundViewFactory} viewF - view factory
6 | * @param {BoundScopeFactory} scopeF - scope factory
7 | * @return {Stream} empty stream, use to end
8 | */
9 |
10 | export default function cycle(viewF, scopeF) {
11 | const start = flyd.stream();
12 | const viewStream = viewF(start);
13 | const scopeStream = scopeF(viewStream);
14 |
15 | flyd.on(val => console.log('view emitted:', val), viewStream);
16 | flyd.on(ctx => console.log('scope emitted:', ctx.get(true)), scopeStream);
17 |
18 | return flyd.on(x => start(x), scopeStream);
19 | }
20 |
--------------------------------------------------------------------------------
/src/utils/choke.js:
--------------------------------------------------------------------------------
1 | /**
2 | * creates a choke that throttle fuction calls
3 | *
4 | * The returned choke function will throttle calls with the same path argument
5 | * for delta milliseconds.
6 | *
7 | * @param {Number} delta - the delta between calls [ms]
8 | * @return {function(path: String, f: Function)}
9 | */
10 |
11 | export default function choke(delta) {
12 | const cache = {};
13 |
14 | function call(path, f) {
15 | const lastTime = cache[ path ];
16 | const currentTime = Date.now();
17 |
18 | if(lastTime && currentTime - lastTime < delta) return;
19 |
20 | cache[ path ] = Date.now();
21 | f();
22 | }
23 |
24 | return call;
25 | }
26 |
--------------------------------------------------------------------------------
/src/scopes/value.js:
--------------------------------------------------------------------------------
1 | import flyd from 'flyd';
2 |
3 | import Context from './../Context';
4 |
5 | /**
6 | * value scope factory
7 | * Scope will update and emit value on every patch action.
8 | * @param {Object} init - init value as plain object
9 | * @param {stream} input - input flyd stream
10 | * @return {Scope}
11 | */
12 | function value(init, input) {
13 | let current = Context.ofState(init);
14 | const output = flyd.stream(current);
15 |
16 | flyd.on(function(patchSet) {
17 | if(patchSet.length === 0) return;
18 | current = current.update(patchSet);
19 | output(current);
20 | }, input);
21 |
22 | return output;
23 | }
24 | export default value;
25 |
--------------------------------------------------------------------------------
/src/decorators/index.js:
--------------------------------------------------------------------------------
1 | import mount from './mount';
2 | import controller from './controller';
3 | import memoize from './memoize';
4 | import styler from './styler';
5 | import checkContextType from './checkContextType';
6 | import component from './component';
7 | import name from './name';
8 | import dispatchChangeSets from './dispatchChangeSets';
9 | import logLifecycle from './log';
10 |
11 | /**
12 | * Higher Order Component
13 | * @typedef {function(component: Component): Component} HOC
14 | */
15 |
16 | export default {
17 | name,
18 | component,
19 | mount,
20 | controller,
21 | memoize,
22 | styler,
23 | checkContextType,
24 | dispatchChangeSets,
25 | logLifecycle
26 | };
27 |
--------------------------------------------------------------------------------
/src/utils/patch.js:
--------------------------------------------------------------------------------
1 | import t from 'tcomb';
2 |
3 | import { box } from './../state';
4 | import JSONPointer from './../JSONPointer';
5 |
6 | function toPatch(path, rawValue) {
7 | const op = JSONPointer.ofString(path).last() === '-' ? 'add' : 'replace';
8 | const value = box(rawValue);
9 |
10 | return {
11 | op,
12 | path,
13 | value
14 | };
15 | }
16 |
17 | export default function handler(context, pairs) {
18 | return pairs.map(function([path, value]) {
19 | if(!t.Function.is(value)) return toPatch(path, value);
20 |
21 | const currentValue = context.get(path, true);
22 | const nextValue = value(currentValue);
23 |
24 | return toPatch(path, nextValue);
25 | });
26 | }
27 |
--------------------------------------------------------------------------------
/tools/gobble-mocha-shell.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const childProcess = require('child_process');
3 |
4 | module.exports = function mochaShell(inputdir, options, cb) {
5 | const mochaCmd = path.join(process.cwd(), './node_modules/.bin/_mocha');
6 | const dest = `${path.join(inputdir, options.files)}`;
7 |
8 | const compiler = Object
9 | .keys(options.compiler || {})
10 | .map(ext => `${ext}:${options.compiler[ ext ]}`)
11 | .join(' ');
12 |
13 | const mochaArgs = compiler === '' ? [] : ['--compilers', compiler];
14 |
15 | mochaArgs.push(dest);
16 |
17 | const cmd = childProcess.spawn(mochaCmd, mochaArgs, { stdio: 'inherit' });
18 |
19 | cmd.on('exit', () => cb());
20 | }
21 |
--------------------------------------------------------------------------------
/examples/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vulp-starter",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "dev": "cp -R src/ dist && find dist -name '*.js' -delete && http-server & watchify src/index.js -d -t [ babelify ] -s examples --outfile dist/examples.js"
8 | },
9 | "author": "",
10 | "license": "ISC",
11 | "dependencies": {
12 | "vulp": "0.7.3",
13 | "tcomb": "^2.7.0"
14 | },
15 | "devDependencies": {
16 | "babel-plugin-transform-react-jsx": "^6.6.5",
17 | "babel-preset-es2015": "^6.6.0",
18 | "babel-preset-stage-2": "^6.5.0",
19 | "babelify": "^7.2.0",
20 | "http-server": "^0.9.0",
21 | "watchify": "^3.7.0"
22 | }
23 | }
--------------------------------------------------------------------------------
/src/decorators/memoize.js:
--------------------------------------------------------------------------------
1 | import dekuMemoize from 'deku-memoize';
2 |
3 | import { normalize } from './utils';
4 |
5 | function defaultShouldUpdate(prev, next) {
6 | return next.context.changed(prev.context);
7 | }
8 |
9 | /**
10 | * memoize component
11 | *
12 | * @param {function(nextModel: Model, prevModel: Model): boolean} shouldUpdate - list of component decorators (hocs)
13 | * @return {HOC}
14 | */
15 |
16 | export default function(shouldUpdate = defaultShouldUpdate) {
17 | return rawComponent => {
18 | const component = normalize(rawComponent);
19 | const decoratedComponent = Object.assign({}, component, { shouldUpdate });
20 |
21 | return dekuMemoize(decoratedComponent);
22 | };
23 | }
24 |
--------------------------------------------------------------------------------
/examples/src/todos/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Deku
7 |
8 |
13 |
14 |
15 |
16 |
17 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/utils/checkType.js:
--------------------------------------------------------------------------------
1 | import t from 'tcomb';
2 |
3 | /**
4 | * tcomb type
5 | * @external {Type} https://github.com/gcanti/tcomb/blob/master/docs/API.md
6 | */
7 |
8 | const Type = t.irreducible('Type', t.isType);
9 |
10 | /**
11 | * 'booleanized' check function from tcomb
12 | * @type Function
13 | * @param {Type} T - tcomb type
14 | * @param {Any} x - value to check
15 | * @return {Boolean}
16 | */
17 |
18 | const check = t.func([Type, t.Any], t.Bool).of(function(T, x) {
19 | let result = null;
20 |
21 | /* eslint space-after-keywords: 0 */
22 | try {
23 | T(x);
24 | result = true;
25 | } catch(e) {
26 | result = false;
27 | }
28 | return result;
29 | });
30 |
31 | export default check;
32 |
--------------------------------------------------------------------------------
/src/views/index.js:
--------------------------------------------------------------------------------
1 | import dom from './dom';
2 |
3 | /**
4 | * view
5 | * - view stream representation
6 | * - views listen on contexts and render them in some way (e.g. dom)
7 | * - a view can emit patchSets
8 | ´* - flyd stream
9 | * @see https://github.com/paldepind/flyd#flydstream
10 | * @listens {Context}
11 | * @emits {PatchSet}
12 | * @typedef View
13 | */
14 |
15 | /**
16 | * view factory
17 | * - creates view, who listen on scops
18 | * - curried: opts -> input -> view
19 | * @typedef {function(...opts: any) : BoundViewFactory} ViewFactory
20 | */
21 |
22 | /**
23 | * bound view factory
24 | * @typedef {function(input: any) : View} BoundViewFactory
25 | */
26 |
27 | export default {
28 | dom
29 | };
30 |
--------------------------------------------------------------------------------
/examples/src/counter/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Deku
7 |
8 |
13 |
14 |
15 |
16 |
17 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/examples/src/readme/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Deku
7 |
8 |
13 |
14 |
15 |
16 |
17 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/examples/src/todos/index.js:
--------------------------------------------------------------------------------
1 | import App from './components/App';
2 | import { views, scopes, decorators, cycle } from './../vulp';
3 |
4 | const initialValue = {
5 | todos: [
6 | { completed: true, title: 'foo' },
7 | { completed: false, title: 'bar' }
8 | ],
9 | draft: ''
10 | };
11 |
12 |
13 | const MountedApp = decorators.mount({
14 | todos: '/state/todos',
15 | draft: '/state/draft',
16 | filter: '/fragment/value'
17 | })(App);
18 |
19 | const view = views.dom(document.body, MountedApp);
20 |
21 | const state = scopes.value(initialValue);
22 | const fragment = scopes.fragment();
23 |
24 | const rootSscope = scopes.combiner({ state, fragment });
25 |
26 | export default () => cycle(view, rootSscope);
27 |
--------------------------------------------------------------------------------
/src/Patch.js:
--------------------------------------------------------------------------------
1 | import t from 'tcomb';
2 |
3 | const specificString = str => t.irreducible(`String: ${str}`, x => str === x);
4 |
5 | const BasePatch = t.struct({
6 | path: t.String
7 | });
8 |
9 | const AddPatch = BasePatch.extend({
10 | op: specificString('add'),
11 | value: t.Any
12 | }, 'AddPatch');
13 |
14 | const RemovePatch = BasePatch.extend({
15 | op: specificString('remove')
16 | }, 'RemovePatch');
17 |
18 | const ReplacePatch = BasePatch.extend({
19 | op: specificString('replace'),
20 | value: t.Any
21 | }, 'ReplacePatch');
22 |
23 | /**
24 | * json patch type.
25 | * - only add, remove and replace patches are supported
26 | * - http://jsonpatch.com
27 | * @typedef {object} Patch
28 | */
29 |
30 | export const Patch = t.union([AddPatch, RemovePatch, ReplacePatch], 'Patch');
31 |
--------------------------------------------------------------------------------
/src/decorators/debug.js:
--------------------------------------------------------------------------------
1 | import { specDecorator } from './utils';
2 |
3 | const warn = msg => console.warn ? console.warn(msg) : console.log(`Waring:\n${msg}`);
4 | const dir = obj => console.dir ? console.dir(obj) : console.log(obj);
5 | const createHandler = name => function(component, model) {
6 | try {
7 | return component[ name ](model);
8 | } catch(e) {
9 | warn(`Error in ${component.name}#${name}`);
10 | console.log('Component:');
11 | dir(component);
12 | throw e;
13 | }
14 | };
15 |
16 | const spec = {
17 | deep: true,
18 | cache: new WeakMap(),
19 | name: 'debug',
20 | render: createHandler('render'),
21 | onCreate: createHandler('onCreate'),
22 | onUpdate: createHandler('onUpdate'),
23 | onRemove: createHandler('onRemove')
24 | };
25 |
26 | export default () => specDecorator(spec, false);
27 |
--------------------------------------------------------------------------------
/examples/src/counter/index.js:
--------------------------------------------------------------------------------
1 | import t from 'tcomb';
2 | import { element, cycle, views, scopes, decorators } from './../vulp';
3 |
4 | const { component, checkContextType, controller, dispatchChangeSets } = decorators;
5 |
6 | const contextType = t.struct({
7 | count: t.Number
8 | });
9 |
10 | const IncBtn = component(
11 | dispatchChangeSets(),
12 | controller({
13 | inc: ['/count', count => count + 1]
14 | })
15 | )(() => (
16 |
17 | ));
18 |
19 | const App = component(
20 | checkContextType(contextType)
21 | )(({ context }) => (
22 |
23 |
24 | {context.get('/count')}
25 |
26 | ));
27 |
28 | const renderSubject = views.dom(document.body, App);
29 | const storeSubject = scopes.value({ count: 0 });
30 |
31 | export default () => cycle(renderSubject, storeSubject);
32 |
--------------------------------------------------------------------------------
/examples/src/todos/components/Header.js:
--------------------------------------------------------------------------------
1 | import { element, decorators } from './../../vulp';
2 |
3 | const { controller, component, dispatchChangeSets, name, memoize } = decorators;
4 |
5 | function key({ event }) {
6 | // keyCode 27 => ESCAPE
7 | if(event.keyCode === 27) return ['/draft', ''];
8 |
9 | // keyCode 13 => Enter
10 | if(event.keyCode === 13) {
11 | const title = event.target.value;
12 |
13 | event.target.value = '';
14 | return [
15 | '/draft', '',
16 | '/todos/-', { completed: false, title }
17 | ];
18 | }
19 |
20 | return ['/draft', event.target.value];
21 | }
22 |
23 | export default component(
24 | memoize(),
25 | dispatchChangeSets(),
26 | controller({ key }),
27 | name('Header')
28 | )(({ context }) => (
29 |
30 |
35 |
36 | ));
37 |
--------------------------------------------------------------------------------
/src/views/dom.js:
--------------------------------------------------------------------------------
1 | import flyd from 'flyd';
2 |
3 | import { createApp as createDekuApp, element } from 'deku';
4 | import debug from './../decorators/debug';
5 |
6 | /**
7 | * This function is used to create the render part of your app.
8 | * If the view receives a context object, it will pass the context through your component.
9 | * All actions dispatched inside component, will be emitted from the view.
10 | * @param {DOMElement} container - container dom element
11 | * @param {Component} rawComponent - deku component
12 | * @return {BoundViewFactory}
13 | */
14 |
15 | export default function dom(container, rawComponent) {
16 | return input => {
17 | const dispatchStream = flyd.stream();
18 | const dispatch = val => dispatchStream(val);
19 | const render = createDekuApp(container, dispatch);
20 | const component = debug()(rawComponent);
21 |
22 | flyd.on(function(ctx) {
23 | const el = element(component);
24 |
25 | return render(el, ctx);
26 | }, input);
27 |
28 | return dispatchStream;
29 | };
30 | }
31 |
--------------------------------------------------------------------------------
/examples/src/todos/components/List.js:
--------------------------------------------------------------------------------
1 | import { element, decorators } from './../../vulp';
2 |
3 | import _Todo from './Todo';
4 |
5 | const { memoize, component, name } = decorators;
6 |
7 | const filters = {
8 | all: () => true,
9 | completed: todo => todo.completed,
10 | active: todo => !todo.completed
11 | };
12 |
13 | function render({ context }) {
14 | let filter = filters[ context.get('/filter') ];
15 |
16 | if(!filter) filter = filters.all;
17 |
18 | const todos = context.get('/todos', true).slice();
19 |
20 | const children = todos
21 | .map((_, i) => i)
22 | .filter(i => filter(todos[ i ]))
23 | .map(function(index) {
24 | const Todo = decorators.mount({
25 | title: `/todos/${index}/title`,
26 | completed: `/todos/${index}/completed`
27 | })(_Todo);
28 |
29 | return ();
30 | });
31 |
32 | return (
33 |
36 | );
37 | }
38 |
39 | export default component(
40 | memoize(),
41 | name('List')
42 | )(render);
43 |
--------------------------------------------------------------------------------
/src/scopes/index.js:
--------------------------------------------------------------------------------
1 | import _value from './value';
2 | import _fragment from './fragment';
3 | import _combiner from './combiner';
4 | import flyd from 'flyd';
5 |
6 | const wrapFactory = f => opts => input => f(opts, input);
7 |
8 | const value = wrapFactory(_value);
9 | const fragment = wrapFactory(_fragment);
10 | const combiner = wrapFactory(_combiner);
11 |
12 | window.flyd = flyd;
13 |
14 |
15 | /**
16 | * scope
17 | * - stream representation of json data that (may) change
18 | ´* - flyd stream
19 | * @see https://github.com/paldepind/flyd#flydstream
20 | * @listens {PatchSet}
21 | * @emits {Context}
22 | * @typedef {function(patchAction: PatchSet) } Scope
23 | */
24 |
25 | /**
26 | * scope factory
27 | * - creates scope, who listen on input
28 | * - curried: opts -> input -> scope
29 | * @typedef {function(...opts: any) : BoundScopeFactory} ScopeFactory
30 | */
31 |
32 | /**
33 | * bound scope factory
34 | * @typedef {function(input: any) : Context} BoundScopeFactory
35 | */
36 |
37 | export default {
38 | value,
39 | fragment,
40 | combiner
41 | };
42 |
--------------------------------------------------------------------------------
/examples/src/todos/components/Footer.js:
--------------------------------------------------------------------------------
1 | import { element, decorators } from './../../vulp';
2 |
3 | const { component, memoize, controller, dispatchChangeSets, name } = decorators;
4 |
5 | const createOption = (name, filter) => (
6 |
9 | );
10 |
11 | function render({ context }) {
12 | const filter = context.get('/filter');
13 |
14 | return (
15 |
16 |
23 |
28 |
29 | );
30 | }
31 |
32 | export default component(
33 | memoize(),
34 | dispatchChangeSets(),
35 | controller({
36 | filterChange: ({ event }) => ['/filter', event.srcElement.value]
37 | }),
38 | name('Footer')
39 | )(render);
40 |
--------------------------------------------------------------------------------
/src/state/index.js:
--------------------------------------------------------------------------------
1 | import t from 'tcomb';
2 | import Immutable from 'immutable';
3 |
4 | export const is = {
5 | Irreducible: x => t.String.is(x) || t.Number.is(x) || t.Boolean.is(x) || t.Nil.is(x),
6 | Object: x => t.Object.is(x),
7 | Array: x => t.Array.is(x),
8 | Map: x => Immutable.Map.isMap(x),
9 | List: x => Immutable.List.isList(x)
10 | };
11 |
12 | export function box(val) {
13 | if(is.Irreducible(val)) return val;
14 | if(is.Array(val)) return Immutable.List(val).map(e => box(e));
15 | if(is.Object(val)) return Immutable.Map(val).map(e => box(e));
16 |
17 | throw new Error('Value is Function...');
18 | }
19 |
20 | export function unbox(state) {
21 | if(is.Map(state) || is.List(state)) return state.map(e => unbox(e)).toJS();
22 | return state;
23 | }
24 |
25 | export function get(state, pointer, notFound = null) {
26 | if(pointer.size() === 0) return state;
27 |
28 | if(is.Irreducible(state)) return notFound;
29 |
30 | const nextState = state.get(pointer.first(), notFound);
31 | const nextPointer = pointer.slice(1);
32 |
33 | return get(nextState, nextPointer, notFound);
34 | }
35 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Dominik Freiberger
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/decorators/checkContextType.js:
--------------------------------------------------------------------------------
1 | import { specDecorator } from './utils';
2 | import check from './../utils/checkType';
3 |
4 | /**
5 | * assert context type
6 | *
7 | * ```javascript
8 | * import t from 'tcomb'
9 | *
10 | * checkContextType(t.struct({
11 | * someProp: t.String
12 | * }))(model => { ... })
13 | * ```
14 | *
15 | * @param {Type} T - type to check context again (tcomb type)
16 | * @return {HOC}
17 | */
18 | export default function checkContextType(T) {
19 | const resultCache = new WeakMap();
20 | const spec = {
21 | deep: false,
22 | model: function(component, model) {
23 | const cacheEntry = resultCache.get(model.context);
24 |
25 | if(cacheEntry === true) return model;
26 | if(cacheEntry === false) T(model.context.get(true));
27 |
28 | const unboxedCtx = model.context.get(true);
29 | const result = check(T, unboxedCtx);
30 |
31 | resultCache.set(model.context, result);
32 | if(result === false) T(unboxedCtx);
33 |
34 | return model;
35 | }
36 | };
37 | const decorator = specDecorator(spec);
38 |
39 | return comp => {
40 | return decorator(comp);
41 | };
42 | }
43 |
--------------------------------------------------------------------------------
/examples/src/todos/components/App.js:
--------------------------------------------------------------------------------
1 | import t from 'tcomb';
2 |
3 | import { element, decorators } from './../../vulp';
4 |
5 | import List from './List';
6 | import Header from './Header';
7 | import Footer from './Footer';
8 | import StateView from './StateView';
9 |
10 | const { component, checkContextType, styler, name } = decorators;
11 |
12 | const contextType = t.struct({
13 | todos: t.list(t.struct({
14 | completed: t.Boolean,
15 | title: t.String
16 | })),
17 | draft: t.String,
18 | filter: t.String
19 | });
20 |
21 |
22 | function render({ context }) {
23 | const containerStyle = {
24 | display: 'flex'
25 | };
26 |
27 | const leftStyle = {
28 | flexBasis: '70%'
29 | };
30 |
31 | const rightStyle = {
32 | flexBasis: '30%'
33 | };
34 |
35 | return (
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | );
47 | }
48 |
49 | export default component(
50 | checkContextType(contextType),
51 | styler(),
52 | name('App')
53 | )(render);
54 |
--------------------------------------------------------------------------------
/src/tests/state.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 |
3 | import { box, unbox, get } from './../state';
4 | import JSONPointer from './../JSONPointer';
5 |
6 | describe('state', function() {
7 | describe('box', function() {
8 | it('boxes Objects', function() {
9 | const value = { foo: { bar: 'baz' } };
10 | const state = box(value);
11 |
12 | expect(state.get('foo').get('bar')).to.equal('baz');
13 | });
14 |
15 | it('boxes Array', function() {
16 | const value = [1, [10, 11]];
17 | const state = box(value);
18 |
19 | expect(state.get(0)).to.equal(1);
20 | expect(state.get(1).get(0)).to.equal(10);
21 | });
22 | });
23 |
24 | it('#unbox', function() {
25 | const unboxed = {
26 | dict: { foo: 1 },
27 | list: [0, 1]
28 | };
29 | const state = box(unboxed);
30 | const value = unbox(state);
31 |
32 | expect(value.dict.foo).to.equal(1);
33 | expect(value.list[ 0 ]).to.equal(0);
34 | });
35 |
36 |
37 | it('#get', function() {
38 | const state = box({
39 | dict: { foo: 1 },
40 | list: [0, 1]
41 | });
42 | const pointerFoo = JSONPointer.ofString('/dict/foo');
43 | const pointer = JSONPointer.ofString('/list/0');
44 |
45 | expect(get(state, pointerFoo)).to.equal(1);
46 | expect(get(state, pointer)).to.equal(0);
47 | });
48 | });
49 |
--------------------------------------------------------------------------------
/src/tests/Context.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 |
3 | import Context from './../Context';
4 |
5 | describe('Context', function() {
6 | describe('#of', function() {
7 | it('creates Context', function() {
8 | const context = Context.ofState({
9 | counter: 1,
10 | label: 'huhu'
11 | });
12 |
13 | expect(context instanceof Context).to.equal(true);
14 | expect(context.state.get('counter')).to.equal(1);
15 | expect(context.state.get('label')).to.equal('huhu');
16 | });
17 | });
18 |
19 | describe('#get', function() {
20 | it('returns value', function() {
21 | const root = Context.ofState({
22 | foo: { bar: 1 },
23 | baz: 'huhu'
24 | });
25 |
26 | expect(root.get('/baz')).to.equal('huhu');
27 | expect(root.get('/foo/bar')).to.equal(1);
28 | });
29 | });
30 |
31 | describe('#changed', function() {
32 | it('empty', function() {
33 | const a = Context.ofState();
34 | const b = Context.ofState();
35 | const c = Context.ofState({ i: 4 });
36 |
37 |
38 | expect(a.changed(b)).to.equal(false);
39 | expect(a.changed(c)).to.equal(true);
40 | });
41 |
42 | it('changed state', function() {
43 | const a = Context.ofState({ i: 4 });
44 | const b = Context.ofState({ i: 5 });
45 |
46 | expect(a.changed(b)).to.equal(true);
47 | });
48 | });
49 | });
50 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * jsx compatible element function.
3 | * @typedef {function} element
4 | * @see http://dekujs.github.io/deku/docs/api/element.html
5 | */
6 | import { element } from 'deku';
7 |
8 | import decorators from './decorators';
9 | import scopes from './scopes';
10 | import views from './views';
11 |
12 | import cycle from './utils/cycle';
13 |
14 | /**
15 | * @see http://dekujs.github.io/deku/docs/basics/components.html#model
16 | * @typedef {Object} model
17 | * @property {Map} model.props
18 | * @property {string} model.path - unique path to the component
19 | * @property {Context} model.context
20 | * @property {Array} model.children
21 | */
22 |
23 | /**
24 | * @see http://dekujs.github.io/deku/docs/basics/components.html
25 | * @typedef {Object} Component
26 | * @property {function(model: model): vnode} Component.render
27 | * @property {function(model: model)} Component.onCreate
28 | * @property {function(model: model)} Component.onUpdate
29 | * @property {function(model: model)} Component.onRemove
30 | */
31 |
32 | /**
33 | * @see http://dekujs.github.io/deku/docs/basics/elements.html
34 | * @typedef {Object} vnode
35 | * @property {string|Component} vnode.type
36 | * @property {Map} vnode.attributes
37 | * @property {Array} vnode.children
38 | */
39 |
40 |
41 | export {
42 | scopes,
43 | views,
44 | decorators,
45 | cycle,
46 | element
47 | };
48 |
--------------------------------------------------------------------------------
/src/scopes/fragment.js:
--------------------------------------------------------------------------------
1 | import flyd from 'flyd';
2 | import filter from 'flyd/module/filter';
3 | import Context from './../Context';
4 | import value from './value';
5 |
6 | const getFragment = () => location.hash.slice(1);
7 |
8 | function fragmentValue(input) {
9 | const output = flyd.stream(getFragment());
10 |
11 | window.addEventListener('hashchange', function() {
12 | const current = getFragment();
13 |
14 | if(output() === current) return;
15 | output(current);
16 | });
17 |
18 | flyd.on(function(fragmentStr) {
19 | if(output() === fragmentStr) return;
20 | location.hash = '#'.concat(fragmentStr);
21 | output(fragmentStr);
22 | }, input);
23 |
24 | return output;
25 | }
26 |
27 | const createCtx = val => Context.ofState({ value: val });
28 |
29 |
30 | /**
31 | * fragment scope factory
32 | * represents value of fragment identifier.
33 | * Json structure:
34 | * {
35 | * value: String
36 | * }
37 | * @param {Object} opts - not used
38 | * @param {Scope} input - input stream
39 | * @return {Scope}
40 | */
41 |
42 | function fragment(opts, input) {
43 | const filteredInput = filter(patchSet => patchSet.length !== 0, input);
44 | const valueStream = value({ value: getFragment() }, filteredInput);
45 | const fragmentInput = flyd.map(ctx => ctx.get('/value'), valueStream);
46 | const fragmentStream = fragmentValue(fragmentInput);
47 | const output = flyd.map(fragmentStr => createCtx(fragmentStr), fragmentStream);
48 |
49 | return output;
50 | }
51 |
52 | export default fragment;
53 |
--------------------------------------------------------------------------------
/src/decorators/log.js:
--------------------------------------------------------------------------------
1 | import pick from '@f/pick';
2 |
3 | import { specDecorator } from './utils';
4 |
5 | const nameCache = new WeakMap();
6 |
7 | function componentName(component) {
8 | if(component.name) return component.name;
9 | const cacheEntry = nameCache.get(component);
10 |
11 | if(cacheEntry) return cacheEntry;
12 | const body = Object.keys(component)
13 | .map(key => [key, `${component[ key ]}`])
14 | .map(([key, value]) => ` ${key}: ${value.split('\n')[ 0 ]}...`)
15 | .join('\n');
16 | const name = `Component#{\n${body}\n}`;
17 |
18 | nameCache.set(component, name);
19 | return name;
20 | }
21 |
22 | const createHandler = name => (component, model) => {
23 | const handler = component[ name.slice(2).toLowerCase() ];
24 |
25 | return () => handler ? handler(model) : null;
26 | };
27 |
28 | const targetHandler = {
29 | render: function(component, model) {
30 | console.log('render', componentName(component));
31 | return component.render(model);
32 | },
33 | onUpdate: createHandler('onUpdate'),
34 | onCreate: createHandler('onCreate'),
35 | onRemove: createHandler('onRemove')
36 | };
37 |
38 | const defaultTargets = ['render', 'onUpdate', 'onCreate', 'onRemove'];
39 |
40 | /**
41 | * log component lifecycle
42 | *
43 | * @return {HOC}
44 | */
45 |
46 | export default function log(targets = defaultTargets) {
47 | const opts = {
48 | deep: false,
49 | name: 'log',
50 | cache: new WeakMap()
51 | };
52 | const spec = Object.assign({}, pick(targets, targetHandler), opts);
53 |
54 | return specDecorator(spec);
55 | }
56 |
--------------------------------------------------------------------------------
/src/decorators/styler.js:
--------------------------------------------------------------------------------
1 | import inflection from 'inflection';
2 | import { vnode as element } from 'deku';
3 |
4 | import { specDecorator } from './utils';
5 |
6 | const { isText, isEmpty } = element;
7 | const toDash = x => inflection.underscore(x).split('_').join('-');
8 |
9 | function decorateChildren(vnode) {
10 | if(isText(vnode) || isEmpty(vnode) || !vnode.children) return vnode;
11 | const children = vnode.children.map(child => decorateVnode(child));
12 |
13 | return Object.assign({}, vnode, { children });
14 | }
15 |
16 | function decorateVnode(vnode) {
17 | if(isText(vnode) || isEmpty(vnode)) return vnode;
18 |
19 | const decoratedNode = decorateChildren(vnode);
20 |
21 | if(!vnode.attributes || !vnode.attributes.style)
22 | return decoratedNode;
23 |
24 | const rule = Object.keys(vnode.attributes.style)
25 | .map(key => `${toDash(key)}:${vnode.attributes.style[ key ]};`)
26 | .join('');
27 |
28 | const attributes = Object.assign({}, decoratedNode.attributes, { style: rule });
29 |
30 | return Object.assign({}, decoratedNode, { attributes });
31 | }
32 |
33 | const spec = {
34 | deep: false,
35 | cache: null,
36 | name: 'styler',
37 | render: function(component, model) {
38 | const vnode = component.render(model);
39 |
40 | return decorateVnode(vnode);
41 | }
42 | };
43 |
44 | const decorator = specDecorator(spec);
45 |
46 | /**
47 | * add css styles as Object
48 | *
49 | * ```javascript
50 | * styler()(() => ())
51 | * ```
52 | *
53 | * @return {HOC}
54 | */
55 |
56 | export default function styler() {
57 | return decorator;
58 | }
59 |
--------------------------------------------------------------------------------
/gobblefile.js:
--------------------------------------------------------------------------------
1 | const gobble = require('gobble');
2 | const browserify = require('./tools/gobble-browserify');
3 | const mocha = require('./tools/gobble-mocha-shell');
4 | const esdoc = require('./tools/gobble-esdoc-shell');
5 | const eslint = require('./tools/gobble-eslint');
6 | const replace = require('./tools/gobble-replace');
7 |
8 | const pkg = gobble([
9 | gobble('LICENSE'),
10 | gobble('README.md'),
11 | gobble('package.json').transform(function(input) {
12 | const json = JSON.parse(input);
13 | delete json.scripts;
14 | delete json.devDependencies;
15 |
16 | return JSON.stringify(json, null, ' ');
17 | }),
18 | gobble('src')
19 | .observe(eslint)
20 | .transform('babel', {})
21 | .moveTo('lib')
22 | .observe(mocha, {
23 | files: 'lib/**/tests/*.js'
24 | })
25 | ]).moveTo('pkg');
26 |
27 | const starter = gobble('examples')
28 | .exclude(['node_modules', 'dist', 'src/vulp.js', '.DS_Store'])
29 | .transform(replace, {})
30 | .moveTo('examples')
31 | .transform('zip', {
32 | dest: 'examples.zip'
33 | });
34 |
35 | const doc = gobble([
36 | gobble('src').transform(esdoc, require('./esdoc.json')),
37 | ]).moveTo('docs');
38 |
39 | const examples = gobble([
40 | gobble([
41 | gobble('src').moveTo('src'),
42 | gobble('examples/src').moveTo('examples')
43 | ])
44 | .transform('babel', {})
45 | .transform(browserify, {
46 | entries: 'examples/index.js',
47 | dest: 'examples.js',
48 | standalone: 'examples'
49 | }),
50 |
51 | gobble('examples/src')
52 | .include(['**/*.html'])
53 | ]).moveTo('examples');
54 |
55 | module.exports = gobble([
56 | pkg,
57 | examples,
58 | doc,
59 | starter
60 | ]);
61 |
--------------------------------------------------------------------------------
/src/decorators/mount.js:
--------------------------------------------------------------------------------
1 | import { specDecorator } from './utils';
2 | import Transform from './../Transform';
3 | import JSONPointer from './../JSONPointer';
4 | import Immutable from 'immutable';
5 | import { get } from './../state';
6 | import Context from './../Context';
7 |
8 | const parsePath = patch => Object.assign({}, patch, { path: JSONPointer.ofString(patch.path) });
9 | const transformPath = transform => patch => Object.assign({}, patch, { path: transform.apply(patch.path) });
10 | const stringifyPath = patch => Object.assign({}, patch, { path: patch.path.toRFC() });
11 |
12 | const createDispatch = (transform, dispatch) => rawPatchSet => {
13 | const patchSet = rawPatchSet
14 | .map(parsePath)
15 | .map(transformPath(transform))
16 | .map(stringifyPath);
17 |
18 | if(patchSet.length === 0) return;
19 | dispatch(patchSet);
20 | };
21 |
22 | function applyTransform(transform, ctx) {
23 | const state = Immutable.Map(transform.map)
24 | .map(pointer => get(ctx.state, pointer));
25 |
26 | return new Context({ state });
27 | }
28 |
29 | /**
30 | * transform ctx of component
31 | *
32 | * ```javascript
33 | * mount({
34 | * foo: '/some/value'
35 | * bar: '/here/is/another/val'
36 | * })(component)
37 | * ```
38 | *
39 | * @param {Map
} targets - transform description
40 | * @return {HOC}
41 | */
42 | export default function(targets) {
43 | const transform = Transform.ofTargets(targets);
44 |
45 | const spec = {
46 | deep: true,
47 | cache: new WeakMap(),
48 | name: 'mount',
49 | model: function(component, model) {
50 | const context = applyTransform(transform, model.context);
51 | const dispatch = createDispatch(transform, model.dispatch);
52 |
53 | return Object.assign({}, model, { context, dispatch });
54 | }
55 | };
56 |
57 | return specDecorator(spec);
58 | }
59 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vulp",
3 | "version": "0.7.3",
4 | "main": "lib/",
5 | "author": "",
6 | "license": "MIT",
7 | "repository": {
8 | "type": "git",
9 | "url": "https://github.com/freemountain/vulp.git"
10 | },
11 | "keywords": [
12 | "vulp",
13 | "deku",
14 | "functional",
15 | "component"
16 | ],
17 | "scripts": {
18 | "build": "rm -rf ./dist && ./node_modules/.bin/gobble build dist",
19 | "dev": "./node_modules/.bin/gobble",
20 | "doc": "./node_modules/.bin/esdoc -c esdoc.json",
21 | "test": "./node_modules/.bin/_mocha --compilers js:babel-register src/tests",
22 | "lint": "./node_modules/.bin/eslint -c ./.eslintrc src/ examples/",
23 | "clean": "rm -rf dist && rm -rf .gobble*"
24 | },
25 | "devDependencies": {
26 | "babel-cli": "^6.5.1",
27 | "babel-eslint": "^5.0.0",
28 | "babel-plugin-transform-react-jsx": "^6.5.2",
29 | "babel-preset-es2015": "^6.3.13",
30 | "babel-preset-stage-2": "^6.3.13",
31 | "babel-register": "^6.4.3",
32 | "babelify": "^7.2.0",
33 | "browserify": "^13.0.0",
34 | "chai": "^3.4.1",
35 | "esdoc": "^0.4.3",
36 | "eslint": "2.2.0",
37 | "eslint-plugin-react": "^4.0.0",
38 | "gobble": "^0.10.2",
39 | "gobble-babel": "^6.0.0",
40 | "gobble-cli": "^0.6.0",
41 | "gobble-eslint": "^0.1.0",
42 | "gobble-replace": "^0.3.1",
43 | "gobble-zip": "0.0.2",
44 | "mocha": "^2.3.4",
45 | "rewire": "^2.5.1",
46 | "semver": "^5.1.0",
47 | "shelljs": "^0.6.0"
48 | },
49 | "dependencies": {
50 | "@f/map-obj": "^1.2.2",
51 | "@f/pick": "^1.1.2",
52 | "@f/zip-obj": "^1.1.1",
53 | "deku": "^2.0.0-rc16",
54 | "deku-memoize": "^1.2.0",
55 | "flyd": "^0.2.1",
56 | "immpatch": "^0.2.0",
57 | "immutable": "^3.7.6",
58 | "inflection": "^1.8.0",
59 | "tcomb": "^2.6.0"
60 | }
61 | }
--------------------------------------------------------------------------------
/src/scopes/combiner.js:
--------------------------------------------------------------------------------
1 | import Immutable from 'immutable';
2 | import t from 'tcomb';
3 | import flyd from 'flyd';
4 |
5 | import zipObj from '@f/zip-obj';
6 | import curry from './../utils/curry';
7 |
8 | import Context from './../Context';
9 | import JSONPointer from './../JSONPointer';
10 |
11 | const slicePath = patch => Object.assign({}, patch, { path: patch.path.slice(1) });
12 | const stringifyPath = patch => Object.assign({}, patch, { path: patch.path.toRFC() });
13 | const parsePath = patch => Object.assign({}, patch, { path: JSONPointer.ofString(patch.path) });
14 |
15 | const filterPatchSet = curry((name, patchSet) => patchSet
16 | .map(parsePath)
17 | .filter(patch => patch.path.first() === name)
18 | .map(slicePath)
19 | .map(stringifyPath));
20 |
21 | function readCtx(names, stores) {
22 | const values = stores.map(store => store());
23 | const stateMap = zipObj(names, values.map(ctx => ctx.state));
24 | const typeMap = zipObj(names, values.map(ctx => ctx.type));
25 |
26 | return new Context({
27 | state: Immutable.Map(stateMap),
28 | type: t.struct(typeMap)
29 | });
30 | }
31 |
32 | function createCombine(keys) {
33 | return function(...args) {
34 | return readCtx(keys, args.slice(0, 2));
35 | };
36 | }
37 |
38 |
39 | /**
40 | * combiner scope factory
41 | * represents map of (sub)scopes.
42 | * @param {Map} scopeMap - the routing map
43 | * @param {Stream} input - input stream
44 | * @return {Scope}
45 | */
46 |
47 | export default function combiner(scopeMap, input) {
48 | const names = Object.keys(scopeMap);
49 | const factories = names.map(name => scopeMap[ name ]);
50 | const inputs = names.map(name => flyd.map(function(rawPatchSet) {
51 | const patchSet = filterPatchSet(name, rawPatchSet);
52 |
53 | return patchSet;
54 | }, input));
55 | const stores = factories.map((f, i) => f(inputs[ i ]));
56 | const combineStreams = createCombine(names);
57 |
58 | return flyd.combine(combineStreams, stores);
59 | }
60 |
--------------------------------------------------------------------------------
/src/Context.js:
--------------------------------------------------------------------------------
1 | import t from 'tcomb';
2 | import applyPatch from 'immpatch';
3 |
4 | import JSONPointer from './JSONPointer';
5 | import { get, unbox, box } from './state';
6 |
7 | const emptyInstance = {
8 | type: t.Any,
9 | state: box({})
10 | };
11 |
12 | /**
13 | * Context is passed to all components.
14 | */
15 | class Context {
16 |
17 | /**
18 | * Context constructors
19 | *
20 | * @param {Object} instance - instances sdds
21 | * @param {Object} instance.state - states sd
22 | * @param {Transform} instance.transform - transform sdds
23 | */
24 | constructor(instance = {}) {
25 | Object.assign(this, emptyInstance, instance);
26 |
27 | Object.freeze(this);
28 | }
29 |
30 |
31 | /**
32 | * create Context of state, transform is identity
33 | * @param {object} state - state of ctx
34 | * @return {Context}
35 | */
36 |
37 | static ofState(state = {}) {
38 | return new Context({
39 | state: box(state)
40 | });
41 | }
42 |
43 | /**
44 | * get value from state
45 | * @param {string|JSONPointer} path = '' - pointer to value
46 | * @param {boolean} toJS = false - unbox value?
47 | * @return {any} value
48 | */
49 | get(path = '', toJS = false) {
50 | if(t.Boolean.is(path)) return this.get('', path);
51 |
52 | const result = get(this.state, JSONPointer.ofString(path));
53 |
54 | return toJS ? unbox(result) : result;
55 | }
56 |
57 | /**
58 | * check if ctx has changed
59 | * @param {Context} last - the last COntext
60 | * @return {boolean}
61 | */
62 | changed(last) {
63 | const keys = last.state.keySeq().toJS();
64 |
65 | return keys.some(key => this.get(key) !== last.get(key));
66 | }
67 |
68 | update(patchSet) {
69 | if(patchSet.length === 0) return this;
70 |
71 | const newState = applyPatch(this.state, patchSet);
72 | // assert type
73 |
74 | const rawState = newState.toJS();
75 |
76 | this.type(rawState);
77 |
78 | return new Context({
79 | type: this.type,
80 | // transform: this.transform,
81 | state: newState
82 | });
83 | }
84 |
85 |
86 | }
87 |
88 | export default Context;
89 |
--------------------------------------------------------------------------------
/src/decorators/controller.js:
--------------------------------------------------------------------------------
1 | import t from 'tcomb';
2 |
3 | import { specDecorator, mapAttributes, isHandler } from './utils';
4 |
5 |
6 | function createModel(model, controller) {
7 | function dispatch(name, payload) {
8 | if(!t.String.is(name)) return model.dispatch(name, payload);
9 | const target = controller[ name ];
10 |
11 | if(t.Nil.is(target)) throw new TypeError(`controller[${name}] is ${target}`);
12 | if(!t.Function.is(target)) return model.dispatch(target);
13 | const targetModel = Object.assign({}, model, { event: payload });
14 | const output = target(targetModel);
15 |
16 | return model.dispatch(output);
17 | }
18 |
19 | return Object.assign({}, model, { dispatch });
20 | }
21 |
22 | function createRender(component, model) {
23 | const output = component.render(model);
24 |
25 | return mapAttributes(output, function(prop, name) {
26 | if(!t.String.is(prop) || !isHandler(name)) return prop;
27 | return event => model.dispatch(prop, event);
28 | }, true);
29 | }
30 |
31 | /**
32 | * controller decorator.
33 | *
34 | * ```javascript
35 | * controller({
36 | * click: (model) => { ... }
37 | * })(({ dispatch }) => (
38 | * dispatch('click', event)} />
39 | * // or
40 | *
41 | * ))
42 | * ```
43 | *
44 | * You can dispatch actions to handlers in your controller when you call
45 | * model.dispatch with the property name as first argument and an optional event as second argument.
46 | *
47 | * If the property is a function, then the function will be called with model as the only argument.
48 | * The model object has an additional property 'event', which contains the second argument from dispatch.
49 | * Otherwise the property will just be dispatched.
50 | *
51 | * @param {Map} controller - action map
52 | * @return {HOC}
53 | */
54 | export default function(controller) {
55 | const spec = {
56 | deep: false,
57 | cache: new WeakMap(),
58 | model: (component, model) => createModel(model, controller),
59 | render: (component, model) => createRender(component, model)
60 | };
61 |
62 | return specDecorator(spec);
63 | }
64 |
--------------------------------------------------------------------------------
/src/tests/Transform.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 |
3 | import Transform from './../Transform';
4 | import JSONPointer from './../JSONPointer';
5 |
6 | // const createCheck = (T, value) => () => T(value);
7 |
8 | describe('Transform', function() {
9 | describe('#create', function() {
10 | it('creates Transform', function() {
11 | const transform = Transform.ofTargets({ foo: '/bar' });
12 |
13 | expect(transform instanceof Transform).to.equal(true);
14 | expect(transform.isIdentity()).to.equal(false);
15 | });
16 |
17 | it('empty call creates identity', function() {
18 | const transform = Transform.ofTargets();
19 |
20 | expect(transform instanceof Transform).to.equal(true);
21 | expect(transform.isIdentity()).to.equal(true);
22 | });
23 | });
24 |
25 | describe('#sub', function() {
26 | it('sub returns Transform', function() {
27 | const rootTransform = Transform.ofTargets({
28 | a: '/foo/aaa',
29 | b: '/bar/bbb'
30 | });
31 |
32 | const subTransform = Transform.ofTargets({
33 | foo: '/a',
34 | bar: '/b/someVal'
35 | });
36 |
37 | const result = rootTransform.sub(subTransform);
38 | const targets = result.toTargets();
39 |
40 | expect(result instanceof Transform).to.equal(true);
41 | expect(targets.foo).to.equal('/foo/aaa');
42 | expect(targets.bar).to.equal('/bar/bbb/someVal');
43 | });
44 | });
45 |
46 | describe('#equals', function() {
47 | it('s', function() {
48 | const map = {
49 | foo: '/bar'
50 | };
51 | const a = Transform.ofTargets(map);
52 | const b = Transform.ofTargets(map);
53 | const c = Transform.ofTargets({
54 | foo: '/someotherprop'
55 | });
56 |
57 | expect(a.equals(b)).to.equal(true);
58 | expect(a.equals(c)).to.equal(false);
59 | });
60 | });
61 |
62 | describe('#apply', function() {
63 | it('applies transform on pointer', function() {
64 | const transform = Transform.ofTargets({
65 | foo: '/aaa/bbb',
66 | bar: '/ccc'
67 | });
68 |
69 | const fooPointer = JSONPointer.ofString('/foo/xxx');
70 | const barPointer = JSONPointer.ofString('/bar/yyy');
71 |
72 | const fooTransformed = transform.apply(fooPointer).toRFC();
73 | const barTransformed = transform.apply(barPointer).toRFC();
74 |
75 | expect(fooTransformed).to.equal('/aaa/bbb/xxx');
76 | expect(barTransformed).to.equal('/ccc/yyy');
77 | });
78 | });
79 | });
80 |
--------------------------------------------------------------------------------
/manual/asset/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
33 |
--------------------------------------------------------------------------------
/src/decorators/dispatchChangeSets.js:
--------------------------------------------------------------------------------
1 | import t from 'tcomb';
2 |
3 | import patchUtil from './../utils/patch';
4 |
5 | import { specDecorator } from './utils';
6 |
7 | const ChangeSet = t.irreducible('ChangeSet', function(x) {
8 | if(!t.Array.is(x) || x.length % 2 !== 0) return false;
9 |
10 | const pairs = x.reduce(function(current, e, i) {
11 | if(i % 2 === 0) current.push([]);
12 | current[ current.length - 1 ].push(e);
13 |
14 | return current;
15 | }, []);
16 |
17 | return t.list(t.tuple([t.String, t.Any])).is(pairs);
18 | });
19 |
20 | function toPairs(changeSet) {
21 | return changeSet.reduce(function(current, e, i) {
22 | if(i % 2 === 0) current.push([]);
23 | current[ current.length - 1 ].push(e);
24 |
25 | return current;
26 | }, []);
27 | }
28 | const dispatch = model => payload => {
29 | model.dispatch(t.match(payload,
30 | ChangeSet, changeSet => patchUtil(model.context, toPairs(changeSet)),
31 | t.Any, x => x
32 | ));
33 | };
34 |
35 | const spec = {
36 | deep: false,
37 | cache: null,
38 | name: 'dispatchChangeSets',
39 | model: (component, model) => {
40 | const decorated = Object.assign({}, model, {
41 | dispatch: pay => {
42 | dispatch(model)(pay);
43 | }
44 | });
45 |
46 | return decorated;
47 | }
48 | };
49 |
50 | const decorator = specDecorator(spec);
51 |
52 | /**
53 | * dispatch change sets
54 | *
55 | * Usage:
56 | * ```javascript
57 | * dispatchChangeSets()(function({ dispatch }) {
58 | * // some code...
59 | * dispatch(['/count', v => v + 1])
60 | * })
61 | * ```
62 | *
63 | * A change set is an array of strings on even positions and some values on odd positions.
64 | * The strings acts as path selector on the context object and the next element as value for the previous path.
65 | * If the value element is a function, this function will be called with the value in the given path as argument.
66 | * If the path string ends with '-' the operator in the resulting patch will be 'add' ([info](http://jsonpatch.com/#json-pointer)).
67 | *
68 | * Example:
69 | * ```javascript
70 | * const changeSet = [
71 | * '/foo/bar' : 42,
72 | * '/someNumber' : n => n + 3,
73 | * '/someList/-' : {name: 'baz'}
74 | * ];
75 | * ```
76 | * The function will be called with `n = model.context.get('/someNumber')`.
77 | * The resulting patch set with `n = 4`:
78 | * ```javascript
79 | * const patchSet = [
80 | * {
81 | * op: 'replace',
82 | * path: '/foo/bar',
83 | * value: 42
84 | * },
85 | * {
86 | * op: 'replace',
87 | * path: '/someNumber',
88 | * value: 7
89 | * },
90 | * {
91 | * op: 'add',
92 | * path: '/someList/-',
93 | * value: { name: 'baz' }
94 | * }
95 | * ];
96 | * ```
97 | *
98 | * @return {HOC}
99 | */
100 | export default function dispatchChangeSets() {
101 | return decorator;
102 | }
103 |
--------------------------------------------------------------------------------
/src/decorators/utils.js:
--------------------------------------------------------------------------------
1 | import pick from '@f/pick';
2 | import mapObj from '@f/map-obj';
3 | import t from 'tcomb';
4 | import { vnode as element } from 'deku';
5 |
6 | const { isThunk, isText, isEmpty } = element;
7 |
8 |
9 | export function normalize(comp) {
10 | const component = t.Function.is(comp) ? { render: comp } : comp;
11 |
12 | if(!t.Object.is(component) && !t.Function.is(component.render))
13 | throw new TypeError('component must be function or object with render function');
14 |
15 | if(!component.name && component.render.name !== '') component.name = component.render.name;
16 | return component;
17 | }
18 |
19 | export function mapAttributes(node, f, deep = false) {
20 | if(isText(node) || isEmpty(node)) return node;
21 | const name = isThunk(node) ? 'props' : 'attributes';
22 | const spec = {};
23 |
24 | spec[ name ] = mapObj(f, node[ name ]);
25 | if(deep) spec.children = node.children.map(child => mapAttributes(child, f, deep));
26 |
27 | return Object.assign({}, node, spec);
28 | }
29 |
30 | export const isHandler = name => (
31 | name.length > 2 &&
32 | name.slice(0, 2) === 'on' &&
33 | name[ 2 ] === name[ 2 ].toUpperCase()
34 | );
35 |
36 | const defaultOpts = {
37 | deep: false,
38 | cache: null,
39 | model: (component, model) => model,
40 | render: (component, model) => component.render(model),
41 | onCreate: (component, model) => component.onCreate ? component.onCreate(model) : null,
42 | onUpdate: (component, model) => component.onUpdate ? component.onUpdate(model) : null,
43 | onRemove: (component, model) => component.onRemove ? component.onRemove(model) : null
44 | };
45 |
46 | function applySpec(spec, rawComponent) {
47 | const cacheEntry = spec.cache ? spec.cache.get(rawComponent) : null;
48 |
49 | if(cacheEntry) return cacheEntry;
50 | const component = normalize(rawComponent);
51 | const hooks = pick(['onCreate', 'onUpdate', 'onRemove'], spec);
52 | const componentSpec = mapObj(hook => model => hook(component, spec.model(component, model)), hooks);
53 |
54 | componentSpec.render = function(model) {
55 | const decoratedModel = spec.model(component, model);
56 | const node = spec.render(component, decoratedModel);
57 |
58 | return spec.deep ? decorateNode(spec, node) : node;
59 | };
60 |
61 | const decorated = Object.assign({}, component, componentSpec);
62 |
63 | if(spec.cache) spec.cache.set(rawComponent, decorated);
64 | return decorated;
65 | }
66 |
67 | function decorateNode(spec, node) {
68 | const nodeSpec = {};
69 |
70 | if(isThunk(node)) nodeSpec.component = applySpec(spec, node.component);
71 | if(!t.Nil.is(node.children)) nodeSpec.children = node.children.map(child => decorateNode(spec, child));
72 |
73 | return Object.assign({}, node, nodeSpec);
74 | }
75 |
76 | export function specDecorator(options) {
77 | const cache = options.deep && !options.cache ? new WeakMap() : options.cache;
78 | const spec = Object.assign({}, defaultOpts, options, { cache });
79 |
80 | return function(component) {
81 | const decorated = applySpec(spec, component);
82 |
83 | return decorated;
84 | };
85 | }
86 |
--------------------------------------------------------------------------------
/src/Transform.js:
--------------------------------------------------------------------------------
1 | import t from 'tcomb';
2 | import mapObj from '@f/map-obj';
3 |
4 | import JSONPointer from './JSONPointer';
5 |
6 | function substitutePointer(pointer, targets) {
7 | const root = pointer.first();
8 |
9 | if(t.Nil.is(targets[ root ]))
10 | throw new Error(`No target found for key ${root}. Possible targets: ${targets.toString()}`);
11 |
12 | const tokens = targets[ root ].tokens.concat(pointer.tokens.slice(1));
13 |
14 | return JSONPointer.ofTokens(tokens);
15 | }
16 |
17 | /**
18 | * Map holds transform informations.
19 | *
20 | * @typedef {Map} Map
21 | */
22 |
23 | const Map = t.dict(t.String, JSONPointer);
24 |
25 | const parseTargets = desc => mapObj(JSONPointer.ofString, desc);
26 |
27 |
28 | /**
29 | * Transform tracks changes to state structure,
30 | * is used for mounting transforms.
31 | */
32 |
33 | class Transform {
34 |
35 | /**
36 | * Transform constructors
37 | *
38 | * @param {Map} map - instances sdds
39 | */
40 |
41 | constructor(map = Map({})) {
42 | this.length = Object.keys(map).length;
43 | this.map = map;
44 |
45 | Object.freeze(this.length);
46 | Object.freeze(this.map);
47 | Object.freeze(this);
48 | }
49 |
50 | /**
51 | * parse target keys to json pointer and return transform
52 | * @param {Object} targets
53 | * @returns {Transform}
54 | */
55 |
56 | static ofTargets(targets = {}) {
57 | const map = parseTargets(targets);
58 |
59 | return new Transform(map);
60 | }
61 |
62 | /**
63 | * resolve keys from child transform with targets from object
64 | * @param {Transform} child - the child
65 | * @returns {Transform}
66 | */
67 |
68 | sub(child) {
69 | if(this.isIdentity()) return child;
70 | if(child.isIdentity()) return this;
71 |
72 | const childMap = mapObj(target => substitutePointer(target, this.map), child.map);
73 |
74 | return new Transform(childMap);
75 | }
76 |
77 | /**
78 | * apply transform on JSONPointer
79 | * @param {JSONPointer} pointer - pointer
80 | * @returns {Transform}
81 | */
82 |
83 | apply(pointer) {
84 | const target = pointer.first();
85 |
86 | if(t.Nil.is(this.map[ target ])) return pointer;
87 |
88 | const tail = pointer.slice(1);
89 |
90 | return this.map[ target ].concat(tail);
91 | }
92 |
93 | /**
94 | * returns targets (target pointer as rfc string)
95 | * @returns {Object}
96 | */
97 | toTargets() {
98 | return mapObj(pointer => pointer.toRFC(), this.map);
99 | }
100 |
101 | /**
102 | * check if transforms are equal
103 | * @param {Transform} x - transfrom to check against
104 | * @returns {boolean}
105 | */
106 | equals(x) {
107 | if(this.length !== x.length) return false;
108 |
109 | return Object
110 | .keys(this.map)
111 | .every(target => x.map[ target ] && this.map[ target ].toRFC() === x.map[ target ].toRFC());
112 | }
113 |
114 | /**
115 | * check if transform is identify
116 | * @returns {boolean}
117 | */
118 | isIdentity() {
119 | return this.length === 0;
120 | }
121 |
122 |
123 | }
124 |
125 | export default Transform;
126 |
--------------------------------------------------------------------------------
/tools/release.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | require('shelljs/global');
4 |
5 | const semver = require('semver');
6 | const fs = require('fs');
7 | const os = require('os');
8 | const path = require('path');
9 |
10 | function getPkgJson(x) {
11 | const target = x ? x : './package.json';
12 | const data = cat(target);
13 |
14 | return JSON.parse(data);
15 | }
16 |
17 | function getEnv() {
18 | const result = {};
19 | result.gh_token = process.env.GITHUB_TOKEN;
20 | result.repo = 'vulp';
21 | result.owner = 'freemountain';
22 |
23 | return result;
24 | }
25 |
26 | function run(command, opts) {
27 | opts = opts || {};
28 | const silent = opts.silent || false;
29 | const ignoreCode = opts.ignoreCode || false;
30 | if(!silent) console.log('$ ' + command + '...\n');
31 |
32 | const result = exec(command);
33 |
34 | if(ignoreCode !== true && result.code !== 0) {
35 | if(!silent)
36 | console.log('\nCommand: ' + command +' returned ' + result.code + '\nExit...');
37 | process.exit(result.code);
38 | }
39 |
40 | return result;
41 | }
42 |
43 | function assertCleanWorkingDir() {
44 | const output = run('git status --porcelain').stdout;
45 |
46 | if(output.length === 0) return;
47 |
48 | console.log('\nworking dir is not clean\nExit...');
49 | process.exit(-1);
50 | }
51 |
52 | function asserMasterBranch() {
53 | const output = run('git rev-parse --abbrev-ref HEAD').stdout.trim();
54 |
55 | if(output === 'master') return;
56 |
57 | console.log('\ncurrent Branch should be master.\nExit...');
58 | process.exit(-1);
59 | }
60 |
61 | function bumbVersionAndTag(delta) {
62 | const pkgJson = getPkgJson();
63 | const currentVersion = pkgJson.version;
64 | const newVersion = semver.inc(currentVersion, delta);
65 | pkgJson.version = newVersion;
66 |
67 | console.log('Bump version from', currentVersion, 'to', newVersion);
68 | JSON.stringify(pkgJson, null, ' ').to('./package.json');
69 |
70 | run('git add package.json');
71 |
72 | return newVersion;
73 | }
74 |
75 | function bumpExamples(vulpVersion) {
76 | const pkgJson = getPkgJson('./examples/package.json');
77 | pkgJson.dependencies.vulp = vulpVersion;
78 | JSON.stringify(pkgJson, null, ' ').to('./examples/package.json');
79 |
80 | run('git add examples/package.json');
81 | }
82 |
83 | function uploadDocs() {
84 | const cwd = process.cwd();
85 | const tmpDir = path.join('/tmp', 'docs-' + Date.now());
86 |
87 | mkdir(tmpDir);
88 | process.chdir(tmpDir);
89 | run('git init');
90 | run('git remote add origin https://github.com/freemountain/vulp.git');
91 | run('git checkout --track -b origin/gh-pages');
92 | run('git pull origin gh-pages');
93 | run('git rm -rf .');
94 | run('cp -R ' + path.join(cwd, 'dist', 'docs/*') + ' ' + tmpDir);
95 | run('git add --all .');
96 | run('git commit -m "updating docs.."');
97 | run('git push origin HEAD:gh-pages');
98 | process.chdir(cwd);
99 | rm('-rf', tmpDir);
100 | }
101 |
102 | const delta = process.argv[ 2 ];
103 |
104 | if(['patch', 'minor', 'major'].indexOf(delta) === -1)
105 | throw new Error('illegal delta');
106 |
107 | assertCleanWorkingDir();
108 | asserMasterBranch();
109 | const version = bumbVersionAndTag(delta);
110 | bumpExamples(version);
111 |
112 | run('git commit -m "Bump version to ' + version + '"');
113 | run('git push');
114 | run('git tag -a "v' + version + '" -m "Release ' + version + '"');
115 | run('git push --tags');
116 | run('git pull');
117 | run('npm run build');
118 | run('npm publish dist/pkg');
119 |
120 | uploadDocs();
121 |
--------------------------------------------------------------------------------
/src/JSONPointer/index.js:
--------------------------------------------------------------------------------
1 | import t from 'tcomb';
2 |
3 | /**
4 | * An array of Strings
5 | * TokenList holds JSONPath tokens.
6 | * @typedef {Array} TokenList
7 | */
8 | const TokenList = t.list(t.String);
9 |
10 | /**
11 | * JSON Pointer defines a string format for identifying a specific value within a JSON document.
12 | * This class holds methods for manipulation of pointers.
13 | *
14 | * For more information:
15 | * - http://tools.ietf.org/html/rfc6901
16 | * - http://jsonpatch.com/#json-pointer
17 | */
18 | class JSONPointer {
19 |
20 | /**
21 | * JSONPointer constructors
22 | *
23 | * @param {Object} opts - options.
24 | * @param {TokenList} opts.tokens - json path tokens
25 | */
26 | constructor(opts = {}) {
27 | const tokens = TokenList(opts.tokens || []);
28 |
29 | this.tokens = tokens;
30 |
31 | Object.freeze(this.tokens);
32 | Object.freeze(this);
33 | }
34 |
35 | static ofTokens(tokens) {
36 | return new JSONPointer({ tokens });
37 | }
38 |
39 | /**
40 | * parse str and return JSONPointer
41 | *
42 | * If x starts with '/' create will treat x as a JSONPointer.
43 | * All other string will be treated like immutable js pointer (eg.: 'foo.bar')
44 | *
45 | * @param {string} str - pointer string
46 | * @returns {JSONPointer}
47 | */
48 | static ofString(str) {
49 | if(!t.String.is(str))
50 | throw new Error('JSONPointer::ofString - expected string');
51 |
52 | if(str === '') return new JSONPointer({
53 | tokens: []
54 | });
55 |
56 | // pointer to key "" (rfc)
57 | if(str === '/') return new JSONPointer({
58 | tokens: ['']
59 | });
60 |
61 | if(str.startsWith('/')) return new JSONPointer({
62 | tokens: str.split('/').slice(1)
63 | });
64 |
65 | return new JSONPointer({
66 | tokens: str.split('.')
67 | });
68 | }
69 |
70 | /**
71 | * get rfc string representation
72 | *
73 | * @returns {string}
74 | */
75 | toRFC() {
76 | return '/'.concat(this.tokens.join('/'));
77 | }
78 |
79 | /**
80 | * get immutable js string representation
81 | *
82 | * @returns {string}
83 | */
84 | toImmutable() {
85 | return this.tokens.join('.');
86 | }
87 |
88 | /**
89 | * get first token
90 | *
91 | * Return value is the first entry from token list.
92 | * @returns {string}
93 | */
94 | first() {
95 | return this.tokens[ 0 ] || null;
96 | }
97 |
98 | /**
99 | * get last token
100 | *
101 | * Return value is the last entry from token list.
102 | * @returns {string}
103 | */
104 | last() {
105 | return this.tokens[ this.tokens.length - 1 ] || null;
106 | }
107 |
108 | /**
109 | * return length of token array
110 | * @returns {number}
111 | */
112 | size() {
113 | return this.tokens.length;
114 | }
115 |
116 | /**
117 | * concat two JSONPointer
118 | *
119 | * @param {JSONPointer} sub - the pointer to concat
120 | * @returns {JSONPointer}
121 | */
122 | concat(sub) {
123 | if(!sub instanceof JSONPointer)
124 | throw new Error('JSONPointer#concat argument should be JSONPointer');
125 |
126 | return new JSONPointer({
127 | tokens: this.tokens.concat(sub.tokens)
128 | });
129 | }
130 |
131 | /**
132 | * slice JSONPointer
133 | *
134 | * slice has the same signature like Array.prototype.slice.
135 | *
136 | * @returns {JSONPointer}
137 | */
138 | slice() {
139 | return new JSONPointer({
140 | tokens: Array.prototype.slice.apply(this.tokens, arguments)
141 | });
142 | }
143 | }
144 |
145 | export default JSONPointer;
146 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | # vulp
4 | vulp is a user interface library with uni-directional dataflow.
5 |
6 | ## Install
7 |
8 | ### NPM
9 | ```shell
10 | npm install --save-dev vulp
11 | ```
12 |
13 | ### Starter Project
14 | - download example.zip from [Github](https://github.com/freemountain/vulp/releases)
15 | - extract and run `npm install && npm run dev`
16 | - this starts a minimal build process with preconfigured babel and browserify
17 | - point your browser to [localhost:8080](http://localhost:8080/dist) to view the examples
18 | - or use this as template for your own project
19 |
20 | ## Usage
21 | ```javascript
22 | import { element, cycle, views, scopes, decorators } from 'vulp';
23 |
24 | const { component, controller, dispatchChangeSets } = decorators;
25 | const App = component(
26 | dispatchChangeSets(),
27 | controller({
28 | inc: ['/count', count => count + 1]
29 | }),
30 | )(function({ context }) {
31 | return (
32 |
33 |
34 | {context.get('/count')}
35 |
36 | );
37 | });
38 |
39 | const view = views.dom(document.body, App);
40 | const scope = scopes.value({ count: 0 });
41 |
42 | cycle(view, scope);
43 | ```
44 |
45 | ## Architecture
46 | 
47 |
48 | - [Context](http://freemountain.github.io/vulp/class/src/Context.js~Context.html)
49 | - immutable data structure (struct)
50 | - holds application context
51 | - emitted from scopes, when they have new data
52 | - consumed from views
53 | - PatchSet
54 | - list of [json patch](http://jsonpatch.com/) objects
55 | - used like actions in flux architecture
56 | - used to manipulate Context
57 | - emitted from views, when they change state
58 | - consumed from scopes
59 | - Scope
60 | - state container
61 | - representation of data that may change over time (stream)
62 | - View
63 | - passes context to [deku component](http://dekujs.github.io/deku/)
64 | - render component to DOM
65 | - component may dispatch PatchSet on user interaction
66 | - [Component](http://freemountain.github.io/vulp/typedef/index.html#static-typedef-Component)
67 | - stateless
68 | - dispatches side effects to scopes
69 | - additional functionality added through decorators
70 |
71 | ## Api
72 | ### vulp
73 | - [cycle](http://freemountain.github.io/vulp/function/index.html#static-function-cycle)
74 | - [element](http://freemountain.github.io/vulp/typedef/index.html#static-typedef-element)
75 |
76 | #### vulp.scopes
77 | - [combiner](http://freemountain.github.io/vulp/function/index.html#static-function-combiner)
78 | - [fragment](http://freemountain.github.io/vulp/function/index.html#static-function-fragment)
79 | - [value](http://freemountain.github.io/vulp/function/index.html#static-function-value)
80 |
81 | #### vulp.views
82 | - [dom](http://freemountain.github.io/vulp/function/index.html#static-function-dom)
83 |
84 | #### vulp.decorators
85 | - [checkContextType](http://freemountain.github.io/vulp/function/index.html#static-function-checkContextType)
86 | - [component](http://freemountain.github.io/vulp/function/index.html#static-function-component)
87 | - [controller](http://freemountain.github.io/vulp/function/index.html#static-function-controller)
88 | - [dispatchChangeSets](http://freemountain.github.io/vulp/function/index.html#static-function-dispatchChangeSets)
89 | - [log](http://freemountain.github.io/vulp/function/index.html#static-function-log)
90 | - [memoize](http://freemountain.github.io/vulp/function/index.html#static-function-memoize)
91 | - [mount](http://freemountain.github.io/vulp/function/index.html#static-function-mount)
92 | - [name](http://freemountain.github.io/vulp/function/index.html#static-function-name)
93 | - [styler](http://freemountain.github.io/vulp/function/index.html#static-function-styler)
94 |
95 | ## Hack
96 | ```shell
97 | git clone https://github.com/freemountain/vulp
98 | cd vulp
99 | npm install
100 | npm run dev
101 | ```
102 | ... and click [here](http://localhost:4567/)
103 |
104 | ## License
105 | The MIT License (MIT)
106 |
--------------------------------------------------------------------------------
/tools/gobble-browserify.js:
--------------------------------------------------------------------------------
1 | var _browserify = require('browserify');
2 | var path = require('path');
3 | var fs = require( 'fs' );
4 |
5 | var SOURCEMAPPING_URL = 'sourceMa';
6 | SOURCEMAPPING_URL += 'ppingURL';
7 | var SOURCEMAP_COMMENT = new RegExp( '\\/\\/[#@]\\s+' + SOURCEMAPPING_URL + '=([^\\s\'"]+)\s*$', 'gm' );
8 |
9 | function ensureArray ( thing ) {
10 | if ( thing == null ) {
11 | return [];
12 | }
13 |
14 | if ( !Array.isArray( thing ) ) {
15 | return [ thing ];
16 | }
17 |
18 | return thing;
19 | }
20 |
21 | function concat ( stream, callback ) {
22 | var body = '';
23 |
24 | stream.on( 'data', function ( chunk ) {
25 | body += chunk.toString();
26 | });
27 |
28 | stream.on( 'end', function () {
29 | callback( body );
30 | });
31 | }
32 |
33 | function cacheDependency ( cache, originalDep, inputdir ) {
34 | var dep = {};
35 | Object.keys( originalDep ).forEach( function ( key ) {
36 | dep[ key ] = originalDep[ key ];
37 | });
38 |
39 | dep.basedir && ( dep.basedir = dep.basedir.replace( inputdir, '@' ) );
40 | dep.id = dep.id.replace( inputdir, '@' );
41 | dep.file = dep.file.replace( inputdir, '@' );
42 |
43 | if ( dep.deps ) {
44 | Object.keys( dep.deps ).forEach( function ( key ) {
45 | dep.deps[ key ] = dep.deps[ key ].replace( inputdir, '@' );
46 | });
47 | }
48 |
49 | cache[ dep.id ] = dep;
50 | }
51 |
52 | var leadingAt = /^@/;
53 |
54 |
55 | module.exports = function browserify ( inputdir, outputdir, options, callback ) {
56 | if ( !options.dest ) {
57 | throw new Error( 'You must specify a `dest` property' );
58 | }
59 |
60 | if ( !options.entries ) {
61 | throw new Error( 'You must specify one or more entry points as `options.entries`' );
62 | }
63 |
64 | // TODO should have a proper, documented way of doing this... e.g. `this.state`.
65 | // Ditto for this.node.cache
66 | if ( !this.node.packageCache ) {
67 | this.node.packageCache = {};
68 | }
69 |
70 | options.basedir = inputdir;
71 | var debug = options.debug = options.debug !== false; // sourcemaps by default
72 | options.cache = {};
73 | options.packageCache = {};
74 |
75 | var b = _browserify( options );
76 | var cache = options.cache;
77 |
78 |
79 |
80 | // TODO watch dependencies outside inputdir, using a future
81 | // gobble API - https://github.com/gobblejs/gobble/issues/26
82 |
83 | // make it possible to expose particular files, without the nutty API.
84 | // Example:
85 | // gobble( 'browserify', {
86 | // entries: [ './app' ],
87 | // dest: 'app.js',
88 | // standalone: 'app',
89 | // expose: { ractive: 'ractive/ractive-legacy.js' } // <-- use ractive-legacy instead of modern build
90 | // })
91 | if ( options.expose ) {
92 | Object.keys( options.expose ).forEach( function ( moduleName ) {
93 | b.require( options.expose[ moduleName ], { expose: moduleName });
94 | });
95 | }
96 |
97 | // allow ignore and exclude to be passed as arrays/strings, rather
98 | // than having to use options.configure
99 | [ 'ignore', 'exclude' ].forEach( function ( method ) {
100 | ensureArray( options[ method ] ).forEach( function ( option ) {
101 | b[ method ]( option );
102 | });
103 | });
104 |
105 | if ( options.configure ) {
106 | options.configure( b );
107 | }
108 |
109 | b.bundle( function ( err, bundle ) {
110 | if ( err ) return callback( err );
111 |
112 | bundle = bundle.toString();
113 |
114 | var dest = path.join( outputdir, options.dest );
115 | var lastSourceMappingURL;
116 |
117 | // browserify leaves sourceMappingURL comments in the files it bundles. This is
118 | // incorrect, as browsers (and other sourcemap tools) will assume that the URL
119 | // is for the bundle's own map, whether or not there is one. So we remove them,
120 | // and store the value of the last one in case we need to process it
121 | bundle = bundle.replace( SOURCEMAP_COMMENT, function ( match, url, a ) {
122 | lastSourceMappingURL = url;
123 | return '';
124 | });
125 |
126 | if ( debug && lastSourceMappingURL ) {
127 | var base64Match = /base64,(.+)/.exec( lastSourceMappingURL );
128 |
129 | if ( !base64Match ) {
130 | callback( new Error( 'Expected to find a base64-encoded sourcemap data URL' ) );
131 | }
132 |
133 | var json = new Buffer( base64Match[1], 'base64' ).toString();
134 | var map = JSON.parse( json );
135 |
136 | // Override sources - make them absolute
137 | map.sources = map.sources.map( function ( relativeToInputdir ) {
138 | return path.resolve( inputdir, relativeToInputdir );
139 | });
140 |
141 | json = JSON.stringify( map );
142 |
143 | var mapFile = dest + '.map';
144 |
145 | // we write the sourcemap out as a separate .map file. Keeping it as an
146 | // inline data URL is silly
147 | bundle += '\n//# sourceMappingURL=' + path.basename( mapFile );
148 |
149 | fs.writeFile( dest, bundle, function () {
150 | fs.writeFile( mapFile, JSON.stringify( map ), callback );
151 | });
152 | } else {
153 | fs.writeFile( dest, bundle, callback );
154 | }
155 | });
156 | };
157 |
--------------------------------------------------------------------------------
/manual/asset/architecture.svg:
--------------------------------------------------------------------------------
1 |
103 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": [
3 | "react"
4 | ],
5 | "settings": {
6 | "react": {
7 | "pragma": "h"
8 | }
9 | },
10 | "parser": "babel-eslint",
11 | "parserOptions": {
12 | "sourceType": "module",
13 | "ecmaFeatures": {
14 | "blockBindings": true,
15 | "forOf": true,
16 | "jsx": true,
17 | "arrowFunctions": true,
18 | "spread": true,
19 | "modules": true,
20 | "asyncFunctions": true,
21 | "functionBind": true
22 | }
23 | },
24 | "env": {
25 | "browser": true,
26 | "node": true,
27 | "es6": true,
28 | "mocha": true
29 | },
30 | "rules": {
31 | "strict": [
32 | 2
33 | ],
34 | "curly": [
35 | 2,
36 | "multi"
37 | ],
38 | "default-case": [
39 | 2
40 | ],
41 | "comma-dangle": [
42 | 2
43 | ],
44 | "no-cond-assign": [
45 | 2
46 | ],
47 | "no-constant-condition": [
48 | 2
49 | ],
50 | "no-empty-character-class": [
51 | 2
52 | ],
53 | "no-empty": [
54 | 2
55 | ],
56 | "no-ex-assign": [
57 | 2
58 | ],
59 | "no-extra-boolean-cast": [
60 | 2
61 | ],
62 | "no-extra-semi": [
63 | 2
64 | ],
65 | "no-func-assign": [
66 | 2
67 | ],
68 | "no-inner-declarations": [
69 | 2
70 | ],
71 | "no-invalid-regexp": [
72 | 2
73 | ],
74 | "no-irregular-whitespace": [
75 | 2
76 | ],
77 | "valid-typeof": [
78 | 2
79 | ],
80 | "no-unexpected-multiline": [
81 | 2
82 | ],
83 | "no-negated-in-lhs": [
84 | 2
85 | ],
86 | "no-obj-calls": [
87 | 2
88 | ],
89 | "no-regex-spaces": [
90 | 2
91 | ],
92 | "no-sparse-arrays": [
93 | 2
94 | ],
95 | "no-unreachable": [
96 | 2
97 | ],
98 | "use-isnan": [
99 | 2
100 | ],
101 | "no-control-regex": [
102 | 2
103 | ],
104 | "no-debugger": [
105 | 2
106 | ],
107 | "no-dupe-keys": [
108 | 2
109 | ],
110 | "no-dupe-args": [
111 | 2
112 | ],
113 | "no-duplicate-case": [
114 | 2
115 | ],
116 | "accessor-pairs": [
117 | 2
118 | ],
119 | "block-scoped-var": [
120 | 2
121 | ],
122 | "no-multi-spaces": [
123 | 2,
124 | {
125 | "exceptions": {
126 | "VariableDeclarator": true,
127 | "AssignmentExpression": true,
128 | "IfStatement": true
129 | }
130 | }
131 | ],
132 | "key-spacing": [
133 | 2,
134 | {
135 | "align": "value"
136 | }
137 | ],
138 | "new-cap": [
139 | 0,
140 | {
141 | "capIsNewExceptions": []
142 | }
143 | ],
144 | "valid-jsdoc": [
145 | 2,
146 | {
147 | "requireReturn": false,
148 | "requireReturnDescription": false
149 | }
150 | ],
151 | "complexity": [
152 | 2,
153 | 5
154 | ],
155 | "consistent-return": [
156 | 2
157 | ],
158 | "dot-notation": [
159 | 2
160 | ],
161 | "dot-location": [
162 | 2,
163 | "property"
164 | ],
165 | "eqeqeq": [
166 | 2
167 | ],
168 | "guard-for-in": [
169 | 2
170 | ],
171 | "no-alert": [
172 | 2
173 | ],
174 | "no-caller": [
175 | 2
176 | ],
177 | "no-div-regex": [
178 | 2
179 | ],
180 | "no-else-return": [
181 | 2
182 | ],
183 | "no-labels": [
184 | 2
185 | ],
186 | "no-eval": [
187 | 2
188 | ],
189 | "no-extra-bind": [
190 | 2
191 | ],
192 | "no-eq-null": [
193 | 2
194 | ],
195 | "no-extend-native": [
196 | 2
197 | ],
198 | "no-fallthrough": [
199 | 2
200 | ],
201 | "no-floating-decimal": [
202 | 2
203 | ],
204 | "no-implicit-coercion": [
205 | 2
206 | ],
207 | "no-implied-eval": [
208 | 2
209 | ],
210 | "no-invalid-this": [
211 | 0
212 | ],
213 | "no-iterator": [
214 | 2
215 | ],
216 | "no-lone-blocks": [
217 | 2
218 | ],
219 | "no-loop-func": [
220 | 2
221 | ],
222 | "no-multi-str": [
223 | 2
224 | ],
225 | "no-native-reassign": [
226 | 2
227 | ],
228 | "no-new-func": [
229 | 2
230 | ],
231 | "no-new-wrappers": [
232 | 2
233 | ],
234 | "no-new": [
235 | 2
236 | ],
237 | "no-octal": [
238 | 2
239 | ],
240 | "no-octal-escape": [
241 | 2
242 | ],
243 | "no-param-reassign": [
244 | 2
245 | ],
246 | "no-process-env": [
247 | 2
248 | ],
249 | "no-proto": [
250 | 2
251 | ],
252 | "no-redeclare": [
253 | 2
254 | ],
255 | "no-return-assign": [
256 | 2
257 | ],
258 | "no-script-url": [
259 | 2
260 | ],
261 | "no-self-compare": [
262 | 2
263 | ],
264 | "no-sequences": [
265 | 2
266 | ],
267 | "no-throw-literal": [
268 | 2
269 | ],
270 | "no-unused-expressions": [
271 | 2
272 | ],
273 | "no-useless-call": [
274 | 2
275 | ],
276 | "no-void": [
277 | 2
278 | ],
279 | "no-warning-comments": [
280 | 2
281 | ],
282 | "no-with": [
283 | 2
284 | ],
285 | "radix": [
286 | 2
287 | ],
288 | "vars-on-top": [
289 | 2
290 | ],
291 | "wrap-iife": [
292 | 2
293 | ],
294 | "yoda": [
295 | 2
296 | ],
297 | "no-undef": [
298 | 2
299 | ],
300 | "no-undefined": [
301 | 2
302 | ],
303 | "init-declarations": [
304 | 2,
305 | "always"
306 | ],
307 | "no-catch-shadow": [
308 | 2
309 | ],
310 | "no-delete-var": [
311 | 2
312 | ],
313 | "no-label-var": [
314 | 2
315 | ],
316 | "no-shadow-restricted-names": [
317 | 2
318 | ],
319 | "no-shadow": [
320 | 2
321 | ],
322 | "no-undef-init": [
323 | 2
324 | ],
325 | "no-unused-vars": [
326 | 2, {"args": "after-used", "argsIgnorePattern": "_"}
327 | ],
328 | "no-use-before-define": [
329 | 2, "nofunc"
330 | ],
331 | "callback-return": [
332 | 2
333 | ],
334 | "handle-callback-err": [
335 | 2
336 | ],
337 | "no-mixed-requires": [
338 | 2
339 | ],
340 | "no-new-require": [
341 | 2
342 | ],
343 | "no-path-concat": [
344 | 2
345 | ],
346 | "no-process-exit": [
347 | 2
348 | ],
349 | "no-sync": [
350 | 2
351 | ],
352 | "func-style": [
353 | 0,
354 | "expression"
355 | ],
356 | "no-inline-comments": [
357 | 2
358 | ],
359 | "no-array-constructor": [
360 | 2
361 | ],
362 | "no-multiple-empty-lines": [
363 | 2
364 | ],
365 | "array-bracket-spacing": [
366 | 2,
367 | "never"
368 | ],
369 | "block-spacing": [
370 | 2,
371 | "always"
372 | ],
373 | "brace-style": [
374 | 2,
375 | "1tbs"
376 | ],
377 | "camelcase": [
378 | 2
379 | ],
380 | "comma-spacing": [
381 | 2,
382 | {
383 | "before": false,
384 | "after": true
385 | }
386 | ],
387 | "computed-property-spacing": [
388 | 2,
389 | "always"
390 | ],
391 | "consistent-this": [
392 | 2,
393 | "self"
394 | ],
395 | "eol-last": [
396 | 2
397 | ],
398 | "id-length": [
399 | 2,
400 | {
401 | "min": 2,
402 | "max": 20,
403 | "exceptions": [
404 | "x",
405 | "e",
406 | "i",
407 | "f",
408 | "T",
409 | "Q",
410 | "$",
411 | "_",
412 | "y",
413 | "a",
414 | "b",
415 | "t",
416 | "c",
417 | "h"
418 | ]
419 | }
420 | ],
421 | "indent": [
422 | 2,
423 | 2
424 | ],
425 | "lines-around-comment": [
426 | 2,
427 | {
428 | "beforeBlockComment": true,
429 | "beforeLineComment": false
430 | }
431 | ],
432 | "linebreak-style": [
433 | 2
434 | ],
435 | "max-nested-callbacks": [
436 | 2,
437 | 3
438 | ],
439 | "new-parens": [
440 | 2
441 | ],
442 | "newline-after-var": [
443 | 2
444 | ],
445 | "no-continue": [
446 | 2
447 | ],
448 | "no-mixed-spaces-and-tabs": [
449 | 2
450 | ],
451 | "no-nested-ternary": [
452 | 2
453 | ],
454 | "no-new-object": [
455 | 2
456 | ],
457 | "no-spaced-func": [
458 | 2
459 | ],
460 | "no-trailing-spaces": [
461 | 2
462 | ],
463 | "no-underscore-dangle": [
464 | 2
465 | ],
466 | "no-unneeded-ternary": [
467 | 2
468 | ],
469 | "object-curly-spacing": [
470 | 2,
471 | "always"
472 | ],
473 | "one-var": [
474 | 2,
475 | "never"
476 | ],
477 | "operator-assignment": [
478 | 2,
479 | "never"
480 | ],
481 | "operator-linebreak": [
482 | 2,
483 | "after"
484 | ],
485 | "padded-blocks": [
486 | 2,
487 | "never"
488 | ],
489 | "quote-props": [
490 | 2,
491 | "consistent-as-needed"
492 | ],
493 | "quotes": [
494 | 2,
495 | "single"
496 | ],
497 | "semi-spacing": [
498 | 2,
499 | {
500 | "before": false,
501 | "after": true
502 | }
503 | ],
504 | "semi": [
505 | 2,
506 | "always"
507 | ],
508 | "keyword-spacing": [
509 | 2,
510 | {
511 | "before": true,
512 | "after": true,
513 | "overrides": {
514 | "if": {"after": false},
515 | "catch": {"after": false}
516 | }
517 | }
518 | ],
519 | "space-before-blocks": [
520 | 2,
521 | "always"
522 | ],
523 | "space-before-function-paren": [
524 | 2,
525 | "never"
526 | ],
527 | "space-in-parens": [
528 | 2,
529 | "never"
530 | ],
531 | "space-infix-ops": [
532 | 2
533 | ],
534 | "space-unary-ops": [
535 | 2,
536 | {
537 | "words": true,
538 | "nonwords": false
539 | }
540 | ],
541 | "spaced-comment": [
542 | 2,
543 | "always"
544 | ],
545 | "arrow-parens": [
546 | 2,
547 | "as-needed"
548 | ],
549 | "arrow-spacing": [
550 | 2,
551 | {
552 | "before": true,
553 | "after": true
554 | }
555 | ],
556 | "constructor-super": [
557 | 2
558 | ],
559 | "generator-star-spacing": [
560 | 2,
561 | {
562 | "before": false,
563 | "after": true
564 | }
565 | ],
566 | "no-class-assign": [
567 | 2
568 | ],
569 | "no-const-assign": [
570 | 2
571 | ],
572 | "no-dupe-class-members": [
573 | 2
574 | ],
575 | "no-this-before-super": [
576 | 2
577 | ],
578 | "no-var": [
579 | 2
580 | ],
581 | "object-shorthand": [
582 | 0,
583 | "never"
584 | ],
585 | "prefer-const": [
586 | 2
587 | ],
588 | "prefer-spread": [
589 | 2
590 | ],
591 | "prefer-reflect": [
592 | 0
593 | ],
594 | "prefer-template": [
595 | 2
596 | ],
597 | "require-yield": [
598 | 2
599 | ],
600 | "max-depth": [
601 | 2,
602 | 5
603 | ],
604 | "max-statements": [
605 | 2,
606 | 10
607 | ]
608 | }
609 | }
610 |
--------------------------------------------------------------------------------