@omer-x/next-openapi-json-generator 2.0.3 → 2.1.0-rc.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.
Files changed (3) hide show
  1. package/dist/index.cjs +36 -147
  2. package/dist/index.js +36 -147
  3. package/package.json +9 -6
package/dist/index.cjs CHANGED
@@ -36,6 +36,7 @@ module.exports = __toCommonJS(index_exports);
36
36
 
37
37
  // src/core/generateOpenApiSpec.ts
38
38
  var import_node_path4 = __toESM(require("path"), 1);
39
+ var import_openapi_optimizer = require("@omer-x/openapi-optimizer");
39
40
  var import_package_metadata = __toESM(require("@omer-x/package-metadata"), 1);
40
41
 
41
42
  // src/utils/object.ts
@@ -281,54 +282,57 @@ function getRoutePathName(filePath, rootPath) {
281
282
  return "/" + import_node_path3.default.relative(rootPath, dirName).replaceAll("[", "{").replaceAll("]", "}").replaceAll("\\", "/");
282
283
  }
283
284
 
284
- // src/utils/deepEqual.ts
285
- function deepEqual(a, b) {
286
- if (typeof a !== typeof b) return false;
287
- switch (typeof a) {
288
- case "object": {
289
- if (!a || !b) return a === b;
290
- if (Array.isArray(a) && Array.isArray(b)) {
291
- return a.every((item, index) => deepEqual(item, b[index]));
292
- }
293
- if (Object.keys(a).length !== Object.keys(b).length) return false;
294
- return Object.entries(a).every(([key, value]) => {
295
- return deepEqual(value, b[key]);
296
- });
285
+ // src/core/route.ts
286
+ function createRouteRecord(method, filePath, rootPath, apiData) {
287
+ return {
288
+ method: method.toLocaleLowerCase(),
289
+ path: getRoutePathName(filePath, rootPath),
290
+ apiData
291
+ };
292
+ }
293
+ function bundlePaths(source) {
294
+ source.sort((a, b) => a.path.localeCompare(b.path));
295
+ return source.reduce((collection, route) => ({
296
+ ...collection,
297
+ [route.path]: {
298
+ ...collection[route.path],
299
+ [route.method]: route.apiData
297
300
  }
298
- case "function":
299
- case "symbol":
300
- return false;
301
- default:
302
- return a === b;
303
- }
301
+ }), {});
304
302
  }
305
303
 
306
304
  // src/core/zod-to-openapi.ts
307
305
  var import_zod2 = require("zod");
306
+ function alterSchema(schema, newShape) {
307
+ if (schema.description) {
308
+ newShape = newShape.describe(schema.description);
309
+ }
310
+ return newShape;
311
+ }
308
312
  function fixSchema(schema) {
309
313
  if ("unwrap" in schema && typeof schema.unwrap === "function") {
310
314
  switch (schema._zod.def.type) {
311
315
  case "nullable":
312
- return fixSchema(schema.unwrap()).nullable();
316
+ return alterSchema(schema, fixSchema(schema.unwrap()).nullable());
313
317
  case "optional":
314
- return fixSchema(schema.unwrap()).optional();
318
+ return alterSchema(schema, fixSchema(schema.unwrap()).optional());
315
319
  case "readonly":
316
- return fixSchema(schema.unwrap()).readonly();
320
+ return alterSchema(schema, fixSchema(schema.unwrap()).readonly());
317
321
  case "array":
318
- return fixSchema(schema.unwrap()).array();
322
+ return alterSchema(schema, fixSchema(schema.unwrap()).array());
319
323
  default:
320
324
  throw new Error(`${schema._zod.def.type} type is not covered in fixSchema (@omer-x/next-openapi-json-generator")`);
321
325
  }
322
326
  }
323
327
  if (schema._zod.def.type === "date") {
324
- return import_zod2.z.iso.datetime();
328
+ return alterSchema(schema, import_zod2.z.iso.datetime());
325
329
  }
326
330
  if (schema._zod.def.type === "object") {
327
331
  const { shape } = schema;
328
332
  const entries = Object.entries(shape);
329
333
  const alteredEntries = entries.map(([propName, prop]) => [propName, fixSchema(prop)]);
330
334
  const newShape = Object.fromEntries(alteredEntries);
331
- return import_zod2.z.object(newShape);
335
+ return alterSchema(schema, import_zod2.z.object(newShape));
332
336
  }
333
337
  return schema;
334
338
  }
@@ -336,128 +340,12 @@ function convertToOpenAPI(schema, isArray) {
336
340
  return import_zod2.z.toJSONSchema(fixSchema(isArray ? schema.array() : schema));
337
341
  }
338
342
 
339
- // src/core/mask.ts
340
- function maskWithReference(schema, storedSchemas, self) {
341
- if (self) {
342
- for (const [schemaName, zodSchema] of Object.entries(storedSchemas)) {
343
- if (deepEqual(schema, convertToOpenAPI(zodSchema, false))) {
344
- return {
345
- $ref: `#/components/schemas/${schemaName}`
346
- };
347
- }
348
- }
349
- }
350
- if ("$ref" in schema) return schema;
351
- if (schema.oneOf) {
352
- return {
353
- ...schema,
354
- oneOf: schema.oneOf.map((i) => maskWithReference(i, storedSchemas, true))
355
- };
356
- }
357
- if (schema.anyOf) {
358
- return {
359
- ...schema,
360
- anyOf: schema.anyOf.map((i) => maskWithReference(i, storedSchemas, true))
361
- };
362
- }
363
- switch (schema.type) {
364
- case "object":
365
- return {
366
- ...schema,
367
- properties: Object.entries(schema.properties ?? {}).reduce((props, [propName, prop]) => ({
368
- ...props,
369
- [propName]: maskWithReference(prop, storedSchemas, true)
370
- }), {})
371
- };
372
- case "array":
373
- if (Array.isArray(schema.items)) {
374
- return {
375
- ...schema,
376
- items: schema.items.map((i) => maskWithReference(i, storedSchemas, true))
377
- };
378
- }
379
- return {
380
- ...schema,
381
- items: maskWithReference(schema.items, storedSchemas, true)
382
- };
383
- }
384
- return schema;
385
- }
386
-
387
- // src/core/operation-mask.ts
388
- function maskSchema(storedSchemas, schema) {
389
- if (!schema) return schema;
390
- return maskWithReference(schema, storedSchemas, true);
391
- }
392
- function maskParameterSchema(param, storedSchemas) {
393
- if ("$ref" in param) return param;
394
- return { ...param, schema: maskSchema(storedSchemas, param.schema) };
395
- }
396
- function maskContentSchema(storedSchemas, bodyContent) {
397
- if (!bodyContent) return bodyContent;
398
- return Object.entries(bodyContent).reduce((collection, [contentType, content]) => ({
399
- ...collection,
400
- [contentType]: {
401
- ...content,
402
- schema: maskSchema(storedSchemas, content.schema)
403
- }
404
- }), {});
405
- }
406
- function maskRequestBodySchema(storedSchemas, body) {
407
- if (!body || "$ref" in body) return body;
408
- return { ...body, content: maskContentSchema(storedSchemas, body.content) };
409
- }
410
- function maskResponseSchema(storedSchemas, response) {
411
- if ("$ref" in response) return response;
412
- return { ...response, content: maskContentSchema(storedSchemas, response.content) };
413
- }
414
- function maskSchemasInResponses(storedSchemas, responses) {
415
- if (!responses) return responses;
416
- return Object.entries(responses).reduce((collection, [key, response]) => ({
417
- ...collection,
418
- [key]: maskResponseSchema(storedSchemas, response)
419
- }), {});
420
- }
421
- function maskOperationSchemas(operation, storedSchemas) {
422
- return {
423
- ...operation,
424
- parameters: operation.parameters?.map((p) => maskParameterSchema(p, storedSchemas)),
425
- requestBody: maskRequestBodySchema(storedSchemas, operation.requestBody),
426
- responses: maskSchemasInResponses(storedSchemas, operation.responses)
427
- };
428
- }
429
-
430
- // src/core/route.ts
431
- function createRouteRecord(method, filePath, rootPath, apiData) {
432
- return {
433
- method: method.toLocaleLowerCase(),
434
- path: getRoutePathName(filePath, rootPath),
435
- apiData
436
- };
437
- }
438
- function bundlePaths(source, storedSchemas) {
439
- source.sort((a, b) => a.path.localeCompare(b.path));
440
- return source.reduce((collection, route) => ({
441
- ...collection,
442
- [route.path]: {
443
- ...collection[route.path],
444
- [route.method]: maskOperationSchemas(route.apiData, storedSchemas)
445
- }
446
- }), {});
447
- }
448
-
449
343
  // src/core/schema.ts
450
344
  function bundleSchemas(schemas) {
451
- const bundledSchemas = Object.keys(schemas).reduce((collection, schemaName) => {
452
- return {
453
- ...collection,
454
- [schemaName]: convertToOpenAPI(schemas[schemaName], false)
455
- };
456
- }, {});
457
- return Object.entries(bundledSchemas).reduce((bundle, [schemaName, schema]) => ({
458
- ...bundle,
459
- [schemaName]: maskWithReference(schema, schemas, false)
460
- }), {});
345
+ const entries = Object.entries(schemas).map(([schemaName, schema]) => {
346
+ return [schemaName, convertToOpenAPI(schema, false)];
347
+ });
348
+ return Object.fromEntries(entries);
461
349
  }
462
350
 
463
351
  // src/core/generateOpenApiSpec.ts
@@ -495,13 +383,13 @@ async function generateOpenApiSpec(schemas, {
495
383
  }
496
384
  const metadata = (0, import_package_metadata.default)();
497
385
  const pathsAndComponents = {
498
- paths: bundlePaths(validRoutes, schemas),
386
+ paths: bundlePaths(validRoutes),
499
387
  components: {
500
388
  schemas: bundleSchemas(schemas),
501
389
  securitySchemes
502
390
  }
503
391
  };
504
- return JSON.parse(JSON.stringify({
392
+ const spec = JSON.parse(JSON.stringify({
505
393
  openapi: "3.1.0",
506
394
  info: {
507
395
  title: metadata.serviceName,
@@ -513,6 +401,7 @@ async function generateOpenApiSpec(schemas, {
513
401
  security,
514
402
  tags: []
515
403
  }));
404
+ return (0, import_openapi_optimizer.optimizeOpenApiSpec)(spec);
516
405
  }
517
406
 
518
407
  // src/index.ts
package/dist/index.js CHANGED
@@ -7,6 +7,7 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
7
7
 
8
8
  // src/core/generateOpenApiSpec.ts
9
9
  import path4 from "path";
10
+ import { optimizeOpenApiSpec } from "@omer-x/openapi-optimizer";
10
11
  import getPackageMetadata from "@omer-x/package-metadata";
11
12
 
12
13
  // src/utils/object.ts
@@ -252,54 +253,57 @@ function getRoutePathName(filePath, rootPath) {
252
253
  return "/" + path3.relative(rootPath, dirName).replaceAll("[", "{").replaceAll("]", "}").replaceAll("\\", "/");
253
254
  }
254
255
 
255
- // src/utils/deepEqual.ts
256
- function deepEqual(a, b) {
257
- if (typeof a !== typeof b) return false;
258
- switch (typeof a) {
259
- case "object": {
260
- if (!a || !b) return a === b;
261
- if (Array.isArray(a) && Array.isArray(b)) {
262
- return a.every((item, index) => deepEqual(item, b[index]));
263
- }
264
- if (Object.keys(a).length !== Object.keys(b).length) return false;
265
- return Object.entries(a).every(([key, value]) => {
266
- return deepEqual(value, b[key]);
267
- });
256
+ // src/core/route.ts
257
+ function createRouteRecord(method, filePath, rootPath, apiData) {
258
+ return {
259
+ method: method.toLocaleLowerCase(),
260
+ path: getRoutePathName(filePath, rootPath),
261
+ apiData
262
+ };
263
+ }
264
+ function bundlePaths(source) {
265
+ source.sort((a, b) => a.path.localeCompare(b.path));
266
+ return source.reduce((collection, route) => ({
267
+ ...collection,
268
+ [route.path]: {
269
+ ...collection[route.path],
270
+ [route.method]: route.apiData
268
271
  }
269
- case "function":
270
- case "symbol":
271
- return false;
272
- default:
273
- return a === b;
274
- }
272
+ }), {});
275
273
  }
276
274
 
277
275
  // src/core/zod-to-openapi.ts
278
276
  import { z as z2 } from "zod";
277
+ function alterSchema(schema, newShape) {
278
+ if (schema.description) {
279
+ newShape = newShape.describe(schema.description);
280
+ }
281
+ return newShape;
282
+ }
279
283
  function fixSchema(schema) {
280
284
  if ("unwrap" in schema && typeof schema.unwrap === "function") {
281
285
  switch (schema._zod.def.type) {
282
286
  case "nullable":
283
- return fixSchema(schema.unwrap()).nullable();
287
+ return alterSchema(schema, fixSchema(schema.unwrap()).nullable());
284
288
  case "optional":
285
- return fixSchema(schema.unwrap()).optional();
289
+ return alterSchema(schema, fixSchema(schema.unwrap()).optional());
286
290
  case "readonly":
287
- return fixSchema(schema.unwrap()).readonly();
291
+ return alterSchema(schema, fixSchema(schema.unwrap()).readonly());
288
292
  case "array":
289
- return fixSchema(schema.unwrap()).array();
293
+ return alterSchema(schema, fixSchema(schema.unwrap()).array());
290
294
  default:
291
295
  throw new Error(`${schema._zod.def.type} type is not covered in fixSchema (@omer-x/next-openapi-json-generator")`);
292
296
  }
293
297
  }
294
298
  if (schema._zod.def.type === "date") {
295
- return z2.iso.datetime();
299
+ return alterSchema(schema, z2.iso.datetime());
296
300
  }
297
301
  if (schema._zod.def.type === "object") {
298
302
  const { shape } = schema;
299
303
  const entries = Object.entries(shape);
300
304
  const alteredEntries = entries.map(([propName, prop]) => [propName, fixSchema(prop)]);
301
305
  const newShape = Object.fromEntries(alteredEntries);
302
- return z2.object(newShape);
306
+ return alterSchema(schema, z2.object(newShape));
303
307
  }
304
308
  return schema;
305
309
  }
@@ -307,128 +311,12 @@ function convertToOpenAPI(schema, isArray) {
307
311
  return z2.toJSONSchema(fixSchema(isArray ? schema.array() : schema));
308
312
  }
309
313
 
310
- // src/core/mask.ts
311
- function maskWithReference(schema, storedSchemas, self) {
312
- if (self) {
313
- for (const [schemaName, zodSchema] of Object.entries(storedSchemas)) {
314
- if (deepEqual(schema, convertToOpenAPI(zodSchema, false))) {
315
- return {
316
- $ref: `#/components/schemas/${schemaName}`
317
- };
318
- }
319
- }
320
- }
321
- if ("$ref" in schema) return schema;
322
- if (schema.oneOf) {
323
- return {
324
- ...schema,
325
- oneOf: schema.oneOf.map((i) => maskWithReference(i, storedSchemas, true))
326
- };
327
- }
328
- if (schema.anyOf) {
329
- return {
330
- ...schema,
331
- anyOf: schema.anyOf.map((i) => maskWithReference(i, storedSchemas, true))
332
- };
333
- }
334
- switch (schema.type) {
335
- case "object":
336
- return {
337
- ...schema,
338
- properties: Object.entries(schema.properties ?? {}).reduce((props, [propName, prop]) => ({
339
- ...props,
340
- [propName]: maskWithReference(prop, storedSchemas, true)
341
- }), {})
342
- };
343
- case "array":
344
- if (Array.isArray(schema.items)) {
345
- return {
346
- ...schema,
347
- items: schema.items.map((i) => maskWithReference(i, storedSchemas, true))
348
- };
349
- }
350
- return {
351
- ...schema,
352
- items: maskWithReference(schema.items, storedSchemas, true)
353
- };
354
- }
355
- return schema;
356
- }
357
-
358
- // src/core/operation-mask.ts
359
- function maskSchema(storedSchemas, schema) {
360
- if (!schema) return schema;
361
- return maskWithReference(schema, storedSchemas, true);
362
- }
363
- function maskParameterSchema(param, storedSchemas) {
364
- if ("$ref" in param) return param;
365
- return { ...param, schema: maskSchema(storedSchemas, param.schema) };
366
- }
367
- function maskContentSchema(storedSchemas, bodyContent) {
368
- if (!bodyContent) return bodyContent;
369
- return Object.entries(bodyContent).reduce((collection, [contentType, content]) => ({
370
- ...collection,
371
- [contentType]: {
372
- ...content,
373
- schema: maskSchema(storedSchemas, content.schema)
374
- }
375
- }), {});
376
- }
377
- function maskRequestBodySchema(storedSchemas, body) {
378
- if (!body || "$ref" in body) return body;
379
- return { ...body, content: maskContentSchema(storedSchemas, body.content) };
380
- }
381
- function maskResponseSchema(storedSchemas, response) {
382
- if ("$ref" in response) return response;
383
- return { ...response, content: maskContentSchema(storedSchemas, response.content) };
384
- }
385
- function maskSchemasInResponses(storedSchemas, responses) {
386
- if (!responses) return responses;
387
- return Object.entries(responses).reduce((collection, [key, response]) => ({
388
- ...collection,
389
- [key]: maskResponseSchema(storedSchemas, response)
390
- }), {});
391
- }
392
- function maskOperationSchemas(operation, storedSchemas) {
393
- return {
394
- ...operation,
395
- parameters: operation.parameters?.map((p) => maskParameterSchema(p, storedSchemas)),
396
- requestBody: maskRequestBodySchema(storedSchemas, operation.requestBody),
397
- responses: maskSchemasInResponses(storedSchemas, operation.responses)
398
- };
399
- }
400
-
401
- // src/core/route.ts
402
- function createRouteRecord(method, filePath, rootPath, apiData) {
403
- return {
404
- method: method.toLocaleLowerCase(),
405
- path: getRoutePathName(filePath, rootPath),
406
- apiData
407
- };
408
- }
409
- function bundlePaths(source, storedSchemas) {
410
- source.sort((a, b) => a.path.localeCompare(b.path));
411
- return source.reduce((collection, route) => ({
412
- ...collection,
413
- [route.path]: {
414
- ...collection[route.path],
415
- [route.method]: maskOperationSchemas(route.apiData, storedSchemas)
416
- }
417
- }), {});
418
- }
419
-
420
314
  // src/core/schema.ts
421
315
  function bundleSchemas(schemas) {
422
- const bundledSchemas = Object.keys(schemas).reduce((collection, schemaName) => {
423
- return {
424
- ...collection,
425
- [schemaName]: convertToOpenAPI(schemas[schemaName], false)
426
- };
427
- }, {});
428
- return Object.entries(bundledSchemas).reduce((bundle, [schemaName, schema]) => ({
429
- ...bundle,
430
- [schemaName]: maskWithReference(schema, schemas, false)
431
- }), {});
316
+ const entries = Object.entries(schemas).map(([schemaName, schema]) => {
317
+ return [schemaName, convertToOpenAPI(schema, false)];
318
+ });
319
+ return Object.fromEntries(entries);
432
320
  }
433
321
 
434
322
  // src/core/generateOpenApiSpec.ts
@@ -466,13 +354,13 @@ async function generateOpenApiSpec(schemas, {
466
354
  }
467
355
  const metadata = getPackageMetadata();
468
356
  const pathsAndComponents = {
469
- paths: bundlePaths(validRoutes, schemas),
357
+ paths: bundlePaths(validRoutes),
470
358
  components: {
471
359
  schemas: bundleSchemas(schemas),
472
360
  securitySchemes
473
361
  }
474
362
  };
475
- return JSON.parse(JSON.stringify({
363
+ const spec = JSON.parse(JSON.stringify({
476
364
  openapi: "3.1.0",
477
365
  info: {
478
366
  title: metadata.serviceName,
@@ -484,6 +372,7 @@ async function generateOpenApiSpec(schemas, {
484
372
  security,
485
373
  tags: []
486
374
  }));
375
+ return optimizeOpenApiSpec(spec);
487
376
  }
488
377
 
489
378
  // src/index.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omer-x/next-openapi-json-generator",
3
- "version": "2.0.3",
3
+ "version": "2.1.0-rc.2",
4
4
  "description": "a Next.js plugin to generate OpenAPI documentation from route handlers",
5
5
  "keywords": [
6
6
  "next.js",
@@ -40,6 +40,8 @@
40
40
  }
41
41
  },
42
42
  "scripts": {
43
+ "lint": "eslint --flag unstable_native_nodejs_ts_config",
44
+ "lint:fix": "eslint --fix --flag unstable_native_nodejs_ts_config",
43
45
  "test": "vitest run --coverage",
44
46
  "test:watch": "vitest --coverage",
45
47
  "dev": "tsup --watch",
@@ -48,20 +50,21 @@
48
50
  "peerDependencies": {
49
51
  "@omer-x/next-openapi-route-handler": "^2",
50
52
  "@omer-x/openapi-types": "^1",
51
- "zod": "^4",
52
- "typescript": "^5"
53
+ "typescript": "^5",
54
+ "zod": "^4"
53
55
  },
54
56
  "dependencies": {
57
+ "@omer-x/openapi-optimizer": "alpha",
55
58
  "@omer-x/package-metadata": "^1.0.2",
56
59
  "minimatch": "^10.1.1"
57
60
  },
58
61
  "devDependencies": {
59
62
  "@omer-x/eslint-config": "^2.2.6",
60
- "@types/node": "^25.0.1",
61
- "@vitest/coverage-v8": "^4.0.15",
63
+ "@types/node": "^25.0.3",
64
+ "@vitest/coverage-v8": "^4.0.16",
62
65
  "eslint": "^9.39.2",
63
66
  "semantic-release": "^25.0.2",
64
67
  "tsup": "^8.5.1",
65
- "vitest": "^4.0.15"
68
+ "vitest": "^4.0.16"
66
69
  }
67
70
  }