Skip to content

Commit

Permalink
feat: experimental /_nitro/openapi.json and /_nitro/swagger for …
Browse files Browse the repository at this point in the history
…dev mode (#1162)
  • Loading branch information
pi0 authored Apr 21, 2023
1 parent 1d218eb commit 720fe29
Show file tree
Hide file tree
Showing 8 changed files with 183 additions and 10 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@
"node-fetch-native": "^1.1.0",
"ofetch": "^1.0.1",
"ohash": "^1.1.1",
"openapi-typescript": "^6.2.1",
"pathe": "^1.1.0",
"perfect-debounce": "^0.1.3",
"pkg-types": "^1.0.2",
Expand Down
30 changes: 25 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,18 @@ export async function loadOptions(
// Resolve plugin paths
options.plugins = options.plugins.map((p) => resolvePath(p, options));

// Add open-api endpoint
if (options.dev) {
options.handlers.push({
route: "/_nitro/openapi.json",
handler: "#internal/nitro/routes/openapi",
});
options.handlers.push({
route: "/_nitro/swagger",
handler: "#internal/nitro/routes/swagger",
});
}

return options;
}

Expand Down
22 changes: 18 additions & 4 deletions src/rollup/plugins/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@ import type { Nitro, NitroRouteRules, NitroEventHandler } from "../../types";
import { virtual } from "./virtual";

export function handlers(nitro: Nitro) {
const getHandlers = () =>
[
...nitro.scannedHandlers,
...nitro.options.handlers,
] as NitroEventHandler[];

return virtual(
{
"#internal/nitro/virtual/server-handlers": () => {
const handlers: NitroEventHandler[] = [
...nitro.scannedHandlers,
...nitro.options.handlers,
];
const handlers = getHandlers();
if (nitro.options.serveStatic) {
handlers.unshift({
middleware: true,
Expand Down Expand Up @@ -38,6 +41,15 @@ export function handlers(nitro: Nitro) {
handlers.filter((h) => h.lazy).map((h) => h.handler)
);

const handlersMeta = getHandlers()
.filter((h) => h.route)
.map((h) => {
return {
route: h.route,
method: h.method,
};
});

const code = `
${imports
.map((handler) => `import ${getImportId(handler)} from '${handler}';`)
Expand All @@ -63,6 +75,8 @@ ${handlers
)
.join(",\n")}
];
export const handlersMeta = ${JSON.stringify(handlersMeta, null, 2)}
`.trim();
return code;
},
Expand Down
69 changes: 69 additions & 0 deletions src/runtime/routes/openapi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { eventHandler } from "h3";
import type {
OpenAPI3,
PathItemObject,
OperationObject,
ParameterObject,
} from "openapi-typescript";
import { handlersMeta } from "#internal/nitro/virtual/server-handlers";

// Served as /_nitro/openapi.json
export default eventHandler((event) => {
return <OpenAPI3>{
openapi: "3.0.0",
info: {
title: "Nitro Server Routes",
version: null,
},
servers: [
{
url: "http://localhost:3000",
description: "Local Development Server",
variables: {},
},
],
schemes: ["http"],
paths: {
...Object.fromEntries(
handlersMeta.map((h) => {
const parameters: ParameterObject[] = [];

let anonymouseCtr = 0;
const route = h.route
.replace(/:(\w+)/g, (_, name) => `{${name}}`)
.replace(/\/(\*)\//g, () => `/{param${++anonymouseCtr}}/`)
.replace(/\*\*{/, "{")
.replace(/\/(\*\*)$/g, () => `/{*param${++anonymouseCtr}}`);

const paramMatches = route.matchAll(/{(\*?\w+)}/g);
for (const match of paramMatches) {
const name = match[1];
if (!parameters.some((p) => p.name === name)) {
parameters.push({ name, in: "path", required: true });
}
}

const tags: string[] = [];
if (route.startsWith("/api/")) {
tags.push("API Routes");
} else if (route.startsWith("/_")) {
tags.push("Internal");
} else {
tags.push("App Routes");
}

const item: PathItemObject = {
[(h.method || "get").toLowerCase()]: <OperationObject>{
tags,
parameters,
responses: {
200: { description: "OK" },
},
},
};
return [route, item];
})
),
},
};
});
44 changes: 44 additions & 0 deletions src/runtime/routes/swagger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { eventHandler } from "h3";

// https://github.com/swagger-api/swagger-ui

// Served as /_nitro/swagger
export default eventHandler((event) => {
const title = "Nitro Swagger UI";
const CDN_BASE = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@^4";
return html`<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="${title}" />
<title>${title}</title>
<link rel="stylesheet" href="${CDN_BASE}/swagger-ui.css" />
</head>
<body>
<div id="swagger-ui"></div>
<script src="${CDN_BASE}/swagger-ui-bundle.js" crossorigin></script>
<script
src="${CDN_BASE}/swagger-ui-standalone-preset.js"
crossorigin
></script>
<script>
window.onload = () => {
window.ui = SwaggerUIBundle({
url: "./openapi.json",
dom_id: "#swagger-ui",
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset,
],
layout2: "StandaloneLayout",
});
};
</script>
</body>
</html> `;
});

function html(str, ...args) {
return String.raw(str, ...args);
}
9 changes: 8 additions & 1 deletion src/runtime/virtual/server-handlers.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { H3EventHandler, LazyEventHandler, RouterMethod } from "h3";

type HandlerDefinition = {
export type HandlerDefinition = {
route: string;
lazy?: boolean;
middleware?: boolean;
Expand All @@ -12,3 +12,10 @@ type HandlerDefinition = {
};

export const handlers: HandlerDefinition[];

export type HandlerMeta = {
route: string;
method?: RouterMethod;
};

export const handlersMeta: HandlerMeta[];
6 changes: 6 additions & 0 deletions test/fixture/nitro.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ export default defineNitroConfig({
},
],
},
handlers: [
{
route: "/api/test/*/foo",
handler: "~/api/hello.ts",
},
],
devProxy: {
"/proxy/example": { target: "https://example.com", changeOrigin: true },
},
Expand Down

0 comments on commit 720fe29

Please sign in to comment.