next-openapi-gen 0.8.9 → 0.9.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/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
 
@@ -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"])
@@ -336,29 +336,34 @@ export class RouteProcessor {
336
336
  getRoutePath(filePath) {
337
337
  // Normalize path separators first
338
338
  const normalizedPath = filePath.replaceAll("\\", "/");
339
- // First, check if it's an app router path
340
- if (normalizedPath.includes("/app/api/")) {
341
- // Get the relative path from the api directory
342
- const apiDirPos = normalizedPath.lastIndexOf("/app/api/");
343
- let relativePath = normalizedPath.substring(apiDirPos + "/app/api".length);
344
- // Remove the /route.ts or /route.tsx suffix
345
- relativePath = relativePath.replace(/\/route\.tsx?$/, "");
346
- // Remove Next.js route groups (folders in parentheses like (authenticated), (marketing))
347
- relativePath = relativePath.replace(/\/\([^)]+\)/g, "");
348
- // Convert Next.js dynamic route syntax to OpenAPI parameter syntax
349
- relativePath = relativePath.replace(/\/\[([^\]]+)\]/g, "/{$1}");
350
- // Handle catch-all routes ([...param])
351
- relativePath = relativePath.replace(/\/\[\.\.\.(.*)\]/g, "/{$1}");
352
- return relativePath;
339
+ // Normalize apiDir to ensure consistent format
340
+ const normalizedApiDir = this.config.apiDir
341
+ .replaceAll("\\", "/")
342
+ .replace(/^\.\//, "")
343
+ .replace(/\/$/, "");
344
+ // Find the apiDir position in the normalized path
345
+ const apiDirIndex = normalizedPath.indexOf(normalizedApiDir);
346
+ if (apiDirIndex === -1) {
347
+ throw new Error(`Could not find apiDir "${this.config.apiDir}" in file path "${filePath}"`);
353
348
  }
354
- // For pages router or other formats
355
- const suffixPath = normalizedPath.split("api")[1];
356
- return suffixPath
357
- .replace(/route\.tsx?$/, "")
358
- .replace(/\/$/, "")
359
- .replace(/\/\([^)]+\)/g, "") // Remove route groups for pages router too
360
- .replace(/\/\[([^\]]+)\]/g, "/{$1}") // Replace [param] with {param}
361
- .replace(/\/\[\.\.\.(.*)\]/g, "/{$1}"); // Replace [...param] with {param}
349
+ // Extract the path after apiDir
350
+ let relativePath = normalizedPath.substring(apiDirIndex + normalizedApiDir.length);
351
+ // Remove the /route.ts or /route.tsx suffix
352
+ relativePath = relativePath.replace(/\/route\.tsx?$/, "");
353
+ // Ensure the path starts with /
354
+ if (!relativePath.startsWith("/")) {
355
+ relativePath = "/" + relativePath;
356
+ }
357
+ // Remove trailing slash
358
+ relativePath = relativePath.replace(/\/$/, "");
359
+ // Remove Next.js route groups (folders in parentheses like (authenticated), (marketing))
360
+ relativePath = relativePath.replace(/\/\([^)]+\)/g, "");
361
+ // Handle catch-all routes ([...param]) before converting dynamic routes
362
+ // This must come first because [...param] would also match the [param] pattern
363
+ relativePath = relativePath.replace(/\/\[\.\.\.(.*?)\]/g, "/{$1}");
364
+ // Convert Next.js dynamic route syntax to OpenAPI parameter syntax
365
+ relativePath = relativePath.replace(/\/\[([^\]]+)\]/g, "/{$1}");
366
+ return relativePath || "/";
362
367
  }
363
368
  getSortedPaths(paths) {
364
369
  function comparePaths(a, b) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "next-openapi-gen",
3
- "version": "0.8.9",
3
+ "version": "0.9.1",
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",