Skip to content

Commit

Permalink
feat() support projects in add
Browse files Browse the repository at this point in the history
add feature to prompt for project
improve help messages
fix test script
remove hard-coded messages
refactor generate action to use utils
display CLI version for info command
clean up prompts
update readme
Make all command help consistent (Initial cap, period)
  • Loading branch information
johnbiundo committed Oct 15, 2019
1 parent 429c915 commit ae386f5
Show file tree
Hide file tree
Showing 18 changed files with 324 additions and 125 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
.envrc
node_modules/
.DS_Store
*.map

# output
lib/**/*.js
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@

## Description

In order to help people manage their projects, the CLI tool has been created. It helps on many grounds at once, from scaffolding the project to build well-structured applications. The Nest CLI is based on the [@angular-devkit](https://github.com/angular/devkit) package. Also, there're special schematics that are dedicated to the Nest development [@nestjs/schematics](https://github.com/nestjs/schematics).
The Nest CLI is a command-line interface tool that helps you to initialize, develop, and maintain your Nest applications. It assists in multiple ways, including scaffolding the project, serving it in development mode, and building and bundling the application for production distribution. It embodies best-practice architectural patterns to encourage well-structured apps.

The CLI works with [schematics](https://github.com/angular/angular-cli/tree/master/packages/angular_devkit/schematics), and provides built in support from the schematics collection at [@nestjs/schematics](https://github.com/nestjs/schematics).


## Installation
Expand Down
178 changes: 152 additions & 26 deletions actions/add.action.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
import chalk from 'chalk';

import { MESSAGES } from '../lib/ui';

import { Answers } from 'inquirer';
import { Input } from '../commands';
import {
AbstractPackageManager,
Expand All @@ -10,49 +15,170 @@ import {
} from '../lib/schematics';
import { AbstractAction } from './abstract.action';

import { getValueOrDefault } from '../lib/compiler/helpers/get-value-or-default';

import {
askForProjectName,
moveDefaultProjectToStart,
shouldAskForProject,
} from '../lib/utils/project-utils';

import { loadConfiguration } from '../lib/utils/load-configuration';

const schematicName = 'nest-add';

export class AddAction extends AbstractAction {
public async handle(inputs: Input[], outputs: any[], extraFlags: string[]) {
const manager: AbstractPackageManager = await PackageManagerFactory.find();
const libraryInput: Input = inputs.find(
input => input.name === 'library',
) as Input;
public async handle(inputs: Input[], options: Input[], extraFlags: string[]) {
const libraryName = this.getLibraryName(inputs);
const packageName = this.getPackageName(libraryName);
const collectionName = this.getCollectionName(libraryName, packageName);
const tagName = this.getTagName(packageName);
const packageInstallSuccess = await this.installPackage(
collectionName,
tagName,
);
if (packageInstallSuccess) {
const sourceRootOption: Input = await this.getSourceRoot(
inputs.concat(options),
);
options.push(sourceRootOption);

if (!libraryInput) {
return;
await this.addLibrary(collectionName, options, extraFlags);
} else {
console.error(
chalk.red(
MESSAGES.LIBRARY_INSTALLATION_FAILED_BAD_PACKAGE(libraryName),
),
);
}
const library: string = libraryInput.value as string;
const packageName = library.startsWith('@')
? library.split('/', 2).join('/')
: library.split('/', 1)[0];
process.exit(0);
}

// Remove the tag/version from the package name.
const collectionName =
(packageName.startsWith('@')
? packageName.split('@', 2).join('@')
: packageName.split('@', 1).join('@')) +
library.slice(packageName.length);
private async getSourceRoot(inputs: Input[]): Promise<Input> {
const configuration = await loadConfiguration();
const configurationProjects = configuration.projects;

let tagName = packageName.startsWith('@')
? packageName.split('@', 3)[2]
: packageName.split('@', 2)[1];
const appName = inputs.find(option => option.name === 'project')!
.value as string;

let sourceRoot = appName
? getValueOrDefault(configuration, 'sourceRoot', appName)
: configuration.sourceRoot;

const shouldAsk = await shouldAskForProject(
schematicName,
configurationProjects,
appName,
);
if (shouldAsk) {
const defaultLabel: string = ' [ Default ]';
let defaultProjectName: string = configuration.sourceRoot + defaultLabel;

for (const property in configurationProjects) {
if (
configurationProjects[property].sourceRoot ===
configuration.sourceRoot
) {
defaultProjectName = property + defaultLabel;
break;
}
}

const projects = moveDefaultProjectToStart(
configuration,
defaultProjectName,
defaultLabel,
);

const answers: Answers = await askForProjectName(
MESSAGES.LIBRARY_PROJECT_SELECTION_QUESTION,
projects,
);
const project: string = answers.appName.replace(defaultLabel, '');
if (project !== configuration.sourceRoot) {
sourceRoot = configurationProjects[project].sourceRoot;
}
}
return { name: 'sourceRoot', value: sourceRoot };
}

private async installPackage(
collectionName: string,
tagName: string,
): Promise<boolean> {
const manager: AbstractPackageManager = await PackageManagerFactory.find();
tagName = tagName || 'latest';
await manager.addProduction([collectionName], tagName);
let installResult = false;
try {
installResult = await manager.addProduction([collectionName], tagName);
} catch (error) {
if (error && error.message) {
console.error(chalk.red(error.message));
}
}
return installResult;
}

private async addLibrary(
collectionName: string,
options: Input[],
extraFlags: string[],
) {
console.info(MESSAGES.LIBRARY_INSTALLATION_STARTS);
const schematicOptions: SchematicOption[] = [];
schematicOptions.push(
new SchematicOption('sourceRoot', options.find(
option => option.name === 'sourceRoot',
)!.value as string),
);
const extraFlagsString = extraFlags ? extraFlags.join(' ') : undefined;

const schematicName = 'nest-add';
try {
const collection: AbstractCollection = CollectionFactory.create(
collectionName,
);
const schematicOptions: SchematicOption[] = [];
const extraFlagsString = extraFlags ? extraFlags.join(' ') : undefined;
await collection.execute(
schematicName,
schematicOptions,
extraFlagsString,
);
} catch (e) {
return;
} catch (error) {
if (error && error.message) {
console.error(chalk.red(error.message));
return Promise.reject();
}
}
}

private getLibraryName(inputs: Input[]): string {
const libraryInput: Input = inputs.find(
input => input.name === 'library',
) as Input;

if (!libraryInput) {
throw new Error('No library found in command input');
}
return libraryInput.value as string;
}

private getPackageName(library: string): string {
return library.startsWith('@')
? library.split('/', 2).join('/')
: library.split('/', 1)[0];
}

private getCollectionName(library: string, packageName: string): string {
return (
(packageName.startsWith('@')
? packageName.split('@', 2).join('@')
: packageName.split('@', 1).join('@')) +
library.slice(packageName.length)
);
}

private getTagName(packageName: string): string {
return packageName.startsWith('@')
? packageName.split('@', 3)[2]
: packageName.split('@', 2)[1];
}
}
71 changes: 16 additions & 55 deletions actions/generate.action.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,8 @@
import chalk from 'chalk';
import * as inquirer from 'inquirer';
import { Answers, Question } from 'inquirer';
import { Answers } from 'inquirer';
import { Input } from '../commands';
import { getValueOrDefault } from '../lib/compiler/helpers/get-value-or-default';
import {
Configuration,
ConfigurationLoader,
ProjectConfiguration,
} from '../lib/configuration';
import { NestConfigurationLoader } from '../lib/configuration/nest-configuration.loader';
import { generateSelect } from '../lib/questions/questions';
import { FileSystemReader } from '../lib/readers';

import {
AbstractCollection,
CollectionFactory,
Expand All @@ -19,6 +11,14 @@ import {
import { MESSAGES } from '../lib/ui';
import { AbstractAction } from './abstract.action';

import {
askForProjectName,
moveDefaultProjectToStart,
shouldAskForProject,
} from '../lib/utils/project-utils';

import { loadConfiguration } from '../lib/utils/load-configuration';

export class GenerateAction extends AbstractAction {
public async handle(inputs: Input[], options: Input[]) {
await generateFiles(inputs.concat(options));
Expand Down Expand Up @@ -49,7 +49,7 @@ const generateFiles = async (inputs: Input[]) => {

// If you only add a `lib` we actually don't have monorepo: true BUT we do have "projects"
// Ensure we don't run for new app/libs schematics
if (shouldAskForProject(schematic, configurationProjects, appName)) {
if (await shouldAskForProject(schematic, configurationProjects, appName)) {
const defaultLabel: string = ' [ Default ]';
let defaultProjectName: string = configuration.sourceRoot + defaultLabel;

Expand All @@ -68,7 +68,11 @@ const generateFiles = async (inputs: Input[]) => {
defaultLabel,
);

const answers: Answers = await askForProjectName(projects);
const answers: Answers = await askForProjectName(
MESSAGES.PROJECT_SELECTION_QUESTION,
projects,
);

const project: string = answers.appName.replace(defaultLabel, '');
if (project !== configuration.sourceRoot) {
sourceRoot = configurationProjects[project].sourceRoot;
Expand All @@ -89,36 +93,6 @@ const generateFiles = async (inputs: Input[]) => {
}
};

const moveDefaultProjectToStart = (
configuration: Configuration,
defaultProjectName: string,
defaultLabel: string,
) => {
let projects: string[] = Object.keys(configuration.projects as {});
if (configuration.sourceRoot !== 'src') {
projects = projects.filter(
p => p !== defaultProjectName.replace(defaultLabel, ''),
);
}
projects.unshift(defaultProjectName);
return projects;
};

const askForProjectName = async (projects: string[]): Promise<Answers> => {
const questions: Question[] = [
generateSelect('appName')(MESSAGES.PROJECT_SELECTION_QUESTION)(projects),
];
const prompt = inquirer.createPromptModule();
return await prompt(questions);
};

const loadConfiguration = async (): Promise<Required<Configuration>> => {
const loader: ConfigurationLoader = new NestConfigurationLoader(
new FileSystemReader(process.cwd()),
);
return loader.load();
};

const mapSchematicOptions = (inputs: Input[]): SchematicOption[] => {
const options: SchematicOption[] = [];
inputs.forEach(input => {
Expand All @@ -128,16 +102,3 @@ const mapSchematicOptions = (inputs: Input[]): SchematicOption[] => {
});
return options;
};

const shouldAskForProject = (
schematic: string,
configurationProjects: { [key: string]: ProjectConfiguration },
appName: string,
) => {
return (
['app', 'sub-app', 'library', 'lib'].includes(schematic) === false &&
configurationProjects &&
Object.entries(configurationProjects).length !== 0 &&
!appName
);
};
16 changes: 13 additions & 3 deletions actions/info.action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,15 @@ const displayPackageManagerVersion = async () => {
const manager: AbstractPackageManager = await PackageManagerFactory.find();
try {
const version: string = await manager.version();
console.info(`${manager.name} Version :`, chalk.blue(version));
console.info(`${manager.name} Version :`, chalk.blue(version), '\n');
} catch {
console.error(`${manager.name} Version :`, chalk.red('Unknown'));
console.error(`${manager.name} Version :`, chalk.red('Unknown'), '\n');
}
};

const displayNestInformation = async () => {
console.info(chalk.green('[Nest Information]'));
displayCliVersion();
console.info(chalk.green('[Nest Platform Information]'));
try {
const dependencies: PackageJsonDependencies = await readProjectPackageJsonDependencies();
displayNestVersions(dependencies);
Expand All @@ -58,6 +59,15 @@ const displayNestInformation = async () => {
}
};

const displayCliVersion = () => {
console.info(chalk.green('[Nest CLI]'));
console.info(
'Nest CLI Version :',
chalk.blue(require('../package.json').version),
'\n',
);
};

const readProjectPackageJsonDependencies = async (): Promise<
PackageJsonDependencies
> => {
Expand Down
12 changes: 10 additions & 2 deletions bin/nest.ts
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,16 @@ import { CommandLoader } from '../commands';
const bootstrap = () => {
const program: CommanderStatic = commander;
program
.version(require('../package.json').version)
.usage('<command> [options]');
.version(
require('../package.json').version,
'-v, --version',
'Output the current version.',
)
.usage('<command> [options]')
.helpOption('-h, --help', 'Output usage information.')
.on('--help', () => {
return 'HelpMe!';
});
CommandLoader.load(program);
commander.parse(process.argv);

Expand Down

0 comments on commit ae386f5

Please sign in to comment.