1
0
mirror of https://github.com/kremalicious/blowfish.git synced 2024-11-15 09:35:14 +01:00

Merge pull request #60 from kremalicious/feature/next.js

Move to Next.js for renderer
This commit is contained in:
Matthias Kretschmann 2020-02-25 21:58:35 +01:00 committed by GitHub
commit 00d96c594c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
63 changed files with 637 additions and 564 deletions

View File

@ -1,14 +0,0 @@
{
"presets": [
[
"@babel/env",
{
"targets": {
"node": "current"
}
}
],
"@babel/react"
],
"plugins": ["@babel/plugin-proposal-class-properties"]
}

View File

@ -1,10 +0,0 @@
# EditorConfig is awesome: http://EditorConfig.org
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
charset = utf-8
trim_trailing_whitespace = true

1
.env.example Normal file
View File

@ -0,0 +1 @@
ETHERSCAN_API_KEY=

3
.gitignore vendored
View File

@ -4,3 +4,6 @@ package-lock.json
build build
dist dist
coverage coverage
.next
out
.env

View File

@ -1,5 +1,6 @@
{ {
"semi": false, "semi": false,
"singleQuote": true, "singleQuote": true,
"trailingComma": "none" "trailingComma": "none",
"tabWidth": 2
} }

View File

@ -22,10 +22,8 @@ before_script:
script: script:
- npm test || travis_terminate 1 - npm test || travis_terminate 1
- npm run dist
after_script:
- ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT
- npm run dist
branches: branches:
except: except:

View File

@ -61,7 +61,7 @@ Alternatively, you can [build the app on your system](#build-packages).
## Development ## Development
The main app is a React app in `src/renderer/` wrapped within an Electron app defined in `src/main/`. The main app is a React app authored with [Next.js](https://nextjs.org) in `src/renderer/` wrapped within an [Electron](https://www.electronjs.org) app defined in `src/main/`.
Clone, and run: Clone, and run:
@ -125,7 +125,7 @@ For the GitHub releases steps a GitHub personal access token, exported as `GITHU
```text ```text
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2019 Matthias Kretschmann Copyright (c) 2020 Matthias Kretschmann
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@ -5,18 +5,20 @@
"description": "🐡 Simple Electron-based desktop app to retrieve and display your total Ocean Token balances.", "description": "🐡 Simple Electron-based desktop app to retrieve and display your total Ocean Token balances.",
"main": "./src/main/index.js", "main": "./src/main/index.js",
"scripts": { "scripts": {
"test": "npm run lint && jest", "start": "electron .",
"test:watch": "jest --watch", "test": "npm run lint && npm run jest",
"test:watch": "npm run jest -- --watch",
"jest": "NODE_ENV=test jest -c tests/jest.config.js",
"lint": "eslint --ignore-path .gitignore ./src/**/*.{js,jsx} && stylelint --ignore-path .gitignore ./src/**/*.{css,scss}", "lint": "eslint --ignore-path .gitignore ./src/**/*.{js,jsx} && stylelint --ignore-path .gitignore ./src/**/*.{css,scss}",
"start": "webpack-dev-server --hot --host 0.0.0.0 --config=./webpack.dev.config.js", "copy:icons": "copy 'src/renderer/images/icon*' build/",
"build": "cross-env NODE_ENV=production webpack --config webpack.common.config.js", "build:react": "cross-env NODE_ENV=production next build src/renderer && next export src/renderer && npm run copy:icons",
"package": "electron-builder build -ml -p never", "build:electron": "electron-builder build -ml -p never",
"package:win": "electron-builder build -w -p never", "build:electron:win": "electron-builder build -w -p never",
"dist": "./scripts/release-prepare.sh", "dist": "./scripts/release-prepare.sh",
"release": "release-it --non-interactive", "release": "release-it --non-interactive",
"changelog": "auto-changelog -p", "changelog": "auto-changelog -p",
"format": "prettier --write 'src/**/*.{js,jsx}' && npm run format:css", "format": "prettier --write --ignore-path .gitignore 'src/**/*.{js,jsx,json}' && npm run format:css",
"format:css": "prettier-stylelint --write --quiet 'src/**/*.{css,scss}'" "format:css": "prettier-stylelint --write --ignore-path .gitignore --quiet 'src/**/*.{css,scss}'"
}, },
"repository": "https://github.com/kremalicious/blowfish.git", "repository": "https://github.com/kremalicious/blowfish.git",
"homepage": "https://github.com/kremalicious/blowfish", "homepage": "https://github.com/kremalicious/blowfish",
@ -27,61 +29,56 @@
}, },
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@coingecko/cryptoformat": "^0.3.3", "@coingecko/cryptoformat": "^0.3.4",
"axios": "^0.19.2",
"electron-is-dev": "^1.1.0",
"electron-next": "^3.1.5",
"electron-store": "^5.1.1",
"ethereum-address": "^0.0.4", "ethereum-address": "^0.0.4",
"ethereum-blockies": "github:MyEtherWallet/blockies", "ethereum-blockies": "github:MyEtherWallet/blockies",
"ms": "^2.1.2" "ethjs-unit": "^0.1.6",
"ms": "^2.1.2",
"shortid": "^2.2.15"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.8.4", "@babel/core": "^7.8.4",
"@babel/plugin-proposal-class-properties": "^7.8.3",
"@babel/preset-env": "^7.8.4", "@babel/preset-env": "^7.8.4",
"@babel/preset-react": "^7.8.3",
"@jest-runner/electron": "^2.0.3", "@jest-runner/electron": "^2.0.3",
"@reach/router": "^1.3.1",
"@react-mock/state": "^0.1.8", "@react-mock/state": "^0.1.8",
"@svgr/webpack": "^5.0.0", "@svgr/webpack": "^5.2.0",
"@testing-library/jest-dom": "^5.0.0", "@testing-library/jest-dom": "^5.1.1",
"@testing-library/react": "^9.4.0", "@testing-library/react": "^9.4.1",
"auto-changelog": "^1.16.2", "auto-changelog": "^1.16.2",
"babel-eslint": "^10.0.3", "babel-eslint": "^10.0.3",
"babel-jest": "^25.1.0", "babel-jest": "^25.1.0",
"babel-loader": "^8.0.6", "copy": "^0.3.2",
"copy-webpack-plugin": "^5.1.1",
"cross-env": "^7.0.0", "cross-env": "^7.0.0",
"css-loader": "^3.4.2", "dotenv": "^8.2.0",
"electron": "^8.0.0", "electron": "^8.0.1",
"electron-builder": "^22.3.2", "electron-builder": "^22.3.2",
"electron-devtools-installer": "^2.2.4", "electron-devtools-installer": "^2.2.4",
"electron-store": "^5.1.0",
"eslint": "^6.8.0", "eslint": "^6.8.0",
"eslint-config-prettier": "^6.10.0", "eslint-config-prettier": "^6.10.0",
"eslint-plugin-react": "^7.18.3", "eslint-plugin-react": "^7.18.3",
"file-loader": "^5.0.2",
"html-webpack-plugin": "^3.2.0",
"identity-obj-proxy": "^3.0.0", "identity-obj-proxy": "^3.0.0",
"jest": "^25.1.0", "jest": "^25.1.0",
"mini-css-extract-plugin": "^0.9.0", "next": "^9.2.2",
"prettier": "^1.19.1", "prettier": "^1.19.1",
"prettier-stylelint": "^0.4.2", "prettier-stylelint": "^0.4.2",
"react": "^16.12.0", "react": "^16.12.0",
"react-dom": "^16.12.0", "react-dom": "^16.12.0",
"react-pose": "^4.0.10", "react-pose": "^4.0.10",
"release-it": "^12.4.3", "release-it": "^12.6.1",
"style-loader": "^1.1.3", "stylelint": "^13.2.0",
"stylelint": "^13.1.0", "stylelint-config-css-modules": "^2.2.0",
"stylelint-config-css-modules": "^2.1.0", "stylelint-config-standard": "^20.0.0"
"stylelint-config-standard": "^19.0.0",
"webpack": "^4.41.5",
"webpack-cli": "^3.3.10",
"webpack-dev-server": "^3.10.3"
}, },
"browserslist": "electron >= 6.0", "browserslist": "electron >= 8.0",
"build": { "build": {
"appId": "com.kremalicious.blowfish", "appId": "com.kremalicious.blowfish",
"files": [ "files": [
"./build/**/*",
"./src/main/**/*", "./src/main/**/*",
"./src/renderer/out/**/*",
"./src/*.js", "./src/*.js",
"package.json" "package.json"
], ],
@ -126,29 +123,5 @@
"npm": { "npm": {
"publish": false "publish": false
} }
},
"jest": {
"rootDir": "src/renderer",
"transform": {
"^.+\\.jsx?$": "babel-jest"
},
"moduleNameMapper": {
".+\\.(css|styl|less|sass|scss)$": "identity-obj-proxy",
".+\\.(jpg|jpeg|png|gif|eot|otf|webp|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/jest/__mocks__/file-mock.js",
"\\.svg": "<rootDir>/jest/__mocks__/svgr-mock.js"
},
"testURL": "http://localhost",
"setupFilesAfterEnv": [
"<rootDir>/jest/setup-test-env.js"
],
"coverageDirectory": "../../coverage/",
"collectCoverage": true,
"collectCoverageFrom": [
"<rootDir>/**/*.{js,jsx}",
"!<rootDir>/jest/**/*.{js,jsx}",
"!**/node_modules/**"
],
"runner": "@jest-runner/electron",
"testEnvironment": "@jest-runner/electron/environment"
} }
} }

View File

@ -1,8 +1,8 @@
#!/bin/bash #!/bin/bash
rm -rf {dist,build}/ && \ rm -rf {dist,build,src/renderer/.next,src/renderer/out}/ && \
npm run build && \ npm run build:react && \
npm run package && \ npm run build:electron && \
if [ -x "$(command -v docker)" ]; then if [ -x "$(command -v docker)" ]; then
docker run --rm \ docker run --rm \
@ -14,6 +14,5 @@ if [ -x "$(command -v docker)" ]; then
-v ~/.cache/electron:/root/.cache/electron \ -v ~/.cache/electron:/root/.cache/electron \
-v ~/.cache/electron-builder:/root/.cache/electron-builder \ -v ~/.cache/electron-builder:/root/.cache/electron-builder \
electronuserland/builder:wine \ electronuserland/builder:wine \
/bin/bash -c "npm i && npm run build && npm run package:win" /bin/bash -c "npm i && npm run build:react && npm run build:electron:win"
fi fi

View File

@ -6,6 +6,8 @@ const {
nativeTheme, nativeTheme,
ipcMain ipcMain
} = require('electron') } = require('electron')
const prepareNext = require('electron-next')
const isDev = require('electron-is-dev')
const pkg = require('../../package.json') const pkg = require('../../package.json')
const buildMenu = require('./menu') const buildMenu = require('./menu')
const { buildTouchbar, updateTouchbar } = require('./touchbar') const { buildTouchbar, updateTouchbar } = require('./touchbar')
@ -13,16 +15,6 @@ const { rgbaToHex } = require('../utils')
let mainWindow let mainWindow
// Keep a reference for dev mode
let isDev = false
if (
process.defaultApp ||
/[\\/]electron-prebuilt[\\/]/.test(process.execPath) ||
/[\\/]electron[\\/]/.test(process.execPath)
) {
isDev = true
}
const width = 640 const width = 640
const height = 450 const height = 450
@ -44,6 +36,7 @@ const createWindow = async () => {
autoHideMenuBar: true, autoHideMenuBar: true,
webPreferences: { webPreferences: {
nodeIntegration: true, nodeIntegration: true,
preload: path.join(__dirname, 'preload.js'),
scrollBounce: true, scrollBounce: true,
enableBlinkFeatures: 'OverlayScrollbars' enableBlinkFeatures: 'OverlayScrollbars'
} }
@ -51,8 +44,8 @@ const createWindow = async () => {
mainWindow.loadURL( mainWindow.loadURL(
isDev isDev
? 'http://localhost:8080' ? 'http://localhost:8000'
: `file://${path.join(__dirname, '../../build/index.html')}` : `file://${path.join(__dirname, '../renderer/out/index.html')}`
) )
createWindowEvents(mainWindow) createWindowEvents(mainWindow)
@ -85,18 +78,22 @@ const createWindow = async () => {
} }
} }
app.on('ready', () => { app.on('ready', async () => {
createWindow() await prepareNext('./src/renderer')
await createWindow()
mainWindow.webContents.on('dom-ready', () => { mainWindow.webContents.on('dom-ready', () => {
switchTheme() switchTheme()
switchAccentColor()
// add platform as class // add platform as class
mainWindow.webContents.executeJavaScript( mainWindow.webContents.executeJavaScript(
`document.getElementsByTagName('html')[0].classList.add('${process.platform}')` `document.getElementsByTagName('html')[0].classList.add('${process.platform}')`
) )
}) })
mainWindow.webContents.on('did-finish-load', () => {
switchAccentColor()
})
}) })
// Quit when all windows are closed. // Quit when all windows are closed.
@ -138,22 +135,22 @@ const installDevTools = async mainWindow => {
const createWindowEvents = mainWindow => { const createWindowEvents = mainWindow => {
mainWindow.on('enter-full-screen', () => mainWindow.on('enter-full-screen', () =>
mainWindow.webContents.executeJavaScript( mainWindow.webContents.executeJavaScript(
'document.getElementsByTagName(\'html\')[0].classList.add(\'fullscreen\')' 'document.getElementsByTagName("html")[0].classList.add("fullscreen")'
) )
) )
mainWindow.on('leave-full-screen', () => mainWindow.on('leave-full-screen', () =>
mainWindow.webContents.executeJavaScript( mainWindow.webContents.executeJavaScript(
'document.getElementsByTagName(\'html\')[0].classList.remove(\'fullscreen\')' 'document.getElementsByTagName("html")[0].classList.remove("fullscreen")'
) )
) )
mainWindow.on('blur', () => mainWindow.on('blur', () =>
mainWindow.webContents.executeJavaScript( mainWindow.webContents.executeJavaScript(
'document.getElementsByTagName(\'html\')[0].classList.add(\'blur\')' 'document.getElementsByTagName("html")[0].classList.add("blur")'
) )
) )
mainWindow.on('focus', () => mainWindow.on('focus', () =>
mainWindow.webContents.executeJavaScript( mainWindow.webContents.executeJavaScript(
'document.getElementsByTagName(\'html\')[0].classList.remove(\'blur\')' 'document.getElementsByTagName("html")[0].classList.remove("blur")'
) )
) )
} }
@ -197,10 +194,10 @@ const switchTheme = () => {
isDarkMode isDarkMode
? mainWindow.webContents.executeJavaScript( ? mainWindow.webContents.executeJavaScript(
'document.getElementsByTagName(\'html\')[0].classList.add(\'dark\')' 'document.getElementsByTagName("html")[0].classList.add("dark")'
) )
: mainWindow.webContents.executeJavaScript( : mainWindow.webContents.executeJavaScript(
'document.getElementsByTagName(\'html\')[0].classList.remove(\'dark\')' 'document.getElementsByTagName("html")[0].classList.remove("dark")'
) )
} }
} }

12
src/main/preload.js Normal file
View File

@ -0,0 +1,12 @@
const { webFrame, ipcRenderer } = require('electron')
const Store = require('electron-store')
const store = new Store()
// Since we disabled nodeIntegration we can reintroduce
// needed node functionality here
process.once('loaded', () => {
global.ipcRenderer = ipcRenderer
global.webFrame = webFrame
global.store = store
})

View File

@ -8,7 +8,7 @@ const createButton = (
value, value,
key, key,
mainWindow, mainWindow,
accentColor, accentColor = '#f6388a',
currentCurrency = 'ocean' currentCurrency = 'ocean'
) => ) =>
new TouchBarButton({ new TouchBarButton({

View File

@ -1,65 +0,0 @@
import React, { useEffect } from 'react'
import PropTypes from 'prop-types'
import {
Router,
createMemorySource,
createHistory,
LocationProvider,
navigate
} from '@reach/router'
import { webFrame, ipcRenderer } from 'electron'
import posed, { PoseGroup } from 'react-pose'
import Titlebar from './components/Titlebar'
import { defaultAnimation } from './components/Animations'
import Home from './screens/Home'
import Preferences from './screens/Preferences'
import styles from './App.module.css'
//
// Disable zooming
//
webFrame.setVisualZoomLevelLimits(1, 1)
webFrame.setLayoutZoomLevelLimits(0, 0)
const Animation = posed.div(defaultAnimation)
// Fix reach-router in packaged Electron
// https://github.com/reach/router/issues/25#issuecomment-394003652
let source = createMemorySource('/')
let history = createHistory(source)
const PosedRouter = ({ children }) => (
<LocationProvider history={history}>
{({ location }) => (
<PoseGroup animateOnMount>
<Animation key={location.key}>
<Router location={location}>{children}</Router>
</Animation>
</PoseGroup>
)}
</LocationProvider>
)
PosedRouter.propTypes = {
children: PropTypes.any.isRequired
}
export default function App() {
useEffect(() => {
ipcRenderer.on('goTo', (evt, route) => {
navigate(route)
})
}, [])
return (
<>
{process.platform === 'darwin' && <Titlebar />}
<div className={styles.app}>
<PosedRouter>
<Home path="/" default />
<Preferences path="/preferences" />
</PosedRouter>
</div>
</>
)
}

View File

@ -1,15 +0,0 @@
import React from 'react'
import { render } from '@testing-library/react'
import AppProvider from './store/AppProvider'
import App from './App'
describe('App', () => {
it('renders correctly', () => {
const { container } = render(
<AppProvider>
<App />
</AppProvider>
)
expect(container.firstChild).toBeInTheDocument()
})
})

26
src/renderer/Layout.jsx Normal file
View File

@ -0,0 +1,26 @@
import React from 'react'
import PropTypes from 'prop-types'
import posed, { PoseGroup } from 'react-pose'
import shortid from 'shortid'
import { defaultAnimation } from './components/Animations'
import Titlebar from './components/Titlebar'
import styles from './Layout.module.css'
const Animation = posed.div(defaultAnimation)
export default function Layout({ children }) {
return (
<>
{process.platform === 'darwin' && <Titlebar />}
<div className={styles.app}>
<PoseGroup animateOnMount>
<Animation key={shortid.generate()}>{children}</Animation>
</PoseGroup>
</div>
</>
)
}
Layout.propTypes = {
children: PropTypes.any.isRequired
}

View File

@ -1,6 +1,6 @@
import React, { useContext } from 'react' import React, { useContext } from 'react'
import { openUrl } from '../../../utils' import { openUrl } from '../../../utils'
import Balance from '../../components/Balance' import Balance from '../Balance'
import { AppContext } from '../../store/createContext' import { AppContext } from '../../store/createContext'
import styles from './Accounts.module.css' import styles from './Accounts.module.css'

View File

@ -0,0 +1,5 @@
.accounts {
composes: balancewrap from '../../pages/index.module.css';
min-height: 55px;
padding-top: 2rem;
}

View File

@ -1,16 +1,16 @@
import React, { useContext } from 'react' import React, { useContext } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import posed, { PoseGroup } from 'react-pose' import posed, { PoseGroup } from 'react-pose'
import { AppContext } from '../../store/createContext' import { AppContext, PriceContext } from '../../store/createContext'
import { cryptoFormatter } from '../../../utils' import { cryptoFormatter } from '../../../utils'
import stylesIndex from './index.module.css' import stylesIndex from '../../pages/index.module.css'
import styles from './Ticker.module.css' import styles from './Ticker.module.css'
import { fadeIn } from '../../components/Animations' import { fadeIn } from '../Animations'
const Item = posed.div(fadeIn) const Item = posed.div(fadeIn)
const Change = ({ currency }) => { const Change = ({ currency }) => {
const { priceChanges } = useContext(AppContext) const { priceChanges } = useContext(PriceContext)
const isNegative = JSON.stringify(priceChanges[currency]).startsWith('-') const isNegative = JSON.stringify(priceChanges[currency]).startsWith('-')
let classes = isNegative ? styles.negative : styles.positive let classes = isNegative ? styles.negative : styles.positive
@ -27,13 +27,10 @@ Change.propTypes = {
} }
const Items = () => { const Items = () => {
const { const { prices } = useContext(PriceContext)
prices, const { needsConfig, currency, toggleCurrencies, accentColor } = useContext(
needsConfig, AppContext
currency, )
toggleCurrencies,
accentColor
} = useContext(AppContext)
const activeStyle = { const activeStyle = {
backgroundColor: accentColor, backgroundColor: accentColor,
@ -60,7 +57,7 @@ const Items = () => {
const Ticker = props => ( const Ticker = props => (
<footer className={styles.ticker} {...props}> <footer className={styles.ticker} {...props}>
<div className={stylesIndex.balanceWrap}> <div className={stylesIndex.balancewrap}>
<PoseGroup animateOnMount> <PoseGroup animateOnMount>
<Items key="items" /> <Items key="items" />
</PoseGroup> </PoseGroup>

View File

@ -1,6 +1,6 @@
import React, { useContext } from 'react' import React, { useContext } from 'react'
import { AppContext } from '../../store/createContext' import { AppContext } from '../../store/createContext'
import Balance from '../../components/Balance' import Balance from '../Balance'
import { conversions } from '../../../config' import { conversions } from '../../../config'
const calculateTotalBalance = (accounts, currency) => { const calculateTotalBalance = (accounts, currency) => {

View File

@ -1,5 +1,5 @@
import React, { useContext } from 'react' import React, { useContext } from 'react'
import { Link } from '@reach/router' import Link from 'next/link'
import { AppContext } from '../../store/createContext' import { AppContext } from '../../store/createContext'
import IconRocket from '../../images/rocket.svg' import IconRocket from '../../images/rocket.svg'
import styles from './Welcome.module.css' import styles from './Welcome.module.css'
@ -10,8 +10,10 @@ const Welcome = () => {
return ( return (
<div className={styles.welcome}> <div className={styles.welcome}>
<IconRocket /> <IconRocket />
<Link style={{ color: accentColor }} to="preferences"> <Link href="/preferences">
Add your first address to get started. <a style={{ color: accentColor }}>
Add your first address to get started.
</a>
</Link> </Link>
</div> </div>
) )

View File

@ -2,7 +2,7 @@ import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { toDataUrl } from 'ethereum-blockies' import { toDataUrl } from 'ethereum-blockies'
import posed, { PoseGroup } from 'react-pose' import posed, { PoseGroup } from 'react-pose'
import { fadeIn } from '../../../components/Animations' import { fadeIn } from '../../Animations'
import styles from './Saved.module.css' import styles from './Saved.module.css'
export default function Saved({ accounts, handleDelete }) { export default function Saved({ accounts, handleDelete }) {

View File

@ -1,15 +1,15 @@
import React, { PureComponent } from 'react' import React, { PureComponent } from 'react'
import Store from 'electron-store'
import ethereum_address from 'ethereum-address' import ethereum_address from 'ethereum-address'
import Store from 'electron-store'
import { AppContext } from '../../../store/createContext' import { AppContext } from '../../../store/createContext'
import Saved from './Saved' import Saved from './Saved'
import New from './New' import New from './New'
import styles from './index.module.css' import styles from './index.module.css'
export default class Accounts extends PureComponent { export default class AccountsList extends PureComponent {
static contextType = AppContext static contextType = AppContext
store = new Store() store = process.env.NODE_ENV === 'test' ? new Store() : global.store
state = { accounts: [], input: '', error: '' } state = { accounts: [], input: '', error: '' }

View File

@ -1,5 +1,5 @@
.preference { .preference {
composes: box from '../../../components/Box.module.css'; composes: box from '../../Box.module.css';
-webkit-app-region: none; -webkit-app-region: none;
-webkit-user-select: text; -webkit-user-select: text;
} }

View File

@ -27,7 +27,7 @@ html.fullscreen {
font-size: 24px; font-size: 24px;
} }
#root { #__next {
height: 100%; height: 100%;
position: relative; position: relative;
font-size: 1rem; font-size: 1rem;
@ -43,7 +43,7 @@ html.fullscreen {
-webkit-user-select: none; -webkit-user-select: none;
} }
.dark #root { .dark #__next {
color: #e2e2e2; color: #e2e2e2;
} }

View File

@ -1,21 +0,0 @@
import React from 'react'
import { render } from 'react-dom'
import AppProvider from './store/AppProvider'
import App from './App'
import pkg from '../../package.json'
import './index.css'
document.body.style.backgroundColor = '#141414'
// Since we are using HtmlWebpackPlugin WITHOUT a template, we should create our own root node in the body element before rendering into it
let root = document.createElement('div')
root.id = 'root'
document.body.appendChild(root)
document.title = pkg.productName
render(
<AppProvider>
<App />
</AppProvider>,
document.getElementById('root')
)

View File

@ -0,0 +1,48 @@
require('dotenv').config()
const withSvgr = (nextConfig = {}) => {
return Object.assign({}, nextConfig, {
webpack(config, options) {
config.module.rules.push({
test: /\.svg$/,
use: [
{
loader: '@svgr/webpack',
options: {
icon: true
}
}
]
})
if (typeof nextConfig.webpack === 'function') {
return nextConfig.webpack(config, options)
}
return config
}
})
}
const withElectron = (nextConfig = {}) => {
return Object.assign({}, nextConfig, {
webpack: config => {
config.target = 'electron-renderer'
return config
}
})
}
module.exports = withSvgr(
withElectron({
env: {
ETHERSCAN_API_KEY: process.env.ETHERSCAN_API_KEY
},
exportPathMap() {
return {
'/': { page: '/' },
'/preferences': { page: '/preferences' }
}
}
})
)

View File

@ -0,0 +1,32 @@
import React, { useEffect } from 'react'
import PropTypes from 'prop-types'
import Router from 'next/router'
// import { ipcRenderer } from 'electron'
import AppProvider from '../store/AppProvider'
import PriceProvider from '../store/PriceProvider'
import Layout from '../Layout'
import '../global.css'
export default function App({ Component, pageProps }) {
useEffect(() => {
global.ipcRenderer.on('goTo', (evt, route) => {
Router.push(route)
})
}, [])
return (
<PriceProvider>
<AppProvider>
<Layout>
<Component {...pageProps} />
</Layout>
</AppProvider>
</PriceProvider>
)
}
App.propTypes = {
Component: PropTypes.any.isRequired,
pageProps: PropTypes.any.isRequired
}

View File

@ -0,0 +1,47 @@
import React, { useContext } from 'react'
import Link from 'next/link'
import { AppContext } from '../store/createContext'
import Welcome from '../components/Home/Welcome'
import Spinner from '../components/Spinner'
import Divider from '../components/Divider'
import Total from '../components/Home/Total'
import Ticker from '../components/Home/Ticker'
import Accounts from '../components/Home/Accounts'
import IconCog from '../images/cog.svg'
import styles from './index.module.css'
function MainContent() {
const { isLoading, error, needsConfig } = useContext(AppContext)
return needsConfig ? (
<Welcome />
) : error ? (
<div className={styles.error}>{error}</div>
) : isLoading ? (
<Spinner />
) : (
<>
<Total />
<Divider />
<Accounts />
</>
)
}
export default function Home() {
return (
<>
<main className={styles.main}>
<Link href="/preferences">
<a className={styles.preferences}>
<IconCog />
</a>
</Link>
<MainContent />
</main>
<Ticker />
</>
)
}

View File

@ -1,5 +1,5 @@
.main { .main {
composes: box from '../../components/Box.module.css'; composes: box from '../components/Box.module.css';
min-height: 222px; min-height: 222px;
display: flex; display: flex;
align-items: center; align-items: center;
@ -25,10 +25,15 @@
opacity: 0.5; opacity: 0.5;
} }
.balanceWrap { .balancewrap {
display: grid; display: grid;
grid-gap: 0.5rem; grid-gap: 0.5rem;
grid-template-columns: repeat(auto-fit, minmax(7rem, 1fr)); grid-template-columns: repeat(auto-fit, minmax(7rem, 1fr));
justify-items: start; justify-items: start;
width: 100%; width: 100%;
} }
.error {
font-size: 0.9rem;
color: lightcoral;
}

View File

@ -0,0 +1,18 @@
import React from 'react'
import Link from 'next/link'
import AccountsList from '../components/Preferences/AccountsList'
import styles from './preferences.module.css'
export default function Preferences() {
return (
<div className={styles.preferences}>
<h1 className={styles.title}>Preferences</h1>{' '}
<Link href="/">
<a className={styles.close} title="Close Preferences">
&times;
</a>
</Link>
<AccountsList />
</div>
)
}

View File

@ -1,5 +0,0 @@
.accounts {
composes: balanceWrap from './index.module.css';
min-height: 55px;
padding-top: 2rem;
}

View File

@ -1,39 +0,0 @@
import React, { useContext } from 'react'
import { Link } from '@reach/router'
import { AppContext } from '../../store/createContext'
import Welcome from './Welcome'
import Spinner from '../../components/Spinner'
import Divider from '../../components/Divider'
import Total from './Total'
import Ticker from './Ticker'
import Accounts from './Accounts'
import IconCog from '../../images/cog.svg'
import styles from './index.module.css'
export default function Home() {
const { isLoading, needsConfig } = useContext(AppContext)
return (
<>
<main className={styles.main}>
<Link className={styles.preferences} to="/preferences">
<IconCog />
</Link>
{needsConfig ? (
<Welcome />
) : isLoading ? (
<Spinner />
) : (
<>
<Total />
<Divider />
<Accounts />
</>
)}
</main>
<Ticker style={isLoading ? { opacity: 0 } : null} />
</>
)
}

View File

@ -1,28 +0,0 @@
import React from 'react'
import { render, fireEvent } from '@testing-library/react'
import { AppContext } from '../../store/createContext'
import context from '../../jest/__fixtures__/context'
import Home from '.'
describe('Home', () => {
it('renders correctly', () => {
const { container, getByText } = render(
<AppContext.Provider value={context}>
<Home />
</AppContext.Provider>
)
expect(container.firstChild).toBeInTheDocument()
fireEvent.click(getByText(/Ξ/))
})
it('renders Welcome without config', () => {
const { container } = render(
<AppContext.Provider value={{ ...context, needsConfig: true }}>
<Home />
</AppContext.Provider>
)
expect(container.firstChild).toHaveTextContent(
'Add your first address to get started.'
)
})
})

View File

@ -1,16 +0,0 @@
import React from 'react'
import { Link } from '@reach/router'
import Accounts from './Accounts'
import styles from './index.module.css'
const Preferences = () => (
<div className={styles.preferences}>
<h1 className={styles.title}>Preferences</h1>{' '}
<Link className={styles.close} title="Close Preferences" to="/">
&times;
</Link>
<Accounts />
</div>
)
export default Preferences

View File

@ -1,120 +1,64 @@
import React, { PureComponent } from 'react' import React, { useContext, useState, useEffect } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import ms from 'ms' import ms from 'ms'
import { ipcRenderer } from 'electron' // import { ipcRenderer } from 'electron'
import Store from 'electron-store' import { AppContext, PriceContext } from './createContext'
import { AppContext } from './createContext' import { refreshInterval, conversions } from '../../config'
import fetchData from '../utils/fetch' import { getAccounts, getBalance } from './helpers'
import { refreshInterval, conversions, oceanTokenContract } from '../../config'
// construct initial prices Map to get consistent export default function AppProvider({ children }) {
// order for Ticker and Touchbar const { prices } = useContext(PriceContext)
let pricesMap = new Map() const [isLoading, setIsLoading] = useState(true)
pricesMap.set('ocean', 1) const [accounts, setAccounts] = useState([])
conversions.map(key => pricesMap.set(key, 0)) const [needsConfig, setNeedsConfig] = useState(false)
const [currency, setCurrency] = useState('ocean')
const [accentColor, setAccentColor] = useState('#f6388a')
const [error, setError] = useState()
export default class AppProvider extends PureComponent { useEffect(() => {
static propTypes = {
children: PropTypes.any.isRequired
}
store = new Store()
state = {
isLoading: true,
accounts: [],
currency: 'ocean',
needsConfig: false,
prices: pricesMap,
priceChanges: Object.assign(
...conversions.map(key => ({
[key]: 0
}))
),
toggleCurrencies: currency => this.toggleCurrencies(currency),
setBalances: () => this.setBalances(),
accentColor: '#f6388a'
}
async componentDidMount() {
// listener for accent color // listener for accent color
ipcRenderer.on('accent-color', (event, accentColor) => { if (process.env.NODE_ENV !== 'test') {
this.setState({ accentColor }) global.ipcRenderer.on('accent-color', (evt, accentColor) => {
}) setAccentColor(accentColor)
})
}
}, [])
// listener for touchbar useEffect(() => {
ipcRenderer.on('setCurrency', (evt, currency) => async function init() {
this.state.toggleCurrencies(currency) try {
) await setBalances()
setIsLoading(false)
} catch (error) {
console.error(error.message)
setError(error.message)
}
const newPrizes = await this.fetchAndSetPrices() // listener for touchbar
this.setState({ prices: newPrizes }) global.ipcRenderer.on('setCurrency', (evt, currency) =>
toggleCurrencies(currency)
await this.setBalances() )
setInterval(this.fetchAndSetPrices, ms(refreshInterval))
setInterval(this.setBalances, ms(refreshInterval))
this.setState({ isLoading: false })
}
getAccounts() {
let accountsPref
if (this.store.has('accounts')) {
accountsPref = this.store.get('accounts')
!accountsPref.length
? this.setState({ needsConfig: true })
: this.setState({ needsConfig: false })
} else {
accountsPref = []
this.setState({ needsConfig: true })
} }
return accountsPref init()
} setInterval(init, ms(refreshInterval))
async getBalance(account) { return () => {
const json = await fetchData( clearInterval(init)
`https://api.etherscan.io/api?module=account&action=tokenbalance&contractaddress=${oceanTokenContract}&address=${account}&tag=latest` }
) }, [prices])
const balance = json.result / 1e18 // Convert from vodka 10^18
return balance
}
fetchAndSetPrices = async () => {
const currencies = conversions.join(',')
const json = await fetchData(
`https://api.coingecko.com/api/v3/simple/price?ids=ocean-protocol&vs_currencies=${currencies}&include_24hr_change=true`
)
let newPrices = new Map(this.state.prices) // make a shallow copy of the Map
conversions.map(key => newPrices.set(key, json['ocean-protocol'][key])) // modify the copy
const newPriceChanges = await Object.assign(
...conversions.map(key => ({
[key]: json['ocean-protocol'][key + '_24h_change']
}))
)
ipcRenderer.send('prices-updated', Array.from(newPrices)) // convert Map to array, ipc messages seem to kill it
this.setState({ prices: newPrices, priceChanges: newPriceChanges })
return newPrices
}
setBalances = async () => {
const accountsPref = await this.getAccounts()
async function setBalances() {
let newAccounts = [] let newAccounts = []
const { needsConfig, accountsPref } = await getAccounts()
setNeedsConfig(needsConfig)
for (const account of accountsPref) { for (const account of accountsPref) {
const oceanBalance = await this.getBalance(account) const oceanBalance = await getBalance(account)
const conversionsBalance = Object.assign( const conversionsBalance = Object.assign(
...conversions.map(key => ({ ...conversions.map(key => ({
[key]: oceanBalance * this.state.prices.get(key) || 0 [key]: oceanBalance * prices.get(key) || 0
})) }))
) )
@ -129,22 +73,31 @@ export default class AppProvider extends PureComponent {
newAccounts.push(newAccount) newAccounts.push(newAccount)
} }
if (newAccounts !== this.state.accounts) { if (newAccounts !== accounts) {
this.setState({ accounts: newAccounts }) setAccounts(newAccounts)
} }
} }
toggleCurrencies(currency) { function toggleCurrencies(currency) {
const pricesNew = Array.from(this.state.prices) setCurrency(currency)
ipcRenderer.send('currency-updated', pricesNew, currency) const pricesNew = Array.from(prices)
this.setState({ currency }) global.ipcRenderer.send('currency-updated', pricesNew, currency)
} }
render() { const context = {
return ( isLoading,
<AppContext.Provider value={this.state}> accounts,
{this.props.children} currency,
</AppContext.Provider> needsConfig,
) accentColor,
error,
toggleCurrencies,
setBalances
} }
return <AppContext.Provider value={context}>{children}</AppContext.Provider>
}
AppProvider.propTypes = {
children: PropTypes.any.isRequired
} }

View File

@ -0,0 +1,53 @@
import React, { useEffect, useState } from 'react'
import PropTypes from 'prop-types'
import ms from 'ms'
import { PriceContext } from './createContext'
import { refreshInterval, conversions } from '../../config'
import { fetchAndSetPrices } from './helpers'
export default function PriceProvider({ children }) {
// construct initial prices Map to get consistent
// order for Ticker and Touchbar
let pricesMap = new Map()
pricesMap.set('ocean', 1)
conversions.map(key => pricesMap.set(key, 0))
const [prices, setPrices] = useState(pricesMap)
const [priceChanges, setPriceChanges] = useState(
Object.assign(
...conversions.map(key => ({
[key]: 0
}))
)
)
useEffect(() => {
async function init() {
try {
const { newPrices, newPriceChanges } = await fetchAndSetPrices(prices)
setPrices(newPrices)
setPriceChanges(newPriceChanges)
global.ipcRenderer.send('prices-updated', Array.from(newPrices)) // convert Map to array, ipc messages seem to kill it
} catch (error) {
console.error(error.message)
}
}
init()
setInterval(init, ms(refreshInterval))
return () => {
clearInterval(init)
}
}, [])
return (
<PriceContext.Provider value={{ prices, priceChanges }}>
{children}
</PriceContext.Provider>
)
}
PriceProvider.propTypes = {
children: PropTypes.any.isRequired
}

View File

@ -1,5 +1,6 @@
import { createContext } from 'react' import { createContext } from 'react'
const AppContext = createContext() const AppContext = createContext()
const PriceContext = createContext()
export { AppContext } export { AppContext, PriceContext }

View File

@ -0,0 +1,47 @@
import Store from 'electron-store'
import unit from 'ethjs-unit'
import { fetchData } from '../../utils'
import { oceanTokenContract, conversions } from '../../config'
export async function fetchAndSetPrices(prices) {
const currencies = conversions.join(',')
const json = await fetchData(
`https://api.coingecko.com/api/v3/simple/price?ids=ocean-protocol&vs_currencies=${currencies}&include_24hr_change=true`
)
let newPrices = new Map(prices) // make a shallow copy of the Map
conversions.map(key => newPrices.set(key, json['ocean-protocol'][key])) // modify the copy
const newPriceChanges = await Object.assign(
...conversions.map(key => ({
[key]: json['ocean-protocol'][key + '_24h_change']
}))
)
return { newPrices, newPriceChanges }
}
export async function getBalance(account) {
const json = await fetchData(
`https://api.etherscan.io/api?module=account&action=tokenbalance&contractaddress=${oceanTokenContract}&address=${account}&tag=latest&apikey=${process.env.ETHERSCAN_API_KEY}`
)
const balance = unit.fromWei(`${json.result}`, 'ether')
return balance
}
export async function getAccounts() {
let needsConfig
let accountsPref
const store = process.env.NODE_ENV === 'test' ? new Store() : global.store
if (store.has('accounts')) {
accountsPref = store.get('accounts')
needsConfig = !accountsPref.length
} else {
accountsPref = []
needsConfig = true
}
return { needsConfig, accountsPref }
}

View File

@ -1,18 +0,0 @@
const fetchData = async url => {
try {
const response = await fetch(url)
if (response.status !== 200) {
return console.log('Non-200 response: ' + response.status) // eslint-disable-line
}
const json = await response.json()
if (!json) return
return json
} catch (error) {
console.log('Error parsing json:' + error) // eslint-disable-line
}
}
export default fetchData

View File

@ -1,5 +1,20 @@
const { app, shell } = require('electron') const { app, shell } = require('electron')
const { formatCurrency } = require('@coingecko/cryptoformat') const { formatCurrency } = require('@coingecko/cryptoformat')
const axios = require('axios')
const fetchData = async url => {
try {
const response = await axios(url)
if (response.status !== 200) {
return console.error('Non-200 response: ' + response.status)
}
return response.data
} catch (error) {
console.error('Error parsing json: ' + error.message)
}
}
const isFiat = currency => currency === 'eur' || currency === 'usd' const isFiat = currency => currency === 'eur' || currency === 'usd'
@ -65,5 +80,6 @@ module.exports = {
rgbaToHex, rgbaToHex,
locale, locale,
numberFormatter, numberFormatter,
cryptoFormatter cryptoFormatter,
fetchData
} }

10
tests/Layout.test.jsx Normal file
View File

@ -0,0 +1,10 @@
import React from 'react'
import { render } from '@testing-library/react'
import Layout from '../src/renderer/Layout'
describe('Layout', () => {
it('renders correctly', () => {
const { container } = render(<Layout>Hello</Layout>)
expect(container.firstChild).toBeInTheDocument()
})
})

22
tests/Providers.test.jsx Normal file
View File

@ -0,0 +1,22 @@
import React from 'react'
import { render, waitForElement } from '@testing-library/react'
import AppProvider from '../src/renderer/store/AppProvider'
import PriceProvider from '../src/renderer/store/PriceProvider'
import { PriceContext } from '../src/renderer/store/createContext'
import { priceContext } from './__fixtures__/context'
describe('Providers', () => {
it('PriceProvider', async () => {
const { getByText } = render(<PriceProvider>Hello</PriceProvider>)
await waitForElement(() => getByText('Hello'))
})
it('AppProvider', async () => {
const { getByText } = render(
<PriceContext.Provider value={priceContext}>
<AppProvider>Hello</AppProvider>
</PriceContext.Provider>
)
await waitForElement(() => getByText('Hello'))
})
})

View File

@ -12,7 +12,12 @@ const priceChanges = {
eth: -17.538786176215627 eth: -17.538786176215627
} }
export default { export const priceContext = {
prices,
priceChanges
}
export const appContext = {
accentColor: '#0a5fff', accentColor: '#0a5fff',
accounts: [ accounts: [
{ {
@ -28,7 +33,5 @@ export default {
], ],
currency: 'ocean', currency: 'ocean',
isLoading: false, isLoading: false,
needsConfig: false, needsConfig: false
prices,
priceChanges
} }

View File

@ -0,0 +1,28 @@
const electron = {
require: jest.fn(),
match: jest.fn(),
app: jest.fn(),
remote: jest.fn(),
shell: jest.fn(),
dialog: jest.fn()
}
const remote = {
getCurrentWindow: jest.fn()
}
const ipcRenderer = {
on: jest.fn()
}
// for the shell module above
const shell = {
openExternal: jest.fn()
}
module.exports = {
electron,
remote,
shell,
ipcRenderer
}

View File

@ -0,0 +1,3 @@
module.exports = {
has: () => jest.fn()
}

16
tests/babel.config.js Normal file
View File

@ -0,0 +1,16 @@
const { devDependencies } = require('../package.json')
module.exports = {
presets: [
[
'next/babel',
{
'preset-env': {
targets: {
electron: devDependencies.electron.replace(/^\^|~/, '')
}
}
}
]
]
}

View File

@ -1,8 +1,8 @@
import React from 'react' import React from 'react'
import { render, fireEvent } from '@testing-library/react' import { render, fireEvent } from '@testing-library/react'
import { AppContext } from '../../../store/createContext'
import { StateMock } from '@react-mock/state' import { StateMock } from '@react-mock/state'
import Accounts from '.' import { AppContext } from '../../src/renderer/store/createContext'
import Accounts from '../../src/renderer/components/Preferences/AccountsList'
describe('Accounts', () => { describe('Accounts', () => {
const ui = ( const ui = (

View File

@ -0,0 +1,10 @@
import React from 'react'
import { render } from '@testing-library/react'
import Spinner from '../../src/renderer/components/Spinner'
describe('Spinner', () => {
it('renders correctly', () => {
const { container } = render(<Spinner />)
expect(container.firstChild).toBeInTheDocument()
})
})

34
tests/jest.config.js Normal file
View File

@ -0,0 +1,34 @@
module.exports = {
rootDir: '../',
transform: {
'^.+\\.[t|j]sx?$': ['babel-jest', { configFile: './tests/babel.config.js' }]
},
moduleNameMapper: {
'.+\\.(css|styl|less|sass|scss)$': 'identity-obj-proxy',
'.+\\.(jpg|jpeg|png|gif|eot|otf|webp|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
'<rootDir>/tests/__mocks__/file-mock.js',
'\\.svg': '<rootDir>/tests/__mocks__/svgr-mock.js'
},
testMatch: ['**/?(*.)+(spec|test).jsx'],
testPathIgnorePatterns: [
'<rootDir>/src/renderer/.next',
'<rootDir>/src/renderer/out',
'<rootDir>/node_modules',
'<rootDir>/build',
'<rootDir>/dist',
'<rootDir>/coverage'
],
testURL: 'http://localhost',
setupFilesAfterEnv: ['<rootDir>/tests/setupTests.js'],
runner: '@jest-runner/electron',
testEnvironment: '@jest-runner/electron/environment',
coverageDirectory: '<rootDir>/coverage/',
collectCoverage: true,
collectCoverageFrom: [
'<rootDir>/src/renderer/**/*.{js,jsx}',
'!<rootDir>/src/renderer/next.config.js',
'!<rootDir>/src/renderer/out/**/*',
'!<rootDir>/src/renderer/.next/**/*',
'!**/node_modules/**'
]
}

View File

@ -0,0 +1,36 @@
import React from 'react'
import { render, fireEvent } from '@testing-library/react'
import {
AppContext,
PriceContext
} from '../../src/renderer/store/createContext'
import { appContext, priceContext } from '../__fixtures__/context'
import Home from '../../src/renderer/pages/index'
describe('Home', () => {
it('renders correctly', () => {
const { container, getByText } = render(
<PriceContext.Provider value={priceContext}>
<AppContext.Provider value={appContext}>
<Home />
</AppContext.Provider>
</PriceContext.Provider>
)
expect(container.firstChild).toBeInTheDocument()
fireEvent.click(getByText(/Ξ/))
// fireEvent.click(getByText(/0x/))
})
it('renders Welcome without config', () => {
const { container } = render(
<PriceContext.Provider value={priceContext}>
<AppContext.Provider value={{ ...appContext, needsConfig: true }}>
<Home />
</AppContext.Provider>
</PriceContext.Provider>
)
expect(container.firstChild).toHaveTextContent(
'Add your first address to get started.'
)
})
})

View File

@ -1,7 +1,7 @@
import React from 'react' import React from 'react'
import { render } from '@testing-library/react' import { render } from '@testing-library/react'
import { AppContext } from '../../store/createContext' import { AppContext } from '../../src/renderer/store/createContext'
import Preferences from '.' import Preferences from '../../src/renderer/pages/preferences'
describe('Preferences', () => { describe('Preferences', () => {
it('renders correctly', () => { it('renders correctly', () => {

View File

@ -1 +1,3 @@
import '@testing-library/jest-dom/extend-expect' import '@testing-library/jest-dom/extend-expect'
jest.mock('electron-store')

View File

@ -1,74 +0,0 @@
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const CopyPlugin = require('copy-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const defaultInclude = [path.resolve(__dirname, 'src', 'renderer')]
const isDevelopment = process.env.NODE_ENV !== 'production'
module.exports = {
mode: isDevelopment ? 'development' : 'production',
entry: path.resolve(__dirname, 'src', 'renderer', 'index.js'),
output: {
path: path.resolve(__dirname, 'build'),
filename: 'bundle.js',
publicPath: isDevelopment ? '/' : './'
},
module: {
rules: [
{
test: /\.(js|jsx)?$/,
use: ['babel-loader'],
include: defaultInclude
},
{
test: /\.css$/,
exclude: /\.module\.css$/,
use: ['style-loader', 'css-loader'],
include: defaultInclude
},
{
test: /\.module\.css$/,
include: defaultInclude,
loader: [
isDevelopment ? 'style-loader' : MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
modules: {
localIdentName: '[name]__[local]___[hash:base64:5]'
},
localsConvention: 'camelCase',
sourceMap: isDevelopment
}
}
]
},
{
test: /\.(jpe?g|png|gif)$/,
use: ['file-loader?name=img/[name]__[hash:base64:5].[ext]'],
include: defaultInclude
},
{
test: /\.svg$/,
use: ['@svgr/webpack'],
include: defaultInclude
}
]
},
resolve: {
extensions: ['*', '.js', '.jsx', '.css']
},
target: 'electron-renderer',
plugins: [
new HtmlWebpackPlugin(),
new MiniCssExtractPlugin({
filename: isDevelopment ? '[name].css' : '[name].[hash].css',
chunkFilename: isDevelopment ? '[id].css' : '[id].[hash].css'
}),
new CopyPlugin([
{ from: './src/renderer/images/icon.*', to: './', flatten: true }
])
]
}

View File

@ -1,20 +0,0 @@
const path = require('path')
const common = require('./webpack.common.config')
const { spawn } = require('child_process')
module.exports = Object.assign({}, common, {
devtool: 'cheap-source-map',
devServer: {
contentBase: path.resolve(__dirname, 'build'),
stats: 'minimal',
before: () => {
spawn('electron', ['.'], {
shell: true,
env: process.env,
stdio: 'inherit'
})
.on('close', () => process.exit(0))
.on('error', spawnError => console.error(spawnError)) // eslint-disable-line no-console
}
}
})