123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399 |
- #!/usr/bin/env node
- const mri = require('mri');
- const fs = require('fs');
- const path = require('path');
- const colors = require('ansi-colors');
- colors.enabled = require('color-support').hasBasic;
- const {
- red,
- yellow,
- underline,
- bold,
- magenta
- } = colors;
- const { promisify } = require('util');
- const tinyGlob = require('tiny-glob');
- const readFile = promisify(fs.readFile);
- const BpmnModdle = require('bpmn-moddle');
- const Linter = require('../lib/linter');
- const NodeResolver = require('../lib/resolver/node-resolver');
- const Table = require('cli-table');
- const pluralize = require('pluralize');
- const { pathStringify } = require('@philippfromme/moddle-helpers');
- const CONFIG_NAME = '.bpmnlintrc';
- const DEFAULT_CONFIG_CONTENTS = `{
- "extends": "bpmnlint:recommended"
- }`;
- const HELP_STRING = `
- Usage
- $ bpmnlint diagram.bpmn
- Options
- --config, -c Path to configuration file. It overrides .bpmnlintrc if present.
- --init Generate a .bpmnlintrc file in the current working directory
- Examples
- $ bpmnlint ./invoice.bpmn
- $ bpmnlint --init
- `;
- const moddle = new BpmnModdle();
- function boldRed(str) {
- return bold(red(str));
- }
- function boldYellow(str) {
- return bold(yellow(str));
- }
- function glob(files) {
- return Promise.all(
- files.map(
- file => tinyGlob(file, { dot: true })
- )
- ).then(files => [].concat(...files));
- }
- /**
- * Reads XML form path and return moddle object
- *
- * @param {string} diagramXML
- *
- * @return { { rootElement: any; warnings: Error[], error?: Error } } parseResult
- */
- async function parseDiagram(diagramXML) {
- try {
- const {
- rootElement: moddleElement,
- warnings = []
- } = await moddle.fromXML(diagramXML);
- return {
- moddleElement,
- warnings
- };
- } catch (error) {
- const {
- warnings = []
- } = error;
- return {
- error,
- warnings
- };
- }
- }
- const categoryMap = {
- warn: 'warning'
- };
- /**
- * Logs a formatted message
- */
- function tableEntry(report) {
- let {
- category,
- id = '',
- message,
- name = '',
- path
- } = report;
- if (path) {
- id = `${ id }#${ pathStringify(path) }`;
- }
- const color = category === 'error' ? red : yellow;
- return [ id, color(categoryMap[ category ] || category), message, name ];
- }
- function createTable() {
- return new Table({
- chars: {
- 'top': '',
- 'top-mid': '',
- 'top-left': '',
- 'top-right': '',
- 'bottom': '',
- 'bottom-mid': '',
- 'bottom-left': '',
- 'bottom-right': '',
- 'left': ' ',
- 'left-mid': '',
- 'mid': '',
- 'mid-mid': '',
- 'right': '',
- 'right-mid': '',
- 'middle': ' '
- },
- style: {
- 'padding-left': 0,
- 'padding-right': 0
- }
- });
- }
- function errorAndExit(...args) {
- console.error(...args);
- process.exit(1);
- }
- function showVersionAndExit() {
- console.log(require('../package.json').version);
- process.exit(0);
- }
- function infoAndExit(...args) {
- console.log(...args);
- process.exit(0);
- }
- /**
- * Prints lint results to the console
- *
- * @param {String} filePath
- * @param {Object} results
- */
- function printReports(filePath, results) {
- let errorCount = 0;
- let warningCount = 0;
- const table = createTable();
- Object.entries(results).forEach(function([ name, reports ]) {
- reports.forEach(function(report) {
- const {
- category,
- id,
- message,
- name: reportName
- } = report;
- table.push(tableEntry({
- category,
- id,
- message,
- name: reportName || name
- }));
- if (category === 'error') {
- errorCount++;
- } else {
- warningCount++;
- }
- });
- });
- const problemCount = warningCount + errorCount;
- if (problemCount) {
- console.log();
- console.log(underline(path.resolve(filePath)));
- console.log(table.toString());
- }
- return {
- errorCount,
- warningCount
- };
- }
- async function lintDiagram(diagramPath, config) {
- let diagramXML;
- try {
- diagramXML = await readFile(path.resolve(diagramPath), 'utf-8');
- } catch (error) {
- return errorAndExit(`Error: Failed to read ${diagramPath}\n\n%s`, error.message);
- }
- const {
- error: importError,
- warnings: importWarnings,
- moddleElement
- } = await parseDiagram(diagramXML);
- if (importError) {
- return printReports(diagramPath, {
- '': [
- {
- message: 'Parse error: ' + importError.message,
- category: 'error'
- }
- ]
- });
- }
- const importReports = importWarnings.length ? {
- '': importWarnings.map(function(warning) {
- const {
- element,
- message
- } = warning;
- const id = element && element.id;
- return {
- id,
- message: 'Import warning: ' + message.split(/\n/)[0],
- category: 'error'
- };
- })
- } : {};
- try {
- const linter = new Linter({
- config,
- resolver: new NodeResolver()
- });
- const lintReports = await linter.lint(moddleElement);
- const allResults = {
- ...importReports,
- ...lintReports
- };
- return printReports(diagramPath, allResults);
- } catch (e) {
- return errorAndExit(e);
- }
- }
- async function lint(files, config) {
- let errorCount = 0;
- let warningCount = 0;
- console.log();
- for (let i = 0; i < files.length; i++) {
- let results = await lintDiagram(files[i], config);
- errorCount += results.errorCount;
- warningCount += results.warningCount;
- }
- const problemCount = errorCount + warningCount;
- let color;
- if (warningCount) {
- color = boldYellow;
- }
- if (errorCount) {
- color = boldRed;
- }
- if (problemCount) {
- console.log();
- console.log(color(
- `✖ ${problemCount} ${pluralize('problem', problemCount)} (${errorCount} ${pluralize('error', errorCount)}, ${warningCount} ${pluralize('warning', warningCount)})`
- ));
- }
- if (errorCount) {
- process.exit(1);
- }
- }
- async function run() {
- const {
- help,
- init,
- version,
- config: configOverridePath,
- _: files
- } = mri(process.argv.slice(2), {
- string: [ 'config' ],
- alias: {
- c: 'config'
- }
- });
- if (version) {
- return showVersionAndExit();
- }
- if (help) {
- return infoAndExit(HELP_STRING);
- }
- if (init) {
- if (fs.existsSync(CONFIG_NAME)) {
- return errorAndExit('Not overriding existing .bpmnlintrc');
- }
- fs.writeFileSync(CONFIG_NAME, DEFAULT_CONFIG_CONTENTS, 'utf8');
- return infoAndExit(`Created ${magenta(CONFIG_NAME)} file`);
- }
- if (files.length === 0) {
- return errorAndExit('Error: bpmn file path missing');
- }
- const configPath = configOverridePath || CONFIG_NAME;
- let configString, config;
- try {
- configString = await readFile(configPath, 'utf-8');
- } catch (error) {
- const message = (
- configOverridePath
- ? `Error: Could not read ${ magenta(configOverridePath) }`
- : `Error: Could not locate local ${ magenta(CONFIG_NAME) } file. Create one via
- ${magenta('bpmnlint --init')}
- Learn more about configuring bpmnlint: https://github.com/bpmn-io/bpmnlint#configuration`
- );
- return errorAndExit(message);
- }
- try {
- config = JSON.parse(configString);
- } catch (err) {
- return errorAndExit('Error: Could not parse %s\n\n%s', configPath, err.message);
- }
- const actualFiles = await glob(files);
- return lint(actualFiles, config);
- }
- run().catch(errorAndExit);
|