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/core": "^7.6.4",
|
||||
"@babel/preset-env": "^7.6.3",
|
||||
"@types/jest": "^24.0.19",
|
||||
"@types/lodash": "^4.14.144",
|
||||
"babel-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"
|
||||
},
|
||||
"license": "MIT",
|
||||
@ -25,11 +30,13 @@
|
||||
"url": "git+https://github.com/yanick/updux.git"
|
||||
},
|
||||
"keywords": [
|
||||
"redux", "updeep"
|
||||
"redux",
|
||||
"updeep"
|
||||
],
|
||||
"author": "Yanick Champoux <yanick@babyl.ca> (http://techblog.babyl.ca)",
|
||||
"bugs": {
|
||||
"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';
|
||||
|
||||
export default function updux(config) {
|
||||
import { UpduxConfig } from './types';
|
||||
|
||||
export default function updux(config: UpduxConfig) {
|
||||
return new Updux(config);
|
||||
}
|
@ -7,7 +7,7 @@ test( 'simple effect', () => {
|
||||
|
||||
const store = updux({
|
||||
effects: {
|
||||
foo: api => next => action => {
|
||||
foo: (api:any) => (next:any) => (action:any) => {
|
||||
tracer();
|
||||
next(action);
|
||||
},
|
||||
@ -30,7 +30,7 @@ test( 'effect and sub-effect', () => {
|
||||
|
||||
const tracer = jest.fn();
|
||||
|
||||
const tracerEffect = signature => api => next => action => {
|
||||
const tracerEffect = ( signature: string ) => ( api:any ) => (next:any) => ( action: any ) => {
|
||||
tracer(signature);
|
||||
next(action);
|
||||
};
|
||||
@ -83,7 +83,7 @@ test( '"*" effect', () => {
|
||||
|
||||
test( 'async effect', async () => {
|
||||
|
||||
function timeout(ms) {
|
||||
function timeout(ms:number) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
@ -1,13 +1,13 @@
|
||||
import updux from '.';
|
||||
import u from 'updeep';
|
||||
|
||||
const tracer = chr => u({ tracer: s => (s||'') + chr });
|
||||
const tracer = (chr:string) => u({ tracer: (s='') => s + chr });
|
||||
|
||||
test( 'mutations, simple', () => {
|
||||
const dux = updux({
|
||||
mutations: {
|
||||
foo: () => tracer('a'),
|
||||
'*': (p,a) => tracer('b'),
|
||||
'*': () => tracer('b'),
|
||||
},
|
||||
});
|
||||
|
||||
@ -28,14 +28,14 @@ test( 'with subduxes', () => {
|
||||
const dux = updux({
|
||||
mutations: {
|
||||
foo: () => tracer('a'),
|
||||
'*': (dummy,a) => tracer('b'),
|
||||
bar: () => ({bar}) => ({ bar, tracer: bar.tracer })
|
||||
'*': () => tracer('b'),
|
||||
bar: () => ({bar}:any) => ({ bar, tracer: bar.tracer })
|
||||
},
|
||||
subduxes: {
|
||||
bar: updux({
|
||||
mutations: {
|
||||
foo: () => tracer('d'),
|
||||
'*': (dummy,a) => tracer('e'),
|
||||
'*': () => tracer('e'),
|
||||
},
|
||||
}),
|
||||
},
|
@ -5,7 +5,7 @@ test('actions from mutations', () => {
|
||||
actions: {foo, bar},
|
||||
} = updux({
|
||||
mutations: {
|
||||
foo: () => x => x,
|
||||
foo: () => (x:any) => x,
|
||||
},
|
||||
});
|
||||
|
||||
@ -24,11 +24,11 @@ test('reducer', () => {
|
||||
const {actions, reducer} = updux({
|
||||
initial: {counter: 1},
|
||||
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});
|
||||
|
||||
@ -41,16 +41,16 @@ test( 'sub reducers', () => {
|
||||
const foo = updux({
|
||||
initial: 1,
|
||||
mutations: {
|
||||
doFoo: () => (x) => x + 1,
|
||||
doAll: () => x => x + 10,
|
||||
doFoo: () => (x:number) => x + 1,
|
||||
doAll: () => (x:number) => x + 10,
|
||||
},
|
||||
});
|
||||
|
||||
const bar = updux({
|
||||
initial: 'a',
|
||||
mutations: {
|
||||
doBar: () => x => x + 'a',
|
||||
doAll: () => x => x + 'b',
|
||||
doBar: () => (x:string) => x + 'a',
|
||||
doAll: () => (x:string) => x + 'b',
|
||||
}
|
||||
});
|
||||
|
||||
@ -64,7 +64,7 @@ test( 'sub reducers', () => {
|
||||
|
||||
expect(Object.keys(actions)).toHaveLength(3);
|
||||
|
||||
let state = reducer(null,{});
|
||||
let state = reducer(null,{type:'noop'});
|
||||
|
||||
expect(state).toEqual({ foo: 1, bar: 'a' });
|
||||
|
||||
@ -92,7 +92,7 @@ test('precedence between root and sub-reducers', () => {
|
||||
foo: { bar: 4 },
|
||||
},
|
||||
mutations: {
|
||||
inc: () => state => {
|
||||
inc: () => (state:any) => {
|
||||
return {
|
||||
...state,
|
||||
surprise: state.foo.bar
|
||||
@ -106,7 +106,7 @@ test('precedence between root and sub-reducers', () => {
|
||||
quux: 3,
|
||||
},
|
||||
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));
|
||||
}
|
||||
|
||||
@ -133,8 +133,8 @@ test( 'middleware', async () => {
|
||||
} = updux({
|
||||
initial: "",
|
||||
mutations: {
|
||||
inc: (addition) => state => state + addition,
|
||||
doEeet: () => state => {
|
||||
inc: (addition:number) => (state:number) => state + addition,
|
||||
doEeet: () => (state:number) => {
|
||||
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": {
|
||||
/* Basic Options */
|
||||
"incremental": true, /* Enable incremental compilation */
|
||||
"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'. */
|
||||
"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. */
|
||||
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
|
||||
"declaration": true, /* Generates corresponding '.d.ts' file. */
|
||||
@ -24,7 +27,7 @@
|
||||
|
||||
/* 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. */
|
||||
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
|
||||
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
|
||||
|
Loading…
Reference in New Issue
Block a user