@spikers/next-openapi-json-generator 2.0.5 → 2.1.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/LICENSE +21 -21
- package/README.md +211 -211
- package/dist/index.cjs +164 -85
- package/dist/index.js +164 -85
- package/package.json +15 -15
package/dist/index.cjs
CHANGED
|
@@ -35,12 +35,14 @@ __export(index_exports, {
|
|
|
35
35
|
module.exports = __toCommonJS(index_exports);
|
|
36
36
|
|
|
37
37
|
// src/core/generateOpenApiSpec.ts
|
|
38
|
-
var import_node_path4 = __toESM(require("path"), 1);
|
|
39
38
|
var import_package_metadata = __toESM(require("@omer-x/package-metadata"), 1);
|
|
39
|
+
var import_node_path4 = __toESM(require("path"), 1);
|
|
40
40
|
|
|
41
41
|
// src/utils/object.ts
|
|
42
42
|
function omit(object, ...keys) {
|
|
43
|
-
return Object.fromEntries(
|
|
43
|
+
return Object.fromEntries(
|
|
44
|
+
Object.entries(object).filter(([key]) => !keys.includes(key))
|
|
45
|
+
);
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
// src/core/clearUnusedSchemas.ts
|
|
@@ -53,17 +55,21 @@ function clearUnusedSchemas({
|
|
|
53
55
|
}) {
|
|
54
56
|
if (!components.schemas) return { paths, components };
|
|
55
57
|
const stringifiedPaths = JSON.stringify(paths);
|
|
56
|
-
const stringifiedSchemas = Object.fromEntries(
|
|
57
|
-
|
|
58
|
-
|
|
58
|
+
const stringifiedSchemas = Object.fromEntries(
|
|
59
|
+
Object.entries(components.schemas).map(([schemaName, schema]) => {
|
|
60
|
+
return [schemaName, JSON.stringify(schema)];
|
|
61
|
+
})
|
|
62
|
+
);
|
|
59
63
|
return {
|
|
60
64
|
paths,
|
|
61
65
|
components: {
|
|
62
66
|
...components,
|
|
63
|
-
schemas: Object.fromEntries(
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
schemas: Object.fromEntries(
|
|
68
|
+
Object.entries(components.schemas).filter(([schemaName]) => {
|
|
69
|
+
const otherSchemas = omit(stringifiedSchemas, schemaName);
|
|
70
|
+
return countReferences(schemaName, stringifiedPaths) > 0 || countReferences(schemaName, Object.values(otherSchemas).join("")) > 0;
|
|
71
|
+
})
|
|
72
|
+
)
|
|
67
73
|
}
|
|
68
74
|
};
|
|
69
75
|
}
|
|
@@ -71,8 +77,8 @@ function clearUnusedSchemas({
|
|
|
71
77
|
// src/core/dir.ts
|
|
72
78
|
var import_fs = require("fs");
|
|
73
79
|
var import_promises = __toESM(require("fs/promises"), 1);
|
|
74
|
-
var import_node_path = __toESM(require("path"), 1);
|
|
75
80
|
var import_minimatch = require("minimatch");
|
|
81
|
+
var import_node_path = __toESM(require("path"), 1);
|
|
76
82
|
async function directoryExists(dirPath) {
|
|
77
83
|
try {
|
|
78
84
|
await import_promises.default.access(dirPath, import_fs.constants.F_OK);
|
|
@@ -101,8 +107,12 @@ function filterDirectoryItems(rootPath, items, include, exclude) {
|
|
|
101
107
|
const excludedPatterns = exclude.map((pattern) => new import_minimatch.Minimatch(pattern));
|
|
102
108
|
return items.filter((item) => {
|
|
103
109
|
const relativePath = import_node_path.default.relative(rootPath, item);
|
|
104
|
-
const isIncluded = includedPatterns.some(
|
|
105
|
-
|
|
110
|
+
const isIncluded = includedPatterns.some(
|
|
111
|
+
(pattern) => pattern.match(relativePath)
|
|
112
|
+
);
|
|
113
|
+
const isExcluded = excludedPatterns.some(
|
|
114
|
+
(pattern) => pattern.match(relativePath)
|
|
115
|
+
);
|
|
106
116
|
return (isIncluded || !include.length) && !isExcluded;
|
|
107
117
|
});
|
|
108
118
|
}
|
|
@@ -123,9 +133,9 @@ async function isDocumentedRoute(routePath) {
|
|
|
123
133
|
}
|
|
124
134
|
|
|
125
135
|
// src/core/next.ts
|
|
136
|
+
var import_next_openapi_route_handler = require("@spikers/next-openapi-route-handler");
|
|
126
137
|
var import_promises3 = __toESM(require("fs/promises"), 1);
|
|
127
138
|
var import_node_path2 = __toESM(require("path"), 1);
|
|
128
|
-
var import_next_openapi_route_handler = require("@spikers/next-openapi-route-handler");
|
|
129
139
|
var import_zod = require("zod");
|
|
130
140
|
|
|
131
141
|
// src/utils/generateRandomString.ts
|
|
@@ -136,14 +146,17 @@ function generateRandomString(length) {
|
|
|
136
146
|
// src/utils/string-preservation.ts
|
|
137
147
|
function preserveStrings(code) {
|
|
138
148
|
let replacements = {};
|
|
139
|
-
const output = code.replace(
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
149
|
+
const output = code.replace(
|
|
150
|
+
/(['"`])((?:\\.|(?!\1).)*)\1/g,
|
|
151
|
+
(match, quote, content) => {
|
|
152
|
+
const replacementId = generateRandomString(32);
|
|
153
|
+
replacements = {
|
|
154
|
+
...replacements,
|
|
155
|
+
[replacementId]: `${quote}${content}${quote}`
|
|
156
|
+
};
|
|
157
|
+
return `<@~${replacementId}~@>`;
|
|
158
|
+
}
|
|
159
|
+
);
|
|
147
160
|
return { output, replacements };
|
|
148
161
|
}
|
|
149
162
|
function restoreStrings(code, replacements) {
|
|
@@ -155,7 +168,13 @@ function restoreStrings(code, replacements) {
|
|
|
155
168
|
// src/core/injectSchemas.ts
|
|
156
169
|
function injectSchemas(code, refName) {
|
|
157
170
|
const { output: preservedCode, replacements } = preserveStrings(code);
|
|
158
|
-
const preservedCodeWithSchemasInjected = preservedCode.replace(new RegExp(`\\b${refName}\\.`, "g"), `global.schemas[${refName}].`).replace(new RegExp(`\\b${refName}\\b`, "g"), `"${refName}"`).replace(
|
|
171
|
+
const preservedCodeWithSchemasInjected = preservedCode.replace(new RegExp(`\\b${refName}\\.`, "g"), `global.schemas[${refName}].`).replace(new RegExp(`\\b${refName}\\b`, "g"), `"${refName}"`).replace(
|
|
172
|
+
new RegExp(`queryParams:\\s*['"\`]${refName}['"\`]`, "g"),
|
|
173
|
+
`queryParams: global.schemas["${refName}"]`
|
|
174
|
+
).replace(
|
|
175
|
+
new RegExp(`pathParams:\\s*['"\`]${refName}['"\`]`, "g"),
|
|
176
|
+
`pathParams: global.schemas["${refName}"]`
|
|
177
|
+
);
|
|
159
178
|
return restoreStrings(preservedCodeWithSchemasInjected, replacements);
|
|
160
179
|
}
|
|
161
180
|
|
|
@@ -224,7 +243,9 @@ async function safeEval(code, routePath) {
|
|
|
224
243
|
fn(exports2, module2, require2);
|
|
225
244
|
return module2.exports;
|
|
226
245
|
} catch (error) {
|
|
227
|
-
console.log(
|
|
246
|
+
console.log(
|
|
247
|
+
`An error occured while evaluating the route exports from "${routePath}"`
|
|
248
|
+
);
|
|
228
249
|
throw error;
|
|
229
250
|
}
|
|
230
251
|
}
|
|
@@ -244,7 +265,12 @@ async function getModuleTranspiler() {
|
|
|
244
265
|
async function getRouteExports(routePath, routeDefinerName, schemas) {
|
|
245
266
|
const rawCode = await import_promises3.default.readFile(routePath, "utf-8");
|
|
246
267
|
const middlewareName = detectMiddlewareName(rawCode);
|
|
247
|
-
const code = transpile(
|
|
268
|
+
const code = transpile(
|
|
269
|
+
true,
|
|
270
|
+
rawCode,
|
|
271
|
+
middlewareName,
|
|
272
|
+
await getModuleTranspiler()
|
|
273
|
+
);
|
|
248
274
|
const fixedCode = Object.keys(schemas).reduce(injectSchemas, code);
|
|
249
275
|
global[routeDefinerName] = import_next_openapi_route_handler.defineRoute;
|
|
250
276
|
global.z = import_zod.z;
|
|
@@ -321,7 +347,9 @@ function fixSchema(schema) {
|
|
|
321
347
|
case "nonoptional":
|
|
322
348
|
return fixSchema(schema.unwrap());
|
|
323
349
|
default:
|
|
324
|
-
throw new Error(
|
|
350
|
+
throw new Error(
|
|
351
|
+
`${schema._zod.def.type} type is not covered in fixSchema (@omer-x/next-openapi-json-generator")`
|
|
352
|
+
);
|
|
325
353
|
}
|
|
326
354
|
}
|
|
327
355
|
if (schema._zod.def.type === "date") {
|
|
@@ -330,14 +358,19 @@ function fixSchema(schema) {
|
|
|
330
358
|
if (schema._zod.def.type === "object") {
|
|
331
359
|
const { shape } = schema;
|
|
332
360
|
const entries = Object.entries(shape);
|
|
333
|
-
const alteredEntries = entries.map(([propName, prop]) => [
|
|
361
|
+
const alteredEntries = entries.map(([propName, prop]) => [
|
|
362
|
+
propName,
|
|
363
|
+
fixSchema(prop)
|
|
364
|
+
]);
|
|
334
365
|
const newShape = Object.fromEntries(alteredEntries);
|
|
335
366
|
return import_zod2.z.object(newShape);
|
|
336
367
|
}
|
|
337
368
|
return schema;
|
|
338
369
|
}
|
|
339
370
|
function convertToOpenAPI(schema, isArray) {
|
|
340
|
-
return import_zod2.z.toJSONSchema(
|
|
371
|
+
return import_zod2.z.toJSONSchema(
|
|
372
|
+
fixSchema(isArray ? schema.array() : schema)
|
|
373
|
+
);
|
|
341
374
|
}
|
|
342
375
|
|
|
343
376
|
// src/core/mask.ts
|
|
@@ -368,16 +401,21 @@ function maskWithReference(schema, storedSchemas, self) {
|
|
|
368
401
|
case "object":
|
|
369
402
|
return {
|
|
370
403
|
...schema,
|
|
371
|
-
properties: Object.entries(schema.properties ?? {}).reduce(
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
404
|
+
properties: Object.entries(schema.properties ?? {}).reduce(
|
|
405
|
+
(props, [propName, prop]) => ({
|
|
406
|
+
...props,
|
|
407
|
+
[propName]: maskWithReference(prop, storedSchemas, true)
|
|
408
|
+
}),
|
|
409
|
+
{}
|
|
410
|
+
)
|
|
375
411
|
};
|
|
376
412
|
case "array":
|
|
377
413
|
if (Array.isArray(schema.items)) {
|
|
378
414
|
return {
|
|
379
415
|
...schema,
|
|
380
|
-
items: schema.items.map(
|
|
416
|
+
items: schema.items.map(
|
|
417
|
+
(i) => maskWithReference(i, storedSchemas, true)
|
|
418
|
+
)
|
|
381
419
|
};
|
|
382
420
|
}
|
|
383
421
|
return {
|
|
@@ -395,37 +433,54 @@ function maskSchema(storedSchemas, schema) {
|
|
|
395
433
|
}
|
|
396
434
|
function maskParameterSchema(param, storedSchemas) {
|
|
397
435
|
if ("$ref" in param) return param;
|
|
398
|
-
return {
|
|
436
|
+
return {
|
|
437
|
+
...param,
|
|
438
|
+
schema: maskSchema(storedSchemas, param.schema)
|
|
439
|
+
};
|
|
399
440
|
}
|
|
400
441
|
function maskContentSchema(storedSchemas, bodyContent) {
|
|
401
442
|
if (!bodyContent) return bodyContent;
|
|
402
|
-
return Object.entries(bodyContent).reduce(
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
443
|
+
return Object.entries(bodyContent).reduce(
|
|
444
|
+
(collection, [contentType, content]) => ({
|
|
445
|
+
...collection,
|
|
446
|
+
[contentType]: {
|
|
447
|
+
...content,
|
|
448
|
+
schema: maskSchema(storedSchemas, content.schema)
|
|
449
|
+
}
|
|
450
|
+
}),
|
|
451
|
+
{}
|
|
452
|
+
);
|
|
409
453
|
}
|
|
410
454
|
function maskRequestBodySchema(storedSchemas, body) {
|
|
411
455
|
if (!body || "$ref" in body) return body;
|
|
412
|
-
return {
|
|
456
|
+
return {
|
|
457
|
+
...body,
|
|
458
|
+
content: maskContentSchema(storedSchemas, body.content)
|
|
459
|
+
};
|
|
413
460
|
}
|
|
414
461
|
function maskResponseSchema(storedSchemas, response) {
|
|
415
462
|
if ("$ref" in response) return response;
|
|
416
|
-
return {
|
|
463
|
+
return {
|
|
464
|
+
...response,
|
|
465
|
+
content: maskContentSchema(storedSchemas, response.content)
|
|
466
|
+
};
|
|
417
467
|
}
|
|
418
468
|
function maskSchemasInResponses(storedSchemas, responses) {
|
|
419
469
|
if (!responses) return responses;
|
|
420
|
-
return Object.entries(responses).reduce(
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
470
|
+
return Object.entries(responses).reduce(
|
|
471
|
+
(collection, [key, response]) => ({
|
|
472
|
+
...collection,
|
|
473
|
+
[key]: maskResponseSchema(storedSchemas, response)
|
|
474
|
+
}),
|
|
475
|
+
{}
|
|
476
|
+
);
|
|
424
477
|
}
|
|
425
478
|
function maskOperationSchemas(operation, storedSchemas) {
|
|
426
479
|
return {
|
|
427
480
|
...operation,
|
|
428
|
-
parameters: operation.parameters?.map(
|
|
481
|
+
parameters: operation.parameters?.map(
|
|
482
|
+
(p) => maskParameterSchema(p, storedSchemas)
|
|
483
|
+
),
|
|
429
484
|
requestBody: maskRequestBodySchema(storedSchemas, operation.requestBody),
|
|
430
485
|
responses: maskSchemasInResponses(storedSchemas, operation.responses)
|
|
431
486
|
};
|
|
@@ -441,27 +496,36 @@ function createRouteRecord(method, filePath, rootPath, apiData) {
|
|
|
441
496
|
}
|
|
442
497
|
function bundlePaths(source, storedSchemas) {
|
|
443
498
|
source.sort((a, b) => a.path.localeCompare(b.path));
|
|
444
|
-
return source.reduce(
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
499
|
+
return source.reduce(
|
|
500
|
+
(collection, route) => ({
|
|
501
|
+
...collection,
|
|
502
|
+
[route.path]: {
|
|
503
|
+
...collection[route.path],
|
|
504
|
+
[route.method]: maskOperationSchemas(route.apiData, storedSchemas)
|
|
505
|
+
}
|
|
506
|
+
}),
|
|
507
|
+
{}
|
|
508
|
+
);
|
|
451
509
|
}
|
|
452
510
|
|
|
453
511
|
// src/core/schema.ts
|
|
454
512
|
function bundleSchemas(schemas) {
|
|
455
|
-
const bundledSchemas = Object.keys(schemas).reduce(
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
513
|
+
const bundledSchemas = Object.keys(schemas).reduce(
|
|
514
|
+
(collection, schemaName) => {
|
|
515
|
+
return {
|
|
516
|
+
...collection,
|
|
517
|
+
[schemaName]: convertToOpenAPI(schemas[schemaName], false)
|
|
518
|
+
};
|
|
519
|
+
},
|
|
520
|
+
{}
|
|
521
|
+
);
|
|
522
|
+
return Object.entries(bundledSchemas).reduce(
|
|
523
|
+
(bundle, [schemaName, schema]) => ({
|
|
524
|
+
...bundle,
|
|
525
|
+
[schemaName]: maskWithReference(schema, schemas, false)
|
|
526
|
+
}),
|
|
527
|
+
{}
|
|
528
|
+
);
|
|
465
529
|
}
|
|
466
530
|
|
|
467
531
|
// src/core/generateOpenApiSpec.ts
|
|
@@ -481,20 +545,33 @@ async function generateOpenApiSpec(schemas, {
|
|
|
481
545
|
if (!appFolderPath) throw new Error("This is not a Next.js application!");
|
|
482
546
|
const rootPath = additionalRootPath ? import_node_path4.default.resolve(appFolderPath, "./" + additionalRootPath) : appFolderPath;
|
|
483
547
|
const routes = await getDirectoryItems(rootPath, "route.ts");
|
|
484
|
-
const verifiedRoutes = filterDirectoryItems(
|
|
548
|
+
const verifiedRoutes = filterDirectoryItems(
|
|
549
|
+
rootPath,
|
|
550
|
+
routes,
|
|
551
|
+
verifiedOptions.include,
|
|
552
|
+
verifiedOptions.exclude
|
|
553
|
+
);
|
|
485
554
|
const validRoutes = [];
|
|
486
555
|
for (const route of verifiedRoutes) {
|
|
487
556
|
const isDocumented = await isDocumentedRoute(route);
|
|
488
557
|
if (!isDocumented) continue;
|
|
489
|
-
const exportedRouteHandlers = await getRouteExports(
|
|
490
|
-
|
|
558
|
+
const exportedRouteHandlers = await getRouteExports(
|
|
559
|
+
route,
|
|
560
|
+
routeDefinerName,
|
|
561
|
+
schemas
|
|
562
|
+
);
|
|
563
|
+
for (const [method, routeHandler] of Object.entries(
|
|
564
|
+
exportedRouteHandlers
|
|
565
|
+
)) {
|
|
491
566
|
if (!routeHandler || !routeHandler.apiData) continue;
|
|
492
|
-
validRoutes.push(
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
567
|
+
validRoutes.push(
|
|
568
|
+
createRouteRecord(
|
|
569
|
+
method.toLocaleLowerCase(),
|
|
570
|
+
route,
|
|
571
|
+
rootPath,
|
|
572
|
+
routeHandler.apiData
|
|
573
|
+
)
|
|
574
|
+
);
|
|
498
575
|
}
|
|
499
576
|
}
|
|
500
577
|
const metadata = (0, import_package_metadata.default)();
|
|
@@ -505,18 +582,20 @@ async function generateOpenApiSpec(schemas, {
|
|
|
505
582
|
securitySchemes
|
|
506
583
|
}
|
|
507
584
|
};
|
|
508
|
-
return JSON.parse(
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
585
|
+
return JSON.parse(
|
|
586
|
+
JSON.stringify({
|
|
587
|
+
openapi: "3.1.0",
|
|
588
|
+
info: {
|
|
589
|
+
title: metadata.serviceName,
|
|
590
|
+
version: metadata.version,
|
|
591
|
+
...info ?? {}
|
|
592
|
+
},
|
|
593
|
+
servers,
|
|
594
|
+
...clearUnusedSchemasOption ? clearUnusedSchemas(pathsAndComponents) : pathsAndComponents,
|
|
595
|
+
security,
|
|
596
|
+
tags: []
|
|
597
|
+
})
|
|
598
|
+
);
|
|
520
599
|
}
|
|
521
600
|
|
|
522
601
|
// src/index.ts
|