@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.
Files changed (5) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +211 -211
  3. package/dist/index.cjs +164 -85
  4. package/dist/index.js +164 -85
  5. 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(Object.entries(object).filter(([key]) => !keys.includes(key)));
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(Object.entries(components.schemas).map(([schemaName, schema]) => {
28
- return [schemaName, JSON.stringify(schema)];
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(Object.entries(components.schemas).filter(([schemaName]) => {
35
- const otherSchemas = omit(stringifiedSchemas, schemaName);
36
- return countReferences(schemaName, stringifiedPaths) > 0 || countReferences(schemaName, Object.values(otherSchemas).join("")) > 0;
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((pattern) => pattern.match(relativePath));
76
- const isExcluded = excludedPatterns.some((pattern) => pattern.match(relativePath));
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(/(['"`])((?:\\.|(?!\1).)*)\1/g, (match, quote, content) => {
111
- const replacementId = generateRandomString(32);
112
- replacements = {
113
- ...replacements,
114
- [replacementId]: `${quote}${content}${quote}`
115
- };
116
- return `<@~${replacementId}~@>`;
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(new RegExp(`queryParams:\\s*['"\`]${refName}['"\`]`, "g"), `queryParams: global.schemas["${refName}"]`).replace(new RegExp(`pathParams:\\s*['"\`]${refName}['"\`]`, "g"), `pathParams: global.schemas["${refName}"]`);
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(`An error occured while evaluating the route exports from "${routePath}"`);
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(true, rawCode, middlewareName, await getModuleTranspiler());
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;
@@ -292,7 +318,9 @@ function fixSchema(schema) {
292
318
  case "nonoptional":
293
319
  return fixSchema(schema.unwrap());
294
320
  default:
295
- throw new Error(`${schema._zod.def.type} type is not covered in fixSchema (@omer-x/next-openapi-json-generator")`);
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]) => [propName, fixSchema(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(fixSchema(isArray ? schema.array() : schema));
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((props, [propName, prop]) => ({
343
- ...props,
344
- [propName]: maskWithReference(prop, storedSchemas, true)
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((i) => maskWithReference(i, storedSchemas, true))
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 { ...param, schema: maskSchema(storedSchemas, param.schema) };
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((collection, [contentType, content]) => ({
374
- ...collection,
375
- [contentType]: {
376
- ...content,
377
- schema: maskSchema(storedSchemas, content.schema)
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 { ...body, content: maskContentSchema(storedSchemas, body.content) };
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 { ...response, content: maskContentSchema(storedSchemas, response.content) };
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((collection, [key, response]) => ({
392
- ...collection,
393
- [key]: maskResponseSchema(storedSchemas, response)
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((p) => maskParameterSchema(p, storedSchemas)),
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((collection, route) => ({
416
- ...collection,
417
- [route.path]: {
418
- ...collection[route.path],
419
- [route.method]: maskOperationSchemas(route.apiData, storedSchemas)
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((collection, schemaName) => {
427
- return {
428
- ...collection,
429
- [schemaName]: convertToOpenAPI(schemas[schemaName], false)
430
- };
431
- }, {});
432
- return Object.entries(bundledSchemas).reduce((bundle, [schemaName, schema]) => ({
433
- ...bundle,
434
- [schemaName]: maskWithReference(schema, schemas, false)
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(rootPath, routes, verifiedOptions.include, verifiedOptions.exclude);
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(route, routeDefinerName, schemas);
461
- for (const [method, routeHandler] of Object.entries(exportedRouteHandlers)) {
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(createRouteRecord(
464
- method.toLocaleLowerCase(),
465
- route,
466
- rootPath,
467
- routeHandler.apiData
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(JSON.stringify({
480
- openapi: "3.1.0",
481
- info: {
482
- title: metadata.serviceName,
483
- version: metadata.version,
484
- ...info ?? {}
485
- },
486
- servers,
487
- ...clearUnusedSchemasOption ? clearUnusedSchemas(pathsAndComponents) : pathsAndComponents,
488
- security,
489
- tags: []
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.5",
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": "eslint --flag unstable_native_nodejs_ts_config",
39
- "lint:fix": "eslint --fix --flag unstable_native_nodejs_ts_config",
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.1.1"
55
+ "minimatch": "^10.2.2"
56
56
  },
57
57
  "devDependencies": {
58
- "@omer-x/eslint-config": "^2.2.6",
59
- "@types/node": "^25.0.3",
60
- "@vitest/coverage-v8": "^4.0.16",
61
- "eslint": "^9.39.2",
62
- "semantic-release": "^25.0.2",
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.16"
64
+ "vitest": "^4.0.18"
65
65
  }
66
66
  }