Compare commits
189 Commits
v0.0.1
...
typescript
Author | SHA1 | Date |
---|---|---|
Yanick Champoux | ace7368b1b | |
Yanick Champoux | 624a60cb7f | |
Yanick Champoux | 82f5d53df2 | |
Yanick Champoux | f206026087 | |
Yanick Champoux | 7f1bcbddef | |
Yanick Champoux | 0b0ea7ea66 | |
Yanick Champoux | dd0dda0970 | |
Yanick Champoux | 23724931e9 | |
Yanick Champoux | 13c9603251 | |
Yanick Champoux | 82e8ba7385 | |
Yanick Champoux | 27e27aa70d | |
Yanick Champoux | 1255876c38 | |
Yanick Champoux | 6b508c9db2 | |
Yanick Champoux | 16397ce3ee | |
Yanick Champoux | fbe624a14c | |
Yanick Champoux | 702a3e76d4 | |
Yanick Champoux | 0f3aeff396 | |
Yanick Champoux | 3e6d2dae55 | |
Yanick Champoux | 51421c1052 | |
Yanick Champoux | 63d8235346 | |
Yanick Champoux | 141d959d28 | |
Yanick Champoux | c5ca566c12 | |
Yanick Champoux | 7583f9e98b | |
Yanick Champoux | e3dce45f50 | |
Yanick Champoux | 80a356ff1a | |
Yanick Champoux | af6db3403c | |
Yanick Champoux | c8bb1e5096 | |
Yanick Champoux | 9255548326 | |
Yanick Champoux | b4a96b67ea | |
Yanick Champoux | fa3300004d | |
Yanick Champoux | 378d61337f | |
Yanick Champoux | d12d114af8 | |
Yanick Champoux | 36b19316ba | |
Yanick Champoux | 6a4525f507 | |
Yanick Champoux | d44986e990 | |
Yanick Champoux | 34acc63bef | |
Yanick Champoux | 43702fe096 | |
Yanick Champoux | 3bb9221c27 | |
Yanick Champoux | 6cb7f14407 | |
Yanick Champoux | 6a6bb1636a | |
Yanick Champoux | 7494fe553e | |
Yanick Champoux | 325ad47731 | |
Yanick Champoux | b96f5e72ac | |
Yanick Champoux | fa837a345e | |
Yanick Champoux | e0ffaef07a | |
Yanick Champoux | c609a19ef8 | |
Yanick Champoux | 46b0565819 | |
Yanick Champoux | 3c78f34d5b | |
Yanick Champoux | 9772ce4bec | |
Yanick Champoux | 4d4a5dde50 | |
Yanick Champoux | 64782096df | |
Yanick Champoux | d20f50e156 | |
Yanick Champoux | 38c91d435f | |
Yanick Champoux | f2468e3b82 | |
Yanick Champoux | ef5751eb69 | |
Yanick Champoux | 3aac2e092e | |
Yanick Champoux | bb5218d2f6 | |
Yanick Champoux | ce666b75bb | |
Yanick Champoux | a272ee0329 | |
Yanick Champoux | f51c8158fc | |
Yanick Champoux | 541790425a | |
Yanick Champoux | f1b7677f0f | |
Yanick Champoux | 8314ff94ca | |
Yanick Champoux | 88d62536ad | |
Yanick Champoux | 707d9ec923 | |
Yanick Champoux | 4e4fa13d90 | |
Yanick Champoux | 27958a6d14 | |
Yanick Champoux | 85e478c02f | |
Yanick Champoux | a8f97940e8 | |
Yanick Champoux | 55812ed6d8 | |
Yanick Champoux | 7cd7e27805 | |
Yanick Champoux | c5a9a7397a | |
Yanick Champoux | 69a8781b4b | |
Yanick Champoux | a657813415 | |
Yanick Champoux | 440c76d408 | |
Yanick Champoux | 3394a00419 | |
Yanick Champoux | 83f28a1720 | |
Yanick Champoux | 6fb14a26d0 | |
Yanick Champoux | 33ded1448e | |
Yanick Champoux | 8c026314f5 | |
Yanick Champoux | d75da07d3f | |
Yanick Champoux | 6dd8b1af9e | |
Yanick Champoux | 9778e04a8d | |
Yanick Champoux | 2e357b71e2 | |
Yanick Champoux | a5e8410768 | |
Yanick Champoux | d14eb08bf0 | |
Yanick Champoux | 912ea85edc | |
Yanick Champoux | c1c1edf588 | |
Yanick Champoux | 5eeb4d4ab7 | |
Yanick Champoux | d61c9478a2 | |
Yanick Champoux | 5247aa2255 | |
Yanick Champoux | 04159ec7cc | |
Yanick Champoux | 2c26f9652b | |
Yanick Champoux | e4eff8a113 | |
Yanick Champoux | b7ada06e3c | |
Yanick Champoux | 0ecb1059ee | |
Yanick Champoux | 1759ce16c6 | |
Yanick Champoux | 1a653fac5b | |
Yanick Champoux | 0870767994 | |
Yanick Champoux | fd064f5996 | |
Yanick Champoux | 003a1309bd | |
Yanick Champoux | c96a6ea07c | |
Yanick Champoux | 1f62028e4e | |
Yanick Champoux | 74c29ce2d0 | |
Yanick Champoux | 99ba091a51 | |
Yanick Champoux | a39715f0a8 | |
Yanick Champoux | 7eab50ff60 | |
Yanick Champoux | 0ebf32c71b | |
Yanick Champoux | afc1a14f56 | |
Yanick Champoux | 9c45ee7efc | |
Yanick Champoux | 86dd272603 | |
Yanick Champoux | f0c1d6f015 | |
Yanick Champoux | 38199a5ec7 | |
Yanick Champoux | 97f21aac2a | |
Trey Bianchini | 84a9c4f77f | |
Yanick Champoux | 26bf0c962a | |
Trey Bianchini | f8edbcaa49 | |
Yanick Champoux | f0653442f3 | |
Yanick Champoux | e3c5aad399 | |
Yanick Champoux | dce52c56d0 | |
Yanick Champoux | 6349d720b8 | |
Yanick Champoux | f6b7c2b15a | |
Yanick Champoux | a71d3f8781 | |
Yanick Champoux | b7339c25e5 | |
Yanick Champoux | 4665ee39f7 | |
Yanick Champoux | 91923bfd49 | |
Yanick Champoux | 64a36c4088 | |
Yanick Champoux | e7fbd582fc | |
Yanick Champoux | 4c0d7b366f | |
Yanick Champoux | 931377b584 | |
Yanick Champoux | f744616cb2 | |
Yanick Champoux | 1825e8d269 | |
Yanick Champoux | 499e987219 | |
Yanick Champoux | 27ae46dbab | |
Yanick Champoux | 7ddc187f2b | |
Yanick Champoux | c8f497f5e9 | |
Yanick Champoux | 1388282d81 | |
Yanick Champoux | 4acfe5b17e | |
Yanick Champoux | 0d0890ffff | |
Yanick Champoux | fa55762efc | |
Yanick Champoux | 93bebc5acf | |
Yanick Champoux | fe34d01a41 | |
Yanick Champoux | 54ad7f2512 | |
Yanick Champoux | d90d72148c | |
Yanick Champoux | 73c2776826 | |
Yanick Champoux | 5465c53cb5 | |
Yanick Champoux | e78568b3cb | |
Yanick Champoux | 057226dfd1 | |
Yanick Champoux | 5c5e614f4b | |
Yanick Champoux | 8d4542fffa | |
Yanick Champoux | 9b5a7981d5 | |
Yanick Champoux | 74d29d3126 | |
Yanick Champoux | 1379055264 | |
Yanick Champoux | da7ac2ef6e | |
Yanick Champoux | 47b78f43cc | |
Yanick Champoux | 0d298cb285 | |
Yanick Champoux | 15e154b6ec | |
Yanick Champoux | 0e03628502 | |
Yanick Champoux | 73973a9588 | |
Yanick Champoux | 82373634ed | |
Yanick Champoux | 122ff71260 | |
Yanick Champoux | 1642bab09a | |
Yanick Champoux | a6b83bbfa0 | |
Yanick Champoux | fb61c9bc1e | |
Yanick Champoux | 87d8a4de38 | |
Yanick Champoux | 23312d2f15 | |
Yanick Champoux | 52649b6b07 | |
Yanick Champoux | ed50c9dab9 | |
Yanick Champoux | 6c89964a1c | |
Yanick Champoux | 3fd4d8961e | |
Yanick Champoux | 9f66f7e494 | |
Yanick Champoux | 44a897ac5a | |
Yanick Champoux | 366bd91cf6 | |
Yanick Champoux | e63f844bdc | |
Yanick Champoux | 07eaf93e48 | |
Yanick Champoux | f0e3b15fa4 | |
Yanick Champoux | 76a75c9120 | |
Yanick Champoux | 0949200bac | |
Yanick Champoux | 125800a8b9 | |
Yanick Champoux | 4754fb2377 | |
Yanick Champoux | beae30d091 | |
Yanick Champoux | ca89c53c0b | |
Yanick Champoux | d9bac9dd7d | |
Yanick Champoux | f092526ded | |
Yanick Champoux | 11ff89501d | |
Yanick Champoux | 199ab2ba31 | |
Yanick Champoux | 3486b71acc | |
Yanick Champoux | c83443c95f | |
Yanick Champoux | 31c6060041 |
|
@ -0,0 +1 @@
|
|||
out/
|
|
@ -0,0 +1,26 @@
|
|||
module.exports = {
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
},
|
||||
env: {
|
||||
es6: true,
|
||||
browser: true,
|
||||
},
|
||||
plugins: ['todo-plz', 'no-only-tests'],
|
||||
overrides: [],
|
||||
rules: {
|
||||
'no-console': ['error'],
|
||||
'todo-plz/ticket-ref': ['error', { pattern: 'GT[0-9]+' }],
|
||||
'no-only-tests/no-only-tests': [
|
||||
'error',
|
||||
{
|
||||
block: ['test'],
|
||||
focus: ['only'],
|
||||
},
|
||||
],
|
||||
},
|
||||
settings: {
|
||||
// ...
|
||||
},
|
||||
};
|
|
@ -1,2 +1,11 @@
|
|||
node_modules/
|
||||
tsconfig.tsbuildinfo
|
||||
**/*.orig
|
||||
dist
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
.nyc_output/
|
||||
pnpm-debug.log
|
||||
yarn-error.log
|
||||
GPUCache/
|
||||
updux-2.0.0.tgz
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
*.test.*
|
||||
test.*
|
||||
docs
|
||||
node_modules/
|
||||
tsconfig.tsbuildinfo
|
||||
**/*.orig
|
||||
dist
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
.nyc_output/
|
||||
pnpm-debug.log
|
||||
pnpm-lock.yaml
|
||||
yarn-error.log
|
||||
GPUCache/
|
||||
updux-2.0.0.tgz
|
||||
.travis.yml
|
||||
.prettierignore
|
||||
tools/gen_sidebar.pl
|
|
@ -0,0 +1,22 @@
|
|||
exclude: static/fontawesome|^build
|
||||
|
||||
default_stages:
|
||||
- merge-commit
|
||||
- push
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.3.0
|
||||
hooks:
|
||||
- id: end-of-file-fixer
|
||||
stages: [merge-commit, push]
|
||||
- id: trailing-whitespace
|
||||
stages: [merge-commit, push]
|
||||
- id: check-merge-conflict
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: lint
|
||||
name: lint
|
||||
entry: task lint:fix --
|
||||
language: system
|
||||
files: ''
|
|
@ -0,0 +1,7 @@
|
|||
dist
|
||||
out
|
||||
pnpm-lock.yaml
|
||||
types
|
||||
docs
|
||||
Changes
|
||||
.prettierignore
|
|
@ -0,0 +1,9 @@
|
|||
module.exports = {
|
||||
semi: true,
|
||||
trailingComma: 'all',
|
||||
singleQuote: true,
|
||||
printWidth: 80,
|
||||
tabWidth: 4,
|
||||
useTabs: false,
|
||||
plugins: [],
|
||||
};
|
|
@ -0,0 +1,16 @@
|
|||
language: node_js
|
||||
|
||||
node_js:
|
||||
- 'node'
|
||||
- 'lts/*'
|
||||
|
||||
install:
|
||||
- npm uninstall typescript --no-save
|
||||
- npm install
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- node_modules
|
||||
|
||||
git:
|
||||
depth: 1
|
|
@ -0,0 +1,69 @@
|
|||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||
|
||||
## 4.0.0 (2022-08-30)
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
- Return to a JavaScript core (sorry Typescript).
|
||||
|
||||
## 3.0.0 (2021-05-02)
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
- 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))
|
||||
|
||||
## [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
|
||||
|
||||
### 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))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 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.
|
|
@ -0,0 +1,128 @@
|
|||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
[yanick@babyl.ca](mailto://yanick@babyl.ca).
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
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
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
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.
|
420
README.md
420
README.md
|
@ -1,23 +1,37 @@
|
|||
|
||||
# What's Updux?
|
||||
|
||||
So, I'm a fan of [Redux][]. Two days ago I discovered
|
||||
[rematch](https://rematch.github.io/rematch) alonside a few other frameworks built atop Redux.
|
||||
So, I'm a fan of [Redux](https://redux.js.org). Two days ago I discovered
|
||||
[rematch](https://rematch.github.io/rematch) alonside a few other frameworks built atop Redux.
|
||||
|
||||
It has a couple of pretty good ideas that removes some of the
|
||||
boilerplate. Keeping mutations and asynchronous effects close to the
|
||||
reducer definition, à la [VueX][]? Nice. Automatically infering the
|
||||
It has a couple of pretty good ideas that removes some of the
|
||||
boilerplate. Keeping mutations and asynchronous effects close to the
|
||||
reducer definition? Nice. Automatically infering the
|
||||
actions from the said mutations and effects? Genius!
|
||||
|
||||
But it also enforces a flat hierarchy of reducers -- where
|
||||
is the fun in that? And I'm also having a strong love for
|
||||
[Updeep][], so I want reducer state updates to leverage the heck out of it.
|
||||
[Updeep](https://github.com/substantial/updeep), so I want reducer state updates to leverage the heck out of it.
|
||||
|
||||
All that to say, I had some fun yesterday and hacked a proto-lovechild
|
||||
of `Rematch` and `Updeep`, with a dash of [VueX][] inspiration.
|
||||
I call it... `Updux`.
|
||||
All that to say, say hello to `Updux`. Heavily inspired by `rematch`, but twisted
|
||||
to work with `updeep` and to fit my peculiar needs. It offers features such as
|
||||
|
||||
- Mimic the way VueX has mutations (reducer reactions to specific actions) and
|
||||
effects (middleware reacting to actions that can be asynchronous and/or
|
||||
have side-effects), so everything pertaining to a store are all defined
|
||||
in the space place.
|
||||
- Automatically gather all actions used by the updux's effects and mutations,
|
||||
and makes then accessible as attributes to the `dispatch` object of the
|
||||
store.
|
||||
- Mutations have a signature that is friendly to Updux and Immer.
|
||||
- Also, the mutation signature auto-unwrap the payload of the actions for you.
|
||||
- TypeScript types.
|
||||
|
||||
Fair warning: this package is still very new, probably very buggy,
|
||||
definitively very badly documented, and very subject to changes. Caveat
|
||||
Maxima Emptor.
|
||||
|
||||
# Synopsis
|
||||
|
||||
```
|
||||
import updux from 'updux';
|
||||
|
||||
|
@ -29,7 +43,7 @@ const {
|
|||
actions,
|
||||
middleware,
|
||||
createStore,
|
||||
} = updux({
|
||||
} = new Updux({
|
||||
initial: {
|
||||
counter: 0,
|
||||
},
|
||||
|
@ -45,6 +59,12 @@ const {
|
|||
next(action);
|
||||
};
|
||||
},
|
||||
actions: {
|
||||
customAction: ( someArg ) => ({
|
||||
type: "custom",
|
||||
payload: { someProp: someArg }
|
||||
}),
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
|
@ -55,308 +75,152 @@ store.dispatch.inc(3);
|
|||
|
||||
# Description
|
||||
|
||||
`Updux` exports one function, `updux`, both as a named export and as
|
||||
its default export.
|
||||
Full documentation can be [found here](https://yanick.github.io/updux/).
|
||||
Right now the best way to understand the whole thing is to go
|
||||
through the [tutorial](https://yanick.github.io/updux/#/tutorial)
|
||||
|
||||
## helpers = updux(config);
|
||||
## Exporting upduxes
|
||||
|
||||
`updux` is a way to minimize and simplify the boilerplate associated with the
|
||||
creation of a `Redux` store. It takes a shorthand configuration
|
||||
object, and generates the appropriate reducer, actions, middleware, etc.
|
||||
In true `Redux`-like fashion, just like reducers can be composed
|
||||
of sub-reducers, upduxs can be made of sub-upduxs.
|
||||
|
||||
### config
|
||||
|
||||
The config object recognize following properties.
|
||||
|
||||
#### initial
|
||||
|
||||
The default initial state of the reducer. Can be anything your
|
||||
heart desires.
|
||||
|
||||
#### subduxes
|
||||
|
||||
Object mapping slices of the state to sub-upduxs.
|
||||
|
||||
For example, if in plain Redux you would do
|
||||
If you are creating upduxes that will be used as subduxes
|
||||
by other upduxes, or as
|
||||
[ducks](https://github.com/erikras/ducks-modular-redux)-like containers, I
|
||||
recommend that you export the Updux instance as the default export:
|
||||
|
||||
```
|
||||
import { combineReducers } from 'redux';
|
||||
import todosReducer from './todos';
|
||||
import statisticsReducer from './statistics';
|
||||
import Updux from 'updux';
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
todos: todosReducer,
|
||||
stats: statisticsReducer,
|
||||
});
|
||||
const updux = new Updux({ ... });
|
||||
|
||||
export default updux;
|
||||
```
|
||||
|
||||
then with Updux you'd do
|
||||
Then you can use them as subduxes like this:
|
||||
|
||||
```
|
||||
import { updux } from 'updux';
|
||||
import todos from './todos';
|
||||
import statistics from './statistics';
|
||||
import Updux from 'updux';
|
||||
import foo from './foo'; // foo is an Updux
|
||||
import bar from './bar'; // bar is an Updux as well
|
||||
|
||||
const rootUpdux = updux({
|
||||
const updux = new Updux({
|
||||
subduxes: {
|
||||
todos, statistics
|
||||
foo, bar
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
#### mutations
|
||||
|
||||
Object mapping actions to the associated state mutation.
|
||||
|
||||
For example, in `Redux` you'd do
|
||||
Or if you want to use it:
|
||||
|
||||
```
|
||||
function todosReducer(state=[],action) {
|
||||
import updux from './myUpdux';
|
||||
|
||||
switch(action.type) {
|
||||
case 'ADD': return [ ...state, action.payload ];
|
||||
|
||||
case 'DONE': return state.map( todo => todo.id === action.payload
|
||||
? { ...todo, done: true } : todo )
|
||||
|
||||
default: return state;
|
||||
}
|
||||
}
|
||||
const {
|
||||
reducer,
|
||||
actions: { doTheThing },
|
||||
createStore,
|
||||
middleware,
|
||||
} = updux;
|
||||
```
|
||||
|
||||
With Updux:
|
||||
## Mapping a mutation to all values of a state
|
||||
|
||||
Say you have a `todos` state that is an array of `todo` sub-states. It's easy
|
||||
enough to have the main reducer maps away all items to the sub-reducer:
|
||||
|
||||
```
|
||||
const todosUpdux = updux({
|
||||
const todo = new Updux({
|
||||
mutations: {
|
||||
add: todo => state => [ ...state, todo ],
|
||||
done: done_id => u.map( u.if( ({id} => id === done_id), {done: true} ) )
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
The signature of the mutations is `(payload,action) => state => newState`.
|
||||
It is designed to play well with `Updeep`. This way, instead of doing
|
||||
|
||||
```
|
||||
mutation: {
|
||||
renameTodo: newName => state => { ...state, name: newName }
|
||||
}
|
||||
```
|
||||
|
||||
we can do
|
||||
|
||||
```
|
||||
mutation: {
|
||||
renameTodo: newName => u({ name: newName })
|
||||
}
|
||||
```
|
||||
|
||||
Also, the special key `*` can be used to match any
|
||||
action not explicitly matched by other mutations.
|
||||
|
||||
```
|
||||
const todosUpdux = updux({
|
||||
mutations: {
|
||||
add: todo => state => [ ...state, todo ],
|
||||
done: done_id => u.map( u.if( ({id} => id === done_id), {done: true} ) ),
|
||||
'*' (payload,action) => state => {
|
||||
console.warn( "unexpected action ", action.type );
|
||||
return state;
|
||||
},
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
#### effects
|
||||
|
||||
Plain object defining asynchronous actions and side-effects triggered by actions.
|
||||
The effects themselves are Redux middleware, expect with the `dispatch`
|
||||
property of the first argument augmented with all the available actions.
|
||||
|
||||
```
|
||||
updux({
|
||||
effects: {
|
||||
fetch: ({dispatch}) => next => async (action) => {
|
||||
next(action);
|
||||
|
||||
let result = await fetch(action.payload.url).then( result => result.json() );
|
||||
dispatch.fetchSuccess(result);
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
## return value
|
||||
|
||||
`updux` returns an object with the following properties:
|
||||
|
||||
### initial
|
||||
|
||||
Default initial state of the reducer. If applicable, merge
|
||||
the initial states of `config` and `subduxes`, with
|
||||
`config` having precedence over `subduxes`.
|
||||
|
||||
If nothing was given, defaults to an empty object.
|
||||
|
||||
### reducer
|
||||
|
||||
A Redux reducer generated using the computed initial state and
|
||||
mutations.
|
||||
|
||||
### mutations
|
||||
|
||||
Merge of the config and subduxes mutations. If an action trigger
|
||||
mutations in both the main updux and its subduxes, the subduxes
|
||||
mutations will be performed first.
|
||||
|
||||
### actions
|
||||
|
||||
Action creators for all actions used in the mutations, effects and subdox
|
||||
of the updox config.
|
||||
|
||||
The action creators have the signature `(payload={},meta={}) => ({type,
|
||||
payload,meta})` (with the extra sugar that if `meta` or `payload` are not
|
||||
specified, the key is not present in the produced action).
|
||||
|
||||
### middleware
|
||||
|
||||
A middleware aggregating all the effects defined in the
|
||||
updox and its subduxes. Effects of the updox itself are
|
||||
done before the subdoxes effects.
|
||||
|
||||
### createStore
|
||||
|
||||
Same as doing
|
||||
|
||||
```
|
||||
import { createStore, applyMiddleware } from 'redux';
|
||||
|
||||
const { initial, reducer, middleware, actions } = updox(...);
|
||||
|
||||
const store = createStore( initial, reducer, applyMiddleware(middleware) );
|
||||
|
||||
for ( let type in actions ) {
|
||||
store.dispatch[type] = (...args) => {
|
||||
store.dispatch(actions[type](...args))
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
So that later on you can do
|
||||
|
||||
```
|
||||
store.dispatch.addTodo(...);
|
||||
|
||||
// still work
|
||||
store.dispatch( actions.addTodo(...) );
|
||||
```
|
||||
|
||||
# Example
|
||||
|
||||
#### battle.js
|
||||
|
||||
```
|
||||
import { updux } from 'updux';
|
||||
|
||||
import game from './game';
|
||||
import log from './log';
|
||||
import bogeys from './bogeys';
|
||||
|
||||
const { createStore } = updux({
|
||||
subduxes: { game, log, bogeys }
|
||||
})
|
||||
|
||||
export default createStore;
|
||||
```
|
||||
|
||||
#### game.js
|
||||
|
||||
|
||||
```
|
||||
import { updux } from 'updux';
|
||||
import _ from 'lodash';
|
||||
import u from 'updeep';
|
||||
|
||||
import { calculateMovement } from 'game/rules';
|
||||
|
||||
export default updux({
|
||||
initial: { game: "", players: [], turn: 0, },
|
||||
mutations: {
|
||||
init_game: ({game: { name, players }}) => {name, players},
|
||||
play_turn: () => u({ turn: x => x+1 }),
|
||||
},
|
||||
effects: {
|
||||
play_turn: ({getState,dispatch}) => next => action => {
|
||||
|
||||
const bogeys = api.getState().bogeys;
|
||||
|
||||
// only allow the turn to be played if
|
||||
// all ships have their orders in
|
||||
if( bogeys.any( bogey => ! bogey.orders ) ) return;
|
||||
|
||||
bogeys.forEach( bogey => {
|
||||
dispatch.move( calculateMovement(bogey) )
|
||||
} );
|
||||
|
||||
next(action);
|
||||
},
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
#### log.js
|
||||
|
||||
```
|
||||
import { updux } from 'updux';
|
||||
|
||||
export default updux({
|
||||
initial: [],
|
||||
mutations: {
|
||||
'*': (payload,action) => state => [ ...state, action ],
|
||||
review: () => u({ reviewed: true}),
|
||||
done: () => u({done: true}),
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
#### bogeys.js
|
||||
const todos = new Updux({ initial: [] });
|
||||
|
||||
todos.addMutation(
|
||||
todo.actions.review,
|
||||
(_,action) => state => state.map( todo.upreducer(action) )
|
||||
);
|
||||
todos.addMutation(
|
||||
todo.actions.done,
|
||||
(id,action) => u.map(u.if(u.is('id',id), todo.upreducer(action))),
|
||||
);
|
||||
|
||||
```
|
||||
import { updux } from 'updux';
|
||||
import _ from 'lodash';
|
||||
|
||||
export default updux({
|
||||
initial: [],
|
||||
But `updeep` can iterate through all the items of an array (or the values of
|
||||
an object) via the special key `*`. So the todos updux above could also be
|
||||
written:
|
||||
|
||||
```
|
||||
const todo = new Updux({
|
||||
mutations: {
|
||||
init_game: ({bogeys}) => () => _.keyBy( bogeys, 'id' ),
|
||||
move: ({position}) => u({ position }),
|
||||
review: () => u({ reviewed: true}),
|
||||
done: () => u({done: true}),
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
#### myGame.js
|
||||
|
||||
```
|
||||
import Battle from './battle';
|
||||
|
||||
const battle = Battle();
|
||||
|
||||
battle.dispatch.init_game({
|
||||
name: 'Gemini Prime',
|
||||
players: [ 'yenzie' ],
|
||||
bogeys: [ { id: 'Enkidu' } ]
|
||||
const todos = new Updux({
|
||||
subduxes: { '*': todo },
|
||||
});
|
||||
|
||||
battle.dispatch.play_game();
|
||||
|
||||
....
|
||||
todos.addMutation(
|
||||
todo.actions.done,
|
||||
(id,action) => u.map(u.if(u.is('id',id), todo.upreducer(action))),
|
||||
true
|
||||
);
|
||||
```
|
||||
|
||||
The advantages being that the actions/mutations/effects of the subdux will be
|
||||
imported by the root updux as usual, and all actions that aren't being
|
||||
overridden by a sink mutation will trickle down automatically.
|
||||
|
||||
[Redux]: https://redux.js.org
|
||||
[Updeep]: https://github.com/substantial/updeep
|
||||
[VueX]: https://vuex.vuejs.org/
|
||||
## Usage with Immer
|
||||
|
||||
While Updux was created with Updeep in mind, it also plays very
|
||||
well with [Immer](https://immerjs.github.io/immer/docs/introduction).
|
||||
|
||||
For example, taking this basic updux:
|
||||
|
||||
```
|
||||
import Updux from 'updux';
|
||||
|
||||
const updux = new Updux({
|
||||
initial: { counter: 0 },
|
||||
mutations: {
|
||||
add: (inc=1) => state => { counter: counter + inc }
|
||||
}
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
Converting it to Immer would look like:
|
||||
|
||||
```
|
||||
import Updux from 'updux';
|
||||
import { produce } from 'Immer';
|
||||
|
||||
const updux = new Updux({
|
||||
initial: { counter: 0 },
|
||||
mutations: {
|
||||
add: (inc=1) => produce( draft => draft.counter += inc ) }
|
||||
}
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
But since typing `produce` over and over is no fun, `groomMutations`
|
||||
can be used to wrap all mutations with it:
|
||||
|
||||
```
|
||||
import Updux from 'updux';
|
||||
import { produce } from 'Immer';
|
||||
|
||||
const updux = new Updux({
|
||||
initial: { counter: 0 },
|
||||
groomMutations: mutation => (...args) => produce( mutation(...args) ),
|
||||
mutations: {
|
||||
add: (inc=1) => draft => draft.counter += inc
|
||||
}
|
||||
});
|
||||
|
||||
```
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
- documentation generator (mkdocs + jsdoc-to-markdown)
|
||||
- createStore
|
|
@ -0,0 +1,46 @@
|
|||
# https://taskfile.dev
|
||||
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
lint:fix:delta:
|
||||
vars:
|
||||
FILES:
|
||||
sh: git diff-ls --diff-filter=d main | grep -v .vitebook
|
||||
deps:
|
||||
- task: 'lint:fix'
|
||||
vars:
|
||||
CLI_ARGS: '{{.FILES | catLines }}'
|
||||
|
||||
lint:
|
||||
cmds:
|
||||
- npx prettier --check {{.CLI_ARGS | default "." }}
|
||||
- npx eslint {{.CLI_ARGS | default "." }}
|
||||
|
||||
lint:fix:
|
||||
cmds:
|
||||
- npx prettier --write {{.CLI_ARGS | default "." }}
|
||||
- npx eslint --fix --quiet {{.CLI_ARGS | default "." }}
|
||||
|
||||
lint:delta:
|
||||
cmds:
|
||||
- task: 'lint:prettier:delta'
|
||||
- task: 'lint:eslint:delta'
|
||||
|
||||
lint:prettier:delta:
|
||||
vars:
|
||||
FILES:
|
||||
sh: git diff-ls --diff-filter=d {{.ROOT_BRANCH | default "main"}} | grep -v .vitebook
|
||||
cmds:
|
||||
- npx prettier --check {{.FILES | catLines }}
|
||||
|
||||
lint:eslint:delta:
|
||||
vars:
|
||||
FILES:
|
||||
sh: git diff-ls --diff-filter=d {{.ROOT_BRANCH | default "main"}} | grep -v .vitebook
|
||||
cmds:
|
||||
- npx eslint --format=unix {{.FILES | catLines }}
|
||||
|
||||
lint:eslint:
|
||||
cmds:
|
||||
- npx eslint {{.FILES | default "src/**" }}
|
|
@ -1,12 +0,0 @@
|
|||
module.exports = {
|
||||
presets: [
|
||||
[
|
||||
'@babel/preset-env',
|
||||
{
|
||||
targets: {
|
||||
node: 'current',
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
};
|
|
@ -0,0 +1,226 @@
|
|||
# What's Updux?
|
||||
|
||||
So, I'm a fan of [Redux](https://redux.js.org). Two days ago I discovered
|
||||
[rematch](https://rematch.github.io/rematch) alonside a few other frameworks built atop Redux.
|
||||
|
||||
It has a couple of pretty good ideas that removes some of the
|
||||
boilerplate. Keeping mutations and asynchronous effects close to the
|
||||
reducer definition? Nice. Automatically infering the
|
||||
actions from the said mutations and effects? Genius!
|
||||
|
||||
But it also enforces a flat hierarchy of reducers -- where
|
||||
is the fun in that? And I'm also having a strong love for
|
||||
[Updeep](https://github.com/substantial/updeep), so I want reducer state updates to leverage the heck out of it.
|
||||
|
||||
All that to say, say hello to `Updux`. Heavily inspired by `rematch`, but twisted
|
||||
to work with `updeep` and to fit my peculiar needs. It offers features such as
|
||||
|
||||
- Mimic the way VueX has mutations (reducer reactions to specific actions) and
|
||||
effects (middleware reacting to actions that can be asynchronous and/or
|
||||
have side-effects), so everything pertaining to a store are all defined
|
||||
in the space place.
|
||||
- Automatically gather all actions used by the updux's effects and mutations,
|
||||
and makes then accessible as attributes to the `dispatch` object of the
|
||||
store.
|
||||
- Mutations have a signature that is friendly to Updux and Immer.
|
||||
- Also, the mutation signature auto-unwrap the payload of the actions for you.
|
||||
- TypeScript types.
|
||||
|
||||
Fair warning: this package is still very new, probably very buggy,
|
||||
definitively very badly documented, and very subject to changes. Caveat
|
||||
Maxima Emptor.
|
||||
|
||||
# Synopsis
|
||||
|
||||
```
|
||||
import updux from 'updux';
|
||||
|
||||
import otherUpdux from './otherUpdux';
|
||||
|
||||
const {
|
||||
initial,
|
||||
reducer,
|
||||
actions,
|
||||
middleware,
|
||||
createStore,
|
||||
} = new Updux({
|
||||
initial: {
|
||||
counter: 0,
|
||||
},
|
||||
subduxes: {
|
||||
otherUpdux,
|
||||
},
|
||||
mutations: {
|
||||
inc: ( increment = 1 ) => u({counter: s => s + increment })
|
||||
},
|
||||
effects: {
|
||||
'*' => api => next => action => {
|
||||
console.log( "hey, look, an action zoomed by!", action );
|
||||
next(action);
|
||||
};
|
||||
},
|
||||
actions: {
|
||||
customAction: ( someArg ) => ({
|
||||
type: "custom",
|
||||
payload: { someProp: someArg }
|
||||
}),
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
const store = createStore();
|
||||
|
||||
store.dispatch.inc(3);
|
||||
```
|
||||
|
||||
# Description
|
||||
|
||||
Full documentation can be [found here](https://yanick.github.io/updux/).
|
||||
Right now the best way to understand the whole thing is to go
|
||||
through the [tutorial](https://yanick.github.io/updux/#/tutorial)
|
||||
|
||||
## Exporting upduxes
|
||||
|
||||
If you are creating upduxes that will be used as subduxes
|
||||
by other upduxes, or as
|
||||
[ducks](https://github.com/erikras/ducks-modular-redux)-like containers, I
|
||||
recommend that you export the Updux instance as the default export:
|
||||
|
||||
```
|
||||
import Updux from 'updux';
|
||||
|
||||
const updux = new Updux({ ... });
|
||||
|
||||
export default updux;
|
||||
```
|
||||
|
||||
Then you can use them as subduxes like this:
|
||||
|
||||
```
|
||||
import Updux from 'updux';
|
||||
import foo from './foo'; // foo is an Updux
|
||||
import bar from './bar'; // bar is an Updux as well
|
||||
|
||||
const updux = new Updux({
|
||||
subduxes: {
|
||||
foo, bar
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
Or if you want to use it:
|
||||
|
||||
```
|
||||
import updux from './myUpdux';
|
||||
|
||||
const {
|
||||
reducer,
|
||||
actions: { doTheThing },
|
||||
createStore,
|
||||
middleware,
|
||||
} = updux;
|
||||
```
|
||||
|
||||
## Mapping a mutation to all values of a state
|
||||
|
||||
Say you have a `todos` state that is an array of `todo` sub-states. It's easy
|
||||
enough to have the main reducer maps away all items to the sub-reducer:
|
||||
|
||||
```
|
||||
const todo = new Updux({
|
||||
mutations: {
|
||||
review: () => u({ reviewed: true}),
|
||||
done: () => u({done: true}),
|
||||
},
|
||||
});
|
||||
|
||||
const todos = new Updux({ initial: [] });
|
||||
|
||||
todos.addMutation(
|
||||
todo.actions.review,
|
||||
(_,action) => state => state.map( todo.upreducer(action) )
|
||||
);
|
||||
todos.addMutation(
|
||||
todo.actions.done,
|
||||
(id,action) => u.map(u.if(u.is('id',id), todo.upreducer(action))),
|
||||
);
|
||||
|
||||
```
|
||||
|
||||
But `updeep` can iterate through all the items of an array (or the values of
|
||||
an object) via the special key `*`. So the todos updux above could also be
|
||||
written:
|
||||
|
||||
```
|
||||
const todo = new Updux({
|
||||
mutations: {
|
||||
review: () => u({ reviewed: true}),
|
||||
done: () => u({done: true}),
|
||||
},
|
||||
});
|
||||
|
||||
const todos = new Updux({
|
||||
subduxes: { '*': todo },
|
||||
});
|
||||
|
||||
todos.addMutation(
|
||||
todo.actions.done,
|
||||
(id,action) => u.map(u.if(u.is('id',id), todo.upreducer(action))),
|
||||
true
|
||||
);
|
||||
```
|
||||
|
||||
The advantages being that the actions/mutations/effects of the subdux will be
|
||||
imported by the root updux as usual, and all actions that aren't being
|
||||
overridden by a sink mutation will trickle down automatically.
|
||||
|
||||
## Usage with Immer
|
||||
|
||||
While Updux was created with Updeep in mind, it also plays very
|
||||
well with [Immer](https://immerjs.github.io/immer/docs/introduction).
|
||||
|
||||
For example, taking this basic updux:
|
||||
|
||||
```
|
||||
import Updux from 'updux';
|
||||
|
||||
const updux = new Updux({
|
||||
initial: { counter: 0 },
|
||||
mutations: {
|
||||
add: (inc=1) => state => { counter: counter + inc }
|
||||
}
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
Converting it to Immer would look like:
|
||||
|
||||
```
|
||||
import Updux from 'updux';
|
||||
import { produce } from 'Immer';
|
||||
|
||||
const updux = new Updux({
|
||||
initial: { counter: 0 },
|
||||
mutations: {
|
||||
add: (inc=1) => produce( draft => draft.counter += inc ) }
|
||||
}
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
But since typing `produce` over and over is no fun, `groomMutations`
|
||||
can be used to wrap all mutations with it:
|
||||
|
||||
```
|
||||
import Updux from 'updux';
|
||||
import { produce } from 'Immer';
|
||||
|
||||
const updux = new Updux({
|
||||
initial: { counter: 0 },
|
||||
groomMutations: mutation => (...args) => produce( mutation(...args) ),
|
||||
mutations: {
|
||||
add: (inc=1) => draft => draft.counter += inc
|
||||
}
|
||||
});
|
||||
|
||||
```
|
|
@ -0,0 +1,3 @@
|
|||
* [Home](/)
|
||||
* [ Tutorial ](tutorial.md)
|
||||
* [ Recipes ](recipes.md)
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,24 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Document</title>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||
<meta name="description" content="Description">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0">
|
||||
<link rel="stylesheet" href="//cdn.jsdelivr.net/npm/docsify@4/lib/themes/vue.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script>
|
||||
window.$docsify = {
|
||||
loadSidebar: true,
|
||||
name: 'updux',
|
||||
repo: '',
|
||||
subMaxLevel: 3,
|
||||
}
|
||||
</script>
|
||||
<!-- Docsify v4 -->
|
||||
<script src="docsify.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,96 @@
|
|||
# Recipes
|
||||
|
||||
## Mapping a mutation to all values of a state
|
||||
|
||||
Say you have a `todos` state that is an array of `todo` sub-states, with some
|
||||
actions that should percolate to all todos, and some that should only
|
||||
percolate to one. One way to model this is via updux's splat subduxes
|
||||
(backed by `updeep`'s own '*'-key behavior).
|
||||
|
||||
```
|
||||
const done = () => (state) => ({...state, done: true});
|
||||
|
||||
const todo = new Updux({
|
||||
initial: { id: 0, done: false },
|
||||
actions: {
|
||||
done: null,
|
||||
doneAll: null,
|
||||
},
|
||||
mutations: {
|
||||
done,
|
||||
doneAll: done,
|
||||
},
|
||||
});
|
||||
|
||||
const todos = new Updux({
|
||||
initial: [],
|
||||
subduxes: { '*': todo },
|
||||
actions: { addTodo: null },
|
||||
mutations: {
|
||||
addTodo: text => state => [ ...state, { text } ]
|
||||
}
|
||||
});
|
||||
|
||||
todos.setMutation(
|
||||
todo.actions.done,
|
||||
(text,action) => u.map(u.if(u.is('text',text), todo.upreducer(action))),
|
||||
true // prevents the subduxes mutations to run automatically
|
||||
);
|
||||
|
||||
```
|
||||
|
||||
## Usage with Immer
|
||||
|
||||
While Updux was created with Updeep in mind, it also plays very
|
||||
well with [Immer](https://immerjs.github.io/immer/docs/introduction).
|
||||
|
||||
For example, taking this basic updux:
|
||||
|
||||
```
|
||||
import Updux from 'updux';
|
||||
|
||||
const updux = new Updux({
|
||||
initial: { counter: 0 },
|
||||
mutations: {
|
||||
add: (inc=1) => state => ({ counter: state.counter + inc })
|
||||
}
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
Converting it to Immer would look like:
|
||||
|
||||
|
||||
```
|
||||
import Updux from 'updux';
|
||||
import { produce } from 'immer';
|
||||
|
||||
const updux = new Updux({
|
||||
initial: { counter: 0 },
|
||||
mutations: {
|
||||
add: (inc=1) => produce( draft => draft.counter += inc ) }
|
||||
}
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
But since typing `produce` over and over is no fun, `groomMutations`
|
||||
can be used to wrap all mutations with it:
|
||||
|
||||
|
||||
```
|
||||
import Updux from 'updux';
|
||||
import { produce } from 'immer';
|
||||
|
||||
const updux = new Updux({
|
||||
initial: { counter: 0 },
|
||||
groomMutations: mutation => (...args) => produce( mutation(...args) ),
|
||||
mutations: {
|
||||
add: (inc=1) => draft => draft.counter += inc
|
||||
}
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
import { test, expect } from 'vitest';
|
||||
|
||||
import u from 'updeep';
|
||||
import { Updux } from '../src/index.js';
|
||||
|
||||
const done = () => (state) => ({...state, done: true});
|
||||
|
||||
const todo = new Updux({
|
||||
initial: { id: 0, done: false },
|
||||
actions: {
|
||||
done: null,
|
||||
doneAll: null,
|
||||
},
|
||||
mutations: {
|
||||
done,
|
||||
doneAll: done,
|
||||
},
|
||||
});
|
||||
|
||||
const todos = new Updux({
|
||||
initial: [],
|
||||
subduxes: { '*': todo },
|
||||
actions: { addTodo: null },
|
||||
mutations: {
|
||||
addTodo: text => state => [ ...state, { text } ]
|
||||
}
|
||||
});
|
||||
|
||||
todos.setMutation(
|
||||
todo.actions.done,
|
||||
(text,action) => u.map(u.if(u.is('text',text), todo.upreducer(action))),
|
||||
true // prevents the subduxes mutations to run automatically
|
||||
);
|
||||
|
||||
test( "tutorial", async () => {
|
||||
const store = todos.createStore();
|
||||
|
||||
store.dispatch.addTodo('one');
|
||||
store.dispatch.addTodo('two');
|
||||
store.dispatch.addTodo('three');
|
||||
|
||||
store.dispatch.done( 'two' );
|
||||
|
||||
expect( store.getState()[1].done ).toBeTruthy();
|
||||
expect( store.getState()[2].done ).toBeFalsy();
|
||||
|
||||
store.dispatch.doneAll();
|
||||
|
||||
expect( store.getState().map( ({done}) => done ) ).toEqual([
|
||||
true, true, true
|
||||
]);
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
import { test, expect } from 'vitest';
|
||||
|
||||
import u from 'updeep';
|
||||
import { action, Updux, dux } from '../src/index.js';
|
||||
|
||||
const addTodoWithId = action('addTodoWithId');
|
||||
const incNextId = action('incNextId');
|
||||
const addTodo = action('addTodo');
|
||||
|
||||
const addTodoEffect = ({ getState, dispatch }) => next => action => {
|
||||
const id = getState.nextId();
|
||||
|
||||
dispatch.incNextId();
|
||||
|
||||
next(action);
|
||||
|
||||
dispatch.addTodoWithId({ description: action.payload, id });
|
||||
}
|
||||
|
||||
const todosDux = new Updux({
|
||||
initial: { nextId: 1, todos: [] },
|
||||
actions: { addTodo, incNextId, addTodoWithId },
|
||||
selectors: {
|
||||
nextId: ({nextId}) => nextId,
|
||||
},
|
||||
mutations: {
|
||||
addTodoWithId: (todo) => u({ todos: (todos) => [...todos, todo] }),
|
||||
incNextId: () => u({ nextId: id => id+1 }),
|
||||
},
|
||||
effects: {
|
||||
'addTodo': addTodoEffect
|
||||
}
|
||||
});
|
||||
|
||||
const store = todosDux.createStore();
|
||||
|
||||
test( "tutorial example", async () => {
|
||||
store.dispatch.addTodo('Do the thing');
|
||||
|
||||
expect( store.getState() ).toMatchObject({
|
||||
nextId:2, todos: [ { description: 'Do the thing', id: 1 } ]
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
test( "catch-all effect", () => {
|
||||
|
||||
let seen = [];
|
||||
|
||||
const foo = new Updux({
|
||||
actions: {
|
||||
one: null,
|
||||
two: null,
|
||||
},
|
||||
effects: {
|
||||
'*': (api) => next => action => {
|
||||
seen.push(action.type);
|
||||
next(action);
|
||||
}
|
||||
}
|
||||
} );
|
||||
|
||||
const store = foo.createStore();
|
||||
|
||||
store.dispatch.one();
|
||||
store.dispatch.two();
|
||||
|
||||
expect(seen).toEqual([ 'one', 'two' ]);
|
||||
} )
|
|
@ -0,0 +1,40 @@
|
|||
import { test, expect } from 'vitest';
|
||||
import u from 'updeep';
|
||||
|
||||
import { Updux } from '../src/index.js';
|
||||
|
||||
const todos = new Updux({
|
||||
initial: [],
|
||||
actions: {
|
||||
setNbrTodos: null,
|
||||
addTodo: null,
|
||||
},
|
||||
mutations: {
|
||||
addTodo: todo => todos => [ ...todos, todo ],
|
||||
},
|
||||
reactions: [
|
||||
({dispatch}) => todos => dispatch.setNbrTodos(todos.length)
|
||||
],
|
||||
});
|
||||
|
||||
const myDux = new Updux({
|
||||
initial: {
|
||||
nbrTodos: 0
|
||||
},
|
||||
subduxes: {
|
||||
todos,
|
||||
},
|
||||
mutations: {
|
||||
setNbrTodos: nbrTodos => u({ nbrTodos })
|
||||
}
|
||||
});
|
||||
|
||||
test( "basic tests", async () => {
|
||||
const store = myDux.createStore();
|
||||
|
||||
store.dispatch.addTodo('one');
|
||||
store.dispatch.addTodo('two');
|
||||
|
||||
expect(store.getState().nbrTodos).toEqual(2);
|
||||
});
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
import { test, expect } from 'vitest';
|
||||
|
||||
import { Updux } from '../src/index.js';
|
||||
|
||||
test( 'selectors', () => {
|
||||
const dux = new Updux({
|
||||
initial: { a: 1, b: 2 },
|
||||
selectors: {
|
||||
getA: ({a}) => a,
|
||||
getBPlus: ({b}) => addition => b + addition,
|
||||
},
|
||||
subduxes: {
|
||||
subbie: new Updux({
|
||||
initial: { d: 3 },
|
||||
selectors: {
|
||||
getD: ({d}) => d
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const store = dux.createStore();
|
||||
|
||||
expect( store.getState.getA() ).toEqual(1);
|
||||
expect( store.getState.getBPlus(7) ).toEqual(9);
|
||||
expect( store.getState.getD() ).toEqual(3);
|
||||
} );
|
|
@ -0,0 +1,91 @@
|
|||
import { test, expect } from 'vitest';
|
||||
|
||||
import u from 'updeep';
|
||||
import R from 'remeda';
|
||||
|
||||
import { Updux } from '../src/index.js';
|
||||
|
||||
const nextIdDux = new Updux({
|
||||
initial: 1,
|
||||
actions: {
|
||||
incrementNextId: null,
|
||||
},
|
||||
selectors: {
|
||||
getNextId: state => state
|
||||
},
|
||||
mutations: {
|
||||
incrementNextId: () => state => state + 1,
|
||||
}
|
||||
});
|
||||
|
||||
const matches = conditions => target => Object.entries(conditions).every(
|
||||
([key,value]) => typeof value === 'function' ? value(target[key]) : target[key] === value
|
||||
);
|
||||
|
||||
const todoDux = new Updux({
|
||||
initial: {
|
||||
id: 0,
|
||||
description: "",
|
||||
done: false,
|
||||
},
|
||||
actions: {
|
||||
todoDone: null,
|
||||
},
|
||||
mutations: {
|
||||
todoDone: id => u.if( matches({id}), { done: true })
|
||||
},
|
||||
selectors: {
|
||||
desc: R.prop('description'),
|
||||
}
|
||||
});
|
||||
|
||||
const todosDux = new Updux({
|
||||
initial: [],
|
||||
subduxes: {
|
||||
'*': todoDux
|
||||
},
|
||||
actions: {
|
||||
addTodoWithId: (description, id) => ({description, id} )
|
||||
},
|
||||
findSelectors: {
|
||||
getTodoById: state => id => state.find(matches({id}))
|
||||
},
|
||||
mutations: {
|
||||
addTodoWithId: todo => todos => [...todos, todo]
|
||||
}
|
||||
});
|
||||
|
||||
const mainDux = new Updux({
|
||||
subduxes: {
|
||||
nextId: nextIdDux,
|
||||
todos: todosDux,
|
||||
},
|
||||
actions: {
|
||||
addTodo: null
|
||||
},
|
||||
effects: {
|
||||
addTodo: ({ getState, dispatch }) => next => action => {
|
||||
const id = getState.getNextId();
|
||||
|
||||
dispatch.incrementNextId()
|
||||
|
||||
next(action);
|
||||
|
||||
dispatch.addTodoWithId( action.payload, id );
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const store = mainDux.createStore();
|
||||
|
||||
|
||||
test( "basic tests", () => {
|
||||
|
||||
const myDesc = 'do the thing';
|
||||
store.dispatch.addTodo(myDesc);
|
||||
|
||||
expect( store.getState.getTodoById(1).desc() ).toEqual(myDesc);
|
||||
|
||||
|
||||
|
||||
});
|
|
@ -0,0 +1,407 @@
|
|||
# Tutorial
|
||||
|
||||
This tutorial walks you through the features of `Updux` using the
|
||||
time-honored example of the implementation of Todo list store.
|
||||
|
||||
We'll be using
|
||||
[updeep](https://www.npmjs.com/package/updeep) to
|
||||
help with immutability and deep merging,
|
||||
but that's totally optional. If `updeep` is not your bag,
|
||||
it can easily be substitued with, say, [immer][], [lodash][], or even
|
||||
plain JavaScript.
|
||||
|
||||
## Definition of the state
|
||||
|
||||
To begin with, let's define that has nothing but an initial state.
|
||||
|
||||
```js
|
||||
import { Updux } from 'updux';
|
||||
|
||||
const todosDux = new Updux({
|
||||
initial: {
|
||||
next_id: 1,
|
||||
todos: [],
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
Congrats! You have written your first Updux object. It
|
||||
doesn't do a lot, but you can already create a store out of it, and its
|
||||
initial state will be automatically set:
|
||||
|
||||
```js
|
||||
const store = todosDux.createStore();
|
||||
|
||||
console.log(store.getState()); // prints { next_id: 1, todos: [] }
|
||||
```
|
||||
|
||||
## Add actions
|
||||
|
||||
This is all good, but a little static. Let's add actions!
|
||||
|
||||
```js
|
||||
const todosDux = new Updux({
|
||||
initial: {
|
||||
next_id: 1,
|
||||
todos: [],
|
||||
},
|
||||
{
|
||||
addTodo: null,
|
||||
todoDone: null,
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Accessing actions
|
||||
|
||||
Once an action is defined, its creator is accessible via the `actions` accessor.
|
||||
|
||||
```js
|
||||
console.log( todosDux.actions.addTodo('write tutorial') );
|
||||
// prints { type: 'addTodo', payload: 'write tutorial' }
|
||||
```
|
||||
### Adding a mutation
|
||||
|
||||
Mutations are the reducing functions associated to actions. They
|
||||
are defined via the `setMutation` method:
|
||||
|
||||
|
||||
```js
|
||||
todosDux.setMutation( 'addTodo', description => ({next_id: id, todos}) => ({
|
||||
next_id: 1 + id,
|
||||
todos: [...todos, { description, id, done: false }]
|
||||
}));
|
||||
```
|
||||
## Effects
|
||||
|
||||
In addition to mutations, Updux also provides action-specific middleware, here
|
||||
called effects.
|
||||
|
||||
Effects use the usual Redux middleware signature, plus a few goodies.
|
||||
The `getState` and `dispatch` functions are augmented with the dux selectors,
|
||||
and actions, respectively. The selectors and actions are also available
|
||||
from the api object.
|
||||
|
||||
```js
|
||||
import u from 'updeep';
|
||||
import { action, Updux } from 'updux';
|
||||
|
||||
// we want to decouple the increment of next_id and the creation of
|
||||
// a new todo. So let's use a new version of the action 'addTodo'.
|
||||
|
||||
const addTodoWithId = action('addTodoWithId');
|
||||
const incNextId = action('incNextId');
|
||||
const addTodo = action('addTodo');
|
||||
|
||||
const addTodoEffect = ({ getState, dispatch }) => next => action => {
|
||||
const id = getState.nextId();
|
||||
|
||||
dispatch.incNextId();
|
||||
|
||||
next(action);
|
||||
|
||||
dispatch.addTodoWithId({ description: action.payload, id });
|
||||
}
|
||||
|
||||
const todosDux = new Updux({
|
||||
initial: { nextId: 1, todos: [] },
|
||||
actions: { addTodo, incNextId, addTodoWithId },
|
||||
selectors: {
|
||||
nextId: ({nextId}) => nextId,
|
||||
},
|
||||
mutations: {
|
||||
addTodoWithId: (todo) => u({ todos: (todos) => [...todos, todo] }),
|
||||
incNextId: () => u({ nextId: id => id+1 }),
|
||||
},
|
||||
effects: {
|
||||
'addTodo': addTodoEffect
|
||||
}
|
||||
});
|
||||
|
||||
const store = todosDux.createStore();
|
||||
|
||||
store.dispatch.addTodo('Do the thing');
|
||||
```
|
||||
|
||||
### Catch-all effect
|
||||
|
||||
It is possible to have an effect match all actions via the special `*` token.
|
||||
|
||||
```
|
||||
todosUpdux.addEffect('*', () => next => action => {
|
||||
console.log( 'seeing action fly by:', action );
|
||||
next(action);
|
||||
});
|
||||
```
|
||||
|
||||
## Adding selectors
|
||||
|
||||
Selectors can be defined to get data derived from the state.
|
||||
From now you should know the drill: selectors can be defined at construction
|
||||
time or via `setSelector`.
|
||||
|
||||
```
|
||||
const getTodoById = ({todos}) => targetId => todos.find(({id}) => id === targetId);
|
||||
|
||||
const todosUpdux = new Updux({
|
||||
selectors: {
|
||||
getTodoById
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```
|
||||
todosDux.setSelector('getTodoById', getTodoById);
|
||||
```
|
||||
|
||||
### Accessing selectors
|
||||
|
||||
The `getState` method of a dux store is augmented
|
||||
with its selectors, with the first call for the state already
|
||||
called in for you.
|
||||
|
||||
```js
|
||||
const store = todosDux.createStore();
|
||||
|
||||
console.log(
|
||||
todosUpdux.getState.getTodoById(1)
|
||||
);
|
||||
```
|
||||
|
||||
## Subduxes
|
||||
|
||||
Now that we have all the building blocks, we can embark on the last and
|
||||
funkiest part of Updux: its recursive nature.
|
||||
|
||||
### Recap: the Todos dux, undivided
|
||||
|
||||
Upduxes can be divided into sub-upduxes that deal with the various parts of
|
||||
the global state. This is better understood by working out an example, so
|
||||
let's recap on the Todos dux we have so far:
|
||||
|
||||
```js
|
||||
import Updux from 'updux';
|
||||
import u from 'updeep';
|
||||
import fp from 'lodash/fp';
|
||||
|
||||
const todosDux = new Updux({
|
||||
initial: {
|
||||
nextId: 1,
|
||||
todos: [],
|
||||
},
|
||||
actions: {
|
||||
addTodo: null,
|
||||
addTodoWithId: (description, id) => ({description, id, done: false}),
|
||||
todoDone: null,
|
||||
incNextId: null,
|
||||
},
|
||||
selectors: {
|
||||
getTodoById: ({todos}) => id => fp.find({id},todos)
|
||||
},
|
||||
mutations: {
|
||||
addTodoWithId: todo =>
|
||||
u.updateIn( 'todos', todos => [ ...todos, todo] ),
|
||||
incrementNextId: () => u({ nextId: fp.add(1) }),
|
||||
todoDone: (id) => u.updateIn('todos',
|
||||
u.map( u.if( fp.matches({id}), todo => u({done: true}, todo) ) )
|
||||
),
|
||||
},
|
||||
effects: {
|
||||
addTodo: ({ getState, dispatch }) => next => action => {
|
||||
const { nextId: id } = getState();
|
||||
|
||||
dispatch.incNextId();
|
||||
|
||||
next(action);
|
||||
|
||||
dispatch.addTodoWithId(action.payload, id);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
This store has two main components: the `nextId`, and the `todos` collection.
|
||||
The `todos` collection is itself composed of the individual `todo`s. Let's
|
||||
create upduxes for each of those.
|
||||
|
||||
### NextId dux
|
||||
|
||||
```
|
||||
// dux/nextId.js
|
||||
|
||||
import { Updux } from 'updux';
|
||||
import u from 'updeep';
|
||||
|
||||
export default new Updux({
|
||||
initial: 1,
|
||||
actions: {
|
||||
incrementNextId: null,
|
||||
},
|
||||
selectors: {
|
||||
getNextId: state => state
|
||||
},
|
||||
mutations: {
|
||||
incrementNextId: () => state => state + 1,
|
||||
}
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
### Todo updux
|
||||
|
||||
```
|
||||
// dux/todos/todo/index.ts
|
||||
|
||||
import { Updux } from 'updux';
|
||||
import u from 'updeep';
|
||||
import fp from 'lodash/fp';
|
||||
|
||||
export default new Updux({
|
||||
initial: {
|
||||
id: 0,
|
||||
description: "",
|
||||
done: false,
|
||||
},
|
||||
actions: {
|
||||
todoDone: null,
|
||||
},
|
||||
mutations: {
|
||||
todoDone: id => u.if( fp.matches({id}), { done: true }) )
|
||||
},
|
||||
selectors: {
|
||||
desc: ({description}) => description,
|
||||
}
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
### Todos updux
|
||||
|
||||
```
|
||||
// dux/todos/index.js
|
||||
|
||||
import { Updux } from 'updux';
|
||||
import u from 'updeep';
|
||||
import fp from 'lodash/fp';
|
||||
|
||||
import todo from './todo/index.js';
|
||||
|
||||
export default new Updux({
|
||||
initial: [],
|
||||
subduxes: {
|
||||
'*': todoDux
|
||||
},
|
||||
actions: {
|
||||
addTodoWithId: (description, id) => ({description, id} )
|
||||
},
|
||||
findSelectors: {
|
||||
getTodoById: state => id => fp.find({id},state)
|
||||
},
|
||||
mutations: {
|
||||
addTodoWithId: todo =>
|
||||
todos => [ ...todos, todo ]
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
Note the special '\*' subdux key used here. This
|
||||
allows the updux to map every item present in its
|
||||
state to a `todo` updux. See [this recipe](/recipes?id=mapping-a-mutation-to-all-values-of-a-state) for details.
|
||||
|
||||
### Main store
|
||||
|
||||
```
|
||||
// dux/index.js
|
||||
|
||||
import Updux from 'updux';
|
||||
|
||||
import todos from './todos';
|
||||
import nextId from './next_id';
|
||||
|
||||
export new Updux({
|
||||
subduxes: {
|
||||
nextId,
|
||||
todos,
|
||||
},
|
||||
actions: {
|
||||
addTodo: null
|
||||
},
|
||||
effects: {
|
||||
addTodo: ({ getState, dispatch }) => next => action => {
|
||||
const id = getState.getNextId();
|
||||
|
||||
dispatch.incrementNextId()
|
||||
|
||||
next(action);
|
||||
|
||||
dispatch.addTodoWithId( action.payload, id );
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
Tadah! We had to define the `addTodo` effect at the top level as it needs to
|
||||
access the `getNextId` selector from `nextId` and the `addTodoWithId`
|
||||
action from the `todos`.
|
||||
|
||||
Note that the `getNextId` selector still gets the
|
||||
right value; when aggregating subduxes selectors Updux auto-wraps them to
|
||||
access the right slice of the top object. ```
|
||||
|
||||
## Reactions
|
||||
|
||||
Reactions -- aka Redux's subscriptions -- can be added to a updux store via the initial config
|
||||
or the method `addSubscription`. The signature of a reaction is:
|
||||
|
||||
```
|
||||
(storeApi) => (state, previousState, unsubscribe) => {
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
Subscriptions registered for an updux and its subduxes are automatically
|
||||
subscribed to the store when calling `createStore`.
|
||||
|
||||
The `state` passed to the subscriptions of the subduxes is the local state.
|
||||
|
||||
Also, all subscriptions are wrapped such that they are called only if the
|
||||
local `state` changed since their last invocation.
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
const todos = new Updux({
|
||||
initial: [],
|
||||
actions: {
|
||||
setNbrTodos: null,
|
||||
addTodo: null,
|
||||
},
|
||||
mutations: {
|
||||
addTodo: todo => todos => [ ...todos, todo ],
|
||||
},
|
||||
reactions: [
|
||||
({dispatch}) => todos => dispatch.setNbrTodos(todos.length)
|
||||
],
|
||||
});
|
||||
|
||||
const myDux = new Updux({
|
||||
initial: {
|
||||
nbrTodos: 0
|
||||
},
|
||||
subduxes: {
|
||||
todos,
|
||||
},
|
||||
mutations: {
|
||||
setNbrTodos: nbrTodos => u({ nbrTodos })
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
[immer]: https://www.npmjs.com/package/immer
|
||||
[lodash]: https://www.npmjs.com/package/lodash
|
||||
[ts-action]: https://www.npmjs.com/package/ts-action
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
import { test, expect } from 'vitest';
|
||||
|
||||
import { Updux } from '../src/index.js';
|
||||
|
||||
test( "basic checks", async () => {
|
||||
|
||||
|
||||
const todosDux = new Updux({
|
||||
initial: {
|
||||
next_id: 1,
|
||||
todos: [],
|
||||
},
|
||||
actions: {
|
||||
addTodo: null,
|
||||
todoDone: null,
|
||||
}
|
||||
});
|
||||
|
||||
const store = todosDux.createStore();
|
||||
|
||||
expect(store.getState()).toEqual({ next_id: 1, todos: [] });
|
||||
|
||||
expect(store.actions.addTodo("learn updux")).toMatchObject({
|
||||
type: 'addTodo', payload: 'learn updux'
|
||||
})
|
||||
});
|
|
@ -0,0 +1,293 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>JSDoc: Class: Updux</title>
|
||||
|
||||
<script src="scripts/prettify/prettify.js"> </script>
|
||||
<script src="scripts/prettify/lang-css.js"> </script>
|
||||
<!--[if lt IE 9]>
|
||||
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
|
||||
<![endif]-->
|
||||
<link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
|
||||
<link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div id="main">
|
||||
|
||||
<h1 class="page-title">Class: Updux</h1>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<section>
|
||||
|
||||
<header>
|
||||
|
||||
<h2><span class="attribs"><span class="type-signature"></span></span>Updux<span class="signature">()</span><span class="type-signature"></span></h2>
|
||||
|
||||
|
||||
</header>
|
||||
|
||||
<article>
|
||||
<div class="container-overview">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<h4 class="name" id="Updux"><span class="type-signature"></span>new Updux<span class="signature">()</span><span class="type-signature"></span></h4>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<dl class="details">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<dt class="tag-source">Source:</dt>
|
||||
<dd class="tag-source"><ul class="dummy"><li>
|
||||
<a href="Updux.js.html">Updux.js</a>, <a href="Updux.js.html#line24">line 24</a>
|
||||
</li></ul></dd>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</dl>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<h3 class="subsection-title">Classes</h3>
|
||||
|
||||
<dl>
|
||||
<dt><a href="Updux.html">Updux</a></dt>
|
||||
<dd></dd>
|
||||
</dl>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<h3 class="subsection-title">Members</h3>
|
||||
|
||||
|
||||
|
||||
<h4 class="name" id="actions"><span class="type-signature"></span>actions<span class="type-signature"></span></h4>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<dl class="details">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<dt class="tag-source">Source:</dt>
|
||||
<dd class="tag-source"><ul class="dummy"><li>
|
||||
<a href="Updux.js.html">Updux.js</a>, <a href="Updux.js.html#line111">line 111</a>
|
||||
</li></ul></dd>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</dl>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<h4 class="name" id="initial"><span class="type-signature"></span>initial<span class="type-signature"></span></h4>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<dl class="details">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<dt class="tag-source">Source:</dt>
|
||||
<dd class="tag-source"><ul class="dummy"><li>
|
||||
<a href="Updux.js.html">Updux.js</a>, <a href="Updux.js.html#line104">line 104</a>
|
||||
</li></ul></dd>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</dl>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</article>
|
||||
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<nav>
|
||||
<h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="Updux.html">Updux</a></li></ul>
|
||||
</nav>
|
||||
|
||||
<br class="clear">
|
||||
|
||||
<footer>
|
||||
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.7</a> on Tue Oct 12 2021 10:16:59 GMT-0400 (Eastern Daylight Time)
|
||||
</footer>
|
||||
|
||||
<script> prettyPrint(); </script>
|
||||
<script src="scripts/linenumber.js"> </script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,368 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>JSDoc: Source: Updux.js</title>
|
||||
|
||||
<script src="scripts/prettify/prettify.js"> </script>
|
||||
<script src="scripts/prettify/lang-css.js"> </script>
|
||||
<!--[if lt IE 9]>
|
||||
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
|
||||
<![endif]-->
|
||||
<link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
|
||||
<link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div id="main">
|
||||
|
||||
<h1 class="page-title">Source: Updux.js</h1>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<section>
|
||||
<article>
|
||||
<pre class="prettyprint source linenums"><code>/* TODO change * for leftovers to +, change subscriptions to reactions */
|
||||
import moize from 'moize';
|
||||
import u from '@yanick/updeep';
|
||||
import { createStore as reduxCreateStore, applyMiddleware } from 'redux';
|
||||
import { get, map, mapValues, merge, difference } from 'lodash-es';
|
||||
|
||||
import { buildInitial } from './buildInitial/index.js';
|
||||
import { buildActions } from './buildActions/index.js';
|
||||
import { buildSelectors } from './buildSelectors/index.js';
|
||||
import { action } from './actions.js';
|
||||
import { buildUpreducer } from './buildUpreducer.js';
|
||||
import {
|
||||
buildMiddleware,
|
||||
augmentMiddlewareApi,
|
||||
} from './buildMiddleware/index.js';
|
||||
|
||||
/**
|
||||
* @public
|
||||
* `Updux` is a way to minimize and simplify the boilerplate associated with the
|
||||
* creation of a `Redux` store. It takes a shorthand configuration
|
||||
* object, and generates the appropriate reducer, actions, middleware, etc.
|
||||
* In true `Redux`-like fashion, upduxes can be made of sub-upduxes (`subduxes` for short) for different slices of the root state.
|
||||
*/
|
||||
export class Updux {
|
||||
/** @type { unknown } */
|
||||
#initial = {};
|
||||
#subduxes = {};
|
||||
|
||||
/** @type Record<string,Function> */
|
||||
#actions = {};
|
||||
#selectors = {};
|
||||
#mutations = {};
|
||||
#effects = [];
|
||||
#subscriptions = [];
|
||||
#splatSelector = undefined;
|
||||
#splatReaction = undefined;
|
||||
|
||||
constructor(config) {
|
||||
this.#initial = config.initial ?? {};
|
||||
this.#subduxes = config.subduxes ?? {};
|
||||
|
||||
if (config.subduxes) {
|
||||
this.#subduxes = mapValues(config.subduxes, (sub) =>
|
||||
sub instanceof Updux ? sub : new Updux(sub)
|
||||
);
|
||||
}
|
||||
|
||||
if (config.actions) {
|
||||
for (const [type, actionArg] of Object.entries(config.actions)) {
|
||||
if (typeof actionArg === 'function' && actionArg.type) {
|
||||
this.#actions[type] = actionArg;
|
||||
} else {
|
||||
this.#actions[type] = action(type, actionArg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.#selectors = config.selectors ?? {};
|
||||
|
||||
this.#mutations = config.mutations ?? {};
|
||||
|
||||
this.#splatSelector = config.splatSelector;
|
||||
|
||||
Object.keys(this.#mutations)
|
||||
.filter((action) => action !== '*')
|
||||
.filter((action) => !this.actions.hasOwnProperty(action))
|
||||
.forEach((action) => {
|
||||
throw new Error(`action '${action}' is not defined`);
|
||||
});
|
||||
|
||||
if (config.effects) {
|
||||
this.#effects = Object.entries(config.effects);
|
||||
}
|
||||
|
||||
this.#subscriptions = config.subscriptions ?? [];
|
||||
|
||||
this.#splatReaction = config.splatReaction;
|
||||
}
|
||||
|
||||
#memoInitial = moize(buildInitial);
|
||||
#memoActions = moize(buildActions);
|
||||
#memoSelectors = moize(buildSelectors);
|
||||
#memoUpreducer = moize(buildUpreducer);
|
||||
#memoMiddleware = moize(buildMiddleware);
|
||||
|
||||
get subscriptions() {
|
||||
return this.#subscriptions;
|
||||
}
|
||||
|
||||
setSplatSelector(name, f) {
|
||||
this.#splatSelector = [name, f];
|
||||
}
|
||||
|
||||
get middleware() {
|
||||
return this.#memoMiddleware(
|
||||
this.#effects,
|
||||
this.actions,
|
||||
this.selectors,
|
||||
this.#subduxes
|
||||
);
|
||||
}
|
||||
|
||||
/** @return { import('./Updux').What } */
|
||||
get initial() {
|
||||
return this.#memoInitial(this.#initial, this.#subduxes);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Record<string,Function>}
|
||||
*/
|
||||
get actions() {
|
||||
return this.#memoActions(this.#actions, this.#subduxes);
|
||||
}
|
||||
|
||||
get selectors() {
|
||||
return this.#memoSelectors(
|
||||
this.#selectors,
|
||||
this.#splatSelector,
|
||||
this.#subduxes
|
||||
);
|
||||
}
|
||||
|
||||
get upreducer() {
|
||||
return this.#memoUpreducer(
|
||||
this.initial,
|
||||
this.#mutations,
|
||||
this.#subduxes
|
||||
);
|
||||
}
|
||||
|
||||
get reducer() {
|
||||
return (state, action) => this.upreducer(action)(state);
|
||||
}
|
||||
|
||||
addSubscription(subscription) {
|
||||
this.#subscriptions = [...this.#subscriptions, subscription];
|
||||
}
|
||||
|
||||
addAction(type, payloadFunc) {
|
||||
const theAction = action(type, payloadFunc);
|
||||
|
||||
this.#actions = { ...this.#actions, [type]: theAction };
|
||||
|
||||
return theAction;
|
||||
}
|
||||
|
||||
addSelector(name, func) {
|
||||
this.#selectors = {
|
||||
...this.#selectors,
|
||||
[name]: func,
|
||||
};
|
||||
return func;
|
||||
}
|
||||
|
||||
addMutation(name, mutation) {
|
||||
if (typeof name === 'function') name = name.type;
|
||||
|
||||
this.#mutations = {
|
||||
...this.#mutations,
|
||||
[name]: mutation,
|
||||
};
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
addEffect(action, effect) {
|
||||
this.#effects = [...this.#effects, [action, effect]];
|
||||
}
|
||||
|
||||
splatSubscriber(store, inner, splatReaction) {
|
||||
const cache = {};
|
||||
|
||||
return () => (state, previous, unsub) => {
|
||||
const cacheKeys = Object.keys(cache);
|
||||
|
||||
const newKeys = difference(Object.keys(state), cacheKeys);
|
||||
|
||||
for (const slice of newKeys) {
|
||||
let localStore = {
|
||||
...store,
|
||||
getState: () => store.getState()[slice],
|
||||
};
|
||||
|
||||
cache[slice] = [];
|
||||
|
||||
if (typeof splatReaction === 'function') {
|
||||
localStore = {
|
||||
...localStore,
|
||||
...splatReaction(localStore, slice),
|
||||
};
|
||||
}
|
||||
|
||||
const { unsub, subscriber, subscriberRaw } =
|
||||
inner.subscribeAll(localStore);
|
||||
|
||||
cache[slice].push({ unsub, subscriber, subscriberRaw });
|
||||
subscriber();
|
||||
}
|
||||
|
||||
const deletedKeys = difference(cacheKeys, Object.keys(state));
|
||||
|
||||
for (const deleted of deletedKeys) {
|
||||
for (const inner of cache[deleted]) {
|
||||
inner.subscriber();
|
||||
inner.unsub();
|
||||
}
|
||||
delete cache[deleted];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
subscribeTo(store, subscription, setupArgs = []) {
|
||||
const localStore = augmentMiddlewareApi(
|
||||
{
|
||||
...store,
|
||||
subscribe: (subscriber) =>
|
||||
this.subscribeTo(store, () => subscriber),
|
||||
},
|
||||
this.actions,
|
||||
this.selectors
|
||||
);
|
||||
|
||||
const subscriber = subscription(localStore, ...setupArgs);
|
||||
|
||||
let previous;
|
||||
|
||||
const memoSub = () => {
|
||||
const state = store.getState();
|
||||
if (state === previous) return;
|
||||
let p = previous;
|
||||
previous = state;
|
||||
subscriber(state, p, unsub);
|
||||
};
|
||||
|
||||
let ret = store.subscribe(memoSub);
|
||||
const unsub = typeof ret === 'function' ? ret : ret.unsub;
|
||||
return {
|
||||
unsub,
|
||||
subscriber: memoSub,
|
||||
subscriberRaw: subscriber,
|
||||
};
|
||||
}
|
||||
|
||||
subscribeAll(store) {
|
||||
let results = this.#subscriptions.map((sub) =>
|
||||
this.subscribeTo(store, sub)
|
||||
);
|
||||
|
||||
for (const subdux in this.#subduxes) {
|
||||
if (subdux !== '*') {
|
||||
const localStore = {
|
||||
...store,
|
||||
getState: () => get(store.getState(), subdux),
|
||||
};
|
||||
results.push(this.#subduxes[subdux].subscribeAll(localStore));
|
||||
}
|
||||
}
|
||||
|
||||
if (this.#splatReaction) {
|
||||
results.push(
|
||||
this.subscribeTo(
|
||||
store,
|
||||
this.splatSubscriber(
|
||||
store,
|
||||
this.#subduxes['*'],
|
||||
this.#splatReaction
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
unsub: () => results.forEach(({ unsub }) => unsub()),
|
||||
subscriber: () => results.forEach(({ subscriber }) => subscriber()),
|
||||
subscriberRaw: (...args) =>
|
||||
results.forEach(({ subscriberRaw }) => subscriberRaw(...args)),
|
||||
};
|
||||
}
|
||||
|
||||
createStore(initial) {
|
||||
const store = reduxCreateStore(
|
||||
this.reducer,
|
||||
initial ?? this.initial,
|
||||
applyMiddleware(this.middleware)
|
||||
);
|
||||
|
||||
store.actions = this.actions;
|
||||
|
||||
store.selectors = this.selectors;
|
||||
|
||||
merge(
|
||||
store.getState,
|
||||
mapValues(this.selectors, (selector) => {
|
||||
return (...args) => {
|
||||
let result = selector(store.getState());
|
||||
|
||||
if (typeof result === 'function') return result(...args);
|
||||
|
||||
return result;
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
for (const action in this.actions) {
|
||||
store.dispatch[action] = (...args) => {
|
||||
return store.dispatch(this.actions[action](...args));
|
||||
};
|
||||
}
|
||||
|
||||
this.subscribeAll(store);
|
||||
|
||||
return store;
|
||||
}
|
||||
}
|
||||
|
||||
const x = new Updux();
|
||||
x.selectors;
|
||||
</code></pre>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<nav>
|
||||
<h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="Updux.html">Updux</a></li></ul>
|
||||
</nav>
|
||||
|
||||
<br class="clear">
|
||||
|
||||
<footer>
|
||||
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.7</a> on Tue Oct 12 2021 10:16:59 GMT-0400 (Eastern Daylight Time)
|
||||
</footer>
|
||||
|
||||
<script> prettyPrint(); </script>
|
||||
<script src="scripts/linenumber.js"> </script>
|
||||
</body>
|
||||
</html>
|
Binary file not shown.
File diff suppressed because it is too large
Load Diff
After Width: | Height: | Size: 116 KiB |
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
After Width: | Height: | Size: 118 KiB |
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
After Width: | Height: | Size: 120 KiB |
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
After Width: | Height: | Size: 114 KiB |
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
After Width: | Height: | Size: 120 KiB |
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
After Width: | Height: | Size: 117 KiB |
Binary file not shown.
|
@ -0,0 +1,65 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>JSDoc: Home</title>
|
||||
|
||||
<script src="scripts/prettify/prettify.js"> </script>
|
||||
<script src="scripts/prettify/lang-css.js"> </script>
|
||||
<!--[if lt IE 9]>
|
||||
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
|
||||
<![endif]-->
|
||||
<link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
|
||||
<link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div id="main">
|
||||
|
||||
<h1 class="page-title">Home</h1>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<h3> </h3>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<nav>
|
||||
<h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="Updux.html">Updux</a></li></ul>
|
||||
</nav>
|
||||
|
||||
<br class="clear">
|
||||
|
||||
<footer>
|
||||
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.7</a> on Tue Oct 12 2021 10:16:59 GMT-0400 (Eastern Daylight Time)
|
||||
</footer>
|
||||
|
||||
<script> prettyPrint(); </script>
|
||||
<script src="scripts/linenumber.js"> </script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,25 @@
|
|||
/*global document */
|
||||
(() => {
|
||||
const source = document.getElementsByClassName('prettyprint source linenums');
|
||||
let i = 0;
|
||||
let lineNumber = 0;
|
||||
let lineId;
|
||||
let lines;
|
||||
let totalLines;
|
||||
let anchorHash;
|
||||
|
||||
if (source && source[0]) {
|
||||
anchorHash = document.location.hash.substring(1);
|
||||
lines = source[0].getElementsByTagName('li');
|
||||
totalLines = lines.length;
|
||||
|
||||
for (; i < totalLines; i++) {
|
||||
lineNumber++;
|
||||
lineId = `line${lineNumber}`;
|
||||
lines[i].id = lineId;
|
||||
if (lineId === anchorHash) {
|
||||
lines[i].className += ' selected';
|
||||
}
|
||||
}
|
||||
}
|
||||
})();
|
|
@ -0,0 +1,202 @@
|
|||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
|
@ -0,0 +1,2 @@
|
|||
PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\f\r ]+/,null," \t\r\n"]],[["str",/^"(?:[^\n\f\r"\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*"/,null],["str",/^'(?:[^\n\f\r'\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*'/,null],["lang-css-str",/^url\(([^"')]*)\)/i],["kwd",/^(?:url|rgb|!important|@import|@page|@media|@charset|inherit)(?=[^\w-]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*)\s*:/i],["com",/^\/\*[^*]*\*+(?:[^*/][^*]*\*+)*\//],["com",
|
||||
/^(?:<\!--|--\>)/],["lit",/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],["lit",/^#[\da-f]{3,6}/i],["pln",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i],["pun",/^[^\s\w"']+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[["kwd",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[["str",/^[^"')]+/]]),["css-str"]);
|
|
@ -0,0 +1 @@
|
|||
declare var q: any;
|
|
@ -0,0 +1,28 @@
|
|||
var q=null;window.PR_SHOULD_USE_CONTINUATION=!0;
|
||||
(function(){function L(a){function m(a){var f=a.charCodeAt(0);if(f!==92)return f;var b=a.charAt(1);return(f=r[b])?f:"0"<=b&&b<="7"?parseInt(a.substring(1),8):b==="u"||b==="x"?parseInt(a.substring(2),16):a.charCodeAt(1)}function e(a){if(a<32)return(a<16?"\\x0":"\\x")+a.toString(16);a=String.fromCharCode(a);if(a==="\\"||a==="-"||a==="["||a==="]")a="\\"+a;return a}function h(a){for(var f=a.substring(1,a.length-1).match(/\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\[0-3][0-7]{0,2}|\\[0-7]{1,2}|\\[\S\s]|[^\\]/g),a=
|
||||
[],b=[],o=f[0]==="^",c=o?1:0,i=f.length;c<i;++c){var j=f[c];if(/\\[bdsw]/i.test(j))a.push(j);else{var j=m(j),d;c+2<i&&"-"===f[c+1]?(d=m(f[c+2]),c+=2):d=j;b.push([j,d]);d<65||j>122||(d<65||j>90||b.push([Math.max(65,j)|32,Math.min(d,90)|32]),d<97||j>122||b.push([Math.max(97,j)&-33,Math.min(d,122)&-33]))}}b.sort(function(a,f){return a[0]-f[0]||f[1]-a[1]});f=[];j=[NaN,NaN];for(c=0;c<b.length;++c)i=b[c],i[0]<=j[1]+1?j[1]=Math.max(j[1],i[1]):f.push(j=i);b=["["];o&&b.push("^");b.push.apply(b,a);for(c=0;c<
|
||||
f.length;++c)i=f[c],b.push(e(i[0])),i[1]>i[0]&&(i[1]+1>i[0]&&b.push("-"),b.push(e(i[1])));b.push("]");return b.join("")}function y(a){for(var f=a.source.match(/\[(?:[^\\\]]|\\[\S\s])*]|\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\\d+|\\[^\dux]|\(\?[!:=]|[()^]|[^()[\\^]+/g),b=f.length,d=[],c=0,i=0;c<b;++c){var j=f[c];j==="("?++i:"\\"===j.charAt(0)&&(j=+j.substring(1))&&j<=i&&(d[j]=-1)}for(c=1;c<d.length;++c)-1===d[c]&&(d[c]=++t);for(i=c=0;c<b;++c)j=f[c],j==="("?(++i,d[i]===void 0&&(f[c]="(?:")):"\\"===j.charAt(0)&&
|
||||
(j=+j.substring(1))&&j<=i&&(f[c]="\\"+d[i]);for(i=c=0;c<b;++c)"^"===f[c]&&"^"!==f[c+1]&&(f[c]="");if(a.ignoreCase&&s)for(c=0;c<b;++c)j=f[c],a=j.charAt(0),j.length>=2&&a==="["?f[c]=h(j):a!=="\\"&&(f[c]=j.replace(/[A-Za-z]/g,function(a){a=a.charCodeAt(0);return"["+String.fromCharCode(a&-33,a|32)+"]"}));return f.join("")}for(var t=0,s=!1,l=!1,p=0,d=a.length;p<d;++p){var g=a[p];if(g.ignoreCase)l=!0;else if(/[a-z]/i.test(g.source.replace(/\\u[\da-f]{4}|\\x[\da-f]{2}|\\[^UXux]/gi,""))){s=!0;l=!1;break}}for(var r=
|
||||
{b:8,t:9,n:10,v:11,f:12,r:13},n=[],p=0,d=a.length;p<d;++p){g=a[p];if(g.global||g.multiline)throw Error(""+g);n.push("(?:"+y(g)+")")}return RegExp(n.join("|"),l?"gi":"g")}function M(a){function m(a){switch(a.nodeType){case 1:if(e.test(a.className))break;for(var g=a.firstChild;g;g=g.nextSibling)m(g);g=a.nodeName;if("BR"===g||"LI"===g)h[s]="\n",t[s<<1]=y++,t[s++<<1|1]=a;break;case 3:case 4:g=a.nodeValue,g.length&&(g=p?g.replace(/\r\n?/g,"\n"):g.replace(/[\t\n\r ]+/g," "),h[s]=g,t[s<<1]=y,y+=g.length,
|
||||
t[s++<<1|1]=a)}}var e=/(?:^|\s)nocode(?:\s|$)/,h=[],y=0,t=[],s=0,l;a.currentStyle?l=a.currentStyle.whiteSpace:window.getComputedStyle&&(l=document.defaultView.getComputedStyle(a,q).getPropertyValue("white-space"));var p=l&&"pre"===l.substring(0,3);m(a);return{a:h.join("").replace(/\n$/,""),c:t}}function B(a,m,e,h){m&&(a={a:m,d:a},e(a),h.push.apply(h,a.e))}function x(a,m){function e(a){for(var l=a.d,p=[l,"pln"],d=0,g=a.a.match(y)||[],r={},n=0,z=g.length;n<z;++n){var f=g[n],b=r[f],o=void 0,c;if(typeof b===
|
||||
"string")c=!1;else{var i=h[f.charAt(0)];if(i)o=f.match(i[1]),b=i[0];else{for(c=0;c<t;++c)if(i=m[c],o=f.match(i[1])){b=i[0];break}o||(b="pln")}if((c=b.length>=5&&"lang-"===b.substring(0,5))&&!(o&&typeof o[1]==="string"))c=!1,b="src";c||(r[f]=b)}i=d;d+=f.length;if(c){c=o[1];var j=f.indexOf(c),k=j+c.length;o[2]&&(k=f.length-o[2].length,j=k-c.length);b=b.substring(5);B(l+i,f.substring(0,j),e,p);B(l+i+j,c,C(b,c),p);B(l+i+k,f.substring(k),e,p)}else p.push(l+i,b)}a.e=p}var h={},y;(function(){for(var e=a.concat(m),
|
||||
l=[],p={},d=0,g=e.length;d<g;++d){var r=e[d],n=r[3];if(n)for(var k=n.length;--k>=0;)h[n.charAt(k)]=r;r=r[1];n=""+r;p.hasOwnProperty(n)||(l.push(r),p[n]=q)}l.push(/[\S\s]/);y=L(l)})();var t=m.length;return e}function u(a){var m=[],e=[];a.tripleQuotedStrings?m.push(["str",/^(?:'''(?:[^'\\]|\\[\S\s]|''?(?=[^']))*(?:'''|$)|"""(?:[^"\\]|\\[\S\s]|""?(?=[^"]))*(?:"""|$)|'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$))/,q,"'\""]):a.multiLineStrings?m.push(["str",/^(?:'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$)|`(?:[^\\`]|\\[\S\s])*(?:`|$))/,
|
||||
q,"'\"`"]):m.push(["str",/^(?:'(?:[^\n\r'\\]|\\.)*(?:'|$)|"(?:[^\n\r"\\]|\\.)*(?:"|$))/,q,"\"'"]);a.verbatimStrings&&e.push(["str",/^@"(?:[^"]|"")*(?:"|$)/,q]);var h=a.hashComments;h&&(a.cStyleComments?(h>1?m.push(["com",/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,q,"#"]):m.push(["com",/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\n\r]*)/,q,"#"]),e.push(["str",/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,q])):m.push(["com",/^#[^\n\r]*/,
|
||||
q,"#"]));a.cStyleComments&&(e.push(["com",/^\/\/[^\n\r]*/,q]),e.push(["com",/^\/\*[\S\s]*?(?:\*\/|$)/,q]));a.regexLiterals&&e.push(["lang-regex",/^(?:^^\.?|[!+-]|!=|!==|#|%|%=|&|&&|&&=|&=|\(|\*|\*=|\+=|,|-=|->|\/|\/=|:|::|;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|[?@[^]|\^=|\^\^|\^\^=|{|\||\|=|\|\||\|\|=|~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\s*(\/(?=[^*/])(?:[^/[\\]|\\[\S\s]|\[(?:[^\\\]]|\\[\S\s])*(?:]|$))+\/)/]);(h=a.types)&&e.push(["typ",h]);a=(""+a.keywords).replace(/^ | $/g,
|
||||
"");a.length&&e.push(["kwd",RegExp("^(?:"+a.replace(/[\s,]+/g,"|")+")\\b"),q]);m.push(["pln",/^\s+/,q," \r\n\t\xa0"]);e.push(["lit",/^@[$_a-z][\w$@]*/i,q],["typ",/^(?:[@_]?[A-Z]+[a-z][\w$@]*|\w+_t\b)/,q],["pln",/^[$_a-z][\w$@]*/i,q],["lit",/^(?:0x[\da-f]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+-]?\d+)?)[a-z]*/i,q,"0123456789"],["pln",/^\\[\S\s]?/,q],["pun",/^.[^\s\w"-$'./@\\`]*/,q]);return x(m,e)}function D(a,m){function e(a){switch(a.nodeType){case 1:if(k.test(a.className))break;if("BR"===a.nodeName)h(a),
|
||||
a.parentNode&&a.parentNode.removeChild(a);else for(a=a.firstChild;a;a=a.nextSibling)e(a);break;case 3:case 4:if(p){var b=a.nodeValue,d=b.match(t);if(d){var c=b.substring(0,d.index);a.nodeValue=c;(b=b.substring(d.index+d[0].length))&&a.parentNode.insertBefore(s.createTextNode(b),a.nextSibling);h(a);c||a.parentNode.removeChild(a)}}}}function h(a){function b(a,d){var e=d?a.cloneNode(!1):a,f=a.parentNode;if(f){var f=b(f,1),g=a.nextSibling;f.appendChild(e);for(var h=g;h;h=g)g=h.nextSibling,f.appendChild(h)}return e}
|
||||
for(;!a.nextSibling;)if(a=a.parentNode,!a)return;for(var a=b(a.nextSibling,0),e;(e=a.parentNode)&&e.nodeType===1;)a=e;d.push(a)}var k=/(?:^|\s)nocode(?:\s|$)/,t=/\r\n?|\n/,s=a.ownerDocument,l;a.currentStyle?l=a.currentStyle.whiteSpace:window.getComputedStyle&&(l=s.defaultView.getComputedStyle(a,q).getPropertyValue("white-space"));var p=l&&"pre"===l.substring(0,3);for(l=s.createElement("LI");a.firstChild;)l.appendChild(a.firstChild);for(var d=[l],g=0;g<d.length;++g)e(d[g]);m===(m|0)&&d[0].setAttribute("value",
|
||||
m);var r=s.createElement("OL");r.className="linenums";for(var n=Math.max(0,m-1|0)||0,g=0,z=d.length;g<z;++g)l=d[g],l.className="L"+(g+n)%10,l.firstChild||l.appendChild(s.createTextNode("\xa0")),r.appendChild(l);a.appendChild(r)}function k(a,m){for(var e=m.length;--e>=0;){var h=m[e];A.hasOwnProperty(h)?window.console&&console.warn("cannot override language handler %s",h):A[h]=a}}function C(a,m){if(!a||!A.hasOwnProperty(a))a=/^\s*</.test(m)?"default-markup":"default-code";return A[a]}function E(a){var m=
|
||||
a.g;try{var e=M(a.h),h=e.a;a.a=h;a.c=e.c;a.d=0;C(m,h)(a);var k=/\bMSIE\b/.test(navigator.userAgent),m=/\n/g,t=a.a,s=t.length,e=0,l=a.c,p=l.length,h=0,d=a.e,g=d.length,a=0;d[g]=s;var r,n;for(n=r=0;n<g;)d[n]!==d[n+2]?(d[r++]=d[n++],d[r++]=d[n++]):n+=2;g=r;for(n=r=0;n<g;){for(var z=d[n],f=d[n+1],b=n+2;b+2<=g&&d[b+1]===f;)b+=2;d[r++]=z;d[r++]=f;n=b}for(d.length=r;h<p;){var o=l[h+2]||s,c=d[a+2]||s,b=Math.min(o,c),i=l[h+1],j;if(i.nodeType!==1&&(j=t.substring(e,b))){k&&(j=j.replace(m,"\r"));i.nodeValue=
|
||||
j;var u=i.ownerDocument,v=u.createElement("SPAN");v.className=d[a+1];var x=i.parentNode;x.replaceChild(v,i);v.appendChild(i);e<o&&(l[h+1]=i=u.createTextNode(t.substring(b,o)),x.insertBefore(i,v.nextSibling))}e=b;e>=o&&(h+=2);e>=c&&(a+=2)}}catch(w){"console"in window&&console.log(w&&w.stack?w.stack:w)}}var v=["break,continue,do,else,for,if,return,while"],w=[[v,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"],
|
||||
"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"],F=[w,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"],G=[w,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"],
|
||||
H=[G,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"],w=[w,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"],I=[v,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"],
|
||||
J=[v,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"],v=[v,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"],K=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/,N=/\S/,O=u({keywords:[F,H,w,"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END"+
|
||||
I,J,v],hashComments:!0,cStyleComments:!0,multiLineStrings:!0,regexLiterals:!0}),A={};k(O,["default-code"]);k(x([],[["pln",/^[^<?]+/],["dec",/^<!\w[^>]*(?:>|$)/],["com",/^<\!--[\S\s]*?(?:--\>|$)/],["lang-",/^<\?([\S\s]+?)(?:\?>|$)/],["lang-",/^<%([\S\s]+?)(?:%>|$)/],["pun",/^(?:<[%?]|[%?]>)/],["lang-",/^<xmp\b[^>]*>([\S\s]+?)<\/xmp\b[^>]*>/i],["lang-js",/^<script\b[^>]*>([\S\s]*?)(<\/script\b[^>]*>)/i],["lang-css",/^<style\b[^>]*>([\S\s]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),
|
||||
["default-markup","htm","html","mxml","xhtml","xml","xsl"]);k(x([["pln",/^\s+/,q," \t\r\n"],["atv",/^(?:"[^"]*"?|'[^']*'?)/,q,"\"'"]],[["tag",/^^<\/?[a-z](?:[\w-.:]*\w)?|\/?>$/i],["atn",/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^\s"'>]*(?:[^\s"'/>]|\/(?=\s)))/],["pun",/^[/<->]+/],["lang-js",/^on\w+\s*=\s*"([^"]+)"/i],["lang-js",/^on\w+\s*=\s*'([^']+)'/i],["lang-js",/^on\w+\s*=\s*([^\s"'>]+)/i],["lang-css",/^style\s*=\s*"([^"]+)"/i],["lang-css",/^style\s*=\s*'([^']+)'/i],["lang-css",
|
||||
/^style\s*=\s*([^\s"'>]+)/i]]),["in.tag"]);k(x([],[["atv",/^[\S\s]+/]]),["uq.val"]);k(u({keywords:F,hashComments:!0,cStyleComments:!0,types:K}),["c","cc","cpp","cxx","cyc","m"]);k(u({keywords:"null,true,false"}),["json"]);k(u({keywords:H,hashComments:!0,cStyleComments:!0,verbatimStrings:!0,types:K}),["cs"]);k(u({keywords:G,cStyleComments:!0}),["java"]);k(u({keywords:v,hashComments:!0,multiLineStrings:!0}),["bsh","csh","sh"]);k(u({keywords:I,hashComments:!0,multiLineStrings:!0,tripleQuotedStrings:!0}),
|
||||
["cv","py"]);k(u({keywords:"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END",hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["perl","pl","pm"]);k(u({keywords:J,hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["rb"]);k(u({keywords:w,cStyleComments:!0,regexLiterals:!0}),["js"]);k(u({keywords:"all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes",
|
||||
hashComments:3,cStyleComments:!0,multilineStrings:!0,tripleQuotedStrings:!0,regexLiterals:!0}),["coffee"]);k(x([],[["str",/^[\S\s]+/]]),["regex"]);window.prettyPrintOne=function(a,m,e){var h=document.createElement("PRE");h.innerHTML=a;e&&D(h,e);E({g:m,i:e,h:h});return h.innerHTML};window.prettyPrint=function(a){function m(){for(var e=window.PR_SHOULD_USE_CONTINUATION?l.now()+250:Infinity;p<h.length&&l.now()<e;p++){var n=h[p],k=n.className;if(k.indexOf("prettyprint")>=0){var k=k.match(g),f,b;if(b=
|
||||
!k){b=n;for(var o=void 0,c=b.firstChild;c;c=c.nextSibling)var i=c.nodeType,o=i===1?o?b:c:i===3?N.test(c.nodeValue)?b:o:o;b=(f=o===b?void 0:o)&&"CODE"===f.tagName}b&&(k=f.className.match(g));k&&(k=k[1]);b=!1;for(o=n.parentNode;o;o=o.parentNode)if((o.tagName==="pre"||o.tagName==="code"||o.tagName==="xmp")&&o.className&&o.className.indexOf("prettyprint")>=0){b=!0;break}b||((b=(b=n.className.match(/\blinenums\b(?::(\d+))?/))?b[1]&&b[1].length?+b[1]:!0:!1)&&D(n,b),d={g:k,h:n,i:b},E(d))}}p<h.length?setTimeout(m,
|
||||
250):a&&a()}for(var e=[document.getElementsByTagName("pre"),document.getElementsByTagName("code"),document.getElementsByTagName("xmp")],h=[],k=0;k<e.length;++k)for(var t=0,s=e[k].length;t<s;++t)h.push(e[k][t]);var e=q,l=Date;l.now||(l={now:function(){return+new Date}});var p=0,d,g=/\blang(?:uage)?-([\w.]+)(?!\S)/;m()};window.PR={createSimpleLexer:x,registerLangHandler:k,sourceDecorator:u,PR_ATTRIB_NAME:"atn",PR_ATTRIB_VALUE:"atv",PR_COMMENT:"com",PR_DECLARATION:"dec",PR_KEYWORD:"kwd",PR_LITERAL:"lit",
|
||||
PR_NOCODE:"nocode",PR_PLAIN:"pln",PR_PUNCTUATION:"pun",PR_SOURCE:"src",PR_STRING:"str",PR_TAG:"tag",PR_TYPE:"typ"}})();
|
|
@ -0,0 +1,358 @@
|
|||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
src: url('../fonts/OpenSans-Regular-webfont.eot');
|
||||
src:
|
||||
local('Open Sans'),
|
||||
local('OpenSans'),
|
||||
url('../fonts/OpenSans-Regular-webfont.eot?#iefix') format('embedded-opentype'),
|
||||
url('../fonts/OpenSans-Regular-webfont.woff') format('woff'),
|
||||
url('../fonts/OpenSans-Regular-webfont.svg#open_sansregular') format('svg');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Open Sans Light';
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
src: url('../fonts/OpenSans-Light-webfont.eot');
|
||||
src:
|
||||
local('Open Sans Light'),
|
||||
local('OpenSans Light'),
|
||||
url('../fonts/OpenSans-Light-webfont.eot?#iefix') format('embedded-opentype'),
|
||||
url('../fonts/OpenSans-Light-webfont.woff') format('woff'),
|
||||
url('../fonts/OpenSans-Light-webfont.svg#open_sanslight') format('svg');
|
||||
}
|
||||
|
||||
html
|
||||
{
|
||||
overflow: auto;
|
||||
background-color: #fff;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
body
|
||||
{
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
line-height: 1.5;
|
||||
color: #4d4e53;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
a, a:visited, a:active {
|
||||
color: #0095dd;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
header
|
||||
{
|
||||
display: block;
|
||||
padding: 0px 4px;
|
||||
}
|
||||
|
||||
tt, code, kbd, samp {
|
||||
font-family: Consolas, Monaco, 'Andale Mono', monospace;
|
||||
}
|
||||
|
||||
.class-description {
|
||||
font-size: 130%;
|
||||
line-height: 140%;
|
||||
margin-bottom: 1em;
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.class-description:empty {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#main {
|
||||
float: left;
|
||||
width: 70%;
|
||||
}
|
||||
|
||||
article dl {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
article img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
section
|
||||
{
|
||||
display: block;
|
||||
background-color: #fff;
|
||||
padding: 12px 24px;
|
||||
border-bottom: 1px solid #ccc;
|
||||
margin-right: 30px;
|
||||
}
|
||||
|
||||
.variation {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.signature-attributes {
|
||||
font-size: 60%;
|
||||
color: #aaa;
|
||||
font-style: italic;
|
||||
font-weight: lighter;
|
||||
}
|
||||
|
||||
nav
|
||||
{
|
||||
display: block;
|
||||
float: right;
|
||||
margin-top: 28px;
|
||||
width: 30%;
|
||||
box-sizing: border-box;
|
||||
border-left: 1px solid #ccc;
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
nav ul {
|
||||
font-family: 'Lucida Grande', 'Lucida Sans Unicode', arial, sans-serif;
|
||||
font-size: 100%;
|
||||
line-height: 17px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
nav ul a, nav ul a:visited, nav ul a:active {
|
||||
font-family: Consolas, Monaco, 'Andale Mono', monospace;
|
||||
line-height: 18px;
|
||||
color: #4D4E53;
|
||||
}
|
||||
|
||||
nav h3 {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
nav li {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
footer {
|
||||
display: block;
|
||||
padding: 6px;
|
||||
margin-top: 12px;
|
||||
font-style: italic;
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4 {
|
||||
font-weight: 200;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h1
|
||||
{
|
||||
font-family: 'Open Sans Light', sans-serif;
|
||||
font-size: 48px;
|
||||
letter-spacing: -2px;
|
||||
margin: 12px 24px 20px;
|
||||
}
|
||||
|
||||
h2, h3.subsection-title
|
||||
{
|
||||
font-size: 30px;
|
||||
font-weight: 700;
|
||||
letter-spacing: -1px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
h3
|
||||
{
|
||||
font-size: 24px;
|
||||
letter-spacing: -0.5px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
h4
|
||||
{
|
||||
font-size: 18px;
|
||||
letter-spacing: -0.33px;
|
||||
margin-bottom: 12px;
|
||||
color: #4d4e53;
|
||||
}
|
||||
|
||||
h5, .container-overview .subsection-title
|
||||
{
|
||||
font-size: 120%;
|
||||
font-weight: bold;
|
||||
letter-spacing: -0.01em;
|
||||
margin: 8px 0 3px 0;
|
||||
}
|
||||
|
||||
h6
|
||||
{
|
||||
font-size: 100%;
|
||||
letter-spacing: -0.01em;
|
||||
margin: 6px 0 3px 0;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
table
|
||||
{
|
||||
border-spacing: 0;
|
||||
border: 0;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
td, th
|
||||
{
|
||||
border: 1px solid #ddd;
|
||||
margin: 0px;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
padding: 4px 6px;
|
||||
display: table-cell;
|
||||
}
|
||||
|
||||
thead tr
|
||||
{
|
||||
background-color: #ddd;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
th { border-right: 1px solid #aaa; }
|
||||
tr > th:last-child { border-right: 1px solid #ddd; }
|
||||
|
||||
.ancestors, .attribs { color: #999; }
|
||||
.ancestors a, .attribs a
|
||||
{
|
||||
color: #999 !important;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.clear
|
||||
{
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.important
|
||||
{
|
||||
font-weight: bold;
|
||||
color: #950B02;
|
||||
}
|
||||
|
||||
.yes-def {
|
||||
text-indent: -1000px;
|
||||
}
|
||||
|
||||
.type-signature {
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.name, .signature {
|
||||
font-family: Consolas, Monaco, 'Andale Mono', monospace;
|
||||
}
|
||||
|
||||
.details { margin-top: 14px; border-left: 2px solid #DDD; }
|
||||
.details dt { width: 120px; float: left; padding-left: 10px; padding-top: 6px; }
|
||||
.details dd { margin-left: 70px; }
|
||||
.details ul { margin: 0; }
|
||||
.details ul { list-style-type: none; }
|
||||
.details li { margin-left: 30px; padding-top: 6px; }
|
||||
.details pre.prettyprint { margin: 0 }
|
||||
.details .object-value { padding-top: 0; }
|
||||
|
||||
.description {
|
||||
margin-bottom: 1em;
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.code-caption
|
||||
{
|
||||
font-style: italic;
|
||||
font-size: 107%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.source
|
||||
{
|
||||
border: 1px solid #ddd;
|
||||
width: 80%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.prettyprint.source {
|
||||
width: inherit;
|
||||
}
|
||||
|
||||
.source code
|
||||
{
|
||||
font-size: 100%;
|
||||
line-height: 18px;
|
||||
display: block;
|
||||
padding: 4px 12px;
|
||||
margin: 0;
|
||||
background-color: #fff;
|
||||
color: #4D4E53;
|
||||
}
|
||||
|
||||
.prettyprint code span.line
|
||||
{
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.prettyprint.linenums
|
||||
{
|
||||
padding-left: 70px;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.prettyprint.linenums ol
|
||||
{
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.prettyprint.linenums li
|
||||
{
|
||||
border-left: 3px #ddd solid;
|
||||
}
|
||||
|
||||
.prettyprint.linenums li.selected,
|
||||
.prettyprint.linenums li.selected *
|
||||
{
|
||||
background-color: lightyellow;
|
||||
}
|
||||
|
||||
.prettyprint.linenums li *
|
||||
{
|
||||
-webkit-user-select: text;
|
||||
-moz-user-select: text;
|
||||
-ms-user-select: text;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.params .name, .props .name, .name code {
|
||||
color: #4D4E53;
|
||||
font-family: Consolas, Monaco, 'Andale Mono', monospace;
|
||||
font-size: 100%;
|
||||
}
|
||||
|
||||
.params td.description > p:first-child,
|
||||
.props td.description > p:first-child
|
||||
{
|
||||
margin-top: 0;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.params td.description > p:last-child,
|
||||
.props td.description > p:last-child
|
||||
{
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
color: #454545;
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
/* JSDoc prettify.js theme */
|
||||
|
||||
/* plain text */
|
||||
.pln {
|
||||
color: #000000;
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
/* string content */
|
||||
.str {
|
||||
color: #006400;
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
/* a keyword */
|
||||
.kwd {
|
||||
color: #000000;
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
/* a comment */
|
||||
.com {
|
||||
font-weight: normal;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* a type name */
|
||||
.typ {
|
||||
color: #000000;
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
/* a literal value */
|
||||
.lit {
|
||||
color: #006400;
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
/* punctuation */
|
||||
.pun {
|
||||
color: #000000;
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
/* lisp open bracket */
|
||||
.opn {
|
||||
color: #000000;
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
/* lisp close bracket */
|
||||
.clo {
|
||||
color: #000000;
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
/* a markup tag name */
|
||||
.tag {
|
||||
color: #006400;
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
/* a markup attribute name */
|
||||
.atn {
|
||||
color: #006400;
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
/* a markup attribute value */
|
||||
.atv {
|
||||
color: #006400;
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
/* a declaration */
|
||||
.dec {
|
||||
color: #000000;
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
/* a variable name */
|
||||
.var {
|
||||
color: #000000;
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
/* a function name */
|
||||
.fun {
|
||||
color: #000000;
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
/* Specify class=linenums on a pre to get line numbering */
|
||||
ol.linenums {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
|
@ -0,0 +1,132 @@
|
|||
/* Tomorrow Theme */
|
||||
/* Original theme - https://github.com/chriskempson/tomorrow-theme */
|
||||
/* Pretty printing styles. Used with prettify.js. */
|
||||
/* SPAN elements with the classes below are added by prettyprint. */
|
||||
/* plain text */
|
||||
.pln {
|
||||
color: #4d4d4c; }
|
||||
|
||||
@media screen {
|
||||
/* string content */
|
||||
.str {
|
||||
color: #718c00; }
|
||||
|
||||
/* a keyword */
|
||||
.kwd {
|
||||
color: #8959a8; }
|
||||
|
||||
/* a comment */
|
||||
.com {
|
||||
color: #8e908c; }
|
||||
|
||||
/* a type name */
|
||||
.typ {
|
||||
color: #4271ae; }
|
||||
|
||||
/* a literal value */
|
||||
.lit {
|
||||
color: #f5871f; }
|
||||
|
||||
/* punctuation */
|
||||
.pun {
|
||||
color: #4d4d4c; }
|
||||
|
||||
/* lisp open bracket */
|
||||
.opn {
|
||||
color: #4d4d4c; }
|
||||
|
||||
/* lisp close bracket */
|
||||
.clo {
|
||||
color: #4d4d4c; }
|
||||
|
||||
/* a markup tag name */
|
||||
.tag {
|
||||
color: #c82829; }
|
||||
|
||||
/* a markup attribute name */
|
||||
.atn {
|
||||
color: #f5871f; }
|
||||
|
||||
/* a markup attribute value */
|
||||
.atv {
|
||||
color: #3e999f; }
|
||||
|
||||
/* a declaration */
|
||||
.dec {
|
||||
color: #f5871f; }
|
||||
|
||||
/* a variable name */
|
||||
.var {
|
||||
color: #c82829; }
|
||||
|
||||
/* a function name */
|
||||
.fun {
|
||||
color: #4271ae; } }
|
||||
/* Use higher contrast and text-weight for printable form. */
|
||||
@media print, projection {
|
||||
.str {
|
||||
color: #060; }
|
||||
|
||||
.kwd {
|
||||
color: #006;
|
||||
font-weight: bold; }
|
||||
|
||||
.com {
|
||||
color: #600;
|
||||
font-style: italic; }
|
||||
|
||||
.typ {
|
||||
color: #404;
|
||||
font-weight: bold; }
|
||||
|
||||
.lit {
|
||||
color: #044; }
|
||||
|
||||
.pun, .opn, .clo {
|
||||
color: #440; }
|
||||
|
||||
.tag {
|
||||
color: #006;
|
||||
font-weight: bold; }
|
||||
|
||||
.atn {
|
||||
color: #404; }
|
||||
|
||||
.atv {
|
||||
color: #060; } }
|
||||
/* Style */
|
||||
/*
|
||||
pre.prettyprint {
|
||||
background: white;
|
||||
font-family: Consolas, Monaco, 'Andale Mono', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
border: 1px solid #ccc;
|
||||
padding: 10px; }
|
||||
*/
|
||||
|
||||
/* Specify class=linenums on a pre to get line numbering */
|
||||
ol.linenums {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0; }
|
||||
|
||||
/* IE indents via margin-left */
|
||||
li.L0,
|
||||
li.L1,
|
||||
li.L2,
|
||||
li.L3,
|
||||
li.L4,
|
||||
li.L5,
|
||||
li.L6,
|
||||
li.L7,
|
||||
li.L8,
|
||||
li.L9 {
|
||||
/* */ }
|
||||
|
||||
/* Alternate shading for lines */
|
||||
li.L1,
|
||||
li.L3,
|
||||
li.L5,
|
||||
li.L7,
|
||||
li.L9 {
|
||||
/* */ }
|
72
package.json
72
package.json
|
@ -1,35 +1,41 @@
|
|||
{
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.15",
|
||||
"redux": "^4.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.6.4",
|
||||
"@babel/core": "^7.6.4",
|
||||
"@babel/preset-env": "^7.6.3",
|
||||
"babel-jest": "^24.9.0",
|
||||
"jest": "^24.9.0",
|
||||
"updeep": "^1.2.0"
|
||||
},
|
||||
"license": "MIT",
|
||||
"main": "dist/index.js",
|
||||
"name": "updux",
|
||||
"description": "Updeep-friendly Redux helper framework",
|
||||
"scripts": {
|
||||
"build": "babel src --out-dir dist",
|
||||
"test": "jest"
|
||||
},
|
||||
"version": "0.0.1",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/yanick/updux.git"
|
||||
},
|
||||
"keywords": [
|
||||
"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"
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"immer": "^9.0.15",
|
||||
"json-schema-shorthand": "^2.0.0",
|
||||
"redux": "^4.2.0",
|
||||
"remeda": "^1.0.1",
|
||||
"updeep": "^1.2.1"
|
||||
},
|
||||
"license": "MIT",
|
||||
"main": "src/index.js",
|
||||
"name": "updux",
|
||||
"description": "Updeep-friendly Redux helper framework",
|
||||
"scripts": {
|
||||
"docsify:serve": "docsify serve docs"
|
||||
},
|
||||
"version": "4.0.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/yanick/updux.git"
|
||||
},
|
||||
"keywords": [
|
||||
"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",
|
||||
"devDependencies": {
|
||||
"@vitest/browser": "^0.23.1",
|
||||
"@vitest/ui": "^0.23.1",
|
||||
"eslint": "^8.22.0",
|
||||
"eslint-plugin-no-only-tests": "^3.0.0",
|
||||
"eslint-plugin-todo-plz": "^1.2.1",
|
||||
"jsdoc-to-markdown": "^7.1.1",
|
||||
"prettier": "^2.7.1",
|
||||
"vitest": "0.23.1"
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,315 @@
|
|||
import R from 'remeda';
|
||||
import u from 'updeep';
|
||||
import { createStore as reduxCreateStore, applyMiddleware } from 'redux';
|
||||
|
||||
import { buildSelectors } from './selectors.js';
|
||||
import { buildUpreducer } from './upreducer.js';
|
||||
import { buildMiddleware, augmentMiddlewareApi } from './middleware.js';
|
||||
import { action, isActionGen } from './actions.js';
|
||||
|
||||
/**
|
||||
* Updux configuration object
|
||||
* @typedef {Object} Updux config
|
||||
* @property {Object} actions - Actions to be made available to the updux.
|
||||
*/
|
||||
|
||||
export class Updux {
|
||||
#name = 'unknown';
|
||||
#localInitial = {};
|
||||
#subduxes = {};
|
||||
#actions;
|
||||
#mutations = {};
|
||||
#config = {};
|
||||
#selectors = {};
|
||||
#effects = [];
|
||||
#localReactions = [];
|
||||
#middlewareWrapper;
|
||||
#splatReactionMapper;
|
||||
|
||||
constructor(config = {}) {
|
||||
this.#config = config;
|
||||
|
||||
this.#name = config.name || 'unknown';
|
||||
|
||||
this.#splatReactionMapper = config.splatReactionMapper;
|
||||
|
||||
this.#middlewareWrapper = config.middlewareWrapper;
|
||||
|
||||
this.#localInitial = config.initial;
|
||||
this.#subduxes = config.subduxes ?? {};
|
||||
|
||||
this.#actions = R.mapValues(config.actions ?? {}, (arg, name) =>
|
||||
isActionGen(arg) ? arg : action(name, arg),
|
||||
);
|
||||
Object.entries(this.#subduxes).forEach(([slice, sub]) =>
|
||||
this.#addSubduxActions(slice, sub),
|
||||
);
|
||||
|
||||
Object.entries(config.mutations ?? {}).forEach((args) =>
|
||||
this.setMutation(...args),
|
||||
);
|
||||
|
||||
this.#selectors = buildSelectors(
|
||||
config.selectors,
|
||||
config.findSelectors,
|
||||
this.#subduxes,
|
||||
);
|
||||
|
||||
if (Array.isArray(config.effects)) {
|
||||
this.#effects = config.effects;
|
||||
} else if (R.isObject(config.effects)) {
|
||||
this.#effects = Object.entries(config.effects);
|
||||
}
|
||||
|
||||
this.#localReactions = config.reactions ?? [];
|
||||
}
|
||||
|
||||
get name() {
|
||||
return this.#name;
|
||||
}
|
||||
|
||||
#addSubduxActions(_slice, subdux) {
|
||||
if (!subdux.actions) return;
|
||||
// TODO action 'blah' defined multiple times: <where>
|
||||
Object.entries(subdux.actions).forEach(([action, gen]) => {
|
||||
if (this.#actions[action]) {
|
||||
if (this.#actions[action] === gen) return;
|
||||
throw new Error(`action '${action}' already defined`);
|
||||
}
|
||||
|
||||
this.#actions[action] = gen;
|
||||
});
|
||||
}
|
||||
|
||||
get selectors() {
|
||||
return this.#selectors;
|
||||
}
|
||||
|
||||
get actions() {
|
||||
return this.#actions;
|
||||
}
|
||||
|
||||
get initial() {
|
||||
if (Object.keys(this.#subduxes).length === 0)
|
||||
return this.#localInitial ?? {};
|
||||
|
||||
if (this.#subduxes['*']) {
|
||||
if (this.#localInitial) return this.#localInitial;
|
||||
return [];
|
||||
}
|
||||
|
||||
return Object.assign(
|
||||
{},
|
||||
this.#localInitial ?? {},
|
||||
R.mapValues(this.#subduxes, ({ initial }) => initial),
|
||||
);
|
||||
}
|
||||
|
||||
get reducer() {
|
||||
return (state, action) => this.upreducer(action)(state);
|
||||
}
|
||||
|
||||
get upreducer() {
|
||||
return buildUpreducer(this.#mutations, this.#subduxes);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string | Function} action - Action triggering the mutation. If
|
||||
* the action is a string, it has to have been previously declared for this
|
||||
* updux, but if it's a function generator, it'll be automatically added to the
|
||||
* updux if not already present (the idea being that making a typo on a string
|
||||
* is easy, but passing a wrong function very more unlikely).
|
||||
* @param {Function} mutation - Mutating function.
|
||||
* @param {bool} terminal - If true, subduxes' mutations won't be invoked on
|
||||
* the action.
|
||||
* @return {void}
|
||||
*/
|
||||
setMutation(action, mutation, terminal = false) {
|
||||
// TODO option strict: false to make it okay to auto-create
|
||||
// the actions as strings?
|
||||
if (action.type) {
|
||||
if (!this.#actions[action.type]) {
|
||||
this.#actions[action.type] = action;
|
||||
} else if (this.#actions[action.type] !== action) {
|
||||
throw new Error(
|
||||
`action '${action.type}' not defined for this updux or definition is different`,
|
||||
);
|
||||
}
|
||||
|
||||
action = action.type;
|
||||
}
|
||||
|
||||
if (!this.#actions[action] && action !== '*') {
|
||||
throw new Error(`action '${action}' is not defined`);
|
||||
}
|
||||
|
||||
if (terminal) {
|
||||
const originalMutation = mutation;
|
||||
mutation = (...args) => originalMutation(...args);
|
||||
mutation.terminal = true;
|
||||
}
|
||||
|
||||
this.#mutations[action] = mutation;
|
||||
}
|
||||
|
||||
get mutations() {
|
||||
return this.#mutations;
|
||||
}
|
||||
|
||||
get middleware() {
|
||||
return buildMiddleware(
|
||||
this.#effects,
|
||||
this.actions,
|
||||
this.selectors,
|
||||
this.#subduxes,
|
||||
this.#middlewareWrapper,
|
||||
);
|
||||
}
|
||||
|
||||
createStore(initial = undefined, enhancerGenerator = undefined) {
|
||||
const enhancer = (enhancerGenerator ?? applyMiddleware)(
|
||||
this.middleware,
|
||||
);
|
||||
|
||||
const store = reduxCreateStore(
|
||||
this.reducer,
|
||||
initial ?? this.initial,
|
||||
enhancer,
|
||||
);
|
||||
|
||||
store.actions = this.actions;
|
||||
|
||||
store.selectors = this.selectors;
|
||||
|
||||
Object.entries(this.selectors).forEach(([selector, fn]) => {
|
||||
store.getState[selector] = (...args) => {
|
||||
let result = fn(store.getState());
|
||||
|
||||
if (typeof result === 'function') return result(...args);
|
||||
|
||||
return result;
|
||||
};
|
||||
});
|
||||
|
||||
for (const action in this.actions) {
|
||||
store.dispatch[action] = (...args) => {
|
||||
return store.dispatch(this.actions[action](...args));
|
||||
};
|
||||
}
|
||||
|
||||
this.subscribeAll(store);
|
||||
|
||||
return store;
|
||||
}
|
||||
|
||||
addEffect(action, effect) {
|
||||
this.#effects = [...this.#effects, [action, effect]];
|
||||
}
|
||||
|
||||
addReaction(reaction) {
|
||||
this.#localReactions = [...this.#localReactions, reaction];
|
||||
}
|
||||
|
||||
augmentMiddlewareApi(api) {
|
||||
return augmentMiddlewareApi(api, this.actions, this.selectors);
|
||||
}
|
||||
|
||||
subscribeTo(store, subscription) {
|
||||
const localStore = this.augmentMiddlewareApi({
|
||||
...store,
|
||||
subscribe: (subscriber) => this.subscribeTo(store, subscriber), // TODO not sure
|
||||
});
|
||||
|
||||
const subscriber = subscription(localStore);
|
||||
|
||||
let previous;
|
||||
let unsub;
|
||||
|
||||
const memoSub = () => {
|
||||
const state = store.getState();
|
||||
if (state === previous) return;
|
||||
let p = previous;
|
||||
previous = state;
|
||||
subscriber(state, p, unsub);
|
||||
};
|
||||
|
||||
return store.subscribe(memoSub);
|
||||
}
|
||||
|
||||
createSplatReaction() {
|
||||
const subdux = this.#subduxes['*'];
|
||||
const mapper = this.#splatReactionMapper;
|
||||
|
||||
return (api) => {
|
||||
const cache = {};
|
||||
|
||||
return (state, previousState, unsubscribe) => {
|
||||
const gone = { ...cache };
|
||||
|
||||
// TODO assuming object here
|
||||
for (const key in state) {
|
||||
if (cache[key]) {
|
||||
delete gone[key];
|
||||
} else {
|
||||
const dux = new Updux({
|
||||
initial: null,
|
||||
actions: { update: null },
|
||||
mutations: {
|
||||
update: (payload) => () => payload,
|
||||
},
|
||||
});
|
||||
const store = dux.createStore();
|
||||
// TODO need to change the store to have the
|
||||
// subscribe pointing to the right slice?
|
||||
const context = {
|
||||
...(api.context ?? {}),
|
||||
[subdux.name]: key,
|
||||
};
|
||||
const unsub = subdux.subscribeAll({
|
||||
...store,
|
||||
context,
|
||||
});
|
||||
cache[key] = { store, unsub };
|
||||
}
|
||||
cache[key].store.dispatch.update(state[key]);
|
||||
}
|
||||
|
||||
for (const key in gone) {
|
||||
cache[key].store.dispatch.update(null);
|
||||
cache[key].unsub();
|
||||
delete cache[key];
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
subscribeSplatReaction(store) {
|
||||
return this.subscribeTo(store, this.createSplatReaction());
|
||||
}
|
||||
|
||||
subscribeAll(store) {
|
||||
let unsubs = this.#localReactions.map((sub) =>
|
||||
this.subscribeTo(store, sub),
|
||||
);
|
||||
|
||||
if (this.#splatReactionMapper) {
|
||||
unsubs.push(this.subscribeSplatReaction(store));
|
||||
}
|
||||
|
||||
unsubs.push(
|
||||
...Object.entries(this.#subduxes)
|
||||
.filter(([slice]) => slice !== '*')
|
||||
.map(([slice, subdux]) => {
|
||||
subdux.subscribeAll({
|
||||
...store,
|
||||
getState: () => store.getState()[slice],
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
return () => unsubs.forEach((u) => u());
|
||||
}
|
||||
}
|
||||
|
||||
export const dux = (config) => new Updux(config);
|
|
@ -0,0 +1,26 @@
|
|||
export function isActionGen(action) {
|
||||
return typeof action === 'function' && action.type;
|
||||
}
|
||||
|
||||
export function action(type, payloadFunction, transformer) {
|
||||
let generator = function (...payloadArg) {
|
||||
const result = { type };
|
||||
|
||||
if (payloadFunction) {
|
||||
result.payload = payloadFunction(...payloadArg);
|
||||
} else {
|
||||
if (payloadArg[0] !== undefined) result.payload = payloadArg[0];
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
if (transformer) {
|
||||
const orig = generator;
|
||||
generator = (...args) => transformer(orig(...args), args);
|
||||
}
|
||||
|
||||
generator.type = type;
|
||||
|
||||
return generator;
|
||||
}
|
|
@ -1,24 +1,86 @@
|
|||
import updux from '.';
|
||||
import u from 'updeep';
|
||||
import { test, expect } from 'vitest';
|
||||
|
||||
test( 'actions defined in effects and mutations, multi-level', () => {
|
||||
import { action } from './actions.js';
|
||||
|
||||
const { actions } = updux({
|
||||
effects: {
|
||||
foo: api => next => action => { },
|
||||
import { Updux } from './Updux.js';
|
||||
|
||||
test('basic action', () => {
|
||||
const foo = action('foo', (thing) => ({ thing }));
|
||||
|
||||
expect(foo('bar')).toEqual({
|
||||
type: 'foo',
|
||||
payload: {
|
||||
thing: 'bar',
|
||||
},
|
||||
mutations: { bar: () => () => null },
|
||||
subduxes: {
|
||||
mysub: updux({
|
||||
effects: { baz: api => next => action => { }, },
|
||||
mutations: { quux: () => () => null },
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
test('Updux config accepts actions', () => {
|
||||
const foo = new Updux({
|
||||
actions: {
|
||||
one: action('one', (x) => ({ x })),
|
||||
two: action('two', (x) => x),
|
||||
},
|
||||
});
|
||||
|
||||
const types = Object.keys(actions);
|
||||
types.sort();
|
||||
|
||||
expect( types).toEqual([ 'bar', 'baz', 'foo', 'quux', ]);
|
||||
expect(Object.keys(foo.actions)).toHaveLength(2);
|
||||
|
||||
expect(foo.actions.one).toBeTypeOf('function');
|
||||
expect(foo.actions.one('potato')).toEqual({
|
||||
type: 'one',
|
||||
payload: {
|
||||
x: 'potato',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
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(
|
||||
() =>
|
||||
new Updux({
|
||||
actions: {
|
||||
foo: action('foo'),
|
||||
},
|
||||
subduxes: {
|
||||
beta: {
|
||||
actions: {
|
||||
foo: action('foo'),
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
).toThrow(/action 'foo' already defined/);
|
||||
});
|
||||
|
||||
test('action definition shortcut', () => {
|
||||
const foo = new Updux({
|
||||
actions: {
|
||||
foo: null,
|
||||
bar: (x) => ({ x }),
|
||||
},
|
||||
});
|
||||
|
||||
expect(foo.actions.foo('hello')).toEqual({ type: 'foo', payload: 'hello' });
|
||||
expect(foo.actions.bar('hello')).toEqual({
|
||||
type: 'bar',
|
||||
payload: { x: 'hello' },
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
import fp from 'lodash/fp';
|
||||
|
||||
function actionFor(type) {
|
||||
return ( (payload = undefined, meta = undefined) =>
|
||||
fp.pickBy(v => v !== null)({type, payload, meta})
|
||||
);
|
||||
}
|
||||
|
||||
export default function buildActions(
|
||||
mutations = {},
|
||||
effects = {},
|
||||
subActions = {},
|
||||
) {
|
||||
|
||||
return { ...subActions,
|
||||
...fp.fromPairs([ ...Object.keys(mutations), ...Object.keys(effects) ]
|
||||
.map( type => [ type, actionFor(type) ]))
|
||||
};
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
};
|
|
@ -1,8 +0,0 @@
|
|||
import fp from 'lodash/fp';
|
||||
|
||||
export default function buildInitial(
|
||||
initial= {},
|
||||
subduxes = {},
|
||||
) {
|
||||
return fp.isPlainObject(initial) ? fp.mergeAll([subduxes, initial]) : initial;
|
||||
}
|
|
@ -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);
|
||||
};
|
||||
};
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
import u from 'updeep';
|
||||
import { mapValues } from 'lodash-es';
|
||||
|
||||
export function buildUpreducer(
|
||||
initial,
|
||||
mutations,
|
||||
subduxes = {},
|
||||
wrapper = undefined,
|
||||
) {
|
||||
const subReducers =
|
||||
Object.keys(subduxes).length > 0
|
||||
? mapValues(subduxes, ({ upreducer }) => upreducer)
|
||||
: null;
|
||||
|
||||
const upreducer = (action) => (state) => {
|
||||
if (!action?.type)
|
||||
throw new Error('upreducer called with a bad action');
|
||||
|
||||
let newState = state ?? initial;
|
||||
|
||||
if (subReducers) {
|
||||
if (subduxes['*']) {
|
||||
newState = u.updateIn(
|
||||
'*',
|
||||
subduxes['*'].upreducer(action),
|
||||
newState,
|
||||
);
|
||||
} else {
|
||||
const update = mapValues(subReducers, (upReducer) =>
|
||||
upReducer(action),
|
||||
);
|
||||
|
||||
newState = u(update, newState);
|
||||
}
|
||||
}
|
||||
|
||||
const a = mutations[action.type] || mutations['+'];
|
||||
|
||||
if (!a) return newState;
|
||||
|
||||
return a(action.payload, action)(newState);
|
||||
};
|
||||
|
||||
return wrapper ? wrapper(upreducer) : upreducer;
|
||||
}
|
|
@ -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);
|
||||
};
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
import { test, expect } from 'vitest';
|
||||
import { Updux } from './Updux.js';
|
||||
|
||||
test('basic createStore', async () => {
|
||||
const foo = new Updux({
|
||||
initial: { a: 1 },
|
||||
actions: {
|
||||
a1: null,
|
||||
},
|
||||
});
|
||||
|
||||
const store = foo.createStore();
|
||||
|
||||
expect(store.getState).toBeTypeOf('function');
|
||||
expect(store.getState()).toEqual({ a: 1 });
|
||||
|
||||
expect(store.actions.a1).toBeTypeOf('function');
|
||||
});
|
|
@ -0,0 +1,69 @@
|
|||
import { test, expect } from 'vitest';
|
||||
import R from 'remeda';
|
||||
|
||||
import { dux, Updux } from './Updux.js';
|
||||
import { matches } from './utils.js';
|
||||
|
||||
test('basic selectors', () => {
|
||||
const foo = dux({
|
||||
initial: {
|
||||
x: 1,
|
||||
},
|
||||
selectors: {
|
||||
getX: ({ x }) => x,
|
||||
},
|
||||
subduxes: {
|
||||
bar: {
|
||||
initial: { y: 2 },
|
||||
selectors: {
|
||||
getY: ({ y }) => y,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(foo.selectors.getY({ bar: { y: 3 } })).toBe(3);
|
||||
});
|
||||
|
||||
test('splat selector', async () => {
|
||||
const bar = new Updux({
|
||||
initial: { id: 0, label: '' },
|
||||
selectors: {
|
||||
getLabel: R.prop('label'),
|
||||
getLabelAppended: (state) => (suffix) => state.label + ' ' + suffix,
|
||||
},
|
||||
});
|
||||
|
||||
const foo = new Updux({
|
||||
initial: [],
|
||||
findSelectors: {
|
||||
getBar: (state) => (id) => {
|
||||
return state.find(matches({ id }));
|
||||
},
|
||||
},
|
||||
selectors: {
|
||||
getNbrBars: (state) => state.length,
|
||||
},
|
||||
subduxes: {
|
||||
'*': bar,
|
||||
},
|
||||
});
|
||||
|
||||
const state = [
|
||||
{ id: 1, label: 'one' },
|
||||
{ id: 2, label: 'two' },
|
||||
];
|
||||
|
||||
const store = foo.createStore(state);
|
||||
|
||||
expect(foo.selectors.getBar(state)(2).state).toMatchObject(state[1]);
|
||||
expect(store.getState.getBar(2).state).toMatchObject(state[1]);
|
||||
|
||||
expect(store.getState.getNbrBars()).toBe(2);
|
||||
|
||||
expect(store.getState.getBar(1).getLabel()).toEqual('one');
|
||||
|
||||
expect(store.getState.getBar(1).getLabelAppended('plus one')).toEqual(
|
||||
'one plus one',
|
||||
);
|
||||
});
|
10
src/index.js
10
src/index.js
|
@ -1,8 +1,2 @@
|
|||
import fp from 'lodash/fp';
|
||||
import u from 'updeep';
|
||||
|
||||
import Updux from './updux';
|
||||
|
||||
export default function updux(config) {
|
||||
return new Updux(config);
|
||||
}
|
||||
export { Updux, dux } from './Updux.js';
|
||||
export { action } from './actions.js';
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
import { test, expect } from 'vitest';
|
||||
|
||||
import { Updux } from './Updux.js';
|
||||
|
||||
const bar = new Updux({ initial: 123 });
|
||||
|
||||
const foo = new Updux({
|
||||
initial: { root: 'abc' },
|
||||
subduxes: {
|
||||
bar,
|
||||
},
|
||||
});
|
||||
|
||||
test('single dux', () => {
|
||||
const foo = new Updux({
|
||||
initial: { a: 1 },
|
||||
});
|
||||
|
||||
expect(foo.initial).toEqual({ a: 1 });
|
||||
});
|
||||
|
||||
test('initial value', () => {
|
||||
expect(foo.initial).toEqual({
|
||||
root: 'abc',
|
||||
bar: 123,
|
||||
});
|
||||
});
|
||||
|
||||
test('splat initial', async () => {
|
||||
const bar = new Updux({
|
||||
initial: { id: 0 },
|
||||
});
|
||||
|
||||
const foo = new Updux({
|
||||
subduxes: { '*': bar },
|
||||
});
|
||||
|
||||
expect(foo.initial).toEqual([]);
|
||||
|
||||
expect(
|
||||
new Updux({
|
||||
initial: 'overriden',
|
||||
subduxes: { '*': bar },
|
||||
}).initial,
|
||||
).toEqual('overriden');
|
||||
});
|
|
@ -0,0 +1,85 @@
|
|||
import R from 'remeda';
|
||||
|
||||
const composeMw = (mws) => (api) => (original_next) =>
|
||||
mws.reduceRight((next, mw) => mw(api)(next), original_next);
|
||||
|
||||
export function augmentMiddlewareApi(api, actions, selectors) {
|
||||
const getState = () => api.getState();
|
||||
const dispatch = (action) => api.dispatch(action);
|
||||
|
||||
Object.assign(
|
||||
getState,
|
||||
R.mapValues(selectors, (selector) => {
|
||||
return (...args) => {
|
||||
let result = selector(api.getState());
|
||||
|
||||
if (typeof result === 'function') return result(...args);
|
||||
|
||||
return result;
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
Object.assign(
|
||||
dispatch,
|
||||
R.mapValues(actions, (action) => {
|
||||
return (...args) => api.dispatch(action(...args));
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
...api,
|
||||
getState,
|
||||
dispatch,
|
||||
actions,
|
||||
selectors,
|
||||
};
|
||||
}
|
||||
|
||||
const sliceMw = (slice, mw) => (api) => {
|
||||
const getSliceState = () => api.getState()[slice];
|
||||
return mw({ ...api, getState: getSliceState });
|
||||
};
|
||||
|
||||
const middlewareFor = (type, middleware) => (api) => (next) => (action) => {
|
||||
if (type !== '*' && action.type !== type) return next(action);
|
||||
|
||||
return middleware(api)(next)(action);
|
||||
};
|
||||
|
||||
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));
|
||||
};
|
||||
|
||||
export function buildMiddleware(
|
||||
effects = [],
|
||||
actions = {},
|
||||
selectors = {},
|
||||
subduxes = {},
|
||||
wrapper = undefined,
|
||||
) {
|
||||
let inner = R.compact(
|
||||
Object.entries(subduxes).map(([slice, { middleware }]) =>
|
||||
((slice !== '*' && middleware) ? sliceMw(slice, middleware) : null),
|
||||
),
|
||||
);
|
||||
|
||||
const local = effects.map((effect) =>
|
||||
effectToMiddleware(effect, actions, selectors),
|
||||
);
|
||||
|
||||
let mws = [...local, ...inner];
|
||||
|
||||
if( wrapper ) mws = wrapper(mws);
|
||||
|
||||
return composeMw(mws);
|
||||
}
|
|
@ -1,111 +1,48 @@
|
|||
import updux from '.';
|
||||
import u from 'updeep';
|
||||
import { test, expect, vi } from 'vitest';
|
||||
|
||||
test( 'simple effect', () => {
|
||||
import { buildMiddleware } from './middleware.js';
|
||||
import { action } from './actions.js';
|
||||
|
||||
const tracer = jest.fn();
|
||||
test('buildMiddleware, effects', async () => {
|
||||
const effectMock = vi.fn();
|
||||
|
||||
const store = updux({
|
||||
effects: {
|
||||
foo: api => next => action => {
|
||||
tracer();
|
||||
next(action);
|
||||
},
|
||||
},
|
||||
}).createStore();
|
||||
const mw = buildMiddleware([
|
||||
['*', (api) => (next) => (action) => effectMock()],
|
||||
]);
|
||||
|
||||
expect(tracer).not.toHaveBeenCalled();
|
||||
|
||||
store.dispatch({ type: 'bar' });
|
||||
|
||||
expect(tracer).not.toHaveBeenCalled();
|
||||
|
||||
store.dispatch.foo();
|
||||
|
||||
expect(tracer).toHaveBeenCalled();
|
||||
mw({})(() => {})({});
|
||||
|
||||
expect(effectMock).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
test( 'effect and sub-effect', () => {
|
||||
test('buildMiddleware, augmented api', async () => {
|
||||
const myAction = action('myAction');
|
||||
|
||||
const tracer = jest.fn();
|
||||
|
||||
const tracerEffect = signature => api => next => action => {
|
||||
tracer(signature);
|
||||
next(action);
|
||||
};
|
||||
|
||||
const store = updux({
|
||||
effects: {
|
||||
foo: tracerEffect('root'),
|
||||
const mw = buildMiddleware(
|
||||
[
|
||||
[
|
||||
'*',
|
||||
(api) => (next) => (action) => {
|
||||
expect(api.getState.mySelector()).toEqual(13);
|
||||
api.dispatch(myAction());
|
||||
next();
|
||||
},
|
||||
],
|
||||
],
|
||||
{
|
||||
myAction,
|
||||
},
|
||||
subduxes: {
|
||||
zzz: updux({effects: {
|
||||
foo: tracerEffect('child'),
|
||||
}})
|
||||
{
|
||||
mySelector: (state) => state?.selected,
|
||||
},
|
||||
}).createStore();
|
||||
|
||||
expect(tracer).not.toHaveBeenCalled();
|
||||
|
||||
store.dispatch({ type: 'bar' });
|
||||
|
||||
expect(tracer).not.toHaveBeenCalled();
|
||||
|
||||
store.dispatch.foo();
|
||||
|
||||
expect(tracer).toHaveBeenNthCalledWith(1,'root');
|
||||
expect(tracer).toHaveBeenNthCalledWith(2,'child');
|
||||
);
|
||||
|
||||
const dispatch = vi.fn();
|
||||
const getState = vi.fn(() => ({ selected: 13 }));
|
||||
const next = vi.fn();
|
||||
|
||||
mw({ dispatch, getState })(next)(myAction());
|
||||
|
||||
});
|
||||
|
||||
test( '"*" effect', () => {
|
||||
|
||||
const tracer = jest.fn();
|
||||
|
||||
const store = updux({
|
||||
effects: {
|
||||
'*': api => next => action => {
|
||||
tracer();
|
||||
next(action);
|
||||
},
|
||||
},
|
||||
}).createStore();
|
||||
|
||||
expect(tracer).not.toHaveBeenCalled();
|
||||
|
||||
store.dispatch({ type: 'bar' });
|
||||
|
||||
expect(tracer).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test( 'async effect', async () => {
|
||||
|
||||
function timeout(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
const tracer = jest.fn();
|
||||
|
||||
const store = updux({
|
||||
effects: {
|
||||
foo: api => next => async action => {
|
||||
next(action);
|
||||
await timeout(1000);
|
||||
tracer();
|
||||
},
|
||||
},
|
||||
}).createStore();
|
||||
|
||||
expect(tracer).not.toHaveBeenCalled();
|
||||
|
||||
store.dispatch.foo();
|
||||
|
||||
expect(tracer).not.toHaveBeenCalled();
|
||||
|
||||
await timeout(1000);
|
||||
|
||||
expect(tracer).toHaveBeenCalled();
|
||||
expect(next).toHaveBeenCalledOnce();
|
||||
expect(dispatch).toHaveBeenCalledWith(myAction());
|
||||
});
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
import { test, expect } from 'vitest';
|
||||
import schema from 'json-schema-shorthand';
|
||||
import u from 'updeep';
|
||||
|
||||
import { action } from './actions.js';
|
||||
|
||||
import { Updux, dux } from './Updux.js';
|
||||
|
||||
test('set a mutation', () => {
|
||||
const dux = new Updux({
|
||||
initial: {
|
||||
x: 'potato',
|
||||
},
|
||||
actions: {
|
||||
foo: action('foo', (x) => ({ x })),
|
||||
bar: action('bar'),
|
||||
},
|
||||
});
|
||||
|
||||
dux.setMutation(dux.actions.foo, (payload, action) => {
|
||||
expect(payload).toEqual({ x: 'hello ' });
|
||||
expect(action).toEqual(dux.actions.foo('hello '));
|
||||
return u({
|
||||
x: payload.x + action.type,
|
||||
});
|
||||
});
|
||||
|
||||
const result = dux.reducer(undefined, dux.actions.foo('hello '));
|
||||
expect(result).toEqual({
|
||||
x: 'hello foo',
|
||||
});
|
||||
});
|
||||
|
||||
test('mutation of a subdux', async () => {
|
||||
const bar = dux({
|
||||
actions: {
|
||||
baz: null,
|
||||
},
|
||||
});
|
||||
bar.setMutation('baz', () => (state) => ({ ...state, x: 1 }));
|
||||
|
||||
const foo = dux({
|
||||
subduxes: { bar },
|
||||
});
|
||||
|
||||
const store = foo.createStore();
|
||||
store.dispatch.baz();
|
||||
expect(store.getState()).toMatchObject({ bar: { x: 1 } });
|
||||
});
|
||||
|
||||
test('strings and generators', async () => {
|
||||
const actionA = action('a');
|
||||
|
||||
const foo = dux({
|
||||
actions: {
|
||||
b: null,
|
||||
a: actionA,
|
||||
},
|
||||
});
|
||||
|
||||
// as a string and defined
|
||||
expect(() => foo.setMutation('a', () => {})).not.toThrow();
|
||||
|
||||
// as a generator and defined
|
||||
expect(() => foo.setMutation(actionA, () => {})).not.toThrow();
|
||||
|
||||
// as a string, not defined
|
||||
expect(() => foo.setMutation('c', () => {})).toThrow();
|
||||
|
||||
foo.setMutation(action('d'), () => {});
|
||||
|
||||
expect(foo.actions.d).toBeTypeOf('function');
|
||||
});
|
||||
|
||||
test('splat mutation', () => {
|
||||
const myDux = new Updux({
|
||||
initial: [],
|
||||
actions: { one: null, two: null },
|
||||
mutations: {
|
||||
'*': (payload) => (state) => payload ? [...state, payload] : state,
|
||||
},
|
||||
});
|
||||
const store = myDux.createStore();
|
||||
expect(store.getState()).toEqual([]);
|
||||
|
||||
store.dispatch.one(11);
|
||||
store.dispatch.two(22);
|
||||
|
||||
expect(store.getState()).toEqual([11, 22]);
|
||||
});
|
|
@ -0,0 +1,50 @@
|
|||
import { test, expect, vi } from 'vitest';
|
||||
|
||||
import { Updux } from './Updux.js';
|
||||
|
||||
test('basic reactions', async () => {
|
||||
const spyA = vi.fn();
|
||||
const spyB = vi.fn();
|
||||
const foo = new Updux({
|
||||
initial: { i: 0 },
|
||||
reactions: [() => spyA],
|
||||
actions: { inc: null },
|
||||
mutations: {
|
||||
inc: () => (state) => ({ ...state, i: state.i + 1 }),
|
||||
},
|
||||
});
|
||||
|
||||
foo.addReaction((api) => spyB);
|
||||
|
||||
const store = foo.createStore();
|
||||
store.dispatch.inc();
|
||||
|
||||
expect(spyA).toHaveBeenCalledOnce();
|
||||
expect(spyB).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
test('subduxes reactions', async () => {
|
||||
const spyA = vi.fn();
|
||||
const spyB = vi.fn();
|
||||
const foo = new Updux({
|
||||
subduxes: {
|
||||
a: new Updux({
|
||||
initial: 1,
|
||||
reactions: [() => (state) => spyA(state)],
|
||||
actions: { inc: null },
|
||||
mutations: {
|
||||
inc: () => (state) => state + 1,
|
||||
},
|
||||
}),
|
||||
b: new Updux({ initial: 10, reactions: [() => spyB] }),
|
||||
},
|
||||
});
|
||||
|
||||
const store = foo.createStore();
|
||||
store.dispatch.inc();
|
||||
store.dispatch.inc();
|
||||
|
||||
expect(spyA).toHaveBeenCalledTimes(2);
|
||||
expect(spyA).toHaveBeenCalledWith(3);
|
||||
expect(spyB).toHaveBeenCalledOnce(); // the original inc initialized the state
|
||||
});
|
|
@ -0,0 +1,19 @@
|
|||
import { test, expect } from 'vitest';
|
||||
|
||||
import { Updux } from './Updux.js';
|
||||
|
||||
test('basic reducer', () => {
|
||||
const dux = new Updux({ initial: { a: 3 } });
|
||||
|
||||
expect(dux.reducer).toBeTypeOf('function');
|
||||
|
||||
expect(dux.reducer({ a: 1 }, { type: 'foo' })).toMatchObject({ a: 1 }); // noop
|
||||
});
|
||||
|
||||
test('basic upreducer', () => {
|
||||
const dux = new Updux({ initial: { a: 3 } });
|
||||
|
||||
expect(dux.upreducer).toBeTypeOf('function');
|
||||
|
||||
expect(dux.upreducer({ type: 'foo' })({ a: 1 })).toMatchObject({ a: 1 }); // noop
|
||||
});
|
|
@ -0,0 +1,43 @@
|
|||
import R from 'remeda';
|
||||
|
||||
export function buildSelectors(
|
||||
localSelectors,
|
||||
findSelectors = {},
|
||||
subduxes = {},
|
||||
) {
|
||||
const subSelectors = Object.entries(subduxes).map(
|
||||
([slice, { selectors }]) => {
|
||||
if (!selectors) return {};
|
||||
if (slice === '*') return {};
|
||||
|
||||
return R.mapValues(
|
||||
selectors,
|
||||
(func) => (state) => func(state[slice]),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
let splat = {};
|
||||
|
||||
for (const name in findSelectors) {
|
||||
splat[name] =
|
||||
(mainState) =>
|
||||
(...args) => {
|
||||
const state = findSelectors[name](mainState)(...args);
|
||||
|
||||
return R.merge(
|
||||
{ state },
|
||||
R.mapValues(
|
||||
subduxes['*']?.selectors ?? {},
|
||||
(selector) =>
|
||||
(...args) => {
|
||||
let value = selector(state);
|
||||
if (typeof value !== 'function') return value;
|
||||
return value(...args);
|
||||
},
|
||||
),
|
||||
);
|
||||
};
|
||||
}
|
||||
return R.mergeAll([...subSelectors, localSelectors, splat]);
|
||||
}
|
|
@ -1,63 +0,0 @@
|
|||
import updux from '.';
|
||||
import u from 'updeep';
|
||||
|
||||
const tracer = chr => u({ tracer: s => (s||'') + chr });
|
||||
|
||||
test( 'mutations, simple', () => {
|
||||
const dux = updux({
|
||||
mutations: {
|
||||
foo: () => tracer('a'),
|
||||
'*': (p,a) => tracer('b'),
|
||||
},
|
||||
});
|
||||
|
||||
const store = dux.createStore();
|
||||
|
||||
expect(store.getState()).toEqual({ tracer: 'b'});
|
||||
|
||||
store.dispatch.foo();
|
||||
|
||||
expect(store.getState()).toEqual({ tracer: 'ba', });
|
||||
|
||||
store.dispatch({ type: 'bar' });
|
||||
|
||||
expect(store.getState()).toEqual({ tracer: 'bab', });
|
||||
});
|
||||
|
||||
test( 'with subduxes', () => {
|
||||
const dux = updux({
|
||||
mutations: {
|
||||
foo: () => tracer('a'),
|
||||
'*': (dummy,a) => tracer('b'),
|
||||
bar: () => ({bar}) => ({ bar, tracer: bar.tracer })
|
||||
},
|
||||
subduxes: {
|
||||
bar: updux({
|
||||
mutations: {
|
||||
foo: () => tracer('d'),
|
||||
'*': (dummy,a) => tracer('e'),
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const store = dux.createStore();
|
||||
|
||||
expect(store.getState()).toEqual({
|
||||
tracer: 'b',
|
||||
bar: { tracer: 'e' } });
|
||||
|
||||
store.dispatch.foo();
|
||||
|
||||
expect(store.getState()).toEqual({
|
||||
tracer: 'ba',
|
||||
bar: { tracer: 'ed' } });
|
||||
|
||||
store.dispatch({type: 'bar'});
|
||||
|
||||
expect(store.getState()).toEqual({
|
||||
tracer: 'ede',
|
||||
bar: { tracer: 'ede' } });
|
||||
|
||||
|
||||
});
|
|
@ -0,0 +1,127 @@
|
|||
import { test, expect, vi, describe } from 'vitest';
|
||||
import u from 'updeep';
|
||||
|
||||
import { Updux } from './Updux.js';
|
||||
import { matches } from './utils';
|
||||
|
||||
const reactionSnitch = vi.fn();
|
||||
const thingReactionSnitch = vi.fn();
|
||||
|
||||
const subThing = new Updux({
|
||||
name: 'subThing',
|
||||
initial: 0,
|
||||
});
|
||||
|
||||
subThing.addReaction((api) => (state, previousState, unsubscribe) => {
|
||||
reactionSnitch({ ...api, state, previousState });
|
||||
});
|
||||
|
||||
const thing = new Updux({
|
||||
name: 'thing',
|
||||
initial: {},
|
||||
subduxes: {
|
||||
'*': subThing,
|
||||
},
|
||||
actions: {
|
||||
setSubThing: (id, value, thingId) => ({ thingId, id, value }),
|
||||
deleteSubThing: (id) => id,
|
||||
},
|
||||
mutations: {
|
||||
setSubThing: ({ id, value }) => u.updateIn(id, value),
|
||||
deleteSubThing: (id) => u.updateIn(id, u.omitted),
|
||||
},
|
||||
splatReactionMapper: ({ id }) => id,
|
||||
});
|
||||
|
||||
thing.addReaction((api) => (state, previousState, unsubscribe) => {
|
||||
thingReactionSnitch({ ...api, state, previousState });
|
||||
});
|
||||
|
||||
const things = new Updux({
|
||||
subduxes: {
|
||||
'*': thing,
|
||||
},
|
||||
initial: {},
|
||||
actions: { newThing: (id) => id },
|
||||
splatReactionMapper: ({ id }) => id,
|
||||
mutations: {
|
||||
newThing: (id) => (state) => ({ ...state, [id]: thing.initial }),
|
||||
},
|
||||
});
|
||||
|
||||
things.setMutation(
|
||||
'setSubThing',
|
||||
({ thingId }, action) => u.updateIn(thingId, thing.upreducer(action)),
|
||||
true,
|
||||
);
|
||||
|
||||
describe('just one level', () => {
|
||||
const store = thing.createStore();
|
||||
|
||||
test('set', async () => {
|
||||
store.dispatch.setSubThing('a', 13);
|
||||
|
||||
expect(reactionSnitch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ state: 13 }),
|
||||
);
|
||||
});
|
||||
|
||||
test('other key', async () => {
|
||||
reactionSnitch.mockReset();
|
||||
|
||||
store.dispatch.setSubThing('b', 23);
|
||||
|
||||
expect(reactionSnitch).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({ state: 13 }),
|
||||
);
|
||||
expect(reactionSnitch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ state: 23 }),
|
||||
);
|
||||
});
|
||||
|
||||
test('delete', async () => {
|
||||
reactionSnitch.mockReset();
|
||||
|
||||
store.dispatch.deleteSubThing('a');
|
||||
|
||||
expect(reactionSnitch).toHaveBeenCalledOnce();
|
||||
|
||||
expect(reactionSnitch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ state: null }),
|
||||
);
|
||||
});
|
||||
|
||||
test('context', async () => {
|
||||
expect(reactionSnitch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
context: { subThing: 'a' },
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('two levels', async () => {
|
||||
const store = things.createStore();
|
||||
|
||||
reactionSnitch.mockReset();
|
||||
thingReactionSnitch.mockReset();
|
||||
|
||||
store.dispatch.newThing('alpha');
|
||||
store.dispatch.newThing('beta');
|
||||
store.dispatch.setSubThing('a', 13, 'alpha');
|
||||
|
||||
expect(reactionSnitch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
context: { thing: 'alpha', subThing: 'a' },
|
||||
state: 13,
|
||||
}),
|
||||
);
|
||||
expect(thingReactionSnitch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
context: {
|
||||
thing: 'alpha',
|
||||
},
|
||||
state: { a: 13 },
|
||||
}),
|
||||
);
|
||||
});
|
171
src/test.js
171
src/test.js
|
@ -1,171 +0,0 @@
|
|||
import updux from '.';
|
||||
|
||||
test('actions from mutations', () => {
|
||||
const {
|
||||
actions: {foo, bar},
|
||||
} = updux({
|
||||
mutations: {
|
||||
foo: () => x => x,
|
||||
},
|
||||
});
|
||||
|
||||
expect(foo()).toEqual({type: 'foo'});
|
||||
|
||||
expect(foo(true)).toEqual({type: 'foo', payload: true});
|
||||
|
||||
expect(foo({bar: 2}, {timestamp: 613})).toEqual({
|
||||
type: 'foo',
|
||||
payload: {bar: 2},
|
||||
meta: {timestamp: 613},
|
||||
});
|
||||
});
|
||||
|
||||
test('reducer', () => {
|
||||
const {actions, reducer} = updux({
|
||||
initial: {counter: 1},
|
||||
mutations: {
|
||||
inc: () => ({counter}) => ({counter: counter + 1}),
|
||||
},
|
||||
});
|
||||
|
||||
let state = reducer(null, {});
|
||||
|
||||
expect(state).toEqual({counter: 1});
|
||||
|
||||
state = reducer(state, actions.inc());
|
||||
|
||||
expect(state).toEqual({counter: 2});
|
||||
});
|
||||
|
||||
test( 'sub reducers', () => {
|
||||
const foo = updux({
|
||||
initial: 1,
|
||||
mutations: {
|
||||
doFoo: () => (x) => x + 1,
|
||||
doAll: () => x => x + 10,
|
||||
},
|
||||
});
|
||||
|
||||
const bar = updux({
|
||||
initial: 'a',
|
||||
mutations: {
|
||||
doBar: () => x => x + 'a',
|
||||
doAll: () => x => x + 'b',
|
||||
}
|
||||
});
|
||||
|
||||
const { initial, actions, reducer } = updux({
|
||||
subduxes: {
|
||||
foo, bar
|
||||
}
|
||||
});
|
||||
|
||||
expect(initial).toEqual({ foo: 1, bar: 'a' });
|
||||
|
||||
expect(Object.keys(actions)).toHaveLength(3);
|
||||
|
||||
let state = reducer(null,{});
|
||||
|
||||
expect(state).toEqual({ foo: 1, bar: 'a' });
|
||||
|
||||
state = reducer(state, actions.doFoo() );
|
||||
|
||||
expect(state).toEqual({ foo: 2, bar: 'a' });
|
||||
|
||||
state = reducer(state, actions.doBar() );
|
||||
|
||||
expect(state).toEqual({ foo: 2, bar: 'aa' });
|
||||
|
||||
state = reducer(state, actions.doAll() );
|
||||
|
||||
expect(state).toEqual({ foo: 12, bar: 'aab' });
|
||||
|
||||
});
|
||||
|
||||
test('precedence between root and sub-reducers', () => {
|
||||
const {
|
||||
initial,
|
||||
reducer,
|
||||
actions,
|
||||
} = updux({
|
||||
initial: {
|
||||
foo: { bar: 4 },
|
||||
},
|
||||
mutations: {
|
||||
inc: () => state => {
|
||||
return {
|
||||
...state,
|
||||
surprise: state.foo.bar
|
||||
}
|
||||
}
|
||||
},
|
||||
subduxes: {
|
||||
foo: updux({
|
||||
initial: {
|
||||
bar: 2,
|
||||
quux: 3,
|
||||
},
|
||||
mutations: {
|
||||
inc: () => state => ({...state, bar: state.bar + 1 })
|
||||
},
|
||||
}),
|
||||
}
|
||||
});
|
||||
|
||||
expect(initial).toEqual({
|
||||
foo: { bar: 4, quux: 3 }
|
||||
});
|
||||
|
||||
expect( reducer(null,actions.inc() ) ).toEqual({
|
||||
foo: { bar: 5, quux: 3 }, surprise: 5
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
function timeout(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
test( 'middleware', async () => {
|
||||
const {
|
||||
middleware,
|
||||
createStore
|
||||
} = updux({
|
||||
initial: "",
|
||||
mutations: {
|
||||
inc: (addition) => state => state + addition,
|
||||
doEeet: () => state => {
|
||||
return state + 'Z';
|
||||
},
|
||||
},
|
||||
effects: {
|
||||
doEeet: api => next => async action => {
|
||||
api.dispatch.inc('a');
|
||||
next(action);
|
||||
await timeout(1000);
|
||||
api.dispatch.inc('c');
|
||||
}
|
||||
},
|
||||
subduxes: {
|
||||
foo: updux({
|
||||
effects: {
|
||||
doEeet: (api) => next => action => {
|
||||
api.dispatch({ type: 'inc', payload: 'b'});
|
||||
next(action);
|
||||
}
|
||||
}
|
||||
}),
|
||||
}
|
||||
});
|
||||
|
||||
const store = createStore();
|
||||
|
||||
store.dispatch.doEeet();
|
||||
|
||||
expect(store.getState()).toEqual( 'abZ' );
|
||||
|
||||
await timeout(1000);
|
||||
|
||||
expect(store.getState()).toEqual( 'abZc' );
|
||||
|
||||
});
|
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.mutations,
|
||||
config.effects,
|
||||
fp.mergeAll( 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;
|
|
@ -0,0 +1,40 @@
|
|||
import R from 'remeda';
|
||||
import u from 'updeep';
|
||||
|
||||
const localMutation = (mutations) => (action) => (state) => {
|
||||
const mutation = mutations[action.type];
|
||||
|
||||
const splatMutation = mutations['*'];
|
||||
|
||||
if (mutation) state = mutation(action.payload, action)(state);
|
||||
|
||||
if (splatMutation) state = splatMutation(action.payload, action)(state);
|
||||
|
||||
return state;
|
||||
};
|
||||
|
||||
const subMutations = (subduxes) => (action) => (state) => {
|
||||
const subReducers =
|
||||
Object.keys(subduxes).length > 0
|
||||
? R.mapValues(subduxes, R.prop('upreducer'))
|
||||
: null;
|
||||
|
||||
if (!subReducers) return state;
|
||||
|
||||
if (subReducers['*']) {
|
||||
return u.updateIn('*', subReducers['*'](action), state);
|
||||
}
|
||||
|
||||
const update = R.mapValues(subReducers, (upReducer) => upReducer(action));
|
||||
|
||||
return u(update, state);
|
||||
};
|
||||
|
||||
export function buildUpreducer(mutations, subduxes) {
|
||||
return (action) => (state) => {
|
||||
if (!mutations[action.type]?.terminal)
|
||||
state = subMutations(subduxes)(action)(state);
|
||||
|
||||
return localMutation(mutations)(action)(state);
|
||||
};
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
export const matches = (conditions) => (target) =>
|
||||
Object.entries(conditions).every(([key, value]) =>
|
||||
typeof value === 'function'
|
||||
? value(target[key])
|
||||
: target[key] === value,
|
||||
);
|
|
@ -1,63 +0,0 @@
|
|||
{
|
||||
"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. */
|
||||
// "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. */
|
||||
"declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
|
||||
"sourceMap": true, /* Generates corresponding '.map' file. */
|
||||
// "outFile": "./", /* Concatenate and emit output to single file. */
|
||||
"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. */
|
||||
// "composite": true, /* Enable project compilation */
|
||||
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
|
||||
"removeComments": true, /* Do not emit comments to output. */
|
||||
// "noEmit": true, /* Do not emit outputs. */
|
||||
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
|
||||
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
|
||||
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
|
||||
|
||||
/* 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. */
|
||||
// "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. */
|
||||
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
|
||||
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
|
||||
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
|
||||
|
||||
/* Additional Checks */
|
||||
// "noUnusedLocals": true, /* Report errors on unused locals. */
|
||||
// "noUnusedParameters": true, /* Report errors on unused parameters. */
|
||||
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
||||
|
||||
/* Module Resolution Options */
|
||||
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
||||
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
|
||||
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
||||
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
||||
// "typeRoots": [], /* List of folders to include type definitions from. */
|
||||
// "types": [], /* Type declaration files to be included in compilation. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
|
||||
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
|
||||
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
|
||||
/* Source Map Options */
|
||||
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
|
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
|
||||
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
|
||||
|
||||
/* Experimental Options */
|
||||
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
{
|
||||
"theme": "minimal",
|
||||
"tsBuildInfoFile": true,
|
||||
"out": "docs",
|
||||
"mode": "file",
|
||||
"excludePrivate": true,
|
||||
"excludeNotExported": false
|
||||
}
|
Loading…
Reference in New Issue