@omer-x/next-openapi-json-generator 1.1.2 → 1.2.1

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.cjs CHANGED
@@ -35,8 +35,39 @@ __export(src_exports, {
35
35
  module.exports = __toCommonJS(src_exports);
36
36
 
37
37
  // src/core/generateOpenApiSpec.ts
38
+ var import_node_path4 = __toESM(require("path"), 1);
38
39
  var import_package_metadata = __toESM(require("@omer-x/package-metadata"), 1);
39
40
 
41
+ // src/utils/object.ts
42
+ function omit(object, ...keys) {
43
+ return Object.fromEntries(Object.entries(object).filter(([key]) => !keys.includes(key)));
44
+ }
45
+
46
+ // src/core/clearUnusedSchemas.ts
47
+ function countReferences(schemaName, source) {
48
+ return (source.match(new RegExp(`"#/components/schemas/${schemaName}"`, "g")) ?? []).length;
49
+ }
50
+ function clearUnusedSchemas({
51
+ paths,
52
+ components
53
+ }) {
54
+ if (!components.schemas) return { paths, components };
55
+ const stringifiedPaths = JSON.stringify(paths);
56
+ const stringifiedSchemas = Object.fromEntries(Object.entries(components.schemas).map(([schemaName, schema]) => {
57
+ return [schemaName, JSON.stringify(schema)];
58
+ }));
59
+ return {
60
+ paths,
61
+ components: {
62
+ ...components,
63
+ schemas: Object.fromEntries(Object.entries(components.schemas).filter(([schemaName]) => {
64
+ const otherSchemas = omit(stringifiedSchemas, schemaName);
65
+ return countReferences(schemaName, stringifiedPaths) > 0 || countReferences(schemaName, Object.values(otherSchemas).join("")) > 0;
66
+ }))
67
+ }
68
+ };
69
+ }
70
+
40
71
  // src/core/dir.ts
41
72
  var import_fs = require("fs");
42
73
  var import_promises = __toESM(require("fs/promises"), 1);
@@ -46,7 +77,7 @@ async function directoryExists(dirPath) {
46
77
  try {
47
78
  await import_promises.default.access(dirPath, import_fs.constants.F_OK);
48
79
  return true;
49
- } catch (err) {
80
+ } catch {
50
81
  return false;
51
82
  }
52
83
  }
@@ -91,6 +122,33 @@ async function isDocumentedRoute(routePath2) {
91
122
  var import_promises3 = __toESM(require("fs/promises"), 1);
92
123
  var import_node_path2 = __toESM(require("path"), 1);
93
124
 
125
+ // src/core/injectSchemas.ts
126
+ function generateRandomString(length) {
127
+ return [...Array(length)].map(() => Math.random().toString(36)[2]).join("");
128
+ }
129
+ function preserveStrings(code2) {
130
+ let replacements = {};
131
+ const output = code2.replace(/(['"`])([^'`"]+)\1/g, (replacedString) => {
132
+ const replacementId = generateRandomString(32);
133
+ replacements = {
134
+ ...replacements,
135
+ [replacementId]: replacedString
136
+ };
137
+ return `<@~${replacementId}~@>`;
138
+ });
139
+ return { output, replacements };
140
+ }
141
+ function restoreStrings(code2, replacements) {
142
+ return code2.replace(/<@~(.*?)~@>/g, (_, replacementId) => {
143
+ return replacements[replacementId];
144
+ });
145
+ }
146
+ function injectSchemas(code2, refName) {
147
+ const { output: preservedCode, replacements } = preserveStrings(code2);
148
+ const preservedCodeWithSchemasInjected = preservedCode.replace(new RegExp(`\\b${refName}\\.`, "g"), `global.schemas[${refName}].`).replace(new RegExp(`\\b${refName}\\b`, "g"), `"${refName}"`);
149
+ return restoreStrings(preservedCodeWithSchemasInjected, replacements);
150
+ }
151
+
94
152
  // src/core/middleware.ts
95
153
  function detectMiddlewareName(code2) {
96
154
  const match = code2.match(/middleware:\s*(\w+)/);
@@ -99,9 +157,13 @@ function detectMiddlewareName(code2) {
99
157
 
100
158
  // src/core/transpile.ts
101
159
  var import_typescript = require("typescript");
160
+
161
+ // src/utils/removeImports.ts
102
162
  function removeImports(code2) {
103
- return code2.replace(/^import\s.+\sfrom\s.+;?$/gm, "").trim();
163
+ return code2.replace(/(^import\s+[^;]+;?$|^import\s+[^;]*\sfrom\s.+;?$)/gm, "").replace(/(^import\s+{[\s\S]+?}\s+from\s+["'][^"']+["'];?)/gm, "").trim();
104
164
  }
165
+
166
+ // src/core/transpile.ts
105
167
  function fixExports(code2) {
106
168
  const validMethods = ["GET", "POST", "PUT", "PATCH", "DELETE"];
107
169
  const exportFixer1 = validMethods.map((method) => `exports.${method} = void 0;
@@ -137,9 +199,6 @@ async function findAppFolderPath() {
137
199
  }
138
200
  return null;
139
201
  }
140
- function injectSchemas(code2, refName) {
141
- return code2.replace(new RegExp(`\\b${refName}\\.`, "g"), `global.schemas[${refName}].`).replace(new RegExp(`\\b${refName}\\b`, "g"), `"${refName}"`);
142
- }
143
202
  function safeEval(code, routePath) {
144
203
  try {
145
204
  return eval(code);
@@ -154,12 +213,8 @@ async function getRouteExports(routePath2, routeDefinerName, schemas) {
154
213
  const code2 = transpile(rawCode, routeDefinerName, middlewareName);
155
214
  const fixedCode = Object.keys(schemas).reduce(injectSchemas, code2);
156
215
  global.schemas = schemas;
157
- if (middlewareName) {
158
- }
159
216
  const result = safeEval(fixedCode, routePath2);
160
217
  delete global.schemas;
161
- if (middlewareName) {
162
- }
163
218
  return result;
164
219
  }
165
220
 
@@ -183,6 +238,13 @@ function verifyOptions(include, exclude) {
183
238
  };
184
239
  }
185
240
 
241
+ // src/core/getRoutePathName.ts
242
+ var import_node_path3 = __toESM(require("path"), 1);
243
+ function getRoutePathName(filePath, rootPath) {
244
+ const dirName = import_node_path3.default.dirname(filePath);
245
+ return "/" + import_node_path3.default.relative(rootPath, dirName).replaceAll("[", "{").replaceAll("]", "}").replaceAll("\\", "/");
246
+ }
247
+
186
248
  // src/utils/deepEqual.ts
187
249
  function deepEqual(a, b) {
188
250
  if (typeof a !== typeof b) return false;
@@ -327,9 +389,6 @@ function maskOperationSchemas(operation, storedSchemas) {
327
389
  }
328
390
 
329
391
  // src/core/route.ts
330
- function getRoutePathName(filePath, rootPath) {
331
- return filePath.replace(rootPath, "").replace("[", "{").replace("]", "}").replaceAll("\\", "/").replace("/route.ts", "");
332
- }
333
392
  function createRouteRecord(method, filePath, rootPath, apiData) {
334
393
  return {
335
394
  method: method.toLocaleLowerCase(),
@@ -366,13 +425,18 @@ function bundleSchemas(schemas) {
366
425
  async function generateOpenApiSpec(schemas, {
367
426
  include: includeOption = [],
368
427
  exclude: excludeOption = [],
369
- routeDefinerName = "defineRoute"
428
+ routeDefinerName = "defineRoute",
429
+ rootPath: additionalRootPath,
430
+ servers,
431
+ security,
432
+ securitySchemes
370
433
  } = {}) {
371
434
  const verifiedOptions = verifyOptions(includeOption, excludeOption);
372
435
  const appFolderPath = await findAppFolderPath();
373
436
  if (!appFolderPath) throw new Error("This is not a Next.js application!");
374
- const routes = await getDirectoryItems(appFolderPath, "route.ts");
375
- const verifiedRoutes = filterDirectoryItems(appFolderPath, routes, verifiedOptions.include, verifiedOptions.exclude);
437
+ const rootPath = additionalRootPath ? import_node_path4.default.resolve(appFolderPath, "./" + additionalRootPath) : appFolderPath;
438
+ const routes = await getDirectoryItems(rootPath, "route.ts");
439
+ const verifiedRoutes = filterDirectoryItems(rootPath, routes, verifiedOptions.include, verifiedOptions.exclude);
376
440
  const validRoutes = [];
377
441
  for (const route of verifiedRoutes) {
378
442
  const isDocumented = await isDocumentedRoute(route);
@@ -383,22 +447,28 @@ async function generateOpenApiSpec(schemas, {
383
447
  validRoutes.push(createRouteRecord(
384
448
  method.toLocaleLowerCase(),
385
449
  route,
386
- appFolderPath,
450
+ rootPath,
387
451
  routeHandler.apiData
388
452
  ));
389
453
  }
390
454
  }
391
455
  const metadata = (0, import_package_metadata.default)();
456
+ const pathsAndComponents = {
457
+ paths: bundlePaths(validRoutes, schemas),
458
+ components: {
459
+ schemas: bundleSchemas(schemas),
460
+ securitySchemes
461
+ }
462
+ };
392
463
  return {
393
464
  openapi: "3.1.0",
394
465
  info: {
395
466
  title: metadata.serviceName,
396
467
  version: metadata.version
397
468
  },
398
- paths: bundlePaths(validRoutes, schemas),
399
- components: {
400
- schemas: bundleSchemas(schemas)
401
- },
469
+ servers,
470
+ ...clearUnusedSchemas(pathsAndComponents),
471
+ security,
402
472
  tags: []
403
473
  };
404
474
  }
package/dist/index.d.cts CHANGED
@@ -1,11 +1,17 @@
1
1
  import { OpenApiDocument } from '@omer-x/openapi-types';
2
+ import { ComponentsObject } from '@omer-x/openapi-types/components';
3
+ import { ServerObject } from '@omer-x/openapi-types/server';
2
4
  import { ZodType } from 'zod';
3
5
 
4
6
  type GeneratorOptions = {
5
7
  include?: string[];
6
8
  exclude?: string[];
7
9
  routeDefinerName?: string;
10
+ rootPath?: string;
11
+ servers?: ServerObject[];
12
+ security?: OpenApiDocument["security"];
13
+ securitySchemes?: ComponentsObject["securitySchemes"];
8
14
  };
9
- declare function generateOpenApiSpec(schemas: Record<string, ZodType>, { include: includeOption, exclude: excludeOption, routeDefinerName, }?: GeneratorOptions): Promise<Omit<OpenApiDocument, "components"> & Required<Pick<OpenApiDocument, "components">>>;
15
+ declare function generateOpenApiSpec(schemas: Record<string, ZodType>, { include: includeOption, exclude: excludeOption, routeDefinerName, rootPath: additionalRootPath, servers, security, securitySchemes, }?: GeneratorOptions): Promise<Omit<OpenApiDocument, "components"> & Required<Pick<OpenApiDocument, "components">>>;
10
16
 
11
17
  export { generateOpenApiSpec as default };
package/dist/index.d.ts CHANGED
@@ -1,11 +1,17 @@
1
1
  import { OpenApiDocument } from '@omer-x/openapi-types';
2
+ import { ComponentsObject } from '@omer-x/openapi-types/components';
3
+ import { ServerObject } from '@omer-x/openapi-types/server';
2
4
  import { ZodType } from 'zod';
3
5
 
4
6
  type GeneratorOptions = {
5
7
  include?: string[];
6
8
  exclude?: string[];
7
9
  routeDefinerName?: string;
10
+ rootPath?: string;
11
+ servers?: ServerObject[];
12
+ security?: OpenApiDocument["security"];
13
+ securitySchemes?: ComponentsObject["securitySchemes"];
8
14
  };
9
- declare function generateOpenApiSpec(schemas: Record<string, ZodType>, { include: includeOption, exclude: excludeOption, routeDefinerName, }?: GeneratorOptions): Promise<Omit<OpenApiDocument, "components"> & Required<Pick<OpenApiDocument, "components">>>;
15
+ declare function generateOpenApiSpec(schemas: Record<string, ZodType>, { include: includeOption, exclude: excludeOption, routeDefinerName, rootPath: additionalRootPath, servers, security, securitySchemes, }?: GeneratorOptions): Promise<Omit<OpenApiDocument, "components"> & Required<Pick<OpenApiDocument, "components">>>;
10
16
 
11
17
  export { generateOpenApiSpec as default };
package/dist/index.js CHANGED
@@ -1,6 +1,37 @@
1
1
  // src/core/generateOpenApiSpec.ts
2
+ import path4 from "node:path";
2
3
  import getPackageMetadata from "@omer-x/package-metadata";
3
4
 
5
+ // src/utils/object.ts
6
+ function omit(object, ...keys) {
7
+ return Object.fromEntries(Object.entries(object).filter(([key]) => !keys.includes(key)));
8
+ }
9
+
10
+ // src/core/clearUnusedSchemas.ts
11
+ function countReferences(schemaName, source) {
12
+ return (source.match(new RegExp(`"#/components/schemas/${schemaName}"`, "g")) ?? []).length;
13
+ }
14
+ function clearUnusedSchemas({
15
+ paths,
16
+ components
17
+ }) {
18
+ if (!components.schemas) return { paths, components };
19
+ const stringifiedPaths = JSON.stringify(paths);
20
+ const stringifiedSchemas = Object.fromEntries(Object.entries(components.schemas).map(([schemaName, schema]) => {
21
+ return [schemaName, JSON.stringify(schema)];
22
+ }));
23
+ return {
24
+ paths,
25
+ components: {
26
+ ...components,
27
+ schemas: Object.fromEntries(Object.entries(components.schemas).filter(([schemaName]) => {
28
+ const otherSchemas = omit(stringifiedSchemas, schemaName);
29
+ return countReferences(schemaName, stringifiedPaths) > 0 || countReferences(schemaName, Object.values(otherSchemas).join("")) > 0;
30
+ }))
31
+ }
32
+ };
33
+ }
34
+
4
35
  // src/core/dir.ts
5
36
  import { constants } from "fs";
6
37
  import fs from "fs/promises";
@@ -10,7 +41,7 @@ async function directoryExists(dirPath) {
10
41
  try {
11
42
  await fs.access(dirPath, constants.F_OK);
12
43
  return true;
13
- } catch (err) {
44
+ } catch {
14
45
  return false;
15
46
  }
16
47
  }
@@ -55,6 +86,33 @@ async function isDocumentedRoute(routePath2) {
55
86
  import fs3 from "node:fs/promises";
56
87
  import path2 from "node:path";
57
88
 
89
+ // src/core/injectSchemas.ts
90
+ function generateRandomString(length) {
91
+ return [...Array(length)].map(() => Math.random().toString(36)[2]).join("");
92
+ }
93
+ function preserveStrings(code2) {
94
+ let replacements = {};
95
+ const output = code2.replace(/(['"`])([^'`"]+)\1/g, (replacedString) => {
96
+ const replacementId = generateRandomString(32);
97
+ replacements = {
98
+ ...replacements,
99
+ [replacementId]: replacedString
100
+ };
101
+ return `<@~${replacementId}~@>`;
102
+ });
103
+ return { output, replacements };
104
+ }
105
+ function restoreStrings(code2, replacements) {
106
+ return code2.replace(/<@~(.*?)~@>/g, (_, replacementId) => {
107
+ return replacements[replacementId];
108
+ });
109
+ }
110
+ function injectSchemas(code2, refName) {
111
+ const { output: preservedCode, replacements } = preserveStrings(code2);
112
+ const preservedCodeWithSchemasInjected = preservedCode.replace(new RegExp(`\\b${refName}\\.`, "g"), `global.schemas[${refName}].`).replace(new RegExp(`\\b${refName}\\b`, "g"), `"${refName}"`);
113
+ return restoreStrings(preservedCodeWithSchemasInjected, replacements);
114
+ }
115
+
58
116
  // src/core/middleware.ts
59
117
  function detectMiddlewareName(code2) {
60
118
  const match = code2.match(/middleware:\s*(\w+)/);
@@ -63,9 +121,13 @@ function detectMiddlewareName(code2) {
63
121
 
64
122
  // src/core/transpile.ts
65
123
  import { transpile as tsTranspile } from "typescript";
124
+
125
+ // src/utils/removeImports.ts
66
126
  function removeImports(code2) {
67
- return code2.replace(/^import\s.+\sfrom\s.+;?$/gm, "").trim();
127
+ return code2.replace(/(^import\s+[^;]+;?$|^import\s+[^;]*\sfrom\s.+;?$)/gm, "").replace(/(^import\s+{[\s\S]+?}\s+from\s+["'][^"']+["'];?)/gm, "").trim();
68
128
  }
129
+
130
+ // src/core/transpile.ts
69
131
  function fixExports(code2) {
70
132
  const validMethods = ["GET", "POST", "PUT", "PATCH", "DELETE"];
71
133
  const exportFixer1 = validMethods.map((method) => `exports.${method} = void 0;
@@ -101,9 +163,6 @@ async function findAppFolderPath() {
101
163
  }
102
164
  return null;
103
165
  }
104
- function injectSchemas(code2, refName) {
105
- return code2.replace(new RegExp(`\\b${refName}\\.`, "g"), `global.schemas[${refName}].`).replace(new RegExp(`\\b${refName}\\b`, "g"), `"${refName}"`);
106
- }
107
166
  function safeEval(code, routePath) {
108
167
  try {
109
168
  return eval(code);
@@ -118,12 +177,8 @@ async function getRouteExports(routePath2, routeDefinerName, schemas) {
118
177
  const code2 = transpile(rawCode, routeDefinerName, middlewareName);
119
178
  const fixedCode = Object.keys(schemas).reduce(injectSchemas, code2);
120
179
  global.schemas = schemas;
121
- if (middlewareName) {
122
- }
123
180
  const result = safeEval(fixedCode, routePath2);
124
181
  delete global.schemas;
125
- if (middlewareName) {
126
- }
127
182
  return result;
128
183
  }
129
184
 
@@ -147,6 +202,13 @@ function verifyOptions(include, exclude) {
147
202
  };
148
203
  }
149
204
 
205
+ // src/core/getRoutePathName.ts
206
+ import path3 from "node:path";
207
+ function getRoutePathName(filePath, rootPath) {
208
+ const dirName = path3.dirname(filePath);
209
+ return "/" + path3.relative(rootPath, dirName).replaceAll("[", "{").replaceAll("]", "}").replaceAll("\\", "/");
210
+ }
211
+
150
212
  // src/utils/deepEqual.ts
151
213
  function deepEqual(a, b) {
152
214
  if (typeof a !== typeof b) return false;
@@ -291,9 +353,6 @@ function maskOperationSchemas(operation, storedSchemas) {
291
353
  }
292
354
 
293
355
  // src/core/route.ts
294
- function getRoutePathName(filePath, rootPath) {
295
- return filePath.replace(rootPath, "").replace("[", "{").replace("]", "}").replaceAll("\\", "/").replace("/route.ts", "");
296
- }
297
356
  function createRouteRecord(method, filePath, rootPath, apiData) {
298
357
  return {
299
358
  method: method.toLocaleLowerCase(),
@@ -330,13 +389,18 @@ function bundleSchemas(schemas) {
330
389
  async function generateOpenApiSpec(schemas, {
331
390
  include: includeOption = [],
332
391
  exclude: excludeOption = [],
333
- routeDefinerName = "defineRoute"
392
+ routeDefinerName = "defineRoute",
393
+ rootPath: additionalRootPath,
394
+ servers,
395
+ security,
396
+ securitySchemes
334
397
  } = {}) {
335
398
  const verifiedOptions = verifyOptions(includeOption, excludeOption);
336
399
  const appFolderPath = await findAppFolderPath();
337
400
  if (!appFolderPath) throw new Error("This is not a Next.js application!");
338
- const routes = await getDirectoryItems(appFolderPath, "route.ts");
339
- const verifiedRoutes = filterDirectoryItems(appFolderPath, routes, verifiedOptions.include, verifiedOptions.exclude);
401
+ const rootPath = additionalRootPath ? path4.resolve(appFolderPath, "./" + additionalRootPath) : appFolderPath;
402
+ const routes = await getDirectoryItems(rootPath, "route.ts");
403
+ const verifiedRoutes = filterDirectoryItems(rootPath, routes, verifiedOptions.include, verifiedOptions.exclude);
340
404
  const validRoutes = [];
341
405
  for (const route of verifiedRoutes) {
342
406
  const isDocumented = await isDocumentedRoute(route);
@@ -347,22 +411,28 @@ async function generateOpenApiSpec(schemas, {
347
411
  validRoutes.push(createRouteRecord(
348
412
  method.toLocaleLowerCase(),
349
413
  route,
350
- appFolderPath,
414
+ rootPath,
351
415
  routeHandler.apiData
352
416
  ));
353
417
  }
354
418
  }
355
419
  const metadata = getPackageMetadata();
420
+ const pathsAndComponents = {
421
+ paths: bundlePaths(validRoutes, schemas),
422
+ components: {
423
+ schemas: bundleSchemas(schemas),
424
+ securitySchemes
425
+ }
426
+ };
356
427
  return {
357
428
  openapi: "3.1.0",
358
429
  info: {
359
430
  title: metadata.serviceName,
360
431
  version: metadata.version
361
432
  },
362
- paths: bundlePaths(validRoutes, schemas),
363
- components: {
364
- schemas: bundleSchemas(schemas)
365
- },
433
+ servers,
434
+ ...clearUnusedSchemas(pathsAndComponents),
435
+ security,
366
436
  tags: []
367
437
  };
368
438
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omer-x/next-openapi-json-generator",
3
- "version": "1.1.2",
3
+ "version": "1.2.1",
4
4
  "description": "a Next.js plugin to generate OpenAPI documentation from route handlers",
5
5
  "keywords": [
6
6
  "next.js",
@@ -48,20 +48,20 @@
48
48
  "@omer-x/package-metadata": "^1.0.0",
49
49
  "minimatch": "^10.0.1",
50
50
  "typescript": "^5.6.3",
51
- "zod-to-json-schema": "^3.23.3"
51
+ "zod-to-json-schema": "^3.23.5"
52
52
  },
53
53
  "devDependencies": {
54
- "@omer-x/eslint-config": "^1.0.7",
55
- "@omer-x/openapi-types": "^0.2.1",
56
- "@types/node": "^22.7.9",
57
- "eslint": "^8.57.1",
58
- "semantic-release": "^24.1.3",
54
+ "@omer-x/eslint-config": "^2.0.2",
55
+ "@types/node": "^22.8.4",
56
+ "eslint": "^9.13.0",
57
+ "semantic-release": "^24.2.0",
59
58
  "ts-jest": "^29.2.5",
60
59
  "ts-node": "^10.9.2",
61
- "tsup": "^8.3.0",
60
+ "tsup": "^8.3.5",
62
61
  "zod": "^3.23.8"
63
62
  },
64
63
  "peerDependencies": {
65
- "@omer-x/next-openapi-route-handler": "^1"
64
+ "@omer-x/next-openapi-route-handler": "^1",
65
+ "@omer-x/openapi-types": "^1"
66
66
  }
67
67
  }