├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .prettierrc.yml ├── .travis.yml ├── .vscode └── settings.json ├── README.md ├── app ├── main │ └── index.js └── renderer │ ├── .eslintrc │ ├── actions │ └── user.js │ ├── app.js │ ├── components │ ├── LoggedIn.js │ └── Login.js │ ├── containers │ ├── LoggedInPage.js │ └── LoginPage.js │ ├── index.html │ ├── reducers │ └── user.js │ ├── routes.js │ └── store.js ├── babel.config.js ├── dist-assets └── .gitkeep ├── electron-builder.yml ├── gulpfile.js ├── init.js ├── package-lock.json ├── package.json ├── tasks ├── assets.js ├── distribution.js ├── electron.js ├── hotreload.js ├── scripts.js └── watch.js └── test ├── .eslintrc ├── actions └── user.spec.js └── reducers └── user.spec.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.{js,json,css,html}] 12 | indent_style = space 13 | indent_size = 2 14 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | cache 4 | lib 5 | dist 6 | webpack.*.js 7 | server.js 8 | build.js 9 | init.js -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": [ 4 | "eslint:recommended", 5 | "plugin:react/recommended", 6 | "prettier" 7 | ], 8 | "parser": "babel-eslint", 9 | "parserOptions": { 10 | "ecmaFeatures": { 11 | "jsx": true, 12 | "modules": true 13 | } 14 | }, 15 | "plugins": [ "react" ], 16 | "rules": { 17 | "prefer-const": "warn", 18 | "no-console": "off", 19 | "no-loop-func": "warn", 20 | "new-cap": "off", 21 | "no-param-reassign": "warn", 22 | "func-names": "off", 23 | "no-unused-expressions" : "error", 24 | "block-scoped-var": "error", 25 | "react/prop-types": "off" 26 | }, 27 | "settings": { 28 | "react": { 29 | "pragma": "React", 30 | "version": "16.2" 31 | } 32 | }, 33 | "env": { 34 | "es6": true, 35 | "node": true 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | build 4 | .DS_Store 5 | *.log 6 | -------------------------------------------------------------------------------- /.prettierrc.yml: -------------------------------------------------------------------------------- 1 | # .prettierrc.yml 2 | # see: https://prettier.io/docs/en/options.html 3 | printWidth: 100 4 | semi: true 5 | singleQuote: true 6 | trailingComma: all 7 | bracketSpacing: true 8 | jsxBracketSameLine: true 9 | arrowParens: always 10 | proseWrap: always -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: node_js 4 | 5 | node_js: 6 | - '10' 7 | cache: npm 8 | services: 9 | - xvfb 10 | 11 | install: 12 | - npm install 13 | 14 | script: 15 | - npm run check-format 16 | - npm run lint 17 | - npm test 18 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "search.exclude": { 3 | "build/": true, 4 | "dist/": true 5 | } 6 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # electron-react-redux-boilerplate 2 | [![Build Status](https://api.travis-ci.org/jschr/electron-react-redux-boilerplate.svg)](https://travis-ci.org/jschr/electron-react-redux-boilerplate) 3 | [![dependencies Status](https://david-dm.org/jschr/electron-react-redux-boilerplate/status.svg)](https://david-dm.org/jschr/electron-react-redux-boilerplate) 4 | [![devDependencies Status](https://david-dm.org/jschr/electron-react-redux-boilerplate/dev-status.svg)](https://david-dm.org/jschr/electron-react-redux-boilerplate?type=dev) 5 | 6 | A minimal boilerplate to get started with [Electron](http://electron.atom.io/), [React](https://facebook.github.io/react/) and [Redux](http://redux.js.org/). 7 | 8 | Including: 9 | 10 | * [React Router](https://reacttraining.com/react-router/) 11 | * [Redux Thunk](https://github.com/gaearon/redux-thunk/) 12 | * [Redux Actions](https://github.com/acdlite/redux-actions/) 13 | * [Redux Local Storage](https://github.com/elgerlambert/redux-localstorage/) 14 | * [Electron Packager](https://github.com/electron-userland/electron-packager) 15 | * [Electron DevTools Installer](https://github.com/MarshallOfSound/electron-devtools-installer) 16 | * [Electron Mocha](https://github.com/jprichardson/electron-mocha) 17 | * [Browsersync](https://browsersync.io/) 18 | 19 | ## Quick start 20 | 21 | Clone the repository 22 | ```bash 23 | git clone --depth=1 https://github.com/jschr/electron-react-redux-boilerplate 24 | ``` 25 | 26 | Install dependencies 27 | ```bash 28 | cd electron-react-redux-boilerplate 29 | npm install 30 | ``` 31 | 32 | Development 33 | ```bash 34 | npm run develop 35 | ``` 36 | 37 | ## DevTools 38 | 39 | Toggle DevTools: 40 | 41 | * macOS: Cmd Alt I or F12 42 | * Linux: Ctrl Shift I or F12 43 | * Windows: Ctrl Shift I or F12 44 | 45 | ## Packaging 46 | 47 | Modify [electron-builder.yml](./electron-builder.yml) to edit package info. 48 | 49 | For a full list of options see: https://www.electron.build/configuration/configuration 50 | 51 | Create a package for macOS, Windows or Linux using one of the following commands: 52 | 53 | ``` 54 | npm run pack:mac 55 | npm run pack:win 56 | npm run pack:linux 57 | ``` 58 | 59 | ## Tests 60 | 61 | ``` 62 | npm run test 63 | ``` 64 | 65 | ## Maintainers 66 | 67 | - [@jschr](https://github.com/jschr) 68 | - [@pronebird](https://github.com/pronebird) 69 | 70 | ## Apps using this boilerplate 71 | 72 | - [Mullvad VPN app](https://github.com/mullvad/mullvadvpn-app) 73 | - [YouTube Downloader Electron](https://github.com/vanzylv/youtube-downloader-electron) 74 | - [Martian: A Websocket test tool](https://github.com/drex44/martian) 75 | -------------------------------------------------------------------------------- /app/main/index.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { app, crashReporter, BrowserWindow, Menu } from 'electron'; 3 | 4 | const isDevelopment = process.env.NODE_ENV === 'development'; 5 | 6 | let mainWindow = null; 7 | let forceQuit = false; 8 | 9 | const installExtensions = async () => { 10 | const installer = require('electron-devtools-installer'); 11 | const extensions = ['REACT_DEVELOPER_TOOLS', 'REDUX_DEVTOOLS']; 12 | const forceDownload = !!process.env.UPGRADE_EXTENSIONS; 13 | for (const name of extensions) { 14 | try { 15 | await installer.default(installer[name], forceDownload); 16 | } catch (e) { 17 | console.log(`Error installing ${name} extension: ${e.message}`); 18 | } 19 | } 20 | }; 21 | 22 | crashReporter.start({ 23 | productName: 'YourName', 24 | companyName: 'YourCompany', 25 | submitURL: 'https://your-domain.com/url-to-submit', 26 | uploadToServer: false, 27 | }); 28 | 29 | app.on('window-all-closed', () => { 30 | // On OS X it is common for applications and their menu bar 31 | // to stay active until the user quits explicitly with Cmd + Q 32 | if (process.platform !== 'darwin') { 33 | app.quit(); 34 | } 35 | }); 36 | 37 | app.on('ready', async () => { 38 | if (isDevelopment) { 39 | await installExtensions(); 40 | } 41 | 42 | mainWindow = new BrowserWindow({ 43 | width: 1000, 44 | height: 800, 45 | minWidth: 640, 46 | minHeight: 480, 47 | show: false, 48 | webPreferences: { 49 | nodeIntegration: true, 50 | }, 51 | }); 52 | 53 | mainWindow.loadFile(path.resolve(path.join(__dirname, '../renderer/index.html'))); 54 | 55 | // show window once on first load 56 | mainWindow.webContents.once('did-finish-load', () => { 57 | mainWindow.show(); 58 | }); 59 | 60 | mainWindow.webContents.on('did-finish-load', () => { 61 | // Handle window logic properly on macOS: 62 | // 1. App should not terminate if window has been closed 63 | // 2. Click on icon in dock should re-open the window 64 | // 3. ⌘+Q should close the window and quit the app 65 | if (process.platform === 'darwin') { 66 | mainWindow.on('close', function (e) { 67 | if (!forceQuit) { 68 | e.preventDefault(); 69 | mainWindow.hide(); 70 | } 71 | }); 72 | 73 | app.on('activate', () => { 74 | mainWindow.show(); 75 | }); 76 | 77 | app.on('before-quit', () => { 78 | forceQuit = true; 79 | }); 80 | } else { 81 | mainWindow.on('closed', () => { 82 | mainWindow = null; 83 | }); 84 | } 85 | }); 86 | 87 | if (isDevelopment) { 88 | // auto-open dev tools 89 | mainWindow.webContents.openDevTools(); 90 | 91 | // add inspect element on right click menu 92 | mainWindow.webContents.on('context-menu', (e, props) => { 93 | Menu.buildFromTemplate([ 94 | { 95 | label: 'Inspect element', 96 | click() { 97 | mainWindow.inspectElement(props.x, props.y); 98 | }, 99 | }, 100 | ]).popup(mainWindow); 101 | }); 102 | } 103 | }); 104 | -------------------------------------------------------------------------------- /app/renderer/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true 4 | } 5 | } -------------------------------------------------------------------------------- /app/renderer/actions/user.js: -------------------------------------------------------------------------------- 1 | import { createAction } from 'redux-actions'; 2 | 3 | export default { 4 | login: createAction('USER_LOGIN'), 5 | logout: createAction('USER_LOGOUT'), 6 | }; 7 | -------------------------------------------------------------------------------- /app/renderer/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import { ConnectedRouter } from 'connected-react-router'; 5 | import { createMemoryHistory } from 'history'; 6 | import routes from './routes'; 7 | import configureStore from './store'; 8 | 9 | const syncHistoryWithStore = (store, history) => { 10 | const { router } = store.getState(); 11 | if (router && router.location) { 12 | history.replace(router.location); 13 | } 14 | }; 15 | 16 | const initialState = {}; 17 | const routerHistory = createMemoryHistory(); 18 | const store = configureStore(initialState, routerHistory); 19 | syncHistoryWithStore(store, routerHistory); 20 | 21 | const rootElement = document.querySelector(document.currentScript.getAttribute('data-container')); 22 | 23 | ReactDOM.render( 24 | 25 | {routes} 26 | , 27 | rootElement, 28 | ); 29 | -------------------------------------------------------------------------------- /app/renderer/components/LoggedIn.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | export default class LoggedIn extends Component { 5 | static propTypes = { 6 | onLogout: PropTypes.func.isRequired, 7 | }; 8 | 9 | handleLogout = () => { 10 | this.props.onLogout({ 11 | username: '', 12 | loggedIn: false, 13 | }); 14 | }; 15 | 16 | render() { 17 | return ( 18 |
19 |

Logged in as {this.props.user.username}

20 | 21 |
22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/renderer/components/Login.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | export default class Login extends Component { 5 | static propTypes = { 6 | onLogin: PropTypes.func.isRequired, 7 | }; 8 | 9 | state = { 10 | username: '', 11 | }; 12 | 13 | handleLogin = () => { 14 | this.props.onLogin({ 15 | username: this.state.username, 16 | loggedIn: true, 17 | }); 18 | }; 19 | 20 | handleChange = (e) => { 21 | this.setState({ 22 | username: e.target.value, 23 | }); 24 | }; 25 | 26 | render() { 27 | return ( 28 |
29 |

Login

30 | 31 | 32 |
33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/renderer/containers/LoggedInPage.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { push } from 'connected-react-router'; 3 | import { bindActionCreators } from 'redux'; 4 | import LoggedIn from '../components/LoggedIn'; 5 | import userActions from '../actions/user'; 6 | 7 | const mapStateToProps = (state) => { 8 | return state; 9 | }; 10 | 11 | const mapDispatchToProps = (dispatch) => { 12 | const user = bindActionCreators(userActions, dispatch); 13 | return { 14 | onLogout: (data) => { 15 | user.logout(data); 16 | dispatch(push('/')); 17 | }, 18 | }; 19 | }; 20 | 21 | export default connect(mapStateToProps, mapDispatchToProps)(LoggedIn); 22 | -------------------------------------------------------------------------------- /app/renderer/containers/LoginPage.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { push } from 'connected-react-router'; 3 | import { bindActionCreators } from 'redux'; 4 | import Login from '../components/Login'; 5 | import userActions from '../actions/user'; 6 | 7 | const mapStateToProps = (state) => { 8 | return state; 9 | }; 10 | 11 | const mapDispatchToProps = (dispatch) => { 12 | const user = bindActionCreators(userActions, dispatch); 13 | return { 14 | onLogin: (data) => { 15 | user.login(data); 16 | dispatch(push('/loggedin')); 17 | }, 18 | }; 19 | }; 20 | 21 | export default connect(mapStateToProps, mapDispatchToProps)(Login); 22 | -------------------------------------------------------------------------------- /app/renderer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | My App 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/renderer/reducers/user.js: -------------------------------------------------------------------------------- 1 | import { handleActions } from 'redux-actions'; 2 | import actions from '../actions/user'; 3 | 4 | export default handleActions( 5 | { 6 | [actions.login]: (state, action) => { 7 | return { ...state, ...action.payload }; 8 | }, 9 | [actions.logout]: (state, action) => { 10 | return { ...state, ...action.payload }; 11 | }, 12 | }, 13 | {}, 14 | ); 15 | -------------------------------------------------------------------------------- /app/renderer/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Switch, Route } from 'react-router'; 3 | 4 | import LoginPage from './containers/LoginPage'; 5 | import LoggedInPage from './containers/LoggedInPage'; 6 | 7 | export default ( 8 | 9 | 10 | 11 | 12 | ); 13 | -------------------------------------------------------------------------------- /app/renderer/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, combineReducers, compose } from 'redux'; 2 | import { connectRouter, routerMiddleware, push } from 'connected-react-router'; 3 | import persistState from 'redux-localstorage'; 4 | import thunk from 'redux-thunk'; 5 | 6 | import user from './reducers/user'; 7 | import userActions from './actions/user'; 8 | 9 | export default function configureStore(initialState, routerHistory) { 10 | const router = routerMiddleware(routerHistory); 11 | 12 | const actionCreators = { 13 | ...userActions, 14 | push, 15 | }; 16 | 17 | const reducers = { 18 | router: connectRouter(routerHistory), 19 | user, 20 | }; 21 | 22 | const middlewares = [thunk, router]; 23 | 24 | const composeEnhancers = (() => { 25 | const compose_ = window && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__; 26 | if (process.env.NODE_ENV === 'development' && compose_) { 27 | return compose_({ actionCreators }); 28 | } 29 | return compose; 30 | })(); 31 | 32 | const enhancer = composeEnhancers(applyMiddleware(...middlewares), persistState()); 33 | const rootReducer = combineReducers(reducers); 34 | 35 | return createStore(rootReducer, initialState, enhancer); 36 | } 37 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { 6 | targets: { 7 | electron: '6.0', 8 | }, 9 | }, 10 | ], 11 | '@babel/preset-react', 12 | ], 13 | plugins: [ 14 | ['@babel/plugin-proposal-decorators', { legacy: true }], 15 | ['@babel/plugin-proposal-class-properties', { loose: true }], 16 | ], 17 | }; 18 | -------------------------------------------------------------------------------- /dist-assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jschr/electron-react-redux-boilerplate/3af7a5ebcbcbe37de980ff95802600486558198a/dist-assets/.gitkeep -------------------------------------------------------------------------------- /electron-builder.yml: -------------------------------------------------------------------------------- 1 | appId: com.example.app 2 | copyright: Example co 3 | productName: MyApp 4 | 5 | asar: true 6 | 7 | directories: 8 | buildResources: dist-assets/ 9 | output: dist/ 10 | 11 | files: 12 | - package.json 13 | - init.js 14 | - build/ 15 | - node_modules/ 16 | 17 | dmg: 18 | contents: 19 | - type: link 20 | path: /Applications 21 | x: 410 22 | y: 150 23 | - type: file 24 | x: 130 25 | y: 150 26 | 27 | mac: 28 | target: dmg 29 | category: public.app-category.tools 30 | 31 | win: 32 | target: nsis 33 | 34 | linux: 35 | target: 36 | - deb 37 | - AppImage 38 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const { task, series } = require('gulp'); 2 | const rimraf = require('rimraf'); 3 | 4 | const scripts = require('./tasks/scripts'); 5 | const assets = require('./tasks/assets'); 6 | const watch = require('./tasks/watch'); 7 | const dist = require('./tasks/distribution'); 8 | 9 | task('clean', function (done) { 10 | rimraf('./build', done); 11 | }); 12 | task('build', series('clean', assets.copyHtml, scripts.build)); 13 | task('develop', series('clean', watch.start)); 14 | task('pack-win', series('build', dist.packWin)); 15 | task('pack-linux', series('build', dist.packLinux)); 16 | task('pack-mac', series('build', dist.packMac)); 17 | -------------------------------------------------------------------------------- /init.js: -------------------------------------------------------------------------------- 1 | require('./build/main'); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron-react-redux-boilerplate", 3 | "version": "0.0.0", 4 | "description": "electron-react-redux-boilerplate", 5 | "main": "init.js", 6 | "author": { 7 | "name": "Jordan Schroter", 8 | "email": "email@author.com" 9 | }, 10 | "repository": "https://github.com/jschr/electron-react-redux-boilerplate", 11 | "license": "MIT", 12 | "dependencies": { 13 | "@babel/register": "^7.9.0", 14 | "connected-react-router": "^6.8.0", 15 | "history": "^4.10.1", 16 | "prop-types": "^15.7.2", 17 | "react": "^16.13.1", 18 | "react-dom": "^16.13.0", 19 | "react-redux": "^7.2.0", 20 | "react-router": "^5.1.2", 21 | "redux": "^4.0.5", 22 | "redux-actions": "^2.6.5", 23 | "redux-localstorage": "^0.4.1", 24 | "redux-thunk": "^2.2.0" 25 | }, 26 | "devDependencies": { 27 | "@babel/core": "^7.9.0", 28 | "@babel/plugin-proposal-class-properties": "^7.8.3", 29 | "@babel/plugin-proposal-decorators": "^7.8.3", 30 | "@babel/preset-env": "^7.9.5", 31 | "@babel/preset-react": "^7.9.4", 32 | "babel-eslint": "^10.1.0", 33 | "browser-sync": "^2.26.7", 34 | "chai": "^4.1.0", 35 | "electron": "^9.4.0", 36 | "electron-builder": "^22.4.1", 37 | "electron-devtools-installer": "^3.0.0", 38 | "electron-mocha": "^8.2.1", 39 | "eslint": "^6.8.0", 40 | "eslint-config-prettier": "^6.10.1", 41 | "eslint-plugin-react": "^7.19.0", 42 | "gulp": "^4.0.2", 43 | "gulp-babel": "^8.0.0", 44 | "gulp-inject-string": "^1.1.2", 45 | "gulp-sourcemaps": "^2.6.5", 46 | "prettier": "^2.0.4", 47 | "redux-mock-store": "^1.5.4", 48 | "rimraf": "^3.0.2" 49 | }, 50 | "scripts": { 51 | "postinstall": "electron-builder install-app-deps", 52 | "develop": "gulp develop", 53 | "test": "electron-mocha --renderer -R spec --require @babel/register test/**/*.spec.js", 54 | "lint": "eslint --no-ignore tasks app test *.js", 55 | "format": "npm run private:format -- --write", 56 | "check-format": "npm run private:format -- --list-different", 57 | "pack:mac": "gulp pack-mac", 58 | "pack:win": "gulp pack-win", 59 | "pack:linux": "gulp pack-linux", 60 | "private:format": "prettier gulpfile.js babel.config.js \"tasks/*.js\" \"app/**/*.js\" \"test/**/*.js\"" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tasks/assets.js: -------------------------------------------------------------------------------- 1 | const { src, dest } = require('gulp'); 2 | 3 | function copyHtml() { 4 | return src('app/renderer/index.html').pipe(dest('build/renderer')); 5 | } 6 | 7 | copyHtml.displayName = 'copy-html'; 8 | 9 | exports.copyHtml = copyHtml; 10 | -------------------------------------------------------------------------------- /tasks/distribution.js: -------------------------------------------------------------------------------- 1 | const builder = require('electron-builder'); 2 | 3 | function packWin() { 4 | return builder.build({ 5 | targets: builder.Platform.WINDOWS.createTarget(), 6 | }); 7 | } 8 | 9 | function packMac() { 10 | return builder.build({ 11 | targets: builder.Platform.MAC.createTarget(), 12 | }); 13 | } 14 | 15 | function packLinux() { 16 | return builder.build({ 17 | targets: builder.Platform.LINUX.createTarget(), 18 | }); 19 | } 20 | 21 | packWin.displayName = 'builder-win'; 22 | packMac.displayName = 'builder-mac'; 23 | packLinux.displayName = 'builder-linux'; 24 | 25 | exports.packWin = packWin; 26 | exports.packMac = packMac; 27 | exports.packLinux = packLinux; 28 | -------------------------------------------------------------------------------- /tasks/electron.js: -------------------------------------------------------------------------------- 1 | const { spawn } = require('child_process'); 2 | const electron = require('electron'); 3 | 4 | let subprocess; 5 | 6 | function startElectron(done) { 7 | subprocess = spawn(electron, ['.', '--no-sandbox'], { 8 | env: { ...process.env, NODE_ENV: 'development' }, 9 | stdio: 'inherit', 10 | }); 11 | done(); 12 | } 13 | 14 | function stopElectron() { 15 | subprocess.kill(); 16 | return subprocess; 17 | } 18 | 19 | startElectron.displayName = 'start-electron'; 20 | stopElectron.displayName = 'stop-electron'; 21 | 22 | exports.start = startElectron; 23 | exports.stop = stopElectron; 24 | -------------------------------------------------------------------------------- /tasks/hotreload.js: -------------------------------------------------------------------------------- 1 | const { series, src, dest } = require('gulp'); 2 | const inject = require('gulp-inject-string'); 3 | 4 | const browserSync = require('browser-sync').create(); 5 | 6 | function startBrowserSync(done) { 7 | browserSync.init( 8 | { 9 | ui: false, 10 | localOnly: true, 11 | port: 35829, 12 | ghostMode: false, 13 | open: false, 14 | notify: false, 15 | logSnippet: false, 16 | }, 17 | function (error) { 18 | done(error); 19 | }, 20 | ); 21 | } 22 | 23 | function injectBrowserSync() { 24 | return src('app/renderer/index.html') 25 | .pipe(inject.before('', browserSync.getOption('snippet'))) 26 | .pipe( 27 | inject.after('script-src', " 'unsafe-eval' " + browserSync.getOption('urls').get('local')), 28 | ) 29 | .pipe(dest('build/renderer')); 30 | } 31 | 32 | function reloadBrowser(done) { 33 | browserSync.reload(); 34 | done(); 35 | } 36 | 37 | startBrowserSync.displayName = 'start-hotreload'; 38 | injectBrowserSync.displayName = 'inject-hotreload'; 39 | reloadBrowser.displayName = 'reload-hotreload'; 40 | 41 | exports.start = series(startBrowserSync, injectBrowserSync); 42 | exports.inject = injectBrowserSync; 43 | exports.reload = reloadBrowser; 44 | -------------------------------------------------------------------------------- /tasks/scripts.js: -------------------------------------------------------------------------------- 1 | const { src, dest } = require('gulp'); 2 | const babel = require('gulp-babel'); 3 | const sourcemaps = require('gulp-sourcemaps'); 4 | const inject = require('gulp-inject-string'); 5 | 6 | function build() { 7 | return src('app/**/*.js') 8 | .pipe(babel()) 9 | .pipe(inject.replace('process.env.NODE_ENV', '"production"')) 10 | .pipe(dest('build')); 11 | } 12 | 13 | function developBuild() { 14 | return src('app/**/*.js') 15 | .pipe(sourcemaps.init()) 16 | .pipe(babel()) 17 | .pipe(sourcemaps.write()) 18 | .pipe(dest('build')); 19 | } 20 | 21 | build.displayName = 'build-scripts'; 22 | developBuild.displayName = 'dev-build-scripts'; 23 | 24 | exports.build = build; 25 | exports.developBuild = developBuild; 26 | -------------------------------------------------------------------------------- /tasks/watch.js: -------------------------------------------------------------------------------- 1 | const { parallel, series, watch } = require('gulp'); 2 | const electron = require('./electron'); 3 | const hotreload = require('./hotreload'); 4 | const assets = require('./assets'); 5 | const scripts = require('./scripts'); 6 | 7 | function watchMainScripts() { 8 | return watch(['app/main/**/*.js'], series(scripts.developBuild, electron.stop, electron.start)); 9 | } 10 | 11 | function watchRendererScripts() { 12 | return watch(['app/renderer/**/*.js'], series(scripts.developBuild, hotreload.reload)); 13 | } 14 | 15 | function watchHtml() { 16 | return watch( 17 | ['app/renderer/index.html'], 18 | series(assets.copyHtml, hotreload.inject, hotreload.reload), 19 | ); 20 | } 21 | 22 | watchMainScripts.displayName = 'watch-main-scripts'; 23 | watchRendererScripts.displayName = 'watch-renderer-scripts'; 24 | watchHtml.displayName = 'watch-html'; 25 | 26 | exports.start = series( 27 | assets.copyHtml, 28 | scripts.developBuild, 29 | hotreload.start, 30 | electron.start, 31 | parallel(watchMainScripts, watchRendererScripts, watchHtml), 32 | ); 33 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | } 5 | } -------------------------------------------------------------------------------- /test/actions/user.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import configureMockStore from 'redux-mock-store'; 3 | import thunk from 'redux-thunk'; 4 | import actions from '../../app/renderer/actions/user'; 5 | 6 | const mockStore = configureMockStore([thunk]); 7 | 8 | describe('actions', () => { 9 | describe('user', () => { 10 | it('should log in', () => { 11 | const store = mockStore({}); 12 | const expectedActions = [ 13 | { 14 | type: 'USER_LOGIN', 15 | payload: { 16 | username: 'John Doe', 17 | loggedIn: true, 18 | }, 19 | }, 20 | ]; 21 | 22 | store.dispatch( 23 | actions.login({ 24 | username: 'John Doe', 25 | loggedIn: true, 26 | }), 27 | ); 28 | 29 | expect(store.getActions()).deep.equal(expectedActions); 30 | }); 31 | 32 | it('should logout', () => { 33 | const store = mockStore({}); 34 | const expectedActions = [ 35 | { 36 | type: 'USER_LOGOUT', 37 | payload: { 38 | username: '', 39 | loggedIn: false, 40 | }, 41 | }, 42 | ]; 43 | 44 | store.dispatch( 45 | actions.logout({ 46 | username: '', 47 | loggedIn: false, 48 | }), 49 | ); 50 | 51 | expect(store.getActions()).deep.equal(expectedActions); 52 | }); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /test/reducers/user.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import reducer from '../../app/renderer/reducers/user'; 3 | 4 | describe('reducers', () => { 5 | describe('user', () => { 6 | it('should handle USER_LOGIN', () => { 7 | const action = { 8 | type: 'USER_LOGIN', 9 | payload: { 10 | username: 'John Doe', 11 | loggedIn: true, 12 | }, 13 | }; 14 | const test = Object.assign({}, action.payload); 15 | expect(reducer({}, action)).to.deep.equal(test); 16 | }); 17 | 18 | it('should handle USER_LOGOUT', () => { 19 | const action = { 20 | type: 'USER_LOGOUT', 21 | payload: { 22 | username: '', 23 | loggedIn: false, 24 | }, 25 | }; 26 | const test = Object.assign({}, action.payload); 27 | expect(reducer({}, action)).to.deep.equal(test); 28 | }); 29 | }); 30 | }); 31 | --------------------------------------------------------------------------------