Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: experimental defineRouteMeta #2102

Merged
merged 35 commits into from
Apr 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
2d41b59
openapi test and parser init
eriksLapins Jan 20, 2024
cc55d92
changed the config type to function
eriksLapins Jan 20, 2024
3d6a39e
first working example
Jan 20, 2024
5fbaf10
project cleanup author change
eriksLapins Jan 20, 2024
a2c0864
split schema into its own object
eriksLapins Jan 20, 2024
a697883
prettier formatting from test
eriksLapins Jan 20, 2024
1c6bd41
changed routes to relative to nitro
eriksLapins Jan 20, 2024
ceb7ee1
defineRouteMeta function and openapi schema at build time
eriksLapins Jan 21, 2024
76aa104
from regex to AST parsing
eriksLapins Jan 22, 2024
4b6f151
prettier lint
eriksLapins Jan 22, 2024
21264e2
added documentation for defineRouteMeta
eriksLapins Jan 22, 2024
8ad1bcd
fix to docs
eriksLapins Jan 22, 2024
0a2926a
Merge branch 'main' into main
eriksLapins Jan 23, 2024
c863fff
Merge branch 'main' into main
eriksLapins Jan 24, 2024
9123ab0
Merge branch 'main' into main
eriksLapins Feb 8, 2024
743c621
Merge branch 'main' into pr/eriksLapins/2102
pi0 Apr 4, 2024
c444513
chore: apply automated fixes
autofix-ci[bot] Apr 4, 2024
1d52917
refactor as rollup plugin for hmr support
pi0 Apr 4, 2024
0cba457
revert unnecessary await
pi0 Apr 9, 2024
30e2b84
small updates
pi0 Apr 9, 2024
0e742ab
update
pi0 Apr 9, 2024
9751c82
update docs
pi0 Apr 9, 2024
4c64ad2
update
pi0 Apr 9, 2024
517a555
update docs
pi0 Apr 10, 2024
d664984
add experimental guard for now
pi0 Apr 10, 2024
e77cd87
update impl
pi0 Apr 10, 2024
b3b9ae6
relax type checks
pi0 Apr 10, 2024
63ee23a
refactor names
pi0 Apr 10, 2024
6d51573
fix typo
pi0 Apr 10, 2024
1b183fc
revert extra change
pi0 Apr 10, 2024
29bbcbd
refactor types
pi0 Apr 10, 2024
6701bc5
update type import
pi0 Apr 10, 2024
fb6be11
add basic test
pi0 Apr 10, 2024
69b2855
Merge branch 'main' into main
pi0 Apr 10, 2024
ec75ad7
remove unused import
pi0 Apr 10, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/1.guide/1.utils.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ Nitro also exposes several built-in utils:
- `defineCachedFunction(fn, options)`{lang=ts} / `cachedFunction(fn, options)`{lang=ts}
- `defineCachedEventHandler(handler, options)`{lang=ts} / `cachedEventHandler(handler, options)`{lang=ts}
- `defineRenderHandler(handler)`{lang=ts}
- `defineRouteMeta(options)`{lang=ts} (experimental)
- `useRuntimeConfig(event?)`{lang=ts}
- `useAppConfig(event?)`{lang=ts}
- `useStorage(base?)`{lang=ts}
Expand Down
23 changes: 23 additions & 0 deletions docs/1.guide/2.routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,29 @@ Middleware in `middleware/` directory are automatically registered for all route
Returning anything from a middleware will close the request and should be avoided! Any returned value from middleware will be the response and further code will not be executed however **this is not recommended to do!**
::

### Route Meta

You can define route handler meta at build-time using `defineRouteMeta` micro in the event handler files.

> [!NOTE]
> This feature is currently available in [nightly channel](https://nitro.unjs.io/guide/nightly) only.

```ts [/api/test.ts]
defineRouteMeta({
openAPI: {
tags: ["test"],
description: "Test route description",
parameters: [{ in: "query", name: "test", required: true }],
},
});

export default defineEventHandler(() => "OK");
```

::read-more{to="https://swagger.io/specification/v3/"}
This feature is currntly usable to specify OpenAPI meta. See swagger specification for available OpenAPI options.
::

### Execution order

Middleware are executed in directory listing order.
Expand Down
1 change: 1 addition & 0 deletions src/imports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const nitroImports: Preset[] = [
"defineNitroPlugin",
"nitroPlugin",
"defineRenderHandler",
"defineRouteMeta",
"getRouteRules",
"useAppConfig",
"useEvent",
Expand Down
6 changes: 6 additions & 0 deletions src/rollup/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { timing } from "./plugins/timing";
import { publicAssets } from "./plugins/public-assets";
import { serverAssets } from "./plugins/server-assets";
import { handlers } from "./plugins/handlers";
import { handlersMeta } from "./plugins/handlers-meta";
import { esbuild } from "./plugins/esbuild";
import { raw } from "./plugins/raw";
import { storage } from "./plugins/storage";
Expand Down Expand Up @@ -306,6 +307,11 @@ export const getRollupConfig = (nitro: Nitro): RollupConfig => {
// Handlers
rollupConfig.plugins.push(handlers(nitro));

// Handlers meta
if (nitro.options.experimental.openAPI) {
rollupConfig.plugins.push(handlersMeta(nitro));
}

// Polyfill
rollupConfig.plugins.push(
virtual(
Expand Down
95 changes: 95 additions & 0 deletions src/rollup/plugins/handlers-meta.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { readFile } from "node:fs/promises";
import { transform } from "esbuild";
import type { Plugin } from "rollup";
import type { Literal, Expression } from "estree";
import { extname } from "pathe";
import { Nitro, NitroEventHandler } from "../../types";

const virtualPrefix = "\0nitro-handler-meta:";

// From esbuild.ts
const esbuildLoaders = {
".ts": "ts",
".js": "js",
".tsx": "tsx",
".jsx": "jsx",
};

export function handlersMeta(nitro: Nitro) {
return {
name: "nitro:handlers-meta",
async resolveId(id) {
if (id.startsWith("\0")) {
return;
}
if (id.endsWith(`?meta`)) {
const resolved = await this.resolve(id.replace(`?meta`, ``));
return virtualPrefix + resolved.id;
}
},
load(id) {
if (id.startsWith(virtualPrefix)) {
const fullPath = id.slice(virtualPrefix.length);
return readFile(fullPath, { encoding: "utf8" });
}
},
async transform(code, id) {
if (!id.startsWith(virtualPrefix)) {
return;
}

let meta: NitroEventHandler["meta"] | null = null;

try {
const ext = extname(id);
const jsCode = await transform(code, {
loader: esbuildLoaders[ext],
}).then((r) => r.code);
const ast = this.parse(jsCode);
for (const node of ast.body) {
if (
node.type === "ExpressionStatement" &&
node.expression.type === "CallExpression" &&
node.expression.callee.type === "Identifier" &&
node.expression.callee.name === "defineRouteMeta" &&
node.expression.arguments.length === 1
) {
meta = astToObject(node.expression.arguments[0] as any);
break;
}
}
} catch (err) {
console.warn(
`[nitro] [handlers-meta] Cannot extra route meta for: ${id}: ${err}`
);
}

return {
code: `export default ${JSON.stringify(meta)};`,
map: null,
};
},
} satisfies Plugin;
}

function astToObject(node: Expression | Literal) {
switch (node.type) {
case "ObjectExpression": {
const obj: Record<string, any> = {};
for (const prop of node.properties) {
if (prop.type === "Property") {
const key = (prop.key as any).name;
obj[key] = astToObject(prop.value as any);
}
}
return obj;
}
case "ArrayExpression": {
return node.elements.map((el) => astToObject(el as any)).filter(Boolean);
}
case "Literal": {
return node.value;
}
// No default
}
}
31 changes: 19 additions & 12 deletions src/rollup/plugins/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,16 +55,7 @@ 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 = `
const code = /* js */ `
${imports
.map((handler) => `import ${getImportId(handler)} from '${handler}';`)
.join("\n")}
Expand All @@ -89,11 +80,27 @@ ${handlers
)
.join(",\n")}
];

export const handlersMeta = ${JSON.stringify(handlersMeta, null, 2)}
`.trim();
return code;
},
"#internal/nitro/virtual/server-handlers-meta": () => {
const handlers = getHandlers();
return /* js */ `
${handlers
.map(
(h) => `import ${getImportId(h.handler)}Meta from "${h.handler}?meta";`
)
.join("\n")}
export const handlersMeta = [
${handlers
.map(
(h) =>
/* js */ `{ route: ${JSON.stringify(h.route)}, method: ${JSON.stringify(h.method)}, meta: ${getImportId(h.handler)}Meta }`
)
.join(",\n")}
];
`;
},
},
nitro.vfs
);
Expand Down
1 change: 1 addition & 0 deletions src/runtime/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export * from "./plugin";
export * from "./task";
export * from "./renderer";
export { getRouteRules, getRouteRulesForPath } from "./route-rules";
export { defineRouteMeta } from "./meta";
export { useStorage } from "./storage";
export { useEvent } from "./context";
export { defineNitroErrorHandler } from "./error";
5 changes: 5 additions & 0 deletions src/runtime/meta.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { NitroRouteMeta } from "nitropack";

export function defineRouteMeta(meta: NitroRouteMeta) {
return meta;
}
13 changes: 10 additions & 3 deletions src/runtime/routes/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type {
PathsObject,
} from "openapi-typescript";
import { joinURL } from "ufo";
import { handlersMeta } from "#internal/nitro/virtual/server-handlers";
import { handlersMeta } from "#internal/nitro/virtual/server-handlers-meta";
import { useRuntimeConfig } from "#internal/nitro";

// Served as /_nitro/openapi.json
Expand Down Expand Up @@ -44,8 +44,8 @@ function getPaths(): PathsObject {
const paths: PathsObject = {};

for (const h of handlersMeta) {
const { route, parameters } = normalizeRoute(h.route);
const tags = defaultTags(h.route);
const { route, parameters } = normalizeRoute(h.route || "");
const tags = defaultTags(h.route || "");
const method = (h.method || "get").toLowerCase();

const item: PathItemObject = {
Expand All @@ -63,6 +63,13 @@ function getPaths(): PathsObject {
} else {
Object.assign(paths[route], item);
}

if (h.meta?.openAPI) {
paths[route][method] = {
...paths[route][method],
...h.meta.openAPI,
};
}
}

return paths;
Expand Down
8 changes: 8 additions & 0 deletions src/runtime/virtual/server-handlers-meta.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { OperationObject } from "openapi-typescript";
import { NitroRouteMeta } from "../../types";

export const handlersMeta: {
route?: string;
method?: string;
meta?: NitroRouteMeta;
}[];
7 changes: 0 additions & 7 deletions src/runtime/virtual/server-handlers.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,3 @@ export type HandlerDefinition = {
};

export const handlers: HandlerDefinition[];

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

export const handlersMeta: HandlerMeta[];
13 changes: 12 additions & 1 deletion src/types/handler.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import type { EventHandler, H3Event, H3Error } from "h3";
import type { EventHandler, H3Error, H3Event } from "h3";
import type { OperationObject } from "openapi-typescript";
import { NitroOptions } from "./nitro";

type MaybeArray<T> = T | T[];

/** @exprerimental */
export interface NitroRouteMeta {
openAPI?: OperationObject;
}

export interface NitroEventHandler {
/**
* Path prefix or route
Expand Down Expand Up @@ -34,6 +40,11 @@ export interface NitroEventHandler {
method?: string;

/**
* Meta
*/
meta?: NitroRouteMeta;

/*
* Environments to include this handler
*/
env?: MaybeArray<
Expand Down
9 changes: 9 additions & 0 deletions test/fixture/api/meta/test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
defineRouteMeta({
openAPI: {
tags: ["test"],
description: "Test route description",
parameters: [{ in: "query", name: "test", required: true }],
},
});

export default defineEventHandler(() => "OK");
37 changes: 37 additions & 0 deletions test/presets/nitro-dev.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { describe, it, expect } from "vitest";
import { isCI } from "std-env";
import type { OpenAPI3 } from "openapi-typescript";
import { setupTest, testNitro } from "../tests";

describe.skipIf(isCI)("nitro:preset:nitro-dev", async () => {
Expand All @@ -21,6 +22,42 @@ describe.skipIf(isCI)("nitro:preset:nitro-dev", async () => {
const { status } = await callHandler({ url: "/proxy/example" });
expect(status).toBe(200);
});

describe("openAPI", () => {
let spec: OpenAPI3;
it("/_nitro/openapi.json", async () => {
spec = ((await callHandler({ url: "/_nitro/openapi.json" })) as any)
.data;
expect(spec.openapi).to.match(/^3\.\d+\.\d+$/);
expect(spec.info.title).toBe("Nitro Test Fixture");
expect(spec.info.description).toBe("Nitro Test Fixture API");
});

it("defineRouteMeta works", () => {
expect(spec.paths["/api/meta/test"]).toMatchInlineSnapshot(`
{
"get": {
"description": "Test route description",
"parameters": [
{
"in": "query",
"name": "test",
"required": true,
},
],
"responses": {
"200": {
"description": "OK",
},
},
"tags": [
"test",
],
},
}
`);
});
});
}
);
});