diff --git a/app/config.js b/app/config.ts similarity index 87% rename from app/config.js rename to app/config.ts index 06bb119d..dfb8d572 100644 --- a/app/config.js +++ b/app/config.ts @@ -6,9 +6,32 @@ import win from './config/windows'; import {cfgPath, cfgDir} from './config/paths'; import {getColorMap} from './utils/colors'; -const watchers = []; -let cfg = {}; -let _watcher; +const watchers: any[] = []; +let cfg: Record = {}; +let _watcher: fs.FSWatcher; + +export const getDeprecatedCSS = (config: Record) => { + const deprecated: string[] = []; + const deprecatedCSS = ['x-screen', 'x-row', 'cursor-node', '::selection']; + deprecatedCSS.forEach(css => { + if ((config.css && config.css.includes(css)) || (config.termCSS && config.termCSS.includes(css))) { + deprecated.push(css); + } + }); + return deprecated; +}; + +const checkDeprecatedConfig = () => { + if (!cfg.config) { + return; + } + const deprecated = getDeprecatedCSS(cfg.config); + if (deprecated.length === 0) { + return; + } + const deprecatedStr = deprecated.join(', '); + notify('Configuration warning', `Your configuration uses some deprecated CSS classes (${deprecatedStr})`); +}; const _watch = () => { if (_watcher) { @@ -36,11 +59,10 @@ const _watch = () => { } else if (curr.mtime.getTime() !== prev.mtime.getTime()) { onChange(); } - }); + }) as any; return; } // macOS/Linux - setWatcher(); function setWatcher() { try { _watcher = fs.watch(cfgPath, eventType => { @@ -61,9 +83,10 @@ const _watch = () => { console.error('error watching config', error); }); } + setWatcher(); }; -export const subscribe = fn => { +export const subscribe = (fn: Function) => { watchers.push(fn); return () => { watchers.splice(watchers.indexOf(fn), 1); @@ -83,7 +106,7 @@ export const openConfig = () => { return _openConfig(); }; -export const getPlugins = () => { +export const getPlugins = (): {plugins: string[]; localPlugins: string[]} => { return { plugins: cfg.plugins, localPlugins: cfg.localPlugins @@ -104,40 +127,16 @@ export const getWin = win.get; export const winRecord = win.recordState; export const windowDefaults = win.defaults; -const getDeprecatedCSS = config => { - const deprecated = []; - const deprecatedCSS = ['x-screen', 'x-row', 'cursor-node', '::selection']; - deprecatedCSS.forEach(css => { - if ((config.css && config.css.includes(css)) || (config.termCSS && config.termCSS.includes(css))) { - deprecated.push(css); - } - }); - return deprecated; -}; -export {getDeprecatedCSS}; - -const checkDeprecatedConfig = () => { - if (!cfg.config) { - return; - } - const deprecated = getDeprecatedCSS(cfg.config); - if (deprecated.length === 0) { - return; - } - const deprecatedStr = deprecated.join(', '); - notify('Configuration warning', `Your configuration uses some deprecated CSS classes (${deprecatedStr})`); -}; - -export const fixConfigDefaults = decoratedConfig => { - const defaultConfig = getDefaultConfig().config; +export const fixConfigDefaults = (decoratedConfig: any) => { + const defaultConfig = getDefaultConfig()?.config; decoratedConfig.colors = getColorMap(decoratedConfig.colors) || {}; // We must have default colors for xterm css. decoratedConfig.colors = Object.assign({}, defaultConfig.colors, decoratedConfig.colors); return decoratedConfig; }; -export const htermConfigTranslate = config => { - const cssReplacements = { +export const htermConfigTranslate = (config: Record) => { + const cssReplacements: Record = { 'x-screen x-row([ {.[])': '.xterm-rows > div$1', '.cursor-node([ {.[])': '.terminal-cursor$1', '::selection([ {.[])': '.terminal .xterm-selection div$1', diff --git a/app/ext-modules.d.ts b/app/ext-modules.d.ts index cbe6f667..443d58ee 100644 --- a/app/ext-modules.d.ts +++ b/app/ext-modules.d.ts @@ -1,3 +1,8 @@ declare module 'git-describe' { export function gitDescribe(...args: any[]): void; } + +declare module 'default-shell' { + const val: string; + export default val; +} diff --git a/app/notifications.js b/app/notifications.ts similarity index 84% rename from app/notifications.js rename to app/notifications.ts index 0167489b..0a92cc36 100644 --- a/app/notifications.js +++ b/app/notifications.ts @@ -1,12 +1,13 @@ import ms from 'ms'; import fetch from 'electron-fetch'; -import {version} from './package'; +import {version} from './package.json'; +import {BrowserWindow} from 'electron'; const NEWS_URL = 'https://hyper-news.now.sh'; -export default function fetchNotifications(win) { +export default function fetchNotifications(win: BrowserWindow) { const {rpc} = win; - const retry = err => { + const retry = (err?: any) => { setTimeout(() => fetchNotifications(win), ms('30m')); if (err) { //eslint-disable-next-line no-console diff --git a/app/notify.js b/app/notify.ts similarity index 88% rename from app/notify.js rename to app/notify.ts index b003397f..8c63a8e2 100644 --- a/app/notify.js +++ b/app/notify.ts @@ -2,14 +2,28 @@ import {resolve} from 'path'; import {app, BrowserWindow} from 'electron'; import isDev from 'electron-is-dev'; -let win; +let win: BrowserWindow; // the hack of all hacks // electron doesn't have a built in notification thing, // so we launch a window on which we can use the // HTML5 `Notification` API :'( -let buffer = []; +let buffer: string[][] = []; + +function notify(title: string, body = '', details: any = {}) { + //eslint-disable-next-line no-console + console.log(`[Notification] ${title}: ${body}`); + if (details.error) { + //eslint-disable-next-line no-console + console.error(details.error); + } + if (win) { + win.webContents.send('notification', {title, body}); + } else { + buffer.push([title, body]); + } +} app.on('ready', () => { const win_ = new BrowserWindow({ @@ -25,22 +39,8 @@ app.on('ready', () => { buffer.forEach(([title, body]) => { notify(title, body); }); - buffer = null; + buffer = []; }); }); -function notify(title, body, details = {}) { - //eslint-disable-next-line no-console - console.log(`[Notification] ${title}: ${body}`); - if (details.error) { - //eslint-disable-next-line no-console - console.error(details.error); - } - if (win) { - win.webContents.send('notification', {title, body}); - } else { - buffer.push([title, body]); - } -} - export default notify; diff --git a/app/plugins.js b/app/plugins.ts similarity index 87% rename from app/plugins.js rename to app/plugins.ts index 98277929..da62f360 100644 --- a/app/plugins.js +++ b/app/plugins.ts @@ -1,4 +1,5 @@ -import {app, dialog} from 'electron'; +/* eslint-disable @typescript-eslint/no-use-before-define */ +import {app, dialog, BrowserWindow, App} from 'electron'; import {resolve, basename} from 'path'; import {writeFileSync} from 'fs'; import Config from 'electron-store'; @@ -26,11 +27,11 @@ let paths = getPaths(); let id = getId(plugins); let modules = requirePlugins(); -function getId(plugins_) { +function getId(plugins_: any) { return JSON.stringify(plugins_); } -const watchers = []; +const watchers: Function[] = []; // we listen on configuration updates to trigger // plugin installation @@ -50,9 +51,10 @@ config.subscribe(() => { // so plugins can `require` them without needing their own version // https://github.com/zeit/hyper/issues/619 function patchModuleLoad() { + // eslint-disable-next-line @typescript-eslint/no-var-requires const Module = require('module'); const originalLoad = Module._load; - Module._load = function _load(modulePath) { + Module._load = function _load(modulePath: string) { // PLEASE NOTE: Code changes here, also need to be changed in // lib/utils/plugins.js switch (modulePath) { @@ -72,6 +74,7 @@ function patchModuleLoad() { case 'hyper/decorate': return Object; default: + // eslint-disable-next-line prefer-rest-params return originalLoad.apply(this, arguments); } }; @@ -95,7 +98,7 @@ function updatePlugins({force = false} = {}) { updating = true; syncPackageJSON(); const id_ = id; - install(err => { + install((err: any) => { updating = false; if (err) { @@ -185,7 +188,10 @@ if (cache.get('hyper.plugins') !== id || process.env.HYPER_FORCE_UPDATE) { const baseConfig = config.getConfig(); if (baseConfig['autoUpdatePlugins']) { // otherwise update plugins every 5 hours - setInterval(updatePlugins, ms(baseConfig['autoUpdatePlugins'] === true ? '5h' : baseConfig['autoUpdatePlugins'])); + setInterval( + updatePlugins, + ms(baseConfig['autoUpdatePlugins'] === true ? '5h' : (baseConfig['autoUpdatePlugins'] as string)) + ); } })(); @@ -210,15 +216,15 @@ function syncPackageJSON() { } } -function alert(message) { +function alert(message: string) { dialog.showMessageBox({ message, buttons: ['Ok'] }); } -function toDependencies(plugins_) { - const obj = {}; +function toDependencies(plugins_: {plugins: string[]}) { + const obj: Record = {}; plugins_.plugins.forEach(plugin => { const regex = /.(@|#)/; const match = regex.exec(plugin); @@ -237,7 +243,7 @@ function toDependencies(plugins_) { return obj; } -export const subscribe = fn => { +export const subscribe = (fn: Function) => { watchers.push(fn); return () => { watchers.splice(watchers.indexOf(fn), 1); @@ -263,11 +269,11 @@ export const getBasePaths = () => { return {path, localPath}; }; -function requirePlugins() { +function requirePlugins(): any[] { const {plugins: plugins_, localPlugins} = paths; - const load = path_ => { - let mod; + const load = (path_: string) => { + let mod: any; try { mod = require(path_); const exposed = mod && Object.keys(mod).some(key => availableExtensions.has(key)); @@ -304,7 +310,7 @@ function requirePlugins() { .filter(v => Boolean(v)); } -export const onApp = app_ => { +export const onApp = (app_: App) => { modules.forEach(plugin => { if (plugin.onApp) { try { @@ -318,7 +324,7 @@ export const onApp = app_ => { }); }; -export const onWindowClass = win => { +export const onWindowClass = (win: BrowserWindow) => { modules.forEach(plugin => { if (plugin.onWindowClass) { try { @@ -332,7 +338,7 @@ export const onWindowClass = win => { }); }; -export const onWindow = win => { +export const onWindow = (win: BrowserWindow) => { modules.forEach(plugin => { if (plugin.onWindow) { try { @@ -348,7 +354,7 @@ export const onWindow = win => { // decorates the base entity by calling plugin[key] // for all the available plugins -function decorateEntity(base, key, type) { +function decorateEntity(base: any, key: string, type: 'object' | 'function') { let decorated = base; modules.forEach(plugin => { if (plugin[key]) { @@ -370,16 +376,16 @@ function decorateEntity(base, key, type) { return decorated; } -function decorateObject(base, key) { +function decorateObject(base: any, key: string) { return decorateEntity(base, key, 'object'); } -function decorateClass(base, key) { +function decorateClass(base: any, key: string) { return decorateEntity(base, key, 'function'); } export const getDeprecatedConfig = () => { - const deprecated = {}; + const deprecated: Record = {}; const baseConfig = config.getConfig(); modules.forEach(plugin => { if (!plugin.decorateConfig) { @@ -404,11 +410,11 @@ export const getDeprecatedConfig = () => { return deprecated; }; -export const decorateMenu = tpl => { +export const decorateMenu = (tpl: any) => { return decorateObject(tpl, 'decorateMenu'); }; -export const getDecoratedEnv = baseEnv => { +export const getDecoratedEnv = (baseEnv: Record) => { return decorateObject(baseEnv, 'decorateEnv'); }; @@ -427,19 +433,19 @@ export const getDecoratedKeymaps = () => { return decoratedKeymaps; }; -export const getDecoratedBrowserOptions = defaults => { +export const getDecoratedBrowserOptions = (defaults: T): T => { return decorateObject(defaults, 'decorateBrowserOptions'); }; -export const decorateWindowClass = defaults => { +export const decorateWindowClass = (defaults: T): T => { return decorateObject(defaults, 'decorateWindowClass'); }; -export const decorateSessionOptions = defaults => { +export const decorateSessionOptions = (defaults: T): T => { return decorateObject(defaults, 'decorateSessionOptions'); }; -export const decorateSessionClass = Session => { +export const decorateSessionClass = (Session: T): T => { return decorateClass(Session, 'decorateSessionClass'); }; diff --git a/app/session.js b/app/session.ts similarity index 83% rename from app/session.js rename to app/session.ts index 8397c8c8..7835795a 100644 --- a/app/session.js +++ b/app/session.ts @@ -2,15 +2,16 @@ import {EventEmitter} from 'events'; import {StringDecoder} from 'string_decoder'; import defaultShell from 'default-shell'; import {getDecoratedEnv} from './plugins'; -import {productName, version} from './package'; +import {productName, version} from './package.json'; import * as config from './config'; +import {IPty, IWindowsPtyForkOptions, spawn as npSpawn} from 'node-pty'; const createNodePtyError = () => new Error( '`node-pty` failed to load. Typically this means that it was built incorrectly. Please check the `readme.md` to more info.' ); -let spawn; +let spawn: typeof npSpawn; try { spawn = require('node-pty').spawn; } catch (err) { @@ -33,7 +34,11 @@ const BATCH_MAX_SIZE = 200 * 1024; // with the window ID which is then stripped on the renderer process and this // overhead is reduced with batching. class DataBatcher extends EventEmitter { - constructor(uid) { + uid: string; + decoder: StringDecoder; + data!: string; + timeout!: NodeJS.Timeout | null; + constructor(uid: string) { super(); this.uid = uid; this.decoder = new StringDecoder('utf8'); @@ -46,7 +51,7 @@ class DataBatcher extends EventEmitter { this.timeout = null; } - write(chunk) { + write(chunk: Buffer) { if (this.data.length + chunk.length >= BATCH_MAX_SIZE) { // We've reached the max batch size. Flush it and start another one if (this.timeout) { @@ -72,8 +77,20 @@ class DataBatcher extends EventEmitter { } } +interface SessionOptions { + uid: string; + rows: number; + cols: number; + cwd: string; + shell: string; + shellArgs: string[]; +} export default class Session extends EventEmitter { - constructor(options) { + pty: IPty | null; + batcher: DataBatcher | null; + shell: string | null; + ended: boolean; + constructor(options: SessionOptions) { super(); this.pty = null; this.batcher = null; @@ -82,8 +99,9 @@ export default class Session extends EventEmitter { this.init(options); } - init({uid, rows, cols: columns, cwd, shell, shellArgs}) { - const osLocale = require('os-locale'); + init({uid, rows, cols: columns, cwd, shell, shellArgs}: SessionOptions) { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const osLocale = require('os-locale') as typeof import('os-locale'); const baseEnv = Object.assign( {}, process.env, @@ -106,7 +124,7 @@ export default class Session extends EventEmitter { const defaultShellArgs = ['--login']; - const options = { + const options: IWindowsPtyForkOptions = { cols: columns, rows, cwd, @@ -133,7 +151,7 @@ export default class Session extends EventEmitter { if (this.ended) { return; } - this.batcher.write(chunk); + this.batcher?.write(chunk as any); }); this.batcher.on('flush', data => { @@ -154,7 +172,7 @@ export default class Session extends EventEmitter { this.destroy(); } - write(data) { + write(data: string) { if (this.pty) { this.pty.write(data); } else { @@ -163,7 +181,7 @@ export default class Session extends EventEmitter { } } - resize({cols, rows}) { + resize({cols, rows}: {cols: number; rows: number}) { if (this.pty) { try { this.pty.resize(cols, rows); diff --git a/app/system-context-menu.js b/app/system-context-menu.ts similarity index 92% rename from app/system-context-menu.js rename to app/system-context-menu.ts index 06b1b6e7..96419769 100644 --- a/app/system-context-menu.js +++ b/app/system-context-menu.ts @@ -8,7 +8,7 @@ const regParts = [ {name: 'Icon', value: `${appPath}`} ]; -function addValues(hyperKey, commandKey, callback) { +function addValues(hyperKey: Registry.Registry, commandKey: Registry.Registry, callback: Function) { hyperKey.set(regParts[1].name, Registry.REG_SZ, regParts[1].value, error => { if (error) { //eslint-disable-next-line no-console @@ -30,7 +30,7 @@ function addValues(hyperKey, commandKey, callback) { }); } -export const add = callback => { +export const add = (callback: Function) => { const hyperKey = new Registry({hive: 'HKCU', key: regKey}); const commandKey = new Registry({ hive: 'HKCU', @@ -78,7 +78,7 @@ export const add = callback => { }); }; -export const remove = callback => { +export const remove = (callback: Function) => { new Registry({hive: 'HKCU', key: regKey}).destroy(err => { if (err) { //eslint-disable-next-line no-console diff --git a/app/ui/window.ts b/app/ui/window.ts index 99d45011..7bac1a0e 100644 --- a/app/ui/window.ts +++ b/app/ui/window.ts @@ -111,7 +111,7 @@ export function newWindow( } }); - function createSession(extraOptions = {}) { + function createSession(extraOptions: any = {}) { const uid = uuid.v4(); // remove the rows and cols, the wrong value of them will break layout when init create diff --git a/package.json b/package.json index 15cb4158..1bc376a3 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "@babel/preset-react": "7.7.4", "@babel/preset-typescript": "7.7.7", "@types/args": "3.0.0", + "@types/async-retry": "1.4.1", "@types/color": "3.0.0", "@types/columnify": "^1.5.0", "@types/electron-devtools-installer": "2.2.0", diff --git a/yarn.lock b/yarn.lock index f1adbad0..42963b04 100644 --- a/yarn.lock +++ b/yarn.lock @@ -596,6 +596,13 @@ resolved "https://registry.yarnpkg.com/@types/args/-/args-3.0.0.tgz#2fd21cfbdaf5e1ed110bd239de42a82330d6ecf6" integrity sha512-2A817ZtVj1/nD44MV0/U/R6xe3GM2n1WDdni4ioCuLjay6dE0bLJd5RafHC/ddqwXL1xa2RQUdJhsGZKyr3vpA== +"@types/async-retry@1.4.1": + version "1.4.1" + resolved "https://registry.yarnpkg.com/@types/async-retry/-/async-retry-1.4.1.tgz#3b136a707b7a850f4947a727eb0f7b473b601992" + integrity sha512-hDI5Ttk9SUmDLcD/Yl2VuWQRGYZjJ7aaJFeRlomUOz/iTKSE7yA55SwY87QwjiZgwhMlVAKoT1rl08UyQoheag== + dependencies: + "@types/retry" "*" + "@types/cacheable-request@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/@types/cacheable-request/-/cacheable-request-6.0.1.tgz#5d22f3dded1fd3a84c0bbeb5039a7419c2c91976" @@ -773,6 +780,11 @@ dependencies: "@types/node" "*" +"@types/retry@*": + version "0.12.0" + resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" + integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA== + "@types/seamless-immutable@7.1.11": version "7.1.11" resolved "https://registry.yarnpkg.com/@types/seamless-immutable/-/seamless-immutable-7.1.11.tgz#89250c3e2587a44c2a051f5798e6f29f0e91bbc9"