Skip to content
This repository was archived by the owner on May 12, 2025. It is now read-only.

Commit 15e42b4

Browse files
committed
add warnings and errors and change up api
1 parent 9b00a44 commit 15e42b4

File tree

12 files changed

+348
-84
lines changed

12 files changed

+348
-84
lines changed

example/src/app.js

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
1-
const path = require('path');
2-
31
const swaggerUi = require('swagger-ui-express');
42
const express = require('express');
53

64
const parseComments = require('../../dist/openapi-comment-parser');
75

8-
const swaggerDefinition = require('./swaggerDefinition');
96
const petRouter = require('./routes/pet');
107
const storeRouter = require('./routes/store');
118
const userRouter = require('./routes/user');
9+
const definition = require('./openapi-definition');
1210

1311
const app = express();
1412
const PORT = 3000;
@@ -17,10 +15,7 @@ const PORT = 3000;
1715
app.use(express.json());
1816
app.use(express.urlencoded({ extended: false }));
1917

20-
const spec = parseComments({
21-
definition: swaggerDefinition,
22-
paths: [path.join(__dirname, '**/*.?(js|yaml|yml)')],
23-
});
18+
const spec = parseComments(definition);
2419

2520
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(spec));
2621

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const swaggerDefinition = {
1+
const definition = {
22
openapi: '3.0.3',
33
info: {
44
title: 'Swagger Petstore',
@@ -42,4 +42,4 @@ const swaggerDefinition = {
4242
},
4343
};
4444

45-
module.exports = swaggerDefinition;
45+
module.exports = definition;

package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,12 @@
3636
"build": "run-s clean bundle",
3737
"clean": "rimraf dist",
3838
"bundle": "rollup -c && cp src/exported.d.ts dist/openapi-comment-parser.d.ts",
39-
"prepublishOnly": "npm run build",
40-
"mid": "nodemon -e yaml,yml,js,mjs,json middleware/src/app.js"
39+
"prepublishOnly": "npm run build"
4140
},
4241
"dependencies": {},
4342
"devDependencies": {
4443
"@rollup/plugin-commonjs": "^11.1.0",
44+
"@rollup/plugin-json": "^4.0.3",
4545
"@rollup/plugin-node-resolve": "^7.1.3",
4646
"@rollup/plugin-sucrase": "^3.0.1",
4747
"@types/chai": "^4.2.11",
@@ -51,13 +51,16 @@
5151
"@types/sinon": "^9.0.4",
5252
"@types/sinon-chai": "^3.2.4",
5353
"body-parser": "1.19.0",
54+
"caller-callsite": "^4.1.0",
5455
"chai": "4.2.0",
56+
"chalk": "^4.0.0",
5557
"comment-parser": "^0.7.4",
5658
"eslint": "6.8.0",
5759
"eslint-config-airbnb-base": "14.1.0",
5860
"eslint-config-prettier": "6.10.1",
5961
"eslint-loader": "3.0.3",
6062
"eslint-plugin-import": "2.20.1",
63+
"eslint-plugin-openapi-jsdoc": "^0.0.3",
6164
"eslint-plugin-prettier": "3.1.2",
6265
"globby": "^11.0.0",
6366
"js-yaml": "^3.13.1",

rollup.config.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
11
import resolve from '@rollup/plugin-node-resolve';
22
import commonjs from '@rollup/plugin-commonjs';
33
import sucrase from '@rollup/plugin-sucrase';
4+
import json from '@rollup/plugin-json';
5+
46
import pkg from './package.json';
57

68
export default [
79
{
810
input: 'src/index.ts',
911
plugins: [
1012
resolve(), // so Rollup can find external packages
11-
commonjs(), // so Rollup can convert external packages to ES modules
13+
commonjs({ ignoreGlobal: true }), // so Rollup can convert external packages to ES modules
1214
sucrase({
1315
// so Rollup can convert TypeScript to JavaScript
1416
exclude: ['node_modules/**', '**/?(*.)test.ts'],
1517
transforms: ['typescript'],
1618
}),
19+
json(),
1720
],
1821
output: [
1922
{ file: pkg.main, format: 'cjs' },

src/exported.d.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
export interface ParserOptions {
2-
definition: BaseDefinition;
3-
paths: string[];
2+
root?: string;
3+
extension?: string[];
4+
include?: string[];
5+
exclude?: string[];
6+
excludeNodeModules?: boolean;
47
verbose?: boolean;
58
}
69

@@ -262,7 +265,7 @@ export interface Map<T> {
262265
[key: string]: T;
263266
}
264267

265-
declare function parseComments({
266-
definition,
267-
paths,
268-
}: ParserOptions): OpenApiObject;
268+
declare function parseComments(
269+
definition: BaseDefinition | string,
270+
options: ParserOptions
271+
): OpenApiObject;

src/index.ts

Lines changed: 93 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,118 @@
1+
import path from 'path';
12
import jsYaml from 'js-yaml';
3+
import callerCallsite from 'caller-callsite';
4+
import chalk from 'chalk';
5+
6+
// @ts-ignore
7+
import { Linter } from 'eslint';
8+
// @ts-ignore
9+
import openapiEslintPlugin from 'eslint-plugin-openapi-jsdoc';
210

311
import parseFile from './util/parseFile';
412
import globList from './util/globList';
513
import SpecBuilder from './SpecBuilder';
6-
import { ParserOptions, OpenApiObject } from './exported';
14+
import { ParserOptions, OpenApiObject, BaseDefinition } from './exported';
715
import yamlLOC from './util/yamlLOC';
16+
import loadDefinition from './util/loadDefinition';
17+
import formatter from './util/formatter';
818

9-
function parseComments({
10-
definition,
11-
paths,
12-
verbose = true,
13-
}: ParserOptions): OpenApiObject {
14-
if (!definition && !paths) {
15-
throw new Error('Provided options are incorrect.');
19+
function getCallerPath() {
20+
const filename = callerCallsite()?.getFileName();
21+
if (filename) {
22+
return path.dirname(filename);
1623
}
24+
return '';
25+
}
26+
27+
function parseComments(
28+
definition: BaseDefinition | string,
29+
{
30+
root = getCallerPath(),
31+
extension = ['.js', '.cjs', '.mjs', '.ts', '.tsx', '.jsx', '.yaml', '.yml'],
32+
include = ['**'],
33+
exclude = [
34+
'coverage/**',
35+
'packages/*/test{,s}/**',
36+
'**/*.d.ts',
37+
'test{,s}/**',
38+
`test{,-*}.{js,cjs,mjs,ts,tsx,jsx,yaml,yml}`,
39+
`**/*{.,-}test.{js,cjs,mjs,ts,tsx,jsx,yaml,yml}`,
40+
'**/__tests__/**',
1741

18-
const spec = new SpecBuilder(definition);
42+
/* Exclude common development tool configuration files */
43+
'**/{ava,babel,nyc}.config.{js,cjs,mjs}',
44+
'**/jest.config.{js,cjs,mjs,ts}',
45+
'**/{karma,rollup,webpack}.config.js',
46+
'**/.{eslint,mocha}rc.{js,cjs}',
1947

20-
const files = globList(paths);
48+
// always ignore '**/node_modules/**'
49+
],
50+
excludeNodeModules = true,
51+
verbose = true,
52+
}: ParserOptions = {}
53+
): OpenApiObject {
54+
if (!definition) {
55+
throw new Error('A base OpenAPI definition is required.');
56+
}
57+
58+
let definitionObject: BaseDefinition;
59+
if (typeof definition === 'string') {
60+
definitionObject = loadDefinition(definition);
61+
} else {
62+
definitionObject = definition;
63+
}
64+
65+
const spec = new SpecBuilder(definitionObject);
66+
67+
const files = globList(root, extension, include, exclude, excludeNodeModules);
68+
69+
const linter = new Linter();
70+
linter.defineRules({
71+
...openapiEslintPlugin.rules,
72+
});
2173

2274
let totalLOC = 0;
75+
let allMessages: any[] = [];
2376
files.forEach((file) => {
24-
const parsedFile = parseFile(file, verbose);
77+
const { parsedFile, messages } = parseFile(file, linter, verbose);
78+
allMessages.push({ filePath: file, messages: messages });
2579
const specs = parsedFile.map((item) => item.spec);
2680
const loc = parsedFile.reduce((acc, cur) => (acc += cur.loc), 0);
2781
totalLOC += loc;
2882
spec.addData(specs);
2983
});
3084

3185
if (verbose) {
32-
const specAsYaml = jsYaml.safeDump(JSON.parse(JSON.stringify(spec)));
33-
const originalLOC = yamlLOC(specAsYaml);
86+
const errorTable = formatter(allMessages);
87+
if (errorTable) {
88+
console.log(errorTable);
89+
}
90+
91+
// Only measure paths and components.
92+
let pathsAsYaml = '';
93+
if (spec.paths) {
94+
pathsAsYaml = jsYaml.safeDump(JSON.parse(JSON.stringify(spec.paths)));
95+
}
96+
let componentAsYaml = '';
97+
if (spec.components) {
98+
componentAsYaml = jsYaml.safeDump(
99+
JSON.parse(JSON.stringify(spec.components))
100+
);
101+
}
102+
const originalLOC = yamlLOC(pathsAsYaml) + yamlLOC(componentAsYaml);
34103
const locDiff = originalLOC - totalLOC;
35104
const savings = (locDiff / originalLOC) * 100;
36-
console.log(
37-
`✨ You've saved ${locDiff} lines of extra YAML (${savings.toFixed(1)}%)`
38-
);
105+
if (locDiff > 0) {
106+
console.log();
107+
console.log(
108+
`✨ You've saved ${chalk.bold(
109+
locDiff
110+
)} lines of extra YAML (${chalk.green.bold(
111+
`\u25BC ${savings.toFixed(1)}%`
112+
)})`
113+
);
114+
console.log();
115+
}
39116
}
40117

41118
return spec;

src/util/formatter.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import chalk from 'chalk';
2+
3+
function pluralize(word: string, count: number) {
4+
return count === 1 ? word : `${word}s`;
5+
}
6+
7+
function formatter(results: any[]) {
8+
let output = '\n';
9+
let errorCount = 0;
10+
let warningCount = 0;
11+
12+
results.forEach((result) => {
13+
const messages = result.messages;
14+
15+
if (messages.length === 0) {
16+
return;
17+
}
18+
19+
output += `${chalk.underline(result.filePath)}\n`;
20+
21+
const maxPositionWidth = messages.reduce((acc: number, message: any) => {
22+
const position = `${message.line || 0}:${message.column || 0}`;
23+
return Math.max(acc, position.length);
24+
}, 0);
25+
26+
output += `${messages
27+
.map((message: any) => {
28+
let position = `${message.line || 0}:${message.column || 0}`;
29+
position += ' '.repeat(maxPositionWidth - position.length);
30+
let messageType;
31+
if (message.severity === 2) {
32+
messageType = chalk.red('error ');
33+
errorCount++;
34+
} else {
35+
messageType = chalk.yellow('warning');
36+
warningCount++;
37+
}
38+
return ` ${chalk.dim(position)} ${messageType} ${message.message}`;
39+
})
40+
.join('\n')}\n\n`;
41+
});
42+
43+
const total = errorCount + warningCount;
44+
45+
const problems = `${total} ${pluralize('problem', total)}`;
46+
const errors = `${errorCount} ${pluralize('error', errorCount)}`;
47+
const warnings = `${warningCount} ${pluralize('warning', warningCount)}`;
48+
49+
const summary = `\u2716 ${problems} (${errors}, ${warnings})`;
50+
51+
if (errorCount > 0) {
52+
output += chalk.red.bold(summary);
53+
} else {
54+
output += chalk.yellow.bold(summary);
55+
}
56+
57+
// Resets output color, for prevent change on top level
58+
return total > 0 ? chalk.reset(output) : '';
59+
}
60+
61+
export default formatter;

src/util/globList.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,34 @@
1+
import path from 'path';
12
import { sync as glob } from 'globby';
23

3-
function convertGlobPaths(globs: string[]): string[] {
4-
return globs.map((globString) => glob(globString)).flat();
4+
function convertGlobPaths(
5+
root: string,
6+
extension: string[],
7+
include: string[],
8+
exclude: string[],
9+
excludeNodeModules: boolean
10+
): string[] {
11+
const included = include
12+
.map((globString) => glob(path.join(root, globString)))
13+
.flat();
14+
15+
if (excludeNodeModules) {
16+
exclude.push('**/node_modules/**');
17+
}
18+
19+
const excluded = exclude
20+
.map((globString) => glob(path.join(root, globString)))
21+
.flat();
22+
23+
return included.filter((file) => {
24+
if (excluded.includes(file)) {
25+
return false;
26+
}
27+
if (extension.includes(path.extname(file))) {
28+
return true;
29+
}
30+
return false;
31+
});
532
}
633

734
export default convertGlobPaths;

src/util/loadDefinition.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import fs from 'fs';
2+
import path from 'path';
3+
4+
import jsYaml from 'js-yaml';
5+
6+
import { BaseDefinition } from '../exported';
7+
8+
function parseFile(file: string): BaseDefinition {
9+
const ext = path.extname(file);
10+
if (ext !== '.yaml' && ext !== '.yml' && ext !== '.json') {
11+
throw new Error('OpenAPI definition path must be YAML or JSON.');
12+
}
13+
14+
const fileContent = fs.readFileSync(file, { encoding: 'utf8' });
15+
16+
if (ext === '.yaml' || ext === '.yml') {
17+
return jsYaml.safeLoad(fileContent);
18+
} else {
19+
return JSON.parse(fileContent);
20+
}
21+
}
22+
23+
export default parseFile;

0 commit comments

Comments
 (0)