Electron App (#854)

* Basic webpack build started.

* Get build working with electron-packager. Not fully satisfied, might investigate electron-builder.

* Custom title bar

* Rewrite all webpack configs to use common function. Organize webpack utils. Split into multiple dist folders.

* Replace electron build with electron-builder. Leave around packager for a bit.

* Check in progress on updater.

* Update modal flow.

* Fix tscheck.

* Adjust publish info.

* Arbitrary version bump.

* Bump version again.

* 5.0.2 bump fix autodownload.

* 5.0.2 bump again, readd dmg

* 5.0.3 bump

* Turn auto update back off. Log errors. Revert versions.

* Add os-specific builds. Improve update failure.

* Open external links in browser in electron.

* Remove custom title bar temporarily.

* Add info about the update download to the modal.

* Turn off development changes.

* Take the postBuild sorting script and move it into a webpack config.

* Initial conversion to typescript and electron-webpack.

* Switch from electron-webpack back to custom config, clean up unused code, typify electron bridge.

* Better typing for bridge.

* Remove unnecessary file.

* Reminify.

* Add shared folder resolving to jest config.

* Add enum to electron events
This commit is contained in:
William O'Beirne 2018-01-22 18:38:06 -05:00 committed by Daniel Ternyak
parent 81beab8bc0
commit 182eaa4329
43 changed files with 1354 additions and 430 deletions

View File

@ -13,6 +13,7 @@ import BroadcastTx from 'containers/Tabs/BroadcastTx';
import ErrorScreen from 'components/ErrorScreen'; import ErrorScreen from 'components/ErrorScreen';
import PageNotFound from 'components/PageNotFound'; import PageNotFound from 'components/PageNotFound';
import LogOutPrompt from 'components/LogOutPrompt'; import LogOutPrompt from 'components/LogOutPrompt';
import { TitleBar } from 'components/ui';
import { Store } from 'redux'; import { Store } from 'redux';
import { pollOfflineStatus } from 'actions/config'; import { pollOfflineStatus } from 'actions/config';
import { AppState } from 'reducers'; import { AppState } from 'reducers';
@ -73,12 +74,16 @@ export default class Root extends Component<Props, State> {
</CaptureRouteNotFound> </CaptureRouteNotFound>
); );
const Router = process.env.BUILD_DOWNLOADABLE ? HashRouter : BrowserRouter; const Router =
process.env.BUILD_DOWNLOADABLE && process.env.NODE_ENV === 'production'
? HashRouter
: BrowserRouter;
return ( return (
<Provider store={store} key={Math.random()}> <Provider store={store} key={Math.random()}>
<Router key={Math.random()}> <Router key={Math.random()}>
<React.Fragment> <React.Fragment>
{process.env.BUILD_ELECTRON && <TitleBar />}
{routes} {routes}
<LegacyRoutes /> <LegacyRoutes />
<LogOutPrompt /> <LogOutPrompt />

View File

@ -0,0 +1,61 @@
@import 'common/sass/variables';
@keyframes new-update-popin {
0% {
opacity: 0;
transform: scale(0.7);
}
60% {
opacity: 1;
transform: scale(1.2);
}
100% {
opacity: 1;
transform: scale(1);
}
}
@keyframes new-update-glow {
0% {
opacity: 1;
transform: scale(1);
},
80%, 100% {
opacity: 0;
transform: scale(2.5);
}
}
.Version {
position: relative;
&-text {
&.has-update:hover {
cursor: pointer;
text-decoration: underline;
}
}
&-new {
position: absolute;
top: 0px;
left: -12px;
width: 8px;
height: 8px;
border-radius: 100%;
background: $brand-warning;
animation: new-update-popin 500ms ease 1;
&:before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 100%;
box-shadow: 0 0 3px $brand-warning;
animation: new-update-glow 1200ms ease infinite;
}
}
}

View File

@ -0,0 +1,48 @@
import React from 'react';
import { VERSION } from 'config/data';
import UpdateModal, { UpdateInfo } from 'components/UpdateModal';
import { addListener } from 'utils/electron';
import EVENTS from 'shared/electronEvents';
import './Version.scss';
interface State {
updateInfo: UpdateInfo | null;
isModalOpen: boolean;
}
export default class Version extends React.Component<{}, State> {
public state: State = {
updateInfo: null,
isModalOpen: false
};
public componentDidMount() {
addListener(EVENTS.UPDATE.UPDATE_AVAILABLE, updateInfo => {
this.setState({ updateInfo });
});
}
public render() {
const { updateInfo, isModalOpen } = this.state;
return (
<div className="Version">
<span className={`Version-text ${updateInfo ? 'has-update' : ''}`} onClick={this.openModal}>
v{VERSION}
</span>
{updateInfo && (
<span>
<span className="Version-new" />
<UpdateModal
isOpen={isModalOpen}
updateInfo={updateInfo}
handleClose={this.closeModal}
/>
</span>
)}
</div>
);
}
private openModal = () => this.setState({ isModalOpen: true });
private closeModal = () => this.setState({ isModalOpen: false });
}

View File

@ -16,7 +16,6 @@ import {
ANNOUNCEMENT_TYPE, ANNOUNCEMENT_TYPE,
languages, languages,
NODES, NODES,
VERSION,
NodeConfig, NodeConfig,
CustomNodeConfig, CustomNodeConfig,
CustomNetworkConfig CustomNetworkConfig
@ -25,6 +24,7 @@ import GasPriceDropdown from './components/GasPriceDropdown';
import Navigation from './components/Navigation'; import Navigation from './components/Navigation';
import CustomNodeModal from './components/CustomNodeModal'; import CustomNodeModal from './components/CustomNodeModal';
import OnlineStatus from './components/OnlineStatus'; import OnlineStatus from './components/OnlineStatus';
import Version from './components/Version';
import { getKeyByValue } from 'utils/helpers'; import { getKeyByValue } from 'utils/helpers';
import { makeCustomNodeId } from 'utils/node'; import { makeCustomNodeId } from 'utils/node';
import { getNetworkConfigFromId } from 'utils/network'; import { getNetworkConfigFromId } from 'utils/network';
@ -128,7 +128,9 @@ export default class Header extends Component<Props, State> {
/> />
</Link> </Link>
<div className="Header-branding-right"> <div className="Header-branding-right">
<span className="Header-branding-right-version hidden-xs">v{VERSION}</span> <span className="Header-branding-right-version hidden-xs">
<Version />
</span>
<div className="Header-branding-right-online"> <div className="Header-branding-right-online">
<OnlineStatus isOffline={isOffline} /> <OnlineStatus isOffline={isOffline} />

View File

@ -0,0 +1,62 @@
@import 'common/sass/variables';
.UpdateModal {
cursor: default;
@media (min-width: 680px) {
min-width: 680px;
}
&-title {
margin-top: 0;
margin-bottom: $space-xs;
}
&-date {
font-size: $font-size-small;
color: $gray;
padding-bottom: $space-sm;
border-bottom: 1px solid $gray-lighter;
}
&-downloader {
padding: 50px 30px 80px;
text-align: center;
&-bar {
position: relative;
margin: 0 20px;
height: 16px;
border-radius: 4px;
background: $gray-lighter;
margin-bottom: $space-sm;
&-inner {
position: absolute;
top: 0;
left: 0;
bottom: 0;
border-radius: 4px;
background: $brand-primary;
transition: width 100ms ease;
}
}
&-info {
color: $gray-light;
font-size: $font-size-xs;
&-bit {
&:after {
display: inline-block;
content: "";
padding: 0 $space-xs;
opacity: 0.8;
}
&:last-child:after {
display: none;
}
}
}
}
}

View File

@ -0,0 +1,142 @@
import React from 'react';
import { connect } from 'react-redux';
import moment from 'moment';
import { showNotification, TShowNotification } from 'actions/notifications';
import { Spinner, NewTabLink } from 'components/ui';
import Modal, { IButton } from 'components/ui/Modal';
import { addListener, sendEvent } from 'utils/electron';
import EVENTS from 'shared/electronEvents';
import { bytesToHuman } from 'utils/formatters';
import './UpdateModal.scss';
export interface UpdateInfo {
version: string;
sha512: string;
releaseDate: string;
releaseName: string;
releaseNotes: string;
}
export interface DownloadProgress {
bytesPerSecond: number;
percent: number;
transferred: number;
total: number;
}
interface Props {
isOpen: boolean;
updateInfo: UpdateInfo;
showNotification: TShowNotification;
handleClose(): void;
}
interface State {
isDownloading: boolean;
downloadProgress: DownloadProgress | null;
}
class UpdateModal extends React.Component<Props, State> {
public state: State = {
isDownloading: false,
downloadProgress: null
};
public componentDidMount() {
addListener(EVENTS.UPDATE.UPDATE_DOWNLOADED, () => {
sendEvent(EVENTS.UPDATE.QUIT_AND_INSTALL);
});
addListener(EVENTS.UPDATE.DOWNLOAD_PROGRESS, downloadProgress => {
this.setState({ downloadProgress });
});
addListener(EVENTS.UPDATE.ERROR, err => {
console.error('Update failed:', err);
this.setState({ isDownloading: false });
this.props.showNotification(
'danger',
<span>
Update could not be downloaded, please visit{' '}
<NewTabLink href="https://github.com/MyEtherWallet/MyEtherWallet/releases">
our github
</NewTabLink>{' '}
to download the latest release
</span>,
Infinity
);
});
}
public render() {
const { isOpen, updateInfo, handleClose } = this.props;
const { isDownloading, downloadProgress } = this.state;
const buttons: IButton[] | undefined = downloadProgress
? undefined
: [
{
text: <span>{isDownloading && <Spinner />} Download Update</span>,
type: 'primary',
onClick: this.downloadUpdate,
disabled: isDownloading
},
{
text: 'Close',
type: 'default',
onClick: handleClose
}
];
return (
<Modal
isOpen={isOpen}
title={`Update Information`}
handleClose={handleClose}
buttons={buttons}
>
<div className="UpdateModal">
{downloadProgress ? (
<div className="UpdateModal-downloader">
<h3 className="UpdateModal-downloader-title">Downloading...</h3>
<div className="UpdateModal-downloader-bar">
<div
className="UpdateModal-downloader-bar-inner"
style={{
width: `${downloadProgress.percent}%`
}}
/>
</div>
<div className="UpdateModal-downloader-info">
<span className="UpdateModal-downloader-info-bit">
Downloaded {downloadProgress.percent.toFixed(1)}%
</span>
<span className="UpdateModal-downloader-info-bit">
{bytesToHuman(downloadProgress.bytesPerSecond)}/s
</span>
<span className="UpdateModal-downloader-info-bit">
Total Size {bytesToHuman(downloadProgress.total)}
</span>
</div>
</div>
) : (
<div>
<h1 className="UpdateModal-title">{updateInfo.releaseName}</h1>
<div className="UpdateModal-date">{moment(updateInfo.releaseDate).format('LL')}</div>
<div
className="UpdateModal-content"
dangerouslySetInnerHTML={{
__html: updateInfo.releaseNotes
}}
/>
</div>
)}
</div>
</Modal>
);
}
private downloadUpdate = () => {
this.setState({ isDownloading: true });
sendEvent('UPDATE:download-update');
};
}
export default connect(undefined, { showNotification })(UpdateModal);

View File

@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import { openInBrowser } from 'utils/electron';
interface AAttributes { interface AAttributes {
charset?: string; charset?: string;
@ -35,10 +36,21 @@ interface NewTabLinkProps extends AAttributes {
children?: React.ReactElement<any> | string; children?: React.ReactElement<any> | string;
} }
const NewTabLink = ({ content, children, ...rest }: NewTabLinkProps) => ( export class NewTabLink extends React.Component<NewTabLinkProps> {
<a target="_blank" rel="noopener noreferrer" {...rest}> public render() {
{content || children} const { content, children, ...rest } = this.props;
</a> return (
); <a target="_blank" rel="noopener noreferrer" onClick={this.handleClick} {...rest}>
{content || children}
</a>
);
}
private handleClick(ev: React.MouseEvent<HTMLAnchorElement>) {
if (openInBrowser(ev.currentTarget.href)) {
ev.preventDefault();
}
}
}
export default NewTabLink; export default NewTabLink;

View File

@ -0,0 +1,27 @@
@import 'common/sass/variables';
$height: 22px;
// TODO - Implement styles for custom title bar on all platforms
.TitleBar,
.TitleBarPlaceholder {
display: none;
}
.TitleBar {
position: fixed;
top: 0;
left: 0;
right: 0;
height: $height;
line-height: $height;
-webkit-user-select: none;
-webkit-app-region: drag;
background: $body-bg;
z-index: $zindex-top;
box-shadow: 0 1px 1px rgba(#000, 0.08);
}
.TitleBarPlaceholder {
height: $height;
}

View File

@ -0,0 +1,11 @@
import React from 'react';
import './TitleBar.scss';
const TitleBar: React.SFC<{}> = () => (
<React.Fragment>
<div className="TitleBar" />
<div className="TitleBarPlaceholder" />
</React.Fragment>
);
export default TitleBar;

View File

@ -10,5 +10,6 @@ export { default as UnitDisplay } from './UnitDisplay';
export { default as Spinner } from './Spinner'; export { default as Spinner } from './Spinner';
export { default as SwapDropdown } from './SwapDropdown'; export { default as SwapDropdown } from './SwapDropdown';
export { default as Tooltip } from './Tooltip'; export { default as Tooltip } from './Tooltip';
export { default as TitleBar } from './TitleBar';
export * from './ConditionalInput'; export * from './ConditionalInput';
export * from './Expandable'; export * from './Expandable';

25
common/utils/electron.ts Normal file
View File

@ -0,0 +1,25 @@
// Handles integrations with Electron. Wherever possible, should stub out
// behavior with noop's if not in the Electron environment, to simplify code.
import { ElectronBridgeFunctions } from 'shared/electronBridge';
const bridge: ElectronBridgeFunctions | null = (window as any).electronBridge;
export const addListener: ElectronBridgeFunctions['addListener'] = (event, cb) => {
if (bridge && bridge.addListener) {
// @ts-ignore unused ev
bridge.addListener(event, (ev, data) => cb(data));
}
};
export const sendEvent: ElectronBridgeFunctions['sendEvent'] = (event, data) => {
if (bridge && bridge.sendEvent) {
bridge.sendEvent(event, data);
}
};
export const openInBrowser: ElectronBridgeFunctions['openInBrowser'] = url => {
if (bridge && bridge.openInBrowser) {
bridge.openInBrowser(url);
return true;
}
return false;
};

View File

@ -100,3 +100,12 @@ export function formatGasLimit(limit: Wei, transactionUnit: string = 'ether') {
export function formatMnemonic(phrase: string) { export function formatMnemonic(phrase: string) {
return phrase.replace(/(\r\n|\n|\r|\s+|,)/gm, ' ').trim(); return phrase.replace(/(\r\n|\n|\r|\s+|,)/gm, ' ').trim();
} }
export function bytesToHuman(bytes: number) {
if (bytes <= 0) {
return '0 b';
}
const sizes = ['b', 'kb', 'mb', 'gb', 'tb'];
const i = Math.round(Math.floor(Math.log(bytes) / Math.log(1024)));
return Math.round(bytes / Math.pow(1024, i)) + ' ' + sizes[i];
}

Binary file not shown.

BIN
electron-app/icons/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View File

@ -0,0 +1,86 @@
import { app, BrowserWindow, Menu } from 'electron';
import * as path from 'path';
import updater from './updater';
import MENU from './menu';
const isDevelopment = process.env.NODE_ENV !== 'production';
// Global reference to mainWindow
// Necessary to prevent window from being garbage collected
let mainWindow;
function createMainWindow() {
// Construct new BrowserWindow
const window = new BrowserWindow({
title: 'MyEtherWallet',
backgroundColor: '#fbfbfb',
width: 1220,
height: 800,
minWidth: 320,
minHeight: 400,
// TODO - Implement styles for custom title bar in components/ui/TitleBar.scss
// frame: false,
// titleBarStyle: 'hidden',
webPreferences: {
devTools: true,
nodeIntegration: false,
preload: path.resolve(__dirname, 'preload.js')
}
});
const url = isDevelopment
? `http://localhost:${process.env.HTTPS ? 3443 : 3000}`
: `file://${__dirname}/index.html`;
window.loadURL(url);
window.on('closed', () => {
mainWindow = null;
});
window.webContents.on('devtools-opened', () => {
window.focus();
setImmediate(() => {
window.focus();
});
});
if (isDevelopment) {
window.webContents.on('did-fail-load', () => {
setTimeout(() => {
if (window && window.webContents) {
window.webContents.reload();
}
}, 500);
});
}
Menu.setApplicationMenu(Menu.buildFromTemplate(MENU));
return window;
}
// Quit application when all windows are closed
app.on('window-all-closed', () => {
// On macOS it is common for applications to stay open
// until the user explicitly quits
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', () => {
// On macOS it is common to re-create a window
// even after all windows have been closed
if (mainWindow === null) {
mainWindow = createMainWindow();
updater(app, mainWindow);
}
});
// Create main BrowserWindow when electron is ready
app.on('ready', () => {
mainWindow = createMainWindow();
mainWindow.webContents.on('did-finish-load', () => {
updater(app, mainWindow);
});
});

81
electron-app/main/menu.ts Normal file
View File

@ -0,0 +1,81 @@
import { MenuItemConstructorOptions, shell } from 'electron';
const MENU: MenuItemConstructorOptions[] = [
{
label: 'Edit',
submenu: [
{ role: 'undo' },
{ role: 'redo' },
{ type: 'separator' },
{ role: 'cut' },
{ role: 'copy' },
{ role: 'paste' },
{ role: 'pasteandmatchstyle' },
{ role: 'delete' },
{ role: 'selectall' }
]
},
{
label: 'View',
submenu: [
{ role: 'reload' },
{ role: 'forcereload' },
{ type: 'separator' },
{ role: 'resetzoom' },
{ role: 'zoomin' },
{ role: 'zoomout' },
{ role: 'togglefullscreen' },
{ type: 'separator' },
{ role: 'toggledevtools' }
]
}
];
const HELP_MENU = {
role: 'help',
submenu: [
{
label: 'Help / FAQ',
click() {
shell.openExternal('https://myetherwallet.github.io/knowledge-base/');
}
},
{
label: 'Report a Bug',
click() {
shell.openExternal('https://github.com/MyEtherWallet/MyEtherWallet/issues/new');
}
}
]
};
if (process.platform === 'darwin') {
MENU.unshift({
label: 'MyEtherWallet',
submenu: [
{ role: 'about' },
{ type: 'separator' },
{ role: 'hide' },
{ role: 'hideothers' },
{ role: 'unhide' },
{ type: 'separator' },
{ role: 'quit' }
]
});
// Modified help menu
MENU.push({
...HELP_MENU,
submenu: [
...HELP_MENU.submenu,
{
label: 'Speech',
submenu: [{ role: 'startspeaking' }, { role: 'stopspeaking' }]
}
]
});
} else {
MENU.push(HELP_MENU);
}
export default MENU;

View File

@ -0,0 +1,19 @@
{
"version": "5.0.0",
"files": [
{
"url": "MyEtherWallet-5.0.0-mac.zip",
"sha512": "b95kHlKspcJTo3Bh5tYAZKOrpjncOGOpS2GVA+nRPw6sLy/90SR/alijbe96m+T2CB9ajxDbYMBfs+wknBHZ4g=="
},
{
"url": "MyEtherWallet-5.0.0.dmg",
"sha512": "p+tMwE0t2svyppZVK1pFXVLEspRsVPYJtGIp07ppRtQWzkz+krz+dyU9sn2wMdtfIVpSjAGWDpoS2nhvLbHb5A==",
"size": 49786443
}
],
"path": "MyEtherWallet-5.0.0-mac.zip",
"sha512": "b95kHlKspcJTo3Bh5tYAZKOrpjncOGOpS2GVA+nRPw6sLy/90SR/alijbe96m+T2CB9ajxDbYMBfs+wknBHZ4g==",
"releaseDate": "2018-01-14T20:45:04.007Z",
"releaseName": "v5.0.0 - Fake Test Release",
"releaseNotes": "<h2>Bug Fixes</h2> <ul> <li>Fixed crash in custom protocols. <a href=\"https://github.com/electron/electron/pull/11020\" class=\"issue-link js-issue-link\" data-error-text=\"Failed to load issue title\" data-id=\"271302062\" data-permission-text=\"Issue title is private\" data-url=\"https://github.com/electron/electron/issues/11020\">#11020</a></li> <li>Fixed webrtc crash. <a href=\"https://github.com/electron/libchromiumcontent/pull/393\" class=\"issue-link js-issue-link\" data-error-text=\"Failed to load issue title\" data-id=\"275548554\" data-permission-text=\"Issue title is private\" data-url=\"https://github.com/electron/libchromiumcontent/issues/393\">electron/libchromiumcontent#393</a></li> </ul> <h3>Linux</h3> <ul> <li>Fixed subpixel font rendering with freetype. <a href=\"https://github.com/electron/electron/issues/11402\" class=\"issue-link js-issue-link\" data-error-text=\"Failed to load issue title\" data-id=\"281001023\" data-permission-text=\"Issue title is private\" data-url=\"https://github.com/electron/electron/issues/11402\">#11402</a></li> </ul> <h3>macOS</h3> <ul> <li>Fixed rendering issues with Nvidia GPU on High Sierra. <a href=\"https://github.com/electron/electron/pull/10898\" class=\"issue-link js-issue-link\" data-error-text=\"Failed to load issue title\" data-id=\"268161077\" data-permission-text=\"Issue title is private\" data-url=\"https://github.com/electron/electron/issues/10898\">#10898</a></li> <li>Fixed incorrectly cropped TouchBar items. <a href=\"https://github.com/electron/electron/pull/11141\" class=\"issue-link js-issue-link\" data-error-text=\"Failed to load issue title\" data-id=\"274633253\" data-permission-text=\"Issue title is private\" data-url=\"https://github.com/electron/electron/issues/11141\">#11141</a></li> </ul>"
}

View File

@ -0,0 +1,97 @@
import { App, BrowserWindow, ipcMain } from 'electron';
import { autoUpdater } from 'electron-updater';
import EVENTS from '../../shared/electronEvents';
import TEST_RELEASE from './testrelease.json';
autoUpdater.autoDownload = false;
enum AutoUpdaterEvents {
CHECKING_FOR_UPDATE = 'checking-for-update',
UPDATE_NOT_AVAILABLE = 'update-not-available',
UPDATE_AVAILABLE = 'update-available',
DOWNLOAD_PROGRESS = 'download-progress',
UPDATE_DOWNLOADED = 'update-downloaded',
ERROR = 'error'
}
export default (app: App, window: BrowserWindow) => {
// Set to 'true' if you want to test update behavior. Requires a recompile.
const shouldMockUpdate = true && process.env.NODE_ENV !== 'production';
// Report update status
autoUpdater.on(AutoUpdaterEvents.CHECKING_FOR_UPDATE, () => {
window.webContents.send(EVENTS.UPDATE.CHECKING_FOR_UPDATE);
});
autoUpdater.on(AutoUpdaterEvents.UPDATE_NOT_AVAILABLE, () => {
window.webContents.send(EVENTS.UPDATE.UPDATE_NOT_AVAILABLE);
});
autoUpdater.on(AutoUpdaterEvents.UPDATE_AVAILABLE, info => {
window.webContents.send(EVENTS.UPDATE.UPDATE_AVAILABLE, info);
});
autoUpdater.on(AutoUpdaterEvents.DOWNLOAD_PROGRESS, progress => {
window.webContents.send(EVENTS.UPDATE.DOWNLOAD_PROGRESS, progress);
});
autoUpdater.on(AutoUpdaterEvents.UPDATE_DOWNLOADED, () => {
window.webContents.send(EVENTS.UPDATE.UPDATE_DOWNLOADED);
});
autoUpdater.on(AutoUpdaterEvents.ERROR, (err, msg) => {
console.error('Update failed with an error');
console.error(err);
window.webContents.send(EVENTS.UPDATE.ERROR, msg);
});
autoUpdater.checkForUpdatesAndNotify();
// Listen for restart request
ipcMain.on(EVENTS.UPDATE.DOWNLOAD_UPDATE, () => {
if (shouldMockUpdate) {
mockDownload(window);
} else {
autoUpdater.downloadUpdate();
}
});
ipcMain.on(EVENTS.UPDATE.QUIT_AND_INSTALL, () => {
if (shouldMockUpdate) {
app.quit();
} else {
autoUpdater.quitAndInstall();
}
});
// Simulate a test release
if (shouldMockUpdate) {
mockUpdateCheck(window);
}
};
// Mock functions for dev testing
function mockUpdateCheck(window: BrowserWindow) {
window.webContents.send(EVENTS.UPDATE.CHECKING_FOR_UPDATE);
setTimeout(() => {
window.webContents.send(EVENTS.UPDATE.UPDATE_AVAILABLE, TEST_RELEASE);
}, 3000);
}
function mockDownload(window: BrowserWindow) {
for (let i = 0; i < 101; i++) {
setTimeout(() => {
const total = 150000000;
window.webContents.send(EVENTS.UPDATE.DOWNLOAD_PROGRESS, {
bytesPerSecond: Math.round(Math.random() * 100000),
percent: i,
transferred: total / i,
total
});
if (i === 100) {
window.webContents.send(EVENTS.UPDATE.UPDATE_DOWNLOADED);
}
}, 50 * i);
}
}

19
electron-app/preload.ts Normal file
View File

@ -0,0 +1,19 @@
// Selectively expose node integration, since all node integrations are
// disabled by default for security purposes.
import { ipcRenderer, shell } from 'electron';
import { ElectronBridgeFunctions } from '../shared/electronBridge';
const win = window as any;
const functions: ElectronBridgeFunctions = {
addListener(event, cb) {
ipcRenderer.on(event, cb);
},
sendEvent(event, data) {
ipcRenderer.send(event, data);
},
openInBrowser(url) {
return shell.openExternal(url);
}
};
win.electronBridge = functions;

View File

@ -10,7 +10,8 @@
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$":
"<rootDir>/jest_config/__mocks__/fileMock.ts", "<rootDir>/jest_config/__mocks__/fileMock.ts",
"\\.(css|scss|less)$": "<rootDir>/jest_config/__mocks__/styleMock.ts", "\\.(css|scss|less)$": "<rootDir>/jest_config/__mocks__/styleMock.ts",
"\\.worker.ts":"<rootDir>/jest_config/__mocks__/workerMock.js" "\\.worker.ts":"<rootDir>/jest_config/__mocks__/workerMock.js",
"^shared(.*)$": "<rootDir>/shared$1"
}, },
"testPathIgnorePatterns": ["<rootDir>/common/config"], "testPathIgnorePatterns": ["<rootDir>/common/config"],
"setupFiles": [ "setupFiles": [

View File

@ -9,7 +9,8 @@
"moduleNameMapper": { "moduleNameMapper": {
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$":
"<rootDir>/jest_config/__mocks__/fileMock.ts", "<rootDir>/jest_config/__mocks__/fileMock.ts",
"\\.(css|scss|less)$": "<rootDir>/jest_config/__mocks__/styleMock.ts" "\\.(css|scss|less)$": "<rootDir>/jest_config/__mocks__/styleMock.ts",
"^shared(.*)$": "<rootDir>/shared$1"
}, },
"testPathIgnorePatterns": ["<rootDir>/common/config"], "testPathIgnorePatterns": ["<rootDir>/common/config"],
"setupFiles": [ "setupFiles": [

View File

@ -1,8 +1,10 @@
{ {
"name": "MyEtherWallet", "name": "MyEtherWallet",
"author": "MyEtherWallet",
"version": "4.0.0-alpha.1", "version": "4.0.0-alpha.1",
"main": "common/index.jsx", "main": "main.js",
"description": "MyEtherWallet v4", "description": "MyEtherWallet v4",
"repository": "https://github.com/wbobeirne/MyEtherWallet-electron",
"engines": { "engines": {
"node": ">= 8.0.0", "node": ">= 8.0.0",
"npm": ">= 5.0.0" "npm": ">= 5.0.0"
@ -13,6 +15,7 @@
"bn.js": "4.11.8", "bn.js": "4.11.8",
"bootstrap-sass": "3.3.7", "bootstrap-sass": "3.3.7",
"classnames": "2.2.5", "classnames": "2.2.5",
"electron-updater": "2.18.2",
"ethereum-blockies": "git+https://github.com/MyEtherWallet/blockies.git", "ethereum-blockies": "git+https://github.com/MyEtherWallet/blockies.git",
"ethereumjs-abi": "0.6.5", "ethereumjs-abi": "0.6.5",
"ethereumjs-tx": "1.3.3", "ethereumjs-tx": "1.3.3",
@ -74,8 +77,11 @@
"bs58": "4.0.1", "bs58": "4.0.1",
"cache-loader": "1.2.0", "cache-loader": "1.2.0",
"check-node-version": "3.2.0", "check-node-version": "3.2.0",
"concurrently": "3.5.1",
"copy-webpack-plugin": "4.3.1", "copy-webpack-plugin": "4.3.1",
"css-loader": "0.28.8", "css-loader": "0.28.8",
"electron": "1.7.10",
"electron-builder": "19.52.1",
"empty": "0.10.1", "empty": "0.10.1",
"enzyme": "3.3.0", "enzyme": "3.3.0",
"enzyme-adapter-react-16": "1.1.1", "enzyme-adapter-react-16": "1.1.1",
@ -131,21 +137,27 @@
"freezer": "webpack --config=./webpack_config/webpack.freezer.js && node ./dist/freezer.js", "freezer": "webpack --config=./webpack_config/webpack.freezer.js && node ./dist/freezer.js",
"freezer:validate": "npm run freezer -- --validate", "freezer:validate": "npm run freezer -- --validate",
"db": "nodemon ./db", "db": "nodemon ./db",
"build": "rimraf dist && webpack --config webpack_config/webpack.prod.js", "build": "webpack --config webpack_config/webpack.prod.js",
"prebuild": "check-node-version --package", "prebuild": "check-node-version --package",
"postbuild": "node ./utils/postBuild.js", "build:downloadable": "webpack --config webpack_config/webpack.html.js",
"build:downloadable": "BUILD_DOWNLOADABLE=true rimraf dist && webpack --config webpack_config/webpack.prod.js", "prebuild:downloadable": "check-node-version --package",
"prebuild:demo": "check-node-version --package", "build:electron": "webpack --config webpack_config/webpack.electron-prod.js && node webpack_config/buildElectron.js",
"build:electron:osx": "webpack --config webpack_config/webpack.electron-prod.js && ELECTRON_OS=osx node webpack_config/buildElectron.js",
"build:electron:windows": "webpack --config webpack_config/webpack.electron-prod.js && ELECTRON_OS=windows node webpack_config/buildElectron.js",
"build:electron:linux": "webpack --config webpack_config/webpack.electron-prod.js && ELECTRON_OS=linux node webpack_config/buildElectron.js",
"prebuild:electron": "check-node-version --package",
"test:coverage": "jest --config=jest_config/jest.config.json --coverage", "test:coverage": "jest --config=jest_config/jest.config.json --coverage",
"test": "jest --config=jest_config/jest.config.json", "test": "jest --config=jest_config/jest.config.json",
"test:unit": "jest --config=jest_config/jest.config.json --coverage", "test:unit": "jest --config=jest_config/jest.config.json --coverage",
"test:int": "jest --config=jest_config/jest.int.config.json --coverage", "test:int": "jest --config=jest_config/jest.int.config.json --coverage",
"updateSnapshot": "jest --config=jest_config/jest.config.json --updateSnapshot", "updateSnapshot": "jest --config=jest_config/jest.config.json --updateSnapshot",
"pretest": "check-node-version --package", "pretest": "check-node-version --package",
"dev": "node webpack_config/server.js", "dev": "node webpack_config/devServer.js",
"predev": "check-node-version --package", "predev": "check-node-version --package",
"dev:https": "HTTPS=true node webpack_config/server.js", "dev:https": "HTTPS=true node webpack_config/devServer.js",
"predev:https": "check-node-version --package", "predev:https": "check-node-version --package",
"dev:electron": "concurrently --kill-others --names 'webpack,electron' 'BUILD_ELECTRON=true node webpack_config/devServer.js' 'webpack --config webpack_config/webpack.electron-dev.js && electron dist/electron-js/main.js'",
"dev:electron:https": "concurrently --kill-others --names 'webpack,electron' 'BUILD_ELECTRON=true HTTPS=true node webpack_config/devServer.js' 'HTTPS=true webpack --config webpack_config/webpack.electron-dev.js && electron dist/electron-js/main.js'",
"tslint": "tslint --project . --exclude common/vendor/**/*", "tslint": "tslint --project . --exclude common/vendor/**/*",
"tscheck": "tsc --noEmit", "tscheck": "tsc --noEmit",
"start": "npm run dev", "start": "npm run dev",

9
shared/electronBridge.ts Normal file
View File

@ -0,0 +1,9 @@
// Provide typescript definitions / mappings between `electron-app/preload.ts`
// and 'common/utils/electron.ts'
export type ElectronBridgeCallback = (data?: any) => void;
export interface ElectronBridgeFunctions {
addListener(event: string, cb: ElectronBridgeCallback);
sendEvent(event: string, data?: any);
openInBrowser(url: string): boolean;
}

12
shared/electronEvents.ts Normal file
View File

@ -0,0 +1,12 @@
export default {
UPDATE: {
CHECKING_FOR_UPDATE: 'UPDATE:checking-for-update',
UPDATE_NOT_AVAILABLE: 'UPDATE:update-not-available',
UPDATE_AVAILABLE: 'UPDATE:update-available',
DOWNLOAD_PROGRESS: 'UPDATE:download-progress',
UPDATE_DOWNLOADED: 'UPDATE:update-downloaded',
ERROR: 'UPDATE:error',
DOWNLOAD_UPDATE: 'UPDATE:download-update',
QUIT_AND_INSTALL: 'UPDATE:quit-and-install'
}
};

View File

@ -8,6 +8,9 @@
"target": "es2015", "target": "es2015",
"allowJs": true, "allowJs": true,
"baseUrl": "./common/", "baseUrl": "./common/",
"paths": {
"shared*": ["../shared*"]
},
"lib": [ "lib": [
"es2017", "es2017",
"dom" "dom"
@ -20,6 +23,7 @@
}, },
"include": [ "include": [
"./common/", "./common/",
"./electron/",
"spec", "spec",
"./node_modules/types-rlp/index.d.ts" "./node_modules/types-rlp/index.d.ts"
], ],

View File

@ -1,46 +0,0 @@
/**
* (1) Parses the '.cache' file in the 'dist/icons' folder
* (2) Sorts the 'cache.result.files' property
* (3) Rewrites the file to ensure a deterministic build
*/
const fs = require('fs')
const path = require('path')
const klawSync = require('klaw-sync')
const DIST_PATH = path.resolve('./dist/')
const CACHE_FILE_REGEX = /.*icons-[a-z0-9]*\/\.cache$/
const findCacheFile = item => CACHE_FILE_REGEX.test(item.path)
console.log('postBuild start')
try {
const cacheFilePaths = klawSync(DIST_PATH, { filter: findCacheFile })
if (!cacheFilePaths.length) {
throw new Error('Could not find .cache file')
}
if (cacheFilePaths.length > 1) {
throw new Error('More than one possible .cache file detected')
}
const cacheFilePath = cacheFilePaths[0].path
const rawCacheFile = fs.readFileSync(cacheFilePath, 'utf8')
const cache = JSON.parse(rawCacheFile)
cache.result.files = cache.result.files.sort()
fs.writeFileSync(cacheFilePath, JSON.stringify(cache), 'utf8')
} catch(err) {
console.log('postBuild fail', err)
process.exit(1)
}
console.log('postBuild finish')

View File

@ -0,0 +1,68 @@
'use strict';
const path = require('path');
const fs = require('fs');
const rimraf = require('rimraf');
const builder = require('electron-builder');
const config = require('./config');
function shouldBuildOs(os) {
return !process.env.ELECTRON_OS || process.env.ELECTRON_OS === os;
}
async function build() {
console.log('Beginning Electron build process...');
const jsBuildDir = path.join(config.path.output, 'electron-js');
const electronBuildsDir = path.join(config.path.output, 'electron-builds');
const compression = 'store';
console.log('Clearing out old builds...');
rimraf.sync(electronBuildsDir);
// Builder requires package.json be in the app directory, so copy it in
fs.copyFileSync(
path.join(config.path.root, 'package.json'),
path.join(jsBuildDir, 'package.json')
);
console.log('Building...');
await builder.build({
mac: shouldBuildOs('mac') ? ['zip', 'dmg'] : undefined,
win: shouldBuildOs('windows') ? ['nsis'] : undefined,
linux: shouldBuildOs('linux') ? ['AppImage'] : undefined,
x64: true,
ia32: true,
config: {
appId: 'com.github.myetherwallet.myetherwallet',
productName: 'MyEtherWallet',
directories: {
app: jsBuildDir,
output: electronBuildsDir,
},
mac: {
category: 'public.app-category.finance',
icon: path.join(config.path.electron, 'icons/icon.icns'),
compression
},
win: {
icon: path.join(config.path.electron, 'icons/icon.ico'),
compression
},
linux: {
category: 'Finance',
compression
},
publish: {
provider: 'github',
owner: 'MyEtherWallet',
repo: 'MyEtherWallet',
vPrefixedTagName: false
},
// IMPORTANT: Prevents extending configs in node_modules
extends: null
}
});
console.info(`Electron builds are finished! Available at ${electronBuildsDir}`);
}
build();

View File

@ -1,13 +1,45 @@
'use strict'; 'use strict';
const path = require('path'); const path = require('path');
const paths = {
root: path.join(__dirname, '../'),
src: path.join(__dirname, '../common'),
output: path.join(__dirname, '../dist'),
assets: path.join(__dirname, '../common/assets'),
static: path.join(__dirname, '../static'),
electron: path.join(__dirname, '../electron-app'),
shared: path.join(__dirname, '../shared'),
modules: path.join(__dirname, '../node_modules'),
}
module.exports = { module.exports = {
// Configuration
port: process.env.HTTPS ? 3443 : 3000, port: process.env.HTTPS ? 3443 : 3000,
title: 'MEW', title: 'MEW',
publicPath: process.env.BUILD_DOWNLOADABLE ? './' : '/', path: paths,
srcPath: path.join(__dirname, './../common'),
// add these dependencies to a standalone vendor bundle // Typescript rule config
vendor: [ typescriptRule: {
test: /\.(ts|tsx)$/,
include: [paths.src, paths.shared, paths.electron],
use: [{ loader: 'ts-loader', options: { happyPackMode: true, logLevel: 'info' } }],
exclude: ['assets', 'sass', 'vendor', 'translations/lang']
.map(dir => path.resolve(paths.src, dir))
.concat([paths.modules])
},
// File resolution
resolve: {
extensions: ['.ts', '.tsx', '.js', '.css', '.json', '.scss', '.less'],
modules: [
paths.src,
paths.modules,
paths.root,
]
},
// Vendor modules
vendorModules: [
'bip39', 'bip39',
'bn.js', 'bn.js',
'classnames', 'classnames',

View File

@ -6,7 +6,7 @@ const https = require('https');
const fs = require('fs'); const fs = require('fs');
const webpackConfig = require('./webpack.dev'); const webpackConfig = require('./webpack.dev');
const config = require('./config'); const config = require('./config');
const LogPlugin = require('./log-plugin'); const LogPlugin = require('./plugins/serverLog');
const app = express(); const app = express();
@ -25,13 +25,13 @@ let compiler;
try { try {
compiler = webpack(webpackConfig); compiler = webpack(webpackConfig);
} catch (err) { } catch (err) {
console.log(err.message); console.error(err.message);
process.exit(1); process.exit(1);
} }
const devMiddleWare = require('webpack-dev-middleware')(compiler, { const devMiddleWare = require('webpack-dev-middleware')(compiler, {
publicPath: webpackConfig.output.publicPath, publicPath: webpackConfig.output.publicPath,
quiet: true, logLevel: 'warn',
inline: true, inline: true,
headers: { headers: {
'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Origin': '*',
@ -45,7 +45,7 @@ const devMiddleWare = require('webpack-dev-middleware')(compiler, {
app.use(devMiddleWare); app.use(devMiddleWare);
app.use( app.use(
require('webpack-hot-middleware')(compiler, { require('webpack-hot-middleware')(compiler, {
log: console.log log: console.info
}) })
); );

View File

@ -0,0 +1,321 @@
'use strict';
const path = require('path');
const webpack = require('webpack');
const threadLoader = require('thread-loader');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin');
const AutoDllPlugin = require('autodll-webpack-plugin');
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
const FaviconsWebpackPlugin = require('favicons-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const ProgressPlugin = require('webpack/lib/ProgressPlugin');
const BabelMinifyPlugin = require('babel-minify-webpack-plugin');
const SriPlugin = require('webpack-subresource-integrity');
const ClearDistPlugin = require('./plugins/clearDist');
const SortCachePlugin = require('./plugins/sortCache');
const config = require('./config');
const DEFAULT_OPTIONS = {
isProduction: false,
isElectronBuild: false,
isHTMLBuild: false,
outputDir: ''
};
module.exports = function(opts = {}) {
const options = Object.assign({}, DEFAULT_OPTIONS, opts);
const isDownloadable = options.isHTMLBuild || options.isElectronBuild;
// ====================
// ====== Entry =======
// ====================
const entry = {
client: './common/index.tsx'
};
if (options.isProduction) {
entry.vendor = config.vendorModules;
}
// ====================
// ====== Rules =======
// ====================
const rules = [];
// Typescript
if (options.isProduction || !process.env.SLOW_BUILD_SPEED) {
rules.push(config.typescriptRule);
}
else {
threadLoader.warmup(
config.typescriptRule.use[0].options,
[config.typescriptRule.use[0].loader]
);
rules.push({
...config.typescriptRule,
use: [{
loader: 'thread-loader',
options: {
workers: 4
}
}, ...config.typescriptRule.use],
});
}
// Styles (CSS, SCSS, LESS)
if (options.isProduction) {
rules.push({
test: /\.css$/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: 'css-loader'
})
}, {
test: /\.scss$/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: ['css-loader', 'sass-loader']
})
}, {
test: /\.less$/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: ['css-loader', 'less-loader']
})
});
} else {
rules.push({
test: /\.css$/,
include: path.resolve(config.path.src, 'vendor'),
use: ['style-loader', 'css-loader']
}, {
test: /\.scss$/,
include: ['components', 'containers', 'sass']
.map(dir => path.resolve(config.path.src, dir))
.concat([config.path.modules]),
exclude: /node_modules(?!\/font-awesome)/,
use: ['style-loader', 'css-loader', 'sass-loader']
}, {
test: /\.less$/,
include: path.resolve(config.path.assets, 'styles'),
use: ['style-loader', 'css-loader', 'less-loader']
});
}
// Web workers
rules.push({
test: /\.worker\.js$/,
loader: 'worker-loader'
});
// Images
rules.push({
include: [
path.resolve(config.path.assets),
path.resolve(config.path.modules)
],
exclude: /node_modules(?!\/font-awesome)/,
test: /\.(gif|png|jpe?g|svg)$/i,
use: [
{
loader: 'file-loader',
options: {
hash: 'sha512',
digest: 'hex',
name: '[path][name].[ext]?[hash:6]'
}
},
{
loader: 'image-webpack-loader',
options: {
bypassOnDebug: true,
optipng: {
optimizationLevel: 4
},
gifsicle: {
interlaced: false
},
mozjpeg: {
quality: 80
},
svgo: {
plugins: [{ removeViewBox: true }, { removeEmptyAttrs: false }, { sortAttrs: true }]
}
}
}
]
});
// Fonts
rules.push({
include: [
path.resolve(config.path.assets),
path.resolve(config.path.modules)
],
exclude: /node_modules(?!\/font-awesome)/,
test: /\.(ico|eot|otf|webp|ttf|woff|woff2)(\?.*)?$/,
loader: 'file-loader'
});
// ====================
// ====== Plugins =====
// ====================
const plugins = [
new HtmlWebpackPlugin({
title: config.title,
template: path.resolve(config.path.src, 'index.html'),
inject: true
}),
new CopyWebpackPlugin([
{
from: config.path.static,
// to the root of dist path
to: './'
}
]),
new webpack.LoaderOptionsPlugin({
minimize: options.isProduction,
debug: !options.isProduction,
options: {
// css-loader relies on context
context: process.cwd()
}
}),
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(options.isProduction ? 'production' : 'development'),
'process.env.BUILD_DOWNLOADABLE': JSON.stringify(isDownloadable),
'process.env.BUILD_HTML': JSON.stringify(options.isHTMLBuild),
'process.env.BUILD_ELECTRON': JSON.stringify(options.isElectronBuild)
}),
];
if (options.isProduction) {
plugins.push(
new BabelMinifyPlugin({
// Mangle seems to be reusing variable identifiers, causing errors
mangle: false,
// These two on top of a lodash file are causing illegal characters for
// safari and ios browsers
evaluate: false,
propertyLiterals: false,
}, {
comments: false
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
filename: 'vendor.[chunkhash:8].js'
}),
new ExtractTextPlugin('[name].[chunkhash:8].css'),
new FaviconsWebpackPlugin({
logo: path.resolve(config.path.static, 'favicon/android-chrome-384x384.png'),
background: '#163151',
inject: true
}),
new SriPlugin({
hashFuncNames: ['sha256', 'sha384'],
enabled: true
}),
new ProgressPlugin(),
new ClearDistPlugin(),
new SortCachePlugin()
)
}
else {
plugins.push(
new AutoDllPlugin({
inject: true, // will inject the DLL bundles to index.html
filename: '[name]_[hash].js',
debug: true,
context: path.join(config.path.root),
entry: {
vendor: [
...config.vendorModules,
'babel-polyfill',
'bootstrap-sass',
'font-awesome'
]
}
}),
new HardSourceWebpackPlugin({
environmentHash: {
root: process.cwd(),
directories: ['webpack_config'],
files: ['package.json']
}
}),
new webpack.HotModuleReplacementPlugin(),
new webpack.NoEmitOnErrorsPlugin(),
new FriendlyErrorsPlugin()
);
}
if (options.isElectronBuild) {
// target: 'electron-renderer' kills scrypt, so manually pull in some
// of its configuration instead
plugins.push(new webpack.ExternalsPlugin("commonjs", [
"desktop-capturer",
"electron",
"ipc",
"ipc-renderer",
"remote",
"web-frame",
"clipboard",
"crash-reporter",
"native-image",
"screen",
"shell"
]));
}
// ====================
// ====== DevTool =====
// ====================
let devtool = false;
if (!options.isProduction) {
if (process.env.SLOW_BUILD_SPEED) {
devtool = 'source-map';
}
else {
devtool = 'cheap-module-eval-source-map';
}
}
// ====================
// ====== Output ======
// ====================
const output = {
path: path.resolve(config.path.output, options.outputDir),
filename: options.isProduction ? '[name].[chunkhash:8].js' : '[name].js',
publicPath: isDownloadable && options.isProduction ? './' : '/',
crossOriginLoading: 'anonymous'
}
// The final bundle
return {
entry,
output,
module: { rules },
plugins,
target: 'web',
resolve: config.resolve,
performance: {
hints: options.isProduction ? 'warning' : false
},
stats: {
// Reduce build output
children: false,
chunks: false,
chunkModules: false,
chunkOrigins: false,
modules: false
}
};
}

View File

@ -0,0 +1,11 @@
'use strict';
const rimraf = require('rimraf');
function ClearDistPlugin() {};
ClearDistPlugin.prototype.apply = function(compiler) {
compiler.plugin('before-run', (params, done) => {
rimraf(params.outputPath, () => done());
});
};
module.exports = ClearDistPlugin;

View File

@ -0,0 +1,15 @@
'use strict';
const DelayPlugin = function(delayMs) {
this.delayMs = delayMs;
};
DelayPlugin.prototype.apply = function(compiler) {
compiler.plugin('before-run', (compiler, done) => {
setTimeout(() => {
done();
}, this.delayMs);
});
};
module.exports = DelayPlugin;

View File

@ -0,0 +1,32 @@
// Makes for a deterministic cache file by sorting it
'use strict';
const fs = require('fs');
const klawSync = require('klaw-sync');
const CACHE_FILE_REGEX = /.*icons-[a-z0-9]*\/\.cache$/;
const findCacheFile = item => CACHE_FILE_REGEX.test(item.path);
function SortCachePlugin() {};
SortCachePlugin.prototype.apply = function(compiler) {
compiler.plugin('done', (stats) => {
const buildDir = stats.compilation.compiler.outputPath;
const cacheFilePaths = klawSync(buildDir, { filter: findCacheFile });
if (!cacheFilePaths.length) {
throw new Error('Could not find .cache file');
}
if (cacheFilePaths.length > 1) {
throw new Error('More than one possible .cache file detected');
}
const cacheFilePath = cacheFilePaths[0].path;
const rawCacheFile = fs.readFileSync(cacheFilePath, 'utf8');
const cache = JSON.parse(rawCacheFile);
cache.result.files = cache.result.files.sort();
fs.writeFileSync(cacheFilePath, JSON.stringify(cache), 'utf8');
});
};
module.exports = SortCachePlugin;

View File

@ -1,28 +0,0 @@
'use strict';
const path = require('path');
const config = require('./config');
const _ = (module.exports = {});
_.cwd = file => {
return path.join(process.cwd(), file || '');
};
_.outputPath = path.join(__dirname, '../dist');
_.outputIndexPath = path.join(__dirname, '../dist/index.html');
_.target = 'web';
_.loadersOptions = () => {
const isProd = process.env.NODE_ENV === 'production';
return {
minimize: isProd,
debug: !isProd,
options: {
// css-loader relies on context
context: process.cwd()
}
};
};

View File

@ -1,115 +0,0 @@
'use strict';
const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const FaviconsWebpackPlugin = require('favicons-webpack-plugin');
const SriPlugin = require('webpack-subresource-integrity');
const config = require('./config');
const _ = require('./utils');
const webpackConfig = {
entry: {
client: './common/index.tsx'
},
output: {
path: _.outputPath,
filename: '[name].js',
publicPath: config.publicPath,
crossOriginLoading: "anonymous"
},
resolve: {
extensions: ['.ts', '.tsx', '.js', '.css', '.json', '.scss', '.less'],
modules: [
// places where to search for required modules
config.srcPath,
_.cwd('node_modules'),
_.cwd('./')
]
},
module: {
rules: [
{
test: /\.(ts|tsx)$/,
include: path.resolve(__dirname, '../common'),
use: [{ loader: 'ts-loader', options: { happyPackMode: true, logLevel: 'info' } }],
exclude: ['assets', 'sass', 'vendor', 'translations/lang']
.map(dir => path.resolve(__dirname, `../common/${dir}`))
.concat([path.resolve(__dirname, '../node_modules')])
},
{
test: /\.worker\.js$/,
loader: 'worker-loader'
},
{
include: [
path.resolve(__dirname, '../common/assets'),
path.resolve(__dirname, '../node_modules')
],
exclude: /node_modules(?!\/font-awesome)/,
test: /\.(gif|png|jpe?g|svg)$/i,
use: [
{
loader: 'file-loader',
options: {
hash: 'sha512',
digest: 'hex',
name: '[path][name].[ext]?[hash:6]'
}
},
{
loader: 'image-webpack-loader',
options: {
bypassOnDebug: true,
optipng: {
optimizationLevel: 4
},
gifsicle: {
interlaced: false
},
mozjpeg: {
quality: 80
},
svgo: {
plugins: [{ removeViewBox: true }, { removeEmptyAttrs: false }, { sortAttrs: true }]
}
}
}
]
},
{
include: [
path.resolve(__dirname, '../common/assets'),
path.resolve(__dirname, '../node_modules')
],
exclude: /node_modules(?!\/font-awesome)/,
test: /\.(ico|eot|otf|webp|ttf|woff|woff2)(\?.*)?$/,
loader: 'file-loader'
}
]
},
plugins: [
new SriPlugin({
hashFuncNames: ['sha256', 'sha384'],
enabled: true
}),
new HtmlWebpackPlugin({
title: config.title,
template: path.resolve(__dirname, '../common/index.html'),
inject: true,
filename: _.outputIndexPath
}),
new CopyWebpackPlugin([
{
from: _.cwd('./static'),
// to the root of dist path
to: './'
}
]),
new webpack.LoaderOptionsPlugin(_.loadersOptions())
],
target: _.target
};
module.exports = webpackConfig;

View File

@ -1,22 +1,19 @@
// Compile derivation checker using the (mostly) same webpack config // Compile derivation checker using the (mostly) same webpack config
'use strict'; 'use strict';
const baseConfig = require('./webpack.base'); const path = require('path');
const config = require('./config');
const derivationConfig = Object.assign({}, baseConfig, { const derivationConfig = {
// Remove the cruft we don't need
plugins: undefined,
target: undefined,
performance: undefined,
module: {
// Typescript loader
loaders: [baseConfig.module.loaders[0]]
},
// Point at derivation checker, make sure it's setup to run in node
target: 'node', target: 'node',
entry: { entry: './common/derivation-checker.ts',
'derivation-checker': './common/derivation-checker.ts' output: {
} path: config.path.output,
}); filename: 'derivation-checker.js'
},
module: {
rules: [config.typescriptRule],
},
resolve: config.resolve,
};
module.exports = derivationConfig; module.exports = derivationConfig;

View File

@ -1,87 +1,7 @@
'use strict'; 'use strict';
process.env.NODE_ENV = 'development'; const makeConfig = require('./makeConfig');
const path = require('path');
const webpack = require('webpack');
const base = require('./webpack.base');
const FriendlyErrors = require('friendly-errors-webpack-plugin');
const AutoDllPlugin = require('autodll-webpack-plugin');
const config = require('./config');
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
const threadLoader = require('thread-loader');
const fullSourceMap = process.env.SLOW_BUILD_SPEED; module.exports = makeConfig({
if (fullSourceMap) { isProduction: false,
base.devtool = fullSourceMap ? 'source-map' : 'cheap-module-eval-source-map'; isElectronBuild: !!process.env.BUILD_ELECTRON
});
threadLoader.warmup(
{
// pool options, like passed to loader options
// must match loader options to boot the correct pool
happyPackMode: true,
logLevel: 'info'
},
[
// modules to load
// can be any module, i. e.
'ts-loader'
]
);
base.module.rules[0].use.unshift({
loader: 'thread-loader',
options: {
workers: 4
}
});
}
base.performance = { hints: false };
base.module.rules.push(
{
test: /\.css$/,
include: path.resolve(__dirname, '../common/vendor'),
use: ['style-loader', 'css-loader']
},
{
test: /\.scss$/,
include: ['components', 'containers', 'sass']
.map(dir => path.resolve(__dirname, `../common/${dir}`))
.concat([path.resolve(__dirname, '../node_modules')]),
exclude: /node_modules(?!\/font-awesome)/,
use: ['style-loader', 'css-loader', 'sass-loader']
},
{
test: /\.less$/,
include: path.resolve(__dirname, '../common/assets/styles'),
use: ['style-loader', 'css-loader', 'less-loader']
}
);
base.plugins.push(
new AutoDllPlugin({
inject: true, // will inject the DLL bundles to index.html
filename: '[name]_[hash].js',
debug: true,
context: path.join(__dirname, '..'),
entry: {
vendor: [...config.vendor, 'babel-polyfill', 'bootstrap-sass', 'font-awesome']
}
}),
new HardSourceWebpackPlugin({
environmentHash: {
root: process.cwd(),
directories: ['webpack_config'],
files: ['package.json']
}
}),
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('development')
}),
new webpack.HotModuleReplacementPlugin(),
new webpack.NoEmitOnErrorsPlugin(),
new FriendlyErrors()
);
module.exports = base;

View File

@ -0,0 +1,36 @@
'use strict';
const webpack = require('webpack');
const path = require('path');
const ClearDistPlugin = require('./plugins/clearDist');
const config = require('./config');
const makeConfig = require('./makeConfig');
const electronConfig = {
target: 'electron-main',
entry: {
main: path.join(config.path.electron, 'main/index.ts'),
preload: path.join(config.path.electron, 'preload.ts')
},
module: {
rules: [config.typescriptRule]
},
resolve: {
extensions: ['.ts', '.js', '.json']
},
output: {
filename: '[name].js',
path: path.resolve(config.path.output, 'electron-js')
},
plugins: [
new ClearDistPlugin(),
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('development')
}),
],
node: {
__dirname: false,
__filename: false
}
};
module.exports = electronConfig;

View File

@ -0,0 +1,24 @@
'use strict';
const webpack = require('webpack');
const path = require('path');
const ClearDistPlugin = require('./plugins/clearDist');
const DelayPlugin = require('./plugins/delay');
const makeConfig = require('./makeConfig');
const electronConfig = require('./webpack.electron-dev.js');
const jsConfig = makeConfig({
isProduction: true,
isElectronBuild: true,
outputDir: 'electron-js'
});
// Redefine plugins with prod specific stuff
electronConfig.plugins = [
new ClearDistPlugin(),
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('production')
}),
new DelayPlugin(500)
];
module.exports = [electronConfig, jsConfig];

View File

@ -1,22 +1,19 @@
// Compile freezer using the (mostly) same webpack config // Compile freezer using the (mostly) same webpack config
'use strict'; 'use strict';
const baseConfig = require('./webpack.base'); const path = require('path');
const config = require('./config');
const freezerConfig = Object.assign({}, baseConfig, { const freezerConfig = {
// Remove the cruft we don't need
plugins: undefined,
target: undefined,
performance: undefined,
module: {
// Typescript loader
loaders: [baseConfig.module.rules[0]]
},
// Point at freezer, make sure it's setup to run in node
target: 'node', target: 'node',
entry: { entry: './common/freezer',
'freezer': './common/freezer' output: {
} path: config.path.output,
}); filename: 'freezer.js'
},
module: {
rules: [config.typescriptRule],
},
resolve: config.resolve,
};
module.exports = freezerConfig; module.exports = freezerConfig;

View File

@ -0,0 +1,8 @@
'use strict';
const makeConfig = require('./makeConfig');
module.exports = makeConfig({
isProduction: true,
isHTMLBuild: true,
outputDir: 'download'
});

View File

@ -1,101 +1,7 @@
'use strict'; 'use strict';
process.env.NODE_ENV = 'production'; const makeConfig = require('./makeConfig');
const FaviconsWebpackPlugin = require('favicons-webpack-plugin');
const path = require('path');
const webpack = require('webpack');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const ProgressPlugin = require('webpack/lib/ProgressPlugin');
const BabelMinifyPlugin = require('babel-minify-webpack-plugin');
// const OfflinePlugin = require('offline-plugin')
const base = require('./webpack.base');
const config = require('./config');
const rimraf = require('rimraf');
const distFolder = 'dist/';
// Clear out build folder module.exports = makeConfig({
rimraf.sync(distFolder, { rmdirSync: true }); isProduction: true,
outputDir: 'prod'
base.devtool = false; });
base.module.rules.push(
{
test: /\.css$/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: 'css-loader'
})
},
{
test: /\.scss$/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: ['css-loader', 'sass-loader']
})
},
{
test: /\.less$/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: ['css-loader', 'less-loader']
})
}
);
// a white list to add dependencies to vendor chunk
base.entry.vendor = config.vendor;
// use hash filename to support long-term caching
base.output.filename = '[name].[chunkhash:8].js';
// add webpack plugins
base.plugins.unshift(
new FaviconsWebpackPlugin({
logo: path.resolve(__dirname, '../static/favicon/android-chrome-384x384.png'),
background: '#163151',
inject: true
})
);
base.plugins.push(
new ProgressPlugin(),
new ExtractTextPlugin('[name].[chunkhash:8].css'),
new webpack.DefinePlugin({
'process.env.BUILD_DOWNLOADABLE': JSON.stringify(!!process.env.BUILD_DOWNLOADABLE)
}),
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('production')
}),
new BabelMinifyPlugin({
// Mangle seems to be reusing variable identifiers, causing errors
mangle: false,
// These two on top of a lodash file are causing illegal characters for
// safari and ios browsers
evaluate: false,
propertyLiterals: false,
}, {
comments: false
}),
// extract vendor chunks
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
filename: 'vendor.[chunkhash:8].js'
})
// For progressive web apps
// new OfflinePlugin({
// relativePaths: false,
// AppCache: false,
// ServiceWorker: {
// events: true
// }
// })
);
// minimize webpack output
base.stats = {
// Add children information
children: false,
// Add chunk information (setting this to `false` allows for a less verbose output)
chunks: false,
// Add built modules information to chunk information
chunkModules: false,
chunkOrigins: false,
modules: false
};
module.exports = base;