From 720fe297384c20593b32ca9820d47ad4221435bc Mon Sep 17 00:00:00 2001 From: pooya parsa Date: Fri, 21 Apr 2023 12:36:11 +0200 Subject: [PATCH] feat: experimental `/_nitro/openapi.json` and `/_nitro/swagger` for dev mode (#1162) --- package.json | 1 + pnpm-lock.yaml | 30 +++++++++-- src/options.ts | 12 +++++ src/rollup/plugins/handlers.ts | 22 ++++++-- src/runtime/routes/openapi.ts | 69 ++++++++++++++++++++++++ src/runtime/routes/swagger.ts | 44 +++++++++++++++ src/runtime/virtual/server-handlers.d.ts | 9 +++- test/fixture/nitro.config.ts | 6 +++ 8 files changed, 183 insertions(+), 10 deletions(-) create mode 100644 src/runtime/routes/openapi.ts create mode 100644 src/runtime/routes/swagger.ts diff --git a/package.json b/package.json index ad49ce01af..6c16a6ca24 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3f3b19688f..3583d2f393 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -134,6 +134,9 @@ importers: ohash: specifier: ^1.1.1 version: 1.1.1 + openapi-typescript: + specifier: ^6.2.1 + version: 6.2.1 pathe: specifier: ^1.1.0 version: 1.1.0 @@ -1512,6 +1515,11 @@ packages: uri-js: 4.4.1 dev: true + /ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + dev: false + /ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -1588,7 +1596,6 @@ packages: /argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - dev: true /array-buffer-byte-length@1.0.0: resolution: {integrity: sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==} @@ -1764,7 +1771,6 @@ packages: engines: {node: '>=10.16.0'} dependencies: streamsearch: 1.1.0 - dev: true /c12@1.4.1: resolution: {integrity: sha512-0x7pWfLZpZsgtyotXtuepJc0rZYE0Aw8PwNAXs0jSG9zq6Sl5xmbWnFqfmRY01ieZLHNbvneSFm9/x88CvzAuw==} @@ -3608,7 +3614,6 @@ packages: hasBin: true dependencies: argparse: 2.0.1 - dev: true /jsesc@2.5.2: resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} @@ -4156,6 +4161,18 @@ packages: is-wsl: 2.2.0 dev: true + /openapi-typescript@6.2.1: + resolution: {integrity: sha512-l0u3fWE7vOCSWl/FxNcB3zu/jDqi/NUCP7MSbffpCZdBwQAbljHbo+CAjPXpf47SAVE+F7ZQKOew+pnXfV1oGA==} + hasBin: true + dependencies: + ansi-colors: 4.1.3 + fast-glob: 3.2.12 + js-yaml: 4.1.0 + supports-color: 9.3.1 + undici: 5.22.0 + yargs-parser: 21.1.1 + dev: false + /optionator@0.9.1: resolution: {integrity: sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==} engines: {node: '>= 0.8.0'} @@ -4759,7 +4776,6 @@ packages: /streamsearch@1.1.0: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} - dev: true /string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} @@ -4857,6 +4873,11 @@ packages: has-flag: 4.0.0 dev: true + /supports-color@9.3.1: + resolution: {integrity: sha512-knBY82pjmnIzK3NifMo3RxEIRD9E0kIzV4BKcyTZ9+9kWgLMxd4PrsTSMoFQUabgRBbF8KOLRDCyKgNV+iK44Q==} + engines: {node: '>=12'} + dev: false + /supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} @@ -5106,7 +5127,6 @@ packages: engines: {node: '>=14.0'} dependencies: busboy: 1.6.0 - dev: true /unenv@1.4.1: resolution: {integrity: sha512-DuFZUDfaBC92zy3fW7QqKTLdYJIPkpwTN0yGZtaxnpOI7HvIfl41NYh9NVv4zcqhT8CGXJ1ELpvO2tecaB6NfA==} diff --git a/src/options.ts b/src/options.ts index 7e71b257b9..7278206977 100644 --- a/src/options.ts +++ b/src/options.ts @@ -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; } diff --git a/src/rollup/plugins/handlers.ts b/src/rollup/plugins/handlers.ts index b66931b411..b053070651 100644 --- a/src/rollup/plugins/handlers.ts +++ b/src/rollup/plugins/handlers.ts @@ -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, @@ -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}';`) @@ -63,6 +75,8 @@ ${handlers ) .join(",\n")} ]; + +export const handlersMeta = ${JSON.stringify(handlersMeta, null, 2)} `.trim(); return code; }, diff --git a/src/runtime/routes/openapi.ts b/src/runtime/routes/openapi.ts new file mode 100644 index 0000000000..d52df1976b --- /dev/null +++ b/src/runtime/routes/openapi.ts @@ -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 { + 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()]: { + tags, + parameters, + responses: { + 200: { description: "OK" }, + }, + }, + }; + return [route, item]; + }) + ), + }, + }; +}); diff --git a/src/runtime/routes/swagger.ts b/src/runtime/routes/swagger.ts new file mode 100644 index 0000000000..e805a493da --- /dev/null +++ b/src/runtime/routes/swagger.ts @@ -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` + + + + + + ${title} + + + +
+ + + + + `; +}); + +function html(str, ...args) { + return String.raw(str, ...args); +} diff --git a/src/runtime/virtual/server-handlers.d.ts b/src/runtime/virtual/server-handlers.d.ts index e224ed54c6..aecad768a5 100644 --- a/src/runtime/virtual/server-handlers.d.ts +++ b/src/runtime/virtual/server-handlers.d.ts @@ -1,6 +1,6 @@ import type { H3EventHandler, LazyEventHandler, RouterMethod } from "h3"; -type HandlerDefinition = { +export type HandlerDefinition = { route: string; lazy?: boolean; middleware?: boolean; @@ -12,3 +12,10 @@ type HandlerDefinition = { }; export const handlers: HandlerDefinition[]; + +export type HandlerMeta = { + route: string; + method?: RouterMethod; +}; + +export const handlersMeta: HandlerMeta[]; diff --git a/test/fixture/nitro.config.ts b/test/fixture/nitro.config.ts index b06f347082..b75728e6ed 100644 --- a/test/fixture/nitro.config.ts +++ b/test/fixture/nitro.config.ts @@ -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 }, },