diff --git a/.eslintignore b/.eslintignore index d853ef6d..f177159b 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,6 +1,8 @@ build -app/dist +app/renderer app/static +app/bin +app/node_modules assets website -node_modules +bin diff --git a/.gitignore b/.gitignore index fa4e7c02..be68f684 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # build output dist +app/renderer # dependencies node_modules diff --git a/.npmrc b/.npmrc deleted file mode 100644 index cffe8cde..00000000 --- a/.npmrc +++ /dev/null @@ -1 +0,0 @@ -save-exact=true diff --git a/.travis.yml b/.travis.yml index 32f23d0a..7fa5d17c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,16 +5,16 @@ language: node_js matrix: include: - - os: osx - node_js: 7.4 - os: linux - node_js: 7.4 + node_js: 8.4.0 env: CC=clang CXX=clang++ npm_config_clang=1 compiler: clang addons: apt: packages: + - gcc-multilib + - g++-multilib - libgnome-keyring-dev - icnsutils - graphicsmagick @@ -24,11 +24,10 @@ addons: - snapd before_install: - - mkdir -p /tmp/git-lfs && curl -L https://github.com/github/git-lfs/releases/download/v1.2.1/git-lfs-$([ "$TRAVIS_OS_NAME" == "linux" ] && echo "linux" || echo "darwin")-amd64-1.2.1.tar.gz | tar -xz -C /tmp/git-lfs --strip-components 1 && /tmp/git-lfs/git-lfs pull - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then export DISPLAY=:99.0; sh -e /etc/init.d/xvfb start; sleep 3; fi - - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew update; fi - - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew install yarn; fi - - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo add-apt-repository ppa:snappy-dev/tools-proposed -y && sudo apt update && sudo apt install snapcraft; fi + - npm install -g yarn + +cache: yarn install: - yarn diff --git a/.yarnrc b/.yarnrc new file mode 100644 index 00000000..95b8581e --- /dev/null +++ b/.yarnrc @@ -0,0 +1 @@ +save-prefix false diff --git a/app/.npmrc b/app/.npmrc deleted file mode 100644 index cffe8cde..00000000 --- a/app/.npmrc +++ /dev/null @@ -1 +0,0 @@ -save-exact=true diff --git a/app/auto-updater.js b/app/auto-updater.js index 215a5bfe..beeb84c2 100644 --- a/app/auto-updater.js +++ b/app/auto-updater.js @@ -1,15 +1,15 @@ +// Packages const {autoUpdater} = require('electron'); const ms = require('ms'); +const retry = require('async-retry'); +// Utilities const notify = require('./notify'); // eslint-disable-line no-unused-vars const {version} = require('./package'); +const {getConfig} = require('./config'); + +const {platform} = process; -// accepted values: `osx`, `win32` -// https://nuts.gitbook.com/update-windows.html -const platform = process.platform === 'darwin' ? - 'osx' : - process.platform; -const FEED_URL = `https://hyper-updates.now.sh/update/${platform}`; let isInit = false; function init() { @@ -17,7 +17,28 @@ function init() { console.error('Error fetching updates', msg + ' (' + err.stack + ')'); }); - autoUpdater.setFeedURL(`${FEED_URL}/${version}`); + const config = retry(() => { + const content = getConfig(); + + if (!content) { + throw new Error('No config content loaded'); + } + + return content; + }); + + // Default to the "stable" update channel + let canaryUpdates = false; + + // If defined in the config, switch to the "canary" channel + if (config.updateChannel && config.updateChannel === 'canary') { + canaryUpdates = true; + } + + const updatePrefix = canaryUpdates ? 'releases-canary' : 'releases'; + const feedURL = `https://${updatePrefix}.hyper.is/update/${platform}`; + + autoUpdater.setFeedURL(`${feedURL}/${version}`); setTimeout(() => { autoUpdater.checkForUpdates(); diff --git a/app/config.js b/app/config.js index c3f4cbe0..6a7c613b 100644 --- a/app/config.js +++ b/app/config.js @@ -1,4 +1,4 @@ -const gaze = require('gaze'); +const chokidar = require('chokidar'); const notify = require('./notify'); const _import = require('./config/import'); const _openConfig = require('./config/open'); @@ -10,20 +10,23 @@ const watchers = []; // https://github.com/zeit/hyper/pull/1772 const watchCfg = process.platform === 'win32' ? {interval: 2000} : {}; let cfg = {}; +let _watcher; const _watch = function () { - gaze(cfgPath, watchCfg, function (err) { - if (err) { - throw err; - } - this.on('changed', () => { - cfg = _import(); - notify('Configuration updated', 'Hyper configuration reloaded!'); - watchers.forEach(fn => fn()); - }); - this.on('error', () => { - // Ignore file watching errors - }); + if (_watcher) { + return _watcher; + } + + _watcher = chokidar.watch(cfgPath, watchCfg); + + _watcher.on('change', () => { + cfg = _import(); + notify('Configuration updated', 'Hyper configuration reloaded!'); + watchers.forEach(fn => fn()); + }); + + _watcher.on('error', error => { + console.error('error watching config', error); }); }; diff --git a/app/config/config-default.js b/app/config/config-default.js index 7221c0df..e50ee254 100644 --- a/app/config/config-default.js +++ b/app/config/config-default.js @@ -4,6 +4,10 @@ module.exports = { config: { + // Choose either "stable" for receiving highly polished, + // or "canary" for less polished but more frequent updates + updateChannel: 'stable', + // default font size in pixels for all tabs fontSize: 12, @@ -116,5 +120,10 @@ module.exports = { // in development, you can create a directory under // `~/.hyper_plugins/local/` and include it here // to load it and avoid it being `npm install`ed - localPlugins: [] + localPlugins: [], + + keymaps: { + // Example + // 'window:devtools': 'cmd+alt+o', + } }; diff --git a/app/config/import.js b/app/config/import.js index 3f18678c..8f6635f2 100644 --- a/app/config/import.js +++ b/app/config/import.js @@ -1,5 +1,6 @@ const {writeFileSync, readFileSync} = require('fs'); -const {defaultCfg, cfgPath} = require('./paths'); +const {sync: mkdirpSync} = require('mkdirp'); +const {defaultCfg, cfgPath, plugs} = require('./paths'); const _init = require('./init'); const _keymaps = require('./keymaps'); @@ -15,13 +16,17 @@ const _write = function (path, data) { }; const _importConf = function () { + // init plugin directories if not present + mkdirpSync(plugs.base); + mkdirpSync(plugs.local); + try { const _defaultCfg = readFileSync(defaultCfg, 'utf8'); try { const _cfgPath = readFileSync(cfgPath, 'utf8'); return {userCfg: _cfgPath, defaultCfg: _defaultCfg}; } catch (err) { - _write(cfgPath, defaultCfg); + _write(cfgPath, _defaultCfg); return {userCfg: {}, defaultCfg: _defaultCfg}; } } catch (err) { diff --git a/app/config/init.js b/app/config/init.js index d761d558..155357f0 100644 --- a/app/config/init.js +++ b/app/config/init.js @@ -16,7 +16,6 @@ const _syntaxValidation = function (cfg) { } catch (err) { notify(`Error loading config: ${err.name}, see DevTools for more info`); console.error('Error loading config:', err); - return; } }; @@ -36,6 +35,9 @@ const _init = function (cfg) { notify('Error reading configuration: `config` key is missing'); return _extractDefault(cfg.defaultCfg); } + // Ignore undefined values in plugin and localPlugins array Issue #1862 + _cfg.plugins = (_cfg.plugins && _cfg.plugins.filter(Boolean)) || []; + _cfg.localPlugins = (_cfg.localPlugins && _cfg.localPlugins.filter(Boolean)) || []; return _cfg; } return _extractDefault(cfg.defaultCfg); diff --git a/app/config/open.js b/app/config/open.js index 9d7d0aa9..c344390d 100644 --- a/app/config/open.js +++ b/app/config/open.js @@ -4,7 +4,56 @@ const {cfgPath} = require('./paths'); module.exports = () => Promise.resolve(shell.openItem(cfgPath)); if (process.platform === 'win32') { - const exec = require('child_process').exec; + const Registry = require('winreg'); + const {exec} = require('child_process'); + + // Windows opens .js files with WScript.exe by default + // If the user hasn't set up an editor for .js files, we fallback to notepad. + const getFileExtKeys = () => new Promise((resolve, reject) => { + Registry({ + hive: Registry.HKCU, + key: '\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\FileExts\\.js' + }) + .keys((error, keys) => { + if (error) { + reject(error); + } else { + resolve(keys || []); + } + }); + }); + + const hasDefaultSet = async () => { + const keys = await getFileExtKeys(); + + const valueGroups = await Promise.all(keys.map(key => new Promise((resolve, reject) => { + key.values((error, items) => { + if (error) { + reject(error); + } + resolve(items.map(item => item.value || '') || []); + }); + }))); + + const values = valueGroups + .reduce((allValues, groupValues) => ([...allValues, ...groupValues]), []) + .filter(value => value && typeof value === 'string'); + + // No default app set + if (values.length === 0) { + return false; + } + + // WScript is in default apps list + if (values.some(value => value.includes('WScript.exe'))) { + const userDefaults = values.filter(value => value.endsWith('.exe') && !value.includes('WScript.exe')); + + // WScript.exe is overidden + return (userDefaults.length > 0); + } + + return true; + }; // This mimics shell.openItem, true if it worked, false if not. const openNotepad = file => new Promise(resolve => { @@ -13,21 +62,16 @@ if (process.platform === 'win32') { }); }); - // Windows opens .js files with WScript.exe by default - // If the user hasn't set up an editor for .js files, we fallback to notepad. - const canOpenNative = () => new Promise((resolve, reject) => { - exec('ftype JSFile', (error, stdout) => { - if (error) { - reject(error); - } else if (stdout && stdout.includes('WScript.exe')) { - reject(new Error('WScript is the default editor for .js files')); - } else { - resolve(true); + module.exports = () => hasDefaultSet() + .then(yes => { + if (yes) { + return shell.openItem(cfgPath); } + console.warn('No default app set for .js files, using notepad.exe fallback'); + return openNotepad(cfgPath); + }) + .catch(err => { + console.error('Open config with default app error:', err); + return openNotepad(cfgPath); }); - }); - - module.exports = () => canOpenNative() - .then(() => shell.openItem(cfgPath)) - .catch(() => openNotepad(cfgPath)); } diff --git a/app/config/paths.js b/app/config/paths.js index 60cf52d2..596ecca1 100644 --- a/app/config/paths.js +++ b/app/config/paths.js @@ -15,6 +15,14 @@ const devDir = resolve(__dirname, '../..'); const devCfg = join(devDir, cfgFile); const defaultCfg = resolve(__dirname, defaultCfgFile); +const plugins = resolve(cfgDir, '.hyper_plugins'); +const plugs = { + base: plugins, + local: resolve(plugins, 'local'), + cache: resolve(plugins, 'cache') +}; +const yarn = resolve(__dirname, '../../bin/yarn-standalone.js'); + const icon = resolve(__dirname, '../static/icon.png'); const keymapPath = resolve(__dirname, '../keymaps'); @@ -44,5 +52,5 @@ if (isDev) { } module.exports = { - cfgDir, cfgPath, cfgFile, defaultCfg, icon, defaultPlatformKeyPath + cfgDir, cfgPath, cfgFile, defaultCfg, icon, defaultPlatformKeyPath, plugs, yarn }; diff --git a/app/index.html b/app/index.html index a1309b99..04a2a4fa 100644 --- a/app/index.html +++ b/app/index.html @@ -31,7 +31,7 @@
- +