Add 'New update available' dialog to the electron version (#299)

* build(tsconfig): Rename root tsconfig.json -> tsconfig.base.json

* feat(auto-update): Add update dialog

When new update available than new message will visible of the top of the screen with 2 buttons 'Update' and 'Close'.
- Update button: Update the application (close and restart)
- Close button: Hide the updatePanel

* fix(auto-update): Add types to the event methods

* style: Add comma after SafeStylePipe import

I forgot add the comma when I rebased the branch

* fix(auto-update): Use electron-is-dev package to detect dev build

I removed the isDev() function from the shared util library because it is electron specific code.

* ci: Change osx_image: xcode8.3

Recommended after the last travis upgrade

* feat(auto-update): Add auto update settings page and save config save on electron platform

* ci: Fix osx image

* ci: Upgrade the electron builder -> 19.6.1

The builder now use the 2 package.json structure and build only
the necessary dependencies.
This commit is contained in:
Róbert Kiss
2017-06-22 14:22:54 +02:00
committed by László Monda
parent 2598109f8c
commit 121807a65a
49 changed files with 1028 additions and 129 deletions

View File

@@ -67,6 +67,7 @@ import { SvgKeyboardWrapComponent } from './shared/components/svg/wrap';
import { appRoutingProviders, routing } from './app/app.routes';
import { AppComponent } from './app/app.component';
import { MainAppComponent } from './main-app';
import { UpdateAvailableComponent } from './components/update-available/update-available.component';
import { CancelableDirective } from './shared/directives';
import { SafeStylePipe } from './shared/pipes';
@@ -76,7 +77,8 @@ import { MapperService } from './shared/services/mapper.service';
import { SvgModuleProviderService } from './shared/services/svg-module-provider.service';
import { UhkDeviceService } from './services/uhk-device.service';
import { KeymapEffects, MacroEffects, UserConfigEffects} from './shared/store/effects';
import { AutoUpdateSettingsEffects, KeymapEffects, MacroEffects, UserConfigEffects } from './shared/store/effects';
import { ApplicationEffect, AppUpdateEffect } from './store/effects';
import { KeymapEditGuard } from './shared/components/keymap/edit';
import { MacroNotFoundGuard } from './shared/components/macro/not-found';
@@ -88,7 +90,9 @@ import { UhkDeviceUninitializedGuard } from './services/uhk-device-uninitialized
import { DATA_STORAGE_REPOSITORY } from './shared/services/datastorage-repository.service';
import { ElectronDataStorageRepositoryService } from './services/electron-datastorage-repository.service';
import { DefaultUserConfigurationService } from './shared/services/default-user-configuration.service';
import { reducer } from '../../shared/src/store/reducers/index';
import { AppUpdateRendererService } from './services/app-update-renderer.service';
import { reducer } from './store';
import { AutoUpdateSettings } from './shared/components/auto-update-settings/auto-update-settings';
@NgModule({
declarations: [
@@ -143,7 +147,9 @@ import { reducer } from '../../shared/src/store/reducers/index';
PrivilegeCheckerComponent,
UhkMessageComponent,
CancelableDirective,
SafeStylePipe
SafeStylePipe,
UpdateAvailableComponent,
AutoUpdateSettings
],
imports: [
BrowserModule,
@@ -163,7 +169,10 @@ import { reducer } from '../../shared/src/store/reducers/index';
Select2Module,
EffectsModule.runAfterBootstrap(KeymapEffects),
EffectsModule.runAfterBootstrap(MacroEffects),
EffectsModule.runAfterBootstrap(UserConfigEffects)
EffectsModule.runAfterBootstrap(UserConfigEffects),
EffectsModule.runAfterBootstrap(AutoUpdateSettingsEffects),
EffectsModule.run(ApplicationEffect),
EffectsModule.run(AppUpdateEffect)
],
providers: [
UhkDeviceConnectedGuard,
@@ -177,9 +186,11 @@ import { reducer } from '../../shared/src/store/reducers/index';
MacroNotFoundGuard,
CaptureService,
UhkDeviceService,
{provide: DATA_STORAGE_REPOSITORY, useClass: ElectronDataStorageRepositoryService},
DefaultUserConfigurationService
{ provide: DATA_STORAGE_REPOSITORY, useClass: ElectronDataStorageRepositoryService },
DefaultUserConfigurationService,
AppUpdateRendererService
],
bootstrap: [AppComponent]
})
export class AppModule { }
export class AppModule {
}

View File

@@ -1 +1,7 @@
<app-update-available
[showUpdateAvailable]="showUpdateAvailable$ | async"
(updateApp)="updateApp()"
(doNotUpdateApp)="doNotUpdateApp()">
</app-update-available>
<router-outlet></router-outlet>

View File

@@ -1,4 +1,9 @@
import { Component, ViewEncapsulation } from '@angular/core';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs/Rx';
import { AppState, getShowAppUpdateAvailable } from '../store';
import { DoNotUpdateAppAction, UpdateAppAction } from '../store/actions/app-update.action';
@Component({
selector: 'app',
@@ -6,4 +11,18 @@ import { Component, ViewEncapsulation } from '@angular/core';
styleUrls: ['app.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class AppComponent { }
export class AppComponent {
showUpdateAvailable$: Observable<boolean>;
constructor(private store: Store<AppState>) {
this.showUpdateAvailable$ = store.select(getShowAppUpdateAvailable);
}
updateApp() {
this.store.dispatch(new UpdateAppAction());
}
doNotUpdateApp() {
this.store.dispatch(new DoNotUpdateAppAction());
}
}

View File

@@ -1,3 +1,4 @@
/// <reference path="../../custom_types/sudo-prompt.d.ts"/>
import { Component } from '@angular/core';
import { Router } from '@angular/router';
@@ -13,7 +14,7 @@ import { remote } from 'electron';
import * as path from 'path';
import * as sudo from 'sudo-prompt';
import { UhkDeviceService } from './../../services/uhk-device.service';
import { UhkDeviceService } from '../../services/uhk-device.service';
@Component({
selector: 'privilege-checker',

View File

@@ -0,0 +1 @@
export { UpdateAvailableComponent } from './update-available.component';

View File

@@ -0,0 +1,5 @@
<div *ngIf="showUpdateAvailable">
New version available.
<button type="button" (click)="updateApp.emit()" class="btn btn-primary">Update</button>
<button type="button" (click)="doNotUpdateApp.emit()" class="btn btn-default">Close</button>
</div>

View File

@@ -0,0 +1,5 @@
:host {
display: flex;
justify-content: center;
margin: 0.5rem;
}

View File

@@ -0,0 +1,13 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
@Component({
selector: 'app-update-available',
templateUrl: './update-available.component.html',
styleUrls: ['./update-available.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class UpdateAvailableComponent {
@Input() showUpdateAvailable: boolean = false;
@Output() updateApp = new EventEmitter<null>();
@Output() doNotUpdateApp = new EventEmitter<null>();
}

View File

@@ -0,0 +1 @@
declare module 'electron-is-dev';

View File

@@ -0,0 +1,22 @@
/// <reference path="./custom_types/electron-is-dev.d.ts"/>
/*
* Install DevTool extensions when Electron is in development mode
*/
import { app } from 'electron';
import * as isDev from 'electron-is-dev';
if (isDev) {
app.once('ready', () => {
const { default: installExtension, REDUX_DEVTOOLS } = require('electron-devtools-installer');
installExtension(REDUX_DEVTOOLS)
.then((name: string) => console.log(`Added Extension: ${name}`))
.catch((err: any) => console.log('An error occurred: ', err));
require('electron-debug')({ showDevTools: true });
});
}

View File

@@ -1,10 +1,27 @@
import { BrowserWindow, app } from 'electron';
/// <reference path="./custom_types/electron-is-dev.d.ts"/>
import { app, BrowserWindow, ipcMain } from 'electron';
import { autoUpdater } from 'electron-updater';
import * as log from 'electron-log';
import * as path from 'path';
import { ProgressInfo } from 'electron-builder-http/out/ProgressCallbackTransform';
import { VersionInfo } from 'electron-builder-http/out/publishOptions';
import * as settings from 'electron-settings';
import * as isDev from 'electron-is-dev';
import { IpcEvents } from './shared/util';
import { ElectronDataStorageRepositoryService } from './services/electron-datastorage-repository.service';
// import './dev-extension';
// require('electron-debug')({ showDevTools: true, enabled: true });
// 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.
let win: Electron.BrowserWindow;
log.transports.file.level = 'debug';
autoUpdater.logger = log;
function createWindow() {
// Create the browser window.
win = new BrowserWindow({
@@ -35,6 +52,9 @@ function createWindow() {
// when you should delete the corresponding element.
win = null;
});
win.webContents.on('did-finish-load', () => {
});
}
// This method will be called when Electron has finished
@@ -51,6 +71,10 @@ app.on('window-all-closed', () => {
}
});
app.on('will-quit', () => {
saveFirtsRun();
});
app.on('activate', () => {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
@@ -61,3 +85,101 @@ app.on('activate', () => {
// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and require them here
// =========================================================================
// Auto update events
// =========================================================================
function checkForUpdate() {
if (isDev) {
const msg = 'Application update is not working in dev mode.';
log.info(msg);
sendIpcToWindow(IpcEvents.autoUpdater.checkForUpdateNotAvailable, msg);
return;
}
if (isFirstRun()) {
const msg = 'Application update is skipping at first run.';
log.info(msg);
sendIpcToWindow(IpcEvents.autoUpdater.checkForUpdateNotAvailable, msg);
return;
}
autoUpdater.allowPrerelease = allowPreRelease();
autoUpdater.checkForUpdates();
}
autoUpdater.on('checking-for-update', () => {
sendIpcToWindow(IpcEvents.autoUpdater.checkingForUpdate);
});
autoUpdater.on('update-available', (ev: any, info: VersionInfo) => {
autoUpdater.downloadUpdate();
sendIpcToWindow(IpcEvents.autoUpdater.updateAvailable, info);
});
autoUpdater.on('update-not-available', (ev: any, info: VersionInfo) => {
sendIpcToWindow(IpcEvents.autoUpdater.updateNotAvailable, info);
});
autoUpdater.on('error', (ev: any, err: Error) => {
sendIpcToWindow(IpcEvents.autoUpdater.autoUpdateError, err);
});
autoUpdater.on('download-progress', (progressObj: ProgressInfo) => {
sendIpcToWindow(IpcEvents.autoUpdater.autoUpdateDownloadProgress, progressObj);
});
autoUpdater.on('update-downloaded', (ev: any, info: VersionInfo) => {
sendIpcToWindow(IpcEvents.autoUpdater.autoUpdateDownloaded, info);
});
ipcMain.on(IpcEvents.autoUpdater.updateAndRestart, () => autoUpdater.quitAndInstall(true));
ipcMain.on(IpcEvents.app.appStarted, () => {
if (checkForUpdateAtStartup()) {
checkForUpdate();
}
});
ipcMain.on(IpcEvents.autoUpdater.checkForUpdate, () => checkForUpdate());
function isFirstRun() {
if (!settings.has('firstRunVersion')) {
return true;
}
const firstRunVersion = settings.get('firstRunVersion');
log.info(`firstRunVersion: ${firstRunVersion}`);
log.info(`package.version: ${app.getVersion()}`);
return firstRunVersion !== app.getVersion();
}
function saveFirtsRun() {
settings.set('firstRunVersion', app.getVersion());
}
function sendIpcToWindow(message: string, arg?: any) {
log.info('sendIpcToWindow:', message, arg);
if (!win || win.isDestroyed()) {
return;
}
win.webContents.send(message, arg);
}
function allowPreRelease() {
const settings = getAutoUpdateSettings();
return settings && settings.usePreReleaseUpdate;
}
function checkForUpdateAtStartup() {
const settings = getAutoUpdateSettings();
return settings && settings.checkForUpdateOnStartUp;
}
function getAutoUpdateSettings() {
const storageService = new ElectronDataStorageRepositoryService();
return storageService.getAutoUpdateSettings();
}

20
electron/src/package.json Normal file
View File

@@ -0,0 +1,20 @@
{
"name": "uhk-agent",
"main": "electron-main.js",
"version": "0.0.0",
"description": "Agent is the configuration application of the Ultimate Hacking Keyboard.",
"author": "Ultimate Gadget Laboratories",
"repository": {
"type": "git",
"url": "git@github.com:UltimateHackingKeyboard/agent.git"
},
"license": "GPL-3.0",
"engines": {
"node": ">=6.9.5 <7.0.0",
"npm": ">=3.10.7 <4.0.0"
},
"dependencies": {
"usb": "git+https://github.com/aktary/node-usb.git"
}
}

View File

@@ -0,0 +1,75 @@
import { Injectable, NgZone } from '@angular/core';
import { Action, Store } from '@ngrx/store';
import { ipcRenderer } from 'electron';
import { IpcEvents } from '../shared/util';
import { AppState } from '../store';
import { UpdateDownloadedAction } from '../store/actions/app-update.action';
import { CheckForUpdateFailedAction, CheckForUpdateSuccessAction } from '../shared/store/actions/auto-update-settings';
/**
* This service handle the application update events in the electron renderer process.
*
* The class contains parameters with 'any' type, because the relevant type definitions in
* import { ProgressInfo } from 'electron-builder-http/out/ProgressCallbackTransform';
* import { VersionInfo } from 'electron-builder-http/out/publishOptions';
* but, typescript allow import these if import 'electron-updater' too, but I i don't want to import
* the updater in renderer process.
*/
@Injectable()
export class AppUpdateRendererService {
constructor(private store: Store<AppState>,
private zone: NgZone) {
this.registerEvents();
}
sendAppStarted() {
ipcRenderer.send(IpcEvents.app.appStarted);
}
sendUpdateAndRestartApp() {
ipcRenderer.send(IpcEvents.autoUpdater.updateAndRestart);
}
checkForUpdate() {
ipcRenderer.send(IpcEvents.autoUpdater.checkForUpdate);
}
private registerEvents() {
ipcRenderer.on(IpcEvents.autoUpdater.updateAvailable, (event: string, arg: any) => {
this.writeUpdateState(IpcEvents.autoUpdater.updateAvailable, arg);
});
ipcRenderer.on(IpcEvents.autoUpdater.updateNotAvailable, () => {
this.writeUpdateState(IpcEvents.autoUpdater.updateNotAvailable);
this.dispachStoreAction(new CheckForUpdateSuccessAction('No update available'));
});
ipcRenderer.on(IpcEvents.autoUpdater.autoUpdateError, (event: string, arg: any) => {
this.writeUpdateState(IpcEvents.autoUpdater.autoUpdateError, arg);
this.dispachStoreAction(new CheckForUpdateFailedAction(arg));
});
ipcRenderer.on(IpcEvents.autoUpdater.autoUpdateDownloadProgress, (event: string, arg: any) => {
this.writeUpdateState(IpcEvents.autoUpdater.autoUpdateDownloadProgress, arg);
});
ipcRenderer.on(IpcEvents.autoUpdater.autoUpdateDownloaded, (event: string, arg: any) => {
this.writeUpdateState(IpcEvents.autoUpdater.autoUpdateDownloaded, arg);
this.dispachStoreAction(new UpdateDownloadedAction());
});
ipcRenderer.on(IpcEvents.autoUpdater.checkForUpdateNotAvailable, (event: string, arg: any) => {
this.writeUpdateState(IpcEvents.autoUpdater.checkForUpdateNotAvailable, arg);
this.dispachStoreAction(new CheckForUpdateFailedAction(arg));
});
}
private dispachStoreAction(action: Action) {
this.zone.run(() => this.store.dispatch(action));
}
private writeUpdateState(event: any, arg?: any) {
console.log({ event, arg });
}
}

View File

@@ -1,13 +1,36 @@
import { UserConfiguration } from '../shared/config-serializer/config-items/UserConfiguration';
import * as storage from 'electron-settings';
export class ElectronDataStorageRepositoryService {
getConfig(): UserConfiguration {
// TODO implement load logic
return;
import { UserConfiguration } from '../shared/config-serializer/config-items/UserConfiguration';
import { DataStorageRepositoryService } from '../shared/services/datastorage-repository.service';
import { AutoUpdateSettings } from '../shared/models/auto-update-settings';
export class ElectronDataStorageRepositoryService implements DataStorageRepositoryService {
static getValue(key: string): any {
const value = storage.get(key);
if (!value) {
return null;
}
return JSON.parse(<string>value);
}
static saveValue(key: string, value: any) {
storage.set(key, JSON.stringify(value));
}
getConfig(): UserConfiguration {
return ElectronDataStorageRepositoryService.getValue('user-config');
}
/* tslint:disable:no-unused-variable */
saveConfig(config: UserConfiguration): void {
// TODO implement save logic
ElectronDataStorageRepositoryService.saveValue('user-config', config.toJsonObject());
}
getAutoUpdateSettings(): AutoUpdateSettings {
return ElectronDataStorageRepositoryService.getValue('auto-update-settings');
}
saveAutoUpdateSettings(settings: AutoUpdateSettings): void {
ElectronDataStorageRepositoryService.saveValue('auto-update-settings', settings);
}
}

View File

@@ -0,0 +1,40 @@
import { Action } from '@ngrx/store';
import { type } from '../../shared/util/';
const PREFIX = '[app-update] ';
// tslint:disable-next-line:variable-name
export const ActionTypes = {
UPDATE_AVAILABLE: type(PREFIX + 'update available'),
UPDATE_APP: type(PREFIX + 'update app'),
DO_NOT_UPDATE_APP: type(PREFIX + 'do not update app'),
UPDATE_DOWNLOADED: type(PREFIX + 'update downloaded'),
UPDATING: type(PREFIX + 'updating')
};
export class UpdateAvailableAction implements Action {
type = ActionTypes.UPDATE_AVAILABLE;
}
export class UpdateAppAction implements Action {
type = ActionTypes.UPDATE_APP;
}
export class DoNotUpdateAppAction implements Action {
type = ActionTypes.DO_NOT_UPDATE_APP;
}
export class UpdateDownloadedAction implements Action {
type = ActionTypes.UPDATE_DOWNLOADED;
}
export class UpdatingAction implements Action {
type = ActionTypes.UPDATING;
}
export type Actions
= UpdateAvailableAction
| UpdateAppAction
| DoNotUpdateAppAction
| UpdateDownloadedAction
| UpdatingAction;

View File

@@ -0,0 +1,22 @@
import { Action } from '@ngrx/store';
import { type } from '../../shared/util/';
const PREFIX = '[app] ';
// tslint:disable-next-line:variable-name
export const ActionTypes = {
APP_BOOTSRAPPED: type(PREFIX + 'bootstrapped'),
APP_STARTED: type(PREFIX + 'started')
};
export class AppBootsrappedAction implements Action {
type = ActionTypes.APP_BOOTSRAPPED;
}
export class AppStartedAction implements Action {
type = ActionTypes.APP_STARTED;
}
export type Actions
= AppStartedAction
| AppBootsrappedAction;

View File

@@ -0,0 +1,32 @@
import { Injectable } from '@angular/core';
import { Action } from '@ngrx/store';
import { Actions, Effect } from '@ngrx/effects';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/do';
import 'rxjs/add/operator/first';
import { ActionTypes } from '../actions/app-update.action';
import { ActionTypes as AutoUpdateActionTypes } from '../../shared/store/actions/auto-update-settings';
import { AppUpdateRendererService } from '../../services/app-update-renderer.service';
@Injectable()
export class AppUpdateEffect {
@Effect({ dispatch: false })
appStart$: Observable<Action> = this.actions$
.ofType(ActionTypes.UPDATE_APP)
.first()
.do(() => {
this.appUpdateRendererService.sendUpdateAndRestartApp();
});
@Effect({ dispatch: false }) checkForUpdate$: Observable<Action> = this.actions$
.ofType(AutoUpdateActionTypes.CHECK_FOR_UPDATE_NOW)
.do(() => {
this.appUpdateRendererService.checkForUpdate();
});
constructor(private actions$: Actions,
private appUpdateRendererService: AppUpdateRendererService) {
}
}

View File

@@ -0,0 +1,24 @@
import { Injectable } from '@angular/core';
import { Action } from '@ngrx/store';
import { Effect, Actions } from '@ngrx/effects';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/do';
import * as app from '../actions/app.action';
import { AppUpdateRendererService } from '../../services/app-update-renderer.service';
@Injectable()
export class ApplicationEffect {
@Effect()
appStart$: Observable<Action> = this.actions$
.ofType(app.ActionTypes.APP_BOOTSRAPPED)
.startWith(new app.AppStartedAction())
.delay(3000) // wait 3 sec to mainRenderer subscribe all events
.do(() => {
this.appUpdateRendererService.sendAppStarted();
});
constructor(
private actions$: Actions,
private appUpdateRendererService: AppUpdateRendererService) { }
}

View File

@@ -0,0 +1,2 @@
export { AppUpdateEffect } from './app-update.effect';
export { ApplicationEffect } from './app.effect';

View File

@@ -0,0 +1,41 @@
/// <reference path="../custom_types/electron-is-dev.d.ts"/>
import { createSelector } from 'reselect';
import { compose } from '@ngrx/core/compose';
import { storeFreeze } from 'ngrx-store-freeze';
import { ActionReducer, combineReducers } from '@ngrx/store';
import { routerReducer } from '@ngrx/router-store';
import * as isDev from 'electron-is-dev';
import { AppState as CommonState } from '../shared/store';
import * as fromApp from './reducers/app.reducer';
import * as fromAppUpdate from './reducers/app-update.reducer';
import { autoUpdateReducer, presetReducer, userConfigurationReducer } from '../shared/store/reducers';
export interface AppState extends CommonState {
app: fromApp.State;
appUpdate: fromAppUpdate.State;
}
const reducers = {
userConfiguration: userConfigurationReducer,
presetKeymaps: presetReducer,
router: routerReducer,
app: fromApp.reducer,
appUpdate: fromAppUpdate.reducer,
autoUpdateSettings: autoUpdateReducer
};
const developmentReducer: ActionReducer<AppState> = compose(storeFreeze, combineReducers)(reducers);
const productionReducer: ActionReducer<AppState> = combineReducers(reducers);
export function reducer(state: any, action: any) {
if (isDev) {
return developmentReducer(state, action);
} else {
return productionReducer(state, action);
}
}
export const appUpdateState = (state: AppState) => state.appUpdate;
export const getShowAppUpdateAvailable = createSelector(appUpdateState, fromAppUpdate.getShowAppUpdateAvailable);

View File

@@ -0,0 +1,37 @@
import { Actions, ActionTypes } from '../actions/app-update.action';
export interface State {
updateAvailable: boolean;
updateDownloaded: boolean;
doNotUpdateApp: boolean;
}
const initialState: State = {
updateAvailable: false,
updateDownloaded: false,
doNotUpdateApp: false
};
export function reducer(state = initialState, action: Actions) {
switch (action.type) {
case ActionTypes.UPDATE_AVAILABLE: {
const newState = Object.assign({}, state);
newState.updateAvailable = true;
return newState;
}
case ActionTypes.UPDATE_DOWNLOADED: {
const newState = Object.assign({}, state);
newState.updateDownloaded = true;
return newState;
}
case ActionTypes.DO_NOT_UPDATE_APP: {
const newState = Object.assign({}, state);
newState.doNotUpdateApp = true;
return newState;
}
default:
return state;
}
}
export const getShowAppUpdateAvailable = (state: State) => state.updateDownloaded && !state.doNotUpdateApp;

View File

@@ -0,0 +1,21 @@
import { Actions, ActionTypes } from '../actions/app.action';
export interface State {
started: boolean;
}
const initialState: State = {
started: false
};
export function reducer(state = initialState, action: Actions) {
switch (action.type) {
case ActionTypes.APP_STARTED: {
const newState = Object.assign({}, state);
newState.started = true;
return newState;
}
default:
return state;
}
}

View File

@@ -1,5 +1,5 @@
{
"extends": "../../tsconfig.json",
"extends": "../../tsconfig.base.json",
"files": [
"electron-main.ts"
]

View File

@@ -1,7 +1,6 @@
{
"extends": "../../tsconfig.json",
"extends": "../../tsconfig.base.json",
"exclude": [
"../dist",
"electron-main.ts",
"webpack.config.*"
]

View File

@@ -80,6 +80,10 @@ module.exports = {
{
from: 'node_modules/usb',
to: 'vendor/usb'
},
{
from: 'electron/src/package.json',
to: 'package.json'
}
]
),