From 003c74fbcbd13a592293831dabfa7f470e8ba16a Mon Sep 17 00:00:00 2001 From: Jeff Dickey <216188+jdxcode@users.noreply.github.com> Date: Sat, 3 Feb 2018 02:23:30 -0800 Subject: [PATCH] feat: move engine logic into config BREAKING CHANGE: this is a big refactor that consolidates the code from @anycli/config and @anycli/engine --- package.json | 8 +- src/command.ts | 150 +++++--- src/config.ts | 325 +++--------------- src/engine.ts | 37 -- src/hooks.ts | 10 +- src/index.ts | 16 +- src/manifest.ts | 44 +++ src/pjson.ts | 19 +- src/plugin.ts | 257 +++++++++++++- src/topic.ts | 2 +- src/ts_node.ts | 93 +++++ src/util.ts | 9 + test/config.test.ts | 6 +- test/fixtures/typescript/package.json | 1 - .../typescript/src/commands/foo/bar/baz.ts | 7 +- test/fixtures/typescript/src/plugins.ts | 3 - test/typescript.test.ts | 26 +- yarn.lock | 155 ++++++++- 18 files changed, 740 insertions(+), 428 deletions(-) delete mode 100644 src/engine.ts create mode 100644 src/manifest.ts create mode 100644 src/ts_node.ts create mode 100644 src/util.ts delete mode 100644 test/fixtures/typescript/src/plugins.ts diff --git a/package.json b/package.json index f9baa8d0..f0c20233 100644 --- a/package.json +++ b/package.json @@ -5,22 +5,28 @@ "author": "Jeff Dickey @jdxcode", "bugs": "https://github.com/anycli/config/issues", "dependencies": { + "cli-ux": "^3.3.13", "debug": "^3.1.0", "fs-extra": "^5.0.0", + "fs-extra-debug": "^1.0.4", + "globby": "^7.1.1", "load-json-file": "^4.0.0", "lodash": "^4.17.4", - "read-pkg": "^3.0.0" + "read-pkg": "^3.0.0", + "tslib": "^1.9.0" }, "devDependencies": { "@anycli/parser": "^3.0.4", "@anycli/tslint": "^0.2.3", "@types/chai": "^4.1.2", "@types/fs-extra": "^5.0.0", + "@types/globby": "^6.1.0", "@types/load-json-file": "^2.0.7", "@types/lodash": "^4.14.100", "@types/mocha": "^2.2.48", "@types/nock": "^9.1.2", "@types/node": "^9.4.0", + "@types/node-notifier": "^0.0.28", "@types/read-pkg": "^3.0.0", "chai": "^4.1.2", "concurrently": "^3.5.1", diff --git a/src/command.ts b/src/command.ts index 5107684f..5ce9e78b 100644 --- a/src/command.ts +++ b/src/command.ts @@ -1,10 +1,9 @@ import * as Parser from '@anycli/parser' +import * as _ from 'lodash' -import {IConfig} from './config' -import {IPlugin} from './plugin' +import * as Config from '.' -export interface ICommandBase { - _base: string +export interface Command { id: string hidden: boolean aliases: string[] @@ -12,63 +11,102 @@ export interface ICommandBase { title?: string usage?: string | string[] examples?: string[] - pluginName?: string type?: string + flags: {[name: string]: Command.Flag.Boolean | Command.Flag.Option} + args: { + name: string + description?: string + required?: boolean + hidden?: boolean + default?: string + options?: string[] + }[] } -export interface ICachedCommand extends ICommandBase { - flags: {[name: string]: ICachedFlag} - args: ICachedArg[] - load(): Promise -} - -export interface IConvertToCachedOptions { - id?: string - pluginName?: string -} +export namespace Command { + export namespace Flag { + export interface Boolean { + type: 'boolean' + name: string + required?: boolean + char?: string + hidden?: boolean + description?: string + } + export interface Option { + type: 'option' + name: string + required?: boolean + char?: string + hidden?: boolean + description?: string + helpValue?: string + default?: string + options?: string[] + } + } -export interface ICommand extends ICommandBase { - plugin?: IPlugin - flags?: Parser.flags.Input - args?: Parser.args.Input - new (argv: string[], opts: ICommandOptions): T - run(this: ICommand, argv: string[], opts?: Partial): Promise - convertToCached(opts?: IConvertToCachedOptions): ICachedCommand -} - -export interface ICommandOptions { - root?: string - config: IConfig -} + export interface Base { + _base: string + id: string + hidden: boolean + aliases: string[] + description?: string + title?: string + usage?: string | string[] + examples?: string[] + } -export interface ICachedArg { - name: string - description?: string - required?: boolean - hidden?: boolean - default?: string - options?: string[] -} + export interface Full extends Base { + plugin?: Config.IPlugin + flags?: Parser.flags.Input + args?: Parser.args.Input + run(argv: string[], config?: Config.Options): Promise + } -export interface ICachedBooleanFlag { - type: 'boolean' - name: string - required?: boolean - char?: string - hidden?: boolean - description?: string -} + export interface Plugin extends Command { + load(): Full + } -export interface ICachedOptionFlag { - type: 'option' - name: string - required?: boolean - char?: string - hidden?: boolean - description?: string - helpValue?: string - default?: string - options?: string[] + export function toCached(c: Full): Command { + return { + title: c.title, + id: c.id, + description: c.description, + usage: c.usage, + hidden: c.hidden, + aliases: c.aliases || [], + flags: _.mapValues(c.flags || {}, (flag, name) => { + if (flag.type === 'boolean') { + return { + name, + type: flag.type, + char: flag.char, + description: flag.description, + hidden: flag.hidden, + required: flag.required, + } + } + return { + name, + type: flag.type, + char: flag.char, + description: flag.description, + hidden: flag.hidden, + required: flag.required, + helpValue: flag.helpValue, + options: flag.options, + default: _.isFunction(flag.default) ? flag.default({options: {}, flags: {}}) : flag.default, + } + }), + args: c.args ? c.args.map(a => ({ + name: a.name, + description: a.description, + required: a.required, + options: a.options, + default: _.isFunction(a.default) ? a.default({}) : a.default, + hidden: a.hidden, + })) : {} as Command['args'], + } + } } - -export type ICachedFlag = ICachedBooleanFlag | ICachedOptionFlag diff --git a/src/config.ts b/src/config.ts index 732f5b05..9c1a2184 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,30 +1,15 @@ -import * as fs from 'fs-extra' -import * as readJSON from 'load-json-file' -import * as _ from 'lodash' import * as os from 'os' import * as path from 'path' -import * as readPkg from 'read-pkg' -import {inspect} from 'util' -import {IEngine} from './engine' -import {IPJSON} from './pjson' -import {ITopic} from './topic' - -const _pjson = require('../package.json') -const _base = `${_pjson.name}@${_pjson.version}` +import * as Plugin from './plugin' export type PlatformTypes = 'darwin' | 'linux' | 'win32' | 'aix' | 'freebsd' | 'openbsd' | 'sunos' export type ArchTypes = 'arm' | 'arm64' | 'mips' | 'mipsel' | 'ppc' | 'ppc64' | 's390' | 's390x' | 'x32' | 'x64' | 'x86' +export type Options = Plugin.Options | string | IConfig -export interface IConfig { - /** - * @anycli/config version - */ - _base: string - /** - * base path of root plugin - */ - root: string +const debug = require('debug')('@anycli/config') + +export interface IConfig extends Plugin.IPlugin { /** * process.arch */ @@ -39,32 +24,6 @@ export interface IConfig { * example ~/Library/Caches/mycli or ~/.cache/mycli */ cacheDir: string - /** - * full path to command dir of plugin - */ - commandsDir: string | undefined - /** - * full path to command dir of plugin's typescript files for development - */ - commandsDirTS?: string - /** - * normalized full paths to hooks - */ - hooks: {[k: string]: string[]} - /** - * normalized full paths to typescript hooks - */ - hooksTS?: {[k: string]: string[]} - /** - * if plugins points to a module this is the full path to that module - * - * for dynamic plugin loading - */ - pluginsModule: string | undefined - /** - * if plugins points to a module this is the full path to that module's typescript - */ - pluginsModuleTS: string | undefined /** * config directory to use for CLI * @@ -93,16 +52,6 @@ export interface IConfig { * example: /home/myuser */ home: string - /** - * CLI name from package.json - */ - name: string - /** - * full package.json - * - * parsed with read-pkg - */ - pjson: IPJSON /** * process.platform */ @@ -111,22 +60,12 @@ export interface IConfig { * active shell */ shell: string - /** - * parsed tsconfig.json - */ - tsconfig: TSConfig | undefined /** * user agent to use for http calls * * example: mycli/1.2.3 (darwin-x64) node-9.0.0 */ userAgent: string - /** - * cli version from package.json - * - * example: 1.2.3 - */ - version: string /** * if windows */ @@ -137,95 +76,40 @@ export interface IConfig { * set by ${BIN}_DEBUG or DEBUG=$BIN */ debug: number - /** - * active @anycli/engine - */ - engine: IEngine /** * npm registry to use for installing plugins */ npmRegistry: string - /** - * a Heroku pre-anycli plugin - */ - legacy: boolean - - /** - * list of topics - */ - topics: ITopic[] -} - -export interface TSConfig { - compilerOptions: { - rootDirs?: string[] - outDir?: string - } -} - -export interface ConfigOptions { - name?: string - root?: string - baseConfig?: IConfig + runCommand(id: string, argv?: string[]): Promise } -const debug = require('debug')('@anycli/config') - -export class Config implements IConfig { - /** - * registers ts-node for reading typescript source (./src) instead of compiled js files (./lib) - * there are likely issues doing this any the tsconfig.json files are not compatible with others - */ - readonly _base = _base +export class Config extends Plugin.Plugin implements IConfig { arch: ArchTypes - bin!: string - cacheDir!: string - configDir!: string - dataDir!: string - dirname!: string - errlog!: string - home!: string - name!: string - pjson: any + bin: string + cacheDir: string + configDir: string + dataDir: string + dirname: string + errlog: string + home: string platform: PlatformTypes - root!: string - shell!: string - version!: string + shell: string windows: boolean - userAgent!: string - commandsDir: string | undefined - commandsDirTS: string | undefined - pluginsModule: string | undefined - pluginsModuleTS: string | undefined - tsconfig: TSConfig | undefined + userAgent: string debug: number = 0 - hooks!: {[k: string]: string[]} - hooksTS?: {[k: string]: string[]} - engine!: IEngine - npmRegistry!: string - legacy = false - topics!: ITopic[] + npmRegistry: string + + constructor(opts: Plugin.Options) { + super(opts) + + this.loadPlugins(true) - constructor() { this.arch = (os.arch() === 'ia32' ? 'x86' : os.arch() as any) this.platform = os.platform() as any this.windows = this.platform === 'win32' - } - - async load(root: string, pjson: readPkg.Package, baseConfig?: IConfig) { - const base: IConfig = baseConfig || {} as any - this.root = root - this.pjson = pjson - - this.name = this.pjson.name - this.version = this.pjson.version - if (!this.pjson.anycli) { - this.legacy = true - this.pjson.anycli = this.pjson['cli-engine'] || {} - } - this.bin = this.pjson.anycli.bin || base.bin || this.name - this.dirname = this.pjson.anycli.dirname || base.dirname || this.name + this.bin = this.pjson.anycli.bin || this.name + this.dirname = this.pjson.anycli.dirname || this.name this.userAgent = `${this.name}/${this.version} (${this.platform}-${this.arch}) node-${process.version}` this.shell = this._shell() this.debug = this._debug() @@ -236,21 +120,20 @@ export class Config implements IConfig { this.dataDir = this.scopedEnvVar('DATA_DIR') || this.dir('data') this.errlog = path.join(this.cacheDir, 'error.log') - this.tsconfig = await this._tsConfig() - if (this.pjson.anycli.commands) { - this.commandsDir = path.join(this.root, this.pjson.anycli.commands) - this.commandsDirTS = await this._tsPath(this.pjson.anycli.commands) - } - this.hooks = _.mapValues(this.pjson.anycli.hooks || {}, h => _.castArray(h).map(h => path.join(this.root, h))) - this.hooksTS = await this._hooks() - if (typeof this.pjson.anycli.plugins === 'string') { - this.pluginsModule = path.join(this.root, this.pjson.anycli.plugins) - this.pluginsModuleTS = await this._tsPath(this.pjson.anycli.plugins) - } this.npmRegistry = this.scopedEnvVar('NPM_REGISTRY') || this.pjson.anycli.npmRegistry || 'https://registry.yarnpkg.com' - this.topics = topicsToArray(this.pjson.anycli.topics || {}) + debug('config done') + } - return this + async runHook(event: string, opts?: T) { + debug('start %s hook', event) + await super.runHook(event, {...opts || {}, config: this}) + debug('done %s hook', event) + } + + async runCommand(id: string, argv: string[] = []) { + debug('runCommand %s %o', id, argv) + const cmd = this.findCommand(id, {must: true}).load() + await cmd.run(argv, this) } scopedEnvVar(k: string) { @@ -269,80 +152,19 @@ export class Config implements IConfig { .toUpperCase() } - protected _topics() { - } - - private dir(category: 'cache' | 'data' | 'config'): string { + protected dir(category: 'cache' | 'data' | 'config'): string { const base = process.env[`XDG_${category.toUpperCase()}_HOME`] || (this.windows && process.env.LOCALAPPDATA) || path.join(this.home, category === 'data' ? '.local/share' : '.' + category) return path.join(base, this.dirname) } - private windowsHome() { return this.windowsHomedriveHome() || this.windowsUserprofileHome() } - private windowsHomedriveHome() { return (process.env.HOMEDRIVE && process.env.HOMEPATH && path.join(process.env.HOMEDRIVE!, process.env.HOMEPATH!)) } - private windowsUserprofileHome() { return process.env.USERPROFILE } - private macosCacheDir(): string | undefined { return this.platform === 'darwin' && path.join(this.home, 'Library', 'Caches', this.dirname) || undefined } - - private async _tsConfig(): Promise { - try { - // // ignore if no .git as it's likely not in dev mode - // if (!await fs.pathExists(path.join(this.root, '.git'))) return - - const tsconfigPath = path.join(this.root, 'tsconfig.json') - const tsconfig = await readJSON(tsconfigPath) - if (!tsconfig || !tsconfig.compilerOptions) return - return tsconfig - } catch (err) { - if (err.code !== 'ENOENT') throw err - } - } - - /** - * convert a path from the compiled ./lib files to the ./src typescript source - * this is for developing typescript plugins/CLIs - * if there is a tsconfig and the original sources exist, it attempts to require ts- - */ - private async _tsPath(orig: string): Promise { - if (!orig || !this.tsconfig) return - orig = path.join(this.root, orig) - let {rootDirs, outDir} = this.tsconfig.compilerOptions - if (!rootDirs || !rootDirs.length || !outDir) return - let rootDir = rootDirs[0] - try { - // rewrite path from ./lib/foo to ./src/foo - const lib = path.join(this.root, outDir) // ./lib - const src = path.join(this.root, rootDir) // ./src - const relative = path.relative(lib, orig) // ./commands - const out = path.join(src, relative) // ./src/commands - // this can be a directory of commands or point to a hook file - // if it's a directory, we check if the path exists. If so, return the path to the directory. - // For hooks, it might point to a module, not a file. Something like "./hooks/myhook" - // That file doesn't exist, and the real file is "./hooks/myhook.ts" - // In that case we attempt to resolve to the filename. If it fails it will revert back to the lib path - if (await fs.pathExists(out) || await fs.pathExists(out + '.ts')) return out - return out - } catch (err) { - debug(err) - return - } - } + protected windowsHome() { return this.windowsHomedriveHome() || this.windowsUserprofileHome() } + protected windowsHomedriveHome() { return (process.env.HOMEDRIVE && process.env.HOMEPATH && path.join(process.env.HOMEDRIVE!, process.env.HOMEPATH!)) } + protected windowsUserprofileHome() { return process.env.USERPROFILE } + protected macosCacheDir(): string | undefined { return this.platform === 'darwin' && path.join(this.home, 'Library', 'Caches', this.dirname) || undefined } - private async _hooks(): Promise<{[k: string]: string[]} | undefined> { - const hooks: {[k: string]: string[]} = {} - if (_.isEmpty(this.pjson.anycli.hooks)) return - for (let [k, h] of Object.entries(this.pjson.anycli.hooks)) { - hooks[k] = [] - for (let m of _.castArray(h)) { - const ts = await this._tsPath(m as string) - if (!ts) return - hooks[k].push(ts) - } - } - return hooks - } - - private _shell(): string { + protected _shell(): string { let shellPath const {SHELL, COMSPEC} = process.env if (SHELL) { @@ -355,7 +177,7 @@ export class Config implements IConfig { return shellPath[shellPath.length - 1] } - private _debug(): number { + protected _debug(): number { try { const {enabled} = require('debug')(this.bin) if (enabled) return 1 @@ -366,61 +188,12 @@ export class Config implements IConfig { } } -/** - * find package root - * for packages installed into node_modules this will go up directories until - * it finds a node_modules directory with the plugin installed into it - * - * This is needed because of the deduping npm does - */ -async function findPkg(name: string | undefined, root: string) { - // essentially just "cd .." - function* up(from: string) { - while (path.dirname(from) !== from) { - yield from - from = path.dirname(from) - } - yield from - } - for (let next of up(root)) { - let cur - if (name) { - cur = path.join(next, 'node_modules', name, 'package.json') - } else { - cur = path.join(next, 'package.json') - } - if (await fs.pathExists(cur)) return cur - } +export function load(opts: Options = (module.parent && module.parent.filename) || __dirname) { + if (typeof opts === 'string') opts = {root: opts} + if (isConfig(opts)) return opts + return new Config(opts) } -/** - * returns true if config is instantiated and not ConfigOptions - */ -export function isIConfig(o: any): o is IConfig { - return !!o._base -} - -/** - * reads a plugin/CLI's config - */ -export async function read(opts: ConfigOptions = {}): Promise { - let root = opts.root || (module.parent && module.parent.parent && module.parent.parent.filename) || __dirname - const pkgPath = await findPkg(opts.name, root) - if (!pkgPath) throw new Error(`could not find package.json with ${inspect(opts)}`) - debug('reading plugin %s', path.dirname(pkgPath)) - const pkg = await readPkg(pkgPath) - const config = new Config() - await config.load(path.dirname(pkgPath), pkg, opts.baseConfig) - return config -} - -function topicsToArray(input: any, base?: string): ITopic[] { - if (!input) return [] - base = base ? `${base}:` : '' - if (Array.isArray(input)) { - return input.concat(_.flatMap(input, t => topicsToArray(t.subtopics, `${base}${t.name}`))) - } - return _.flatMap(Object.keys(input), k => { - return [{...input[k], name: `${base}${k}`}].concat(topicsToArray(input[k].subtopics, `${base}${input[k].name}`)) - }) +function isConfig(o: any): o is IConfig { + return o && !!o._base } diff --git a/src/engine.ts b/src/engine.ts deleted file mode 100644 index 2149df28..00000000 --- a/src/engine.ts +++ /dev/null @@ -1,37 +0,0 @@ -import {ICachedCommand} from './command' -import {IConfig} from './config' -import {IPlugin} from './plugin' -import {ITopic} from './topic' - -export interface LoadPluginOptions { - type: 'core' | 'user' | 'link' | 'dev' - root?: string - name?: string - config?: IConfig - tag?: string - useCache?: boolean - loadDevPlugins?: boolean -} - -export interface IEngine { - readonly config: IConfig - readonly plugins: IPlugin[] - - readonly topics: ITopic[] - readonly commands: ICachedCommand[] - readonly commandIDs: string[] - readonly rootTopics: ITopic[] - readonly rootCommands: ICachedCommand[] - - findCommand(id: string, must: true): ICachedCommand - findCommand(id: string, must?: boolean): ICachedCommand | undefined - - findTopic(id: string, must: true): ITopic - findTopic(id: string, must?: boolean): ITopic | undefined - - runHook(event: string, opts: T): Promise - - load(config: IConfig): Promise - - loadPlugin(opts: LoadPluginOptions): Promise -} diff --git a/src/hooks.ts b/src/hooks.ts index d3a007fb..27158d9d 100644 --- a/src/hooks.ts +++ b/src/hooks.ts @@ -1,18 +1,16 @@ -import {ICommand} from './command' -import {IConfig} from './config' -import {IPlugin} from './plugin' +import * as Config from '.' export interface Hooks { init: {id: string} update: {} 'command_not_found': {id: string}, 'plugins:parse': { - pjson: IPlugin + pjson: Config.IPlugin } prerun: { - Command: ICommand + Command: Config.Command.Full argv: string[] } } -export type IHook = (options: T & {config: IConfig}) => any +export type Hook = (options: Hooks[K] & {config: Config.IConfig}) => any diff --git a/src/index.ts b/src/index.ts index ab46b727..04ca6478 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,9 @@ -export * from './command' -export * from './config' -export * from './engine' -export * from './hooks' -export * from './plugin' -export * from './topic' -export {IPJSON} from './pjson' +import 'fs-extra-debug' + +export {Command} from './command' +export {Hook, Hooks} from './hooks' +export {Manifest} from './manifest' +export {PJSON} from './pjson' +export {IPlugin, Plugin} from './plugin' +export {IConfig, Config, Options, load} from './config' +export {Topic} from './topic' diff --git a/src/manifest.ts b/src/manifest.ts new file mode 100644 index 00000000..4e9cee78 --- /dev/null +++ b/src/manifest.ts @@ -0,0 +1,44 @@ +import cli from 'cli-ux' +import * as globby from 'globby' +import * as _ from 'lodash' +import * as path from 'path' + +import * as Config from '.' + +export interface Manifest { + version: string + commands: {[id: string]: Config.Command} +} + +const debug = require('debug')('@anycli/config') + +export namespace Manifest { + export type FindCommandCB = (id: string) => Config.Command.Full + + export function build(version: string, dir: string, findCommand: FindCommandCB): Manifest { + debug(`loading IDs from ${dir}`) + const ids = globby.sync(['**/*.+(js|ts)', '!**/*.+(d.ts|test.ts|test.js)'], {cwd: dir}) + .map(file => { + const p = path.parse(file) + const topics = p.dir.split('/') + let command = p.name !== 'index' && p.name + return _([...topics, command]).compact().join(':') + }) + debug('found ids', ids) + let commands = ids.map(id => { + try { + return [id, Config.Command.toCached(findCommand(id))] + } catch (err) { + cli.warn(err) + } + }) + + return { + version, + commands: _(commands) + .compact() + .fromPairs() + .value() + } + } +} diff --git a/src/pjson.ts b/src/pjson.ts index 2b9206ac..e8e7408f 100644 --- a/src/pjson.ts +++ b/src/pjson.ts @@ -1,6 +1,6 @@ import {Package} from 'read-pkg' -export interface IPJSON extends Package { +export interface PJSON extends Package { name: string version: string anycli: { @@ -11,16 +11,27 @@ export interface IPJSON extends Package { dirname?: string commands?: string hooks: { [name: string]: string[] } - plugins?: string[] | string - devPlugins?: string[] + plugins?: PJSON.Plugin[] + devPlugins?: PJSON.Plugin[] title?: string description?: string topics: { [k: string]: { description?: string - subtopics?: IPJSON['anycli']['topics'] + subtopics?: PJSON['anycli']['topics'] hidden?: boolean } } } } + +export namespace PJSON { + export type Plugin = string | { + type: 'user' + name: string + tag?: string + } | { + type: 'link' + root: string + } +} diff --git a/src/plugin.ts b/src/plugin.ts index e0245812..dcc7ca93 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -1,22 +1,259 @@ -import {ICachedCommand} from './command' -import {IConfig} from './config' -import {ITopic} from './topic' +import cli from 'cli-ux' +import * as fs from 'fs-extra' +import * as loadJSON from 'load-json-file' +import * as _ from 'lodash' +import * as path from 'path' +import * as readPkg from 'read-pkg' +import {inspect} from 'util' + +import * as Config from '.' +import {tsPath} from './ts_node' +import {undefault} from './util' + +export interface Options { + root: string + name?: string + type?: string + tag?: string +} export interface IPlugin { + /** + * @anycli/config version + */ + _base: string + /** + * name from package.json + */ name: string + /** + * version from package.json + * + * example: 1.2.3 + */ version: string + /** + * full package.json + * + * parsed with read-pkg + */ + pjson: Config.PJSON + /** + * used to tell the user how the plugin was installed + * examples: core, link, user, dev + */ type: string + /** + * base path of plugin + */ root: string + /** + * npm dist-tag of plugin + * only used for user plugins + */ tag?: string - config: IConfig - manifest: IPluginManifest - topics: ITopic[] + /** + * subplugins of this plugin + */ plugins: IPlugin[] - hooks: {[k: string]: string[]} + /** + * if it appears to be an npm package but does not look like it's really a CLI plugin, this is set to false + */ valid: boolean + + allCommands(): Config.Command.Plugin[] + allTopics(): Config.Topic[] + findCommand(id: string, opts: {must: true}): Config.Command.Plugin + findCommand(id: string, opts?: {must: boolean}): Config.Command.Plugin | undefined + findTopic(id: string, opts: {must: true}): Config.Topic + findTopic(id: string, opts?: {must: boolean}): Config.Topic | undefined + runHook(event: string, opts?: T): Promise } -export interface IPluginManifest { - version: string - commands: ICachedCommand[] +const debug = require('debug')('@anycli/config') +const _pjson = require('../package.json') + +const loadedPlugins: {[name: string]: Plugin} = {} + +export class Plugin implements IPlugin { + _base = `${_pjson.name}@${_pjson.version}` + name!: string + version!: string + pjson!: Config.PJSON + type: string + root!: string + tag?: string + manifest!: Config.Manifest + topics!: Config.Topic[] + plugins: IPlugin[] = [] + hooks!: {[k: string]: string[]} + valid!: boolean + + constructor(opts: Options) { + this.type = opts.type || 'core' + const root = findRoot(opts.name, opts.root) + if (!root) throw new Error(`could not find package.json with ${inspect(opts)}`) + if (loadedPlugins[root]) return loadedPlugins[root] + loadedPlugins[root] = this + this.root = root + debug('reading plugin %s', root) + this.pjson = readPkg.sync(path.join(root, 'package.json')) as any + this.name = this.pjson.name + this.version = this.pjson.version + if (!this.pjson.anycli) { + this.pjson.anycli = this.pjson['cli-engine'] || {} + } + this.valid = this.pjson.anycli.schema === 1 + + this.topics = topicsToArray(this.pjson.anycli.topics || {}) + this.hooks = _.mapValues(this.pjson.anycli.hooks || {}, _.castArray) + + this.manifest = this._manifest() + this.loadPlugins() + } + + get commandsDir() { + return tsPath(this.root, this.pjson.anycli.commands) + } + + allTopics() { + let topics = [...this.topics] + for (let plugin of this.plugins) { + topics = [...topics, ...plugin.allTopics()] + } + return topics + } + + allCommands() { + let commands = Object.entries(this.manifest.commands) + .map(([id, c]) => ({...c, load: () => this._findCommand(id)})) + for (let plugin of this.plugins) { + commands = [...commands, ...plugin.allCommands()] + } + return commands + } + + findCommand(id: string, opts: {must: true}): Config.Command.Plugin + findCommand(id: string, opts?: {must: boolean}): Config.Command.Plugin | undefined + findCommand(id: string, opts: {must?: boolean} = {}): Config.Command.Plugin | undefined { + let command = this.manifest.commands[id] + if (command) return {...command, load: () => this._findCommand(id)} + for (let plugin of this.plugins) { + let command = plugin.findCommand(id) + if (command) return command + } + if (opts.must) throw new Error(`command ${id} not found`) + } + + _findCommand(id: string): Config.Command.Full { + const search = (cmd: any) => { + if (_.isFunction(cmd.run)) return cmd + return Object.values(cmd).find((cmd: any) => _.isFunction(cmd.run)) + } + const p = require.resolve(path.join(this.commandsDir!, ...id.split(':'))) + debug('require', p) + const cmd = search(require(p)) + cmd.id = id + return cmd + } + + findTopic(id: string, opts: {must: true}): Config.Topic + findTopic(id: string, opts?: {must: boolean}): Config.Topic | undefined + findTopic(name: string, opts: {must?: boolean} = {}) { + let topic = this.topics.find(t => t.name === name) + if (topic) return topic + for (let plugin of this.plugins) { + let topic = plugin.findTopic(name) + if (topic) return topic + } + if (opts.must) throw new Error(`topic ${name} not found`) + } + + async runHook(event: string, opts?: T) { + const promises = (this.hooks[event] || []) + .map(async hook => { + try { + await undefault(require(tsPath(this.root, hook)))(opts) + } catch (err) { + if (err.code === 'EEXIT') throw err + cli.warn(err) + } + }) + promises.push(...this.plugins.map(p => p.runHook(event, opts))) + await Promise.all(promises) + } + + // findCommand(id: string, opts?: {must: boolean}): ICommand | undefined + // findManifestCommand(id: string, opts: {must: true}): IManifestCommand + // findManifestCommand(id: string, opts?: {must: boolean}): IManifestCommand | undefined + // findTopic(id: string, opts: {must: true}): ITopic + // findTopic(id: string, opts?: {must: boolean}): ITopic | undefined + + protected _manifest(): Config.Manifest { + try { + const manifest: Config.Manifest = loadJSON.sync(path.join(this.root, '.anycli.manifest.json')) + if (manifest.version !== this.version) { + cli.warn(`Mismatched version in ${this.name} plugin manifest. Expected: ${this.version} Received: ${manifest.version}`) + } else return manifest + } catch (err) { + if (err.code !== 'ENOENT') cli.warn(err) + } + if (this.commandsDir) return Config.Manifest.build(this.version, this.commandsDir, id => this._findCommand(id)) + return {version: this.version, commands: {}} + } + + protected loadPlugins(dev = false) { + const plugins = this.pjson.anycli[dev ? 'devPlugins' : 'plugins'] + if (!plugins || !plugins.length) return + debug(`loading ${dev ? 'dev ' : ''}plugins`, plugins) + for (let plugin of plugins || []) { + try { + let opts: Options = {type: this.type, root: this.root} + if (typeof plugin === 'string') opts.name = plugin + else opts = {...opts, ...plugin} + this.plugins.push(new Plugin(opts)) + } catch (err) { + cli.warn(err) + } + } + return plugins + } +} + +function topicsToArray(input: any, base?: string): Config.Topic[] { + if (!input) return [] + base = base ? `${base}:` : '' + if (Array.isArray(input)) { + return input.concat(_.flatMap(input, t => topicsToArray(t.subtopics, `${base}${t.name}`))) + } + return _.flatMap(Object.keys(input), k => { + return [{...input[k], name: `${base}${k}`}].concat(topicsToArray(input[k].subtopics, `${base}${input[k].name}`)) + }) +} + +/** + * find package root + * for packages installed into node_modules this will go up directories until + * it finds a node_modules directory with the plugin installed into it + * + * This is needed because of the deduping npm does + */ +function findRoot(name: string | undefined, root: string) { + // essentially just "cd .." + function* up(from: string) { + while (path.dirname(from) !== from) { + yield from + from = path.dirname(from) + } + yield from + } + for (let next of up(root)) { + let cur + if (name) { + cur = path.join(next, 'node_modules', name, 'package.json') + } else { + cur = path.join(next, 'package.json') + } + if (fs.pathExistsSync(cur)) return path.dirname(cur) + } } diff --git a/src/topic.ts b/src/topic.ts index 97e35f72..4360f024 100644 --- a/src/topic.ts +++ b/src/topic.ts @@ -1,4 +1,4 @@ -export interface ITopic { +export interface Topic { name: string description?: string hidden?: boolean diff --git a/src/ts_node.ts b/src/ts_node.ts new file mode 100644 index 00000000..2ca84164 --- /dev/null +++ b/src/ts_node.ts @@ -0,0 +1,93 @@ +import * as fs from 'fs-extra' +import * as loadJSON from 'load-json-file' +import * as path from 'path' +import * as TSNode from 'ts-node' + +const tsconfigs: {[root: string]: TSConfig} = {} +const rootDirs: string[] = [] +const typeRoots = [`${__dirname}/../node_modules/@types`] + +const debug = require('debug')('@anycli/config') + +export interface TSConfig { + compilerOptions: { + rootDirs?: string[] + outDir?: string + target?: string + } +} + +function registerTSNode(root: string) { + if (tsconfigs[root]) return + const tsconfig = loadTSConfig(root) + if (!tsconfig) return + tsconfigs[root] = tsconfig + debug('registering ts-node at', root) + const tsNode: typeof TSNode = require('ts-node') + typeRoots.push(`${root}/../node_modules/@types`) + if (tsconfig.compilerOptions.rootDirs) { + rootDirs.push(...tsconfig.compilerOptions.rootDirs.map(r => path.join(root, r))) + } else { + rootDirs.push(`${root}/src`) + } + tsNode.register({ + project: false, + // cache: false, + // typeCheck: true, + compilerOptions: { + target: tsconfig.compilerOptions.target || 'es2017', + module: 'commonjs', + rootDirs, + typeRoots, + } + }) +} + +function loadTSConfig(root: string): TSConfig | undefined { + try { + // // ignore if no .git as it's likely not in dev mode + // if (!await fs.pathExists(path.join(this.root, '.git'))) return + + const tsconfigPath = path.join(root, 'tsconfig.json') + const tsconfig = loadJSON.sync(tsconfigPath) + if (!tsconfig || !tsconfig.compilerOptions) return + return tsconfig + } catch (err) { + if (err.code !== 'ENOENT') throw err + } +} + +/** + * convert a path from the compiled ./lib files to the ./src typescript source + * this is for developing typescript plugins/CLIs + * if there is a tsconfig and the original sources exist, it attempts to require ts- + */ +export function tsPath(root: string, orig: string): string +export function tsPath(root: string, orig: string | undefined): string | undefined +export function tsPath(root: string, orig: string | undefined): string | undefined { + if (!orig) return orig + orig = path.join(root, orig) + try { + registerTSNode(root) + const tsconfig = tsconfigs[root] + if (!tsconfig) return + const {rootDirs, outDir} = tsconfig.compilerOptions + const rootDir = (rootDirs || [])[0] + if (!rootDir || !outDir) return orig + // rewrite path from ./lib/foo to ./src/foo + const lib = path.join(root, outDir) // ./lib + const src = path.join(root, rootDir) // ./src + const relative = path.relative(lib, orig) // ./commands + const out = path.join(src, relative) // ./src/commands + // this can be a directory of commands or point to a hook file + // if it's a directory, we check if the path exists. If so, return the path to the directory. + // For hooks, it might point to a module, not a file. Something like "./hooks/myhook" + // That file doesn't exist, and the real file is "./hooks/myhook.ts" + // In that case we attempt to resolve to the filename. If it fails it will revert back to the lib path + if (fs.pathExistsSync(out) || fs.pathExistsSync(out + '.ts')) return out + else return orig + } catch (err) { + debug(err) + return orig + } +} diff --git a/src/util.ts b/src/util.ts new file mode 100644 index 00000000..c00c7014 --- /dev/null +++ b/src/util.ts @@ -0,0 +1,9 @@ +export interface IESModule { + __esModule: true + default: T +} + +export function undefault(obj: T | IESModule): T { + if ((obj as any).__esModule === true) return (obj as any).default + return obj as any +} diff --git a/test/config.test.ts b/test/config.test.ts index 8f8839af..23e5aaa7 100644 --- a/test/config.test.ts +++ b/test/config.test.ts @@ -9,7 +9,7 @@ describe('PluginConfig', () => { .env({}, {clear: true}) .stub(os, 'homedir', () => path.join('/my/home')) .stub(os, 'platform', () => 'darwin') - .add('config', Config.read) + .add('config', () => Config.load()) .end('darwin', ({config}) => { expect(config).to.include({ cacheDir: path.join('/my/home/Library/Caches/@anycli/config'), @@ -24,7 +24,7 @@ describe('PluginConfig', () => { .env({}, {clear: true}) .stub(os, 'homedir', () => path.join('/my/home')) .stub(os, 'platform', () => 'linux') - .add('config', Config.read) + .add('config', () => Config.load()) .end('linux', ({config}) => { expect(config).to.include({ cacheDir: path.join('/my/home/.cache/@anycli/config'), @@ -39,7 +39,7 @@ describe('PluginConfig', () => { .env({LOCALAPPDATA: '/my/home/localappdata'}, {clear: true}) .stub(os, 'homedir', () => path.join('/my/home')) .stub(os, 'platform', () => 'win32') - .add('config', Config.read) + .add('config', () => Config.load()) .end('win32', ({config}) => { expect(config).to.include({ cacheDir: path.join('/my/home/localappdata/@anycli/config'), diff --git a/test/fixtures/typescript/package.json b/test/fixtures/typescript/package.json index a0936df4..9eaccc34 100644 --- a/test/fixtures/typescript/package.json +++ b/test/fixtures/typescript/package.json @@ -2,7 +2,6 @@ "name": "ts-plugin", "private": true, "anycli": { - "plugins": "./lib/plugins", "commands": "./lib/commands", "hooks": { "init": "./lib/hooks/init" diff --git a/test/fixtures/typescript/src/commands/foo/bar/baz.ts b/test/fixtures/typescript/src/commands/foo/bar/baz.ts index 86eab1fd..778ed847 100644 --- a/test/fixtures/typescript/src/commands/foo/bar/baz.ts +++ b/test/fixtures/typescript/src/commands/foo/bar/baz.ts @@ -1,8 +1,5 @@ -// import Command from '@anycli/command' -// import cli from 'cli-ux' - -export default class { - async run() { +export class Command { + static run() { console.log('it works!') } } diff --git a/test/fixtures/typescript/src/plugins.ts b/test/fixtures/typescript/src/plugins.ts deleted file mode 100644 index c8c6a63b..00000000 --- a/test/fixtures/typescript/src/plugins.ts +++ /dev/null @@ -1,3 +0,0 @@ -export default function () { - console.log('loading plugins') -} diff --git a/test/typescript.test.ts b/test/typescript.test.ts index 3dff9f70..2ca5b292 100644 --- a/test/typescript.test.ts +++ b/test/typescript.test.ts @@ -1,24 +1,26 @@ import {expect, fancy} from 'fancy-test' -import * as fs from 'fs-extra' import * as path from 'path' import * as Config from '../src' const root = path.resolve(__dirname, 'fixtures/typescript') +const p = (p: string) => path.join(root, p) + +const withConfig = fancy +.add('config', () => Config.load(root)) describe('typescript', () => { - fancy - .it('has props', async () => { - const p = (p: string) => path.join(root, p) - await fs.outputFile(p('.git'), '') - const config = await Config.read({root}) + withConfig + .it('has commandsDir', ({config}) => { expect(config).to.deep.include({ - commandsDir: p('lib/commands'), - commandsDirTS: p('src/commands'), - pluginsModule: p('lib/plugins'), - pluginsModuleTS: p('src/plugins'), - hooks: {init: [p('lib/hooks/init')]}, - hooksTS: {init: [p('src/hooks/init')]}, + commandsDir: p('src/commands'), }) }) + + withConfig + .stdout() + .it('runs ts command', async ctx => { + ctx.config.runCommand('foo:bar:baz') + expect(ctx.stdout).to.equal('it works!\n') + }) }) diff --git a/yarn.lock b/yarn.lock index 1b7bb7ec..9a73522e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -21,16 +21,38 @@ tslint "^5.9.1" tslint-xo "^0.6.0" +"@heroku/linewrap@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@heroku/linewrap/-/linewrap-1.0.0.tgz#a9d4e99f0a3e423a899b775f5f3d6747a1ff15c6" + "@types/chai@^4.1.2": version "4.1.2" resolved "https://registry.npmjs.org/@types/chai/-/chai-4.1.2.tgz#f1af664769cfb50af805431c407425ed619daa21" +"@types/events@*": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@types/events/-/events-1.1.0.tgz#93b1be91f63c184450385272c47b6496fd028e02" + "@types/fs-extra@^5.0.0": version "5.0.0" resolved "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-5.0.0.tgz#d3e225b35eb5c6d3a5a782c28219df365c781413" dependencies: "@types/node" "*" +"@types/glob@*": + version "5.0.35" + resolved "https://registry.yarnpkg.com/@types/glob/-/glob-5.0.35.tgz#1ae151c802cece940443b5ac246925c85189f32a" + dependencies: + "@types/events" "*" + "@types/minimatch" "*" + "@types/node" "*" + +"@types/globby@^6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@types/globby/-/globby-6.1.0.tgz#7c25b975512a89effea2a656ca8cf6db7fb29d11" + dependencies: + "@types/glob" "*" + "@types/load-json-file@^2.0.7": version "2.0.7" resolved "https://registry.npmjs.org/@types/load-json-file/-/load-json-file-2.0.7.tgz#c887826f5230b7507d5230994d26315c6776be06" @@ -39,6 +61,10 @@ version "4.14.100" resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.100.tgz#f353dd9d3a9785638b6cb8023e6639097bd31969" +"@types/minimatch@*": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" + "@types/mocha@^2.2.48": version "2.2.48" resolved "https://registry.npmjs.org/@types/mocha/-/mocha-2.2.48.tgz#3523b126a0b049482e1c3c11877460f76622ffab" @@ -49,6 +75,12 @@ dependencies: "@types/node" "*" +"@types/node-notifier@^0.0.28": + version "0.0.28" + resolved "https://registry.yarnpkg.com/@types/node-notifier/-/node-notifier-0.0.28.tgz#86ba3d3aa8d918352cc3191d88de328b20dc93c1" + dependencies: + "@types/node" "*" + "@types/node@*", "@types/node@^9.4.0": version "9.4.0" resolved "https://registry.npmjs.org/@types/node/-/node-9.4.0.tgz#b85a0bcf1e1cc84eb4901b7e96966aedc6f078d1" @@ -122,12 +154,16 @@ ansi-styles@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" -ansi-styles@^3.1.0: +ansi-styles@^3.1.0, ansi-styles@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.0.tgz#c159b8d5be0f9e5a6f346dab94f16ce022161b88" dependencies: color-convert "^1.9.0" +ansicolors@~0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/ansicolors/-/ansicolors-0.2.1.tgz#be089599097b74a5c9c4a84a0cdbcdb62bd87aef" + argparse@^1.0.7: version "1.0.9" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.9.tgz#73d83bc263f86e97f8cc4f6bae1b0e90a7d22c86" @@ -144,7 +180,7 @@ array-uniq@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" -arrify@^1.0.0: +arrify@^1.0.0, arrify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" @@ -189,6 +225,13 @@ callsites@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-0.2.0.tgz#afab96262910a7f33c19a5775825c69f34e350ca" +cardinal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/cardinal/-/cardinal-1.0.0.tgz#50e21c1b0aa37729f9377def196b5a9cec932ee9" + dependencies: + ansicolors "~0.2.1" + redeyed "~1.0.0" + chai@^4.1.2: version "4.1.2" resolved "https://registry.npmjs.org/chai/-/chai-4.1.2.tgz#0f64584ba642f0f2ace2806279f4f06ca23ad73c" @@ -246,12 +289,36 @@ clean-regexp@^1.0.0: dependencies: escape-string-regexp "^1.0.5" +clean-stack@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-1.3.0.tgz#9e821501ae979986c46b1d66d2d432db2fd4ae31" + cli-cursor@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5" dependencies: restore-cursor "^2.0.0" +cli-ux@^3.3.13: + version "3.3.13" + resolved "https://registry.yarnpkg.com/cli-ux/-/cli-ux-3.3.13.tgz#123e0c7a29d1f743447b919500a9055486992df6" + dependencies: + "@anycli/screen" "^0.0.3" + "@heroku/linewrap" "^1.0.0" + ansi-styles "^3.2.0" + cardinal "^1.0.0" + chalk "^2.3.0" + clean-stack "^1.3.0" + extract-stack "^1.0.0" + fs-extra "^5.0.0" + indent-string "^3.2.0" + lodash "^4.17.4" + node-notifier "^5.2.1" + password-prompt "^1.0.4" + semver "^5.5.0" + strip-ansi "^4.0.0" + supports-color "^5.1.0" + cli-width@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639" @@ -359,6 +426,13 @@ diff@^3.1.0, diff@^3.2.0: version "3.4.0" resolved "https://registry.yarnpkg.com/diff/-/diff-3.4.0.tgz#b1d85507daf3964828de54b37d0d73ba67dda56c" +dir-glob@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-2.0.0.tgz#0b205d2b6aef98238ca286598a8204d29d0a0034" + dependencies: + arrify "^1.0.1" + path-type "^3.0.0" + doctrine@^0.7.2: version "0.7.2" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-0.7.2.tgz#7cb860359ba3be90e040b26b729ce4bfa654c523" @@ -499,6 +573,10 @@ esprima@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804" +esprima@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.0.0.tgz#53cf247acda77313e551c3aa2e73342d3fb4f7d9" + esquery@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.0.tgz#cfba8b57d7fba93f17298a8a006a04cda13d80fa" @@ -532,6 +610,10 @@ external-editor@^2.0.4: iconv-lite "^0.4.17" tmp "^0.0.33" +extract-stack@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/extract-stack/-/extract-stack-1.0.0.tgz#b97acaf9441eea2332529624b732fc5a1c8165fa" + fancy-test@^0.6.6: version "0.6.6" resolved "https://registry.yarnpkg.com/fancy-test/-/fancy-test-0.6.6.tgz#cda1afbf57ea2fb05291edbff34c0bbc2600d956" @@ -573,6 +655,12 @@ flat-cache@^1.2.1: graceful-fs "^4.1.2" write "^0.2.1" +fs-extra-debug@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/fs-extra-debug/-/fs-extra-debug-1.0.4.tgz#5efa3bd2a7ef6753fa79cfd810aab36445fa4788" + dependencies: + debug "^3.1.0" + fs-extra@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-5.0.0.tgz#414d0110cdd06705734d055652c5411260c31abd" @@ -619,6 +707,17 @@ globby@^5.0.0: pify "^2.0.0" pinkie-promise "^2.0.0" +globby@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/globby/-/globby-7.1.1.tgz#fb2ccff9401f8600945dfada97440cca972b8680" + dependencies: + array-union "^1.0.1" + dir-glob "^2.0.0" + glob "^7.1.2" + ignore "^3.3.5" + pify "^3.0.0" + slash "^1.0.0" + graceful-fs@^4.1.2, graceful-fs@^4.1.6: version "4.1.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" @@ -627,6 +726,10 @@ growl@1.10.3: version "1.10.3" resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.3.tgz#1926ba90cf3edfe2adb4927f5880bc22c66c790f" +growly@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" + has-ansi@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-0.1.0.tgz#84f265aae8c0e6a88a12d7022894b7568894c62e" @@ -665,7 +768,7 @@ iconv-lite@^0.4.17: version "0.4.19" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" -ignore@^3.3.3, ignore@^3.3.6: +ignore@^3.3.3, ignore@^3.3.5, ignore@^3.3.6: version "3.3.7" resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.7.tgz#612289bfb3c220e186a58118618d5be8c1bab021" @@ -677,6 +780,10 @@ imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" +indent-string@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-3.2.0.tgz#4a5fd6d27cc332f37e5419a504dbb837105c9289" + inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -892,6 +999,15 @@ natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" +node-notifier@^5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-5.2.1.tgz#fa313dd08f5517db0e2502e5758d664ac69f9dea" + dependencies: + growly "^1.3.0" + semver "^5.4.1" + shellwords "^0.1.1" + which "^1.3.0" + normalize-package-data@^2.3.2: version "2.4.0" resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f" @@ -943,6 +1059,13 @@ parse-passwd@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6" +password-prompt@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/password-prompt/-/password-prompt-1.0.4.tgz#933bac8db3528fcb27e9fdbc0a6592adcbdb5ed9" + dependencies: + ansi-escapes "^3.0.0" + cross-spawn "^5.1.0" + path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" @@ -1027,6 +1150,12 @@ readable-stream@^2.2.2: string_decoder "~1.0.3" util-deprecate "~1.0.1" +redeyed@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/redeyed/-/redeyed-1.0.1.tgz#e96c193b40c0816b00aec842698e61185e55498a" + dependencies: + esprima "~3.0.0" + require-uncached@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/require-uncached/-/require-uncached-1.0.3.tgz#4e0d56d6c9662fd31e43011c4b95aa49955421d3" @@ -1081,7 +1210,7 @@ safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" -"semver@2 || 3 || 4 || 5", semver@^5.3.0: +"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.4.1, semver@^5.5.0: version "5.5.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab" @@ -1099,10 +1228,18 @@ shebang-regex@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" +shellwords@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" + signal-exit@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" +slash@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" + slice-ansi@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-1.0.0.tgz#044f1a49d8842ff307aad6b505ed178bd950134d" @@ -1213,6 +1350,12 @@ supports-color@^4.0.0: dependencies: has-flag "^2.0.0" +supports-color@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.1.0.tgz#058a021d1b619f7ddf3980d712ea3590ce7de3d5" + dependencies: + has-flag "^2.0.0" + table@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/table/-/table-4.0.2.tgz#a33447375391e766ad34d3486e6e2aedc84d2e36" @@ -1266,7 +1409,7 @@ tsconfig@^7.0.0: strip-bom "^3.0.0" strip-json-comments "^2.0.0" -tslib@^1.0.0, tslib@^1.7.1, tslib@^1.8.0, tslib@^1.8.1: +tslib@^1.0.0, tslib@^1.7.1, tslib@^1.8.0, tslib@^1.8.1, tslib@^1.9.0: version "1.9.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.0.tgz#e37a86fda8cbbaf23a057f473c9f4dc64e5fc2e8" @@ -1365,7 +1508,7 @@ validate-npm-package-license@^3.0.1: spdx-correct "~1.0.0" spdx-expression-parse "~1.0.0" -which@^1.2.9: +which@^1.2.9, which@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a" dependencies: