diff --git a/packages/@angular/cli/blueprints/class/index.ts b/packages/@angular/cli/blueprints/class/index.ts index 7698572f8567..60190c15c700 100644 --- a/packages/@angular/cli/blueprints/class/index.ts +++ b/packages/@angular/cli/blueprints/class/index.ts @@ -7,6 +7,7 @@ const Blueprint = require('../../ember-cli/lib/models/blueprint'); const getFiles = Blueprint.prototype.files; export default Blueprint.extend({ + name: 'class', description: '', aliases: ['cl'], diff --git a/packages/@angular/cli/blueprints/component/index.ts b/packages/@angular/cli/blueprints/component/index.ts index ff501944d21b..cb5abaaddabe 100644 --- a/packages/@angular/cli/blueprints/component/index.ts +++ b/packages/@angular/cli/blueprints/component/index.ts @@ -36,6 +36,7 @@ function correctCase(options: any) { } export default Blueprint.extend({ + name: 'component', description: '', aliases: ['c'], @@ -271,6 +272,7 @@ export default Blueprint.extend({ this._writeStatusToUI(chalk.yellow, moduleStatus, path.relative(this.project.root, this.pathToModule)); + this.addModifiedFile(this.pathToModule); })); } diff --git a/packages/@angular/cli/blueprints/directive/index.ts b/packages/@angular/cli/blueprints/directive/index.ts index 7de1252489f3..89da8ce83969 100644 --- a/packages/@angular/cli/blueprints/directive/index.ts +++ b/packages/@angular/cli/blueprints/directive/index.ts @@ -14,6 +14,7 @@ const Blueprint = require('../../ember-cli/lib/models/blueprint'); const getFiles = Blueprint.prototype.files; export default Blueprint.extend({ + name: 'directive', description: '', aliases: ['d'], @@ -165,6 +166,7 @@ export default Blueprint.extend({ this._writeStatusToUI(chalk.yellow, 'update', path.relative(this.project.root, this.pathToModule)); + this.addModifiedFile(this.pathToModule); } return Promise.all(returns); diff --git a/packages/@angular/cli/blueprints/enum/index.ts b/packages/@angular/cli/blueprints/enum/index.ts index 5c45e960b670..6d5726d385f4 100644 --- a/packages/@angular/cli/blueprints/enum/index.ts +++ b/packages/@angular/cli/blueprints/enum/index.ts @@ -5,6 +5,7 @@ const stringUtils = require('ember-cli-string-utils'); const Blueprint = require('../../ember-cli/lib/models/blueprint'); export default Blueprint.extend({ + name: 'enum', description: '', aliases: ['e'], diff --git a/packages/@angular/cli/blueprints/guard/index.ts b/packages/@angular/cli/blueprints/guard/index.ts index 5b3c8c233bcf..076b4930a6c7 100644 --- a/packages/@angular/cli/blueprints/guard/index.ts +++ b/packages/@angular/cli/blueprints/guard/index.ts @@ -13,6 +13,7 @@ const astUtils = require('../../utilities/ast-utils'); const getFiles = Blueprint.prototype.files; export default Blueprint.extend({ + name: 'guard', description: '', aliases: ['g'], @@ -110,6 +111,7 @@ export default Blueprint.extend({ this._writeStatusToUI(chalk.yellow, 'update', path.relative(this.project.root, this.pathToModule)); + this.addModifiedFile(this.pathToModule); } return Promise.all(returns); diff --git a/packages/@angular/cli/blueprints/interface/index.ts b/packages/@angular/cli/blueprints/interface/index.ts index 9b745d35bb24..f947bde7bd62 100644 --- a/packages/@angular/cli/blueprints/interface/index.ts +++ b/packages/@angular/cli/blueprints/interface/index.ts @@ -6,6 +6,7 @@ const stringUtils = require('ember-cli-string-utils'); const Blueprint = require('../../ember-cli/lib/models/blueprint'); export default Blueprint.extend({ + name: 'interface', description: '', aliases: ['i'], diff --git a/packages/@angular/cli/blueprints/module/index.ts b/packages/@angular/cli/blueprints/module/index.ts index 91fcd7a5c608..b08884d1d25e 100644 --- a/packages/@angular/cli/blueprints/module/index.ts +++ b/packages/@angular/cli/blueprints/module/index.ts @@ -7,6 +7,7 @@ const Blueprint = require('../../ember-cli/lib/models/blueprint'); const getFiles = Blueprint.prototype.files; export default Blueprint.extend({ + name: 'module', description: '', aliases: ['m'], diff --git a/packages/@angular/cli/blueprints/pipe/index.ts b/packages/@angular/cli/blueprints/pipe/index.ts index 9e440d4cc0a5..e3c23e85d630 100644 --- a/packages/@angular/cli/blueprints/pipe/index.ts +++ b/packages/@angular/cli/blueprints/pipe/index.ts @@ -13,6 +13,7 @@ const Blueprint = require('../../ember-cli/lib/models/blueprint'); const getFiles = Blueprint.prototype.files; export default Blueprint.extend({ + name: 'pipe', description: '', aliases: ['p'], @@ -143,6 +144,7 @@ export default Blueprint.extend({ this._writeStatusToUI(chalk.yellow, 'update', path.relative(this.project.root, this.pathToModule)); + this.addModifiedFile(this.pathToModule); } return Promise.all(returns); diff --git a/packages/@angular/cli/blueprints/service/index.ts b/packages/@angular/cli/blueprints/service/index.ts index 23f5cbd34049..8d974576e1b3 100644 --- a/packages/@angular/cli/blueprints/service/index.ts +++ b/packages/@angular/cli/blueprints/service/index.ts @@ -13,6 +13,7 @@ const astUtils = require('../../utilities/ast-utils'); const getFiles = Blueprint.prototype.files; export default Blueprint.extend({ + name: 'service', description: '', aliases: ['s'], @@ -122,6 +123,7 @@ export default Blueprint.extend({ this._writeStatusToUI(chalk.yellow, 'update', path.relative(this.project.root, this.pathToModule)); + this.addModifiedFile(this.pathToModule); } return Promise.all(returns); diff --git a/packages/@angular/cli/commands/generate.ts b/packages/@angular/cli/commands/generate.ts index c0581c46c526..429e632c23f1 100644 --- a/packages/@angular/cli/commands/generate.ts +++ b/packages/@angular/cli/commands/generate.ts @@ -1,69 +1,143 @@ +import * as chalk from 'chalk'; import * as fs from 'fs'; -import * as path from 'path'; import * as os from 'os'; +import * as path from 'path'; +import { oneLine } from 'common-tags'; +import { CliConfig } from '../models/config'; -const chalk = require('chalk'); -const EmberGenerateCommand = require('../ember-cli/lib/commands/generate'); +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'); -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))); +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))); + + return blueprints; +} -const GenerateCommand = EmberGenerateCommand.extend({ +export default Command.extend({ name: 'generate', + description: 'Generates and/or modifies files based on a blueprint.', + aliases: ['g'], + + availableOptions: [ + { + name: 'dry-run', + type: Boolean, + default: false, + aliases: ['d'], + description: 'Run through without making any changes.' + }, + { + name: 'lint-fix', + type: Boolean, + aliases: ['lf'], + description: 'Use lint to fix files after generation.' + }, + { + name: 'verbose', + type: Boolean, + default: false, + aliases: ['v'], + description: 'Adds more details to output logging.' + } + ], - blueprints: blueprints, + anonymousOptions: [ + '' + ], beforeRun: function (rawArgs: string[]) { if (!rawArgs.length) { return; } - // map the blueprint name to allow for aliases - rawArgs[0] = mapBlueprintName(rawArgs[0]); + const isHelp = ['--help', '-h'].includes(rawArgs[0]); + if (isHelp) { + return; + } + + this.blueprints = loadBlueprints(); - const isHelp: boolean = ['--help', '-h'].indexOf(rawArgs[0]) > -1; - if (!isHelp && !fs.existsSync(path.join(__dirname, '..', 'blueprints', rawArgs[0]))) { + 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: ${rawArgs[0]}`); + `Invalid blueprint: ${name}`); } - if (!isHelp && !rawArgs[1]) { + if (!rawArgs[1]) { SilentError.debugOrThrow('@angular/cli/commands/generate', - `The \`ng generate ${rawArgs[0]}\` command requires a name to be specified.`); + `The \`ng generate ${name}\` command requires a name to be specified.`); } - // Override default help to hide ember blueprints - EmberGenerateCommand.prototype.printDetailedHelp = function () { - this.ui.writeLine(chalk.cyan(' Available blueprints')); - this.ui.writeLine(blueprints.map(bp => bp.printBasicHelp(false)).join(os.EOL)); + rawArgs[0] = blueprint.name; + this.registerOptions(blueprint); + }, + + 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)); + }, + + 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". + `)); + } + + const blueprint = this.blueprints.find((bp: any) => bp.name === name + || (bp.aliases && bp.aliases.includes(name))); + + const blueprintOptions = { + target: this.project.root, + entity: { + name: rawArgs[1], + options: parseOptions(rawArgs.slice(2)) + }, + ui: this.ui, + project: this.project, + settings: this.settings, + testing: this.testing, + args: rawArgs, + ...commandOptions }; - return EmberGenerateCommand.prototype.beforeRun.apply(this, arguments); - } -}); + return blueprint.install(blueprintOptions) + .then(() => { + const lintFix = commandOptions.lintFix !== undefined ? + commandOptions.lintFix : CliConfig.getValue('defaults.lintFix'); -function mapBlueprintName(name: string): string { - let mappedName: string = aliasMap[name]; - return mappedName ? mappedName : name; -} + if (lintFix && blueprint.modifiedFiles) { + const LintTask = require('../tasks/lint').default; + const lintTask = new LintTask({ + ui: this.ui, + project: this.project + }); -const aliasMap: { [alias: string]: string } = { - 'cl': 'class', - 'c': 'component', - 'd': 'directive', - 'e': 'enum', - 'g': 'guard', - 'i': 'interface', - 'm': 'module', - 'p': 'pipe', - 'r': 'route', - 's': 'service' -}; - -export default GenerateCommand; -GenerateCommand.overrideCore = true; + return lintTask.run({ + fix: true, + force: true, + silent: true, + configs: [{ + files: blueprint.modifiedFiles.filter((file: string) => /.ts$/.test(file)) + }] + }); + } + }); + } +}); diff --git a/packages/@angular/cli/commands/lint.ts b/packages/@angular/cli/commands/lint.ts index 041b57d04e7b..e218502978b9 100644 --- a/packages/@angular/cli/commands/lint.ts +++ b/packages/@angular/cli/commands/lint.ts @@ -1,4 +1,5 @@ -import {oneLine} from 'common-tags'; +import { oneLine } from 'common-tags'; +import { CliConfig } from '../models/config'; const Command = require('../ember-cli/lib/models/command'); @@ -52,6 +53,9 @@ export default Command.extend({ project: this.project }); - return lintTask.run(commandOptions); + return lintTask.run({ + ...commandOptions, + configs: CliConfig.fromProject().config.lint + }); } }); diff --git a/packages/@angular/cli/ember-cli/lib/models/blueprint.js b/packages/@angular/cli/ember-cli/lib/models/blueprint.js index 6928082b3360..8b20725f8307 100644 --- a/packages/@angular/cli/ember-cli/lib/models/blueprint.js +++ b/packages/@angular/cli/ember-cli/lib/models/blueprint.js @@ -364,6 +364,13 @@ Blueprint.prototype._writeStatusToUI = function(chalkColor, keyword, message) { } }; +Blueprint.prototype.addModifiedFile = function(file) { + if (!this.modifiedFiles) { + this.modifiedFiles = []; + } + this.modifiedFiles.push(file); +} + /** @private @method _writeFile @@ -372,6 +379,7 @@ Blueprint.prototype._writeStatusToUI = function(chalkColor, keyword, message) { */ Blueprint.prototype._writeFile = function(info) { if (!this.dryRun) { + this.addModifiedFile(info.outputPath); return writeFile(info.outputPath, info.render()); } }; diff --git a/packages/@angular/cli/lib/config/schema.json b/packages/@angular/cli/lib/config/schema.json index 57467fb1c48e..a18bae04ae31 100644 --- a/packages/@angular/cli/lib/config/schema.json +++ b/packages/@angular/cli/lib/config/schema.json @@ -299,6 +299,11 @@ "description": "How often to check for file updates.", "type": "number" }, + "lintFix": { + "description": "Use lint to fix files after generation", + "type": "boolean", + "default": false + }, "class": { "description": "Options for generating a class.", "type": "object", diff --git a/packages/@angular/cli/tasks/lint.ts b/packages/@angular/cli/tasks/lint.ts index 394862d21bc9..83b888f178fa 100644 --- a/packages/@angular/cli/tasks/lint.ts +++ b/packages/@angular/cli/tasks/lint.ts @@ -1,26 +1,40 @@ -const Task = require('../ember-cli/lib/models/task'); import * as chalk from 'chalk'; +import * as fs from 'fs'; import * as glob from 'glob'; +import * as path from 'path'; import * as ts from 'typescript'; import { requireProjectModule } from '../utilities/require-project-module'; -import { CliConfig } from '../models/config'; -import { LintCommandOptions } from '../commands/lint'; -interface CliLintConfig { +const SilentError = require('silent-error'); +const Task = require('../ember-cli/lib/models/task'); + +export interface CliLintConfig { files?: (string | string[]); project?: string; tslintConfig?: string; exclude?: (string | string[]); } +export class LintTaskOptions { + fix: boolean; + force: boolean; + format? = 'prose'; + silent? = false; + typeCheck? = false; + configs: Array; +} + export default Task.extend({ - run: function (commandOptions: LintCommandOptions) { + run: function (options: LintTaskOptions) { + options = { ...new LintTaskOptions(), ...options }; const ui = this.ui; const projectRoot = this.project.root; - const lintConfigs: CliLintConfig[] = CliConfig.fromProject().config.lint || []; + const lintConfigs = options.configs || []; if (lintConfigs.length === 0) { - ui.writeLine(chalk.yellow('No lint configuration(s) found.')); + if (!options.silent) { + ui.writeLine(chalk.yellow('No lint configuration(s) found.')); + } return Promise.resolve(0); } @@ -30,22 +44,37 @@ export default Task.extend({ const result = lintConfigs .map((config) => { - const program: ts.Program = Linter.createProgram(config.project); + let program: ts.Program; + if (config.project) { + program = Linter.createProgram(config.project); + } else if (options.typeCheck) { + if (!options.silent) { + ui.writeLine(chalk.yellow('A "project" must be specified to enable type checking.')); + } + } const files = getFilesToLint(program, config, Linter); const lintOptions = { - fix: commandOptions.fix, - formatter: commandOptions.format + fix: options.fix, + formatter: options.format }; - const lintProgram = commandOptions.typeCheck ? program : undefined; + const lintProgram = options.typeCheck ? program : undefined; const linter = new Linter(lintOptions, lintProgram); + let lastDirectory: string; + let configLoad: any; files.forEach((file) => { - const sourceFile = program.getSourceFile(file); - if (!sourceFile) { + const fileContents = getFileContents(file, program); + if (!fileContents) { return; } - const fileContents = sourceFile.getFullText(); - const configLoad = Configuration.findConfiguration(config.tslintConfig, file); + + // Only check for a new tslint config if path changes + const currentDirectory = path.dirname(file); + if (currentDirectory !== lastDirectory) { + configLoad = Configuration.findConfiguration(config.tslintConfig, file); + lastDirectory = currentDirectory; + } + linter.lint(file, fileContents, configLoad.results); }); @@ -65,26 +94,35 @@ export default Task.extend({ fixes: undefined }); - const Formatter = tslint.findFormatter(commandOptions.format); - const formatter = new Formatter(); - - const output = formatter.format(result.failures, result.fixes); - if (output) { - ui.writeLine(output); + if (!options.silent) { + const Formatter = tslint.findFormatter(options.format); + if (!Formatter) { + throw new SilentError(chalk.red(`Invalid lint format "${options.format}".`)); + } + const formatter = new Formatter(); + + const output = formatter.format(result.failures, result.fixes); + if (output) { + ui.writeLine(output); + } } // print formatter output directly for non human-readable formats - if (['prose', 'verbose', 'stylish'].indexOf(commandOptions.format) == -1) { - return (result.failures.length == 0 || commandOptions.force) + if (['prose', 'verbose', 'stylish'].indexOf(options.format) == -1) { + return (result.failures.length == 0 || options.force) ? Promise.resolve(0) : Promise.resolve(2); } if (result.failures.length > 0) { - ui.writeLine(chalk.red('Lint errors found in the listed files.')); - return commandOptions.force ? Promise.resolve(0) : Promise.resolve(2); + if (!options.silent) { + ui.writeLine(chalk.red('Lint errors found in the listed files.')); + } + return options.force ? Promise.resolve(0) : Promise.resolve(2); } - ui.writeLine(chalk.green('All files pass linting.')); + if (!options.silent) { + ui.writeLine(chalk.green('All files pass linting.')); + } return Promise.resolve(0); } }); @@ -92,15 +130,15 @@ export default Task.extend({ function getFilesToLint(program: ts.Program, lintConfig: CliLintConfig, Linter: any): string[] { let files: string[] = []; - if (lintConfig.files !== null) { + if (lintConfig.files) { files = Array.isArray(lintConfig.files) ? lintConfig.files : [lintConfig.files]; - } else { + } else if (program) { files = Linter.getFileNames(program); } let globOptions = {}; - if (lintConfig.exclude !== null) { + if (lintConfig.exclude) { const excludePatterns = Array.isArray(lintConfig.exclude) ? lintConfig.exclude : [lintConfig.exclude]; @@ -114,3 +152,23 @@ function getFilesToLint(program: ts.Program, lintConfig: CliLintConfig, Linter: return files; } + +function getFileContents(file: string, program?: ts.Program): string { + let contents: string; + + if (program) { + const sourceFile = program.getSourceFile(file); + if (sourceFile) { + contents = sourceFile.getFullText(); + } + } else { + // NOTE: The tslint CLI checks for and excludes MPEG transport streams; this does not. + try { + contents = fs.readFileSync(file, 'utf8'); + } catch (e) { + throw new SilentError(`Could not read file "${file}".`); + } + } + + return contents; +} diff --git a/tests/e2e/tests/generate/lint-fix.ts b/tests/e2e/tests/generate/lint-fix.ts new file mode 100644 index 000000000000..1512ff501766 --- /dev/null +++ b/tests/e2e/tests/generate/lint-fix.ts @@ -0,0 +1,37 @@ +import { ng } from '../../utils/process'; +import { writeFile } from '../../utils/fs'; +import { expectToFail } from '../../utils/utils'; + +export default function () { + const nestedConfigContent = ` + { + "rules": { + "quotemark": [ + true, + "double", + "avoid-escape" + ] + } + }`; + + return Promise.resolve() + // setup a double-quote tslint config + .then(() => writeFile('src/app/tslint.json', nestedConfigContent)) + + // Generate a fixed new component but don't fix rest of app + .then(() => ng('generate', 'component', 'test-component1', '--lint-fix')) + .then(() => expectToFail(() => ng('lint'))) + + // Fix rest of app and generate new component + .then(() => ng('lint', '--fix')) + .then(() => ng('generate', 'component', 'test-component2', '--lint-fix')) + .then(() => ng('lint')) + + // Enable default option and generate all other module related blueprints + .then(() => ng('set', 'defaults.lintFix', 'true')) + .then(() => ng('generate', 'directive', 'test-directive')) + .then(() => ng('generate', 'service', 'test-service', '--module', 'app.module.ts')) + .then(() => ng('generate', 'pipe', 'test-pipe')) + .then(() => ng('generate', 'guard', 'test-guard', '--module', 'app.module.ts')) + .then(() => ng('lint')); +} diff --git a/tests/e2e/tests/lint/lint-no-project.ts b/tests/e2e/tests/lint/lint-no-project.ts new file mode 100644 index 000000000000..d558a76c8d50 --- /dev/null +++ b/tests/e2e/tests/lint/lint-no-project.ts @@ -0,0 +1,26 @@ +import { ng } from '../../utils/process'; +import { writeFile } from '../../utils/fs'; +import { expectToFail } from '../../utils/utils'; +import { oneLine } from 'common-tags'; + +export default function () { + return Promise.resolve() + .then(() => ng('set', 'lint.0.project', '')) + .then(() => ng('lint', '--type-check')) + .then(({ stdout }) => { + if (!stdout.match(/A "project" must be specified to enable type checking./)) { + throw new Error(oneLine` + Expected to match "A "project" must be specified to enable type checking." + in ${stdout}. + `); + } + + return stdout; + }) + .then(() => ng('set', 'lint.0.files', '"**/baz.ts"')) + .then(() => writeFile('src/app/foo.ts', 'const foo = "";\n')) + .then(() => writeFile('src/app/baz.ts', 'const baz = \'\';\n')) + .then(() => ng('lint')) + .then(() => ng('set', 'lint.0.files', '"**/foo.ts"')) + .then(() => expectToFail(() => ng('lint'))); +}