├── tsconfig.prod.json ├── .babelrc ├── images.d.ts ├── public ├── favicon.ico ├── manifest.json └── index.html ├── app ├── renderer │ ├── index.css │ ├── reducers │ │ ├── index.ts │ │ └── counter.ts │ ├── actions │ │ └── counter │ │ │ ├── action_type.ts │ │ │ └── index.ts │ ├── components │ │ ├── NavBar │ │ │ └── index.tsx │ │ ├── Home │ │ │ ├── home.css │ │ │ ├── index.tsx │ │ │ └── logo.svg │ │ └── Counter │ │ │ └── index.tsx │ ├── types │ │ └── index.ts │ ├── containers │ │ ├── App.tsx │ │ ├── App.css │ │ ├── App.test.tsx │ │ └── counter.tsx │ ├── routes │ │ └── index.tsx │ ├── index.tsx │ ├── store │ │ └── index.ts │ └── registerServiceWorker.ts └── main │ ├── windows │ └── mainWindow.ts │ ├── tray.ts │ ├── menu.ts │ └── main.ts ├── assets └── electron.png ├── tsconfig.test.json ├── config ├── jest │ ├── typescriptTransform.js │ ├── fileTransform.js │ └── cssTransform.js ├── polyfills.js ├── webpack.config.electron.js ├── paths.js ├── env.js ├── webpackDevServer.config.js ├── webpack.config.dev.js └── webpack.config.prod.js ├── .gitignore ├── README.md ├── tsconfig.json ├── scripts ├── test.js ├── start.js └── build.js ├── LICENSE ├── tslint.json └── package.json /tsconfig.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json" 3 | } -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "env", "react-app" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /images.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg' 2 | declare module '*.png' 3 | declare module '*.jpg' 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Leskd/electron-react-typescript/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /app/renderer/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /assets/electron.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Leskd/electron-react-typescript/HEAD/assets/electron.png -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs" 5 | } 6 | } -------------------------------------------------------------------------------- /app/renderer/reducers/index.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import counter from './counter'; 3 | 4 | const rootReducers = combineReducers({ 5 | counter 6 | }); 7 | 8 | export default rootReducers; 9 | -------------------------------------------------------------------------------- /app/renderer/actions/counter/action_type.ts: -------------------------------------------------------------------------------- 1 | export const INCREMENT = 'increment'; 2 | export type INCREMENT = typeof INCREMENT; 3 | 4 | export const DECREMENT = 'DECREMENT'; 5 | export type DECREMENT = typeof DECREMENT; 6 | -------------------------------------------------------------------------------- /config/jest/typescriptTransform.js: -------------------------------------------------------------------------------- 1 | // Copyright 2004-present Facebook. All Rights Reserved. 2 | 3 | 'use strict'; 4 | 5 | const tsJestPreprocessor = require('ts-jest/preprocessor'); 6 | 7 | module.exports = tsJestPreprocessor; 8 | -------------------------------------------------------------------------------- /app/renderer/components/NavBar/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | const NavBar = () => ( 5 |
6 |
HomeCounter
7 |
8 | ); 9 | 10 | export default NavBar; 11 | -------------------------------------------------------------------------------- /app/renderer/types/index.ts: -------------------------------------------------------------------------------- 1 | import { RouterState } from 'connected-react-router'; 2 | 3 | export interface RouterState { 4 | router: RouterState; 5 | } 6 | 7 | export interface CounterState { 8 | count: number; 9 | } 10 | 11 | export interface StoreState { 12 | counter: CounterState; 13 | } 14 | -------------------------------------------------------------------------------- /config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | // This is a custom Jest transformer turning file imports into filenames. 6 | // http://facebook.github.io/jest/docs/en/webpack.html 7 | 8 | module.exports = { 9 | process(src, filename) { 10 | return `module.exports = ${JSON.stringify(path.basename(filename))};`; 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This is a custom Jest transformer turning style imports into empty objects. 4 | // http://facebook.github.io/jest/docs/en/webpack.html 5 | 6 | module.exports = { 7 | process() { 8 | return 'module.exports = {};'; 9 | }, 10 | getCacheKey() { 11 | // The output is always the same. 12 | return 'cssTransform'; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | /package 12 | /dist 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | /main.js 25 | /package-lock.json 26 | -------------------------------------------------------------------------------- /app/renderer/actions/counter/index.ts: -------------------------------------------------------------------------------- 1 | import { INCREMENT, DECREMENT } from './action_type'; 2 | 3 | export type counterActions = Increment | Decrement; 4 | 5 | interface Increment { 6 | type: INCREMENT; 7 | } 8 | 9 | export function increment(): Increment { 10 | return { 11 | type: INCREMENT, 12 | }; 13 | } 14 | 15 | interface Decrement { 16 | type: DECREMENT; 17 | } 18 | 19 | export function decrement(): Decrement { 20 | return { 21 | type: DECREMENT 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /app/renderer/containers/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { RouterState } from '../types'; 4 | import Routes from '../routes'; 5 | 6 | class App extends Component { 7 | render() { 8 | return ( 9 | 10 | ); 11 | } 12 | } 13 | 14 | function mapStateToProps(state: RouterState) { 15 | return { 16 | location: state.router.location.pathname 17 | }; 18 | } 19 | 20 | export default connect(mapStateToProps)(App); 21 | -------------------------------------------------------------------------------- /app/renderer/containers/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | animation: App-logo-spin infinite 20s linear; 7 | height: 80px; 8 | } 9 | 10 | .App-header { 11 | background-color: #222; 12 | height: 150px; 13 | padding: 20px; 14 | color: white; 15 | } 16 | 17 | .App-title { 18 | font-size: 1.5em; 19 | } 20 | 21 | .App-intro { 22 | font-size: large; 23 | } 24 | 25 | @keyframes App-logo-spin { 26 | from { transform: rotate(0deg); } 27 | to { transform: rotate(360deg); } 28 | } 29 | -------------------------------------------------------------------------------- /app/renderer/components/Home/home.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | animation: App-logo-spin infinite 20s linear; 7 | height: 80px; 8 | } 9 | 10 | .App-header { 11 | background-color: #222; 12 | height: 150px; 13 | padding: 20px; 14 | color: white; 15 | } 16 | 17 | .App-title { 18 | font-size: 1.5em; 19 | } 20 | 21 | .App-intro { 22 | font-size: large; 23 | } 24 | 25 | @keyframes App-logo-spin { 26 | from { transform: rotate(0deg); } 27 | to { transform: rotate(360deg); } 28 | } 29 | -------------------------------------------------------------------------------- /app/renderer/routes/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import { Route, Switch } from 'react-router'; 3 | import Home from '../components/Home'; 4 | import Counter from '../containers/counter'; 5 | import NavBar from '../components/NavBar'; 6 | 7 | export default function Routes() { 8 | return ( 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /app/renderer/components/Counter/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | interface CounterProps { 4 | count: number; 5 | increment: () => void; 6 | decrement: () => void; 7 | } 8 | 9 | export default class Counter extends Component { 10 | render() { 11 | const { count, increment, decrement } = this.props; 12 | 13 | return ( 14 |
15 |
Counter: {count}
16 | 17 | 18 |
19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/renderer/components/Home/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import logo from './logo.svg'; 3 | import './home.css'; 4 | export default class Home extends Component { 5 | render() { 6 | return ( 7 |
8 |
9 | logo 10 |

Welcome to React

11 |
12 |

13 | To get started, edit renderer/containers/App.tsx and save to reload. 14 |

15 |
16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/renderer/containers/App.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | import {configureStore, history} from '../store'; 5 | import {ConnectedRouter} from 'connected-react-router'; 6 | import {Provider} from 'react-redux'; 7 | 8 | it('renders without crashing', () => { 9 | const div = document.createElement('div'); 10 | const store = configureStore(); 11 | ReactDOM.render( 12 | 13 | 14 | 15 | , div); 16 | ReactDOM.unmountComponentAtNode(div); 17 | }); 18 | -------------------------------------------------------------------------------- /app/renderer/containers/counter.tsx: -------------------------------------------------------------------------------- 1 | import { bindActionCreators, Dispatch } from 'redux'; 2 | import { connect } from 'react-redux'; 3 | import { StoreState } from '../types'; 4 | import Counter from '../components/Counter'; 5 | import { counterActions, increment, decrement } from '../actions/counter'; 6 | 7 | function mapStateToProps(state: StoreState) { 8 | return { 9 | count: state.counter.count 10 | }; 11 | } 12 | 13 | function mapDispatchToProps(dispatch: Dispatch) { 14 | return bindActionCreators({increment, decrement}, dispatch); 15 | } 16 | 17 | export default connect(mapStateToProps, mapDispatchToProps)(Counter); 18 | -------------------------------------------------------------------------------- /app/renderer/reducers/counter.ts: -------------------------------------------------------------------------------- 1 | import { INCREMENT, DECREMENT } from '../actions/counter/action_type'; 2 | import { counterActions } from '../actions/counter'; 3 | import { CounterState } from '../types'; 4 | 5 | const INITIAL_STATE = { 6 | count: 1 7 | }; 8 | 9 | export default function counter(state = INITIAL_STATE, action: counterActions): CounterState { 10 | switch (action.type) { 11 | case INCREMENT: 12 | return { 13 | count: state.count + 1 14 | }; 15 | 16 | case DECREMENT: 17 | return { 18 | count: state.count - 1 19 | }; 20 | 21 | default: 22 | return state; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/main/windows/mainWindow.ts: -------------------------------------------------------------------------------- 1 | import { BrowserWindow } from 'electron'; 2 | 3 | export default class MainWindow { 4 | mainWindow: BrowserWindow; 5 | constructor() { 6 | this.createMainWindow(); 7 | } 8 | 9 | createMainWindow() { 10 | this.mainWindow = new BrowserWindow({ 11 | height: 600, 12 | width: 800, 13 | show: false 14 | }); 15 | } 16 | 17 | loadURL(url: string) { 18 | this.mainWindow.loadURL(url); 19 | // this.mainWindow.webContents.openDevTools(); 20 | } 21 | 22 | openDevTools() { 23 | this.mainWindow.webContents.openDevTools(); 24 | } 25 | 26 | show() { 27 | this.mainWindow.show(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/renderer/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import { ConnectedRouter } from 'connected-react-router'; 5 | import { configureStore, history } from './store'; 6 | import App from './containers/App'; 7 | import './index.css'; 8 | import registerServiceWorker from './registerServiceWorker'; 9 | 10 | const store = configureStore(); 11 | ReactDOM.render( 12 | 13 | 14 | 15 | 16 | , 17 | document.getElementById('root') as HTMLElement 18 | ); 19 | registerServiceWorker(); 20 | -------------------------------------------------------------------------------- /app/main/tray.ts: -------------------------------------------------------------------------------- 1 | import { Tray, Menu } from 'electron'; 2 | 3 | const contextMenu = Menu.buildFromTemplate([ 4 | { label: 'Item1', type: 'radio' }, 5 | { label: 'Item2', type: 'radio' }, 6 | { label: 'Item3', type: 'radio', checked: true }, 7 | { label: 'Item4', type: 'radio' } 8 | ]); 9 | 10 | export default class TrayCreator { 11 | trayInstance: Tray; 12 | path: string; 13 | constructor(iconPath: string) { 14 | this.path = iconPath; 15 | } 16 | 17 | initTray() { 18 | this.trayInstance = new Tray(this.path); 19 | this.setContextMenu(contextMenu); 20 | } 21 | 22 | setContextMenu(menu: Menu) { 23 | this.trayInstance.setContextMenu(menu); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/main/menu.ts: -------------------------------------------------------------------------------- 1 | import { Menu, MenuItemConstructorOptions } from 'electron'; 2 | 3 | const mainMenuTemplate: MenuItemConstructorOptions[] = [ 4 | { 5 | label: 'Edit', 6 | submenu: [ 7 | { label: 'Undo', accelerator: 'CmdOrCtrl+Z', role: 'undo' }, 8 | { label: 'Redo', accelerator: 'Shift+CmdOrCtrl+Z', role: 'redo' }, 9 | { type: 'separator' }, 10 | { label: 'Cut', accelerator: 'CmdOrCtrl+X', role: 'cut' }, 11 | { label: 'Copy', accelerator: 'CmdOrCtrl+C', role: 'copy' }, 12 | { label: 'Paste', accelerator: 'CmdOrCtrl+V', role: 'paste' }, 13 | { label: 'Select All', accelerator: 'CmdOrCtrl+A', role: 'selectall' }, 14 | ]} 15 | ]; 16 | 17 | const mainMenu = Menu.buildFromTemplate(mainMenuTemplate); 18 | 19 | export { mainMenu }; 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Electron-TypeScript-React 2 | 3 | ### Create An Electron App with TypeScript, React, Redux, React-Router 4 | 5 | This project was bootstrapped with [Create React App](https://github.com/facebookincubator/create-react-app). 6 | 7 | ## To Use 8 | 9 | ```bash 10 | # Clone this repository 11 | git clone https://github.com/Leskd/electron-react-typescript.git 12 | # Go into the repository 13 | cd 14 | # Install dependencies 15 | npm install 16 | # Run the app 17 | npm start 18 | ``` 19 | 20 | ## Resources for Learning 21 | 22 | - [electronjs.org/docs](https://electronjs.org/docs) - all of Electron's documentation 23 | - [TypeScript](https://www.typescriptlang.org/) 24 | - [React](https://reactjs.org/) 25 | - [Redux](https://redux.js.org/) 26 | 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "outDir": "build/dist", 5 | "module": "esnext", 6 | "target": "es5", 7 | "lib": ["es6", "dom"], 8 | "sourceMap": true, 9 | "allowJs": true, 10 | "jsx": "react", 11 | "moduleResolution": "node", 12 | "rootDir": "app", 13 | "forceConsistentCasingInFileNames": true, 14 | "noImplicitReturns": true, 15 | "noImplicitThis": true, 16 | "noImplicitAny": true, 17 | "strictNullChecks": true, 18 | "suppressImplicitAnyIndexErrors": true, 19 | "noUnusedLocals": true, 20 | "allowSyntheticDefaultImports": true, 21 | "paths": { 22 | 23 | } 24 | }, 25 | "exclude": [ 26 | "node_modules", 27 | "build", 28 | "scripts", 29 | "acceptance-tests", 30 | "webpack", 31 | "jest", 32 | "src/setupTests.ts" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /app/renderer/store/index.ts: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | import logger from 'redux-logger'; 4 | import * as History from 'history'; 5 | import { connectRouter, routerMiddleware } from 'connected-react-router'; 6 | import rootReducer from '../reducers'; 7 | 8 | const isDev = process.env.NODE_ENV === 'development' ? true : false; 9 | 10 | export const history = History.createHashHistory(); 11 | 12 | const router = routerMiddleware(history); 13 | let middleArgs = []; 14 | 15 | if (isDev) { 16 | middleArgs = [ thunk, router, logger ]; 17 | } else { 18 | middleArgs = [ thunk, router ]; 19 | } 20 | 21 | const enhancer = compose(applyMiddleware(...middleArgs)); 22 | 23 | export function configureStore(initialState: {} = {}) { 24 | return createStore(connectRouter(history)(rootReducer), initialState, enhancer); 25 | } 26 | -------------------------------------------------------------------------------- /scripts/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'test'; 5 | process.env.NODE_ENV = 'test'; 6 | process.env.PUBLIC_URL = ''; 7 | 8 | // Makes the script crash on unhandled rejections instead of silently 9 | // ignoring them. In the future, promise rejections that are not handled will 10 | // terminate the Node.js process with a non-zero exit code. 11 | process.on('unhandledRejection', err => { 12 | throw err; 13 | }); 14 | 15 | // Ensure environment variables are read. 16 | require('../config/env'); 17 | 18 | const jest = require('jest'); 19 | let argv = process.argv.slice(2); 20 | 21 | // Watch unless on CI, in coverage mode, or explicitly running all tests 22 | if ( 23 | !process.env.CI && 24 | argv.indexOf('--coverage') === -1 && 25 | argv.indexOf('--watchAll') === -1 26 | ) { 27 | argv.push('--watch'); 28 | } 29 | 30 | 31 | jest.run(argv); 32 | -------------------------------------------------------------------------------- /config/polyfills.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | if (typeof Promise === 'undefined') { 4 | // Rejection tracking prevents a common issue where React gets into an 5 | // inconsistent state due to an error, but it gets swallowed by a Promise, 6 | // and the user has no idea what causes React's erratic future behavior. 7 | require('promise/lib/rejection-tracking').enable(); 8 | window.Promise = require('promise/lib/es6-extensions.js'); 9 | } 10 | 11 | // fetch() polyfill for making API calls. 12 | require('whatwg-fetch'); 13 | 14 | // Object.assign() is commonly used with React. 15 | // It will use the native implementation if it's present and isn't buggy. 16 | Object.assign = require('object-assign'); 17 | 18 | // In tests, polyfill requestAnimationFrame since jsdom doesn't provide it yet. 19 | // We don't polyfill it in the browser--this is user's responsibility. 20 | if (process.env.NODE_ENV === 'test') { 21 | require('raf').polyfill(global); 22 | } 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | React App 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /app/main/main.ts: -------------------------------------------------------------------------------- 1 | import { app, Menu } from 'electron'; 2 | import path from 'path'; 3 | import url from 'url'; 4 | import MainWindow from './windows/mainWindow'; 5 | import { mainMenu } from './menu'; 6 | import TrayCreator from './tray'; 7 | 8 | const isDev = process.env.NODE_ENV === 'development'; 9 | const port = parseInt(process.env.PORT!, 10) || 3000; 10 | const devUrl = `http://localhost:${port}/`; 11 | 12 | // https://github.com/jarek-foksa/xel/issues/23 13 | const prodUrl = url.format({ 14 | pathname: path.resolve(__dirname, 'build/index.html'), 15 | protocol: 'file:', 16 | slashes: true 17 | }); 18 | const indexUrl = isDev ? devUrl : prodUrl; 19 | 20 | class Electron { 21 | mainWindowInstance: MainWindow; 22 | 23 | init() { 24 | this.initApp(); 25 | } 26 | 27 | initApp() { 28 | app.on('ready', async () => { 29 | this.createMainWindow(); 30 | this.mainWindowInstance.loadURL(indexUrl); 31 | Menu.setApplicationMenu(mainMenu); 32 | const appIconPath = path.join(__dirname, './assets/electron.png'); 33 | const tray = new TrayCreator(appIconPath); 34 | tray.initTray(); 35 | if (isDev) { 36 | this.mainWindowInstance.openDevTools(); 37 | await this.installExtensions(); 38 | } 39 | }); 40 | app.on('window-all-closed', () => { 41 | app.quit(); 42 | }); 43 | } 44 | 45 | createMainWindow() { 46 | this.mainWindowInstance = new MainWindow(); 47 | this.mainWindowInstance.show(); 48 | } 49 | 50 | installExtensions = async () => { 51 | const { default: installer, REACT_DEVELOPER_TOOLS } = require('electron-devtools-installer'); 52 | 53 | installer([REACT_DEVELOPER_TOOLS]) 54 | .then((name: string) => console.log(`Added Extension: ${name}`)) 55 | .catch((err: Error) => console.log('An error occurred: ', err)); 56 | } 57 | } 58 | 59 | new Electron().init(); 60 | -------------------------------------------------------------------------------- /config/webpack.config.electron.js: -------------------------------------------------------------------------------- 1 | 2 | const webpack = require('webpack'); 3 | const path = require('path'); 4 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); 5 | const paths = require('./paths'); 6 | 7 | module.exports = { 8 | entry: paths.appEntryJs, 9 | output: { 10 | path: path.resolve(__dirname, '../'), 11 | filename: 'main.js' 12 | }, 13 | resolve: { 14 | extensions: ['.js', '.ts', '.json'] 15 | }, 16 | module: { 17 | rules: [ 18 | { 19 | test: /\.tsx?$/, 20 | include: paths.appMain, 21 | use: [ 22 | { 23 | loader: require.resolve('ts-loader'), 24 | options: { 25 | // disable type checker - we will use it in fork plugin 26 | transpileOnly: true, 27 | }, 28 | }, 29 | ], 30 | } 31 | ] 32 | }, 33 | plugins: [ 34 | new webpack.DefinePlugin({ 35 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV) || 'production' 36 | }), 37 | new UglifyJsPlugin({ 38 | uglifyOptions: { 39 | parse: { 40 | // we want uglify-js to parse ecma 8 code. However we want it to output 41 | // ecma 5 compliant code, to avoid issues with older browsers, this is 42 | // whey we put `ecma: 5` to the compress and output section 43 | // https://github.com/facebook/create-react-app/pull/4234 44 | ecma: 8, 45 | }, 46 | compress: { 47 | ecma: 5, 48 | warnings: false, 49 | // Disabled because of an issue with Uglify breaking seemingly valid code: 50 | // https://github.com/facebook/create-react-app/issues/2376 51 | // Pending further investigation: 52 | // https://github.com/mishoo/UglifyJS2/issues/2011 53 | comparisons: false, 54 | }, 55 | mangle: { 56 | safari10: true, 57 | }, 58 | output: { 59 | ecma: 5, 60 | comments: false, 61 | // Turned on because emoji and regex is not minified properly using default 62 | // https://github.com/facebook/create-react-app/issues/2488 63 | ascii_only: true, 64 | }, 65 | } 66 | }) 67 | ], 68 | node: { 69 | __dirname: false, 70 | __filename: false 71 | }, 72 | target: 'electron-main' 73 | } 74 | -------------------------------------------------------------------------------- /config/paths.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const url = require('url'); 6 | 7 | // Make sure any symlinks in the project folder are resolved: 8 | // https://github.com/facebookincubator/create-react-app/issues/637 9 | const appDirectory = fs.realpathSync(process.cwd()); 10 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath); 11 | 12 | const envPublicUrl = process.env.PUBLIC_URL; 13 | 14 | function ensureSlash(path, needsSlash) { 15 | const hasSlash = path.endsWith('/'); 16 | if (hasSlash && !needsSlash) { 17 | return path.substr(path, path.length - 1); 18 | } else if (!hasSlash && needsSlash) { 19 | return `${path}/`; 20 | } else { 21 | return path; 22 | } 23 | } 24 | 25 | const getPublicUrl = appPackageJson => 26 | envPublicUrl || require(appPackageJson).homepage; 27 | 28 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer 29 | // "public path" at which the app is served. 30 | // Webpack needs to know it to put the right