Skip to content

Commit

Permalink
fix(cli): remove "init" module loading system SHELL-1402
Browse files Browse the repository at this point in the history
BREAKING CHANGE: The top level "init" function loading feature has been
moved to an internal constructor option of the CliLoader class: "initFunctions".
Custom "init.js" files located in the project's cli directory will no longer be loaded automatically as custom CLI initialization functions.
  • Loading branch information
KalleV committed Jun 17, 2018
1 parent cdd5303 commit cfea075
Show file tree
Hide file tree
Showing 9 changed files with 148 additions and 211 deletions.
3 changes: 2 additions & 1 deletion docs/package-cli.md
Expand Up @@ -36,7 +36,8 @@ exports.hello = function (name, callback) {
};

exports.goodbye = function (name) {
this.log.info('Goodbye ' + name + '!'); // Note: `this` stores a reference to the initialized CLI app which contains logging capabilities
this.log.info('Goodbye ' + name + '!'); // Note: `this` stores a reference to the initialized CLI app which
// contains logging capabilities provided by Winston
};
```

Expand Down
12 changes: 8 additions & 4 deletions lib/bin/lsc.ts
Expand Up @@ -5,13 +5,17 @@
import semver = require('semver')
import fs = require('fs')
import path = require('path')
import {start} from "../cli";
import {start, init} from "../cli";

const manifestPath = path.join(__dirname, '..', '..', 'package.json'),
requiredNodeVersion = JSON.parse(fs.readFileSync(manifestPath).toString()).engines.node;
const manifestPath = path.join(__dirname, '..', '..', 'package.json');
const requiredNodeVersion = JSON.parse(fs.readFileSync(manifestPath).toString()).engines.node;

if (!semver.satisfies(process.versions.node, requiredNodeVersion)) {
throw new Error('LSC requires Node 6 or higher installed!');
}

start();
start({
initFunctions: [
init
]
});
3 changes: 2 additions & 1 deletion lib/cli/index.ts
@@ -1,4 +1,5 @@
export * from './start'
export * from './loader'
export * from './utils'
export * from './loader-plugin'
export * from './loader-plugin'
export * from './init'
11 changes: 6 additions & 5 deletions cli/init.ts → lib/cli/init.ts
Expand Up @@ -2,18 +2,19 @@
* @exports This module is used to initialize the global LabShare object
*/


'use strict';

import _ = require('lodash')
import path = require('path')
import yargs = require('yargs')
import {configLoaderSync} from '../lib/config'
import {Logger} from "../lib/log";
import labShare from '../lib/labshare'
import {configLoaderSync} from "../config";
import {Logger} from "../log";
import labShare from '../labshare';

const lscRoot = path.join(__dirname, '..');

export = function init() {
export function init(): void {
let argv = yargs.options({
configFile: {
alias: ['config', 'conf'],
Expand All @@ -38,4 +39,4 @@ export = function init() {
logDirectory,
fluentD
});
};
}
74 changes: 32 additions & 42 deletions lib/cli/loader.ts
Expand Up @@ -6,7 +6,8 @@
* second argument to the constructor. To locate commands, specify options.main
* and/or a list of package directories in options.directories. View the constructor documentation below
* for more details on the available options.
* - Call .load() to locate and load all the cli commands and run the 'init' modules defined by LabShare CLI packages.
* - Call .load() to locate and load all the cli commands and run the list of "initFunctions" passed to
* options.initFunctions.
* - Call .unload() to remove all the commands assigned to the Flatiron app set by .load().
* - Call .displayHelp() to display the list of loaded commands to the terminal
*/
Expand Down Expand Up @@ -35,21 +36,23 @@ function createCommandHeader(name) {
return ['', name, _.repeat('-', name.length)];
}

interface CliLoaderOptions {
interface ICliLoaderOptions {
configFilePath?: string
main?: string
packageDirectory?: string
directories?: string | string[]
timeout?: number
pattern?: string
initFunctions?: ((error?: Error) => any)[]
}

export class CliLoader {

public _commands;
private initModules: string[];

private initFunctions;
private app;
private options: CliLoaderOptions;
private options: ICliLoaderOptions;

/**
* @throws Error if the given app is not an initialized Flatiron app.
Expand All @@ -64,13 +67,13 @@ export class CliLoader {
*
* @constructor
*/
public constructor(app, options: CliLoaderOptions) {
public constructor(app, options: ICliLoaderOptions) {
assert.ok(app.commands && hasLogger(app), '`app` must be a Flatiron app with command storage and logging capabilities');
assert.ok(app.plugins && app.plugins.cli, 'The Flatiron cli plugin must be loaded by the app');

this.app = app;
this._commands = {}; // format: {packageName1: {cmd1Name: 'path/to/cmd1Name', cmd2Name: 'path/to/cmd2Name', ...}, packageName2: ...}
this.initModules = []; // format: ['path/to/init.js', ...]
this._commands = {}; // format: {packageName1: {cmd1Name: 'path/to/cmd1Name', cmd2Name: 'path/to/cmd2Name', ...}, packageName2: ...}
this.initFunctions = options.initFunctions || [];

if (_.get(options, 'main')) {
assert.ok(_.isString(options.main), '`options.main` must be a string');
Expand All @@ -94,13 +97,12 @@ export class CliLoader {

/**
* @description Synchronously loads and caches all the LabShare package CLI command modules
* found in the dependencies of options.main and/or options.directories. It also caches the
* CLI 'init' functions defined by the modules.
* found in the dependencies of options.main and/or options.directories.
*
* An error is logged if two different packages try to load a command with the same name, a command could not be loaded, or
* if a command module does not contain help text.
*/
public async load() {
public async load(): Promise<void> {
let app = this.app;

// Cache all init and command modules
Expand All @@ -118,13 +120,13 @@ export class CliLoader {
await this.init();

// Load the commands into the app
_.each(this._commands, pkg => {
_.each(this._commands, (pkg) => {
_.each(pkg, (modulePath, name) => {
if (_.has(app.commands, name)) {
return app.log.error(`Unable to load command "${name}" from "${modulePath}". A command with the same name has already been loaded by a different package.`);
}

let command = require(modulePath);
const command = require(modulePath);

if (!command.usage) {
app.log.warn(`The command module "${modulePath}" is missing a "usage" property that defines help text`);
Expand Down Expand Up @@ -173,43 +175,38 @@ export class CliLoader {
}

/**
* @description Runs the stored LabShare CLI package init functions
* @description Runs the stored initializer functions
* @returns {Promise}
* @private
*/
private init() {
private async init() {
let promises = [];

_.each(this.initModules, (initModulePath: string) => {
_.each(this.initFunctions, (initFn) => {
promises.push(new Promise((resolve, reject) => {
let init = null;

try {
init = require(initModulePath);
} catch (error) {
return reject(new Error(`LSC LOAD ERROR: failed to load init module at ${initModulePath}: ${error.stack}`));
}

// if it has a callback parameter
if (init.length) {
init(error => {
if (initFn.length) {
initFn((error) => {
if (error) {
reject(error);
}

resolve();
});

setTimeout(() => {
reject(new Error(`LSC TIMEOUT ERROR: init function in "${initModulePath}" failed to callback within ${this.options.timeout / 1000} seconds`));
reject(new Error('LSC TIMEOUT ERROR: init function timed out. The function' +
` "${initFn.toString()}" failed to callback within ${this.options.timeout / 1000}` +
' seconds`))'));
}, this.options.timeout);
} else {
init();
initFn();
resolve();
}
}));
});

return Promise.all(promises);
return await Promise.all(promises);
}

/**
Expand Down Expand Up @@ -247,7 +244,7 @@ export class CliLoader {
commands = this._commands;

_.each(commands, (pkgCommands, pkgName: string) => {
let commandList = _.keys(pkgCommands);
const commandList = _.keys(pkgCommands);
if (_.isEmpty(commandList)) {
return;
}
Expand All @@ -268,16 +265,10 @@ export class CliLoader {
let commands = {},
commandFilePaths = getMatchingFilesSync(directory, pattern);

_.each(commandFilePaths, (commandFilePath: string) => {
let baseName = getBaseName(commandFilePath);

// Store 'init' modules separately
if (/^init$/i.test(baseName)) {
this.initModules.push(commandFilePath);
} else {
commands[baseName] = commandFilePath;
}
});
for (const commandFilePath of commandFilePaths) {
const baseName = getBaseName(commandFilePath);
commands[baseName] = commandFilePath;
}

return commands;
}
Expand All @@ -291,11 +282,10 @@ export class CliLoader {
let appCommands = _.keys(this.app.commands),
storedCommands = _.values(this._commands);

return _.filter(appCommands, command => {
return !_.some(storedCommands, storedCommand => {
return _.filter(appCommands, (command) => {
return !_.some(storedCommands, (storedCommand) => {
return _.has(storedCommand, command);
});
});
}
}

44 changes: 26 additions & 18 deletions lib/cli/start.ts
Expand Up @@ -13,9 +13,10 @@ const {app} = flatiron,
lscRoot = path.join(__dirname, '..', '..');

export interface IStartOptions {
directories: string[]
pattern: string
main: string
directories?: string[]
pattern?: string
main?: string
initFunctions?: ((error?: Error) => any)[]
}

interface IPackageJson {
Expand All @@ -24,35 +25,37 @@ interface IPackageJson {
}

/**
* @param {object} options
* @param {string} options.main
* @param {Array} options.directories
* @param {string} options.pattern
* @description Bootstraps the CLI
* @param {string} main - Root project location
* @param {Array<string>} [directories] - Additional project directories to search for CLI commands
* @param {string} pattern - The CLI module pattern to search for (glob syntax)
* @param {Array<Function>} initModules - Array of custom initializer functions
*/
export function start(options: IStartOptions = {
main: cwd,
directories: [lscRoot],
pattern: '{src/cli,cli}/*.js'
}) {
export function start({
main = cwd,
directories = [lscRoot],
pattern = '{src/cli,cli}/*.js',
initFunctions = []
}: IStartOptions) {
let pkg: IPackageJson;

checkVersion({name: 'lsc', logger: app.log});

if (isPackageSync(options.main)) {
if (isPackageSync(main)) {
app.config.file({
file: path.join(options.main, 'config.json')
file: path.join(main, 'config.json')
});
pkg = require(path.join(options.main, 'package.json'));
pkg = require(path.join(main, 'package.json'));
} else {
pkg = require(path.join(lscRoot, 'package.json'));
app.config.file({
file: path.join(lscRoot, 'config.json')
});
}

app.Title = `${pkg.description} ${pkg.version}`;
app.title = `${pkg.description} ${pkg.version}`;
app.use(flatiron.plugins.cli, {
usage: [app.Title,
usage: [app.title,
'',
'Usage:',
'lsc <command> - run a command',
Expand All @@ -65,7 +68,12 @@ export function start(options: IStartOptions = {
});

app.use(require('flatiron-cli-config'));
app.use(loaderPlugin, options);
app.use(loaderPlugin, {
main,
directories,
pattern,
initFunctions
});

global.LabShare = labShare;

Expand Down

This file was deleted.

0 comments on commit cfea075

Please sign in to comment.