diff --git a/Taskfile.yaml b/Taskfile.yaml index def9a12..677cab8 100644 --- a/Taskfile.yaml +++ b/Taskfile.yaml @@ -3,6 +3,11 @@ version: '3' tasks: + build: tsc + + checks: + deps: [test, build] + test: vitest run src test:dev: vitest src diff --git a/foo.ts b/foo.ts deleted file mode 100644 index 589e79a..0000000 --- a/foo.ts +++ /dev/null @@ -1,6 +0,0 @@ -const foo = (x: Partial<{ a: A; b: B }>) => x as B; - -const y = foo({ - a: 'potato', - b: 2, -}); diff --git a/src/Updux.ts b/src/Updux.ts index 59c1456..7aad5e3 100644 --- a/src/Updux.ts +++ b/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; @@ -20,6 +22,7 @@ type Mutation = ( export default class Updux< T_LocalState = Record, T_LocalActions = {}, + SUBDUXES extends Record = {}, > { #localInitial: T_LocalState; #localActions: T_LocalActions; @@ -27,16 +30,28 @@ export default class Updux< string, Mutation> > = {}; + #subduxes: SUBDUXES; + + #actions: Record; 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 { + 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; diff --git a/src/actions.test.todo b/src/actions.test.todo index eb6e22f..c442f36 100644 --- a/src/actions.test.todo +++ b/src/actions.test.todo @@ -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( diff --git a/src/actions.test.ts b/src/actions.test.ts new file mode 100644 index 0000000..76cdb07 --- /dev/null +++ b/src/actions.test.ts @@ -0,0 +1,27 @@ +import Updux, { createAction } from './index.js'; + +test('subduxes actions', () => { + const bar = createAction('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'); +}); diff --git a/src/actions.ts b/src/actions.ts index f19ced8..d36171b 100644 --- a/src/actions.ts +++ b/src/actions.ts @@ -1,5 +1,7 @@ +import { createAction } from '@reduxjs/toolkit'; + export { createAction } from '@reduxjs/toolkit'; export const withPayload =

() => - (payload: P) => ({ payload }); + (payload: P) => ({ payload }); diff --git a/src/buildMiddleware/index.js b/src/buildMiddleware/index.js new file mode 100644 index 0000000..2085412 --- /dev/null +++ b/src/buildMiddleware/index.js @@ -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); +} diff --git a/src/buildMiddleware/index.ts b/src/buildMiddleware/index.ts.todo similarity index 100% rename from src/buildMiddleware/index.ts rename to src/buildMiddleware/index.ts.todo diff --git a/src/buildSelectors/index.js b/src/buildSelectors/index.js new file mode 100644 index 0000000..dfd7550 --- /dev/null +++ b/src/buildSelectors/index.js @@ -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); +} diff --git a/src/buildSelectors/index.ts b/src/buildSelectors/index.ts.todo similarity index 100% rename from src/buildSelectors/index.ts rename to src/buildSelectors/index.ts.todo diff --git a/src/index.ts b/src/index.ts index e542a2f..6488bac 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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; diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..3ef2a48 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,21 @@ +import { Action, ActionCreator } from 'redux'; + +export type Dux< + STATE = any, + ACTIONS extends Record> = {}, +> = Partial<{ + initial: STATE; + actions: ACTIONS; +}>; + +type ActionsOf = DUX extends { actions: infer A } ? A : {}; + +export type AggregateActions = UnionToIntersection< + ActionsOf | A +>; + +export type UnionToIntersection = ( + U extends any ? (k: U) => void : never +) extends (k: infer I) => void + ? I + : never; diff --git a/tsconfig.json b/tsconfig.json index d62408e..4baa5e4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -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. */ diff --git a/vitest.config.ts b/vitest.config.js similarity index 100% rename from vitest.config.ts rename to vitest.config.js