├── packages ├── .gitkeep ├── plugins │ ├── xstate-immer │ │ ├── src │ │ │ ├── index.ts │ │ │ └── plugin.ts │ │ ├── tests │ │ │ ├── modern-app-env.d.ts │ │ │ ├── tsconfig.json │ │ │ └── .eslintrc.js │ │ ├── .eslintrc.js │ │ ├── tsconfig.json │ │ ├── modern.config.js │ │ ├── .npmignore │ │ ├── README.md │ │ ├── LICENSE │ │ ├── package.json │ │ └── CHANGELOG.md │ ├── xstate │ │ ├── src │ │ │ ├── types │ │ │ │ ├── index.ts │ │ │ │ ├── map.ts │ │ │ │ ├── machine.ts │ │ │ │ └── override.ts │ │ │ ├── machine.ts │ │ │ ├── index.ts │ │ │ ├── utils.ts │ │ │ ├── check.ts │ │ │ ├── const.ts │ │ │ ├── map.ts │ │ │ └── plugin.ts │ │ ├── tests │ │ │ ├── modern-app-env.d.ts │ │ │ ├── tsconfig.json │ │ │ └── .eslintrc.js │ │ ├── .eslintrc.js │ │ ├── modern.config.js │ │ ├── tsconfig.json │ │ ├── .npmignore │ │ ├── README.md │ │ ├── LICENSE │ │ ├── package.json │ │ └── CHANGELOG.md │ ├── auto-actions │ │ ├── src │ │ │ ├── primitive.ts │ │ │ ├── utils.ts │ │ │ ├── object.ts │ │ │ ├── array.ts │ │ │ └── index.ts │ │ ├── tests │ │ │ ├── modern-app-env.d.ts │ │ │ ├── tsconfig.json │ │ │ ├── .eslintrc.js │ │ │ ├── object.test.ts │ │ │ ├── primitive.test.ts │ │ │ └── array.test.ts │ │ ├── .eslintrc.js │ │ ├── tsconfig.json │ │ ├── modern.config.js │ │ ├── .npmignore │ │ ├── README.md │ │ ├── LICENSE │ │ └── package.json │ ├── effects │ │ ├── tests │ │ │ ├── modern-app-env.d.ts │ │ │ ├── tsconfig.json │ │ │ ├── .eslintrc.js │ │ │ └── index.test.ts │ │ ├── src │ │ │ ├── index.ts │ │ │ └── plugin.ts │ │ ├── .eslintrc.js │ │ ├── tsconfig.json │ │ ├── modern.config.js │ │ ├── .npmignore │ │ ├── README.md │ │ ├── LICENSE │ │ └── package.json │ ├── immer │ │ ├── tests │ │ │ ├── modern-app-env.d.ts │ │ │ ├── tsconfig.json │ │ │ ├── .eslintrc.js │ │ │ └── index.test.ts │ │ ├── .eslintrc.js │ │ ├── tsconfig.json │ │ ├── modern.config.js │ │ ├── src │ │ │ └── index.ts │ │ ├── .npmignore │ │ ├── README.md │ │ ├── LICENSE │ │ └── package.json │ └── devtools │ │ ├── .eslintrc.js │ │ ├── tsconfig.json │ │ ├── modern.config.js │ │ ├── src │ │ └── index.ts │ │ ├── .npmignore │ │ ├── README.md │ │ ├── LICENSE │ │ └── package.json ├── react │ ├── src │ │ ├── .eslintrc.json │ │ ├── hook.ts │ │ ├── index.ts │ │ ├── utils │ │ │ └── useIsomorphicLayoutEffect.ts │ │ └── batchManager.ts │ ├── tests │ │ ├── modern-app-env.d.ts │ │ ├── tsconfig.json │ │ ├── .eslintrc.js │ │ └── batch.test.tsx │ ├── .eslintrc.js │ ├── type.d.ts │ ├── tsconfig.json │ ├── .npmignore │ ├── README.md │ ├── LICENSE │ ├── modern.config.js │ └── package.json └── store │ ├── src │ ├── utils │ │ ├── index.ts │ │ ├── memoize.ts │ │ └── misc.ts │ ├── types │ │ ├── index.ts │ │ ├── utils.ts │ │ ├── plugin.ts │ │ └── app.ts │ ├── plugin │ │ ├── index.ts │ │ ├── createPlugin.ts │ │ └── core.ts │ ├── index.ts │ ├── __tsd__ │ │ ├── computed.tsd.ts │ │ ├── selector.tsd.ts │ │ └── model.tsd.ts │ ├── model │ │ ├── subscribe.ts │ │ ├── model.ts │ │ └── useModel.ts │ └── store │ │ ├── createStore.ts │ │ └── context.ts │ ├── tests │ ├── modern-app-env.d.ts │ ├── tsconfig.json │ ├── .eslintrc.js │ ├── store.test.ts │ ├── selector.test.ts │ ├── onMount.test.ts │ ├── subscribe.test.ts │ ├── useModel.test.ts │ └── model.test.ts │ ├── .eslintrc.js │ ├── tsconfig.json │ ├── modern.config.js │ ├── .npmignore │ ├── README.md │ ├── LICENSE │ ├── package.json │ └── CHANGELOG.md ├── .nvmrc ├── scripts ├── src │ ├── index.js │ └── get-release-version.ts ├── tsconfig.json ├── .eslintrc.js ├── package.json └── bin │ └── only-allow-pnpm.js ├── .npmrc ├── examples ├── vite-counter │ ├── src │ │ ├── vite-env.d.ts │ │ ├── main.tsx │ │ ├── App.tsx │ │ └── model │ │ │ └── count.ts │ ├── tsconfig.node.json │ ├── .gitignore │ ├── index.html │ ├── vite.config.ts │ ├── package.json │ └── tsconfig.json ├── todos │ ├── README.md │ ├── src │ │ ├── setupTests.js │ │ ├── index.tsx │ │ ├── components │ │ │ ├── App.tsx │ │ │ ├── Link.tsx │ │ │ ├── Todo.tsx │ │ │ ├── Footer.tsx │ │ │ └── TodoList.tsx │ │ ├── containers │ │ │ ├── VisibleTodoList.tsx │ │ │ ├── AddTodo.tsx │ │ │ └── FilterLink.tsx │ │ └── models │ │ │ └── todos.ts │ ├── tsconfig.json │ ├── .gitignore │ ├── public │ │ └── index.html │ └── package.json ├── vanilla-counter │ ├── package.json │ └── index.html └── with-middleware │ ├── src │ ├── setupTests.js │ ├── index.tsx │ ├── model │ │ └── count.ts │ └── App.tsx │ ├── tsconfig.json │ ├── .gitignore │ ├── public │ └── index.html │ └── package.json ├── .prettierrc ├── .husky ├── pre-commit └── commit-msg ├── common ├── package.json └── config.js ├── modern.config.js ├── pnpm-workspace.yaml ├── .pnpmfile.cjs ├── .changeset └── config.json ├── .editorconfig ├── .gitignore ├── .vscode ├── extensions.json └── settings.json ├── LICENSE ├── .github └── workflows │ ├── main.yml │ ├── release-pull-request.yml │ └── release.yml ├── README.md ├── CONTRIBUTING.md └── package.json /packages/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | lts/fermium 2 | -------------------------------------------------------------------------------- /scripts/src/index.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/plugins/xstate-immer/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './plugin'; 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | link-workspace-packages=false 2 | strict-peer-dependencies=false 3 | -------------------------------------------------------------------------------- /examples/vite-counter/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/react/src/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@modern-js-app"] 3 | } 4 | -------------------------------------------------------------------------------- /packages/store/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './memoize'; 2 | export * from './misc'; 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "arrowParens": "avoid" 5 | } 6 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx --no-install lint-staged 5 | -------------------------------------------------------------------------------- /packages/store/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './model'; 2 | export * from './app'; 3 | export * from './utils'; 4 | -------------------------------------------------------------------------------- /packages/store/src/types/utils.ts: -------------------------------------------------------------------------------- 1 | export interface NestedType { 2 | [key: string]: T | NestedType; 3 | } 4 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --edit $1 5 | -------------------------------------------------------------------------------- /packages/store/src/plugin/index.ts: -------------------------------------------------------------------------------- 1 | export * from './core'; 2 | export { default as createPlugin } from './createPlugin'; 3 | -------------------------------------------------------------------------------- /packages/plugins/xstate/src/types/index.ts: -------------------------------------------------------------------------------- 1 | import './override'; 2 | 3 | export * from './map'; 4 | export * from './machine'; 5 | -------------------------------------------------------------------------------- /packages/plugins/auto-actions/src/primitive.ts: -------------------------------------------------------------------------------- 1 | const setState = (_state: T, payload: T): T => payload; 2 | 3 | export { setState }; 4 | -------------------------------------------------------------------------------- /examples/todos/README.md: -------------------------------------------------------------------------------- 1 | # Reduck Todos Example 2 | 3 | This example is modified from https://github.com/reduxjs/redux/tree/master/examples/todos 4 | -------------------------------------------------------------------------------- /common/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "common", 3 | "private": true, 4 | "devDependencies": { 5 | "@modern-js/module-tools": "2.21.1" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /modern.config.js: -------------------------------------------------------------------------------- 1 | const monorepoTools = require('@modern-js/monorepo-tools').default; 2 | 3 | module.exports = { 4 | plugins: [monorepoTools()], 5 | }; 6 | -------------------------------------------------------------------------------- /packages/plugins/xstate/src/machine.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * export useful functions from xstate 3 | */ 4 | export { createMachine, interpret, spawn } from 'xstate'; 5 | -------------------------------------------------------------------------------- /packages/react/tests/modern-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /packages/store/tests/modern-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /packages/plugins/effects/tests/modern-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /packages/plugins/immer/tests/modern-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /packages/plugins/xstate/tests/modern-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /packages/react/tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": "../" 5 | }, 6 | "include": ["**/*"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/store/tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": "../" 5 | }, 6 | "include": ["**/*"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/plugins/auto-actions/tests/modern-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /packages/plugins/immer/tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": "../" 5 | }, 6 | "include": ["**/*"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/plugins/xstate-immer/tests/modern-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /packages/react/tests/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: '@modern-js', 3 | parserOptions: { 4 | project: require.resolve('./tsconfig.json'), 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /packages/store/tests/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: '@modern-js', 3 | parserOptions: { 4 | project: require.resolve('./tsconfig.json'), 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'apps/**' 3 | - 'services/**' 4 | - 'features/**' 5 | - 'packages/**' 6 | - 'examples/**' 7 | - 'scripts' 8 | - 'common' 9 | -------------------------------------------------------------------------------- /packages/plugins/effects/tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": "../" 5 | }, 6 | "include": ["**/*"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/plugins/xstate/tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": "../" 5 | }, 6 | "include": ["**/*"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/plugins/auto-actions/tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": "../" 5 | }, 6 | "include": ["**/*"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/plugins/effects/tests/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: '@modern-js', 3 | parserOptions: { 4 | project: require.resolve('./tsconfig.json'), 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /packages/plugins/immer/tests/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: '@modern-js', 3 | parserOptions: { 4 | project: require.resolve('./tsconfig.json'), 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /packages/plugins/xstate-immer/tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": "../" 5 | }, 6 | "include": ["**/*"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/plugins/xstate/tests/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: '@modern-js', 3 | parserOptions: { 4 | project: require.resolve('./tsconfig.json'), 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /packages/plugins/auto-actions/tests/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: '@modern-js', 3 | parserOptions: { 4 | project: require.resolve('./tsconfig.json'), 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /packages/plugins/xstate-immer/tests/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: '@modern-js', 3 | parserOptions: { 4 | project: require.resolve('./tsconfig.json'), 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /scripts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@modern-js/tsconfig/base", 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist" 6 | }, 7 | "include": ["src"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/react/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@modern-js'], 3 | parserOptions: { 4 | tsconfigRootDir: __dirname, 5 | project: ['./tsconfig.json'], 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /packages/store/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@modern-js'], 3 | parserOptions: { 4 | tsconfigRootDir: __dirname, 5 | project: ['./tsconfig.json'], 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /packages/plugins/effects/src/index.ts: -------------------------------------------------------------------------------- 1 | import plugin from './plugin'; 2 | import handleEffect from './utils/handleEffect'; 3 | 4 | export { plugin, handleEffect }; 5 | 6 | export default plugin; 7 | -------------------------------------------------------------------------------- /packages/plugins/immer/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@modern-js'], 3 | parserOptions: { 4 | tsconfigRootDir: __dirname, 5 | project: ['./tsconfig.json'], 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /packages/plugins/xstate/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@modern-js'], 3 | parserOptions: { 4 | tsconfigRootDir: __dirname, 5 | project: ['./tsconfig.json'], 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /packages/plugins/devtools/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@modern-js'], 3 | parserOptions: { 4 | tsconfigRootDir: __dirname, 5 | project: ['./tsconfig.json'], 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /packages/plugins/effects/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@modern-js'], 3 | parserOptions: { 4 | tsconfigRootDir: __dirname, 5 | project: ['./tsconfig.json'], 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /packages/plugins/auto-actions/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@modern-js'], 3 | parserOptions: { 4 | tsconfigRootDir: __dirname, 5 | project: ['./tsconfig.json'], 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /packages/plugins/xstate-immer/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@modern-js'], 3 | parserOptions: { 4 | tsconfigRootDir: __dirname, 5 | project: ['./tsconfig.json'], 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /packages/plugins/xstate/src/index.ts: -------------------------------------------------------------------------------- 1 | import plugin from './plugin'; 2 | 3 | export * from './types'; 4 | export * from './machine'; 5 | export { isMachineModel } from './check'; 6 | 7 | export { plugin }; 8 | -------------------------------------------------------------------------------- /examples/vite-counter/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "esnext", 5 | "moduleResolution": "node" 6 | }, 7 | "include": ["vite.config.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/plugins/xstate/src/types/map.ts: -------------------------------------------------------------------------------- 1 | import type { Interpreter } from 'xstate/lib/interpreter'; 2 | 3 | export type MachineMap = { 4 | [K in string]?: { 5 | service: Interpreter; 6 | }; 7 | }; 8 | -------------------------------------------------------------------------------- /scripts/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: '@modern-js', 3 | parserOptions: { 4 | project: require.resolve('./tsconfig.json'), 5 | }, 6 | rules: { 7 | 'no-console': 'off', 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /packages/plugins/xstate/src/types/machine.ts: -------------------------------------------------------------------------------- 1 | import type { ModelDesc } from '@modern-js-reduck/store'; 2 | 3 | export type MachineModelDesc = { 4 | [K in keyof T]: T[K]; 5 | } & { machine: NonNullable }; 6 | -------------------------------------------------------------------------------- /packages/react/src/hook.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from './createApp'; 2 | 3 | const { Provider, useModel, useStaticModel, useLocalModel, useStore } = 4 | createApp({}); 5 | 6 | export { Provider, useModel, useStaticModel, useLocalModel, useStore }; 7 | -------------------------------------------------------------------------------- /examples/vanilla-counter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vanilla-counter", 3 | "private": true, 4 | "version": "0.0.0", 5 | "scripts": { 6 | "dev": "serve ./ -p 3000" 7 | }, 8 | "dependencies": { 9 | "serve": "latest" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/store/src/plugin/createPlugin.ts: -------------------------------------------------------------------------------- 1 | import { PluginContext, PluginLifeCycle } from '@/types/plugin'; 2 | 3 | const createPlugin = ( 4 | defineLifeCycle: (context: PluginContext) => PluginLifeCycle, 5 | ) => defineLifeCycle; 6 | 7 | export default createPlugin; 8 | -------------------------------------------------------------------------------- /examples/todos/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /packages/plugins/xstate/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { ActionTypeNames, MachineActionPrefix } from './const'; 2 | 3 | export function getEventType(name: string, type: ActionTypeNames) { 4 | const path = [name, MachineActionPrefix, type]; 5 | 6 | return path.join('/').toUpperCase(); 7 | } 8 | -------------------------------------------------------------------------------- /examples/vite-counter/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client'; 2 | import App from './App'; 3 | import { Provider } from '@modern-js-reduck/react'; 4 | 5 | createRoot(document.getElementById('root')!).render( 6 | 7 | 8 | , 9 | ); 10 | -------------------------------------------------------------------------------- /examples/with-middleware/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /packages/plugins/immer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@modern-js/tsconfig/base", 3 | "compilerOptions": { 4 | "declaration": false, 5 | "jsx": "preserve", 6 | "baseUrl": "./", 7 | "paths": { 8 | "@/*": ["./src/*"] 9 | } 10 | }, 11 | "include": ["src"] 12 | } 13 | -------------------------------------------------------------------------------- /packages/plugins/devtools/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@modern-js/tsconfig/base", 3 | "compilerOptions": { 4 | "declaration": false, 5 | "jsx": "preserve", 6 | "baseUrl": "./", 7 | "paths": { 8 | "@/*": ["./src/*"] 9 | } 10 | }, 11 | "include": ["src"] 12 | } 13 | -------------------------------------------------------------------------------- /packages/plugins/effects/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@modern-js/tsconfig/base", 3 | "compilerOptions": { 4 | "declaration": false, 5 | "jsx": "preserve", 6 | "baseUrl": "./", 7 | "paths": { 8 | "@/*": ["./src/*"] 9 | } 10 | }, 11 | "include": ["src"] 12 | } 13 | -------------------------------------------------------------------------------- /packages/plugins/xstate/src/check.ts: -------------------------------------------------------------------------------- 1 | import type { ModelDesc } from '@modern-js-reduck/store'; 2 | import type { MachineModelDesc } from './types'; 3 | 4 | export function isMachineModel( 5 | model: T, 6 | ): model is MachineModelDesc { 7 | return Boolean(model.machine); 8 | } 9 | -------------------------------------------------------------------------------- /packages/plugins/auto-actions/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@modern-js/tsconfig/base", 3 | "compilerOptions": { 4 | "declaration": false, 5 | "jsx": "preserve", 6 | "baseUrl": "./", 7 | "paths": { 8 | "@/*": ["./src/*"] 9 | } 10 | }, 11 | "include": ["src"] 12 | } 13 | -------------------------------------------------------------------------------- /packages/plugins/xstate-immer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@modern-js/tsconfig/base", 3 | "compilerOptions": { 4 | "declaration": false, 5 | "jsx": "preserve", 6 | "baseUrl": "./", 7 | "paths": { 8 | "@/*": ["./src/*"] 9 | } 10 | }, 11 | "include": ["src"] 12 | } 13 | -------------------------------------------------------------------------------- /packages/react/src/index.ts: -------------------------------------------------------------------------------- 1 | import { handleEffect } from '@modern-js-reduck/plugin-effects'; 2 | 3 | export * from '@modern-js-reduck/store'; 4 | 5 | export { createApp, getDefaultPlugins } from './createApp'; 6 | 7 | export * from './hook'; 8 | export * from './connect'; 9 | export { handleEffect }; 10 | -------------------------------------------------------------------------------- /packages/plugins/auto-actions/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { ModelDesc } from '@modern-js-reduck/store'; 2 | 3 | const mergeActions = (modelDesc: ModelDesc, actions: any) => ({ 4 | ...modelDesc, 5 | actions: { 6 | ...actions, 7 | ...modelDesc.actions, 8 | }, 9 | }); 10 | 11 | export { mergeActions }; 12 | -------------------------------------------------------------------------------- /packages/react/type.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | /// 5 | 6 | export * from './dist/types'; 7 | -------------------------------------------------------------------------------- /packages/store/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@modern-js/tsconfig/base", 3 | "compilerOptions": { 4 | "declaration": false, 5 | "jsx": "preserve", 6 | "baseUrl": "./", 7 | "strict": false, 8 | "paths": { 9 | "@/*": ["./src/*"] 10 | } 11 | }, 12 | "include": ["src"] 13 | } 14 | -------------------------------------------------------------------------------- /examples/todos/src/index.tsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom/client'; 2 | import { Provider } from '@modern-js-reduck/react'; 3 | import App from './components/App'; 4 | 5 | const root = ReactDOM.createRoot(document.getElementById('root')!); 6 | root.render( 7 | 8 | 9 | , 10 | ); 11 | -------------------------------------------------------------------------------- /packages/plugins/xstate/src/const.ts: -------------------------------------------------------------------------------- 1 | export const MachineActionPrefix = '__MACHINE__'; 2 | export const MachineStateSymbol = Symbol('__machine__'); 3 | 4 | export const ActionTypes = { 5 | SEND: 'SEND', 6 | SET: 'SET', 7 | } as const; 8 | 9 | export type ActionTypeNames = typeof ActionTypes[keyof typeof ActionTypes]; 10 | -------------------------------------------------------------------------------- /examples/todos/src/components/App.tsx: -------------------------------------------------------------------------------- 1 | import Footer from './Footer'; 2 | import AddTodo from '../containers/AddTodo'; 3 | import VisibleTodoList from '../containers/VisibleTodoList'; 4 | 5 | const App = () => ( 6 |
7 | 8 | 9 |
10 |
11 | ); 12 | 13 | export default App; 14 | -------------------------------------------------------------------------------- /packages/store/modern.config.js: -------------------------------------------------------------------------------- 1 | const { 2 | default: moduleTools, 3 | defineConfig, 4 | } = require('@modern-js/module-tools'); 5 | const test = require('@modern-js/plugin-testing').default; 6 | const config = require('../../common/config'); 7 | 8 | module.exports = defineConfig({ 9 | plugins: [moduleTools(), test()], 10 | ...config, 11 | }); 12 | -------------------------------------------------------------------------------- /packages/plugins/immer/modern.config.js: -------------------------------------------------------------------------------- 1 | const { 2 | default: moduleTools, 3 | defineConfig, 4 | } = require('@modern-js/module-tools'); 5 | const test = require('@modern-js/plugin-testing').default; 6 | const config = require('../../../common/config'); 7 | 8 | module.exports = defineConfig({ 9 | plugins: [moduleTools(), test()], 10 | ...config, 11 | }); 12 | -------------------------------------------------------------------------------- /examples/with-middleware/src/index.tsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom/client'; 2 | import { Provider } from '@modern-js-reduck/react'; 3 | import App from './App'; 4 | import logger from 'redux-logger'; 5 | 6 | ReactDOM.createRoot(document.getElementById('root')!).render( 7 | 8 | 9 | , 10 | ); 11 | -------------------------------------------------------------------------------- /packages/plugins/devtools/modern.config.js: -------------------------------------------------------------------------------- 1 | const { 2 | default: moduleTools, 3 | defineConfig, 4 | } = require('@modern-js/module-tools'); 5 | const test = require('@modern-js/plugin-testing').default; 6 | const config = require('../../../common/config'); 7 | 8 | module.exports = defineConfig({ 9 | plugins: [moduleTools(), test()], 10 | ...config, 11 | }); 12 | -------------------------------------------------------------------------------- /packages/plugins/effects/modern.config.js: -------------------------------------------------------------------------------- 1 | const { 2 | default: moduleTools, 3 | defineConfig, 4 | } = require('@modern-js/module-tools'); 5 | const test = require('@modern-js/plugin-testing').default; 6 | const config = require('../../../common/config'); 7 | 8 | module.exports = defineConfig({ 9 | plugins: [moduleTools(), test()], 10 | ...config, 11 | }); 12 | -------------------------------------------------------------------------------- /packages/plugins/xstate/modern.config.js: -------------------------------------------------------------------------------- 1 | const { 2 | default: moduleTools, 3 | defineConfig, 4 | } = require('@modern-js/module-tools'); 5 | const test = require('@modern-js/plugin-testing').default; 6 | const config = require('../../../common/config'); 7 | 8 | module.exports = defineConfig({ 9 | plugins: [moduleTools(), test()], 10 | ...config, 11 | }); 12 | -------------------------------------------------------------------------------- /packages/plugins/xstate/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@modern-js/tsconfig/base", 3 | "compilerOptions": { 4 | "declaration": false, 5 | "jsx": "preserve", 6 | "baseUrl": "./", 7 | "paths": { 8 | "@/*": ["./src/*"] 9 | }, 10 | "noUnusedParameters": false, 11 | "noUnusedLocals": false 12 | }, 13 | "include": ["src"] 14 | } 15 | -------------------------------------------------------------------------------- /packages/react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@modern-js/tsconfig/base", 3 | "compilerOptions": { 4 | "esModuleInterop": true, 5 | "declaration": false, 6 | "jsx": "preserve", 7 | "baseUrl": "./", 8 | "paths": { 9 | "@/*": ["./src/*"], 10 | "redux": ["./node_modules/redux"] 11 | } 12 | }, 13 | "include": ["src"] 14 | } 15 | -------------------------------------------------------------------------------- /packages/plugins/auto-actions/modern.config.js: -------------------------------------------------------------------------------- 1 | const { 2 | default: moduleTools, 3 | defineConfig, 4 | } = require('@modern-js/module-tools'); 5 | const test = require('@modern-js/plugin-testing').default; 6 | const config = require('../../../common/config'); 7 | 8 | module.exports = defineConfig({ 9 | plugins: [moduleTools(), test()], 10 | ...config, 11 | }); 12 | -------------------------------------------------------------------------------- /packages/plugins/xstate-immer/modern.config.js: -------------------------------------------------------------------------------- 1 | const { 2 | default: moduleTools, 3 | defineConfig, 4 | } = require('@modern-js/module-tools'); 5 | const test = require('@modern-js/plugin-testing').default; 6 | const config = require('../../../common/config'); 7 | 8 | module.exports = defineConfig({ 9 | plugins: [moduleTools(), test()], 10 | ...config, 11 | }); 12 | -------------------------------------------------------------------------------- /examples/todos/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "esModuleInterop": false, 5 | "strict": true, 6 | "module": "ESNext", 7 | "noEmit": true, 8 | "jsx": "react-jsx", 9 | "allowSyntheticDefaultImports": true, 10 | "moduleResolution": "node" 11 | }, 12 | "include": ["src"], 13 | } 14 | -------------------------------------------------------------------------------- /examples/with-middleware/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "esModuleInterop": false, 5 | "strict": true, 6 | "module": "ESNext", 7 | "noEmit": true, 8 | "jsx": "react-jsx", 9 | "allowSyntheticDefaultImports": true, 10 | "moduleResolution": "node" 11 | }, 12 | "include": ["src"], 13 | } 14 | -------------------------------------------------------------------------------- /examples/vite-counter/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /examples/todos/src/containers/VisibleTodoList.tsx: -------------------------------------------------------------------------------- 1 | import { useModel } from '@modern-js-reduck/react'; 2 | import todos from '../models/todos'; 3 | import TodoList from '../components/TodoList'; 4 | 5 | const VisibleTodoList = () => { 6 | const [{ visibleTodos }, { toggleTodo }] = useModel(todos); 7 | 8 | return ; 9 | }; 10 | 11 | export default VisibleTodoList; 12 | -------------------------------------------------------------------------------- /examples/todos/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /examples/vite-counter/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/with-middleware/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /examples/todos/src/components/Link.tsx: -------------------------------------------------------------------------------- 1 | import { PropsWithChildren } from 'react'; 2 | 3 | interface Props { 4 | active: boolean; 5 | onClick: () => void; 6 | } 7 | 8 | const Link = ({ active, children, onClick }: PropsWithChildren) => ( 9 | 18 | ); 19 | 20 | export default Link; 21 | -------------------------------------------------------------------------------- /examples/vite-counter/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react({ 7 | jsxRuntime: 'classic', 8 | })], 9 | // resolve: { 10 | // alias: { 11 | // // FIXME: https://github.com/vitejs/vite/issues/6215 12 | // 'react/jsx-runtime': 'react/jsx-runtime.js', 13 | // }, 14 | // }, 15 | }); 16 | -------------------------------------------------------------------------------- /examples/todos/src/components/Todo.tsx: -------------------------------------------------------------------------------- 1 | import { PropsWithChildren } from "react" 2 | 3 | interface Props { 4 | text: string, 5 | completed: boolean, 6 | onClick: () => void 7 | } 8 | 9 | const Todo = ({ onClick, completed, text }: PropsWithChildren) => ( 10 |
  • 16 | {text} 17 |
  • 18 | ) 19 | 20 | export default Todo 21 | -------------------------------------------------------------------------------- /packages/store/src/index.ts: -------------------------------------------------------------------------------- 1 | import createStore from './store/createStore'; 2 | import model from './model/model'; 3 | import { createPlugin } from './plugin'; 4 | import * as utils from './utils'; 5 | 6 | export type { 7 | ModelDesc, 8 | Store, 9 | StoreConfig, 10 | GetActions, 11 | GetState, 12 | ModelDescOptions, 13 | GetModelState, 14 | GetModelActions, 15 | Model, 16 | } from '@/types'; 17 | 18 | export { createStore, model, createPlugin, utils }; 19 | -------------------------------------------------------------------------------- /packages/plugins/immer/src/index.ts: -------------------------------------------------------------------------------- 1 | import { createPlugin } from '@modern-js-reduck/store'; 2 | import { produce, enableES5, enableMapSet, setAutoFreeze } from 'immer'; 3 | 4 | enableES5(); 5 | enableMapSet(); 6 | setAutoFreeze(false); 7 | 8 | export default createPlugin(() => ({ 9 | beforeReducer(reducer) { 10 | return (state: any, payload: any, ...extraArgs: any[]) => 11 | produce(state, (draft: any) => reducer(draft, payload, ...extraArgs)); 12 | }, 13 | })); 14 | -------------------------------------------------------------------------------- /.pnpmfile.cjs: -------------------------------------------------------------------------------- 1 | function readPackage(pkg, _context) { 2 | // Override the manifest of foo@1.x after downloading it from the registry 3 | // fix @samverschueren/stream-to-observable bug 4 | if (pkg.name === '@samverschueren/stream-to-observable') { 5 | pkg.dependencies = { 6 | ...pkg.dependencies, 7 | 'any-observable': '^0.5.1', 8 | }; 9 | } 10 | 11 | return pkg; 12 | } 13 | 14 | module.exports = { 15 | hooks: { 16 | readPackage, 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /examples/vite-counter/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useModel } from '@modern-js-reduck/react'; 2 | import { modelA, modelB } from './model/count'; 3 | function App() { 4 | const [state, actions] = useModel(modelA, modelB); 5 | 6 | return ( 7 |
    8 |

    a: {state.a}

    9 |

    b: {state.b}

    10 | 11 | 12 |
    13 | ); 14 | } 15 | 16 | export default App; 17 | -------------------------------------------------------------------------------- /examples/with-middleware/src/model/count.ts: -------------------------------------------------------------------------------- 1 | import { model } from '@modern-js-reduck/react'; 2 | 3 | export const modelA = model('modelA').define({ 4 | state: { 5 | a: 1, 6 | }, 7 | actions: { 8 | incA(s) { 9 | return { a: s.a + 1 }; 10 | }, 11 | }, 12 | }); 13 | 14 | export const modelB = model('modelB').define({ 15 | state: { 16 | b: 1, 17 | }, 18 | actions: { 19 | incB(s) { 20 | return { b: s.b + 1 }; 21 | }, 22 | }, 23 | }); 24 | -------------------------------------------------------------------------------- /examples/with-middleware/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useModel } from "@modern-js-reduck/react"; 2 | import { modelA, modelB } from "./model/count"; 3 | function App() { 4 | const [state, actions] = useModel([modelA, modelB]); 5 | 6 | return ( 7 |
    8 |

    a: {state.a}

    9 |

    b: {state.b}

    10 | 11 | 12 |
    13 | ); 14 | } 15 | 16 | export default App; 17 | -------------------------------------------------------------------------------- /packages/plugins/devtools/src/index.ts: -------------------------------------------------------------------------------- 1 | import { createPlugin } from '@modern-js-reduck/store'; 2 | import { devToolsEnhancer, EnhancerOptions } from '@redux-devtools/extension'; 3 | 4 | export type DevToolsOptions = EnhancerOptions; 5 | 6 | export default (config?: EnhancerOptions) => 7 | createPlugin(() => ({ 8 | config: storeConfig => { 9 | const { enhancers = [] } = storeConfig; 10 | 11 | storeConfig.enhancers = [devToolsEnhancer(config), ...enhancers]; 12 | 13 | return storeConfig; 14 | }, 15 | })); 16 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@1.6.0/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [["@modern-js-reduck/*"]], 6 | "access": "restricted", 7 | "baseBranch": "main", 8 | "updateInternalDependencies": "patch", 9 | "___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": { 10 | "onlyUpdatePeerDependentsWhenOutOfRange": true 11 | }, 12 | "ignore": [ 13 | "@modern-js-reduck/plugin-xstate", 14 | "@modern-js-reduck/plugin-xstate-immer" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | indent_style = space 9 | insert_final_newline = true 10 | 11 | [*.{js,jsx,ts,tsx,mjs,mjsx,cjs,cjsx,sh,rb}] 12 | indent_size = 2 13 | max_line_length = 80 14 | trim_trailing_whitespace = true 15 | 16 | [*.py] 17 | indent_size = 4 18 | max_line_length = 80 19 | trim_trailing_whitespace = true 20 | 21 | [*.{css,scss,less,html,hbs,ejs,json,code-workspace,yml,yaml,gql}] 22 | indent_size = 2 23 | trim_trailing_whitespace = true 24 | -------------------------------------------------------------------------------- /examples/todos/src/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import FilterLink from '../containers/FilterLink' 2 | import { VisibilityFilters } from '../models/todos' 3 | 4 | const Footer = () => ( 5 |
    6 | Show: 7 | 8 | All 9 | 10 | 11 | Active 12 | 13 | 14 | Completed 15 | 16 |
    17 | ) 18 | 19 | export default Footer 20 | -------------------------------------------------------------------------------- /packages/plugins/xstate-immer/.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | __test__ 4 | *.test.ts 5 | *.test.js 6 | *.test.tsx 7 | *.test.jsx 8 | __tsd__ 9 | *.tsd.ts 10 | 11 | node_modules/ 12 | .npm 13 | .lock-wscript 14 | .yarn-integrity 15 | .node_repl_history 16 | .nyc_output 17 | *.tsbuildinfo 18 | .eslintcache 19 | .sonarlint 20 | 21 | coverage/ 22 | release/ 23 | output/ 24 | output_resource/ 25 | 26 | .vscode/**/* 27 | !.vscode/settings.json 28 | !.vscode/extensions.json 29 | .idea/ 30 | 31 | **/*/api/typings/auto-generated 32 | **/*/adapters/**/index.ts 33 | **/*/adapters/**/index.js 34 | -------------------------------------------------------------------------------- /examples/vite-counter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-counter", 3 | "private": true, 4 | "version": "0.0.0", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "tsc && vite build", 8 | "preview": "vite preview" 9 | }, 10 | "dependencies": { 11 | "@modern-js-reduck/react": "latest", 12 | "react": "^18.0.2", 13 | "react-dom": "^18.0.2" 14 | }, 15 | "devDependencies": { 16 | "@types/react": "^18.0.2", 17 | "@types/react-dom": "^18.0.2", 18 | "@vitejs/plugin-react": "^1.3.2", 19 | "typescript": "^4.6.3", 20 | "vite": "^2.9.14" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/vite-counter/src/model/count.ts: -------------------------------------------------------------------------------- 1 | import { model } from '@modern-js-reduck/react'; 2 | 3 | export const modelA = model('modelA').define({ 4 | state: { 5 | a: 1, 6 | }, 7 | computed: { 8 | // add1: (state) => state.a + 1 9 | }, 10 | actions: { 11 | incA(s) { 12 | return { a: s.a + 1 }; 13 | }, 14 | }, 15 | }); 16 | export const modelB = model('modelB').define({ 17 | state: { 18 | b: 1, 19 | }, 20 | computed: { 21 | add2: (state) => state.b + 1 22 | }, 23 | actions: { 24 | incB(s) { 25 | return { b: s.b + 1 }; 26 | }, 27 | }, 28 | }); 29 | -------------------------------------------------------------------------------- /scripts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@modern-js-reduck/scripts", 3 | "private": true, 4 | "version": "0.0.0", 5 | "main": "./src/index.js", 6 | "bin": { 7 | "only-allow-pnpm": "./bin/only-allow-pnpm.js" 8 | }, 9 | "scripts": { 10 | "get-release-version": "tsx ./src/get-release-version.ts" 11 | }, 12 | "dependencies": { 13 | "@changesets/assemble-release-plan": "^5.2.1", 14 | "@changesets/config": "^2.1.1", 15 | "@changesets/read": "^0.6.0", 16 | "@manypkg/get-packages": "^1.1.3" 17 | }, 18 | "devDependencies": { 19 | "@types/node": "^18.0.1", 20 | "tsx": "^3.12.7" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/plugins/immer/.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | __test__ 4 | *.test.ts 5 | *.test.js 6 | *.test.tsx 7 | *.test.jsx 8 | __tsd__ 9 | *.tsd.ts 10 | 11 | .pnp 12 | .pnp.js 13 | .env.*.local 14 | .history 15 | .rts* 16 | *.log* 17 | *.pid 18 | *.pid.* 19 | *.report 20 | *.lcov 21 | lib-cov 22 | 23 | node_modules/ 24 | .npm 25 | .lock-wscript 26 | .yarn-integrity 27 | .node_repl_history 28 | .nyc_output 29 | *.tsbuildinfo 30 | .eslintcache 31 | .sonarlint 32 | 33 | coverage/ 34 | release/ 35 | output/ 36 | output_resource/ 37 | 38 | .vscode/**/* 39 | !.vscode/settings.json 40 | !.vscode/extensions.json 41 | .idea/ 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | .pnp 4 | .pnp.js 5 | .env.*.local 6 | .history 7 | .rts* 8 | *.log* 9 | *.pid 10 | *.pid.* 11 | *.report 12 | *.lcov 13 | lib-cov 14 | 15 | node_modules/ 16 | .npm 17 | .lock-wscript 18 | .yarn-integrity 19 | .node_repl_history 20 | .nyc_output 21 | *.tsbuildinfo 22 | .eslintcache 23 | .sonarlint 24 | 25 | dist/ 26 | coverage/ 27 | release/ 28 | output/ 29 | output_resource/ 30 | 31 | .vscode/**/* 32 | !.vscode/settings.json 33 | !.vscode/extensions.json 34 | .idea/ 35 | 36 | **/*/typings/auto-generated 37 | **/*/adapters/**/index.ts 38 | **/*/adapters/**/index.js 39 | 40 | **/*/tsconfig.temp.json 41 | 42 | .changeset/pre.json 43 | -------------------------------------------------------------------------------- /examples/vite-counter/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /packages/plugins/auto-actions/src/object.ts: -------------------------------------------------------------------------------- 1 | type ObjectDispatchActions> = { 2 | [key in string & keyof T as `set${Capitalize}`]: ( 3 | payload: T[key], 4 | ) => void; 5 | }; 6 | 7 | const createObjectActions = (state: any) => { 8 | const result: Record = {}; 9 | 10 | Object.keys(state).forEach(key => { 11 | result[`set${key[0].toUpperCase()}${key.substring(1)}`] = ( 12 | _state: any, 13 | payload: any, 14 | ) => ({ 15 | ..._state, 16 | [key]: payload, 17 | }); 18 | }); 19 | 20 | return result; 21 | }; 22 | 23 | export { createObjectActions }; 24 | 25 | export type { ObjectDispatchActions }; 26 | -------------------------------------------------------------------------------- /examples/todos/src/components/TodoList.tsx: -------------------------------------------------------------------------------- 1 | import Todo from './Todo' 2 | import todos from '../models/todos' 3 | import type {Todo as TTodo} from '../models/todos' 4 | import { GetModelActions } from '@modern-js-reduck/react' 5 | import { PropsWithChildren } from 'react' 6 | 7 | interface Props { 8 | todos: TTodo[], 9 | toggleTodo: GetModelActions['toggleTodo'] 10 | } 11 | 12 | const TodoList = ({ todos, toggleTodo }: PropsWithChildren) => ( 13 |
      14 | {todos.map(todo => 15 | toggleTodo(todo.id)} 19 | /> 20 | )} 21 |
    22 | ) 23 | 24 | export default TodoList 25 | -------------------------------------------------------------------------------- /packages/react/.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | __test__ 4 | *.test.ts 5 | *.test.js 6 | *.test.tsx 7 | *.test.jsx 8 | __tsd__ 9 | *.tsd.ts 10 | 11 | .pnp 12 | .pnp.js 13 | .env.*.local 14 | .history 15 | .rts* 16 | *.log* 17 | *.pid 18 | *.pid.* 19 | *.report 20 | *.lcov 21 | lib-cov 22 | 23 | node_modules/ 24 | .npm 25 | .lock-wscript 26 | .yarn-integrity 27 | .node_repl_history 28 | .nyc_output 29 | *.tsbuildinfo 30 | .eslintcache 31 | .sonarlint 32 | 33 | coverage/ 34 | release/ 35 | output/ 36 | output_resource/ 37 | 38 | .vscode/**/* 39 | !.vscode/settings.json 40 | !.vscode/extensions.json 41 | .idea/ 42 | 43 | **/*/api/typings/auto-generated 44 | **/*/adapters/**/index.ts 45 | **/*/adapters/**/index.js 46 | -------------------------------------------------------------------------------- /packages/store/.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | __test__ 4 | *.test.ts 5 | *.test.js 6 | *.test.tsx 7 | *.test.jsx 8 | __tsd__ 9 | *.tsd.ts 10 | 11 | .pnp 12 | .pnp.js 13 | .env.*.local 14 | .history 15 | .rts* 16 | *.log* 17 | *.pid 18 | *.pid.* 19 | *.report 20 | *.lcov 21 | lib-cov 22 | 23 | node_modules/ 24 | .npm 25 | .lock-wscript 26 | .yarn-integrity 27 | .node_repl_history 28 | .nyc_output 29 | *.tsbuildinfo 30 | .eslintcache 31 | .sonarlint 32 | 33 | coverage/ 34 | release/ 35 | output/ 36 | output_resource/ 37 | 38 | .vscode/**/* 39 | !.vscode/settings.json 40 | !.vscode/extensions.json 41 | .idea/ 42 | 43 | **/*/api/typings/auto-generated 44 | **/*/adapters/**/index.ts 45 | **/*/adapters/**/index.js 46 | -------------------------------------------------------------------------------- /packages/plugins/devtools/.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | __test__ 4 | *.test.ts 5 | *.test.js 6 | *.test.tsx 7 | *.test.jsx 8 | __tsd__ 9 | *.tsd.ts 10 | 11 | .pnp 12 | .pnp.js 13 | .env.*.local 14 | .history 15 | .rts* 16 | *.log* 17 | *.pid 18 | *.pid.* 19 | *.report 20 | *.lcov 21 | lib-cov 22 | 23 | node_modules/ 24 | .npm 25 | .lock-wscript 26 | .yarn-integrity 27 | .node_repl_history 28 | .nyc_output 29 | *.tsbuildinfo 30 | .eslintcache 31 | .sonarlint 32 | 33 | coverage/ 34 | release/ 35 | output/ 36 | output_resource/ 37 | 38 | .vscode/**/* 39 | !.vscode/settings.json 40 | !.vscode/extensions.json 41 | .idea/ 42 | 43 | **/*/api/typings/auto-generated 44 | **/*/adapters/**/index.ts 45 | **/*/adapters/**/index.js 46 | -------------------------------------------------------------------------------- /packages/plugins/effects/.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | __test__ 4 | *.test.ts 5 | *.test.js 6 | *.test.tsx 7 | *.test.jsx 8 | __tsd__ 9 | *.tsd.ts 10 | 11 | .pnp 12 | .pnp.js 13 | .env.*.local 14 | .history 15 | .rts* 16 | *.log* 17 | *.pid 18 | *.pid.* 19 | *.report 20 | *.lcov 21 | lib-cov 22 | 23 | node_modules/ 24 | .npm 25 | .lock-wscript 26 | .yarn-integrity 27 | .node_repl_history 28 | .nyc_output 29 | *.tsbuildinfo 30 | .eslintcache 31 | .sonarlint 32 | 33 | coverage/ 34 | release/ 35 | output/ 36 | output_resource/ 37 | 38 | .vscode/**/* 39 | !.vscode/settings.json 40 | !.vscode/extensions.json 41 | .idea/ 42 | 43 | **/*/api/typings/auto-generated 44 | **/*/adapters/**/index.ts 45 | **/*/adapters/**/index.js 46 | -------------------------------------------------------------------------------- /packages/plugins/xstate/.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | __test__ 4 | *.test.ts 5 | *.test.js 6 | *.test.tsx 7 | *.test.jsx 8 | __tsd__ 9 | *.tsd.ts 10 | 11 | .pnp 12 | .pnp.js 13 | .env.*.local 14 | .history 15 | .rts* 16 | *.log* 17 | *.pid 18 | *.pid.* 19 | *.report 20 | *.lcov 21 | lib-cov 22 | 23 | node_modules/ 24 | .npm 25 | .lock-wscript 26 | .yarn-integrity 27 | .node_repl_history 28 | .nyc_output 29 | *.tsbuildinfo 30 | .eslintcache 31 | .sonarlint 32 | 33 | coverage/ 34 | release/ 35 | output/ 36 | output_resource/ 37 | 38 | .vscode/**/* 39 | !.vscode/settings.json 40 | !.vscode/extensions.json 41 | .idea/ 42 | 43 | **/*/api/typings/auto-generated 44 | **/*/adapters/**/index.ts 45 | **/*/adapters/**/index.js 46 | -------------------------------------------------------------------------------- /packages/plugins/auto-actions/.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | __test__ 4 | *.test.ts 5 | *.test.js 6 | *.test.tsx 7 | *.test.jsx 8 | __tsd__ 9 | *.tsd.ts 10 | 11 | .pnp 12 | .pnp.js 13 | .env.*.local 14 | .history 15 | .rts* 16 | *.log* 17 | *.pid 18 | *.pid.* 19 | *.report 20 | *.lcov 21 | lib-cov 22 | 23 | node_modules/ 24 | .npm 25 | .lock-wscript 26 | .yarn-integrity 27 | .node_repl_history 28 | .nyc_output 29 | *.tsbuildinfo 30 | .eslintcache 31 | .sonarlint 32 | 33 | coverage/ 34 | release/ 35 | output/ 36 | output_resource/ 37 | 38 | .vscode/**/* 39 | !.vscode/settings.json 40 | !.vscode/extensions.json 41 | .idea/ 42 | 43 | **/*/api/typings/auto-generated 44 | **/*/adapters/**/index.ts 45 | **/*/adapters/**/index.js 46 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "jpoissonnier.vscode-styled-components", 4 | "kumar-harsh.graphql-for-vscode", 5 | "dtsvet.vscode-wasm", 6 | "cpylua.language-postcss", 7 | "EditorConfig.editorconfig", 8 | "dbaeumer.vscode-eslint", 9 | "drKnoxy.eslint-disable-snippets", 10 | "mkaufman.htmlhint", 11 | "streetsidesoftware.code-spell-checker", 12 | "msjsdiag.debugger-for-chrome", 13 | "ms-vscode.node-debug2", 14 | "codezombiech.gitignore", 15 | "aaron-bond.better-comments", 16 | "ziyasal.vscode-open-in-github", 17 | "jasonnutter.search-node-modules", 18 | "jock.svg", 19 | "andrejunges.handlebars", 20 | "bungcip.better-toml", 21 | "mikestead.dotenv", 22 | "gruntfuggly.todo-tree", 23 | "vscode-icons-team.vscode-icons" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /examples/todos/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Reduck Todos Example 7 | 8 | 9 | 10 |
    11 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /examples/with-middleware/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Reduck Todos Example 7 | 8 | 9 | 10 |
    11 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /common/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import('@modern-js/module-tools').ModuleUserConfig} ModuleUserConfig 3 | */ 4 | 5 | /** 6 | * @type {ModuleUserConfig} 7 | */ 8 | module.exports = { 9 | buildPreset({ preset }) { 10 | return preset['modern-js-universal'].filter(config => { 11 | // remove esm code that run in node environment. 12 | return !config.outDir.includes('esm-node'); 13 | }).map(config => { 14 | // check es5 config, instead of es2015 15 | if (config.target === 'es5') { 16 | config.target = 'es2015'; 17 | } 18 | return config; 19 | }); 20 | }, 21 | testing: { 22 | jest: { 23 | collectCoverage: true, 24 | collectCoverageFrom: [ 25 | '/**/src/**/*.{ts,tsx}', 26 | '!/**/src/types/**/*.{ts,tsx}', 27 | ], 28 | }, 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /packages/plugins/auto-actions/tests/object.test.ts: -------------------------------------------------------------------------------- 1 | import { createStore, model } from '@modern-js-reduck/store'; 2 | import plugin from '../src'; 3 | 4 | const testModel = model('name').define({ 5 | state: { 6 | a: 1, 7 | b: '1', 8 | c: { a: '1' }, 9 | }, 10 | }); 11 | 12 | const store = createStore({ 13 | plugins: [plugin], 14 | }); 15 | 16 | const [, { setA, setB, setC }] = store.use(testModel); 17 | 18 | const expectState = (key: 'a' | 'b' | 'c', state: any) => { 19 | expect(store.use(testModel)[0][key]).toEqual(state); 20 | }; 21 | 22 | describe('test object auto actions', () => { 23 | it('setA', () => { 24 | setA(2); 25 | expectState('a', 2); 26 | }); 27 | 28 | it('setB', () => { 29 | setB('1234'); 30 | expectState('b', '1234'); 31 | }); 32 | 33 | it('setC', () => { 34 | setC({ a: '666' }); 35 | expectState('c', { a: '666' }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /packages/plugins/xstate/src/map.ts: -------------------------------------------------------------------------------- 1 | import { interpret } from 'xstate/lib/interpreter'; 2 | import { MachineModelDesc } from './types'; 3 | import type { MachineMap } from './types/map'; 4 | 5 | /** 6 | * merge service map at model mounting 7 | */ 8 | export function mergeMachineMap( 9 | map: MachineMap, 10 | modelDesc: MachineModelDesc, 11 | ): void { 12 | // FIXME: mounting model must have name, should fix it's type 13 | const modelName = modelDesc.name; 14 | 15 | // warning for replacing service 16 | const prevService = map[modelName]; 17 | if (prevService) { 18 | console.warn( 19 | `Mounting a model <${modelName}> with existed service. The service would be overridden.`, 20 | ); 21 | } 22 | 23 | // generate service from machine schema 24 | const service = interpret(modelDesc.machine as any); 25 | service.start(); 26 | 27 | map[modelName] = { 28 | service, 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /packages/store/src/utils/memoize.ts: -------------------------------------------------------------------------------- 1 | export const areArgumentsShallowlyEqual = (prev: any, next: any) => { 2 | if (prev === next) { 3 | return true; 4 | } 5 | 6 | if (Array.isArray(prev) && Array.isArray(next)) { 7 | if (prev.length !== next.length) { 8 | return false; 9 | } 10 | const { length } = prev; 11 | for (let i = 0; i < length; i++) { 12 | if (prev[i] !== next[i]) { 13 | return false; 14 | } 15 | } 16 | return true; 17 | } 18 | 19 | return false; 20 | }; 21 | 22 | const defaultMemoize = (func: (...args: any[]) => any) => { 23 | let lastArgs: any = null; 24 | let lastResult: any = null; 25 | return (...args: any[]) => { 26 | if (!areArgumentsShallowlyEqual(lastArgs, args)) { 27 | lastResult = func(...args); 28 | lastArgs = args; 29 | } 30 | 31 | return lastResult; 32 | }; 33 | }; 34 | 35 | export const memorize = (fn: any) => { 36 | return defaultMemoize(fn); 37 | }; 38 | -------------------------------------------------------------------------------- /examples/todos/src/containers/AddTodo.tsx: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react'; 2 | import { useModel } from '@modern-js-reduck/react'; 3 | import todos from '../models/todos'; 4 | 5 | const AddTodo = () => { 6 | const [, actions] = useModel(todos); 7 | 8 | // won't re-render when todos state change, because the state selector always return null 9 | // const [,actions] = useModel(todos, (state) => null); 10 | 11 | const inputRef = useRef(null); 12 | 13 | return ( 14 |
    15 |
    { 17 | e.preventDefault(); 18 | if (!inputRef.current?.value.trim()) { 19 | return; 20 | } 21 | actions.addTodo(inputRef.current.value); 22 | inputRef.current.value = ''; 23 | }} 24 | > 25 | 26 | 27 |
    28 |
    29 | ); 30 | }; 31 | 32 | export default AddTodo; 33 | -------------------------------------------------------------------------------- /packages/plugins/auto-actions/tests/primitive.test.ts: -------------------------------------------------------------------------------- 1 | import { createStore, model } from '@modern-js-reduck/store'; 2 | import plugin from '../src'; 3 | 4 | const testModel = model('name').define({ 5 | state: 0, 6 | }); 7 | 8 | const testModel1 = model('name1').define({ 9 | state: 0, 10 | actions: { 11 | setState(_state, payload: number) { 12 | return payload + 1; 13 | }, 14 | }, 15 | }); 16 | 17 | const store = createStore({ 18 | plugins: [plugin], 19 | }); 20 | 21 | describe('test primitive auto actions', () => { 22 | it('state is a number, setState action should work', () => { 23 | const [, actions] = store.use(testModel); 24 | 25 | actions.setState(2); 26 | expect(store.use(testModel)[0]).toBe(2); 27 | }); 28 | 29 | it("user's action priority is higher than auto actions", () => { 30 | const [, actions] = store.use(testModel1); 31 | 32 | actions.setState(2); 33 | expect(store.use(testModel1)[0]).toBe(3); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /scripts/bin/only-allow-pnpm.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /** 3 | * modified from https://github.com/pnpm/only-allow 4 | * license at https://github.com/pnpm/only-allow/blob/master/LICENSE 5 | */ 6 | 7 | function whichPMRuns() { 8 | if (!process.env.npm_config_user_agent) { 9 | return undefined; 10 | } 11 | const userAgent = process.env.npm_config_user_agent; 12 | const pmSpec = userAgent.split(' ')[0]; 13 | const separatorPos = pmSpec.lastIndexOf('/'); 14 | const name = pmSpec.substring(0, separatorPos); 15 | return { 16 | name: name === 'npminstall' ? 'cnpm' : name, 17 | version: pmSpec.substring(separatorPos + 1), 18 | }; 19 | } 20 | 21 | const usedPM = whichPMRuns(); 22 | if (usedPM && usedPM.name !== 'pnpm') { 23 | console.warn(` 24 | Please use "pnpm" in this project. 25 | If you don't have pnpm, install it via "npm i -g pnpm". 26 | For more details, go to https://pnpm.js.org/ 27 | `); 28 | // eslint-disable-next-line no-process-exit 29 | process.exit(1); 30 | } 31 | -------------------------------------------------------------------------------- /scripts/src/get-release-version.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import assembleReleasePlan from '@changesets/assemble-release-plan'; 3 | import { read } from '@changesets/config'; 4 | import readChangesets from '@changesets/read'; 5 | import { getPackages } from '@manypkg/get-packages'; 6 | 7 | async function run() { 8 | const cwd = process.cwd(); 9 | const repoDir = path.join(cwd, '..'); 10 | const changesets = await readChangesets(repoDir, process.env.BASE_BRANCH); 11 | const packages = await getPackages(repoDir); 12 | const config = await read(repoDir, packages); 13 | const releasePlan = assembleReleasePlan( 14 | changesets, 15 | packages, 16 | config, 17 | undefined, 18 | ); 19 | if (releasePlan.releases.length === 0) { 20 | return; 21 | } 22 | const releaseVersion = `v${releasePlan.releases[0].newVersion}`; 23 | console.log(releaseVersion); 24 | } 25 | 26 | run().catch(e => { 27 | console.error(e); 28 | // eslint-disable-next-line no-process-exit 29 | process.exit(1); 30 | }); 31 | -------------------------------------------------------------------------------- /examples/todos/src/containers/FilterLink.tsx: -------------------------------------------------------------------------------- 1 | import { useModel } from '@modern-js-reduck/react'; 2 | import { PropsWithChildren } from 'react'; 3 | import Link from '../components/Link'; 4 | import todos, { VisibilityFilters } from '../models/todos'; 5 | 6 | interface Props { 7 | filter: VisibilityFilters; 8 | } 9 | 10 | const FilterLink = (props: PropsWithChildren) => { 11 | const { filter, children } = props; 12 | const [{ visibilityFilter }, { setVisibilityFilter }] = useModel(todos); 13 | 14 | // won't re-render when only the `data` slice of state change, 15 | // because the state selector lets `FilterLink` only subscribe the `visibilityFilter` slice of the state 16 | // const [visibilityFilter, {setVisibilityFilter}] = useModel(todos, (state) => state.visibilityFilter) 17 | 18 | return ( 19 | setVisibilityFilter(filter)} 22 | > 23 | {children} 24 | 25 | ); 26 | }; 27 | 28 | export default FilterLink; 29 | -------------------------------------------------------------------------------- /packages/react/README.md: -------------------------------------------------------------------------------- 1 | 2 |

    3 | Modern.js Logo 4 |

    5 |

    6 | 现代 Web 工程体系 7 |
    8 | 9 | modernjs.dev 10 | 11 |

    12 |

    13 | The meta-framework suite designed from scratch for frontend-focused modern web development 14 |

    15 | 16 | # Introduction 17 | 18 | > The doc site ([modernjs.dev](https://modernjs.dev)) and articles are only available in Chinese for now, we are planning to add English versions soon. 19 | 20 | - [Modern.js: Hello, World!](https://zhuanlan.zhihu.com/p/426707646) 21 | 22 | ## Getting Started 23 | 24 | - [Quick Start](https://modernjs.dev/docs/start) 25 | - [Guides](https://modernjs.dev/docs/guides) 26 | - [API References](https://modernjs.dev/docs/api) 27 | 28 | ## Contributing 29 | 30 | - [Contributing Guide](/CONTRIBUTING.md) 31 | -------------------------------------------------------------------------------- /packages/store/README.md: -------------------------------------------------------------------------------- 1 | 2 |

    3 | Modern.js Logo 4 |

    5 |

    6 | 现代 Web 工程体系 7 |
    8 | 9 | modernjs.dev 10 | 11 |

    12 |

    13 | The meta-framework suite designed from scratch for frontend-focused modern web development 14 |

    15 | 16 | # Introduction 17 | 18 | > The doc site ([modernjs.dev](https://modernjs.dev)) and articles are only available in Chinese for now, we are planning to add English versions soon. 19 | 20 | - [Modern.js: Hello, World!](https://zhuanlan.zhihu.com/p/426707646) 21 | 22 | ## Getting Started 23 | 24 | - [Quick Start](https://modernjs.dev/docs/start) 25 | - [Guides](https://modernjs.dev/docs/guides) 26 | - [API References](https://modernjs.dev/docs/api) 27 | 28 | ## Contributing 29 | 30 | - [Contributing Guide](/CONTRIBUTING.md) 31 | -------------------------------------------------------------------------------- /packages/plugins/immer/README.md: -------------------------------------------------------------------------------- 1 | 2 |

    3 | Modern.js Logo 4 |

    5 |

    6 | 现代 Web 工程体系 7 |
    8 | 9 | modernjs.dev 10 | 11 |

    12 |

    13 | The meta-framework suite designed from scratch for frontend-focused modern web development 14 |

    15 | 16 | # Introduction 17 | 18 | > The doc site ([modernjs.dev](https://modernjs.dev)) and articles are only available in Chinese for now, we are planning to add English versions soon. 19 | 20 | - [Modern.js: Hello, World!](https://zhuanlan.zhihu.com/p/426707646) 21 | 22 | ## Getting Started 23 | 24 | - [Quick Start](https://modernjs.dev/docs/start) 25 | - [Guides](https://modernjs.dev/docs/guides) 26 | - [API References](https://modernjs.dev/docs/api) 27 | 28 | ## Contributing 29 | 30 | - [Contributing Guide](/CONTRIBUTING.md) 31 | -------------------------------------------------------------------------------- /packages/plugins/xstate/README.md: -------------------------------------------------------------------------------- 1 | 2 |

    3 | Modern.js Logo 4 |

    5 |

    6 | 现代 Web 工程体系 7 |
    8 | 9 | modernjs.dev 10 | 11 |

    12 |

    13 | The meta-framework suite designed from scratch for frontend-focused modern web development 14 |

    15 | 16 | # Introduction 17 | 18 | > The doc site ([modernjs.dev](https://modernjs.dev)) and articles are only available in Chinese for now, we are planning to add English versions soon. 19 | 20 | - [Modern.js: Hello, World!](https://zhuanlan.zhihu.com/p/426707646) 21 | 22 | ## Getting Started 23 | 24 | - [Quick Start](https://modernjs.dev/docs/start) 25 | - [Guides](https://modernjs.dev/docs/guides) 26 | - [API References](https://modernjs.dev/docs/api) 27 | 28 | ## Contributing 29 | 30 | - [Contributing Guide](/CONTRIBUTING.md) 31 | -------------------------------------------------------------------------------- /packages/plugins/devtools/README.md: -------------------------------------------------------------------------------- 1 | 2 |

    3 | Modern.js Logo 4 |

    5 |

    6 | 现代 Web 工程体系 7 |
    8 | 9 | modernjs.dev 10 | 11 |

    12 |

    13 | The meta-framework suite designed from scratch for frontend-focused modern web development 14 |

    15 | 16 | # Introduction 17 | 18 | > The doc site ([modernjs.dev](https://modernjs.dev)) and articles are only available in Chinese for now, we are planning to add English versions soon. 19 | 20 | - [Modern.js: Hello, World!](https://zhuanlan.zhihu.com/p/426707646) 21 | 22 | ## Getting Started 23 | 24 | - [Quick Start](https://modernjs.dev/docs/start) 25 | - [Guides](https://modernjs.dev/docs/guides) 26 | - [API References](https://modernjs.dev/docs/api) 27 | 28 | ## Contributing 29 | 30 | - [Contributing Guide](/CONTRIBUTING.md) 31 | -------------------------------------------------------------------------------- /packages/plugins/effects/README.md: -------------------------------------------------------------------------------- 1 | 2 |

    3 | Modern.js Logo 4 |

    5 |

    6 | 现代 Web 工程体系 7 |
    8 | 9 | modernjs.dev 10 | 11 |

    12 |

    13 | The meta-framework suite designed from scratch for frontend-focused modern web development 14 |

    15 | 16 | # Introduction 17 | 18 | > The doc site ([modernjs.dev](https://modernjs.dev)) and articles are only available in Chinese for now, we are planning to add English versions soon. 19 | 20 | - [Modern.js: Hello, World!](https://zhuanlan.zhihu.com/p/426707646) 21 | 22 | ## Getting Started 23 | 24 | - [Quick Start](https://modernjs.dev/docs/start) 25 | - [Guides](https://modernjs.dev/docs/guides) 26 | - [API References](https://modernjs.dev/docs/api) 27 | 28 | ## Contributing 29 | 30 | - [Contributing Guide](/CONTRIBUTING.md) 31 | -------------------------------------------------------------------------------- /packages/plugins/auto-actions/README.md: -------------------------------------------------------------------------------- 1 | 2 |

    3 | Modern.js Logo 4 |

    5 |

    6 | 现代 Web 工程体系 7 |
    8 | 9 | modernjs.dev 10 | 11 |

    12 |

    13 | The meta-framework suite designed from scratch for frontend-focused modern web development 14 |

    15 | 16 | # Introduction 17 | 18 | > The doc site ([modernjs.dev](https://modernjs.dev)) and articles are only available in Chinese for now, we are planning to add English versions soon. 19 | 20 | - [Modern.js: Hello, World!](https://zhuanlan.zhihu.com/p/426707646) 21 | 22 | ## Getting Started 23 | 24 | - [Quick Start](https://modernjs.dev/docs/start) 25 | - [Guides](https://modernjs.dev/docs/guides) 26 | - [API References](https://modernjs.dev/docs/api) 27 | 28 | ## Contributing 29 | 30 | - [Contributing Guide](/CONTRIBUTING.md) 31 | -------------------------------------------------------------------------------- /packages/plugins/xstate-immer/README.md: -------------------------------------------------------------------------------- 1 | 2 |

    3 | Modern.js Logo 4 |

    5 |

    6 | 现代 Web 工程体系 7 |
    8 | 9 | modernjs.dev 10 | 11 |

    12 |

    13 | The meta-framework suite designed from scratch for frontend-focused modern web development 14 |

    15 | 16 | # Introduction 17 | 18 | > The doc site ([modernjs.dev](https://modernjs.dev)) and articles are only available in Chinese for now, we are planning to add English versions soon. 19 | 20 | - [Modern.js: Hello, World!](https://zhuanlan.zhihu.com/p/426707646) 21 | 22 | ## Getting Started 23 | 24 | - [Quick Start](https://modernjs.dev/docs/start) 25 | - [Guides](https://modernjs.dev/docs/guides) 26 | - [API References](https://modernjs.dev/docs/api) 27 | 28 | ## Contributing 29 | 30 | - [Contributing Guide](/CONTRIBUTING.md) 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Modern.js 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/store/src/__tsd__/computed.tsd.ts: -------------------------------------------------------------------------------- 1 | import { expectType } from 'tsd'; 2 | import { createStore, model } from '..'; 3 | 4 | type StateA = { 5 | a: number; 6 | }; 7 | const modelA = model('modelA').define({ 8 | state: { 9 | a: 1, 10 | }, 11 | computed: { 12 | double(s) { 13 | return s.a + 1; 14 | }, 15 | }, 16 | }); 17 | 18 | type StateB = { 19 | b: string; 20 | }; 21 | 22 | const modelB = model('modelB').define({ 23 | state: { 24 | b: '10', 25 | }, 26 | computed: { 27 | str: [ 28 | modelA, 29 | (s, other: StateA) => { 30 | return s.b.repeat(other.a); 31 | }, 32 | ], 33 | }, 34 | }); 35 | 36 | describe('test computed', () => { 37 | const store = createStore(); 38 | 39 | test('basic usage', () => { 40 | const [state] = store.use(modelA); 41 | expectType(state); 42 | }); 43 | 44 | test('depend on other models', () => { 45 | const use = () => store.use(modelA, modelB); 46 | const [state] = use(); 47 | expectType(state); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /packages/react/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Modern.js 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/store/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Modern.js 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/plugins/immer/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Modern.js 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/plugins/xstate/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Modern.js 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/plugins/devtools/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Modern.js 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/plugins/effects/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Modern.js 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/plugins/auto-actions/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Modern.js 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/plugins/xstate-immer/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Modern.js 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/todos/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todos", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@modern-js-reduck/react": "latest", 7 | "@testing-library/jest-dom": "^5.16.4", 8 | "@testing-library/react": "^13.3.0", 9 | "@testing-library/user-event": "^13.5.0", 10 | "@types/jest": "^27.5.1", 11 | "@types/node": "^14", 12 | "@types/react": "^18", 13 | "@types/react-dom": "^18", 14 | "react": "^18.0.2", 15 | "react-dom": "^18.0.2", 16 | "react-scripts": "5.0.1", 17 | "typescript": "^4.7.4", 18 | "web-vitals": "^2.1.4" 19 | }, 20 | "scripts": { 21 | "start": "react-scripts start", 22 | "build": "react-scripts build", 23 | "test": "react-scripts test", 24 | "eject": "react-scripts eject" 25 | }, 26 | "eslintConfig": { 27 | "extends": [ 28 | "react-app", 29 | "react-app/jest" 30 | ] 31 | }, 32 | "browserslist": { 33 | "production": [ 34 | ">0.2%", 35 | "not dead", 36 | "not op_mini all" 37 | ], 38 | "development": [ 39 | "last 1 chrome version", 40 | "last 1 firefox version", 41 | "last 1 safari version" 42 | ] 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: CI 4 | 5 | # Controls when the workflow will run 6 | on: 7 | # Triggers the workflow on push or pull request events but only for the main branch 8 | push: 9 | branches: [ main ] 10 | pull_request: 11 | branches: [ main ] 12 | 13 | # Allows you to run this workflow manually from the Actions tab 14 | workflow_dispatch: 15 | 16 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 17 | jobs: 18 | # This workflow contains a single job called "build" 19 | test: 20 | # The type of runner that the job will run on 21 | runs-on: ubuntu-latest 22 | 23 | # Steps represent a sequence of tasks that will be executed as part of the job 24 | steps: 25 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 26 | - uses: actions/checkout@v2 27 | 28 | # Runs a single command using the runners shell 29 | - name: install pnpm 30 | run: npm i -g pnpm@7 31 | 32 | - name: build 33 | run: pnpm i --no-frozen-lockfile 34 | 35 | - name: test 36 | run: pnpm -r --filter "./packages/**" test && pnpm -r --filter "./packages/**" test-type 37 | -------------------------------------------------------------------------------- /packages/react/modern.config.js: -------------------------------------------------------------------------------- 1 | const { 2 | default: moduleTools, 3 | defineConfig, 4 | } = require('@modern-js/module-tools'); 5 | const test = require('@modern-js/plugin-testing').default; 6 | const config = require('../../common/config'); 7 | 8 | const react17Config = { 9 | displayName: 'ReactDOM 17', 10 | moduleNameMapper: { 11 | '^react$': 'react-17', 12 | '^react-dom$': 'react-dom-17', 13 | '^@testing-library/react$': '@testing-library/react-12', 14 | }, 15 | }; 16 | 17 | const react18Config = { 18 | displayName: 'ReactDOM 18', 19 | }; 20 | 21 | module.exports = defineConfig({ 22 | ...config, 23 | plugins: [moduleTools(), test()], 24 | testing: { 25 | jest: options => { 26 | const { moduleNameMapper } = options; 27 | delete options.moduleNameMapper; 28 | return { 29 | ...options, 30 | collectCoverage: true, 31 | projects: [ 32 | { 33 | ...react17Config, 34 | moduleNameMapper: { 35 | ...moduleNameMapper, 36 | ...react17Config.moduleNameMapper, 37 | }, 38 | }, 39 | { 40 | ...react18Config, 41 | moduleNameMapper, 42 | }, 43 | ], 44 | }; 45 | }, 46 | }, 47 | }); 48 | -------------------------------------------------------------------------------- /packages/store/src/plugin/core.ts: -------------------------------------------------------------------------------- 1 | import { Plugin, PluginContext, PluginLifeCycle } from '@/types/plugin'; 2 | 3 | type Stage = keyof PluginLifeCycle; 4 | 5 | export const createPluginCore = (pluginContext: PluginContext) => { 6 | const lifeCycleList: PluginLifeCycle[] = []; 7 | const findHandlers = (stage: S) => 8 | lifeCycleList.map(liftCycle => liftCycle[stage]).filter(Boolean); 9 | 10 | return { 11 | usePlugin: (plugin: Plugin) => { 12 | lifeCycleList.push(plugin(pluginContext)); 13 | }, 14 | invokePipeline: ( 15 | stage: S, 16 | bypassParams: Parameters[0], 17 | ...args: Parameters extends [any, ...infer T] ? T : [] 18 | ) => { 19 | const handlers = findHandlers(stage); 20 | 21 | let params = bypassParams; 22 | 23 | for (const handler of handlers) { 24 | params = (handler as any)(params, ...args); 25 | } 26 | 27 | return params; 28 | }, 29 | invokeWaterFall: ( 30 | stage: S, 31 | ...args: Parameters 32 | ) => { 33 | const handlers = findHandlers(stage); 34 | 35 | return handlers.forEach(handler => (handler as any)(...args)); 36 | }, 37 | }; 38 | }; 39 | -------------------------------------------------------------------------------- /examples/with-middleware/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "with-middleware", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@modern-js-reduck/react": "latest", 7 | "react": "^18.0.2", 8 | "react-dom": "^18.0.2", 9 | "react-scripts": "5.0.1", 10 | "redux-logger": "^3.0.6", 11 | "typescript": "^4.7.4", 12 | "web-vitals": "^2.1.4" 13 | }, 14 | "scripts": { 15 | "start": "react-scripts start", 16 | "build": "react-scripts build", 17 | "test": "react-scripts test", 18 | "eject": "react-scripts eject" 19 | }, 20 | "eslintConfig": { 21 | "extends": [ 22 | "react-app", 23 | "react-app/jest" 24 | ] 25 | }, 26 | "browserslist": { 27 | "production": [ 28 | ">0.2%", 29 | "not dead", 30 | "not op_mini all" 31 | ], 32 | "development": [ 33 | "last 1 chrome version", 34 | "last 1 firefox version", 35 | "last 1 safari version" 36 | ] 37 | }, 38 | "devDependencies": { 39 | "@testing-library/jest-dom": "^5.16.4", 40 | "@testing-library/react": "^13.3.0", 41 | "@testing-library/user-event": "^13.5.0", 42 | "@types/jest": "^27.5.1", 43 | "@types/node": "^14", 44 | "@types/react": "^18", 45 | "@types/react-dom": "^18", 46 | "@types/redux-logger": "^3.0.9" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/react/src/utils/useIsomorphicLayoutEffect.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * copied from https://github.com/reduxjs/react-redux/blob/master/src/utils/useIsomorphicLayoutEffect.ts 3 | * license at https://github.com/reduxjs/react-redux/blob/master/LICENSE.md 4 | */ 5 | 6 | import { useEffect, useLayoutEffect } from 'react'; 7 | 8 | // React currently throws a warning when using useLayoutEffect on the server. 9 | // To get around it, we can conditionally useEffect on the server (no-op) and 10 | // useLayoutEffect in the browser. We need useLayoutEffect to ensure the store 11 | // subscription callback always has the selector from the latest render commit 12 | // available, otherwise a store update may happen between render and the effect, 13 | // which may cause missed updates; we also must ensure the store subscription 14 | // is created synchronously, otherwise a store update may occur before the 15 | // subscription is created and an inconsistent state may be observed 16 | 17 | // Matches logic in React's `shared/ExecutionEnvironment` file 18 | export const canUseDOM = Boolean( 19 | typeof window !== 'undefined' && 20 | typeof window.document !== 'undefined' && 21 | typeof window.document.createElement !== 'undefined', 22 | ); 23 | 24 | export const useIsomorphicLayoutEffect = canUseDOM 25 | ? useLayoutEffect 26 | : useEffect; 27 | -------------------------------------------------------------------------------- /packages/plugins/auto-actions/tests/array.test.ts: -------------------------------------------------------------------------------- 1 | import { createStore, model } from '@modern-js-reduck/store'; 2 | import plugin from '../src'; 3 | 4 | const testModel = model('name').define({ 5 | state: [1, 2, 3], 6 | }); 7 | 8 | const store = createStore({ 9 | plugins: [plugin], 10 | }); 11 | 12 | const [, actions] = store.use(testModel); 13 | 14 | const expectState = (state: any) => { 15 | expect(store.use(testModel)[0]).toEqual(state); 16 | }; 17 | 18 | describe('test array auto actions', () => { 19 | test('push', () => { 20 | actions.push(4); 21 | 22 | expectState([1, 2, 3, 4]); 23 | }); 24 | 25 | test('pop', () => { 26 | actions.pop(); 27 | expectState([1, 2, 3]); 28 | }); 29 | 30 | test('shift', () => { 31 | actions.shift(); 32 | expectState([2, 3]); 33 | }); 34 | 35 | test('unshift', () => { 36 | actions.unshift(1); 37 | expectState([1, 2, 3]); 38 | }); 39 | 40 | test('concat', () => { 41 | actions.concat([4, 5]); 42 | expectState([1, 2, 3, 4, 5]); 43 | }); 44 | 45 | test('splice', () => { 46 | actions.splice(0, 2); 47 | expectState([3, 4, 5]); 48 | 49 | actions.splice(0, 0, 1, 2); 50 | expectState([1, 2, 3, 4, 5]); 51 | }); 52 | 53 | test('filter', () => { 54 | actions.filter(value => value <= 2); 55 | expectState([1, 2]); 56 | 57 | actions.push(3); 58 | expectState([1, 2, 3]); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /packages/plugins/xstate-immer/src/plugin.ts: -------------------------------------------------------------------------------- 1 | import { createPlugin } from '@modern-js-reduck/store'; 2 | import { isMachineModel } from '@modern-js-reduck/plugin-xstate'; 3 | import { assign } from '@xstate/immer'; 4 | 5 | export const plugin = createPlugin(() => ({ 6 | prepareModelDesc(modelDesc) { 7 | if (!isMachineModel(modelDesc)) { 8 | return modelDesc; 9 | } 10 | 11 | const path: string[] = []; 12 | 13 | // FIXME: 14 | traverse((modelDesc.machine as any).config.states); 15 | 16 | return modelDesc; 17 | 18 | /** 19 | * traverse for immerifing actions 20 | */ 21 | function traverse(obj: any) { 22 | Object.keys(obj).forEach(key => { 23 | path.push(key); 24 | const realPath = path.join('/'); 25 | 26 | const isOnActions = /\/on\/\S+\/actions$/.exec(realPath); 27 | 28 | if (typeof obj[key] === 'string') { 29 | path.pop(); 30 | return; 31 | } 32 | 33 | const isFunction = typeof obj[key] === 'function'; 34 | const isAssignObj = 35 | Object.keys(obj[key]) === ['type', 'assignment'] && 36 | obj[key].type === 'xstate.assign'; 37 | if (isOnActions && isFunction) { 38 | obj[key] = assign(obj[key]); 39 | } else if (isAssignObj) { 40 | obj[key] = assign(obj[key].assignment); 41 | } else { 42 | traverse(obj[key]); 43 | } 44 | path.pop(); 45 | }); 46 | } 47 | }, 48 | })); 49 | -------------------------------------------------------------------------------- /packages/store/src/utils/misc.ts: -------------------------------------------------------------------------------- 1 | import { Model } from '@/types'; 2 | 3 | export const initializerSymbol = Symbol.for('__reduck_model_initializer__'); 4 | 5 | export const getModelInitializer = (_model: Model) => _model[initializerSymbol]; 6 | 7 | export const isModel = (_model: any): _model is Model => 8 | _model && Boolean(getModelInitializer(_model)); 9 | 10 | export const getComputedDepModels = (computed: any) => { 11 | const depModels: Model[] = []; 12 | const computedArr = Array.isArray(computed) ? computed : [computed]; 13 | 14 | computedArr.forEach(_computed => { 15 | computed && 16 | Object.keys(computed).forEach(key => { 17 | const selector = computed[key]; 18 | if (Array.isArray(selector)) { 19 | selector.forEach(s => { 20 | if (!depModels.includes(s) && isModel(s)) { 21 | depModels.push(s); 22 | } 23 | }); 24 | } 25 | }); 26 | }); 27 | 28 | return depModels; 29 | }; 30 | 31 | export enum StateType { 32 | Primitive = 'primitive', 33 | Array = 'array', 34 | Object = 'object', 35 | } 36 | 37 | export const getStateType = (value: any): StateType => { 38 | if (Array.isArray(value)) { 39 | return StateType.Array; 40 | // eslint-disable-next-line eqeqeq 41 | } else if (typeof value === 'object' && value != 'undefined') { 42 | return StateType.Object; 43 | } else { 44 | // ignore other types of checking which are not supported by Redux 45 | return StateType.Primitive; 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /examples/vanilla-counter/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | reduck-vanilla-counter 5 | 6 | 7 | 10 |
    11 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /packages/store/tests/store.test.ts: -------------------------------------------------------------------------------- 1 | import { createStore, model } from '../src'; 2 | 3 | const countModel = model<{ value: number }>('counter').define({ 4 | name: 'counter', 5 | state: { 6 | value: 1, 7 | }, 8 | actions: { 9 | add(state) { 10 | return { 11 | ...state, 12 | value: state.value + 1, 13 | }; 14 | }, 15 | }, 16 | }); 17 | 18 | describe('createStore', () => { 19 | test('create store should work', () => { 20 | const store = createStore(); 21 | 22 | expect(store.getState()).toEqual({}); 23 | }); 24 | 25 | test('store use model should work', () => { 26 | const store = createStore(); 27 | const [state] = store.use(countModel); 28 | 29 | expect(state).toEqual({ value: 1 }); 30 | expect(store.getState()).toEqual({ 31 | counter: { value: 1 }, 32 | }); 33 | }); 34 | 35 | test('model actions should work', () => { 36 | const store = createStore(); 37 | const [state, actions] = store.use(countModel); 38 | 39 | expect(state).toEqual({ value: 1 }); 40 | expect(store.getState()).toEqual({ 41 | counter: { value: 1 }, 42 | }); 43 | 44 | actions.add(); 45 | expect(store.getState()).toEqual({ 46 | counter: { value: 2 }, 47 | }); 48 | }); 49 | 50 | test('mount models in createStore', () => { 51 | const store = createStore({ models: [countModel] }); 52 | 53 | expect(store.getState()).toEqual({ counter: { value: 1 } }); 54 | 55 | const [, actions] = store.use(countModel); 56 | actions.add(); 57 | 58 | expect(store.getState()).toEqual({ 59 | counter: { value: 2 }, 60 | }); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /packages/store/tests/selector.test.ts: -------------------------------------------------------------------------------- 1 | import { createStore, model } from '../src'; 2 | 3 | const count1Model = model('count1').define({ 4 | state: { 5 | value: 1, 6 | }, 7 | actions: { 8 | add(state) { 9 | return { 10 | ...state, 11 | value: state.value + 1, 12 | }; 13 | }, 14 | }, 15 | }); 16 | 17 | const count2Model = model('count2').define({ 18 | state: { 19 | value: 10, 20 | }, 21 | actions: { 22 | add1(state) { 23 | return { 24 | ...state, 25 | value: state.value + 1, 26 | }; 27 | }, 28 | }, 29 | }); 30 | 31 | describe('test selector', () => { 32 | const store = createStore(); 33 | 34 | test('select state should work', () => { 35 | const [state] = store.use(count1Model, count2Model, (state1, state2) => ({ 36 | one: state1.value, 37 | two: state2.value, 38 | })); 39 | 40 | expect(state).toEqual({ one: 1, two: 10 }); 41 | }); 42 | 43 | test('select actions should work', () => { 44 | const use = () => 45 | store.use( 46 | count1Model, 47 | count2Model, 48 | (state1, state2) => ({ 49 | one: state1.value, 50 | two: state2.value, 51 | }), 52 | (actions1, actions2) => ({ 53 | oneAdd: actions1.add, 54 | twoAdd: actions2.add1, 55 | }), 56 | ); 57 | 58 | const [state, actions] = use(); 59 | 60 | expect(state).toEqual({ one: 1, two: 10 }); 61 | 62 | actions.oneAdd(); 63 | 64 | expect(use()[0]).toEqual({ one: 2, two: 10 }); 65 | 66 | actions.twoAdd(); 67 | 68 | expect(use()[0]).toEqual({ one: 2, two: 11 }); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /.github/workflows/release-pull-request.yml: -------------------------------------------------------------------------------- 1 | name: Release Pull Request 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | type: choice 8 | description: 'Release Type(next, beta, alpha, latest)' 9 | required: true 10 | default: 'latest' 11 | options: 12 | - next 13 | - beta 14 | - alpha 15 | - latest 16 | 17 | jobs: 18 | release: 19 | name: Create Release Pull Request 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: Checkout Repo 23 | uses: actions/checkout@master 24 | with: 25 | fetch-depth: 100 26 | 27 | - name: Install Pnpm 28 | run: corepack enable 29 | 30 | - name: Setup Node.js 16 31 | uses: actions/setup-node@v3 32 | with: 33 | node-version: "16" 34 | cache: 'pnpm' 35 | 36 | - name: Install Dependencies 37 | run: pnpm install --ignore-scripts 38 | 39 | - name: Prepare Monorepo-Tools 40 | run: pnpm run --filter @modern-js/monorepo-tools... build 41 | 42 | - name: Create Release Pull Request 43 | uses: web-infra-dev/actions@v2 44 | with: 45 | # this expects you to have a script called release which does a build for your packages and calls changeset publish 46 | version: ${{ github.event.inputs.version }} 47 | versionNumber: 'auto' 48 | type: 'pull request' 49 | tools: 'modern' 50 | env: 51 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 52 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 53 | REPOSITORY: ${{ github.repository }} 54 | REF: ${{ github.ref }} 55 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | type: choice 8 | description: 'Release Type(next, beta, alpha, latest)' 9 | required: true 10 | default: 'latest' 11 | options: 12 | - next 13 | - beta 14 | - alpha 15 | - latest 16 | branch: 17 | description: 'Release Branch(confirm release branch)' 18 | required: true 19 | default: 'main' 20 | 21 | jobs: 22 | release: 23 | name: Release 24 | if: ${{ github.event_name == 'workflow_dispatch' }} 25 | runs-on: ubuntu-latest 26 | steps: 27 | - name: Checkout Repo 28 | uses: actions/checkout@v3 29 | with: 30 | # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits 31 | fetch-depth: 1 32 | 33 | - name: Install Pnpm 34 | run: corepack enable 35 | 36 | - name: Setup Node.js 16 37 | uses: actions/setup-node@v3 38 | with: 39 | node-version: "16" 40 | cache: 'pnpm' 41 | 42 | - name: Install Dependencies && Build 43 | run: pnpm install 44 | 45 | - name: Release 46 | uses: web-infra-dev/actions@v2 47 | with: 48 | version: ${{ github.event.inputs.version }} 49 | branch: ${{ github.event.inputs.branch }} 50 | type: 'release' 51 | tools: 'modern' 52 | env: 53 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 54 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 55 | REPOSITORY: ${{ github.repository }} 56 | REF: ${{ github.ref }} 57 | 58 | 59 | -------------------------------------------------------------------------------- /packages/store/src/types/plugin.ts: -------------------------------------------------------------------------------- 1 | import type { Store } from 'redux'; 2 | import type { Context, StoreConfig } from './app'; 3 | import type { Action, Model, ModelDesc, MountedModel } from './model'; 4 | 5 | export interface PluginContext { 6 | store: Store; 7 | } 8 | 9 | export interface PluginLifeCycle { 10 | /** 11 | * Before createStore, this hook will be invoked. Use to change config. 12 | */ 13 | config?: (config: T) => T; 14 | 15 | /** 16 | * Runs after store created 17 | */ 18 | afterCreateStore?: ( 19 | store: T, 20 | ) => T; 21 | 22 | /** 23 | * Runs when a model mounted for first time. 24 | */ 25 | modelMount?: ( 26 | params: T, 27 | api: { 28 | /** 29 | * path: ['todo', 'load'] 30 | */ 31 | setDispatchAction: (path: string[], dispatchAction: any) => void; 32 | }, 33 | ) => T; 34 | 35 | /** 36 | * invoke before useModel value return. 37 | * You can custom returned value in this hook. 38 | */ 39 | useModel?: ( 40 | bypassParams: T, 41 | { 42 | models, 43 | mountedModels, 44 | }: { 45 | models: Model[]; 46 | mountedModels: MountedModel[]; 47 | }, 48 | ) => T; 49 | 50 | prepareModelDesc?: (modelDesc: ModelDesc) => ModelDesc; 51 | 52 | /** 53 | * invoke before reducer execute. You can wrap and return your reducer. 54 | */ 55 | beforeReducer?: ( 56 | reducer: Action, 57 | options: { name: string; computedDescriptors: any }, 58 | ) => Action; 59 | } 60 | 61 | export type Plugin = (context: PluginContext) => PluginLifeCycle; 62 | -------------------------------------------------------------------------------- /packages/plugins/auto-actions/src/array.ts: -------------------------------------------------------------------------------- 1 | const push = (state: T, payload: T[0]) => 2 | state.concat(payload); 3 | 4 | const pop = (state: T) => { 5 | const newState = []; 6 | 7 | for (let i = 0; i < state.length - 1; i++) { 8 | newState.push(state[i]); 9 | } 10 | 11 | return newState; 12 | }; 13 | 14 | const shift = (state: T) => { 15 | const newState = []; 16 | 17 | for (let i = 1; i < state.length; i++) { 18 | newState.push(state[i]); 19 | } 20 | 21 | return newState; 22 | }; 23 | 24 | const unshift = (state: T, payload: T[0]) => [ 25 | payload, 26 | ...state, 27 | ]; 28 | 29 | const concat = (state: T, payload: T) => [ 30 | ...state, 31 | ...payload, 32 | ]; 33 | 34 | const splice = ( 35 | state: T, 36 | start: number, 37 | deleteCount: number, 38 | ...items: T 39 | ) => { 40 | const newState = state.slice(); 41 | newState.splice(start, deleteCount, ...items); 42 | 43 | return newState; 44 | }; 45 | 46 | const filter = ( 47 | state: T, 48 | filterFn: (value: T[0], index: number, array: T[0][]) => boolean, 49 | ) => { 50 | const newState = state.filter(filterFn); 51 | 52 | return newState; 53 | }; 54 | 55 | type ArrayDispatchActions = { 56 | push: (payload: T[0]) => void; 57 | pop: () => void; 58 | shift: () => void; 59 | unshift: (payload: T[0]) => void; 60 | concat: (payload: T) => void; 61 | splice: (start: number, deleteCount: number, ...items: T) => void; 62 | filter: ( 63 | filterFn: (value: T[0], index: number, array: T[0][]) => boolean, 64 | ) => void; 65 | }; 66 | 67 | export { push, pop, shift, unshift, concat, splice, filter }; 68 | export type { ArrayDispatchActions }; 69 | -------------------------------------------------------------------------------- /packages/store/src/types/app.ts: -------------------------------------------------------------------------------- 1 | import { Reducer, Store as ReduxStore, Middleware, StoreEnhancer } from 'redux'; 2 | import { Model, MountedModel } from './model'; 3 | import { Plugin } from './plugin'; 4 | import { createUseModel } from '@/model/useModel'; 5 | import { createPluginCore } from '@/plugin'; 6 | import { createSubscribe } from '@/model/subscribe'; 7 | 8 | export interface ReduckContext { 9 | store: Context['store']; 10 | } 11 | 12 | /** 13 | * Context of reduck app 14 | */ 15 | export interface Context { 16 | /** 17 | * Store instance 18 | */ 19 | store: ReduxStore & { 20 | use: ReturnType; 21 | unmount: (model: Model) => void; 22 | }; 23 | apis: { 24 | addReducers: (reducers: Record) => void; 25 | addModel: (model: M, mountModel: MountedModel) => void; 26 | 27 | getModel: (model: M) => MountedModel | null; 28 | 29 | useModel: ReturnType; 30 | 31 | getModelSubscribe: (model: Model) => ReturnType; 32 | 33 | /** 34 | * Get mountedModel instance by modelname 35 | */ 36 | getModelByName: (name: string) => MountedModel | null; 37 | 38 | /** 39 | * Tag that model with name is `param name` is in mounting. 40 | */ 41 | mountingModel: (modelname: string) => void; 42 | 43 | /** 44 | * Unmount model 45 | */ 46 | unmountModel: (model: Model) => void; 47 | }; 48 | pluginCore: ReturnType; 49 | } 50 | 51 | export interface StoreConfig { 52 | initialState?: Record; 53 | middlewares?: Middleware[]; 54 | models?: Model[]; 55 | plugins?: Plugin[]; 56 | enhancers?: StoreEnhancer[]; 57 | } 58 | 59 | export type Store = Context['store']; 60 | -------------------------------------------------------------------------------- /examples/todos/src/models/todos.ts: -------------------------------------------------------------------------------- 1 | import { model } from '@modern-js-reduck/react'; 2 | 3 | export enum VisibilityFilters { 4 | SHOW_ALL = 'SHOW_ALL', 5 | SHOW_COMPLETED = 'SHOW_COMPLETED', 6 | SHOW_ACTIVE = 'SHOW_ACTIVE', 7 | } 8 | 9 | let nextTodoId = 0; 10 | 11 | export interface Todo { 12 | id: number; 13 | text: string; 14 | completed: boolean; 15 | } 16 | 17 | interface State { 18 | data: Todo[]; 19 | visibilityFilter: VisibilityFilters; 20 | } 21 | 22 | const todos = model('todos').define({ 23 | state: { 24 | data: [], 25 | visibilityFilter: VisibilityFilters.SHOW_ALL, 26 | }, 27 | computed: { 28 | visibleTodos: state => { 29 | const { data, visibilityFilter } = state; 30 | switch (visibilityFilter) { 31 | case VisibilityFilters.SHOW_ALL: 32 | return data; 33 | case VisibilityFilters.SHOW_COMPLETED: 34 | return data.filter(t => t.completed); 35 | case VisibilityFilters.SHOW_ACTIVE: 36 | return data.filter(t => !t.completed); 37 | default: 38 | throw new Error('Unknown filter: ' + visibilityFilter); 39 | } 40 | }, 41 | completedTodoCount: state => 42 | state.data.reduce( 43 | (count, todo) => (todo.completed ? count + 1 : count), 44 | 0, 45 | ), 46 | }, 47 | actions: { 48 | addTodo: (state, text: string) => { 49 | state.data.push({ id: nextTodoId++, text, completed: false }); 50 | }, 51 | toggleTodo: (state, id: number) => { 52 | state.data.forEach(todo => 53 | todo.id === id ? (todo.completed = !todo.completed) : todo, 54 | ); 55 | }, 56 | setVisibilityFilter: (state, filter: VisibilityFilters) => { 57 | state.visibilityFilter = filter; 58 | }, 59 | }, 60 | }); 61 | 62 | export default todos; 63 | -------------------------------------------------------------------------------- /packages/plugins/auto-actions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@modern-js-reduck/plugin-auto-actions", 3 | "version": "1.1.13", 4 | "files": [ 5 | "dist" 6 | ], 7 | "jsnext:source": "./src/index.ts", 8 | "types": "./dist/types/index.d.ts", 9 | "main": "./dist/cjs/index.js", 10 | "module": "./dist/esm/index.js", 11 | "description": "The meta-framework suite designed from scratch for frontend-focused modern web development.", 12 | "homepage": "https://modernjs.dev", 13 | "bugs": "https://github.com/modern-js-dev/reduck/issues", 14 | "license": "MIT", 15 | "keywords": [ 16 | "react", 17 | "framework", 18 | "modern", 19 | "modern.js", 20 | "state", 21 | "reduck" 22 | ], 23 | "exports": { 24 | ".": { 25 | "types": "./dist/types/index.d.ts", 26 | "require": "./dist/cjs/index.js", 27 | "default": "./dist/esm/index.js" 28 | } 29 | }, 30 | "scripts": { 31 | "prepare": "pnpm build", 32 | "prepublishOnly": "only-allow-pnpm && pnpm build --platform", 33 | "new": "modern new", 34 | "build": "modern build", 35 | "test": "modern test" 36 | }, 37 | "dependencies": { 38 | "@swc/helpers": "0.5.1" 39 | }, 40 | "devDependencies": { 41 | "@modern-js-reduck/store": "workspace:*", 42 | "@modern-js/module-tools": "2.21.1", 43 | "@modern-js/plugin-testing": "2.21.1", 44 | "@types/jest": "^27.5.1", 45 | "@types/node": "^14", 46 | "typescript": "^4", 47 | "@modern-js-reduck/scripts": "workspace:*" 48 | }, 49 | "modernSettings": {}, 50 | "sideEffects": false, 51 | "peerDependencies": { 52 | "@modern-js-reduck/store": "workspace:^1.1.13" 53 | }, 54 | "publishConfig": { 55 | "registry": "https://registry.npmjs.org/", 56 | "access": "public" 57 | }, 58 | "repository": "modern-js-dev/reduck" 59 | } 60 | -------------------------------------------------------------------------------- /packages/plugins/immer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@modern-js-reduck/plugin-immutable", 3 | "version": "1.1.13", 4 | "files": [ 5 | "dist" 6 | ], 7 | "jsnext:source": "./src/index.ts", 8 | "types": "./dist/types/index.d.ts", 9 | "main": "./dist/cjs/index.js", 10 | "module": "./dist/esm/index.js", 11 | "description": "The meta-framework suite designed from scratch for frontend-focused modern web development.", 12 | "homepage": "https://modernjs.dev", 13 | "bugs": "https://github.com/modern-js-dev/reduck/issues", 14 | "license": "MIT", 15 | "keywords": [ 16 | "react", 17 | "framework", 18 | "modern", 19 | "modern.js", 20 | "state", 21 | "reduck" 22 | ], 23 | "exports": { 24 | ".": { 25 | "types": "./dist/types/index.d.ts", 26 | "require": "./dist/cjs/index.js", 27 | "default": "./dist/esm/index.js" 28 | } 29 | }, 30 | "scripts": { 31 | "prepare": "pnpm build", 32 | "prepublishOnly": "only-allow-pnpm && pnpm build --platform", 33 | "new": "modern new", 34 | "build": "modern build", 35 | "test": "modern test" 36 | }, 37 | "dependencies": { 38 | "@swc/helpers": "0.5.1", 39 | "immer": "^9.0.5" 40 | }, 41 | "devDependencies": { 42 | "@modern-js-reduck/store": "workspace:*", 43 | "@modern-js/module-tools": "2.21.1", 44 | "@modern-js/plugin-testing": "2.21.1", 45 | "@types/jest": "^27.5.1", 46 | "@types/node": "^14", 47 | "typescript": "^4", 48 | "@modern-js-reduck/scripts": "workspace:*" 49 | }, 50 | "modernSettings": {}, 51 | "sideEffects": false, 52 | "peerDependencies": { 53 | "@modern-js-reduck/store": "workspace:^1.1.13" 54 | }, 55 | "publishConfig": { 56 | "registry": "https://registry.npmjs.org/", 57 | "access": "public" 58 | }, 59 | "repository": "modern-js-dev/reduck" 60 | } 61 | -------------------------------------------------------------------------------- /packages/plugins/devtools/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@modern-js-reduck/plugin-devtools", 3 | "version": "1.1.13", 4 | "files": [ 5 | "dist" 6 | ], 7 | "jsnext:source": "./src/index.ts", 8 | "types": "./dist/types/index.d.ts", 9 | "main": "./dist/cjs/index.js", 10 | "module": "./dist/esm/index.js", 11 | "description": "The meta-framework suite designed from scratch for frontend-focused modern web development.", 12 | "homepage": "https://modernjs.dev", 13 | "bugs": "https://github.com/modern-js-dev/reduck/issues", 14 | "license": "MIT", 15 | "keywords": [ 16 | "react", 17 | "framework", 18 | "modern", 19 | "modern.js", 20 | "state", 21 | "reduck" 22 | ], 23 | "exports": { 24 | ".": { 25 | "types": "./dist/types/index.d.ts", 26 | "require": "./dist/cjs/index.js", 27 | "default": "./dist/esm/index.js" 28 | } 29 | }, 30 | "scripts": { 31 | "prepare": "pnpm build", 32 | "prepublishOnly": "only-allow-pnpm && pnpm build --platform", 33 | "new": "modern new", 34 | "build": "modern build", 35 | "test": "modern test --passWithNoTests" 36 | }, 37 | "dependencies": { 38 | "@swc/helpers": "0.5.1", 39 | "@redux-devtools/extension": "^3.2.2", 40 | "redux": "^4.1.1" 41 | }, 42 | "devDependencies": { 43 | "@modern-js-reduck/store": "workspace:*", 44 | "@modern-js/module-tools": "2.21.1", 45 | "@modern-js/plugin-testing": "2.21.1", 46 | "@types/jest": "^27.5.1", 47 | "@types/node": "^14", 48 | "typescript": "^4", 49 | "@modern-js-reduck/scripts": "workspace:*" 50 | }, 51 | "modernSettings": {}, 52 | "sideEffects": false, 53 | "peerDependencies": { 54 | "@modern-js-reduck/store": "workspace:^1.1.13" 55 | }, 56 | "publishConfig": { 57 | "registry": "https://registry.npmjs.org/", 58 | "access": "public" 59 | }, 60 | "repository": "modern-js-dev/reduck" 61 | } 62 | -------------------------------------------------------------------------------- /packages/plugins/auto-actions/src/index.ts: -------------------------------------------------------------------------------- 1 | import { createPlugin, utils } from '@modern-js-reduck/store'; 2 | import { Model } from '@modern-js-reduck/store/types'; 3 | import { mergeActions } from './utils'; 4 | import * as primitiveActions from './primitive'; 5 | import { ArrayDispatchActions } from './array'; 6 | import * as arrayActions from './array'; 7 | import { ObjectDispatchActions, createObjectActions } from './object'; 8 | 9 | type ExtractDispatchAction = { 10 | [key in keyof T]: T[key] extends (state: any) => any 11 | ? () => void 12 | : T[key] extends (state: any, payload: any) => any 13 | ? (payload: State) => void 14 | : never; 15 | }; 16 | 17 | declare module '@modern-js-reduck/store' { 18 | // Overload GetActions interface to add actions type to useModel's return 19 | interface GetActions { 20 | autoActions: M['_']['state'] extends 21 | | string 22 | | number 23 | | null 24 | | undefined 25 | | ((...args: any[]) => any) 26 | | RegExp 27 | | symbol 28 | ? ExtractDispatchAction 29 | : M['_']['state'] extends any[] 30 | ? ArrayDispatchActions 31 | : M['_']['state'] extends Record 32 | ? ObjectDispatchActions 33 | : Record; 34 | } 35 | } 36 | 37 | export default createPlugin(() => ({ 38 | prepareModelDesc(modelDesc) { 39 | const initialState = modelDesc.state; 40 | const type = utils.getStateType(initialState); 41 | 42 | if (type === 'primitive') { 43 | return mergeActions(modelDesc, primitiveActions); 44 | } 45 | 46 | if (type === 'array') { 47 | return mergeActions(modelDesc, arrayActions); 48 | } 49 | 50 | if (type === 'object') { 51 | return mergeActions(modelDesc, createObjectActions(modelDesc.state)); 52 | } 53 | 54 | return modelDesc; 55 | }, 56 | })); 57 | -------------------------------------------------------------------------------- /packages/plugins/effects/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@modern-js-reduck/plugin-effects", 3 | "version": "1.1.13", 4 | "files": [ 5 | "dist" 6 | ], 7 | "jsnext:source": "./src/index.ts", 8 | "types": "./dist/types/index.d.ts", 9 | "main": "./dist/cjs/index.js", 10 | "module": "./dist/esm/index.js", 11 | "description": "The meta-framework suite designed from scratch for frontend-focused modern web development.", 12 | "homepage": "https://modernjs.dev", 13 | "bugs": "https://github.com/modern-js-dev/reduck/issues", 14 | "license": "MIT", 15 | "keywords": [ 16 | "react", 17 | "framework", 18 | "modern", 19 | "modern.js", 20 | "state", 21 | "reduck" 22 | ], 23 | "exports": { 24 | ".": { 25 | "types": "./dist/types/index.d.ts", 26 | "require": "./dist/cjs/index.js", 27 | "default": "./dist/esm/index.js" 28 | } 29 | }, 30 | "scripts": { 31 | "prepare": "pnpm build", 32 | "prepublishOnly": "only-allow-pnpm && pnpm build --platform", 33 | "new": "modern new", 34 | "build": "modern build", 35 | "test": "modern test" 36 | }, 37 | "dependencies": { 38 | "@swc/helpers": "0.5.1", 39 | "redux": "^4.1.1", 40 | "redux-promise-middleware": "^6.1.2" 41 | }, 42 | "devDependencies": { 43 | "@modern-js-reduck/store": "workspace:*", 44 | "@modern-js/module-tools": "2.21.1", 45 | "@modern-js/plugin-testing": "2.21.1", 46 | "@types/jest": "^27.5.1", 47 | "@types/node": "^14", 48 | "@types/redux-logger": "^3.0.9", 49 | "redux-logger": "^3.0.6", 50 | "typescript": "^4", 51 | "@modern-js-reduck/scripts": "workspace:*" 52 | }, 53 | "modernSettings": {}, 54 | "sideEffects": false, 55 | "peerDependencies": { 56 | "@modern-js-reduck/store": "workspace:^1.1.13" 57 | }, 58 | "publishConfig": { 59 | "registry": "https://registry.npmjs.org/", 60 | "access": "public" 61 | }, 62 | "repository": "modern-js-dev/reduck" 63 | } 64 | -------------------------------------------------------------------------------- /packages/plugins/xstate/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@modern-js-reduck/plugin-xstate", 3 | "version": "1.1.0", 4 | "private": true, 5 | "jsnext:source": "./src/index.ts", 6 | "types": "./dist/types/index.d.ts", 7 | "main": "./dist/cjs/index.js", 8 | "module": "./dist/esm/index.js", 9 | "description": "The meta-framework suite designed from scratch for frontend-focused modern web development.", 10 | "homepage": "https://modernjs.dev", 11 | "bugs": "https://github.com/modern-js-dev/reduck/issues", 12 | "license": "MIT", 13 | "keywords": [ 14 | "react", 15 | "framework", 16 | "modern", 17 | "modern.js", 18 | "state", 19 | "reduck" 20 | ], 21 | "exports": { 22 | ".": { 23 | "types": "./dist/types/index.d.ts", 24 | "require": "./dist/cjs/index.js", 25 | "default": "./dist/esm/index.js" 26 | } 27 | }, 28 | "scripts": { 29 | "prepare": "pnpm build", 30 | "prepublishOnly": "only-allow-pnpm && pnpm build --platform", 31 | "new": "modern new", 32 | "build": "modern build", 33 | "test": "modern test" 34 | }, 35 | "dependencies": { 36 | "@swc/helpers": "0.5.1", 37 | "xstate": "^4.23.1" 38 | }, 39 | "devDependencies": { 40 | "@modern-js-reduck/store": "workspace:*", 41 | "@modern-js/module-tools": "2.21.1", 42 | "@modern-js/plugin-testing": "2.21.1", 43 | "@testing-library/jest-dom": "^5.14.1", 44 | "@testing-library/react": "^12.0.0", 45 | "@types/jest": "^27.5.1", 46 | "@types/node": "^14", 47 | "@types/testing-library__jest-dom": "^5.14.1", 48 | "typescript": "^4", 49 | "@modern-js-reduck/scripts": "workspace:*" 50 | }, 51 | "modernSettings": {}, 52 | "sideEffects": false, 53 | "peerDependencies": { 54 | "@modern-js-reduck/store": "workspace:^1.1.13" 55 | }, 56 | "publishConfig": { 57 | "registry": "https://registry.npmjs.org/", 58 | "access": "public" 59 | }, 60 | "repository": "modern-js-dev/reduck" 61 | } 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

    2 | Modern.js Logo 3 |

    4 | 5 |

    Reduck

    6 | 7 |

    8 | A Redux-based state management library. 9 |

    10 | 11 |

    12 | npm version 13 | downloads 14 | License 15 |

    16 | 17 | ## Introduction 18 | 19 | Reduck is a Redux-based state management library that offers simple APIs and less boilerplate codes. 20 | It can be used out of the box within Modern.js, which is also recommended, or it can be used directly. 21 | 22 | ## Documentation 23 | 24 | The Reduck docs are located at : https://modernjs.dev/en/guides/topic-detail/model/quick-start. 25 | 26 | # Examples 27 | - [todos](https://stackblitz.com/github/modern-js-dev/reduck/tree/main/examples/todos?file=src%2Findex.tsx&terminal=start&title=todos) 28 | - [vanilla-counter](https://stackblitz.com/github/modern-js-dev/reduck/tree/main/examples/vanilla-counter?file=index.html&terminal=dev&title=vanilla-counter) 29 | - [vite-counter](https://stackblitz.com/github/modern-js-dev/reduck/tree/main/examples/vite-counter?file=src%2FApp.tsx&terminal=dev&title=vite-counter) 30 | 31 | 32 | ## Contributing 33 | 34 | > New contributors welcome! 35 | 36 | Please read the [Contributing Guide](https://github.com/web-infra-dev/modern.js/blob/main/CONTRIBUTING.md). 37 | 38 | ### Code of Conduct 39 | 40 | This repo has adopted the Bytedance Open Source Code of Conduct. Please check [Code of Conduct](./CODE_OF_CONDUCT.md) for more details. 41 | 42 | 43 | ## License 44 | 45 | Reduck is [MIT licensed](https://github.com/web-infra-dev/reduck/blob/main/LICENSE). 46 | -------------------------------------------------------------------------------- /packages/store/src/__tsd__/selector.tsd.ts: -------------------------------------------------------------------------------- 1 | import { expectType } from 'tsd'; 2 | import { createStore, model } from '..'; 3 | import { ReduxAction } from '@/types'; 4 | 5 | const count1Model = model('count1').define({ 6 | state: { 7 | value: 1, 8 | }, 9 | actions: { 10 | add(state) { 11 | return { 12 | ...state, 13 | value: state.value + 1, 14 | }; 15 | }, 16 | sub(state, n: number) { 17 | return { 18 | ...state, 19 | value: state.value - n, 20 | }; 21 | }, 22 | }, 23 | }); 24 | 25 | const count2Model = model('count2').define({ 26 | state: { 27 | value: 10, 28 | }, 29 | actions: { 30 | add1(state) { 31 | return { 32 | ...state, 33 | value: state.value + 1, 34 | }; 35 | }, 36 | sub1(state, n: number) { 37 | return { 38 | ...state, 39 | value: state.value - n, 40 | }; 41 | }, 42 | }, 43 | }); 44 | type State = { 45 | one: number; 46 | two: number; 47 | }; 48 | describe('test selector', () => { 49 | const store = createStore(); 50 | 51 | test('select state should work', () => { 52 | const [state] = store.use(count1Model, count2Model, (state1, state2) => ({ 53 | one: state1.value, 54 | two: state2.value, 55 | })); 56 | expectType(state); 57 | }); 58 | 59 | test('select actions should work', () => { 60 | const use = () => 61 | store.use( 62 | count1Model, 63 | count2Model, 64 | (state1, state2) => ({ 65 | one: state1.value, 66 | two: state2.value, 67 | }), 68 | (actions1, actions2) => ({ 69 | oneAdd: actions1.add, 70 | twoAdd: actions2.add1, 71 | oneSub: actions1.sub, 72 | twoSub: actions2.sub1, 73 | }), 74 | ); 75 | 76 | const [state, actions] = use(); 77 | expectType(state); 78 | expectType<{ 79 | oneAdd: () => ReduxAction; 80 | twoAdd: () => ReduxAction; 81 | oneSub: (n: number) => ReduxAction; 82 | twoSub: (n: number) => ReduxAction; 83 | }>(actions); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /packages/plugins/xstate-immer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@modern-js-reduck/plugin-xstate-immer", 3 | "version": "1.1.0", 4 | "private": true, 5 | "files": [ 6 | "dist" 7 | ], 8 | "jsnext:source": "./src/index.ts", 9 | "types": "./dist/types/index.d.ts", 10 | "main": "./dist/cjs/index.js", 11 | "module": "./dist/esm/index.js", 12 | "description": "The meta-framework suite designed from scratch for frontend-focused modern web development.", 13 | "homepage": "https://modernjs.dev", 14 | "bugs": "https://github.com/modern-js-dev/reduck/issues", 15 | "license": "MIT", 16 | "keywords": [ 17 | "react", 18 | "framework", 19 | "modern", 20 | "modern.js", 21 | "state", 22 | "reduck" 23 | ], 24 | "exports": { 25 | ".": { 26 | "types": "./dist/types/index.d.ts", 27 | "require": "./dist/cjs/index.js", 28 | "default": "./dist/esm/index.js" 29 | } 30 | }, 31 | "scripts": { 32 | "prepare": "pnpm build", 33 | "prepublishOnly": "only-allow-pnpm && pnpm build --platform", 34 | "new": "modern new", 35 | "build": "modern build", 36 | "test": "modern test" 37 | }, 38 | "dependencies": { 39 | "@swc/helpers": "0.5.1", 40 | "@xstate/immer": "^0.3.1", 41 | "immer": "^9.0.5" 42 | }, 43 | "devDependencies": { 44 | "@modern-js-reduck/plugin-xstate": "workspace:*", 45 | "@modern-js-reduck/store": "workspace:*", 46 | "@modern-js/module-tools": "2.21.1", 47 | "@modern-js/plugin-testing": "2.21.1", 48 | "@testing-library/jest-dom": "^5.14.1", 49 | "@testing-library/react": "^12.0.0", 50 | "@types/jest": "^27.5.1", 51 | "@types/node": "^14", 52 | "@types/testing-library__jest-dom": "^5.14.1", 53 | "typescript": "^4", 54 | "xstate": "*", 55 | "@modern-js-reduck/scripts": "workspace:*" 56 | }, 57 | "modernSettings": {}, 58 | "sideEffects": false, 59 | "peerDependencies": { 60 | "@modern-js-reduck/store": "workspace:^1.1.13", 61 | "@modern-js-reduck/plugin-xstate": "workspace:^1.1.0" 62 | }, 63 | "publishConfig": { 64 | "registry": "https://registry.npmjs.org/", 65 | "access": "public" 66 | }, 67 | "repository": "modern-js-dev/reduck" 68 | } 69 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Reduck Contributing Guide 2 | 3 | Thanks for that you are interested in contributing to Reduck. 4 | 5 | ## Developing 6 | 7 | To develop locally: 8 | 9 | 1. [Fork](https://help.github.com/articles/fork-a-repo/) this repository to your 10 | own GitHub account and then 11 | [clone](https://help.github.com/articles/cloning-a-repository/) it to your 12 | local. 13 | 2. Create a new branch: 14 | 15 | ```zsh 16 | git checkout -b MY_BRANCH_NAME 17 | ``` 18 | 19 | 3. Install pnpm: 20 | 21 | ```zsh 22 | npm install -g pnpm 23 | ``` 24 | 25 | 4. Install the dependencies with: 26 | 27 | ```zsh 28 | pnpm run setup 29 | ``` 30 | 31 | 5. Go into package which you want to contribute. 32 | 33 | ```zsh 34 | cd ./packages/ 35 | ``` 36 | 37 | 6. Start developing. 38 | 39 | 40 | ## Building 41 | 42 | You can build single package, with: 43 | 44 | ```zsh 45 | cd ./packages/* 46 | pnpm build 47 | ``` 48 | 49 | build all packages, with: 50 | 51 | ```zsh 52 | pnpm -r prepare 53 | ``` 54 | 55 | If you need to clean all `node_modules/*` the project for any reason, with 56 | 57 | ```zsh 58 | pnpm reset 59 | ``` 60 | 61 | ## Testing 62 | 63 | You need write new test cases for new feature or modify existing test cases for changes. 64 | 65 | We wish you write unit tests at `PACKAGE_DIR/__test__`. Test syntax is based on [jest](https://jestjs.io/). 66 | 67 | ### Run Testing 68 | 69 | ```sh 70 | pnpm -r test 71 | ``` 72 | 73 | ## Linting 74 | 75 | To check the formatting of your code: 76 | 77 | ```zsh 78 | pnpm lint 79 | ``` 80 | 81 | ## Publishing 82 | 83 | We use **Modern.js Monorepo Solution** to manage version and changelog. 84 | 85 | Repository maintainers can publish a new version of all packages to npm. 86 | 87 | 1. Fetch newest code at branch `main`. 88 | 2. Install 89 | 90 | ```zsh 91 | pnpm run setup 92 | ``` 93 | 94 | 3. Add changeset 95 | 96 | ```zsh 97 | pnpm change 98 | ``` 99 | 100 | 4. Bump version 101 | 102 | ```zsh 103 | pnpm bump 104 | ``` 105 | 106 | 5. Commit version change. The format of commit message should be `chore: va.b.c` which is the main version of current release. 107 | 108 | ```zsh 109 | git add . 110 | git commit -m "chore: va.b.c" 111 | ``` 112 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "reduck-monorepo", 4 | "description": "The meta-framework suite designed from scratch for frontend-focused modern web development.", 5 | "homepage": "https://modernjs.dev", 6 | "bugs": "https://github.com/modern-js-dev/reduck/issues", 7 | "repository": "modern-js-dev/reduck", 8 | "license": "MIT", 9 | "keywords": [ 10 | "react", 11 | "framework", 12 | "modern", 13 | "modern.js", 14 | "state", 15 | "reduck" 16 | ], 17 | "scripts": { 18 | "new": "modern new", 19 | "setup": "npm run reset && pnpm install", 20 | "reset": "pnpm -r exec rm -rf node_modules", 21 | "lint": "modern lint", 22 | "change": "modern change", 23 | "bump": "modern bump", 24 | "pre": "modern pre", 25 | "release": "modern release", 26 | "prepare": "husky install", 27 | "build": "pnpm run --filter './packages/**' prepare", 28 | "preinstall": "only-allow-pnpm", 29 | "gen-release-note": "modern gen-release-note", 30 | "get-release-version": "cd scripts && pnpm run get-release-version" 31 | }, 32 | "engines": { 33 | "node": ">=12.13.0" 34 | }, 35 | "packageManager": "pnpm@8.6.1", 36 | "commitlint": { 37 | "extends": [ 38 | "@commitlint/config-conventional" 39 | ] 40 | }, 41 | "lint-staged": { 42 | "*.{ts,tsx}": [ 43 | "pnpm exec eslint --fix --color --cache --quiet" 44 | ], 45 | "*.{js,jsx,mjs,mjsx,cjs,cjsx}": [ 46 | "pnpm exec eslint --fix --color --cache --quiet" 47 | ] 48 | }, 49 | "eslintConfig": { 50 | "extends": [ 51 | "@modern-js" 52 | ], 53 | "ignorePatterns": [ 54 | "dist", 55 | "lcov-report", 56 | "examples", 57 | "common" 58 | ] 59 | }, 60 | "eslintIgnore": [ 61 | "node_modules/", 62 | "dist/", 63 | "lib/", 64 | ".rpt2_cache/" 65 | ], 66 | "workspaces": { 67 | "packages": [ 68 | "packages/*", 69 | "packages/**/*" 70 | ] 71 | }, 72 | "devDependencies": { 73 | "@commitlint/cli": "^17.0.0", 74 | "@commitlint/config-conventional": "^17.0.0", 75 | "@modern-js-reduck/scripts": "workspace:*", 76 | "@modern-js/monorepo-tools": "2.21.1", 77 | "@modern-js/tsconfig": "2.21.1", 78 | "@modern-js/eslint-config": "2.21.1", 79 | "husky": "^8.0.0", 80 | "lint-staged": "^11.2.6", 81 | "webpack": "^5.54.0" 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /packages/store/src/model/subscribe.ts: -------------------------------------------------------------------------------- 1 | import { Context, Model } from '@/types'; 2 | 3 | export const GetUnsubscribe = Symbol('getUnsubscribe'); 4 | 5 | const createSubscribe = (context: Context, model: Model) => { 6 | const mountedModel = context.apis.getModel(model); 7 | 8 | if (!mountedModel) { 9 | return null; 10 | } 11 | 12 | const { name } = mountedModel; 13 | let lastState = context.store.getState()[name]; 14 | let unsubscribeStore: ReturnType; 15 | const handlers = new Set(); 16 | 17 | const setupSubscribeStore = () => { 18 | // Already subscribed store 19 | if (unsubscribeStore) { 20 | return unsubscribeStore; 21 | } 22 | 23 | unsubscribeStore = context.store.subscribe(() => { 24 | const curState = context.store.getState()[name]; 25 | 26 | if (lastState !== curState) { 27 | lastState = curState; 28 | 29 | handlers.forEach(handler => handler()); 30 | } 31 | }); 32 | 33 | return unsubscribeStore; 34 | }; 35 | 36 | const ret = (handler: () => void) => { 37 | unsubscribeStore = setupSubscribeStore(); 38 | handlers.add(handler); 39 | 40 | return () => { 41 | handlers.delete(handler); 42 | if (handlers.size === 0) { 43 | unsubscribeStore?.(); 44 | unsubscribeStore = null; 45 | } 46 | }; 47 | }; 48 | 49 | // manually unsubscribe when model is unmounted 50 | ret[GetUnsubscribe] = () => unsubscribeStore; 51 | return ret; 52 | }; 53 | 54 | const combineSubscribe = ( 55 | context: Context, 56 | ...subscribes: ReturnType[] 57 | ) => { 58 | const { store } = context; 59 | let changed = false; 60 | const handlers = new Set(); 61 | 62 | return (handler: () => void) => { 63 | handlers.add(handler); 64 | 65 | const disposer = []; 66 | 67 | subscribes.forEach(subscribe => { 68 | disposer.push( 69 | subscribe(() => { 70 | changed = true; 71 | }), 72 | ); 73 | }); 74 | 75 | const unsubscribeStore = store.subscribe(() => { 76 | if (changed) { 77 | changed = false; 78 | handlers.forEach(h => h()); 79 | } 80 | }); 81 | 82 | return () => { 83 | unsubscribeStore(); 84 | disposer.forEach(dispose => dispose()); 85 | }; 86 | }; 87 | }; 88 | 89 | export { createSubscribe, combineSubscribe }; 90 | -------------------------------------------------------------------------------- /packages/store/src/store/createStore.ts: -------------------------------------------------------------------------------- 1 | import { 2 | applyMiddleware, 3 | compose, 4 | createStore as createReduxStore, 5 | type Action, 6 | type Reducer, 7 | type StoreEnhancer, 8 | type StoreEnhancerStoreCreator, 9 | } from 'redux'; 10 | import { createContext } from './context'; 11 | import type { Context, StoreConfig } from '@/types'; 12 | 13 | const createStore = (props: StoreConfig = {}): Context['store'] => { 14 | const store: any = {}; 15 | const context = createContext(store); 16 | 17 | // Load all available plugins 18 | props?.plugins?.forEach(plugin => context.pluginCore.usePlugin(plugin)); 19 | 20 | const finalProps = context.pluginCore.invokePipeline('config', props); 21 | 22 | const { 23 | initialState = {}, 24 | middlewares, 25 | enhancers = [], 26 | models = [], 27 | } = finalProps; 28 | 29 | Object.assign( 30 | store, 31 | createReduxStore( 32 | (state => state) as Reducer, 33 | initialState, 34 | compose>( 35 | ...[ 36 | mergeInitialState(), 37 | middlewares ? applyMiddleware(...middlewares) : undefined, 38 | ...(enhancers || []), 39 | ].filter(Boolean), 40 | ), 41 | ), 42 | ); 43 | 44 | store.use = context.apis.useModel; 45 | store.unmount = context.apis.unmountModel; 46 | 47 | if (models.length > 0) { 48 | store.use(models); 49 | } 50 | 51 | context.pluginCore.invokeWaterFall('afterCreateStore', store); 52 | 53 | return store; 54 | }; 55 | 56 | /** 57 | * Merge prev global state when mounting new models 58 | * to avoid to miss the initial state of the mounting models 59 | */ 60 | function mergeInitialState(): StoreEnhancer { 61 | return createStore => (reducer, initialState) => { 62 | const liftReducer = (r: Reducer) => { 63 | if (typeof r !== 'function') { 64 | throw new Error('Expected the reducer to be a function.'); 65 | } 66 | 67 | return (state = initialState, action: Action) => { 68 | const nextState = r(state, action); 69 | if (/^@@redux\/REPLACE/.test(action.type)) { 70 | return { ...state, ...nextState }; 71 | } else { 72 | return nextState; 73 | } 74 | }; 75 | }; 76 | 77 | const store = createStore(liftReducer(reducer)); 78 | 79 | return { 80 | ...store, 81 | replaceReducer: reducer => { 82 | return store.replaceReducer(liftReducer(reducer)); 83 | }, 84 | }; 85 | }; 86 | } 87 | 88 | export default createStore; 89 | -------------------------------------------------------------------------------- /packages/store/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@modern-js-reduck/store", 3 | "version": "1.1.13", 4 | "files": [ 5 | "dist" 6 | ], 7 | "jsnext:source": "./src/index.ts", 8 | "types": "./dist/types/index.d.ts", 9 | "main": "./dist/cjs/index.js", 10 | "module": "./dist/esm/index.js", 11 | "jsnext:modern": "./dist/js/modern/index.js", 12 | "description": "The meta-framework suite designed from scratch for frontend-focused modern web development.", 13 | "homepage": "https://modernjs.dev", 14 | "bugs": "https://github.com/modern-js-dev/reduck/issues", 15 | "license": "MIT", 16 | "keywords": [ 17 | "react", 18 | "framework", 19 | "modern", 20 | "modern.js", 21 | "state", 22 | "reduck", 23 | "store", 24 | "redux" 25 | ], 26 | "exports": { 27 | ".": { 28 | "types": "./dist/types/index.d.ts", 29 | "require": "./dist/cjs/index.js", 30 | "default": "./dist/esm/index.js" 31 | }, 32 | "./types": { 33 | "types": "./dist/types/types/index.d.ts", 34 | "require": "./dist/cjs/types/index.js", 35 | "default": "./dist/esm/types/index.js" 36 | }, 37 | "./utils": { 38 | "types": "./dist/types/utils/index.d.ts", 39 | "require": "./dist/cjs/utils/index.js", 40 | "default": "./dist/esm/utils/index.js" 41 | } 42 | }, 43 | "typesVersions": { 44 | "*": { 45 | ".": [ 46 | "./dist/types/index.d.ts" 47 | ], 48 | "types": [ 49 | "./dist/types/types/index.d.ts" 50 | ], 51 | "utils": [ 52 | "./dist/types/utils/index.d.ts" 53 | ] 54 | } 55 | }, 56 | "scripts": { 57 | "prepare": "pnpm build", 58 | "prepublishOnly": "only-allow-pnpm && pnpm build --platform", 59 | "new": "modern new", 60 | "build": "modern build", 61 | "test": "modern test", 62 | "test-type": "tsd" 63 | }, 64 | "tsd": { 65 | "directory": "./src/__tsd__" 66 | }, 67 | "dependencies": { 68 | "@swc/helpers": "0.5.1", 69 | "redux": "^4.1.1" 70 | }, 71 | "devDependencies": { 72 | "@modern-js/module-tools": "2.21.1", 73 | "@modern-js/plugin-testing": "2.21.1", 74 | "@modern-js/runtime": "2.21.1", 75 | "@types/jest": "^27.5.1", 76 | "@types/node": "^14", 77 | "tsd": "latest", 78 | "typescript": "latest", 79 | "@modern-js-reduck/scripts": "workspace:*" 80 | }, 81 | "modernSettings": {}, 82 | "sideEffects": false, 83 | "publishConfig": { 84 | "registry": "https://registry.npmjs.org/", 85 | "access": "public" 86 | }, 87 | "repository": "modern-js-dev/reduck" 88 | } 89 | -------------------------------------------------------------------------------- /packages/store/tests/onMount.test.ts: -------------------------------------------------------------------------------- 1 | import { createStore, model } from '../src'; 2 | 3 | const createCountModel = (onMountCreator: (onMount: any, use: any) => void) => 4 | model<{ value: number }>('count').define((_, { onMount, use }) => { 5 | onMountCreator(onMount, use); 6 | 7 | return { 8 | state: { 9 | value: 1, 10 | }, 11 | actions: { 12 | addValue(state, value) { 13 | return { 14 | ...state, 15 | // FIXME: ESlint 校验时,无法正确获取参数 state 的类型信息,识别为 any 16 | // eslint-disable-next-line @typescript-eslint/restrict-plus-operands 17 | value: state.value + value, 18 | }; 19 | }, 20 | }, 21 | }; 22 | }); 23 | 24 | describe('test onMount hook', () => { 25 | test('onMount hook should invoked when model mounted', () => { 26 | const store = createStore(); 27 | const fn = jest.fn(); 28 | const onMountCreator = onMount => { 29 | onMount(() => { 30 | fn(); 31 | }); 32 | }; 33 | const count = createCountModel(onMountCreator); 34 | 35 | store.use(count); 36 | 37 | expect(fn).toBeCalledTimes(1); 38 | }); 39 | 40 | test('onMount hook should invoked only once when store.use model multiple times', () => { 41 | const store = createStore(); 42 | const fn = jest.fn(); 43 | const onMountCreator = onMount => { 44 | onMount(() => { 45 | fn(); 46 | }); 47 | }; 48 | const count = createCountModel(onMountCreator); 49 | 50 | store.use(count); 51 | store.use(count); 52 | store.use(count); 53 | store.use(count); 54 | 55 | expect(fn).toBeCalledTimes(1); 56 | }); 57 | 58 | test('through `use` to get newest state in onMount', () => { 59 | const store = createStore(); 60 | const onMountCreator = (onMount, use) => { 61 | onMount(() => { 62 | const [state, actions] = use(count); 63 | 64 | expect(state).toEqual({ value: 1 }); 65 | 66 | actions.addValue(1); 67 | 68 | expect(use(count)[0]).toEqual({ value: 2 }); 69 | }); 70 | }; 71 | const count = createCountModel(onMountCreator); 72 | store.use(count); 73 | }); 74 | 75 | test('actions should return correct value', () => { 76 | const store = createStore(); 77 | const onMountCreator = (onMount, use) => { 78 | onMount(() => { 79 | const [, actions] = use(count); 80 | 81 | const result = actions.addValue(1); 82 | 83 | expect(result).toEqual({ 84 | type: 'COUNT/ADDVALUE', 85 | payload: 1, 86 | extraArgs: [], 87 | }); 88 | }); 89 | }; 90 | const count = createCountModel(onMountCreator); 91 | store.use(count); 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /packages/plugins/immer/tests/index.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/restrict-plus-operands */ 2 | import { model, createStore } from '@modern-js-reduck/store'; 3 | import immerPlugin from '../src'; 4 | 5 | const count = model('count').define({ 6 | state: { 7 | value: 1, 8 | }, 9 | actions: { 10 | add(state) { 11 | state.value += 1; 12 | }, 13 | pureAdd(state) { 14 | return { 15 | ...state, 16 | value: state.value + 1, 17 | }; 18 | }, 19 | }, 20 | }); 21 | 22 | const count2 = model('count2').define({ 23 | state: { 24 | value: 1, 25 | }, 26 | computed: { 27 | addOne: (state: any) => state.value + 1, 28 | sum: [count, (state: any, state2: any) => state.value + state2.value], 29 | }, 30 | actions: { 31 | add(state) { 32 | state.value += 1; 33 | }, 34 | pureAdd(state) { 35 | return { 36 | ...state, 37 | value: state.value + 1, 38 | }; 39 | }, 40 | }, 41 | }); 42 | 43 | describe('test immer', () => { 44 | let store: any; 45 | beforeEach(() => { 46 | store = createStore({ 47 | plugins: [immerPlugin], 48 | }); 49 | }); 50 | 51 | test('mutable state state in action should work', () => { 52 | const [, actions, subscribe] = store.use(count); 53 | let stateUpdated = false; 54 | 55 | const unsubscribe = subscribe(() => { 56 | expect(store.use(count)[0]).toEqual({ value: 2 }); 57 | stateUpdated = true; 58 | }); 59 | 60 | actions.add(); 61 | unsubscribe(); 62 | 63 | expect(stateUpdated).toBe(true); 64 | }); 65 | 66 | test('pure action should work', () => { 67 | const [, actions, subscribe] = store.use(count); 68 | let stateUpdated = false; 69 | 70 | const unsubscribe = subscribe(() => { 71 | expect(store.use(count)[0]).toEqual({ value: 2 }); 72 | stateUpdated = true; 73 | }); 74 | 75 | actions.pureAdd(); 76 | unsubscribe(); 77 | 78 | expect(stateUpdated).toBe(true); 79 | }); 80 | 81 | test('computed properties should work', () => { 82 | const [, count1Actions] = store.use(count); 83 | const [, count2Actions, subscribe] = store.use(count2); 84 | 85 | count2Actions.pureAdd(); 86 | const [count2State] = store.use(count2); 87 | expect(count2State.addOne).toEqual(3); 88 | expect(count2State.sum).toEqual(3); 89 | 90 | let stateUpdated = false; 91 | const unsubscribe = subscribe(() => { 92 | stateUpdated = true; 93 | }); 94 | 95 | count1Actions.add(); 96 | const [updateCount2State] = store.use(count2); 97 | expect(stateUpdated).toBe(true); 98 | expect(updateCount2State.addOne).toEqual(3); 99 | expect(updateCount2State.sum).toEqual(4); 100 | 101 | unsubscribe(); 102 | }); 103 | }); 104 | /* eslint-enable @typescript-eslint/restrict-plus-operands */ 105 | -------------------------------------------------------------------------------- /packages/react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@modern-js-reduck/react", 3 | "version": "1.1.13", 4 | "files": [ 5 | "dist", 6 | "type.d.ts" 7 | ], 8 | "jsnext:source": "./src/index.ts", 9 | "types": "./type.d.ts", 10 | "main": "./dist/cjs/index.js", 11 | "module": "./dist/esm/index.js", 12 | "jsnext:modern": "./dist/js/modern/index.js", 13 | "description": "The meta-framework suite designed from scratch for frontend-focused modern web development.", 14 | "homepage": "https://modernjs.dev", 15 | "bugs": "https://github.com/modern-js-dev/reduck/issues", 16 | "license": "MIT", 17 | "keywords": [ 18 | "react", 19 | "framework", 20 | "modern", 21 | "modern.js", 22 | "state", 23 | "reduck" 24 | ], 25 | "exports": { 26 | ".": { 27 | "types": "./type.d.ts", 28 | "require": "./dist/cjs/index.js", 29 | "default": "./dist/esm/index.js" 30 | } 31 | }, 32 | "scripts": { 33 | "prepare": "pnpm build", 34 | "prepublishOnly": "only-allow-pnpm && pnpm build --platform", 35 | "new": "modern new", 36 | "build": "modern build", 37 | "test": "modern test" 38 | }, 39 | "dependencies": { 40 | "@swc/helpers": "0.5.1", 41 | "@modern-js-reduck/store": "workspace:*", 42 | "@modern-js-reduck/plugin-effects": "workspace:*", 43 | "@modern-js-reduck/plugin-immutable": "workspace:*", 44 | "@modern-js-reduck/plugin-devtools": "workspace:*", 45 | "@modern-js-reduck/plugin-auto-actions": "workspace:*", 46 | "hoist-non-react-statics": "3.3.2", 47 | "invariant": "^2.2.4" 48 | }, 49 | "devDependencies": { 50 | "@modern-js/module-tools": "2.21.1", 51 | "@modern-js/plugin-testing": "2.21.1", 52 | "@testing-library/jest-dom": "^5.16.5", 53 | "@testing-library/react": "^13.3.0", 54 | "@testing-library/react-12": "npm:@testing-library/react@^12", 55 | "@types/hoist-non-react-statics": "3.3.1", 56 | "@types/invariant": "^2.2.34", 57 | "@types/jest": "^27.5.1", 58 | "@types/node": "^14", 59 | "@types/react": "^18", 60 | "@types/react-dom": "^18", 61 | "@types/testing-library__jest-dom": "^5.14.1", 62 | "react": "^18.0.0", 63 | "react-dom": "^18.0.0", 64 | "react-17": "npm:react@^17", 65 | "react-dom-17": "npm:react-dom@^17", 66 | "redux": "^4.1.1", 67 | "typescript": "^4", 68 | "@modern-js-reduck/scripts": "workspace:*" 69 | }, 70 | "modernSettings": {}, 71 | "sideEffects": false, 72 | "peerDependencies": { 73 | "@types/react": "^16.8 || ^17.0 || ^18.0", 74 | "@types/react-dom": "^16.8 || ^17.0 || ^18.0", 75 | "react": "^16.8 || ^17.0 || ^18.0", 76 | "react-dom": "^16.8 || ^17.0 || ^18.0" 77 | }, 78 | "peerDependenciesMeta": { 79 | "@types/react": { 80 | "optional": true 81 | }, 82 | "@types/react-dom": { 83 | "optional": true 84 | } 85 | }, 86 | "publishConfig": { 87 | "registry": "https://registry.npmjs.org/", 88 | "access": "public" 89 | }, 90 | "repository": "modern-js-dev/reduck" 91 | } 92 | -------------------------------------------------------------------------------- /packages/store/tests/subscribe.test.ts: -------------------------------------------------------------------------------- 1 | import { createStore, model } from '../src'; 2 | 3 | interface State { 4 | value: number; 5 | } 6 | 7 | const count1Model = model('count1').define({ 8 | state: { 9 | value: 1, 10 | }, 11 | actions: { 12 | add(state) { 13 | return { 14 | ...state, 15 | value: state.value + 1, 16 | }; 17 | }, 18 | }, 19 | }); 20 | 21 | const count2Model = model('count2').define({ 22 | state: { 23 | value: 1, 24 | }, 25 | computed: { 26 | sum: [ 27 | count1Model, 28 | (state, state2) => { 29 | // eslint-disable-next-line @typescript-eslint/restrict-plus-operands 30 | return state.value + state2.value; 31 | }, 32 | ], 33 | }, 34 | actions: { 35 | add1(state) { 36 | return { 37 | ...state, 38 | value: state.value + 1, 39 | }; 40 | }, 41 | }, 42 | }); 43 | 44 | describe('test subscribe', () => { 45 | test('subsribe should work for simple model', () => { 46 | const store = createStore(); 47 | 48 | const [, actions, subscribe] = store.use(count1Model); 49 | const fn = jest.fn(); 50 | 51 | subscribe(() => { 52 | fn(); 53 | }); 54 | 55 | actions.add(); 56 | 57 | expect(fn).toBeCalledTimes(1); 58 | }); 59 | 60 | test('subscribe should work for multiple model', () => { 61 | const store = createStore(); 62 | 63 | const [, actions, subscribe] = store.use([count1Model, count2Model]); 64 | 65 | const fn = jest.fn(); 66 | 67 | subscribe(() => { 68 | fn(); 69 | }); 70 | 71 | actions.add(); 72 | 73 | expect(fn).toBeCalledTimes(1); 74 | expect(store.getState()).toEqual({ 75 | count1: { 76 | value: 2, 77 | }, 78 | count2: { 79 | value: 1, 80 | }, 81 | }); 82 | 83 | actions.add1(); 84 | 85 | expect(fn).toBeCalledTimes(2); 86 | 87 | expect(store.getState()).toEqual({ 88 | count1: { 89 | value: 2, 90 | }, 91 | count2: { 92 | value: 2, 93 | }, 94 | }); 95 | }); 96 | 97 | test('subscribe should work for computed property depending on other models', () => { 98 | const store = createStore(); 99 | 100 | const [, action] = store.use(count1Model); 101 | const [state2, , subscribe] = store.use(count2Model); 102 | 103 | const fn = jest.fn(); 104 | subscribe(() => { 105 | fn(); 106 | }); 107 | 108 | expect(fn).toBeCalledTimes(0); 109 | action.add(); 110 | expect(fn).toBeCalledTimes(1); 111 | const [updateState2] = store.use(count2Model); 112 | // state from use is immutable 113 | expect(state2.sum).toBe(2); 114 | expect(updateState2.sum).toBe(3); 115 | }); 116 | 117 | test('unsubscribe should work', () => { 118 | const store = createStore(); 119 | 120 | const [, actions, subscribe] = store.use(count1Model); 121 | const fn = jest.fn(); 122 | 123 | const unsubscribe = subscribe(() => { 124 | fn(); 125 | }); 126 | 127 | actions.add(); 128 | 129 | expect(fn).toBeCalledTimes(1); 130 | 131 | unsubscribe(); 132 | 133 | expect(fn).toBeCalledTimes(1); 134 | }); 135 | }); 136 | -------------------------------------------------------------------------------- /packages/plugins/effects/tests/index.test.ts: -------------------------------------------------------------------------------- 1 | import { createStore, model } from '@modern-js-reduck/store'; 2 | import logger from 'redux-logger'; 3 | import { plugin } from '../src'; 4 | 5 | const todoModel = model('todo').define((_, { use }) => ({ 6 | state: { 7 | items: [], 8 | }, 9 | actions: { 10 | load: { 11 | fulfilled: (state: any, payload: any) => ({ 12 | ...state, 13 | items: payload, 14 | }), 15 | }, 16 | loadWithParams: { 17 | fulfilled: (state: any, payload: any) => ({ 18 | ...state, 19 | items: payload, 20 | }), 21 | }, 22 | }, 23 | effects: { 24 | async load() { 25 | return Promise.resolve(['1']); 26 | }, 27 | 28 | async loadWithParams(a: string) { 29 | return Promise.resolve([a]); 30 | }, 31 | 32 | async boolRetEffect() { 33 | return Promise.resolve(false); 34 | }, 35 | 36 | loadThunk() { 37 | const actions = use(todoModel)[1]; 38 | 39 | // cannot get `dispatch` and `getState` params, thunk effect not work correctlly 40 | // maybe we could only support promise effect? 41 | return () => { 42 | actions.load.fulfilled(['2']); 43 | }; 44 | }, 45 | 46 | voidEffect() { 47 | // do some effect thing, for example: localStorage.setItem('hello', 'reduck'); 48 | return 'success'; 49 | }, 50 | }, 51 | })); 52 | 53 | describe('reduck effects plugin', () => { 54 | test('promise middleware', async () => { 55 | const store = createStore({ 56 | plugins: [plugin], 57 | middlewares: [logger], 58 | }); 59 | 60 | const [, actions] = store.use(todoModel); 61 | 62 | const res = await actions.load(); 63 | 64 | expect(res).toEqual(['1']); 65 | expect(store.use(todoModel)[0]).toEqual({ items: ['1'] }); 66 | }); 67 | 68 | test('promise middleware params', async () => { 69 | const store = createStore({ 70 | plugins: [plugin], 71 | middlewares: [logger], 72 | }); 73 | 74 | const [, actions] = store.use(todoModel); 75 | 76 | await actions.loadWithParams('dddd'); 77 | 78 | expect(store.use(todoModel)[0]).toEqual({ items: ['dddd'] }); 79 | }); 80 | 81 | test('thunk middleware', () => { 82 | const store = createStore({ 83 | plugins: [plugin], 84 | middlewares: [logger], 85 | }); 86 | 87 | const [, actions] = store.use(todoModel); 88 | 89 | actions.loadThunk(); 90 | 91 | expect(store.use(todoModel)[0]).toEqual({ items: ['2'] }); 92 | }); 93 | 94 | test('void effect', () => { 95 | const store = createStore({ 96 | plugins: [plugin], 97 | middlewares: [logger], 98 | }); 99 | 100 | const [, actions] = store.use(todoModel); 101 | 102 | const res = actions.voidEffect(); 103 | 104 | expect(res).toEqual('success'); 105 | }); 106 | 107 | test('promise effect return bool', async () => { 108 | const store = createStore({ 109 | plugins: [plugin], 110 | middlewares: [logger], 111 | }); 112 | 113 | const [, actions] = store.use(todoModel); 114 | 115 | const res = await actions.boolRetEffect(); 116 | 117 | expect(res).toEqual(false); 118 | }); 119 | }); 120 | -------------------------------------------------------------------------------- /packages/plugins/xstate/src/types/override.ts: -------------------------------------------------------------------------------- 1 | import { Model } from '@modern-js-reduck/store/types'; 2 | import { Interpreter } from 'xstate/lib/interpreter'; 3 | import { 4 | StateMachine, 5 | EventObject, 6 | MachineConfig, 7 | MachineOptions, 8 | } from 'xstate/lib/types'; 9 | 10 | /** 11 | * override types of `reduck/core`'s states, actions 12 | */ 13 | declare module '@modern-js-reduck/store' { 14 | // Add `machine` type when use model({machine}). 15 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 16 | interface ModelDesc { 17 | machine?: 18 | | StateMachine< 19 | MDO['machine'] extends { context: any } 20 | ? MDO['machine']['context'] 21 | : any, 22 | any, 23 | MDO['machine'] extends { event: any } ? MDO['machine']['event'] : any 24 | > 25 | | MachineConfig< 26 | MDO['machine'] extends { context: any } 27 | ? MDO['machine']['context'] 28 | : any, 29 | any, 30 | MDO['machine'] extends { event: any } ? MDO['machine']['event'] : any 31 | >; 32 | machineOptions?: Partial< 33 | MachineOptions< 34 | MDO['machine'] extends { context: any } 35 | ? MDO['machine']['context'] 36 | : any, 37 | MDO['machine'] extends { event: any } ? MDO['machine']['event'] : any 38 | > 39 | >; 40 | } 41 | 42 | interface ModelDescOptions { 43 | machine?: { 44 | context?: any; 45 | event?: EventObject; 46 | }; 47 | } 48 | 49 | interface GetState { 50 | machineState: { 51 | /** 52 | * choose context, meta, value of Interpreter State and whole State 53 | */ 54 | machine: M['_']['machine'] extends StateMachine< 55 | infer TContext, 56 | infer TStateSchema, 57 | infer TEvent 58 | > // analyse StateMachine and MachineConfig 59 | ? Pick< 60 | Interpreter['state'], 61 | 'context' | 'meta' | 'value' 62 | > & { 63 | state: Interpreter['state']; 64 | } 65 | : M['_']['machine'] extends MachineConfig< 66 | infer TContext, 67 | any, 68 | infer TEvent 69 | > 70 | ? Pick< 71 | Interpreter['state'], 72 | 'context' | 'meta' | 'value' 73 | > & { 74 | state: Interpreter['state']; 75 | } 76 | : StateMachine; 77 | }; 78 | } 79 | 80 | interface GetActions { 81 | machineActions: { 82 | /** machine send function */ 83 | send: M['_']['machine'] extends StateMachine< 84 | infer TContext, 85 | infer TStateSchema, 86 | infer TEvent 87 | > // analyse StateMachine and MachineConfig 88 | ? Interpreter['send'] 89 | : M['_']['machine'] extends MachineConfig< 90 | infer TContext, 91 | any, 92 | infer TEvent 93 | > 94 | ? Interpreter['send'] 95 | : (event: any) => void; 96 | }; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /packages/react/src/batchManager.ts: -------------------------------------------------------------------------------- 1 | import { Store, Model, utils } from '@modern-js-reduck/store'; 2 | import { unstable_batchedUpdates } from 'react-dom'; 3 | 4 | const combineSubscribe = ( 5 | store: Store, 6 | subscribes: ((handler: () => void) => () => void)[], 7 | ) => { 8 | let changed = false; 9 | const handlers = new Set(); 10 | 11 | return (handler: () => void) => { 12 | handlers.add(handler); 13 | 14 | const disposer: any[] = []; 15 | 16 | subscribes.forEach(subscribe => { 17 | disposer.push( 18 | subscribe(() => { 19 | changed = true; 20 | }), 21 | ); 22 | }); 23 | 24 | const unsubscribeStore = store.subscribe(() => { 25 | if (changed) { 26 | handlers.forEach(h => h()); 27 | } 28 | 29 | changed = false; 30 | }); 31 | 32 | return () => { 33 | unsubscribeStore(); 34 | disposer.forEach(dispose => dispose()); 35 | }; 36 | }; 37 | }; 38 | 39 | const createBatchManager = (store: Store) => { 40 | // Models are in using now 41 | const usingModelsMap = new Map(); 42 | 43 | let unsubscribe: undefined | (() => void); 44 | const updateList: (() => void)[] = []; 45 | 46 | // listen to models in using 47 | const setupSubscribe = () => { 48 | if (typeof unsubscribe === 'function') { 49 | unsubscribe(); 50 | } 51 | 52 | const modelSet = new Set(); 53 | 54 | for (const [model, count] of usingModelsMap) { 55 | if (count !== 0) { 56 | modelSet.add(model); 57 | } 58 | } 59 | 60 | const subscribe = combineSubscribe( 61 | store, 62 | [...modelSet].map(m => store.use(m)[2]), 63 | ); 64 | 65 | unsubscribe = subscribe(() => { 66 | unstable_batchedUpdates(() => { 67 | let update: (() => void) | undefined = updateList.shift(); 68 | 69 | while (update) { 70 | update(); 71 | 72 | update = updateList.shift(); 73 | } 74 | }); 75 | }); 76 | }; 77 | 78 | const changeModels = (action: 'remove' | 'add', ...models: Model[]) => { 79 | models.forEach(model => { 80 | if (!utils.isModel(model)) { 81 | return; 82 | } 83 | 84 | let usingCount = usingModelsMap.get(model); 85 | 86 | if (action === 'add') { 87 | usingModelsMap.set(model, (usingCount || 0) + 1); 88 | } else if (action === 'remove') { 89 | if (usingCount) { 90 | usingCount -= 1; 91 | 92 | if (usingCount === 0) { 93 | usingModelsMap.delete(model); 94 | } else { 95 | usingModelsMap.set(model, usingCount); 96 | } 97 | } 98 | } 99 | }); 100 | 101 | setupSubscribe(); 102 | }; 103 | 104 | // add models to listen 105 | const addModels = (...args: Model[]) => changeModels('add', ...args); 106 | 107 | // remove models to listen 108 | const removeModels = (...args: Model[]) => changeModels('remove', ...args); 109 | 110 | const pushUpdate = (update: () => void) => { 111 | updateList.push(update); 112 | }; 113 | 114 | return { 115 | addModels, 116 | removeModels, 117 | pushUpdate, 118 | }; 119 | }; 120 | 121 | export { createBatchManager }; 122 | -------------------------------------------------------------------------------- /packages/store/src/__tsd__/model.tsd.ts: -------------------------------------------------------------------------------- 1 | // import { expectType, expectAssignable } from 'tsd'; 2 | // import { useModel } from '@modern-js/runtime/model'; 3 | // import { model } from '..'; 4 | // import { ReduxAction } from '@/types'; 5 | 6 | // type StateManual = { count: number; name: 'a' | 'b' }; 7 | // const counterManual = model('counter').define({ 8 | // state: { count: 1 }, 9 | // actions: { 10 | // add(state, n: number) { 11 | // expectType(state); 12 | // return { count: state.count + n, name: 'a' }; 13 | // }, 14 | // empty(state) { 15 | // expectType(state); 16 | // }, 17 | // test: { 18 | // a(s) { 19 | // return s; 20 | // }, 21 | // }, 22 | // }, 23 | // }); 24 | 25 | // type StateInfer = { count: number; name: string }; 26 | // const counterInfer = model('counter').define({ 27 | // state: { count: 1, name: 'a' }, 28 | // actions: { 29 | // add(state, n: number) { 30 | // expectType(state); 31 | // return { count: state.count + n, name: 'b' }; 32 | // }, 33 | // empty(state) { 34 | // expectType(state); 35 | // }, 36 | // test: { 37 | // a(state) { 38 | // expectType(state); 39 | // return state; 40 | // }, 41 | // }, 42 | // }, 43 | // }); 44 | 45 | // describe('action and state manually type', () => { 46 | // expectType(counterManual.name); 47 | 48 | // expectAssignable<(s: StateManual, n: number) => StateManual>( 49 | // counterManual._.actions.add, 50 | // ); 51 | // expectType<(s: StateManual) => void>(counterManual._.actions.empty); 52 | // const [state, actions] = useModel(counterManual); 53 | // expectType(state); 54 | // expectType<(n: number) => ReduxAction>(actions.add); 55 | // }); 56 | 57 | // describe('action and state auto infer', () => { 58 | // expectType(counterInfer.name); 59 | // expectType<(s: StateInfer, n: number) => StateInfer>( 60 | // counterInfer._.actions.add, 61 | // ); 62 | // expectType<(s: StateInfer) => void>(counterInfer._.actions.empty); 63 | // const [state, actions] = useModel(counterInfer); 64 | // expectType(state); 65 | // expectType<(n: number) => ReduxAction>(actions.add); 66 | // }); 67 | 68 | // describe('action and state union type', () => { 69 | // const [state] = useModel(counterManual); 70 | // expectType<'a' | 'b'>(state.name); 71 | // }); 72 | 73 | // describe('action and state function Initial', () => { 74 | // const counter = model('counter').define(() => ({ 75 | // state: { c: 1 }, 76 | // actions: { 77 | // add(state, payload: number) { 78 | // expectType(state.c); 79 | // return { c: state.c + payload }; 80 | // }, 81 | // test: { 82 | // a(s) { 83 | // return s; 84 | // }, 85 | // b(s, p: number) { 86 | // return { ...s, c: s.c + p }; 87 | // }, 88 | // }, 89 | // }, 90 | // })); 91 | // const [state, actions] = useModel(counter); 92 | // expectType<(s: { c: number }, n: number) => { c: number }>( 93 | // counter._.actions.add, 94 | // ); 95 | // expectType(state.c); 96 | // expectType<() => ReduxAction>(actions.test.a); 97 | // expectType<(n: number) => ReduxAction>(actions.test.b); 98 | // }); 99 | -------------------------------------------------------------------------------- /packages/store/src/model/model.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ReduckContext, 3 | Context, 4 | ModelDesc, 5 | OnMountHook, 6 | Actions, 7 | Computed, 8 | } from '@/types'; 9 | import { initializerSymbol } from '@/utils/misc'; 10 | 11 | type ModelDescWithoutName = Omit, 'name'>; 12 | 13 | type ModelInitialParams = [ 14 | context: ReduckContext, 15 | hook: { 16 | use: Context['apis']['useModel']; 17 | onMount: OnMountHook; 18 | }, 19 | ]; 20 | 21 | type ModelInitial = (...args: ModelInitialParams) => ModelDescWithoutName; 22 | 23 | type ExtDesc = { 24 | actions?: Actions; 25 | computed?: Computed; 26 | }; 27 | 28 | type ModelFn = ( 29 | name: string, 30 | ) => { 31 | define: (< 32 | S, 33 | M extends ExtDesc & { state: S } = ExtDesc & { 34 | state: S; 35 | }, 36 | Resp = { 37 | _name: string; 38 | _: Omit & { state: State extends void ? S : State }; 39 | }, 40 | >( 41 | c: (...args: ModelInitialParams) => M & { state: S }, 42 | ) => Resp & 43 | ((ns: string) => Resp & ((ns: string) => Resp)) & { 44 | state: State extends void ? S : State; 45 | }) & 46 | (< 47 | S, 48 | M extends ExtDesc & { state: S } = ExtDesc & { 49 | state: S; 50 | }, 51 | Resp = { 52 | _name: string; 53 | _: Omit & { state: State extends void ? S : State }; 54 | }, 55 | >( 56 | c: M & { state: S }, 57 | ) => Resp & { 58 | (ns: string): Resp & ((ns: string) => Resp); 59 | _name: string; 60 | _: Omit & { state: State extends void ? S : State }; 61 | }); 62 | }; 63 | const model: ModelFn = name => ({ 64 | define(modelDesc) { 65 | let modelInitializer: ModelInitial; 66 | 67 | if (typeof modelDesc === 'function') { 68 | modelInitializer = modelDesc; 69 | } else { 70 | modelInitializer = () => modelDesc; 71 | } 72 | 73 | const modelCache = new Map>(); 74 | 75 | const createResponse = (initializer: ModelInitial) => { 76 | /** 77 | * Use to change model namespace when mount model 78 | * @example 79 | * use(someModel('hello')) 80 | */ 81 | const response = (namespace: string) => { 82 | const cachedModel = modelCache.get(namespace); 83 | 84 | if (cachedModel) { 85 | return cachedModel; 86 | } 87 | 88 | const clonedModelInitializer = (...args: [Context, any]) => { 89 | const result = initializer(...args); 90 | 91 | return result; 92 | }; 93 | 94 | const modelInstance = createResponse(clonedModelInitializer); 95 | modelCache.set(namespace, modelInstance); 96 | modelInstance._name = namespace || name; 97 | 98 | return modelInstance; 99 | }; 100 | 101 | response._name = name; 102 | 103 | Object.defineProperty(response, initializerSymbol, { 104 | configurable: false, 105 | enumerable: false, 106 | value: initializer, 107 | }); 108 | 109 | return response as any; 110 | }; 111 | 112 | return createResponse(modelInitializer); 113 | }, 114 | }); 115 | 116 | export default model; 117 | -------------------------------------------------------------------------------- /packages/store/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @modern-js-reduck/store 2 | 3 | ## 1.1.13 4 | 5 | ### Patch Changes 6 | 7 | - d5d3a2c: fix: should run prepare before publish 8 | 9 | ## 1.1.12 10 | 11 | ## 1.1.11 12 | 13 | ### Patch Changes 14 | 15 | - 1b6f119: chore: update build tools and build config, reduck product size. 16 | chore: 更新构建工具和配置,减少产物体积 17 | - 3979805: fix: add node to package.json exports 18 | - 96afcec: fix: correct exports 19 | 20 | ## 1.1.10 21 | 22 | ### Patch Changes 23 | 24 | - 8b81fc9: fix: fix initial state missing 25 | 26 | ## 1.1.9 27 | 28 | ### Patch Changes 29 | 30 | - d5d5f8b: chore: add subpath exports "types" in @modern-js-reduck/store 31 | chore: 添加 @modern-js-reduck/store 的 types 子路径导出 32 | 33 | ## 1.1.8 34 | 35 | ## 1.1.7 36 | 37 | ### Patch Changes 38 | 39 | - 856a79d: fix: add the types field in package.json 40 | fix: 在 package.json 中添加 types 字段 41 | - 1b6773e: feat: remove node entry in all pkg package.json 42 | 43 | ## 1.1.6 44 | 45 | ### Patch Changes 46 | 47 | - 4a4e8925: fix: fix computed type 48 | 49 | ## 1.1.5 50 | 51 | ## 1.1.4 52 | 53 | ### Patch Changes 54 | 55 | - chore: modify export types 56 | 57 | ## 1.1.3 58 | 59 | ## 1.1.2 60 | 61 | ### Patch Changes 62 | 63 | - feat: export util functions from main entry 64 | 65 | ## 1.1.1 66 | 67 | ### Patch Changes 68 | 69 | - chore: release v1.1.1 70 | 71 | ## 1.1.0 72 | 73 | ## 1.0.6 74 | 75 | ### Patch Changes 76 | 77 | - feat: uniform package version 78 | 79 | ## 1.0.5 80 | 81 | ### Patch Changes 82 | 83 | - ac05b04: fix: typos 84 | - 30b1167: fix: fix action return value 85 | - ed80d6e: build: build modern js dist 86 | - b33f42a: feat: computed properties 87 | - 9c09d2a: fix: fix `connect` missing component props 88 | - 51721bc: feat: `handleEffect` API 89 | 90 | ## 1.0.4 91 | 92 | ### Patch Changes 93 | 94 | - 2b7d19f: fix: merge actions bug when use mutiple models 95 | 96 | ## 1.0.1 97 | 98 | ### Patch Changes 99 | 100 | - fix: fix git url 101 | 102 | ## 1.0.0 103 | 104 | ### Minor Changes 105 | 106 | - feat: ready for publish 1.0.0 107 | 108 | ### Patch Changes 109 | 110 | - a7bd932: release: rc.7 111 | - 26d5144: fix(type): actions can return void 112 | - 9452589: fix: ts type 113 | - a7bd932: fix: use model State type 114 | - fd81b6b: feat: useLocalModel support 115 | - feat: ready for publish 1.0.0 116 | - a7bd932: fix: model type 117 | - a7bd932: fix: model when State passed use State 118 | 119 | ## 1.0.0-rc.10 120 | 121 | ### Patch Changes 122 | 123 | - a7bd932: release: rc.7 124 | - 26d5144: fix(type): actions can return void 125 | - 9452589: fix: ts type 126 | - a7bd932: fix: use model State type 127 | - feat: useLocalModel support 128 | - a7bd932: fix: model type 129 | - a7bd932: fix: model when State passed use State 130 | 131 | ## 1.0.0-rc.9 132 | 133 | ### Patch Changes 134 | 135 | - ac4124f: release: rc.7 136 | - 26d5144: fix(type): actions can return void 137 | - 9452589: fix: ts type 138 | - 1085d2d: fix: use model State type 139 | - fix: model type 140 | - 1085d2d: fix: model when State passed use State 141 | 142 | ## 1.0.0-rc.8 143 | 144 | ### Patch Changes 145 | 146 | - release: rc.7 147 | - 26d5144: fix(type): actions can return void 148 | - 9452589: fix: ts type 149 | - 1085d2d: fix: use model State type 150 | - 1085d2d: fix: model when State passed use State 151 | 152 | ## 1.0.0-next.7 153 | 154 | ### Patch Changes 155 | 156 | - 26d5144: fix(type): actions can return void 157 | - 9452589: fix: ts type 158 | - fix: use model State type 159 | - fix: model when State passed use State 160 | 161 | ## 1.0.0-rc.6 162 | 163 | ### Patch Changes 164 | 165 | - fix(type): actions can return void 166 | - 9452589: fix: ts type 167 | 168 | ## 1.0.0-rc.5 169 | 170 | ### Patch Changes 171 | 172 | - fix: ts type 173 | -------------------------------------------------------------------------------- /packages/plugins/effects/src/plugin.ts: -------------------------------------------------------------------------------- 1 | import { createPlugin } from '@modern-js-reduck/store'; 2 | import { Model } from '@modern-js-reduck/store/types'; 3 | import { createPromise } from 'redux-promise-middleware'; 4 | 5 | type AsyncEffect = (...args: any[]) => Promise; 6 | type VoidEffect = (...args: any[]) => void; 7 | type ThunkEffect = (...args: any[]) => () => any; 8 | 9 | interface Effects { 10 | [key: string]: AsyncEffect | VoidEffect | ThunkEffect | Effects; 11 | } 12 | 13 | declare module '@modern-js-reduck/store' { 14 | // Add `effects` type when use model({effects}). 15 | interface ModelDesc { 16 | effects?: Effects; 17 | } 18 | 19 | // Overload GetActions interface to add actions type to useModel's return 20 | interface GetActions { 21 | effectsActions: M['_']['effects'] & { 22 | [key in keyof M['_']['actions']]: unknown; 23 | }; 24 | } 25 | } 26 | 27 | const isReduxPromiseFulfilled = (data: any) => { 28 | return ( 29 | typeof data === 'object' && 30 | data.hasOwnProperty('action') && 31 | data.hasOwnProperty('value') 32 | ); 33 | }; 34 | 35 | const isPromise = (value: any) => { 36 | if (value !== null && typeof value === 'object') { 37 | return value && typeof value.then === 'function'; 38 | } 39 | 40 | return false; 41 | }; 42 | 43 | /** 44 | * Generate dispatch action from effects definitions. 45 | */ 46 | const createDispatchActionsFromEffects = ( 47 | store: any, 48 | name: string, 49 | effects: Effects, 50 | setDispatchAction: (path: string[], action: any) => void, 51 | ) => { 52 | const path = [name]; 53 | 54 | const traverse = (_effects: Effects[string]) => { 55 | if (typeof _effects === 'function') { 56 | const type = path.join('/').toUpperCase(); 57 | 58 | setDispatchAction(path.slice(), (...args: any[]) => { 59 | const value = (_effects as (..._args: any[]) => any)(...args); 60 | const dispatch = (payload: any) => 61 | store.dispatch({ 62 | type, 63 | payload, 64 | }); 65 | 66 | // Handled by promise middleware or redux thunk 67 | // Otherwise, do not dispatch action, just exec the effect function. 68 | if (isPromise(value) || typeof value === 'function') { 69 | const res = dispatch(value); 70 | if (isPromise(res)) { 71 | // parse redux-promise result, return orginal value of the effect 72 | return res.then((data: any) => 73 | isReduxPromiseFulfilled(data) ? data.value : data, 74 | ); 75 | } 76 | return res; 77 | } 78 | 79 | return value; 80 | }); 81 | } else { 82 | Object.keys(_effects).forEach(key => { 83 | path.push(key); 84 | traverse(_effects[key]); 85 | path.pop(); 86 | }); 87 | } 88 | }; 89 | 90 | traverse(effects); 91 | }; 92 | 93 | const plugin = createPlugin(context => ({ 94 | config(storeConfig) { 95 | return { 96 | ...storeConfig, 97 | middlewares: [ 98 | createPromise({ promiseTypeDelimiter: '/' }), 99 | // middlewares from config are at the end 100 | ...(storeConfig.middlewares || []), 101 | ], 102 | }; 103 | }, 104 | modelMount({ modelDesc, mountedModel }, { setDispatchAction }) { 105 | const { effects } = modelDesc; 106 | 107 | if (!effects) { 108 | return { 109 | modelDesc, 110 | mountedModel, 111 | }; 112 | } 113 | 114 | createDispatchActionsFromEffects( 115 | context.store, 116 | modelDesc.name, 117 | modelDesc.effects!, 118 | setDispatchAction, 119 | ); 120 | 121 | return { 122 | modelDesc, 123 | mountedModel, 124 | } as any; 125 | }, 126 | })); 127 | 128 | export default plugin; 129 | -------------------------------------------------------------------------------- /packages/store/tests/useModel.test.ts: -------------------------------------------------------------------------------- 1 | import { createStore, model } from '../src'; 2 | 3 | const count1Model = model<{ value: number }>('count1').define(() => ({ 4 | state: { 5 | value: 1, 6 | }, 7 | actions: { 8 | add(state) { 9 | return { 10 | ...state, 11 | value: state.value + 1, 12 | }; 13 | }, 14 | }, 15 | })); 16 | 17 | const count2Model = model<{ value: number }>('count2').define((_, { use }) => { 18 | use(count1Model); 19 | 20 | return { 21 | state: { 22 | value: 1, 23 | }, 24 | actions: { 25 | addCount1Value(state) { 26 | const [{ value: count1Value }] = use(count1Model); 27 | 28 | return { 29 | ...state, 30 | value: state.value + count1Value, 31 | }; 32 | }, 33 | }, 34 | }; 35 | }); 36 | 37 | const count3Model = model<{ value: number }>('count3').define(() => { 38 | return { 39 | state: { 40 | value: 1, 41 | }, 42 | computed: { 43 | addOne: state => state.value + 1, 44 | }, 45 | }; 46 | }); 47 | 48 | describe('test useModel', () => { 49 | test('actions should work', () => { 50 | const store = createStore(); 51 | 52 | const [, actions] = store.use(count2Model); 53 | const [, count1Actions] = store.use(count1Model); 54 | 55 | expect(store.getState()).toEqual({ 56 | count1: { value: 1 }, 57 | count2: { value: 1 }, 58 | }); 59 | 60 | actions.addCount1Value(); 61 | 62 | expect(store.getState()).toEqual({ 63 | count1: { value: 1 }, 64 | count2: { value: 2 }, 65 | }); 66 | 67 | count1Actions.add(); 68 | 69 | expect(store.getState()).toEqual({ 70 | count1: { value: 2 }, 71 | count2: { value: 2 }, 72 | }); 73 | 74 | actions.addCount1Value(); 75 | 76 | expect(store.getState()).toEqual({ 77 | count1: { value: 2 }, 78 | count2: { value: 4 }, 79 | }); 80 | }); 81 | 82 | test('state reference is same, when passing same single model without computed properties', () => { 83 | const store = createStore(); 84 | 85 | const [count1State] = store.use(count1Model); 86 | const [newCount1State] = store.use(count1Model); 87 | expect(count1State).toBe(newCount1State); 88 | }); 89 | 90 | test('state reference changes, when passing multiple models', () => { 91 | const store = createStore(); 92 | 93 | const [state] = store.use([count1Model, count2Model]); 94 | const [state2] = store.use([count1Model, count2Model]); 95 | expect(state).not.toBe(state2); 96 | expect(state).toEqual(state2); 97 | }); 98 | 99 | test('state reference changes, when passing model with computed properties', () => { 100 | const store = createStore(); 101 | 102 | const [count3State] = store.use(count3Model); 103 | const [newCount3State] = store.use(count3Model); 104 | expect(count3State).not.toBe(newCount3State); 105 | expect(count3State).toEqual(newCount3State); 106 | }); 107 | 108 | test('use models with same name would be ignored', () => { 109 | const store = createStore(); 110 | const countModel = model('count1').define({ 111 | state: 2, 112 | }); 113 | 114 | const [state] = store.use(count1Model, countModel); 115 | 116 | expect(store.getState()).toEqual({ count1: { value: 1 } }); 117 | expect(state).toEqual({ value: 1 }); 118 | }); 119 | 120 | test('use self in model will get error', () => { 121 | const test = model('name').define((_, { use }) => { 122 | use(test); 123 | 124 | return { 125 | state: 1, 126 | }; 127 | }); 128 | 129 | expect(() => createStore().use(test)).toThrow(Error); 130 | }); 131 | 132 | test('use multiple model with primitive state get error', () => { 133 | const test = model('name').define({ 134 | state: 1, 135 | }); 136 | 137 | expect(() => createStore().use(test, count1Model)).toThrow(Error); 138 | }); 139 | }); 140 | -------------------------------------------------------------------------------- /packages/store/src/store/context.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers, Reducer, Store } from 'redux'; 2 | import { createUseModel } from '@/model/useModel'; 3 | import { Context, Model, MountedModel } from '@/types'; 4 | import { createPluginCore } from '@/plugin'; 5 | import { createSubscribe, GetUnsubscribe } from '@/model/subscribe'; 6 | 7 | const dummyReducer = '__REDUCK_DUMMY_REDUCER__'; 8 | 9 | const createContext = (store: Store) => { 10 | const reducers: Record = {}; 11 | const mountedModels = new Map(); 12 | const subscriptions = new Map>(); 13 | const mountingModelNames = new Set(); 14 | let lastState: any; 15 | 16 | /** 17 | * Dynamic add reducer to store 18 | */ 19 | const addReducers: Context['apis']['addReducers'] = _reducers => { 20 | if (!lastState) { 21 | store.subscribe(() => { 22 | lastState = store.getState(); 23 | }); 24 | } 25 | 26 | // remove dummy reducer we may add when unmountting a model 27 | if (reducers[dummyReducer]) { 28 | delete reducers[dummyReducer]; 29 | } 30 | 31 | Object.assign(reducers, _reducers); 32 | Object.keys(_reducers).forEach(key => mountingModelNames.delete(key)); 33 | store.replaceReducer(combineReducers(reducers)); 34 | }; 35 | 36 | /** 37 | * Add to exported models 38 | */ 39 | const addModel: Context['apis']['addModel'] = (model, mountedModel) => { 40 | mountedModels.set(model._name, mountedModel); 41 | subscriptions.set(model._name, createSubscribe(context, model)); 42 | }; 43 | 44 | const getModel: Context['apis']['getModel'] = model => { 45 | const mountedModel = getModelByName(model._name); 46 | 47 | if (!mountedModel) { 48 | return null; 49 | } 50 | 51 | return { 52 | name: mountedModel.name, 53 | state: lastState[mountedModel.name], 54 | actions: mountedModel.actions, 55 | modelDesc: mountedModel.modelDesc, 56 | } as any; 57 | }; 58 | 59 | const getModelByName = (name: string) => { 60 | let model = null; 61 | 62 | for (const [, mountedModel] of mountedModels) { 63 | if (mountedModel.name === name) { 64 | model = mountedModel; 65 | break; 66 | } 67 | } 68 | 69 | return model; 70 | }; 71 | 72 | // Get function to subscribe model 73 | const getModelSubscribe: Context['apis']['getModelSubscribe'] = ( 74 | model: Model, 75 | ) => subscriptions.get(model._name); 76 | 77 | const mountingModel = (name: string) => { 78 | if (mountingModelNames.has(name)) { 79 | throw new Error( 80 | `You are mounting the model: ${name} which is already in mounting process`, 81 | ); 82 | } 83 | 84 | mountingModelNames.add(name); 85 | }; 86 | 87 | const unmountModel = (model: Model) => { 88 | if (!getModel(model)) { 89 | return; 90 | } 91 | 92 | const subscription = subscriptions.get(model._name); 93 | subscription[GetUnsubscribe]()?.(); 94 | 95 | mountedModels.delete(model._name); 96 | subscriptions.delete(model._name); 97 | 98 | delete lastState[model._name]; 99 | delete reducers[model._name]; 100 | 101 | // redux cannot accept empty reducers, so we fake one. 102 | if (Object.keys(reducers).length === 0) { 103 | reducers[dummyReducer] = () => { 104 | return null; 105 | }; 106 | } 107 | 108 | store.replaceReducer(combineReducers(reducers)); 109 | }; 110 | 111 | const pluginCore = createPluginCore({ store }); 112 | 113 | /** 114 | * Add all to context 115 | */ 116 | const context = { 117 | store, 118 | apis: { 119 | addReducers, 120 | addModel, 121 | getModel, 122 | getModelSubscribe, 123 | mountingModel, 124 | unmountModel, 125 | }, 126 | pluginCore, 127 | } as Context; 128 | 129 | context.apis.useModel = createUseModel(context); 130 | 131 | return context; 132 | }; 133 | 134 | export { createContext }; 135 | -------------------------------------------------------------------------------- /packages/react/tests/batch.test.tsx: -------------------------------------------------------------------------------- 1 | import { model } from '@modern-js-reduck/store'; 2 | import { render, fireEvent, act, screen } from '@testing-library/react'; 3 | import '@testing-library/jest-dom/extend-expect'; 4 | import { useModel, Provider } from '../src'; 5 | 6 | const countModel = model('name').define({ 7 | state: { 8 | value: 1, 9 | value1: 1, 10 | }, 11 | actions: { 12 | addValue(state) { 13 | return { 14 | ...state, 15 | value: state.value + 1, 16 | }; 17 | }, 18 | addValue1(state) { 19 | return { 20 | ...state, 21 | value1: state.value1 + 1, 22 | }; 23 | }, 24 | }, 25 | }); 26 | 27 | describe('test batch', () => { 28 | test('once store change, update should batch in one render', async () => { 29 | let renderCount = 0; 30 | 31 | function SubApp() { 32 | renderCount += 1; 33 | const [{ value1 }, { addValue1 }] = useModel(countModel); 34 | 35 | return ( 36 | <> 37 |
    value1:{value1}
    38 |
    { 40 | // React 17 not auto batch updates in setTimeout. 41 | // FIXME: In test environment, React 17 seems to always batch updates. 42 | setTimeout(() => { 43 | act(() => { 44 | addValue1(); 45 | }); 46 | }, 10); 47 | }} 48 | > 49 | addValue1 50 |
    51 | 52 | ); 53 | } 54 | 55 | function App() { 56 | const [{ value }, { addValue }] = useModel(countModel); 57 | return ( 58 |
    59 |
    value:{value}
    60 |
    addValue()}>addValue
    61 | 62 |
    63 | ); 64 | } 65 | 66 | const result = render( 67 | 68 | 69 | , 70 | ); 71 | 72 | expect(renderCount).toBe(1); 73 | fireEvent.click(result.getByText('addValue')); 74 | expect(renderCount).toBe(2); 75 | expect(result.getByText('value:2')).toBeInTheDocument(); 76 | 77 | fireEvent.click(result.getByText('addValue1')); 78 | 79 | await screen.findByText('value1:2'); 80 | expect(renderCount).toBe(3); 81 | }); 82 | 83 | test('state selector should reduce the rerender times', () => { 84 | let parentRenderCount = 0; 85 | let childRenderCount = 0; 86 | 87 | function SubApp() { 88 | childRenderCount += 1; 89 | 90 | const [{ value1 }, { addValue1 }] = useModel(countModel, state => ({ 91 | value1: state.value1, 92 | })); 93 | 94 | return ( 95 | <> 96 |
    value1:{value1}
    97 |
    { 99 | addValue1(); 100 | }} 101 | > 102 | addValue1 103 |
    104 | 105 | ); 106 | } 107 | 108 | function App() { 109 | parentRenderCount += 1; 110 | const [{ value }, { addValue }] = useModel(countModel, state => ({ 111 | value: state.value, 112 | })); 113 | 114 | return ( 115 |
    116 |
    value:{value}
    117 |
    addValue()}>addValue
    118 | 119 |
    120 | ); 121 | } 122 | 123 | const result = render( 124 | 125 | 126 | , 127 | ); 128 | 129 | expect(parentRenderCount).toBe(1); 130 | expect(childRenderCount).toBe(1); 131 | 132 | fireEvent.click(result.getByText('addValue')); 133 | expect(parentRenderCount).toBe(2); 134 | expect(childRenderCount).toBe(2); 135 | 136 | fireEvent.click(result.getByText('addValue1')); 137 | expect(parentRenderCount).toBe(2); 138 | expect(childRenderCount).toBe(3); 139 | 140 | expect(result.getByText('value:2')).toBeInTheDocument(); 141 | expect(result.getByText('value1:2')).toBeInTheDocument(); 142 | }); 143 | }); 144 | -------------------------------------------------------------------------------- /packages/plugins/xstate/src/plugin.ts: -------------------------------------------------------------------------------- 1 | import { createPlugin } from '@modern-js-reduck/store'; 2 | import type { EventData, SCXML, SingleOrArray } from 'xstate'; 3 | import { isMachineModel } from './check'; 4 | import { ActionTypes, MachineActionPrefix, MachineStateSymbol } from './const'; 5 | import { mergeMachineMap } from './map'; 6 | import type { MachineMap } from './types'; 7 | import { getEventType } from './utils'; 8 | 9 | const machineMap: MachineMap = {}; 10 | 11 | const plugin = createPlugin(context => ({ 12 | /** modify modelDesc */ 13 | prepareModelDesc(modelDesc) { 14 | // don't change modelDesc if no 'machine' in modelDesc 15 | if (!isMachineModel(modelDesc)) { 16 | return modelDesc; 17 | } 18 | 19 | // guarantee same id between machine and modelDesc 20 | if (modelDesc.machine.id && modelDesc.machine.id !== modelDesc.name) { 21 | console.warn( 22 | 'Unexpected machine id is not consistent with model name, it would be changed to model name', 23 | ); 24 | } 25 | 26 | modelDesc.machine.id = modelDesc.name; 27 | 28 | function setAction(state: any) { 29 | return { 30 | ...state, 31 | [MachineStateSymbol]: '', 32 | }; 33 | } 34 | 35 | return { 36 | ...modelDesc, 37 | state: { 38 | ...modelDesc.state, 39 | }, 40 | actions: { 41 | ...modelDesc.actions, 42 | [MachineActionPrefix]: { 43 | [ActionTypes.SET]: setAction, 44 | }, 45 | }, 46 | }; 47 | }, 48 | modelMount({ modelDesc, mountedModel }) { 49 | if (!isMachineModel(modelDesc)) { 50 | return { modelDesc, mountedModel }; 51 | } 52 | 53 | mergeMachineMap(machineMap, modelDesc); 54 | 55 | return { modelDesc, mountedModel } as any; 56 | }, 57 | useModel(bypassParams, { mountedModels }) { 58 | if (mountedModels.length !== 1) { 59 | const hasMachineModel = mountedModels.some(mountedModel => 60 | isMachineModel(mountedModel.modelDesc), 61 | ); 62 | 63 | if (hasMachineModel) { 64 | throw new Error( 65 | 'model.machine not support array parameter for useModel currently.', 66 | ); 67 | } 68 | 69 | return bypassParams; 70 | } 71 | 72 | const { modelDesc } = mountedModels[0]; 73 | 74 | // don't change modelDesc if no 'machine' in modelDesc 75 | if (!isMachineModel(modelDesc)) { 76 | return bypassParams; 77 | } 78 | 79 | const machine = machineMap[modelDesc.name]; 80 | 81 | if (!machine) { 82 | throw new Error( 83 | `Unexpected no machine service for model <${modelDesc.name}>`, 84 | ); 85 | } 86 | 87 | const machineState = { 88 | machine: { 89 | context: machine.service.state.context, 90 | meta: machine.service.state.meta, 91 | value: machine.service.state.value, 92 | state: machine.service.state, 93 | }, 94 | }; 95 | 96 | delete bypassParams.state[MachineStateSymbol]; 97 | 98 | const state = { 99 | ...bypassParams.state, 100 | ...machineState, 101 | }; 102 | 103 | const sendAction = ( 104 | event: SingleOrArray | SCXML.Event, 105 | payload?: EventData | undefined, 106 | ) => { 107 | const sendType = getEventType(modelDesc.name, ActionTypes.SEND); 108 | const setType = getEventType(modelDesc.name, ActionTypes.SET); 109 | 110 | context.store.dispatch({ 111 | type: sendType, 112 | payload: { event, payload }, 113 | }); 114 | const results = machine.service.send(event, payload); 115 | 116 | context.store.dispatch({ type: setType, payload: results }); 117 | return results; 118 | }; 119 | 120 | // remove internal machine action 121 | delete bypassParams.actions[MachineActionPrefix]; 122 | 123 | const actions: Record = { 124 | ...bypassParams.actions, 125 | send: sendAction, 126 | }; 127 | 128 | return { 129 | ...bypassParams, 130 | state, 131 | actions, 132 | }; 133 | }, 134 | })); 135 | 136 | export default plugin; 137 | -------------------------------------------------------------------------------- /packages/store/tests/model.test.ts: -------------------------------------------------------------------------------- 1 | import { model, createStore } from '../src'; 2 | 3 | interface State { 4 | value: number; 5 | } 6 | 7 | const countModel = model('counter').define({ 8 | state: { 9 | value: 1, 10 | }, 11 | actions: { 12 | add(state: State) { 13 | return { 14 | ...state, 15 | value: state.value + 1, 16 | }; 17 | }, 18 | }, 19 | }); 20 | 21 | const count2Model = model('counter2').define({ 22 | state: { 23 | value: 1, 24 | }, 25 | actions: { 26 | add2(state: State) { 27 | return { 28 | ...state, 29 | value: state.value + 1, 30 | }; 31 | }, 32 | }, 33 | }); 34 | 35 | let store = createStore(); 36 | 37 | describe('test model', () => { 38 | beforeEach(() => { 39 | store = createStore(); 40 | }); 41 | 42 | test('model can be used by store', () => { 43 | store.use(countModel); 44 | expect(store.getState()).toEqual({ 45 | counter: { 46 | value: 1, 47 | }, 48 | }); 49 | }); 50 | 51 | test('model(name) will return a new model', () => { 52 | store.use(countModel('counter1')); 53 | expect(store.getState()).toEqual({ 54 | counter1: { 55 | value: 1, 56 | }, 57 | }); 58 | 59 | store.use(countModel('counter1'))[1].add(); 60 | expect(store.getState()).toEqual({ 61 | counter1: { 62 | value: 2, 63 | }, 64 | }); 65 | }); 66 | 67 | test(`someModel('a') and someModel('a') should return same reference model`, () => { 68 | expect(countModel('1')).toBe(countModel('1')); 69 | }); 70 | 71 | test('unmount model when store only has mounted single model', () => { 72 | const [, actions, subscribe] = store.use(countModel); 73 | const mockFn = jest.fn(); 74 | subscribe(mockFn); 75 | 76 | actions.add(); 77 | expect(store.getState()).toEqual({ 78 | counter: { 79 | value: 2, 80 | }, 81 | }); 82 | expect(mockFn).toBeCalledTimes(1); 83 | 84 | store.unmount(countModel); 85 | expect(store.getState().counter).toBeUndefined(); 86 | 87 | mockFn.mockClear(); 88 | actions.add(); 89 | expect(mockFn).toBeCalledTimes(0); 90 | 91 | const [, newActions, newSubscribe] = store.use(countModel); 92 | newSubscribe(mockFn); 93 | newActions.add(); 94 | expect(mockFn).toBeCalledTimes(1); 95 | expect(store.getState()).toEqual({ 96 | counter: { 97 | value: 2, 98 | }, 99 | }); 100 | }); 101 | 102 | test('unmount model when store has mounted multiple models', () => { 103 | const [, actions, subscribe] = store.use(countModel); 104 | const [, count2Actions, count2Subscribe] = store.use(count2Model); 105 | 106 | const countCbFn = jest.fn(); 107 | const count2CbFn = jest.fn(); 108 | subscribe(countCbFn); 109 | count2Subscribe(count2CbFn); 110 | 111 | actions.add(); 112 | expect(store.getState()).toEqual({ 113 | counter: { 114 | value: 2, 115 | }, 116 | counter2: { 117 | value: 1, 118 | }, 119 | }); 120 | expect(countCbFn).toBeCalledTimes(1); 121 | expect(count2CbFn).toBeCalledTimes(0); 122 | 123 | store.unmount(countModel); 124 | expect(store.getState().counter).toBeUndefined(); 125 | 126 | count2Actions.add2(); 127 | expect(count2CbFn).toBeCalledTimes(1); 128 | 129 | expect(store.getState()).toEqual({ 130 | counter2: { 131 | value: 2, 132 | }, 133 | }); 134 | 135 | countCbFn.mockClear(); 136 | count2CbFn.mockClear(); 137 | 138 | actions.add(); 139 | expect(countCbFn).toBeCalledTimes(0); 140 | expect(count2CbFn).toBeCalledTimes(0); 141 | 142 | const [, newActions, newSubscribe] = store.use(countModel); 143 | newSubscribe(countCbFn); 144 | newActions.add(); 145 | expect(countCbFn).toBeCalledTimes(1); 146 | expect(count2CbFn).toBeCalledTimes(0); 147 | 148 | count2Actions.add2(); 149 | expect(count2CbFn).toBeCalledTimes(1); 150 | 151 | expect(store.getState()).toEqual({ 152 | counter: { 153 | value: 2, 154 | }, 155 | counter2: { 156 | value: 3, 157 | }, 158 | }); 159 | }); 160 | }); 161 | -------------------------------------------------------------------------------- /packages/store/src/model/useModel.ts: -------------------------------------------------------------------------------- 1 | import mountModel from './mountModel'; 2 | import { combineSubscribe } from './subscribe'; 3 | import { Context, Model, UseModel } from '@/types'; 4 | import { getComputedDepModels, isModel } from '@/utils/misc'; 5 | 6 | function createUseModel(context: Context): UseModel { 7 | function useModel(...args: any) { 8 | const flattenedArgs = Array.isArray(args[0]) 9 | ? [...args[0], ...args.slice(1)] 10 | : args; 11 | 12 | flattenedArgs.forEach(model => { 13 | if (isModel(model)) { 14 | mountModel(context, model); 15 | } 16 | }); 17 | 18 | const { getState, getActions, actualModels, subscribe } = parseModelParams( 19 | context, 20 | flattenedArgs, 21 | ); 22 | 23 | const computedArr = actualModels.map(m => { 24 | const { 25 | modelDesc: { computed }, 26 | } = context.apis.getModel(m); 27 | return computed; 28 | }); 29 | 30 | const computedDepModels = getComputedDepModels(computedArr); 31 | 32 | computedDepModels.forEach(model => { 33 | if (isModel(model)) { 34 | mountModel(context, model); 35 | } 36 | }); 37 | 38 | let [state, actions] = [getState(), getActions()]; 39 | 40 | ({ state, actions } = context.pluginCore.invokePipeline( 41 | 'useModel', 42 | { 43 | state, 44 | actions, 45 | }, 46 | { 47 | models: actualModels, 48 | mountedModels: actualModels.map(model => context.apis.getModel(model)), 49 | }, 50 | )); 51 | 52 | return [state, actions, subscribe]; 53 | } 54 | 55 | return useModel as UseModel; 56 | } 57 | 58 | const parseModelParams = (context: Context, _models: any) => { 59 | const models = Array.isArray(_models) ? _models : [_models]; 60 | const actualModels = []; 61 | const selectors = []; 62 | 63 | for (const model of models) { 64 | if (isModel(model)) { 65 | actualModels.push(model); 66 | } else { 67 | selectors.push(model); 68 | } 69 | } 70 | const [stateSelector, actionSelector] = selectors; 71 | 72 | if (actualModels.length > 1) { 73 | actualModels.forEach(m => { 74 | if ( 75 | Object.prototype.toString.call(context.apis.getModel(m).state) !== 76 | '[object Object]' 77 | ) { 78 | throw new Error( 79 | `You cant use multiple model one of which's state is primitive data`, 80 | ); 81 | } 82 | }); 83 | } 84 | 85 | const getStateWithComputed = (model: Model) => { 86 | const { 87 | state, 88 | modelDesc: { computed }, 89 | } = context.apis.getModel(model); 90 | 91 | let computedState: any; 92 | 93 | if (computed) { 94 | computedState = Object.keys(computed).reduce((curState, computedKey) => { 95 | curState[computedKey] = state[computedKey]; 96 | return curState; 97 | }, {}); 98 | // state reference always changes when model has computed properties 99 | return { ...state, ...computedState }; 100 | } 101 | 102 | return state; 103 | }; 104 | 105 | const finalStateSelector = (...models: any[]) => { 106 | if (stateSelector) { 107 | return stateSelector( 108 | ...actualModels.map(model => getStateWithComputed(model)), 109 | ); 110 | } 111 | 112 | if (models.length === 1) { 113 | return getStateWithComputed(models[0]); 114 | } 115 | 116 | return models.reduce( 117 | (res, model) => ({ ...res, ...getStateWithComputed(model) }), 118 | {}, 119 | ); 120 | }; 121 | 122 | const finalActionSelector = 123 | actionSelector || 124 | ((...actions: any[]) => 125 | actions.reduce((res, action) => Object.assign(res, action), {})); 126 | 127 | return { 128 | getState: () => finalStateSelector(...actualModels), 129 | getActions: () => 130 | finalActionSelector( 131 | ...actualModels.map(model => context.apis.getModel(model)!.actions), 132 | ), 133 | subscribe: (handler: () => void) => 134 | combineSubscribe( 135 | context, 136 | ...actualModels.map(model => context.apis.getModelSubscribe(model)), 137 | )(handler), 138 | actualModels, 139 | }; 140 | }; 141 | 142 | export { createUseModel }; 143 | -------------------------------------------------------------------------------- /packages/plugins/xstate/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @modern-js-reduck/plugin-xstate 2 | 3 | ## 1.1.0 4 | 5 | ### Minor Changes 6 | 7 | - feat: the real v1.0 8 | 9 | ### Patch Changes 10 | 11 | - @modern-js-reduck/store@1.1.0 12 | 13 | ## 1.0.6 14 | 15 | ### Patch Changes 16 | 17 | - feat: uniform package version 18 | - Updated dependencies 19 | - @modern-js-reduck/store@1.0.6 20 | 21 | ## 1.0.3 22 | 23 | ### Patch Changes 24 | 25 | - ed80d6e: build: build modern js dist 26 | - Updated dependencies [ac05b04] 27 | - Updated dependencies [30b1167] 28 | - Updated dependencies [ed80d6e] 29 | - Updated dependencies [b33f42a] 30 | - Updated dependencies [9c09d2a] 31 | - Updated dependencies [51721bc] 32 | - @modern-js-reduck/store@1.0.5 33 | 34 | ## 1.0.2 35 | 36 | ### Patch Changes 37 | 38 | - bump dep 39 | - Updated dependencies [2b7d19f] 40 | - @modern-js-reduck/store@1.0.4 41 | 42 | ## 1.0.1 43 | 44 | ### Patch Changes 45 | 46 | - fix: fix git url 47 | - Updated dependencies [undefined] 48 | - @modern-js-reduck/store@1.0.1 49 | 50 | ## 1.0.0 51 | 52 | ### Minor Changes 53 | 54 | - feat: ready for publish 1.0.0 55 | 56 | ### Patch Changes 57 | 58 | - a7bd932: release: rc.7 59 | - 26d5144: fix(type): actions can return void 60 | - 9452589: fix: ts type 61 | - a7bd932: fix: use model State type 62 | - fd81b6b: feat: useLocalModel support 63 | - feat: ready for publish 1.0.0 64 | - a7bd932: fix: model type 65 | - a7bd932: fix: model when State passed use State 66 | - Updated dependencies [a7bd932] 67 | - Updated dependencies [undefined] 68 | - Updated dependencies [26d5144] 69 | - Updated dependencies [9452589] 70 | - Updated dependencies [a7bd932] 71 | - Updated dependencies [fd81b6b] 72 | - Updated dependencies [undefined] 73 | - Updated dependencies [a7bd932] 74 | - Updated dependencies [a7bd932] 75 | - @modern-js-reduck/store@1.0.0 76 | 77 | ## 1.0.0-rc.10 78 | 79 | ### Patch Changes 80 | 81 | - a7bd932: release: rc.7 82 | - 26d5144: fix(type): actions can return void 83 | - 9452589: fix: ts type 84 | - a7bd932: fix: use model State type 85 | - feat: useLocalModel support 86 | - a7bd932: fix: model type 87 | - a7bd932: fix: model when State passed use State 88 | - Updated dependencies [a7bd932] 89 | - Updated dependencies [26d5144] 90 | - Updated dependencies [9452589] 91 | - Updated dependencies [a7bd932] 92 | - Updated dependencies [undefined] 93 | - Updated dependencies [a7bd932] 94 | - Updated dependencies [a7bd932] 95 | - @modern-js-reduck/store@1.0.0-rc.10 96 | 97 | ## 1.0.0-rc.9 98 | 99 | ### Patch Changes 100 | 101 | - ac4124f: release: rc.7 102 | - 26d5144: fix(type): actions can return void 103 | - 9452589: fix: ts type 104 | - 1085d2d: fix: use model State type 105 | - fix: model type 106 | - 1085d2d: fix: model when State passed use State 107 | - Updated dependencies [ac4124f] 108 | - Updated dependencies [26d5144] 109 | - Updated dependencies [9452589] 110 | - Updated dependencies [1085d2d] 111 | - Updated dependencies [undefined] 112 | - Updated dependencies [1085d2d] 113 | - @modern-js-reduck/store@1.0.0-rc.9 114 | 115 | ## 1.0.0-rc.8 116 | 117 | ### Patch Changes 118 | 119 | - release: rc.7 120 | - 26d5144: fix(type): actions can return void 121 | - 9452589: fix: ts type 122 | - 1085d2d: fix: use model State type 123 | - 1085d2d: fix: model when State passed use State 124 | - Updated dependencies [undefined] 125 | - Updated dependencies [26d5144] 126 | - Updated dependencies [9452589] 127 | - Updated dependencies [1085d2d] 128 | - Updated dependencies [1085d2d] 129 | - @modern-js-reduck/store@1.0.0-rc.8 130 | 131 | ## 1.0.0-next.7 132 | 133 | ### Patch Changes 134 | 135 | - 26d5144: fix(type): actions can return void 136 | - 9452589: fix: ts type 137 | - fix: use model State type 138 | - fix: model when State passed use State 139 | - Updated dependencies [26d5144] 140 | - Updated dependencies [9452589] 141 | - Updated dependencies [undefined] 142 | - Updated dependencies [undefined] 143 | - @modern-js-reduck/store@1.0.0-next.7 144 | 145 | ## 1.0.0-rc.6 146 | 147 | ### Patch Changes 148 | 149 | - fix(type): actions can return void 150 | - 9452589: fix: ts type 151 | - Updated dependencies [undefined] 152 | - Updated dependencies [9452589] 153 | - @modern-js-reduck/store@1.0.0-rc.6 154 | 155 | ## 1.0.0-rc.5 156 | 157 | ### Patch Changes 158 | 159 | - fix: ts type 160 | - Updated dependencies [undefined] 161 | - @modern-js-reduck/store@1.0.0-rc.5 162 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | ".code-workspace": "jsonc", 4 | ".babelrc": "json", 5 | ".eslintrc": "jsonc", 6 | ".eslintrc*.json": "jsonc", 7 | ".stylelintrc": "jsonc", 8 | "stylelintrc": "jsonc", 9 | ".htmlhintrc": "jsonc", 10 | "htmlhintrc": "jsonc", 11 | "Procfile*": "shellscript", 12 | "README": "markdown" 13 | }, 14 | "files.exclude": { 15 | "**/*/adapters/**/index.ts": true, 16 | "**/*/adapters/**/index.js": true 17 | }, 18 | "search.useIgnoreFiles": true, 19 | "search.exclude": { 20 | "**/build": true, 21 | "**/output": true, 22 | "**/dist": true, 23 | "**/yarn.lock": true, 24 | "**/package-lock.json": true, 25 | "**/*.log": true, 26 | "**/*.pid": true, 27 | "**/.git": true, 28 | "**/node_modules": true, 29 | "**/bower_components": true 30 | }, 31 | // 32 | "editor.rulers": [80, 120], 33 | "files.eol": "\n", 34 | "files.trimTrailingWhitespace": true, 35 | "files.insertFinalNewline": true, 36 | // 37 | "todo-tree.general.tags": ["TODO:", "FIXME:"], 38 | "todo-tree.highlights.defaultHighlight": { 39 | "gutterIcon": true 40 | }, 41 | "todo-tree.highlights.customHighlight": { 42 | "TODO:": { 43 | "foreground": "#fff", 44 | "background": "#ffbd2a", 45 | "iconColour": "#ffbd2a" 46 | }, 47 | "FIXME:": { 48 | "foreground": "#fff", 49 | "background": "#f06292", 50 | "icon": "flame", 51 | "iconColour": "#f06292" 52 | } 53 | }, 54 | // 55 | "cSpell.diagnosticLevel": "Hint", 56 | "[javascript]": { 57 | "editor.defaultFormatter": "esbenp.prettier-vscode" 58 | }, 59 | "[javascriptreact]": { 60 | "editor.defaultFormatter": "esbenp.prettier-vscode" 61 | }, 62 | "[typescript]": { 63 | "editor.defaultFormatter": "esbenp.prettier-vscode" 64 | }, 65 | "[typescriptreact]": { 66 | "editor.defaultFormatter": "esbenp.prettier-vscode" 67 | }, 68 | "eslint.alwaysShowStatus": true, 69 | // "eslint.nodePath": "./node_modules", 70 | // "eslint.packageManager": "pnpm", 71 | "eslint.run": "onType", 72 | "eslint.options": { 73 | "rules": { 74 | "no-debugger": "off" 75 | } 76 | }, 77 | "eslint.probe": [ 78 | "javascript", 79 | "javascriptreact", 80 | "typescript", 81 | "typescriptreact", 82 | "vue" 83 | ], 84 | "eslint.format.enable": true, 85 | "eslint.lintTask.enable": true, 86 | "javascript.validate.enable": false, 87 | "typescript.validate.enable": true, 88 | "flow.enabled": false, 89 | // 90 | "htmlhint.enable": true, 91 | // 92 | "stylelint.enable": false, 93 | "css.validate": false, 94 | "scss.validate": false, 95 | "less.validate": false, 96 | "[css]": { 97 | "editor.defaultFormatter": "esbenp.prettier-vscode", 98 | "editor.formatOnType": true, 99 | "editor.formatOnPaste": true, 100 | "editor.formatOnSave": true 101 | }, 102 | "[scss]": { 103 | "editor.defaultFormatter": "esbenp.prettier-vscode", 104 | "editor.formatOnType": true, 105 | "editor.formatOnPaste": true, 106 | "editor.formatOnSave": true 107 | }, 108 | "[less]": { 109 | "editor.defaultFormatter": "esbenp.prettier-vscode", 110 | "editor.formatOnType": true, 111 | "editor.formatOnPaste": true, 112 | "editor.formatOnSave": true 113 | }, 114 | "prettier.trailingComma": "all", 115 | "prettier.printWidth": 80, 116 | "prettier.semi": true, 117 | "prettier.arrowParens": "avoid", 118 | "prettier.bracketSpacing": true, 119 | "prettier.jsxBracketSameLine": true, 120 | // 121 | "editor.codeActionsOnSave": { 122 | "source.fixAll.eslint": "explicit" 123 | }, 124 | "editor.codeActionsOnSaveTimeout": 5000, 125 | "editor.defaultFormatter": "EditorConfig.EditorConfig", 126 | "javascript.format.enable": false, 127 | "typescript.format.enable": false, 128 | // 129 | "json.format.enable": false, 130 | "[json]": { 131 | "editor.defaultFormatter": "esbenp.prettier-vscode", 132 | "editor.tabSize": 2, 133 | "editor.formatOnType": true, 134 | "editor.formatOnPaste": true, 135 | "editor.formatOnSave": true 136 | }, 137 | "[jsonc]": { 138 | "editor.defaultFormatter": "esbenp.prettier-vscode", 139 | "editor.tabSize": 2, 140 | "editor.formatOnType": true, 141 | "editor.formatOnPaste": true, 142 | "editor.formatOnSave": true 143 | }, 144 | "emmet.triggerExpansionOnTab": true, 145 | "typescript.tsdk": "node_modules/typescript/lib", 146 | "deno.enable": false, 147 | "cSpell.words": ["immer", "middlewares", "reduck", "xstate"] 148 | } 149 | -------------------------------------------------------------------------------- /packages/plugins/xstate-immer/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @modern-js-reduck/plugin-xstate-immer 2 | 3 | ## 1.1.0 4 | 5 | ### Minor Changes 6 | 7 | - feat: the real v1.0 8 | 9 | ### Patch Changes 10 | 11 | - Updated dependencies 12 | - @modern-js-reduck/plugin-xstate@1.1.0 13 | - @modern-js-reduck/store@1.1.0 14 | 15 | ## 1.0.6 16 | 17 | ### Patch Changes 18 | 19 | - feat: uniform package version 20 | - Updated dependencies 21 | - @modern-js-reduck/plugin-xstate@1.0.4 22 | - @modern-js-reduck/store@1.0.6 23 | 24 | ## 1.0.2 25 | 26 | ### Patch Changes 27 | 28 | - ed80d6e: build: build modern js dist 29 | - Updated dependencies [ac05b04] 30 | - Updated dependencies [30b1167] 31 | - Updated dependencies [ed80d6e] 32 | - Updated dependencies [b33f42a] 33 | - Updated dependencies [9c09d2a] 34 | - Updated dependencies [51721bc] 35 | - @modern-js-reduck/store@1.0.5 36 | - @modern-js-reduck/plugin-xstate@1.0.3 37 | 38 | ## 1.0.1 39 | 40 | ### Patch Changes 41 | 42 | - fix: fix git url 43 | - Updated dependencies [undefined] 44 | - @modern-js-reduck/plugin-xstate@1.0.1 45 | - @modern-js-reduck/store@1.0.1 46 | 47 | ## 1.0.0 48 | 49 | ### Minor Changes 50 | 51 | - feat: ready for publish 1.0.0 52 | 53 | ### Patch Changes 54 | 55 | - a7bd932: release: rc.7 56 | - 26d5144: fix(type): actions can return void 57 | - 9452589: fix: ts type 58 | - a7bd932: fix: use model State type 59 | - fd81b6b: feat: useLocalModel support 60 | - feat: ready for publish 1.0.0 61 | - a7bd932: fix: model type 62 | - a7bd932: fix: model when State passed use State 63 | - Updated dependencies [a7bd932] 64 | - Updated dependencies [undefined] 65 | - Updated dependencies [26d5144] 66 | - Updated dependencies [9452589] 67 | - Updated dependencies [a7bd932] 68 | - Updated dependencies [fd81b6b] 69 | - Updated dependencies [undefined] 70 | - Updated dependencies [a7bd932] 71 | - Updated dependencies [a7bd932] 72 | - @modern-js-reduck/plugin-xstate@1.0.0 73 | - @modern-js-reduck/store@1.0.0 74 | 75 | ## 1.0.0-rc.10 76 | 77 | ### Patch Changes 78 | 79 | - a7bd932: release: rc.7 80 | - 26d5144: fix(type): actions can return void 81 | - 9452589: fix: ts type 82 | - a7bd932: fix: use model State type 83 | - feat: useLocalModel support 84 | - a7bd932: fix: model type 85 | - a7bd932: fix: model when State passed use State 86 | - Updated dependencies [a7bd932] 87 | - Updated dependencies [26d5144] 88 | - Updated dependencies [9452589] 89 | - Updated dependencies [a7bd932] 90 | - Updated dependencies [undefined] 91 | - Updated dependencies [a7bd932] 92 | - Updated dependencies [a7bd932] 93 | - @modern-js-reduck/plugin-xstate@1.0.0-rc.10 94 | - @modern-js-reduck/store@1.0.0-rc.10 95 | 96 | ## 1.0.0-rc.9 97 | 98 | ### Patch Changes 99 | 100 | - ac4124f: release: rc.7 101 | - 26d5144: fix(type): actions can return void 102 | - 9452589: fix: ts type 103 | - 1085d2d: fix: use model State type 104 | - fix: model type 105 | - 1085d2d: fix: model when State passed use State 106 | - Updated dependencies [ac4124f] 107 | - Updated dependencies [26d5144] 108 | - Updated dependencies [9452589] 109 | - Updated dependencies [1085d2d] 110 | - Updated dependencies [undefined] 111 | - Updated dependencies [1085d2d] 112 | - @modern-js-reduck/plugin-xstate@1.0.0-rc.9 113 | - @modern-js-reduck/store@1.0.0-rc.9 114 | 115 | ## 1.0.0-rc.8 116 | 117 | ### Patch Changes 118 | 119 | - release: rc.7 120 | - 26d5144: fix(type): actions can return void 121 | - 9452589: fix: ts type 122 | - 1085d2d: fix: use model State type 123 | - 1085d2d: fix: model when State passed use State 124 | - Updated dependencies [undefined] 125 | - Updated dependencies [26d5144] 126 | - Updated dependencies [9452589] 127 | - Updated dependencies [1085d2d] 128 | - Updated dependencies [1085d2d] 129 | - @modern-js-reduck/plugin-xstate@1.0.0-rc.8 130 | - @modern-js-reduck/store@1.0.0-rc.8 131 | 132 | ## 1.0.0-next.7 133 | 134 | ### Patch Changes 135 | 136 | - 26d5144: fix(type): actions can return void 137 | - 9452589: fix: ts type 138 | - fix: use model State type 139 | - fix: model when State passed use State 140 | - Updated dependencies [26d5144] 141 | - Updated dependencies [9452589] 142 | - Updated dependencies [undefined] 143 | - Updated dependencies [undefined] 144 | - @modern-js-reduck/plugin-xstate@1.0.0-next.7 145 | - @modern-js-reduck/store@1.0.0-next.7 146 | 147 | ## 1.0.0-rc.6 148 | 149 | ### Patch Changes 150 | 151 | - fix(type): actions can return void 152 | - 9452589: fix: ts type 153 | - Updated dependencies [undefined] 154 | - Updated dependencies [9452589] 155 | - @modern-js-reduck/plugin-xstate@1.0.0-rc.6 156 | - @modern-js-reduck/store@1.0.0-rc.6 157 | 158 | ## 1.0.0-rc.5 159 | 160 | ### Patch Changes 161 | 162 | - fix: ts type 163 | - Updated dependencies [undefined] 164 | - @modern-js-reduck/plugin-xstate@1.0.0-rc.5 165 | - @modern-js-reduck/store@1.0.0-rc.5 166 | --------------------------------------------------------------------------------