@visulima/jsdoc-open-api 1.0.0

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/dist/index.mjs ADDED
@@ -0,0 +1,633 @@
1
+ // src/util/object-merge.ts
2
+ function objectMerge(a, b) {
3
+ Object.keys(b).forEach((key) => {
4
+ if (a[key] === void 0) {
5
+ a[key] = {
6
+ ...b[key]
7
+ };
8
+ } else {
9
+ Object.keys(b[key]).forEach((subKey) => {
10
+ a[key][subKey] = {
11
+ ...a[key][subKey],
12
+ ...b[key][subKey]
13
+ };
14
+ });
15
+ }
16
+ });
17
+ }
18
+ var object_merge_default = objectMerge;
19
+
20
+ // src/spec-builder.ts
21
+ var SpecBuilder = class {
22
+ constructor(baseDefinition) {
23
+ this.openapi = baseDefinition.openapi;
24
+ this.info = baseDefinition.info;
25
+ this.servers = baseDefinition.servers;
26
+ this.paths = baseDefinition.paths || {};
27
+ this.components = baseDefinition.components;
28
+ this.security = baseDefinition.security;
29
+ this.tags = baseDefinition.tags;
30
+ this.externalDocs = baseDefinition.externalDocs;
31
+ }
32
+ addData(parsedFile) {
33
+ parsedFile.forEach((file) => {
34
+ const { paths, components, ...rest } = file;
35
+ object_merge_default(this, {
36
+ paths,
37
+ components
38
+ });
39
+ Object.entries(rest).forEach(([key, value]) => {
40
+ this[key] = value;
41
+ });
42
+ });
43
+ }
44
+ };
45
+ var spec_builder_default = SpecBuilder;
46
+
47
+ // src/parse-file.ts
48
+ import fs from "fs";
49
+ import path from "path";
50
+ import yaml from "yaml";
51
+
52
+ // src/util/yaml-loc.ts
53
+ function yamlLoc(string) {
54
+ const split = string.split(/\r\n|\r|\n/);
55
+ const filtered = split.filter((line) => {
56
+ if (/^\s*(#\s*.*)?$/.test(line)) {
57
+ return false;
58
+ }
59
+ return line.trim().length > 0;
60
+ });
61
+ return filtered.length;
62
+ }
63
+ var yaml_loc_default = yamlLoc;
64
+
65
+ // src/parse-file.ts
66
+ var ALLOWED_KEYS = /* @__PURE__ */ new Set(["openapi", "info", "servers", "security", "tags", "externalDocs", "components", "paths"]);
67
+ var ParseError = class extends Error {
68
+ };
69
+ var parseFile = (file, commentsToOpenApi3, verbose) => {
70
+ const fileContent = fs.readFileSync(file, { encoding: "utf8" });
71
+ const extension = path.extname(file);
72
+ if (extension === ".yaml" || extension === ".yml") {
73
+ const spec = yaml.parse(fileContent);
74
+ const invalidKeys = Object.keys(spec).filter((key) => !ALLOWED_KEYS.has(key));
75
+ if (invalidKeys.length > 0) {
76
+ const error = new ParseError(`Unexpected keys: ${invalidKeys.join(", ")}`);
77
+ error.filePath = file;
78
+ throw error;
79
+ }
80
+ if (Object.keys(spec).some((key) => ALLOWED_KEYS.has(key))) {
81
+ const loc = yaml_loc_default(fileContent);
82
+ return [{ spec, loc }];
83
+ }
84
+ return [];
85
+ }
86
+ try {
87
+ return commentsToOpenApi3(fileContent, verbose);
88
+ } catch (error) {
89
+ error.filePath = file;
90
+ throw error;
91
+ }
92
+ };
93
+ var parse_file_default = parseFile;
94
+
95
+ // src/webpack/swagger-compiler-plugin.ts
96
+ import SwaggerParser from "@apidevtools/swagger-parser";
97
+ import { collect } from "@visulima/readdir";
98
+ import _debug from "debug";
99
+ import { exit } from "process";
100
+
101
+ // src/jsdoc/comments-to-open-api.ts
102
+ import { parse as parseComments } from "comment-parser";
103
+ import mergeWith from "lodash.mergewith";
104
+
105
+ // src/util/customizer.ts
106
+ var customizer = (objectValue, sourceValue) => {
107
+ if (Array.isArray(objectValue)) {
108
+ return [...objectValue, ...sourceValue];
109
+ }
110
+ return void 0;
111
+ };
112
+ var customizer_default = customizer;
113
+
114
+ // src/jsdoc/comments-to-open-api.ts
115
+ function fixSecurityObject(thing) {
116
+ if (thing.security) {
117
+ thing.security = Object.keys(thing.security).map((s) => {
118
+ return {
119
+ [s]: thing.security[s]
120
+ };
121
+ });
122
+ }
123
+ }
124
+ var primitiveTypes = /* @__PURE__ */ new Set(["integer", "number", "string", "boolean", "object", "array"]);
125
+ var formatMap = {
126
+ int32: "integer",
127
+ int64: "integer",
128
+ float: "number",
129
+ double: "number",
130
+ date: "string",
131
+ "date-time": "string",
132
+ password: "string",
133
+ byte: "string",
134
+ binary: "string"
135
+ };
136
+ function parseDescription(tag) {
137
+ const rawType = tag.type;
138
+ const isArray = rawType && rawType.endsWith("[]");
139
+ let parsedType;
140
+ if (rawType) {
141
+ parsedType = rawType.replace(/\[]$/, "");
142
+ }
143
+ const isPrimitive = primitiveTypes.has(parsedType);
144
+ const isFormat = Object.keys(formatMap).includes(parsedType);
145
+ let defaultValue;
146
+ if (tag.default) {
147
+ switch (parsedType) {
148
+ case "integer":
149
+ case "int32":
150
+ case "int64": {
151
+ defaultValue = Number.parseInt(tag.default, 10);
152
+ break;
153
+ }
154
+ case "number":
155
+ case "double":
156
+ case "float": {
157
+ defaultValue = Number.parseFloat(tag.default);
158
+ break;
159
+ }
160
+ default: {
161
+ defaultValue = tag.default;
162
+ break;
163
+ }
164
+ }
165
+ }
166
+ let rootType;
167
+ if (isPrimitive) {
168
+ rootType = { type: parsedType, default: defaultValue };
169
+ } else if (isFormat) {
170
+ rootType = {
171
+ type: formatMap[parsedType],
172
+ format: parsedType,
173
+ default: defaultValue
174
+ };
175
+ } else {
176
+ rootType = { $ref: `#/components/schemas/${parsedType}` };
177
+ }
178
+ let schema = isArray ? {
179
+ type: "array",
180
+ items: {
181
+ ...rootType
182
+ }
183
+ } : {
184
+ ...rootType
185
+ };
186
+ if (parsedType === void 0) {
187
+ schema = void 0;
188
+ }
189
+ let description = tag.description.trim().replace(/^- /, "");
190
+ if (description === "") {
191
+ description = void 0;
192
+ }
193
+ return {
194
+ name: tag.name,
195
+ description,
196
+ required: !tag.optional,
197
+ schema,
198
+ rawType
199
+ };
200
+ }
201
+ function tagsToObjects(tags, verbose) {
202
+ return tags.map((tag) => {
203
+ const parsedResponse = parseDescription(tag);
204
+ let nameAndDescription = "";
205
+ if (parsedResponse.name) {
206
+ nameAndDescription += parsedResponse.name;
207
+ }
208
+ if (parsedResponse.description) {
209
+ nameAndDescription += ` ${parsedResponse.description.trim()}`;
210
+ }
211
+ switch (tag.tag) {
212
+ case "operationId":
213
+ case "summary":
214
+ case "description": {
215
+ return { [tag.tag]: nameAndDescription };
216
+ }
217
+ case "deprecated": {
218
+ return { deprecated: true };
219
+ }
220
+ case "externalDocs": {
221
+ return {
222
+ externalDocs: {
223
+ url: parsedResponse.name,
224
+ description: parsedResponse.description
225
+ }
226
+ };
227
+ }
228
+ case "server": {
229
+ return {
230
+ servers: [
231
+ {
232
+ url: parsedResponse.name,
233
+ description: parsedResponse.description
234
+ }
235
+ ]
236
+ };
237
+ }
238
+ case "tag": {
239
+ return { tags: [nameAndDescription] };
240
+ }
241
+ case "cookieParam":
242
+ case "headerParam":
243
+ case "queryParam":
244
+ case "pathParam": {
245
+ return {
246
+ parameters: [
247
+ {
248
+ name: parsedResponse.name,
249
+ in: tag.tag.replace(/Param$/, ""),
250
+ description: parsedResponse.description,
251
+ required: parsedResponse.required,
252
+ schema: parsedResponse.schema
253
+ }
254
+ ]
255
+ };
256
+ }
257
+ case "bodyContent": {
258
+ return {
259
+ requestBody: {
260
+ content: {
261
+ [parsedResponse.name.replace("*\\/*", "*/*")]: {
262
+ schema: parsedResponse.schema
263
+ }
264
+ }
265
+ }
266
+ };
267
+ }
268
+ case "bodyExample": {
269
+ const [contentType, example] = parsedResponse.name.split(".");
270
+ return {
271
+ requestBody: {
272
+ content: {
273
+ [contentType]: {
274
+ examples: {
275
+ [example]: {
276
+ $ref: `#/components/examples/${parsedResponse.rawType}`
277
+ }
278
+ }
279
+ }
280
+ }
281
+ }
282
+ };
283
+ }
284
+ case "bodyDescription": {
285
+ return { requestBody: { description: nameAndDescription } };
286
+ }
287
+ case "bodyRequired": {
288
+ return { requestBody: { required: true } };
289
+ }
290
+ case "response": {
291
+ return {
292
+ responses: {
293
+ [parsedResponse.name]: {
294
+ description: parsedResponse.description
295
+ }
296
+ }
297
+ };
298
+ }
299
+ case "callback": {
300
+ return {
301
+ callbacks: {
302
+ [parsedResponse.name]: {
303
+ $ref: `#/components/callbacks/${parsedResponse.rawType}`
304
+ }
305
+ }
306
+ };
307
+ }
308
+ case "responseContent": {
309
+ const [status, contentType] = parsedResponse.name.split(".");
310
+ return {
311
+ responses: {
312
+ [status]: {
313
+ content: {
314
+ [contentType]: {
315
+ schema: parsedResponse.schema
316
+ }
317
+ }
318
+ }
319
+ }
320
+ };
321
+ }
322
+ case "responseHeaderComponent": {
323
+ const [status, header] = parsedResponse.name.split(".");
324
+ return {
325
+ responses: {
326
+ [status]: {
327
+ headers: {
328
+ [header]: {
329
+ $ref: `#/components/headers/${parsedResponse.rawType}`
330
+ }
331
+ }
332
+ }
333
+ }
334
+ };
335
+ }
336
+ case "responseHeader": {
337
+ const [status, header] = parsedResponse.name.split(".");
338
+ return {
339
+ responses: {
340
+ [status]: {
341
+ headers: {
342
+ [header]: {
343
+ description: parsedResponse.description,
344
+ schema: parsedResponse.schema
345
+ }
346
+ }
347
+ }
348
+ }
349
+ };
350
+ }
351
+ case "responseExample": {
352
+ const [status, contentType, example] = parsedResponse.name.split(".");
353
+ return {
354
+ responses: {
355
+ [status]: {
356
+ content: {
357
+ [contentType]: {
358
+ examples: {
359
+ [example]: {
360
+ $ref: `#/components/examples/${parsedResponse.rawType}`
361
+ }
362
+ }
363
+ }
364
+ }
365
+ }
366
+ }
367
+ };
368
+ }
369
+ case "responseLink": {
370
+ const [status, link] = parsedResponse.name.split(".");
371
+ return {
372
+ responses: {
373
+ [status]: {
374
+ links: {
375
+ [link]: {
376
+ $ref: `#/components/links/${parsedResponse.rawType}`
377
+ }
378
+ }
379
+ }
380
+ }
381
+ };
382
+ }
383
+ case "bodyComponent": {
384
+ return {
385
+ requestBody: {
386
+ $ref: `#/components/requestBodies/${parsedResponse.rawType}`
387
+ }
388
+ };
389
+ }
390
+ case "responseComponent": {
391
+ return {
392
+ responses: {
393
+ [parsedResponse.name]: {
394
+ $ref: `#/components/responses/${parsedResponse.rawType}`
395
+ }
396
+ }
397
+ };
398
+ }
399
+ case "paramComponent": {
400
+ return {
401
+ parameters: [{ $ref: `#/components/parameters/${parsedResponse.rawType}` }]
402
+ };
403
+ }
404
+ case "security": {
405
+ const [security, scopeItem] = parsedResponse.name.split(".");
406
+ let scope = [];
407
+ if (scopeItem) {
408
+ scope = [scopeItem];
409
+ }
410
+ return {
411
+ security: { [security]: scope }
412
+ };
413
+ }
414
+ default: {
415
+ return {};
416
+ }
417
+ }
418
+ });
419
+ }
420
+ var commentsToOpenApi = (fileContents, verbose) => {
421
+ const openAPIRegex = /^(GET|PUT|POST|DELETE|OPTIONS|HEAD|PATCH|TRACE) \/.*$/;
422
+ const jsDocumentComments = parseComments(fileContents, { spacing: "preserve" });
423
+ return jsDocumentComments.filter((comment) => openAPIRegex.test(comment.description.trim())).map((comment) => {
424
+ const loc = comment.tags.length + 1;
425
+ const result = mergeWith({}, ...tagsToObjects(comment.tags, verbose), customizer_default);
426
+ fixSecurityObject(result);
427
+ const [method, path2] = comment.description.split(" ");
428
+ const pathsObject = {
429
+ [path2.trim()]: {
430
+ [method.toLowerCase().trim()]: {
431
+ ...result
432
+ }
433
+ }
434
+ };
435
+ const spec = JSON.parse(JSON.stringify({ paths: pathsObject }));
436
+ return {
437
+ spec,
438
+ loc
439
+ };
440
+ });
441
+ };
442
+ var comments_to_open_api_default = commentsToOpenApi;
443
+
444
+ // src/swagger-jsdoc/comments-to-open-api.ts
445
+ import { parse as parseComments2 } from "comment-parser";
446
+ import mergeWith3 from "lodash.mergewith";
447
+ import yaml2 from "yaml";
448
+
449
+ // src/swagger-jsdoc/utils.ts
450
+ import mergeWith2 from "lodash.mergewith";
451
+ var mergeDeep = (first, second) => mergeWith2({}, first, second, (a, b) => b === null ? a : void 0);
452
+ var hasEmptyProperty = (object) => Object.keys(object).map((key) => object[key]).every((keyObject) => typeof keyObject === "object" && Object.keys(keyObject).every((key) => !(key in keyObject)));
453
+ var isTagPresentInTags = (tag, tags) => tags.some((targetTag) => tag.name === targetTag.name);
454
+ var getSwaggerVersionFromSpec = (tag) => {
455
+ switch (tag.tag) {
456
+ case "openapi": {
457
+ return "v3";
458
+ }
459
+ case "asyncapi": {
460
+ return "v4";
461
+ }
462
+ case "swagger": {
463
+ return "v2";
464
+ }
465
+ default: {
466
+ return "v2";
467
+ }
468
+ }
469
+ };
470
+
471
+ // src/swagger-jsdoc/organize-swagger-object.ts
472
+ var organizeSwaggerObject = (swaggerObject, annotation, property) => {
473
+ if (property === "x-webhooks") {
474
+ swaggerObject[property] = annotation[property];
475
+ }
476
+ if (property.startsWith("x-")) {
477
+ return;
478
+ }
479
+ const commonProperties = [
480
+ "components",
481
+ "consumes",
482
+ "produces",
483
+ "paths",
484
+ "schemas",
485
+ "securityDefinitions",
486
+ "responses",
487
+ "parameters",
488
+ "definitions",
489
+ "channels"
490
+ ];
491
+ if (commonProperties.includes(property)) {
492
+ Object.keys(annotation[property]).forEach((definition) => {
493
+ swaggerObject[property][definition] = mergeDeep(swaggerObject[property][definition], annotation[property][definition]);
494
+ });
495
+ } else if (property === "tags") {
496
+ const { tags } = annotation;
497
+ if (Array.isArray(tags)) {
498
+ tags.forEach((tag) => {
499
+ if (!isTagPresentInTags(tag, swaggerObject.tags)) {
500
+ swaggerObject.tags.push(tag);
501
+ }
502
+ });
503
+ } else if (!isTagPresentInTags(tags, swaggerObject.tags)) {
504
+ swaggerObject.tags.push(tags);
505
+ }
506
+ } else if (property === "security") {
507
+ const { security } = annotation;
508
+ swaggerObject.security = security;
509
+ } else if (property.startsWith("/")) {
510
+ swaggerObject.paths[property] = mergeDeep(swaggerObject.paths[property], annotation[property]);
511
+ }
512
+ };
513
+ var organize_swagger_object_default = organizeSwaggerObject;
514
+
515
+ // src/swagger-jsdoc/comments-to-open-api.ts
516
+ var specificationTemplate = {
517
+ v2: ["paths", "definitions", "responses", "parameters", "securityDefinitions"],
518
+ v3: ["paths", "definitions", "responses", "parameters", "securityDefinitions", "components"],
519
+ v4: ["components", "channels"]
520
+ };
521
+ var tagsToObjects2 = (specs, verbose) => specs.map((spec) => {
522
+ if ((spec.tag === "openapi" || spec.tag === "swagger" || spec.tag === "asyncapi") && spec.description !== "") {
523
+ const parsed = yaml2.parseDocument(spec.description);
524
+ if (parsed.errors && parsed.errors.length > 0) {
525
+ parsed.errors.map((error) => {
526
+ const newError = error;
527
+ newError.annotation = spec.description;
528
+ return newError;
529
+ });
530
+ let errorString = "Error parsing YAML in @openapi spec:";
531
+ errorString += verbose ? parsed.errors.map((error) => {
532
+ var _a;
533
+ return `${error.toString()}
534
+ Imbedded within:
535
+ \`\`\`
536
+ ${(_a = error == null ? void 0 : error.annotation) == null ? void 0 : _a.replace(/\n/g, "\n ")}
537
+ \`\`\``;
538
+ }).join("\n") : parsed.errors.map((error) => error.toString()).join("\n");
539
+ throw new Error(errorString);
540
+ }
541
+ const parsedDocument = parsed.toJSON();
542
+ const specification = {
543
+ tags: []
544
+ };
545
+ specificationTemplate[getSwaggerVersionFromSpec(spec)].forEach((property) => {
546
+ specification[property] = specification[property] || {};
547
+ });
548
+ Object.keys(parsedDocument).forEach((property) => {
549
+ organize_swagger_object_default(specification, parsedDocument, property);
550
+ });
551
+ return specification;
552
+ }
553
+ return {};
554
+ });
555
+ var commentsToOpenApi2 = (fileContents, verbose) => {
556
+ const jsDocumentComments = parseComments2(fileContents, { spacing: "preserve" });
557
+ return jsDocumentComments.map((comment) => {
558
+ const loc = comment.tags.length + 1;
559
+ const result = mergeWith3({}, ...tagsToObjects2(comment.tags, verbose), customizer_default);
560
+ ["definitions", "responses", "parameters", "securityDefinitions", "components", "tags"].forEach((property) => {
561
+ if (typeof result[property] !== "undefined" && hasEmptyProperty(result[property])) {
562
+ delete result[property];
563
+ }
564
+ });
565
+ const spec = JSON.parse(JSON.stringify(result));
566
+ return {
567
+ spec,
568
+ loc
569
+ };
570
+ });
571
+ };
572
+ var comments_to_open_api_default2 = commentsToOpenApi2;
573
+
574
+ // src/webpack/swagger-compiler-plugin.ts
575
+ var debug = _debug("visulima:jsdoc-open-api:swagger-compiler-plugin");
576
+ var SwaggerCompilerPlugin = class {
577
+ constructor(assetsPath, sources, swaggerDefinition, options) {
578
+ this.assetsPath = assetsPath;
579
+ this.swaggerDefinition = swaggerDefinition;
580
+ this.sources = sources;
581
+ this.verbose = options.verbose || false;
582
+ this.ignore = options.ignore || [];
583
+ }
584
+ apply(compiler) {
585
+ compiler.hooks.make.tapAsync("SwaggerCompilerPlugin", async (compilation, callback) => {
586
+ console.log("Build paused");
587
+ console.log("switching to swagger build");
588
+ const spec = new spec_builder_default(this.swaggerDefinition);
589
+ for await (const dir of this.sources) {
590
+ const files = await collect(dir, {
591
+ skip: [...this.ignore, "node_modules/**"]
592
+ });
593
+ if (this.verbose) {
594
+ console.log(`Found ${files.length} files in ${dir}`);
595
+ console.log(files);
596
+ }
597
+ files.forEach((file) => {
598
+ debug(`Parsing file ${file}`);
599
+ const parsedJsDocumentFile = parse_file_default(file, comments_to_open_api_default, this.verbose);
600
+ spec.addData(parsedJsDocumentFile.map((item) => item.spec));
601
+ const parsedSwaggerJsDocumentFile = parse_file_default(file, comments_to_open_api_default2, this.verbose);
602
+ spec.addData(parsedSwaggerJsDocumentFile.map((item) => item.spec));
603
+ });
604
+ }
605
+ try {
606
+ await SwaggerParser.validate(JSON.parse(JSON.stringify(spec)));
607
+ } catch (error) {
608
+ console.error(error.toJSON());
609
+ exit(1);
610
+ }
611
+ compilation.assets[this.assetsPath] = {
612
+ source() {
613
+ return JSON.stringify(spec, null, 2);
614
+ },
615
+ size() {
616
+ return Object.keys(spec).length;
617
+ }
618
+ };
619
+ console.log("switching back to normal build");
620
+ callback();
621
+ });
622
+ }
623
+ };
624
+ var swagger_compiler_plugin_default = SwaggerCompilerPlugin;
625
+ export {
626
+ spec_builder_default as SpecBuilder,
627
+ swagger_compiler_plugin_default as SwaggerCompilerPlugin,
628
+ comments_to_open_api_default as jsDocumentCommentsToOpenApi,
629
+ parse_file_default as parseFile,
630
+ comments_to_open_api_default2 as swaggerJsDocumentCommentsToOpenApi,
631
+ yaml_loc_default as yamlLoc
632
+ };
633
+ //# sourceMappingURL=index.mjs.map