diff --git a/docs/documentation/generate.md b/docs/documentation/generate.md index f1c928ab9583..6dd875a1cee4 100644 --- a/docs/documentation/generate.md +++ b/docs/documentation/generate.md @@ -49,3 +49,13 @@ Adds more details to output logging.

+ +
+ collection +

+ --collection (aliases: -c) default value: @schematics/angular +

+

+ Schematics collection to use. +

+
diff --git a/package-lock.json b/package-lock.json index 55b2cd6829db..1d82e1dbb259 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@angular/cli", - "version": "1.3.0-rc.5", + "version": "1.4.0-beta.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -14,6 +14,22 @@ "typescript": "2.4.2" } }, + "@angular-devkit/core": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-0.0.10.tgz", + "integrity": "sha512-B3oJ1/ALpTC/Lyp9xP0QXt3hwMjUvUFYAIdLAeGF54FVdIkj58IiG+m6s2vTn0FKIcR1jZbHvGTQhd+Oeowcag==" + }, + "@angular-devkit/schematics": { + "version": "0.0.17", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-0.0.17.tgz", + "integrity": "sha512-maL79fRoorHfFhIp+1PTiHwyVsAfuhvH1WMuCYI9Y8CUaMpDoThssyJUvTnhryi0shCpARgKRVjXayBeOH4ePQ==", + "requires": { + "@angular-devkit/core": "0.0.10", + "@ngtools/json-schema": "1.1.0", + "minimist": "1.2.0", + "rxjs": "5.4.2" + } + }, "@angular/compiler": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-4.3.3.tgz", @@ -52,6 +68,16 @@ "tsickle": "0.21.6" } }, + "@ngtools/json-schema": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@ngtools/json-schema/-/json-schema-1.1.0.tgz", + "integrity": "sha1-w6DFRNYjkqzCgTpCyKDcb1j4aSI=" + }, + "@schematics/angular": { + "version": "0.0.27", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-0.0.27.tgz", + "integrity": "sha512-vrGIEWBI/1b+m1I8aDEaTZ2fGPKKqdnvtrDlKx1x/EofLy3BngiTBR/9Hu9lWY55UE6ZJlF8af/agzeIbF+uSA==" + }, "@types/chalk": { "version": "0.4.31", "resolved": "https://registry.npmjs.org/@types/chalk/-/chalk-0.4.31.tgz", diff --git a/package.json b/package.json index a95b549a6aab..70fb5551b256 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,8 @@ "homepage": "https://github.com/angular/angular-cli", "dependencies": { "@angular-devkit/build-optimizer": "0.0.13", + "@angular-devkit/schematics": "0.0.17", + "@schematics/angular": "0.0.27", "autoprefixer": "^6.5.3", "chalk": "^2.0.1", "circular-dependency-plugin": "^3.0.0", diff --git a/packages/@angular/cli/commands/generate.ts b/packages/@angular/cli/commands/generate.ts index 1c8e95ab9966..9d44cedbbd70 100644 --- a/packages/@angular/cli/commands/generate.ts +++ b/packages/@angular/cli/commands/generate.ts @@ -1,28 +1,28 @@ -import * as chalk from 'chalk'; -import * as fs from 'fs'; -import * as os from 'os'; -import * as path from 'path'; +import { cyan, yellow } from 'chalk'; +const stringUtils = require('ember-cli-string-utils'); import { oneLine } from 'common-tags'; import { CliConfig } from '../models/config'; +import 'rxjs/add/observable/of'; +import 'rxjs/add/operator/ignoreElements'; +import { + getCollection, + getEngineHost +} from '../utilities/schematics'; +import { DynamicPathOptions, dynamicPathParser } from '../utilities/dynamic-path-parser'; +import { getAppFromConfig } from '../utilities/app-utils'; +import * as path from 'path'; +import { SchematicAvailableOptions } from '../tasks/schematic-get-options'; + const Command = require('../ember-cli/lib/models/command'); -const Blueprint = require('../ember-cli/lib/models/blueprint'); -const parseOptions = require('../ember-cli/lib/utilities/parse-options'); const SilentError = require('silent-error'); -function loadBlueprints(): Array { - const blueprintList = fs.readdirSync(path.join(__dirname, '..', 'blueprints')); - const blueprints = blueprintList - .filter(bp => bp.indexOf('-test') === -1) - .filter(bp => bp !== 'ng') - .map(bp => Blueprint.load(path.join(__dirname, '..', 'blueprints', bp))); +const separatorRegEx = /[\/\\]/g; - return blueprints; -} export default Command.extend({ name: 'generate', - description: 'Generates and/or modifies files based on a blueprint.', + description: 'Generates and/or modifies files based on a schematic.', aliases: ['g'], availableOptions: [ @@ -34,117 +34,145 @@ export default Command.extend({ description: 'Run through without making any changes.' }, { - name: 'lint-fix', + name: 'force', type: Boolean, - aliases: ['lf'], - description: 'Use lint to fix files after generation.' + default: false, + aliases: ['f'], + description: 'Forces overwriting of files.' }, { - name: 'verbose', + name: 'app', + type: String, + aliases: ['a'], + description: 'Specifies app name to use.' + }, + { + name: 'collection', + type: String, + aliases: ['c'], + description: 'Schematics collection to use.' + }, + { + name: 'lint-fix', type: Boolean, - default: false, - aliases: ['v'], - description: 'Adds more details to output logging.' + aliases: ['lf'], + description: 'Use lint to fix files after generation.' } ], anonymousOptions: [ - '' + '' ], - beforeRun: function (rawArgs: string[]) { - if (!rawArgs.length) { - return; + getCollectionName(rawArgs: string[]) { + let collectionName = CliConfig.getValue('defaults.schematics.collection'); + if (rawArgs) { + const parsedArgs = this.parseArgs(rawArgs, false); + if (parsedArgs.options.collection) { + collectionName = parsedArgs.options.collection; + } } + return collectionName; + }, + + beforeRun: function(rawArgs: string[]) { const isHelp = ['--help', '-h'].includes(rawArgs[0]); if (isHelp) { return; } - this.blueprints = loadBlueprints(); - - const name = rawArgs[0]; - const blueprint = this.blueprints.find((bp: any) => bp.name === name - || (bp.aliases && bp.aliases.includes(name))); - - if (!blueprint) { - SilentError.debugOrThrow('@angular/cli/commands/generate', - `Invalid blueprint: ${name}`); - } - - if (!rawArgs[1]) { - SilentError.debugOrThrow('@angular/cli/commands/generate', - `The \`ng generate ${name}\` command requires a name to be specified.`); + const schematicName = rawArgs[0]; + if (!schematicName) { + return Promise.reject(new SilentError(oneLine` + The "ng generate" command requires a + schematic name to be specified. + For more details, use "ng help". + `)); } if (/^\d/.test(rawArgs[1])) { SilentError.debugOrThrow('@angular/cli/commands/generate', - `The \`ng generate ${name} ${rawArgs[1]}\` file name cannot begin with a digit.`); + `The \`ng generate ${schematicName} ${rawArgs[1]}\` file name cannot begin with a digit.`); } - rawArgs[0] = blueprint.name; - this.registerOptions(blueprint); - }, + const SchematicGetOptionsTask = require('../tasks/schematic-get-options').default; - printDetailedHelp: function () { - if (!this.blueprints) { - this.blueprints = loadBlueprints(); - } - this.ui.writeLine(chalk.cyan(' Available blueprints')); - this.ui.writeLine(this.blueprints.map((bp: any) => bp.printBasicHelp(false)).join(os.EOL)); + const getOptionsTask = new SchematicGetOptionsTask({ + ui: this.ui, + project: this.project + }); + const collectionName = this.getCollectionName(rawArgs); + + return getOptionsTask.run({ + schematicName, + collectionName + }) + .then((availableOptions: SchematicAvailableOptions) => { + let anonymousOptions: string[] = []; + if (collectionName === '@schematics/angular' && schematicName === 'interface') { + anonymousOptions = ['']; + } + + this.registerOptions({ + anonymousOptions: anonymousOptions, + availableOptions: availableOptions + }); + }); }, run: function (commandOptions: any, rawArgs: string[]) { - const name = rawArgs[0]; - if (!name) { - return Promise.reject(new SilentError(oneLine` - The "ng generate" command requires a - blueprint name to be specified. - For more details, use "ng help". - `)); + if (rawArgs[0] === 'module' && !rawArgs[1]) { + throw 'The `ng generate module` command requires a name to be specified.'; } - const blueprint = this.blueprints.find((bp: any) => bp.name === name - || (bp.aliases && bp.aliases.includes(name))); - - const projectName = CliConfig.getValue('project.name'); - const blueprintOptions = { - target: this.project.root, - entity: { - name: rawArgs[1], - options: parseOptions(rawArgs.slice(2)) - }, - projectName, - ui: this.ui, + const entityName = rawArgs[1]; + commandOptions.name = stringUtils.dasherize(entityName.split(separatorRegEx).pop()); + + const appConfig = getAppFromConfig(commandOptions.app); + const dynamicPathOptions: DynamicPathOptions = { project: this.project, - settings: this.settings, - testing: this.testing, - args: rawArgs, - ...commandOptions + entityName: entityName, + appConfig: appConfig, + dryRun: commandOptions.dryRun }; + const parsedPath = dynamicPathParser(dynamicPathOptions); + commandOptions.sourceDir = appConfig.root; + commandOptions.path = parsedPath.dir + .replace(appConfig.root + path.sep, '') + .replace(separatorRegEx, '/'); - return blueprint.install(blueprintOptions) - .then(() => { - const lintFix = commandOptions.lintFix !== undefined ? - commandOptions.lintFix : CliConfig.getValue('defaults.lintFix'); - - if (lintFix && blueprint.modifiedFiles) { - const LintTask = require('../tasks/lint').default; - const lintTask = new LintTask({ - ui: this.ui, - project: this.project - }); - - return lintTask.run({ - fix: true, - force: true, - silent: true, - configs: [{ - files: blueprint.modifiedFiles.filter((file: string) => /.ts$/.test(file)) - }] - }); - } + const cwd = this.project.root; + const schematicName = rawArgs[0]; + + const SchematicRunTask = require('../tasks/schematic-run').default; + const schematicRunTask = new SchematicRunTask({ + ui: this.ui, + project: this.project + }); + const collectionName = this.getCollectionName(rawArgs); + + if (collectionName === '@schematics/angular' && schematicName === 'interface' && rawArgs[2]) { + commandOptions.type = rawArgs[2]; + } + + return schematicRunTask.run({ + taskOptions: commandOptions, + workingDir: cwd, + collectionName, + schematicName }); + }, + + printDetailedHelp: function () { + const engineHost = getEngineHost(); + const collectionName = this.getCollectionName(); + const collection = getCollection(collectionName); + const schematicNames: string[] = engineHost.listSchematics(collection); + this.ui.writeLine(cyan('Available schematics:')); + schematicNames.forEach(schematicName => { + this.ui.writeLine(yellow(` ${schematicName}`)); + }); + this.ui.writeLine(''); } }); diff --git a/packages/@angular/cli/commands/new.ts b/packages/@angular/cli/commands/new.ts index e52c0dea1095..5a82368ea306 100644 --- a/packages/@angular/cli/commands/new.ts +++ b/packages/@angular/cli/commands/new.ts @@ -1,24 +1,17 @@ import * as fs from 'fs'; import * as path from 'path'; import * as chalk from 'chalk'; -import denodeify = require('denodeify'); import InitCommand from './init'; import { CliConfig } from '../models/config'; import { validateProjectName } from '../utilities/validate-project-name'; import { oneLine } from 'common-tags'; +import { SchematicAvailableOptions } from '../tasks/schematic-get-options'; const Command = require('../ember-cli/lib/models/command'); const Project = require('../ember-cli/lib/models/project'); const SilentError = require('silent-error'); -// There's some problem with the generic typings for fs.makedir. -// Couldn't find matching types for the callbacks so leaving it as any for now. -const mkdir = denodeify(fs.mkdir as any); - -const configFile = '.angular-cli.json'; -const changeLater = (path: string) => `You can later change the value in "${configFile}" (${path})`; - const NewCommand = Command.extend({ name: 'new', aliases: ['n'], @@ -65,13 +58,6 @@ const NewCommand = Command.extend({ aliases: ['sg'], description: 'Skip initializing a git repository.' }, - { - name: 'skip-tests', - type: Boolean, - default: false, - aliases: ['st'], - description: 'Skip creating spec files.' - }, { name: 'skip-commit', type: Boolean, @@ -80,69 +66,59 @@ const NewCommand = Command.extend({ description: 'Skip committing the first commit to git.' }, { - name: 'directory', - type: String, - aliases: ['dir'], - description: 'The directory name to create the app in.' - }, - { - name: 'source-dir', - type: String, - default: 'src', - aliases: ['sd'], - description: `The name of the source directory. ${changeLater('apps[0].root')}.` - }, - { - name: 'style', - type: String, - default: 'css', - description: oneLine`The style file default extension. - Possible values: css, scss, less, sass, styl(stylus). - ${changeLater('defaults.styleExt')}. - ` - }, - { - name: 'prefix', + name: 'collection', type: String, - default: 'app', - aliases: ['p'], - description: oneLine` - The prefix to use for all component selectors. - ${changeLater('apps[0].prefix')}. - ` - }, - { - name: 'routing', - type: Boolean, - default: false, - description: 'Generate a routing module.' - }, - { - name: 'inline-style', - type: Boolean, - default: false, - aliases: ['is'], - description: 'Should have an inline style.' - }, - { - name: 'inline-template', - type: Boolean, - default: false, - aliases: ['it'], - description: 'Should have an inline template.' - }, - { - name: 'minimal', - type: Boolean, - default: false, - description: 'Should create a minimal app.' - } + aliases: ['c'], + description: 'Schematics collection to use.' + } ], isProject: function (projectPath: string) { return CliConfig.fromProject(projectPath) !== null; }, + getCollectionName(rawArgs: string[]) { + let collectionName = CliConfig.fromGlobal().get('defaults.schematics.collection'); + if (rawArgs) { + const parsedArgs = this.parseArgs(rawArgs, false); + if (parsedArgs.options.collection) { + collectionName = parsedArgs.options.collection; + } + } + return collectionName; + }, + + beforeRun: function (rawArgs: string[]) { + const isHelp = ['--help', '-h'].includes(rawArgs[0]); + if (isHelp) { + return; + } + + const schematicName = CliConfig.getValue('defaults.schematics.newApp'); + + if (/^\d/.test(rawArgs[1])) { + SilentError.debugOrThrow('@angular/cli/commands/generate', + `The \`ng new ${rawArgs[0]}\` file name cannot begin with a digit.`); + } + + const SchematicGetOptionsTask = require('../tasks/schematic-get-options').default; + + const getOptionsTask = new SchematicGetOptionsTask({ + ui: this.ui, + project: this.project + }); + + return getOptionsTask.run({ + schematicName, + collectionName: this.getCollectionName(rawArgs) + }) + .then((availableOptions: SchematicAvailableOptions) => { + this.registerOptions({ + availableOptions: availableOptions + }); + }); + }, + run: function (commandOptions: any, rawArgs: string[]) { const packageName = rawArgs.shift(); @@ -159,8 +135,16 @@ const NewCommand = Command.extend({ commandOptions.skipGit = true; } - const directoryName = path.join(process.cwd(), - commandOptions.directory ? commandOptions.directory : packageName); + commandOptions.directory = commandOptions.directory || packageName; + const directoryName = path.join(process.cwd(), commandOptions.directory); + + if (fs.existsSync(directoryName) && this.isProject(directoryName)) { + throw new SilentError(oneLine` + Directory ${directoryName} exists and is already an Angular CLI project. + `); + } + + commandOptions.collectionName = this.getCollectionName(rawArgs); const initCommand = new InitCommand({ ui: this.ui, @@ -168,34 +152,9 @@ const NewCommand = Command.extend({ project: Project.nullProject(this.ui, this.cli) }); - let createDirectory; - if (commandOptions.dryRun) { - createDirectory = Promise.resolve() - .then(() => { - if (fs.existsSync(directoryName) && this.isProject(directoryName)) { - throw new SilentError(oneLine` - Directory ${directoryName} exists and is already an Angular CLI project. - `); - } - }); - } else { - createDirectory = mkdir(directoryName) - .catch((err) => { - if (err.code === 'EEXIST') { - if (this.isProject(directoryName)) { - throw new SilentError(oneLine` - Directory ${directoryName} exists and is already an Angular CLI project. - `); - } - } else { - throw err; - } - }) - .then(() => process.chdir(directoryName)); - } - - return createDirectory + return Promise.resolve() .then(initCommand.run.bind(initCommand, commandOptions, rawArgs)); + } }); diff --git a/packages/@angular/cli/ember-cli/lib/models/command.js b/packages/@angular/cli/ember-cli/lib/models/command.js index eea7afb92e4a..5a62554e5b06 100644 --- a/packages/@angular/cli/ember-cli/lib/models/command.js +++ b/packages/@angular/cli/ember-cli/lib/models/command.js @@ -496,7 +496,7 @@ let Command = CoreObject.extend({ @param {Object} commandArgs @return {Object|null} */ - parseArgs(commandArgs) { + parseArgs(commandArgs, showErrors = true) { let knownOpts = {}; // Parse options let commandOptions = {}; let parsedOptions; @@ -507,7 +507,7 @@ let Command = CoreObject.extend({ let validateParsed = function(key) { // ignore 'argv', 'h', and 'help' - if (!commandOptions.hasOwnProperty(key) && key !== 'argv' && key !== 'h' && key !== 'help') { + if (!commandOptions.hasOwnProperty(key) && key !== 'argv' && key !== 'h' && key !== 'help' && showErrors) { this.ui.writeLine(chalk.yellow(`The option '--${key}' is not registered with the ${this.name} command. ` + `Run \`ng ${this.name} --help\` for a list of supported options.`)); } diff --git a/packages/@angular/cli/lib/config/schema.json b/packages/@angular/cli/lib/config/schema.json index 7a8b2c08e4c5..9a890308dbab 100644 --- a/packages/@angular/cli/lib/config/schema.json +++ b/packages/@angular/cli/lib/config/schema.json @@ -536,6 +536,23 @@ "type": "string" } } + }, + "schematics": { + "description": "Properties about schematics.", + "type": "object", + "properties": { + "collection": { + "description": "The schematics collection to use.", + "type": "string", + "default": "@schematics/angular" + }, + "newApp": { + "description": "The new app schematic.", + "type": "string", + "default": "application" + } + }, + "additionalProperties": false } }, "additionalProperties": false diff --git a/packages/@angular/cli/package.json b/packages/@angular/cli/package.json index 5db8da720528..90a4e859e32e 100644 --- a/packages/@angular/cli/package.json +++ b/packages/@angular/cli/package.json @@ -28,8 +28,10 @@ "homepage": "https://github.com/angular/angular-cli", "dependencies": { "@angular-devkit/build-optimizer": "0.0.13", + "@angular-devkit/schematics": "0.0.17", "@ngtools/json-schema": "1.1.0", "@ngtools/webpack": "1.7.0-beta.0", + "@schematics/angular": "0.0.27", "autoprefixer": "^6.5.3", "chalk": "^2.0.1", "circular-dependency-plugin": "^3.0.0", diff --git a/packages/@angular/cli/tasks/init.ts b/packages/@angular/cli/tasks/init.ts index e868d1040959..5eccb41c2e12 100644 --- a/packages/@angular/cli/tasks/init.ts +++ b/packages/@angular/cli/tasks/init.ts @@ -7,22 +7,16 @@ import {CliConfig} from '../models/config'; const Task = require('../ember-cli/lib/models/task'); const SilentError = require('silent-error'); -const normalizeBlueprint = require('../ember-cli/lib/utilities/normalize-blueprint-option'); const GitInit = require('../tasks/git-init'); -const InstallBlueprint = require('../ember-cli/lib/tasks/install-blueprint'); export default Task.extend({ + run: function (commandOptions: any, rawArgs: string[]) { if (commandOptions.dryRun) { commandOptions.skipInstall = true; } - const installBlueprint = new InstallBlueprint({ - ui: this.ui, - project: this.project - }); - // needs an explicit check in case it's just 'undefined' // due to passing of options from 'new' and 'addon' let gitInit: any; @@ -64,46 +58,54 @@ export default Task.extend({ return Promise.reject(new SilentError(message)); } - const blueprintOpts = { - dryRun: commandOptions.dryRun, - blueprint: 'ng', - rawName: packageName, - targetFiles: rawArgs || '', - rawArgs: rawArgs.toString(), - sourceDir: commandOptions.sourceDir, - style: commandOptions.style, - prefix: commandOptions.prefix.trim() || 'app', - routing: commandOptions.routing, - inlineStyle: commandOptions.inlineStyle, - inlineTemplate: commandOptions.inlineTemplate, - minimal: commandOptions.minimal, - ignoredUpdateFiles: ['favicon.ico'], - skipGit: commandOptions.skipGit, - skipTests: commandOptions.skipTests - }; - validateProjectName(packageName); - blueprintOpts.blueprint = normalizeBlueprint(blueprintOpts.blueprint); + const SchematicRunTask = require('../tasks/schematic-run').default; + const schematicRunTask = new SchematicRunTask({ + ui: this.ui, + project: this.project + }); + + const cwd = this.project.root; + const schematicName = CliConfig.fromGlobal().get('defaults.schematics.newApp'); + + const runOptions = { + taskOptions: commandOptions, + workingDir: cwd, + collectionName: commandOptions.collectionName, + schematicName + }; - return installBlueprint.run(blueprintOpts) + return schematicRunTask.run(runOptions) + .then(function () { + if (!commandOptions.dryRun) { + process.chdir(commandOptions.directory); + } + }) .then(function () { if (!commandOptions.skipInstall) { return checkYarnOrCNPM().then(() => npmInstall.run()); } }) .then(function () { - if (commandOptions.skipGit === false) { + if (!commandOptions.dryRun && commandOptions.skipGit === false) { return gitInit.run(commandOptions, rawArgs); } }) .then(function () { - if (commandOptions.linkCli) { + if (!commandOptions.dryRun && commandOptions.skipInstall === false) { + return npmInstall.run(); + } + }) + .then(function () { + if (!commandOptions.dryRun && commandOptions.linkCli) { return linkCli.run(); } }) .then(() => { - this.ui.writeLine(chalk.green(`Project '${packageName}' successfully created.`)); + if (!commandOptions.dryRun) { + this.ui.writeLine(chalk.green(`Project '${packageName}' successfully created.`)); + } }); } }); diff --git a/packages/@angular/cli/tasks/schematic-get-options.ts b/packages/@angular/cli/tasks/schematic-get-options.ts new file mode 100644 index 000000000000..47ecf721d132 --- /dev/null +++ b/packages/@angular/cli/tasks/schematic-get-options.ts @@ -0,0 +1,59 @@ +const Task = require('../ember-cli/lib/models/task'); +const stringUtils = require('ember-cli-string-utils'); +import { CliConfig } from '../models/config'; +import { getCollection, getSchematic } from '../utilities/schematics'; + +export interface SchematicGetOptions { + collectionName: string; + schematicName: string; +} + +export interface SchematicAvailableOptions { + name: string; + description: string; + aliases: string[]; + type: any; +} + +export default Task.extend({ + run: function (options: SchematicGetOptions): Promise { + const collectionName = options.collectionName || + CliConfig.getValue('defaults.schematics.collection'); + + const collection = getCollection(collectionName); + + const schematic = getSchematic(collection, options.schematicName); + + const properties = schematic.description.schemaJson.properties; + const keys = Object.keys(properties); + const availableOptions = keys + .map(key => ({...properties[key], ...{name: stringUtils.dasherize(key)}})) + .map(opt => { + let type; + switch (opt.type) { + case 'string': + type = String; + break; + case 'boolean': + type = Boolean; + break; + } + let aliases: string[] = []; + if (opt.alias) { + aliases = [...aliases, opt.alias]; + } + if (opt.aliases) { + aliases = [...aliases, ...opt.aliases]; + } + + return { + ...opt, + aliases, + type, + default: undefined // do not carry over schematics defaults + }; + }); + + return Promise.resolve(availableOptions); + } +}); diff --git a/packages/@angular/cli/tasks/schematic-run.ts b/packages/@angular/cli/tasks/schematic-run.ts new file mode 100644 index 000000000000..5c40c4de0154 --- /dev/null +++ b/packages/@angular/cli/tasks/schematic-run.ts @@ -0,0 +1,230 @@ +import { + DryRunEvent, + DryRunSink, + 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; + 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, 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 host = Observable.of(new FileSystemTree(new FileSystemHost(workingDir))); + + 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 (schematic.description.name === 'component') { + 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); +} diff --git a/packages/@angular/cli/utilities/schematics.ts b/packages/@angular/cli/utilities/schematics.ts new file mode 100644 index 000000000000..32912b81c6cf --- /dev/null +++ b/packages/@angular/cli/utilities/schematics.ts @@ -0,0 +1,53 @@ +/** + * Refer to the angular shematics library to let the dependency validator + * know it is used.. + * + * require('@schematics/angular') + */ + +import { + Collection, + Schematic, + SchematicEngine, +} from '@angular-devkit/schematics'; +import { + FileSystemSchematicDesc, + NodeModulesEngineHost +} from '@angular-devkit/schematics/tools'; +import { SchemaClassFactory } from '@ngtools/json-schema'; +import 'rxjs/add/operator/concatMap'; +import 'rxjs/add/operator/map'; + +const SilentError = require('silent-error'); + +export function getEngineHost() { + const engineHost = new NodeModulesEngineHost(); + return engineHost; +} + +export function getCollection(collectionName: string): Collection { + const engineHost = getEngineHost(); + const engine = new SchematicEngine(engineHost); + + // Add support for schemaJson. + engineHost.registerOptionsTransform((schematic: FileSystemSchematicDesc, options: any) => { + if (schematic.schema) { + const SchemaMetaClass = SchemaClassFactory(schematic.schemaJson!); + const schemaClass = new SchemaMetaClass(options); + return schemaClass.$$root(); + } + return options; + }); + + const collection = engine.createCollection(collectionName); + + if (collection === null) { + throw new SilentError(`Invalid collection (${collectionName}).`); + } + return collection; +} + +export function getSchematic(collection: Collection, + schematicName: string): Schematic { + return collection.createSchematic(schematicName); +} diff --git a/tests/e2e/tests/generate/component/component-duplicate.ts b/tests/e2e/tests/generate/component/component-duplicate.ts index 25de588cdeed..9099d487b084 100644 --- a/tests/e2e/tests/generate/component/component-duplicate.ts +++ b/tests/e2e/tests/generate/component/component-duplicate.ts @@ -1,4 +1,3 @@ -import * as path from 'path'; import { ng } from '../../../utils/process'; import { oneLine } from 'common-tags'; @@ -8,17 +7,17 @@ export default function () { if (!output.stdout.match(/update src[\\|\/]app[\\|\/]app.module.ts/)) { throw new Error(oneLine` Expected to match - "update src${path.sep}app${path.sep}app.module.ts" - in ${output}.`); + "update src/app/app.module.ts" + in ${output.stdout}.`); } }) .then(() => ng('generate', 'component', 'test-component')) .then((output) => { - if (!output.stdout.match(/identical src[\\|\/]app[\\|\/]app.module.ts/)) { + if (!output.stdout.match(/error! src[\\|\/]app[\\|\/]test-component[\\|\/]test-component.component.ts already exists./)) { throw new Error(oneLine` Expected to match - "identical src${path.sep}app${path.sep}app.module.ts" - in ${output}.`); + "ERROR! src/app/test-component/test-component.ts" + in ${output.stdout}.`); } }); } diff --git a/tests/e2e/tests/generate/module/module-import.ts b/tests/e2e/tests/generate/module/module-import.ts index 2f2f2104fe9c..05ba3a8a1e3b 100644 --- a/tests/e2e/tests/generate/module/module-import.ts +++ b/tests/e2e/tests/generate/module/module-import.ts @@ -27,17 +27,19 @@ export default function () { .then(() => expectFileToMatch(modulePath, /imports: \[(.|\s)*Test3Module(.|\s)*\]/m)) .then(() => ng('generate', 'module', 'test4', '--routing', '--module', 'app')) - .then(() => expectFileToMatch(modulePath, - /import { Test4RoutingModule } from '.\/test4\/test4-routing.module'/)) - .then(() => expectFileToMatch(modulePath, /imports: \[(.|\s)*Test4RoutingModule(.|\s)*\]/m)) + .then(() => expectFileToMatch(modulePath, /imports: \[(.|\s)*Test4Module(.|\s)*\]/m)) + .then(() => expectFileToMatch(join('src', 'app', 'test4', 'test4.module.ts'), + /import { Test4RoutingModule } from '.\/test4-routing.module'/)) + .then(() => expectFileToMatch(join('src', 'app', 'test4', 'test4.module.ts'), + /imports: \[(.|\s)*Test4RoutingModule(.|\s)*\]/m)) .then(() => ng('generate', 'module', 'test5', '--module', 'sub')) .then(() => expectFileToMatch(subModulePath, - /import { Test5Module } from '.\/..\/test5\/test5.module'/)) + /import { Test5Module } from '..\/test5\/test5.module'/)) .then(() => expectFileToMatch(subModulePath, /imports: \[(.|\s)*Test5Module(.|\s)*\]/m)) .then(() => ng('generate', 'module', 'test6', '--module', join('sub', 'deep')) .then(() => expectFileToMatch(deepSubModulePath, - /import { Test6Module } from '.\/..\/..\/test6\/test6.module'/)) + /import { Test6Module } from '..\/..\/test6\/test6.module'/)) .then(() => expectFileToMatch(deepSubModulePath, /imports: \[(.|\s)*Test6Module(.|\s)*\]/m))); } diff --git a/yarn.lock b/yarn.lock index 91dea9eb2c57..c248f212faa4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,15 +2,28 @@ # yarn lockfile v1 -"@angular-devkit/build-optimizer@0.0.3": - version "0.0.3" - resolved "https://registry.yarnpkg.com/@angular-devkit/build-optimizer/-/build-optimizer-0.0.3.tgz#092bdf732b79a779ce540f9bb99d6590dd971204" +"@angular-devkit/build-optimizer@0.0.5": + version "0.0.5" + resolved "https://registry.yarnpkg.com/@angular-devkit/build-optimizer/-/build-optimizer-0.0.5.tgz#321b141126ce462843e4d13e9d5603877e044860" dependencies: loader-utils "^1.1.0" magic-string "^0.19.1" source-map "^0.5.6" typescript "^2.3.3" +"@angular-devkit/core@0.0.6", "@angular-devkit/core@^0.0.6": + version "0.0.6" + resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-0.0.6.tgz#caf25c0c7928196e244b5fe5124256fcef6bce7c" + +"@angular-devkit/schematics@0.0.9": + version "0.0.9" + resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-0.0.9.tgz#84668c0196648de7e88e1727b2e7defbd7962dfd" + dependencies: + "@angular-devkit/core" "0.0.6" + "@ngtools/json-schema" "^1.1.0" + minimist "^1.2.0" + rxjs "^5.4.2" + "@angular/compiler-cli@^4.0.0": version "4.2.4" resolved "https://registry.yarnpkg.com/@angular/compiler-cli/-/compiler-cli-4.2.4.tgz#cce941a28362fc1c042ab85890fcaab1e233dd57" @@ -37,6 +50,16 @@ dependencies: tsickle "^0.21.0" +"@ngtools/json-schema@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@ngtools/json-schema/-/json-schema-1.1.0.tgz#c3a0c544d62392acc2813a42c8a0dc6f58f86922" + +"@schematics/angular@0.0.9": + version "0.0.9" + resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-0.0.9.tgz#c9ff31078af3079990e448ddd07b735ed3c1b4bd" + dependencies: + "@angular-devkit/schematics" "0.0.9" + "@types/chalk@^0.4.28": version "0.4.31" resolved "https://registry.yarnpkg.com/@types/chalk/-/chalk-0.4.31.tgz#a31d74241a6b1edbb973cf36d97a2896834a51f9"