1 | import { Options as PrettierOptions } from 'prettier';
|
2 |
|
3 | import * as fs from 'fs';
|
4 | import * as glob from 'glob';
|
5 | import * as optimist from 'optimist';
|
6 | import * as path from 'path';
|
7 | import * as tslint from 'tslint';
|
8 | import { promisify } from 'util';
|
9 |
|
10 |
|
11 | const realpathAsync = promisify(fs.realpath);
|
12 | const readFileAsync = promisify(fs.readFile);
|
13 | const accessAsync = promisify(fs.access);
|
14 | const statAsync = promisify(fs.stat);
|
15 | const writeFileAsync = promisify(fs.writeFile);
|
16 |
|
17 | const existsAsync = async (filename: string) => {
|
18 | try {
|
19 | await accessAsync(filename);
|
20 | return true;
|
21 | } catch {
|
22 | return false;
|
23 | }
|
24 | };
|
25 |
|
26 | const globAsync = promisify(glob);
|
27 |
|
28 | interface ResinLintConfig {
|
29 | configPath: string;
|
30 | configFileName: string;
|
31 | extensions: string[];
|
32 | lang: 'coffeescript' | 'typescript';
|
33 | prettierCheck?: boolean;
|
34 | testsCheck?: boolean;
|
35 | }
|
36 |
|
37 | const configurations: { [key: string]: ResinLintConfig } = {
|
38 | coffeescript: {
|
39 | configPath: path.join(__dirname, '../config/coffeelint.json'),
|
40 | configFileName: 'coffeelint.json',
|
41 | extensions: ['coffee'],
|
42 | lang: 'coffeescript',
|
43 | },
|
44 | typescript: {
|
45 | configPath: path.join(__dirname, '../config/tslint.json'),
|
46 | configFileName: 'tslint.json',
|
47 | extensions: ['ts', 'tsx'],
|
48 | lang: 'typescript',
|
49 | },
|
50 | typescriptPrettier: {
|
51 | configPath: path.join(__dirname, '../config/tslint-prettier.json'),
|
52 | configFileName: 'tslint.json',
|
53 | extensions: ['ts', 'tsx'],
|
54 | lang: 'typescript',
|
55 | },
|
56 | };
|
57 |
|
58 | const prettierConfigPath = path.join(__dirname, '../config/.prettierrc');
|
59 |
|
60 |
|
61 |
|
62 |
|
63 |
|
64 |
|
65 |
|
66 |
|
67 |
|
68 | const getPackageJsonDir = async (dir: string): Promise<string> => {
|
69 | const name = await findFile('package.json', dir);
|
70 | if (name === null) {
|
71 | throw new Error('Could not find package.json!');
|
72 | }
|
73 | return path.dirname(name);
|
74 | };
|
75 |
|
76 | const read = async (filepath: string): Promise<string> => {
|
77 | const realPath = await realpathAsync(filepath);
|
78 | return readFileAsync(realPath, 'utf8');
|
79 | };
|
80 |
|
81 | const findFile = async (name: string, dir?: string): Promise<string | null> => {
|
82 | dir = dir || process.cwd();
|
83 | const filename = path.join(dir, name);
|
84 | if (await existsAsync(filename)) {
|
85 | return filename;
|
86 | }
|
87 | const parent = path.dirname(dir);
|
88 | if (dir === parent) {
|
89 | return null;
|
90 | }
|
91 | return findFile(name, parent);
|
92 | };
|
93 |
|
94 | const parseJSON = async (file: string): Promise<{}> => {
|
95 | try {
|
96 | return JSON.parse(await readFileAsync(file, 'utf8'));
|
97 | } catch (err) {
|
98 | console.error(`Could not parse ${file}`);
|
99 | throw err;
|
100 | }
|
101 | };
|
102 |
|
103 | const findFiles = async (
|
104 | extensions: string[],
|
105 | paths: string[] = [],
|
106 | ): Promise<string[]> => {
|
107 | const files: string[] = [];
|
108 | await Promise.all(
|
109 | paths.map(async p => {
|
110 | if ((await statAsync(p)).isDirectory()) {
|
111 | files.push(
|
112 | ...(await globAsync(`${p}/**/*.@(${extensions.join('|')})`)),
|
113 | );
|
114 | } else {
|
115 | files.push(p);
|
116 | }
|
117 | }),
|
118 | );
|
119 |
|
120 | return files.map(p => path.join(p));
|
121 | };
|
122 |
|
123 | const lintCoffeeFiles = async (
|
124 | files: string[],
|
125 | config: {},
|
126 | ): Promise<number> => {
|
127 | const coffeelint: any = require('coffeelint');
|
128 | const errorReport = new coffeelint.getErrorReport();
|
129 |
|
130 | await Promise.all(
|
131 | files.map(async file => {
|
132 | const source = await read(file);
|
133 | errorReport.lint(file, source, config);
|
134 | }),
|
135 | );
|
136 |
|
137 | const reporter: any = require('coffeelint/lib/reporters/default');
|
138 | const report = new reporter(errorReport, {
|
139 | colorize: process.stdout.isTTY,
|
140 | quiet: false,
|
141 | });
|
142 |
|
143 | report.publish();
|
144 |
|
145 | return errorReport.getExitCode();
|
146 | };
|
147 |
|
148 | const lintTsFiles = async function(
|
149 | files: string[],
|
150 | config: {},
|
151 | prettierConfig: PrettierOptions | undefined,
|
152 | autoFix: boolean,
|
153 | ): Promise<number> {
|
154 | const prettier = prettierConfig ? await import('prettier') : undefined;
|
155 | const linter = new tslint.Linter({
|
156 | fix: autoFix,
|
157 | formatter: 'stylish',
|
158 | });
|
159 |
|
160 | const exitCodes = await Promise.all(
|
161 | files.map(async file => {
|
162 | let source = await read(file);
|
163 | linter.lint(
|
164 | file,
|
165 | source,
|
166 | config as tslint.Configuration.IConfigurationFile,
|
167 | );
|
168 | if (prettier) {
|
169 | if (autoFix) {
|
170 | const newSource = prettier.format(source, prettierConfig);
|
171 | if (source !== newSource) {
|
172 | source = newSource;
|
173 | await writeFileAsync(file, source);
|
174 | }
|
175 | } else {
|
176 | const isPrettified = prettier.check(source, prettierConfig);
|
177 | if (!isPrettified) {
|
178 | console.log(
|
179 | `Error: File ${file} hasn't been formatted with prettier`,
|
180 | );
|
181 | return 1;
|
182 | }
|
183 | }
|
184 | }
|
185 | return 0;
|
186 | }),
|
187 | );
|
188 | const failureCode = exitCodes.find(exitCode => exitCode !== 0);
|
189 | if (failureCode) {
|
190 | return failureCode;
|
191 | }
|
192 |
|
193 | const errorReport = linter.getResult();
|
194 |
|
195 |
|
196 | console.log(linter.getResult().output);
|
197 |
|
198 | return errorReport.errorCount === 0 ? 0 : 1;
|
199 | };
|
200 |
|
201 | const lintMochaTestFiles = async function(files: string[]): Promise<number> {
|
202 | const { lintMochaTests } = await import('./mocha-tests-lint');
|
203 | const res = await lintMochaTests(files);
|
204 | if (res.isError) {
|
205 | console.error('Mocha tests check FAILED!');
|
206 | console.error(res.message);
|
207 | return 1;
|
208 | }
|
209 | return 0;
|
210 | };
|
211 |
|
212 | const runLint = async function(
|
213 | resinLintConfig: ResinLintConfig,
|
214 | paths: string[],
|
215 | config: {},
|
216 | autoFix: boolean,
|
217 | ) {
|
218 | let linterExitCode: number | undefined;
|
219 | const scripts = await findFiles(resinLintConfig.extensions, paths);
|
220 |
|
221 | if (resinLintConfig.lang === 'typescript') {
|
222 | let prettierConfig: PrettierOptions | undefined;
|
223 | if (resinLintConfig.prettierCheck) {
|
224 | prettierConfig = (await parseJSON(prettierConfigPath)) as PrettierOptions;
|
225 | prettierConfig.parser = 'typescript';
|
226 | }
|
227 |
|
228 | linterExitCode = await lintTsFiles(
|
229 | scripts,
|
230 | config,
|
231 | prettierConfig,
|
232 | autoFix,
|
233 | );
|
234 | }
|
235 |
|
236 | if (resinLintConfig.lang === 'coffeescript') {
|
237 | linterExitCode = await lintCoffeeFiles(scripts, config);
|
238 | }
|
239 |
|
240 | if (resinLintConfig.testsCheck) {
|
241 | const testsExitCode = await lintMochaTestFiles(scripts);
|
242 | if (linterExitCode === 0) {
|
243 | linterExitCode = testsExitCode;
|
244 | }
|
245 | }
|
246 |
|
247 | process.on('exit', () => process.exit(linterExitCode));
|
248 | };
|
249 |
|
250 | export const lint = async (passedParams: any) => {
|
251 | const options = optimist(passedParams)
|
252 | .usage('Usage: resin-lint [options] [...]')
|
253 | .describe(
|
254 | 'f',
|
255 | 'Specify a linting config file to extend and override resin-lint rules',
|
256 | )
|
257 | .describe('p', 'Print default resin-lint linting rules')
|
258 | .describe(
|
259 | 'i',
|
260 | 'Ignore linting config files in project directory and its parents',
|
261 | )
|
262 | .boolean('typescript', 'Lint typescript files instead of coffeescript')
|
263 | .boolean('fix', 'Attempt to automatically fix lint errors')
|
264 | .boolean('no-prettier', 'Disables the prettier code format checks')
|
265 | .boolean(
|
266 | 'tests',
|
267 | 'Treat input files as test sources to perform extra relevant checks',
|
268 | )
|
269 | .boolean('u', 'Run unused import check');
|
270 |
|
271 | if (options.argv._.length < 1 && !options.argv.p) {
|
272 | options.showHelp();
|
273 | process.exit(1);
|
274 | }
|
275 |
|
276 | if (options.argv.u) {
|
277 | const depcheck = await import('depcheck');
|
278 | await Promise.all(
|
279 | options.argv._.map(async (dir: string) => {
|
280 | dir = await getPackageJsonDir(dir);
|
281 | const { dependencies } = await depcheck(path.resolve('./', dir), {
|
282 | ignoreMatches: [
|
283 | '@types/*',
|
284 | 'supervisor',
|
285 | 'coffee-script',
|
286 | 'coffeescript',
|
287 | 'colors',
|
288 | 'coffeescope2',
|
289 | ],
|
290 | });
|
291 | if (dependencies.length > 0) {
|
292 | console.log(`${dependencies.length} unused dependencies:`);
|
293 | for (const dep of dependencies) {
|
294 | console.log(`\t${dep}`);
|
295 | }
|
296 | process.exit(1);
|
297 | }
|
298 | console.log('No unused dependencies!');
|
299 | console.log();
|
300 | }),
|
301 | );
|
302 | }
|
303 |
|
304 | let configOverridePath;
|
305 |
|
306 | const prettierCheck = options.argv.prettier !== false;
|
307 | const testsCheck = options.argv.tests === true;
|
308 | const typescriptCheck = options.argv.typescript;
|
309 | const autoFix = options.argv.fix === true;
|
310 | const resinLintConfiguration = typescriptCheck
|
311 | ? prettierCheck
|
312 | ? configurations.typescriptPrettier
|
313 | : configurations.typescript
|
314 | : configurations.coffeescript;
|
315 |
|
316 | if (options.argv.p) {
|
317 | console.log(await readFileAsync(resinLintConfiguration.configPath, 'utf8'));
|
318 | process.exit(0);
|
319 | }
|
320 |
|
321 |
|
322 |
|
323 | let config: {} = typescriptCheck
|
324 | ? tslint.Configuration.loadConfigurationFromPath(
|
325 | resinLintConfiguration.configPath,
|
326 | )
|
327 | : await parseJSON(resinLintConfiguration.configPath);
|
328 |
|
329 | if (options.argv.f) {
|
330 | configOverridePath = await realpathAsync(options.argv.f);
|
331 | }
|
332 |
|
333 | if (!options.argv.i && !configOverridePath) {
|
334 | configOverridePath = await findFile(resinLintConfiguration.configFileName);
|
335 | }
|
336 |
|
337 | if (configOverridePath) {
|
338 |
|
339 | if (typescriptCheck) {
|
340 | const configOverride = tslint.Configuration.loadConfigurationFromPath(
|
341 | configOverridePath,
|
342 | );
|
343 | config = tslint.Configuration.extendConfigurationFile(
|
344 | config as tslint.Configuration.IConfigurationFile,
|
345 | configOverride,
|
346 | );
|
347 | } else {
|
348 | const configOverride = await parseJSON(configOverridePath);
|
349 | const { merge } = await import('lodash');
|
350 | config = merge(config, configOverride);
|
351 | }
|
352 | }
|
353 |
|
354 | const paths = options.argv._;
|
355 |
|
356 | resinLintConfiguration.prettierCheck = prettierCheck;
|
357 | resinLintConfiguration.testsCheck = testsCheck;
|
358 | await runLint(resinLintConfiguration, paths, config, autoFix);
|
359 | };
|