Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit df1b56c

Browse files
hanslKeen Yee Liau
authored and
Keen Yee Liau
committedFeb 19, 2019
feat(@angular-devkit/build-angular): move tslint to new API
It is only new files and the old builder is still available. The new one can only be used by the new Architect API.
1 parent 685d4d0 commit df1b56c

File tree

3 files changed

+513
-0
lines changed

3 files changed

+513
-0
lines changed
 

‎packages/angular_devkit/build_angular/builders.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"description": "Run protractor over a dev server."
3333
},
3434
"tslint": {
35+
"implementation": "./src/tslint/index2",
3536
"class": "./src/tslint",
3637
"schema": "./src/tslint/schema.json",
3738
"description": "Run tslint over a TS project."
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
import { BuilderContext, BuilderOutput, createBuilder } from '@angular-devkit/architect/src/index2';
9+
import { json } from '@angular-devkit/core';
10+
import { readFileSync } from 'fs';
11+
import * as glob from 'glob';
12+
import { Minimatch } from 'minimatch';
13+
import * as path from 'path';
14+
import * as tslint from 'tslint'; // tslint:disable-line:no-implicit-dependencies
15+
import * as ts from 'typescript'; // tslint:disable-line:no-implicit-dependencies
16+
import { stripBom } from '../angular-cli-files/utilities/strip-bom';
17+
import { Schema as RealTslintBuilderOptions } from './schema';
18+
19+
20+
type TslintBuilderOptions = RealTslintBuilderOptions & json.JsonObject;
21+
22+
23+
async function _loadTslint() {
24+
let tslint;
25+
try {
26+
tslint = await import('tslint'); // tslint:disable-line:no-implicit-dependencies
27+
} catch {
28+
throw new Error('Unable to find TSLint. Ensure TSLint is installed.');
29+
}
30+
31+
const version = tslint.Linter.VERSION && tslint.Linter.VERSION.split('.');
32+
if (!version || version.length < 2 || Number(version[0]) < 5 || Number(version[1]) < 5) {
33+
throw new Error('TSLint must be version 5.5 or higher.');
34+
}
35+
36+
return tslint;
37+
}
38+
39+
40+
async function _run(config: TslintBuilderOptions, context: BuilderContext): Promise<BuilderOutput> {
41+
const systemRoot = context.workspaceRoot;
42+
process.chdir(context.currentDirectory);
43+
const options = config;
44+
const projectName = context.target && context.target.project || '<???>';
45+
46+
// Print formatter output only for non human-readable formats.
47+
const printInfo = ['prose', 'verbose', 'stylish'].includes(options.format || '')
48+
&& !options.silent;
49+
50+
context.reportStatus(`Linting ${JSON.stringify(projectName)}...`);
51+
if (printInfo) {
52+
context.logger.info(`Linting ${JSON.stringify(projectName)}...`);
53+
}
54+
55+
if (!options.tsConfig && options.typeCheck) {
56+
throw new Error('A "project" must be specified to enable type checking.');
57+
}
58+
59+
const projectTslint = await _loadTslint();
60+
const tslintConfigPath = options.tslintConfig
61+
? path.resolve(systemRoot, options.tslintConfig)
62+
: null;
63+
const Linter = projectTslint.Linter;
64+
65+
let result: undefined | tslint.LintResult = undefined;
66+
if (options.tsConfig) {
67+
const tsConfigs = Array.isArray(options.tsConfig) ? options.tsConfig : [options.tsConfig];
68+
context.reportProgress(0, tsConfigs.length);
69+
const allPrograms = tsConfigs.map(tsConfig => {
70+
return Linter.createProgram(path.resolve(systemRoot, tsConfig));
71+
});
72+
73+
let i = 0;
74+
for (const program of allPrograms) {
75+
const partial
76+
= await _lint(projectTslint, systemRoot, tslintConfigPath, options, program, allPrograms);
77+
if (result === undefined) {
78+
result = partial;
79+
} else {
80+
result.failures = result.failures
81+
.filter(curr => {
82+
return !partial.failures.some(prev => curr.equals(prev));
83+
})
84+
.concat(partial.failures);
85+
86+
// we are not doing much with 'errorCount' and 'warningCount'
87+
// apart from checking if they are greater than 0 thus no need to dedupe these.
88+
result.errorCount += partial.errorCount;
89+
result.warningCount += partial.warningCount;
90+
91+
if (partial.fixes) {
92+
result.fixes = result.fixes ? result.fixes.concat(partial.fixes) : partial.fixes;
93+
}
94+
}
95+
96+
context.reportProgress(++i, allPrograms.length);
97+
}
98+
} else {
99+
result = await _lint(projectTslint, systemRoot, tslintConfigPath, options);
100+
}
101+
102+
if (result == undefined) {
103+
throw new Error('Invalid lint configuration. Nothing to lint.');
104+
}
105+
106+
if (!options.silent) {
107+
const Formatter = projectTslint.findFormatter(options.format || '');
108+
if (!Formatter) {
109+
throw new Error(`Invalid lint format "${options.format}".`);
110+
}
111+
const formatter = new Formatter();
112+
113+
const output = formatter.format(result.failures, result.fixes);
114+
if (output.trim()) {
115+
context.logger.info(output);
116+
}
117+
}
118+
119+
if (result.warningCount > 0 && printInfo) {
120+
context.logger.warn('Lint warnings found in the listed files.');
121+
}
122+
123+
if (result.errorCount > 0 && printInfo) {
124+
context.logger.error('Lint errors found in the listed files.');
125+
}
126+
127+
if (result.warningCount === 0 && result.errorCount === 0 && printInfo) {
128+
context.logger.info('All files pass linting.');
129+
}
130+
131+
return {
132+
success: options.force || result.errorCount === 0,
133+
};
134+
}
135+
136+
137+
export default createBuilder<TslintBuilderOptions>(_run);
138+
139+
140+
async function _lint(
141+
projectTslint: typeof tslint,
142+
systemRoot: string,
143+
tslintConfigPath: string | null,
144+
options: TslintBuilderOptions,
145+
program?: ts.Program,
146+
allPrograms?: ts.Program[],
147+
) {
148+
const Linter = projectTslint.Linter;
149+
const Configuration = projectTslint.Configuration;
150+
151+
const files = getFilesToLint(systemRoot, options, Linter, program);
152+
const lintOptions = {
153+
fix: !!options.fix,
154+
formatter: options.format,
155+
};
156+
157+
const linter = new Linter(lintOptions, program);
158+
159+
let lastDirectory: string | undefined = undefined;
160+
let configLoad;
161+
for (const file of files) {
162+
if (program && allPrograms) {
163+
// If it cannot be found in ANY program, then this is an error.
164+
if (allPrograms.every(p => p.getSourceFile(file) === undefined)) {
165+
throw new Error(
166+
`File ${JSON.stringify(file)} is not part of a TypeScript project '${options.tsConfig}'.`,
167+
);
168+
} else if (program.getSourceFile(file) === undefined) {
169+
// The file exists in some other programs. We will lint it later (or earlier) in the loop.
170+
continue;
171+
}
172+
}
173+
174+
const contents = getFileContents(file);
175+
176+
// Only check for a new tslint config if the path changes.
177+
const currentDirectory = path.dirname(file);
178+
if (currentDirectory !== lastDirectory) {
179+
configLoad = Configuration.findConfiguration(tslintConfigPath, file);
180+
lastDirectory = currentDirectory;
181+
}
182+
183+
if (configLoad) {
184+
// Give some breathing space to other promises that might be waiting.
185+
await Promise.resolve();
186+
linter.lint(file, contents, configLoad.results);
187+
}
188+
}
189+
190+
return linter.getResult();
191+
}
192+
193+
function getFilesToLint(
194+
root: string,
195+
options: TslintBuilderOptions,
196+
linter: typeof tslint.Linter,
197+
program?: ts.Program,
198+
): string[] {
199+
const ignore = options.exclude;
200+
const files = options.files || [];
201+
202+
if (files.length > 0) {
203+
return files
204+
.map(file => glob.sync(file, { cwd: root, ignore, nodir: true }))
205+
.reduce((prev, curr) => prev.concat(curr), [])
206+
.map(file => path.join(root, file));
207+
}
208+
209+
if (!program) {
210+
return [];
211+
}
212+
213+
let programFiles = linter.getFileNames(program);
214+
215+
if (ignore && ignore.length > 0) {
216+
// normalize to support ./ paths
217+
const ignoreMatchers = ignore
218+
.map(pattern => new Minimatch(path.normalize(pattern), { dot: true }));
219+
220+
programFiles = programFiles
221+
.filter(file => !ignoreMatchers.some(matcher => matcher.match(path.relative(root, file))));
222+
}
223+
224+
return programFiles;
225+
}
226+
227+
function getFileContents(file: string): string {
228+
// NOTE: The tslint CLI checks for and excludes MPEG transport streams; this does not.
229+
try {
230+
return stripBom(readFileSync(file, 'utf-8'));
231+
} catch {
232+
throw new Error(`Could not read file '${file}'.`);
233+
}
234+
}
Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import { WorkspaceNodeModulesArchitectHost } from '@angular-devkit/architect/node';
10+
import { Architect, Target } from '@angular-devkit/architect/src/index2';
11+
import { TestingArchitectHost } from '@angular-devkit/architect/testing/index2';
12+
import { experimental, join, logging, normalize, schema } from '@angular-devkit/core';
13+
import { NodeJsSyncHost } from '@angular-devkit/core/node';
14+
import * as fs from 'fs';
15+
import * as path from 'path';
16+
17+
const devkitRoot = normalize((global as any)._DevKitRoot); // tslint:disable-line:no-any
18+
const workspaceRoot = join(devkitRoot, 'tests/angular_devkit/build_angular/hello-world-app/');
19+
const lintTarget: Target = { project: 'app', target: 'lint' };
20+
21+
// tslint:disable-next-line:no-big-function
22+
describe('Tslint Target', () => {
23+
// const filesWithErrors = { 'src/foo.ts': 'const foo = "";\n' };
24+
let testArchitectHost: TestingArchitectHost;
25+
let architect: Architect;
26+
27+
beforeEach(async () => {
28+
const vfHost = new NodeJsSyncHost();
29+
const configContent = fs.readFileSync(path.join(workspaceRoot, 'angular.json'), 'utf-8');
30+
const workspaceJson = JSON.parse(configContent);
31+
32+
const registry = new schema.CoreSchemaRegistry();
33+
registry.addPostTransform(schema.transforms.addUndefinedDefaults);
34+
35+
const workspace = new experimental.workspace.Workspace(workspaceRoot, vfHost);
36+
await workspace.loadWorkspaceFromJson(workspaceJson).toPromise();
37+
38+
testArchitectHost = new TestingArchitectHost(
39+
workspaceRoot, workspaceRoot,
40+
new WorkspaceNodeModulesArchitectHost(workspace, workspaceRoot),
41+
);
42+
architect = new Architect(testArchitectHost, registry);
43+
});
44+
45+
it('works', async () => {
46+
const run = await architect.scheduleTarget({ project: 'app', target: 'lint' });
47+
const output = await run.result;
48+
expect(output.success).toBe(true);
49+
await run.stop();
50+
}, 30000);
51+
52+
it(`should show project name as status and in the logs`, async () => {
53+
// Check logs.
54+
const logger = new logging.Logger('lint-info');
55+
const allLogs: string[] = [];
56+
logger.subscribe(entry => allLogs.push(entry.message));
57+
58+
const run = await architect.scheduleTarget(lintTarget, {}, { logger });
59+
60+
// Check status updates.
61+
const allStatus: string[] = [];
62+
run.progress.subscribe(progress => {
63+
if (progress.status !== undefined) {
64+
allStatus.push(progress.status);
65+
}
66+
});
67+
68+
const output = await run.result;
69+
expect(output.success).toBe(true);
70+
expect(allStatus).toContain(jasmine.stringMatching(/linting.*"app".*/i));
71+
expect(allLogs).toContain(jasmine.stringMatching(/linting.*"app".*/i));
72+
await run.stop();
73+
});
74+
75+
it(`should not show project name when formatter is non human readable`, async () => {
76+
const overrides = {
77+
format: 'checkstyle',
78+
};
79+
80+
// Check logs.
81+
const logger = new logging.Logger('lint-info');
82+
const allLogs: string[] = [];
83+
logger.subscribe(entry => allLogs.push(entry.message));
84+
85+
const run = await architect.scheduleTarget(lintTarget, overrides, { logger });
86+
87+
// Check status updates.
88+
const allStatus: string[] = [];
89+
run.progress.subscribe(progress => {
90+
if (progress.status !== undefined) {
91+
allStatus.push(progress.status);
92+
}
93+
});
94+
95+
const output = await run.result;
96+
expect(output.success).toBe(true);
97+
expect(allStatus).toContain(jasmine.stringMatching(/linting.*"app".*/i));
98+
expect(allLogs).not.toContain(jasmine.stringMatching(/linting.*"app".*/i));
99+
await run.stop();
100+
}, 30000);
101+
102+
// it('should report lint error once', (done) => {
103+
// host.writeMultipleFiles({'src/app/app.component.ts': 'const foo = "";\n' });
104+
// const logger = new TestLogger('lint-error');
105+
//
106+
// runTargetSpec(host, tslintTargetSpec, undefined, DefaultTimeout, logger).pipe(
107+
// tap((buildEvent) => expect(buildEvent.success).toBe(false)),
108+
// tap(() => {
109+
// // this is to make sure there are no duplicates
110+
// expect(logger.includes(`" should be \'\nERROR`)).toBe(false);
111+
//
112+
// expect(logger.includes(`" should be '`)).toBe(true);
113+
// expect(logger.includes(`Lint errors found in the listed files`)).toBe(true);
114+
// }),
115+
// ).toPromise().then(done, done.fail);
116+
// }, 30000);
117+
//
118+
// it('supports exclude with glob', (done) => {
119+
// host.writeMultipleFiles(filesWithErrors);
120+
// const overrides: Partial<TslintBuilderOptions> = { exclude: ['**/foo.ts'] };
121+
//
122+
// runTargetSpec(host, tslintTargetSpec, overrides).pipe(
123+
// tap((buildEvent) => expect(buildEvent.success).toBe(true)),
124+
// ).toPromise().then(done, done.fail);
125+
// }, 30000);
126+
//
127+
// it('supports exclude with relative paths', (done) => {
128+
// host.writeMultipleFiles(filesWithErrors);
129+
// const overrides: Partial<TslintBuilderOptions> = { exclude: ['src/foo.ts'] };
130+
//
131+
// runTargetSpec(host, tslintTargetSpec, overrides).pipe(
132+
// tap((buildEvent) => expect(buildEvent.success).toBe(true)),
133+
// ).toPromise().then(done, done.fail);
134+
// }, 30000);
135+
//
136+
// it(`supports exclude with paths starting with './'`, (done) => {
137+
// host.writeMultipleFiles(filesWithErrors);
138+
// const overrides: Partial<TslintBuilderOptions> = { exclude: ['./src/foo.ts'] };
139+
//
140+
// runTargetSpec(host, tslintTargetSpec, overrides).pipe(
141+
// tap((buildEvent) => expect(buildEvent.success).toBe(true)),
142+
// ).toPromise().then(done, done.fail);
143+
// }, 30000);
144+
//
145+
// it('supports fix', (done) => {
146+
// host.writeMultipleFiles(filesWithErrors);
147+
// const overrides: Partial<TslintBuilderOptions> = { fix: true };
148+
//
149+
// runTargetSpec(host, tslintTargetSpec, overrides).pipe(
150+
// tap((buildEvent) => expect(buildEvent.success).toBe(true)),
151+
// tap(() => {
152+
// const fileName = normalize('src/foo.ts');
153+
// const content = virtualFs.fileBufferToString(host.scopedSync().read(fileName));
154+
// expect(content).toContain(`const foo = '';`);
155+
// }),
156+
// ).toPromise().then(done, done.fail);
157+
// }, 30000);
158+
//
159+
// it('supports force', (done) => {
160+
// host.writeMultipleFiles(filesWithErrors);
161+
// const logger = new TestLogger('lint-force');
162+
// const overrides: Partial<TslintBuilderOptions> = { force: true };
163+
//
164+
// runTargetSpec(host, tslintTargetSpec, overrides, DefaultTimeout, logger).pipe(
165+
// tap((buildEvent) => expect(buildEvent.success).toBe(true)),
166+
// tap(() => {
167+
// expect(logger.includes(`" should be '`)).toBe(true);
168+
// expect(logger.includes(`Lint errors found in the listed files`)).toBe(true);
169+
// }),
170+
// ).toPromise().then(done, done.fail);
171+
// }, 30000);
172+
//
173+
// it('supports format', (done) => {
174+
// host.writeMultipleFiles(filesWithErrors);
175+
// const logger = new TestLogger('lint-format');
176+
// const overrides: Partial<TslintBuilderOptions> = { format: 'stylish' };
177+
//
178+
// runTargetSpec(host, tslintTargetSpec, overrides, DefaultTimeout, logger).pipe(
179+
// tap((buildEvent) => expect(buildEvent.success).toBe(false)),
180+
// tap(() => {
181+
// expect(logger.includes(`quotemark`)).toBe(true);
182+
// }),
183+
// ).toPromise().then(done, done.fail);
184+
// }, 30000);
185+
//
186+
// it('supports finding configs', (done) => {
187+
// host.writeMultipleFiles({
188+
// 'src/app/foo/foo.ts': `const foo = '';\n`,
189+
// 'src/app/foo/tslint.json': `
190+
// {
191+
// "rules": {
192+
// "quotemark": [
193+
// true,
194+
// "double"
195+
// ]
196+
// }
197+
// }
198+
// `,
199+
// });
200+
// const overrides: Partial<TslintBuilderOptions> = { tslintConfig: undefined };
201+
//
202+
// runTargetSpec(host, tslintTargetSpec, overrides).pipe(
203+
// tap((buildEvent) => expect(buildEvent.success).toBe(false)),
204+
// ).toPromise().then(done, done.fail);
205+
// }, 30000);
206+
//
207+
// it('supports overriding configs', (done) => {
208+
// host.writeMultipleFiles({
209+
// 'src/app/foo/foo.ts': `const foo = '';\n`,
210+
// 'src/app/foo/tslint.json': `
211+
// {
212+
// "rules": {
213+
// "quotemark": [
214+
// true,
215+
// "double"
216+
// ]
217+
// }
218+
// }
219+
// `,
220+
// });
221+
// const overrides: Partial<TslintBuilderOptions> = { tslintConfig: 'tslint.json' };
222+
//
223+
// runTargetSpec(host, tslintTargetSpec, overrides).pipe(
224+
// tap((buildEvent) => expect(buildEvent.success).toBe(true)),
225+
// ).toPromise().then(done, done.fail);
226+
// }, 30000);
227+
//
228+
// it('supports using files with no project', (done) => {
229+
// const overrides: Partial<TslintBuilderOptions> = {
230+
// tsConfig: undefined,
231+
// files: ['src/app/**/*.ts'],
232+
// };
233+
//
234+
// runTargetSpec(host, tslintTargetSpec, overrides).pipe(
235+
// tap((buildEvent) => expect(buildEvent.success).toBe(true)),
236+
// ).toPromise().then(done, done.fail);
237+
// }, 30000);
238+
//
239+
// it('supports using one project as a string', (done) => {
240+
// const overrides: Partial<TslintBuilderOptions> = {
241+
// tsConfig: 'src/tsconfig.app.json',
242+
// };
243+
//
244+
// runTargetSpec(host, tslintTargetSpec, overrides).pipe(
245+
// tap((buildEvent) => expect(buildEvent.success).toBe(true)),
246+
// ).toPromise().then(done, done.fail);
247+
// }, 30000);
248+
//
249+
// it('supports using one project as an array', (done) => {
250+
// const overrides: Partial<TslintBuilderOptions> = {
251+
// tsConfig: ['src/tsconfig.app.json'],
252+
// };
253+
//
254+
// runTargetSpec(host, tslintTargetSpec, overrides).pipe(
255+
// tap((buildEvent) => expect(buildEvent.success).toBe(true)),
256+
// ).toPromise().then(done, done.fail);
257+
// }, 30000);
258+
//
259+
// it('supports using two projects', (done) => {
260+
// const overrides: Partial<TslintBuilderOptions> = {
261+
// tsConfig: ['src/tsconfig.app.json', 'src/tsconfig.spec.json'],
262+
// };
263+
//
264+
// runTargetSpec(host, tslintTargetSpec, overrides).pipe(
265+
// tap((buildEvent) => expect(buildEvent.success).toBe(true)),
266+
// ).toPromise().then(done, done.fail);
267+
// }, 30000);
268+
//
269+
// it('errors when type checking is used without a project', (done) => {
270+
// const overrides: Partial<TslintBuilderOptions> = {
271+
// tsConfig: undefined,
272+
// typeCheck: true,
273+
// };
274+
//
275+
// runTargetSpec(host, tslintTargetSpec, overrides)
276+
// .subscribe(undefined, () => done(), done.fail);
277+
// }, 30000);
278+
});

0 commit comments

Comments
 (0)
Please sign in to comment.