UNPKG

9.69 kBPlain TextView Raw
1import { Options as PrettierOptions } from 'prettier';
2
3import * as fs from 'fs';
4import * as glob from 'glob';
5import * as optimist from 'optimist';
6import * as path from 'path';
7import * as tslint from 'tslint';
8import { promisify } from 'util';
9
10// TODO: Switch to using fs.promises when dropping node 8 support
11const realpathAsync = promisify(fs.realpath);
12const readFileAsync = promisify(fs.readFile);
13const accessAsync = promisify(fs.access);
14const statAsync = promisify(fs.stat);
15const writeFileAsync = promisify(fs.writeFile);
16
17const existsAsync = async (filename: string) => {
18 try {
19 await accessAsync(filename);
20 return true;
21 } catch {
22 return false;
23 }
24};
25
26const globAsync = promisify(glob);
27
28interface ResinLintConfig {
29 configPath: string;
30 configFileName: string;
31 extensions: string[];
32 lang: 'coffeescript' | 'typescript';
33 prettierCheck?: boolean;
34 testsCheck?: boolean;
35}
36
37const 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
58const prettierConfigPath = path.join(__dirname, '../config/.prettierrc');
59
60/**
61 * The linter expects the path to actual source files, for example:
62 * src/
63 * test/
64 * but depcheck expects the root of a project directory (where the
65 * package.json is). This function takes a path and propagates upwards
66 * until it contains a package.json
67 */
68const 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
76const read = async (filepath: string): Promise<string> => {
77 const realPath = await realpathAsync(filepath);
78 return readFileAsync(realPath, 'utf8');
79};
80
81const 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
94const 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
103const 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
123const 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
148const 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 // Print the linter results
196 console.log(linter.getResult().output);
197
198 return errorReport.errorCount === 0 ? 0 : 1;
199};
200
201const 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
212const 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
250export 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/*', // ignore typescript type declarations
284 'supervisor', // isn't used directly from source
285 'coffee-script', // Gives false positives
286 'coffeescript', // An alias
287 'colors', // Generally imported via colors/safe, which doesn't trigger depcheck
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 // optimist converts all --no-xyz args to a argv.xyz === false
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 // TSLint config needs to be loaded with `loadConfigurationFromPath`
322 // Coffeelint needs to be loaded as a plain file
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 // Extend/override default config
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};