@spikers/next-openapi-json-generator 2.0.4 → 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 +165 -86
- package/dist/index.js +165 -86
- package/package.json +15 -15
package/dist/index.js
CHANGED
|
@@ -6,12 +6,14 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
|
|
|
6
6
|
});
|
|
7
7
|
|
|
8
8
|
// src/core/generateOpenApiSpec.ts
|
|
9
|
-
import path4 from "path";
|
|
10
9
|
import getPackageMetadata from "@omer-x/package-metadata";
|
|
10
|
+
import path4 from "path";
|
|
11
11
|
|
|
12
12
|
// src/utils/object.ts
|
|
13
13
|
function omit(object, ...keys) {
|
|
14
|
-
return Object.fromEntries(
|
|
14
|
+
return Object.fromEntries(
|
|
15
|
+
Object.entries(object).filter(([key]) => !keys.includes(key))
|
|
16
|
+
);
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
// src/core/clearUnusedSchemas.ts
|
|
@@ -24,17 +26,21 @@ function clearUnusedSchemas({
|
|
|
24
26
|
}) {
|
|
25
27
|
if (!components.schemas) return { paths, components };
|
|
26
28
|
const stringifiedPaths = JSON.stringify(paths);
|
|
27
|
-
const stringifiedSchemas = Object.fromEntries(
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
const stringifiedSchemas = Object.fromEntries(
|
|
30
|
+
Object.entries(components.schemas).map(([schemaName, schema]) => {
|
|
31
|
+
return [schemaName, JSON.stringify(schema)];
|
|
32
|
+
})
|
|
33
|
+
);
|
|
30
34
|
return {
|
|
31
35
|
paths,
|
|
32
36
|
components: {
|
|
33
37
|
...components,
|
|
34
|
-
schemas: Object.fromEntries(
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
+
schemas: Object.fromEntries(
|
|
39
|
+
Object.entries(components.schemas).filter(([schemaName]) => {
|
|
40
|
+
const otherSchemas = omit(stringifiedSchemas, schemaName);
|
|
41
|
+
return countReferences(schemaName, stringifiedPaths) > 0 || countReferences(schemaName, Object.values(otherSchemas).join("")) > 0;
|
|
42
|
+
})
|
|
43
|
+
)
|
|
38
44
|
}
|
|
39
45
|
};
|
|
40
46
|
}
|
|
@@ -42,8 +48,8 @@ function clearUnusedSchemas({
|
|
|
42
48
|
// src/core/dir.ts
|
|
43
49
|
import { constants } from "fs";
|
|
44
50
|
import fs from "fs/promises";
|
|
45
|
-
import path from "path";
|
|
46
51
|
import { Minimatch } from "minimatch";
|
|
52
|
+
import path from "path";
|
|
47
53
|
async function directoryExists(dirPath) {
|
|
48
54
|
try {
|
|
49
55
|
await fs.access(dirPath, constants.F_OK);
|
|
@@ -72,8 +78,12 @@ function filterDirectoryItems(rootPath, items, include, exclude) {
|
|
|
72
78
|
const excludedPatterns = exclude.map((pattern) => new Minimatch(pattern));
|
|
73
79
|
return items.filter((item) => {
|
|
74
80
|
const relativePath = path.relative(rootPath, item);
|
|
75
|
-
const isIncluded = includedPatterns.some(
|
|
76
|
-
|
|
81
|
+
const isIncluded = includedPatterns.some(
|
|
82
|
+
(pattern) => pattern.match(relativePath)
|
|
83
|
+
);
|
|
84
|
+
const isExcluded = excludedPatterns.some(
|
|
85
|
+
(pattern) => pattern.match(relativePath)
|
|
86
|
+
);
|
|
77
87
|
return (isIncluded || !include.length) && !isExcluded;
|
|
78
88
|
});
|
|
79
89
|
}
|
|
@@ -94,9 +104,9 @@ async function isDocumentedRoute(routePath) {
|
|
|
94
104
|
}
|
|
95
105
|
|
|
96
106
|
// src/core/next.ts
|
|
107
|
+
import { defineRoute } from "@spikers/next-openapi-route-handler";
|
|
97
108
|
import fs3 from "fs/promises";
|
|
98
109
|
import path2 from "path";
|
|
99
|
-
import { defineRoute } from "@spikers/next-openapi-route-handler";
|
|
100
110
|
import { z } from "zod";
|
|
101
111
|
|
|
102
112
|
// src/utils/generateRandomString.ts
|
|
@@ -107,14 +117,17 @@ function generateRandomString(length) {
|
|
|
107
117
|
// src/utils/string-preservation.ts
|
|
108
118
|
function preserveStrings(code) {
|
|
109
119
|
let replacements = {};
|
|
110
|
-
const output = code.replace(
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
120
|
+
const output = code.replace(
|
|
121
|
+
/(['"`])((?:\\.|(?!\1).)*)\1/g,
|
|
122
|
+
(match, quote, content) => {
|
|
123
|
+
const replacementId = generateRandomString(32);
|
|
124
|
+
replacements = {
|
|
125
|
+
...replacements,
|
|
126
|
+
[replacementId]: `${quote}${content}${quote}`
|
|
127
|
+
};
|
|
128
|
+
return `<@~${replacementId}~@>`;
|
|
129
|
+
}
|
|
130
|
+
);
|
|
118
131
|
return { output, replacements };
|
|
119
132
|
}
|
|
120
133
|
function restoreStrings(code, replacements) {
|
|
@@ -126,7 +139,13 @@ function restoreStrings(code, replacements) {
|
|
|
126
139
|
// src/core/injectSchemas.ts
|
|
127
140
|
function injectSchemas(code, refName) {
|
|
128
141
|
const { output: preservedCode, replacements } = preserveStrings(code);
|
|
129
|
-
const preservedCodeWithSchemasInjected = preservedCode.replace(new RegExp(`\\b${refName}\\.`, "g"), `global.schemas[${refName}].`).replace(new RegExp(`\\b${refName}\\b`, "g"), `"${refName}"`).replace(
|
|
142
|
+
const preservedCodeWithSchemasInjected = preservedCode.replace(new RegExp(`\\b${refName}\\.`, "g"), `global.schemas[${refName}].`).replace(new RegExp(`\\b${refName}\\b`, "g"), `"${refName}"`).replace(
|
|
143
|
+
new RegExp(`queryParams:\\s*['"\`]${refName}['"\`]`, "g"),
|
|
144
|
+
`queryParams: global.schemas["${refName}"]`
|
|
145
|
+
).replace(
|
|
146
|
+
new RegExp(`pathParams:\\s*['"\`]${refName}['"\`]`, "g"),
|
|
147
|
+
`pathParams: global.schemas["${refName}"]`
|
|
148
|
+
);
|
|
130
149
|
return restoreStrings(preservedCodeWithSchemasInjected, replacements);
|
|
131
150
|
}
|
|
132
151
|
|
|
@@ -195,7 +214,9 @@ async function safeEval(code, routePath) {
|
|
|
195
214
|
fn(exports2, module, require2);
|
|
196
215
|
return module.exports;
|
|
197
216
|
} catch (error) {
|
|
198
|
-
console.log(
|
|
217
|
+
console.log(
|
|
218
|
+
`An error occured while evaluating the route exports from "${routePath}"`
|
|
219
|
+
);
|
|
199
220
|
throw error;
|
|
200
221
|
}
|
|
201
222
|
}
|
|
@@ -215,7 +236,12 @@ async function getModuleTranspiler() {
|
|
|
215
236
|
async function getRouteExports(routePath, routeDefinerName, schemas) {
|
|
216
237
|
const rawCode = await fs3.readFile(routePath, "utf-8");
|
|
217
238
|
const middlewareName = detectMiddlewareName(rawCode);
|
|
218
|
-
const code = transpile(
|
|
239
|
+
const code = transpile(
|
|
240
|
+
true,
|
|
241
|
+
rawCode,
|
|
242
|
+
middlewareName,
|
|
243
|
+
await getModuleTranspiler()
|
|
244
|
+
);
|
|
219
245
|
const fixedCode = Object.keys(schemas).reduce(injectSchemas, code);
|
|
220
246
|
global[routeDefinerName] = defineRoute;
|
|
221
247
|
global.z = z;
|
|
@@ -251,7 +277,7 @@ function verifyOptions(include, exclude) {
|
|
|
251
277
|
import path3 from "path";
|
|
252
278
|
function getRoutePathName(filePath, rootPath) {
|
|
253
279
|
const dirName = path3.dirname(filePath);
|
|
254
|
-
return "/" + path3.relative(rootPath, dirName).replaceAll("[", "{").replaceAll("]", "}").replaceAll("\\", "/").replaceAll(/\([^)]*\)\//g, "
|
|
280
|
+
return "/" + path3.relative(rootPath, dirName).replaceAll("[", "{").replaceAll("]", "}").replaceAll("\\", "/").replaceAll(/\([^)]*\)\//g, "");
|
|
255
281
|
}
|
|
256
282
|
|
|
257
283
|
// src/utils/deepEqual.ts
|
|
@@ -292,7 +318,9 @@ function fixSchema(schema) {
|
|
|
292
318
|
case "nonoptional":
|
|
293
319
|
return fixSchema(schema.unwrap());
|
|
294
320
|
default:
|
|
295
|
-
throw new Error(
|
|
321
|
+
throw new Error(
|
|
322
|
+
`${schema._zod.def.type} type is not covered in fixSchema (@omer-x/next-openapi-json-generator")`
|
|
323
|
+
);
|
|
296
324
|
}
|
|
297
325
|
}
|
|
298
326
|
if (schema._zod.def.type === "date") {
|
|
@@ -301,14 +329,19 @@ function fixSchema(schema) {
|
|
|
301
329
|
if (schema._zod.def.type === "object") {
|
|
302
330
|
const { shape } = schema;
|
|
303
331
|
const entries = Object.entries(shape);
|
|
304
|
-
const alteredEntries = entries.map(([propName, prop]) => [
|
|
332
|
+
const alteredEntries = entries.map(([propName, prop]) => [
|
|
333
|
+
propName,
|
|
334
|
+
fixSchema(prop)
|
|
335
|
+
]);
|
|
305
336
|
const newShape = Object.fromEntries(alteredEntries);
|
|
306
337
|
return z2.object(newShape);
|
|
307
338
|
}
|
|
308
339
|
return schema;
|
|
309
340
|
}
|
|
310
341
|
function convertToOpenAPI(schema, isArray) {
|
|
311
|
-
return z2.toJSONSchema(
|
|
342
|
+
return z2.toJSONSchema(
|
|
343
|
+
fixSchema(isArray ? schema.array() : schema)
|
|
344
|
+
);
|
|
312
345
|
}
|
|
313
346
|
|
|
314
347
|
// src/core/mask.ts
|
|
@@ -339,16 +372,21 @@ function maskWithReference(schema, storedSchemas, self) {
|
|
|
339
372
|
case "object":
|
|
340
373
|
return {
|
|
341
374
|
...schema,
|
|
342
|
-
properties: Object.entries(schema.properties ?? {}).reduce(
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
375
|
+
properties: Object.entries(schema.properties ?? {}).reduce(
|
|
376
|
+
(props, [propName, prop]) => ({
|
|
377
|
+
...props,
|
|
378
|
+
[propName]: maskWithReference(prop, storedSchemas, true)
|
|
379
|
+
}),
|
|
380
|
+
{}
|
|
381
|
+
)
|
|
346
382
|
};
|
|
347
383
|
case "array":
|
|
348
384
|
if (Array.isArray(schema.items)) {
|
|
349
385
|
return {
|
|
350
386
|
...schema,
|
|
351
|
-
items: schema.items.map(
|
|
387
|
+
items: schema.items.map(
|
|
388
|
+
(i) => maskWithReference(i, storedSchemas, true)
|
|
389
|
+
)
|
|
352
390
|
};
|
|
353
391
|
}
|
|
354
392
|
return {
|
|
@@ -366,37 +404,54 @@ function maskSchema(storedSchemas, schema) {
|
|
|
366
404
|
}
|
|
367
405
|
function maskParameterSchema(param, storedSchemas) {
|
|
368
406
|
if ("$ref" in param) return param;
|
|
369
|
-
return {
|
|
407
|
+
return {
|
|
408
|
+
...param,
|
|
409
|
+
schema: maskSchema(storedSchemas, param.schema)
|
|
410
|
+
};
|
|
370
411
|
}
|
|
371
412
|
function maskContentSchema(storedSchemas, bodyContent) {
|
|
372
413
|
if (!bodyContent) return bodyContent;
|
|
373
|
-
return Object.entries(bodyContent).reduce(
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
414
|
+
return Object.entries(bodyContent).reduce(
|
|
415
|
+
(collection, [contentType, content]) => ({
|
|
416
|
+
...collection,
|
|
417
|
+
[contentType]: {
|
|
418
|
+
...content,
|
|
419
|
+
schema: maskSchema(storedSchemas, content.schema)
|
|
420
|
+
}
|
|
421
|
+
}),
|
|
422
|
+
{}
|
|
423
|
+
);
|
|
380
424
|
}
|
|
381
425
|
function maskRequestBodySchema(storedSchemas, body) {
|
|
382
426
|
if (!body || "$ref" in body) return body;
|
|
383
|
-
return {
|
|
427
|
+
return {
|
|
428
|
+
...body,
|
|
429
|
+
content: maskContentSchema(storedSchemas, body.content)
|
|
430
|
+
};
|
|
384
431
|
}
|
|
385
432
|
function maskResponseSchema(storedSchemas, response) {
|
|
386
433
|
if ("$ref" in response) return response;
|
|
387
|
-
return {
|
|
434
|
+
return {
|
|
435
|
+
...response,
|
|
436
|
+
content: maskContentSchema(storedSchemas, response.content)
|
|
437
|
+
};
|
|
388
438
|
}
|
|
389
439
|
function maskSchemasInResponses(storedSchemas, responses) {
|
|
390
440
|
if (!responses) return responses;
|
|
391
|
-
return Object.entries(responses).reduce(
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
441
|
+
return Object.entries(responses).reduce(
|
|
442
|
+
(collection, [key, response]) => ({
|
|
443
|
+
...collection,
|
|
444
|
+
[key]: maskResponseSchema(storedSchemas, response)
|
|
445
|
+
}),
|
|
446
|
+
{}
|
|
447
|
+
);
|
|
395
448
|
}
|
|
396
449
|
function maskOperationSchemas(operation, storedSchemas) {
|
|
397
450
|
return {
|
|
398
451
|
...operation,
|
|
399
|
-
parameters: operation.parameters?.map(
|
|
452
|
+
parameters: operation.parameters?.map(
|
|
453
|
+
(p) => maskParameterSchema(p, storedSchemas)
|
|
454
|
+
),
|
|
400
455
|
requestBody: maskRequestBodySchema(storedSchemas, operation.requestBody),
|
|
401
456
|
responses: maskSchemasInResponses(storedSchemas, operation.responses)
|
|
402
457
|
};
|
|
@@ -412,27 +467,36 @@ function createRouteRecord(method, filePath, rootPath, apiData) {
|
|
|
412
467
|
}
|
|
413
468
|
function bundlePaths(source, storedSchemas) {
|
|
414
469
|
source.sort((a, b) => a.path.localeCompare(b.path));
|
|
415
|
-
return source.reduce(
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
470
|
+
return source.reduce(
|
|
471
|
+
(collection, route) => ({
|
|
472
|
+
...collection,
|
|
473
|
+
[route.path]: {
|
|
474
|
+
...collection[route.path],
|
|
475
|
+
[route.method]: maskOperationSchemas(route.apiData, storedSchemas)
|
|
476
|
+
}
|
|
477
|
+
}),
|
|
478
|
+
{}
|
|
479
|
+
);
|
|
422
480
|
}
|
|
423
481
|
|
|
424
482
|
// src/core/schema.ts
|
|
425
483
|
function bundleSchemas(schemas) {
|
|
426
|
-
const bundledSchemas = Object.keys(schemas).reduce(
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
484
|
+
const bundledSchemas = Object.keys(schemas).reduce(
|
|
485
|
+
(collection, schemaName) => {
|
|
486
|
+
return {
|
|
487
|
+
...collection,
|
|
488
|
+
[schemaName]: convertToOpenAPI(schemas[schemaName], false)
|
|
489
|
+
};
|
|
490
|
+
},
|
|
491
|
+
{}
|
|
492
|
+
);
|
|
493
|
+
return Object.entries(bundledSchemas).reduce(
|
|
494
|
+
(bundle, [schemaName, schema]) => ({
|
|
495
|
+
...bundle,
|
|
496
|
+
[schemaName]: maskWithReference(schema, schemas, false)
|
|
497
|
+
}),
|
|
498
|
+
{}
|
|
499
|
+
);
|
|
436
500
|
}
|
|
437
501
|
|
|
438
502
|
// src/core/generateOpenApiSpec.ts
|
|
@@ -452,20 +516,33 @@ async function generateOpenApiSpec(schemas, {
|
|
|
452
516
|
if (!appFolderPath) throw new Error("This is not a Next.js application!");
|
|
453
517
|
const rootPath = additionalRootPath ? path4.resolve(appFolderPath, "./" + additionalRootPath) : appFolderPath;
|
|
454
518
|
const routes = await getDirectoryItems(rootPath, "route.ts");
|
|
455
|
-
const verifiedRoutes = filterDirectoryItems(
|
|
519
|
+
const verifiedRoutes = filterDirectoryItems(
|
|
520
|
+
rootPath,
|
|
521
|
+
routes,
|
|
522
|
+
verifiedOptions.include,
|
|
523
|
+
verifiedOptions.exclude
|
|
524
|
+
);
|
|
456
525
|
const validRoutes = [];
|
|
457
526
|
for (const route of verifiedRoutes) {
|
|
458
527
|
const isDocumented = await isDocumentedRoute(route);
|
|
459
528
|
if (!isDocumented) continue;
|
|
460
|
-
const exportedRouteHandlers = await getRouteExports(
|
|
461
|
-
|
|
529
|
+
const exportedRouteHandlers = await getRouteExports(
|
|
530
|
+
route,
|
|
531
|
+
routeDefinerName,
|
|
532
|
+
schemas
|
|
533
|
+
);
|
|
534
|
+
for (const [method, routeHandler] of Object.entries(
|
|
535
|
+
exportedRouteHandlers
|
|
536
|
+
)) {
|
|
462
537
|
if (!routeHandler || !routeHandler.apiData) continue;
|
|
463
|
-
validRoutes.push(
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
538
|
+
validRoutes.push(
|
|
539
|
+
createRouteRecord(
|
|
540
|
+
method.toLocaleLowerCase(),
|
|
541
|
+
route,
|
|
542
|
+
rootPath,
|
|
543
|
+
routeHandler.apiData
|
|
544
|
+
)
|
|
545
|
+
);
|
|
469
546
|
}
|
|
470
547
|
}
|
|
471
548
|
const metadata = getPackageMetadata();
|
|
@@ -476,18 +553,20 @@ async function generateOpenApiSpec(schemas, {
|
|
|
476
553
|
securitySchemes
|
|
477
554
|
}
|
|
478
555
|
};
|
|
479
|
-
return JSON.parse(
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
556
|
+
return JSON.parse(
|
|
557
|
+
JSON.stringify({
|
|
558
|
+
openapi: "3.1.0",
|
|
559
|
+
info: {
|
|
560
|
+
title: metadata.serviceName,
|
|
561
|
+
version: metadata.version,
|
|
562
|
+
...info ?? {}
|
|
563
|
+
},
|
|
564
|
+
servers,
|
|
565
|
+
...clearUnusedSchemasOption ? clearUnusedSchemas(pathsAndComponents) : pathsAndComponents,
|
|
566
|
+
security,
|
|
567
|
+
tags: []
|
|
568
|
+
})
|
|
569
|
+
);
|
|
491
570
|
}
|
|
492
571
|
|
|
493
572
|
// src/index.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spikers/next-openapi-json-generator",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "a Next.js plugin to generate OpenAPI documentation from route handlers",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"next.js",
|
|
@@ -35,32 +35,32 @@
|
|
|
35
35
|
}
|
|
36
36
|
},
|
|
37
37
|
"scripts": {
|
|
38
|
-
"lint": "
|
|
39
|
-
"lint:fix": "
|
|
38
|
+
"lint": "prettier --check './src/**/*.{js,jsx,mjs,cjs,ts,tsx,json}' --plugin=prettier-plugin-organize-imports",
|
|
39
|
+
"lint:fix": "prettier --write './src/**/*.{js,jsx,mjs,cjs,ts,tsx,json}' --plugin=prettier-plugin-organize-imports",
|
|
40
40
|
"test": "vitest run --coverage",
|
|
41
41
|
"test:watch": "vitest --coverage",
|
|
42
42
|
"dev": "tsup --watch",
|
|
43
43
|
"build": "tsup"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
|
-
"@omer-x/json-schema-types": "^1",
|
|
47
|
-
"@spikers/next-openapi-route-handler": "^2",
|
|
48
|
-
"@omer-x/openapi-types": "^1",
|
|
49
|
-
"typescript": "^5",
|
|
50
|
-
"zod": "^4"
|
|
46
|
+
"@omer-x/json-schema-types": "^1.0.4",
|
|
47
|
+
"@spikers/next-openapi-route-handler": "^2.0.1",
|
|
48
|
+
"@omer-x/openapi-types": "^1.3.0",
|
|
49
|
+
"typescript": "^5.9.3",
|
|
50
|
+
"zod": "^4.2.1"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
53
|
"@omer-x/openapi-optimizer": "alpha",
|
|
54
54
|
"@omer-x/package-metadata": "^1.0.2",
|
|
55
|
-
"minimatch": "^10.
|
|
55
|
+
"minimatch": "^10.2.2"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
|
-
"@
|
|
59
|
-
"@
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"semantic-release": "^25.0.
|
|
58
|
+
"@types/node": "^25.3.0",
|
|
59
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
60
|
+
"prettier": "^3.8.1",
|
|
61
|
+
"prettier-plugin-organize-imports": "^4.3.0",
|
|
62
|
+
"semantic-release": "^25.0.3",
|
|
63
63
|
"tsup": "^8.5.1",
|
|
64
|
-
"vitest": "^4.0.
|
|
64
|
+
"vitest": "^4.0.18"
|
|
65
65
|
}
|
|
66
66
|
}
|