├── .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 | ![overview](asset/architecture.png) 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 |
    24 |
    all
    25 |
    active
    26 |
    completed
    27 |
    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 | 14 | 15 | 17 | 19 | 21 | 23 | 25 | 27 | 29 | 30 | 31 | 32 | 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 | ![overview](manual/asset/architecture.png) 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 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | Canvas 1 20 | 21 | 22 | Layer 1 23 | 24 | 25 | 26 | 27 | 28 | Context 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | PatchSet 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | View 78 | 79 | 80 | 81 | 82 | 83 | 84 | Component 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | Scope 99 | 100 | 101 | 102 | 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 | --------------------------------------------------------------------------------