Merge branch 'generics' into return-to-ts

typescript
Yanick Champoux 2021-10-18 10:18:09 -04:00
commit 8314ff94ca
24 changed files with 222 additions and 168 deletions

View File

@ -1,2 +1,7 @@
dist
out
pnpm-lock.yaml
types
docs
Changes
.prettierignore

View File

@ -1,16 +1,16 @@
language: node_js
node_js:
- 'node'
- 'lts/*'
- 'node'
- 'lts/*'
install:
- npm uninstall typescript --no-save
- npm install
- npm uninstall typescript --no-save
- npm install
cache:
directories:
- node_modules
directories:
- node_modules
git:
depth: 1
depth: 1

View File

@ -6,54 +6,58 @@ All notable changes to this project will be documented in this file. See [standa
### ⚠ BREAKING CHANGES
* Upgrade to Typescript 4.
* Switch from 'updeep' to '@yanick/updeep'.
- Upgrade to Typescript 4.
- Switch from 'updeep' to '@yanick/updeep'.
## [2.1.0](https://github.com/yanick/updux/compare/v2.0.0...v2.1.0) (2020-06-19)
### Features
* add support for subscriptions ([9c45ee7](https://github.com/yanick/updux/commit/9c45ee7efcb623defb9da5d01165fbad0e4424f9))
- add support for subscriptions ([9c45ee7](https://github.com/yanick/updux/commit/9c45ee7efcb623defb9da5d01165fbad0e4424f9))
## [2.0.0](https://github.com/yanick/updux/compare/v1.2.0...v2.0.0) (2020-06-13)
### ⚠ BREAKING CHANGES
* use ts-action for action creation
* middleware support refined
- use ts-action for action creation
- middleware support refined
### Features
* allow adding actionCreators via addAction() ([27ae46d](https://github.com/yanick/updux/commit/27ae46dbab289b27ea99aca149aaa3b7c90ee7d0))
* middleware support refined ([d90d721](https://github.com/yanick/updux/commit/d90d72148c2d4ba186a19650d961c64df5791c55))
* moving documentation to docsify ([fa55762](https://github.com/yanick/updux/commit/fa55762efcbd4db356150f6022fd62750adc27a9))
* use ts-action for action creation ([6349d72](https://github.com/yanick/updux/commit/6349d720b8aba4b443a7225d6a377c5c929a3021))
- allow adding actionCreators via addAction() ([27ae46d](https://github.com/yanick/updux/commit/27ae46dbab289b27ea99aca149aaa3b7c90ee7d0))
- middleware support refined ([d90d721](https://github.com/yanick/updux/commit/d90d72148c2d4ba186a19650d961c64df5791c55))
- moving documentation to docsify ([fa55762](https://github.com/yanick/updux/commit/fa55762efcbd4db356150f6022fd62750adc27a9))
- use ts-action for action creation ([6349d72](https://github.com/yanick/updux/commit/6349d720b8aba4b443a7225d6a377c5c929a3021))
### Bug Fixes
* state is a PreloadedState<S> ([93bebc5](https://github.com/yanick/updux/commit/93bebc5acf193752aa6b4857507f05d52b1b7665))
- state is a PreloadedState<S> ([93bebc5](https://github.com/yanick/updux/commit/93bebc5acf193752aa6b4857507f05d52b1b7665))
## 1.2.0 2019-11-06
- The middleware's 'getState' returns the local state of its updux,
instead of the root state. Plus we add `getRootState` to get
the root state.
## 1.1.0 2019-11-05
- Document mapping behavior of the '*' subdux.
- add subduxUpreducer.
- add sink mutations.
## 1.0.0 2019-11-04
- Pretty big rework.
- Better documentation.
## 0.2.0 2019-10-24
- Converted everything to Typescript.
## 0.1.0 2019-10-22
- Add 'actions' in the config.
## 0.0.1 2019-10-22
- Initial release.

View File

@ -17,24 +17,24 @@ diverse, inclusive, and healthy community.
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
- Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
- The use of sexualized language or imagery, and sexual attention or
advances of any kind
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email
address, without their explicit permission
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
@ -106,7 +106,7 @@ Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
@ -126,4 +126,3 @@ enforcement ladder](https://github.com/mozilla/diversity).
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.

View File

@ -3,9 +3,23 @@
version: '3'
vars:
GREETING: Hello, World!
GREETING: Hello, World!
tasks:
tsc:
cmds:
- tsc
- echo 🙌 typescript compilation successful
test: jest
'check:prettier': prettier --check .
'check:prettier:fix': prettier --write .
check:
deps: [tsc, test, 'check:prettier']
'test:types': tsd
docs:
cmds:

View File

@ -1,6 +1,6 @@
module.exports = {
presets: [
['@babel/preset-env', {targets: {node: 'current'}}],
'@babel/preset-typescript',
],
presets: [
['@babel/preset-env', { targets: { node: 'current' } }],
'@babel/preset-typescript',
],
};

View File

@ -1,3 +1,3 @@
export default {
roots: [ './src' ]
}
roots: ['./src'],
};

View File

@ -8,7 +8,7 @@
"sourceType": "module",
"tags": {
"allowUnknownTags": true,
"dictionaries": ["jsdoc","closure"]
"dictionaries": ["jsdoc", "closure"]
},
"templates": {
"cleverLinks": false,
@ -19,4 +19,3 @@
"tutorials": "./tutorials"
}
}

16
src/Updux.d.ts vendored
View File

@ -1,19 +1,17 @@
type UpduxConfig<TState> = Partial<{
initial: TState;
subduxes: Record<string,any>;
subduxes: Record<string, any>;
actions: Record<string, any>;
selectors: Record<string, Function>;
mutations: Record<string, Function>;
mappedSelectors: Record<string,Function>;
effects: Record<string,Function>;
reactions: Record<string,Function>;
mappedSelectors: Record<string, Function>;
effects: Record<string, Function>;
reactions: Record<string, Function>;
mappedReaction: Function;
}>
}>;
export class Updux<TState=unknown> {
constructor( config: UpduxConfig<TState> );
export class Updux<TState = unknown> {
constructor(config: UpduxConfig<TState>);
get initial(): TState;
get selectors(): unknown;

View File

@ -9,17 +9,19 @@ import { buildActions } from './buildActions';
import { buildSelectors } from './buildSelectors';
import { action } from './actions';
import { buildUpreducer } from './buildUpreducer';
import {
buildMiddleware,
augmentMiddlewareApi,
} from './buildMiddleware';
import { buildMiddleware, augmentMiddlewareApi } from './buildMiddleware';
import { Dict } from './types';
import { AggregateDuxActions, AggregateDuxState, Dict } from './types';
/**
* Configuration object typically passed to the constructor of the class Updux.
*/
export interface UpduxConfig<TState = unknown> {
export interface UpduxConfig<
TState = any,
TActions = {},
TSelectors = {},
TSubduxes = {}
> {
/**
* Local initial state.
* @default {}
@ -29,12 +31,12 @@ export interface UpduxConfig<TState = unknown> {
/**
* Subduxes to be merged to this dux.
*/
subduxes?: Dict<Updux | UpduxConfig>;
subduxes?: TSubduxes;
/**
* Local actions.
*/
actions?: Record<string, any>;
actions?: TActions;
/**
* Local selectors.
@ -70,7 +72,12 @@ export interface UpduxConfig<TState = unknown> {
mappedReaction?: Function | boolean;
}
export class Updux {
export class Updux<
TState extends any = {},
TActions extends object = {},
TSelectors = {},
TSubduxes extends object = {}
> {
/** @type { unknown } */
#initial = {};
#subduxes = {};
@ -84,7 +91,7 @@ export class Updux {
#mappedSelectors = undefined;
#mappedReaction = undefined;
constructor(config: UpduxConfig) {
constructor(config: UpduxConfig<TState, TActions, TSelectors, TSubduxes>) {
this.#initial = config.initial ?? {};
this.#subduxes = config.subduxes ?? {};
@ -135,7 +142,7 @@ export class Updux {
this.#mappedSelectors = {
...this.#mappedSelectors,
[name]: f,
}
};
}
get middleware() {
@ -148,12 +155,12 @@ export class Updux {
}
/** @member { unknown } */
get initial() {
get initial(): AggregateDuxState<TState, TSubduxes> {
return this.#memoInitial(this.#initial, this.#subduxes);
}
get actions(): Record<string, Function> {
return this.#memoActions(this.#actions, this.#subduxes);
get actions(): AggregateDuxActions<TActions, TSubduxes> {
return this.#memoActions(this.#actions, this.#subduxes) as any;
}
get selectors() {
@ -323,14 +330,15 @@ export class Updux {
}
createStore(initial?: unknown, enhancerGenerator?: Function) {
const enhancer = (enhancerGenerator ?? applyMiddleware)(
this.middleware
);
const enhancer = (enhancerGenerator ?? applyMiddleware)(this.middleware);
const store : {
getState: Function & Record<string,Function>,
dispatch: Function & Record<string,Function>,
selectors: Record<string,Function>,
actions: Record<string,Function>,
const store: {
getState: Function & Record<string, Function>;
dispatch: Function & Record<string, Function>;
selectors: Record<string, Function>;
actions: AggregateDuxActions<TActions, TSubduxes>;
} = reduxCreateStore(
this.reducer,
initial ?? this.initial,

View File

@ -1,13 +1,13 @@
import { action } from './actions.js';
import { action } from './actions';
test('action generators', () => {
const foo = action('foo');
expect(foo.type).toEqual( 'foo');
expect(foo()).toMatchObject( { type: 'foo' });
expect(foo.type).toEqual('foo');
expect(foo()).toMatchObject({ type: 'foo' });
const bar = action('bar');
expect(bar.type).toEqual( 'bar');
expect(bar()).toMatchObject( { type: 'bar' });
expect(bar.type).toEqual('bar');
expect(bar()).toMatchObject({ type: 'bar' });
});

View File

@ -1,11 +1,16 @@
export type Action<T extends string=string,TPayload=unknown> = {
type: T; meta?: Record<string,unknown>; } & (
{ payload?: TPayload }
)
export type Action<T extends string = string, TPayload = unknown> = {
type: T;
meta?: Record<string, unknown>;
} & {
payload?: TPayload;
};
export type ActionGenerator<TType extends string = string, TPayloadGen = undefined> = {
export type ActionGenerator<
TType extends string = string,
TPayloadGen = undefined
> = {
type: TType;
} & (TPayloadGen extends (...args:any) => any
} & (TPayloadGen extends (...args: any) => any
? (...args: Parameters<TPayloadGen>) => {
type: TType;
payload: ReturnType<TPayloadGen>;
@ -22,7 +27,7 @@ export type ActionGenerator<TType extends string = string, TPayloadGen = undefin
export function action(type, payloadFunction = null) {
const generator = function (...payloadArg) {
const result :Action = { type };
const result: Action = { type };
if (payloadFunction) {
result.payload = payloadFunction(...payloadArg);

View File

@ -1,9 +1,7 @@
import { buildInitial } from '.';
test('basic', () => {
expect(
buildInitial({ a: 1 }, { b: { initial: { c: 2 } } })
).toMatchObject({
expect(buildInitial({ a: 1 }, { b: { initial: { c: 2 } } })).toMatchObject({
a: 1,
b: { c: 2 },
});

View File

@ -1,16 +1,23 @@
import { map, mapValues, merge } from 'lodash';
export function buildSelectors(localSelectors, splatSelector = {}, subduxes ={}) {
export function buildSelectors(
localSelectors,
splatSelector = {},
subduxes = {}
) {
const subSelectors = map(subduxes, ({ selectors }, slice) => {
if (!selectors) return {};
if (slice === '*') return {};
return mapValues(selectors, (func: Function) => (state) => func(state[slice]));
return mapValues(
selectors,
(func: Function) => (state) => func(state[slice])
);
});
let splat = {};
for ( const name in splatSelector ) {
for (const name in splatSelector) {
splat[name] =
(state) =>
(...args) => {
@ -25,7 +32,7 @@ export function buildSelectors(localSelectors, splatSelector = {}, subduxes ={})
)
);
};
}
}
return merge({}, ...subSelectors, localSelectors, splat);
}

View File

@ -8,7 +8,8 @@ export function buildUpreducer(initial, mutations, subduxes = {}) {
: null;
return (action) => (state) => {
if( !action?.type ) throw new Error("upreducer called with a bad action");
if (!action?.type)
throw new Error('upreducer called with a bad action');
let newState = state ?? initial;

View File

@ -32,7 +32,7 @@ test('README.md', () => {
});
test('tutorial', () => {
const todosDux = new Updux({
const todosDux = new Updux<any, any>({
initial: {
next_id: 1,
todos: [],

View File

@ -14,9 +14,11 @@ test('basic', () => {
},
});
expect(dux.reducer(undefined, dux.actions.doIt())).toEqual( 'bingo');
expect(dux.reducer(undefined, dux.actions.doIt())).toEqual('bingo');
expect(dux.reducer(undefined, dux.actions.thisToo())).toEqual( 'straight type');
expect(dux.reducer(undefined, dux.actions.thisToo())).toEqual(
'straight type'
);
});
test('override', () => {
@ -48,8 +50,7 @@ test('override', () => {
undefined
);
expect(state).toMatchObject(
{
expect(state).toMatchObject({
alpha: ['foo', 'bar'],
subbie: 1,
});
@ -77,8 +78,9 @@ test('order of processing', () => {
},
});
expect(dux.reducer(undefined, foo()))
.toMatchObject({ x: ['subdux', 'main'] });
expect(dux.reducer(undefined, foo())).toMatchObject({
x: ['subdux', 'main'],
});
});
test('setMutation', () => {
@ -89,21 +91,20 @@ test('setMutation', () => {
});
// noop
expect(dux.reducer(undefined, foo())).toEqual( '');
expect(dux.reducer(undefined, foo())).toEqual('');
dux.setMutation('foo', () => () => 'foo');
expect(dux.reducer(undefined, foo())).toEqual( 'foo');
expect(dux.reducer(undefined, foo())).toEqual('foo');
});
test('setMutation, name as function', () => {
const bar = action('bar');
const bar = action('bar');
const dux = new Updux({
initial: '',
});
dux.setMutation(bar, () => () => 'bar');
expect(dux.reducer(undefined, bar())).toEqual( 'bar');
const dux = new Updux({
initial: '',
});
dux.setMutation(bar, () => () => 'bar');
expect(dux.reducer(undefined, bar())).toEqual('bar');
});

View File

@ -7,7 +7,7 @@ test('basic reducer', () => {
expect(typeof dux.reducer).toBe('function');
expect(dux.reducer({ a: 1 }, { type: 'foo' })).toMatchObject({a:1}); // noop
expect(dux.reducer({ a: 1 }, { type: 'foo' })).toMatchObject({ a: 1 }); // noop
});
test('basic upreducer', () => {
@ -15,7 +15,7 @@ test('basic upreducer', () => {
expect(typeof dux.upreducer).toBe('function');
expect(dux.upreducer({type:'foo'})({ a: 1 })).toMatchObject({a:1}); // noop
expect(dux.upreducer({ type: 'foo' })({ a: 1 })).toMatchObject({ a: 1 }); // noop
});
test('reducer with action', () => {
@ -28,5 +28,5 @@ test('reducer with action', () => {
},
});
expect(dux.reducer({ a: 1 }, { type: 'inc' })).toMatchObject({a:2});
expect(dux.reducer({ a: 1 }, { type: 'inc' })).toMatchObject({ a: 2 });
});

View File

@ -135,8 +135,7 @@ test('splat subscriptions, more', () => {
initial: { a: 1 },
actions: { foo: null, incAll: null },
mutations: {
foo: (id) => (state) =>
state.a === id ? { ...state, b: 1 } : state,
foo: (id) => (state) => state.a === id ? { ...state, b: 1 } : state,
incAll: () => (state) => ({ ...state, a: state.a + 1 }),
},
reactions: [() => snitch],
@ -163,16 +162,8 @@ test('splat subscriptions, more', () => {
expect(snitch).toHaveBeenCalledTimes(2);
expect(snitch).toHaveBeenCalledWith(
{ a: 1 },
undefined,
expect.anything()
);
expect(snitch).toHaveBeenCalledWith(
{ a: 2 },
undefined,
expect.anything()
);
expect(snitch).toHaveBeenCalledWith({ a: 1 }, undefined, expect.anything());
expect(snitch).toHaveBeenCalledWith({ a: 2 }, undefined, expect.anything());
snitch.mockReset();
@ -194,11 +185,7 @@ test('splat subscriptions, more', () => {
expect(snitch).toHaveBeenCalledTimes(1);
expect(snitch).toHaveBeenCalledWith(
undefined,
{ a: 1 },
expect.anything()
);
expect(snitch).toHaveBeenCalledWith(undefined, { a: 1 }, expect.anything());
// only one subscriber left
snitch.mockReset();
@ -246,9 +233,7 @@ test('many levels down', () => {
mutations: {
add: () => (x) => x + 1,
},
reactions: [
(store) => (state) => snitch(state, store),
],
reactions: [(store) => (state) => snitch(state, store)],
},
},
},

View File

@ -109,8 +109,7 @@ test('subscription within subduxes', () => {
let innerState = jest.fn(() => null);
let outerState = jest.fn(() => null);
const resetMocks = () =>
[innerState, outerState].map((f) => f.mockReset());
const resetMocks = () => [innerState, outerState].map((f) => f.mockReset());
const inner = new Updux({
initial: 1,

View File

@ -1,2 +1,37 @@
export type Dict<T> = Record<string, T>;
export type UnionToIntersection<U> = (
U extends any ? (k: U) => void : never
) extends (k: infer I) => void
? I
: never;
type Subdux<TState = any> = {
initial: TState;
};
type StateOf<D> = D extends { initial: infer I } ? I : unknown;
type ActionsOf<C> = C extends { actions: infer A }
? {
[K in keyof A]: Function;
}
: {};
type Subduxes = Record<string, Subdux>;
export type DuxStateSubduxes<C> = C extends { '*': infer I }
? {
[key: string]: StateOf<I>;
[index: number]: StateOf<I>;
}
: { [K in keyof C]: StateOf<C[K]> };
export type AggregateDuxState<TState, TSubduxes> = TState &
DuxStateSubduxes<TSubduxes>;
type DuxActionsSubduxes<C> = C extends object ? ActionsOf<C[keyof C]> : unknown;
type ItemsOf<C> = C extends object ? C[keyof C] : unknown;
export type AggregateDuxActions<TActions, TSubduxes> = TActions &
UnionToIntersection<ActionsOf<ItemsOf<TSubduxes>>>;

View File

@ -1,19 +1,17 @@
{
"include": [ "./src" ],
"exclude": [ "./docs", "./dist" ],
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"target": "es2020",
"lib": [
"es2020"
],
"module": "ES2020",
"moduleResolution": "Node",
"strict": false,
"sourceMap": true,
"allowSyntheticDefaultImports": true,
"declaration": true,
"allowJs": true
}
"include": ["./src"],
"exclude": ["./docs", "./dist"],
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"target": "es2020",
"lib": ["es2020"],
"module": "ES2020",
"moduleResolution": "Node",
"strict": false,
"sourceMap": true,
"allowSyntheticDefaultImports": true,
"declaration": true,
"allowJs": true
}
}

View File

@ -1,15 +1,13 @@
{
"name": "Updux",
"entryPoints": [
"./types/index.d.ts"
],
"out": "docs/API",
"excludeExternals": true,
"excludePrivate": true,
"excludeProtected": true,
"disableSources": true,
"listInvalidSymbolLinks": true,
"markedOptions": {
"mangle": false
}
"name": "Updux",
"entryPoints": ["./types/index.d.ts"],
"out": "docs/API",
"excludeExternals": true,
"excludePrivate": true,
"excludeProtected": true,
"disableSources": true,
"listInvalidSymbolLinks": true,
"markedOptions": {
"mangle": false
}
}