From afab80ab59dd794a61acf9797756eb71d067b32f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Ma=C5=82kowski?= Date: Tue, 2 Mar 2021 21:04:18 +0100 Subject: [PATCH 1/4] 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 --- examples/basic/src/main/index.ts | 5 +- examples/basic/src/renderer/index.ts | 2 +- examples/basic/src/renderer/preload.ts | 1 + examples/basic/webpack.renderer.js | 1 + main/package.json | 6 ++ package.json | 9 ++- preload/package.json | 6 ++ renderer/package.json | 6 ++ rollup.config.js | 36 +++++------- src/composeWithStateSync.ts | 57 ++++++++++-------- src/fetchState/index.ts | 4 -- src/forwardAction.ts | 51 ---------------- src/index.ts | 14 +---- src/{mainStateSyncEnhancer.ts => main.ts} | 20 +++++-- src/main/forwardActionToRenderers.ts | 17 ++++++ src/preload.ts | 39 +++++++++++++ src/renderer.ts | 58 +++++++++++++++++++ .../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/fetchState.spec.ts | 4 +- tests/typescript/composeWithStateSync.ts | 27 +++------ tests/typescript/mainStateSyncEnhancer.ts | 4 +- tests/typescript/rendererStateSyncEnhancer.ts | 8 ++- 31 files changed, 281 insertions(+), 216 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} (67%) 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..276f2a29 100644 --- a/examples/basic/src/main/index.ts +++ b/examples/basic/src/main/index.ts @@ -1,6 +1,6 @@ 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' @@ -13,7 +13,8 @@ async function createWindow() { width: 800, height: 600, webPreferences: { - nodeIntegration: true, + preload: `${__dirname}/preload.js`, + contextIsolation: true, }, }) 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..b0a9a2d4 --- /dev/null +++ b/examples/basic/src/renderer/preload.ts @@ -0,0 +1 @@ +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..40ab0be1 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", 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..ce79c50f 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -15,30 +15,24 @@ const basePlugins = [ 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 67% rename from src/mainStateSyncEnhancer.ts rename to src/main.ts index 801242a3..8463d664 100644 --- a/src/mainStateSyncEnhancer.ts +++ b/src/main.ts @@ -1,18 +1,21 @@ 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 { 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 +) => { return (reducer, preloadedState) => { const store = createStore(reducer, preloadedState) @@ -42,6 +45,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..5b6e047e --- /dev/null +++ b/src/preload.ts @@ -0,0 +1,39 @@ +import { contextBridge } from 'electron' +import { fetchInitialState } from './renderer/fetchInitialState' +import { fetchInitialStateAsync } from './renderer/fetchInitialStateAsync' +import { forwardActionToMain } from './renderer/forwardActionToMain' +import { subscribeToIPCAction } from './renderer/subscribeToIPCAction' + +declare global { + interface Bridge { + fetchInitialState: typeof fetchInitialState + fetchInitialStateAsync: typeof fetchInitialStateAsync + forwardActionToMain: typeof forwardActionToMain + subscribeToIPCAction: typeof subscribeToIPCAction + } + interface Window { + __ElectronReduxBridge: Bridge + } + + const __ElectronReduxBridge: Bridge +} + +export const preload = (): void => { + console.log('preloading...') + const bridge = { + fetchInitialState, + fetchInitialStateAsync, + forwardActionToMain, + subscribeToIPCAction, + } + + try { + contextBridge.exposeInMainWorld('__ElectronReduxBridge', bridge) + } catch { + console.log('exposeInMainWorld not avaiable') + window.__ElectronReduxBridge = bridge + } +} + +// run it! +preload() diff --git a/src/renderer.ts b/src/renderer.ts new file mode 100644 index 00000000..ae60f1ff --- /dev/null +++ b/src/renderer.ts @@ -0,0 +1,58 @@ +import { Action, StoreEnhancer } from 'redux' +import { forwardAction } from './utils/forwardAction' +import { replaceState, withStoreReplacer } from './utils/replaceState' +import { RendererStateSyncEnhancerOptions } from './options/RendererStateSyncEnhancerOptions' +import { stopForwarding } from './utils' +import { StateSyncOptions } from './options/StateSyncOptions' +import { createComposer } from './composeWithStateSync' + +/** + * 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 => (createStore) => { + return (reducer, state) => { + if (typeof __ElectronReduxBridge === undefined) { + throw new Error( + 'Looks like this renderer process has not been configured properly. Did you forgot to include preload script?' + ) + } + + const initialState = options.lazyInit + ? state + : __ElectronReduxBridge.fetchInitialState(options) + + const store = createStore( + options.lazyInit ? withStoreReplacer(reducer) : reducer, + initialState + ) + + if (options.lazyInit) { + __ElectronReduxBridge.fetchInitialStateAsync(options, (asyncState) => { + store.dispatch(replaceState(asyncState) as never) + }) + } + + // When receiving an action from main + __ElectronReduxBridge.subscribeToIPCAction((action: Action) => + store.dispatch(stopForwarding(action)) + ) + + return forwardAction(store, __ElectronReduxBridge.forwardActionToMain, options) + } +} + +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types +export const composeWithStateSync = ( + firstFuncOrOpts: StoreEnhancer | StateSyncOptions, + ...funcs: StoreEnhancer[] +) => + createComposer(stateSyncEnhancer, __ElectronReduxBridge.forwardActionToMain)( + firstFuncOrOpts, + ...funcs + ) 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/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: [] }) +) From 8afce8ffb08eb42467569f8f785f4d7669063c22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Ma=C5=82kowski?= Date: Wed, 3 Mar 2021 00:09:15 +0100 Subject: [PATCH 2/4] feat: fixing issues with test enviroment --- examples/basic/src/main/index.ts | 6 +++++- examples/basic/src/renderer/preload.ts | 1 + package.json | 1 - rollup.config.js | 9 +------- src/preload.ts | 2 -- tests/e2e.spec.ts | 7 +++++-- yarn.lock | 29 -------------------------- 7 files changed, 12 insertions(+), 43 deletions(-) diff --git a/examples/basic/src/main/index.ts b/examples/basic/src/main/index.ts index 276f2a29..5290aba1 100644 --- a/examples/basic/src/main/index.ts +++ b/examples/basic/src/main/index.ts @@ -4,6 +4,7 @@ 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 @@ -14,7 +15,10 @@ async function createWindow() { height: 600, webPreferences: { preload: `${__dirname}/preload.js`, - contextIsolation: true, + // 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/preload.ts b/examples/basic/src/renderer/preload.ts index b0a9a2d4..dfd3e38b 100644 --- a/examples/basic/src/renderer/preload.ts +++ b/examples/basic/src/renderer/preload.ts @@ -1 +1,2 @@ +// Include me in your preload script! require('electron-redux/preload') diff --git a/package.json b/package.json index 40ab0be1..339924a1 100644 --- a/package.json +++ b/package.json @@ -51,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/rollup.config.js b/rollup.config.js index ce79c50f..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,13 +6,7 @@ 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 || {}), diff --git a/src/preload.ts b/src/preload.ts index 5b6e047e..d7bfc74c 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -19,7 +19,6 @@ declare global { } export const preload = (): void => { - console.log('preloading...') const bridge = { fetchInitialState, fetchInitialStateAsync, @@ -30,7 +29,6 @@ export const preload = (): void => { try { contextBridge.exposeInMainWorld('__ElectronReduxBridge', bridge) } catch { - console.log('exposeInMainWorld not avaiable') window.__ElectronReduxBridge = bridge } } 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/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" From debd89ade9e8366421eb4f45f6ce87d199862dac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Ma=C5=82kowski?= Date: Wed, 3 Mar 2021 00:26:21 +0100 Subject: [PATCH 3/4] fix: add missing preventDoubleInitialization() check --- src/main.ts | 4 +++- src/renderer.ts | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main.ts b/src/main.ts index 8463d664..c9705ca8 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,7 +4,7 @@ import { IPCEvents } from './constants' 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' @@ -16,6 +16,8 @@ import { createComposer } from './composeWithStateSync' export const stateSyncEnhancer = (options: MainStateSyncEnhancerOptions = {}): StoreEnhancer => ( createStore ) => { + preventDoubleInitialization() + return (reducer, preloadedState) => { const store = createStore(reducer, preloadedState) diff --git a/src/renderer.ts b/src/renderer.ts index ae60f1ff..f0de1a7f 100644 --- a/src/renderer.ts +++ b/src/renderer.ts @@ -2,7 +2,7 @@ import { Action, StoreEnhancer } from 'redux' import { forwardAction } from './utils/forwardAction' import { replaceState, withStoreReplacer } from './utils/replaceState' import { RendererStateSyncEnhancerOptions } from './options/RendererStateSyncEnhancerOptions' -import { stopForwarding } from './utils' +import { preventDoubleInitialization, stopForwarding } from './utils' import { StateSyncOptions } from './options/StateSyncOptions' import { createComposer } from './composeWithStateSync' @@ -16,6 +16,8 @@ import { createComposer } from './composeWithStateSync' export const stateSyncEnhancer = ( options: RendererStateSyncEnhancerOptions = {} ): StoreEnhancer => (createStore) => { + preventDoubleInitialization() + return (reducer, state) => { if (typeof __ElectronReduxBridge === undefined) { throw new Error( From 5c1b94e4f8c1ed75ee838904ad5eea5f70be2925 Mon Sep 17 00:00:00 2001 From: matmalkowski Date: Sat, 29 May 2021 00:45:21 +0200 Subject: [PATCH 4/4] change the scope of the contextBridge bindings to only expose high level API --- src/preload.ts | 50 +++++++++++++++++++++++++++++++++++------- src/renderer.ts | 58 +++++++++++++++---------------------------------- 2 files changed, 59 insertions(+), 49 deletions(-) diff --git a/src/preload.ts b/src/preload.ts index d7bfc74c..beb52c1d 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -1,15 +1,20 @@ 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 { - fetchInitialState: typeof fetchInitialState - fetchInitialStateAsync: typeof fetchInitialStateAsync - forwardActionToMain: typeof forwardActionToMain - subscribeToIPCAction: typeof subscribeToIPCAction + stateSyncEnhancer: typeof stateSyncEnhancer + composeWithStateSync: typeof composeWithStateSync } interface Window { __ElectronReduxBridge: Bridge @@ -18,12 +23,41 @@ declare global { 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 = { - fetchInitialState, - fetchInitialStateAsync, - forwardActionToMain, - subscribeToIPCAction, + stateSyncEnhancer, + composeWithStateSync, } try { diff --git a/src/renderer.ts b/src/renderer.ts index f0de1a7f..59f6ed95 100644 --- a/src/renderer.ts +++ b/src/renderer.ts @@ -1,10 +1,7 @@ -import { Action, StoreEnhancer } from 'redux' -import { forwardAction } from './utils/forwardAction' -import { replaceState, withStoreReplacer } from './utils/replaceState' +import { StoreEnhancer } from 'redux' import { RendererStateSyncEnhancerOptions } from './options/RendererStateSyncEnhancerOptions' -import { preventDoubleInitialization, stopForwarding } from './utils' +import { preventDoubleInitialization } from './utils' import { StateSyncOptions } from './options/StateSyncOptions' -import { createComposer } from './composeWithStateSync' /** * Creates new instance of renderer process redux enhancer. @@ -15,46 +12,25 @@ import { createComposer } from './composeWithStateSync' */ export const stateSyncEnhancer = ( options: RendererStateSyncEnhancerOptions = {} -): StoreEnhancer => (createStore) => { +): StoreEnhancer => { preventDoubleInitialization() - - return (reducer, state) => { - if (typeof __ElectronReduxBridge === undefined) { - throw new Error( - 'Looks like this renderer process has not been configured properly. Did you forgot to include preload script?' - ) - } - - const initialState = options.lazyInit - ? state - : __ElectronReduxBridge.fetchInitialState(options) - - const store = createStore( - options.lazyInit ? withStoreReplacer(reducer) : reducer, - initialState - ) - - if (options.lazyInit) { - __ElectronReduxBridge.fetchInitialStateAsync(options, (asyncState) => { - store.dispatch(replaceState(asyncState) as never) - }) - } - - // When receiving an action from main - __ElectronReduxBridge.subscribeToIPCAction((action: Action) => - store.dispatch(stopForwarding(action)) - ) - - return forwardAction(store, __ElectronReduxBridge.forwardActionToMain, options) - } + assertElectronReduxBridgeAvailability() + return __ElectronReduxBridge.stateSyncEnhancer(options) } // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types export const composeWithStateSync = ( firstFuncOrOpts: StoreEnhancer | StateSyncOptions, ...funcs: StoreEnhancer[] -) => - createComposer(stateSyncEnhancer, __ElectronReduxBridge.forwardActionToMain)( - firstFuncOrOpts, - ...funcs - ) +) => { + 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?' + ) + } +}