From 58b456d48784291e32ba4263d3180cfece395b2f Mon Sep 17 00:00:00 2001 From: Alexey Shpakov Date: Thu, 14 Dec 2017 00:17:56 +1100 Subject: [PATCH] Support passing custom config, add mocha reporter, stay silent in case of continuous integration environment --- README.md | 3 ++- bin/cli.js | 2 +- fuse.js | 10 ++++++-- package.json | 5 +++- src/cli.ts | 28 ++++++++++++++++++---- src/config/index.ts | 4 ++-- src/config/read-config.ts | 3 ++- src/debug.ts | 5 ++++ src/index.ts | 30 ++---------------------- src/logger/console.test.ts | 19 --------------- src/logger/console.ts | 14 ++++------- src/logger/index.ts | 25 +++++--------------- src/logger/mocha.ts | 47 +++++++++++++++++++++++++++++++++++++ src/stricter.ts | 48 ++++++++++++++++++++++++++++++++++++++ src/types/index.ts | 12 +++++++++- types/missing.d.ts | 4 ++++ yarn.lock | 10 ++++++++ 17 files changed, 180 insertions(+), 89 deletions(-) create mode 100644 src/debug.ts create mode 100644 src/logger/mocha.ts create mode 100644 src/stricter.ts diff --git a/README.md b/README.md index 5a50c153..eaae32f0 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ yarn add stricter --dev ``` yarn stricter ``` +You can run `yarn stricter --help` for help. # Configuration Stricter uses `stricter.config.js` to read configuration. @@ -95,7 +96,7 @@ interface RuleDefinition { `onProject` should return an array of strings, describing violations, or an empty array if there is none. # Debugging -It helps to use `src/cli.ts` as an entry point for debugging. +It helps to use `src/debug.ts` as an entry point for debugging. A sample launch.json for VS Code might look like ```json { diff --git a/bin/cli.js b/bin/cli.js index c74b40eb..ec538869 100644 --- a/bin/cli.js +++ b/bin/cli.js @@ -1,5 +1,5 @@ #!/usr/bin/env node -var run = require('stricter').default; +var run = require('stricter').cli; var result = run(); process.exit(result); diff --git a/fuse.js b/fuse.js index e7538198..7c6458d7 100644 --- a/fuse.js +++ b/fuse.js @@ -1,7 +1,8 @@ const FuseBox = require('fuse-box').FuseBox; -const { TypeScriptHelpers } = require('fuse-box'); +const { ReplacePlugin , TypeScriptHelpers } = require('fuse-box'); const TypeHelper = require('fuse-box-typechecker').TypeHelper; const isProduction = process.env.NODE_ENV === 'production'; +const version = require('./package.json').version; const typeHelper = TypeHelper({ tsConfig: './tsconfig.json', @@ -16,7 +17,12 @@ const fuse = FuseBox.init({ name: 'stricter', entry: 'src/index.js', }, - plugins: [TypeScriptHelpers()], + plugins: [ + ReplacePlugin({ + 'process.env.STRICTER_VERSION': JSON.stringify(version), + }), + TypeScriptHelpers(), + ], homeDir: 'src', output: 'dist/$name.js', target: 'server', diff --git a/package.json b/package.json index 61c87c44..030c6497 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ }, "devDependencies": { "@types/chalk": "^2.2.0", + "@types/commander": "^2.12.2", "@types/jest": "^21.1.8", "@types/node": "^8.0.58", "cross-env": "^5.1.1", @@ -66,7 +67,9 @@ "@babel/traverse": "7.0.0-beta.34", "babylon": "^7.0.0-beta.34", "chalk": "^2.3.0", - "cosmiconfig": "^3.1.0" + "commander": "^2.12.2", + "cosmiconfig": "^3.1.0", + "is-ci": "^1.0.10" }, "engines": { "node": ">=8.0.0" diff --git a/src/cli.ts b/src/cli.ts index 6aa39c9e..1befffd3 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,5 +1,23 @@ -// This is not the actual CLI deploye with the app. -// The sole purpose of the file is to help debugging. -let run = require('.').default; -let result = run(); -process.exit(result); +import * as program from 'commander'; +import * as isCi from 'is-ci'; +import stricter from './stricter'; + +export default (): number => { + program + .version(process.env.STRICTER_VERSION as string) + .option('-c, --config ', 'specify config location') + .option( + '-r, --reporter ', + 'specify reporter', + /^(console|mocha)$/i, + 'console', + ) + .parse(process.argv); + const result = stricter({ + configPath: program.config, + reporter: program.reporter, + silent: isCi, + }); + + return result; +}; diff --git a/src/config/index.ts b/src/config/index.ts index 9f8aa2d5..0125379c 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -3,8 +3,8 @@ import readConfig from './read-config'; import processConfig from './process-config'; import validateConfig from './validate-config'; -export const getConfig = (): Config => { - const foundConfig = readConfig(); +export const getConfig = (configPath?: string): Config => { + const foundConfig = readConfig(configPath); validateConfig(foundConfig); const processedConfig = processConfig(foundConfig); diff --git a/src/config/read-config.ts b/src/config/read-config.ts index c5f6116a..14e2a203 100644 --- a/src/config/read-config.ts +++ b/src/config/read-config.ts @@ -3,8 +3,9 @@ import { CosmiConfig } from './../types'; const moduleName = 'stricter'; -export default (): CosmiConfig => { +export default (configPath?: string): CosmiConfig => { const explorer = cosmiconfig(moduleName, { + configPath, sync: true, packageProp: false, rc: false, diff --git a/src/debug.ts b/src/debug.ts new file mode 100644 index 00000000..bd4cce6d --- /dev/null +++ b/src/debug.ts @@ -0,0 +1,5 @@ +// This is not the actual CLI deploye with the app. +// The sole purpose of the file is to help debugging. +let run = require('.').cli; +let result = run(); +process.exit(result); diff --git a/src/index.ts b/src/index.ts index 7f8d869a..6018fdf6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,28 +1,2 @@ -import { getConfig } from './config'; -import { getRuleDefinitions, getRuleApplications, filterFilesToProcess } from './rule'; -import { applyProjectRules, readDependencies, readFilesData } from './processor'; -import { consoleLogger } from './logger'; -import { listFiles } from './utils'; - -export default (): number => { - console.log('Stricter: Checking...'); - const config = getConfig(); - - const fileList = listFiles(config.root); - - const ruleDefinitions = getRuleDefinitions(config); - const ruleApplications = getRuleApplications(config, ruleDefinitions); - const filesToProcess = filterFilesToProcess(config.root, fileList, ruleApplications); - - const filesData = readFilesData(filesToProcess); - const dependencies = readDependencies(filesData, config); - const projectResult = applyProjectRules(config.root, filesData, dependencies, ruleApplications); - - const result = consoleLogger(projectResult); - - if (result === 0) { - console.log('Stricter: No errors'); - } - - return result; -}; +export { default as stricter } from './stricter'; +export { default as cli } from './cli'; diff --git a/src/logger/console.test.ts b/src/logger/console.test.ts index ad9c86c8..cddbf185 100644 --- a/src/logger/console.test.ts +++ b/src/logger/console.test.ts @@ -13,7 +13,6 @@ describe('consoleLogger', () => { it('runs warn for every warning', () => { const warn = { - filePath: 'filePath', rule: 'rule', warnings: ['warning1', 'warning2'], }; @@ -24,7 +23,6 @@ describe('consoleLogger', () => { it('runs error for every error', () => { const error = { - filePath: 'filePath', rule: 'rule', errors: ['error1', 'error2'], }; @@ -33,25 +31,8 @@ describe('consoleLogger', () => { expect(errorMock.mock.calls.length).toBe(4); }); - it('log different file names', () => { - const error1 = { - filePath: 'filePath1', - rule: 'rule', - errors: ['error1'], - }; - const error2 = { - filePath: 'filePath2', - rule: 'rule', - errors: ['error2'], - }; - logConsole([error1, error2]); - - expect(logMock.mock.calls.length).toBe(2); - }); - it("doesn't log same file name twice", () => { const error = { - filePath: 'filePath', rule: 'rule', errors: ['error1', 'error2'], }; diff --git a/src/logger/console.ts b/src/logger/console.ts index 32b0d71e..9afdecc2 100644 --- a/src/logger/console.ts +++ b/src/logger/console.ts @@ -2,17 +2,13 @@ import chalk from 'chalk'; import { LogEntry } from './../types'; export default (logs: LogEntry[]): void => { - let previousFilePath: string | undefined; + if (!logs.length) { + return; + } - logs.forEach(log => { - if (previousFilePath !== log.filePath) { - previousFilePath = log.filePath; - - if (log.filePath) { - console.log(chalk.white(log.filePath)); - } - } + console.log(chalk.bgBlackBright('Project')); + logs.forEach(log => { if (log.warnings) { log.warnings.forEach(warning => { console.warn(chalk.yellow('warning: ') + chalk.gray(log.rule) + ' ' + warning); diff --git a/src/logger/index.ts b/src/logger/index.ts index 14f34674..4bc00e28 100644 --- a/src/logger/index.ts +++ b/src/logger/index.ts @@ -1,20 +1,7 @@ -import chalk from 'chalk'; -import { RuleToRuleApplicationResult } from './../types'; -import logToConsole from './console'; -import { compactProjectLogs } from './flatten'; +import { LogEntry } from './../types'; -export const consoleLogger = (projectResult: RuleToRuleApplicationResult): number => { - const projectLogs = compactProjectLogs(projectResult); - - if (projectLogs.length) { - console.log(chalk.bgBlackBright('Project')); - logToConsole(projectLogs); - } - - const errorCount = Object.values(projectLogs).reduce( - (acc, i) => acc + ((i.errors && i.errors.length) || 0), - 0, - ); - - return errorCount; -}; +export { default as consoleLogger } from './console'; +export { default as mochaLogger } from './mocha'; +export { compactProjectLogs } from './flatten'; +export const getErrorCount = (projectLogs: LogEntry[]) => + Object.values(projectLogs).reduce((acc, i) => acc + ((i.errors && i.errors.length) || 0), 0); diff --git a/src/logger/mocha.ts b/src/logger/mocha.ts new file mode 100644 index 00000000..85e0fd4d --- /dev/null +++ b/src/logger/mocha.ts @@ -0,0 +1,47 @@ +import * as fs from 'fs'; +import { LogEntry } from './../types'; + +const reportFileName = 'stricter.json'; + +const encode = (str: string) => { + const substitutions = { + '&:': '&', + '"': '"', + "'": ''', + '<': '<', + '>': '>', + }; + + const result = Object.entries(substitutions).reduce((acc, [original, substitution]) => { + return acc.replace(new RegExp(original, 'g'), substitution); + }, str); + + return result; +}; + +export default (logs: LogEntry[]): void => { + const now = new Date(); + const failuresCount = logs.reduce((acc, i) => acc + ((i.errors && i.errors.length) || 0), 0); + + const report = { + stats: { + tests: failuresCount, + passes: 0, + failures: failuresCount, + duration: 0, + start: now, + end: now, + }, + failures: logs.map(log => ({ + title: log.rule, + fullTitle: log.rule, + duration: 0, + errorCount: (log.errors && log.errors.length) || 0, + error: log.errors && log.errors.map(i => encode(i)).join('\n'), + })), + passes: [], + skipped: [], + }; + + fs.writeFileSync(reportFileName, JSON.stringify(report, null, 2), 'utf-8'); +}; diff --git a/src/stricter.ts b/src/stricter.ts new file mode 100644 index 00000000..a97ee14a --- /dev/null +++ b/src/stricter.ts @@ -0,0 +1,48 @@ +import { getConfig } from './config'; +import { getRuleDefinitions, getRuleApplications, filterFilesToProcess } from './rule'; +import { applyProjectRules, readDependencies, readFilesData } from './processor'; +import { consoleLogger, mochaLogger, compactProjectLogs, getErrorCount } from './logger'; +import { listFiles } from './utils'; +import { StricterArguments, Reporter } from './types'; + +export default ({ + silent = false, + reporter = Reporter.CONSOLE, + configPath, +}: StricterArguments): number => { + if (!silent) { + console.log('Stricter: Checking...'); + } + + const config = getConfig(configPath); + + const fileList = listFiles(config.root); + + const ruleDefinitions = getRuleDefinitions(config); + const ruleApplications = getRuleApplications(config, ruleDefinitions); + const filesToProcess = filterFilesToProcess(config.root, fileList, ruleApplications); + + const filesData = readFilesData(filesToProcess); + const dependencies = readDependencies(filesData, config); + const projectResult = applyProjectRules(config.root, filesData, dependencies, ruleApplications); + + const logs = compactProjectLogs(projectResult); + + if (reporter === Reporter.MOCHA) { + mochaLogger(logs); + } else { + consoleLogger(logs); + } + + const result = getErrorCount(logs); + + if (!silent) { + if (result === 0) { + console.log('Stricter: No errors'); + } else { + console.log(`Stricter: ${result} error${result > 1 ? 's' : ''}`); + } + } + + return result; +}; diff --git a/src/types/index.ts b/src/types/index.ts index 7364e96d..0306359b 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -11,6 +11,11 @@ export enum Level { OFF = 'off', } +export enum Reporter { + CONSOLE = 'console', + MOCHA = 'mocha', +} + export interface RuleUsageConfig { [prop: string]: any; } @@ -81,8 +86,13 @@ export interface FileToRule { } export interface LogEntry { - filePath?: string; rule: string; errors?: string[]; warnings?: string[]; } + +export interface StricterArguments { + silent?: boolean; + configPath?: string; + reporter?: Reporter; +} diff --git a/types/missing.d.ts b/types/missing.d.ts index 50ae4666..f3a95d52 100644 --- a/types/missing.d.ts +++ b/types/missing.d.ts @@ -1,3 +1,7 @@ declare module 'cosmiconfig'; // does not exist declare module 'babylon'; // existing libdef misses plugins we use declare module '@babel/traverse'; // existing libdef misses plugins we use +declare module 'is-ci' { + var isCi: boolean; + export = isCi; +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 8ab486ba..a39f1b3f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -64,6 +64,12 @@ dependencies: chalk "*" +"@types/commander@^2.12.2": + version "2.12.2" + resolved "https://registry.yarnpkg.com/@types/commander/-/commander-2.12.2.tgz#183041a23842d4281478fa5d23c5ca78e6fd08ae" + dependencies: + commander "*" + "@types/diff@^3.2.1": version "3.2.2" resolved "https://registry.yarnpkg.com/@types/diff/-/diff-3.2.2.tgz#4d6f45537322a7a420d353a0939513c7e96d14a6" @@ -765,6 +771,10 @@ combined-stream@^1.0.5, combined-stream@~1.0.5: dependencies: delayed-stream "~1.0.0" +commander@*, commander@^2.12.2: + version "2.12.2" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.12.2.tgz#0f5946c427ed9ec0d91a46bb9def53e54650e555" + commander@^2.9.0: version "2.11.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563"