Skip to content
This repository has been archived by the owner on Aug 22, 2023. It is now read-only.

Commit

Permalink
fix: make plugin more easily extendable
Browse files Browse the repository at this point in the history
  • Loading branch information
jdx committed Feb 6, 2018
1 parent 088c23f commit 735667c
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 72 deletions.
16 changes: 14 additions & 2 deletions src/debug.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
// tslint:disable no-console
let debug: any
try { debug = require('debug') } catch {}

export default (...scope: string[]) => (...args: any[]) => {
if (debug) debug(['@anycli/config', ...scope].join(':'))(...args)
function displayWarnings() {
if (process.listenerCount('warning') > 1) return
process.on('warning', (warning: any) => {
console.error(warning.stack)
if (warning.detail) console.error(warning.detail)
})
}

export default (...scope: string[]) => {
if (!debug) return (..._: any[]) => {}
const d = debug(['@anycli/config', ...scope].join(':'))
if (d.enabled) displayWarnings()
return (...args: any[]) => d(...args)
}
46 changes: 0 additions & 46 deletions src/manifest.ts
Original file line number Diff line number Diff line change
@@ -1,52 +1,6 @@
import * as Globby from 'globby'
import * as path from 'path'

import {Command} from './command'
import Debug from './debug'

export interface Manifest {
version: string
commands: {[id: string]: Command}
}

const debug = Debug()

export namespace Manifest {
export type FindCommandCB = (id: string) => Command.Class

export function build(version: string, dir: string, findCommand: FindCommandCB): Manifest {
let globby: typeof Globby
try {
globby = require('globby')
} catch {
debug('not loading plugins, globby not found')
return {} as any
}
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].filter(f => f).join(':')
})
debug('found ids', ids)
let commands = ids.map(id => {
try {
return [id, Command.toCached(findCommand(id))]
} catch (err) {
process.emitWarning(err)
}
})

return {
version,
commands: commands
.filter((f): f is [string, Command] => !!f)
.reduce((commands, [id, c]) => {
commands[id] = c
return commands
}, {} as {[k: string]: Command})
}
}
}
104 changes: 80 additions & 24 deletions src/plugin.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as fs from 'fs'
import * as Globby from 'globby'
import * as path from 'path'
import {inspect} from 'util'

Expand All @@ -10,7 +11,7 @@ import {Manifest} from './manifest'
import {PJSON} from './pjson'
import {Topic} from './topic'
import {tsPath} from './ts_node'
import {flatMap, loadJSONSync, mapValues} from './util'
import {compact, flatMap, loadJSONSync, mapValues} from './util'

export interface Options {
root: string
Expand Down Expand Up @@ -93,6 +94,7 @@ export class Plugin implements IPlugin {
hooks!: {[k: string]: string[]}
valid = false
alreadyLoaded = false
protected warned = false

constructor(opts: Options) {
this.type = opts.type || 'core'
Expand Down Expand Up @@ -136,7 +138,7 @@ export class Plugin implements IPlugin {

get commands() {
let commands = Object.entries(this.manifest.commands)
.map(([id, c]) => ({...c, load: () => this._findCommand(id)}))
.map(([id, c]) => ({...c, load: () => this._findCommand(id, {must: true})}))
for (let plugin of this.plugins) {
commands = [...commands, ...plugin.commands]
}
Expand All @@ -155,28 +157,14 @@ export class Plugin implements IPlugin {
findCommand(id: string, opts?: {must: boolean}): Command.Plugin | undefined
findCommand(id: string, opts: {must?: boolean} = {}): Command.Plugin | undefined {
let command = this.manifest.commands[id]
if (command) return {...command, load: () => this._findCommand(id)}
if (command) return {...command, load: () => this._findCommand(id, {must: true})}
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): Command.Class {
const search = (cmd: any) => {
if (typeof cmd.run === 'function') return cmd
if (cmd.default && cmd.default.run) return cmd.default
return Object.values(cmd).find((cmd: any) => typeof cmd.run === 'function')
}
const p = require.resolve(path.join(this.commandsDir!, ...id.split(':')))
debug('require', p)
const cmd = search(require(p))
cmd.id = id
cmd.plugin = this
return cmd
}

findTopic(id: string, opts: {must: true}): Topic
findTopic(id: string, opts?: {must: boolean}): Topic | undefined
findTopic(name: string, opts: {must?: boolean} = {}) {
Expand Down Expand Up @@ -216,13 +204,64 @@ export class Plugin implements IPlugin {
await search(require(p)).call(context, opts)
} catch (err) {
if (err && err['cli-ux'] && err['cli-ux']) throw err
process.emitWarning(err)
this.warn(err, `runHook ${event}`)
}
})
promises.push(...this.plugins.map(p => p.runHook(event, opts)))
await Promise.all(promises)
}

protected get _commandIDs(): string[] {
if (!this.commandsDir) return []
let globby: typeof Globby
try {
globby = require('globby')
} catch {
debug('not loading plugins, globby not found')
return {} as any
}
debug(`loading IDs from ${this.commandsDir}`)
const ids = globby.sync(['**/*.+(js|ts)', '!**/*.+(d.ts|test.ts|test.js)'], {cwd: this.commandsDir})
.map(file => {
const p = path.parse(file)
const topics = p.dir.split('/')
let command = p.name !== 'index' && p.name
return [...topics, command].filter(f => f).join(':')
})
debug('found ids', ids)
return ids
}

protected _findCommand(id: string, opts: {must: true}): Command.Class
protected _findCommand(id: string, opts?: {must: boolean}): Command.Class | undefined
protected _findCommand(id: string, opts: {must?: boolean} = {}): Command.Class | undefined {
const fetch = () => {
if (!this.commandsDir) return
const search = (cmd: any) => {
if (typeof cmd.run === 'function') return cmd
if (cmd.default && cmd.default.run) return cmd.default
return Object.values(cmd).find((cmd: any) => typeof cmd.run === 'function')
}
const p = require.resolve(path.join(this.commandsDir, ...id.split(':')))
debug('require', p)
let m
try {
m = require(p)
} catch (err) {
if (err.code === 'MODULE_NOT_FOUND') return
throw err
}
const cmd = search(m)
if (!cmd) return
cmd.id = id
cmd.plugin = this
return cmd
}
const cmd = fetch()
if (!cmd && opts.must) throw new Error(`command ${id} not found`)
return cmd
}

protected _manifest(ignoreManifest: boolean): Manifest {
const readManifest = () => {
try {
Expand All @@ -235,15 +274,27 @@ export class Plugin implements IPlugin {
return manifest
}
} catch (err) {
if (err.code !== 'ENOENT') process.emitWarning(err)
if (err.code !== 'ENOENT') this.warn(err, 'readManifest')
}
}
if (!ignoreManifest) {
let manifest = readManifest()
if (manifest) return manifest
}
if (this.commandsDir) return Manifest.build(this.version, this.commandsDir, id => this._findCommand(id))
return {version: this.version, commands: {}}

return {
version: this.version,
commands: this._commandIDs.map(id => {
try {
return [id, Command.toCached(this._findCommand(id, {must: true}))]
} catch (err) { this.warn(err, 'toCached') }
})
.filter((f): f is [string, Command] => !!f)
.reduce((commands, [id, c]) => {
commands[id] = c
return commands
}, {} as {[k: string]: Command})
}
}

protected loadPlugins(root: string, plugins: (string | PJSON.Plugin)[]) {
Expand All @@ -262,12 +313,17 @@ export class Plugin implements IPlugin {
opts.root = plugin.root || opts.root
}
this.plugins.push(new Plugin(opts))
} catch (err) {
process.emitWarning(err)
}
} catch (err) { this.warn(err, 'loadPlugins') }
}
return plugins
}

protected warn(err: any, scope?: string) {
if (this.warned) return
err.name = `${err.name} Plugin: ${this.name}`
err.detail = compact([err.detail, `module: ${this._base}`, scope && `task: ${scope}`, `plugin: ${this.name}`, `root: ${this.root}`]).join('\n')
process.emitWarning(err)
}
}

function topicsToArray(input: any, base?: string): Topic[] {
Expand Down
1 change: 1 addition & 0 deletions src/ts_node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ function registerTSNode(root: string) {
compilerOptions: {
target: tsconfig.compilerOptions.target || 'es2017',
module: 'commonjs',
sourceMap: true,
rootDirs,
typeRoots,
}
Expand Down
4 changes: 4 additions & 0 deletions src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,7 @@ export function loadJSONSync(path: string): any {
if (loadJSON) return loadJSON.sync(path)
return JSON.parse(fs.readFileSync(path, 'utf8'))
}

export function compact<T>(a: (T | undefined)[]): T[] {
return a.filter((a): a is T => !!a)
}

0 comments on commit 735667c

Please sign in to comment.