-
Notifications
You must be signed in to change notification settings - Fork 69
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Added ESM loading support - tests pass - MacOS / Linux / Windows works #144
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,8 +13,8 @@ import {Hook} from '../interfaces/hooks' | |
import {PJSON} from '../interfaces/pjson' | ||
import * as Plugin from './plugin' | ||
import {Topic} from '../interfaces/topic' | ||
import {tsPath} from './ts-node' | ||
import {compact, flatMap, loadJSON, uniq} from './util' | ||
import ModuleLoader from '../module-loader' | ||
|
||
// eslint-disable-next-line new-cap | ||
const debug = Debug() | ||
|
@@ -59,6 +59,8 @@ export class Config implements IConfig { | |
|
||
home!: string | ||
|
||
isESM = false | ||
|
||
platform!: PlatformTypes | ||
|
||
shell!: string | ||
|
@@ -110,6 +112,7 @@ export class Config implements IConfig { | |
this.root = plugin.root | ||
this.pjson = plugin.pjson | ||
this.name = this.pjson.name | ||
this.isESM = plugin.isESM | ||
this.version = this.options.version || this.pjson.version || '0.0.0' | ||
this.channel = this.options.channel || channelFromVersion(this.version) | ||
this.valid = plugin.valid | ||
|
@@ -201,7 +204,14 @@ export class Config implements IConfig { | |
|
||
async runHook<T>(event: string, opts: T) { | ||
debug('start %s hook', event) | ||
const promises = this.plugins.map(p => { | ||
|
||
const search = (m: any): Hook<T> => { | ||
if (typeof m === 'function') return m | ||
if (m.default && typeof m.default === 'function') return m.default | ||
return Object.values(m).find((m: any) => typeof m === 'function') as Hook<T> | ||
} | ||
|
||
for (const p of this.plugins) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We don't have a |
||
const debug = require('debug')([this.bin, p.name, 'hooks', event].join(':')) | ||
const context: Hook.Context = { | ||
config: this, | ||
|
@@ -219,26 +229,27 @@ export class Config implements IConfig { | |
warn(message) | ||
}, | ||
} | ||
return Promise.all((p.hooks[event] || []) | ||
.map(async hook => { | ||
|
||
const hooks = p.hooks[event] || [] | ||
|
||
for (const hook of hooks) { | ||
try { | ||
const f = tsPath(p.root, hook) | ||
debug('start', f) | ||
const search = (m: any): Hook<T> => { | ||
if (typeof m === 'function') return m | ||
if (m.default && typeof m.default === 'function') return m.default | ||
return Object.values(m).find((m: any) => typeof m === 'function') as Hook<T> | ||
} | ||
|
||
await search(require(f)).call(context, {...opts as any, config: this}) | ||
/* eslint-disable no-await-in-loop */ | ||
const {isESM, module, filePath} = await ModuleLoader.loadGetData(p, hook) | ||
|
||
debug('start', isESM ? '(import)' : '(require)', filePath) | ||
|
||
await search(module).call(context, {...opts as any, config: this}) | ||
/* eslint-enable no-await-in-loop */ | ||
|
||
debug('done') | ||
} catch (error) { | ||
if (error && error.oclif && error.oclif.exit !== undefined) throw error | ||
this.warn(error, `runHook ${event}`) | ||
} | ||
})) | ||
}) | ||
await Promise.all(promises) | ||
} | ||
} | ||
|
||
debug('%s hook done', event) | ||
} | ||
|
||
|
@@ -249,7 +260,7 @@ export class Config implements IConfig { | |
await this.runHook('command_not_found', {id}) | ||
throw new CLIError(`command ${id} not found`) | ||
} | ||
const command = c.load() | ||
const command = await c.load() | ||
await this.runHook('prerun', {Command: command, argv}) | ||
const result = await command.run(argv, this) | ||
await this.runHook('postrun', {Command: command, result: result, argv}) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,6 +12,7 @@ import {PJSON} from '../interfaces/pjson' | |
import {Topic} from '../interfaces/topic' | ||
import {tsPath} from './ts-node' | ||
import {compact, exists, flatMap, loadJSON, mapValues} from './util' | ||
import ModuleLoader from '../module-loader' | ||
|
||
const _pjson = require('../../package.json') | ||
|
||
|
@@ -73,6 +74,8 @@ export class Plugin implements IPlugin { | |
|
||
pjson!: PJSON.Plugin | ||
|
||
isESM = false | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can't we completely hide this in ModuleLoader and just call |
||
|
||
type!: string | ||
|
||
root!: string | ||
|
@@ -110,6 +113,7 @@ export class Plugin implements IPlugin { | |
this._debug('reading %s plugin %s', this.type, root) | ||
this.pjson = await loadJSON(path.join(root, 'package.json')) as any | ||
this.name = this.pjson.name | ||
this.isESM = this.pjson.type === 'module' | ||
const pjsonPath = path.join(root, 'package.json') | ||
if (!this.name) throw new Error(`no name in ${pjsonPath}`) | ||
if (process.env.NODE_DEV === 'development' && !this.pjson.files) this.warn(`files attribute must be specified in ${pjsonPath}`) | ||
|
@@ -126,7 +130,7 @@ export class Plugin implements IPlugin { | |
|
||
this.manifest = await this._manifest(Boolean(this.options.ignoreManifest), Boolean(this.options.errorOnManifestCreate)) | ||
this.commands = Object.entries(this.manifest.commands) | ||
.map(([id, c]) => ({...c, load: () => this.findCommand(id, {must: true})})) | ||
.map(([id, c]) => ({...c, load: async () => this.findCommand(id, {must: true})})) | ||
this.commands.sort((a, b) => { | ||
if (a.id < b.id) return -1 | ||
if (a.id > b.id) return 1 | ||
|
@@ -168,23 +172,23 @@ export class Plugin implements IPlugin { | |
return ids | ||
} | ||
|
||
findCommand(id: string, opts: {must: true}): Command.Class | ||
async findCommand(id: string, opts: {must: true}): Promise<Command.Class> | ||
|
||
findCommand(id: string, opts?: {must: boolean}): Command.Class | undefined | ||
async findCommand(id: string, opts?: {must: boolean}): Promise<Command.Class | undefined> | ||
|
||
findCommand(id: string, opts: {must?: boolean} = {}): Command.Class | undefined { | ||
const fetch = () => { | ||
async findCommand(id: string, opts: {must?: boolean} = {}): Promise<Command.Class | undefined> { | ||
const fetch = async () => { | ||
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(':'))) | ||
this._debug('require', p) | ||
this._debug(this.isESM ? '(import)' : '(require)', p) | ||
let m | ||
try { | ||
m = require(p) | ||
m = this.isESM ? await ModuleLoader.importDynamic(p) : require(p) | ||
} catch (error) { | ||
if (!opts.must && error.code === 'MODULE_NOT_FOUND') return | ||
throw error | ||
|
@@ -195,7 +199,7 @@ export class Plugin implements IPlugin { | |
cmd.plugin = this | ||
return cmd | ||
} | ||
const cmd = fetch() | ||
const cmd = await fetch() | ||
if (!cmd && opts.must) error(`command ${id} not found`) | ||
return cmd | ||
} | ||
|
@@ -227,15 +231,15 @@ export class Plugin implements IPlugin { | |
return { | ||
version: this.version, | ||
// eslint-disable-next-line array-callback-return | ||
commands: this.commandIDs.map(id => { | ||
commands: (await Promise.all(this.commandIDs.map(async id => { | ||
try { | ||
return [id, toCached(this.findCommand(id, {must: true}), this)] | ||
return [id, toCached(await this.findCommand(id, {must: true}), this)] | ||
} catch (error) { | ||
const scope = 'toCached' | ||
if (Boolean(errorOnManifestCreate) === false) this.warn(error, scope) | ||
else throw this.addErrorScope(error, scope) | ||
} | ||
}) | ||
}))) | ||
.filter((f): f is [string, Command] => Boolean(f)) | ||
.reduce((commands, [id, c]) => { | ||
commands[id] = c | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,28 +2,23 @@ import lodashTemplate = require('lodash.template') | |
|
||
import {Config as IConfig, HelpOptions} from '../interfaces' | ||
import {Help, HelpBase} from '.' | ||
import * as Config from '../config' | ||
import ModuleLoader from '../module-loader' | ||
|
||
interface HelpBaseDerived { | ||
new(config: IConfig, opts?: Partial<HelpOptions>): HelpBase; | ||
} | ||
|
||
function extractExport(config: IConfig, classPath: string): HelpBaseDerived { | ||
const helpClassPath = Config.tsPath(config.root, classPath) | ||
return require(helpClassPath) as HelpBaseDerived | ||
} | ||
|
||
function extractClass(exported: any): HelpBaseDerived { | ||
return exported && exported.default ? exported.default : exported | ||
} | ||
|
||
export function getHelpClass(config: IConfig): HelpBaseDerived { | ||
export async function getHelpClass(config: IConfig): Promise<HelpBaseDerived> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we change this to |
||
const pjson = config.pjson | ||
const configuredClass = pjson && pjson.oclif && pjson.oclif.helpClass | ||
|
||
if (configuredClass) { | ||
try { | ||
const exported = extractExport(config, configuredClass) | ||
const exported = await ModuleLoader.load(config, configuredClass) as HelpBaseDerived | ||
return extractClass(exported) as HelpBaseDerived | ||
} catch (error) { | ||
throw new Error(`Unable to load configured help class "${configuredClass}", failed with message:\n${error.message}`) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't love shipping with 0.x releases. I took a look in the repo and I can't tell if there are plans to have a 1.x release. They might be waiting for a node equivalent?