diff --git a/packages/angular/cli/models/architect-command.ts b/packages/angular/cli/models/architect-command.ts index 58a811eed629..7cf7aee77241 100644 --- a/packages/angular/cli/models/architect-command.ts +++ b/packages/angular/cli/models/architect-command.ts @@ -5,15 +5,20 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ - -// tslint:disable:no-global-tslint-disable no-any import { Architect, BuildEvent, BuilderDescription, TargetSpecifier, } from '@angular-devkit/architect'; -import { JsonObject, experimental, schema, strings } from '@angular-devkit/core'; +import { + JsonObject, + UnknownException, + experimental, + schema, + strings, + tags, +} from '@angular-devkit/core'; import { NodeJsSyncHost, createConsoleLogger } from '@angular-devkit/core/node'; import { of } from 'rxjs'; import { from } from 'rxjs'; @@ -90,19 +95,53 @@ export abstract class ArchitectCommand extends Command const projectNames = this.getProjectNamesByTarget(this.target); const { overrides } = this._makeTargetSpecifier(options); if (projectNames.length > 1 && Object.keys(overrides || {}).length > 0) { - throw new Error('Architect commands with multiple targets cannot specify overrides.' - + `'${this.target}' would be run on the following projects: ${projectNames.join()}`); + // Verify that all builders are the same, otherwise error out (since the meaning of an + // option could vary from builder to builder). + + const builders: string[] = []; + for (const projectName of projectNames) { + const targetSpec: TargetSpecifier = this._makeTargetSpecifier(options); + const targetDesc = this._architect.getBuilderConfiguration({ + project: projectName, + target: targetSpec.target, + }); + + if (builders.indexOf(targetDesc.builder) == -1) { + builders.push(targetDesc.builder); + } + } + + if (builders.length > 1) { + throw new Error(tags.oneLine` + Architect commands with command line overrides cannot target different builders. The + '${this.target}' target would run on projects ${projectNames.join()} which have the + following builders: ${'\n ' + builders.join('\n ')} + `); + } } } return true; } - protected mapArchitectOptions(schema: any) { + protected mapArchitectOptions(schema: JsonObject) { const properties = schema.properties; + if (typeof properties != 'object' || properties === null || Array.isArray(properties)) { + throw new UnknownException('Invalid schema.'); + } const keys = Object.keys(properties); keys - .map(key => ({ ...properties[key], ...{ name: strings.dasherize(key) } })) + .map(key => { + const value = properties[key]; + if (typeof value != 'object') { + throw new UnknownException('Invalid schema.'); + } + + return { + ...value, + name: strings.dasherize(key), + } as any; // tslint:disable-line:no-any + }) .map(opt => { let type; const schematicType = opt.type; diff --git a/packages/angular_devkit/schematics/tasks/tslint-fix/executor.ts b/packages/angular_devkit/schematics/tasks/tslint-fix/executor.ts index 2afb3f05ed1e..dc8f19b83cc6 100644 --- a/packages/angular_devkit/schematics/tasks/tslint-fix/executor.ts +++ b/packages/angular_devkit/schematics/tasks/tslint-fix/executor.ts @@ -31,9 +31,9 @@ function _loadConfiguration( if (options.tslintConfig) { return Configuration.parseConfigFile(options.tslintConfig, root); } else if (options.tslintPath) { - const tslintPath = path.join(root, options.tslintPath); - - return Configuration.findConfiguration(tslintPath, file && path.join(root, file)).results; + return Configuration.findConfiguration(path.join(root, options.tslintPath)).results; + } else if (file) { + return Configuration.findConfiguration(null, file).results; } else { throw new Error('Executor must specify a tslint configuration.'); } @@ -103,13 +103,18 @@ export default function(): TaskExecutor { ? options.includes : (options.includes ? [options.includes] : []) ); + const files = ( + Array.isArray(options.files) + ? options.files + : (options.files ? [options.files] : []) + ); const Linter = tslint.Linter as LinterT; const Configuration = tslint.Configuration as ConfigurationT; let program: ts.Program | undefined = undefined; - let filesToLint: string[] = []; + let filesToLint: string[] = files; - if (options.tsConfigPath) { + if (options.tsConfigPath && files.length == 0) { const tsConfigPath = path.join(process.cwd(), options.tsConfigPath); if (!fs.existsSync(tsConfigPath)) { @@ -148,9 +153,16 @@ export default function(): TaskExecutor { }; const linter = new Linter(lintOptions, program); - const config = _loadConfiguration(Configuration, options, root); + // If directory doesn't change, we + let lastDirectory: string | null = null; + let config; for (const file of filesToLint) { + const dir = path.dirname(file); + if (lastDirectory !== dir) { + lastDirectory = dir; + config = _loadConfiguration(Configuration, options, root, file); + } const content = _getFileContent(file, options, program); if (!content) { diff --git a/packages/angular_devkit/schematics/tasks/tslint-fix/options.ts b/packages/angular_devkit/schematics/tasks/tslint-fix/options.ts index e6b3ac520f95..64243f27d520 100644 --- a/packages/angular_devkit/schematics/tasks/tslint-fix/options.ts +++ b/packages/angular_devkit/schematics/tasks/tslint-fix/options.ts @@ -17,16 +17,10 @@ export interface TslintFixTaskOptionsBase { ignoreErrors?: boolean; includes?: string | string[]; -} - -export interface TslintFixTaskOptionsPath extends TslintFixTaskOptionsBase { - tslintPath: string; - tslintConfig?: never; -} + files?: string | string[]; -export interface TslintFixTaskOptionsConfig extends TslintFixTaskOptionsBase { - tslintPath?: never; - tslintConfig: JsonObject; + tslintPath?: string; + tslintConfig?: JsonObject; } -export type TslintFixTaskOptions = TslintFixTaskOptionsPath | TslintFixTaskOptionsConfig; +export type TslintFixTaskOptions = TslintFixTaskOptionsBase; diff --git a/packages/angular_devkit/schematics/tasks/tslint-fix/task.ts b/packages/angular_devkit/schematics/tasks/tslint-fix/task.ts index 3b6eaa000b2e..99d478e3013f 100644 --- a/packages/angular_devkit/schematics/tasks/tslint-fix/task.ts +++ b/packages/angular_devkit/schematics/tasks/tslint-fix/task.ts @@ -11,19 +11,34 @@ import { TslintFixName, TslintFixTaskOptions, TslintFixTaskOptionsBase } from '. export class TslintFixTask implements TaskConfigurationGenerator { + protected _configOrPath: null | string | JsonObject; + protected _options: TslintFixTaskOptionsBase; + constructor(config: JsonObject, options: TslintFixTaskOptionsBase); + constructor(options: TslintFixTaskOptionsBase); constructor(path: string, options: TslintFixTaskOptionsBase); constructor( - protected _configOrPath: string | JsonObject, - protected _options: TslintFixTaskOptionsBase, - ) {} + configOrPath: string | JsonObject | TslintFixTaskOptionsBase, + options?: TslintFixTaskOptionsBase, + ) { + if (options) { + this._configOrPath = configOrPath as string | JsonObject; + this._options = options; + } else { + this._options = configOrPath as TslintFixTaskOptionsBase; + this._configOrPath = null; + } + } toConfiguration(): TaskConfiguration { + const path = typeof this._configOrPath == 'string' ? { tslintPath: this._configOrPath } : {}; + const config = typeof this._configOrPath == 'object' && this._configOrPath !== null + ? { tslintConfig: this._configOrPath } + : {}; const options = { ...this._options, - ...((typeof this._configOrPath == 'string' - ? { tslintPath: this._configOrPath } - : { tslintConfig: this._configOrPath })), + ...path, + ...config, }; return { name: TslintFixName, options }; diff --git a/packages/schematics/angular/component/index.ts b/packages/schematics/angular/component/index.ts index b74bfa98421b..b2d35f50c6c4 100644 --- a/packages/schematics/angular/component/index.ts +++ b/packages/schematics/angular/component/index.ts @@ -29,6 +29,7 @@ import { import { InsertChange } from '../utility/change'; import { getWorkspace } from '../utility/config'; import { buildRelativePath, findModuleFromOptions } from '../utility/find-module'; +import { applyLintFix } from '../utility/lint-fix'; import { parseName } from '../utility/parse-name'; import { buildDefaultPath } from '../utility/project'; import { validateHtmlSelector, validateName } from '../utility/validation'; @@ -164,6 +165,7 @@ export default function(options: ComponentOptions): Rule { addDeclarationToNgModule(options), mergeWith(templateSource), ])), + options.lintFix ? applyLintFix(options.path) : noop(), ]); }; } diff --git a/packages/schematics/angular/component/schema.d.ts b/packages/schematics/angular/component/schema.d.ts index cc7b8cd5185d..58a899b66c87 100644 --- a/packages/schematics/angular/component/schema.d.ts +++ b/packages/schematics/angular/component/schema.d.ts @@ -71,4 +71,8 @@ export interface Schema { * Specifies if the component is an entry component of declaring module. */ entryComponent?: boolean; + /** + * Specifies whether to apply lint fixes after generating the component. + */ + lintFix?: boolean; } diff --git a/packages/schematics/angular/component/schema.json b/packages/schematics/angular/component/schema.json index c9a6a58800e1..d28117756ad9 100644 --- a/packages/schematics/angular/component/schema.json +++ b/packages/schematics/angular/component/schema.json @@ -103,6 +103,11 @@ "type": "boolean", "default": false, "description": "Specifies if the component is an entry component of declaring module." + }, + "lintFix": { + "type": "boolean", + "default": false, + "description": "Specifies whether to apply lint fixes after generating the component." } }, "required": [], diff --git a/packages/schematics/angular/directive/index.ts b/packages/schematics/angular/directive/index.ts index efed2b4a5050..0830f260695b 100644 --- a/packages/schematics/angular/directive/index.ts +++ b/packages/schematics/angular/directive/index.ts @@ -25,6 +25,7 @@ import { addDeclarationToModule, addExportToModule } from '../utility/ast-utils' import { InsertChange } from '../utility/change'; import { getWorkspace } from '../utility/config'; import { buildRelativePath, findModuleFromOptions } from '../utility/find-module'; +import { applyLintFix } from '../utility/lint-fix'; import { parseName } from '../utility/parse-name'; import { buildDefaultPath } from '../utility/project'; import { validateHtmlSelector } from '../utility/validation'; @@ -137,6 +138,7 @@ export default function (options: DirectiveOptions): Rule { addDeclarationToNgModule(options), mergeWith(templateSource), ])), + options.lintFix ? applyLintFix(options.path) : noop(), ]); }; } diff --git a/packages/schematics/angular/directive/schema.d.ts b/packages/schematics/angular/directive/schema.d.ts index 039c01c7498d..d7d73803801f 100644 --- a/packages/schematics/angular/directive/schema.d.ts +++ b/packages/schematics/angular/directive/schema.d.ts @@ -47,4 +47,8 @@ export interface Schema { * Specifies if declaring module exports the directive. */ export?: boolean; + /** + * Specifies whether to apply lint fixes after generating the component. + */ + lintFix?: boolean; } diff --git a/packages/schematics/angular/directive/schema.json b/packages/schematics/angular/directive/schema.json index 4f4aafe9626e..ddfe644b0e51 100644 --- a/packages/schematics/angular/directive/schema.json +++ b/packages/schematics/angular/directive/schema.json @@ -68,6 +68,11 @@ "type": "boolean", "default": false, "description": "Specifies if declaring module exports the directive." + }, + "lintFix": { + "type": "boolean", + "default": false, + "description": "Specifies whether to apply lint fixes after generating the directive." } }, "required": [], diff --git a/packages/schematics/angular/enum/index.ts b/packages/schematics/angular/enum/index.ts index 3dacffdbe56e..2784e9bdfaac 100644 --- a/packages/schematics/angular/enum/index.ts +++ b/packages/schematics/angular/enum/index.ts @@ -16,10 +16,12 @@ import { chain, mergeWith, move, + noop, template, url, } from '@angular-devkit/schematics'; import { getWorkspace } from '../utility/config'; +import { applyLintFix } from '../utility/lint-fix'; import { parseName } from '../utility/parse-name'; import { buildDefaultPath } from '../utility/project'; import { Schema as EnumOptions } from './schema'; @@ -53,6 +55,7 @@ export default function (options: EnumOptions): Rule { branchAndMerge(chain([ mergeWith(templateSource), ])), + options.lintFix ? applyLintFix(options.path) : noop(), ]); }; } diff --git a/packages/schematics/angular/enum/schema.d.ts b/packages/schematics/angular/enum/schema.d.ts index 47671ee9669f..3f5a7b610b79 100644 --- a/packages/schematics/angular/enum/schema.d.ts +++ b/packages/schematics/angular/enum/schema.d.ts @@ -19,4 +19,8 @@ export interface Schema { * The name of the project. */ project?: string; + /** + * Specifies whether to apply lint fixes after generating the component. + */ + lintFix?: boolean; } diff --git a/packages/schematics/angular/enum/schema.json b/packages/schematics/angular/enum/schema.json index 4b01b9adad75..180c6b67406d 100644 --- a/packages/schematics/angular/enum/schema.json +++ b/packages/schematics/angular/enum/schema.json @@ -24,6 +24,11 @@ "$default": { "$source": "projectName" } + }, + "lintFix": { + "type": "boolean", + "default": false, + "description": "Specifies whether to apply lint fixes after generating the enum." } }, "required": [], diff --git a/packages/schematics/angular/guard/index.ts b/packages/schematics/angular/guard/index.ts index f2d1fea73c43..55d715242f20 100644 --- a/packages/schematics/angular/guard/index.ts +++ b/packages/schematics/angular/guard/index.ts @@ -21,6 +21,7 @@ import { url, } from '@angular-devkit/schematics'; import { getWorkspace } from '../utility/config'; +import { applyLintFix } from '../utility/lint-fix'; import { parseName } from '../utility/parse-name'; import { buildDefaultPath } from '../utility/project'; import { Schema as GuardOptions } from './schema'; @@ -55,6 +56,7 @@ export default function (options: GuardOptions): Rule { branchAndMerge(chain([ mergeWith(templateSource), ])), + options.lintFix ? applyLintFix(options.path) : noop(), ]); }; } diff --git a/packages/schematics/angular/guard/schema.d.ts b/packages/schematics/angular/guard/schema.d.ts index a8095a5b49b3..0ed1b0abbb46 100644 --- a/packages/schematics/angular/guard/schema.d.ts +++ b/packages/schematics/angular/guard/schema.d.ts @@ -27,4 +27,8 @@ export interface Schema { * The name of the project. */ project?: string; + /** + * Specifies whether to apply lint fixes after generating the component. + */ + lintFix?: boolean; } diff --git a/packages/schematics/angular/guard/schema.json b/packages/schematics/angular/guard/schema.json index 2ed99ba4648b..7fba553b2eba 100644 --- a/packages/schematics/angular/guard/schema.json +++ b/packages/schematics/angular/guard/schema.json @@ -34,6 +34,11 @@ "$default": { "$source": "projectName" } + }, + "lintFix": { + "type": "boolean", + "default": false, + "description": "Specifies whether to apply lint fixes after generating the guard." } }, "required": [], diff --git a/packages/schematics/angular/interface/index.ts b/packages/schematics/angular/interface/index.ts index 46196e1cd765..84ce555d7cc3 100644 --- a/packages/schematics/angular/interface/index.ts +++ b/packages/schematics/angular/interface/index.ts @@ -15,10 +15,12 @@ import { chain, mergeWith, move, + noop, template, url, } from '@angular-devkit/schematics'; import { getWorkspace } from '../utility/config'; +import { applyLintFix } from '../utility/lint-fix'; import { parseName } from '../utility/parse-name'; import { buildDefaultPath } from '../utility/project'; import { Schema as InterfaceOptions } from './schema'; @@ -55,6 +57,7 @@ export default function (options: InterfaceOptions): Rule { branchAndMerge(chain([ mergeWith(templateSource), ])), + options.lintFix ? applyLintFix(options.path) : noop(), ]); }; } diff --git a/packages/schematics/angular/interface/schema.d.ts b/packages/schematics/angular/interface/schema.d.ts index f38ea5158c90..f6ac71bf592b 100644 --- a/packages/schematics/angular/interface/schema.d.ts +++ b/packages/schematics/angular/interface/schema.d.ts @@ -27,4 +27,8 @@ export interface Schema { * Specifies the type of interface. */ type?: string; + /** + * Specifies whether to apply lint fixes after generating the component. + */ + lintFix?: boolean; } diff --git a/packages/schematics/angular/interface/schema.json b/packages/schematics/angular/interface/schema.json index afa9f77b9fbc..ff468a67f38c 100644 --- a/packages/schematics/angular/interface/schema.json +++ b/packages/schematics/angular/interface/schema.json @@ -37,6 +37,11 @@ "$source": "argv", "index": 1 } + }, + "lintFix": { + "type": "boolean", + "default": false, + "description": "Specifies whether to apply lint fixes after generating the directive." } }, "required": [], diff --git a/packages/schematics/angular/pipe/index.ts b/packages/schematics/angular/pipe/index.ts index f726240977b4..d9bad4ec691d 100644 --- a/packages/schematics/angular/pipe/index.ts +++ b/packages/schematics/angular/pipe/index.ts @@ -25,6 +25,7 @@ import { addDeclarationToModule, addExportToModule } from '../utility/ast-utils' import { InsertChange } from '../utility/change'; import { getWorkspace } from '../utility/config'; import { buildRelativePath, findModuleFromOptions } from '../utility/find-module'; +import { applyLintFix } from '../utility/lint-fix'; import { parseName } from '../utility/parse-name'; import { buildDefaultPath } from '../utility/project'; import { Schema as PipeOptions } from './schema'; @@ -117,6 +118,7 @@ export default function (options: PipeOptions): Rule { chain([ addDeclarationToNgModule(options), mergeWith(templateSource), + options.lintFix ? applyLintFix(options.path) : noop(), ]), ); }; diff --git a/packages/schematics/angular/pipe/schema.d.ts b/packages/schematics/angular/pipe/schema.d.ts index 70a37a9f3c63..4a8eba445c2d 100644 --- a/packages/schematics/angular/pipe/schema.d.ts +++ b/packages/schematics/angular/pipe/schema.d.ts @@ -39,4 +39,8 @@ export interface Schema { * Specifies if declaring module exports the pipe. */ export?: boolean; + /** + * Specifies whether to apply lint fixes after generating the component. + */ + lintFix?: boolean; } diff --git a/packages/schematics/angular/pipe/schema.json b/packages/schematics/angular/pipe/schema.json index 52ed3a778bc6..cf1ddf8ba0c7 100644 --- a/packages/schematics/angular/pipe/schema.json +++ b/packages/schematics/angular/pipe/schema.json @@ -50,6 +50,11 @@ "type": "boolean", "default": false, "description": "Specifies if declaring module exports the pipe." + }, + "lintFix": { + "type": "boolean", + "default": false, + "description": "Specifies whether to apply lint fixes after generating the pipe." } }, "required": [], diff --git a/packages/schematics/angular/service/index.ts b/packages/schematics/angular/service/index.ts index 02d63b22aba5..f5b78f19e931 100644 --- a/packages/schematics/angular/service/index.ts +++ b/packages/schematics/angular/service/index.ts @@ -11,6 +11,7 @@ import { SchematicsException, Tree, apply, + chain, filter, mergeWith, move, @@ -19,6 +20,7 @@ import { url, } from '@angular-devkit/schematics'; import { getWorkspace } from '../utility/config'; +import { applyLintFix } from '../utility/lint-fix'; import { parseName } from '../utility/parse-name'; import { buildDefaultPath } from '../utility/project'; import { Schema as ServiceOptions } from './schema'; @@ -49,6 +51,9 @@ export default function (options: ServiceOptions): Rule { move(parsedPath.path), ]); - return mergeWith(templateSource); + return chain([ + mergeWith(templateSource), + options.lintFix ? applyLintFix(options.path) : noop(), + ]); }; } diff --git a/packages/schematics/angular/service/schema.d.ts b/packages/schematics/angular/service/schema.d.ts index 0dc5a421561b..3196cfe1a5ee 100644 --- a/packages/schematics/angular/service/schema.d.ts +++ b/packages/schematics/angular/service/schema.d.ts @@ -27,4 +27,8 @@ export interface Schema { * Specifies if a spec file is generated. */ spec?: boolean; + /** + * Specifies whether to apply lint fixes after generating the component. + */ + lintFix?: boolean; } diff --git a/packages/schematics/angular/service/schema.json b/packages/schematics/angular/service/schema.json index e2c3b4a720bd..bf93afba4c57 100644 --- a/packages/schematics/angular/service/schema.json +++ b/packages/schematics/angular/service/schema.json @@ -34,6 +34,11 @@ "type": "boolean", "default": true, "description": "Specifies if a spec file is generated." + }, + "lintFix": { + "type": "boolean", + "default": false, + "description": "Specifies whether to apply lint fixes after generating the pipe." } }, "required": [], diff --git a/packages/schematics/angular/utility/lint-fix.ts b/packages/schematics/angular/utility/lint-fix.ts new file mode 100644 index 000000000000..6376942c548c --- /dev/null +++ b/packages/schematics/angular/utility/lint-fix.ts @@ -0,0 +1,50 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { + DirEntry, + Rule, + SchematicContext, + SchematicsException, + Tree, +} from '@angular-devkit/schematics'; +import { TslintFixTask } from '@angular-devkit/schematics/tasks'; + +export function applyLintFix(path = '/'): Rule { + return (tree: Tree, context: SchematicContext) => { + // Find the closest tsling.json + let dir: DirEntry | null = tree.getDir(path.substr(0, path.lastIndexOf('/'))); + + do { + if ((dir.subfiles as string[]).includes('tslint.json')) { + break; + } + + dir = dir.parent; + } while (dir !== null); + + if (dir === null) { + throw new SchematicsException('Asked to run lint fixes, but could not find a tslint.json.'); + } + + // Only include files that have been touched. + const files = tree.actions.reduce((acc: Set, action) => { + const path = action.path.substr(1); // Remove the starting '/'. + if (path.endsWith('.ts') && dir && action.path.startsWith(dir.path)) { + acc.add(path); + } + + return acc; + }, new Set()); + + context.addTask(new TslintFixTask({ + ignoreErrors: true, + tsConfigPath: 'tsconfig.json', + files: [...files], + })); + }; +} diff --git a/tests/legacy-cli/e2e/tests/generate/lint-fix.ts b/tests/legacy-cli/e2e/tests/generate/lint-fix.ts index f7925489c8c7..10394ca45561 100644 --- a/tests/legacy-cli/e2e/tests/generate/lint-fix.ts +++ b/tests/legacy-cli/e2e/tests/generate/lint-fix.ts @@ -14,9 +14,6 @@ export default function () { } }`; - // TODO(architect): reenable after figuring out what happens to --lint-fix. - return; - return Promise.resolve() // setup a double-quote tslint config @@ -32,10 +29,13 @@ export default function () { .then(() => ng('lint')) // Enable default option and generate all other module related blueprints - .then(() => ng('config', 'defaults.lintFix', 'true')) + .then(() => ng('config', 'schematics.@schematics/angular.directive.lintFix', 'true')) + .then(() => ng('config', 'schematics.@schematics/angular.service.lintFix', 'true')) + .then(() => ng('config', 'schematics.@schematics/angular.pipe.lintFix', 'true')) + .then(() => ng('config', 'schematics.@schematics/angular.guard.lintFix', 'true')) .then(() => ng('generate', 'directive', 'test-directive')) .then(() => ng('generate', 'service', 'test-service')) .then(() => ng('generate', 'pipe', 'test-pipe')) - .then(() => ng('generate', 'guard', 'test-guard', '--module', 'app.module.ts')) + .then(() => ng('generate', 'guard', 'test-guard')) .then(() => ng('lint')); }