diff --git a/packages/cli/.eslintignore b/packages/cli/.eslintignore new file mode 100644 index 0000000000..a65b41774a --- /dev/null +++ b/packages/cli/.eslintignore @@ -0,0 +1 @@ +lib diff --git a/packages/cli/.eslintrc b/packages/cli/.eslintrc new file mode 100644 index 0000000000..68c6a917b5 --- /dev/null +++ b/packages/cli/.eslintrc @@ -0,0 +1,8 @@ +{ + "extends": [ + "oclif", + "oclif-typescript" + ], + "rules": { + } +} diff --git a/packages/cli/package.json b/packages/cli/package.json index 71f0905008..0892196f30 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -59,7 +59,6 @@ "devDependencies": { "@oclif/dev-cli": "^1.21.3", "@oclif/test": "^1.2.4", - "@oclif/tslint": "^3.1.1", "@types/ansi-styles": "^3.2.1", "@types/chai": "^4.1.7", "@types/debug": "^4.1.2", @@ -74,6 +73,9 @@ "@types/write-json-file": "^2.2.1", "aws-sdk": "^2.421.0", "chai": "^4.2.0", + "eslint": "^6.7.2", + "eslint-config-oclif": "^3.1.0", + "eslint-config-oclif-typescript": "^0.1.0", "globby": "^10.0.1", "lerna": "^3.18.0", "lodash": "^4.17.11", @@ -83,7 +85,6 @@ "read-pkg": "^4.0.1", "sinon": "^7.2.4", "ts-node": "^8.0.2", - "tslint": "^5.11.0", "typescript": "3.3.3333" }, "engines": { @@ -274,11 +275,13 @@ }, "repository": "heroku/cli", "scripts": { + "lint": "eslint . --ext .ts --config .eslintrc", "build": "rm -rf lib && tsc", "postpublish": "rm -f oclif.manifest.json", - "posttest": "tsc -p test --noEmit && tslint -p test -t stylish", "prepack": "yarn run build && oclif-dev manifest", + "pretest": "tsc -p test --noEmit", "test": "mocha --forbid-only \"test/**/*.test.ts\"", + "posttest": "yarn lint", "version": "oclif-dev readme --multi && git add README.md ../../docs" }, "types": "lib/index.d.ts" diff --git a/packages/cli/src/analytics.ts b/packages/cli/src/analytics.ts index ac65791b9e..8019de47ae 100644 --- a/packages/cli/src/analytics.ts +++ b/packages/cli/src/analytics.ts @@ -8,31 +8,33 @@ import deps from './deps' const debug = require('debug')('heroku:analytics') export interface RecordOpts { - Command: Config.Command.Class - argv: string[] + Command: Config.Command.Class; + argv: string[]; } export interface AnalyticsInterface { - source: string, - event: string, + source: string; + event: string; properties: { - cli: string, - command: string, - completion: number, - version: string, - plugin: string, - plugin_version: string, - os: string, - shell: string, - valid: boolean, - language: string, - install_id: string, - } + cli: string; + command: string; + completion: number; + version: string; + plugin: string; + plugin_version: string; + os: string; + shell: string; + valid: boolean; + language: string; + install_id: string; + }; } export default class AnalyticsCommand { config: Config.IConfig + userConfig!: typeof deps.UserConfig.prototype + http: typeof deps.HTTP constructor(config: Config.IConfig) { @@ -67,15 +69,14 @@ export default class AnalyticsCommand { valid: true, language: 'node', install_id: this.userConfig.install, - } + }, } const data = Buffer.from(JSON.stringify(analyticsData)).toString('base64') if (this.authorizationToken) { return this.http.get(`${this.url}?data=${data}`, {headers: {authorization: `Bearer ${this.authorizationToken}`}}).catch(error => debug(error)) - } else { - return this.http.get(`${this.url}?data=${data}`).catch(error => debug(error)) } + return this.http.get(`${this.url}?data=${data}`).catch(error => debug(error)) } get url(): string { @@ -92,7 +93,7 @@ export default class AnalyticsCommand { get usingHerokuAPIKey(): boolean { const k = process.env.HEROKU_API_KEY - return !!(k && k.length > 0) + return Boolean(k && k.length > 0) } get netrcLogin(): string | undefined { @@ -106,8 +107,8 @@ export default class AnalyticsCommand { async _acAnalytics(id: string): Promise<number> { if (id === 'autocomplete:options') return 0 - let root = path.join(this.config.cacheDir, 'autocomplete', 'completion_analytics') - let meta = { + const root = path.join(this.config.cacheDir, 'autocomplete', 'completion_analytics') + const meta = { cmd: deps.file.exists(path.join(root, 'command')), flag: deps.file.exists(path.join(root, 'flag')), value: deps.file.exists(path.join(root, 'value')), diff --git a/packages/cli/src/deps.ts b/packages/cli/src/deps.ts index 2d9e75a025..ca9d94c043 100644 --- a/packages/cli/src/deps.ts +++ b/packages/cli/src/deps.ts @@ -1,8 +1,16 @@ -import FS = require('fs-extra') import {HTTP} from 'http-call' +import UserConfig from './user-config' +import FS = require('fs-extra') import file = require('./file') -import UserConfig from './user-config' + +const cache: any = {} +function fetch(s: string) { + if (!cache[s]) { + cache[s] = require(s) + } + return cache[s] +} export default { get fs(): typeof FS { @@ -18,12 +26,3 @@ export default { return fetch('./user-config').default }, } - -const cache: any = {} - -function fetch(s: string) { - if (!cache[s]) { - cache[s] = require(s) - } - return cache[s] -} diff --git a/packages/cli/src/file.ts b/packages/cli/src/file.ts index 921f31517a..07545ed97c 100644 --- a/packages/cli/src/file.ts +++ b/packages/cli/src/file.ts @@ -6,8 +6,6 @@ import deps from './deps' const debug = require('debug')('heroku-cli:file') export function exists(f: string): Promise<boolean> { - // debug('exists', f) - // @ts-ignore return deps.fs.pathExists(f) } @@ -28,8 +26,8 @@ export async function remove(file: string) { } export async function ls(dir: string): Promise<{ path: string; stat: FS.Stats }[]> { - let files = await deps.fs.readdir(dir) - let paths = files.map(f => path.join(dir, f)) + const files = await deps.fs.readdir(dir) + const paths = files.map(f => path.join(dir, f)) return Promise.all(paths.map(path => deps.fs.stat(path).then(stat => ({path, stat})))) } @@ -37,14 +35,15 @@ export async function removeEmptyDirs(dir: string): Promise<void> { let files try { files = await ls(dir) - } catch (err) { - if (err.code === 'ENOENT') return - throw err + } catch (error) { + if (error.code === 'ENOENT') return + throw error } - let dirs = files.filter(f => f.stat.isDirectory()).map(f => f.path) - for (let p of dirs.map(removeEmptyDirs)) await p + const dirs = files.filter(f => f.stat.isDirectory()).map(f => f.path) + // eslint-disable-next-line no-await-in-loop + for (const p of dirs.map(removeEmptyDirs)) await p files = await ls(dir) - if (!files.length) await remove(dir) + if (files.length === 0) await remove(dir) } export async function readJSON(file: string) { diff --git a/packages/cli/src/global.d.ts b/packages/cli/src/global.d.ts index b5b4e7eed4..5cecf5b3bd 100644 --- a/packages/cli/src/global.d.ts +++ b/packages/cli/src/global.d.ts @@ -2,7 +2,7 @@ declare namespace NodeJS { interface Global { - columns?: number - testing?: boolean + columns?: number; + testing?: boolean; } } diff --git a/packages/cli/src/hooks/init/version.ts b/packages/cli/src/hooks/init/version.ts index b4286ce9cd..f3d9df9e0d 100644 --- a/packages/cli/src/hooks/init/version.ts +++ b/packages/cli/src/hooks/init/version.ts @@ -13,9 +13,9 @@ const Whitelist = [ export const version: Hook.Init = async function () { if (['-v', '--version', 'version'].includes(process.argv[2])) { - for (let env of Whitelist) { + for (const env of Whitelist) { if (process.env[env]) { - let value = env === 'HEROKU_API_KEY' ? 'to [REDACTED]' : `to ${process.env[env]}` + const value = env === 'HEROKU_API_KEY' ? 'to [REDACTED]' : `to ${process.env[env]}` this.warn(`${env} set ${value}`) } } diff --git a/packages/cli/src/hooks/update/b.ts b/packages/cli/src/hooks/update/b.ts index 7931e54625..3ab8968d30 100644 --- a/packages/cli/src/hooks/update/b.ts +++ b/packages/cli/src/hooks/update/b.ts @@ -13,8 +13,8 @@ function brew(args: string[], opts: SpawnSyncOptions = {}) { interface InstallReceipt { source: { - tap: string - } + tap: string; + }; } export const brewHook: Hook<'update'> = async function () { @@ -24,9 +24,9 @@ export const brewHook: Hook<'update'> = async function () { let binPath try { binPath = fs.realpathSync(path.join(brewRoot, 'bin/heroku')) - } catch (err) { - if (err.code === 'ENOENT') return - throw err + } catch (error) { + if (error.code === 'ENOENT') return + throw error } let cellarPath: string if (binPath && binPath.startsWith(path.join(brewRoot, 'Cellar'))) { @@ -39,7 +39,7 @@ export const brewHook: Hook<'update'> = async function () { } const needsMigrate = async (): Promise<boolean> => { - let receipt = await fetchInstallReceipt() + const receipt = await fetchInstallReceipt() if (!receipt) return false return receipt.source.tap === 'homebrew/core' } diff --git a/packages/cli/src/hooks/update/brew.ts b/packages/cli/src/hooks/update/brew.ts index 7931e54625..3ab8968d30 100644 --- a/packages/cli/src/hooks/update/brew.ts +++ b/packages/cli/src/hooks/update/brew.ts @@ -13,8 +13,8 @@ function brew(args: string[], opts: SpawnSyncOptions = {}) { interface InstallReceipt { source: { - tap: string - } + tap: string; + }; } export const brewHook: Hook<'update'> = async function () { @@ -24,9 +24,9 @@ export const brewHook: Hook<'update'> = async function () { let binPath try { binPath = fs.realpathSync(path.join(brewRoot, 'bin/heroku')) - } catch (err) { - if (err.code === 'ENOENT') return - throw err + } catch (error) { + if (error.code === 'ENOENT') return + throw error } let cellarPath: string if (binPath && binPath.startsWith(path.join(brewRoot, 'Cellar'))) { @@ -39,7 +39,7 @@ export const brewHook: Hook<'update'> = async function () { } const needsMigrate = async (): Promise<boolean> => { - let receipt = await fetchInstallReceipt() + const receipt = await fetchInstallReceipt() if (!receipt) return false return receipt.source.tap === 'homebrew/core' } diff --git a/packages/cli/src/hooks/update/plugin-migrate.ts b/packages/cli/src/hooks/update/plugin-migrate.ts index 261c574792..3eafbfa7a4 100644 --- a/packages/cli/src/hooks/update/plugin-migrate.ts +++ b/packages/cli/src/hooks/update/plugin-migrate.ts @@ -28,25 +28,27 @@ export const migrate: Hook<'init'> = async function () { const p = path.join(pluginsDir, 'user.json') if (await fs.pathExists(p)) { const {manifest} = await fs.readJSON(p) - for (let plugin of Object.keys(manifest.plugins)) { + for (const plugin of Object.keys(manifest.plugins)) { process.stderr.write(`heroku-cli: migrating ${plugin}\n`) + // eslint-disable-next-line no-await-in-loop await exec('heroku', ['plugins:install', plugin]) } } - } catch (err) { - this.warn(err) + } catch (error) { + this.warn(error) } try { const p = path.join(pluginsDir, 'link.json') if (await fs.pathExists(p)) { const {manifest} = await fs.readJSON(path.join(pluginsDir, 'link.json')) - for (let {root} of Object.values(manifest.plugins) as any) { + for (const {root} of Object.values(manifest.plugins) as any) { process.stderr.write(`heroku-cli: migrating ${root}\n`) + // eslint-disable-next-line no-await-in-loop await exec('heroku', ['plugins:link', root]) } } - } catch (err) { - this.warn(err) + } catch (error) { + this.warn(error) } await fs.remove(pluginsDir) process.stderr.write('heroku: done migrating plugins\n') diff --git a/packages/cli/src/hooks/update/tidy.ts b/packages/cli/src/hooks/update/tidy.ts index c9a32e0076..d97bbe46f9 100644 --- a/packages/cli/src/hooks/update/tidy.ts +++ b/packages/cli/src/hooks/update/tidy.ts @@ -5,13 +5,13 @@ import deps from '../../deps' export const tidy: Hook<'update'> = async function () { const cleanupPlugins = async () => { - let pluginsDir = path.join(this.config.dataDir, 'plugins') + const pluginsDir = path.join(this.config.dataDir, 'plugins') if (await deps.file.exists(path.join(pluginsDir, 'plugins.json'))) return let pjson try { pjson = await deps.file.readJSON(path.join(pluginsDir, 'package.json')) - } catch (err) { - if (err.code !== 'ENOENT') throw err + } catch (error) { + if (error.code !== 'ENOENT') throw error return } if (!pjson.dependencies || pjson.dependencies === {}) { diff --git a/packages/cli/src/user-config.ts b/packages/cli/src/user-config.ts index d9818dd41b..1e1f48bd75 100644 --- a/packages/cli/src/user-config.ts +++ b/packages/cli/src/user-config.ts @@ -4,27 +4,34 @@ import * as path from 'path' import deps from './deps' export interface ConfigJSON { - schema: 1 - install?: string - skipAnalytics?: boolean + schema: 1; + install?: string; + skipAnalytics?: boolean; } export default class UserConfig { private needsSave = false + private body!: ConfigJSON + private mtime?: number + private saving?: Promise<void> + private _init!: Promise<void> + // eslint-disable-next-line no-useless-constructor constructor(private readonly config: Config.IConfig) {} public get install() { return this.body.install || this.genInstall() } + public set install(install: string) { this.body.install = install this.needsSave = true } + public get skipAnalytics() { if (this.config.scopedEnvVar('SKIP_ANALYTICS') === '1') return true if (typeof this.body.skipAnalytics !== 'boolean') { @@ -37,7 +44,8 @@ export default class UserConfig { public async init() { await this.saving if (this._init) return this._init - return (this._init = (async () => { + + this._init = (async () => { this.debug('init') this.body = (await this.read()) || {schema: 1} @@ -51,7 +59,9 @@ export default class UserConfig { this.skipAnalytics if (this.needsSave) await this.save() - })()) + })() + + return this._init } private get debug() { @@ -78,17 +88,17 @@ export default class UserConfig { await this.migrate() try { this.mtime = await this.getLastUpdated() - let body = await deps.file.readJSON(this.file) + const body = await deps.file.readJSON(this.file) return body - } catch (err) { - if (err.code !== 'ENOENT') throw err + } catch (error) { + if (error.code !== 'ENOENT') throw error this.debug('not found') } } private async migrate() { if (await deps.file.exists(this.file)) return - let old = path.join(this.config.configDir, 'config.json') + const old = path.join(this.config.configDir, 'config.json') if (!await deps.file.exists(old)) return this.debug('moving config into new place') await deps.file.rename(old, this.file) @@ -103,8 +113,8 @@ export default class UserConfig { try { const stat = await deps.file.stat(this.file) return stat.mtime.getTime() - } catch (err) { - if (err.code !== 'ENOENT') throw err + } catch (error) { + if (error.code !== 'ENOENT') throw error } } diff --git a/packages/cli/test/acceptance/plugin.test.ts b/packages/cli/test/acceptance/plugin.test.ts index 36dc837f6b..6e50838052 100755 --- a/packages/cli/test/acceptance/plugin.test.ts +++ b/packages/cli/test/acceptance/plugin.test.ts @@ -11,7 +11,7 @@ const plugins = ['heroku-ps-exec'] const skipOnWindows = process.platform === 'win32' ? it.skip : it describe.skip('plugins', () => { - plugins.map(plugin => { + plugins.forEach(plugin => { skipOnWindows(plugin, async () => { const cwd = path.join(__dirname, '../../tmp/plugin', plugin) await fs.remove(cwd) diff --git a/packages/cli/test/acceptance/smoke.test.ts b/packages/cli/test/acceptance/smoke.test.ts index 08ef696ffe..97c690ef78 100755 --- a/packages/cli/test/acceptance/smoke.test.ts +++ b/packages/cli/test/acceptance/smoke.test.ts @@ -25,7 +25,7 @@ describe('smoke', () => { }) it('heroku apps', async () => { - let cmd = await run('apps') + const cmd = await run('apps') expect(cmd.stdout).to.match(/^===.*Apps/) }) @@ -43,7 +43,7 @@ describe('smoke', () => { }) it('asserts oclif plugins are in core', async () => { - let cmd = await run('plugins --core') + const cmd = await run('plugins --core') expect(cmd.stdout).to.contain('@oclif/plugin-commands') expect(cmd.stdout).to.contain('@oclif/plugin-help') expect(cmd.stdout).to.contain('@oclif/plugin-legacy') @@ -56,11 +56,11 @@ describe('smoke', () => { it('asserts monorepo plugins are in core', async () => { let paths = await globby(['packages/*/package.json']) - let cmd = await run('plugins --core') + const cmd = await run('plugins --core') paths = paths.map((p: string) => p.replace('packages/', '').replace('/package.json', '')) console.log(paths) paths = paths.filter((p: string) => p === 'cli') - paths.map((plugin: string) => { + paths.forEach((plugin: string) => { expect(cmd.stdout).to.contain(plugin) }) }) diff --git a/packages/cli/test/analytics.test.ts b/packages/cli/test/analytics.test.ts index bb2775d4d2..5e872d5ad2 100644 --- a/packages/cli/test/analytics.test.ts +++ b/packages/cli/test/analytics.test.ts @@ -8,19 +8,19 @@ import AnalyticsCommand, {AnalyticsInterface} from '../src/analytics' import UserConfig from '../src/user-config' function createBackboardMock(expectedGetter: (data: AnalyticsInterface) => any, actual: any) { - let backboard = nock('https://backboard.heroku.com/', { + const backboard = nock('https://backboard.heroku.com/', { reqheaders: { 'user-agent': '@oclif/command/1.5.6 darwin-x64 node-v10.2.1', - } + }, }) - .get('/hamurai') - .query(({data: analyticsData}: { data: string }) => { - const data: AnalyticsInterface = JSON.parse(Buffer.from(analyticsData, 'base64').toString()) - const expected = expectedGetter(data) - expect(expected).to.eq(actual) - return true - }) - .reply(200) + .get('/hamurai') + .query(({data: analyticsData}: { data: string }) => { + const data: AnalyticsInterface = JSON.parse(Buffer.from(analyticsData, 'base64').toString()) + const expected = expectedGetter(data) + expect(expected).to.eq(actual) + return true + }) + .reply(200) return backboard } @@ -36,9 +36,9 @@ async function runAnalyticsTest(expectedCbk: (data: AnalyticsInterface) => any, Login.plugin = {name: 'foo', version: '123'} as any Login.id = 'login' - let backboard = createBackboardMock(expectedCbk, actual) + const backboard = createBackboardMock(expectedCbk, actual) await analytics.record({ - Command: Login, argv: ['foo', 'bar'] + Command: Login, argv: ['foo', 'bar'], }) backboard.done() } @@ -52,10 +52,10 @@ describe('analytics (backboard has an error)', () => { }) it('does not show an error on console', async () => { - let backboard = nock('https://backboard.heroku.com/') - .get('/hamurai') - .query(() => true) - .reply(500) + const backboard = nock('https://backboard.heroku.com/') + .get('/hamurai') + .query(() => true) + .reply(500) const config = await Config.load() config.platform = 'win32' @@ -69,7 +69,7 @@ describe('analytics (backboard has an error)', () => { try { await analytics.record({ - Command: Login, argv: ['foo', 'bar'] + Command: Login, argv: ['foo', 'bar'], }) } catch { throw new Error('Expected analytics hook to 🦃 error') diff --git a/packages/cli/tslint.json b/packages/cli/tslint.json deleted file mode 100644 index f5703540c9..0000000000 --- a/packages/cli/tslint.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "@oclif/tslint" -}