From 83146af41ffcf1c94c781c415d465f087c256e3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Ma=C5=82kowski?= Date: Thu, 3 Jun 2021 04:18:32 +0700 Subject: [PATCH] feat: use contextBridge in favour of requiring nodeIntegration (#300) * feat: use contextBridge in favour of requiring nodeIntegration Due to security concerns related to usage of nodeIntegration flag, according to best electron practices, renderer functions should be exposed with contextBridge. This PR does exactly that. It also changes a bit API to accomodate for this feature * feat: fixing issues with test enviroment * fix: add missing preventDoubleInitialization() check * change the scope of the contextBridge bindings to only expose high level API --- examples/basic/src/main/index.ts | 9 ++- examples/basic/src/renderer/index.ts | 2 +- examples/basic/src/renderer/preload.ts | 2 + examples/basic/webpack.renderer.js | 1 + main/package.json | 6 ++ package.json | 10 +-- preload/package.json | 6 ++ renderer/package.json | 6 ++ rollup.config.js | 45 +++++------- src/composeWithStateSync.ts | 57 ++++++++------- src/fetchState/index.ts | 4 -- src/forwardAction.ts | 51 ------------- src/index.ts | 14 +--- src/{mainStateSyncEnhancer.ts => main.ts} | 24 +++++-- src/main/forwardActionToRenderers.ts | 17 +++++ src/preload.ts | 71 +++++++++++++++++++ src/renderer.ts | 36 ++++++++++ .../fetchInitialState.ts | 4 +- .../fetchInitialStateAsync.ts | 4 +- src/renderer/forwardActionToMain.ts | 13 ++++ src/renderer/subscribeToIPCAction.ts | 9 +++ src/rendererStateSyncEnhancer.ts | 41 ----------- src/stateSyncEnhancer.ts | 17 ----- src/utils/forwardAction.ts | 23 ++++++ src/utils/misc.ts | 3 - src/{fetchState => utils}/replaceState.ts | 4 +- src/utils/types.ts | 4 ++ tests/e2e.spec.ts | 7 +- tests/fetchState.spec.ts | 4 +- tests/typescript/composeWithStateSync.ts | 27 +++---- tests/typescript/mainStateSyncEnhancer.ts | 4 +- tests/typescript/rendererStateSyncEnhancer.ts | 8 ++- yarn.lock | 29 -------- 33 files changed, 305 insertions(+), 257 deletions(-) create mode 100644 examples/basic/src/renderer/preload.ts create mode 100644 main/package.json create mode 100644 preload/package.json create mode 100644 renderer/package.json delete mode 100644 src/fetchState/index.ts delete mode 100644 src/forwardAction.ts rename src/{mainStateSyncEnhancer.ts => main.ts} (63%) create mode 100644 src/main/forwardActionToRenderers.ts create mode 100644 src/preload.ts create mode 100644 src/renderer.ts rename src/{fetchState => renderer}/fetchInitialState.ts (72%) rename src/{fetchState => renderer}/fetchInitialStateAsync.ts (90%) create mode 100644 src/renderer/forwardActionToMain.ts create mode 100644 src/renderer/subscribeToIPCAction.ts delete mode 100644 src/rendererStateSyncEnhancer.ts delete mode 100644 src/stateSyncEnhancer.ts create mode 100644 src/utils/forwardAction.ts rename src/{fetchState => utils}/replaceState.ts (90%) create mode 100644 src/utils/types.ts diff --git a/examples/basic/src/main/index.ts b/examples/basic/src/main/index.ts index a841b980..5290aba1 100644 --- a/examples/basic/src/main/index.ts +++ b/examples/basic/src/main/index.ts @@ -1,9 +1,10 @@ import url from 'url' import { app, BrowserWindow } from 'electron' -import { stateSyncEnhancer } from 'electron-redux' +import { stateSyncEnhancer } from 'electron-redux/main' import { createStore } from 'redux' import { rootReducer } from '../store' +const TESTING = process.env.SPECTRON === 'true' // ================================================================== // electron related boiler-plate to create window with singe renderer let mainWindow: BrowserWindow | null @@ -13,7 +14,11 @@ async function createWindow() { width: 800, height: 600, webPreferences: { - nodeIntegration: true, + preload: `${__dirname}/preload.js`, + // PROD app should be running with contextIsolation: true for security reasons. Disabled only while running e2e tests + contextIsolation: !TESTING, + // ONLY TRUE FOR TESTING - SPECTRON needs node integration to be able to access the remote modules. + nodeIntegration: TESTING, }, }) await mainWindow.loadURL( diff --git a/examples/basic/src/renderer/index.ts b/examples/basic/src/renderer/index.ts index f90b5dbf..552d44bf 100644 --- a/examples/basic/src/renderer/index.ts +++ b/examples/basic/src/renderer/index.ts @@ -2,7 +2,7 @@ import { createStore } from 'redux' import { rootReducer } from '../store' -import { stateSyncEnhancer } from 'electron-redux' +import { stateSyncEnhancer } from 'electron-redux/renderer' import { decrementGlobalCounter, decrementLocalCounter, diff --git a/examples/basic/src/renderer/preload.ts b/examples/basic/src/renderer/preload.ts new file mode 100644 index 00000000..dfd3e38b --- /dev/null +++ b/examples/basic/src/renderer/preload.ts @@ -0,0 +1,2 @@ +// Include me in your preload script! +require('electron-redux/preload') diff --git a/examples/basic/webpack.renderer.js b/examples/basic/webpack.renderer.js index 53f8a22b..0e4ee33f 100644 --- a/examples/basic/webpack.renderer.js +++ b/examples/basic/webpack.renderer.js @@ -9,6 +9,7 @@ module.exports = { mode: 'production', entry: { renderer: './src/renderer/index.ts', + preload: './src/renderer/preload.ts', }, target: 'electron-renderer', plugins: [ diff --git a/main/package.json b/main/package.json new file mode 100644 index 00000000..c8b5d6c5 --- /dev/null +++ b/main/package.json @@ -0,0 +1,6 @@ +{ + "internal": true, + "main": "../lib/main.js", + "module": "../es/main.js", + "types": "../types/main.d.ts" +} diff --git a/package.json b/package.json index eee20b96..339924a1 100644 --- a/package.json +++ b/package.json @@ -10,13 +10,16 @@ ], "license": "MIT", "private": false, - "main": "lib/electron-redux.js", - "module": "es/electron-redux.js", + "main": "lib/index.js", + "module": "es/index.js", "types": "types/index.d.ts", "files": [ "lib", "es", - "types" + "types", + "main", + "renderer", + "preload" ], "scripts": { "clean": "rimraf lib es coverage types", @@ -48,7 +51,6 @@ "@babel/preset-env": "^7.11.5", "@babel/preset-typescript": "^7.10.4", "@rollup/plugin-commonjs": "^15.0.0", - "@rollup/plugin-node-resolve": "^9.0.0", "@types/jest": "^26.0.14", "@types/lodash.isplainobject": "^4.0.6", "@types/lodash.isstring": "^4.0.6", diff --git a/preload/package.json b/preload/package.json new file mode 100644 index 00000000..15ef9312 --- /dev/null +++ b/preload/package.json @@ -0,0 +1,6 @@ +{ + "internal": true, + "main": "../lib/preload.js", + "module": "../es/preload.js", + "types": "../types/preload.d.ts" +} diff --git a/renderer/package.json b/renderer/package.json new file mode 100644 index 00000000..481546ab --- /dev/null +++ b/renderer/package.json @@ -0,0 +1,6 @@ +{ + "internal": true, + "main": "../lib/renderer.js", + "module": "../es/renderer.js", + "types": "../types/renderer.d.ts" +} diff --git a/rollup.config.js b/rollup.config.js index aa5143f4..9cc15d6a 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -1,4 +1,3 @@ -import nodeResolve from '@rollup/plugin-node-resolve' import babel from 'rollup-plugin-babel' import commonjs from '@rollup/plugin-commonjs' import typescript from 'rollup-plugin-typescript2' @@ -7,38 +6,26 @@ import pkg from './package.json' const extensions = ['.ts'] -const basePlugins = [ - commonjs(), - nodeResolve({ - extensions, - }), - typescript({ useTsconfigDeclarationDir: true }), -] +const basePlugins = [commonjs(), typescript({ useTsconfigDeclarationDir: true })] + +const baseConfig = { + external: Object.keys(pkg.peerDependencies || {}), + plugins: [ + ...basePlugins, + babel({ + extensions, + }), + ], +} export default [ // CommonJS { - input: 'src/index.ts', - output: { file: 'lib/electron-redux.js', format: 'cjs', indent: false }, - external: Object.keys(pkg.peerDependencies || {}), - plugins: [ - ...basePlugins, - babel({ - extensions, - }), - ], - }, - - // ES - { - input: 'src/index.ts', - output: { file: 'es/electron-redux.js', format: 'es', indent: false }, - external: Object.keys(pkg.peerDependencies || {}), - plugins: [ - ...basePlugins, - babel({ - extensions, - }), + ...baseConfig, + input: ['src/index.ts', 'src/main.ts', 'src/renderer.ts', 'src/preload.ts'], + output: [ + { dir: 'lib', format: 'cjs' }, + { dir: 'es', format: 'es' }, ], }, ] diff --git a/src/composeWithStateSync.ts b/src/composeWithStateSync.ts index b512d4c3..cdc4e094 100644 --- a/src/composeWithStateSync.ts +++ b/src/composeWithStateSync.ts @@ -1,44 +1,51 @@ /* eslint-disable @typescript-eslint/ban-types */ import { StoreEnhancer } from 'redux' -import { forwardAction } from './forwardAction' +import { forwardAction, ProcessForwarder } from './utils/forwardAction' import { StateSyncOptions } from './options/StateSyncOptions' -import { stateSyncEnhancer } from './stateSyncEnhancer' +import { StateSyncEnhancer } from './utils/types' -const forwardActionEnhancer = (options?: StateSyncOptions): StoreEnhancer => (createStore) => ( - reducer, - preloadedState -) => { +const forwardActionEnhancer = ( + processForwarder: ProcessForwarder, + options?: StateSyncOptions +): StoreEnhancer => (createStore) => (reducer, preloadedState) => { const store = createStore(reducer, preloadedState) - return forwardAction(store, options) + return forwardAction(store, processForwarder, options) } -const extensionCompose = (options: StateSyncOptions) => ( - ...funcs: StoreEnhancer[] -): StoreEnhancer => { +const extensionCompose = ( + stateSyncEnhancer: StateSyncEnhancer, + processForwarder: ProcessForwarder, + options: StateSyncOptions +) => (...funcs: StoreEnhancer[]): StoreEnhancer => { return (createStore) => { return [ stateSyncEnhancer({ ...options, preventActionReplay: true }), ...funcs, - forwardActionEnhancer(options), + forwardActionEnhancer(processForwarder, options), ].reduceRight((composed, f) => f(composed), createStore) } } -export function composeWithStateSync( - options: StateSyncOptions -): (...funcs: Function[]) => StoreEnhancer -export function composeWithStateSync(...funcs: StoreEnhancer[]): StoreEnhancer -export function composeWithStateSync( - firstFuncOrOpts: StoreEnhancer | StateSyncOptions, - ...funcs: StoreEnhancer[] -): StoreEnhancer | ((...funcs: StoreEnhancer[]) => StoreEnhancer) { - if (arguments.length === 0) { - return stateSyncEnhancer() - } - if (arguments.length === 1 && typeof firstFuncOrOpts === 'object') { - return extensionCompose(firstFuncOrOpts) +export function createComposer( + stateSyncEnhancer: StateSyncEnhancer, + processForwarder: ProcessForwarder +) { + return function composeWithStateSync( + firstFuncOrOpts: StoreEnhancer | StateSyncOptions, + ...funcs: Array + ): StoreEnhancer { + if (arguments.length === 0) { + return stateSyncEnhancer({}) + } + if (arguments.length === 1 && typeof firstFuncOrOpts === 'object') { + return extensionCompose(stateSyncEnhancer, processForwarder, firstFuncOrOpts)() + } + return extensionCompose( + stateSyncEnhancer, + processForwarder, + {} + )(firstFuncOrOpts as StoreEnhancer, ...funcs) } - return extensionCompose({})(firstFuncOrOpts as StoreEnhancer, ...funcs) } diff --git a/src/fetchState/index.ts b/src/fetchState/index.ts deleted file mode 100644 index 4af0da40..00000000 --- a/src/fetchState/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -import fetchInitialState from './fetchInitialState' -import fetchInitialStateAsync from './fetchInitialStateAsync' - -export { fetchInitialState, fetchInitialStateAsync } diff --git a/src/forwardAction.ts b/src/forwardAction.ts deleted file mode 100644 index 5bb73bd9..00000000 --- a/src/forwardAction.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { ipcRenderer, webContents } from 'electron' -import { Store } from 'redux' -import { IPCEvents } from './constants' -import { MainStateSyncEnhancerOptions } from './options/MainStateSyncEnhancerOptions' -import { RendererStateSyncEnhancerOptions } from './options/RendererStateSyncEnhancerOptions' -import { StateSyncOptions } from './options/StateSyncOptions' -import { isMain, isRenderer, validateAction } from './utils' - -export const processActionMain = ( - action: A, - options: MainStateSyncEnhancerOptions = {} -): void => { - if (validateAction(action, options.denyList)) { - webContents.getAllWebContents().forEach((contents) => { - // Ignore chromium devtools - if (contents.getURL().startsWith('devtools://')) return - contents.send(IPCEvents.ACTION, action) - }) - } -} - -export const processActionRenderer = ( - action: A, - options: RendererStateSyncEnhancerOptions = {} -): void => { - if (validateAction(action, options.denyList)) { - ipcRenderer.send(IPCEvents.ACTION, action) - } -} - -export const forwardAction = >( - store: S, - options?: StateSyncOptions -): S => { - return { - ...store, - dispatch: (action) => { - const value = store.dispatch(action) - - if (!options?.preventActionReplay) { - if (isMain) { - processActionMain(action, options) - } else if (isRenderer) { - processActionRenderer(action, options) - } - } - - return value - }, - } -} diff --git a/src/index.ts b/src/index.ts index 23e259f4..63366cad 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,13 +1,3 @@ -import { mainStateSyncEnhancer } from './mainStateSyncEnhancer' -import { stopForwarding } from './utils' -import { rendererStateSyncEnhancer } from './rendererStateSyncEnhancer' -import { stateSyncEnhancer } from './stateSyncEnhancer' -import { composeWithStateSync } from './composeWithStateSync' +import { stopForwarding } from './utils/actions' -export { - mainStateSyncEnhancer, - rendererStateSyncEnhancer, - stopForwarding, - stateSyncEnhancer, - composeWithStateSync, -} +export { stopForwarding } diff --git a/src/mainStateSyncEnhancer.ts b/src/main.ts similarity index 63% rename from src/mainStateSyncEnhancer.ts rename to src/main.ts index 801242a3..c9705ca8 100644 --- a/src/mainStateSyncEnhancer.ts +++ b/src/main.ts @@ -1,18 +1,23 @@ import { ipcMain, webContents } from 'electron' import { Action, StoreEnhancer } from 'redux' import { IPCEvents } from './constants' -import { forwardAction } from './forwardAction' +import { forwardAction } from './utils/forwardAction' +import { forwardActionToRenderers } from './main/forwardActionToRenderers' import { MainStateSyncEnhancerOptions } from './options/MainStateSyncEnhancerOptions' -import { stopForwarding } from './utils' +import { preventDoubleInitialization, stopForwarding } from './utils' +import { StateSyncOptions } from './options/StateSyncOptions' +import { createComposer } from './composeWithStateSync' /** * Creates new instance of main process redux enhancer. * @param {MainStateSyncEnhancerOptions} options Additional enhancer options * @returns StoreEnhancer */ -export const mainStateSyncEnhancer = ( - options: MainStateSyncEnhancerOptions = {} -): StoreEnhancer => (createStore) => { +export const stateSyncEnhancer = (options: MainStateSyncEnhancerOptions = {}): StoreEnhancer => ( + createStore +) => { + preventDoubleInitialization() + return (reducer, preloadedState) => { const store = createStore(reducer, preloadedState) @@ -42,6 +47,13 @@ export const mainStateSyncEnhancer = ( }) }) - return forwardAction(store, options) + return forwardAction(store, forwardActionToRenderers, options) } } + +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types +export const composeWithStateSync = ( + firstFuncOrOpts: StoreEnhancer | StateSyncOptions, + ...funcs: StoreEnhancer[] +): StoreEnhancer => + createComposer(stateSyncEnhancer, forwardActionToRenderers)(firstFuncOrOpts, ...funcs) diff --git a/src/main/forwardActionToRenderers.ts b/src/main/forwardActionToRenderers.ts new file mode 100644 index 00000000..e6435533 --- /dev/null +++ b/src/main/forwardActionToRenderers.ts @@ -0,0 +1,17 @@ +import { webContents } from 'electron' +import { IPCEvents } from 'src/constants' +import { MainStateSyncEnhancerOptions } from 'src/options/MainStateSyncEnhancerOptions' +import { validateAction } from 'src/utils' + +export const forwardActionToRenderers = ( + action: A, + options: MainStateSyncEnhancerOptions = {} +): void => { + if (validateAction(action, options.denyList)) { + webContents.getAllWebContents().forEach((contents) => { + // Ignore chromium devtools + if (contents.getURL().startsWith('devtools://')) return + contents.send(IPCEvents.ACTION, action) + }) + } +} diff --git a/src/preload.ts b/src/preload.ts new file mode 100644 index 00000000..beb52c1d --- /dev/null +++ b/src/preload.ts @@ -0,0 +1,71 @@ +import { contextBridge } from 'electron' +import { StoreEnhancer, Action } from 'redux' +import { createComposer } from './composeWithStateSync' +import { RendererStateSyncEnhancerOptions } from './options/RendererStateSyncEnhancerOptions' +import { StateSyncOptions } from './options/StateSyncOptions' +import { fetchInitialState } from './renderer/fetchInitialState' +import { fetchInitialStateAsync } from './renderer/fetchInitialStateAsync' +import { forwardActionToMain } from './renderer/forwardActionToMain' +import { subscribeToIPCAction } from './renderer/subscribeToIPCAction' +import { preventDoubleInitialization, stopForwarding } from './utils' +import { forwardAction } from './utils/forwardAction' +import { withStoreReplacer, replaceState } from './utils/replaceState' + +declare global { + interface Bridge { + stateSyncEnhancer: typeof stateSyncEnhancer + composeWithStateSync: typeof composeWithStateSync + } + interface Window { + __ElectronReduxBridge: Bridge + } + + const __ElectronReduxBridge: Bridge +} + +const stateSyncEnhancer = (options: RendererStateSyncEnhancerOptions = {}): StoreEnhancer => ( + createStore +) => { + preventDoubleInitialization() + + return (reducer, state) => { + const initialState = options.lazyInit ? state : fetchInitialState(options) + + const store = createStore( + options.lazyInit ? withStoreReplacer(reducer) : reducer, + initialState + ) + + if (options.lazyInit) { + fetchInitialStateAsync(options, (asyncState) => { + store.dispatch(replaceState(asyncState) as never) + }) + } + + // When receiving an action from main + subscribeToIPCAction((action: Action) => store.dispatch(stopForwarding(action))) + + return forwardAction(store, forwardActionToMain, options) + } +} + +const composeWithStateSync = ( + firstFuncOrOpts: StoreEnhancer | StateSyncOptions, + ...funcs: StoreEnhancer[] +) => createComposer(stateSyncEnhancer, forwardActionToMain)(firstFuncOrOpts, ...funcs) + +export const preload = (): void => { + const bridge = { + stateSyncEnhancer, + composeWithStateSync, + } + + try { + contextBridge.exposeInMainWorld('__ElectronReduxBridge', bridge) + } catch { + window.__ElectronReduxBridge = bridge + } +} + +// run it! +preload() diff --git a/src/renderer.ts b/src/renderer.ts new file mode 100644 index 00000000..59f6ed95 --- /dev/null +++ b/src/renderer.ts @@ -0,0 +1,36 @@ +import { StoreEnhancer } from 'redux' +import { RendererStateSyncEnhancerOptions } from './options/RendererStateSyncEnhancerOptions' +import { preventDoubleInitialization } from './utils' +import { StateSyncOptions } from './options/StateSyncOptions' + +/** + * Creates new instance of renderer process redux enhancer. + * Upon initialization, it will fetch the state from the main process & subscribe for event + * communication required to keep the actions in sync. + * @param {RendererStateSyncEnhancerOptions} options Additional settings for enhancer + * @returns StoreEnhancer + */ +export const stateSyncEnhancer = ( + options: RendererStateSyncEnhancerOptions = {} +): StoreEnhancer => { + preventDoubleInitialization() + assertElectronReduxBridgeAvailability() + return __ElectronReduxBridge.stateSyncEnhancer(options) +} + +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types +export const composeWithStateSync = ( + firstFuncOrOpts: StoreEnhancer | StateSyncOptions, + ...funcs: StoreEnhancer[] +) => { + assertElectronReduxBridgeAvailability() + return __ElectronReduxBridge.composeWithStateSync(firstFuncOrOpts, ...funcs) +} + +const assertElectronReduxBridgeAvailability = () => { + if (typeof __ElectronReduxBridge === undefined) { + throw new Error( + 'Looks like this renderer process has not been configured properly. Did you forgot to include preload script?' + ) + } +} diff --git a/src/fetchState/fetchInitialState.ts b/src/renderer/fetchInitialState.ts similarity index 72% rename from src/fetchState/fetchInitialState.ts rename to src/renderer/fetchInitialState.ts index 88dfe693..09ba8df0 100644 --- a/src/fetchState/fetchInitialState.ts +++ b/src/renderer/fetchInitialState.ts @@ -2,9 +2,7 @@ import { ipcRenderer } from 'electron' import { IPCEvents } from '../constants' import { RendererStateSyncEnhancerOptions } from '../options/RendererStateSyncEnhancerOptions' -function fetchInitialState(options: RendererStateSyncEnhancerOptions): T { +export function fetchInitialState(options: RendererStateSyncEnhancerOptions): T { const state = ipcRenderer.sendSync(IPCEvents.INIT_STATE) return JSON.parse(state, options.deserializer) } - -export default fetchInitialState diff --git a/src/fetchState/fetchInitialStateAsync.ts b/src/renderer/fetchInitialStateAsync.ts similarity index 90% rename from src/fetchState/fetchInitialStateAsync.ts rename to src/renderer/fetchInitialStateAsync.ts index ac785479..a8c6477b 100644 --- a/src/fetchState/fetchInitialStateAsync.ts +++ b/src/renderer/fetchInitialStateAsync.ts @@ -2,7 +2,7 @@ import { ipcRenderer } from 'electron' import { IPCEvents } from '../constants' import { RendererStateSyncEnhancerOptions } from '../options/RendererStateSyncEnhancerOptions' -async function fetchInitialStateAsync( +export async function fetchInitialStateAsync( options: RendererStateSyncEnhancerOptions, callback: (state: unknown) => void ): Promise { @@ -18,5 +18,3 @@ async function fetchInitialStateAsync( ) } } - -export default fetchInitialStateAsync diff --git a/src/renderer/forwardActionToMain.ts b/src/renderer/forwardActionToMain.ts new file mode 100644 index 00000000..3d9d4241 --- /dev/null +++ b/src/renderer/forwardActionToMain.ts @@ -0,0 +1,13 @@ +import { ipcRenderer } from 'electron' +import { IPCEvents } from '../constants' +import { RendererStateSyncEnhancerOptions } from '../options/RendererStateSyncEnhancerOptions' +import { validateAction } from '../utils' + +export const forwardActionToMain = ( + action: A, + options: RendererStateSyncEnhancerOptions = {} +): void => { + if (validateAction(action, options.denyList)) { + ipcRenderer.send(IPCEvents.ACTION, action) + } +} diff --git a/src/renderer/subscribeToIPCAction.ts b/src/renderer/subscribeToIPCAction.ts new file mode 100644 index 00000000..229596aa --- /dev/null +++ b/src/renderer/subscribeToIPCAction.ts @@ -0,0 +1,9 @@ +import { ipcRenderer } from 'electron' +import { Action } from 'redux' +import { IPCEvents } from 'src/constants' + +export const subscribeToIPCAction = (callback: (action: Action) => void): void => { + ipcRenderer.on(IPCEvents.ACTION, (_, action: Action) => { + callback(action) + }) +} diff --git a/src/rendererStateSyncEnhancer.ts b/src/rendererStateSyncEnhancer.ts deleted file mode 100644 index 52f3a978..00000000 --- a/src/rendererStateSyncEnhancer.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { ipcRenderer } from 'electron' -import { Action, StoreEnhancer } from 'redux' -import { IPCEvents } from './constants' -import { forwardAction } from './forwardAction' -import { fetchInitialState, fetchInitialStateAsync } from './fetchState' -import { replaceState, withStoreReplacer } from './fetchState/replaceState' -import { RendererStateSyncEnhancerOptions } from './options/RendererStateSyncEnhancerOptions' -import { stopForwarding } from './utils' - -/** - * Creates new instance of renderer process redux enhancer. - * Upon initialization, it will fetch the state from the main process & subscribe for event - * communication required to keep the actions in sync. - * @param {RendererStateSyncEnhancerOptions} options Additional settings for enhancer - * @returns StoreEnhancer - */ -export const rendererStateSyncEnhancer = ( - options: RendererStateSyncEnhancerOptions = {} -): StoreEnhancer => (createStore) => { - return (reducer, state) => { - const initialState = options.lazyInit ? state : fetchInitialState(options) - - const store = createStore( - options.lazyInit ? withStoreReplacer(reducer) : reducer, - initialState - ) - - if (options.lazyInit) { - fetchInitialStateAsync(options, (asyncState) => { - store.dispatch(replaceState(asyncState) as never) - }) - } - - // When receiving an action from main - ipcRenderer.on(IPCEvents.ACTION, (_, action: Action) => { - store.dispatch(stopForwarding(action)) - }) - - return forwardAction(store, options) - } -} diff --git a/src/stateSyncEnhancer.ts b/src/stateSyncEnhancer.ts deleted file mode 100644 index 4c042ec0..00000000 --- a/src/stateSyncEnhancer.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { StoreEnhancer } from 'redux' -import { mainStateSyncEnhancer } from './mainStateSyncEnhancer' -import { StateSyncOptions } from './options/StateSyncOptions' -import { rendererStateSyncEnhancer } from './rendererStateSyncEnhancer' -import { isMain, isRenderer, preventDoubleInitialization } from './utils' - -export const stateSyncEnhancer = (config: StateSyncOptions = {}): StoreEnhancer => { - preventDoubleInitialization() - - if (isRenderer) { - return rendererStateSyncEnhancer(config) - } else if (isMain) { - return mainStateSyncEnhancer(config) - } - - throw new Error(`Unsupported process: process.type = ${process?.type}`) -} diff --git a/src/utils/forwardAction.ts b/src/utils/forwardAction.ts new file mode 100644 index 00000000..339bb631 --- /dev/null +++ b/src/utils/forwardAction.ts @@ -0,0 +1,23 @@ +import { Store } from 'redux' +import { StateSyncOptions } from '../options/StateSyncOptions' + +export type ProcessForwarder = (forwarderAction: any, forwarderOptions: StateSyncOptions) => void + +export const forwardAction = >( + store: S, + processForwarder: ProcessForwarder, + options: StateSyncOptions = {} +): S => { + return { + ...store, + dispatch: (action) => { + const value = store.dispatch(action) + + if (!options?.preventActionReplay) { + processForwarder(action, options) + } + + return value + }, + } +} diff --git a/src/utils/misc.ts b/src/utils/misc.ts index 2b75a2b6..8fa915b3 100644 --- a/src/utils/misc.ts +++ b/src/utils/misc.ts @@ -31,6 +31,3 @@ export const trimProperties = (props: T[], obj: X) => { Object.entries(obj).filter(([key]) => !props.includes(key as T)) ) as Omit } - -export const isRenderer = process.type === 'renderer' -export const isMain = process.type === 'browser' diff --git a/src/fetchState/replaceState.ts b/src/utils/replaceState.ts similarity index 90% rename from src/fetchState/replaceState.ts rename to src/utils/replaceState.ts index da10a81f..34e88fb5 100644 --- a/src/fetchState/replaceState.ts +++ b/src/utils/replaceState.ts @@ -1,6 +1,6 @@ import { AnyAction, Reducer } from 'redux' -import { FluxStandardAction } from '../utils/isFSA' -import { ActionMeta } from '../utils' +import { FluxStandardAction } from './isFSA' +import { ActionMeta } from '.' interface ReplaceStateAction extends FluxStandardAction { payload: S diff --git a/src/utils/types.ts b/src/utils/types.ts new file mode 100644 index 00000000..09e37baf --- /dev/null +++ b/src/utils/types.ts @@ -0,0 +1,4 @@ +import { StoreEnhancer } from 'redux' +import { StateSyncOptions } from '../options/StateSyncOptions' + +export type StateSyncEnhancer = (options: StateSyncOptions) => StoreEnhancer diff --git a/tests/e2e.spec.ts b/tests/e2e.spec.ts index f0653a3c..4611f4d3 100644 --- a/tests/e2e.spec.ts +++ b/tests/e2e.spec.ts @@ -14,11 +14,14 @@ describe('End to End Tests', () => { startTimeout: 5000, host: process.env.CHROMEDRIVER_HOST || 'localhost', port: parseInt(process.env.CHROMEDRIVER_PORT || '9222'), + env: { + SPECTRON: 'true', + }, }) await app.start() - // eslint-disable-next-line @typescript-eslint/await-thenable - await app.browserWindow.isVisible() + + await app.client.waitUntilWindowLoaded() }) afterEach(async () => { diff --git a/tests/fetchState.spec.ts b/tests/fetchState.spec.ts index 61892e6a..0186607f 100644 --- a/tests/fetchState.spec.ts +++ b/tests/fetchState.spec.ts @@ -1,4 +1,6 @@ -import { fetchInitialState, fetchInitialStateAsync } from '../src/fetchState' +import { fetchInitialStateAsync } from '../src/renderer/fetchInitialStateAsync' +import { fetchInitialState } from '../src/renderer/fetchInitialState' + import { IPCEvents } from '../src/constants' jest.mock('electron', () => ({ ipcRenderer: { diff --git a/tests/typescript/composeWithStateSync.ts b/tests/typescript/composeWithStateSync.ts index ea517e7c..9ea26612 100644 --- a/tests/typescript/composeWithStateSync.ts +++ b/tests/typescript/composeWithStateSync.ts @@ -1,24 +1,15 @@ -import { composeWithStateSync } from '../../types' -import { applyMiddleware, createStore, Store, StoreEnhancer } from 'redux' -import { reducer, CounterState, Actions } from '../counter' +import { applyMiddleware, createStore } from 'redux' +import { reducer } from '../counter' import { countMiddleware } from '../middleware' +import { composeWithStateSync } from '../../main' -// This is just a dummy enhancer, this does nothing -const someOtherEnhancer: StoreEnhancer = (next) => { - return (reducer, state) => { - return next(reducer, state) - } -} +const middleware = [countMiddleware] -const middleware = applyMiddleware(countMiddleware) +const _store1 = createStore(reducer, composeWithStateSync(applyMiddleware(...middleware))) -const enhancerWithoutOptions: StoreEnhancer = composeWithStateSync(middleware, someOtherEnhancer) +const _store2 = createStore(reducer, composeWithStateSync({}, applyMiddleware(...middleware))) -const store: Store = createStore(reducer, enhancerWithoutOptions) - -const enhancerWithOptions: StoreEnhancer = composeWithStateSync({ denyList: [] })( - middleware, - someOtherEnhancer +const _store3 = createStore( + reducer, + composeWithStateSync({ denyList: [] }, applyMiddleware(...middleware)) ) - -const store2: Store = createStore(reducer, enhancerWithOptions) diff --git a/tests/typescript/mainStateSyncEnhancer.ts b/tests/typescript/mainStateSyncEnhancer.ts index 60fe3541..0f5d51b9 100644 --- a/tests/typescript/mainStateSyncEnhancer.ts +++ b/tests/typescript/mainStateSyncEnhancer.ts @@ -1,5 +1,5 @@ -import { mainStateSyncEnhancer } from '../../types' +import { stateSyncEnhancer } from '../../main' import { createStore, Store } from 'redux' import { reducer, CounterState, Actions } from '../counter' -const store: Store = createStore(reducer, mainStateSyncEnhancer()) +const store: Store = createStore(reducer, stateSyncEnhancer()) diff --git a/tests/typescript/rendererStateSyncEnhancer.ts b/tests/typescript/rendererStateSyncEnhancer.ts index 3199b66d..76975f86 100644 --- a/tests/typescript/rendererStateSyncEnhancer.ts +++ b/tests/typescript/rendererStateSyncEnhancer.ts @@ -1,5 +1,9 @@ -import { rendererStateSyncEnhancer } from '../../types' +import { stateSyncEnhancer } from '../../renderer' import { createStore, Store } from 'redux' import { reducer, CounterState, Actions } from '../counter' -const store: Store = createStore(reducer, rendererStateSyncEnhancer()) +const store: Store = createStore(reducer, stateSyncEnhancer()) +const store2: Store = createStore( + reducer, + stateSyncEnhancer({ denyList: [] }) +) diff --git a/yarn.lock b/yarn.lock index d54d8d47..54d8004f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1353,18 +1353,6 @@ magic-string "^0.25.7" resolve "^1.17.0" -"@rollup/plugin-node-resolve@^9.0.0": - version "9.0.0" - resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-9.0.0.tgz#39bd0034ce9126b39c1699695f440b4b7d2b62e6" - integrity sha512-gPz+utFHLRrd41WMP13Jq5mqqzHL3OXrfj3/MkSyB6UBIcuNt9j60GCbarzMzdf1VHFpOxfQh/ez7wyadLMqkg== - dependencies: - "@rollup/pluginutils" "^3.1.0" - "@types/resolve" "1.17.1" - builtin-modules "^3.1.0" - deepmerge "^4.2.2" - is-module "^1.0.0" - resolve "^1.17.0" - "@rollup/pluginutils@^3.1.0": version "3.1.0" resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-3.1.0.tgz#706b4524ee6dc8b103b3c995533e5ad680c02b9b" @@ -1682,13 +1670,6 @@ dependencies: "@types/node" "*" -"@types/resolve@1.17.1": - version "1.17.1" - resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6" - integrity sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw== - dependencies: - "@types/node" "*" - "@types/responselike@*", "@types/responselike@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@types/responselike/-/responselike-1.0.0.tgz#251f4fe7d154d2bad125abe1b429b23afd262e29" @@ -2467,11 +2448,6 @@ buffer@^5.2.1, buffer@^5.5.0: base64-js "^1.3.1" ieee754 "^1.1.13" -builtin-modules@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.1.0.tgz#aad97c15131eb76b65b50ef208e7584cd76a7484" - integrity sha512-k0KL0aWZuBt2lrxrcASWDfwOLMnodeQjodT/1SxEQAXsHANgo6ZC/VEaSEHCXt7aSTZ4/4H5LKa+tBXmW7Vtvw== - builtins@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/builtins/-/builtins-1.0.3.tgz#cb94faeb61c8696451db36534e1422f94f0aee88" @@ -5380,11 +5356,6 @@ is-installed-globally@^0.1.0: global-dirs "^0.1.0" is-path-inside "^1.0.0" -is-module@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591" - integrity sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE= - is-negative-zero@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.0.tgz#9553b121b0fac28869da9ed459e20c7543788461"