next-openapi-gen 0.8.8 → 0.9.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/README.md CHANGED
@@ -14,15 +14,12 @@ Automatically generate OpenAPI 3.0 documentation from Next.js projects, with sup
14
14
 
15
15
  ## Supported interfaces
16
16
 
17
- - Scalar 🆕
17
+ - Scalar 💡(default)
18
18
  - Swagger
19
19
  - Redoc
20
20
  - Stoplight Elements
21
21
  - RapiDoc
22
22
 
23
- > [!TIP]
24
- > You can use the `--ui none` option during initialization to skip UI setup if you only care about generating the OpenAPI documentation.
25
-
26
23
  ## Installation
27
24
 
28
25
  ```bash
@@ -33,14 +30,27 @@ npm install next-openapi-gen --save-dev
33
30
 
34
31
  ```bash
35
32
  # Initialize OpenAPI configuration
36
- npx next-openapi-gen init --ui scalar --docs-url api-docs --schema zod
33
+ npx next-openapi-gen init
37
34
 
38
35
  # Generate OpenAPI documentation
39
36
  npx next-openapi-gen generate
40
37
  ```
41
38
 
42
39
  > [!TIP]
43
- > Use the `--output` option in the `init` command to specify a custom output file for the template. Then you can use the `--template` option in the `generate` command to point to that file.
40
+ > Scalar UI and Zod are set by default
41
+
42
+
43
+ ### Init Command Options
44
+
45
+ | Option | Choices | Default | Description |
46
+ |--------|---------|---------|-------------|
47
+ | `--ui` | `scalar`, `swagger`, `redoc`, `stoplight`, `rapidoc`, `none` | `scalar` | UI framework for API docs |
48
+ | `--schema` | `zod`, `typescript` | `zod` | Schema validation tool |
49
+ | `--docs-url` | any string | `api-docs` | URL path for documentation page |
50
+ | `--output` | any path | `next.openapi.json` | Output file for OpenAPI template |
51
+
52
+ > [!TIP]
53
+ > Use `--ui none` to skip UI setup and only generate the OpenAPI specification file.
44
54
 
45
55
  ## Configuration
46
56
 
@@ -784,6 +794,51 @@ Factory functions work with any naming convention and support:
784
794
  - Imported schemas
785
795
  - Multiple factory patterns in the same project
786
796
 
797
+ ### Schema Composition with `.extend()`
798
+
799
+ Zod's `.extend()` method allows you to build upon existing schemas:
800
+
801
+ ```typescript
802
+ // src/schemas/user.ts
803
+
804
+ // Base user schema
805
+ export const BaseUserSchema = z.object({
806
+ id: z.string().uuid().describe("User ID"),
807
+ email: z.string().email().describe("Email address"),
808
+ });
809
+
810
+ // Extend with additional fields
811
+ export const UserProfileSchema = BaseUserSchema.extend({
812
+ name: z.string().describe("Full name"),
813
+ bio: z.string().optional().describe("User biography"),
814
+ });
815
+
816
+ // Multiple levels of extension
817
+ export const AdminUserSchema = UserProfileSchema.extend({
818
+ role: z.enum(["admin", "moderator"]).describe("Admin role"),
819
+ permissions: z.array(z.string()).describe("Permission list"),
820
+ });
821
+
822
+ export const UserIdParams = z.object({
823
+ id: z.string().uuid().describe("User ID"),
824
+ });
825
+
826
+ // src/app/api/users/[id]/route.ts
827
+
828
+ /**
829
+ * Get user profile
830
+ * @pathParams UserIdParams
831
+ * @response UserProfileSchema
832
+ * @openapi
833
+ */
834
+ export async function GET(
835
+ request: NextRequest,
836
+ { params }: { params: { id: string } }
837
+ ) {
838
+ // Returns: { id, email, name, bio? }
839
+ }
840
+ ```
841
+
787
842
  ### Drizzle-Zod Support
788
843
 
789
844
  The library fully supports **drizzle-zod** for generating Zod schemas from Drizzle ORM table definitions. This provides a single source of truth for your database schema, validation, and API documentation.
@@ -5,13 +5,23 @@ import ora from "ora";
5
5
  import { exec } from "child_process";
6
6
  import util from "util";
7
7
  import openapiTemplate from "../openapi-template.js";
8
- import { scalarDeps, ScalarUI } from "../components/scalar.js";
9
- import { swaggerDeps, SwaggerUI } from "../components/swagger.js";
10
- import { redocDeps, RedocUI } from "../components/redoc.js";
11
- import { stoplightDeps, StoplightUI } from "../components/stoplight.js";
12
- import { rapidocDeps, RapidocUI } from "../components/rapidoc.js";
8
+ import { scalarDeps, scalarDevDeps, ScalarUI } from "../components/scalar.js";
9
+ import { swaggerDeps, swaggerDevDeps, SwaggerUI } from "../components/swagger.js";
10
+ import { redocDeps, redocDevDeps, RedocUI } from "../components/redoc.js";
11
+ import { stoplightDeps, stoplightDevDeps, StoplightUI } from "../components/stoplight.js";
12
+ import { rapidocDeps, rapidocDevDeps, RapidocUI } from "../components/rapidoc.js";
13
13
  const execPromise = util.promisify(exec);
14
14
  const spinner = ora("Initializing project with OpenAPI template...\n");
15
+ async function hasDependency(packageName) {
16
+ try {
17
+ const packageJsonPath = path.join(process.cwd(), "package.json");
18
+ const packageJson = await fse.readJson(packageJsonPath);
19
+ return !!(packageJson.dependencies?.[packageName] || packageJson.devDependencies?.[packageName]);
20
+ }
21
+ catch {
22
+ return false;
23
+ }
24
+ }
15
25
  const getPackageManager = async () => {
16
26
  let currentDir = process.cwd();
17
27
  while (true) {
@@ -84,6 +94,25 @@ function getDocsPageDependencies(ui) {
84
94
  }
85
95
  return deps.join(" ");
86
96
  }
97
+ function getDocsPageDevDependencies(ui) {
98
+ let devDeps = [];
99
+ if (ui === "scalar") {
100
+ devDeps = scalarDevDeps;
101
+ }
102
+ else if (ui === "swagger") {
103
+ devDeps = swaggerDevDeps;
104
+ }
105
+ else if (ui === "redoc") {
106
+ devDeps = redocDevDeps;
107
+ }
108
+ else if (ui === "stoplight") {
109
+ devDeps = stoplightDevDeps;
110
+ }
111
+ else if (ui === "rapidoc") {
112
+ devDeps = rapidocDevDeps;
113
+ }
114
+ return devDeps.join(" ");
115
+ }
87
116
  async function createDocsPage(ui, outputFile) {
88
117
  if (ui === "none") {
89
118
  return;
@@ -100,17 +129,41 @@ async function createDocsPage(ui, outputFile) {
100
129
  await fs.promises.writeFile(componentPath, docsPage.trim());
101
130
  spinner.succeed(`Created ${paths.join("/")}/page.tsx for ${ui}.`);
102
131
  }
103
- async function installDependencies(ui) {
104
- if (ui === "none") {
105
- return;
106
- }
132
+ async function installDependencies(ui, schema) {
107
133
  const packageManager = await getPackageManager();
108
134
  const installCmd = `${packageManager} ${packageManager === "npm" ? "install" : "add"}`;
109
- const deps = getDocsPageDependencies(ui);
110
- const flags = getDocsPageInstallFlags(ui, packageManager);
111
- spinner.succeed(`Installing ${deps} dependencies...`);
112
- const resp = await execPromise(`${installCmd} ${deps} ${flags}`);
113
- spinner.succeed(`Successfully installed ${deps}.`);
135
+ // Install UI dependencies
136
+ if (ui !== "none") {
137
+ const deps = getDocsPageDependencies(ui);
138
+ const devDeps = getDocsPageDevDependencies(ui);
139
+ const flags = getDocsPageInstallFlags(ui, packageManager);
140
+ if (deps) {
141
+ spinner.succeed(`Installing ${deps} dependencies...`);
142
+ await execPromise(`${installCmd} ${deps} ${flags}`);
143
+ spinner.succeed(`Successfully installed ${deps}.`);
144
+ }
145
+ if (devDeps) {
146
+ const devFlag = packageManager === "npm" ? "--save-dev" : "-D";
147
+ spinner.succeed(`Installing ${devDeps} dev dependencies...`);
148
+ await execPromise(`${installCmd} ${devFlag} ${devDeps} ${flags}`);
149
+ spinner.succeed(`Successfully installed ${devDeps}.`);
150
+ }
151
+ }
152
+ // Install schema dependencies
153
+ const schemaTypes = Array.isArray(schema) ? schema : [schema];
154
+ for (const schemaType of schemaTypes) {
155
+ if (schemaType === "zod" && !(await hasDependency("zod"))) {
156
+ spinner.succeed(`Installing zod...`);
157
+ await execPromise(`${installCmd} zod`);
158
+ spinner.succeed(`Successfully installed zod.`);
159
+ }
160
+ else if (schemaType === "typescript" && !(await hasDependency("typescript"))) {
161
+ const devFlag = packageManager === "npm" ? "--save-dev" : "-D";
162
+ spinner.succeed(`Installing typescript...`);
163
+ await execPromise(`${installCmd} ${devFlag} typescript`);
164
+ spinner.succeed(`Successfully installed typescript.`);
165
+ }
166
+ }
114
167
  }
115
168
  function extendOpenApiTemplate(spec, options) {
116
169
  spec.ui = options.ui ?? spec.ui;
@@ -124,7 +177,7 @@ function getOutputPath(output) {
124
177
  return path.join(process.cwd(), "next.openapi.json");
125
178
  }
126
179
  export async function init(options) {
127
- const { ui, output } = options;
180
+ const { ui, output, schema } = options;
128
181
  spinner.start();
129
182
  try {
130
183
  const outputPath = getOutputPath(output);
@@ -133,7 +186,7 @@ export async function init(options) {
133
186
  await fse.writeJson(outputPath, template, { spaces: 2 });
134
187
  spinner.succeed(`Created OpenAPI template in ${outputPath}`);
135
188
  createDocsPage(ui, template.outputFile);
136
- installDependencies(ui);
189
+ installDependencies(ui, schema);
137
190
  }
138
191
  catch (error) {
139
192
  spinner.fail(`Failed to initialize project: ${error.message}`);
@@ -1,4 +1,5 @@
1
1
  export const rapidocDeps = ["rapidoc"];
2
+ export const rapidocDevDeps = [];
2
3
  export function RapidocUI(outputFile) {
3
4
  return `
4
5
  "use client";
@@ -1,4 +1,5 @@
1
1
  export const redocDeps = ["redoc"];
2
+ export const redocDevDeps = [];
2
3
  export function RedocUI(outputFile) {
3
4
  return `
4
5
  "use client";
@@ -1,4 +1,5 @@
1
1
  export const scalarDeps = ["@scalar/api-reference-react", "ajv"];
2
+ export const scalarDevDeps = [];
2
3
  export function ScalarUI(outputFile) {
3
4
  return `
4
5
  "use client";
@@ -1,4 +1,5 @@
1
1
  export const stoplightDeps = ["@stoplight/elements"];
2
+ export const stoplightDevDeps = [];
2
3
  export function StoplightUI(outputFile) {
3
4
  return `
4
5
  "use client";
@@ -1,4 +1,5 @@
1
1
  export const swaggerDeps = ["swagger-ui", "swagger-ui-react"];
2
+ export const swaggerDevDeps = ["@types/swagger-ui-react"];
2
3
  export function SwaggerUI(outputFile) {
3
4
  return `
4
5
  import "swagger-ui-react/swagger-ui.css";
@@ -6,7 +7,6 @@ import "swagger-ui-react/swagger-ui.css";
6
7
  import dynamic from "next/dynamic";
7
8
 
8
9
  const SwaggerUI = dynamic(() => import("swagger-ui-react"), {
9
- ssr: false,
10
10
  loading: () => <p>Loading Component...</p>,
11
11
  });
12
12
 
package/dist/index.js CHANGED
@@ -11,7 +11,7 @@ program
11
11
  .command("init")
12
12
  .addOption(new Option("-i, --ui <type>", "Specify the UI type, e.g., scalar. Use \"none\" for no UI")
13
13
  .choices(["scalar", "swagger", "redoc", "stoplight", "rapidoc", "none"])
14
- .default("swagger"))
14
+ .default("scalar"))
15
15
  .option("-u, --docs-url <url>", "Specify the docs URL", "api-docs")
16
16
  .addOption(new Option("-s, --schema <schemaType>", "Specify the schema tool")
17
17
  .choices(["zod", "typescript"])
@@ -400,6 +400,54 @@ export class ZodSchemaConverter {
400
400
  });
401
401
  }
402
402
  break;
403
+ case "extend":
404
+ // Extend the schema with new properties
405
+ if (node.arguments.length > 0 &&
406
+ t.isObjectExpression(node.arguments[0])) {
407
+ const extensionProperties = {};
408
+ const extensionRequired = [];
409
+ node.arguments[0].properties.forEach((prop) => {
410
+ if (t.isObjectProperty(prop)) {
411
+ const key = t.isIdentifier(prop.key)
412
+ ? prop.key.name
413
+ : t.isStringLiteral(prop.key)
414
+ ? prop.key.value
415
+ : null;
416
+ if (key) {
417
+ // Process the Zod type for this property
418
+ const propSchema = this.processZodNode(prop.value);
419
+ if (propSchema) {
420
+ extensionProperties[key] = propSchema;
421
+ // Check if the schema itself has nullable set (which processZodNode sets for optional fields)
422
+ const isOptional = propSchema.nullable === true;
423
+ if (!isOptional) {
424
+ extensionRequired.push(key);
425
+ }
426
+ }
427
+ }
428
+ }
429
+ });
430
+ // Merge with existing schema
431
+ if (schema.properties) {
432
+ schema.properties = {
433
+ ...schema.properties,
434
+ ...extensionProperties,
435
+ };
436
+ }
437
+ else {
438
+ schema.properties = extensionProperties;
439
+ }
440
+ // Merge required arrays
441
+ if (extensionRequired.length > 0) {
442
+ schema.required = [
443
+ ...(schema.required || []),
444
+ ...extensionRequired,
445
+ ];
446
+ // Deduplicate
447
+ schema.required = [...new Set(schema.required)];
448
+ }
449
+ }
450
+ break;
403
451
  }
404
452
  return schema;
405
453
  };
@@ -989,8 +1037,9 @@ export class ZodSchemaConverter {
989
1037
  properties,
990
1038
  };
991
1039
  if (required.length > 0) {
1040
+ // Deduplicate required array using Set
992
1041
  // @ts-ignore
993
- schema.required = required;
1042
+ schema.required = [...new Set(required)];
994
1043
  }
995
1044
  return schema;
996
1045
  }
@@ -1371,7 +1420,24 @@ export class ZodSchemaConverter {
1371
1420
  if (node.arguments.length > 0 &&
1372
1421
  t.isObjectExpression(node.arguments[0])) {
1373
1422
  // Get the base schema by processing the object that extend is called on
1374
- const baseSchema = this.processZodNode(node.callee.object);
1423
+ const baseSchemaResult = this.processZodNode(node.callee.object);
1424
+ // If it's a reference, resolve it to the actual schema
1425
+ let baseSchema = baseSchemaResult;
1426
+ if (baseSchemaResult && baseSchemaResult.$ref) {
1427
+ const schemaName = baseSchemaResult.$ref.replace("#/components/schemas/", "");
1428
+ // Try to convert the base schema if not already processed
1429
+ if (!this.zodSchemas[schemaName]) {
1430
+ logger.debug(`[extend] Base schema ${schemaName} not found, attempting to convert it`);
1431
+ this.convertZodSchemaToOpenApi(schemaName);
1432
+ }
1433
+ // Now retrieve the converted schema
1434
+ if (this.zodSchemas[schemaName]) {
1435
+ baseSchema = this.zodSchemas[schemaName];
1436
+ }
1437
+ else {
1438
+ logger.debug(`Could not resolve reference for extend: ${schemaName}`);
1439
+ }
1440
+ }
1375
1441
  // Process the extension object
1376
1442
  const extendNode = {
1377
1443
  type: "CallExpression",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "next-openapi-gen",
3
- "version": "0.8.8",
3
+ "version": "0.9.0",
4
4
  "description": "Automatically generate OpenAPI 3.0 documentation from Next.js projects, with support for Zod schemas and TypeScript types.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",