Skip to content

Commit df1b56c

Browse files
hanslKeen Yee Liau
authored and
Keen Yee Liau
committed
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

+1
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."
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+
}

0 commit comments

Comments
 (0)