@omer-x/next-openapi-json-generator 1.0.1 → 1.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/dist/index.cjs ADDED
@@ -0,0 +1,394 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var src_exports = {};
32
+ __export(src_exports, {
33
+ default: () => src_default
34
+ });
35
+ module.exports = __toCommonJS(src_exports);
36
+
37
+ // src/core/generateOpenApiSpec.ts
38
+ var import_package_metadata = __toESM(require("@omer-x/package-metadata"), 1);
39
+
40
+ // src/core/dir.ts
41
+ var import_fs = require("fs");
42
+ var import_promises = __toESM(require("fs/promises"), 1);
43
+ var import_node_path = __toESM(require("path"), 1);
44
+ var import_minimatch = require("minimatch");
45
+ async function directoryExists(dirPath) {
46
+ try {
47
+ await import_promises.default.access(dirPath, import_fs.constants.F_OK);
48
+ return true;
49
+ } catch (err) {
50
+ return false;
51
+ }
52
+ }
53
+ async function getDirectoryItems(dirPath, targetFileName) {
54
+ const collection = [];
55
+ const files = await import_promises.default.readdir(dirPath);
56
+ for (const itemName of files) {
57
+ const itemPath = import_node_path.default.resolve(dirPath, itemName);
58
+ const stats = await import_promises.default.stat(itemPath);
59
+ if (stats.isDirectory()) {
60
+ const children = await getDirectoryItems(itemPath, targetFileName);
61
+ collection.push(...children);
62
+ } else if (itemName === targetFileName) {
63
+ collection.push(itemPath);
64
+ }
65
+ }
66
+ return collection;
67
+ }
68
+ function filterDirectoryItems(rootPath, items, include, exclude) {
69
+ const includedPatterns = include.map((pattern) => new import_minimatch.Minimatch(pattern));
70
+ const excludedPatterns = exclude.map((pattern) => new import_minimatch.Minimatch(pattern));
71
+ return items.filter((item) => {
72
+ const relativePath = import_node_path.default.relative(rootPath, item);
73
+ const isIncluded = includedPatterns.some((pattern) => pattern.match(relativePath));
74
+ const isExcluded = excludedPatterns.some((pattern) => pattern.match(relativePath));
75
+ return (isIncluded || !include.length) && !isExcluded;
76
+ });
77
+ }
78
+
79
+ // src/core/next.ts
80
+ var import_promises2 = __toESM(require("fs/promises"), 1);
81
+ var import_node_path2 = __toESM(require("path"), 1);
82
+
83
+ // src/core/middleware.ts
84
+ function detectMiddlewareName(code2) {
85
+ const match = code2.match(/middleware:\s*(\w+)/);
86
+ return match ? match[1] : null;
87
+ }
88
+
89
+ // src/core/transpile.ts
90
+ var import_typescript = require("typescript");
91
+ function removeImports(code2) {
92
+ return code2.replace(/^import\s.+\sfrom\s.+;?$/gm, "").trim();
93
+ }
94
+ function fixExports(code2) {
95
+ const validMethods = ["GET", "POST", "PUT", "PATCH", "DELETE"];
96
+ const exportFixer1 = validMethods.map((method) => `exports.${method} = void 0;
97
+ `);
98
+ const exportFixer2 = `module.exports = { ${validMethods.map((m) => `${m}: exports.${m}`).join(", ")} }`;
99
+ return `${exportFixer1}
100
+ ${code2}
101
+ ${exportFixer2}`;
102
+ }
103
+ function injectMiddlewareFixer(middlewareName) {
104
+ return `const ${middlewareName} = (handler) => handler;`;
105
+ }
106
+ function transpile(rawCode, routeDefinerName, middlewareName) {
107
+ const code2 = fixExports(removeImports(rawCode));
108
+ const parts = [
109
+ `import ${routeDefinerName} from '@omer-x/next-openapi-route-handler';`,
110
+ "import z from 'zod';",
111
+ middlewareName ? injectMiddlewareFixer(middlewareName) : "",
112
+ code2
113
+ ];
114
+ return (0, import_typescript.transpile)(parts.join("\n"));
115
+ }
116
+
117
+ // src/core/next.ts
118
+ async function findAppFolderPath() {
119
+ const inSrc = import_node_path2.default.resolve(process.cwd(), "src", "app");
120
+ if (await directoryExists(inSrc)) {
121
+ return inSrc;
122
+ }
123
+ const inRoot = import_node_path2.default.resolve(process.cwd(), "app");
124
+ if (await directoryExists(inRoot)) {
125
+ return inRoot;
126
+ }
127
+ return null;
128
+ }
129
+ function injectSchemas(code2, refName) {
130
+ return code2.replace(new RegExp(`\\b${refName}\\.`, "g"), `global.schemas[${refName}].`).replace(new RegExp(`\\b${refName}\\b`, "g"), `"${refName}"`);
131
+ }
132
+ function safeEval(code, routePath) {
133
+ try {
134
+ return eval(code);
135
+ } catch (error) {
136
+ console.log(`An error occured while evaluating the route exports from "${routePath}"`);
137
+ throw error;
138
+ }
139
+ }
140
+ async function getRouteExports(routePath2, routeDefinerName, schemas) {
141
+ const rawCode = await import_promises2.default.readFile(routePath2, "utf-8");
142
+ const middlewareName = detectMiddlewareName(rawCode);
143
+ const code2 = transpile(rawCode, routeDefinerName, middlewareName);
144
+ const fixedCode = Object.keys(schemas).reduce(injectSchemas, code2);
145
+ global.schemas = schemas;
146
+ if (middlewareName) {
147
+ }
148
+ const result = safeEval(fixedCode, routePath2);
149
+ delete global.schemas;
150
+ if (middlewareName) {
151
+ }
152
+ return result;
153
+ }
154
+
155
+ // src/core/options.ts
156
+ function verifyOptions(include, exclude) {
157
+ if (process.env.NODE_ENV === "development") {
158
+ for (const item of include) {
159
+ if (!item.endsWith("/route.ts")) {
160
+ console.log(`${item} is not a valid route handler path`);
161
+ }
162
+ }
163
+ for (const item of exclude) {
164
+ if (!item.endsWith("/route.ts")) {
165
+ console.log(`${item} is not a valid route handler path`);
166
+ }
167
+ }
168
+ }
169
+ return {
170
+ include: include.filter((item) => item.endsWith("/route.ts")),
171
+ exclude: exclude.filter((item) => item.endsWith("/route.ts"))
172
+ };
173
+ }
174
+
175
+ // src/utils/deepEqual.ts
176
+ function deepEqual(a, b) {
177
+ if (typeof a !== typeof b) return false;
178
+ switch (typeof a) {
179
+ case "object": {
180
+ if (!a || !b) return a === b;
181
+ if (Array.isArray(a) && Array.isArray(b)) {
182
+ return a.every((item, index) => deepEqual(item, b[index]));
183
+ }
184
+ if (Object.keys(a).length !== Object.keys(b).length) return false;
185
+ return Object.entries(a).every(([key, value]) => {
186
+ return deepEqual(value, b[key]);
187
+ });
188
+ }
189
+ case "function":
190
+ case "symbol":
191
+ return false;
192
+ default:
193
+ return a === b;
194
+ }
195
+ }
196
+
197
+ // src/core/zod-to-openapi.ts
198
+ var import_zod_to_json_schema = __toESM(require("zod-to-json-schema"), 1);
199
+
200
+ // src/utils/zod-schema.ts
201
+ function isFile(schema) {
202
+ const result = schema.safeParse(new File([], "nothing.txt"));
203
+ return result.success;
204
+ }
205
+
206
+ // src/core/zod-to-openapi.ts
207
+ function convertToOpenAPI(schema, isArray) {
208
+ const result = (0, import_zod_to_json_schema.default)(isArray ? schema.array() : schema, {
209
+ target: "openApi3",
210
+ $refStrategy: "none"
211
+ });
212
+ if (result.type === "object" && result.properties) {
213
+ for (const [propName, prop] of Object.entries(schema.shape)) {
214
+ if (isFile(prop)) {
215
+ result.properties[propName] = {
216
+ type: "string",
217
+ format: "binary",
218
+ description: prop.description
219
+ // contentEncoding: "base64", // swagger-ui-react doesn't support this
220
+ };
221
+ }
222
+ }
223
+ }
224
+ return result;
225
+ }
226
+
227
+ // src/core/mask.ts
228
+ function maskWithReference(schema, storedSchemas, self) {
229
+ if (self) {
230
+ for (const [schemaName, zodSchema] of Object.entries(storedSchemas)) {
231
+ if (deepEqual(schema, convertToOpenAPI(zodSchema, false))) {
232
+ return {
233
+ $ref: `#/components/schemas/${schemaName}`
234
+ };
235
+ }
236
+ }
237
+ }
238
+ if ("$ref" in schema) return schema;
239
+ if (schema.oneOf) {
240
+ return {
241
+ ...schema,
242
+ oneOf: schema.oneOf.map((i) => maskWithReference(i, storedSchemas, true))
243
+ };
244
+ }
245
+ if (schema.anyOf) {
246
+ return {
247
+ ...schema,
248
+ anyOf: schema.anyOf.map((i) => maskWithReference(i, storedSchemas, true))
249
+ };
250
+ }
251
+ switch (schema.type) {
252
+ case "object":
253
+ return {
254
+ ...schema,
255
+ properties: Object.entries(schema.properties ?? {}).reduce((props, [propName, prop]) => ({
256
+ ...props,
257
+ [propName]: maskWithReference(prop, storedSchemas, true)
258
+ }), {})
259
+ };
260
+ case "array":
261
+ if (Array.isArray(schema.items)) {
262
+ return {
263
+ ...schema,
264
+ items: schema.items.map((i) => maskWithReference(i, storedSchemas, true))
265
+ };
266
+ }
267
+ return {
268
+ ...schema,
269
+ items: maskWithReference(schema.items, storedSchemas, true)
270
+ };
271
+ }
272
+ return schema;
273
+ }
274
+
275
+ // src/core/operation-mask.ts
276
+ function maskSchema(storedSchemas, schema) {
277
+ if (!schema) return schema;
278
+ return maskWithReference(schema, storedSchemas, true);
279
+ }
280
+ function maskParameterSchema(param, storedSchemas) {
281
+ if ("$ref" in param) return param;
282
+ return { ...param, schema: maskSchema(storedSchemas, param.schema) };
283
+ }
284
+ function maskContentSchema(storedSchemas, bodyContent) {
285
+ if (!bodyContent) return bodyContent;
286
+ return Object.entries(bodyContent).reduce((collection, [contentType, content]) => ({
287
+ ...collection,
288
+ [contentType]: {
289
+ ...content,
290
+ schema: maskSchema(storedSchemas, content.schema)
291
+ }
292
+ }), {});
293
+ }
294
+ function maskRequestBodySchema(storedSchemas, body) {
295
+ if (!body || "$ref" in body) return body;
296
+ return { ...body, content: maskContentSchema(storedSchemas, body.content) };
297
+ }
298
+ function maskResponseSchema(storedSchemas, response) {
299
+ if ("$ref" in response) return response;
300
+ return { ...response, content: maskContentSchema(storedSchemas, response.content) };
301
+ }
302
+ function maskSchemasInResponses(storedSchemas, responses) {
303
+ if (!responses) return responses;
304
+ return Object.entries(responses).reduce((collection, [key, response]) => ({
305
+ ...collection,
306
+ [key]: maskResponseSchema(storedSchemas, response)
307
+ }), {});
308
+ }
309
+ function maskOperationSchemas(operation, storedSchemas) {
310
+ return {
311
+ ...operation,
312
+ parameters: operation.parameters?.map((p) => maskParameterSchema(p, storedSchemas)),
313
+ requestBody: maskRequestBodySchema(storedSchemas, operation.requestBody),
314
+ responses: maskSchemasInResponses(storedSchemas, operation.responses)
315
+ };
316
+ }
317
+
318
+ // src/core/route.ts
319
+ function getRoutePathName(filePath, rootPath) {
320
+ return filePath.replace(rootPath, "").replace("[", "{").replace("]", "}").replaceAll("\\", "/").replace("/route.ts", "");
321
+ }
322
+ function createRouteRecord(method, filePath, rootPath, apiData) {
323
+ return {
324
+ method: method.toLocaleLowerCase(),
325
+ path: getRoutePathName(filePath, rootPath),
326
+ apiData
327
+ };
328
+ }
329
+ function bundlePaths(source, storedSchemas) {
330
+ source.sort((a, b) => a.path.localeCompare(b.path));
331
+ return source.reduce((collection, route) => ({
332
+ ...collection,
333
+ [route.path]: {
334
+ ...collection[route.path],
335
+ [route.method]: maskOperationSchemas(route.apiData, storedSchemas)
336
+ }
337
+ }), {});
338
+ }
339
+
340
+ // src/core/schema.ts
341
+ function bundleSchemas(schemas) {
342
+ const bundledSchemas = Object.keys(schemas).reduce((collection, schemaName) => {
343
+ return {
344
+ ...collection,
345
+ [schemaName]: convertToOpenAPI(schemas[schemaName], false)
346
+ };
347
+ }, {});
348
+ return Object.entries(bundledSchemas).reduce((bundle, [schemaName, schema]) => ({
349
+ ...bundle,
350
+ [schemaName]: maskWithReference(schema, schemas, false)
351
+ }), {});
352
+ }
353
+
354
+ // src/core/generateOpenApiSpec.ts
355
+ async function generateOpenApiSpec(schemas, {
356
+ include: includeOption = [],
357
+ exclude: excludeOption = [],
358
+ routeDefinerName = "defineRoute"
359
+ } = {}) {
360
+ const verifiedOptions = verifyOptions(includeOption, excludeOption);
361
+ const appFolderPath = await findAppFolderPath();
362
+ if (!appFolderPath) throw new Error("This is not a Next.js application!");
363
+ const routes = await getDirectoryItems(appFolderPath, "route.ts");
364
+ const verifiedRoutes = filterDirectoryItems(appFolderPath, routes, verifiedOptions.include, verifiedOptions.exclude);
365
+ const validRoutes = [];
366
+ for (const route of verifiedRoutes) {
367
+ const exportedRouteHandlers = await getRouteExports(route, routeDefinerName, schemas);
368
+ for (const [method, routeHandler] of Object.entries(exportedRouteHandlers)) {
369
+ if (!routeHandler || !routeHandler.apiData) continue;
370
+ validRoutes.push(createRouteRecord(
371
+ method.toLocaleLowerCase(),
372
+ route,
373
+ appFolderPath,
374
+ routeHandler.apiData
375
+ ));
376
+ }
377
+ }
378
+ const metadata = (0, import_package_metadata.default)();
379
+ return {
380
+ openapi: "3.1.0",
381
+ info: {
382
+ title: metadata.serviceName,
383
+ version: metadata.version
384
+ },
385
+ paths: bundlePaths(validRoutes, schemas),
386
+ components: {
387
+ schemas: bundleSchemas(schemas)
388
+ },
389
+ tags: []
390
+ };
391
+ }
392
+
393
+ // src/index.ts
394
+ var src_default = generateOpenApiSpec;
@@ -0,0 +1,11 @@
1
+ import { OpenApiDocument } from '@omer-x/openapi-types';
2
+ import { ZodType } from 'zod';
3
+
4
+ type GeneratorOptions = {
5
+ include?: string[];
6
+ exclude?: string[];
7
+ routeDefinerName?: string;
8
+ };
9
+ declare function generateOpenApiSpec(schemas: Record<string, ZodType>, { include: includeOption, exclude: excludeOption, routeDefinerName, }?: GeneratorOptions): Promise<Omit<OpenApiDocument, "components"> & Required<Pick<OpenApiDocument, "components">>>;
10
+
11
+ export { generateOpenApiSpec as default };
package/dist/index.js CHANGED
@@ -44,6 +44,12 @@ function filterDirectoryItems(rootPath, items, include, exclude) {
44
44
  import fs2 from "node:fs/promises";
45
45
  import path2 from "node:path";
46
46
 
47
+ // src/core/middleware.ts
48
+ function detectMiddlewareName(code2) {
49
+ const match = code2.match(/middleware:\s*(\w+)/);
50
+ return match ? match[1] : null;
51
+ }
52
+
47
53
  // src/core/transpile.ts
48
54
  import { transpile as tsTranspile } from "typescript";
49
55
  function removeImports(code2) {
@@ -58,11 +64,15 @@ function fixExports(code2) {
58
64
  ${code2}
59
65
  ${exportFixer2}`;
60
66
  }
61
- function transpile(rawCode, routeDefinerName) {
67
+ function injectMiddlewareFixer(middlewareName) {
68
+ return `const ${middlewareName} = (handler) => handler;`;
69
+ }
70
+ function transpile(rawCode, routeDefinerName, middlewareName) {
62
71
  const code2 = fixExports(removeImports(rawCode));
63
72
  const parts = [
64
73
  `import ${routeDefinerName} from '@omer-x/next-openapi-route-handler';`,
65
74
  "import z from 'zod';",
75
+ middlewareName ? injectMiddlewareFixer(middlewareName) : "",
66
76
  code2
67
77
  ];
68
78
  return tsTranspile(parts.join("\n"));
@@ -92,12 +102,17 @@ function safeEval(code, routePath) {
92
102
  }
93
103
  }
94
104
  async function getRouteExports(routePath2, routeDefinerName, schemas) {
95
- const content = await fs2.readFile(routePath2, "utf-8");
96
- const code2 = transpile(content, routeDefinerName);
105
+ const rawCode = await fs2.readFile(routePath2, "utf-8");
106
+ const middlewareName = detectMiddlewareName(rawCode);
107
+ const code2 = transpile(rawCode, routeDefinerName, middlewareName);
97
108
  const fixedCode = Object.keys(schemas).reduce(injectSchemas, code2);
98
109
  global.schemas = schemas;
110
+ if (middlewareName) {
111
+ }
99
112
  const result = safeEval(fixedCode, routePath2);
100
113
  delete global.schemas;
114
+ if (middlewareName) {
115
+ }
101
116
  return result;
102
117
  }
103
118
 
@@ -130,7 +145,10 @@ function deepEqual(a, b) {
130
145
  if (Array.isArray(a) && Array.isArray(b)) {
131
146
  return a.every((item, index) => deepEqual(item, b[index]));
132
147
  }
133
- return Object.entries(a).every(([key, value]) => deepEqual(value, b[key]));
148
+ if (Object.keys(a).length !== Object.keys(b).length) return false;
149
+ return Object.entries(a).every(([key, value]) => {
150
+ return deepEqual(value, b[key]);
151
+ });
134
152
  }
135
153
  case "function":
136
154
  case "symbol":
@@ -142,11 +160,31 @@ function deepEqual(a, b) {
142
160
 
143
161
  // src/core/zod-to-openapi.ts
144
162
  import zodToJsonSchema from "zod-to-json-schema";
163
+
164
+ // src/utils/zod-schema.ts
165
+ function isFile(schema) {
166
+ const result = schema.safeParse(new File([], "nothing.txt"));
167
+ return result.success;
168
+ }
169
+
170
+ // src/core/zod-to-openapi.ts
145
171
  function convertToOpenAPI(schema, isArray) {
146
172
  const result = zodToJsonSchema(isArray ? schema.array() : schema, {
147
173
  target: "openApi3",
148
174
  $refStrategy: "none"
149
175
  });
176
+ if (result.type === "object" && result.properties) {
177
+ for (const [propName, prop] of Object.entries(schema.shape)) {
178
+ if (isFile(prop)) {
179
+ result.properties[propName] = {
180
+ type: "string",
181
+ format: "binary",
182
+ description: prop.description
183
+ // contentEncoding: "base64", // swagger-ui-react doesn't support this
184
+ };
185
+ }
186
+ }
187
+ }
150
188
  return result;
151
189
  }
152
190
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omer-x/next-openapi-json-generator",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "a Next.js plugin to generate OpenAPI documentation from route handlers",
5
5
  "keywords": [
6
6
  "next.js",
@@ -29,11 +29,16 @@
29
29
  },
30
30
  "license": "MIT",
31
31
  "type": "module",
32
- "main": "dist/index.js",
33
- "types": "dist/index.d.ts",
34
32
  "files": [
35
33
  "dist/"
36
34
  ],
35
+ "exports": {
36
+ ".": {
37
+ "types": "./dist/index.d.ts",
38
+ "import": "./dist/index.js",
39
+ "require": "./dist/index.cjs"
40
+ }
41
+ },
37
42
  "scripts": {
38
43
  "test": "jest",
39
44
  "dev": "tsup --watch",