├── example ├── frame │ ├── redux │ │ ├── state.ts │ │ ├── ActionTypes.ts │ │ ├── actions.ts │ │ ├── saga.ts │ │ └── reducer.ts │ ├── types.ts │ └── Frame.tsx ├── ModuleReUse │ ├── state.ts │ ├── OpPanel │ │ ├── types.ts │ │ ├── actions.ts │ │ ├── index.ts │ │ └── OpPanel.tsx │ ├── ActionTypes.ts │ ├── types.ts │ ├── actions.ts │ ├── index.ts │ ├── reducer.ts │ └── Container.tsx ├── PassDownState │ ├── state.ts │ ├── child │ │ ├── state.ts │ │ ├── reducer.ts │ │ ├── actions.ts │ │ ├── types.ts │ │ ├── index.ts │ │ └── Child.tsx │ ├── types.ts │ ├── reducer.ts │ ├── actions.ts │ ├── index.ts │ └── Parent.tsx ├── ScopedPage │ ├── state.ts │ ├── reducer.ts │ ├── types.ts │ ├── index.ts │ ├── actions.ts │ ├── saga.ts │ └── scopedPage.tsx ├── tsconfig.json ├── configureStore.ts ├── expressServer.js ├── server.js ├── index.tsx ├── package.json ├── webpack.config.dev.js ├── index.html └── webpack.config.prod.js ├── .gitignore ├── test ├── testUtils │ ├── index.ts │ └── createMount.ts ├── sceneBundleForTestB │ ├── state.ts │ ├── actions.ts │ ├── types.ts │ ├── reducer.ts │ ├── index.ts │ └── Page.tsx ├── sceneBundleForTestA │ ├── state.ts │ ├── reducer.ts │ ├── types.ts │ ├── actions.ts │ ├── index.ts │ ├── Page.tsx │ └── saga.ts ├── integration │ └── ArenaScene │ │ ├── types.ts │ │ ├── createBundleMounter.tsx │ │ ├── TestHOC.tsx │ │ ├── mountAndUnount.spec.ts │ │ └── hotReplace.spec.tsx ├── test.webpack.js └── karma.conf.js ├── .vscode └── settings.json ├── src ├── core │ ├── reducers │ │ ├── getSceneInitState.ts │ │ ├── types │ │ │ ├── index.ts │ │ │ ├── arenaState.ts │ │ │ └── curtainState.ts │ │ ├── getCurtainInitState.ts │ │ ├── getArenaInitState.ts │ │ ├── sceneReducerWrapper.ts │ │ ├── index.ts │ │ ├── createSceneReducer.ts │ │ ├── createCurtainReducer.ts │ │ └── arenaReducer.ts │ ├── types │ │ ├── reducer.ts │ │ ├── reducerDict.ts │ │ ├── index.ts │ │ ├── actions.ts │ │ └── bundle.ts │ ├── enhancedRedux │ │ ├── index.ts │ │ ├── createArenaStore.spec.ts │ │ ├── createArenaStore.ts │ │ ├── bindArenaActionCreators.ts │ │ ├── createPropsPicker.ts │ │ └── createEnhancedStore.ts │ ├── sagas │ │ ├── index.ts │ │ ├── audienceSaga.ts │ │ ├── arenaCurtainSaga.ts │ │ ├── sceneBundleSaga.ts │ │ └── sceneReduxSaga.ts │ ├── index.ts │ └── ActionTypes.ts ├── ActionTypes │ └── index.ts ├── hocs │ ├── ReducerDictOverrider │ │ ├── index.ts │ │ ├── types.ts │ │ └── ReducerDictOverrider.tsx │ ├── ArenaScene │ │ ├── index.ts │ │ ├── types.ts │ │ └── ArenaScene.tsx │ ├── BundleComponent │ │ ├── index.ts │ │ ├── actions.ts │ │ ├── types.ts │ │ ├── curtainConnect.ts │ │ └── BundleComponent.tsx │ └── index.ts ├── utils │ ├── index.ts │ ├── addStateTreeNode.ts │ ├── replaceReducer.ts │ ├── buildReducerDict.ts │ └── addReducer.ts ├── effects │ ├── getSceneActions.ts │ ├── getSceneState.ts │ ├── getArenaReducerDictEntry.ts │ ├── setSceneState.ts │ ├── index.ts │ ├── putSceneAction.ts │ ├── takeEverySceneAction.ts │ ├── takeLatestSceneAction.ts │ └── takeSceneAction.ts ├── tools │ ├── index.ts │ ├── autoFill.ts │ ├── index.spec.tsx │ ├── types.ts │ ├── bundleToComponent.tsx │ └── bundleToElement.tsx └── index.ts ├── subModules ├── tools │ └── package.json ├── effects │ └── package.json └── actionTypes │ └── package.json ├── .travis.yml ├── tsconfig.json ├── CHANGELOG.md ├── package.json ├── scripts └── build.js ├── README.zh-CN.MD ├── README.MD └── LICENSE /example/frame/redux/state.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | cnt: 0 3 | }; 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | build/ 4 | example/build 5 | tmp/ -------------------------------------------------------------------------------- /example/ModuleReUse/state.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | panelNum: 3 3 | }; 4 | -------------------------------------------------------------------------------- /test/testUtils/index.ts: -------------------------------------------------------------------------------- 1 | export { default as createMount } from "./createMount"; 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules\\typescript\\lib" 3 | } -------------------------------------------------------------------------------- /example/PassDownState/state.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | name: "Parent", 3 | cnt: 0 4 | }; 5 | -------------------------------------------------------------------------------- /example/PassDownState/child/state.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | name: "Child", 3 | cnt: 0 4 | }; 5 | -------------------------------------------------------------------------------- /example/ModuleReUse/OpPanel/types.ts: -------------------------------------------------------------------------------- 1 | export type Actions = { 2 | addCnt: (step: number) => void; 3 | }; 4 | -------------------------------------------------------------------------------- /src/core/reducers/getSceneInitState.ts: -------------------------------------------------------------------------------- 1 | export default function getSceneInitState() { 2 | return {}; 3 | } 4 | -------------------------------------------------------------------------------- /test/sceneBundleForTestB/state.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | name: "PageB", 3 | pageB: true, 4 | cnt: 0 5 | }; 6 | -------------------------------------------------------------------------------- /example/frame/types.ts: -------------------------------------------------------------------------------- 1 | export type Props = { 2 | cnt: number; 3 | addCnt: () => void; 4 | clearCnt: () => void; 5 | }; 6 | -------------------------------------------------------------------------------- /src/ActionTypes/index.ts: -------------------------------------------------------------------------------- 1 | enum ActionTypes { 2 | ARENA_SCENE_SET_STATE = "ARENA_SCENE_SET_STATE" 3 | } 4 | 5 | export default ActionTypes; 6 | -------------------------------------------------------------------------------- /src/hocs/ReducerDictOverrider/index.ts: -------------------------------------------------------------------------------- 1 | import ReducerDictOverrider from "./ReducerDictOverrider"; 2 | 3 | export default ReducerDictOverrider; 4 | -------------------------------------------------------------------------------- /example/ScopedPage/state.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | name: "ScopedPage", 3 | dynamicState: 0, 4 | isDynamicStateEnable: false, 5 | cnt: 0 6 | }; 7 | -------------------------------------------------------------------------------- /test/sceneBundleForTestB/actions.ts: -------------------------------------------------------------------------------- 1 | export function addCnt() { 2 | return { 3 | type: "ADD_CNT" 4 | }; 5 | } 6 | 7 | export default { addCnt }; 8 | -------------------------------------------------------------------------------- /example/ModuleReUse/ActionTypes.ts: -------------------------------------------------------------------------------- 1 | enum ActionTypes { 2 | ADD_PANEL = "ADD_PANEL", 3 | DEL_PANEL = "DEL_PANEL" 4 | } 5 | 6 | export default ActionTypes; 7 | -------------------------------------------------------------------------------- /src/hocs/ArenaScene/index.ts: -------------------------------------------------------------------------------- 1 | export { default, default as ArenaScene } from "./ArenaScene"; 2 | export { Props, ExtraProps, State, Context } from "./types"; 3 | -------------------------------------------------------------------------------- /example/ModuleReUse/OpPanel/actions.ts: -------------------------------------------------------------------------------- 1 | function addCnt(num: number) { 2 | return { 3 | type: "ADD_CNT", 4 | num 5 | }; 6 | } 7 | 8 | export default { addCnt }; 9 | -------------------------------------------------------------------------------- /example/ModuleReUse/types.ts: -------------------------------------------------------------------------------- 1 | export type State = { 2 | panelNum: number; 3 | }; 4 | 5 | export type Actions = { 6 | addPanel: () => void; 7 | delPanel: () => void; 8 | }; 9 | -------------------------------------------------------------------------------- /test/sceneBundleForTestA/state.ts: -------------------------------------------------------------------------------- 1 | import { State } from "./types"; 2 | 3 | export default { 4 | name: "PageA", 5 | pageA: true, 6 | sagaCnt: 0, 7 | cnt: 0 8 | } as State; 9 | -------------------------------------------------------------------------------- /example/PassDownState/types.ts: -------------------------------------------------------------------------------- 1 | export type State = { 2 | name: string; 3 | cnt: number; 4 | }; 5 | 6 | export type Actions = { 7 | addCnt: () => void; 8 | clearCnt: () => void; 9 | }; 10 | -------------------------------------------------------------------------------- /subModules/tools/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-arena/tools", 3 | "private": true, 4 | "main": "../lib/tools/index.js", 5 | "module": "../es/tools/index.js", 6 | "jsnext:main": "../es/tools/index.js" 7 | } 8 | -------------------------------------------------------------------------------- /subModules/effects/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-arena/effects", 3 | "private": true, 4 | "main": "../lib/effects/index.js", 5 | "module": "../es/effects/index.js", 6 | "jsnext:main": "../es/effects/index.js" 7 | } 8 | -------------------------------------------------------------------------------- /src/hocs/BundleComponent/index.ts: -------------------------------------------------------------------------------- 1 | export { default, default as BundleComponent } from "./BundleComponent"; 2 | export { Props, BaseProps, ConnectedProps, State } from "./types"; 3 | export { default as curtainConnect } from "./curtainConnect"; 4 | -------------------------------------------------------------------------------- /src/hocs/ReducerDictOverrider/types.ts: -------------------------------------------------------------------------------- 1 | import { ReactElement } from "react"; 2 | import { ReducerDict } from "../../core"; 3 | 4 | export type ReducerDictOverriderProps = { 5 | reducerDict: ReducerDict; 6 | children: ReactElement<{}>; 7 | }; 8 | -------------------------------------------------------------------------------- /subModules/actionTypes/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-arena/ActionTypes", 3 | "private": true, 4 | "main": "../lib/ActionTypes/index.js", 5 | "module": "../es/ActionTypes/index.js", 6 | "jsnext:main": "../es/ActionTypes/index.js" 7 | } 8 | -------------------------------------------------------------------------------- /example/ModuleReUse/OpPanel/index.ts: -------------------------------------------------------------------------------- 1 | import { bundleToComponent } from "redux-arena/tools"; 2 | import actions from "./actions"; 3 | import OpPanel from "./OpPanel"; 4 | 5 | export default bundleToComponent({ 6 | Component: OpPanel, 7 | actions 8 | }); 9 | -------------------------------------------------------------------------------- /example/frame/redux/ActionTypes.ts: -------------------------------------------------------------------------------- 1 | const FRAME_SET_STATE = "FRAME_SET_STATE"; 2 | const ADD_CNT = "ADD_CNT"; 3 | const FRAME_CLEAR_CNT = "FRAME_CLEAR_CNT"; 4 | 5 | export default { 6 | FRAME_SET_STATE, 7 | ADD_CNT, 8 | FRAME_CLEAR_CNT 9 | }; 10 | -------------------------------------------------------------------------------- /src/core/types/reducer.ts: -------------------------------------------------------------------------------- 1 | import { AnyAction } from "redux"; 2 | export type SceneReducer = ( 3 | state: S, 4 | action: AnyAction, 5 | isSceneAction: boolean 6 | ) => S; 7 | 8 | export type ReducerFactory = (reducerKey: string) => SceneReducer<{}>; 9 | -------------------------------------------------------------------------------- /test/integration/ArenaScene/types.ts: -------------------------------------------------------------------------------- 1 | import { ReactWrapper } from "enzyme"; 2 | import { EnhancedStore, SceneBundle } from "src"; 3 | 4 | export type MountBundle = ( 5 | store: EnhancedStore, 6 | sceneBundle: SceneBundle<{}, {}, {}, {}> 7 | ) => ReactWrapper; 8 | -------------------------------------------------------------------------------- /test/sceneBundleForTestB/types.ts: -------------------------------------------------------------------------------- 1 | import { ActionCreator } from "redux"; 2 | 3 | export type State = { 4 | name: string; 5 | pageB: boolean; 6 | cnt: number; 7 | }; 8 | 9 | export type Props = { 10 | actions: { 11 | addCnt: ActionCreator<{}>; 12 | }; 13 | } & State; 14 | -------------------------------------------------------------------------------- /test/test.webpack.js: -------------------------------------------------------------------------------- 1 | const context = require.context("../src", true, /\.spec\.tsx?$/); 2 | context.keys().forEach(context); 3 | const integrationContext = require.context( 4 | "./integration", 5 | true, 6 | /\.spec\.tsx?$/ 7 | ); 8 | integrationContext.keys().forEach(integrationContext); 9 | -------------------------------------------------------------------------------- /src/core/reducers/types/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | CurtainState, 3 | CurtainReduxInfo, 4 | CurtainMutableObj 5 | } from "./curtainState"; 6 | export { 7 | ArenaState, 8 | StateTreeNode, 9 | StateTreeDictItem, 10 | StateTreeDict, 11 | StateTree, 12 | RootState 13 | } from "./arenaState"; 14 | -------------------------------------------------------------------------------- /example/ScopedPage/reducer.ts: -------------------------------------------------------------------------------- 1 | import initState from "./state"; 2 | 3 | export default function reducer(state = initState, action) { 4 | switch (action.type) { 5 | case "ADD_CNT": 6 | return Object.assign({}, state, { cnt: state.cnt + 1 }); 7 | default: 8 | return state; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.5.3", 3 | "compilerOptions": { 4 | "target": "es5", 5 | "sourceMap": true, 6 | "module": "esnext", 7 | "moduleResolution": "node", 8 | "lib": ["es6", "dom"], 9 | "jsx": "react" 10 | }, 11 | "exclude": ["node_modules", "example"] 12 | } 13 | -------------------------------------------------------------------------------- /example/PassDownState/reducer.ts: -------------------------------------------------------------------------------- 1 | import initState from "./state"; 2 | 3 | export default function reducer(state = initState, action) { 4 | switch (action.type) { 5 | case "ADD_CNT": 6 | return Object.assign({}, state, { cnt: state.cnt + 1 }); 7 | default: 8 | return state; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /example/ScopedPage/types.ts: -------------------------------------------------------------------------------- 1 | export type State = { 2 | name: string; 3 | dynamicState: number; 4 | isDynamicStateEnable: boolean; 5 | cnt: number; 6 | }; 7 | 8 | export type Actions = { 9 | addCnt: () => void; 10 | clearCnt: () => void; 11 | switchDynamicState: (isEnabled: boolean) => void; 12 | }; 13 | -------------------------------------------------------------------------------- /src/core/reducers/getCurtainInitState.ts: -------------------------------------------------------------------------------- 1 | import { CurtainState } from "./types"; 2 | 3 | export default function getCurtainInitState(): CurtainState<{}> { 4 | return { 5 | PlayingScene: null, 6 | curSceneBundle: null, 7 | reduxInfo: null, 8 | mutableObj: { isObsolete: false } 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /example/ModuleReUse/actions.ts: -------------------------------------------------------------------------------- 1 | import ActionTypes from "./ActionTypes"; 2 | 3 | function addPanel() { 4 | return { 5 | type: ActionTypes.ADD_PANEL 6 | }; 7 | } 8 | 9 | function delPanel() { 10 | return { 11 | type: ActionTypes.DEL_PANEL 12 | }; 13 | } 14 | 15 | export default { addPanel, delPanel }; 16 | -------------------------------------------------------------------------------- /example/PassDownState/child/reducer.ts: -------------------------------------------------------------------------------- 1 | import initState from "./state"; 2 | 3 | export default function reducer(state = initState, action) { 4 | switch (action.type) { 5 | case "ADD_CNT": 6 | return Object.assign({}, state, { cnt: state.cnt + 1 }); 7 | default: 8 | return state; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /example/frame/redux/actions.ts: -------------------------------------------------------------------------------- 1 | import ActionTypes from "./ActionTypes"; 2 | 3 | function addCnt() { 4 | return { 5 | type: ActionTypes.ADD_CNT 6 | }; 7 | } 8 | 9 | function clearCnt() { 10 | return { 11 | type: ActionTypes.FRAME_CLEAR_CNT 12 | }; 13 | } 14 | 15 | export default { addCnt, clearCnt }; 16 | -------------------------------------------------------------------------------- /src/core/types/reducerDict.ts: -------------------------------------------------------------------------------- 1 | import { ActionCreatorsMapObject } from "redux"; 2 | 3 | export type ReducerDictItem = { 4 | reducerKey: string; 5 | vReducerKey: string | null | undefined; 6 | actions: ActionCreatorsMapObject; 7 | }; 8 | 9 | export type ReducerDict = { [key: string]: ReducerDictItem }; 10 | -------------------------------------------------------------------------------- /src/core/reducers/getArenaInitState.ts: -------------------------------------------------------------------------------- 1 | import { Map } from "immutable"; 2 | import { ArenaState } from "./types"; 3 | 4 | export default function getInitState(): ArenaState { 5 | return { 6 | audienceSagaTask: null, 7 | propsLock: false, 8 | stateTree: Map(), 9 | stateTreeDict: Map() 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import addStateTreeNode from "./addStateTreeNode"; 2 | export { addStateTreeNode }; 3 | export { sceneAddReducer, curtainAddReducer } from "./addReducer"; 4 | export { sceneReplaceReducer } from "./replaceReducer"; 5 | export { 6 | buildCurtainReducerDict, 7 | buildSceneReducerDict 8 | } from "./buildReducerDict"; 9 | -------------------------------------------------------------------------------- /src/core/enhancedRedux/index.ts: -------------------------------------------------------------------------------- 1 | export { default as bindArenaActionCreators } from "./bindArenaActionCreators"; 2 | export { 3 | default as createEnhancedStore, 4 | EnhancedStore 5 | } from "./createEnhancedStore"; 6 | export { default as createPropsPicker } from "./createPropsPicker"; 7 | export { default as createArenaStore } from "./createArenaStore"; 8 | -------------------------------------------------------------------------------- /example/ModuleReUse/index.ts: -------------------------------------------------------------------------------- 1 | import { bundleToComponent } from "redux-arena/tools"; 2 | import state from "./state"; 3 | import reducer from "./reducer"; 4 | import actions from "./actions"; 5 | import Container from "./Container"; 6 | 7 | export default bundleToComponent({ 8 | Component: Container, 9 | state, 10 | reducer, 11 | actions 12 | }); 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - stable 5 | 6 | script: 7 | - npm run-script test-travis 8 | 9 | before_script: 10 | - export DISPLAY=:99.0 11 | - sh -e /etc/init.d/xvfb start 12 | 13 | after_script: 14 | - npm install coveralls && cat ./coverage/lcov.info | ./node_modules/.bin/coveralls 15 | 16 | notifications: 17 | email: false -------------------------------------------------------------------------------- /test/sceneBundleForTestA/reducer.ts: -------------------------------------------------------------------------------- 1 | import initState from "./state"; 2 | import { AnyAction } from "redux"; 3 | 4 | export default function reducer(state = initState, action: AnyAction) { 5 | switch (action.type) { 6 | case "ADD_CNT": 7 | return Object.assign({}, state, { cnt: state.cnt + 1 }); 8 | default: 9 | return state; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/sceneBundleForTestB/reducer.ts: -------------------------------------------------------------------------------- 1 | import initState from "./state"; 2 | import { AnyAction } from "redux"; 3 | 4 | export default function reducer(state = initState, action: AnyAction) { 5 | switch (action.type) { 6 | case "ADD_CNT": 7 | return Object.assign({}, state, { cnt: state.cnt + 1 }); 8 | default: 9 | return state; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /example/PassDownState/actions.ts: -------------------------------------------------------------------------------- 1 | import ActionTypes from "redux-arena/ActionTypes"; 2 | 3 | function addCnt() { 4 | return { 5 | type: "ADD_CNT" 6 | }; 7 | } 8 | 9 | function clearCnt() { 10 | return { 11 | type: ActionTypes.ARENA_SCENE_SET_STATE, 12 | state: { 13 | cnt: 0 14 | } 15 | }; 16 | } 17 | 18 | export default { addCnt, clearCnt }; 19 | -------------------------------------------------------------------------------- /src/core/types/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | SceneBundleOptions, 3 | SceneBundle, 4 | PropsPicker, 5 | StateDict 6 | } from "./bundle"; 7 | export { ReducerDict, ReducerDictItem } from "./reducerDict"; 8 | export { 9 | CurtainLoadSceneAction, 10 | DefaultSceneActions, 11 | ActionsDict 12 | } from "./actions"; 13 | export { SceneReducer, ReducerFactory } from "./reducer"; 14 | -------------------------------------------------------------------------------- /example/PassDownState/child/actions.ts: -------------------------------------------------------------------------------- 1 | import ActionTypes from "redux-arena/ActionTypes"; 2 | 3 | function addCnt() { 4 | return { 5 | type: "ADD_CNT" 6 | }; 7 | } 8 | 9 | function clearCnt() { 10 | return { 11 | type: ActionTypes.ARENA_SCENE_SET_STATE, 12 | state: { 13 | cnt: 0 14 | } 15 | }; 16 | } 17 | 18 | export default { addCnt, clearCnt }; 19 | -------------------------------------------------------------------------------- /example/frame/redux/saga.ts: -------------------------------------------------------------------------------- 1 | import { takeLatest, put } from "redux-saga/effects"; 2 | 3 | import ActionTypes from "./ActionTypes"; 4 | 5 | function* clearCnt() { 6 | yield put({ 7 | type: ActionTypes.FRAME_SET_STATE, 8 | state: { cnt: 0 } 9 | }); 10 | } 11 | 12 | export default function* saga() { 13 | yield takeLatest(ActionTypes.FRAME_CLEAR_CNT, clearCnt); 14 | } 15 | -------------------------------------------------------------------------------- /test/sceneBundleForTestA/types.ts: -------------------------------------------------------------------------------- 1 | import { ActionCreator } from "redux"; 2 | 3 | export type State = { 4 | name: string; 5 | pageA: boolean; 6 | sagaCnt: number; 7 | cnt: number; 8 | }; 9 | 10 | export type Props = { 11 | actions: { 12 | addCnt: ActionCreator<{}>; 13 | addCntBySaga: ActionCreator<{}>; 14 | addSagaCnt: ActionCreator<{}>; 15 | }; 16 | } & State; 17 | -------------------------------------------------------------------------------- /example/PassDownState/index.ts: -------------------------------------------------------------------------------- 1 | import { bundleToComponent } from "redux-arena"; 2 | import state from "./state"; 3 | import reducer from "./reducer"; 4 | import Parent from "./Parent"; 5 | import actions from "./actions"; 6 | 7 | export default bundleToComponent({ 8 | Component: Parent, 9 | state, 10 | actions, 11 | reducer, 12 | options: { 13 | vReducerKey: "parent" 14 | } 15 | }); 16 | -------------------------------------------------------------------------------- /example/ScopedPage/index.ts: -------------------------------------------------------------------------------- 1 | import { bundleToComponent } from "redux-arena/tools"; 2 | import state from "./state"; 3 | import reducer from "./reducer"; 4 | import saga from "./saga"; 5 | import actions from "./actions"; 6 | import ScopedPage from "./ScopedPage"; 7 | 8 | export default bundleToComponent({ 9 | Component: ScopedPage, 10 | state, 11 | saga, 12 | reducer, 13 | actions 14 | }); 15 | -------------------------------------------------------------------------------- /src/utils/addStateTreeNode.ts: -------------------------------------------------------------------------------- 1 | import { EnhancedStore } from "../core"; 2 | import ActionTypes from "../core/ActionTypes"; 3 | 4 | export default function addStateTreeNode( 5 | store: EnhancedStore, 6 | pReducerKey: string, 7 | reducerKey: string 8 | ) { 9 | store.dispatch({ 10 | type: ActionTypes.ARENA_STATETREE_NODE_ADD, 11 | pReducerKey, 12 | reducerKey 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /test/sceneBundleForTestA/actions.ts: -------------------------------------------------------------------------------- 1 | export function addCnt() { 2 | return { 3 | type: "ADD_CNT" 4 | }; 5 | } 6 | 7 | export function addCntBySaga() { 8 | return { 9 | type: "ADD_CNT_BY_SAGA" 10 | }; 11 | } 12 | 13 | export function addSagaCnt() { 14 | return { 15 | type: "ADD_SAGA_CNT" 16 | }; 17 | } 18 | 19 | export default { 20 | addCnt, 21 | addCntBySaga, 22 | addSagaCnt 23 | }; 24 | -------------------------------------------------------------------------------- /src/effects/getSceneActions.ts: -------------------------------------------------------------------------------- 1 | import { call, CallEffect } from "redux-saga/effects"; 2 | import getArenaReducerDictEntry from "./getArenaReducerDictEntry"; 3 | 4 | function* _getSceneActions(key: string) { 5 | let entry = yield getArenaReducerDictEntry(key); 6 | return entry.actions; 7 | } 8 | 9 | export default function getSceneActions(key?: string) { 10 | return call(_getSceneActions, key ? key : "_arenaScene"); 11 | } 12 | -------------------------------------------------------------------------------- /src/effects/getSceneState.ts: -------------------------------------------------------------------------------- 1 | import { select, call, CallEffect } from "redux-saga/effects"; 2 | import getArenaReducerDictEntry from "./getArenaReducerDictEntry"; 3 | 4 | function* _getSceneState(key: string) { 5 | let entry = yield getArenaReducerDictEntry(key); 6 | return yield select((state: any) => state[entry.reducerKey]); 7 | } 8 | 9 | export default function getSceneState(key?: string) { 10 | return call(_getSceneState, key ? key : "_arenaScene"); 11 | } 12 | -------------------------------------------------------------------------------- /example/PassDownState/child/types.ts: -------------------------------------------------------------------------------- 1 | import { State as ParentState, Actions as ParentActions } from "../types"; 2 | 3 | export type State = { 4 | name: string; 5 | cnt: number; 6 | }; 7 | 8 | export type ConnectedProps = { 9 | parentState: ParentState; 10 | parentActions: ParentActions; 11 | }; 12 | 13 | export type Actions = { 14 | addCnt: () => void; 15 | clearCnt: () => void; 16 | }; 17 | 18 | export type Props = State & ConnectedProps & { actions: Actions }; 19 | -------------------------------------------------------------------------------- /src/effects/getArenaReducerDictEntry.ts: -------------------------------------------------------------------------------- 1 | import { getContext, call, CallEffect } from "redux-saga/effects"; 2 | 3 | function* _getArenaReducerDictEntry(key: string) { 4 | let dict = yield getContext("arenaReducerDict"); 5 | let entry = dict[key]; 6 | if (entry == null) throw new Error(`can not get entry of key: "${key}"`); 7 | return entry; 8 | } 9 | 10 | export default function getArenaReducerDictEntry(key: string) { 11 | return call(_getArenaReducerDictEntry, key); 12 | } 13 | -------------------------------------------------------------------------------- /src/core/reducers/sceneReducerWrapper.ts: -------------------------------------------------------------------------------- 1 | import { AnyAction } from "redux"; 2 | import { SceneReducer } from "../types"; 3 | 4 | export default function sceneReducerWrapper(srcReducer: SceneReducer) { 5 | return function(state: S, action: AnyAction, isSceneAction: boolean) { 6 | if (isSceneAction || (action.type && action.type.indexOf("@@") === 0)) { 7 | return srcReducer(state, action, isSceneAction); 8 | } else { 9 | return state; 10 | } 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /src/tools/index.ts: -------------------------------------------------------------------------------- 1 | export { default as bundleToComponent } from "./bundleToComponent"; 2 | export { default as bundleToElement } from "./bundleToElement"; 3 | export { 4 | DefaultPickedProps, 5 | Diff, 6 | Omit, 7 | DefaultState, 8 | ActionsProps, 9 | SceneBundleNo, 10 | SceneBundleNoS, 11 | SceneBundleNoPP, 12 | SceneBundleNoA, 13 | SceneBundleNoSPP, 14 | SceneBundleNoSA, 15 | SceneBundleNoSAPP, 16 | SceneBundleBase, 17 | SceneBundlePart 18 | } from "./types"; 19 | -------------------------------------------------------------------------------- /example/ScopedPage/actions.ts: -------------------------------------------------------------------------------- 1 | import ActionTypes from "redux-arena/ActionTypes"; 2 | 3 | function addCnt() { 4 | return { 5 | type: "ADD_CNT" 6 | }; 7 | } 8 | 9 | function clearCnt() { 10 | return { 11 | type: ActionTypes.ARENA_SCENE_SET_STATE, 12 | state: { 13 | cnt: 0 14 | } 15 | }; 16 | } 17 | 18 | function switchDynamicState(isEnabled) { 19 | return { type: "SWITCH_DYNAMIC_STATE", isEnabled }; 20 | } 21 | 22 | export default { addCnt, clearCnt, switchDynamicState }; 23 | -------------------------------------------------------------------------------- /example/frame/redux/reducer.ts: -------------------------------------------------------------------------------- 1 | import ActionTypes from "./ActionTypes"; 2 | import initState from "./state"; 3 | 4 | export default function reducer(state = initState, action) { 5 | switch (action.type) { 6 | case ActionTypes.FRAME_SET_STATE: 7 | return Object.assign({}, state, action.state); 8 | case ActionTypes.ADD_CNT: 9 | return Object.assign({}, state, { 10 | cnt: state.cnt + (action.num != null ? action.num : 1) 11 | }); 12 | default: 13 | return state; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/hocs/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | ArenaScene, 3 | Props as ArenaSceneProps, 4 | ExtraProps as ArenaSceneExtraProps, 5 | State as ArenaSceneState, 6 | Context as ArenaSceneContext 7 | } from "./ArenaScene"; 8 | export { 9 | BundleComponent, 10 | BaseProps as BundleComponentBaseProps, 11 | ConnectedProps as BundleComponentConnectedProps, 12 | Props as BundleComponentProps, 13 | State as BundleComponentState 14 | } from "./BundleComponent"; 15 | export { default as ReducerDictOverrider } from "./ReducerDictOverrider"; 16 | -------------------------------------------------------------------------------- /test/sceneBundleForTestB/index.ts: -------------------------------------------------------------------------------- 1 | import { SceneBundle } from "src"; 2 | import state from "./state"; 3 | import reducer from "./reducer"; 4 | import actions from "./actions"; 5 | import Page from "./Page"; 6 | import { State, Props } from "./types"; 7 | 8 | export default { 9 | Component: Page, 10 | state, 11 | reducer, 12 | actions, 13 | propsPicker: ({ _arenaScene: state }, { _arenaScene: actions }) => ({ 14 | ...state, 15 | actions 16 | }) 17 | } as SceneBundle<{}, {}, {}, {}>; 18 | 19 | export { State, Props } from "./types"; 20 | -------------------------------------------------------------------------------- /example/configureStore.ts: -------------------------------------------------------------------------------- 1 | import thunk from "redux-thunk"; 2 | import { createArenaStore } from "redux-arena"; 3 | import saga from "./frame/redux/saga"; 4 | import reducer from "./frame/redux/reducer"; 5 | import state from "./frame/redux/state"; 6 | 7 | let middlewares = [thunk]; 8 | 9 | export default function configureStore(history) { 10 | let store = createArenaStore( 11 | { frame: reducer }, 12 | { 13 | initialStates: { frame: state }, 14 | middlewares 15 | } 16 | ); 17 | store.runSaga(saga); 18 | return store; 19 | } 20 | -------------------------------------------------------------------------------- /src/core/sagas/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-constant-condition */ 2 | import { 3 | fork, 4 | all, 5 | setContext, 6 | AllEffect, 7 | SetContextEffect 8 | } from "redux-saga/effects"; 9 | import arenaCurtainSaga from "./arenaCurtainSaga"; 10 | import audienceSaga from "./audienceSaga"; 11 | 12 | /** 13 | * This is a function that starts saga 14 | * 15 | * @export 16 | * @param {any} ctx 17 | */ 18 | export default function* root(ctx: any) { 19 | yield setContext(ctx); 20 | yield all([fork(audienceSaga), fork(arenaCurtainSaga)]); 21 | } 22 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | createArenaStore, 3 | SceneBundle, 4 | EnhancedStore, 5 | DefaultSceneActions, 6 | ReducerDict, 7 | ReducerDictItem, 8 | SceneReducer, 9 | StateDict, 10 | ActionsDict 11 | } from "./core"; 12 | export { ArenaScene, ReducerDictOverrider } from "./hocs"; 13 | 14 | export { 15 | bundleToComponent, 16 | bundleToElement, 17 | SceneBundlePart, 18 | DefaultState, 19 | ActionsProps 20 | } from "./tools"; 21 | 22 | import * as effects from "./effects"; 23 | export { effects }; 24 | 25 | export { default as ActionTypes } from "./ActionTypes"; 26 | -------------------------------------------------------------------------------- /test/sceneBundleForTestA/index.ts: -------------------------------------------------------------------------------- 1 | import { SceneBundle } from "src"; 2 | import state from "./state"; 3 | import saga from "./saga"; 4 | import reducer from "./reducer"; 5 | import actions from "./actions"; 6 | import Page from "./Page"; 7 | import { State, Props } from "./types"; 8 | 9 | export default { 10 | Component: Page, 11 | state, 12 | saga, 13 | reducer, 14 | actions, 15 | propsPicker: ({ _arenaScene: state }, { _arenaScene: actions }) => ({ 16 | ...state, 17 | actions 18 | }) 19 | } as SceneBundle<{}, {}, {}, {}>; 20 | 21 | export { State, Props } from "./types"; 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.5.3", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "paths": { 6 | "src": ["src"] 7 | }, 8 | "outDir": "tmp", 9 | "target": "es5", 10 | "sourceMap": true, 11 | "declaration": true, 12 | "module": "esnext", 13 | "removeComments": false, 14 | "strictNullChecks": true, 15 | "noImplicitReturns": true, 16 | "noImplicitUseStrict": true, 17 | "noImplicitAny": true, 18 | "moduleResolution": "node", 19 | "lib": ["es6", "dom"], 20 | "jsx": "react" 21 | }, 22 | "exclude": ["node_modules", "example"] 23 | } 24 | -------------------------------------------------------------------------------- /src/effects/setSceneState.ts: -------------------------------------------------------------------------------- 1 | import { put, call, CallEffect } from "redux-saga/effects"; 2 | import getArenaReducerDictEntry from "./getArenaReducerDictEntry"; 3 | import ActionTypes from "../core/ActionTypes"; 4 | 5 | function* _setSceneState(state: any, key: string) { 6 | let entry = yield getArenaReducerDictEntry(key); 7 | yield put({ 8 | type: ActionTypes.ARENA_SCENE_SET_STATE, 9 | _sceneReducerKey: entry.reducerKey, 10 | state 11 | }); 12 | } 13 | 14 | export default function setSceneState(state: any, key?: string) { 15 | return call(_setSceneState, state, key ? key : "_arenaScene"); 16 | } 17 | -------------------------------------------------------------------------------- /test/integration/ArenaScene/createBundleMounter.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { createMount } from "../../testUtils"; 3 | import { EnhancedStore, SceneBundle } from "src"; 4 | import { MountBundle } from "./types"; 5 | import TestHOC from "./TestHOC"; 6 | 7 | export default function createBundleMounter(): [MountBundle, () => void] { 8 | let [mount, cleanUp] = createMount(); 9 | let mountWithProps = ( 10 | store: EnhancedStore, 11 | sceneBundle: SceneBundle<{}, {}, {}, {}> 12 | ) => mount(); 13 | return [mountWithProps, cleanUp]; 14 | } 15 | -------------------------------------------------------------------------------- /src/effects/index.ts: -------------------------------------------------------------------------------- 1 | export { default as setSceneState } from "./setSceneState"; 2 | export { default as getSceneState } from "./getSceneState"; 3 | export { 4 | default as getArenaReducerDictEntry 5 | } from "./getArenaReducerDictEntry"; 6 | export { default as getSceneActions } from "./getSceneActions"; 7 | export { default as takeSceneAction, TakeSceneAction } from "./takeSceneAction"; 8 | export { default as takeEverySceneAction } from "./takeEverySceneAction"; 9 | export { default as takeLatestSceneAction } from "./takeLatestSceneAction"; 10 | export { default as putSceneAction, PutSceneAction } from "./putSceneAction"; 11 | -------------------------------------------------------------------------------- /src/hocs/ReducerDictOverrider/ReducerDictOverrider.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as PropTypes from "prop-types"; 3 | import { ActionCreatorsMapObject } from "redux"; 4 | import { ReducerDictOverriderProps } from "./types"; 5 | 6 | export default class ReducerDictOverrider extends React.Component< 7 | ReducerDictOverriderProps 8 | > { 9 | static childContextTypes = { 10 | arenaReducerDict: PropTypes.object 11 | }; 12 | 13 | getChildContext() { 14 | return { 15 | arenaReducerDict: this.props.reducerDict 16 | }; 17 | } 18 | 19 | render() { 20 | return this.props.children; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /example/ModuleReUse/reducer.ts: -------------------------------------------------------------------------------- 1 | import initState from "./state"; 2 | import ActionTypes from "./ActionTypes"; 3 | 4 | export default function reducer(state = initState, action) { 5 | let newPanelNum; 6 | switch (action.type) { 7 | case ActionTypes.ADD_PANEL: 8 | newPanelNum = state.panelNum + 1; 9 | return Object.assign({}, state, { 10 | panelNum: newPanelNum > 10 ? 10 : newPanelNum 11 | }); 12 | case ActionTypes.DEL_PANEL: 13 | newPanelNum = state.panelNum - 1; 14 | return Object.assign({}, state, { 15 | panelNum: newPanelNum < 0 ? 0 : newPanelNum 16 | }); 17 | default: 18 | return state; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/core/reducers/index.ts: -------------------------------------------------------------------------------- 1 | export { default as arenaReducer } from "./arenaReducer"; 2 | export { default as createSceneReducer } from "./createSceneReducer"; 3 | export { default as createCurtainReducer } from "./createCurtainReducer"; 4 | export { default as getArenaInitState } from "./getArenaInitState"; 5 | export { default as getSceneInitState } from "./getSceneInitState"; 6 | export { default as sceneReducerWrapper } from "./sceneReducerWrapper"; 7 | export { 8 | CurtainState, 9 | CurtainReduxInfo, 10 | CurtainMutableObj, 11 | ArenaState, 12 | RootState, 13 | StateTreeNode, 14 | StateTreeDictItem, 15 | StateTreeDict, 16 | StateTree 17 | } from "./types"; 18 | -------------------------------------------------------------------------------- /example/expressServer.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const express = require("express"); 3 | const compression = require("compression"); 4 | 5 | const port = process.env.npm_package_config_port || 8080; 6 | const host = process.env.npm_package_config_host || "localhost"; 7 | 8 | const app = express(); 9 | 10 | app.use(compression()); 11 | app.use("/redux-arena", express.static(path.join(__dirname, "build"))); 12 | app.get("*", function(req, res) { 13 | res.sendFile(path.join(__dirname, "build", "index.html")); 14 | }); 15 | app.listen(port, (err, result) => { 16 | if (err) { 17 | return console.log(err); 18 | } 19 | console.log(`The server is running at http://${host}:${port}/`); 20 | }); 21 | -------------------------------------------------------------------------------- /src/hocs/BundleComponent/actions.ts: -------------------------------------------------------------------------------- 1 | import { ActionCreatorsMapObject } from "redux"; 2 | import ActionTypes from "../../core/ActionTypes"; 3 | import { CurtainLoadSceneAction, ReducerDict, SceneBundle } from "../../core"; 4 | 5 | export function curtainLoadScene< 6 | P extends PP, 7 | S, 8 | A extends ActionCreatorsMapObject, 9 | PP 10 | >( 11 | arenaReducerDict: ReducerDict, 12 | sceneBundle: SceneBundle, 13 | isInitial: any, 14 | loadedCb: () => void 15 | ): CurtainLoadSceneAction { 16 | return { 17 | type: ActionTypes.ARENA_CURTAIN_LOAD_SCENE, 18 | arenaReducerDict, 19 | sceneBundle, 20 | isInitial, 21 | loadedCb 22 | }; 23 | } 24 | 25 | export default { curtainLoadScene }; 26 | -------------------------------------------------------------------------------- /src/tools/autoFill.ts: -------------------------------------------------------------------------------- 1 | import { ActionCreatorsMapObject } from "redux"; 2 | import { StateDict } from "../core"; 3 | import { DefaultPickedProps } from "./types"; 4 | import ActionTypes from "../ActionTypes"; 5 | 6 | export function defaultPropsPicker( 7 | { _arenaScene: state }: StateDict, 8 | { _arenaScene: actions }: { _arenaScene: {} } 9 | ): DefaultPickedProps { 10 | return Object.assign({}, state, { 11 | actions 12 | }); 13 | } 14 | 15 | export const defaultActions = { 16 | setState: (state: any) => ({ type: ActionTypes.ARENA_SCENE_SET_STATE, state }) 17 | }; 18 | 19 | export const defaultReducerCreator = (defaultState: object = {}) => ( 20 | state: any = defaultState 21 | ) => state; 22 | -------------------------------------------------------------------------------- /example/ModuleReUse/OpPanel/OpPanel.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Actions } from "./types"; 3 | 4 | export default class OpPanel extends React.Component<{ 5 | actions: Actions; 6 | step: number; 7 | }> { 8 | addCnt = () => this.props.actions.addCnt(this.props.step); 9 | 10 | render() { 11 | return ( 12 |
20 |
Add-Total-Count Panel
21 | 24 |
25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/core/types/actions.ts: -------------------------------------------------------------------------------- 1 | import { ActionCreatorsMapObject } from "redux"; 2 | import ActionTypes from "../ActionTypes"; 3 | import { ReducerDict } from "./reducerDict"; 4 | import { SceneBundle } from "./bundle"; 5 | 6 | export type CurtainLoadSceneAction< 7 | P extends PP, 8 | S, 9 | A extends ActionCreatorsMapObject, 10 | PP 11 | > = { 12 | type: ActionTypes.ARENA_CURTAIN_LOAD_SCENE; 13 | arenaReducerDict: ReducerDict; 14 | sceneBundle: SceneBundle; 15 | isInitial: boolean; 16 | loadedCb: () => void; 17 | }; 18 | 19 | export type DefaultSceneActions = { 20 | setState: (state: S) => void; 21 | }; 22 | 23 | export type ActionsDict = { 24 | _arenaScene: A; 25 | $0: A; 26 | } & D; 27 | -------------------------------------------------------------------------------- /src/effects/putSceneAction.ts: -------------------------------------------------------------------------------- 1 | import { AnyAction } from "redux"; 2 | import { call, CallEffect, put } from "redux-saga/effects"; 3 | import getArenaReducerDictEntry from "./getArenaReducerDictEntry"; 4 | 5 | function* _putSceneAction(action: AnyAction, key: string) { 6 | let entry = yield getArenaReducerDictEntry(key); 7 | let newAction = Object.assign({}, action, { 8 | _sceneReducerKey: entry.reducerKey 9 | }); 10 | yield put(newAction); 11 | } 12 | 13 | export type PutSceneAction = { 14 | (action: AnyAction, key?: string): void; 15 | }; 16 | 17 | const putSceneAction: any = function(action: AnyAction, key?: string) { 18 | return call(_putSceneAction, action, key ? key : "_arenaScene"); 19 | }; 20 | 21 | export default putSceneAction; 22 | -------------------------------------------------------------------------------- /example/server.js: -------------------------------------------------------------------------------- 1 | var webpack = require("webpack"); 2 | var WebpackDevServer = require("webpack-dev-server"); 3 | var config = require("./webpack.config.dev"); 4 | var path = require("path"); 5 | const port = process.env.npm_package_config_port || 8080; 6 | const host = process.env.npm_package_config_host || "localhost"; 7 | 8 | new WebpackDevServer(webpack(config), { 9 | publicPath: config.output.publicPath, 10 | hot: true, 11 | historyApiFallback: { 12 | disableDotRule: true 13 | }, 14 | stats: { 15 | colors: true, 16 | chunks: false, 17 | "errors-only": true 18 | } 19 | }).listen(port, host, function(err, result) { 20 | if (err) { 21 | return console.error(err); 22 | } 23 | console.log(`Listening at http://${host}:${port}/`); 24 | }); 25 | -------------------------------------------------------------------------------- /src/core/reducers/types/arenaState.ts: -------------------------------------------------------------------------------- 1 | import { Map, List } from "immutable"; 2 | 3 | export type StateTreeNode = { 4 | reducerKey: string; 5 | pReducerKey: string | null | undefined; 6 | children: Map; 7 | }; 8 | 9 | export type StateTreeDictItem = { 10 | reducerKey: { 11 | path: List; 12 | isObsolete: boolean; 13 | }; 14 | }; 15 | 16 | export type StateTreeDict = Map>; 17 | 18 | export type StateTree = Map>; 19 | 20 | export type ArenaState = { 21 | audienceSagaTask: object | null | undefined; 22 | propsLock: boolean; 23 | stateTree: StateTree; 24 | stateTreeDict: StateTreeDict; 25 | }; 26 | 27 | export type RootState = { 28 | arena: ArenaState; 29 | [key: string]: any; 30 | }; 31 | -------------------------------------------------------------------------------- /src/core/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | createArenaStore, 3 | createEnhancedStore, 4 | EnhancedStore 5 | } from "./enhancedRedux"; 6 | export { 7 | CurtainLoadSceneAction, 8 | SceneReducer, 9 | SceneBundleOptions, 10 | SceneBundle, 11 | ReducerDict, 12 | ReducerDictItem, 13 | ReducerFactory, 14 | DefaultSceneActions, 15 | ActionsDict, 16 | StateDict, 17 | PropsPicker 18 | } from "./types"; 19 | export { 20 | arenaReducer, 21 | createSceneReducer, 22 | createCurtainReducer, 23 | getArenaInitState, 24 | getSceneInitState, 25 | sceneReducerWrapper, 26 | CurtainState, 27 | CurtainReduxInfo, 28 | CurtainMutableObj, 29 | ArenaState, 30 | RootState, 31 | StateTreeNode, 32 | StateTreeDictItem, 33 | StateTreeDict, 34 | StateTree 35 | } from "./reducers"; 36 | -------------------------------------------------------------------------------- /src/hocs/ArenaScene/types.ts: -------------------------------------------------------------------------------- 1 | import { ComponentClass, ComponentElement } from "react"; 2 | import { EnhancedStore, ReducerDict, SceneBundle } from "../../core"; 3 | import { Props as BCProps } from "../BundleComponent"; 4 | 5 | export type ExtraProps = { 6 | reducerKey?: string; 7 | vReducerKey?: string; 8 | }; 9 | export type Props = ExtraProps & { 10 | sceneProps?: {}; 11 | sceneBundle: SceneBundle<{}, {}, {}, {}>; 12 | }; 13 | 14 | export type State = { 15 | parentReducerKey: string; 16 | arenaReducerDict: ReducerDict; 17 | ConnectedBundleComponent: ComponentClass; 18 | connectedBundleElement: ComponentElement; 19 | }; 20 | 21 | export type Context = { 22 | store: EnhancedStore; 23 | arenaReducerDict: ReducerDict | null | undefined; 24 | }; 25 | -------------------------------------------------------------------------------- /test/integration/ArenaScene/TestHOC.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Provider } from "react-redux"; 3 | import { ArenaScene, EnhancedStore, SceneBundle } from "src"; 4 | 5 | export type TestHOCProps = { 6 | store: EnhancedStore; 7 | sceneBundle: SceneBundle<{}, {}, {}, {}>; 8 | reducerKey?: string; 9 | vReducerKey?: string; 10 | }; 11 | 12 | export default class TestHOC extends React.Component { 13 | render() { 14 | let props = this.props; 15 | let ProviderA = Provider; 16 | return ( 17 | 18 | 23 | 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/core/sagas/audienceSaga.ts: -------------------------------------------------------------------------------- 1 | import { takeLatest, fork, put, ForkEffect } from "redux-saga/effects"; 2 | import { Action } from "redux"; 3 | import ActionTypes from "../ActionTypes"; 4 | 5 | /** 6 | *This function is run at the beginning of the incoming saga, and then saved in the arena 7 | * 8 | * @param {any} { saga } 9 | */ 10 | 11 | interface InitAudienceAction extends Action { 12 | saga: () => null; 13 | } 14 | 15 | function* initAudienceSaga({ saga }: InitAudienceAction) { 16 | let newAudienceSagaTask = yield fork(saga); 17 | yield put({ 18 | type: ActionTypes.ARENA_SET_STATE, 19 | state: { 20 | audienceSagaTask: newAudienceSagaTask 21 | } 22 | }); 23 | } 24 | 25 | export default function* saga() { 26 | yield takeLatest(ActionTypes.ARENA_INIT_AUDIENCE_SAGA, initAudienceSaga); 27 | } 28 | -------------------------------------------------------------------------------- /src/core/enhancedRedux/createArenaStore.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { spy } from "sinon"; 3 | import { applyMiddleware } from "redux"; 4 | import createArenaStore from "./createArenaStore"; 5 | 6 | describe("Redux-Arena start up", () => { 7 | it("should initial with config correctly", () => { 8 | let store = createArenaStore( 9 | { frame: state => state || {} }, 10 | { 11 | initialStates: { frame: {} }, 12 | middlewares: [ 13 | ({ dispatch, getState }) => next => action => { 14 | return next(action); 15 | } 16 | ] 17 | } 18 | ); 19 | let state = store.getState(); 20 | expect(state.arena).to.not.be.null; 21 | expect(state.frame).to.not.be.null; 22 | expect(state.frame.audienceSagaTask).to.not.be.null; 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /example/PassDownState/child/index.ts: -------------------------------------------------------------------------------- 1 | import { StateDict, ActionsDict, bundleToComponent } from "redux-arena"; 2 | import state from "./state"; 3 | import reducer from "./reducer"; 4 | import Child from "./Child"; 5 | import actions from "./actions"; 6 | import { Props, State, Actions } from "./types"; 7 | import { State as ParentState, Actions as ParentActions } from "../types"; 8 | 9 | const propsPicker = ( 10 | { $0: state, parent: parentState }: StateDict, 11 | { $0: actions, parent: parentActions }: ActionsDict 12 | ) => ({ 13 | name: state.name, 14 | cnt: state.cnt, 15 | actions, 16 | parentState: parentState, 17 | parentActions: parentActions 18 | }); 19 | 20 | export default bundleToComponent({ 21 | Component: Child, 22 | state, 23 | actions, 24 | reducer, 25 | propsPicker 26 | }); 27 | -------------------------------------------------------------------------------- /example/PassDownState/Parent.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import Child from "./child"; 3 | import { State, Actions } from "./types"; 4 | 5 | export default class Parent extends React.Component< 6 | State & { actions: Actions } 7 | > { 8 | render() { 9 | let { name, cnt } = this.props; 10 | return ( 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
state_keystate_value
name:{name}
cnt:{cnt}
28 | 29 |
30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/effects/takeEverySceneAction.ts: -------------------------------------------------------------------------------- 1 | import { fork, ForkEffect, take, Pattern } from "redux-saga/effects"; 2 | import getArenaReducerDictEntry from "./getArenaReducerDictEntry"; 3 | 4 | function* _takeEverySceneAction( 5 | pattern: Pattern, 6 | saga: (...params: any[]) => any, 7 | key: string, 8 | args: any 9 | ) { 10 | while (true) { 11 | let action = yield take(pattern); 12 | let entry = yield getArenaReducerDictEntry(key); 13 | if (action._sceneReducerKey === entry.reducerKey) { 14 | yield fork(saga, ...args, action); 15 | } 16 | } 17 | } 18 | 19 | export default function takeEverySceneAction( 20 | pattern: Pattern, 21 | saga: (...params: any[]) => any, 22 | key?: string, 23 | ...args: any[] 24 | ) { 25 | return fork( 26 | _takeEverySceneAction, 27 | pattern, 28 | saga, 29 | key ? key : "_arenaScene", 30 | args 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /src/utils/replaceReducer.ts: -------------------------------------------------------------------------------- 1 | import ActionTypes from "../core/ActionTypes"; 2 | import { EnhancedStore, SceneReducer } from "../core"; 3 | export function sceneReplaceReducer( 4 | store: EnhancedStore, 5 | reducerKey: string, 6 | reducerFactory: (reducerKey: string) => SceneReducer<{}>, 7 | state: {} | null | undefined 8 | ) { 9 | store.dispatch({ 10 | type: ActionTypes.ARENA_GLOBAL_PROPSPICKER_LOCK, 11 | lock: true 12 | }); 13 | let newReducerKey = store.replaceSingleReducer({ 14 | reducerKey, 15 | reducer: reducerFactory(reducerKey), 16 | state 17 | }); 18 | if (state) 19 | store.dispatch({ 20 | type: ActionTypes.ARENA_SCENE_REPLACE_STATE, 21 | _sceneReducerKey: newReducerKey, 22 | state 23 | }); 24 | store.dispatch({ 25 | type: ActionTypes.ARENA_GLOBAL_PROPSPICKER_LOCK, 26 | lock: false 27 | }); 28 | return reducerKey; 29 | } 30 | -------------------------------------------------------------------------------- /test/sceneBundleForTestB/Page.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Props } from "./types"; 3 | 4 | export default class PageB extends React.Component { 5 | render() { 6 | return ( 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
state_keystate_value
name:{this.props.name}
pageB:{this.props.pageB}
cnt:{this.props.cnt}
28 | 29 |
30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /example/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as ReactDOM from "react-dom"; 3 | import { Provider } from "react-redux"; 4 | import Frame from "./frame/Frame"; 5 | import configureStore from "./configureStore"; 6 | 7 | let store = configureStore(history); 8 | 9 | document.getElementById("loadingsStyle").remove(); 10 | document.getElementById("app").className = ""; 11 | 12 | let appDom = document.getElementById("app"); 13 | 14 | let render = (FrameComponent, version) => { 15 | let AProvider = Provider as any; 16 | ReactDOM.render( 17 | 18 | 19 | , 20 | appDom 21 | ); 22 | }; 23 | 24 | let version = 0; 25 | render(Frame, version); 26 | if ((module as any).hot) { 27 | (module as any).hot.accept("./frame/Frame", () => { 28 | let UpdatedFrame = require("./frame/Frame").default; 29 | render(UpdatedFrame, ++version); 30 | }); 31 | } 32 | -------------------------------------------------------------------------------- /src/core/ActionTypes.ts: -------------------------------------------------------------------------------- 1 | enum ActionTypes { 2 | ARENA_SET_STATE = "ARENA_SET_STATE", 3 | ARENA_REPLACE_STATE = "ARENA_REPLACE_STATE", 4 | 5 | ARENA_SCENE_SET_STATE = "ARENA_SCENE_SET_STATE", 6 | ARENA_SCENE_REPLACE_STATE = "ARENA_SCENE_REPLACE_STATE", 7 | 8 | ARENA_CURTAIN_SET_STATE = "ARENA_CURTAIN_SET_STATE", 9 | ARENA_CURTAIN_REPLACE_STATE = "ARENA_CURTAIN_REPLACE_STATE", 10 | ARENA_CURTAIN_INIT_SAGA = "ARENA_CURTAIN_INIT_SAGA", 11 | ARENA_CURTAIN_CLEAR_REDUX = "ARENA_CURTAIN_CLEAR_REDUX", 12 | ARENA_CURTAIN_LOAD_SCENE = "ARENA_CURTAIN_LOAD_SCENE", 13 | 14 | ARENA_INIT_AUDIENCE_SAGA = "ARENA_INIT_AUDIENCE_SAGA", 15 | 16 | ARENA_GLOBAL_PROPSPICKER_LOCK = "ARENA_GLOBAL_PROPSPICKER_LOCK", 17 | 18 | ARENA_STATETREE_NODE_ADD = "ARENA_STATETREE_NODE_ADD", 19 | ARENA_STATETREE_NODE_DISABLE = "ARENA_STATETREE_NODE_DISABLE", 20 | ARENA_STATETREE_NODE_DELETE = "ARENA_STATETREE_NODE_DELETE" 21 | } 22 | 23 | export default ActionTypes; 24 | -------------------------------------------------------------------------------- /src/effects/takeLatestSceneAction.ts: -------------------------------------------------------------------------------- 1 | import { fork, take, cancel, Pattern, ForkEffect } from "redux-saga/effects"; 2 | import getArenaReducerDictEntry from "./getArenaReducerDictEntry"; 3 | 4 | function* _takeLatestSceneAction( 5 | pattern: Pattern, 6 | saga: () => void, 7 | key: string, 8 | args: any 9 | ) { 10 | let lastTask; 11 | while (true) { 12 | let action = yield take(pattern); 13 | let entry = yield getArenaReducerDictEntry(key); 14 | if (action._sceneReducerKey === entry.reducerKey) { 15 | if (lastTask) yield cancel(lastTask); 16 | lastTask = yield fork(saga, ...args, action); 17 | } 18 | } 19 | } 20 | 21 | export default function takeLatestSceneAction( 22 | pattern: Pattern, 23 | saga: (...params: any[]) => any, 24 | key?: string, 25 | ...args: any[] 26 | ) { 27 | return fork( 28 | _takeLatestSceneAction, 29 | pattern, 30 | saga, 31 | key ? key : "_arenaScene", 32 | args 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /src/hocs/BundleComponent/types.ts: -------------------------------------------------------------------------------- 1 | import { ActionCreatorsMapObject } from "redux"; 2 | import { 3 | CurtainLoadSceneAction, 4 | CurtainState, 5 | ReducerDict, 6 | SceneBundle 7 | } from "../../core"; 8 | 9 | export type State = { 10 | loadedPromise: Promise; 11 | }; 12 | 13 | export type BaseProps = CurtainState<{}> & { 14 | clearCurtain: () => void; 15 | }; 16 | 17 | export type ConnectedProps = BaseProps & 18 | Props & { 19 | curtainLoadScene: CurtainLoadScene<{}, {}, {}, {}>; 20 | }; 21 | 22 | export type Props = { 23 | arenaReducerDict: ReducerDict; 24 | sceneBundle: SceneBundle<{}, {}, {}, {}>; 25 | sceneProps: any; 26 | }; 27 | 28 | export type CurtainLoadScene< 29 | P extends PP, 30 | S, 31 | A extends ActionCreatorsMapObject, 32 | PP 33 | > = ( 34 | arenaReducerDict: ReducerDict, 35 | sceneBundle: SceneBundle, 36 | isInitial: any, 37 | loadedCb: () => void 38 | ) => CurtainLoadSceneAction; 39 | -------------------------------------------------------------------------------- /test/testUtils/createMount.ts: -------------------------------------------------------------------------------- 1 | import { ReactElement } from "react"; 2 | import { unmountComponentAtNode } from "react-dom"; 3 | import { configure } from "enzyme"; 4 | import * as Adapter from "enzyme-adapter-react-16"; 5 | configure({ adapter: new Adapter() }); 6 | 7 | import { mount } from "enzyme"; 8 | 9 | // Generate an enhanced mount function. 10 | export default function createMount() { 11 | let attachTo = window.document.createElement("div"); 12 | attachTo.setAttribute("id", "app"); 13 | window.document.body.insertBefore(attachTo, window.document.body.firstChild); 14 | 15 | let mountWithContext: any = function( 16 | node: ReactElement<{}>, 17 | mountOptions = {} 18 | ) { 19 | return mount(node, { 20 | attachTo, 21 | ...mountOptions 22 | }); 23 | }; 24 | return [ 25 | mountWithContext, 26 | () => { 27 | unmountComponentAtNode(attachTo); 28 | attachTo.parentNode && attachTo.parentNode.removeChild(attachTo); 29 | } 30 | ]; 31 | } 32 | -------------------------------------------------------------------------------- /src/core/reducers/types/curtainState.ts: -------------------------------------------------------------------------------- 1 | import { SFC } from "react"; 2 | import { ActionCreatorsMapObject } from "redux"; 3 | import { SceneBundle, ReducerDict, SceneBundleOptions } from "../../types"; 4 | 5 | export type CurtainReduxInfo = { 6 | reducerKey: string; 7 | state?: S | null | undefined; 8 | origArenaReducerDict: ReducerDict; 9 | actions: ActionCreatorsMapObject | null | undefined; 10 | options: SceneBundleOptions; 11 | saga: (...params: any[]) => any; 12 | bindedActions: ActionCreatorsMapObject; 13 | arenaReducerDict: ReducerDict; 14 | }; 15 | 16 | export type CurtainMutableObj = { 17 | isObsolete: boolean; 18 | }; 19 | 20 | export type CurtainState< 21 | P extends PP, 22 | S = {}, 23 | A extends ActionCreatorsMapObject = {}, 24 | PP = {} 25 | > = { 26 | PlayingScene: SFC

| null | undefined; 27 | curSceneBundle: SceneBundle | null | undefined; 28 | reduxInfo: CurtainReduxInfo | null | undefined; 29 | mutableObj: CurtainMutableObj; 30 | }; 31 | -------------------------------------------------------------------------------- /src/core/types/bundle.ts: -------------------------------------------------------------------------------- 1 | import { ComponentType } from "react"; 2 | import { ActionCreatorsMapObject } from "redux"; 3 | import { ActionsDict } from "./actions"; 4 | import { SceneReducer } from "./reducer"; 5 | import { CurtainState } from "../reducers"; 6 | import { RootState } from "../reducers/types"; 7 | 8 | export type StateDict = { 9 | _arenaScene: S; 10 | _arenaCurtain: CurtainState; 11 | $0: S; 12 | } & D; 13 | 14 | export type PropsPicker< 15 | P extends PP, 16 | S, 17 | A extends ActionCreatorsMapObject, 18 | PP 19 | > = (stateDict: StateDict, actionsDict: ActionsDict) => PP; 20 | 21 | export type SceneBundleOptions = { 22 | reducerKey?: string; 23 | vReducerKey?: string; 24 | isSceneActions?: boolean; 25 | isSceneReducer?: boolean; 26 | }; 27 | 28 | export type SceneBundle< 29 | P extends PP, 30 | S, 31 | A extends ActionCreatorsMapObject, 32 | PP 33 | > = { 34 | Component: ComponentType

; 35 | state: S; 36 | actions: A; 37 | propsPicker: PropsPicker; 38 | saga?: (...params: any[]) => any; 39 | reducer: SceneReducer; 40 | options?: SceneBundleOptions; 41 | }; 42 | -------------------------------------------------------------------------------- /src/core/reducers/createSceneReducer.ts: -------------------------------------------------------------------------------- 1 | import { AnyAction } from "redux"; 2 | import ActionTypes from "../ActionTypes"; 3 | import getSceneInitState from "./getSceneInitState"; 4 | import { SceneReducer } from "../types"; 5 | 6 | function sceneReducer(state = getSceneInitState(), action: AnyAction) { 7 | switch (action.type) { 8 | case ActionTypes.ARENA_SCENE_SET_STATE: 9 | return Object.assign({}, state, action.state); 10 | case ActionTypes.ARENA_SCENE_REPLACE_STATE: 11 | return Object.assign({}, action.state); 12 | default: 13 | return state; 14 | } 15 | } 16 | 17 | export default function createSceneReducer( 18 | extendSceneReducer: SceneReducer, 19 | initState: any, 20 | sceneReducerKey: string 21 | ) { 22 | return function(state = initState, action: AnyAction) { 23 | let isSceneAction = 24 | action._sceneReducerKey && action._sceneReducerKey === sceneReducerKey 25 | ? true 26 | : false; 27 | if (extendSceneReducer) { 28 | state = extendSceneReducer(state, action, isSceneAction); 29 | } 30 | return isSceneAction ? sceneReducer(state, action) : state; 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /example/ModuleReUse/Container.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import OpPanel from "./OpPanel"; 3 | import { State, Actions } from "./types"; 4 | 5 | export default class Container extends React.Component< 6 | State & { actions: Actions } 7 | > { 8 | render() { 9 | let { panelNum, actions } = this.props; 10 | return ( 11 |

36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test/sceneBundleForTestA/Page.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Props } from "./types"; 3 | 4 | export default class PageA extends React.Component { 5 | componentWillMount() { 6 | this.props.actions.addCnt(); 7 | this.props.actions.addSagaCnt(); 8 | this.props.actions.addCntBySaga(); 9 | } 10 | 11 | render() { 12 | return ( 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
state_keystate_value
name:{this.props.name}
pageA:{this.props.pageA}
sagaCnt:{this.props.sagaCnt}
cnt:{this.props.cnt}
38 | 39 |
40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /test/sceneBundleForTestA/saga.ts: -------------------------------------------------------------------------------- 1 | import { fork, ForkEffect } from "redux-saga/effects"; 2 | import { delay } from "redux-saga"; 3 | import { 4 | setSceneState, 5 | getSceneState, 6 | takeLatestSceneAction, 7 | takeEverySceneAction, 8 | takeSceneAction, 9 | putSceneAction, 10 | getSceneActions 11 | } from "src/effects"; 12 | 13 | function* addSagaCnt() { 14 | let { sagaCnt } = yield getSceneState(); 15 | yield setSceneState({ sagaCnt: sagaCnt + 1 }); 16 | } 17 | 18 | function* addCntBySaga() { 19 | while (true) { 20 | let action = yield takeSceneAction("ADD_CNT_BY_SAGA"); 21 | yield putSceneAction({ type: "ADD_CNT" }); 22 | } 23 | } 24 | 25 | function* addCntBySagaMaybe() { 26 | while (true) { 27 | yield takeSceneAction.maybe("ADD_CNT_BY_SAGA"); 28 | yield putSceneAction({ type: "ADD_CNT" }); 29 | } 30 | } 31 | 32 | function* sceneActionForward() { 33 | let { addCnt } = yield getSceneActions(); 34 | addCnt(); 35 | } 36 | 37 | export default function* saga() { 38 | yield takeLatestSceneAction("ADD_SAGA_CNT", addSagaCnt); 39 | yield takeEverySceneAction("ADD_CNT_BY_SAGA", sceneActionForward); 40 | yield fork(addCntBySaga); 41 | yield fork(addCntBySagaMaybe); 42 | } 43 | -------------------------------------------------------------------------------- /src/core/reducers/createCurtainReducer.ts: -------------------------------------------------------------------------------- 1 | import { ActionCreatorsMapObject, ActionCreator } from "redux"; 2 | import { ForkEffect } from "redux-saga/effects"; 3 | import { SceneReducer } from "../types"; 4 | import ActionTypes from "../ActionTypes"; 5 | import getCurtainInitState from "./getCurtainInitState"; 6 | import { AnyAction } from "redux"; 7 | import { CurtainState } from "./types"; 8 | 9 | function curtainReducer( 10 | state: CurtainState<{}>, 11 | action: AnyAction, 12 | bindedReducerKey: string 13 | ) { 14 | switch (action.type) { 15 | case ActionTypes.ARENA_CURTAIN_SET_STATE: 16 | return Object.assign({}, state, action.state); 17 | case ActionTypes.ARENA_CURTAIN_REPLACE_STATE: 18 | return Object.assign({}, action.state); 19 | default: 20 | return state; 21 | } 22 | } 23 | 24 | export default function createCurtainReducer( 25 | bindedReducerKey: string 26 | ): SceneReducer> { 27 | return function( 28 | state: CurtainState<{}> = getCurtainInitState(), 29 | action: AnyAction 30 | ) { 31 | if (bindedReducerKey === action._reducerKey) { 32 | state = curtainReducer(state, action, bindedReducerKey); 33 | } 34 | return state; 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /example/ScopedPage/saga.ts: -------------------------------------------------------------------------------- 1 | import { fork } from "redux-saga/effects"; 2 | import { delay } from "redux-saga"; 3 | import { 4 | setSceneState, 5 | getSceneState, 6 | takeLatestSceneAction 7 | } from "redux-arena/effects"; 8 | 9 | function randLetter() { 10 | var letters = [ 11 | "a", 12 | "b", 13 | "c", 14 | "d", 15 | "e", 16 | "f", 17 | "g", 18 | "h", 19 | "i", 20 | "j", 21 | "k", 22 | "l", 23 | "m", 24 | "n", 25 | "o", 26 | "p", 27 | "q", 28 | "r", 29 | "s", 30 | "t", 31 | "u", 32 | "v", 33 | "w", 34 | "x", 35 | "y", 36 | "z" 37 | ]; 38 | var letter = letters[Math.floor(Math.random() * letters.length)]; 39 | return letter; 40 | } 41 | 42 | function* setLetter() { 43 | while (true) { 44 | yield setSceneState({ dynamicState: randLetter() }); 45 | yield delay(500); 46 | } 47 | } 48 | 49 | function* switchDynamicState({ isEnabled }) { 50 | let { isDynamicStateEnable } = yield getSceneState(); 51 | if (isEnabled) { 52 | yield fork(setLetter); 53 | } 54 | yield setSceneState({ 55 | isDynamicStateEnable: isEnabled 56 | }); 57 | } 58 | 59 | export default function* saga() { 60 | yield takeLatestSceneAction("SWITCH_DYNAMIC_STATE", switchDynamicState); 61 | } 62 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-arena-example", 3 | "version": "0.0.0", 4 | "description": "Example for redux-arena", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "node server.js", 8 | "build": "npm run clean:build &&webpack --colors --config webpack.config.prod.js", 9 | "clean:build": "rimraf build", 10 | "start:build": "node expressServer.js" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/hapood/redux-arena.git" 15 | }, 16 | "license": "MIT", 17 | "bugs": { 18 | "url": "https://github.com/hapood/redux-arena/issues" 19 | }, 20 | "homepage": "https://github.com/hapood/redux-arena#readme", 21 | "dependencies": { 22 | "@types/react": "^16.0.22", 23 | "@types/react-dom": "^16.0.3", 24 | "react": "^16.1.1", 25 | "react-dom": "^16.1.1", 26 | "redux-arena": "^0.9.0-rc-2", 27 | "redux-thunk": "^2.2.0" 28 | }, 29 | "devDependencies": { 30 | "express": "^4.16.2", 31 | "html-webpack-plugin": "^2.30.1", 32 | "react-hot-loader": "^3.1.2", 33 | "script-ext-html-webpack-plugin": "^1.8.8", 34 | "ts-loader": "^3.1.1", 35 | "typescript": "^2.6.1", 36 | "webpack": "^3.8.1", 37 | "webpack-dev-server": "^2.9.4" 38 | }, 39 | "config": { 40 | "host": "localhost", 41 | "port": 8080 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/hocs/BundleComponent/curtainConnect.ts: -------------------------------------------------------------------------------- 1 | import { ComponentClass, StatelessComponent } from "react"; 2 | import { 3 | bindActionCreators, 4 | Dispatch, 5 | ActionCreator, 6 | ActionCreatorsMapObject, 7 | AnyAction 8 | } from "redux"; 9 | import { connect } from "react-redux"; 10 | import actions from "./actions"; 11 | import BundleComponent from "./BundleComponent"; 12 | import { Props, BaseProps, CurtainLoadScene } from "./types"; 13 | import { CurtainState } from "../../core"; 14 | 15 | export default function curtainConnect( 16 | reducerKey: string, 17 | clearCurtain: () => void 18 | ) { 19 | let mapDispatchToProps = ( 20 | dispatch: Dispatch 21 | ): { curtainLoadScene: CurtainLoadScene<{}, {}, {}, {}> } => { 22 | return bindActionCreators(actions, dispatch); 23 | }; 24 | 25 | let mapStateToProps = (state: any): BaseProps => { 26 | return { 27 | PlayingScene: state[reducerKey].PlayingScene, 28 | curSceneBundle: state[reducerKey].curSceneBundle, 29 | reduxInfo: state[reducerKey].reduxInfo, 30 | mutableObj: state[reducerKey].mutableObj, 31 | clearCurtain 32 | }; 33 | }; 34 | 35 | let ConnectedComponent = connect(mapStateToProps, mapDispatchToProps)( 36 | BundleComponent 37 | ); 38 | 39 | ConnectedComponent.displayName = `curtainConnect({reducerKey:${reducerKey}})`; 40 | return ConnectedComponent; 41 | } 42 | -------------------------------------------------------------------------------- /src/effects/takeSceneAction.ts: -------------------------------------------------------------------------------- 1 | import { take, call, CallEffect, Pattern } from "redux-saga/effects"; 2 | import getArenaReducerDictEntry from "./getArenaReducerDictEntry"; 3 | import { END } from "redux-saga"; 4 | 5 | function* _takeSceneAction(pattern: Pattern, key: string) { 6 | while (true) { 7 | let action = yield take(pattern); 8 | let entry = yield getArenaReducerDictEntry(key); 9 | if (action._sceneReducerKey === entry.reducerKey) { 10 | return action; 11 | } 12 | } 13 | } 14 | 15 | function* _takeSceneActionMaybe(pattern: Pattern, key: string) { 16 | while (true) { 17 | let action = yield take.maybe(pattern); 18 | let entry = yield getArenaReducerDictEntry(key); 19 | if ( 20 | action._sceneReducerKey === entry.reducerKey || 21 | action.type === END.type 22 | ) { 23 | return action; 24 | } 25 | } 26 | } 27 | 28 | export type TakeSceneAction = { 29 | (pattern: Pattern, key?: string): void; 30 | maybe: (pattern: Pattern, key?: string) => void; 31 | }; 32 | 33 | const takeSceneAction: any = function(pattern: Pattern, key?: string) { 34 | return call(_takeSceneAction, pattern, key ? key : "_arenaScene"); 35 | }; 36 | 37 | takeSceneAction.maybe = function(pattern: Pattern, key?: string) { 38 | return call(_takeSceneActionMaybe, pattern, key ? key : "_arenaScene"); 39 | }; 40 | 41 | export default takeSceneAction; 42 | -------------------------------------------------------------------------------- /src/utils/buildReducerDict.ts: -------------------------------------------------------------------------------- 1 | import { ReducerDict } from "../core"; 2 | import { ActionCreatorsMapObject } from "redux"; 3 | 4 | function buildReducerKey( 5 | arenaReducerDict: ReducerDict | null | undefined, 6 | reducerKey: string, 7 | vReducerKey: string | null | undefined, 8 | actions: ActionCreatorsMapObject, 9 | curReducerKey: string 10 | ): ReducerDict { 11 | let item = { actions, reducerKey, vReducerKey }; 12 | let newDict = Object.assign({}, arenaReducerDict, { 13 | [reducerKey]: item 14 | }); 15 | if (vReducerKey) { 16 | newDict[vReducerKey] = item; 17 | } 18 | newDict[curReducerKey] = item; 19 | return newDict; 20 | } 21 | 22 | export function buildCurtainReducerDict( 23 | arenaReducerDict: ReducerDict | null | undefined, 24 | reducerKey: string, 25 | vReducerKey: string | null | undefined 26 | ) { 27 | let newDict: any = buildReducerKey( 28 | arenaReducerDict, 29 | reducerKey, 30 | vReducerKey, 31 | {}, 32 | "_arenaCurtain" 33 | ); 34 | newDict._arenaScene = null; 35 | return newDict; 36 | } 37 | 38 | export function buildSceneReducerDict( 39 | arenaReducerDict: ReducerDict | null | undefined, 40 | reducerKey: string, 41 | vReducerKey: string | null | undefined, 42 | actions: ActionCreatorsMapObject | null | undefined 43 | ) { 44 | return buildReducerKey( 45 | arenaReducerDict, 46 | reducerKey, 47 | vReducerKey, 48 | actions || {}, 49 | "_arenaScene" 50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /example/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | var webpack = require("webpack"); 2 | var path = require("path"); 3 | var HtmlWebpackPlugin = require("html-webpack-plugin"); 4 | 5 | module.exports = { 6 | entry: [ 7 | "react-hot-loader/patch", 8 | `webpack-dev-server/client?http://${process.env.npm_package_config_host}:${ 9 | process.env.npm_package_config_port 10 | }`, // WebpackDevServer host and port 11 | "webpack/hot/only-dev-server", // "only" prevents reload on syntax errors 12 | "./index.tsx" // "only" prevents reload on syntax errors 13 | ], 14 | output: { 15 | path: path.join(__dirname, "build"), 16 | filename: "[name].[hash].js", 17 | chunkFilename: "[name].[id].[hash].js", 18 | publicPath: "" 19 | }, 20 | devtool: "inline-source-map", 21 | resolve: { 22 | extensions: [".ts", ".tsx", ".js", ".js", ".jsx"] 23 | }, 24 | module: { 25 | rules: [ 26 | { 27 | test: /\.tsx?$/, 28 | exclude: /node_modules/, 29 | loader: "ts-loader" 30 | } 31 | ] 32 | }, 33 | plugins: [ 34 | new HtmlWebpackPlugin({ 35 | template: "./index.html", // Load a custom template 36 | inject: "body", // Inject all scripts into the body 37 | title: "redux-arena", 38 | filename: "index.html", 39 | base: "/redux-arena" 40 | }), 41 | new webpack.HotModuleReplacementPlugin(), 42 | new webpack.NamedModulesPlugin(), 43 | new webpack.NoEmitOnErrorsPlugin() 44 | ] 45 | }; 46 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | <%= htmlWebpackPlugin.options.title %> 46 | 47 | 48 | 49 | 50 |
51 |
52 |
53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /example/webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | var webpack = require("webpack"); 2 | var path = require("path"); 3 | var HtmlWebpackPlugin = require("html-webpack-plugin"); 4 | var ScriptExtHtmlWebpackPlugin = require("script-ext-html-webpack-plugin"); 5 | 6 | module.exports = { 7 | entry: { 8 | app: "./index.tsx" 9 | }, 10 | output: { 11 | path: path.join(__dirname, "build"), 12 | filename: "[name].[hash].js", 13 | chunkFilename: "[name].[id].[hash].js", 14 | publicPath: "/redux-arena/" 15 | }, 16 | resolve: { 17 | extensions: [".ts", ".tsx", ".js", ".js", ".jsx"] 18 | }, 19 | module: { 20 | rules: [ 21 | { 22 | test: /\.tsx?$/, 23 | exclude: /node_modules/, 24 | loader: "ts-loader" 25 | } 26 | ] 27 | }, 28 | plugins: [ 29 | new webpack.NamedModulesPlugin(), 30 | new HtmlWebpackPlugin({ 31 | template: "./index.html", // Load a custom template 32 | inject: "body", // Inject all scripts into the body 33 | title: "redux-arena", 34 | base: "/redux-arena", 35 | filename: "index.html", 36 | chunksSortMode: function(c1, c2) { 37 | var orders = ["manifest", "vendor", "babelPolyfill", "app"]; 38 | let o1 = orders.indexOf(c1.names[0]); 39 | let o2 = orders.indexOf(c2.names[0]); 40 | return o1 - o2; 41 | } 42 | }), 43 | new ScriptExtHtmlWebpackPlugin({ 44 | defaultAttribute: "defer" 45 | }), 46 | new webpack.DefinePlugin({ 47 | "process.env.NODE_ENV": '"production"' 48 | }), 49 | new webpack.optimize.UglifyJsPlugin({ 50 | output: { 51 | comments: false 52 | } 53 | }), 54 | new webpack.NoEmitOnErrorsPlugin() 55 | ] 56 | }; 57 | -------------------------------------------------------------------------------- /src/core/enhancedRedux/createArenaStore.ts: -------------------------------------------------------------------------------- 1 | import { applyMiddleware, compose } from "redux"; 2 | import ActionTypes from "../ActionTypes"; 3 | import { ReducersMapObject, GenericStoreEnhancer, Middleware } from "redux"; 4 | import createSagaMiddleware, { END, SagaMiddlewareOptions } from "redux-saga"; 5 | import { getArenaInitState, arenaReducer } from "../reducers"; 6 | import createEnhancedStore, { EnhancedStore } from "./createEnhancedStore"; 7 | import rootSaga from "../sagas"; 8 | 9 | export type ArenaStoreOptions = { 10 | sagaOptions?: SagaMiddlewareOptions<{}>; 11 | initialStates?: any; 12 | enhencers?: GenericStoreEnhancer[]; 13 | middlewares?: Middleware[]; 14 | }; 15 | 16 | export default function createArenaStore( 17 | reducers: ReducersMapObject = {}, 18 | options: ArenaStoreOptions = {} 19 | ) { 20 | let { enhencers, sagaOptions, initialStates, middlewares } = options; 21 | let sagaMiddleware = createSagaMiddleware(sagaOptions); 22 | let mergedMiddlewares: Middleware[] = [sagaMiddleware]; 23 | if (middlewares) { 24 | mergedMiddlewares = mergedMiddlewares.concat(middlewares); 25 | } 26 | let store = createEnhancedStore( 27 | Object.assign( 28 | { 29 | arena: arenaReducer 30 | }, 31 | reducers 32 | ), 33 | Object.assign( 34 | { 35 | arena: getArenaInitState() 36 | }, 37 | initialStates || {} 38 | ), 39 | enhencers 40 | ? compose(applyMiddleware(...mergedMiddlewares), ...enhencers) 41 | : applyMiddleware(...mergedMiddlewares) 42 | ); 43 | sagaMiddleware.run(rootSaga, { store }); 44 | store.close = () => store.dispatch(END); 45 | store.runSaga = saga => 46 | store.dispatch({ 47 | type: ActionTypes.ARENA_INIT_AUDIENCE_SAGA, 48 | saga 49 | }); 50 | return store; 51 | } 52 | -------------------------------------------------------------------------------- /src/utils/addReducer.ts: -------------------------------------------------------------------------------- 1 | import ActionTypes from "../core/ActionTypes"; 2 | import { EnhancedStore, SceneReducer, ReducerFactory } from "../core"; 3 | 4 | function addReducer( 5 | store: EnhancedStore, 6 | reducerKey: string | null | undefined, 7 | reducerFactory: ReducerFactory 8 | ) { 9 | let newReducerKey = reducerKey; 10 | if (newReducerKey != null) { 11 | let flag = store.addSingleReducer({ 12 | reducerKey: newReducerKey, 13 | reducer: reducerFactory(newReducerKey) 14 | }); 15 | if (flag === false) { 16 | throw new Error(`Reducer key [${newReducerKey}] already exsit.`); 17 | } 18 | } else { 19 | do { 20 | newReducerKey = String(Math.random()).slice(2); 21 | let flag = store.addSingleReducer({ 22 | reducerKey: newReducerKey, 23 | reducer: reducerFactory(newReducerKey) 24 | }); 25 | if (flag === false) newReducerKey = null; 26 | } while (newReducerKey == null); 27 | } 28 | return newReducerKey; 29 | } 30 | 31 | export function sceneAddReducer( 32 | store: EnhancedStore, 33 | reducerKey: string | null | undefined, 34 | reducerFactory: ReducerFactory, 35 | state?: any 36 | ) { 37 | let newReducerKey = addReducer(store, reducerKey, reducerFactory); 38 | if (state) 39 | store.dispatch({ 40 | type: ActionTypes.ARENA_SCENE_REPLACE_STATE, 41 | _sceneReducerKey: newReducerKey, 42 | state 43 | }); 44 | return newReducerKey; 45 | } 46 | 47 | export function curtainAddReducer( 48 | store: EnhancedStore, 49 | reducerKey: string | null | undefined, 50 | reducerFactory: ReducerFactory, 51 | state?: any 52 | ) { 53 | let newReducerKey = addReducer(store, reducerKey, reducerFactory); 54 | if (state) 55 | store.dispatch({ 56 | type: ActionTypes.ARENA_CURTAIN_REPLACE_STATE, 57 | _reducerKey: newReducerKey, 58 | state 59 | }); 60 | return newReducerKey; 61 | } 62 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.9.1 (Dec 15, 2017) 2 | 3 | * Fix PropsPicker match (#21) 4 | 5 | ## 0.9.0 (Dec 13, 2017) 6 | 7 | * Add typescript support (#13) 8 | * Separate animition hoc to other repository (#15) 9 | * Change propsPicker for typescript type inferring (#18) 10 | * Add relative level popspicker (#20) 11 | 12 | ## 0.8.0 (Oct 16, 2017) 13 | 14 | * Move async bundle loading of ArenaScene to ArenaSceneLoadMotion 15 | * Rename ArenaSceneMotion to ArenaSceneLoadMotion 16 | * Remove asyncBundleToComponent and asyncBundleToElement in tools 17 | * Add state tree description in redux store and asyncBundleToElement in tools 18 | * Fix unmounted component's child null reference issue 19 | 20 | ## 0.7.1 (Oct 10, 2017) 21 | 22 | * Add ReducerDictOverrider HOC 23 | 24 | ## 0.7.0 (Oct 2, 2017) 25 | 26 | * Sub-module bug fixed 27 | 28 | ## 0.7.0-beta (Sep 29, 2017) 29 | 30 | * Support redux-arena-router and redux-arena-form 31 | * Add react-motion animation support 32 | * Remove deprecated helpers and sagaOps 33 | * Add bundleToElement/asyncBundleToElement tools 34 | 35 | ## 0.6.3 (Sep 18, 2017) 36 | 37 | * Fix isNotifyOn props pass down bug 38 | * Remove props SceneLoadingComponent in SceneBulde component 39 | 40 | ## 0.6.0 (Sep 12, 2017) 41 | 42 | * RouteScene and SoloScene is refactored 43 | * Fix bundle hot reload bugs 44 | 45 | ## 0.5.1 (Sep 12, 2017) 46 | 47 | * Fixed state illusion bug after bundle reload 48 | 49 | ## 0.5.0 (Aug 20, 2017) 50 | 51 | * Bug fixed and API update 52 | 53 | ## 0.4.1 (Aug 20, 2017) 54 | 55 | * Virtual reducer key support 56 | 57 | ## 0.3.0 (Aug 20, 2017) 58 | 59 | * Rename IndependentScene to SoloScene 60 | * Rename SceneSwitch to ArenaScene 61 | * Fix some bugs 62 | 63 | ## 0.2.3 (Aug 20, 2017) 64 | 65 | * Add IndependentScene 66 | * Add annotations 67 | * Add route infomation in scene actions 68 | 69 | ## 0.2.0 (Aug 19, 2017) 70 | 71 | * Support react-hot-loader 72 | * Support redux-devtools 73 | 74 | ## 0.1.0 (Aug 03, 2017) 75 | 76 | * Initial public release 77 | -------------------------------------------------------------------------------- /src/core/enhancedRedux/bindArenaActionCreators.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AnyAction, 3 | Dispatch, 4 | ActionCreatorsMapObject, 5 | ActionCreator 6 | } from "redux"; 7 | import { RootState } from "../reducers/types"; 8 | function bindArenaActionCreator( 9 | actionCreator: ActionCreator, 10 | dispatch: Dispatch, 11 | sceneReducerKey: string 12 | ) { 13 | return (...args: any[]) => { 14 | let action = actionCreator(...args); 15 | if (action && action._sceneReducerKey) { 16 | console.warn( 17 | '"Action with redux-arena should not contain an user specified "_sceneReducerKey" property.\n' + 18 | `Occurred in type: ${action.type}, _sceneReducerKey: ${ 19 | sceneReducerKey 20 | }.` 21 | ); 22 | } 23 | typeof action === "object" 24 | ? dispatch( 25 | Object.assign({}, { _sceneReducerKey: sceneReducerKey }, action) 26 | ) 27 | : dispatch(action); 28 | }; 29 | } 30 | 31 | export default function bindArenaActionCreators< 32 | M extends ActionCreatorsMapObject 33 | >( 34 | actionCreators: ActionCreatorsMapObject, 35 | dispatch: Dispatch, 36 | sceneReducerKey: string 37 | ): M { 38 | if ( 39 | typeof actionCreators === "function" || 40 | typeof actionCreators !== "object" || 41 | actionCreators === null 42 | ) { 43 | throw new Error( 44 | `bindArenaActionCreators expected an object, instead received ${ 45 | actionCreators === null ? "null" : typeof actionCreators 46 | }. ` + 47 | `Did you write "import ActionCreators from" instead of "import * as ActionCreators from"?` 48 | ); 49 | } 50 | 51 | let keys = Object.keys(actionCreators); 52 | let bindedActionCreators: M = {}; 53 | for (let i = 0; i < keys.length; i++) { 54 | let key = keys[i]; 55 | let actionCreator = actionCreators[key]; 56 | if (typeof actionCreator === "function") { 57 | bindedActionCreators[key] = bindArenaActionCreator( 58 | actionCreator, 59 | dispatch, 60 | sceneReducerKey 61 | ); 62 | } 63 | } 64 | return bindedActionCreators; 65 | } 66 | -------------------------------------------------------------------------------- /src/tools/index.spec.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { expect } from "chai"; 3 | import { spy } from "sinon"; 4 | import { StateDict, ActionsDict } from "src"; 5 | import { DefaultSceneActions } from "src/core/types"; 6 | import bundleToComponent from "./bundleToComponent"; 7 | import bundleToElement from "./bundleToElement"; 8 | import { ActionsProps } from "./types"; 9 | 10 | class TestComponent extends React.Component< 11 | { 12 | a: string; 13 | b: string; 14 | } & ActionsProps> 15 | > { 16 | render() { 17 | return
{this.props.a + this.props.b}
; 18 | } 19 | } 20 | 21 | const bundleWithDefaultA = { 22 | Component: TestComponent, 23 | state: { a: "a" }, 24 | propsPicker: ( 25 | { $0: state }: StateDict<{ a: string }>, 26 | { $0: actions }: ActionsDict> 27 | ) => ({ 28 | actions, 29 | a: state.a 30 | }) 31 | }; 32 | 33 | const bundleWithDefaultAPP = { 34 | Component: TestComponent, 35 | state: { a: "a" } 36 | }; 37 | 38 | class TestComponent2 extends React.Component< 39 | { 40 | a: string; 41 | b: string; 42 | } & { actions: { addCnt: () => void } } 43 | > { 44 | render() { 45 | return
{this.props.a + this.props.b}
; 46 | } 47 | } 48 | 49 | const bundleWithDefaultSPP = { 50 | Component: TestComponent, 51 | actions: { addCnt: () => null } 52 | }; 53 | 54 | const bundleWithDefaultSAPP = { 55 | Component: TestComponent 56 | }; 57 | 58 | describe("Redux-Arena tools bundle transform", () => { 59 | it("should referring right types", () => { 60 | let A = bundleToComponent(bundleWithDefaultA); 61 |
; 62 | bundleToElement(bundleWithDefaultA, { b: "b" }); 63 | let APP = bundleToComponent(bundleWithDefaultAPP); 64 | ; 65 | bundleToElement(bundleWithDefaultAPP, { b: "b" }); 66 | let SPP = bundleToComponent(bundleWithDefaultSPP); 67 | ; 68 | bundleToElement(bundleWithDefaultSPP, { a: "a", b: "b" }); 69 | let SAPP = bundleToComponent(bundleWithDefaultSAPP); 70 | ; 71 | bundleToElement(bundleWithDefaultSAPP, { a: "a", b: "b" }); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /example/PassDownState/child/Child.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Props } from "./types"; 3 | 4 | export default class Child extends React.Component { 5 | render() { 6 | let { name, cnt, parentState, actions, parentActions } = this.props; 7 | return ( 8 |
16 | 17 | Map parent's state to props parentState by vReducerKey{" "} 18 | "parent". 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 35 | 36 | 37 | 38 | 39 | 40 | 41 |
state_keystate_value
name:{name}
parentState: 33 | {JSON.stringify(parentState)} 34 |
cnt:{cnt}
42 |
49 | 50 | Map parent's actions to props parentActions by vReducerKey{" "} 51 | "parent". 52 | 53 |
54 | 55 | 58 |
59 |
60 | 61 | 67 |
68 |
69 |
70 | ); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /test/integration/ArenaScene/mountAndUnount.spec.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { ReactWrapper } from "enzyme"; 3 | import { expect } from "chai"; 4 | import { spy } from "sinon"; 5 | import { createArenaStore, EnhancedStore, SceneBundle } from "src"; 6 | import sceneBundleForTestA from "../../sceneBundleForTestA"; 7 | import { MountBundle } from "./types"; 8 | import createBundleMounter from "./createBundleMounter"; 9 | 10 | function selectNeededStates(allStates: any, name: string): any { 11 | let { arena, ...otherState } = allStates; 12 | let metaState, bundleState; 13 | Object.keys(otherState).forEach(key => { 14 | if (otherState[key].name === name) { 15 | bundleState = otherState[key]; 16 | } else if (otherState[key].curSceneBundle != null) { 17 | metaState = otherState[key]; 18 | } 19 | }); 20 | return { 21 | arena, 22 | metaState, 23 | bundleState 24 | }; 25 | } 26 | 27 | describe(" integration", () => { 28 | let store: EnhancedStore, 29 | mountSceneBundle: MountBundle, 30 | wrapper: ReactWrapper, 31 | cleanUp: () => void; 32 | 33 | before(() => { 34 | [mountSceneBundle, cleanUp] = createBundleMounter(); 35 | store = createArenaStore(); 36 | }); 37 | 38 | after(() => { 39 | store.close(); 40 | }); 41 | 42 | it("should mount with right redux state", () => { 43 | let flagPromise = new Promise(resolve => { 44 | let unsubscribe = store.subscribe(() => { 45 | let { arena, metaState, bundleState } = selectNeededStates( 46 | store.getState(), 47 | "PageA" 48 | ); 49 | if (arena && metaState && bundleState) { 50 | if (bundleState.cnt !== 4 || bundleState.sagaCnt !== 1) return; 51 | unsubscribe(); 52 | expect(bundleState.pageA).to.be.true; 53 | resolve(true); 54 | } 55 | }); 56 | }); 57 | wrapper = mountSceneBundle(store, sceneBundleForTestA); 58 | return flagPromise; 59 | }); 60 | 61 | it("should unmount with right redux state", () => { 62 | cleanUp(); 63 | let flagPromise = new Promise(resolve => { 64 | let unsubscribe = store.subscribe(() => { 65 | let state: any = store.getState(); 66 | Object.keys(state).length == 1; 67 | if (Object.keys(state).length == 1) { 68 | unsubscribe(); 69 | expect(state.arena).to.not.be.null; 70 | resolve(true); 71 | } 72 | }); 73 | }); 74 | return flagPromise; 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-arena", 3 | "version": "0.9.1", 4 | "description": "Bundling reducers, actions, saga and react-component when using Redux", 5 | "scripts": { 6 | "prettier": "prettier --write \"**/*.js\" \"**/*.jsx\" \"**/*.tsx\" \"**/*.ts\"", 7 | "build": "node scripts/build.js", 8 | "postpublish": "npm run build && cd build && npm publish", 9 | "test": "karma start test/karma.conf.js", 10 | "test-travis": "karma start test/karma.conf.js --browsers Firefox --single-run" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/hapood/redux-arena.git" 15 | }, 16 | "keywords": [ 17 | "react", 18 | "redux", 19 | "react-router", 20 | "redux-saga", 21 | "context-switch" 22 | ], 23 | "author": "Hapood Wang", 24 | "license": "Apache-2.0", 25 | "bugs": { 26 | "url": "https://github.com/hapood/redux-arena/issues" 27 | }, 28 | "homepage": "https://github.com/hapood/redux-arena#readme", 29 | "peerDependencies": { 30 | "react": ">=15" 31 | }, 32 | "dependencies": { 33 | "@types/prop-types": "^15.5.2", 34 | "@types/react-redux": "^5.0.14", 35 | "immutable": "^3.8.2", 36 | "prop-types": "^15.6.0", 37 | "react-redux": "^5.0.6", 38 | "redux": "^3.7.2", 39 | "redux-saga": "^0.16.0" 40 | }, 41 | "devDependencies": { 42 | "@types/chai": "^4.0.10", 43 | "@types/enzyme": "^3.1.5", 44 | "@types/enzyme-adapter-react-16": "^1.0.1", 45 | "@types/mocha": "^2.2.44", 46 | "@types/react": "^16.0.29", 47 | "@types/react-dom": "^16.0.3", 48 | "@types/sinon": "^4.1.2", 49 | "chai": "^4.1.2", 50 | "enzyme": "^3.2.0", 51 | "enzyme-adapter-react-16": "^1.1.0", 52 | "fs-extra": "^5.0.0", 53 | "istanbul-instrumenter-loader": "^3.0.0", 54 | "karma": "^1.7.1", 55 | "karma-chai": "^0.1.0", 56 | "karma-chrome-launcher": "^2.2.0", 57 | "karma-coverage-istanbul-reporter": "^1.3.0", 58 | "karma-firefox-launcher": "^1.1.0", 59 | "karma-mocha": "^1.3.0", 60 | "karma-sinon": "^1.0.5", 61 | "karma-sourcemap-loader": "^0.3.7", 62 | "karma-typescript": "^3.0.8", 63 | "karma-verbose-reporter": "^0.0.6", 64 | "karma-webpack": "^2.0.6", 65 | "mocha": "^4.0.1", 66 | "prettier": "^1.9.2", 67 | "react": "^16.2.0", 68 | "react-dom": "^16.2.0", 69 | "react-test-renderer": "16", 70 | "sinon": "^4.1.3", 71 | "ts-loader": "^3.2.0", 72 | "typescript": "^2.6.2", 73 | "webpack": "^3.10.0", 74 | "webpack-dev-server": "^2.9.7" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/tools/types.ts: -------------------------------------------------------------------------------- 1 | import { ComponentType } from "react"; 2 | import { ActionCreatorsMapObject } from "redux"; 3 | import { 4 | PropsPicker, 5 | SceneReducer, 6 | SceneBundleOptions, 7 | ActionsDict, 8 | DefaultSceneActions 9 | } from "../core"; 10 | 11 | export type Diff = ({ [P in T]: P } & 12 | { [P in U]: never } & { [x: string]: never })[T]; 13 | export type Omit = Pick>; 14 | 15 | export type DefaultPickedProps = { 16 | actions: A; 17 | } & S; 18 | 19 | export type DefaultState = {}; 20 | 21 | export type ActionsProps
= { actions: A }; 22 | 23 | export type SceneBundleBase = { 24 | Component: ComponentType

; 25 | saga?: (...params: any[]) => any; 26 | options?: SceneBundleOptions; 27 | reducer?: SceneReducer; 28 | }; 29 | 30 | export type SceneBundleNo< 31 | P extends PP, 32 | S, 33 | A extends ActionCreatorsMapObject, 34 | PP 35 | > = { 36 | state: S; 37 | actions: A; 38 | propsPicker: PropsPicker; 39 | } & SceneBundleBase; 40 | 41 | export type SceneBundleNoS< 42 | P extends PP, 43 | A extends ActionCreatorsMapObject, 44 | PP 45 | > = { 46 | actions: A; 47 | propsPicker: PropsPicker; 48 | } & SceneBundleBase; 49 | 50 | export type SceneBundleNoPP = { 51 | state: S; 52 | actions: A; 53 | } & SceneBundleBase

; 54 | 55 | export type SceneBundleNoA

= { 56 | state: S; 57 | propsPicker: PropsPicker, PP>; 58 | } & SceneBundleBase; 59 | 60 | export type SceneBundleNoSPP = { 61 | actions: A; 62 | } & SceneBundleBase

; 63 | 64 | export type SceneBundleNoSA

= { 65 | propsPicker: PropsPicker, PP>; 66 | } & SceneBundleBase; 67 | 68 | export type SceneBundleNoAPP = { 69 | state: S; 70 | } & SceneBundleBase; 71 | 72 | export type SceneBundleNoSAPP

= SceneBundleBase; 73 | 74 | export type SceneBundlePart< 75 | P extends PP, 76 | S, 77 | A extends ActionCreatorsMapObject, 78 | PP 79 | > = 80 | | SceneBundleNo 81 | | SceneBundleNoS 82 | | SceneBundleNoPP 83 | | SceneBundleNoA 84 | | SceneBundleNoSPP 85 | | SceneBundleNoAPP 86 | | SceneBundleNoSA 87 | | SceneBundleNoSAPP

; 88 | -------------------------------------------------------------------------------- /src/core/enhancedRedux/createPropsPicker.ts: -------------------------------------------------------------------------------- 1 | import { ActionCreatorsMapObject } from "redux"; 2 | import { PropsPicker, StateDict, ActionsDict } from "../types"; 3 | import { 4 | CurtainReduxInfo, 5 | CurtainMutableObj, 6 | RootState 7 | } from "../reducers/types"; 8 | 9 | function getRelativeLevel(name: string) { 10 | let result = name.match(/^\$(\d+)$/); 11 | return result && parseInt(result[1]); 12 | } 13 | 14 | function getLevelKey( 15 | rootState: RootState, 16 | reducerKey: string, 17 | levelNum: number 18 | ) { 19 | if (levelNum === 0) return reducerKey; 20 | let { stateTree, stateTreeDict } = rootState.arena; 21 | let path = stateTreeDict.getIn([reducerKey, "path"]); 22 | return path.get(path.count() - 1 - 2 * levelNum); 23 | } 24 | 25 | export default function createPropsPicker< 26 | S, 27 | A extends ActionCreatorsMapObject, 28 | P 29 | >( 30 | propsPicker: PropsPicker>, 31 | reduxInfo: CurtainReduxInfo, 32 | mutableObj: CurtainMutableObj 33 | ) { 34 | let { arenaReducerDict } = reduxInfo; 35 | let sceneReducerKey = arenaReducerDict._arenaScene.reducerKey; 36 | let latestProps: Partial

; 37 | let stateHandler = { 38 | get: function(target: { state: any }, name: string) { 39 | let levelNum = getRelativeLevel(name); 40 | if (levelNum != null) { 41 | name = getLevelKey(target.state, sceneReducerKey, levelNum); 42 | } 43 | let dictItem = arenaReducerDict[name]; 44 | if (dictItem == null) return null; 45 | return target.state[dictItem.reducerKey]; 46 | } 47 | }; 48 | let actionsHandler = { 49 | get: function(target: { state: any }, name: string) { 50 | let levelNum = getRelativeLevel(name); 51 | if (levelNum != null) { 52 | name = getLevelKey(target.state, sceneReducerKey, levelNum); 53 | } 54 | let dictItem = arenaReducerDict[name]; 55 | if (dictItem == null) return null; 56 | return dictItem.actions; 57 | } 58 | }; 59 | let stateObj = { state: null }; 60 | let stateDict: StateDict = new Proxy(stateObj, stateHandler) as any; 61 | let actionsDict: ActionsDict = new Proxy(stateObj, actionsHandler) as any; 62 | return (state: any) => { 63 | stateObj.state = state; 64 | if ( 65 | mutableObj.isObsolete === true || 66 | state.arena.propsLock !== false || 67 | state.arena.stateTreeDict.getIn([sceneReducerKey, "isObsolete"]) === true 68 | ) { 69 | return latestProps; 70 | } else { 71 | latestProps = propsPicker(stateDict, actionsDict); 72 | return latestProps; 73 | } 74 | }; 75 | } 76 | -------------------------------------------------------------------------------- /src/hocs/BundleComponent/BundleComponent.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as PropTypes from "prop-types"; 3 | import { ReducerDict, SceneBundle } from "../../core"; 4 | import { ConnectedProps, State } from "./types"; 5 | 6 | export default class BundleComponent extends React.Component< 7 | ConnectedProps, 8 | State 9 | > { 10 | static childContextTypes = { 11 | arenaReducerDict: PropTypes.object 12 | }; 13 | 14 | _isValid = false; 15 | 16 | getChildContext() { 17 | return { 18 | arenaReducerDict: 19 | this.props.reduxInfo && this.props.reduxInfo.arenaReducerDict 20 | }; 21 | } 22 | 23 | buildLoadScenePromise( 24 | arenaReducerDict: ReducerDict, 25 | sceneBundle: SceneBundle<{}, {}, {}, {}>, 26 | isInitial: any 27 | ): Promise { 28 | if (isInitial) { 29 | return new Promise(resolve => 30 | setImmediate(() => 31 | this.props.curtainLoadScene( 32 | this.props.arenaReducerDict, 33 | this.props.sceneBundle, 34 | true, 35 | resolve 36 | ) 37 | ) 38 | ); 39 | } else { 40 | return new Promise(resolve => 41 | this.props.curtainLoadScene( 42 | this.props.arenaReducerDict, 43 | this.props.sceneBundle, 44 | true, 45 | resolve 46 | ) 47 | ); 48 | } 49 | } 50 | 51 | componentWillMount() { 52 | this._isValid = true; 53 | let loadedPromise = this.buildLoadScenePromise( 54 | this.props.arenaReducerDict, 55 | this.props.sceneBundle, 56 | true 57 | ); 58 | this.setState({ 59 | loadedPromise 60 | }); 61 | } 62 | 63 | componentWillReceiveProps(nextProps: ConnectedProps) { 64 | let { sceneBundle } = nextProps; 65 | if (sceneBundle !== this.props.sceneBundle) { 66 | this.state.loadedPromise.then(() => { 67 | if (this._isValid) { 68 | let loadedPromise = this.buildLoadScenePromise( 69 | nextProps.arenaReducerDict, 70 | nextProps.sceneBundle, 71 | false 72 | ); 73 | this.setState({ 74 | loadedPromise 75 | }); 76 | } 77 | }); 78 | } 79 | } 80 | 81 | componentWillUnmount() { 82 | this._isValid = false; 83 | this.props.clearCurtain(); 84 | this.props.mutableObj.isObsolete = true; 85 | } 86 | 87 | render() { 88 | let { PlayingScene, sceneProps } = this.props; 89 | if (PlayingScene != null) { 90 | return ; 91 | } else { 92 | return

; 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /example/ScopedPage/scopedPage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { State, Actions } from "./types"; 3 | 4 | export default class ScopedPage extends React.Component< 5 | State & { actions: Actions } 6 | > { 7 | render() { 8 | let { name, dynamicState, cnt, actions } = this.props; 9 | return ( 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
state_keystate_value
name:{name}
dynamicState:{dynamicState}
cnt:{cnt}
31 |
39 |
40 | and{" "} 41 | [Add Total Count] has same 42 | action type "ADD_CNT", but scoped reducer will ignore other moudle's 43 | "ADD_CNT". 44 |
45 |
51 | {" "} 52 | dispatch action type "ARENA_SCENE_SET_STATE" and set this page's cnt 53 | 0. 54 |
55 |
56 |
65 | Saga can be scoped by using takeLatestSceneAction. 66 |
67 | 70 | 76 |
77 |
78 |
79 | ); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/tools/bundleToComponent.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { ActionCreatorsMapObject } from "redux"; 3 | import { DefaultSceneActions } from "../core/types"; 4 | import { ArenaSceneExtraProps, ArenaScene } from "../hocs"; 5 | import { 6 | Omit, 7 | ActionsProps, 8 | SceneBundleNo, 9 | SceneBundleNoS, 10 | SceneBundleNoA, 11 | SceneBundleNoPP, 12 | SceneBundleNoAPP, 13 | SceneBundleNoSA, 14 | SceneBundleNoSPP, 15 | SceneBundleNoSAPP 16 | } from "./types"; 17 | import { 18 | defaultPropsPicker, 19 | defaultActions, 20 | defaultReducerCreator 21 | } from "./autoFill"; 22 | 23 | function bundleToComponent< 24 | P extends PP, 25 | S, 26 | A extends ActionCreatorsMapObject, 27 | PP 28 | >( 29 | bundle: SceneBundleNo, 30 | extraProps?: ArenaSceneExtraProps 31 | ): React.SFC>; 32 | function bundleToComponent

( 33 | bundle: SceneBundleNoS, 34 | extraProps?: ArenaSceneExtraProps 35 | ): React.SFC>; 36 | function bundleToComponent

( 37 | bundle: SceneBundleNoA, 38 | extraProps?: ArenaSceneExtraProps 39 | ): React.SFC>; 40 | function bundleToComponent< 41 | P extends S & ActionsProps, 42 | S, 43 | A extends ActionCreatorsMapObject 44 | >( 45 | bundle: SceneBundleNoPP, 46 | extraProps?: ArenaSceneExtraProps 47 | ): React.SFC)>>; 48 | function bundleToComponent

( 49 | bundle: SceneBundleNoSA, 50 | extraProps?: ArenaSceneExtraProps 51 | ): React.SFC>; 52 | function bundleToComponent< 53 | P extends ActionsProps, 54 | A extends ActionCreatorsMapObject 55 | >( 56 | bundle: SceneBundleNoSPP, 57 | extraProps?: ArenaSceneExtraProps 58 | ): React.SFC)>>; 59 | function bundleToComponent< 60 | P extends S & ActionsProps>, 61 | S 62 | >( 63 | bundle: SceneBundleNoAPP, 64 | extraProps?: ArenaSceneExtraProps 65 | ): React.SFC>)>>; 66 | function bundleToComponent

>>( 67 | bundle: SceneBundleNoSAPP

, 68 | extraProps?: ArenaSceneExtraProps 69 | ): React.SFC>)>>; 70 | function bundleToComponent(bundle: any, extraProps?: ArenaSceneExtraProps) { 71 | let newBundle = Object.assign( 72 | { 73 | propsPicker: defaultPropsPicker, 74 | actions: defaultActions, 75 | reducer: defaultReducerCreator(bundle.state) 76 | }, 77 | bundle 78 | ); 79 | let WrapperClass: React.SFC<{}> = props => ( 80 | 81 | ); 82 | WrapperClass.displayName = "ScenePropsProxy"; 83 | return WrapperClass; 84 | } 85 | export default bundleToComponent; 86 | -------------------------------------------------------------------------------- /src/core/sagas/arenaCurtainSaga.ts: -------------------------------------------------------------------------------- 1 | import ActionTypes from "../ActionTypes"; 2 | import { 3 | takeEvery, 4 | take, 5 | put, 6 | fork, 7 | select, 8 | cancel, 9 | setContext, 10 | getContext, 11 | ForkEffect 12 | } from "redux-saga/effects"; 13 | import { Task } from "redux-saga"; 14 | import { Action } from "redux"; 15 | import { applySceneBundle } from "./sceneBundleSaga"; 16 | import { CurtainState } from "../reducers/types"; 17 | 18 | function* takeEverySceneBundleAction() { 19 | let _reducerKey = yield getContext("_reducerKey"); 20 | let lastTask; 21 | while (true) { 22 | let action = yield take(ActionTypes.ARENA_CURTAIN_LOAD_SCENE); 23 | if (action.arenaReducerDict._arenaCurtain.reducerKey === _reducerKey) { 24 | if (lastTask && lastTask.isRunning()) { 25 | yield cancel(lastTask); 26 | } 27 | lastTask = yield fork(applySceneBundle, action); 28 | } 29 | } 30 | } 31 | 32 | /** 33 | * Listen to the loading of each scene, 34 | * and handle different processing functions when handling scene switches. 35 | * 36 | * @param {any} ctx 37 | */ 38 | 39 | function* forkSagaWithContext(ctx: any) { 40 | yield setContext(ctx); 41 | yield fork(takeEverySceneBundleAction); 42 | } 43 | 44 | /** 45 | * It is used to initialize the ArenaSwitch layer. 46 | * 47 | * @param {any} { reducerKey, setSagaTask } 48 | */ 49 | 50 | interface InitArenaCurtainAction extends Action { 51 | reducerKey: string; 52 | setSagaTask: (sagaTask: Task) => void; 53 | } 54 | 55 | function* initArenaCurtainSaga({ 56 | reducerKey, 57 | setSagaTask 58 | }: InitArenaCurtainAction) { 59 | let sagaTask = yield fork(forkSagaWithContext, { 60 | _reducerKey: reducerKey 61 | }); 62 | setSagaTask(sagaTask); 63 | } 64 | 65 | /** 66 | * It is used to cancel the task of the ArenaSwitch layer. 67 | * 68 | * @param {any} { sagaTaskPromise } 69 | */ 70 | 71 | interface KillArenaCurtainAction extends Action { 72 | sagaTaskPromise: Promise; 73 | reducerKey: string; 74 | } 75 | 76 | function* killArenaCurtainSaga({ 77 | sagaTaskPromise, 78 | reducerKey 79 | }: KillArenaCurtainAction) { 80 | let sagaTask = yield sagaTaskPromise; 81 | if (sagaTask) yield cancel(sagaTask); 82 | let store = yield getContext("store"); 83 | yield put({ 84 | type: ActionTypes.ARENA_STATETREE_NODE_DISABLE, 85 | reducerKey 86 | }); 87 | let { reduxInfo } = (yield select( 88 | (state: any) => state[reducerKey] 89 | )) as CurtainState<{}>; 90 | if (reduxInfo && reduxInfo.reducerKey != null) { 91 | yield put({ 92 | type: ActionTypes.ARENA_STATETREE_NODE_DELETE, 93 | reducerKey: reduxInfo.reducerKey 94 | }); 95 | store.removeSingleReducer(reduxInfo.reducerKey); 96 | } 97 | store.removeSingleReducer(reducerKey); 98 | } 99 | 100 | export default function* saga() { 101 | yield takeEvery(ActionTypes.ARENA_CURTAIN_INIT_SAGA, initArenaCurtainSaga); 102 | yield takeEvery(ActionTypes.ARENA_CURTAIN_CLEAR_REDUX, killArenaCurtainSaga); 103 | } 104 | -------------------------------------------------------------------------------- /example/frame/Frame.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { bindActionCreators } from "redux"; 3 | import { connect } from "react-redux"; 4 | import actions from "./redux/actions"; 5 | 6 | import ModuleReUse from "../ModuleReUse"; 7 | import ScopedPage from "../ScopedPage"; 8 | import PassDownState from "../passDownState"; 9 | import { Props } from "./types"; 10 | 11 | const linkStyle = { 12 | textDecoration: "underline", 13 | color: "blue", 14 | cursor: "pointer" 15 | }; 16 | 17 | class Frame extends React.Component { 18 | constructor(props) { 19 | super(props); 20 | } 21 | componentWillMount() { 22 | this.setState({ 23 | page: "emptyPage" 24 | }); 25 | } 26 | 27 | render() { 28 | let { cnt, addCnt, clearCnt } = this.props; 29 | return ( 30 |
31 |
32 | 68 |
69 |
total count: {cnt}
70 | 73 | 76 |
77 |
78 |
79 |
80 | {this.state.page === "scopedPage" ? ( 81 | 82 | ) : this.state.page === "passDownStateAndActions" ? ( 83 | 84 | ) : this.state.page === "moduleReUse" ? ( 85 | 86 | ) : null} 87 |
88 |
89 |
90 |
91 | ); 92 | } 93 | } 94 | 95 | function mapDispatchToProps(dispatch) { 96 | return bindActionCreators(actions, dispatch); 97 | } 98 | 99 | function mapStateToProps(state) { 100 | return { cnt: state.frame.cnt }; 101 | } 102 | 103 | export default connect(mapStateToProps, mapDispatchToProps)(Frame); 104 | -------------------------------------------------------------------------------- /src/core/enhancedRedux/createEnhancedStore.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Create redux-arena proxy store 3 | */ 4 | import { 5 | createStore, 6 | combineReducers, 7 | Store, 8 | ReducersMapObject, 9 | GenericStoreEnhancer, 10 | Dispatch, 11 | Unsubscribe 12 | } from "redux"; 13 | import { ForkEffect } from "redux-saga/effects"; 14 | import { ArenaState } from "../reducers/types"; 15 | import { SceneReducer } from "../types"; 16 | 17 | type ArenaStoreState = { 18 | arena: ArenaState; 19 | }; 20 | 21 | export type ReducerObject = { 22 | reducerKey: string; 23 | reducer: SceneReducer; 24 | state?: {} | null; 25 | }; 26 | 27 | export interface EnhancedStore extends Store { 28 | addSingleReducer: (reducerObject: ReducerObject) => boolean; 29 | removeSingleReducer: (reducerKey: string) => boolean; 30 | replaceSingleReducer: (reducerObject: ReducerObject) => boolean; 31 | close: () => void; 32 | runSaga: (saga: (...params: any[]) => any) => void; 33 | } 34 | 35 | function storeEnhancer( 36 | store: Store, 37 | reducers: ReducersMapObject 38 | ): EnhancedStore { 39 | let _currentReducers = reducers; 40 | let handler = { 41 | get: function(target: Store, name: string) { 42 | if (name === "addSingleReducer") { 43 | return ({ reducerKey, reducer }: ReducerObject) => { 44 | let allStates = target.getState(); 45 | if (allStates.arena.stateTreeDict.get(reducerKey) != null) 46 | return false; 47 | _currentReducers = Object.assign({}, _currentReducers, { 48 | [reducerKey]: reducer 49 | }); 50 | target.replaceReducer(combineReducers(_currentReducers)); 51 | return true; 52 | }; 53 | } 54 | if (name === "removeSingleReducer") { 55 | return (reducerKey: string) => { 56 | if (reducerKey == null) { 57 | throw new Error("Can not remove reducerKey of null."); 58 | } 59 | let newReducers = Object.assign({}, _currentReducers); 60 | let allStates: any = target.getState(); 61 | delete newReducers[reducerKey]; 62 | _currentReducers = newReducers; 63 | delete allStates[reducerKey]; 64 | target.replaceReducer(combineReducers(newReducers)); 65 | return true; 66 | }; 67 | } 68 | if (name === "replaceSingleReducer") { 69 | return ({ reducerKey, reducer }: ReducerObject) => { 70 | if (reducerKey == null) 71 | throw new Error(`reducerKey can not be null.`); 72 | let allStates = target.getState(); 73 | if (_currentReducers[reducerKey] == null) 74 | throw new Error(`reducer for key [${reducerKey}] doesn't exsit.`); 75 | _currentReducers = Object.assign({}, _currentReducers, { 76 | [reducerKey]: reducer 77 | }); 78 | target.replaceReducer(combineReducers(_currentReducers)); 79 | return reducerKey; 80 | }; 81 | } 82 | return (target as any)[name]; 83 | } 84 | }; 85 | return new Proxy(store, handler) as any; 86 | } 87 | 88 | export default function createEnhancedStore( 89 | reducers: ReducersMapObject, 90 | initialState: any, 91 | enhencer: GenericStoreEnhancer 92 | ) { 93 | let store = createStore(combineReducers(reducers), initialState, enhencer); 94 | return storeEnhancer(store, reducers); 95 | } 96 | -------------------------------------------------------------------------------- /test/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Sat Jul 29 2017 16:06:43 GMT+0800 (中国标准时间) 3 | let webpack = require("webpack"); 4 | let path = require("path"); 5 | 6 | module.exports = function(config) { 7 | config.set({ 8 | // base path that will be used to resolve all patterns (eg. files, exclude) 9 | basePath: "", 10 | 11 | // frameworks to use 12 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 13 | frameworks: ["mocha", "chai", "sinon"], 14 | 15 | // list of files / patterns to load in the browser 16 | files: ["test.webpack.js"], 17 | 18 | // list of files to exclude 19 | exclude: [], 20 | 21 | // preprocess matching files before serving them to the browser 22 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 23 | preprocessors: { "test.webpack.js": ["webpack", "sourcemap"] }, 24 | 25 | // test results reporter to use 26 | // possible values: 'dots', 'progress' 27 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 28 | reporters: ["verbose", "coverage-istanbul"], 29 | 30 | coverageIstanbulReporter: { 31 | reports: ["text-summary", "lcovonly", "html"], 32 | fixWebpackSourcePaths: true, 33 | dir: path.join(__dirname, "..", "coverage"), 34 | "report-config": { 35 | // all options available at: https://github.com/istanbuljs/istanbul-reports/blob/590e6b0089f67b723a1fdf57bc7ccc080ff189d7/lib/html/index.js#L135-L137 36 | html: { 37 | // outputs the report in ./coverage/html 38 | subdir: "html" 39 | } 40 | } 41 | }, 42 | 43 | // web server port 44 | port: 9876, 45 | 46 | // enable / disable colors in the output (reporters and logs) 47 | colors: true, 48 | 49 | // level of logging 50 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 51 | logLevel: config.LOG_INFO, 52 | 53 | // enable / disable watching file and executing tests whenever any file changes 54 | autoWatch: true, 55 | 56 | // start these browsers 57 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 58 | browsers: ["Chrome"], 59 | 60 | // Continuous Integration mode 61 | // if true, Karma captures browsers, runs the tests and exits 62 | singleRun: false, 63 | 64 | // Concurrency level 65 | // how many browser should be started simultaneous 66 | concurrency: Infinity, 67 | 68 | webpack: { 69 | resolve: { 70 | alias: { src: path.resolve(__dirname, "../src") }, 71 | extensions: [".js", ".jsx", ".ts", ".tsx"] 72 | }, 73 | devtool: "inline-source-map", 74 | module: { 75 | loaders: [ 76 | { 77 | test: /\.tsx?$/, 78 | exclude: /node_modules/, 79 | loader: "ts-loader" 80 | }, 81 | { 82 | test: /\.tsx?$/, 83 | use: { 84 | loader: "istanbul-instrumenter-loader", 85 | options: { esModules: true } 86 | }, 87 | enforce: "post", 88 | include: path.resolve(__dirname, "../src"), 89 | exclude: /node_modules|\.spec\.tsx?$/ 90 | } 91 | ] 92 | } 93 | }, 94 | 95 | webpackServer: { 96 | noInfo: true 97 | } 98 | }); 99 | }; 100 | -------------------------------------------------------------------------------- /src/core/sagas/sceneBundleSaga.ts: -------------------------------------------------------------------------------- 1 | import { 2 | call, 3 | put, 4 | select, 5 | getContext, 6 | GetContextEffect, 7 | CallEffect, 8 | PutEffect, 9 | SelectEffect 10 | } from "redux-saga/effects"; 11 | import { ActionCreatorsMapObject } from "redux"; 12 | import { connect } from "react-redux"; 13 | import { createPropsPicker } from "../enhancedRedux"; 14 | import { sceneApplyRedux, sceneUpdateRedux } from "./sceneReduxSaga"; 15 | import ActionTypes from "../ActionTypes"; 16 | import { CurtainState, CurtainReduxInfo } from "../reducers/types"; 17 | import { SceneBundle, CurtainLoadSceneAction } from "../types"; 18 | 19 | export function* applySceneBundle< 20 | P extends PP, 21 | S, 22 | A extends ActionCreatorsMapObject, 23 | PP 24 | >({ 25 | isInitial, 26 | arenaReducerDict, 27 | sceneBundle, 28 | loadedCb 29 | }: CurtainLoadSceneAction) { 30 | let arenaCurtainReducerKey = arenaReducerDict._arenaCurtain.reducerKey; 31 | let curtainState: CurtainState

= yield select( 32 | (state: any) => state[arenaCurtainReducerKey] 33 | ); 34 | let { 35 | curSceneBundle, 36 | reduxInfo, 37 | PlayingScene: OldPlayingScene, 38 | mutableObj 39 | } = curtainState; 40 | mutableObj.isObsolete = true; 41 | let newReduxInfo: CurtainReduxInfo; 42 | //Use yield* because there is fork effect in sceneApplyRedux and sceneUpdateRedux 43 | if (isInitial) { 44 | newReduxInfo = yield* sceneApplyRedux({ 45 | arenaReducerDict, 46 | state: sceneBundle.state, 47 | saga: sceneBundle.saga, 48 | actions: sceneBundle.actions, 49 | reducer: sceneBundle.reducer, 50 | options: sceneBundle.options 51 | }); 52 | } else { 53 | newReduxInfo = yield* sceneUpdateRedux({ 54 | arenaReducerDict, 55 | state: sceneBundle.state, 56 | saga: sceneBundle.saga, 57 | actions: sceneBundle.actions, 58 | reducer: sceneBundle.reducer, 59 | options: sceneBundle.options, 60 | curSceneBundle: curSceneBundle as SceneBundle, 61 | reduxInfo: reduxInfo as CurtainReduxInfo 62 | }); 63 | } 64 | let newMutableObj = { isObsolete: false }; 65 | yield put({ 66 | type: ActionTypes.ARENA_CURTAIN_SET_STATE, 67 | _reducerKey: arenaCurtainReducerKey, 68 | state: { 69 | reduxInfo: newReduxInfo, 70 | mutableObj: newMutableObj 71 | } 72 | }); 73 | let propsPicker = createPropsPicker( 74 | sceneBundle.propsPicker, 75 | newReduxInfo, 76 | newMutableObj 77 | ); 78 | let PlayingScene = connect(propsPicker)(sceneBundle.Component); 79 | let displayName = 80 | sceneBundle.Component.displayName || 81 | sceneBundle.Component.name || 82 | "Unknown"; 83 | PlayingScene.displayName = `SceneConnect({reducerKey:${ 84 | newReduxInfo.reducerKey 85 | },Component:${displayName}})`; 86 | let newArenaState = { 87 | PlayingScene, 88 | curSceneBundle: sceneBundle 89 | }; 90 | yield put({ 91 | type: ActionTypes.ARENA_CURTAIN_SET_STATE, 92 | _reducerKey: arenaCurtainReducerKey, 93 | state: newArenaState 94 | }); 95 | loadedCb(); 96 | if ( 97 | reduxInfo != null && 98 | newReduxInfo.reducerKey !== reduxInfo.reducerKey && 99 | reduxInfo.reducerKey != null 100 | ) { 101 | let arenaStore = yield getContext("store"); 102 | yield put({ 103 | type: ActionTypes.ARENA_STATETREE_NODE_DISABLE, 104 | reducerKey: reduxInfo.reducerKey 105 | }); 106 | arenaStore.removeSingleReducer(reduxInfo.reducerKey); 107 | yield put({ 108 | type: ActionTypes.ARENA_STATETREE_NODE_DELETE, 109 | reducerKey: reduxInfo.reducerKey 110 | }); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/tools/bundleToElement.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { ActionCreatorsMapObject } from "redux"; 3 | import { DefaultSceneActions } from "../core/types"; 4 | import { ArenaSceneExtraProps, ArenaScene } from "../hocs"; 5 | import { 6 | Omit, 7 | ActionsProps, 8 | SceneBundleNo, 9 | SceneBundleNoS, 10 | SceneBundleNoA, 11 | SceneBundleNoPP, 12 | SceneBundleNoAPP, 13 | SceneBundleNoSA, 14 | SceneBundleNoSPP, 15 | SceneBundleNoSAPP 16 | } from "./types"; 17 | import { 18 | defaultPropsPicker, 19 | defaultActions, 20 | defaultReducerCreator 21 | } from "./autoFill"; 22 | 23 | function bundleToElement< 24 | P extends PP, 25 | S, 26 | A extends ActionCreatorsMapObject, 27 | PP 28 | >( 29 | bundle: SceneBundleNo, 30 | props?: JSX.IntrinsicAttributes & Omit, 31 | extraProps?: ArenaSceneExtraProps 32 | ): React.ReactElement>; 33 | function bundleToElement

( 34 | bundle: SceneBundleNoS, 35 | props?: JSX.IntrinsicAttributes & Omit, 36 | extraProps?: ArenaSceneExtraProps 37 | ): React.ReactElement>; 38 | function bundleToElement

( 39 | bundle: SceneBundleNoA, 40 | props?: JSX.IntrinsicAttributes & Omit, 41 | extraProps?: ArenaSceneExtraProps 42 | ): React.ReactElement>; 43 | function bundleToElement< 44 | P extends S & ActionsProps, 45 | S, 46 | A extends ActionCreatorsMapObject 47 | >( 48 | bundle: SceneBundleNoPP, 49 | props?: JSX.IntrinsicAttributes & Omit)>, 50 | extraProps?: ArenaSceneExtraProps 51 | ): React.ReactElement< 52 | JSX.IntrinsicAttributes & Omit)> 53 | >; 54 | function bundleToElement

( 55 | bundle: SceneBundleNoSA, 56 | props?: JSX.IntrinsicAttributes & Omit, 57 | extraProps?: ArenaSceneExtraProps 58 | ): React.ReactElement>; 59 | function bundleToElement< 60 | P extends ActionsProps, 61 | A extends ActionCreatorsMapObject 62 | >( 63 | bundle: SceneBundleNoSPP, 64 | props?: JSX.IntrinsicAttributes & Omit)>, 65 | extraProps?: ArenaSceneExtraProps 66 | ): React.ReactElement< 67 | JSX.IntrinsicAttributes & Omit)> 68 | >; 69 | function bundleToElement

>, S>( 70 | bundle: SceneBundleNoAPP, 71 | props?: JSX.IntrinsicAttributes & 72 | Omit>)>, 73 | extraProps?: ArenaSceneExtraProps 74 | ): React.ReactElement< 75 | JSX.IntrinsicAttributes & 76 | Omit>)> 77 | >; 78 | function bundleToElement

>>( 79 | bundle: SceneBundleNoSAPP

, 80 | props?: JSX.IntrinsicAttributes & 81 | Omit>)>, 82 | extraProps?: ArenaSceneExtraProps 83 | ): React.ReactElement< 84 | JSX.IntrinsicAttributes & 85 | Omit>)> 86 | >; 87 | function bundleToElement( 88 | bundle: any, 89 | props?: any, 90 | extraProps?: ArenaSceneExtraProps 91 | ) { 92 | let newBundle = Object.assign( 93 | { 94 | propsPicker: defaultPropsPicker, 95 | actions: defaultActions, 96 | reducer: defaultReducerCreator(bundle.state) 97 | }, 98 | bundle 99 | ); 100 | return ( 101 | 102 | ); 103 | } 104 | 105 | export default bundleToElement; 106 | -------------------------------------------------------------------------------- /src/hocs/ArenaScene/ArenaScene.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as PropTypes from "prop-types"; 3 | import { ActionCreatorsMapObject, AnyAction } from "redux"; 4 | import { EnhancedStore, ReducerDict, SceneBundle } from "../../core"; 5 | import ActionTypes from "../../core/ActionTypes"; 6 | import { createCurtainReducer } from "../../core/reducers"; 7 | import { 8 | addStateTreeNode, 9 | curtainAddReducer, 10 | buildCurtainReducerDict 11 | } from "../../utils"; 12 | import { curtainConnect } from "../BundleComponent"; 13 | import { Props, State, Context } from "./types"; 14 | 15 | function buildConnectedSceneBundle( 16 | reducerKey: string, 17 | store: EnhancedStore 18 | ) { 19 | let sagaTaskPromise = new Promise(resolve => 20 | store.dispatch({ 21 | type: ActionTypes.ARENA_CURTAIN_INIT_SAGA, 22 | reducerKey, 23 | setSagaTask: resolve 24 | }) 25 | ); 26 | return curtainConnect(reducerKey, () => 27 | store.dispatch({ 28 | type: ActionTypes.ARENA_CURTAIN_CLEAR_REDUX, 29 | reducerKey, 30 | sagaTaskPromise 31 | }) 32 | ); 33 | } 34 | 35 | function getParentReducerKey(arenaReducerDict: ReducerDict) { 36 | return ( 37 | arenaReducerDict && 38 | arenaReducerDict._arenaScene && 39 | arenaReducerDict._arenaScene.reducerKey 40 | ); 41 | } 42 | 43 | export default class ArenaScene extends React.Component { 44 | static contextTypes = { 45 | store: PropTypes.any, 46 | arenaReducerDict: PropTypes.object 47 | }; 48 | 49 | componentWillMount() { 50 | let { store, arenaReducerDict } = this.context; 51 | let { sceneBundle, sceneProps, reducerKey, vReducerKey } = this.props; 52 | let newReducerKey = curtainAddReducer( 53 | store, 54 | reducerKey, 55 | createCurtainReducer 56 | ); 57 | let parentReducerKey = getParentReducerKey(arenaReducerDict); 58 | addStateTreeNode(store, parentReducerKey, newReducerKey); 59 | let newArenaReducerDict = buildCurtainReducerDict( 60 | arenaReducerDict, 61 | newReducerKey, 62 | vReducerKey 63 | ); 64 | let ConnectedBundleComponent = buildConnectedSceneBundle( 65 | newReducerKey, 66 | this.context.store 67 | ); 68 | let connectedBundleElement = React.createElement(ConnectedBundleComponent, { 69 | arenaReducerDict: newArenaReducerDict, 70 | sceneBundle, 71 | sceneProps 72 | }); 73 | this.setState({ 74 | parentReducerKey, 75 | arenaReducerDict: newArenaReducerDict, 76 | ConnectedBundleComponent, 77 | connectedBundleElement 78 | }); 79 | } 80 | 81 | componentWillReceiveProps(nextProps: Props, nextContext: Context) { 82 | let refreshFlag = false; 83 | let state: State = Object.assign({}, this.state); 84 | let { reducerKey, vReducerKey, sceneBundle, sceneProps } = nextProps; 85 | let curReducerKey = state.arenaReducerDict._arenaCurtain.reducerKey; 86 | let newReducerKey = curReducerKey; 87 | if (reducerKey != null && reducerKey !== curReducerKey) { 88 | refreshFlag = true; 89 | newReducerKey = curtainAddReducer( 90 | nextContext.store, 91 | reducerKey, 92 | createCurtainReducer 93 | ); 94 | addStateTreeNode( 95 | nextContext.store, 96 | this.state.parentReducerKey, 97 | newReducerKey 98 | ); 99 | state.ConnectedBundleComponent = buildConnectedSceneBundle( 100 | newReducerKey, 101 | nextContext.store 102 | ); 103 | } 104 | if ( 105 | nextContext.arenaReducerDict !== this.context.arenaReducerDict || 106 | reducerKey !== this.props.reducerKey || 107 | vReducerKey !== this.props.vReducerKey || 108 | sceneBundle !== this.props.sceneBundle || 109 | sceneProps !== this.props.sceneProps || 110 | refreshFlag === true 111 | ) { 112 | refreshFlag = true; 113 | state.arenaReducerDict = buildCurtainReducerDict( 114 | nextContext.arenaReducerDict, 115 | newReducerKey, 116 | nextProps.vReducerKey 117 | ); 118 | state.connectedBundleElement = React.createElement( 119 | state.ConnectedBundleComponent, 120 | { 121 | sceneBundle, 122 | sceneProps, 123 | arenaReducerDict: state.arenaReducerDict 124 | } 125 | ); 126 | } 127 | this.setState(state); 128 | } 129 | 130 | render() { 131 | return this.state.connectedBundleElement; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs-extra"); 2 | const path = require("path"); 3 | const ts = require("typescript"); 4 | 5 | const files = [ 6 | "README.md", 7 | "LICENSE", 8 | "subModules/tools", 9 | "subModules/ActionTypes", 10 | "subModules/effects" 11 | ]; 12 | 13 | // make sure we're in the right folder 14 | process.chdir(path.resolve(__dirname, "..")); 15 | 16 | const binFolder = path.resolve("node_modules/.bin/"); 17 | const buildFolder = "build"; 18 | 19 | fs.removeSync(buildFolder); 20 | 21 | function runTypeScriptBuild(outDir, target, moduleKind, isDeclarationOut) { 22 | console.log( 23 | `Running typescript build (target: ${ 24 | ts.ScriptTarget[target] 25 | }, moduleKind: ${moduleKind}) in ${outDir}/` 26 | ); 27 | 28 | const tsConfig = path.resolve("tsconfig.json"); 29 | const json = ts.parseConfigFileTextToJson( 30 | tsConfig, 31 | ts.sys.readFile(tsConfig), 32 | true 33 | ); 34 | 35 | const { options } = ts.parseJsonConfigFileContent( 36 | json.config, 37 | ts.sys, 38 | path.dirname(tsConfig) 39 | ); 40 | 41 | options.target = target; 42 | options.outDir = outDir; 43 | options.paths = undefined; 44 | if (isDeclarationOut) { 45 | options.declaration = true; 46 | options.declarationDir = buildFolder; 47 | } else { 48 | options.declaration = false; 49 | } 50 | 51 | options.module = moduleKind; 52 | options.sourceMap = false; 53 | 54 | const rootFile = path.resolve("src", "index.ts"); 55 | const host = ts.createCompilerHost(options, true); 56 | const prog = ts.createProgram([rootFile], options, host); 57 | const result = prog.emit(); 58 | if (result.emitSkipped) { 59 | const message = result.diagnostics 60 | .map( 61 | d => 62 | `${ts.DiagnosticCategory[d.category]} ${d.code} (${d.file}:${ 63 | d.start 64 | }): ${d.messageText}` 65 | ) 66 | .join("\n"); 67 | 68 | throw new Error(`Failed to compile typescript:\n\n${message}`); 69 | } 70 | } 71 | 72 | function copyFile(file) { 73 | return new Promise(resolve => { 74 | fs.copy(file, path.resolve(buildFolder, path.basename(file)), err => { 75 | if (err) throw err; 76 | resolve(); 77 | }); 78 | }).then(() => console.log(`Copied ${file} to ${buildFolder}`)); 79 | } 80 | 81 | function createPackageFile() { 82 | return new Promise(resolve => { 83 | fs.readFile(path.resolve("package.json"), "utf8", (err, data) => { 84 | if (err) { 85 | throw err; 86 | } 87 | resolve(data); 88 | }); 89 | }) 90 | .then(data => JSON.parse(data)) 91 | .then(packageData => { 92 | const { 93 | name, 94 | author, 95 | version, 96 | description, 97 | keywords, 98 | repository, 99 | license, 100 | bugs, 101 | homepage, 102 | peerDependencies, 103 | dependencies 104 | } = packageData; 105 | 106 | const minimalPackage = { 107 | name, 108 | author, 109 | version, 110 | description, 111 | main: "lib/index.js", 112 | module: "es/index.js", 113 | "jsnext:main": "es/index.js", 114 | typings: "index.d.ts", 115 | keywords, 116 | repository, 117 | license, 118 | bugs, 119 | homepage, 120 | peerDependencies, 121 | dependencies 122 | }; 123 | 124 | return new Promise(resolve => { 125 | const buildPath = path.resolve(`${buildFolder}/package.json`); 126 | const data = JSON.stringify(minimalPackage, null, 2); 127 | fs.writeFile(buildPath, data, err => { 128 | if (err) throw err; 129 | console.log(`Created package.json in ${buildPath}`); 130 | resolve(); 131 | }); 132 | }); 133 | }); 134 | } 135 | 136 | function build() { 137 | let buildCJSPromise = new Promise(resolve => { 138 | runTypeScriptBuild( 139 | `${buildFolder}/lib`, 140 | ts.ScriptTarget.ES5, 141 | ts.ModuleKind.CommonJS, 142 | true 143 | ); 144 | resolve(); 145 | }); 146 | let buildESPromise = new Promise(resolve => { 147 | runTypeScriptBuild( 148 | `${buildFolder}/es`, 149 | ts.ScriptTarget.ES5, 150 | ts.ModuleKind.ES2015 151 | ); 152 | resolve(); 153 | }); 154 | return Promise.all([buildCJSPromise, buildESPromise]).then(() => 155 | Promise.all(files.map(file => copyFile(file))).then(() => 156 | createPackageFile() 157 | ) 158 | ); 159 | } 160 | 161 | build().catch(e => { 162 | console.error(e); 163 | if (e.frame) { 164 | console.error(e.frame); 165 | } 166 | process.exit(1); 167 | }); 168 | -------------------------------------------------------------------------------- /test/integration/ArenaScene/hotReplace.spec.tsx: -------------------------------------------------------------------------------- 1 | import { AnyAction } from "redux"; 2 | import { ReactWrapper } from "enzyme"; 3 | import { expect } from "chai"; 4 | import { spy } from "sinon"; 5 | import { createArenaStore, EnhancedStore, SceneBundle } from "src"; 6 | import sceneBundleForTestA from "../../sceneBundleForTestA"; 7 | import sceneBundleForTestB from "../../sceneBundleForTestB"; 8 | import { MountBundle } from "./types"; 9 | import createBundleMounter from "./createBundleMounter"; 10 | 11 | function selectNeededStates(allStates: any, name: string): any { 12 | let { arena, ...otherState } = allStates; 13 | let metaState, bundleState; 14 | Object.keys(otherState).forEach(key => { 15 | if (otherState[key].name === name) { 16 | bundleState = otherState[key]; 17 | } else if (otherState[key].curSceneBundle != null) { 18 | metaState = otherState[key]; 19 | } 20 | }); 21 | return { 22 | arena, 23 | metaState, 24 | bundleState 25 | }; 26 | } 27 | 28 | describe(" integration", () => { 29 | let store: EnhancedStore, 30 | mountSceneBundle: MountBundle, 31 | wrapper: ReactWrapper, 32 | cleanUp: () => void; 33 | 34 | before(() => { 35 | store = createArenaStore(); 36 | [mountSceneBundle, cleanUp] = createBundleMounter(); 37 | wrapper = mountSceneBundle(store, sceneBundleForTestA); 38 | }); 39 | 40 | after(() => { 41 | cleanUp(); 42 | store.close(); 43 | }); 44 | 45 | it("should hot replace state correctly", () => { 46 | let newProps = { 47 | sceneBundle: Object.assign({}, sceneBundleForTestA, { 48 | state: Object.assign({}, sceneBundleForTestA.state, { 49 | pageA: false 50 | }) 51 | }) 52 | }; 53 | wrapper.setProps(newProps); 54 | let flagPromise = new Promise(resolve => { 55 | let unsubscribe = store.subscribe(() => { 56 | let { arena, metaState, bundleState } = selectNeededStates( 57 | store.getState(), 58 | "PageA" 59 | ); 60 | if (arena && metaState && bundleState) { 61 | if ( 62 | bundleState.cnt !== 4 || 63 | bundleState.sagaCnt !== 1 || 64 | bundleState.pageA !== false 65 | ) 66 | return; 67 | unsubscribe(); 68 | resolve(true); 69 | } 70 | }); 71 | }); 72 | return flagPromise; 73 | }); 74 | 75 | it("should hot replace bundle correctly", () => { 76 | let newProps = { 77 | sceneBundle: Object.assign({}, sceneBundleForTestB, { 78 | options: { 79 | reducerKey: "testReducerKey", 80 | vReducerKey: "testVReducerKey" 81 | } 82 | }) 83 | }; 84 | let flagPromise = new Promise(resolve => { 85 | let unsubscribe = store.subscribe(() => { 86 | let state: any = store.getState(); 87 | let { arena, metaState, bundleState } = selectNeededStates( 88 | state, 89 | "PageB" 90 | ); 91 | if (state.testReducerKey == null) return; 92 | if (arena && metaState && bundleState) { 93 | expect(bundleState.cnt).to.be.equal(0); 94 | expect(bundleState.sagaCnt).to.be.undefined; 95 | expect(bundleState.pageB).to.be.true; 96 | unsubscribe(); 97 | resolve(true); 98 | } 99 | }); 100 | }); 101 | wrapper.setProps(newProps); 102 | return flagPromise; 103 | }); 104 | 105 | it("should hot replace reducer correctly", () => { 106 | let newProps = { 107 | sceneBundle: Object.assign({}, sceneBundleForTestB, { 108 | reducer: ( 109 | state: any = sceneBundleForTestA.state, 110 | action: AnyAction 111 | ): any => { 112 | switch (action.type) { 113 | case "ADD_CNT": 114 | return Object.assign({}, state, { cnt: state.cnt + 16 }); 115 | default: 116 | return state; 117 | } 118 | }, 119 | options: { 120 | reducerKey: "testReducerKey2", 121 | vReducerKey: "testVReducerKey2", 122 | isSceneReducer: false, 123 | isSceneActions: false 124 | } 125 | }) 126 | }; 127 | let flagPromise = new Promise(resolve => { 128 | let unsubscribe = store.subscribe(() => { 129 | let { arena, metaState, bundleState } = selectNeededStates( 130 | store.getState(), 131 | "PageB" 132 | ); 133 | if (bundleState) { 134 | if (bundleState.cnt !== 16) return; 135 | unsubscribe(); 136 | resolve(true); 137 | } 138 | }); 139 | }); 140 | wrapper.setProps(newProps); 141 | setTimeout(() => store.dispatch({ type: "ADD_CNT" }), 100); 142 | return flagPromise; 143 | }); 144 | }); 145 | -------------------------------------------------------------------------------- /src/core/reducers/arenaReducer.ts: -------------------------------------------------------------------------------- 1 | import { AnyAction } from "redux"; 2 | import { Map, List } from "immutable"; 3 | import ActionTypes from "../ActionTypes"; 4 | import getArenaInitState from "./getArenaInitState"; 5 | import { ArenaState, StateTreeNode, StateTree, StateTreeDict } from "./types"; 6 | 7 | function addStateTreeNode( 8 | state: ArenaState, 9 | pReducerKey: string, 10 | reducerKey: string 11 | ) { 12 | let { stateTree, stateTreeDict } = state; 13 | let newStateTree, newStateTreeDict; 14 | let plainNewNode: StateTreeNode = { 15 | pReducerKey: null, 16 | reducerKey, 17 | children: Map() 18 | }; 19 | let newNode = Map(plainNewNode); 20 | if (pReducerKey == null) { 21 | newStateTree = stateTree.set(reducerKey, newNode); 22 | newStateTreeDict = stateTreeDict.set( 23 | reducerKey, 24 | Map({ path: List([reducerKey]) }) 25 | ); 26 | } else { 27 | let pPath = stateTreeDict.getIn([pReducerKey, "path"]); 28 | let pNode = stateTree.getIn(pPath); 29 | newNode = newNode.set("pReducerKey", pNode.get("reducerKey")); 30 | let path = pPath.concat(["children", reducerKey]); 31 | let newPNode = pNode.setIn(["children", reducerKey], newNode); 32 | newStateTree = stateTree.setIn(pPath, newPNode); 33 | newStateTreeDict = stateTreeDict.set( 34 | reducerKey, 35 | Map({ path, isObsolete: false }) 36 | ); 37 | } 38 | return Object.assign({}, state, { 39 | stateTree: newStateTree, 40 | stateTreeDict: newStateTreeDict 41 | }); 42 | } 43 | 44 | function getReducerKeysOfNode( 45 | node: Map, 46 | breakChecker?: (node: Map) => boolean 47 | ): List { 48 | if (breakChecker && breakChecker(node)) return List(); 49 | return node 50 | .get("children") 51 | .map((child: Map) => getReducerKeysOfNode(child, breakChecker)) 52 | .reduce( 53 | (prev: List, cur: List) => prev.concat(cur), 54 | List([node.get("reducerKey")]) 55 | ); 56 | } 57 | 58 | function disableStateTreeNode(state: ArenaState, reducerKey: string) { 59 | let { stateTreeDict, stateTree } = state; 60 | let path = stateTreeDict.getIn([reducerKey, "path"]); 61 | let node = stateTree.getIn(path); 62 | let obsoleteKeyList = getReducerKeysOfNode( 63 | node, 64 | node => stateTreeDict.getIn([node.get("reducerKey"), "isObsolete"]) === true 65 | ); 66 | let deltaDict = Map( 67 | obsoleteKeyList.map((tmpKey: string) => [ 68 | tmpKey, 69 | stateTreeDict.get(tmpKey).set("isObsolete", true) 70 | ]) 71 | ); 72 | let newStateTreeDict = stateTreeDict.merge(deltaDict); 73 | return Object.assign({}, state, { 74 | stateTreeDict: newStateTreeDict 75 | }); 76 | } 77 | 78 | function findNodeUpward( 79 | path: List, 80 | stateTree: StateTree, 81 | stateTreeDict: StateTreeDict, 82 | checker: (node: Map, dictItem: Map) => boolean 83 | ): List | null { 84 | let node = stateTree.getIn(path); 85 | let dictItem = stateTreeDict.get(node.get("reducerKey")); 86 | if (checker(node, dictItem)) { 87 | if (path.count() > 1) { 88 | let nextPath: List | null = findNodeUpward( 89 | path.skipLast(2).toList(), 90 | stateTree, 91 | stateTreeDict, 92 | checker 93 | ); 94 | return nextPath == null ? path : nextPath; 95 | } else { 96 | return path; 97 | } 98 | } else { 99 | return null; 100 | } 101 | } 102 | 103 | function deleteStateTreeNode(state: ArenaState, reducerKey: string) { 104 | let { stateTree, stateTreeDict } = state; 105 | let path = stateTreeDict.getIn([reducerKey, "path"]); 106 | let node = stateTree.getIn(path); 107 | if (node.get("children").count() === 0) { 108 | let path4Del = findNodeUpward( 109 | path, 110 | stateTree, 111 | stateTreeDict, 112 | (pNode, dictItem) => 113 | dictItem.get("isObsolete") === true && 114 | pNode.get("children").count() === 1 115 | ); 116 | if (path4Del != null) { 117 | let obsoleteKeyList = getReducerKeysOfNode(stateTree.getIn(path4Del)); 118 | let newStateTree = stateTree.deleteIn(path4Del); 119 | let newStateTreeDict = stateTreeDict.filterNot( 120 | (_, key) => key === undefined || obsoleteKeyList.includes(key) 121 | ); 122 | return Object.assign({}, state, { 123 | stateTree: newStateTree, 124 | stateTreeDict: newStateTreeDict 125 | }); 126 | } 127 | } 128 | return state; 129 | } 130 | 131 | export default function reducer( 132 | state = getArenaInitState(), 133 | action: AnyAction 134 | ) { 135 | switch (action.type) { 136 | case ActionTypes.ARENA_SET_STATE: 137 | return Object.assign({}, state, action.state); 138 | case ActionTypes.ARENA_REPLACE_STATE: 139 | return Object.assign({}, action.state); 140 | case ActionTypes.ARENA_GLOBAL_PROPSPICKER_LOCK: 141 | return Object.assign({ propsLock: action.lock }); 142 | case ActionTypes.ARENA_STATETREE_NODE_ADD: 143 | return addStateTreeNode(state, action.pReducerKey, action.reducerKey); 144 | case ActionTypes.ARENA_STATETREE_NODE_DISABLE: 145 | return disableStateTreeNode(state, action.reducerKey); 146 | case ActionTypes.ARENA_STATETREE_NODE_DELETE: 147 | return deleteStateTreeNode(state, action.reducerKey); 148 | default: 149 | return state; 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/core/sagas/sceneReduxSaga.ts: -------------------------------------------------------------------------------- 1 | import ActionTypes from "../ActionTypes"; 2 | import { 3 | put, 4 | PutEffect, 5 | fork, 6 | ForkEffect, 7 | select, 8 | SelectEffect, 9 | getContext, 10 | setContext, 11 | CallEffectFactory, 12 | GetContextEffect 13 | } from "redux-saga/effects"; 14 | import { bindActionCreators, ActionCreatorsMapObject, Dispatch } from "redux"; 15 | import { bindArenaActionCreators } from "../enhancedRedux"; 16 | import { createSceneReducer, sceneReducerWrapper } from "../reducers"; 17 | import { 18 | addStateTreeNode, 19 | sceneAddReducer, 20 | sceneReplaceReducer, 21 | buildSceneReducerDict 22 | } from "../../utils"; 23 | import { 24 | ReducerDict, 25 | SceneReducer, 26 | SceneBundleOptions, 27 | SceneBundle 28 | } from "../types"; 29 | import { CurtainReduxInfo } from "../reducers/types"; 30 | 31 | function bindActions( 32 | actions: ActionCreatorsMapObject, 33 | reducerKey: string, 34 | dispatch: Dispatch, 35 | isSceneActions: boolean 36 | ) { 37 | if (isSceneActions === false) { 38 | return bindActionCreators(actions, dispatch); 39 | } else { 40 | return bindArenaActionCreators(actions, dispatch, reducerKey); 41 | } 42 | } 43 | 44 | function* forkSagaWithContext(saga: () => null, ctx: any) { 45 | yield setContext(ctx); 46 | yield fork(saga); 47 | } 48 | 49 | function buildReducerFactory( 50 | reducer: SceneReducer, 51 | state: S, 52 | isSceneReducer: boolean 53 | ) { 54 | return isSceneReducer === false 55 | ? (bindingReducerKey: string) => 56 | createSceneReducer(reducer, state, bindingReducerKey) 57 | : (bindingReducerKey: string) => 58 | createSceneReducer( 59 | sceneReducerWrapper(reducer), 60 | state, 61 | bindingReducerKey 62 | ); 63 | } 64 | 65 | function getParentReducerKey(arenaReducerDict: ReducerDict) { 66 | return ( 67 | arenaReducerDict && 68 | arenaReducerDict._arenaCurtain && 69 | arenaReducerDict._arenaCurtain.reducerKey 70 | ); 71 | } 72 | 73 | export interface ApplyReduxPayload< 74 | P, 75 | S, 76 | A extends ActionCreatorsMapObject, 77 | PP 78 | > { 79 | arenaReducerDict: ReducerDict; 80 | state: S; 81 | saga: ((...params: any[]) => any) | null | undefined; 82 | actions: ActionCreatorsMapObject; 83 | reducer: SceneReducer; 84 | options: SceneBundleOptions | null | undefined; 85 | } 86 | 87 | export function* sceneApplyRedux({ 88 | arenaReducerDict, 89 | state, 90 | saga, 91 | actions, 92 | reducer, 93 | options 94 | }: ApplyReduxPayload): any { 95 | let curOptions = options || {}; 96 | let arenaStore = yield getContext("store"); 97 | let reducerFactory = buildReducerFactory( 98 | reducer, 99 | state, 100 | curOptions.isSceneReducer === false ? false : true 101 | ); 102 | let newReducerKey = sceneAddReducer( 103 | arenaStore, 104 | curOptions.reducerKey, 105 | reducerFactory, 106 | state 107 | ); 108 | let bindedActions = bindActions( 109 | actions, 110 | newReducerKey, 111 | arenaStore.dispatch, 112 | curOptions.isSceneActions === false ? false : true 113 | ); 114 | let newArenaReducerDict = buildSceneReducerDict( 115 | arenaReducerDict, 116 | newReducerKey, 117 | curOptions.vReducerKey, 118 | bindedActions 119 | ); 120 | let newReduxInfo = { 121 | reducerKey: newReducerKey, 122 | origArenaReducerDict: arenaReducerDict, 123 | actions, 124 | options: curOptions, 125 | saga, 126 | bindedActions, 127 | arenaReducerDict: newArenaReducerDict 128 | }; 129 | addStateTreeNode( 130 | arenaStore, 131 | getParentReducerKey(newReduxInfo.arenaReducerDict), 132 | newReducerKey 133 | ); 134 | if (saga) { 135 | newReduxInfo.saga = yield fork(forkSagaWithContext, saga, { 136 | arenaReducerDict: newReduxInfo.arenaReducerDict 137 | }); 138 | } 139 | return newReduxInfo as CurtainReduxInfo; 140 | } 141 | export interface UpdateReduxPayload< 142 | P extends PP, 143 | S, 144 | A extends ActionCreatorsMapObject, 145 | PP 146 | > extends ApplyReduxPayload { 147 | curSceneBundle: SceneBundle; 148 | reduxInfo: CurtainReduxInfo; 149 | } 150 | 151 | export function* sceneUpdateRedux< 152 | P extends PP, 153 | S, 154 | A extends ActionCreatorsMapObject, 155 | PP 156 | >({ 157 | arenaReducerDict, 158 | state, 159 | saga, 160 | actions, 161 | reducer, 162 | options, 163 | curSceneBundle, 164 | reduxInfo 165 | }: UpdateReduxPayload): any { 166 | let curOptions = options || {}; 167 | let newReducerKey = reduxInfo.reducerKey; 168 | let arenaStore = yield getContext("store"); 169 | let reducerFactory = buildReducerFactory( 170 | reducer, 171 | state, 172 | curOptions.isSceneReducer === false ? false : true 173 | ); 174 | let newReduxInfo = Object.assign({}, reduxInfo, { options: curOptions }); 175 | if ( 176 | curOptions.reducerKey != null && 177 | curOptions.reducerKey !== reduxInfo.reducerKey 178 | ) { 179 | let oldState = yield select((state: any) => state[reduxInfo.reducerKey]); 180 | newReducerKey = sceneAddReducer( 181 | arenaStore, 182 | curOptions.reducerKey, 183 | reducerFactory, 184 | state === curSceneBundle.state ? oldState : state 185 | ); 186 | addStateTreeNode( 187 | arenaStore, 188 | getParentReducerKey(newReduxInfo.arenaReducerDict), 189 | newReducerKey 190 | ); 191 | } else if (curOptions.reducerKey === reduxInfo.reducerKey) { 192 | if ( 193 | reducer !== curSceneBundle.reducer || 194 | curOptions.isSceneReducer !== reduxInfo.options.isSceneReducer 195 | ) { 196 | newReducerKey = sceneReplaceReducer( 197 | arenaStore, 198 | reduxInfo.reducerKey, 199 | reducerFactory, 200 | state === curSceneBundle.state ? null : state 201 | ); 202 | } else if (state !== curSceneBundle.state) { 203 | arenaStore.dispatch({ 204 | type: ActionTypes.ARENA_SCENE_REPLACE_STATE, 205 | _sceneReducerKey: newReducerKey, 206 | state 207 | }); 208 | } 209 | } else if (state !== curSceneBundle.state) { 210 | yield put({ 211 | type: ActionTypes.ARENA_SCENE_REPLACE_STATE, 212 | _sceneReducerKey: newReducerKey, 213 | state 214 | }); 215 | } 216 | newReduxInfo.reducerKey = newReducerKey; 217 | if ( 218 | actions !== reduxInfo.actions || 219 | curOptions.isSceneActions !== reduxInfo.options.isSceneActions 220 | ) { 221 | newReduxInfo.actions = actions; 222 | newReduxInfo.bindedActions = bindActions( 223 | actions, 224 | newReducerKey, 225 | arenaStore.dispatch, 226 | curOptions.isSceneActions === false ? false : true 227 | ); 228 | } 229 | if ( 230 | newReducerKey !== reduxInfo.reducerKey || 231 | curOptions.vReducerKey !== reduxInfo.options.vReducerKey || 232 | arenaReducerDict !== reduxInfo.origArenaReducerDict 233 | ) { 234 | newReduxInfo.arenaReducerDict = buildSceneReducerDict( 235 | arenaReducerDict, 236 | newReducerKey, 237 | curOptions.vReducerKey, 238 | newReduxInfo.actions 239 | ); 240 | } 241 | if (saga) { 242 | newReduxInfo.saga = yield fork(forkSagaWithContext, saga, { 243 | arenaReducerDict: newReduxInfo.arenaReducerDict 244 | }); 245 | } 246 | return newReduxInfo as CurtainReduxInfo; 247 | } 248 | -------------------------------------------------------------------------------- /README.zh-CN.MD: -------------------------------------------------------------------------------- 1 | # redux-arena 2 | 3 | [![Build Status](https://travis-ci.org/hapood/redux-arena.svg?branch=master)](https://travis-ci.org/hapood/redux-arena) 4 | [![Coverage Status](https://coveralls.io/repos/github/hapood/redux-arena/badge.svg?branch=master)](https://coveralls.io/github/hapood/redux-arena?branch=master) 5 | [![npm version](https://img.shields.io/npm/v/redux-arena.svg?style=flat-square)](https://www.npmjs.com/package/redux-arena) 6 | [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md#pull-requests) 7 | 8 | Redux毫无疑问是一个模型简洁漂亮,扩展性极佳的状态管理器,但是在其与React整合使用时,我们有时会希望React能与Redux的代码整合起来形成一个可复用的复杂组件,具体的描述可以参见 [RFC: Reuse complex components implemented in React plus Redux #278](https://github.com/reactjs/react-redux/issues/278), Redux-Arena就是一个为解决这个问题开发的Redux模块化管理器。 9 | 10 | 11 | ## 功能特性 12 | 13 | Redux-Arena会将Redux/Redux-Saga的代码与React组件导出成一个React高阶组件以供复用: 14 | 1. 在高阶组件被挂载(Mount)时,会自动初始化Redux-Saga的任务,初始化组件的reducer并在Redux维护的State上注册自己的节点。 15 | 2. 在高阶组件被卸载(Unmout)时,会自动取消Redux-Saga的任务,销毁组件的reducer并从Redux维护的State上删除自己的节点。 16 | 3. 提供组件信道机制,组件发送的Action默认只能被组件自己的reducer接收。也可以通过配置放弃信道,接收全局的action。 17 | 4. 提供vReducerKey机制,Redux中如果组件间想共享state信息,需要知道知道真实的节点名称,在可复用的Redux组件中很容易引发冲突,Redux-Arena提供vReducerKey机制保证了state节点真实名称永远不会冲突。vReducerKey在同名时,下层组件会覆盖掉上层组件的vReducerKey信息。 18 | 5. 提供单向的(类似flux的 one-way data flow)组件状态和actions的共享方案,下层组件可以通过vReducerKey获取上层组件的state和actions。 19 | 6. 与Redux-Saga深度整合,在Redux-Saga中也可以选择只发送和接收组件自己的action。 20 | 21 | 此外,Redux-Arena还提供了与React-Router的整合方案。 22 | 23 | ## 安装 24 | 25 | ``` 26 | npm install redux-arena --save 27 | ``` 28 | 29 | ## [示例](https://hapood.github.io/redux-arena/) 30 | 31 | `/example`目录下包含了一个完整的示例,包括了多个HOC的使用。并且使用了redux-devtool动态展示state的变化。 32 | 在线版本的示例点击[这里](https://hapood.github.io/redux-arena/) 33 | 34 | ### Screenshots 35 | 36 | 37 | ## 快速入门 38 | 39 | 1. 将react组件、actions、reducer、saga 文件导出成React组件 40 | 41 | ```javascript 42 | import { bundleToComponent } from "redux-arena/tools"; 43 | import state from "./state"; 44 | import saga from "./saga"; 45 | import * as actions from "./actions"; 46 | import PageA from "./PageA"; 47 | 48 | export default bundleToComponent({ 49 | Component: PageA, 50 | state, 51 | saga, 52 | actions 53 | }) 54 | ``` 55 | 56 | 2. 初始化arenaStore并将其提供给redux。PageA组件是上一步导出的。 57 | 58 | ```javascript 59 | import React from "react"; 60 | import ReactDOM from "react-dom"; 61 | import { Provider } from "react-redux"; 62 | import { createArenaStore } from "redux-arena"; 63 | import PageA from "./pageA"; 64 | 65 | let store = createArenaStore(); 66 | 67 | let app = document.getElementById("app"); 68 | ReactDOM.render( 69 | 70 | 71 | , 72 | app 73 | ); 74 | ``` 75 | 76 | # API Reference 77 | 78 | * [`EnhancedRedux API`](#enhancedredux-api) 79 | * [`createArenaStore(reducers, initialStates, enhencers, sagaOptions): enhancedStore`](#createarenastorereducers-initialstates-enhancers-sagaoptions-enhancedstore) 80 | * [`Bundle API`](#bundle-api) 81 | * [`Tools API`](#tools-api) 82 | * [`bundleToComponent(bundle, extraProps)`](#bundletocomponentbundle-extraProps) 83 | * [`asyncBundleToComponent(asyncBundle, extraProps)`](#bundletoelementbundle-props-extraProps) 84 | * [`Saga API`](#tools-api) 85 | * [`getSceneState()`](#getscenestate) 86 | * [`getSceneActions()`](#getsceneactions) 87 | * [`putSceneAction(action)`](#putsceneactionaction) 88 | * [`setSceneState(state)`](#setscenestatestate) 89 | * [`takeEverySceneAction(pattern, saga, ...args)`](#takeeverysceneactionpattern-saga-args) 90 | * [`takeLatestSceneAction(pattern, saga, ..args)`](#takelatestsceneactionpattern-saga-args) 91 | * [`takeSceneAction(pattern)`](#takesceneactionpattern) 92 | 93 | ## EnhancedRedux API 94 | 95 | ### `createArenaStore(reducers, initialStates, enhancers, sagaOptions): enhancedStore` 96 | 97 | 为redux-arena创建增强版store 98 | 99 | - `reducers: object` - 对象形式的redux的reducer。 100 | 101 | **Example** 102 | 103 | ```javascript 104 | { 105 | frame: (state)=>state, 106 | page: (state)=>state, 107 | ... 108 | } 109 | ``` 110 | 111 | - `options: object` - Store的设置选项。 112 | 113 | - `initialStates: object` - 对象形式的redux的state。 114 | 115 | **Example** 116 | 117 | ```javascript 118 | { 119 | frame: { location:"/" }, 120 | page: { cnt:0 }, 121 | ... 122 | } 123 | ``` 124 | 125 | - `enhencers: array` - 数组形式的redux enhencers。 126 | 127 | **Example** 128 | 129 | ```javascript 130 | import { applyMiddleware } from "redux"; 131 | import thunk from "redux-thunk"; 132 | 133 | let enhancers = [applyMiddleware(thunk)]; 134 | ``` 135 | 136 | - `middlewares: array` - 数组形式的redux middlewares。 137 | 138 | **Example** 139 | 140 | ```javascript 141 | import thunk from "redux-thunk"; 142 | 143 | let middlewares = [thunk]; 144 | ``` 145 | 146 | - `sagaOptions:object` - redux-saga的扩展选项。 147 | 148 | - `enhancedStore:object` - 函数返回值,拥有以下方法: 149 | 150 | - `runSaga(saga)` - 开始一个saga任务. 151 | 152 | 153 | ## Bundle API 154 | 155 | Bundle是一个包含react-component, actions, reducer, saga ,options的对象, 用于ArenaScene高阶组件。 156 | 157 | **Example** 158 | 159 | ```javascript 160 | import state from "./state"; 161 | import saga from "./saga"; 162 | import * as actions from "./actions"; 163 | import Component from "./Component"; 164 | 165 | export default { 166 | Component, 167 | state, 168 | saga, 169 | actions, 170 | options:{ 171 | vReducerkey:"vKey1" 172 | } 173 | } 174 | ``` 175 | ### `createArenaStore(reducers, initialStates, enhencers, sagaOptions): enhancedStore` 176 | 177 | - `Component: React.Component` - 用于绑定Redux的React组件。 178 | 179 | - `state: object` - Bundle的初始状态。 180 | 181 | - `actions: object` - 与Redux的actions相同,当组件被挂载时初始化。 182 | 183 | - `saga: function*` - Redux-Saga的生成器函数,当组件被挂载时初始化。 184 | 185 | - `propsPicker: function(stateDict, actionsDict)` - 挑选state与actions到组件的props。$是相对位置表示符号,$0可以快速拿到当前层次的信息,同理$1可以拿到上一层的信息。如果该项为空,默认会将state中的所有实体绑定至同名的props中,并将actions绑定至props的actions中。 186 | 187 | **Example** 188 | 189 | ```javascript 190 | import state from "./state"; 191 | import saga from "./saga"; 192 | import * as actions from "./actions"; 193 | import Component from "./Component"; 194 | 195 | export default { 196 | Component, 197 | state, 198 | actions, 199 | propsPicker:({$0: state}, {$0: actions})=>({ 200 | a: state.a, 201 | actions 202 | }) 203 | } 204 | ``` 205 | 206 | - `options: object` - Bundle的选项。 207 | 208 | - `reducerKey: string` - 为bundle指定reducerKey。 209 | 210 | - `vReducerKey: string` - 为bundle指定虚拟reducerKey。 211 | 212 | - `isSceneAction: bool` - 如果为false, "_sceneReducerKey"不会添加到发送的action中。 213 | 214 | - `isSceneReducer: bool` - 如果为false, reducer会接收其他bundle发送的action。 215 | 216 | ## Tools API 217 | 218 | ### `bundleToComponent(bundle, extraProps)` 219 | 220 | 将Bundle转化为组件的帮手函数。 221 | 222 | ### `bundleToElement(bundle, props, extraProps)` 223 | 224 | 将Bundle转化为element的帮手函数。 225 | 226 | ## Saga API 227 | 228 | ### `bundleToComponent(bundle, extraProps)` 229 | 230 | ### `getSceneState()` 231 | 232 | 获取当前scene的state。 233 | 234 | **Example** 235 | 236 | ```javascript 237 | import { setSceneState, takeLatestSceneAction } from "redux-arena/effects"; 238 | 239 | function * doSomthing({ payload }){ 240 | yield setSceneState({ payload }) 241 | } 242 | 243 | export function* saga (){ 244 | yield takeLatestSceneAction("DO_SOMETHING", doSomthing) 245 | } 246 | ``` 247 | 248 | ### `getSceneActions()` 249 | 250 | 获取当前scene的state。 251 | 252 | ### `putSceneAction(action)` 253 | 254 | 发送当前scene的action。 255 | 256 | ### `setSceneState(state)` 257 | 258 | 设置当前scene的state。 259 | 260 | **Example** 261 | 262 | ```javascript 263 | import { setSceneState, getSceneState } from "redux-arena/effects"; 264 | 265 | function * doSomthing(){ 266 | let { a } = yield getSceneState() 267 | yield setSceneState({ a : a+1 }) 268 | } 269 | ``` 270 | 271 | ### `takeEverySceneAction(pattern, saga, ...args)` 272 | 273 | 获取当前scene的每一个action 274 | 275 | ### `takeLatestSceneAction(pattern, saga, ..args)` 276 | 277 | 获取当前scene的最新action 278 | 279 | ### `takeSceneAction(pattern)` 280 | 281 | 获取当前scene的action -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # redux-arena 2 | 3 | [![Build Status](https://travis-ci.org/hapood/redux-arena.svg?branch=master)](https://travis-ci.org/hapood/redux-arena) 4 | [![Coverage Status](https://coveralls.io/repos/github/hapood/redux-arena/badge.svg?branch=master)](https://coveralls.io/github/hapood/redux-arena?branch=master) 5 | [![npm version](https://img.shields.io/npm/v/redux-arena.svg?style=flat-square)](https://www.npmjs.com/package/redux-arena) 6 | [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md#pull-requests) 7 | 8 | Redux is a great state management container, which is elaborate and can be easily extent. But there are some problems when resuing a React component binded with Redux, refs to [RFC: Reuse complex components implemented in React plus Redux #278](https://github.com/reactjs/react-redux/issues/278). 9 | 10 | ## Features 11 | 12 | Redux-Arena will export Redux/Redux-Saga code with React component as a high order component for reuse: 13 | 1. When hoc is mounted, it will start Redux-Saga task, initializing reducer of component, and register node on state. 14 | 2. When hoc is unmounted, it will cancel Redux-Saga task, destroy reducer of component, and delete node on state. 15 | 3. Reducer of component will only accept actions dispatched by current component by default. Revert reducer to accept all actions by set options. 16 | 4. Virtual ReducerKey: Sharing state in Redux will know the node's name of state, it will cause name conflict when reuse hoc sometime. Using vReducerKey will never cause name conflict, same vReducerKey will be replaced by child hoc. 17 | 5. Like one-way data flow of Flux, child hoc could get state and actions of parent by vReducerKey. 18 | 6. Integration deeply with Redux-Saga, accept actions dispatched by current component and set state of current component is more easily. 19 | 20 | Integration with React-Router is included. 21 | 22 | ## Install 23 | 24 | ``` 25 | npm install redux-arena --save 26 | ``` 27 | 28 | ## [Example](https://hapood.github.io/redux-arena/) 29 | 30 | A complete example is under `/example` directory, including a lot of HOC. And add redux-devtools for state changing show. 31 | Online example can be found here: [Here](https://hapood.github.io/redux-arena/) 32 | 33 | ### Screenshots 34 | 35 | 36 | ## Quick Start 37 | 38 | 1. Export react component, actions, reducer, saga as React component. 39 | 40 | ```javascript 41 | import { bundleToComponent } from "redux-arena/tools"; 42 | import state from "./state"; 43 | import saga from "./saga"; 44 | import * as actions from "./actions"; 45 | import PageA from "./PageA"; 46 | 47 | export default bundleToComponent({ 48 | Component: PageA, 49 | state, 50 | saga, 51 | actions 52 | }) 53 | ``` 54 | 55 | 2. Initial arenaStore and provide it for redux. PageA Component is exported in last step. 56 | 57 | ```javascript 58 | import React from "react"; 59 | import ReactDOM from "react-dom"; 60 | import { Provider } from "react-redux"; 61 | import { createArenaStore } from "redux-arena"; 62 | import PageA from "./pageA"; 63 | 64 | let store = createArenaStore(); 65 | 66 | let app = document.getElementById("app"); 67 | ReactDOM.render( 68 | 69 | 70 | , 71 | app 72 | ); 73 | ``` 74 | 75 | # API Reference 76 | 77 | * [`EnhancedRedux API`](#enhancedredux-api) 78 | * [`createArenaStore(reducers, initialStates, enhencers, sagaOptions): enhancedStore`](#createarenastorereducers-initialstates-enhancers-sagaoptions-enhancedstore) 79 | * [`Bundle API`](#bundle-api) 80 | * [`Tools API`](#tools-api) 81 | * [`bundleToComponent(bundle, extraProps)`](#bundletocomponentbundle-extraProps) 82 | * [`bundleToElement(bundle, props, extraProps)`](#asyncbundletoelementasyncbundle-props-extraProps) 83 | * [`Saga API`](#tools-api) 84 | * [`getSceneState()`](#getscenestate) 85 | * [`getSceneActions()`](#getsceneactions) 86 | * [`putSceneAction(action)`](#putsceneactionaction) 87 | * [`setSceneState(state)`](#setscenestatestate) 88 | * [`takeEverySceneAction(pattern, saga, ...args)`](#takeeverysceneactionpattern-saga-args) 89 | * [`takeLatestSceneAction(pattern, saga, ..args)`](#takelatestsceneactionpattern-saga-args) 90 | * [`takeSceneAction(pattern)`](#takesceneactionpattern) 91 | 92 | ## EnhancedRedux API 93 | 94 | ### `createArenaStore(reducers, options): enhancedStore` 95 | 96 | Creates a enhanced redux store for redux-arena 97 | 98 | - `reducers: object` - A set of reducers. 99 | 100 | **Example** 101 | 102 | ```javascript 103 | { 104 | frame: (state)=>state, 105 | page: (state)=>state, 106 | ... 107 | } 108 | ``` 109 | 110 | - `options: object` - Options of redux arena store. 111 | 112 | - `initialStates: object` - A set of initial states. 113 | **Example** 114 | 115 | ```javascript 116 | { 117 | frame: { location:"/" }, 118 | page: { cnt:0 }, 119 | ... 120 | } 121 | ``` 122 | 123 | 124 | - `enhencers: array` - An array of redux enhencers. 125 | 126 | **Example** 127 | 128 | ```javascript 129 | import { applyMiddleware } from "redux"; 130 | import thunk from "redux-thunk"; 131 | 132 | let enhancers = [applyMiddleware(thunk)]; 133 | ``` 134 | 135 | - `sagaOptions:object` - Options used for redux-saga. 136 | 137 | - `middlewares: array` - An array of redux middlewares. 138 | 139 | **Example** 140 | 141 | ```javascript 142 | import thunk from "redux-thunk"; 143 | 144 | let middlewares = [thunk]; 145 | ``` 146 | 147 | - `enhancedStore:object` - An enhanced redux store which owning following method. 148 | 149 | - `runSaga(saga)` - start a saga task. 150 | 151 | 152 | ## Bundle API 153 | 154 | A Bundle is an object which contains react-component, actions, reducer, saga and options, used for ArenaScene high order component. 155 | 156 | **Example** 157 | 158 | ```javascript 159 | import state from "./state"; 160 | import saga from "./saga"; 161 | import * as actions from "./actions"; 162 | import Component from "./Component"; 163 | 164 | export default { 165 | Component, 166 | state, 167 | saga, 168 | actions, 169 | options:{ 170 | vReducerkey:"vKey1" 171 | } 172 | } 173 | ``` 174 | ### `createArenaStore(reducers, initialStates, enhencers, sagaOptions): enhancedStore` 175 | 176 | - `Component: React.Component` - React component for binding redux. 177 | 178 | - `state: object` - Initial state of bundle. 179 | 180 | - `actions: object` - Same as redux's actions, connected with redux when component be mounted. 181 | 182 | - `saga: function*` - Generator of redux-Ssga, initialize when component be mounted. 183 | 184 | - `propsPicker: function(stateDict, actionsDict)` - Pick state and actions to props. $ is relative location symbol, $0 could get current location fast,and $1 will get parent location. If this option is unset, an default propsPicker will map all state entities to props with same key, actions will alse pass to props as actions. 185 | 186 | **Example** 187 | 188 | ```javascript 189 | import state from "./state"; 190 | import saga from "./saga"; 191 | import * as actions from "./actions"; 192 | import Component from "./Component"; 193 | 194 | export default { 195 | Component, 196 | state, 197 | actions, 198 | propsPicker:({$0: state}, {$0: actions})=>({ 199 | a: state.a, 200 | actions 201 | }) 202 | } 203 | ``` 204 | 205 | - `options: object` - Options of bundle. 206 | 207 | - `reducerKey: string` - Specify a fixed reducer key for bundle. 208 | 209 | - `vReducerKey: string` - Specify a fixed vitural reducer key for bundle. 210 | 211 | - `isSceneAction: bool` - If false, "_sceneReducerKey" will not add to actions in bundle. 212 | 213 | - `isSceneReducer: bool` - If false, reducer will accept actions dispatched by other bundle. 214 | 215 | ## Tools API 216 | 217 | ### `bundleToComponent(bundle, extraProps)` 218 | 219 | A helper function of transforming bundle to react component. 220 | 221 | ### `bundleToElement(bundle, props, extraProps)` 222 | 223 | A helper function of transforming bundle to react element. 224 | 225 | ## Saga API 226 | 227 | ### `bundleToComponent(bundle, extraProps)` 228 | 229 | ### `getSceneState()` 230 | 231 | Get state of current scene. 232 | 233 | **Example** 234 | 235 | ```javascript 236 | import { setSceneState, takeLatestSceneAction } from "redux-arena/effects"; 237 | 238 | function * doSomthing({ payload }){ 239 | yield setSceneState({ payload }) 240 | } 241 | 242 | export function* saga (){ 243 | yield takeLatestSceneAction("DO_SOMETHING", doSomthing) 244 | } 245 | ``` 246 | 247 | ### `getSceneActions()` 248 | 249 | Get actions of current scene. 250 | 251 | ### `putSceneAction(action)` 252 | 253 | Put an action of current scene. 254 | 255 | ### `setSceneState(state)` 256 | 257 | Set state of current scene. 258 | 259 | **Example** 260 | 261 | ```javascript 262 | import { setSceneState, getSceneState } from "redux-arena/effects"; 263 | 264 | function * doSomthing(){ 265 | let { a } = yield getSceneState() 266 | yield setSceneState({ a : a+1 }) 267 | } 268 | ``` 269 | 270 | ### `takeEverySceneAction(pattern, saga, ...args)` 271 | 272 | Take every scene action of pattern. 273 | 274 | ### `takeLatestSceneAction(pattern, saga, ..args)` 275 | 276 | Take latest scene action of pattern. 277 | 278 | ### `takeSceneAction(pattern)` 279 | 280 | Take scene action of pattern. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------