diff --git a/.compilerc b/.compilerc index 484258f..b019e33 100644 --- a/.compilerc +++ b/.compilerc @@ -2,7 +2,8 @@ "env": { "development": { "application/javascript": { - "presets": ["node7"], + "presets": ["node7", "react"], + "plugins": ["transform-class-properties"], "sourceMaps": "inline" }, "text/less": { @@ -11,7 +12,8 @@ }, "production": { "application/javascript": { - "presets": ["node7"], + "presets": ["node7", "react"], + "plugins": ["transform-class-properties"], "sourceMaps": "none" } } diff --git a/package.json b/package.json index e0b8cdc..ac13438 100644 --- a/package.json +++ b/package.json @@ -84,6 +84,9 @@ "node7" ] }, + "standard": { + "parser": "babel-eslint" + }, "dependencies": { "electron-compile": "^6.4.1", "electron-log": "^2.2.7", @@ -91,22 +94,31 @@ "electron-window-state": "^4.1.1", "flexboxgrid": "^6.3.1", "font-awesome": "^4.7.0", - "once": "^1.4.0", "osenv": "^0.1.4", "prismjs": "^1.6.0", + "prop-types": "^15.5.10", + "react": "^15.6.1", + "react-dom": "^15.6.1", + "react-fontawesome": "^1.6.1", + "react-prism": "^4.3.0", "request": "^2.81.0", "semver": "^5.3.0", "yo-yo": "^1.4.1" }, "devDependencies": { "ava": "^0.21.0", + "babel-eslint": "^7.2.3", + "babel-plugin-transform-class-properties": "^6.24.1", "babel-preset-node7": "^1.5.0", + "babel-preset-react": "^6.24.1", "babel-register": "^6.24.1", "devtron": "^1.4.0", + "electron-devtools-installer": "^2.2.0", "electron-forge": "^3.0.5", "electron-prebuilt-compile": "1.7.4", "electron-process-manager": "0.0.4", "mockery": "^2.1.0", + "react-hot-loader": "^3.0.0-beta.6", "sinon": "^2.3.8", "spectron": "^3.7.2", "standard": "^10.0.2" diff --git a/spec/__mocks__/electron-compile.js b/spec/__mocks__/electron-compile.js new file mode 100644 index 0000000..5fac68f --- /dev/null +++ b/spec/__mocks__/electron-compile.js @@ -0,0 +1,5 @@ +import sinon from 'sinon' + +export const electronCompileMock = { + enableLiveReload: sinon.spy() +} diff --git a/spec/__mocks__/electron-devtools-installer.js b/spec/__mocks__/electron-devtools-installer.js new file mode 100644 index 0000000..b12e374 --- /dev/null +++ b/spec/__mocks__/electron-devtools-installer.js @@ -0,0 +1,10 @@ +import sinon from 'sinon' + +const installStub = sinon.stub() +installStub.returns(Promise.resolve()) + +export const electronDevtoolsInstallerMock = { + __esModule: true, + default: installStub, + REACT_DEVELOPER_TOOLS: 'react' +} diff --git a/spec/__mocks__/electron.js b/spec/__mocks__/electron.js index 6987e48..94e1015 100644 --- a/spec/__mocks__/electron.js +++ b/spec/__mocks__/electron.js @@ -52,12 +52,19 @@ class BrowserWindow extends EventEmitter { } } +class CommandLine { + constructor () { + this.appendSwitch = sinon.stub() + } +} + class App extends EventEmitter { constructor () { super() this.getName = sinon.stub() - this.getPah = sinon.stub() + this.getPath = sinon.stub() + this.commandLine = new CommandLine() } } @@ -90,7 +97,8 @@ export const electronMock = { getCurrentWindow: sinon.stub(), require: sinon.stub(), Menu: MockMenu, - MenuItem: MockMenuItem + MenuItem: MockMenuItem, + app: new App() }, ipcRenderer: { send: sinon.stub() @@ -104,3 +112,5 @@ export const electronMock = { screen: new Screen(), BrowserWindow } + +export const electronMainMock = Object.assign({}, electronMock, { remote: null }) diff --git a/spec/main/developer-spec.js b/spec/main/developer-spec.js index 4bc3177..593417b 100644 --- a/spec/main/developer-spec.js +++ b/spec/main/developer-spec.js @@ -2,12 +2,12 @@ import test from 'ava' import mockery from 'mockery' import sinon from 'sinon' -import { electronMock } from '../../spec/__mocks__/electron' +import { electronMainMock } from '../../spec/__mocks__/electron' require('../setup').setup(test) test('it installs devtron if not installed', (t) => { - mockery.registerMock('electron', electronMock) + mockery.registerMock('electron', electronMainMock) mockery.registerMock('devtron', { install: sinon.spy() }) const { BrowserWindow } = require('electron') @@ -23,7 +23,7 @@ test('it installs devtron if not installed', (t) => { }) test('does not it installs devtron if already installed', (t) => { - mockery.registerMock('electron', electronMock) + mockery.registerMock('electron', electronMainMock) mockery.registerMock('devtron', { install: sinon.spy() }) const { BrowserWindow } = require('electron') @@ -32,8 +32,39 @@ test('does not it installs devtron if already installed', (t) => { const { DeveloperFeatures } = require('../../src/main/developer') //eslint-disable-next-line - const developerFeatures = new DeveloperFeatures() + new DeveloperFeatures() const devtron = require('devtron') t.is(devtron.install.callCount, 0) }) + +test('it enables react HMR', (t) => { + mockery.registerMock('electron', electronMainMock) + mockery.registerMock('devtron', { install: sinon.spy() }) + mockery.registerMock('electron-compile', { enableLiveReload: sinon.spy() }) + + const { DeveloperFeatures } = require('../../src/main/developer') + //eslint-disable-next-line + new DeveloperFeatures() + + const electronCompile = require('electron-compile') + + t.is(electronCompile.enableLiveReload.callCount, 1) + t.is(electronCompile.enableLiveReload.firstCall.args[0].strategy, 'react-hmr') +}) + +test('it attempts to install react dev tools', (t) => { + mockery.registerMock('electron', electronMainMock) + mockery.registerMock('devtron', { install: sinon.spy() }) + mockery.registerMock('electron-devtools-installer', { __esModule: true, default: sinon.stub(), REACT_DEVELOPER_TOOLS: 'react-tools' }) + + const electronDevtoolsInstaller = require('electron-devtools-installer') + electronDevtoolsInstaller.default.returns(Promise.resolve()) + + const { DeveloperFeatures } = require('../../src/main/developer') + //eslint-disable-next-line + new DeveloperFeatures() + + t.is(electronDevtoolsInstaller.default.callCount, 1) + t.is(electronDevtoolsInstaller.default.firstCall.args[0], 'react-tools') +}) diff --git a/spec/setup.js b/spec/setup.js index 56b2fa6..9ebc6d3 100644 --- a/spec/setup.js +++ b/spec/setup.js @@ -1,12 +1,16 @@ import mockery from 'mockery' import { logMock } from './__mocks__/electron-log' +import { electronDevtoolsInstallerMock } from './__mocks__/electron-devtools-installer' +import { electronCompileMock } from './__mocks__/electron-compile' import { electronMock } from './__mocks__/electron' import { windowStateMock } from './__mocks__/electron-window-state' export function setup (test) { test.before((t) => { - mockery.enable() + mockery.enable({ + useCleanCache: true + }) mockery.warnOnUnregistered(false) }) @@ -16,6 +20,8 @@ export function setup (test) { mockery.registerMock('electron-window-state', windowStateMock) mockery.registerMock('electron', electronMock) mockery.registerMock('electron-log', logMock) + mockery.registerMock('electron-devtools-installer', electronDevtoolsInstallerMock) + mockery.registerMock('electron-compile', electronCompileMock) }) test.after((t) => { diff --git a/src/main/developer.js b/src/main/developer.js index 5114e9b..473fa45 100644 --- a/src/main/developer.js +++ b/src/main/developer.js @@ -1,12 +1,22 @@ import { BrowserWindow } from 'electron' import devtron from 'devtron' +import * as electronCompile from 'electron-compile' +import installDevTools, { REACT_DEVELOPER_TOOLS } from 'electron-devtools-installer' import { logger } from '../logger' export class DeveloperFeatures { constructor () { this.extensions = BrowserWindow.getDevToolsExtensions() + this.enableHMR() this.installDevtron() + this.installReactTools() + } + + enableHMR () { + electronCompile.enableLiveReload({ + strategy: 'react-hmr' + }) } installDevtron () { @@ -17,4 +27,9 @@ export class DeveloperFeatures { devtron.install() } } + + installReactTools () { + installDevTools(REACT_DEVELOPER_TOOLS) + .catch((err) => logger.error('Failed to install React dev tools', err)) + } } diff --git a/src/main/main.js b/src/main/main.js index 445d86a..b17cc1f 100644 --- a/src/main/main.js +++ b/src/main/main.js @@ -36,6 +36,12 @@ export class App { this.onReady() } }) + + // Fancy scrollbars + app.commandLine.appendSwitch('enable-smooth-scrolling', '1') + app.commandLine.appendSwitch('enable-overlay-scrollbar', '1') + // Fix HDPI zoom bug + app.commandLine.appendSwitch('enable-use-zoom-for-dsf', 'false') } onReady () { diff --git a/src/main/window-manager.js b/src/main/window-manager.js index 6703409..3390f3b 100644 --- a/src/main/window-manager.js +++ b/src/main/window-manager.js @@ -19,7 +19,8 @@ class WindowManager { x: mainWindowState.x, y: mainWindowState.y, width: mainWindowState.width, - height: mainWindowState.height + height: mainWindowState.height, + minHeight: 450 }) mainWindowState.manage(browserWindow) diff --git a/src/renderer/components/App.jsx b/src/renderer/components/App.jsx new file mode 100644 index 0000000..f8d28f4 --- /dev/null +++ b/src/renderer/components/App.jsx @@ -0,0 +1,98 @@ +import React from 'react' + +import { InstallingOverlay } from './InstallingOverlay' +import { LeftPanel } from './LeftPanel' +import { RightPanel } from './RightPanel' + +import { Installer } from '../lib/Installer' +import getInstalledVersion from '../lib/check-node' +import getVersions from '../lib/versions' + +const INSTALL_STATUS = { + NOT_INSTALLING: 0, + INSTALLING: 1, + SUCCESS: 2, + ERROR: 3 +} + +export class App extends React.Component { + state = { + installStatus: INSTALL_STATUS.NOT_INSTALLING, + installError: null, + versions: null, + currentVersion: null + } + + componentDidMount () { + this.loadVersionList() + getInstalledVersion((err, version) => { + if (!err) { + this.setState({ + currentVersion: version + }) + } + }) + } + + cancelInstall = () => { + if (this._currentInstalller) { + this._currentInstalller.cancel() + } + this.setState({ + installStatus: INSTALL_STATUS.NOT_INSTALLING, + installError: null + }) + } + + installVersion = (version) => { + this.setState({ + installStatus: INSTALL_STATUS.INSTALLING + }) + const installer = new Installer(version) + this._currentInstalller = installer + installer.on('error', (err) => { + console.error(err) + this.setState({ + installStatus: INSTALL_STATUS.ERROR, + installError: err + }) + }) + installer.on('done', () => { + this.setState({ + currentVersion: version, + installStatus: INSTALL_STATUS.SUCCESS + }) + delete this._currentInstalller + }) + installer.install() + } + + loadVersionList () { + window.fetch('https://nodejs.org/dist/index.json') + .then(r => r.json()) + .then(getVersions) + .then((versions) => { + this.setState({ + versions + }) + }) + } + + render () { + return ( +
{this.props.error.message}
+ : null + } + + { + this.props.installing + ? 'Cancel Install' + : 'Return to installer' + } + +
+ Installed version:
+++ {this.state.example.code} + +
+ Open source workshops that teach web software skills.
+ Do them on your own or at a workshop nearby.
+ https://nodeschool.io/
+
Installed version:
-Open source workshops that teach web software skills.
Do them on your own or at a workshop nearby.
http://nodeschool.io/
-
-
-
-