bpmnlint.js 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399
  1. #!/usr/bin/env node
  2. const mri = require('mri');
  3. const fs = require('fs');
  4. const path = require('path');
  5. const colors = require('ansi-colors');
  6. colors.enabled = require('color-support').hasBasic;
  7. const {
  8. red,
  9. yellow,
  10. underline,
  11. bold,
  12. magenta
  13. } = colors;
  14. const { promisify } = require('util');
  15. const tinyGlob = require('tiny-glob');
  16. const readFile = promisify(fs.readFile);
  17. const BpmnModdle = require('bpmn-moddle');
  18. const Linter = require('../lib/linter');
  19. const NodeResolver = require('../lib/resolver/node-resolver');
  20. const Table = require('cli-table');
  21. const pluralize = require('pluralize');
  22. const { pathStringify } = require('@philippfromme/moddle-helpers');
  23. const CONFIG_NAME = '.bpmnlintrc';
  24. const DEFAULT_CONFIG_CONTENTS = `{
  25. "extends": "bpmnlint:recommended"
  26. }`;
  27. const HELP_STRING = `
  28. Usage
  29. $ bpmnlint diagram.bpmn
  30. Options
  31. --config, -c Path to configuration file. It overrides .bpmnlintrc if present.
  32. --init Generate a .bpmnlintrc file in the current working directory
  33. Examples
  34. $ bpmnlint ./invoice.bpmn
  35. $ bpmnlint --init
  36. `;
  37. const moddle = new BpmnModdle();
  38. function boldRed(str) {
  39. return bold(red(str));
  40. }
  41. function boldYellow(str) {
  42. return bold(yellow(str));
  43. }
  44. function glob(files) {
  45. return Promise.all(
  46. files.map(
  47. file => tinyGlob(file, { dot: true })
  48. )
  49. ).then(files => [].concat(...files));
  50. }
  51. /**
  52. * Reads XML form path and return moddle object
  53. *
  54. * @param {string} diagramXML
  55. *
  56. * @return { { rootElement: any; warnings: Error[], error?: Error } } parseResult
  57. */
  58. async function parseDiagram(diagramXML) {
  59. try {
  60. const {
  61. rootElement: moddleElement,
  62. warnings = []
  63. } = await moddle.fromXML(diagramXML);
  64. return {
  65. moddleElement,
  66. warnings
  67. };
  68. } catch (error) {
  69. const {
  70. warnings = []
  71. } = error;
  72. return {
  73. error,
  74. warnings
  75. };
  76. }
  77. }
  78. const categoryMap = {
  79. warn: 'warning'
  80. };
  81. /**
  82. * Logs a formatted message
  83. */
  84. function tableEntry(report) {
  85. let {
  86. category,
  87. id = '',
  88. message,
  89. name = '',
  90. path
  91. } = report;
  92. if (path) {
  93. id = `${ id }#${ pathStringify(path) }`;
  94. }
  95. const color = category === 'error' ? red : yellow;
  96. return [ id, color(categoryMap[ category ] || category), message, name ];
  97. }
  98. function createTable() {
  99. return new Table({
  100. chars: {
  101. 'top': '',
  102. 'top-mid': '',
  103. 'top-left': '',
  104. 'top-right': '',
  105. 'bottom': '',
  106. 'bottom-mid': '',
  107. 'bottom-left': '',
  108. 'bottom-right': '',
  109. 'left': ' ',
  110. 'left-mid': '',
  111. 'mid': '',
  112. 'mid-mid': '',
  113. 'right': '',
  114. 'right-mid': '',
  115. 'middle': ' '
  116. },
  117. style: {
  118. 'padding-left': 0,
  119. 'padding-right': 0
  120. }
  121. });
  122. }
  123. function errorAndExit(...args) {
  124. console.error(...args);
  125. process.exit(1);
  126. }
  127. function showVersionAndExit() {
  128. console.log(require('../package.json').version);
  129. process.exit(0);
  130. }
  131. function infoAndExit(...args) {
  132. console.log(...args);
  133. process.exit(0);
  134. }
  135. /**
  136. * Prints lint results to the console
  137. *
  138. * @param {String} filePath
  139. * @param {Object} results
  140. */
  141. function printReports(filePath, results) {
  142. let errorCount = 0;
  143. let warningCount = 0;
  144. const table = createTable();
  145. Object.entries(results).forEach(function([ name, reports ]) {
  146. reports.forEach(function(report) {
  147. const {
  148. category,
  149. id,
  150. message,
  151. name: reportName
  152. } = report;
  153. table.push(tableEntry({
  154. category,
  155. id,
  156. message,
  157. name: reportName || name
  158. }));
  159. if (category === 'error') {
  160. errorCount++;
  161. } else {
  162. warningCount++;
  163. }
  164. });
  165. });
  166. const problemCount = warningCount + errorCount;
  167. if (problemCount) {
  168. console.log();
  169. console.log(underline(path.resolve(filePath)));
  170. console.log(table.toString());
  171. }
  172. return {
  173. errorCount,
  174. warningCount
  175. };
  176. }
  177. async function lintDiagram(diagramPath, config) {
  178. let diagramXML;
  179. try {
  180. diagramXML = await readFile(path.resolve(diagramPath), 'utf-8');
  181. } catch (error) {
  182. return errorAndExit(`Error: Failed to read ${diagramPath}\n\n%s`, error.message);
  183. }
  184. const {
  185. error: importError,
  186. warnings: importWarnings,
  187. moddleElement
  188. } = await parseDiagram(diagramXML);
  189. if (importError) {
  190. return printReports(diagramPath, {
  191. '': [
  192. {
  193. message: 'Parse error: ' + importError.message,
  194. category: 'error'
  195. }
  196. ]
  197. });
  198. }
  199. const importReports = importWarnings.length ? {
  200. '': importWarnings.map(function(warning) {
  201. const {
  202. element,
  203. message
  204. } = warning;
  205. const id = element && element.id;
  206. return {
  207. id,
  208. message: 'Import warning: ' + message.split(/\n/)[0],
  209. category: 'error'
  210. };
  211. })
  212. } : {};
  213. try {
  214. const linter = new Linter({
  215. config,
  216. resolver: new NodeResolver()
  217. });
  218. const lintReports = await linter.lint(moddleElement);
  219. const allResults = {
  220. ...importReports,
  221. ...lintReports
  222. };
  223. return printReports(diagramPath, allResults);
  224. } catch (e) {
  225. return errorAndExit(e);
  226. }
  227. }
  228. async function lint(files, config) {
  229. let errorCount = 0;
  230. let warningCount = 0;
  231. console.log();
  232. for (let i = 0; i < files.length; i++) {
  233. let results = await lintDiagram(files[i], config);
  234. errorCount += results.errorCount;
  235. warningCount += results.warningCount;
  236. }
  237. const problemCount = errorCount + warningCount;
  238. let color;
  239. if (warningCount) {
  240. color = boldYellow;
  241. }
  242. if (errorCount) {
  243. color = boldRed;
  244. }
  245. if (problemCount) {
  246. console.log();
  247. console.log(color(
  248. `✖ ${problemCount} ${pluralize('problem', problemCount)} (${errorCount} ${pluralize('error', errorCount)}, ${warningCount} ${pluralize('warning', warningCount)})`
  249. ));
  250. }
  251. if (errorCount) {
  252. process.exit(1);
  253. }
  254. }
  255. async function run() {
  256. const {
  257. help,
  258. init,
  259. version,
  260. config: configOverridePath,
  261. _: files
  262. } = mri(process.argv.slice(2), {
  263. string: [ 'config' ],
  264. alias: {
  265. c: 'config'
  266. }
  267. });
  268. if (version) {
  269. return showVersionAndExit();
  270. }
  271. if (help) {
  272. return infoAndExit(HELP_STRING);
  273. }
  274. if (init) {
  275. if (fs.existsSync(CONFIG_NAME)) {
  276. return errorAndExit('Not overriding existing .bpmnlintrc');
  277. }
  278. fs.writeFileSync(CONFIG_NAME, DEFAULT_CONFIG_CONTENTS, 'utf8');
  279. return infoAndExit(`Created ${magenta(CONFIG_NAME)} file`);
  280. }
  281. if (files.length === 0) {
  282. return errorAndExit('Error: bpmn file path missing');
  283. }
  284. const configPath = configOverridePath || CONFIG_NAME;
  285. let configString, config;
  286. try {
  287. configString = await readFile(configPath, 'utf-8');
  288. } catch (error) {
  289. const message = (
  290. configOverridePath
  291. ? `Error: Could not read ${ magenta(configOverridePath) }`
  292. : `Error: Could not locate local ${ magenta(CONFIG_NAME) } file. Create one via
  293. ${magenta('bpmnlint --init')}
  294. Learn more about configuring bpmnlint: https://github.com/bpmn-io/bpmnlint#configuration`
  295. );
  296. return errorAndExit(message);
  297. }
  298. try {
  299. config = JSON.parse(configString);
  300. } catch (err) {
  301. return errorAndExit('Error: Could not parse %s\n\n%s', configPath, err.message);
  302. }
  303. const actualFiles = await glob(files);
  304. return lint(actualFiles, config);
  305. }
  306. run().catch(errorAndExit);