Skip to content

Commit

Permalink
feat(@angular/cli): add the add command
Browse files Browse the repository at this point in the history
  • Loading branch information
hansl authored and Brocco committed Mar 9, 2018
1 parent 88fc93f commit 093e4ea
Show file tree
Hide file tree
Showing 15 changed files with 266 additions and 115 deletions.
154 changes: 43 additions & 111 deletions package-lock.json

Large diffs are not rendered by default.

97 changes: 97 additions & 0 deletions packages/@angular/cli/commands/add.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import chalk from 'chalk';
import { Command, CommandScope, Option } from '../models/command';
import { parseOptions } from '../models/command-runner';
import { CliConfig } from '../models/config';
import { SchematicAvailableOptions } from '../tasks/schematic-get-options';

const SilentError = require('silent-error');


export default class AddCommand extends Command {
readonly name = 'add';
readonly description = 'Add support for a library to your project.';
scope = CommandScope.inProject;
arguments = ['collection'];
options: Option[] = [];

private async _parseSchematicOptions(collectionName: string): Promise<any> {
const SchematicGetOptionsTask = require('../tasks/schematic-get-options').default;

const getOptionsTask = new SchematicGetOptionsTask({
ui: this.ui,
project: this.project
});

const availableOptions: SchematicAvailableOptions[] = await getOptionsTask.run({
schematicName: 'ng-add',
collectionName,
});

const options = this.options.concat(availableOptions || []);

return parseOptions(this._rawArgs, options, []);
}

validate(options: any) {
const collectionName = options.collection;

if (!collectionName) {
throw new SilentError(
`The "ng ${this.name}" command requires a name argument to be specified eg. `
+ `${chalk.yellow('ng add [name] ')}. For more details, use "ng help".`
);
}

return true;
}

async run(commandOptions: any) {
const collectionName = commandOptions.collection;

if (!collectionName) {
throw new SilentError(
`The "ng ${this.name}" command requires a name argument to be specified eg. `
+ `${chalk.yellow('ng add [name] ')}. For more details, use "ng help".`
);
}

const packageManager = CliConfig.fromGlobal().get('packageManager');

const NpmInstall = require('../tasks/npm-install').default;
const SchematicRunTask = require('../tasks/schematic-run').default;

const packageName = collectionName.startsWith('@')
? collectionName.split('/', 2).join('/')
: collectionName.split('/', 1)[0];

// We don't actually add the package to package.json, that would be the work of the package
// itself.
let npmInstall = new NpmInstall({
ui: this.ui,
project: this.project,
packageManager,
packageName,
save: false,
});

const schematicRunTask = new SchematicRunTask({
ui: this.ui,
project: this.project
});

await npmInstall.run();

// Reparse the options with the new schematic accessible.
commandOptions = await this._parseSchematicOptions(collectionName);

const runOptions = {
taskOptions: commandOptions,
workingDir: this.project.root,
collectionName,
schematicName: 'ng-add',
allowPrivate: true,
};

await schematicRunTask.run(runOptions);
}
}
1 change: 1 addition & 0 deletions packages/@angular/cli/lib/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const UI = require('../../ember-cli/lib/ui');

function loadCommands() {
return {
'add': require('../../commands/add').default,
'build': require('../../commands/build').default,
'serve': require('../../commands/serve').default,
'eject': require('../../commands/eject').default,
Expand Down
5 changes: 4 additions & 1 deletion packages/@angular/cli/models/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ export enum CommandScope {
}

export abstract class Command {
protected _rawArgs: string[];

constructor(context: CommandContext, logger: logging.Logger) {
this.logger = logger;
if (context) {
Expand All @@ -22,7 +24,8 @@ export abstract class Command {
}
}

async initializeRaw(args: any): Promise<any> {
async initializeRaw(args: string[]): Promise<any> {
this._rawArgs = args;
return args;
}
async initialize(_options: any): Promise<void> {
Expand Down
61 changes: 61 additions & 0 deletions packages/@angular/cli/tasks/npm-install.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { ModuleNotFoundException, resolve } from '@angular-devkit/core/node';

const Task = require('../ember-cli/lib/models/task');
import chalk from 'chalk';
import { spawn } from 'child_process';


export default Task.extend({
run: async function () {
const ui = this.ui;
let packageManager = this.packageManager;
if (packageManager === 'default') {
packageManager = 'npm';
}

ui.writeLine(chalk.green(`Installing packages for tooling via ${packageManager}.`));

const installArgs = ['install'];
if (packageManager === 'npm') {
installArgs.push('--quiet');
}
if (this.packageName) {
try {
// Verify if we need to install the package (it might already be there).
// If it's available and we shouldn't save, simply return. Nothing to be done.
resolve(this.packageName, { checkLocal: true, basedir: this.project.root });

if (!this.save) {
return;
}
} catch (e) {
if (!(e instanceof ModuleNotFoundException)) {
throw e;
}
}
installArgs.push(this.packageName);
}

if (!this.save) {
installArgs.push('--no-save');
}
const installOptions = {
stdio: 'inherit',
shell: true
};

await new Promise((resolve, reject) => {
spawn(packageManager, installArgs, installOptions)
.on('close', (code: number) => {
if (code === 0) {
ui.writeLine(chalk.green(`Installed packages for tooling via ${packageManager}.`));
resolve();
} else {
const message = 'Package install failed, see above.';
ui.writeLine(chalk.red(message));
reject(message);
}
});
});
}
});
3 changes: 2 additions & 1 deletion packages/@angular/cli/tasks/schematic-run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export interface SchematicRunOptions {
emptyHost: boolean;
collectionName: string;
schematicName: string;
allowPrivate?: boolean;
}

export interface SchematicOptions {
Expand Down Expand Up @@ -73,7 +74,7 @@ export default Task.extend({
);

const collection = getCollection(collectionName);
const schematic = getSchematic(collection, schematicName);
const schematic = getSchematic(collection, schematicName, options.allowPrivate);

const projectRoot = !!this.project ? this.project.root : workingDir;

Expand Down
5 changes: 3 additions & 2 deletions packages/@angular/cli/utilities/schematics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export function getCollection(collectionName: string): Collection<any, any> {
}

export function getSchematic(collection: Collection<any, any>,
schematicName: string): Schematic<any, any> {
return collection.createSchematic(schematicName);
schematicName: string,
allowPrivate?: boolean): Schematic<any, any> {
return collection.createSchematic(schematicName, allowPrivate);
}
9 changes: 9 additions & 0 deletions tests/collections/ng-add-simple/collection.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "@angular-devkit-tests/ng-add-simple",
"schematics": {
"ng-add": {
"factory": "./index.js",
"description": "Create an empty application"
}
}
}
1 change: 1 addition & 0 deletions tests/collections/ng-add-simple/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
exports.default = () => tree => tree.create('/ng-add-test', 'hello world');
5 changes: 5 additions & 0 deletions tests/collections/ng-add-simple/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "@angular-devkit-tests/ng-add-simple",
"version": "1.0.0",
"schematics": "./collection.json"
}
8 changes: 8 additions & 0 deletions tests/e2e/assets/add-collection/collection.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"schematics": {
"ng-add": {
"factory": "./index.js",
"description": "Add empty file to your application."
}
}
}
1 change: 1 addition & 0 deletions tests/e2e/assets/add-collection/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
exports.default = (options) => tree => tree.create(options.name || 'empty-file', '');
4 changes: 4 additions & 0 deletions tests/e2e/assets/add-collection/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "empty-app",
"schematics": "./collection.json"
}
9 changes: 9 additions & 0 deletions tests/e2e/tests/commands/add/add.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { assetDir } from '../../../utils/assets';
import { expectFileToExist, symlinkFile } from '../../../utils/fs';
import { ng } from '../../../utils/process';


export default async function () {
await ng('add', '@angular-devkit-tests/ng-add-simple');
await expectFileToExist('ng-add-test');
}
18 changes: 18 additions & 0 deletions tests/e2e/tests/commands/add/base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { assetDir } from '../../../utils/assets';
import { expectFileToExist, symlinkFile } from '../../../utils/fs';
import { ng } from '../../../utils/process';
import { expectToFail } from '../../../utils/utils';


export default async function () {
await symlinkFile(assetDir('add-collection'), `./node_modules/add-collection`, 'dir');

await ng('add', 'add-collection');
await expectFileToExist('empty-file');

await ng('add', 'add-collection', '--name=blah');
await expectFileToExist('blah');

// TODO: reenable this check when schematics fail the CLI command.
await expectToFail(() => ng('add', 'add-collection')); // File already exists.
}

0 comments on commit 093e4ea

Please sign in to comment.