WIP
This commit is contained in:
parent
99ba091a51
commit
74c29ce2d0
32
.eslintrc.js
32
.eslintrc.js
@ -1,32 +0,0 @@
|
|||||||
// @format
|
|
||||||
|
|
||||||
// https://dev.to/robertcoopercode/using-eslint-and-prettier-in-a-typescript-project-53jb
|
|
||||||
|
|
||||||
const _ = require('lodash');
|
|
||||||
|
|
||||||
const ts_nope = [
|
|
||||||
'no-explicit-any',
|
|
||||||
'explicit-function-return-type',
|
|
||||||
'no-object-literal-type-assertion',
|
|
||||||
'camelcase',
|
|
||||||
'member-delimiter-style',
|
|
||||||
'prefer-interface',
|
|
||||||
'indent',
|
|
||||||
].map(r => '@typescript-eslint/' + r);
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
parser: '@typescript-eslint/parser', // Specifies the ESLint parser
|
|
||||||
extends: [
|
|
||||||
'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin
|
|
||||||
],
|
|
||||||
plugins: ['import'],
|
|
||||||
parserOptions: {
|
|
||||||
ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features
|
|
||||||
sourceType: 'module', // Allows for the use of imports
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
..._.fromPairs(ts_nope.map(r => [r, 'off'])),
|
|
||||||
// Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs
|
|
||||||
// e.g. "@typescript-eslint/explicit-function-return-type": "off",
|
|
||||||
},
|
|
||||||
};
|
|
32
Promake
32
Promake
@ -1,32 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
const Promake = require('promake');
|
|
||||||
const glob = require('glob').sync;
|
|
||||||
|
|
||||||
const {
|
|
||||||
task, cli, rule, exec
|
|
||||||
} = new Promake();
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const compile = task( 'compile', rule( 'tsconfig.tsbuildinfo', glob('src/**.ts'),
|
|
||||||
async () => {
|
|
||||||
return exec( 'tsc' );
|
|
||||||
}) );
|
|
||||||
|
|
||||||
const docs = task( 'docs', [ rule(
|
|
||||||
glob('docs/4-API/*.md'),
|
|
||||||
[ ...glob('./src/**.ts'), compile ], async() => {
|
|
||||||
await exec( "typedoc src" );
|
|
||||||
}
|
|
||||||
)]);
|
|
||||||
|
|
||||||
const sidebar = task(
|
|
||||||
'sidebar',
|
|
||||||
rule('./docs/_sidebar.md', [...glob('./docs/4-API/**.md'), docs], async () => {
|
|
||||||
return exec('docsify-auto-sidebar -d docs');
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
cli();
|
|
@ -1,369 +0,0 @@
|
|||||||
/**
|
|
||||||
* Config file for API Extractor. For more info, please visit: https://api-extractor.com
|
|
||||||
*/
|
|
||||||
{
|
|
||||||
"$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Optionally specifies another JSON config file that this file extends from. This provides a way for
|
|
||||||
* standard settings to be shared across multiple projects.
|
|
||||||
*
|
|
||||||
* If the path starts with "./" or "../", the path is resolved relative to the folder of the file that contains
|
|
||||||
* the "extends" field. Otherwise, the first path segment is interpreted as an NPM package name, and will be
|
|
||||||
* resolved using NodeJS require().
|
|
||||||
*
|
|
||||||
* SUPPORTED TOKENS: none
|
|
||||||
* DEFAULT VALUE: ""
|
|
||||||
*/
|
|
||||||
// "extends": "./shared/api-extractor-base.json"
|
|
||||||
// "extends": "my-package/include/api-extractor-base.json"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines the "<projectFolder>" token that can be used with other config file settings. The project folder
|
|
||||||
* typically contains the tsconfig.json and package.json config files, but the path is user-defined.
|
|
||||||
*
|
|
||||||
* The path is resolved relative to the folder of the config file that contains the setting.
|
|
||||||
*
|
|
||||||
* The default value for "projectFolder" is the token "<lookup>", which means the folder is determined by traversing
|
|
||||||
* parent folders, starting from the folder containing api-extractor.json, and stopping at the first folder
|
|
||||||
* that contains a tsconfig.json file. If a tsconfig.json file cannot be found in this way, then an error
|
|
||||||
* will be reported.
|
|
||||||
*
|
|
||||||
* SUPPORTED TOKENS: <lookup>
|
|
||||||
* DEFAULT VALUE: "<lookup>"
|
|
||||||
*/
|
|
||||||
// "projectFolder": "..",
|
|
||||||
|
|
||||||
/**
|
|
||||||
* (REQUIRED) Specifies the .d.ts file to be used as the starting point for analysis. API Extractor
|
|
||||||
* analyzes the symbols exported by this module.
|
|
||||||
*
|
|
||||||
* The file extension must be ".d.ts" and not ".ts".
|
|
||||||
*
|
|
||||||
* The path is resolved relative to the folder of the config file that contains the setting; to change this,
|
|
||||||
* prepend a folder token such as "<projectFolder>".
|
|
||||||
*
|
|
||||||
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
|
|
||||||
*/
|
|
||||||
"mainEntryPointFilePath": "./dist/index.d.ts",
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A list of NPM package names whose exports should be treated as part of this package.
|
|
||||||
*
|
|
||||||
* For example, suppose that Webpack is used to generate a distributed bundle for the project "library1",
|
|
||||||
* and another NPM package "library2" is embedded in this bundle. Some types from library2 may become part
|
|
||||||
* of the exported API for library1, but by default API Extractor would generate a .d.ts rollup that explicitly
|
|
||||||
* imports library2. To avoid this, we can specify:
|
|
||||||
*
|
|
||||||
* "bundledPackages": [ "library2" ],
|
|
||||||
*
|
|
||||||
* This would direct API Extractor to embed those types directly in the .d.ts rollup, as if they had been
|
|
||||||
* local files for library1.
|
|
||||||
*/
|
|
||||||
"bundledPackages": [ ],
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines how the TypeScript compiler engine will be invoked by API Extractor.
|
|
||||||
*/
|
|
||||||
"compiler": {
|
|
||||||
/**
|
|
||||||
* Specifies the path to the tsconfig.json file to be used by API Extractor when analyzing the project.
|
|
||||||
*
|
|
||||||
* The path is resolved relative to the folder of the config file that contains the setting; to change this,
|
|
||||||
* prepend a folder token such as "<projectFolder>".
|
|
||||||
*
|
|
||||||
* Note: This setting will be ignored if "overrideTsconfig" is used.
|
|
||||||
*
|
|
||||||
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
|
|
||||||
* DEFAULT VALUE: "<projectFolder>/tsconfig.json"
|
|
||||||
*/
|
|
||||||
// "tsconfigFilePath": "<projectFolder>/tsconfig.json",
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides a compiler configuration that will be used instead of reading the tsconfig.json file from disk.
|
|
||||||
* The object must conform to the TypeScript tsconfig schema:
|
|
||||||
*
|
|
||||||
* http://json.schemastore.org/tsconfig
|
|
||||||
*
|
|
||||||
* If omitted, then the tsconfig.json file will be read from the "projectFolder".
|
|
||||||
*
|
|
||||||
* DEFAULT VALUE: no overrideTsconfig section
|
|
||||||
*/
|
|
||||||
// "overrideTsconfig": {
|
|
||||||
// . . .
|
|
||||||
// }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This option causes the compiler to be invoked with the --skipLibCheck option. This option is not recommended
|
|
||||||
* and may cause API Extractor to produce incomplete or incorrect declarations, but it may be required when
|
|
||||||
* dependencies contain declarations that are incompatible with the TypeScript engine that API Extractor uses
|
|
||||||
* for its analysis. Where possible, the underlying issue should be fixed rather than relying on skipLibCheck.
|
|
||||||
*
|
|
||||||
* DEFAULT VALUE: false
|
|
||||||
*/
|
|
||||||
// "skipLibCheck": true,
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configures how the API report file (*.api.md) will be generated.
|
|
||||||
*/
|
|
||||||
"apiReport": {
|
|
||||||
/**
|
|
||||||
* (REQUIRED) Whether to generate an API report.
|
|
||||||
*/
|
|
||||||
"enabled": true,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The filename for the API report files. It will be combined with "reportFolder" or "reportTempFolder" to produce
|
|
||||||
* a full file path.
|
|
||||||
*
|
|
||||||
* The file extension should be ".api.md", and the string should not contain a path separator such as "\" or "/".
|
|
||||||
*
|
|
||||||
* SUPPORTED TOKENS: <packageName>, <unscopedPackageName>
|
|
||||||
* DEFAULT VALUE: "<unscopedPackageName>.api.md"
|
|
||||||
*/
|
|
||||||
// "reportFileName": "<unscopedPackageName>.api.md",
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Specifies the folder where the API report file is written. The file name portion is determined by
|
|
||||||
* the "reportFileName" setting.
|
|
||||||
*
|
|
||||||
* The API report file is normally tracked by Git. Changes to it can be used to trigger a branch policy,
|
|
||||||
* e.g. for an API review.
|
|
||||||
*
|
|
||||||
* The path is resolved relative to the folder of the config file that contains the setting; to change this,
|
|
||||||
* prepend a folder token such as "<projectFolder>".
|
|
||||||
*
|
|
||||||
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
|
|
||||||
* DEFAULT VALUE: "<projectFolder>/etc/"
|
|
||||||
*/
|
|
||||||
"reportFolder": "<projectFolder>/temp/",
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Specifies the folder where the temporary report file is written. The file name portion is determined by
|
|
||||||
* the "reportFileName" setting.
|
|
||||||
*
|
|
||||||
* After the temporary file is written to disk, it is compared with the file in the "reportFolder".
|
|
||||||
* If they are different, a production build will fail.
|
|
||||||
*
|
|
||||||
* The path is resolved relative to the folder of the config file that contains the setting; to change this,
|
|
||||||
* prepend a folder token such as "<projectFolder>".
|
|
||||||
*
|
|
||||||
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
|
|
||||||
* DEFAULT VALUE: "<projectFolder>/temp/"
|
|
||||||
*/
|
|
||||||
"reportTempFolder": "<projectFolder>/temp/"
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configures how the doc model file (*.api.json) will be generated.
|
|
||||||
*/
|
|
||||||
"docModel": {
|
|
||||||
/**
|
|
||||||
* (REQUIRED) Whether to generate a doc model file.
|
|
||||||
*/
|
|
||||||
"enabled": true,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The output path for the doc model file. The file extension should be ".api.json".
|
|
||||||
*
|
|
||||||
* The path is resolved relative to the folder of the config file that contains the setting; to change this,
|
|
||||||
* prepend a folder token such as "<projectFolder>".
|
|
||||||
*
|
|
||||||
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
|
|
||||||
* DEFAULT VALUE: "<projectFolder>/temp/<unscopedPackageName>.api.json"
|
|
||||||
*/
|
|
||||||
// "apiJsonFilePath": "<projectFolder>/temp/<unscopedPackageName>.api.json"
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configures how the .d.ts rollup file will be generated.
|
|
||||||
*/
|
|
||||||
"dtsRollup": {
|
|
||||||
/**
|
|
||||||
* (REQUIRED) Whether to generate the .d.ts rollup file.
|
|
||||||
*/
|
|
||||||
"enabled": true,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Specifies the output path for a .d.ts rollup file to be generated without any trimming.
|
|
||||||
* This file will include all declarations that are exported by the main entry point.
|
|
||||||
*
|
|
||||||
* If the path is an empty string, then this file will not be written.
|
|
||||||
*
|
|
||||||
* The path is resolved relative to the folder of the config file that contains the setting; to change this,
|
|
||||||
* prepend a folder token such as "<projectFolder>".
|
|
||||||
*
|
|
||||||
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
|
|
||||||
* DEFAULT VALUE: "<projectFolder>/dist/<unscopedPackageName>.d.ts"
|
|
||||||
*/
|
|
||||||
// "untrimmedFilePath": "<projectFolder>/dist/<unscopedPackageName>.d.ts",
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Specifies the output path for a .d.ts rollup file to be generated with trimming for a "beta" release.
|
|
||||||
* This file will include only declarations that are marked as "@public" or "@beta".
|
|
||||||
*
|
|
||||||
* The path is resolved relative to the folder of the config file that contains the setting; to change this,
|
|
||||||
* prepend a folder token such as "<projectFolder>".
|
|
||||||
*
|
|
||||||
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
|
|
||||||
* DEFAULT VALUE: ""
|
|
||||||
*/
|
|
||||||
// "betaTrimmedFilePath": "<projectFolder>/dist/<unscopedPackageName>-beta.d.ts",
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Specifies the output path for a .d.ts rollup file to be generated with trimming for a "public" release.
|
|
||||||
* This file will include only declarations that are marked as "@public".
|
|
||||||
*
|
|
||||||
* If the path is an empty string, then this file will not be written.
|
|
||||||
*
|
|
||||||
* The path is resolved relative to the folder of the config file that contains the setting; to change this,
|
|
||||||
* prepend a folder token such as "<projectFolder>".
|
|
||||||
*
|
|
||||||
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
|
|
||||||
* DEFAULT VALUE: ""
|
|
||||||
*/
|
|
||||||
// "publicTrimmedFilePath": "<projectFolder>/dist/<unscopedPackageName>-public.d.ts",
|
|
||||||
|
|
||||||
/**
|
|
||||||
* When a declaration is trimmed, by default it will be replaced by a code comment such as
|
|
||||||
* "Excluded from this release type: exampleMember". Set "omitTrimmingComments" to true to remove the
|
|
||||||
* declaration completely.
|
|
||||||
*
|
|
||||||
* DEFAULT VALUE: false
|
|
||||||
*/
|
|
||||||
// "omitTrimmingComments": true
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configures how the tsdoc-metadata.json file will be generated.
|
|
||||||
*/
|
|
||||||
"tsdocMetadata": {
|
|
||||||
/**
|
|
||||||
* Whether to generate the tsdoc-metadata.json file.
|
|
||||||
*
|
|
||||||
* DEFAULT VALUE: true
|
|
||||||
*/
|
|
||||||
// "enabled": true,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Specifies where the TSDoc metadata file should be written.
|
|
||||||
*
|
|
||||||
* The path is resolved relative to the folder of the config file that contains the setting; to change this,
|
|
||||||
* prepend a folder token such as "<projectFolder>".
|
|
||||||
*
|
|
||||||
* The default value is "<lookup>", which causes the path to be automatically inferred from the "tsdocMetadata",
|
|
||||||
* "typings" or "main" fields of the project's package.json. If none of these fields are set, the lookup
|
|
||||||
* falls back to "tsdoc-metadata.json" in the package folder.
|
|
||||||
*
|
|
||||||
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
|
|
||||||
* DEFAULT VALUE: "<lookup>"
|
|
||||||
*/
|
|
||||||
// "tsdocMetadataFilePath": "<projectFolder>/dist/tsdoc-metadata.json"
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Specifies what type of newlines API Extractor should use when writing output files. By default, the output files
|
|
||||||
* will be written with Windows-style newlines. To use POSIX-style newlines, specify "lf" instead.
|
|
||||||
* To use the OS's default newline kind, specify "os".
|
|
||||||
*
|
|
||||||
* DEFAULT VALUE: "crlf"
|
|
||||||
*/
|
|
||||||
// "newlineKind": "crlf",
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configures how API Extractor reports error and warning messages produced during analysis.
|
|
||||||
*
|
|
||||||
* There are three sources of messages: compiler messages, API Extractor messages, and TSDoc messages.
|
|
||||||
*/
|
|
||||||
"messages": {
|
|
||||||
/**
|
|
||||||
* Configures handling of diagnostic messages reported by the TypeScript compiler engine while analyzing
|
|
||||||
* the input .d.ts files.
|
|
||||||
*
|
|
||||||
* TypeScript message identifiers start with "TS" followed by an integer. For example: "TS2551"
|
|
||||||
*
|
|
||||||
* DEFAULT VALUE: A single "default" entry with logLevel=warning.
|
|
||||||
*/
|
|
||||||
"compilerMessageReporting": {
|
|
||||||
/**
|
|
||||||
* Configures the default routing for messages that don't match an explicit rule in this table.
|
|
||||||
*/
|
|
||||||
"default": {
|
|
||||||
/**
|
|
||||||
* Specifies whether the message should be written to the the tool's output log. Note that
|
|
||||||
* the "addToApiReportFile" property may supersede this option.
|
|
||||||
*
|
|
||||||
* Possible values: "error", "warning", "none"
|
|
||||||
*
|
|
||||||
* Errors cause the build to fail and return a nonzero exit code. Warnings cause a production build fail
|
|
||||||
* and return a nonzero exit code. For a non-production build (e.g. when "api-extractor run" includes
|
|
||||||
* the "--local" option), the warning is displayed but the build will not fail.
|
|
||||||
*
|
|
||||||
* DEFAULT VALUE: "warning"
|
|
||||||
*/
|
|
||||||
"logLevel": "warning",
|
|
||||||
|
|
||||||
/**
|
|
||||||
* When addToApiReportFile is true: If API Extractor is configured to write an API report file (.api.md),
|
|
||||||
* then the message will be written inside that file; otherwise, the message is instead logged according to
|
|
||||||
* the "logLevel" option.
|
|
||||||
*
|
|
||||||
* DEFAULT VALUE: false
|
|
||||||
*/
|
|
||||||
// "addToApiReportFile": false
|
|
||||||
},
|
|
||||||
|
|
||||||
// "TS2551": {
|
|
||||||
// "logLevel": "warning",
|
|
||||||
// "addToApiReportFile": true
|
|
||||||
// },
|
|
||||||
//
|
|
||||||
// . . .
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configures handling of messages reported by API Extractor during its analysis.
|
|
||||||
*
|
|
||||||
* API Extractor message identifiers start with "ae-". For example: "ae-extra-release-tag"
|
|
||||||
*
|
|
||||||
* DEFAULT VALUE: See api-extractor-defaults.json for the complete table of extractorMessageReporting mappings
|
|
||||||
*/
|
|
||||||
"extractorMessageReporting": {
|
|
||||||
"default": {
|
|
||||||
"logLevel": "warning",
|
|
||||||
// "addToApiReportFile": false
|
|
||||||
},
|
|
||||||
|
|
||||||
// "ae-extra-release-tag": {
|
|
||||||
// "logLevel": "warning",
|
|
||||||
// "addToApiReportFile": true
|
|
||||||
// },
|
|
||||||
//
|
|
||||||
// . . .
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configures handling of messages reported by the TSDoc parser when analyzing code comments.
|
|
||||||
*
|
|
||||||
* TSDoc message identifiers start with "tsdoc-". For example: "tsdoc-link-tag-unescaped-text"
|
|
||||||
*
|
|
||||||
* DEFAULT VALUE: A single "default" entry with logLevel=warning.
|
|
||||||
*/
|
|
||||||
"tsdocMessageReporting": {
|
|
||||||
"default": {
|
|
||||||
"logLevel": "warning",
|
|
||||||
// "addToApiReportFile": false
|
|
||||||
}
|
|
||||||
|
|
||||||
// "tsdoc-link-tag-unescaped-text": {
|
|
||||||
// "logLevel": "warning",
|
|
||||||
// "addToApiReportFile": true
|
|
||||||
// },
|
|
||||||
//
|
|
||||||
// . . .
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
1
index.d.ts
vendored
1
index.d.ts
vendored
@ -1 +0,0 @@
|
|||||||
|
|
@ -1,17 +0,0 @@
|
|||||||
import { expectType, expectError } from 'tsd';
|
|
||||||
|
|
||||||
import buildInitial from './src/buildInitial';
|
|
||||||
|
|
||||||
expectType<{}>(buildInitial());
|
|
||||||
|
|
||||||
type MyState = {
|
|
||||||
foo: {
|
|
||||||
bar: number
|
|
||||||
},
|
|
||||||
baz: string,
|
|
||||||
}
|
|
||||||
|
|
||||||
expectType<MyState>(buildInitial<MyState>());
|
|
||||||
|
|
||||||
expectError( buildInitial<MyState>({ foo: { bar: "potato" } }) );
|
|
||||||
|
|
@ -1,121 +0,0 @@
|
|||||||
import Updux, { dux, coduxes } from '.';
|
|
||||||
import { action, payload } from 'ts-action';
|
|
||||||
import { test } from 'tap';
|
|
||||||
import { expectAssignable } from 'tsd';
|
|
||||||
import { DuxActionsCoduxes } from './types';
|
|
||||||
|
|
||||||
const foo_actions = {
|
|
||||||
aaa: action('aaa', payload<number>()),
|
|
||||||
};
|
|
||||||
|
|
||||||
const fooDux = dux({
|
|
||||||
actions: foo_actions,
|
|
||||||
});
|
|
||||||
|
|
||||||
const bar_actions = {
|
|
||||||
bbb: action('bbb', (x: string) => ({ payload: '1' + x })),
|
|
||||||
};
|
|
||||||
|
|
||||||
test('actions are present', t => {
|
|
||||||
const barDux = dux({
|
|
||||||
subduxes: { foo: fooDux },
|
|
||||||
actions: bar_actions,
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = Object.keys(barDux.actions);
|
|
||||||
result.sort();
|
|
||||||
t.same(result, ['aaa', 'bbb']);
|
|
||||||
t.end();
|
|
||||||
});
|
|
||||||
|
|
||||||
const a = action('a');
|
|
||||||
const b = action('b');
|
|
||||||
|
|
||||||
test('typing', t => {
|
|
||||||
t.test('nothing at all', t => {
|
|
||||||
const foo = new Updux();
|
|
||||||
t.same(foo.actions, {});
|
|
||||||
t.end();
|
|
||||||
});
|
|
||||||
|
|
||||||
t.test('with two coduxes', t => {
|
|
||||||
const myDux = new Updux({
|
|
||||||
...coduxes(dux({ actions: { a } }), dux({ actions: { b } })),
|
|
||||||
});
|
|
||||||
|
|
||||||
t.ok(myDux.actions.a);
|
|
||||||
t.end();
|
|
||||||
});
|
|
||||||
|
|
||||||
t.test('empty dux', t => {
|
|
||||||
const empty = dux({});
|
|
||||||
|
|
||||||
expectAssignable<object>(empty.actions);
|
|
||||||
|
|
||||||
t.same(empty.actions, {}, 'no actions there');
|
|
||||||
|
|
||||||
t.end();
|
|
||||||
});
|
|
||||||
|
|
||||||
t.test('coduxes actions', t => {
|
|
||||||
const typeOf = <C>(x: C) => (x as any) as DuxActionsCoduxes<C>;
|
|
||||||
|
|
||||||
expectAssignable<{ a: any }>(typeOf([dux({ actions: { a } })]));
|
|
||||||
expectAssignable<{}>(typeOf([dux({})]));
|
|
||||||
|
|
||||||
expectAssignable<{ a: any; b: any }>(
|
|
||||||
typeOf([dux({ actions: { a } }), dux({ actions: { b } })])
|
|
||||||
);
|
|
||||||
|
|
||||||
const co = coduxes(dux({ actions: { a } }), dux({})).coduxes;
|
|
||||||
|
|
||||||
expectAssignable<{ a: any }>(typeOf(co));
|
|
||||||
|
|
||||||
t.end();
|
|
||||||
});
|
|
||||||
|
|
||||||
t.test('with empty coduxes', t => {
|
|
||||||
const emptyDux = dux({});
|
|
||||||
|
|
||||||
const myDux = dux({
|
|
||||||
...coduxes(dux({ actions: { a } }), emptyDux),
|
|
||||||
});
|
|
||||||
|
|
||||||
t.ok(myDux.actions.a);
|
|
||||||
t.end();
|
|
||||||
});
|
|
||||||
|
|
||||||
t.test('with three coduxes', t => {
|
|
||||||
const emptyDux = new Updux();
|
|
||||||
emptyDux.actions;
|
|
||||||
|
|
||||||
const dux = new Updux({
|
|
||||||
coduxes: [
|
|
||||||
emptyDux,
|
|
||||||
new Updux({ actions: { a } }),
|
|
||||||
new Updux({ actions: { b } }),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
t.ok(dux.actions.b);
|
|
||||||
t.end();
|
|
||||||
});
|
|
||||||
|
|
||||||
t.test('with grandchild', t => {
|
|
||||||
const dux = new Updux({
|
|
||||||
subduxes: {
|
|
||||||
bar: new Updux({
|
|
||||||
subduxes: {
|
|
||||||
baz: new Updux({
|
|
||||||
actions: { a },
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
t.ok(dux.actions.a);
|
|
||||||
t.end();
|
|
||||||
});
|
|
||||||
t.end();
|
|
||||||
});
|
|
@ -1,78 +0,0 @@
|
|||||||
import { action, payload } from 'ts-action';
|
|
||||||
|
|
||||||
import Updux, { dux } from '.';
|
|
||||||
import { test } from 'tap';
|
|
||||||
import { expectAssignable } from 'tsd';
|
|
||||||
|
|
||||||
const noopEffect = () => () => () => null;
|
|
||||||
|
|
||||||
test(
|
|
||||||
'actions defined in effects and mutations, multi-level',
|
|
||||||
{ autoend: true },
|
|
||||||
t => {
|
|
||||||
const bar = action('bar', (payload, meta) => ({ payload, meta }));
|
|
||||||
const foo = action('foo', (limit: number) => ({
|
|
||||||
payload: { limit },
|
|
||||||
}));
|
|
||||||
|
|
||||||
const { actions }: any = dux({
|
|
||||||
effects: [[foo, noopEffect] as any],
|
|
||||||
mutations: [[bar, () => () => null]],
|
|
||||||
subduxes: {
|
|
||||||
mysub: dux({
|
|
||||||
effects: { baz: noopEffect },
|
|
||||||
mutations: { quux: () => () => null },
|
|
||||||
actions: {
|
|
||||||
foo,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
myothersub: dux({
|
|
||||||
effects: [[foo, noopEffect]],
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const types = Object.keys(actions);
|
|
||||||
types.sort();
|
|
||||||
|
|
||||||
t.match(types, ['bar', 'baz', 'foo', 'quux']);
|
|
||||||
|
|
||||||
t.match(actions.bar(), { type: 'bar' });
|
|
||||||
t.match(actions.bar('xxx'), { type: 'bar', payload: 'xxx' });
|
|
||||||
t.match(actions.bar(undefined, 'yyy'), {
|
|
||||||
type: 'bar',
|
|
||||||
payload: undefined,
|
|
||||||
meta: 'yyy',
|
|
||||||
});
|
|
||||||
|
|
||||||
t.same(actions.foo(12), { type: 'foo', payload: { limit: 12 } });
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
test('different calls to addAction', t => {
|
|
||||||
const updux = new Updux<any,any>();
|
|
||||||
|
|
||||||
updux.addAction(action('foo', payload()));
|
|
||||||
t.match(updux.actions.foo('yo'), {
|
|
||||||
type: 'foo',
|
|
||||||
payload: 'yo',
|
|
||||||
});
|
|
||||||
|
|
||||||
updux.addAction('baz', x => ({ x }));
|
|
||||||
t.match(updux.actions.baz(3), {
|
|
||||||
type: 'baz',
|
|
||||||
payload: { x: 3 },
|
|
||||||
});
|
|
||||||
|
|
||||||
t.end();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('types', t => {
|
|
||||||
const {actions} = dux({ actions: {
|
|
||||||
foo: action('foo')
|
|
||||||
}});
|
|
||||||
|
|
||||||
expectAssignable<object>( actions );
|
|
||||||
|
|
||||||
t.end();
|
|
||||||
});
|
|
@ -1,25 +0,0 @@
|
|||||||
import { action } from 'ts-action';
|
|
||||||
import tap from 'tap';
|
|
||||||
|
|
||||||
import Updux from "./updux";
|
|
||||||
|
|
||||||
type MyState = {
|
|
||||||
sum: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
tap.test("added mutation is present", t => {
|
|
||||||
const updux = new Updux<MyState>({
|
|
||||||
initial: { sum: 0 }
|
|
||||||
});
|
|
||||||
|
|
||||||
const add = action("add", (n: number) => ({ payload: { n } }));
|
|
||||||
|
|
||||||
updux.addMutation(add, ({ n }) => ({ sum }) => ({ sum: sum + n }));
|
|
||||||
|
|
||||||
const store = updux.createStore();
|
|
||||||
store.dispatch(add(3));
|
|
||||||
|
|
||||||
t.same(store.getState(), {sum: 3});
|
|
||||||
|
|
||||||
t.end();
|
|
||||||
});
|
|
@ -1,22 +0,0 @@
|
|||||||
import fp from 'lodash/fp';
|
|
||||||
import {
|
|
||||||
ActionCreator
|
|
||||||
} from 'ts-action';
|
|
||||||
|
|
||||||
import {
|
|
||||||
Dictionary,
|
|
||||||
} from '../types';
|
|
||||||
|
|
||||||
type ActionPair = [string, ActionCreator];
|
|
||||||
|
|
||||||
function buildActions(actions: ActionPair[] = []): Dictionary<ActionCreator<string,(...args: any) => {type: string} >>{
|
|
||||||
// priority => generics => generic subs => craft subs => creators
|
|
||||||
|
|
||||||
const [crafted, generic] = fp.partition(([type, f]) => !f._genericAction)(
|
|
||||||
fp.compact(actions)
|
|
||||||
);
|
|
||||||
|
|
||||||
return fp.fromPairs([...generic, ...crafted]);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default buildActions;
|
|
@ -1,31 +0,0 @@
|
|||||||
import {
|
|
||||||
createStore as reduxCreateStore,
|
|
||||||
applyMiddleware,
|
|
||||||
Middleware,
|
|
||||||
Reducer,
|
|
||||||
PreloadedState,
|
|
||||||
Store,
|
|
||||||
} from 'redux';
|
|
||||||
|
|
||||||
function buildCreateStore<S,A = {}>(
|
|
||||||
reducer: Reducer<S>,
|
|
||||||
middleware: Middleware,
|
|
||||||
actions: A = {} as A,
|
|
||||||
): (initial?: S, injectEnhancer?: Function) => Store<S> & { actions: A } {
|
|
||||||
return function createStore(initial?: S, injectEnhancer?: Function ): Store<S> & { actions: A } {
|
|
||||||
|
|
||||||
let enhancer = injectEnhancer ? injectEnhancer(middleware) : applyMiddleware(middleware);
|
|
||||||
|
|
||||||
const store = reduxCreateStore(
|
|
||||||
reducer,
|
|
||||||
initial as PreloadedState<S>,
|
|
||||||
enhancer
|
|
||||||
);
|
|
||||||
|
|
||||||
(store as any).actions = actions;
|
|
||||||
|
|
||||||
return store as any;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default buildCreateStore;
|
|
@ -1,28 +0,0 @@
|
|||||||
import tap from 'tap';
|
|
||||||
import { expectType } from 'tsd';
|
|
||||||
|
|
||||||
import buildCreateStore from '.';
|
|
||||||
import { action, ActionCreator } from 'ts-action';
|
|
||||||
import {Reducer} from 'redux';
|
|
||||||
|
|
||||||
const foo = action('foo');
|
|
||||||
const bar = action('bar');
|
|
||||||
|
|
||||||
type State = {
|
|
||||||
x: number,
|
|
||||||
y: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const store = buildCreateStore(
|
|
||||||
((state: State|undefined) => state ?? {x: 1} ) as Reducer<State>,
|
|
||||||
() => () => () => {return},
|
|
||||||
{ foo, bar }
|
|
||||||
)();
|
|
||||||
|
|
||||||
expectType<State>( store.getState() );
|
|
||||||
expectType<number>( store.getState().x );
|
|
||||||
|
|
||||||
expectType<{ foo: ActionCreator }>( store.actions );
|
|
||||||
|
|
||||||
tap.pass();
|
|
||||||
|
|
@ -1,89 +0,0 @@
|
|||||||
import fp from 'lodash/fp';
|
|
||||||
import { ActionCreator } from 'ts-action';
|
|
||||||
|
|
||||||
import { Middleware, MiddlewareAPI, Dispatch } from 'redux';
|
|
||||||
import {
|
|
||||||
Dictionary,
|
|
||||||
Action,
|
|
||||||
UpduxMiddleware,
|
|
||||||
UpduxMiddlewareAPI,
|
|
||||||
Selector,
|
|
||||||
} from '../types';
|
|
||||||
import Updux from '..';
|
|
||||||
|
|
||||||
const MiddlewareFor = (
|
|
||||||
type: any,
|
|
||||||
mw: Middleware
|
|
||||||
): Middleware => api => next => action => {
|
|
||||||
if (!type.includes('*') && action.type !== type) return next(action);
|
|
||||||
|
|
||||||
return mw(api)(next)(action);
|
|
||||||
};
|
|
||||||
|
|
||||||
type Next = (action: Action) => any;
|
|
||||||
|
|
||||||
function sliceMw(slice: string, mw: UpduxMiddleware): UpduxMiddleware {
|
|
||||||
return api => {
|
|
||||||
const getSliceState = () => fp.get(slice, api.getState());
|
|
||||||
return mw({ ...api, getState: getSliceState } as any);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
type Submws = Dictionary<UpduxMiddleware>;
|
|
||||||
|
|
||||||
type MwGen = () => UpduxMiddleware;
|
|
||||||
export type Effect = [string, UpduxMiddleware|MwGen, boolean? ];
|
|
||||||
|
|
||||||
export const subMiddleware = () => next => action => next(action);
|
|
||||||
export const subEffects : Effect = [ '*', subMiddleware ] as any;
|
|
||||||
|
|
||||||
export const effectToMw = (
|
|
||||||
effect: Effect,
|
|
||||||
actions: Dictionary<ActionCreator>,
|
|
||||||
selectors: Dictionary<Selector>,
|
|
||||||
) => {
|
|
||||||
let [type, mw, gen]: any = effect;
|
|
||||||
|
|
||||||
if ( mw === subMiddleware ) return subMiddleware;
|
|
||||||
|
|
||||||
if (gen) mw = mw();
|
|
||||||
|
|
||||||
const augmented = api => mw({ ...api, actions, selectors });
|
|
||||||
|
|
||||||
if (type === '*') return augmented;
|
|
||||||
|
|
||||||
return api => next => action => {
|
|
||||||
if (action.type !== type) return next(action);
|
|
||||||
|
|
||||||
return augmented(api)(next)(action);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const composeMw = (mws: UpduxMiddleware[]) => (
|
|
||||||
api: UpduxMiddlewareAPI<any>
|
|
||||||
) => (original_next: Next) =>
|
|
||||||
mws.reduceRight((next, mw) => mw(api)(next), original_next);
|
|
||||||
|
|
||||||
export function buildMiddleware<S = unknown>(
|
|
||||||
local: UpduxMiddleware[] = [],
|
|
||||||
co: UpduxMiddleware[] = [],
|
|
||||||
sub: Submws = {}
|
|
||||||
): UpduxMiddleware<S> {
|
|
||||||
let inner = [
|
|
||||||
...co,
|
|
||||||
...Object.entries(sub).map(([slice, mw]) => sliceMw(slice, mw)),
|
|
||||||
];
|
|
||||||
|
|
||||||
let found = false;
|
|
||||||
let mws = local.flatMap(e => {
|
|
||||||
if (e !== subMiddleware) return e;
|
|
||||||
found = true;
|
|
||||||
return inner;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!found) mws = [...mws, ...inner];
|
|
||||||
|
|
||||||
return composeMw(mws) as UpduxMiddleware<S>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default buildMiddleware;
|
|
@ -1,67 +0,0 @@
|
|||||||
import { buildMiddleware, subMiddleware } from '.';
|
|
||||||
import tap from 'tap';
|
|
||||||
import sinon from 'sinon';
|
|
||||||
|
|
||||||
const myMiddleware = (tag: string) => api => next => action => {
|
|
||||||
next({
|
|
||||||
...action,
|
|
||||||
payload: [...action.payload, tag],
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const local1 = myMiddleware('local1');
|
|
||||||
const local2 = myMiddleware('local2');
|
|
||||||
const co = myMiddleware('co');
|
|
||||||
const sub = myMiddleware('sub');
|
|
||||||
|
|
||||||
tap.test('basic', t => {
|
|
||||||
const next = sinon.fake();
|
|
||||||
|
|
||||||
const mw = buildMiddleware([local1, local2], [co], { sub });
|
|
||||||
|
|
||||||
mw({} as any)(next)({ type: 'foo', payload: [] });
|
|
||||||
|
|
||||||
t.match(next.firstCall.args, [
|
|
||||||
{
|
|
||||||
type: 'foo',
|
|
||||||
payload: ['local1', 'local2', 'co', 'sub'],
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
t.end();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('inner in the middle', t => {
|
|
||||||
const next = sinon.fake();
|
|
||||||
|
|
||||||
const mw = buildMiddleware([local1, subMiddleware, local2], [co], {
|
|
||||||
sub,
|
|
||||||
});
|
|
||||||
|
|
||||||
mw({} as any)(next)({ type: 'foo', payload: [] });
|
|
||||||
|
|
||||||
t.match(next.firstCall.lastArg, {
|
|
||||||
type: 'foo',
|
|
||||||
payload: ['local1', 'co', 'sub', 'local2'],
|
|
||||||
});
|
|
||||||
|
|
||||||
t.end();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('sub-mw get their store sliced', t => {
|
|
||||||
const next = sinon.fake();
|
|
||||||
|
|
||||||
const sub = ({ getState }) => next => action => next(getState());
|
|
||||||
|
|
||||||
const mw = buildMiddleware([], [], { sub });
|
|
||||||
|
|
||||||
mw({
|
|
||||||
getState() {
|
|
||||||
return { foo: 1, sub: 2 };
|
|
||||||
},
|
|
||||||
} as any)(next)({ type: 'noop' });
|
|
||||||
|
|
||||||
t.same( next.firstCall.lastArg, 2 );
|
|
||||||
|
|
||||||
t.end();
|
|
||||||
});
|
|
@ -1,57 +0,0 @@
|
|||||||
import fp from 'lodash/fp';
|
|
||||||
import u from '@yanick/updeep';
|
|
||||||
import {
|
|
||||||
Mutation,
|
|
||||||
Action,
|
|
||||||
Dictionary,
|
|
||||||
MutationEntry,
|
|
||||||
Upreducer,
|
|
||||||
} from '../types';
|
|
||||||
import Updux from '..';
|
|
||||||
|
|
||||||
const composeMutations = (mutations: Mutation[]) => {
|
|
||||||
if (mutations.length == 0) return () => state => state;
|
|
||||||
|
|
||||||
return mutations.reduce(
|
|
||||||
(m1, m2) => (payload: any = null, action: Action) => state =>
|
|
||||||
m2(payload, action)(m1(payload, action)(state))
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
type SubMutations = {
|
|
||||||
[slice: string]: Dictionary<Mutation>;
|
|
||||||
};
|
|
||||||
|
|
||||||
function buildMutations(
|
|
||||||
mutations: Dictionary<Mutation | [Mutation, boolean | undefined]> = {},
|
|
||||||
subduxes = {},
|
|
||||||
coduxes: Upreducer[] = []
|
|
||||||
) {
|
|
||||||
const submuts = Object.entries(subduxes).map(
|
|
||||||
([slice, upreducer]: [string, any]) =>
|
|
||||||
<Mutation>(
|
|
||||||
((payload, action: Action) =>
|
|
||||||
(u.updateIn as any)(slice, upreducer(action)))
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const comuts = coduxes.map(c => (payload, action: Action) => c(action));
|
|
||||||
|
|
||||||
const subreducer = composeMutations([...submuts, ...comuts]);
|
|
||||||
|
|
||||||
let merged = {};
|
|
||||||
|
|
||||||
Object.entries(mutations).forEach(([type, mutation]) => {
|
|
||||||
const [m, sink] = Array.isArray(mutation)
|
|
||||||
? mutation
|
|
||||||
: [mutation, false];
|
|
||||||
|
|
||||||
merged[type] = sink ? m : composeMutations([subreducer, m]);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!merged['*']) merged['*'] = subreducer;
|
|
||||||
|
|
||||||
return merged;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default buildMutations;
|
|
@ -1,19 +0,0 @@
|
|||||||
import fp from 'lodash/fp';
|
|
||||||
|
|
||||||
import { Dictionary, Mutation, Action, Upreducer } from '../types';
|
|
||||||
|
|
||||||
function buildUpreducer<S>(initial: S, mutations: Dictionary<Mutation<S>> ): Upreducer<S> {
|
|
||||||
return (action :Action) => (state: S) => {
|
|
||||||
if (state === undefined) state = initial;
|
|
||||||
|
|
||||||
const a =
|
|
||||||
mutations[action.type] ||
|
|
||||||
mutations['*'];
|
|
||||||
|
|
||||||
if(!a) return state;
|
|
||||||
|
|
||||||
return a(action.payload, action)(state);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default buildUpreducer;
|
|
@ -1,36 +0,0 @@
|
|||||||
import Updux from '.';
|
|
||||||
import { action } from 'ts-action';
|
|
||||||
import { Effect } from './buildMiddleware';
|
|
||||||
import tap from 'tap';
|
|
||||||
import sinon from 'sinon';
|
|
||||||
|
|
||||||
const myEffect: Effect = [
|
|
||||||
'*',
|
|
||||||
() => next => action => next({ ...action, hello: true }),
|
|
||||||
];
|
|
||||||
|
|
||||||
const codux = new Updux({
|
|
||||||
actions: {
|
|
||||||
foo: action('foo'),
|
|
||||||
},
|
|
||||||
effects: [myEffect],
|
|
||||||
});
|
|
||||||
|
|
||||||
const dux = new Updux({
|
|
||||||
coduxes: [codux],
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('actions', t => {
|
|
||||||
t.ok(dux.actions.foo);
|
|
||||||
t.end();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('effects', t => {
|
|
||||||
const next = sinon.fake();
|
|
||||||
|
|
||||||
dux.middleware({} as any)(next)({ type: 'foo' });
|
|
||||||
|
|
||||||
t.same(next.lastCall.lastArg, { type: 'foo', hello: true });
|
|
||||||
|
|
||||||
t.end();
|
|
||||||
});
|
|
@ -1,14 +0,0 @@
|
|||||||
import tap from 'tap';
|
|
||||||
import { expectType } from 'tsd';
|
|
||||||
|
|
||||||
import { dux, DuxState } from '.';
|
|
||||||
|
|
||||||
const myDux = dux({
|
|
||||||
initial: { a: 1, b: "potato" }
|
|
||||||
});
|
|
||||||
|
|
||||||
type State = DuxState<typeof myDux>;
|
|
||||||
|
|
||||||
expectType<State>({ a: 12, b: "something" });
|
|
||||||
|
|
||||||
tap.pass();
|
|
@ -1,14 +0,0 @@
|
|||||||
import tap from 'tap';
|
|
||||||
import Updux from '.';
|
|
||||||
import { action, payload } from 'ts-action';
|
|
||||||
import { expectType } from 'tsd';
|
|
||||||
|
|
||||||
const dux = new Updux({ });
|
|
||||||
|
|
||||||
const myAction = action('mine',payload<string>() );
|
|
||||||
|
|
||||||
dux.addEffect( myAction, () => () => action => {
|
|
||||||
expectType<{payload: string; type: "mine"}>(action);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.pass("pure type checking");
|
|
22
src/index.ts
22
src/index.ts
@ -1,22 +0,0 @@
|
|||||||
import Updux from "./updux";
|
|
||||||
import { UpduxConfig, Dux, Dictionary, Selector, Mutation, AggDuxState, Action,
|
|
||||||
Upreducer, UpduxMiddleware, DuxActions, DuxSelectors } from "./types";
|
|
||||||
|
|
||||||
import {Creator} from 'ts-action';
|
|
||||||
import { AnyAction, Store } from 'redux';
|
|
||||||
|
|
||||||
export { default as Updux } from "./updux";
|
|
||||||
export { UpduxConfig, DuxState } from "./types";
|
|
||||||
export { subEffects } from './buildMiddleware';
|
|
||||||
export * from './types';
|
|
||||||
|
|
||||||
export default Updux;
|
|
||||||
|
|
||||||
export const coduxes = <C extends Dux, U extends [C,...C[]]>(...coduxes: U): { coduxes: U } => ({
|
|
||||||
coduxes });
|
|
||||||
|
|
||||||
export const dux = <S=unknown,A=unknown,X=unknown,C extends UpduxConfig= {}>(config: C ) => {
|
|
||||||
/// : Dux<S,A,X,C> => {
|
|
||||||
return ( new Updux<S,A,X,C>(config) ).asDux;
|
|
||||||
}
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
|||||||
import Updux from './updux';
|
|
||||||
import u from '@yanick/updeep';
|
|
||||||
import tap from 'tap';
|
|
||||||
|
|
||||||
const todo: any = new Updux<any>({
|
|
||||||
mutations: {
|
|
||||||
review: () => u({ reviewed: true }),
|
|
||||||
done: () => u({ done: true }),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const todos: any = new Updux({
|
|
||||||
subduxes: { '*': todo },
|
|
||||||
});
|
|
||||||
|
|
||||||
todos.addMutation(
|
|
||||||
todo.actions.done,
|
|
||||||
(id, action) => u.map(u.if(u.is('id', id), todo.upreducer(action))),
|
|
||||||
true
|
|
||||||
);
|
|
||||||
|
|
||||||
tap.test('* for mapping works', async t => {
|
|
||||||
const reducer = todos.reducer;
|
|
||||||
let state = [{ id: 0 }, { id: 1 }];
|
|
||||||
state = reducer(state, todos.actions.review());
|
|
||||||
state = reducer(state, todos.actions.done(1));
|
|
||||||
|
|
||||||
t.same(state, [
|
|
||||||
{ id: 0, reviewed: true },
|
|
||||||
{ id: 1, reviewed: true, done: true },
|
|
||||||
]);
|
|
||||||
});
|
|
@ -1,278 +0,0 @@
|
|||||||
import u from '@yanick/updeep';
|
|
||||||
import { action } from 'ts-action';
|
|
||||||
import tap from 'tap';
|
|
||||||
import sinon from 'sinon';
|
|
||||||
|
|
||||||
import Updux, { dux, subEffects } from '.';
|
|
||||||
import mwUpdux from './middleware_aux';
|
|
||||||
|
|
||||||
tap.test('simple effect', async t => {
|
|
||||||
const tracer = sinon.fake();
|
|
||||||
|
|
||||||
const store = new Updux({
|
|
||||||
effects: {
|
|
||||||
foo: () => (next: any) => (action: any) => {
|
|
||||||
tracer();
|
|
||||||
next(action);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}).createStore();
|
|
||||||
|
|
||||||
t.ok(!tracer.called);
|
|
||||||
|
|
||||||
store.dispatch({ type: 'bar' });
|
|
||||||
|
|
||||||
t.ok(!tracer.called);
|
|
||||||
|
|
||||||
store.dispatch( (store as any).actions.foo() );
|
|
||||||
|
|
||||||
t.ok(tracer.called);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('effect and sub-effect', async t => {
|
|
||||||
const tracer = sinon.fake();
|
|
||||||
|
|
||||||
const tracerEffect = (signature: string) => () => (next: any) => (
|
|
||||||
action: any
|
|
||||||
) => {
|
|
||||||
tracer(signature);
|
|
||||||
next(action);
|
|
||||||
};
|
|
||||||
|
|
||||||
const store = new Updux({
|
|
||||||
effects: {
|
|
||||||
foo: tracerEffect('root'),
|
|
||||||
},
|
|
||||||
subduxes: {
|
|
||||||
zzz: dux({
|
|
||||||
effects: {
|
|
||||||
foo: tracerEffect('child'),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
}).createStore();
|
|
||||||
|
|
||||||
t.ok(!tracer.called);
|
|
||||||
|
|
||||||
store.dispatch({ type: 'bar' });
|
|
||||||
|
|
||||||
t.ok(!tracer.called);
|
|
||||||
|
|
||||||
store.dispatch( (store.actions as any).foo() );
|
|
||||||
|
|
||||||
t.is( tracer.firstCall.lastArg, 'root' );
|
|
||||||
t.is( tracer.secondCall.lastArg, 'child' );
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('"*" effect', async t => {
|
|
||||||
t.test('from the constructor', async t => {
|
|
||||||
const tracer = sinon.fake();
|
|
||||||
|
|
||||||
const store = new Updux({
|
|
||||||
effects: {
|
|
||||||
'*': () => next => action => {
|
|
||||||
tracer();
|
|
||||||
next(action);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}).createStore();
|
|
||||||
|
|
||||||
t.ok(!tracer.called);
|
|
||||||
|
|
||||||
store.dispatch({ type: 'bar' });
|
|
||||||
t.ok(tracer.called);
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
t.test('from addEffect', async t => {
|
|
||||||
const tracer = sinon.fake();
|
|
||||||
|
|
||||||
const updux = new Updux({});
|
|
||||||
|
|
||||||
updux.addEffect('*', () => next => action => {
|
|
||||||
tracer();
|
|
||||||
next(action);
|
|
||||||
});
|
|
||||||
|
|
||||||
t.ok(!tracer.called);
|
|
||||||
|
|
||||||
updux.createStore().dispatch({ type: 'bar' });
|
|
||||||
|
|
||||||
t.ok(tracer.called);
|
|
||||||
});
|
|
||||||
|
|
||||||
t.test('action can be modified', async t => {
|
|
||||||
|
|
||||||
const mw = mwUpdux.middleware;
|
|
||||||
|
|
||||||
const next = sinon.fake();
|
|
||||||
|
|
||||||
mw({dispatch:{}} as any)(next as any)({type: 'bar'});
|
|
||||||
|
|
||||||
t.ok(next.called);
|
|
||||||
t.match( next.firstCall.args[0], {meta: 'gotcha' } );
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('async effect', async t => {
|
|
||||||
function timeout(ms: number) {
|
|
||||||
return new Promise(resolve => setTimeout(resolve, ms));
|
|
||||||
}
|
|
||||||
|
|
||||||
const tracer = sinon.fake();
|
|
||||||
|
|
||||||
const store = new Updux({
|
|
||||||
effects: {
|
|
||||||
foo: () => next => async action => {
|
|
||||||
next(action);
|
|
||||||
await timeout(1000);
|
|
||||||
tracer();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}).createStore();
|
|
||||||
|
|
||||||
t.ok(!tracer.called);
|
|
||||||
|
|
||||||
store.dispatch( (store.actions as any).foo() );
|
|
||||||
|
|
||||||
t.ok(!tracer.called);
|
|
||||||
|
|
||||||
await timeout(1000);
|
|
||||||
|
|
||||||
t.ok(tracer.called);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('getState is local', async t => {
|
|
||||||
let childState;
|
|
||||||
let rootState;
|
|
||||||
|
|
||||||
const child = new Updux({
|
|
||||||
initial: { alpha: 12 },
|
|
||||||
effects: {
|
|
||||||
doIt: ({ getState }) => next => action => {
|
|
||||||
childState = getState();
|
|
||||||
next(action);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const root = new Updux({
|
|
||||||
initial: { beta: 24 },
|
|
||||||
subduxes: { child: child.asDux },
|
|
||||||
effects: {
|
|
||||||
doIt: ({ getState }) => next => action => {
|
|
||||||
rootState = getState();
|
|
||||||
next(action);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const store = root.createStore();
|
|
||||||
store.dispatch( (store.actions as any).doIt() );
|
|
||||||
|
|
||||||
t.match(rootState,{ beta: 24, child: { alpha: 12 } });
|
|
||||||
t.match(childState,{ alpha: 12 });
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('middleware as map', async t => {
|
|
||||||
|
|
||||||
const doIt = action('doIt', () => ({payload: ''}));
|
|
||||||
|
|
||||||
const child = new Updux({
|
|
||||||
initial: '',
|
|
||||||
effects: [
|
|
||||||
[
|
|
||||||
doIt,
|
|
||||||
() => next => action => {
|
|
||||||
next(
|
|
||||||
u(
|
|
||||||
{ payload: (p: string) => p + 'Child' },
|
|
||||||
action
|
|
||||||
) as any
|
|
||||||
);
|
|
||||||
},
|
|
||||||
],
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
const root = new Updux({
|
|
||||||
initial: { message: '' },
|
|
||||||
subduxes: { child: child.asDux },
|
|
||||||
effects: [
|
|
||||||
[
|
|
||||||
'*',
|
|
||||||
() => next => action => {
|
|
||||||
next(
|
|
||||||
u({ payload: (p: string) => p + 'Pre' }, action) as any
|
|
||||||
);
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[
|
|
||||||
doIt,
|
|
||||||
() => next => action => {
|
|
||||||
next(
|
|
||||||
u({ payload: (p: string) => p + 'Root' }, action) as any
|
|
||||||
);
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'*',
|
|
||||||
() => next => action => {
|
|
||||||
next(
|
|
||||||
u(
|
|
||||||
{ payload: (p: string) => p + 'After' },
|
|
||||||
action
|
|
||||||
) as any
|
|
||||||
);
|
|
||||||
},
|
|
||||||
],
|
|
||||||
subEffects,
|
|
||||||
[
|
|
||||||
'*',
|
|
||||||
() => next => action => {
|
|
||||||
next(
|
|
||||||
u({ payload: (p: string) => p + 'End' }, action) as any
|
|
||||||
);
|
|
||||||
},
|
|
||||||
],
|
|
||||||
],
|
|
||||||
mutations: [[doIt, (message: any) => () => ({ message })]],
|
|
||||||
});
|
|
||||||
|
|
||||||
const store = root.createStore();
|
|
||||||
const actions: any = store.actions;
|
|
||||||
store.dispatch( actions.doIt('') );
|
|
||||||
|
|
||||||
t.same(store.getState(),{ message: 'PreRootAfterChildEnd' });
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('generator', async t => {
|
|
||||||
const updux = new Updux({
|
|
||||||
initial: 0,
|
|
||||||
mutations: [['doIt', payload => () => payload]],
|
|
||||||
effects: [
|
|
||||||
[
|
|
||||||
'doIt',
|
|
||||||
() => {
|
|
||||||
let i = 0;
|
|
||||||
return () => (next: any) => (action: any) =>
|
|
||||||
next({ ...action, payload: ++i });
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
],
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
const store1 = updux.createStore();
|
|
||||||
store1.dispatch( (store1 as any).actions.doIt() );
|
|
||||||
|
|
||||||
t.is(store1.getState(),1);
|
|
||||||
store1.dispatch( (store1 as any).actions.doIt() );
|
|
||||||
|
|
||||||
t.is(store1.getState(),2);
|
|
||||||
|
|
||||||
const store2 = updux.createStore();
|
|
||||||
store2.dispatch( (store2 as any).actions.doIt() );
|
|
||||||
t.is(store2.getState(),1);
|
|
||||||
});
|
|
@ -1,13 +0,0 @@
|
|||||||
import Updux, {dux} from '.';
|
|
||||||
|
|
||||||
const updux = new Updux({
|
|
||||||
subduxes: {
|
|
||||||
foo: dux({ initial: "banana" })
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
updux.addEffect('*', () => next => action => {
|
|
||||||
next({...action, meta: "gotcha" });
|
|
||||||
});
|
|
||||||
|
|
||||||
export default updux;
|
|
@ -1,88 +0,0 @@
|
|||||||
import { action, empty } from 'ts-action';
|
|
||||||
import Updux, { dux } from '.';
|
|
||||||
import tap from 'tap';
|
|
||||||
|
|
||||||
import u from '@yanick/updeep';
|
|
||||||
|
|
||||||
tap.test('as array of arrays', async t => {
|
|
||||||
const doIt = action('doIt');
|
|
||||||
|
|
||||||
const updux = new Updux({
|
|
||||||
initial: '',
|
|
||||||
actions: { doIt },
|
|
||||||
mutations: [
|
|
||||||
[doIt, () => () => 'bingo'],
|
|
||||||
['thisToo', () => () => 'straight type'],
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
const store = updux.createStore();
|
|
||||||
|
|
||||||
t.test('doIt', async t => {
|
|
||||||
store.dispatch( store.actions.doIt() );
|
|
||||||
t.is(store.getState(),'bingo');
|
|
||||||
});
|
|
||||||
|
|
||||||
t.test('straight type', async t => {
|
|
||||||
store.dispatch( (store.actions as any).thisToo() );
|
|
||||||
t.is(store.getState(),'straight type');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('override', async t => {
|
|
||||||
const d = new Updux<any>({
|
|
||||||
initial: { alpha: [] },
|
|
||||||
mutations: {
|
|
||||||
'*': (payload, action) => state => ({
|
|
||||||
...state,
|
|
||||||
alpha: [...state.alpha, action.type],
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
subduxes: {
|
|
||||||
subbie: dux({
|
|
||||||
initial: 0,
|
|
||||||
mutations: {
|
|
||||||
foo: () => state => state + 1,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const store = d.createStore();
|
|
||||||
store.dispatch({ type: 'foo' });
|
|
||||||
store.dispatch({ type: 'bar' });
|
|
||||||
|
|
||||||
t.pass();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('coduxes and subduxes', async t => {
|
|
||||||
const foo = action('foo',empty());
|
|
||||||
|
|
||||||
const d = new Updux({
|
|
||||||
initial: {
|
|
||||||
x: '',
|
|
||||||
},
|
|
||||||
actions: {
|
|
||||||
foo
|
|
||||||
},
|
|
||||||
mutations: [[foo, () => (u.updateIn as any)('x', x => x + 'm')]],
|
|
||||||
subduxes: {
|
|
||||||
x: dux({
|
|
||||||
mutations: [[foo, () => x => x + 's']],
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
coduxes: [
|
|
||||||
dux({
|
|
||||||
mutations: [[foo, () => (u.updateIn as any)('x', x => x + 'c')]],
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
const store = d.createStore();
|
|
||||||
|
|
||||||
store.dispatch(d.actions.foo());
|
|
||||||
|
|
||||||
t.same(store.getState(),{
|
|
||||||
x: 'scm',
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,57 +0,0 @@
|
|||||||
import Updux, { dux } from '.';
|
|
||||||
import tap from 'tap';
|
|
||||||
import { action } from 'ts-action';
|
|
||||||
|
|
||||||
const foo = dux({
|
|
||||||
initial: 0,
|
|
||||||
actions: {
|
|
||||||
doIt: action('doIt'),
|
|
||||||
doTheThing: action('doTheThing'),
|
|
||||||
},
|
|
||||||
mutations: {
|
|
||||||
doIt: () => (state: number) => {
|
|
||||||
return state + 1;
|
|
||||||
},
|
|
||||||
doTheThing: () => (state: number) => {
|
|
||||||
return state + 3;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const bar: any = new Updux<{ foo: number }>({
|
|
||||||
subduxes: { foo },
|
|
||||||
});
|
|
||||||
|
|
||||||
bar.addMutation(
|
|
||||||
foo.actions.doTheThing,
|
|
||||||
(_, action) => state => {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
baz: foo.upreducer(action)(state.foo),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
true
|
|
||||||
);
|
|
||||||
|
|
||||||
bar.addMutation(
|
|
||||||
foo.actions.doIt,
|
|
||||||
() => (state: any) => ({ ...state, bar: 'yay' }),
|
|
||||||
true
|
|
||||||
);
|
|
||||||
|
|
||||||
tap.same(bar.initial, { foo: 0 });
|
|
||||||
|
|
||||||
tap.test('foo alone', t => {
|
|
||||||
t.is(foo.reducer(undefined, foo.actions.doIt()), 1);
|
|
||||||
t.end();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('sink mutations', t => {
|
|
||||||
t.same(
|
|
||||||
bar.reducer(undefined, bar.actions.doIt()), {
|
|
||||||
foo: 0,
|
|
||||||
bar: 'yay',
|
|
||||||
});
|
|
||||||
|
|
||||||
t.end();
|
|
||||||
});
|
|
@ -1,113 +0,0 @@
|
|||||||
import tap from 'tap';
|
|
||||||
import Updux, {dux} from '.';
|
|
||||||
import u from '@yanick/updeep';
|
|
||||||
import { expectType } from 'tsd';
|
|
||||||
|
|
||||||
const tracer = (chr: string) => u({ tracer: (s = '') => s + chr });
|
|
||||||
|
|
||||||
tap.test('mutations, simple', t => {
|
|
||||||
const dux : any = new Updux({
|
|
||||||
mutations: {
|
|
||||||
foo: () => tracer('a'),
|
|
||||||
'*': () => tracer('b'),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const store = dux.createStore();
|
|
||||||
|
|
||||||
t.same(store.getState(),{ tracer: 'b' });
|
|
||||||
|
|
||||||
store.dispatch( store.actions.foo() );
|
|
||||||
|
|
||||||
t.same(store.getState(),{ tracer: 'ba' });
|
|
||||||
|
|
||||||
store.dispatch({ type: 'bar' });
|
|
||||||
|
|
||||||
t.same(store.getState(),{ tracer: 'bab' });
|
|
||||||
|
|
||||||
t.end();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('with subduxes', t => {
|
|
||||||
const d = dux({
|
|
||||||
mutations: {
|
|
||||||
foo: () => tracer('a'),
|
|
||||||
'*': () => tracer('b'),
|
|
||||||
bar: () => ({ bar }: any) => ({ bar, tracer: bar.tracer }),
|
|
||||||
},
|
|
||||||
subduxes: {
|
|
||||||
bar: dux({
|
|
||||||
mutations: {
|
|
||||||
foo: () => tracer('d'),
|
|
||||||
'*': () => tracer('e'),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const store = d.createStore();
|
|
||||||
|
|
||||||
t.same(store.getState(),{
|
|
||||||
tracer: 'b',
|
|
||||||
bar: { tracer: 'e' },
|
|
||||||
});
|
|
||||||
|
|
||||||
store.dispatch( (store.actions as any).foo() );
|
|
||||||
|
|
||||||
t.same(store.getState(),{
|
|
||||||
tracer: 'ba',
|
|
||||||
bar: { tracer: 'ed' },
|
|
||||||
});
|
|
||||||
|
|
||||||
store.dispatch({ type: 'bar' });
|
|
||||||
|
|
||||||
t.same(store.getState(),{
|
|
||||||
tracer: 'ede',
|
|
||||||
bar: { tracer: 'ede' },
|
|
||||||
});
|
|
||||||
|
|
||||||
t.end();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test( 'splat and state', async t => {
|
|
||||||
|
|
||||||
const inner = dux({ initial: 3 });
|
|
||||||
inner.initial;
|
|
||||||
|
|
||||||
const arrayDux = dux({
|
|
||||||
initial: [],
|
|
||||||
subduxes: {
|
|
||||||
'*': inner
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
expectType<number[]>(arrayDux.initial)
|
|
||||||
t.same(arrayDux.initial,[]);
|
|
||||||
|
|
||||||
const objDux = dux({
|
|
||||||
initial: {},
|
|
||||||
subduxes: {
|
|
||||||
'*': dux({initial: 3})
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
expectType<{
|
|
||||||
[key: string]: number
|
|
||||||
}>(objDux.initial);
|
|
||||||
|
|
||||||
t.same(objDux.initial,{});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test( 'multi-splat', async t => {
|
|
||||||
|
|
||||||
dux({
|
|
||||||
subduxes: {
|
|
||||||
foo: dux({ mutations: { '*': () => s => s }}),
|
|
||||||
bar: dux({ mutations: { '*': () => s => s }}),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
t.pass();
|
|
||||||
|
|
||||||
});
|
|
205
src/test.ts
205
src/test.ts
@ -1,205 +0,0 @@
|
|||||||
import Updux, {dux} from '.';
|
|
||||||
import u from '@yanick/updeep';
|
|
||||||
import { action, payload } from 'ts-action';
|
|
||||||
import tap from 'tap';
|
|
||||||
|
|
||||||
tap.test('actions from mutations', async t => {
|
|
||||||
const {
|
|
||||||
actions: {foo, bar},
|
|
||||||
} : any = new Updux({
|
|
||||||
mutations: {
|
|
||||||
foo: () => (x:any) => x,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
t.match( foo(), { type: 'foo' } );
|
|
||||||
t.same( foo(true), {type: 'foo', payload: true});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('reducer', async t => {
|
|
||||||
const inc = action('inc');
|
|
||||||
const {actions, reducer} = dux({
|
|
||||||
initial: {counter: 1},
|
|
||||||
actions: { inc },
|
|
||||||
mutations: {
|
|
||||||
inc: () => ({counter}:{counter:number}) => ({counter: counter + 1}),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
let state = reducer(undefined, {type:'noop'});
|
|
||||||
|
|
||||||
t.same(state,{counter: 1});
|
|
||||||
|
|
||||||
state = reducer(state, actions.inc());
|
|
||||||
|
|
||||||
t.same(state,{counter: 2});
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test( 'sub reducers', async t => {
|
|
||||||
const doAll = action('doAll', payload<number>() );
|
|
||||||
|
|
||||||
const foo = dux({
|
|
||||||
initial: 1,
|
|
||||||
actions: { doAll },
|
|
||||||
mutations: {
|
|
||||||
doFoo: () => (x:number) => x + 1,
|
|
||||||
doAll: () => (x:number) => x + 10,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const bar = dux({
|
|
||||||
initial: 'a',
|
|
||||||
actions: { doAll },
|
|
||||||
mutations: {
|
|
||||||
doBar: () => (x:string) => x + 'a',
|
|
||||||
doAll: () => (x:string) => x + 'b',
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const { initial, actions, reducer } = new Updux({
|
|
||||||
subduxes: {
|
|
||||||
foo, bar
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
t.same(initial,{ foo: 1, bar: 'a' });
|
|
||||||
|
|
||||||
t.is(Object.keys(actions).length,3);
|
|
||||||
|
|
||||||
let state = reducer(undefined,{type:'noop'});
|
|
||||||
|
|
||||||
t.same(state,{ foo: 1, bar: 'a' });
|
|
||||||
|
|
||||||
state = reducer(state, (actions as any).doFoo() );
|
|
||||||
|
|
||||||
t.same(state,{ foo: 2, bar: 'a' });
|
|
||||||
|
|
||||||
state = reducer(state, (actions as any).doBar() );
|
|
||||||
|
|
||||||
t.same(state,{ foo: 2, bar: 'aa' });
|
|
||||||
|
|
||||||
state = reducer(state, (actions as any).doAll() );
|
|
||||||
|
|
||||||
t.same(state,{ foo: 12, bar: 'aab' });
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('precedence between root and sub-reducers', async t => {
|
|
||||||
const {
|
|
||||||
initial,
|
|
||||||
reducer,
|
|
||||||
actions,
|
|
||||||
} = dux({
|
|
||||||
initial: {
|
|
||||||
foo: { bar: 4 },
|
|
||||||
},
|
|
||||||
mutations: {
|
|
||||||
inc: () => (state:any) => {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
surprise: state.foo.bar
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
subduxes: {
|
|
||||||
foo: dux({
|
|
||||||
initial: {
|
|
||||||
bar: 2,
|
|
||||||
quux: 3,
|
|
||||||
},
|
|
||||||
mutations: {
|
|
||||||
inc: () => (state:any) => ({...state, bar: state.bar + 1 })
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// quick fix until https://github.com/facebook/jest/issues/9531
|
|
||||||
t.same(initial,{
|
|
||||||
foo: { bar: 4, quux: 3 }
|
|
||||||
});
|
|
||||||
|
|
||||||
t.same( reducer(undefined,(actions as any).inc() ) ,{
|
|
||||||
foo: { bar: 5, quux: 3 }, surprise: 5
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
function timeout(ms:number) {
|
|
||||||
return new Promise(resolve => setTimeout(resolve, ms));
|
|
||||||
}
|
|
||||||
|
|
||||||
tap.test( 'middleware', async t => {
|
|
||||||
const {
|
|
||||||
middleware,
|
|
||||||
createStore,
|
|
||||||
actions,
|
|
||||||
} = dux({
|
|
||||||
initial: { result: [] },
|
|
||||||
mutations: {
|
|
||||||
inc: (addition:number) => u({ result: r => [ ...r, addition ]}),
|
|
||||||
doEeet: () => u({ result: r => [ ...r, 'Z' ]}),
|
|
||||||
},
|
|
||||||
effects: {
|
|
||||||
doEeet: api => next => async action => {
|
|
||||||
api.dispatch( api.actions.inc('a') );
|
|
||||||
next(action);
|
|
||||||
await timeout(1000);
|
|
||||||
api.dispatch( api.actions.inc('c') );
|
|
||||||
}
|
|
||||||
},
|
|
||||||
subduxes: {
|
|
||||||
foo: dux({
|
|
||||||
effects: {
|
|
||||||
doEeet: (api:any) => ( next:any ) => ( action: any ) => {
|
|
||||||
api.dispatch({ type: 'inc', payload: 'b'});
|
|
||||||
next(action);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const store :any = createStore();
|
|
||||||
|
|
||||||
store.dispatch( (actions as any).doEeet() );
|
|
||||||
|
|
||||||
t.is(store.getState().result.join(''),'abZ' );
|
|
||||||
|
|
||||||
await timeout(1000);
|
|
||||||
|
|
||||||
t.is(store.getState().result.join(''), 'abZc' );
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
tap.test( "subduxes and mutations", async t => {
|
|
||||||
const quux = action('quux');
|
|
||||||
|
|
||||||
const foo = dux({
|
|
||||||
actions: { quux },
|
|
||||||
mutations: {
|
|
||||||
quux: () => () => 'x',
|
|
||||||
blart: () => () => 'a',
|
|
||||||
}});
|
|
||||||
const bar = dux({
|
|
||||||
actions: { quux },
|
|
||||||
mutations: {
|
|
||||||
quux: () => () => 'y'
|
|
||||||
}});
|
|
||||||
const baz = dux({
|
|
||||||
actions: { quux },
|
|
||||||
mutations: {
|
|
||||||
quux: () => (state:any) => ({...state, "baz": "z" })
|
|
||||||
}, subduxes: { foo, bar } });
|
|
||||||
|
|
||||||
let state = baz.reducer(undefined, (baz.actions as any).quux() );
|
|
||||||
|
|
||||||
t.same(state,{
|
|
||||||
foo: "x",
|
|
||||||
bar: "y",
|
|
||||||
baz: "z",
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
@ -1,34 +0,0 @@
|
|||||||
import tap from 'tap';
|
|
||||||
|
|
||||||
import Updux from '..';
|
|
||||||
import {action, payload} from 'ts-action';
|
|
||||||
|
|
||||||
type Todo = {
|
|
||||||
id: number;
|
|
||||||
description: string;
|
|
||||||
done: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
type TodoStore = {
|
|
||||||
next_id: number;
|
|
||||||
todos: Todo[];
|
|
||||||
};
|
|
||||||
|
|
||||||
const todosUpdux = new Updux({
|
|
||||||
initial: {
|
|
||||||
next_id: 1,
|
|
||||||
todos: [],
|
|
||||||
} as TodoStore
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
tap.test('initial state', async t => {
|
|
||||||
const store = todosUpdux.createStore();
|
|
||||||
|
|
||||||
t.like(store.getState(), {
|
|
||||||
next_id: 1,
|
|
||||||
todos: [],
|
|
||||||
}, 'initial state'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
377
src/types.ts
377
src/types.ts
@ -1,377 +0,0 @@
|
|||||||
import { ActionCreator } from 'ts-action';
|
|
||||||
|
|
||||||
type MaybePayload<P> = P extends object | string | boolean | number
|
|
||||||
? {
|
|
||||||
payload: P;
|
|
||||||
}
|
|
||||||
: { payload?: P };
|
|
||||||
|
|
||||||
export type Action<T extends string = string, P = any> = {
|
|
||||||
type: T;
|
|
||||||
} & MaybePayload<P>;
|
|
||||||
|
|
||||||
export type Dictionary<T> = { [key: string]: T };
|
|
||||||
|
|
||||||
export type Mutation<S = any, A extends Action = Action> = (
|
|
||||||
payload: A['payload'],
|
|
||||||
action: A
|
|
||||||
) => (state: S) => S;
|
|
||||||
|
|
||||||
export type ActionPayloadGenerator = (...args: any[]) => any;
|
|
||||||
|
|
||||||
export type MutationEntry = [
|
|
||||||
ActionCreator | string,
|
|
||||||
Mutation<any, Action<string, any>>,
|
|
||||||
boolean?
|
|
||||||
];
|
|
||||||
|
|
||||||
export type GenericActions = Dictionary<
|
|
||||||
ActionCreator<string, (...args: any) => { type: string }>
|
|
||||||
>;
|
|
||||||
|
|
||||||
export type UnionToIntersection<U> = (U extends any
|
|
||||||
? (k: U) => void
|
|
||||||
: never) extends (k: infer I) => void
|
|
||||||
? I
|
|
||||||
: never;
|
|
||||||
|
|
||||||
export type StateOf<D> = D extends { initial: infer I } ? I : unknown;
|
|
||||||
|
|
||||||
export type DuxStateCoduxes<C> = C extends Array<infer U>
|
|
||||||
? UnionToIntersection<StateOf<U>>
|
|
||||||
: unknown;
|
|
||||||
export type DuxStateSubduxes<C> = C extends { '*': infer I }
|
|
||||||
? {
|
|
||||||
[key: string]: StateOf<I>;
|
|
||||||
[index: number]: StateOf<I>;
|
|
||||||
}
|
|
||||||
: C extends object
|
|
||||||
? { [K in keyof C]: StateOf<C[K]> }
|
|
||||||
: unknown;
|
|
||||||
|
|
||||||
type DuxStateGlobSub<S> = S extends { '*': infer I } ? StateOf<I> : unknown;
|
|
||||||
|
|
||||||
type LocalDuxState<S> = S extends never[] ? unknown[] : S;
|
|
||||||
|
|
||||||
/** @ignore */
|
|
||||||
type AggDuxState2<L, S, C> = (L extends never[]
|
|
||||||
? Array<DuxStateGlobSub<S>>
|
|
||||||
: L & DuxStateSubduxes<S>) &
|
|
||||||
DuxStateCoduxes<C>;
|
|
||||||
|
|
||||||
/** @ignore */
|
|
||||||
export type AggDuxState<O, S extends UpduxConfig> = unknown extends O
|
|
||||||
? AggDuxState2<S['initial'], S['subduxes'], S['coduxes']>
|
|
||||||
: O;
|
|
||||||
|
|
||||||
type SelectorsOf<C> = C extends { selectors: infer S } ? S : unknown;
|
|
||||||
|
|
||||||
/** @ignore */
|
|
||||||
export type DuxSelectorsSubduxes<C> = C extends object
|
|
||||||
? UnionToIntersection<SelectorsOf<C[keyof C]>>
|
|
||||||
: unknown;
|
|
||||||
|
|
||||||
/** @ignore */
|
|
||||||
export type DuxSelectorsCoduxes<C> = C extends Array<infer U>
|
|
||||||
? UnionToIntersection<SelectorsOf<U>>
|
|
||||||
: unknown;
|
|
||||||
|
|
||||||
type MaybeReturnType<X> = X extends (...args: any) => any
|
|
||||||
? ReturnType<X>
|
|
||||||
: unknown;
|
|
||||||
|
|
||||||
type RebaseSelector<S, X> = {
|
|
||||||
[K in keyof X]: (state: S) => MaybeReturnType<X[K]>;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ActionsOf<C> = C extends { actions: infer A } ? A : {};
|
|
||||||
|
|
||||||
type DuxActionsSubduxes<C> = C extends object ? ActionsOf<C[keyof C]> : unknown;
|
|
||||||
export type DuxActionsCoduxes<C> = C extends Array<infer I>
|
|
||||||
? UnionToIntersection<ActionsOf<I>>
|
|
||||||
: {};
|
|
||||||
|
|
||||||
type ItemsOf<C> = C extends object ? C[keyof C] : unknown;
|
|
||||||
|
|
||||||
export type DuxActions<A, C extends UpduxConfig> = A extends object
|
|
||||||
? A
|
|
||||||
: UnionToIntersection<
|
|
||||||
ActionsOf<C | ItemsOf<C['subduxes']> | ItemsOf<C['coduxes']>>
|
|
||||||
>;
|
|
||||||
|
|
||||||
export type DuxSelectors<S, X, C extends UpduxConfig> = unknown extends X
|
|
||||||
? RebaseSelector<
|
|
||||||
S,
|
|
||||||
C['selectors'] &
|
|
||||||
DuxSelectorsCoduxes<C['coduxes']> &
|
|
||||||
DuxSelectorsSubduxes<C['subduxes']>
|
|
||||||
>
|
|
||||||
: X;
|
|
||||||
|
|
||||||
export type Dux<S = unknown, A = unknown, X = unknown, C = unknown> = {
|
|
||||||
subduxes: Dictionary<Dux>;
|
|
||||||
coduxes: Dux[];
|
|
||||||
initial: AggDuxState<S, C>;
|
|
||||||
actions: A;
|
|
||||||
subscriptions: Function[];
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configuration object given to Updux's constructor.
|
|
||||||
*
|
|
||||||
* #### arguments
|
|
||||||
*
|
|
||||||
* ##### initial
|
|
||||||
*
|
|
||||||
* Default initial state of the reducer. If applicable, is merged with
|
|
||||||
* the subduxes initial states, with the parent having precedence.
|
|
||||||
*
|
|
||||||
* If not provided, defaults to an empty object.
|
|
||||||
*
|
|
||||||
* ##### actions
|
|
||||||
*
|
|
||||||
* [Actions](/concepts/Actions) used by the updux.
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* import { dux } from 'updux';
|
|
||||||
* import { action, payload } from 'ts-action';
|
|
||||||
*
|
|
||||||
* const bar = action('BAR', payload<int>());
|
|
||||||
* const foo = action('FOO');
|
|
||||||
*
|
|
||||||
* const myDux = dux({
|
|
||||||
* actions: {
|
|
||||||
* bar
|
|
||||||
* },
|
|
||||||
* mutations: [
|
|
||||||
* [ foo, () => state => state ]
|
|
||||||
* ]
|
|
||||||
* });
|
|
||||||
*
|
|
||||||
* myDux.actions.foo({ x: 1, y: 2 }); // => { type: foo, x:1, y:2 }
|
|
||||||
* myDux.actions.bar(2); // => { type: bar, payload: 2 }
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* New actions used directly in mutations and effects will be added to the
|
|
||||||
* dux actions -- that is, they will be accessible via `dux.actions` -- but will
|
|
||||||
* not appear as part of its Typescript type.
|
|
||||||
*
|
|
||||||
* ##### selectors
|
|
||||||
*
|
|
||||||
* Dictionary of selectors for the current updux. The updux also
|
|
||||||
* inherit its subduxes' selectors.
|
|
||||||
*
|
|
||||||
* The selectors are available via the class' getter.
|
|
||||||
*
|
|
||||||
* ##### mutations
|
|
||||||
*
|
|
||||||
* mutations: [
|
|
||||||
* [ action, mutation, isSink ],
|
|
||||||
* ...
|
|
||||||
* ]
|
|
||||||
*
|
|
||||||
* or
|
|
||||||
*
|
|
||||||
* mutations: {
|
|
||||||
* action: mutation,
|
|
||||||
* ...
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* List of mutations for assign to the dux. If you want Typescript goodness, you
|
|
||||||
* probably want to use `addMutation()` instead.
|
|
||||||
*
|
|
||||||
* In its generic array-of-array form,
|
|
||||||
* each mutation tuple contains: the action, the mutation,
|
|
||||||
* and boolean indicating if this is a sink mutation.
|
|
||||||
*
|
|
||||||
* The action can be an action creator function or a string. If it's a string, it's considered to be the
|
|
||||||
* action type and a generic `action( actionName, payload() )` creator will be
|
|
||||||
* generated for it. If an action is not already defined in the `actions`
|
|
||||||
* parameter, it'll be automatically added.
|
|
||||||
*
|
|
||||||
* The pseudo-action type `*` can be used to match any action not explicitly matched by other mutations.
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* 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;
|
|
||||||
* },
|
|
||||||
* }
|
|
||||||
* });
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* The signature of the mutations is `(payload,action) => state => newState`.
|
|
||||||
* It is designed to play well with `Updeep` (and [Immer](https://immerjs.github.io/immer/docs/introduction)). This way, instead of doing
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* mutation: {
|
|
||||||
* renameTodo: newName => state => { ...state, name: newName }
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* we can do
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* mutation: {
|
|
||||||
* renameTodo: newName => u({ name: newName })
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* The final argument is the optional boolean `isSink`. If it is true, it'll
|
|
||||||
* prevent subduxes' mutations on the same action. It defaults to `false`.
|
|
||||||
*
|
|
||||||
* The object version of the argument can be used as a shortcut when all actions
|
|
||||||
* are strings. In that case, `isSink` is `false` for all mutations.
|
|
||||||
*
|
|
||||||
* ##### groomMutations
|
|
||||||
*
|
|
||||||
* Function that can be provided to alter all local mutations of the updux
|
|
||||||
* (the mutations of subduxes are left untouched).
|
|
||||||
*
|
|
||||||
* Can be used, for example, for Immer integration:
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* 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
|
|
||||||
* }
|
|
||||||
* });
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* Or perhaps for debugging:
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* import Updux from 'updux';
|
|
||||||
*
|
|
||||||
* const updux = new Updux({
|
|
||||||
* initial: { counter: 0 },
|
|
||||||
* groomMutations: mutation => (...args) => state => {
|
|
||||||
* console.log( "got action ", args[1] );
|
|
||||||
* return mutation(...args)(state);
|
|
||||||
* }
|
|
||||||
* });
|
|
||||||
* ```
|
|
||||||
* ##### subduxes
|
|
||||||
*
|
|
||||||
* Object mapping slices of the state to sub-upduxes. In addition to creating
|
|
||||||
* sub-reducers for those slices, it'll make the parend updux inherit all the
|
|
||||||
* actions and middleware from its subduxes.
|
|
||||||
*
|
|
||||||
* For example, if in plain Redux you would do
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* import { combineReducers } from 'redux';
|
|
||||||
* import todosReducer from './todos';
|
|
||||||
* import statisticsReducer from './statistics';
|
|
||||||
*
|
|
||||||
* const rootReducer = combineReducers({
|
|
||||||
* todos: todosReducer,
|
|
||||||
* stats: statisticsReducer,
|
|
||||||
* });
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* then with Updux you'd do
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* import { updux } from 'updux';
|
|
||||||
* import todos from './todos';
|
|
||||||
* import statistics from './statistics';
|
|
||||||
*
|
|
||||||
* const rootUpdux = updux({
|
|
||||||
* subduxes: {
|
|
||||||
* todos,
|
|
||||||
* statistics
|
|
||||||
* }
|
|
||||||
* });
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* ##### effects
|
|
||||||
*
|
|
||||||
* Array of arrays or plain object defining asynchronous actions and side-effects triggered by actions.
|
|
||||||
* The effects themselves are Redux middleware, 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);
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
* });
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
*
|
|
||||||
* ```
|
|
||||||
* import Updux from 'updux';
|
|
||||||
* import { actions, payload } from 'ts-action';
|
|
||||||
* import u from '@yanick/updeep';
|
|
||||||
*
|
|
||||||
* const todoUpdux = new Updux({
|
|
||||||
* initial: {
|
|
||||||
* done: false,
|
|
||||||
* note: "",
|
|
||||||
* },
|
|
||||||
* actions: {
|
|
||||||
* finish: action('FINISH', payload()),
|
|
||||||
* edit: action('EDIT', payload()),
|
|
||||||
* },
|
|
||||||
* mutations: [
|
|
||||||
* [ edit, note => u({note}) ]
|
|
||||||
* ],
|
|
||||||
* selectors: {
|
|
||||||
* getNote: state => state.note
|
|
||||||
* },
|
|
||||||
* groomMutations: mutation => transform(mutation),
|
|
||||||
* subduxes: {
|
|
||||||
* foo
|
|
||||||
* },
|
|
||||||
* effects: {
|
|
||||||
* finish: () => next => action => {
|
|
||||||
* console.log( "Woo! one more bites the dust" );
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
* })
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export type UpduxConfig = Partial<{
|
|
||||||
initial: unknown /** foo */;
|
|
||||||
subduxes: Dictionary<Dux>;
|
|
||||||
coduxes: Dux[];
|
|
||||||
actions: Dictionary<ActionCreator>;
|
|
||||||
selectors: Dictionary<Selector>;
|
|
||||||
mutations: any;
|
|
||||||
groomMutations: (m: Mutation) => Mutation;
|
|
||||||
effects: any;
|
|
||||||
subscriptions: Function[];
|
|
||||||
}>;
|
|
||||||
|
|
||||||
export type Upreducer<S = any> = (action: Action) => (state: S) => S;
|
|
||||||
|
|
||||||
/** @ignore */
|
|
||||||
export interface UpduxMiddlewareAPI<S = any, X = Dictionary<Selector>> {
|
|
||||||
dispatch: Function;
|
|
||||||
getState(): S;
|
|
||||||
selectors: X;
|
|
||||||
actions: Dictionary<ActionCreator>;
|
|
||||||
}
|
|
||||||
export type UpduxMiddleware<S = any, X = Dictionary<Selector>, A = Action> = (
|
|
||||||
api: UpduxMiddlewareAPI<S, X>
|
|
||||||
) => (next: Function) => (action: A) => any;
|
|
||||||
|
|
||||||
export type Selector<S = any> = (state: S) => unknown;
|
|
||||||
|
|
||||||
export type DuxState<D> = D extends { initial: infer S } ? S : unknown;
|
|
576
src/updux.ts
576
src/updux.ts
@ -1,576 +0,0 @@
|
|||||||
import fp from 'lodash/fp';
|
|
||||||
import { action, payload, ActionCreator, ActionType } from 'ts-action';
|
|
||||||
import { AnyAction } from 'redux';
|
|
||||||
|
|
||||||
import buildInitial from './buildInitial';
|
|
||||||
import buildMutations from './buildMutations';
|
|
||||||
|
|
||||||
import buildCreateStore from './buildCreateStore';
|
|
||||||
import buildMiddleware, { effectToMw, Effect } from './buildMiddleware';
|
|
||||||
import buildUpreducer from './buildUpreducer';
|
|
||||||
import {
|
|
||||||
UpduxConfig,
|
|
||||||
Dictionary,
|
|
||||||
Action,
|
|
||||||
Mutation,
|
|
||||||
Upreducer,
|
|
||||||
UpduxMiddleware,
|
|
||||||
Selector,
|
|
||||||
Dux,
|
|
||||||
UnionToIntersection,
|
|
||||||
AggDuxState,
|
|
||||||
DuxSelectors,
|
|
||||||
DuxActions,
|
|
||||||
} from './types';
|
|
||||||
|
|
||||||
import { Store, PreloadedState } from 'redux';
|
|
||||||
import buildSelectors from './buildSelectors';
|
|
||||||
|
|
||||||
type Merge<T> = UnionToIntersection<T[keyof T]>;
|
|
||||||
|
|
||||||
type ActionsOf<U> = U extends Updux ? U['actions'] : {};
|
|
||||||
|
|
||||||
//| ActionsOf<SubduxesOf<U>[keyof SubduxesOf<U>]>
|
|
||||||
export type UpduxActions<U> = U extends Updux
|
|
||||||
? UnionToIntersection<
|
|
||||||
UpduxLocalActions<U> | ActionsOf<CoduxesOf<U>[keyof CoduxesOf<U>]>
|
|
||||||
>
|
|
||||||
: {};
|
|
||||||
|
|
||||||
export type UpduxLocalActions<S> = S extends Updux<any, null>
|
|
||||||
? {}
|
|
||||||
: S extends Updux<any, infer A>
|
|
||||||
? A
|
|
||||||
: {};
|
|
||||||
export type CoduxesOf<U> = U extends Updux<any, any, any, infer S> ? S : [];
|
|
||||||
|
|
||||||
type StoreWithDispatchActions<
|
|
||||||
S = any,
|
|
||||||
Actions = { [action: string]: (...args: any) => Action }
|
|
||||||
> = Store<S> & {
|
|
||||||
dispatch: { [type in keyof Actions]: (...args: any) => void };
|
|
||||||
};
|
|
||||||
|
|
||||||
function wrap_subscription(sub) {
|
|
||||||
return store => {
|
|
||||||
const sub_curried = sub(store);
|
|
||||||
let previous: unknown;
|
|
||||||
|
|
||||||
return (state, unsubscribe) => {
|
|
||||||
if (state === previous) return;
|
|
||||||
previous = state;
|
|
||||||
|
|
||||||
return sub_curried(state, unsubscribe);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function _subscribeToStore(store: any, subscriptions: Function[] = []) {
|
|
||||||
subscriptions.forEach(sub => {
|
|
||||||
const subscriber = sub(store);
|
|
||||||
let unsub = store.subscribe(() => {
|
|
||||||
const state = store.getState();
|
|
||||||
return subscriber(state, unsub);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function sliced_subscription(slice, sub) {
|
|
||||||
return store => {
|
|
||||||
const sub_curried = sub(store);
|
|
||||||
|
|
||||||
return (state, unsubscribe) =>
|
|
||||||
sub_curried(fp.get(slice, state), unsubscribe);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @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<
|
|
||||||
S = unknown,
|
|
||||||
A = null,
|
|
||||||
X = unknown,
|
|
||||||
C extends UpduxConfig = {}
|
|
||||||
> {
|
|
||||||
subduxes: Dictionary<Dux>;
|
|
||||||
coduxes: Dux[];
|
|
||||||
|
|
||||||
private localSelectors: Dictionary<Selector> = {};
|
|
||||||
|
|
||||||
private localInitial: unknown;
|
|
||||||
|
|
||||||
groomMutations: (mutation: Mutation<S>) => Mutation<S>;
|
|
||||||
|
|
||||||
private localEffects: Effect[] = [];
|
|
||||||
|
|
||||||
private localActions: Dictionary<ActionCreator> = {};
|
|
||||||
|
|
||||||
private localMutations: Dictionary<
|
|
||||||
Mutation<S> | [Mutation<S>, boolean | undefined]
|
|
||||||
> = {};
|
|
||||||
|
|
||||||
private localSubscriptions: Function[] = [];
|
|
||||||
|
|
||||||
get initial(): AggDuxState<S, C> {
|
|
||||||
return buildInitial(
|
|
||||||
this.localInitial,
|
|
||||||
this.coduxes.map(({ initial }) => initial),
|
|
||||||
fp.mapValues('initial', this.subduxes)
|
|
||||||
) as any;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param config an [[UpduxConfig]] plain object
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
constructor(config: C = {} as C) {
|
|
||||||
this.localInitial = config.initial ?? {};
|
|
||||||
this.localSelectors = config.selectors ?? {};
|
|
||||||
this.coduxes = config.coduxes ?? [];
|
|
||||||
this.subduxes = config.subduxes ?? {};
|
|
||||||
|
|
||||||
Object.entries(config.actions ?? {}).forEach(args =>
|
|
||||||
(this.addAction as any)(...args)
|
|
||||||
);
|
|
||||||
|
|
||||||
this.coduxes.forEach((c: any) =>
|
|
||||||
Object.entries(c.actions).forEach(args =>
|
|
||||||
(this.addAction as any)(...args)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
Object.values(this.subduxes).forEach((c: any) => {
|
|
||||||
Object.entries(c.actions).forEach(args => {
|
|
||||||
(this.addAction as any)(...args);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (config.subscriptions) {
|
|
||||||
config.subscriptions.forEach(sub => this.addSubscription(sub));
|
|
||||||
}
|
|
||||||
|
|
||||||
this.groomMutations = config.groomMutations ?? ((x: Mutation<S>) => x);
|
|
||||||
|
|
||||||
let effects = config.effects ?? [];
|
|
||||||
|
|
||||||
if (!Array.isArray(effects)) {
|
|
||||||
effects = (Object.entries(effects) as unknown) as Effect[];
|
|
||||||
}
|
|
||||||
effects.forEach(effect => (this.addEffect as any)(...effect));
|
|
||||||
|
|
||||||
let mutations = config.mutations ?? [];
|
|
||||||
|
|
||||||
if (!Array.isArray(mutations)) {
|
|
||||||
mutations = fp.toPairs(mutations);
|
|
||||||
}
|
|
||||||
|
|
||||||
mutations.forEach(args => (this.addMutation as any)(...args));
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
Object.entries(selectors).forEach(([name, sel]: [string, Function]) =>
|
|
||||||
this.addSelector(name, sel as Selector)
|
|
||||||
);
|
|
||||||
|
|
||||||
Object.entries(
|
|
||||||
fp.mapValues((value: UpduxConfig | Updux) =>
|
|
||||||
fp.isPlainObject(value) ? new Updux(value as any) : value
|
|
||||||
)(fp.getOr({}, 'subduxes', config))
|
|
||||||
).forEach(([slice, sub]) => (this.subduxes[slice] = sub as any));
|
|
||||||
|
|
||||||
const actions = fp.getOr({}, 'actions', config);
|
|
||||||
Object.entries(actions as any).forEach(([type, p]: [string, any]): any =>
|
|
||||||
this.addAction((p as any).type ? p : action(type, p))
|
|
||||||
);
|
|
||||||
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Array of middlewares aggregating all the effects defined in the
|
|
||||||
* updux and its subduxes. Effects of the updux itself are
|
|
||||||
* done before the subduxes effects.
|
|
||||||
* Note that `getState` will always return the state of the
|
|
||||||
* local updux.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
*
|
|
||||||
* ```
|
|
||||||
* const middleware = updux.middleware;
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
get middleware(): UpduxMiddleware<
|
|
||||||
AggDuxState<S, C>,
|
|
||||||
DuxSelectors<AggDuxState<S, C>, X, C>
|
|
||||||
> {
|
|
||||||
const selectors = this.selectors;
|
|
||||||
const actions = this.actions;
|
|
||||||
return buildMiddleware(
|
|
||||||
this.localEffects.map(effect =>
|
|
||||||
effectToMw(effect, actions as any, selectors as any)
|
|
||||||
),
|
|
||||||
(this.coduxes as any).map(fp.get('middleware')) as any,
|
|
||||||
fp.mapValues('middleware', this.subduxes)
|
|
||||||
) as any;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Action creators for all actions defined or used in the actions, mutations, effects and subduxes
|
|
||||||
* of the updux config.
|
|
||||||
*
|
|
||||||
* Non-custom action creators defined in `actions` have the signature `(payload={},meta={}) => ({type,
|
|
||||||
* payload,meta})` (with the extra sugar that if `meta` or `payload` are not
|
|
||||||
* specified, that key won't be present in the produced action).
|
|
||||||
*
|
|
||||||
* The same action creator can be included
|
|
||||||
* in multiple subduxes. However, if two different creators
|
|
||||||
* are included for the same action, an error will be thrown.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
*
|
|
||||||
* ```
|
|
||||||
* const actions = updux.actions;
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
get actions(): DuxActions<A, C> {
|
|
||||||
// UpduxActions<Updux<S,A,SUB,CO>> {
|
|
||||||
return this.localActions as any;
|
|
||||||
}
|
|
||||||
|
|
||||||
get upreducer(): Upreducer<S> {
|
|
||||||
return buildUpreducer(this.initial, this.mutations as any) as any;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A Redux reducer generated using the computed initial state and
|
|
||||||
* mutations.
|
|
||||||
*/
|
|
||||||
get reducer(): (state: S | undefined, action: Action) => S {
|
|
||||||
return (state, action) => this.upreducer(action)(state as S);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Merge of the updux and subduxes mutations. If an action triggers
|
|
||||||
* mutations in both the main updux and its subduxes, the subduxes
|
|
||||||
* mutations will be performed first.
|
|
||||||
*/
|
|
||||||
get mutations(): Dictionary<Mutation<S>> {
|
|
||||||
return buildMutations(
|
|
||||||
this.localMutations,
|
|
||||||
fp.mapValues('upreducer', this.subduxes as any),
|
|
||||||
fp.map('upreducer', this.coduxes as any)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the upreducer made of the merge of all sudbuxes reducers, without
|
|
||||||
* the local mutations. Useful, for example, for sink mutations.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
*
|
|
||||||
* ```
|
|
||||||
* import todo from './todo'; // updux for a single todo
|
|
||||||
* import Updux from 'updux';
|
|
||||||
* import u from '@yanick/updeep';
|
|
||||||
*
|
|
||||||
* const todos = new Updux({ initial: [], subduxes: { '*': todo } });
|
|
||||||
* todos.addMutation(
|
|
||||||
* todo.actions.done,
|
|
||||||
* ({todo_id},action) => u.map( u.if( u.is('id',todo_id) ), todos.subduxUpreducer(action) )
|
|
||||||
* true
|
|
||||||
* );
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
get subduxUpreducer() {
|
|
||||||
return buildUpreducer(this.initial, buildMutations({}, this.subduxes));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a `createStore` function that takes two argument:
|
|
||||||
* `initial` and `injectEnhancer`. `initial` is a custom
|
|
||||||
* initial state for the store, and `injectEnhancer` is a function
|
|
||||||
* taking in the middleware built by the updux object and allowing
|
|
||||||
* you to wrap it in any enhancer you want.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
*
|
|
||||||
* ```
|
|
||||||
* const createStore = updux.createStore;
|
|
||||||
*
|
|
||||||
* const store = createStore(initial);
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
createStore(...args: any) {
|
|
||||||
const store = buildCreateStore<AggDuxState<S, C>, DuxActions<A, C>>(
|
|
||||||
this.reducer as any,
|
|
||||||
this.middleware as any,
|
|
||||||
this.actions
|
|
||||||
)(...args);
|
|
||||||
|
|
||||||
_subscribeToStore(store, this.subscriptions);
|
|
||||||
|
|
||||||
return store;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an array of all subscription functions registered for the dux.
|
|
||||||
* Subdux subscriptions are wrapped such that they are getting their
|
|
||||||
* local state. Also all subscriptions are further wrapped such that
|
|
||||||
* they are only called when the local state changed
|
|
||||||
*/
|
|
||||||
get subscriptions() {
|
|
||||||
let subscriptions = ([
|
|
||||||
this.localSubscriptions,
|
|
||||||
Object.entries(this.subduxes).map(([slice, subdux]) => {
|
|
||||||
return subdux.subscriptions.map(sub =>
|
|
||||||
sliced_subscription(slice, sub)
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
] as any).flat(Infinity);
|
|
||||||
|
|
||||||
return subscriptions.map(sub => wrap_subscription(sub));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a <a href="https://github.com/erikras/ducks-modular-redux">ducks</a>-like
|
|
||||||
* plain object holding the reducer from the Updux object and all
|
|
||||||
* its trimmings.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
*
|
|
||||||
* ```
|
|
||||||
* const {
|
|
||||||
* createStore,
|
|
||||||
* upreducer,
|
|
||||||
* subduxes,
|
|
||||||
* coduxes,
|
|
||||||
* middleware,
|
|
||||||
* actions,
|
|
||||||
* reducer,
|
|
||||||
* mutations,
|
|
||||||
* initial,
|
|
||||||
* selectors,
|
|
||||||
* subscriptions,
|
|
||||||
* } = myUpdux.asDux;
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
get asDux() {
|
|
||||||
return {
|
|
||||||
createStore: this.createStore.bind(this),
|
|
||||||
upreducer: this.upreducer,
|
|
||||||
subduxes: this.subduxes,
|
|
||||||
coduxes: this.coduxes,
|
|
||||||
middleware: this.middleware,
|
|
||||||
actions: this.actions,
|
|
||||||
reducer: this.reducer,
|
|
||||||
mutations: this.mutations,
|
|
||||||
initial: this.initial,
|
|
||||||
selectors: this.selectors,
|
|
||||||
subscriptions: this.subscriptions,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a mutation and its associated action to the updux.
|
|
||||||
*
|
|
||||||
* @param isSink - If `true`, disables the subduxes mutations for this action. To
|
|
||||||
* conditionally run the subduxes mutations, check out [[subduxUpreducer]]. Defaults to `false`.
|
|
||||||
*
|
|
||||||
* @remarks
|
|
||||||
*
|
|
||||||
* If a local mutation was already associated to the action,
|
|
||||||
* it will be replaced by the new one.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* updux.addMutation(
|
|
||||||
* action('ADD', payload<int>() ),
|
|
||||||
* inc => state => state + in
|
|
||||||
* );
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
addMutation<A extends ActionCreator>(
|
|
||||||
creator: A,
|
|
||||||
mutation: Mutation<S, ActionType<A>>,
|
|
||||||
isSink?: boolean
|
|
||||||
);
|
|
||||||
addMutation<A extends ActionCreator = any>(
|
|
||||||
creator: string,
|
|
||||||
mutation: Mutation<S, any>,
|
|
||||||
isSink?: boolean
|
|
||||||
);
|
|
||||||
addMutation<A extends ActionCreator = any>(creator, mutation, isSink) {
|
|
||||||
const c = this.addAction(creator);
|
|
||||||
|
|
||||||
this.localMutations[c.type] = [
|
|
||||||
this.groomMutations(mutation as any) as Mutation<S>,
|
|
||||||
isSink,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
addEffect<AC extends ActionCreator>(
|
|
||||||
creator: AC,
|
|
||||||
middleware: UpduxMiddleware<
|
|
||||||
AggDuxState<S, C>,
|
|
||||||
DuxSelectors<AggDuxState<S, C>, X, C>,
|
|
||||||
ReturnType<AC>
|
|
||||||
>,
|
|
||||||
isGenerator?: boolean
|
|
||||||
);
|
|
||||||
addEffect(
|
|
||||||
creator: string,
|
|
||||||
middleware: UpduxMiddleware<
|
|
||||||
AggDuxState<S, C>,
|
|
||||||
DuxSelectors<AggDuxState<S, C>, X, C>
|
|
||||||
>,
|
|
||||||
isGenerator?: boolean
|
|
||||||
);
|
|
||||||
addEffect(creator, middleware, isGenerator = false) {
|
|
||||||
const c = this.addAction(creator);
|
|
||||||
this.localEffects.push([c.type, middleware, isGenerator] as any);
|
|
||||||
}
|
|
||||||
|
|
||||||
// can be
|
|
||||||
//addAction( actionCreator )
|
|
||||||
// addAction( 'foo', transform )
|
|
||||||
/**
|
|
||||||
* Adds an action to the updux. It can take an already defined action
|
|
||||||
* creator, or any arguments that can be passed to `actionCreator`.
|
|
||||||
* @example
|
|
||||||
* ```
|
|
||||||
* const action = updux.addAction( name, ...creatorArgs );
|
|
||||||
* const action = updux.addAction( otherActionCreator );
|
|
||||||
* ```
|
|
||||||
* @example
|
|
||||||
* ```
|
|
||||||
* import {actionCreator, Updux} from 'updux';
|
|
||||||
*
|
|
||||||
* const updux = new Updux();
|
|
||||||
*
|
|
||||||
* const foo = updux.addAction('foo');
|
|
||||||
* const bar = updux.addAction( 'bar', (x) => ({stuff: x+1}) );
|
|
||||||
*
|
|
||||||
* const baz = actionCreator( 'baz' );
|
|
||||||
*
|
|
||||||
* foo({ a: 1}); // => { type: 'foo', payload: { a: 1 } }
|
|
||||||
* bar(2); // => { type: 'bar', payload: { stuff: 3 } }
|
|
||||||
* baz(); // => { type: 'baz', payload: undefined }
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
addAction(theaction: string, transform?: any): ActionCreator<string, any>;
|
|
||||||
addAction(
|
|
||||||
theaction: string | ActionCreator<any>,
|
|
||||||
transform?: never
|
|
||||||
): ActionCreator<string, any>;
|
|
||||||
addAction(actionIn: any, transform: any) {
|
|
||||||
let name: string;
|
|
||||||
let creator: ActionCreator;
|
|
||||||
|
|
||||||
if (typeof actionIn === 'string') {
|
|
||||||
name = actionIn;
|
|
||||||
|
|
||||||
if (transform) {
|
|
||||||
creator = transform.type
|
|
||||||
? transform
|
|
||||||
: action(name, (...args: any) => ({
|
|
||||||
payload: transform(...args),
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
creator = this.localActions[name] ?? action(name, payload());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
name = actionIn.type;
|
|
||||||
creator = actionIn;
|
|
||||||
}
|
|
||||||
|
|
||||||
const already = this.localActions[name];
|
|
||||||
|
|
||||||
if (!already)
|
|
||||||
return ((this.localActions as any)[name] = creator) as any;
|
|
||||||
|
|
||||||
if (already !== creator && already.type !== '*') {
|
|
||||||
throw new Error(`action ${name} already exists`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return already;
|
|
||||||
}
|
|
||||||
|
|
||||||
get _middlewareEntries() {
|
|
||||||
const groupByOrder = (mws: any) =>
|
|
||||||
fp.groupBy(
|
|
||||||
([, , actionType]: any) =>
|
|
||||||
['^', '$'].includes(actionType) ? actionType : 'middle',
|
|
||||||
mws
|
|
||||||
);
|
|
||||||
|
|
||||||
const subs = fp.flow([
|
|
||||||
fp.toPairs,
|
|
||||||
fp.map(([slice, updux]) =>
|
|
||||||
updux._middlewareEntries.map(([u, ps, ...args]: any) => [
|
|
||||||
u,
|
|
||||||
[slice, ...ps],
|
|
||||||
...args,
|
|
||||||
])
|
|
||||||
),
|
|
||||||
fp.flatten,
|
|
||||||
groupByOrder,
|
|
||||||
])(this.subduxes);
|
|
||||||
|
|
||||||
const local = groupByOrder(
|
|
||||||
this.localEffects.map(x => [this, [], ...x])
|
|
||||||
);
|
|
||||||
|
|
||||||
return fp.flatten(
|
|
||||||
[
|
|
||||||
local['^'],
|
|
||||||
subs['^'],
|
|
||||||
local.middle,
|
|
||||||
subs.middle,
|
|
||||||
subs['$'],
|
|
||||||
local['$'],
|
|
||||||
].filter(x => x)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
addSelector(name: string, selector: Selector) {
|
|
||||||
this.localSelectors[name] = selector;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
A dictionary of the updux's selectors. Subduxes'
|
|
||||||
selectors are included as well (with the mapping to the
|
|
||||||
sub-state already taken care of you).
|
|
||||||
*/
|
|
||||||
get selectors(): DuxSelectors<AggDuxState<S, C>, X, C> {
|
|
||||||
return buildSelectors(
|
|
||||||
this.localSelectors,
|
|
||||||
fp.map('selectors', this.coduxes),
|
|
||||||
fp.mapValues('selectors', this.subduxes)
|
|
||||||
) as any;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a subscription to the dux.
|
|
||||||
*/
|
|
||||||
addSubscription(subscription: Function) {
|
|
||||||
this.localSubscriptions = [...this.localSubscriptions, subscription];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Updux;
|
|
@ -1,22 +0,0 @@
|
|||||||
import tap from 'tap';
|
|
||||||
import { expectType } from 'tsd';
|
|
||||||
|
|
||||||
import { dux } from '..';
|
|
||||||
|
|
||||||
import { action } from 'ts-action';
|
|
||||||
|
|
||||||
const d = dux({
|
|
||||||
initial: 123,
|
|
||||||
actions: {
|
|
||||||
foo: action('foo'),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expectType<number>( d.createStore().getState() );
|
|
||||||
|
|
||||||
tap.equal(d.createStore().getState(), 123, 'default initial state');
|
|
||||||
|
|
||||||
tap.equal(d.createStore(456).getState(), 456, 'given initial state');
|
|
||||||
|
|
||||||
expectType<{ foo: Function }>( d.createStore().actions );
|
|
||||||
tap.pass('we have actions');
|
|
@ -1,75 +0,0 @@
|
|||||||
#!/usr/bin/env perl
|
|
||||||
|
|
||||||
use 5.30.0;
|
|
||||||
use warnings;
|
|
||||||
use experimental qw/
|
|
||||||
signatures
|
|
||||||
postderef
|
|
||||||
/;
|
|
||||||
|
|
||||||
|
|
||||||
use Path::Tiny;
|
|
||||||
use Path::Tiny::Glob pathglob => { all => 1};
|
|
||||||
use File::Serialize;
|
|
||||||
use List::AllUtils qw/ before_incl /;
|
|
||||||
use List::UtilsBy qw/ partition_by /;
|
|
||||||
|
|
||||||
my $api = deserialize_file './temp/updux.api.json';
|
|
||||||
|
|
||||||
my @lines = generate_index([$api]);
|
|
||||||
|
|
||||||
my $sidebar = path('docs/_sidebar.md');
|
|
||||||
|
|
||||||
$sidebar->spew(
|
|
||||||
( before_incl { /!-- API/ } $sidebar->lines ),
|
|
||||||
map { "$_\n" } @lines
|
|
||||||
);
|
|
||||||
|
|
||||||
sub generate_index($entries,$indent = 2) {
|
|
||||||
my @lines;
|
|
||||||
|
|
||||||
my %sections = partition_by {
|
|
||||||
$_->{kind}
|
|
||||||
} @$entries;
|
|
||||||
|
|
||||||
|
|
||||||
for my $type ( sort keys %sections ) {
|
|
||||||
my $entries = $sections{$type};
|
|
||||||
|
|
||||||
if ( $type eq 'Constructor' ) {
|
|
||||||
push @lines, sprintf "%s- [%s](%s)",
|
|
||||||
" " x $indent, 'Constructor', ref2link(@$entries);
|
|
||||||
next;
|
|
||||||
}
|
|
||||||
|
|
||||||
push @lines, join '', " " x $indent, '- ', $type unless $type eq 'EntryPoint';
|
|
||||||
|
|
||||||
for my $entry ( @$entries ) {
|
|
||||||
my $i = $indent;
|
|
||||||
|
|
||||||
if( $entry->{name} ){
|
|
||||||
my $link = ref2link($entry);
|
|
||||||
|
|
||||||
push @lines, sprintf "%s- [%s](%s)",
|
|
||||||
" " x (++$i), $entry->{name}, $link;
|
|
||||||
}
|
|
||||||
|
|
||||||
my $members = $entry->{members} or next;
|
|
||||||
|
|
||||||
push @lines, generate_index($members, $i+1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return @lines;
|
|
||||||
}
|
|
||||||
|
|
||||||
sub ref2link($entry) {
|
|
||||||
return "/4-API/".
|
|
||||||
lc( $entry->{canonicalReference} =~ s/!|#/./gr
|
|
||||||
=~ s/:constructor/._constructor_/r
|
|
||||||
=~ s/:\w+//r
|
|
||||||
=~ s/\)//r
|
|
||||||
=~ s/\((\d+)/'_' . ($1-1)/er
|
|
||||||
=~ s/_0//r ). '.md'
|
|
||||||
;
|
|
||||||
}
|
|
@ -1,66 +0,0 @@
|
|||||||
{
|
|
||||||
"include": [
|
|
||||||
"./src/**/*"
|
|
||||||
],
|
|
||||||
"compilerOptions": {
|
|
||||||
/* Basic Options */
|
|
||||||
"incremental": true, /* Enable incremental compilation */
|
|
||||||
"target": "ES2019", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
|
|
||||||
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
|
|
||||||
"lib": [ "dom", "es2019" ], /* Specify library files to be included in the compilation. */
|
|
||||||
"allowJs": false, /* Allow javascript files to be compiled. */
|
|
||||||
// "checkJs": true, /* Report errors in .js files. */
|
|
||||||
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
|
|
||||||
"declaration": true, /* Generates corresponding '.d.ts' file. */
|
|
||||||
"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": false, /* 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. */
|
|
||||||
}
|
|
||||||
}
|
|
10
typedoc.json
10
typedoc.json
@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"theme": "markdown",
|
|
||||||
"out": "docs/API",
|
|
||||||
"mode": "modules",
|
|
||||||
"excludePrivate": true,
|
|
||||||
"excludeNotExported": true,
|
|
||||||
"hideSources": true,
|
|
||||||
"includeVersion": true,
|
|
||||||
"exclude": [ "src/**/*test.ts" ]
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user