Merge branch 'actions-subdux' into typescript
This commit is contained in:
commit
f5cdc4207a
@ -3,6 +3,11 @@
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
build: tsc
|
||||
|
||||
checks:
|
||||
deps: [test, build]
|
||||
|
||||
test: vitest run src
|
||||
test:dev: vitest src
|
||||
|
||||
|
6
foo.ts
6
foo.ts
@ -1,6 +0,0 @@
|
||||
const foo = <A, B>(x: Partial<{ a: A; b: B }>) => x as B;
|
||||
|
||||
const y = foo({
|
||||
a: 'potato',
|
||||
b: 2,
|
||||
});
|
21
src/Updux.ts
21
src/Updux.ts
@ -1,3 +1,4 @@
|
||||
import * as R from 'remeda';
|
||||
import {
|
||||
createStore as reduxCreateStore,
|
||||
applyMiddleware,
|
||||
@ -5,7 +6,8 @@ import {
|
||||
Action,
|
||||
} from 'redux';
|
||||
import { configureStore, Reducer, createAction } from '@reduxjs/toolkit';
|
||||
import { withPayload } from './actions';
|
||||
import { withPayload } from './actions.js';
|
||||
import { AggregateActions, Dux, UnionToIntersection } from './types.js';
|
||||
|
||||
type ActionCreator = ReturnType<typeof createAction>;
|
||||
|
||||
@ -20,6 +22,7 @@ type Mutation<A extends ActionCreator = ActionCreator, S = any> = (
|
||||
export default class Updux<
|
||||
T_LocalState = Record<any, any>,
|
||||
T_LocalActions = {},
|
||||
SUBDUXES extends Record<string, Dux> = {},
|
||||
> {
|
||||
#localInitial: T_LocalState;
|
||||
#localActions: T_LocalActions;
|
||||
@ -27,16 +30,28 @@ export default class Updux<
|
||||
string,
|
||||
Mutation<ActionCreator, AggregateState<T_LocalState>>
|
||||
> = {};
|
||||
#subduxes: SUBDUXES;
|
||||
|
||||
#actions: Record<string, ActionCreator>;
|
||||
|
||||
constructor(
|
||||
config: Partial<{
|
||||
initial: T_LocalState;
|
||||
actions: T_LocalActions;
|
||||
subduxes: SUBDUXES;
|
||||
}>,
|
||||
) {
|
||||
// TODO check that we can't alter the initial after the fact
|
||||
this.#localInitial = config.initial ?? ({} as T_LocalState);
|
||||
this.#localActions = config.actions ?? ({} as T_LocalActions);
|
||||
this.#subduxes = config.subduxes ?? ({} as SUBDUXES);
|
||||
}
|
||||
|
||||
get actions(): AggregateActions<T_LocalActions, SUBDUXES> {
|
||||
return R.mergeAll([
|
||||
this.#localActions,
|
||||
...Object.values(this.#subduxes).map(R.pathOr(['actions'], {})),
|
||||
]) as any;
|
||||
}
|
||||
|
||||
// TODO memoize?
|
||||
@ -44,10 +59,6 @@ export default class Updux<
|
||||
return this.#localInitial;
|
||||
}
|
||||
|
||||
get actions() {
|
||||
return this.#localActions;
|
||||
}
|
||||
|
||||
createStore(
|
||||
options: Partial<{
|
||||
initial: T_LocalState;
|
||||
|
@ -34,23 +34,6 @@ test('Updux config accepts actions', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('subduxes actions', () => {
|
||||
const foo = new Updux({
|
||||
actions: {
|
||||
foo: null,
|
||||
},
|
||||
subduxes: {
|
||||
beta: {
|
||||
actions: {
|
||||
bar: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(foo.actions).toHaveProperty('foo');
|
||||
expect(foo.actions).toHaveProperty('bar');
|
||||
});
|
||||
|
||||
test('throw if double action', () => {
|
||||
expect(
|
||||
|
27
src/actions.test.ts
Normal file
27
src/actions.test.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import Updux, { createAction } from './index.js';
|
||||
|
||||
test('subduxes actions', () => {
|
||||
const bar = createAction<number>('bar');
|
||||
const baz = createAction('baz');
|
||||
|
||||
const foo = new Updux({
|
||||
actions: {
|
||||
bar,
|
||||
},
|
||||
subduxes: {
|
||||
beta: {
|
||||
actions: {
|
||||
baz,
|
||||
},
|
||||
},
|
||||
// to check if we can deal with empty actions
|
||||
gamma: {},
|
||||
},
|
||||
});
|
||||
|
||||
expect(foo.actions).toHaveProperty('bar');
|
||||
expect(foo.actions).toHaveProperty('baz');
|
||||
|
||||
expect(foo.actions.bar(2)).toHaveProperty('type', 'bar');
|
||||
expect(foo.actions.baz()).toHaveProperty('type', 'baz');
|
||||
});
|
@ -1,5 +1,7 @@
|
||||
import { createAction } from '@reduxjs/toolkit';
|
||||
|
||||
export { createAction } from '@reduxjs/toolkit';
|
||||
|
||||
export const withPayload =
|
||||
<P>() =>
|
||||
(payload: P) => ({ payload });
|
||||
(payload: P) => ({ payload });
|
||||
|
48
src/buildMiddleware/index.js
Normal file
48
src/buildMiddleware/index.js
Normal file
@ -0,0 +1,48 @@
|
||||
import { mapValues, map, get } from 'lodash-es';
|
||||
const middlewareFor = (type, middleware) => (api) => (next) => (action) => {
|
||||
if (type !== '*' && action.type !== type)
|
||||
return next(action);
|
||||
return middleware(api)(next)(action);
|
||||
};
|
||||
const sliceMw = (slice, mw) => (api) => {
|
||||
const getSliceState = () => get(api.getState(), slice);
|
||||
return mw(Object.assign(Object.assign({}, api), { getState: getSliceState }));
|
||||
};
|
||||
export function augmentMiddlewareApi(api, actions, selectors) {
|
||||
const getState = () => api.getState();
|
||||
const dispatch = (action) => api.dispatch(action);
|
||||
Object.assign(getState, mapValues(selectors, (selector) => {
|
||||
return (...args) => {
|
||||
let result = selector(api.getState());
|
||||
if (typeof result === 'function')
|
||||
return result(...args);
|
||||
return result;
|
||||
};
|
||||
}));
|
||||
Object.assign(dispatch, mapValues(actions, (action) => {
|
||||
return (...args) => api.dispatch(action(...args));
|
||||
}));
|
||||
return Object.assign(Object.assign({}, api), { getState,
|
||||
dispatch,
|
||||
actions,
|
||||
selectors });
|
||||
}
|
||||
export const effectToMiddleware = (effect, actions, selectors) => {
|
||||
let mw = effect;
|
||||
let action = '*';
|
||||
if (Array.isArray(effect)) {
|
||||
action = effect[0];
|
||||
mw = effect[1];
|
||||
mw = middlewareFor(action, mw);
|
||||
}
|
||||
return (api) => mw(augmentMiddlewareApi(api, actions, selectors));
|
||||
};
|
||||
const composeMw = (mws) => (api) => (original_next) => mws.reduceRight((next, mw) => mw(api)(next), original_next);
|
||||
export function buildMiddleware(effects = [], actions = {}, selectors = {}, sub = {}, wrapper = undefined, dux = undefined) {
|
||||
let inner = map(sub, ({ middleware }, slice) => slice !== '*' && middleware ? sliceMw(slice, middleware) : undefined).filter((x) => x);
|
||||
const local = effects.map((effect) => effectToMiddleware(effect, actions, selectors));
|
||||
let mws = [...local, ...inner];
|
||||
if (wrapper)
|
||||
mws = wrapper(mws, dux);
|
||||
return composeMw(mws);
|
||||
}
|
20
src/buildSelectors/index.js
Normal file
20
src/buildSelectors/index.js
Normal file
@ -0,0 +1,20 @@
|
||||
import { map, mapValues, merge } from 'lodash-es';
|
||||
export function buildSelectors(localSelectors, splatSelector = {}, subduxes = {}) {
|
||||
const subSelectors = map(subduxes, ({ selectors }, slice) => {
|
||||
if (!selectors)
|
||||
return {};
|
||||
if (slice === '*')
|
||||
return {};
|
||||
return mapValues(selectors, (func) => (state) => func(state[slice]));
|
||||
});
|
||||
let splat = {};
|
||||
for (const name in splatSelector) {
|
||||
splat[name] =
|
||||
(state) => (...args) => {
|
||||
const value = splatSelector[name](state)(...args);
|
||||
const res = () => value;
|
||||
return merge(res, mapValues(subduxes['*'].selectors, (selector) => () => selector(value)));
|
||||
};
|
||||
}
|
||||
return merge({}, ...subSelectors, localSelectors, splat);
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import Updux from './Updux';
|
||||
import Updux from './Updux.js';
|
||||
|
||||
export { withPayload, createAction } from './actions';
|
||||
export { withPayload, createAction } from './actions.js';
|
||||
|
||||
export default Updux;
|
||||
|
21
src/types.ts
Normal file
21
src/types.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { Action, ActionCreator } from 'redux';
|
||||
|
||||
export type Dux<
|
||||
STATE = any,
|
||||
ACTIONS extends Record<string, ActionCreator<string>> = {},
|
||||
> = Partial<{
|
||||
initial: STATE;
|
||||
actions: ACTIONS;
|
||||
}>;
|
||||
|
||||
type ActionsOf<DUX> = DUX extends { actions: infer A } ? A : {};
|
||||
|
||||
export type AggregateActions<A, S> = UnionToIntersection<
|
||||
ActionsOf<S[keyof S]> | A
|
||||
>;
|
||||
|
||||
export type UnionToIntersection<U> = (
|
||||
U extends any ? (k: U) => void : never
|
||||
) extends (k: infer I) => void
|
||||
? I
|
||||
: never;
|
@ -25,9 +25,9 @@
|
||||
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
|
||||
|
||||
/* Modules */
|
||||
"module": "ES2020" /* Specify what module code is generated. */,
|
||||
// "rootDir": "./", /* Specify the root folder within your source files. */
|
||||
"moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */,
|
||||
"module": "nodenext" /* Specify what module code is generated. */,
|
||||
"rootDir": "./src" /* Specify the root folder within your source files. */,
|
||||
"moduleResolution": "nodenext" /* Specify how TypeScript looks up a file from a given module specifier. */,
|
||||
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||
@ -51,7 +51,7 @@
|
||||
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
||||
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
||||
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
|
||||
// "outDir": "./", /* Specify an output folder for all emitted files. */
|
||||
"outDir": "./dist" /* Specify an output folder for all emitted files. */,
|
||||
// "removeComments": true, /* Disable emitting comments. */
|
||||
// "noEmit": true, /* Disable emitting files from a compilation. */
|
||||
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||
@ -78,7 +78,7 @@
|
||||
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
|
||||
|
||||
/* Type Checking */
|
||||
"strict": true /* Enable all strict type-checking options. */,
|
||||
"strict": false /* Enable all strict type-checking options. */,
|
||||
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
|
||||
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
|
||||
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||
|
Loading…
Reference in New Issue
Block a user