@visulima/jsdoc-open-api 1.2.0 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/parse-file.ts DELETED
@@ -1,52 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import yaml from "yaml";
4
-
5
- import { OpenApiObject } from "./exported";
6
- import yamlLoc from "./util/yaml-loc";
7
-
8
- const ALLOWED_KEYS = new Set(["openapi", "info", "servers", "security", "tags", "externalDocs", "components", "paths"]);
9
-
10
- class ParseError extends Error {
11
- filePath?: string;
12
- }
13
-
14
- const parseFile = (
15
- file: string,
16
- commentsToOpenApi: (fileContent: string, verbose?: boolean) => { spec: OpenApiObject; loc: number }[],
17
- verbose?: boolean,
18
- ): { spec: OpenApiObject; loc: number }[] => {
19
- const fileContent = fs.readFileSync(file, { encoding: "utf8" });
20
- const extension = path.extname(file);
21
-
22
- if (extension === ".yaml" || extension === ".yml") {
23
- const spec = yaml.parse(fileContent);
24
- const invalidKeys = Object.keys(spec).filter((key) => !ALLOWED_KEYS.has(key));
25
-
26
- if (invalidKeys.length > 0) {
27
- const error = new ParseError(`Unexpected keys: ${invalidKeys.join(", ")}`);
28
-
29
- error.filePath = file;
30
-
31
- throw error;
32
- }
33
-
34
- if (Object.keys(spec).some((key) => ALLOWED_KEYS.has(key))) {
35
- const loc = yamlLoc(fileContent);
36
-
37
- return [{ spec, loc }];
38
- }
39
-
40
- return [];
41
- }
42
-
43
- try {
44
- return commentsToOpenApi(fileContent, verbose);
45
- } catch (error: any) {
46
- error.filePath = file;
47
-
48
- throw error;
49
- }
50
- };
51
-
52
- export default parseFile;
@@ -1,61 +0,0 @@
1
- import {
2
- BaseDefinition,
3
- ComponentsObject,
4
- ExternalDocumentationObject,
5
- InfoObject,
6
- OpenApiObject,
7
- PathsObject,
8
- SecurityRequirementObject,
9
- ServerObject,
10
- TagObject,
11
- } from "./exported";
12
- import objectMerge from "./util/object-merge";
13
-
14
- class SpecBuilder implements OpenApiObject {
15
- openapi: string;
16
-
17
- info: InfoObject;
18
-
19
- servers?: ServerObject[];
20
-
21
- paths: PathsObject;
22
-
23
- components?: ComponentsObject;
24
-
25
- security?: SecurityRequirementObject[];
26
-
27
- tags?: TagObject[];
28
-
29
- externalDocs?: ExternalDocumentationObject;
30
-
31
- constructor(baseDefinition: BaseDefinition) {
32
- this.openapi = baseDefinition.openapi;
33
- this.info = baseDefinition.info;
34
- this.servers = baseDefinition.servers;
35
- this.paths = baseDefinition.paths || {};
36
- this.components = baseDefinition.components;
37
- this.security = baseDefinition.security;
38
- this.tags = baseDefinition.tags;
39
- this.externalDocs = baseDefinition.externalDocs;
40
- }
41
-
42
- addData(parsedFile: OpenApiObject[]) {
43
- parsedFile.forEach((file) => {
44
- const { paths, components, ...rest } = file;
45
-
46
- // only merge paths and components
47
- objectMerge(this, {
48
- paths: paths || {},
49
- components: components || {},
50
- } as OpenApiObject);
51
-
52
- // overwrite everything else:
53
- Object.entries(rest).forEach(([key, value]) => {
54
- // @ts-ignore
55
- this[key as keyof OpenApiObject] = value;
56
- });
57
- });
58
- }
59
- }
60
-
61
- export default SpecBuilder;
@@ -1,89 +0,0 @@
1
- import type { Spec } from "comment-parser";
2
- import { parse as parseComments } from "comment-parser";
3
- import mergeWith from "lodash.mergewith";
4
- import yaml, { YAMLError } from "yaml";
5
-
6
- import { OpenApiObject } from "../exported";
7
- import customizer from "../util/customizer";
8
- import organizeSwaggerObject from "./organize-swagger-object";
9
- import { getSwaggerVersionFromSpec, hasEmptyProperty } from "./utils";
10
-
11
- const specificationTemplate = {
12
- v2: ["paths", "definitions", "responses", "parameters", "securityDefinitions"],
13
- v3: ["paths", "definitions", "responses", "parameters", "securityDefinitions", "components"],
14
- v4: ["components", "channels"],
15
- };
16
-
17
- type ExtendedYAMLError = YAMLError & { annotation?: string };
18
-
19
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
20
- const tagsToObjects = (specs: Spec[], verbose?: boolean) => specs.map((spec: Spec) => {
21
- if ((spec.tag === "openapi" || spec.tag === "swagger" || spec.tag === "asyncapi") && spec.description !== "") {
22
- const parsed = yaml.parseDocument(spec.description);
23
-
24
- if (parsed.errors && parsed.errors.length > 0) {
25
- parsed.errors.map<ExtendedYAMLError>((error) => {
26
- const newError: ExtendedYAMLError = error;
27
-
28
- newError.annotation = spec.description;
29
-
30
- return newError;
31
- });
32
-
33
- let errorString = "Error parsing YAML in @openapi spec:";
34
-
35
- errorString += verbose
36
- ? (parsed.errors as ExtendedYAMLError[])
37
- .map((error) => `${error.toString()}\nImbedded within:\n\`\`\`\n ${error?.annotation?.replace(/\n/g, "\n ")}\n\`\`\``)
38
- .join("\n")
39
- : parsed.errors.map((error) => error.toString()).join("\n");
40
-
41
- throw new Error(errorString);
42
- }
43
-
44
- const parsedDocument = parsed.toJSON();
45
- const specification: Record<string, any> = {
46
- tags: [],
47
- };
48
-
49
- specificationTemplate[getSwaggerVersionFromSpec(spec)].forEach((property) => {
50
- specification[property] = specification[property] || {};
51
- });
52
-
53
- Object.keys(parsedDocument).forEach((property) => {
54
- organizeSwaggerObject(specification, parsedDocument, property);
55
- });
56
-
57
- return specification;
58
- }
59
-
60
- return {};
61
- });
62
-
63
- const commentsToOpenApi = (fileContents: string, verbose?: boolean): { spec: OpenApiObject; loc: number }[] => {
64
- const jsDocumentComments = parseComments(fileContents, { spacing: "preserve" });
65
-
66
- return jsDocumentComments.map((comment) => {
67
- // Line count, number of tags + 1 for description.
68
- // - Don't count line-breaking due to long descriptions
69
- // - Don't count empty lines
70
- const loc = comment.tags.length + 1;
71
- const result = mergeWith({}, ...tagsToObjects(comment.tags, verbose), customizer);
72
-
73
- ["definitions", "responses", "parameters", "securityDefinitions", "components", "tags"].forEach((property) => {
74
- if (typeof result[property] !== "undefined" && hasEmptyProperty(result[property])) {
75
- delete result[property];
76
- }
77
- });
78
-
79
- // Purge all undefined objects/arrays.
80
- const spec = JSON.parse(JSON.stringify(result));
81
-
82
- return {
83
- spec,
84
- loc,
85
- };
86
- });
87
- };
88
-
89
- export default commentsToOpenApi;
@@ -1,66 +0,0 @@
1
- import { isTagPresentInTags, mergeDeep } from "./utils";
2
-
3
- /**
4
- * @param {object} swaggerObject
5
- * @param {object} annotation
6
- * @param {string} property
7
- */
8
- // eslint-disable-next-line radar/no-duplicate-string
9
- const organizeSwaggerObject = (swaggerObject: Record<string, any>, annotation: Record<string, any>, property: string) => {
10
- // Root property on purpose.
11
- // eslint-disable-next-line no-secrets/no-secrets
12
- // @see https://github.com/OAI/OpenAPI-Specification/blob/master/proposals/002_Webhooks.md#proposed-solution
13
- if (property === "x-webhooks") {
14
- // eslint-disable-next-line no-param-reassign
15
- swaggerObject[property] = annotation[property];
16
- }
17
-
18
- // Other extensions can be in varying places depending on different vendors and opinions.
19
- // The following return makes it so that they are not put in `paths` in the last case.
20
- // New specific extensions will need to be handled on case-by-case if to be included in `paths`.
21
- if (property.startsWith("x-")) {
22
- return;
23
- }
24
-
25
- const commonProperties = [
26
- "components",
27
- "consumes",
28
- "produces",
29
- "paths",
30
- "schemas",
31
- "securityDefinitions",
32
- "responses",
33
- "parameters",
34
- "definitions",
35
- "channels",
36
- ];
37
- if (commonProperties.includes(property)) {
38
- Object.keys(annotation[property]).forEach((definition) => {
39
- // eslint-disable-next-line no-param-reassign
40
- swaggerObject[property][definition] = mergeDeep(swaggerObject[property][definition], annotation[property][definition]);
41
- });
42
- } else if (property === "tags") {
43
- const { tags } = annotation;
44
-
45
- if (Array.isArray(tags)) {
46
- tags.forEach((tag) => {
47
- if (!isTagPresentInTags(tag, swaggerObject.tags)) {
48
- swaggerObject.tags.push(tag);
49
- }
50
- });
51
- } else if (!isTagPresentInTags(tags, swaggerObject.tags)) {
52
- swaggerObject.tags.push(tags);
53
- }
54
- } else if (property === "security") {
55
- const { security } = annotation;
56
-
57
- // eslint-disable-next-line no-param-reassign
58
- swaggerObject.security = security;
59
- } else if (property.startsWith("/")) {
60
- // Paths which are not defined as "paths" property, starting with a slash "/"
61
- // eslint-disable-next-line no-param-reassign
62
- swaggerObject.paths[property] = mergeDeep(swaggerObject.paths[property], annotation[property]);
63
- }
64
- };
65
-
66
- export default organizeSwaggerObject;
@@ -1,43 +0,0 @@
1
- import type { Spec } from "comment-parser";
2
- import mergeWith from "lodash.mergewith";
3
-
4
- /**
5
- * A recursive deep-merge that ignores null values when merging.
6
- * This returns the merged object and does not mutate.
7
- * @param {object} first the first object to get merged
8
- * @param {object} second the second object to get merged
9
- */
10
- export const mergeDeep = (first?: object, second?: object) => mergeWith({}, first, second, (a, b) => (b === null ? a : undefined));
11
-
12
- /**
13
- * Checks if there is any properties of the input object which are an empty object
14
- * @param {object} object - the object to check
15
- * @returns {boolean}
16
- */
17
- export const hasEmptyProperty = (object: Record<string, any>): boolean => Object.keys(object)
18
- .map((key) => object[key])
19
- .every((keyObject) => typeof keyObject === "object" && Object.keys(keyObject).every((key) => !(key in keyObject)));
20
-
21
- /**
22
- * @param {object} tag
23
- * @param {array} tags
24
- * @returns {boolean}
25
- */
26
- export const isTagPresentInTags = (tag: Spec, tags: Spec[]) => tags.some((targetTag) => tag.name === targetTag.name);
27
-
28
- export const getSwaggerVersionFromSpec = (tag: Spec) => {
29
- switch (tag.tag) {
30
- case "openapi": {
31
- return "v3";
32
- }
33
- case "asyncapi": {
34
- return "v4";
35
- }
36
- case "swagger": {
37
- return "v2";
38
- }
39
- default: {
40
- return "v2";
41
- }
42
- }
43
- };
@@ -1,9 +0,0 @@
1
- const customizer = (objectValue: any, sourceValue: any) => {
2
- if (Array.isArray(objectValue)) {
3
- return [...objectValue, ...sourceValue];
4
- }
5
- // eslint-disable-next-line unicorn/no-useless-undefined
6
- return undefined;
7
- };
8
-
9
- export default customizer;
@@ -1,22 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import yaml from "yaml";
4
-
5
- import type { BaseDefinition } from "../exported";
6
-
7
- function parseFile(file: string): BaseDefinition {
8
- const extension = path.extname(file);
9
- if (extension !== ".yaml" && extension !== ".yml" && extension !== ".json") {
10
- throw new Error("OpenAPI definition path must be YAML or JSON.");
11
- }
12
-
13
- const fileContent = fs.readFileSync(file, { encoding: "utf8" });
14
-
15
- if (extension === ".yaml" || extension === ".yml") {
16
- return yaml.parse(fileContent);
17
- }
18
-
19
- return JSON.parse(fileContent);
20
- }
21
-
22
- export default parseFile;
@@ -1,20 +0,0 @@
1
- function objectMerge<T>(a: T, b: T) {
2
- Object.keys(b as object).forEach((key) => {
3
- if (a[key as keyof typeof b] === undefined) {
4
- // eslint-disable-next-line no-param-reassign
5
- a[key as keyof typeof b] = {
6
- ...b[key as keyof typeof b],
7
- };
8
- } else {
9
- Object.keys(b[key as keyof typeof b] as object).forEach((subKey) => {
10
- // eslint-disable-next-line no-param-reassign
11
- (a[key as keyof typeof b] as { [key: string]: object })[subKey] = {
12
- ...(a[key as keyof typeof b] as { [key: string]: object })[subKey],
13
- ...(b[key as keyof typeof b] as { [key: string]: object })[subKey],
14
- };
15
- });
16
- }
17
- });
18
- }
19
-
20
- export default objectMerge;
@@ -1,17 +0,0 @@
1
- function yamlLoc(string: string): number {
2
- // Break string into lines.
3
- const split = string.split(/\r\n|\r|\n/);
4
-
5
- const filtered = split.filter((line) => {
6
- // Remove comments.
7
- if (/^\s*(#\s*.*)?$/.test(line)) {
8
- return false;
9
- }
10
- // Remove empty lines.
11
- return line.trim().length > 0;
12
- });
13
-
14
- return filtered.length;
15
- }
16
-
17
- export default yamlLoc;
@@ -1,169 +0,0 @@
1
- import SwaggerParser from "@apidevtools/swagger-parser";
2
- import { collect } from "@visulima/readdir";
3
- import fs from "node:fs";
4
- import { dirname } from "node:path";
5
- import { exit } from "node:process";
6
- // eslint-disable-next-line import/no-extraneous-dependencies
7
- import { Compiler } from "webpack";
8
-
9
- import type { BaseDefinition } from "../exported";
10
- import jsDocumentCommentsToOpenApi from "../jsdoc/comments-to-open-api";
11
- import parseFile from "../parse-file";
12
- import SpecBuilder from "../spec-builder";
13
- import swaggerJsDocumentCommentsToOpenApi from "../swagger-jsdoc/comments-to-open-api";
14
-
15
- const exclude = [
16
- "coverage/**",
17
- ".github/**",
18
- "packages/*/test{,s}/**",
19
- "**/*.d.ts",
20
- "test{,s}/**",
21
- "test{,-*}.{js,cjs,mjs,ts,tsx,jsx,yaml,yml}",
22
- "**/*{.,-}test.{js,cjs,mjs,ts,tsx,jsx,yaml,yml}",
23
- "**/__tests__/**",
24
- "**/{ava,babel,nyc}.config.{js,cjs,mjs}",
25
- "**/jest.config.{js,cjs,mjs,ts}",
26
- "**/{karma,rollup,webpack}.config.js",
27
- "**/.{eslint,mocha}rc.{js,cjs}",
28
- "**/.{travis,yarnrc}.yml",
29
- "**/{docker-compose,docker}.yml",
30
- "**/.yamllint.{yaml,yml}",
31
- "**/node_modules/**",
32
- "**/pnpm-lock.yaml",
33
- "**/pnpm-workspace.yaml",
34
- "**/{package,package-lock}.json",
35
- "**/yarn.lock",
36
- "**/package.json5",
37
- "**/.next/**",
38
- ];
39
-
40
- const errorHandler = (error: any) => {
41
- if (error) {
42
- // eslint-disable-next-line no-console
43
- console.error(error);
44
- exit(1);
45
- }
46
- };
47
-
48
- class SwaggerCompilerPlugin {
49
- private readonly swaggerDefinition: BaseDefinition;
50
-
51
- private readonly sources: string[];
52
-
53
- private readonly verbose: boolean;
54
-
55
- private readonly ignore: string | ReadonlyArray<string>;
56
-
57
- assetsPath: string;
58
-
59
- constructor(
60
- assetsPath: string,
61
- sources: string[],
62
- swaggerDefinition: BaseDefinition,
63
- options: {
64
- verbose?: boolean;
65
- ignore?: string | ReadonlyArray<string>;
66
- },
67
- ) {
68
- this.assetsPath = assetsPath;
69
- this.swaggerDefinition = swaggerDefinition;
70
- this.sources = sources;
71
- this.verbose = options.verbose || false;
72
- this.ignore = options.ignore || [];
73
- }
74
-
75
- apply(compiler: Compiler) {
76
- compiler.hooks.make.tapAsync("SwaggerCompilerPlugin", async (_, callback: VoidFunction) => {
77
- // eslint-disable-next-line no-console
78
- console.log("Build paused, switching to swagger build");
79
-
80
- const spec = new SpecBuilder(this.swaggerDefinition);
81
-
82
- // eslint-disable-next-line no-restricted-syntax,unicorn/prevent-abbreviations
83
- for await (const dir of this.sources) {
84
- const files = await collect(dir, {
85
- // eslint-disable-next-line @rushstack/security/no-unsafe-regexp
86
- skip: [...this.ignore, ...exclude],
87
- extensions: [".js", ".cjs", ".mjs", ".ts", ".tsx", ".jsx", ".yaml", ".yml"],
88
- includeDirs: false,
89
- minimatchOptions: {
90
- match: {
91
- debug: this.verbose,
92
- matchBase: true,
93
- },
94
- skip: {
95
- debug: this.verbose,
96
- matchBase: true,
97
- },
98
- },
99
- });
100
-
101
- if (this.verbose) {
102
- // eslint-disable-next-line no-console
103
- console.log(`Found ${files.length} files in ${dir}`);
104
- // eslint-disable-next-line no-console
105
- console.log(files);
106
- }
107
-
108
- files.forEach((file) => {
109
- if (this.verbose) {
110
- // eslint-disable-next-line no-console
111
- console.log(`Parsing file ${file}`);
112
- }
113
-
114
- try {
115
- const parsedJsDocumentFile = parseFile(file, jsDocumentCommentsToOpenApi, this.verbose);
116
-
117
- spec.addData(parsedJsDocumentFile.map((item) => item.spec));
118
-
119
- const parsedSwaggerJsDocumentFile = parseFile(file, swaggerJsDocumentCommentsToOpenApi, this.verbose);
120
-
121
- spec.addData(parsedSwaggerJsDocumentFile.map((item) => item.spec));
122
- } catch (error) {
123
- // eslint-disable-next-line no-console
124
- console.error(error);
125
- exit(1);
126
- }
127
- });
128
- }
129
-
130
- try {
131
- if (this.verbose) {
132
- // eslint-disable-next-line no-console
133
- console.log("Validating swagger spec");
134
- // eslint-disable-next-line no-console
135
- console.log(JSON.stringify(spec, null, 2));
136
- }
137
-
138
- await SwaggerParser.validate(JSON.parse(JSON.stringify(spec)));
139
- } catch (error: any) {
140
- // eslint-disable-next-line no-console
141
- console.error(error.toJSON());
142
- exit(1);
143
- }
144
-
145
- const { assetsPath } = this;
146
-
147
- fs.mkdir(dirname(assetsPath), { recursive: true }, (error) => {
148
- if (error) {
149
- errorHandler(error);
150
- }
151
-
152
- fs.writeFile(assetsPath, JSON.stringify(spec, null, 2), errorHandler);
153
- });
154
-
155
- // eslint-disable-next-line unicorn/consistent-destructuring
156
- if (this.verbose) {
157
- // eslint-disable-next-line no-console,unicorn/consistent-destructuring
158
- console.log(`Written swagger spec to "${this.assetsPath}" file`);
159
- }
160
-
161
- // eslint-disable-next-line no-console
162
- console.log("switching back to normal build");
163
-
164
- callback();
165
- });
166
- }
167
- }
168
-
169
- export default SwaggerCompilerPlugin;