Merge branch 'typescript'
This commit is contained in:
commit
9f66f7e494
1
index.d.ts
vendored
Normal file
1
index.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
17
index.test-d.ts
Normal file
17
index.test-d.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { expectType, expectError } from 'tsd';
|
||||||
|
|
||||||
|
import buildInitial from './src/buildInitial';
|
||||||
|
|
||||||
|
expectType<{}>(buildInitial());
|
||||||
|
|
||||||
|
type MyState = {
|
||||||
|
foo: {
|
||||||
|
bar: number
|
||||||
|
},
|
||||||
|
baz: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
expectType<MyState>(buildInitial<MyState>());
|
||||||
|
|
||||||
|
expectError( buildInitial<MyState>({ foo: { bar: "potato" } }) );
|
||||||
|
|
9
jest.config.js
Normal file
9
jest.config.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
module.exports = {
|
||||||
|
"roots": [
|
||||||
|
"./src"
|
||||||
|
],
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.ts$": "ts-jest",
|
||||||
|
"^.+\\.js$": "babel-jest",
|
||||||
|
},
|
||||||
|
}
|
11
package.json
11
package.json
@ -7,8 +7,13 @@
|
|||||||
"@babel/cli": "^7.6.4",
|
"@babel/cli": "^7.6.4",
|
||||||
"@babel/core": "^7.6.4",
|
"@babel/core": "^7.6.4",
|
||||||
"@babel/preset-env": "^7.6.3",
|
"@babel/preset-env": "^7.6.3",
|
||||||
|
"@types/jest": "^24.0.19",
|
||||||
|
"@types/lodash": "^4.14.144",
|
||||||
"babel-jest": "^24.9.0",
|
"babel-jest": "^24.9.0",
|
||||||
"jest": "^24.9.0",
|
"jest": "^24.9.0",
|
||||||
|
"ts-jest": "^24.1.0",
|
||||||
|
"tsd": "^0.10.0",
|
||||||
|
"typescript": "^3.6.4",
|
||||||
"updeep": "^1.2.0"
|
"updeep": "^1.2.0"
|
||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@ -25,11 +30,13 @@
|
|||||||
"url": "git+https://github.com/yanick/updux.git"
|
"url": "git+https://github.com/yanick/updux.git"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"redux", "updeep"
|
"redux",
|
||||||
|
"updeep"
|
||||||
],
|
],
|
||||||
"author": "Yanick Champoux <yanick@babyl.ca> (http://techblog.babyl.ca)",
|
"author": "Yanick Champoux <yanick@babyl.ca> (http://techblog.babyl.ca)",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/yanick/updux/issues"
|
"url": "https://github.com/yanick/updux/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/yanick/updux#readme"
|
"homepage": "https://github.com/yanick/updux#readme",
|
||||||
|
"types": "./index.d.ts"
|
||||||
}
|
}
|
||||||
|
@ -1,38 +0,0 @@
|
|||||||
import updux from '.';
|
|
||||||
import u from 'updeep';
|
|
||||||
|
|
||||||
test( 'actions defined in effects and mutations, multi-level', () => {
|
|
||||||
|
|
||||||
const { actions } = updux({
|
|
||||||
effects: {
|
|
||||||
foo: api => next => action => { },
|
|
||||||
},
|
|
||||||
mutations: { bar: () => () => null },
|
|
||||||
subduxes: {
|
|
||||||
mysub: {
|
|
||||||
effects: { baz: api => next => action => { }, },
|
|
||||||
mutations: { quux: () => () => null },
|
|
||||||
actions: {
|
|
||||||
foo: (limit) => ({limit}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
myothersub: {
|
|
||||||
effects: {
|
|
||||||
foo: () => () => () => {},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const types = Object.keys(actions);
|
|
||||||
types.sort();
|
|
||||||
|
|
||||||
expect( types).toEqual([ 'bar', 'baz', 'foo', 'quux', ]);
|
|
||||||
|
|
||||||
expect( actions.bar() ).toEqual({ type: 'bar' });
|
|
||||||
expect( actions.bar('xxx') ).toEqual({ type: 'bar', payload: 'xxx' });
|
|
||||||
expect( actions.bar(undefined,'yyy') ).toEqual({ type: 'bar',meta: 'yyy' });
|
|
||||||
|
|
||||||
expect(actions.foo(12)).toEqual({type: 'foo', payload: { limit: 12 }});
|
|
||||||
|
|
||||||
});
|
|
38
src/actions.test.ts
Normal file
38
src/actions.test.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import updux from '.';
|
||||||
|
import u from 'updeep';
|
||||||
|
|
||||||
|
const noopEffect = () => () => () => {};
|
||||||
|
|
||||||
|
test('actions defined in effects and mutations, multi-level', () => {
|
||||||
|
const {actions} = updux({
|
||||||
|
effects: {
|
||||||
|
foo: noopEffect,
|
||||||
|
},
|
||||||
|
mutations: {bar: () => () => null},
|
||||||
|
subduxes: {
|
||||||
|
mysub: {
|
||||||
|
effects: {baz: noopEffect },
|
||||||
|
mutations: {quux: () => () => null},
|
||||||
|
actions: {
|
||||||
|
foo: (limit:number) => ({limit}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
myothersub: {
|
||||||
|
effects: {
|
||||||
|
foo: noopEffect,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const types = Object.keys(actions);
|
||||||
|
types.sort();
|
||||||
|
|
||||||
|
expect(types).toEqual(['bar', 'baz', 'foo', 'quux']);
|
||||||
|
|
||||||
|
expect(actions.bar()).toEqual({type: 'bar'});
|
||||||
|
expect(actions.bar('xxx')).toEqual({type: 'bar', payload: 'xxx'});
|
||||||
|
expect(actions.bar(undefined, 'yyy')).toEqual({type: 'bar', meta: 'yyy'});
|
||||||
|
|
||||||
|
expect(actions.foo(12)).toEqual({type: 'foo', payload: {limit: 12}});
|
||||||
|
});
|
@ -1,40 +0,0 @@
|
|||||||
import fp from 'lodash/fp';
|
|
||||||
|
|
||||||
function actionFor(type) {
|
|
||||||
const creator = ( (payload = undefined, meta = undefined) =>
|
|
||||||
fp.pickBy(v => v !== undefined)({type, payload, meta})
|
|
||||||
);
|
|
||||||
|
|
||||||
creator._genericAction = true;
|
|
||||||
|
|
||||||
return creator;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function buildActions(
|
|
||||||
creators = {},
|
|
||||||
mutations = {},
|
|
||||||
effects = {},
|
|
||||||
subActions = [],
|
|
||||||
) {
|
|
||||||
|
|
||||||
// priority => generics => generic subs => craft subs => creators
|
|
||||||
|
|
||||||
const [ crafted, generic ] = fp.partition(
|
|
||||||
([type,f]) => !f._genericAction
|
|
||||||
)( fp.flatten( subActions.map( x => Object.entries(x) ) ).filter(
|
|
||||||
([_,f]) => f
|
|
||||||
) )
|
|
||||||
|
|
||||||
const actions = [
|
|
||||||
...([ ...Object.keys(mutations), ...Object.keys(effects) ]
|
|
||||||
.map( type => [ type, actionFor(type) ] )),
|
|
||||||
...generic,
|
|
||||||
...crafted,
|
|
||||||
...Object.entries(creators).map(
|
|
||||||
([type, payload]) => [type, (...args) => ({ type, payload: payload(...args) })]
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
return fp.fromPairs(actions);
|
|
||||||
|
|
||||||
}
|
|
46
src/buildActions/index.ts
Normal file
46
src/buildActions/index.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import fp from 'lodash/fp';
|
||||||
|
import { Action, ActionPayloadGenerator, Dictionary } from '../types';
|
||||||
|
|
||||||
|
interface ActionCreator {
|
||||||
|
( ...args: any[] ): Action;
|
||||||
|
_genericAction?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
function actionFor(type:string) {
|
||||||
|
const creator : ActionCreator = ( (payload = undefined, meta = undefined) =>
|
||||||
|
fp.pickBy(v => v !== undefined)({type, payload, meta}) as Action
|
||||||
|
);
|
||||||
|
|
||||||
|
creator._genericAction = true;
|
||||||
|
|
||||||
|
return creator;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActionPair = [ string, ActionCreator ];
|
||||||
|
|
||||||
|
function buildActions(
|
||||||
|
generators : Dictionary<ActionPayloadGenerator> = {},
|
||||||
|
actionNames: string[] = [],
|
||||||
|
subActions : ActionPair[] = [],
|
||||||
|
):Dictionary<ActionCreator> {
|
||||||
|
|
||||||
|
// priority => generics => generic subs => craft subs => creators
|
||||||
|
|
||||||
|
const [ crafted, generic ] = fp.partition(
|
||||||
|
([type,f]) => !f._genericAction
|
||||||
|
)( subActions );
|
||||||
|
|
||||||
|
const actions = [
|
||||||
|
...(actionNames.map( type => [ type, actionFor(type) ] )),
|
||||||
|
...generic,
|
||||||
|
...crafted,
|
||||||
|
...Object.entries(generators).map(
|
||||||
|
([type, payload]: [ string, Function ]) => [type, (...args: any) => ({ type, payload: payload(...args) })]
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
return fp.fromPairs(actions);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default buildActions;
|
@ -1,17 +0,0 @@
|
|||||||
import { createStore as reduxCreateStore, applyMiddleware } from 'redux';
|
|
||||||
|
|
||||||
export default function buildCreateStore( reducer, initial, middleware,
|
|
||||||
actions ) {
|
|
||||||
return () => {
|
|
||||||
const store = reduxCreateStore( reducer, initial,
|
|
||||||
applyMiddleware( middleware)
|
|
||||||
);
|
|
||||||
for ( let a in actions ) {
|
|
||||||
store.dispatch[a] = (...args) => {
|
|
||||||
store.dispatch(actions[a](...args))
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return store;
|
|
||||||
}
|
|
||||||
};
|
|
31
src/buildCreateStore/index.ts
Normal file
31
src/buildCreateStore/index.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import {
|
||||||
|
createStore as reduxCreateStore,
|
||||||
|
applyMiddleware,
|
||||||
|
Middleware,
|
||||||
|
Reducer,
|
||||||
|
} from 'redux';
|
||||||
|
import { ActionCreator, Dictionary } from '../types';
|
||||||
|
|
||||||
|
function buildCreateStore<S>(
|
||||||
|
reducer: Reducer<S>,
|
||||||
|
initial: S,
|
||||||
|
middleware: Middleware,
|
||||||
|
actions: Dictionary<ActionCreator>,
|
||||||
|
) {
|
||||||
|
return () => {
|
||||||
|
const store = reduxCreateStore(
|
||||||
|
reducer,
|
||||||
|
initial,
|
||||||
|
applyMiddleware(middleware),
|
||||||
|
);
|
||||||
|
for (let a in actions) {
|
||||||
|
( store.dispatch as any)[a] = (...args: any[]) => {
|
||||||
|
store.dispatch(actions[a](...args));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return store;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default buildCreateStore;
|
@ -1,8 +0,0 @@
|
|||||||
import fp from 'lodash/fp';
|
|
||||||
|
|
||||||
export default function buildInitial(
|
|
||||||
initial= {},
|
|
||||||
subduxes = {},
|
|
||||||
) {
|
|
||||||
return fp.isPlainObject(initial) ? fp.mergeAll([subduxes, initial]) : initial;
|
|
||||||
}
|
|
13
src/buildInitial/index.ts
Normal file
13
src/buildInitial/index.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import fp from 'lodash/fp';
|
||||||
|
import { Dictionary } from '../types';
|
||||||
|
|
||||||
|
function buildInitial<S extends number|string|boolean>( initial: S, subduxes?: Dictionary<undefined> ): S;
|
||||||
|
function buildInitial<S extends object>( initial?: Partial<S>, subduxes?: Partial<S> ): S extends object ? S : never;
|
||||||
|
function buildInitial(
|
||||||
|
initial : any = {},
|
||||||
|
subduxes : any = {} ,
|
||||||
|
) {
|
||||||
|
return fp.isPlainObject(initial) ? fp.mergeAll([subduxes, initial]) : initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default buildInitial;
|
@ -1,30 +0,0 @@
|
|||||||
import fp from 'lodash/fp';
|
|
||||||
|
|
||||||
const MiddlewareFor = (type,mw) => api => next => action => {
|
|
||||||
if (type !== '*' && action.type !== type) return next(action);
|
|
||||||
|
|
||||||
return mw(api)(next)(action);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function buildMiddleware(
|
|
||||||
effects = {},
|
|
||||||
actions = {},
|
|
||||||
subduxes = {},
|
|
||||||
) {
|
|
||||||
return api => {
|
|
||||||
for (let type in actions) {
|
|
||||||
api.dispatch[type] = (...args) => api.dispatch(actions[type](...args));
|
|
||||||
}
|
|
||||||
|
|
||||||
return original_next => {
|
|
||||||
return [
|
|
||||||
...fp.toPairs(effects).map(([type, effect]) =>
|
|
||||||
MiddlewareFor(type,effect)
|
|
||||||
),
|
|
||||||
...fp.map('middleware', subduxes),
|
|
||||||
]
|
|
||||||
.filter(x => x)
|
|
||||||
.reduceRight((next, mw) => mw(api)(next), original_next);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
38
src/buildMiddleware/index.ts
Normal file
38
src/buildMiddleware/index.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import fp from 'lodash/fp';
|
||||||
|
|
||||||
|
import { Middleware, MiddlewareAPI, Dispatch } from 'redux';
|
||||||
|
import { Dictionary, ActionCreator, Action, UpduxDispatch } from '../types';
|
||||||
|
|
||||||
|
const MiddlewareFor = (type: any, mw: Middleware ): Middleware => api => next => action => {
|
||||||
|
if (type !== '*' && action.type !== type) return next(action);
|
||||||
|
|
||||||
|
return mw(api)(next)(action);
|
||||||
|
};
|
||||||
|
|
||||||
|
type Next = (action: Action) => any;
|
||||||
|
|
||||||
|
function buildMiddleware<S=any>(
|
||||||
|
effects : Dictionary<Middleware<{},S,UpduxDispatch>>= {},
|
||||||
|
actions : Dictionary<ActionCreator>= {},
|
||||||
|
subMiddlewares :Middleware<{},S,UpduxDispatch>[] = [],
|
||||||
|
): Middleware<{},S,UpduxDispatch>
|
||||||
|
{
|
||||||
|
return (api: MiddlewareAPI<UpduxDispatch,S>) => {
|
||||||
|
for (let type in actions) {
|
||||||
|
api.dispatch[type] = (...args:any[]) => api.dispatch(((actions as any)[type] as any)(...args));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (original_next: Next)=> {
|
||||||
|
return [
|
||||||
|
...fp.toPairs(effects).map(([type, effect]) =>
|
||||||
|
MiddlewareFor(type,effect as Middleware)
|
||||||
|
),
|
||||||
|
...subMiddlewares
|
||||||
|
]
|
||||||
|
.filter(x => x)
|
||||||
|
.reduceRight((next, mw) => mw(api)(next), original_next);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default buildMiddleware;
|
@ -1,53 +0,0 @@
|
|||||||
import fp from 'lodash/fp';
|
|
||||||
import u from 'updeep';
|
|
||||||
|
|
||||||
const composeMutations = (mutations) =>
|
|
||||||
mutations.reduce( (m1,m2) =>
|
|
||||||
(payload=null,action={}) => state => m2(payload,action)(
|
|
||||||
m1(payload,action)(state) ));
|
|
||||||
|
|
||||||
export default function buildMutations(mutations = {}, subduxes= {}) {
|
|
||||||
// we have to differentiate the subduxes with '*' than those
|
|
||||||
// without, as the root '*' is not the same as any sub-'*'
|
|
||||||
|
|
||||||
const actions = fp.uniq( Object.keys(mutations).concat(
|
|
||||||
...Object.values( subduxes ).map( ({mutations = {}}) => Object.keys(mutations) )
|
|
||||||
) );
|
|
||||||
|
|
||||||
let mergedMutations = {};
|
|
||||||
|
|
||||||
let [ globby, nonGlobby ] = fp.partition(
|
|
||||||
([_,{mutations={}}]) => mutations['*'],
|
|
||||||
Object.entries(subduxes)
|
|
||||||
);
|
|
||||||
|
|
||||||
globby =
|
|
||||||
fp.flow([
|
|
||||||
fp.fromPairs,
|
|
||||||
fp.mapValues(
|
|
||||||
({reducer}) => (_,action={}) => state =>
|
|
||||||
reducer(state,action) ),
|
|
||||||
])(globby);
|
|
||||||
|
|
||||||
const globbyMutation = (payload,action) => u(
|
|
||||||
fp.mapValues( (mut) => mut(payload,action) )(globby)
|
|
||||||
);
|
|
||||||
|
|
||||||
actions.forEach( action => {
|
|
||||||
mergedMutations[action] = [ globbyMutation ]
|
|
||||||
});
|
|
||||||
|
|
||||||
nonGlobby.forEach( ([slice, {mutations={},reducer={}}]) => {
|
|
||||||
Object.entries(mutations).forEach(([type,mutation]) => {
|
|
||||||
const localized = (payload=null,action={}) => u.updateIn( slice )( (mutation)(payload,action) );
|
|
||||||
|
|
||||||
mergedMutations[type].push(localized);
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
Object.entries(mutations).forEach(([type,mutation]) => {
|
|
||||||
mergedMutations[type].push(mutation);
|
|
||||||
});
|
|
||||||
|
|
||||||
return fp.mapValues( composeMutations )(mergedMutations);
|
|
||||||
}
|
|
66
src/buildMutations/index.ts
Normal file
66
src/buildMutations/index.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import fp from 'lodash/fp';
|
||||||
|
import u from 'updeep';
|
||||||
|
import {Mutation, Action, Dictionary} from '../types';
|
||||||
|
|
||||||
|
const composeMutations = (mutations: Mutation[]) =>
|
||||||
|
mutations.reduce((m1, m2) => (payload: any = null, action: Action) => state =>
|
||||||
|
m2(payload, action)(m1(payload, action)(state)),
|
||||||
|
);
|
||||||
|
|
||||||
|
type SubMutations = {
|
||||||
|
[ slice: string ]: Dictionary<Mutation>
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildMutations(
|
||||||
|
mutations :Dictionary<Mutation> = {},
|
||||||
|
subduxes = {}
|
||||||
|
) {
|
||||||
|
// we have to differentiate the subduxes with '*' than those
|
||||||
|
// without, as the root '*' is not the same as any sub-'*'
|
||||||
|
|
||||||
|
const actions = fp.uniq(
|
||||||
|
Object.keys(mutations).concat(
|
||||||
|
...Object.values(subduxes).map(({mutations = {}}:any) =>
|
||||||
|
Object.keys(mutations),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
let mergedMutations :Dictionary<Mutation[]> = {};
|
||||||
|
|
||||||
|
let [globby, nonGlobby] = fp.partition(
|
||||||
|
([_, {mutations = {}}]:any) => mutations['*'],
|
||||||
|
Object.entries(subduxes),
|
||||||
|
);
|
||||||
|
|
||||||
|
globby = fp.flow([
|
||||||
|
fp.fromPairs,
|
||||||
|
fp.mapValues(({reducer}) => (_:any, action :Action) => ( state: any ) =>
|
||||||
|
reducer(state, action),
|
||||||
|
),
|
||||||
|
])(globby);
|
||||||
|
|
||||||
|
const globbyMutation = (payload:any, action:Action) =>
|
||||||
|
u(fp.mapValues((mut:any) => mut(payload, action))(globby));
|
||||||
|
|
||||||
|
actions.forEach(action => {
|
||||||
|
mergedMutations[action] = [globbyMutation];
|
||||||
|
});
|
||||||
|
|
||||||
|
nonGlobby.forEach(([slice, {mutations = {}, reducer = {}}]:any[]) => {
|
||||||
|
Object.entries(mutations).forEach(([type, mutation]) => {
|
||||||
|
const localized = (payload = null, action :Action) =>
|
||||||
|
u.updateIn(slice)((mutation as Mutation)(payload, action));
|
||||||
|
|
||||||
|
mergedMutations[type].push(localized);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.entries(mutations).forEach(([type, mutation]) => {
|
||||||
|
mergedMutations[type].push(mutation);
|
||||||
|
});
|
||||||
|
|
||||||
|
return fp.mapValues(composeMutations)(mergedMutations);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default buildMutations;
|
@ -1,15 +0,0 @@
|
|||||||
import fp from 'lodash/fp';
|
|
||||||
|
|
||||||
export default function buildUpreducer(initial, mutations) {
|
|
||||||
return (action = {}) => (state) => {
|
|
||||||
if (state === null) state = initial;
|
|
||||||
|
|
||||||
const a =
|
|
||||||
mutations[(action).type] ||
|
|
||||||
mutations['*'];
|
|
||||||
|
|
||||||
if(!a) return state;
|
|
||||||
|
|
||||||
return a((action).payload, action)(state);
|
|
||||||
};
|
|
||||||
}
|
|
19
src/buildUpreducer/index.ts
Normal file
19
src/buildUpreducer/index.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import fp from 'lodash/fp';
|
||||||
|
|
||||||
|
import { Dictionary, Mutation, Action, Upreducer } from '../types';
|
||||||
|
|
||||||
|
function buildUpreducer<S>(initial: S, mutations: Dictionary<Mutation<S>> ): Upreducer<S> {
|
||||||
|
return (action :Action) => (state: S) => {
|
||||||
|
if (state === null) state = initial;
|
||||||
|
|
||||||
|
const a =
|
||||||
|
mutations[action.type] ||
|
||||||
|
mutations['*'];
|
||||||
|
|
||||||
|
if(!a) return state;
|
||||||
|
|
||||||
|
return a(action.payload, action)(state);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default buildUpreducer;
|
@ -3,6 +3,8 @@ import u from 'updeep';
|
|||||||
|
|
||||||
import Updux from './updux';
|
import Updux from './updux';
|
||||||
|
|
||||||
export default function updux(config) {
|
import { UpduxConfig } from './types';
|
||||||
|
|
||||||
|
export default function updux(config: UpduxConfig) {
|
||||||
return new Updux(config);
|
return new Updux(config);
|
||||||
}
|
}
|
@ -7,7 +7,7 @@ test( 'simple effect', () => {
|
|||||||
|
|
||||||
const store = updux({
|
const store = updux({
|
||||||
effects: {
|
effects: {
|
||||||
foo: api => next => action => {
|
foo: (api:any) => (next:any) => (action:any) => {
|
||||||
tracer();
|
tracer();
|
||||||
next(action);
|
next(action);
|
||||||
},
|
},
|
||||||
@ -30,7 +30,7 @@ test( 'effect and sub-effect', () => {
|
|||||||
|
|
||||||
const tracer = jest.fn();
|
const tracer = jest.fn();
|
||||||
|
|
||||||
const tracerEffect = signature => api => next => action => {
|
const tracerEffect = ( signature: string ) => ( api:any ) => (next:any) => ( action: any ) => {
|
||||||
tracer(signature);
|
tracer(signature);
|
||||||
next(action);
|
next(action);
|
||||||
};
|
};
|
||||||
@ -83,7 +83,7 @@ test( '"*" effect', () => {
|
|||||||
|
|
||||||
test( 'async effect', async () => {
|
test( 'async effect', async () => {
|
||||||
|
|
||||||
function timeout(ms) {
|
function timeout(ms:number) {
|
||||||
return new Promise(resolve => setTimeout(resolve, ms));
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
|
|
@ -1,13 +1,13 @@
|
|||||||
import updux from '.';
|
import updux from '.';
|
||||||
import u from 'updeep';
|
import u from 'updeep';
|
||||||
|
|
||||||
const tracer = chr => u({ tracer: s => (s||'') + chr });
|
const tracer = (chr:string) => u({ tracer: (s='') => s + chr });
|
||||||
|
|
||||||
test( 'mutations, simple', () => {
|
test( 'mutations, simple', () => {
|
||||||
const dux = updux({
|
const dux = updux({
|
||||||
mutations: {
|
mutations: {
|
||||||
foo: () => tracer('a'),
|
foo: () => tracer('a'),
|
||||||
'*': (p,a) => tracer('b'),
|
'*': () => tracer('b'),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -28,14 +28,14 @@ test( 'with subduxes', () => {
|
|||||||
const dux = updux({
|
const dux = updux({
|
||||||
mutations: {
|
mutations: {
|
||||||
foo: () => tracer('a'),
|
foo: () => tracer('a'),
|
||||||
'*': (dummy,a) => tracer('b'),
|
'*': () => tracer('b'),
|
||||||
bar: () => ({bar}) => ({ bar, tracer: bar.tracer })
|
bar: () => ({bar}:any) => ({ bar, tracer: bar.tracer })
|
||||||
},
|
},
|
||||||
subduxes: {
|
subduxes: {
|
||||||
bar: updux({
|
bar: updux({
|
||||||
mutations: {
|
mutations: {
|
||||||
foo: () => tracer('d'),
|
foo: () => tracer('d'),
|
||||||
'*': (dummy,a) => tracer('e'),
|
'*': () => tracer('e'),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
},
|
},
|
@ -5,7 +5,7 @@ test('actions from mutations', () => {
|
|||||||
actions: {foo, bar},
|
actions: {foo, bar},
|
||||||
} = updux({
|
} = updux({
|
||||||
mutations: {
|
mutations: {
|
||||||
foo: () => x => x,
|
foo: () => (x:any) => x,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -24,11 +24,11 @@ test('reducer', () => {
|
|||||||
const {actions, reducer} = updux({
|
const {actions, reducer} = updux({
|
||||||
initial: {counter: 1},
|
initial: {counter: 1},
|
||||||
mutations: {
|
mutations: {
|
||||||
inc: () => ({counter}) => ({counter: counter + 1}),
|
inc: () => ({counter}:{counter:number}) => ({counter: counter + 1}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
let state = reducer(null, {});
|
let state = reducer(null, {type:'noop'});
|
||||||
|
|
||||||
expect(state).toEqual({counter: 1});
|
expect(state).toEqual({counter: 1});
|
||||||
|
|
||||||
@ -41,16 +41,16 @@ test( 'sub reducers', () => {
|
|||||||
const foo = updux({
|
const foo = updux({
|
||||||
initial: 1,
|
initial: 1,
|
||||||
mutations: {
|
mutations: {
|
||||||
doFoo: () => (x) => x + 1,
|
doFoo: () => (x:number) => x + 1,
|
||||||
doAll: () => x => x + 10,
|
doAll: () => (x:number) => x + 10,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const bar = updux({
|
const bar = updux({
|
||||||
initial: 'a',
|
initial: 'a',
|
||||||
mutations: {
|
mutations: {
|
||||||
doBar: () => x => x + 'a',
|
doBar: () => (x:string) => x + 'a',
|
||||||
doAll: () => x => x + 'b',
|
doAll: () => (x:string) => x + 'b',
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -64,7 +64,7 @@ test( 'sub reducers', () => {
|
|||||||
|
|
||||||
expect(Object.keys(actions)).toHaveLength(3);
|
expect(Object.keys(actions)).toHaveLength(3);
|
||||||
|
|
||||||
let state = reducer(null,{});
|
let state = reducer(null,{type:'noop'});
|
||||||
|
|
||||||
expect(state).toEqual({ foo: 1, bar: 'a' });
|
expect(state).toEqual({ foo: 1, bar: 'a' });
|
||||||
|
|
||||||
@ -92,7 +92,7 @@ test('precedence between root and sub-reducers', () => {
|
|||||||
foo: { bar: 4 },
|
foo: { bar: 4 },
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
inc: () => state => {
|
inc: () => (state:any) => {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
surprise: state.foo.bar
|
surprise: state.foo.bar
|
||||||
@ -106,7 +106,7 @@ test('precedence between root and sub-reducers', () => {
|
|||||||
quux: 3,
|
quux: 3,
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
inc: () => state => ({...state, bar: state.bar + 1 })
|
inc: () => (state:any) => ({...state, bar: state.bar + 1 })
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
@ -122,7 +122,7 @@ test('precedence between root and sub-reducers', () => {
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function timeout(ms) {
|
function timeout(ms:number) {
|
||||||
return new Promise(resolve => setTimeout(resolve, ms));
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -133,8 +133,8 @@ test( 'middleware', async () => {
|
|||||||
} = updux({
|
} = updux({
|
||||||
initial: "",
|
initial: "",
|
||||||
mutations: {
|
mutations: {
|
||||||
inc: (addition) => state => state + addition,
|
inc: (addition:number) => (state:number) => state + addition,
|
||||||
doEeet: () => state => {
|
doEeet: () => (state:number) => {
|
||||||
return state + 'Z';
|
return state + 'Z';
|
||||||
},
|
},
|
||||||
},
|
},
|
29
src/types.ts
Normal file
29
src/types.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { Dispatch, Middleware } from 'redux';
|
||||||
|
|
||||||
|
export type Action = {
|
||||||
|
type: string,
|
||||||
|
payload?: any,
|
||||||
|
meta?: any,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Dictionary<T> = { [key: string]: T };
|
||||||
|
|
||||||
|
export type Mutation<S=any> = (payload: any, action: Action) => (state: S) => S ;
|
||||||
|
|
||||||
|
export type ActionPayloadGenerator = (...args:any[]) => any;
|
||||||
|
|
||||||
|
export type ActionCreator = (...args: any[] ) => Action;
|
||||||
|
|
||||||
|
export type UpduxDispatch = Dispatch & Dictionary<ActionCreator>;
|
||||||
|
|
||||||
|
export type UpduxConfig<S=any> = Partial<{
|
||||||
|
initial: S,
|
||||||
|
subduxes: {},
|
||||||
|
actions: {
|
||||||
|
[ type: string ]: ActionPayloadGenerator
|
||||||
|
},
|
||||||
|
mutations: any,
|
||||||
|
effects: Dictionary<Middleware<{},S,UpduxDispatch>>,
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export type Upreducer<S=any> = (action:Action) => (state:S) => S;
|
53
src/updux.js
53
src/updux.js
@ -1,53 +0,0 @@
|
|||||||
import fp from 'lodash/fp';
|
|
||||||
import buildActions from './buildActions';
|
|
||||||
import buildInitial from './buildInitial';
|
|
||||||
import buildMutations from './buildMutations';
|
|
||||||
|
|
||||||
import buildCreateStore from './buildCreateStore';
|
|
||||||
import buildMiddleware from './buildMiddleware';
|
|
||||||
import buildUpreducer from './buildUpreducer';
|
|
||||||
|
|
||||||
export class Updux {
|
|
||||||
|
|
||||||
constructor(config) {
|
|
||||||
|
|
||||||
this.subduxes = fp.mapValues(
|
|
||||||
value => fp.isPlainObject(value) ? new Updux(value ) : value )(fp.getOr({},'subduxes',config)
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
this.actions = buildActions(
|
|
||||||
config.actions,
|
|
||||||
config.mutations,
|
|
||||||
config.effects,
|
|
||||||
Object.values( this.subduxes ).map( ({actions}) => actions ),
|
|
||||||
)
|
|
||||||
|
|
||||||
this.initial = buildInitial(
|
|
||||||
config.initial, fp.mapValues( ({initial}) => initial )(this.subduxes)
|
|
||||||
);
|
|
||||||
|
|
||||||
this.mutations = buildMutations(
|
|
||||||
config.mutations, this.subduxes
|
|
||||||
);
|
|
||||||
|
|
||||||
this.upreducer = buildUpreducer(
|
|
||||||
this.initial, this.mutations
|
|
||||||
);
|
|
||||||
|
|
||||||
this.reducer = (state,action) => {
|
|
||||||
return this.upreducer(action)(state);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.middleware = buildMiddleware(
|
|
||||||
config.effects,
|
|
||||||
this.actions,
|
|
||||||
config.subduxes,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.createStore = buildCreateStore(this.reducer,this.initial,this.middleware,this.actions);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Updux;
|
|
77
src/updux.ts
Normal file
77
src/updux.ts
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import fp from 'lodash/fp';
|
||||||
|
import buildActions from './buildActions';
|
||||||
|
import buildInitial from './buildInitial';
|
||||||
|
import buildMutations from './buildMutations';
|
||||||
|
|
||||||
|
import buildCreateStore from './buildCreateStore';
|
||||||
|
import buildMiddleware from './buildMiddleware';
|
||||||
|
import buildUpreducer from './buildUpreducer';
|
||||||
|
import { UpduxConfig, Dictionary, Action, ActionCreator, Mutation, Upreducer, UpduxDispatch } from './types';
|
||||||
|
|
||||||
|
import { Middleware, Store } from 'redux';
|
||||||
|
|
||||||
|
type StoreWithDispatchActions<S=any,Actions={ [action: string]: (...args:any) => Action }> = Store<S> & {
|
||||||
|
dispatch: { [ type in keyof Actions ]: (...args:any) => void }
|
||||||
|
};
|
||||||
|
|
||||||
|
export class Updux<S=any> {
|
||||||
|
|
||||||
|
subduxes: Dictionary<Updux>;
|
||||||
|
|
||||||
|
actions: Dictionary<ActionCreator>
|
||||||
|
|
||||||
|
initial: S;
|
||||||
|
|
||||||
|
mutations: Dictionary<Mutation>;
|
||||||
|
|
||||||
|
upreducer: Upreducer<S>;
|
||||||
|
|
||||||
|
reducer: (state:S|undefined,action:Action) => S;
|
||||||
|
|
||||||
|
middleware: Middleware<{},S,UpduxDispatch>;
|
||||||
|
|
||||||
|
createStore: () => StoreWithDispatchActions<S>;
|
||||||
|
|
||||||
|
constructor(config: UpduxConfig) {
|
||||||
|
|
||||||
|
this.subduxes = fp.mapValues(
|
||||||
|
(value:UpduxConfig|Updux) => fp.isPlainObject(value) ? new Updux(value) : value )(fp.getOr({},'subduxes',config)
|
||||||
|
) as Dictionary<Updux>;
|
||||||
|
|
||||||
|
|
||||||
|
this.actions = buildActions(
|
||||||
|
config.actions,
|
||||||
|
[ ...Object.keys(config.mutations||{}), ...Object.keys(config.effects||{} ) ],
|
||||||
|
fp.flatten( Object.values( this.subduxes ).map( ({actions}:Updux) => Object.entries(actions) ) ),
|
||||||
|
)
|
||||||
|
|
||||||
|
this.initial = buildInitial<any>(
|
||||||
|
config.initial, fp.mapValues( ({initial}) => initial )(this.subduxes)
|
||||||
|
);
|
||||||
|
|
||||||
|
this.mutations = buildMutations(
|
||||||
|
config.mutations, this.subduxes
|
||||||
|
);
|
||||||
|
|
||||||
|
this.upreducer = buildUpreducer(
|
||||||
|
this.initial, this.mutations
|
||||||
|
);
|
||||||
|
|
||||||
|
this.reducer = (state,action) => {
|
||||||
|
return this.upreducer(action)(state as S);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.middleware = buildMiddleware(
|
||||||
|
config.effects,
|
||||||
|
this.actions,
|
||||||
|
Object.values(this.subduxes).map( sd => sd.middleware )
|
||||||
|
);
|
||||||
|
|
||||||
|
const actions = this.actions;
|
||||||
|
this.createStore = buildCreateStore<S>(this.reducer,this.initial,this.middleware as Middleware,this.actions) as
|
||||||
|
() => StoreWithDispatchActions< S, typeof actions >;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Updux;
|
@ -1,11 +1,14 @@
|
|||||||
{
|
{
|
||||||
|
"include": [
|
||||||
|
"./src/**/*"
|
||||||
|
],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
/* Basic Options */
|
/* Basic Options */
|
||||||
"incremental": true, /* Enable incremental compilation */
|
"incremental": true, /* Enable incremental compilation */
|
||||||
"target": "ES2019", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
|
"target": "ES2019", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
|
||||||
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
|
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
|
||||||
"lib": [ "dom", "es2019" ], /* Specify library files to be included in the compilation. */
|
"lib": [ "dom", "es2019" ], /* Specify library files to be included in the compilation. */
|
||||||
// "allowJs": true, /* Allow javascript files to be compiled. */
|
"allowJs": false, /* Allow javascript files to be compiled. */
|
||||||
// "checkJs": true, /* Report errors in .js files. */
|
// "checkJs": true, /* Report errors in .js files. */
|
||||||
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
|
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
|
||||||
"declaration": true, /* Generates corresponding '.d.ts' file. */
|
"declaration": true, /* Generates corresponding '.d.ts' file. */
|
||||||
@ -14,7 +17,7 @@
|
|||||||
// "outFile": "./", /* Concatenate and emit output to single file. */
|
// "outFile": "./", /* Concatenate and emit output to single file. */
|
||||||
"outDir": "./dist", /* Redirect output structure to the directory. */
|
"outDir": "./dist", /* Redirect output structure to the directory. */
|
||||||
"rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
"rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
||||||
// "composite": true, /* Enable project compilation */
|
//"composite": true, /* Enable project compilation */
|
||||||
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
|
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
|
||||||
"removeComments": true, /* Do not emit comments to output. */
|
"removeComments": true, /* Do not emit comments to output. */
|
||||||
// "noEmit": true, /* Do not emit outputs. */
|
// "noEmit": true, /* Do not emit outputs. */
|
||||||
@ -24,7 +27,7 @@
|
|||||||
|
|
||||||
/* Strict Type-Checking Options */
|
/* Strict Type-Checking Options */
|
||||||
"strict": true, /* Enable all strict type-checking options. */
|
"strict": true, /* Enable all strict type-checking options. */
|
||||||
"noImplicitAny": false, /* Raise error on expressions and declarations with an implied 'any' type. */
|
"noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
|
||||||
// "strictNullChecks": true, /* Enable strict null checks. */
|
// "strictNullChecks": true, /* Enable strict null checks. */
|
||||||
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
|
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
|
||||||
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
|
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
|
||||||
|
Loading…
Reference in New Issue
Block a user