diff --git a/.babelrc b/.babelrc index a422e231..11ce00d5 100644 --- a/.babelrc +++ b/.babelrc @@ -1,5 +1,5 @@ { - "presets": ["es2015-node6/object-rest", "es2017", "react"], + "presets": ["es2015-node6/object-rest", "es2017", "stage-1", "react"], "plugins": [ "transform-class-properties", "transform-decorators-legacy", diff --git a/README.md b/README.md index 5d22353f..50ffa4c4 100644 --- a/README.md +++ b/README.md @@ -78,8 +78,7 @@ nvm use v6 ``` - Install `gulp-cli`: `npm i -g gulp-cli`. -- Go into the `xde/` directory where you cloned the Git repo and run `npm install`. -- Go into the `xde/app` directory and run `npm install`. +- Go into the `xde/` directory where you cloned the Git repo and run `yarn` or `npm install`. - Once that completes, run `npm start` from `xde/` to start the GUI. - If you get a watchman error, you may need to increase your "max_queued_events" limit. On linux you can find this at /proc/sys/fs/inotify/max_queued_events. -- If you get `ENOENT: no such file or directory, open '.../node_modules/electron-prebuilt/path.txt'`, run `cd node_modules/electron-prebuilt && node install.js` from `xde/`. See the issue here: https://github.com/electron-userland/electron-prebuilt/issues/76. +- If you get `ENOENT: no such file or directory, open '.../node_modules/electron/path.txt'`, run `cd node_modules/electron && node install.js` from `xde/`. See the issue here: https://github.com/electron-userland/electron-prebuilt/issues/76. diff --git a/app/web/index.html b/app/web/index.html index d55fc6b6..e986b920 100644 --- a/app/web/index.html +++ b/app/web/index.html @@ -58,20 +58,16 @@ document.getElementById('app-loading').addEventListener('click', () => { remote.getCurrentWindow().openDevTools(); }, false); - - const React = require('react'); - const ReactDOM = require('react-dom'); - const App = require('../build/ui/App'); - let rootElement = React.createElement(App, { - segment: analytics, - }); - let rootNode = document.getElementById('app'); - ReactDOM.render(rootElement, rootNode); - - window.addEventListener('beforeunload', () => { - ReactDOM.unmountComponentAtNode(rootNode); - }); })(); + diff --git a/gulp/build-tasks.js b/gulp/build-tasks.js index 1a021f6a..c18a2ad6 100644 --- a/gulp/build-tasks.js +++ b/gulp/build-tasks.js @@ -1,6 +1,6 @@ import 'instapromise'; -import electronPrebuilt from 'electron-prebuilt'; +import electron from 'electron'; import { installNodeHeaders, rebuildNativeModules, @@ -8,10 +8,7 @@ import { } from 'electron-rebuild'; import fs from 'fs'; import gulp from 'gulp'; -import babel from 'gulp-babel'; -import changed from 'gulp-changed'; import rename from 'gulp-rename'; -import sourcemaps from 'gulp-sourcemaps'; import logger from 'gulplog'; import path from 'path'; import rimraf from 'rimraf'; @@ -27,44 +24,32 @@ const paths = { }; let tasks = { - async buildNativeModules() { - let shouldRebuild = await shouldRebuildNativeModules(electronPrebuilt); - if (!shouldRebuild) { - return; - } - - let versionResult = await spawnAsync(electronPrebuilt, ['--version']); - let electronVersion = /v(\d+\.\d+\.\d+)/.exec(versionResult.stdout)[1]; - - // When Node and Electron share the same ABI version again (discussion here: - // https://github.com/electron/electron/issues/5851) we can remove this - // check and rely solely on shouldRebuildNativeModules again - let hasHeaders = await hasNodeHeadersAsync(electronVersion); - if (hasHeaders) { - return; - } + buildNativeModules(force = false) { + return async function() { + let shouldRebuild = await shouldRebuildNativeModules(electron); + if (!shouldRebuild && !force) { + return; + } - logger.info(`Rebuilding native Node modules for Electron ${electronVersion}...`); - await installNodeHeaders(electronVersion); - await rebuildNativeModules(electronVersion, paths.nodeModules); - }, + let versionResult = await spawnAsync(electron, ['--version']); + let electronVersion = /v(\d+\.\d+\.\d+)/.exec(versionResult.stdout)[1]; - babel() { - return gulp.src(paths.source.js) - .pipe(changed(paths.build)) - .pipe(sourcemaps.init()) - .pipe(babel()) - .pipe(sourcemaps.write('__sourcemaps__')) - .pipe(gulp.dest(paths.build)); - }, + // When Node and Electron share the same ABI version again (discussion here: + // https://github.com/electron/electron/issues/5851) we can remove this + // check and rely solely on shouldRebuildNativeModules again + let hasHeaders = await hasNodeHeadersAsync(electronVersion); + if (hasHeaders && !force) { + return; + } - watchBabel(done) { - gulp.watch(paths.source.js, tasks.babel); - done(); + logger.info(`Rebuilding native Node modules for Electron ${electronVersion}...`); + await installNodeHeaders(electronVersion); + await rebuildNativeModules(electronVersion, paths.nodeModules); + }; }, icon() { - let contentsPath = path.dirname(path.dirname(electronPrebuilt)); + let contentsPath = path.dirname(path.dirname(electron)); let resourcesPath = path.join(contentsPath, 'Resources'); return gulp.src(paths.macIcon) .pipe(rename('electron.icns')) diff --git a/gulpfile.babel.js b/gulpfile.babel.js index 0edf8791..4131da9e 100644 --- a/gulpfile.babel.js +++ b/gulpfile.babel.js @@ -25,37 +25,22 @@ function getReleaseTask(platforms) { }; } -gulp.task('build:deploy', tasks.babel); -gulp.task('build', gulp.parallel( - tasks.buildNativeModules, - tasks.babel, +gulp.task('rebuild', gulp.parallel( + tasks.buildNativeModules(), tasks.icon, )); -gulp.task('watch', gulp.parallel( - tasks.buildNativeModules, - gulp.series(tasks.babel, tasks.watchBabel), +gulp.task('rebuild:force', gulp.parallel( + tasks.buildNativeModules(true), tasks.icon, )); gulp.task('release', gulp.series( - tasks.clean, - tasks.babel, getReleaseTask(['mac', 'win', 'linux']), tasks.verifyMacApp, )); gulp.task('release:mac', gulp.series( - tasks.clean, - tasks.babel, getReleaseTask(['mac']), tasks.verifyMacApp, )); -gulp.task('release:windows', gulp.series( - tasks.clean, - tasks.babel, - getReleaseTask(['win']), -)); -gulp.task('release:linux', gulp.series( - tasks.clean, - tasks.babel, - getReleaseTask(['linux']), -)); +gulp.task('release:windows', getReleaseTask(['win'])); +gulp.task('release:linux', getReleaseTask(['linux'])); gulp.task('clean', tasks.clean); diff --git a/package.json b/package.json index 5012dc2f..1bfcdce0 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,24 @@ { "private": true, "scripts": { - "start": "gulp build && cross-env XDE_NPM_START=1 electron ./app", - "staging": "gulp build && cross-env XDE_NPM_START=1 EXPONENT_STAGING=1 electron ./app", + "start": "cross-env XDE_NPM_START=1 npm run _start", + "start-hot": "cross-env HOT=1 npm run start", + "start-staging": "cross-env XDE_NPM_START=1 EXPONENT_STAGING=1 npm run _start", + "start-staging-hot": "cross-env HOT=1 npm run start-staging", + "start-local": "cross-env XDE_NPM_START=1 EXPONENT_LOCAL=1 npm run _start", + "start-local-hot": "cross-env HOT=1 npm run start-local", + "_start": "concurrently --kill-others --raw \"npm run webpack\" \"npm run app\"", + "app": "gulp rebuild && electron ./app", + "webpack": "if [ -n \"$HOT\" ]; then npm run webpack-hot; else npm run webpack-dev; fi", + "webpack-dev": "webpack -w --env.dev", + "webpack-hot": "webpack --env.dev && cross-env BABEL_ENV=hot webpack-dev-server -w --env.hmr --env.dev", + "build": "cross-env NODE_ENV=production webpack --env.prod", + "postinstall": "cd ./app && yarn && cd ../ && npm run build", "lint": "eslint src", - "local": "gulp build && cross-env XDE_NPM_START=1 EXPONENT_LOCAL=1 electron ./app", - "dist": "gulp release", - "mac": "gulp release:mac", - "win": "gulp release:windows", - "linux": "gulp release:linux" + "dist": "gulp clean && npm run build && gulp release", + "mac": "gulp clean && npm run build && gulp release:mac", + "win": "gulp clean && npm run build && gulp release:windows", + "linux": "gulp clean && npm run build && gulp release:linux" }, "build": { "asar": false, @@ -46,30 +56,50 @@ "@ccheever/crayon": "^5.0.0", "@exponent/json-file": "^5.0.1", "@exponent/spawn-async": "^1.2.5", - "babel-eslint": "^7.0.0", - "babel-plugin-transform-class-properties": "^6.16.0", + "babel-core": "^6.18.0", + "babel-eslint": "^7.1.0", + "babel-loader": "^6.2.5", + "babel-plugin-dev-expression": "^0.2.1", + "babel-plugin-flow-react-proptypes": "^0.15.0", + "babel-plugin-transform-class-properties": "^6.18.0", "babel-plugin-transform-decorators-legacy": "^1.3.4", "babel-plugin-transform-object-rest-spread": "^6.16.0", "babel-plugin-transform-runtime": "^6.9.0", + "babel-preset-babili": "^0.0.5", + "babel-preset-es2015": "^6.18.0", "babel-preset-es2015-node6": "^0.3.0", "babel-preset-es2017": "^6.16.0", "babel-preset-react": "^6.5.0", + "babel-preset-react-optimize": "^1.0.1", + "babel-preset-stage-1": "^6.16.0", + "concurrently": "^3.1.0", "cross-env": "^3.0.0", - "electron-builder": "^7.10.2", - "electron-prebuilt": "^1.3.1", - "electron-rebuild": "^1.1.5", + "electron": "1.4.6", + "electron-builder": "^7.14.2", + "electron-devtools-installer": "^2.0.1", + "electron-rebuild": "^1.3.0", "eslint": "^3.1.1", "eslint-config-exponent": "^4.0.0", "eslint-plugin-babel": "^3.3.0", "eslint-plugin-react": "^6.3.0", - "gulp": "gulpjs/gulp#4.0", + "getenv": "^0.7.0", + "gulp": "git+https://github.com/gulpjs/gulp#4.0", "gulp-babel": "^6.1.2", "gulp-changed": "^1.3.0", + "gulp-plumber": "^1.1.0", "gulp-rename": "^1.2.2", "gulp-sourcemaps": "^1.6.0", "gulp-watch": "^4.3.6", "gulplog": "^1.0.0", "instapromise": "2.0.7-rc.1", - "rimraf": "^2.5.2" + "json-loader": "^0.5.4", + "react": "^15.3.2", + "react-dom": "^15.3.2", + "react-hot-loader": "3.0.0-beta.6", + "rimraf": "^2.5.2", + "source-map-support": "^0.4.5", + "webpack": "2.1.0-beta.25", + "webpack-dev-server": "2.1.0-beta.9", + "webpack-node-externals": "^1.5.4" } } diff --git a/src/main.js b/src/main.js index 28e8d58c..7dc5f465 100644 --- a/src/main.js +++ b/src/main.js @@ -59,6 +59,14 @@ if (!require('electron-squirrel-startup')) { }); app.on('ready', () => { + if (process.env.NODE_ENV === 'development') { + const devToolsInstaller = require('electron-devtools-installer'); + const { default: installExtension, REACT_DEVELOPER_TOOLS } = devToolsInstaller; + + installExtension(REACT_DEVELOPER_TOOLS) + .then((name) => console.log(`Added Extension: ${name}`)) + .catch((err) => console.log('An error occurred: ', err)); + } // Create the browser window. mainWindow = new BrowserWindow({ width: 1200, diff --git a/src/renderer-hot.js b/src/renderer-hot.js new file mode 100644 index 00000000..e7523f43 --- /dev/null +++ b/src/renderer-hot.js @@ -0,0 +1,51 @@ +/** + * @flow + */ + +import React from 'react'; +import ReactDOM from 'react-dom'; +import Redbox from 'redbox-react'; + +import { AppContainer } from 'react-hot-loader'; + +import App from './ui/App'; + +const rootNode = document.getElementById('app'); + +const render = () => { + if (window.HMR) { + ReactDOM.render( + + + , + rootNode + ); + } else { + ReactDOM.render( + , + rootNode + ); + } +}; + +window.addEventListener('beforeunload', () => { + ReactDOM.unmountComponentAtNode(rootNode); +}); + +if (window.HMR) { + // Hot Module Replacement API + if (module.hot) { + try { + // host re-render + // $FlowFixMe + module.hot.accept('./ui/App', render); + } + catch (error) { + // hot re-render failed. display a nice error page like inwebpack-hot-middleware + const RedBox = require('redbox-react'); + ReactDOM.render(, rootNode); + } + } +} + +render(); diff --git a/src/renderer.js b/src/renderer.js new file mode 100644 index 00000000..2caf2cb4 --- /dev/null +++ b/src/renderer.js @@ -0,0 +1,19 @@ +/** + * @flow + */ + +import React from 'react'; +import ReactDOM from 'react-dom'; + +import App from './ui/App'; + +const rootNode = document.getElementById('app'); + +ReactDOM.render( + , + rootNode +); + +window.addEventListener('beforeunload', () => { + ReactDOM.unmountComponentAtNode(rootNode); +}); diff --git a/src/ui/App.js b/src/ui/App.js index 68da414a..c61fceed 100644 --- a/src/ui/App.js +++ b/src/ui/App.js @@ -552,7 +552,7 @@ class App extends React.Component { }; async _versionStringAsync() { - let pkgJsonFile = new JsonFile(path.join(__dirname, '../../package.json')); + let pkgJsonFile = new JsonFile(path.join(__dirname, '../../app/package.json')); let versionString = await pkgJsonFile.getAsync('version'); return versionString; } diff --git a/src/ui/index.js b/src/ui/index.js new file mode 100644 index 00000000..39065d69 --- /dev/null +++ b/src/ui/index.js @@ -0,0 +1,2 @@ + +export { default as App } from './App'; diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 00000000..a1a548a8 --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,198 @@ +// Native +const path = require('path'); + +// Packages +const webpack = require('webpack'); +const nodeExternals = require('webpack-node-externals'); +const getenv = require('getenv'); + +const outputPath = path.join(__dirname, 'app', 'build'); +const nodeEnv = process.env.NODE_ENV || 'development'; + +module.exports = env => { + let babelConfig = { + cacheDirectory: true, + babelrc: false, + presets: ['es2017', 'stage-1', 'react'], + plugins: [ + 'flow-react-proptypes', + 'transform-es2015-destructuring', + 'transform-es2015-parameters', + 'transform-class-properties', + 'transform-decorators-legacy', + 'transform-runtime', + ], + }; + + if (env.prod) { + babelConfig = Object.assign({}, babelConfig, { + presets: [...babelConfig.presets, 'react-optimize'], + passPerPreset: true, + compact: true, + comments: false, + }); + } else if (env.hmr) { + babelConfig = Object.assign({}, babelConfig, { + presets: ['es2017', 'stage-1', 'react'], + plugins: [ + 'react-hot-loader/babel', + 'flow-react-proptypes', + 'transform-decorators-legacy', + 'transform-class-properties', + 'transform-es2015-classes', + 'transform-es2015-destructuring', + 'transform-es2015-parameters', + 'transform-runtime', + ], + }); + } + + const moduleConfig = { + rules: [ + { + test: /\.jsx?$/, + exclude: /node_modules/, + use: [{ + loader: 'babel-loader', + options: babelConfig, + }], + }, + { + test: /\.json/, + loader: 'json', + }, + ], + }; + + const commonPlugins = [ + new webpack.DefinePlugin({ + 'process.env': { + NODE_ENV: JSON.stringify('production'), + XDE_NPM_START: JSON.stringify(getenv.boolish('XDE_NPM_START', false)), + }, + }), + //prints more readable module names in the browser console on HMR updates + new webpack.NamedModulesPlugin(), + ]; + + const config = [ + { + name: 'renderer', + entry: './src/renderer.js', + target: 'electron-renderer', + output: { + path: outputPath, + filename: 'renderer.js', + }, + node: { + __dirname: false, + __filename: false, + }, + externals: [ + nodeExternals({ + modulesDir: './app/node_modules', + }), + ], + devtool: env.dev ? 'eval' : 'source-map', + module: moduleConfig, + resolve: { + extensions: [ + '.svg', + '.js', + '.jsx', + '.json', + ], + }, + plugins: [ + ...commonPlugins, + ], + }, + { + name: 'electron', + entry: './src/main.js', + target: 'electron-main', + output: { + path: outputPath, + filename: 'main.js', + }, + node: { + __dirname: false, + __filename: false, + }, + externals(context, request, callback) { + callback(null, request.startsWith('.') ? false : `require('${request}')`); + }, + devtool: env.dev ? 'inline-source-map' : 'source-map', + resolve: { + modules: [ + 'node_modules', + ], + }, + module: moduleConfig, + plugins: [ + new webpack.BannerPlugin({ + banner: 'require(\"source-map-support\").install();', + raw: true, + entryOnly: false, + }), + ...commonPlugins, + ], + }, + ]; + + // Hot Loading + if (env.hmr) { + const devServerPort = getenv.int('DEVSERVER_PORT', 8282); + + let rendererConfig = config[0]; + rendererConfig.entry = [ + 'react-hot-loader/patch', + config[0].entry, + ]; + + rendererConfig.output = Object.assign({}, rendererConfig.output, { + publicPath: `http://localhost:${devServerPort}/`, + }); + + rendererConfig.devServer = { + //activate hot reloading + hot: true, + //match the output path + contentBase: outputPath, + //match the output publicPath + publicPath: '/', + // dev server port + port: devServerPort, + }; + + rendererConfig.plugins = [ + //activates HMR + new webpack.HotModuleReplacementPlugin(), + new webpack.NoErrorsPlugin(), + ...rendererConfig.plugins, + ]; + + config[0] = rendererConfig; + } + + // Prod Setup + if (env.prod) { + // Stubbed here for future prod customizations + + const rendererConfig = config[0]; + const mainConfig = config[1]; + + rendererConfig.plugins = [ + ...rendererConfig.plugins, + ]; + + mainConfig.plugins = [ + ...mainConfig.plugins, + ]; + + config[0] = rendererConfig; + config[1] = mainConfig; + } + + return config; +};