diff --git a/cli/api.ts b/cli/api.ts index 832b7328..1ee95365 100644 --- a/cli/api.ts +++ b/cli/api.ts @@ -1,10 +1,11 @@ -const fs = require('fs'); -const os = require('os'); -const got = require('got'); -const registryUrl = require('registry-url')(); -const pify = require('pify'); -const recast = require('recast'); -const path = require('path'); +import fs from 'fs'; +import os from 'os'; +import got from 'got'; +import registryUrlModule from 'registry-url'; +const registryUrl = registryUrlModule(); +import pify from 'pify'; +import * as recast from 'recast'; +import path from 'path'; // If the user defines XDG_CONFIG_HOME they definitely want their config there, // otherwise use the home directory in linux/mac and userdata in windows @@ -12,12 +13,12 @@ const applicationDirectory = process.env.XDG_CONFIG_HOME !== undefined ? path.join(process.env.XDG_CONFIG_HOME, 'hyper') : process.platform == 'win32' - ? path.join(process.env.APPDATA, 'Hyper') + ? path.join(process.env.APPDATA!, 'Hyper') : os.homedir(); const devConfigFileName = path.join(__dirname, `../.hyper.js`); -let fileName = +const fileName = process.env.NODE_ENV !== 'production' && fs.existsSync(devConfigFileName) ? devConfigFileName : path.join(applicationDirectory, '.hyper.js'); @@ -27,16 +28,16 @@ let fileName = * statically analyze the hyper configuration isn't fatal for all kinds of * subcommands. We can use memoization to make reading and parsing lazy. */ -function memoize(fn) { +function memoize any>(fn: T): T { let hasResult = false; - let result; - return (...args) => { + let result: any; + return ((...args: any[]) => { if (!hasResult) { result = fn(...args); hasResult = true; } return result; - }; + }) as T; } const getFileContents = memoize(() => { @@ -51,18 +52,18 @@ const getFileContents = memoize(() => { return null; }); -const getParsedFile = memoize(() => recast.parse(getFileContents())); +const getParsedFile = memoize(() => recast.parse(getFileContents()!)); -const getProperties = memoize(() => getParsedFile().program.body.map(obj => obj)); +const getProperties = memoize(() => (getParsedFile().program.body as any[]).map(obj => obj)); const getPlugins = memoize(() => { const properties = getProperties(); for (let i = 0; i < properties.length; i++) { - const rightProperties = Object.values(properties[i].expression.right.properties); + const rightProperties = Object.values(properties[i].expression.right.properties); for (let j = 0; j < rightProperties.length; j++) { const plugin = rightProperties[j]; if (plugin.key.name === 'plugins') { - return plugin.value.elements; + return plugin.value.elements as any[]; } } } @@ -71,11 +72,11 @@ const getPlugins = memoize(() => { const getLocalPlugins = memoize(() => { const properties = getProperties(); for (let i = 0; i < properties.length; i++) { - const rightProperties = Object.values(properties[i].expression.right.properties); + const rightProperties = Object.values(properties[i].expression.right.properties); for (let j = 0; j < rightProperties.length; j++) { const plugin = rightProperties[j]; if (plugin.key.name === 'localPlugins') { - return plugin.value.elements; + return plugin.value.elements as any[]; } } } @@ -85,7 +86,7 @@ function exists() { return getFileContents() !== undefined; } -function isInstalled(plugin, locally) { +function isInstalled(plugin: string, locally?: boolean) { const array = (locally ? getLocalPlugins() : getPlugins()) || []; if (array && Array.isArray(array)) { return array.some(entry => entry.value === plugin); @@ -97,32 +98,34 @@ function save() { return pify(fs.writeFile)(fileName, recast.print(getParsedFile()).code, 'utf8'); } -function existsOnNpm(plugin) { - const name = getPackageName(plugin); - return got.get(registryUrl + name.toLowerCase(), {timeout: 10000, json: true}).then(res => { - if (!res.body.versions) { - return Promise.reject(res); - } else { - return res; - } - }); -} - -function getPackageName(plugin) { +function getPackageName(plugin: string) { const isScoped = plugin[0] === '@'; const nameWithoutVersion = plugin.split('#')[0]; if (isScoped) { - return '@' + nameWithoutVersion.split('@')[1].replace('/', '%2f'); + return `@${nameWithoutVersion.split('@')[1].replace('/', '%2f')}`; } return nameWithoutVersion.split('@')[0]; } -function install(plugin, locally) { +function existsOnNpm(plugin: string) { + const name = getPackageName(plugin); + return got + .get(registryUrl + name.toLowerCase(), {timeout: 10000, responseType: 'json'}) + .then(res => { + if (!res.body.versions) { + return Promise.reject(res); + } else { + return res; + } + }); +} + +function install(plugin: string, locally?: boolean) { const array = (locally ? getLocalPlugins() : getPlugins()) || []; return existsOnNpm(plugin) - .catch(err => { + .catch((err: any) => { const {statusCode} = err; if (statusCode && (statusCode === 404 || statusCode === 200)) { return Promise.reject(`${plugin} not found on npm`); @@ -139,29 +142,24 @@ function install(plugin, locally) { }); } -function uninstall(plugin) { +function uninstall(plugin: string) { if (!isInstalled(plugin)) { return Promise.reject(`${plugin} is not installed`); } - const index = getPlugins().findIndex(entry => entry.value === plugin); - getPlugins().splice(index, 1); + const index = getPlugins()!.findIndex(entry => entry.value === plugin); + getPlugins()!.splice(index, 1); return save(); } function list() { if (Array.isArray(getPlugins())) { - return getPlugins() + return getPlugins()! .map(plugin => plugin.value) .join('\n'); } return false; } -module.exports.configPath = fileName; -module.exports.exists = exists; -module.exports.existsOnNpm = existsOnNpm; -module.exports.isInstalled = isInstalled; -module.exports.install = install; -module.exports.uninstall = uninstall; -module.exports.list = list; +export const configPath = fileName; +export {exists, existsOnNpm, isInstalled, install, uninstall, list}; diff --git a/cli/index.ts b/cli/index.ts index 272ffece..ed1703cc 100644 --- a/cli/index.ts +++ b/cli/index.ts @@ -1,23 +1,23 @@ // This is a CLI tool, using console is OK /* eslint no-console: 0 */ -const {spawn, exec} = require('child_process'); -const {isAbsolute, resolve} = require('path'); -const {existsSync} = require('fs'); -const {version} = require('../app/package'); -const pify = require('pify'); -const args = require('args'); -const chalk = require('chalk'); -const open = require('open'); -const columnify = require('columnify'); -const got = require('got'); -const ora = require('ora'); -const api = require('./api'); +import {spawn, exec} from 'child_process'; +import {isAbsolute, resolve} from 'path'; +import {existsSync} from 'fs'; +import {version} from '../app/package.json'; +import pify from 'pify'; +import args from 'args'; +import chalk from 'chalk'; +import open from 'open'; +import columnify from 'columnify'; +import got from 'got'; +import ora from 'ora'; +import * as api from './api'; const PLUGIN_PREFIX = 'hyper-'; -let commandPromise; +let commandPromise: Promise; -const assertPluginName = pluginName => { +const assertPluginName = (pluginName: string) => { if (!pluginName) { console.error(chalk.red('Plugin name is required')); process.exit(1); @@ -34,43 +34,58 @@ const checkConfig = () => { process.exit(1); }; -args.command(['i', 'install'], 'Install a plugin', (name, args_) => { - checkConfig(); - const pluginName = args_[0]; - assertPluginName(pluginName); - commandPromise = api - .install(pluginName) - .then(() => console.log(chalk.green(`${pluginName} installed successfully!`))) - .catch(err => console.error(chalk.red(err))); -}); +args.command( + 'install', + 'Install a plugin', + (name, args_) => { + checkConfig(); + const pluginName = args_[0]; + assertPluginName(pluginName); + commandPromise = api + .install(pluginName) + .then(() => console.log(chalk.green(`${pluginName} installed successfully!`))) + .catch((err: any) => console.error(chalk.red(err))); + }, + ['i', 'install'] +); -args.command(['u', 'uninstall', 'rm', 'remove'], 'Uninstall a plugin', (name, args_) => { - checkConfig(); - const pluginName = args_[0]; - assertPluginName(pluginName); - commandPromise = api - .uninstall(pluginName) - .then(() => console.log(chalk.green(`${pluginName} uninstalled successfully!`))) - .catch(err => console.log(chalk.red(err))); -}); +args.command( + 'uninstall', + 'Uninstall a plugin', + (name, args_) => { + checkConfig(); + const pluginName = args_[0]; + assertPluginName(pluginName); + commandPromise = api + .uninstall(pluginName) + .then(() => console.log(chalk.green(`${pluginName} uninstalled successfully!`))) + .catch(err => console.log(chalk.red(err))); + }, + ['u', 'uninstall', 'rm', 'remove'] +); -args.command(['ls', 'list'], 'List installed plugins', () => { - checkConfig(); - let plugins = api.list(); +args.command( + 'list', + 'List installed plugins', + () => { + checkConfig(); + const plugins = api.list(); - if (plugins) { - console.log(plugins); - } else { - console.log(chalk.red(`No plugins installed yet.`)); - } - process.exit(0); -}); + if (plugins) { + console.log(plugins); + } else { + console.log(chalk.red(`No plugins installed yet.`)); + } + process.exit(0); + }, + ['ls', 'list'] +); -const lsRemote = pattern => { +const lsRemote = (pattern?: string) => { // note that no errors are catched by this function const URL = `https://api.npms.io/v2/search?q=${(pattern && `${pattern}+`) || ''}keywords:hyper-plugin,hyper-theme`; return got(URL) - .then(response => JSON.parse(response.body).results) + .then(response => JSON.parse(response.body).results as any[]) .then(entries => entries.map(entry => entry.package)) .then(entries => entries.filter(entry => entry.name.indexOf(PLUGIN_PREFIX) === 0)) .then(entries => @@ -86,71 +101,91 @@ const lsRemote = pattern => { ); }; -args.command(['s', 'search'], 'Search for plugins on npm', (name, args_) => { - const spinner = ora('Searching').start(); - const query = args_[0] ? args_[0].toLowerCase() : ''; +args.command( + 'search', + 'Search for plugins on npm', + (name, args_) => { + const spinner = ora('Searching').start(); + const query = args_[0] ? args_[0].toLowerCase() : ''; - commandPromise = lsRemote(query) - .then(entries => { - if (entries.length === 0) { + commandPromise = lsRemote(query) + .then(entries => { + if (entries.length === 0) { + spinner.fail(); + console.error(chalk.red(`Your search '${query}' did not match any plugins`)); + console.error(`${chalk.red('Try')} ${chalk.green('hyper ls-remote')}`); + process.exit(1); + } else { + let msg = columnify(entries); + spinner.succeed(); + msg = msg.substring(msg.indexOf('\n') + 1); // remove header + console.log(msg); + } + }) + .catch(err => { spinner.fail(); - console.error(chalk.red(`Your search '${query}' did not match any plugins`)); - console.error(`${chalk.red('Try')} ${chalk.green('hyper ls-remote')}`); - process.exit(1); - } else { + console.error(chalk.red(err)); // TODO + }); + }, + ['s', 'search'] +); + +args.command( + 'list-remote', + 'List plugins available on npm', + () => { + const spinner = ora('Searching').start(); + + commandPromise = lsRemote() + .then(entries => { let msg = columnify(entries); + spinner.succeed(); msg = msg.substring(msg.indexOf('\n') + 1); // remove header console.log(msg); - } - }) - .catch(err => { - spinner.fail(); - console.error(chalk.red(err)); // TODO - }); -}); + }) + .catch(err => { + spinner.fail(); + console.error(chalk.red(err)); // TODO + }); + }, + ['lsr', 'list-remote', 'ls-remote'] +); -args.command(['lsr', 'list-remote', 'ls-remote'], 'List plugins available on npm', () => { - const spinner = ora('Searching').start(); +args.command( + 'docs', + 'Open the npm page of a plugin', + (name, args_) => { + const pluginName = args_[0]; + assertPluginName(pluginName); + open(`http://ghub.io/${pluginName}`, {wait: false, url: true}); + process.exit(0); + }, + ['d', 'docs', 'h', 'home'] +); - commandPromise = lsRemote() - .then(entries => { - let msg = columnify(entries); +args.command( + 'version', + 'Show the version of hyper', + () => { + console.log(version); + process.exit(0); + }, + ['version'] +); - spinner.succeed(); - msg = msg.substring(msg.indexOf('\n') + 1); // remove header - console.log(msg); - }) - .catch(err => { - spinner.fail(); - console.error(chalk.red(err)); // TODO - }); -}); - -args.command(['d', 'docs', 'h', 'home'], 'Open the npm page of a plugin', (name, args_) => { - const pluginName = args_[0]; - assertPluginName(pluginName); - open(`http://ghub.io/${pluginName}`, {wait: false, url: true}); - process.exit(0); -}); - -args.command(['version'], 'Show the version of hyper', () => { - console.log(version); - process.exit(0); -}); - -args.command([''], 'Launch Hyper'); +args.command('', 'Launch Hyper'); args.option(['v', 'verbose'], 'Verbose mode', false); -const main = argv => { +const main = (argv: string[]) => { const flags = args.parse(argv, { name: 'hyper', version: false, mri: { boolean: ['v', 'verbose'] } - }); + } as any); if (commandPromise) { return commandPromise; @@ -168,7 +203,7 @@ const main = argv => { env['ELECTRON_ENABLE_LOGGING'] = '1'; } - const options = { + const options: any = { detached: true, env }; @@ -207,13 +242,13 @@ const main = argv => { return Promise.resolve(); }; -function eventuallyExit(code) { +function eventuallyExit(code: number) { setTimeout(() => process.exit(code), 100); } main(process.argv) .then(() => eventuallyExit(0)) - .catch(err => { + .catch((err: any) => { console.error(err.stack ? err.stack : err); eventuallyExit(1); }); diff --git a/package.json b/package.json index efc10757..0fa5a7b8 100644 --- a/package.json +++ b/package.json @@ -256,7 +256,7 @@ "color": "3.1.2", "columnify": "1.5.4", "css-loader": "3.2.1", - "got": "10.0.2", + "got": "10.0.4", "json-loader": "0.5.7", "mousetrap": "chabou/mousetrap#useCapture", "ms": "2.1.2", @@ -294,9 +294,12 @@ "@babel/plugin-proposal-object-rest-spread": "^7.7.4", "@babel/preset-react": "7.7.4", "@babel/preset-typescript": "7.7.4", + "@types/args": "3.0.0", "@types/color": "3.0.0", + "@types/columnify": "^1.5.0", "@types/mousetrap": "^1.6.3", "@types/node": "^12.12.15", + "@types/pify": "3.0.2", "@types/plist": "3.0.2", "@types/react": "^16.9.16", "@types/react-dom": "^16.9.4", diff --git a/tsconfig.json b/tsconfig.json index 9dc03b69..1ca87952 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,8 @@ "include": [ "./app/", "./lib/", - "./test/" + "./test/", + "./cli/" ], "references": [ { diff --git a/webpack.config.js b/webpack.config.js index 1def91ed..b4de0bb2 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -107,10 +107,10 @@ module.exports = [ mode: 'none', name: 'hyper-cli', resolve: { - extensions: ['.js', '.jsx', '.json'] + extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'] }, devtool: isProd ? 'none' : 'cheap-module-source-map', - entry: './cli/index.js', + entry: './cli/index.ts', output: { path: path.join(__dirname, 'bin'), filename: 'cli.js' @@ -118,7 +118,7 @@ module.exports = [ module: { rules: [ { - test: /\.(js|jsx)$/, + test: /\.(js|jsx|ts|tsx)$/, exclude: /node_modules/, loader: 'babel-loader' }, diff --git a/yarn.lock b/yarn.lock index 57e4918b..81227758 100644 --- a/yarn.lock +++ b/yarn.lock @@ -576,6 +576,11 @@ dependencies: defer-to-connect "^1.1.1" +"@types/args@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/args/-/args-3.0.0.tgz#2fd21cfbdaf5e1ed110bd239de42a82330d6ecf6" + integrity sha512-2A817ZtVj1/nD44MV0/U/R6xe3GM2n1WDdni4ioCuLjay6dE0bLJd5RafHC/ddqwXL1xa2RQUdJhsGZKyr3vpA== + "@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" @@ -605,6 +610,11 @@ dependencies: "@types/color-convert" "*" +"@types/columnify@^1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@types/columnify/-/columnify-1.5.0.tgz#94bb31f14c66bab6d968caa455ff13af1f0e7632" + integrity sha512-f39gUbf4NuSTVG9XEUJzYQ8L9e3l91jwywbJdU0fJixCRHKzQnyWxa9KfRPEuC3PhH5TIAi79kX2sAkXXSTB9Q== + "@types/debug@^4.1.4", "@types/debug@^4.1.5": version "4.1.5" resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.5.tgz#b14efa8852b7768d898906613c23f688713e02cd" @@ -674,6 +684,11 @@ resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA== +"@types/pify@3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/pify/-/pify-3.0.2.tgz#1bc75dac43e31dba981c37e0a08edddc1b49cd39" + integrity sha512-a5AKF1/9pCU3HGMkesgY6LsBdXHUY3WU+I2qgpU0J+I8XuJA1aFr59eS84/HP0+dxsyBSNbt+4yGI2adUpHwSg== + "@types/plist@3.0.2": version "3.0.2" resolved "https://registry.yarnpkg.com/@types/plist/-/plist-3.0.2.tgz#61b3727bba0f5c462fe333542534a0c3e19ccb01" @@ -4247,10 +4262,10 @@ globule@^1.0.0: lodash "~4.17.10" minimatch "~3.0.2" -got@10.0.2: - version "10.0.2" - resolved "https://registry.yarnpkg.com/got/-/got-10.0.2.tgz#f03040d966e0ff8da516d92e8689a3f9def44a3c" - integrity sha512-ojjUBCvrhkbEiQRAI8OIcSsCpM+EiVEX5xaoUAS6wFGlnoNa3KnDTiavRfAWXO9x29rA7sl2igh3E7z8glq9Gg== +got@10.0.4: + version "10.0.4" + resolved "https://registry.yarnpkg.com/got/-/got-10.0.4.tgz#d3a5a6cafd2f6c342d562513cc2d2d7b6afdcbb4" + integrity sha512-yMaRLGZJ7iINsDcZ8hso+v44IXVOejz7xrqEabSvUewdHS3zOf57IqU3sWIBYwHlekSrk+CC2PCeLzabOBxnVA== dependencies: "@sindresorhus/is" "^1.0.0" "@szmarczak/http-timer" "^3.1.1"