diff --git a/.gitignore b/.gitignore index 4ca3ffc8..2631a892 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ es types *.log e2e_dist +.DS_Store diff --git a/.npmignore b/.npmignore new file mode 100644 index 00000000..23100330 --- /dev/null +++ b/.npmignore @@ -0,0 +1,10 @@ +node_modules +coverage +*.log +e2e_dist +src +tests +.github +.vscode + +.DS_Store \ No newline at end of file diff --git a/package.json b/package.json index 4cc4335c..4b36b8b9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "electron-redux", - "version": "2.0.0-alpha.0", + "version": "0.0.0-dev.0", "description": "Redux & Electron: Make sure all your stores are on the same page", "repository": "https://github.com/klarna/electron-redux.git", "authors": [ @@ -11,18 +11,15 @@ "license": "MIT", "private": false, "main": "lib/electron-redux.js", - "unpkg": "dist/electron-redux.js", "module": "es/electron-redux.js", "types": "types/index.d.ts", "files": [ - "dist", "lib", "es", - "src", "types" ], "scripts": { - "clean": "rimraf lib dist es coverage types", + "clean": "rimraf lib es coverage types", "start:electron": "electron-webpack dev", "build": "rollup -c", "build:electron": "electron-webpack", @@ -53,7 +50,6 @@ "@babel/preset-typescript": "^7.10.4", "@rollup/plugin-commonjs": "^15.0.0", "@rollup/plugin-node-resolve": "^9.0.0", - "@rollup/plugin-replace": "^2.3.3", "@types/jest": "^26.0.14", "@types/node": "^14.11.1", "@typescript-eslint/eslint-plugin": "^4.4.0", @@ -78,7 +74,6 @@ "rimraf": "^3.0.2", "rollup": "^2.27.1", "rollup-plugin-babel": "^4.4.0", - "rollup-plugin-terser": "^7.0.2", "rollup-plugin-typescript2": "^0.27.2", "semantic-release": "^17.2.1", "spectron": "^11.1.0", @@ -87,14 +82,6 @@ "webpack": "^4.44.2" }, "npmName": "electron-redux", - "npmFileMap": [ - { - "basePath": "/dist/", - "files": [ - "*.js" - ] - } - ], "jest": { "testRegex": "(/tests/.*\\.spec\\.[tj]s)$" }, diff --git a/rollup.config.js b/rollup.config.js index a5bee61e..aa5143f4 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -1,9 +1,7 @@ import nodeResolve from '@rollup/plugin-node-resolve' import babel from 'rollup-plugin-babel' -import replace from '@rollup/plugin-replace' import commonjs from '@rollup/plugin-commonjs' import typescript from 'rollup-plugin-typescript2' -import { terser } from 'rollup-plugin-terser' import pkg from './package.json' @@ -43,78 +41,4 @@ export default [ }), ], }, - - // ES for Browsers - { - input: 'src/index.ts', - output: { file: 'es/electron-redux.mjs', format: 'es', indent: false }, - plugins: [ - ...basePlugins, - replace({ - 'process.env.NODE_ENV': JSON.stringify('production'), - }), - babel({ - extensions, - exclude: 'node_modules/**', - }), - terser({ - compress: { - pure_getters: true, - unsafe: true, - unsafe_comps: true, - warnings: false, - }, - }), - ], - }, - - // UMD Development - { - input: 'src/index.ts', - output: { - file: 'dist/electron-redux.js', - format: 'umd', - name: 'ElectronRedux', - indent: false, - }, - plugins: [ - ...basePlugins, - babel({ - extensions, - exclude: 'node_modules/**', - }), - replace({ - 'process.env.NODE_ENV': JSON.stringify('development'), - }), - ], - }, - - // UMD Production - { - input: 'src/index.ts', - output: { - file: 'dist/electron-redux.min.js', - format: 'umd', - name: 'ElectronRedux', - indent: false, - }, - plugins: [ - ...basePlugins, - babel({ - extensions, - exclude: 'node_modules/**', - }), - replace({ - 'process.env.NODE_ENV': JSON.stringify('production'), - }), - terser({ - compress: { - pure_getters: true, - unsafe: true, - unsafe_comps: true, - warnings: false, - }, - }), - ], - }, ] diff --git a/src/index.ts b/src/index.ts index fa8ef06f..a4a98709 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ -import { syncMain } from './syncMain' +import { mainStateSyncEnhancer } from './mainStateSyncEnhancer' import { stopForwarding } from './utils' -import { syncRenderer } from './syncRenderer' +import { rendererStateSyncEnhancer } from './rendererStateSyncEnhancer' -export { syncMain, syncRenderer, stopForwarding } +export { mainStateSyncEnhancer, rendererStateSyncEnhancer, stopForwarding } diff --git a/src/mainStateSyncEnhancer.ts b/src/mainStateSyncEnhancer.ts new file mode 100644 index 00000000..64837679 --- /dev/null +++ b/src/mainStateSyncEnhancer.ts @@ -0,0 +1,64 @@ +import { ipcMain, webContents } from 'electron' +import { Action, applyMiddleware, Middleware, StoreCreator, StoreEnhancer } from 'redux' + +import { preventDoubleInitialization, stopForwarding, validateAction } from './utils' + +function createMiddleware(options: MainStateSyncEnhancerOptions) { + const middleware: Middleware = (store) => { + ipcMain.handle('electron-redux.INIT_STATE', async () => { + // Serialize the initial state using custom replacer + return JSON.stringify(store.getState(), options.replacer) + }) + + // When receiving an action from a renderer + ipcMain.on('electron-redux.ACTION', (event, action: Action) => { + const localAction = stopForwarding(action) + store.dispatch(localAction) + + // Forward it to all of the other renderers + webContents.getAllWebContents().forEach((contents) => { + // Ignore the renderer that sent the action + if (contents.id !== event.sender.id) { + contents.send('electron-redux.ACTION', localAction) + } + }) + }) + + return (next) => (action) => { + if (validateAction(action)) { + webContents.getAllWebContents().forEach((contents) => { + contents.send('electron-redux.ACTION', action) + }) + } + + return next(action) + } + } + return middleware +} + +export type MainStateSyncEnhancerOptions = { + /** + * Custom store serializaton function. This function is called for each member of the object. + * If a member contains nested objects, + * the nested objects are transformed before the parent object is. + */ + replacer?: (this: unknown, key: string, value: unknown) => unknown +} + +const defaultOptions: MainStateSyncEnhancerOptions = {} + +/** + * Creates new instance of main process redux enhancer. + * @param {MainStateSyncEnhancerOptions} options Additional enhancer options + * @returns StoreEnhancer + */ +export const mainStateSyncEnhancer = (options = defaultOptions): StoreEnhancer => ( + createStore: StoreCreator +) => { + preventDoubleInitialization() + const middleware = createMiddleware(options) + return (reducer, state) => { + return createStore(reducer, state, applyMiddleware(middleware)) + } +} diff --git a/src/syncRenderer.ts b/src/rendererStateSyncEnhancer.ts similarity index 64% rename from src/syncRenderer.ts rename to src/rendererStateSyncEnhancer.ts index d808e9be..aef47148 100644 --- a/src/syncRenderer.ts +++ b/src/rendererStateSyncEnhancer.ts @@ -1,19 +1,24 @@ import { ipcRenderer } from 'electron' import { Action, applyMiddleware, Middleware, Reducer, StoreCreator, StoreEnhancer } from 'redux' -import { hydrate, preventDoubleInitialization, stopForwarding, validateAction } from './utils' +import { preventDoubleInitialization, stopForwarding, validateAction } from './utils' -async function getRendererState(callback: (state: unknown) => void) { +async function fetchInitialState( + options: RendererStateSyncEnhancerOptions, + callback: (state: unknown) => void +) { // Electron will throw an error if there isn't a handler for the channel. // We catch it so that we can throw a more useful error - const state = await ipcRenderer.invoke('electron-redux.FETCH_STATE').catch((error) => { - console.error(error) - throw new Error('No Redux store found in main process. Did you use the syncMain enhancer?') + const state = await ipcRenderer.invoke('electron-redux.INIT_STATE').catch((error) => { + console.warn(error) + throw new Error( + 'No Redux store found in main process. Did you use the mainStateSyncEnhancer in the MAIN process?' + ) }) // We do some fancy hydration on certain types like Map and Set. // See also `freeze` - callback(JSON.parse(state, hydrate)) + callback(JSON.parse(state, options.reviver)) } /** @@ -61,7 +66,27 @@ const middleware: Middleware = (store) => { } } -export const syncRenderer: StoreEnhancer = (createStore: StoreCreator) => { +export type RendererStateSyncEnhancerOptions = { + /** + * Custom function used during de-serialization of the redux store to transform the object. + * This function is called for each member of the object. If a member contains nested objects, + * the nested objects are transformed before the parent object is. + */ + reviver?: (this: unknown, key: string, value: unknown) => unknown +} + +const defaultOptions: RendererStateSyncEnhancerOptions = {} + +/** + * 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 = defaultOptions): StoreEnhancer => ( + createStore: StoreCreator +) => { preventDoubleInitialization() return (reducer, state) => { @@ -74,7 +99,7 @@ export const syncRenderer: StoreEnhancer = (createStore: StoreCreator) => { // This is the reason we need to be an enhancer, rather than a middleware. // We use this (along with the wrapReducer function above) to dispatch an // action that initializes the store without needing to fetch it synchronously. - getRendererState((state) => { + fetchInitialState(options, (state) => { store.dispatch(replaceState(state)) }) diff --git a/src/syncMain.ts b/src/syncMain.ts deleted file mode 100644 index 79d59d6a..00000000 --- a/src/syncMain.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { ipcMain, webContents } from 'electron' -import { Action, applyMiddleware, Middleware, StoreCreator, StoreEnhancer } from 'redux' - -import { freeze, preventDoubleInitialization, stopForwarding, validateAction } from './utils' - -const middleware: Middleware = (store) => { - ipcMain.handle('electron-redux.FETCH_STATE', async () => { - // Stringify the current state, and freeze it to preserve certain types - // that you might want to use in your state, but aren't JSON serializable - // by default. - return JSON.stringify(store.getState(), freeze) - }) - - // When receiving an action from a renderer - ipcMain.on('electron-redux.ACTION', (event, action: Action) => { - const localAction = stopForwarding(action) - store.dispatch(localAction) - - // Forward it to all of the other renderers - webContents.getAllWebContents().forEach((contents) => { - // Ignore the renderer that sent the action - if (contents.id !== event.sender.id) { - contents.send('electron-redux.ACTION', localAction) - } - }) - }) - - return (next) => (action) => { - if (validateAction(action)) { - webContents.getAllWebContents().forEach((contents) => { - contents.send('electron-redux.ACTION', action) - }) - } - - return next(action) - } -} - -export const syncMain: StoreEnhancer = (createStore: StoreCreator) => { - preventDoubleInitialization() - - return (reducer, state) => { - return createStore(reducer, state, applyMiddleware(middleware)) - } -} diff --git a/src/utils/index.ts b/src/utils/index.ts index 57d37246..3ed54537 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,3 +1,2 @@ export * from './actions' -export * from './json' export * from './misc' diff --git a/src/utils/json.ts b/src/utils/json.ts deleted file mode 100644 index 3b5780df..00000000 --- a/src/utils/json.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Preserves some types like Map and Set when serializing - */ -export const freeze = (_: string, value: unknown): unknown => { - if (value instanceof Map) { - const obj = Object.fromEntries(value) - obj.__hydrate_type = '__hydrate_map' - return obj - } else if (value instanceof Set) { - return { - __hydrate_type: '__hydrate_set', - items: Array.from(value), - } - } - - return value -} - -/** - * Hydrates some types like Map and Set when deserializing - */ -export const hydrate = (_: string, value: any) => { - if (value?.__hydrate_type === '__hydrate_map') { - return new Map(Object.entries(value).filter(([key]) => key !== '__hydrate_type')) - } else if (value?.__hydrate_type === '__hydrate_set') { - return new Set(value.items) - } - - return value -} diff --git a/tests/e2e/main/index.ts b/tests/e2e/main/index.ts index 0a8dae5a..da27e899 100644 --- a/tests/e2e/main/index.ts +++ b/tests/e2e/main/index.ts @@ -3,11 +3,11 @@ import url from 'url' import { app, BrowserWindow } from 'electron' import { createStore } from 'redux' import { reducer } from '../../counter' -import { syncMain } from '../../..' +import { mainStateSyncEnhancer } from '../../..' const isDevelopment = process.env.NODE_ENV !== 'production' -const store = createStore(reducer, syncMain) +const store = createStore(reducer, mainStateSyncEnhancer()) // Keep a global reference of the window object, if you don't, the window will // be closed automatically when the JavaScript object is garbage collected. diff --git a/tests/e2e/renderer/index.ts b/tests/e2e/renderer/index.ts index 97968d84..1da575fe 100644 --- a/tests/e2e/renderer/index.ts +++ b/tests/e2e/renderer/index.ts @@ -1,8 +1,8 @@ import { createStore } from 'redux' import { reducer } from '../../counter' -import { syncRenderer } from '../../../' +import { rendererStateSyncEnhancer } from '../../../' -const store = createStore(reducer, syncRenderer) +const store = createStore(reducer, rendererStateSyncEnhancer()) function mount() { document.getElementById('app')!.innerHTML = ` diff --git a/tests/typescript/syncRenderer.ts b/tests/typescript/mainStateSyncEnhancer.ts similarity index 68% rename from tests/typescript/syncRenderer.ts rename to tests/typescript/mainStateSyncEnhancer.ts index 1f4b9806..60fe3541 100644 --- a/tests/typescript/syncRenderer.ts +++ b/tests/typescript/mainStateSyncEnhancer.ts @@ -1,5 +1,5 @@ -import { syncRenderer } from '../..' +import { mainStateSyncEnhancer } from '../../types' import { createStore, Store } from 'redux' import { reducer, CounterState, Actions } from '../counter' -const store: Store = createStore(reducer, syncRenderer) +const store: Store = createStore(reducer, mainStateSyncEnhancer()) diff --git a/tests/typescript/syncMain.ts b/tests/typescript/rendererStateSyncEnhancer.ts similarity index 66% rename from tests/typescript/syncMain.ts rename to tests/typescript/rendererStateSyncEnhancer.ts index f788c80b..3199b66d 100644 --- a/tests/typescript/syncMain.ts +++ b/tests/typescript/rendererStateSyncEnhancer.ts @@ -1,5 +1,5 @@ -import { syncMain } from '../..' +import { rendererStateSyncEnhancer } from '../../types' import { createStore, Store } from 'redux' import { reducer, CounterState, Actions } from '../counter' -const store: Store = createStore(reducer, syncMain) +const store: Store = createStore(reducer, rendererStateSyncEnhancer())