import { DryRunEvent, DryRunSink, EmptyTree, FileSystemSink, FileSystemTree, Schematic, Tree } from '@angular-devkit/schematics'; import { FileSystemHost } from '@angular-devkit/schematics/tools'; import { Observable } from 'rxjs/Observable'; import * as path from 'path'; import { green, red, yellow } from 'chalk'; import { CliConfig } from '../models/config'; import 'rxjs/add/operator/concatMap'; import 'rxjs/add/operator/map'; import { getCollection, getSchematic } from '../utilities/schematics'; import { getAppFromConfig } from '../utilities/app-utils'; const Task = require('../ember-cli/lib/models/task'); export interface SchematicRunOptions { taskOptions: SchematicOptions; workingDir: string; emptyHost: boolean; collectionName: string; schematicName: string; } export interface SchematicOptions { dryRun: boolean; force: boolean; [key: string]: any; } export interface SchematicOutput { modifiedFiles: string[]; } interface OutputLogging { color: (msg: string) => string; keyword: string; message: string; } export default Task.extend({ run: function (options: SchematicRunOptions): Promise { const { taskOptions, workingDir, emptyHost, collectionName, schematicName } = options; const ui = this.ui; const collection = getCollection(collectionName); const schematic = getSchematic(collection, schematicName); let modifiedFiles: string[] = []; let appConfig; try { appConfig = getAppFromConfig(taskOptions.app); } catch (err) {} const projectRoot = !!this.project ? this.project.root : workingDir; const preppedOptions = prepOptions(schematic, taskOptions); const opts = { ...taskOptions, ...preppedOptions }; const tree = emptyHost ? new EmptyTree() : new FileSystemTree(new FileSystemHost(workingDir)); const host = Observable.of(tree); const dryRunSink = new DryRunSink(workingDir, opts.force); const fsSink = new FileSystemSink(workingDir, opts.force); let error = false; const loggingQueue: OutputLogging[] = []; dryRunSink.reporter.subscribe((event: DryRunEvent) => { const eventPath = event.path.startsWith('/') ? event.path.substr(1) : event.path; switch (event.kind) { case 'error': const desc = event.description == 'alreadyExist' ? 'already exists' : 'does not exist.'; ui.writeLine(`error! ${eventPath} ${desc}.`); error = true; break; case 'update': loggingQueue.push({ color: yellow, keyword: 'update', message: `${eventPath} (${event.content.length} bytes)` }); modifiedFiles = [...modifiedFiles, event.path]; break; case 'create': loggingQueue.push({ color: green, keyword: 'create', message: `${eventPath} (${event.content.length} bytes)` }); modifiedFiles = [...modifiedFiles, event.path]; break; case 'delete': loggingQueue.push({ color: red, keyword: 'remove', message: `${eventPath}` }); break; case 'rename': const eventToPath = event.to.startsWith('/') ? event.to.substr(1) : event.to; loggingQueue.push({ color: yellow, keyword: 'rename', message: `${eventPath} => ${eventToPath}` }); break; } }); return new Promise((resolve, reject) => { schematic.call(opts, host) .map((tree: Tree) => Tree.optimize(tree)) .concatMap((tree: Tree) => { return dryRunSink.commit(tree).ignoreElements().concat(Observable.of(tree)); }) .concatMap((tree: Tree) => { if (!error) { // Output the logging queue. loggingQueue.forEach(log => ui.writeLine(` ${log.color(log.keyword)} ${log.message}`)); } if (opts.dryRun || error) { return Observable.of(tree); } return fsSink.commit(tree).ignoreElements().concat(Observable.of(tree)); }) .subscribe({ error(err) { ui.writeLine(red(`Error: ${err.message}`)); reject(err.message); }, complete() { if (opts.dryRun) { ui.writeLine(yellow(`\nNOTE: Run with "dry run" no changes were made.`)); } resolve({modifiedFiles}); } }); }) .then((output: SchematicOutput) => { const modifiedFiles = output.modifiedFiles; const lintFix = taskOptions.lintFix !== undefined ? taskOptions.lintFix : CliConfig.getValue('defaults.lintFix'); if (lintFix && modifiedFiles) { const LintTask = require('./lint').default; const lintTask = new LintTask({ ui: this.ui, project: this.project }); return lintTask.run({ fix: true, force: true, silent: true, configs: [{ files: modifiedFiles .filter((file: string) => /.ts$/.test(file)) .map((file: string) => path.join(projectRoot, file)) }] }); } }); } }); function prepOptions(schematic: Schematic<{}, {}>, options: SchematicOptions): SchematicOptions { const properties = (schematic.description).schemaJson.properties; const keys = Object.keys(properties); if (['component', 'c', 'directive', 'd'].indexOf(schematic.description.name) !== -1) { options.prefix = (options.prefix === 'false' || options.prefix === '') ? '' : options.prefix; } let preppedOptions = { ...options, ...readDefaults(schematic.description.name, keys, options) }; preppedOptions = { ...preppedOptions, ...normalizeOptions(schematic.description.name, keys, options) }; return preppedOptions; } function readDefaults(schematicName: string, optionKeys: string[], options: any): any { return optionKeys.reduce((acc: any, key) => { acc[key] = options[key] !== undefined ? options[key] : readDefault(schematicName, key); return acc; }, {}); } const viewEncapsulationMap: any = { 'emulated': 'Emulated', 'native': 'Native', 'none': 'None' }; const changeDetectionMap: any = { 'default': 'Default', 'onpush': 'OnPush' }; function normalizeOptions(schematicName: string, optionKeys: string[], options: any): any { return optionKeys.reduce((acc: any, key) => { if (schematicName === 'application' || schematicName === 'component') { if (key === 'viewEncapsulation' && options[key]) { acc[key] = viewEncapsulationMap[options[key].toLowerCase()]; } else if (key === 'changeDetection' && options[key]) { acc[key] = changeDetectionMap[options[key].toLowerCase()]; } } return acc; }, {}); } function readDefault(schematicName: String, key: string) { const jsonPath = `defaults.${schematicName}.${key}`; return CliConfig.getValue(jsonPath); }