@kizunajs/next 2.0.0-beta.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/LICENSE.MD +9 -0
- package/README.md +40 -0
- package/dist/index.cjs +219 -0
- package/dist/index.d.cts +107 -0
- package/dist/index.d.mts +107 -0
- package/dist/index.mjs +204 -0
- package/package.json +74 -0
package/LICENSE.MD
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sondre Ørland
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# @kizunajs/next
|
|
2
|
+
|
|
3
|
+
`@kizunajs/next` connects a Kizuna API to the Next.js App Router, as a catch-all route handler.
|
|
4
|
+
|
|
5
|
+
**Requires Next.js >= 16.**
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
pnpm add @kizunajs/next
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
// kizuna.config.ts
|
|
17
|
+
import { defineConfig } from 'kizunajs';
|
|
18
|
+
import { nextAdapter } from '@kizunajs/next';
|
|
19
|
+
import { routes } from './src/routes';
|
|
20
|
+
|
|
21
|
+
export default defineConfig({
|
|
22
|
+
adapter: nextAdapter(),
|
|
23
|
+
routes,
|
|
24
|
+
});
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Export the handlers from a catch-all route:
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
// src/app/api/[...kizuna]/route.ts
|
|
31
|
+
import kizuna from '../../../../kizuna.config';
|
|
32
|
+
|
|
33
|
+
export const { GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS } = kizuna.api.mount({
|
|
34
|
+
basePath: '/api',
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Documentation
|
|
39
|
+
|
|
40
|
+
[Next.js adapter](https://kizunajs.com/docs/adapters/next)
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let kizunajs_adapter = require("kizunajs/adapter");
|
|
3
|
+
let next_server_js = require("next/server.js");
|
|
4
|
+
//#region src/server.ts
|
|
5
|
+
const jsonResponse = (status, body, headers, raw = false) => new next_server_js.NextResponse(body === null || body === void 0 ? null : raw ? body : JSON.stringify(body), {
|
|
6
|
+
status,
|
|
7
|
+
headers
|
|
8
|
+
});
|
|
9
|
+
const handleNextRequest = async (request, routes, router, options, guards, schemes, requestContext, pluginExports, jobs) => {
|
|
10
|
+
const url = new URL(request.url);
|
|
11
|
+
const adapter = (0, kizunajs_adapter.createAdapter)({
|
|
12
|
+
buildHandlerContext: (adapterRequest) => ({ request: adapterRequest.request }),
|
|
13
|
+
respond: (result) => {
|
|
14
|
+
if (result.kind === "raw-response") return result.response;
|
|
15
|
+
const rendered = (0, kizunajs_adapter.renderJsonResult)(result, options?.formatError, request, request.method);
|
|
16
|
+
if (rendered.stream) return new next_server_js.NextResponse(rendered.stream({
|
|
17
|
+
signal: request.signal,
|
|
18
|
+
validate: options?.responseValidation
|
|
19
|
+
}), {
|
|
20
|
+
status: rendered.status,
|
|
21
|
+
headers: rendered.headers
|
|
22
|
+
});
|
|
23
|
+
return jsonResponse(rendered.status, rendered.body, rendered.headers, rendered.raw);
|
|
24
|
+
},
|
|
25
|
+
onError: async (error) => {
|
|
26
|
+
if (!options?.onError) {
|
|
27
|
+
console.error("[kizuna/next] handler error:", error);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const override = await options.onError(error, request);
|
|
31
|
+
if (override) return {
|
|
32
|
+
kind: "raw-response",
|
|
33
|
+
response: override
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
if (jobs) {
|
|
38
|
+
const matchedJob = (0, kizunajs_adapter.matchRoute)(request.method, url.pathname, jobs.routes, options?.basePath);
|
|
39
|
+
if (matchedJob.kind === "matched") {
|
|
40
|
+
const jobRequest = {
|
|
41
|
+
request,
|
|
42
|
+
method: request.method,
|
|
43
|
+
resolution: {
|
|
44
|
+
kind: "pre-resolved",
|
|
45
|
+
routeKey: matchedJob.match.routeKey,
|
|
46
|
+
route: matchedJob.match.route,
|
|
47
|
+
params: matchedJob.match.params
|
|
48
|
+
},
|
|
49
|
+
query: Object.fromEntries(url.searchParams),
|
|
50
|
+
headers: (0, kizunajs_adapter.headersToObject)(request.headers),
|
|
51
|
+
readBody: (route) => (0, kizunajs_adapter.parseFetchBody)(request, route)
|
|
52
|
+
};
|
|
53
|
+
return adapter.handle({
|
|
54
|
+
routes: jobs.routes,
|
|
55
|
+
router: jobs.router,
|
|
56
|
+
request: jobRequest,
|
|
57
|
+
responseContext: {},
|
|
58
|
+
guards,
|
|
59
|
+
schemes,
|
|
60
|
+
requestContext,
|
|
61
|
+
pluginExports,
|
|
62
|
+
jobs: jobs.runner,
|
|
63
|
+
responseValidation: options?.responseValidation
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const globalMiddleware = options?.requestMiddleware;
|
|
68
|
+
if (globalMiddleware && globalMiddleware.length > 0) {
|
|
69
|
+
const matched = (0, kizunajs_adapter.matchRoute)(request.method, url.pathname, routes, options?.basePath);
|
|
70
|
+
if (matched.kind === "matched") {
|
|
71
|
+
const middlewareRoute = {
|
|
72
|
+
path: matched.match.route.path,
|
|
73
|
+
method: matched.match.route.method
|
|
74
|
+
};
|
|
75
|
+
for (const handler of globalMiddleware) {
|
|
76
|
+
const result = await handler(request, middlewareRoute);
|
|
77
|
+
if (result instanceof Response) return new next_server_js.NextResponse(result.body, {
|
|
78
|
+
status: result.status,
|
|
79
|
+
statusText: result.statusText,
|
|
80
|
+
headers: result.headers
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
const adapterRequest = {
|
|
84
|
+
request,
|
|
85
|
+
method: request.method,
|
|
86
|
+
resolution: {
|
|
87
|
+
kind: "pre-resolved",
|
|
88
|
+
routeKey: matched.match.routeKey,
|
|
89
|
+
route: matched.match.route,
|
|
90
|
+
params: matched.match.params
|
|
91
|
+
},
|
|
92
|
+
query: Object.fromEntries(url.searchParams),
|
|
93
|
+
headers: (0, kizunajs_adapter.headersToObject)(request.headers),
|
|
94
|
+
readBody: (route) => (0, kizunajs_adapter.parseFetchBody)(request, route)
|
|
95
|
+
};
|
|
96
|
+
return adapter.handle({
|
|
97
|
+
routes,
|
|
98
|
+
router,
|
|
99
|
+
request: adapterRequest,
|
|
100
|
+
responseContext: {},
|
|
101
|
+
guards,
|
|
102
|
+
schemes,
|
|
103
|
+
requestContext,
|
|
104
|
+
jobs: jobs?.runner,
|
|
105
|
+
responseValidation: options?.responseValidation
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const adapterRequest = {
|
|
110
|
+
request,
|
|
111
|
+
method: request.method,
|
|
112
|
+
resolution: {
|
|
113
|
+
kind: "core-match",
|
|
114
|
+
path: url.pathname
|
|
115
|
+
},
|
|
116
|
+
query: Object.fromEntries(url.searchParams),
|
|
117
|
+
headers: (0, kizunajs_adapter.headersToObject)(request.headers),
|
|
118
|
+
readBody: (route) => (0, kizunajs_adapter.parseFetchBody)(request, route)
|
|
119
|
+
};
|
|
120
|
+
return adapter.handle({
|
|
121
|
+
routes,
|
|
122
|
+
router,
|
|
123
|
+
request: adapterRequest,
|
|
124
|
+
responseContext: {},
|
|
125
|
+
guards,
|
|
126
|
+
schemes,
|
|
127
|
+
requestContext,
|
|
128
|
+
pluginExports,
|
|
129
|
+
jobs: jobs?.runner,
|
|
130
|
+
basePath: options?.basePath,
|
|
131
|
+
responseValidation: options?.responseValidation
|
|
132
|
+
});
|
|
133
|
+
};
|
|
134
|
+
/**
|
|
135
|
+
* Create endpoints for a Next.js App Router catch-all route.
|
|
136
|
+
*
|
|
137
|
+
* @example
|
|
138
|
+
* // app/api/[...kizuna]/route.ts
|
|
139
|
+
* export const { GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS } = api.mount({
|
|
140
|
+
* basePath: '/api',
|
|
141
|
+
* });
|
|
142
|
+
*/
|
|
143
|
+
function mountNext(api, options) {
|
|
144
|
+
const guards = api[kizunajs_adapter.GUARDS_META];
|
|
145
|
+
const schemes = api[kizunajs_adapter.SCHEMES_META];
|
|
146
|
+
const requestContext = api[kizunajs_adapter.REQUEST_CONTEXT_META];
|
|
147
|
+
const jobsMeta = api[kizunajs_adapter.JOBS_META];
|
|
148
|
+
const mountedJobs = jobsMeta ? {
|
|
149
|
+
routes: (0, kizunajs_adapter.jobRoutes)(jobsMeta),
|
|
150
|
+
router: (0, kizunajs_adapter.jobRouter)(jobsMeta),
|
|
151
|
+
runner: (0, kizunajs_adapter.jobRunnerFrom)(jobsMeta)
|
|
152
|
+
} : void 0;
|
|
153
|
+
const handlerOptions = {
|
|
154
|
+
basePath: options?.basePath,
|
|
155
|
+
onError: options?.onError,
|
|
156
|
+
requestMiddleware: options?.requestMiddleware,
|
|
157
|
+
responseValidation: options?.responseValidation
|
|
158
|
+
};
|
|
159
|
+
const pluginExports = (0, kizunajs_adapter.pluginExportsOf)(api);
|
|
160
|
+
const pluginRoutes = (0, kizunajs_adapter.pluginRoutesOf)(api);
|
|
161
|
+
const pluginRouter = (0, kizunajs_adapter.pluginRouterOf)(api);
|
|
162
|
+
const hasPluginRoutes = Object.keys(pluginRoutes).length > 0;
|
|
163
|
+
const handler = async (request) => {
|
|
164
|
+
if (hasPluginRoutes) {
|
|
165
|
+
const pathname = new URL(request.url).pathname;
|
|
166
|
+
if (!((0, kizunajs_adapter.matchRoute)(request.method, pathname, api.routes, options?.basePath).kind === "matched") && (0, kizunajs_adapter.matchRoute)(request.method, pathname, pluginRoutes, options?.basePath).kind === "matched") return handleNextRequest(request, pluginRoutes, pluginRouter, handlerOptions, guards, schemes, requestContext, pluginExports, void 0);
|
|
167
|
+
}
|
|
168
|
+
return handleNextRequest(request, api.routes, api[kizunajs_adapter.ROUTER_META], handlerOptions, guards, schemes, requestContext, pluginExports, mountedJobs);
|
|
169
|
+
};
|
|
170
|
+
return {
|
|
171
|
+
GET: handler,
|
|
172
|
+
HEAD: handler,
|
|
173
|
+
POST: handler,
|
|
174
|
+
PUT: handler,
|
|
175
|
+
PATCH: handler,
|
|
176
|
+
DELETE: handler,
|
|
177
|
+
OPTIONS: handler
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Serve an API on Next. What it takes is what every route is served with. Next
|
|
182
|
+
* routes by file, so mounting returns the route handlers to re-export rather
|
|
183
|
+
* than registering them on an app.
|
|
184
|
+
*
|
|
185
|
+
* @example
|
|
186
|
+
* // kizuna.config.ts
|
|
187
|
+
* export default defineConfig({
|
|
188
|
+
* adapter: nextAdapter(),
|
|
189
|
+
* routes,
|
|
190
|
+
* });
|
|
191
|
+
*
|
|
192
|
+
* // src/app/api/[...kizuna]/route.ts
|
|
193
|
+
* export const { GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS } = kizuna.api.mount({
|
|
194
|
+
* basePath: '/api',
|
|
195
|
+
* });
|
|
196
|
+
*/
|
|
197
|
+
const nextAdapter = (defaults) => ({
|
|
198
|
+
name: "next",
|
|
199
|
+
mount: (api, options) => mountNext(api, {
|
|
200
|
+
...defaults,
|
|
201
|
+
...options
|
|
202
|
+
})
|
|
203
|
+
});
|
|
204
|
+
//#endregion
|
|
205
|
+
Object.defineProperty(exports, "NextRequest", {
|
|
206
|
+
enumerable: true,
|
|
207
|
+
get: function() {
|
|
208
|
+
return next_server_js.NextRequest;
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
Object.defineProperty(exports, "NextResponse", {
|
|
212
|
+
enumerable: true,
|
|
213
|
+
get: function() {
|
|
214
|
+
return next_server_js.NextResponse;
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
exports.handleNextRequest = handleNextRequest;
|
|
218
|
+
exports.mountNext = mountNext;
|
|
219
|
+
exports.nextAdapter = nextAdapter;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { Adapter, ApiWithRouter, ErrorFormatter, GUARDS_META, GuardMap, JOBS_META, JobRunner, Jobs, REQUEST_CONTEXT_META, RequestContextMap, Router, Routes, SCHEMES_META } from "kizunajs/adapter";
|
|
2
|
+
import { SecurityScheme } from "kizunajs";
|
|
3
|
+
import { NextRequest, NextRequest as NextRequest$1, NextResponse, NextResponse as NextResponse$1 } from "next/server.js";
|
|
4
|
+
|
|
5
|
+
//#region src/server.d.ts
|
|
6
|
+
interface NextHandlerContext {
|
|
7
|
+
request: NextRequest$1;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* An api's jobs paired with their handlers, both in the shape the request
|
|
11
|
+
* pipeline takes.
|
|
12
|
+
*/
|
|
13
|
+
interface MountedJobs {
|
|
14
|
+
routes: Routes;
|
|
15
|
+
router: Router<Routes, NextHandlerContext>;
|
|
16
|
+
runner: JobRunner<Jobs>;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Passed to each middleware function as the second argument.
|
|
20
|
+
*/
|
|
21
|
+
interface NextMiddlewareRoute {
|
|
22
|
+
path: string;
|
|
23
|
+
method: string;
|
|
24
|
+
}
|
|
25
|
+
type NextMiddlewareHandler = (request: NextRequest$1, route: NextMiddlewareRoute) => Response | void | Promise<Response | void>;
|
|
26
|
+
interface NextHandlerOptions {
|
|
27
|
+
basePath?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Map a thrown error into a response. Return a `NextResponse` (e.g. built
|
|
30
|
+
* from `problemDetails(...)`) to handle the error, or `void` to fall through
|
|
31
|
+
* to the default 500.
|
|
32
|
+
*/
|
|
33
|
+
onError?: (error: unknown, request: NextRequest$1) => NextResponse$1 | Promise<NextResponse$1> | void | Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* Reshape error (status >= 400) response bytes before they are sent. See
|
|
36
|
+
* {@link ErrorFormatter}.
|
|
37
|
+
*/
|
|
38
|
+
formatError?: ErrorFormatter<NextRequest$1>;
|
|
39
|
+
/**
|
|
40
|
+
* Middleware functions that run after route matching but before the handler.
|
|
41
|
+
* Each receives `(request, route)`; return a `Response` to short-circuit.
|
|
42
|
+
* Authentication belongs in a guard.
|
|
43
|
+
*/
|
|
44
|
+
requestMiddleware?: Array<NextMiddlewareHandler>;
|
|
45
|
+
/**
|
|
46
|
+
* Validate handler return values against the routes' response schemas.
|
|
47
|
+
* Mismatches surface as 500 errors. Intended for development; disable in
|
|
48
|
+
* production.
|
|
49
|
+
*
|
|
50
|
+
* @default false
|
|
51
|
+
*/
|
|
52
|
+
responseValidation?: boolean;
|
|
53
|
+
}
|
|
54
|
+
declare const handleNextRequest: <T extends Routes>(request: NextRequest$1, routes: T, router: Router<T, NextHandlerContext>, options?: NextHandlerOptions, guards?: GuardMap<NextHandlerContext>, schemes?: Record<string, SecurityScheme>, requestContext?: RequestContextMap<NextHandlerContext>, pluginExports?: Record<string, unknown>, jobs?: MountedJobs) => Promise<NextResponse$1>;
|
|
55
|
+
type HttpHandlers = {
|
|
56
|
+
GET: HttpHandler;
|
|
57
|
+
HEAD: HttpHandler;
|
|
58
|
+
POST: HttpHandler;
|
|
59
|
+
PUT: HttpHandler;
|
|
60
|
+
PATCH: HttpHandler;
|
|
61
|
+
DELETE: HttpHandler;
|
|
62
|
+
OPTIONS: HttpHandler;
|
|
63
|
+
};
|
|
64
|
+
type HttpHandler = (request: NextRequest$1) => Promise<NextResponse$1>;
|
|
65
|
+
type NextApiWithRouter = ApiWithRouter & {
|
|
66
|
+
readonly [GUARDS_META]?: unknown;
|
|
67
|
+
readonly [SCHEMES_META]?: unknown;
|
|
68
|
+
readonly [REQUEST_CONTEXT_META]?: unknown;
|
|
69
|
+
readonly [JOBS_META]?: unknown;
|
|
70
|
+
};
|
|
71
|
+
type NextApi<R extends Routes = Routes> = ApiWithRouter<R> & {
|
|
72
|
+
readonly [GUARDS_META]?: unknown;
|
|
73
|
+
readonly [SCHEMES_META]?: unknown;
|
|
74
|
+
readonly [REQUEST_CONTEXT_META]?: unknown;
|
|
75
|
+
readonly [JOBS_META]?: unknown;
|
|
76
|
+
mount: (options?: NextHandlerOptions) => HttpHandlers;
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Create endpoints for a Next.js App Router catch-all route.
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* // app/api/[...kizuna]/route.ts
|
|
83
|
+
* export const { GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS } = api.mount({
|
|
84
|
+
* basePath: '/api',
|
|
85
|
+
* });
|
|
86
|
+
*/
|
|
87
|
+
declare function mountNext(api: NextApiWithRouter, options?: NextHandlerOptions): HttpHandlers;
|
|
88
|
+
/**
|
|
89
|
+
* Serve an API on Next. What it takes is what every route is served with. Next
|
|
90
|
+
* routes by file, so mounting returns the route handlers to re-export rather
|
|
91
|
+
* than registering them on an app.
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* // kizuna.config.ts
|
|
95
|
+
* export default defineConfig({
|
|
96
|
+
* adapter: nextAdapter(),
|
|
97
|
+
* routes,
|
|
98
|
+
* });
|
|
99
|
+
*
|
|
100
|
+
* // src/app/api/[...kizuna]/route.ts
|
|
101
|
+
* export const { GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS } = kizuna.api.mount({
|
|
102
|
+
* basePath: '/api',
|
|
103
|
+
* });
|
|
104
|
+
*/
|
|
105
|
+
declare const nextAdapter: (defaults?: NextHandlerOptions) => Adapter<NextHandlerContext, [options?: NextHandlerOptions], HttpHandlers>;
|
|
106
|
+
//#endregion
|
|
107
|
+
export { MountedJobs, NextApi, NextApiWithRouter, NextHandlerContext, NextHandlerOptions, NextMiddlewareHandler, NextMiddlewareRoute, NextRequest, NextResponse, handleNextRequest, mountNext, nextAdapter };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { Adapter, ApiWithRouter, ErrorFormatter, GUARDS_META, GuardMap, JOBS_META, JobRunner, Jobs, REQUEST_CONTEXT_META, RequestContextMap, Router, Routes, SCHEMES_META } from "kizunajs/adapter";
|
|
2
|
+
import { NextRequest, NextRequest as NextRequest$1, NextResponse, NextResponse as NextResponse$1 } from "next/server.js";
|
|
3
|
+
import { SecurityScheme } from "kizunajs";
|
|
4
|
+
|
|
5
|
+
//#region src/server.d.ts
|
|
6
|
+
interface NextHandlerContext {
|
|
7
|
+
request: NextRequest$1;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* An api's jobs paired with their handlers, both in the shape the request
|
|
11
|
+
* pipeline takes.
|
|
12
|
+
*/
|
|
13
|
+
interface MountedJobs {
|
|
14
|
+
routes: Routes;
|
|
15
|
+
router: Router<Routes, NextHandlerContext>;
|
|
16
|
+
runner: JobRunner<Jobs>;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Passed to each middleware function as the second argument.
|
|
20
|
+
*/
|
|
21
|
+
interface NextMiddlewareRoute {
|
|
22
|
+
path: string;
|
|
23
|
+
method: string;
|
|
24
|
+
}
|
|
25
|
+
type NextMiddlewareHandler = (request: NextRequest$1, route: NextMiddlewareRoute) => Response | void | Promise<Response | void>;
|
|
26
|
+
interface NextHandlerOptions {
|
|
27
|
+
basePath?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Map a thrown error into a response. Return a `NextResponse` (e.g. built
|
|
30
|
+
* from `problemDetails(...)`) to handle the error, or `void` to fall through
|
|
31
|
+
* to the default 500.
|
|
32
|
+
*/
|
|
33
|
+
onError?: (error: unknown, request: NextRequest$1) => NextResponse$1 | Promise<NextResponse$1> | void | Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* Reshape error (status >= 400) response bytes before they are sent. See
|
|
36
|
+
* {@link ErrorFormatter}.
|
|
37
|
+
*/
|
|
38
|
+
formatError?: ErrorFormatter<NextRequest$1>;
|
|
39
|
+
/**
|
|
40
|
+
* Middleware functions that run after route matching but before the handler.
|
|
41
|
+
* Each receives `(request, route)`; return a `Response` to short-circuit.
|
|
42
|
+
* Authentication belongs in a guard.
|
|
43
|
+
*/
|
|
44
|
+
requestMiddleware?: Array<NextMiddlewareHandler>;
|
|
45
|
+
/**
|
|
46
|
+
* Validate handler return values against the routes' response schemas.
|
|
47
|
+
* Mismatches surface as 500 errors. Intended for development; disable in
|
|
48
|
+
* production.
|
|
49
|
+
*
|
|
50
|
+
* @default false
|
|
51
|
+
*/
|
|
52
|
+
responseValidation?: boolean;
|
|
53
|
+
}
|
|
54
|
+
declare const handleNextRequest: <T extends Routes>(request: NextRequest$1, routes: T, router: Router<T, NextHandlerContext>, options?: NextHandlerOptions, guards?: GuardMap<NextHandlerContext>, schemes?: Record<string, SecurityScheme>, requestContext?: RequestContextMap<NextHandlerContext>, pluginExports?: Record<string, unknown>, jobs?: MountedJobs) => Promise<NextResponse$1>;
|
|
55
|
+
type HttpHandlers = {
|
|
56
|
+
GET: HttpHandler;
|
|
57
|
+
HEAD: HttpHandler;
|
|
58
|
+
POST: HttpHandler;
|
|
59
|
+
PUT: HttpHandler;
|
|
60
|
+
PATCH: HttpHandler;
|
|
61
|
+
DELETE: HttpHandler;
|
|
62
|
+
OPTIONS: HttpHandler;
|
|
63
|
+
};
|
|
64
|
+
type HttpHandler = (request: NextRequest$1) => Promise<NextResponse$1>;
|
|
65
|
+
type NextApiWithRouter = ApiWithRouter & {
|
|
66
|
+
readonly [GUARDS_META]?: unknown;
|
|
67
|
+
readonly [SCHEMES_META]?: unknown;
|
|
68
|
+
readonly [REQUEST_CONTEXT_META]?: unknown;
|
|
69
|
+
readonly [JOBS_META]?: unknown;
|
|
70
|
+
};
|
|
71
|
+
type NextApi<R extends Routes = Routes> = ApiWithRouter<R> & {
|
|
72
|
+
readonly [GUARDS_META]?: unknown;
|
|
73
|
+
readonly [SCHEMES_META]?: unknown;
|
|
74
|
+
readonly [REQUEST_CONTEXT_META]?: unknown;
|
|
75
|
+
readonly [JOBS_META]?: unknown;
|
|
76
|
+
mount: (options?: NextHandlerOptions) => HttpHandlers;
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Create endpoints for a Next.js App Router catch-all route.
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* // app/api/[...kizuna]/route.ts
|
|
83
|
+
* export const { GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS } = api.mount({
|
|
84
|
+
* basePath: '/api',
|
|
85
|
+
* });
|
|
86
|
+
*/
|
|
87
|
+
declare function mountNext(api: NextApiWithRouter, options?: NextHandlerOptions): HttpHandlers;
|
|
88
|
+
/**
|
|
89
|
+
* Serve an API on Next. What it takes is what every route is served with. Next
|
|
90
|
+
* routes by file, so mounting returns the route handlers to re-export rather
|
|
91
|
+
* than registering them on an app.
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* // kizuna.config.ts
|
|
95
|
+
* export default defineConfig({
|
|
96
|
+
* adapter: nextAdapter(),
|
|
97
|
+
* routes,
|
|
98
|
+
* });
|
|
99
|
+
*
|
|
100
|
+
* // src/app/api/[...kizuna]/route.ts
|
|
101
|
+
* export const { GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS } = kizuna.api.mount({
|
|
102
|
+
* basePath: '/api',
|
|
103
|
+
* });
|
|
104
|
+
*/
|
|
105
|
+
declare const nextAdapter: (defaults?: NextHandlerOptions) => Adapter<NextHandlerContext, [options?: NextHandlerOptions], HttpHandlers>;
|
|
106
|
+
//#endregion
|
|
107
|
+
export { MountedJobs, NextApi, NextApiWithRouter, NextHandlerContext, NextHandlerOptions, NextMiddlewareHandler, NextMiddlewareRoute, NextRequest, NextResponse, handleNextRequest, mountNext, nextAdapter };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { GUARDS_META, JOBS_META, REQUEST_CONTEXT_META, ROUTER_META, SCHEMES_META, createAdapter, headersToObject, jobRouter, jobRoutes, jobRunnerFrom, matchRoute, parseFetchBody, pluginExportsOf, pluginRouterOf, pluginRoutesOf, renderJsonResult } from "kizunajs/adapter";
|
|
2
|
+
import { NextRequest, NextResponse, NextResponse as NextResponse$1 } from "next/server.js";
|
|
3
|
+
//#region src/server.ts
|
|
4
|
+
const jsonResponse = (status, body, headers, raw = false) => new NextResponse$1(body === null || body === void 0 ? null : raw ? body : JSON.stringify(body), {
|
|
5
|
+
status,
|
|
6
|
+
headers
|
|
7
|
+
});
|
|
8
|
+
const handleNextRequest = async (request, routes, router, options, guards, schemes, requestContext, pluginExports, jobs) => {
|
|
9
|
+
const url = new URL(request.url);
|
|
10
|
+
const adapter = createAdapter({
|
|
11
|
+
buildHandlerContext: (adapterRequest) => ({ request: adapterRequest.request }),
|
|
12
|
+
respond: (result) => {
|
|
13
|
+
if (result.kind === "raw-response") return result.response;
|
|
14
|
+
const rendered = renderJsonResult(result, options?.formatError, request, request.method);
|
|
15
|
+
if (rendered.stream) return new NextResponse$1(rendered.stream({
|
|
16
|
+
signal: request.signal,
|
|
17
|
+
validate: options?.responseValidation
|
|
18
|
+
}), {
|
|
19
|
+
status: rendered.status,
|
|
20
|
+
headers: rendered.headers
|
|
21
|
+
});
|
|
22
|
+
return jsonResponse(rendered.status, rendered.body, rendered.headers, rendered.raw);
|
|
23
|
+
},
|
|
24
|
+
onError: async (error) => {
|
|
25
|
+
if (!options?.onError) {
|
|
26
|
+
console.error("[kizuna/next] handler error:", error);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const override = await options.onError(error, request);
|
|
30
|
+
if (override) return {
|
|
31
|
+
kind: "raw-response",
|
|
32
|
+
response: override
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
if (jobs) {
|
|
37
|
+
const matchedJob = matchRoute(request.method, url.pathname, jobs.routes, options?.basePath);
|
|
38
|
+
if (matchedJob.kind === "matched") {
|
|
39
|
+
const jobRequest = {
|
|
40
|
+
request,
|
|
41
|
+
method: request.method,
|
|
42
|
+
resolution: {
|
|
43
|
+
kind: "pre-resolved",
|
|
44
|
+
routeKey: matchedJob.match.routeKey,
|
|
45
|
+
route: matchedJob.match.route,
|
|
46
|
+
params: matchedJob.match.params
|
|
47
|
+
},
|
|
48
|
+
query: Object.fromEntries(url.searchParams),
|
|
49
|
+
headers: headersToObject(request.headers),
|
|
50
|
+
readBody: (route) => parseFetchBody(request, route)
|
|
51
|
+
};
|
|
52
|
+
return adapter.handle({
|
|
53
|
+
routes: jobs.routes,
|
|
54
|
+
router: jobs.router,
|
|
55
|
+
request: jobRequest,
|
|
56
|
+
responseContext: {},
|
|
57
|
+
guards,
|
|
58
|
+
schemes,
|
|
59
|
+
requestContext,
|
|
60
|
+
pluginExports,
|
|
61
|
+
jobs: jobs.runner,
|
|
62
|
+
responseValidation: options?.responseValidation
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const globalMiddleware = options?.requestMiddleware;
|
|
67
|
+
if (globalMiddleware && globalMiddleware.length > 0) {
|
|
68
|
+
const matched = matchRoute(request.method, url.pathname, routes, options?.basePath);
|
|
69
|
+
if (matched.kind === "matched") {
|
|
70
|
+
const middlewareRoute = {
|
|
71
|
+
path: matched.match.route.path,
|
|
72
|
+
method: matched.match.route.method
|
|
73
|
+
};
|
|
74
|
+
for (const handler of globalMiddleware) {
|
|
75
|
+
const result = await handler(request, middlewareRoute);
|
|
76
|
+
if (result instanceof Response) return new NextResponse$1(result.body, {
|
|
77
|
+
status: result.status,
|
|
78
|
+
statusText: result.statusText,
|
|
79
|
+
headers: result.headers
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
const adapterRequest = {
|
|
83
|
+
request,
|
|
84
|
+
method: request.method,
|
|
85
|
+
resolution: {
|
|
86
|
+
kind: "pre-resolved",
|
|
87
|
+
routeKey: matched.match.routeKey,
|
|
88
|
+
route: matched.match.route,
|
|
89
|
+
params: matched.match.params
|
|
90
|
+
},
|
|
91
|
+
query: Object.fromEntries(url.searchParams),
|
|
92
|
+
headers: headersToObject(request.headers),
|
|
93
|
+
readBody: (route) => parseFetchBody(request, route)
|
|
94
|
+
};
|
|
95
|
+
return adapter.handle({
|
|
96
|
+
routes,
|
|
97
|
+
router,
|
|
98
|
+
request: adapterRequest,
|
|
99
|
+
responseContext: {},
|
|
100
|
+
guards,
|
|
101
|
+
schemes,
|
|
102
|
+
requestContext,
|
|
103
|
+
jobs: jobs?.runner,
|
|
104
|
+
responseValidation: options?.responseValidation
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const adapterRequest = {
|
|
109
|
+
request,
|
|
110
|
+
method: request.method,
|
|
111
|
+
resolution: {
|
|
112
|
+
kind: "core-match",
|
|
113
|
+
path: url.pathname
|
|
114
|
+
},
|
|
115
|
+
query: Object.fromEntries(url.searchParams),
|
|
116
|
+
headers: headersToObject(request.headers),
|
|
117
|
+
readBody: (route) => parseFetchBody(request, route)
|
|
118
|
+
};
|
|
119
|
+
return adapter.handle({
|
|
120
|
+
routes,
|
|
121
|
+
router,
|
|
122
|
+
request: adapterRequest,
|
|
123
|
+
responseContext: {},
|
|
124
|
+
guards,
|
|
125
|
+
schemes,
|
|
126
|
+
requestContext,
|
|
127
|
+
pluginExports,
|
|
128
|
+
jobs: jobs?.runner,
|
|
129
|
+
basePath: options?.basePath,
|
|
130
|
+
responseValidation: options?.responseValidation
|
|
131
|
+
});
|
|
132
|
+
};
|
|
133
|
+
/**
|
|
134
|
+
* Create endpoints for a Next.js App Router catch-all route.
|
|
135
|
+
*
|
|
136
|
+
* @example
|
|
137
|
+
* // app/api/[...kizuna]/route.ts
|
|
138
|
+
* export const { GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS } = api.mount({
|
|
139
|
+
* basePath: '/api',
|
|
140
|
+
* });
|
|
141
|
+
*/
|
|
142
|
+
function mountNext(api, options) {
|
|
143
|
+
const guards = api[GUARDS_META];
|
|
144
|
+
const schemes = api[SCHEMES_META];
|
|
145
|
+
const requestContext = api[REQUEST_CONTEXT_META];
|
|
146
|
+
const jobsMeta = api[JOBS_META];
|
|
147
|
+
const mountedJobs = jobsMeta ? {
|
|
148
|
+
routes: jobRoutes(jobsMeta),
|
|
149
|
+
router: jobRouter(jobsMeta),
|
|
150
|
+
runner: jobRunnerFrom(jobsMeta)
|
|
151
|
+
} : void 0;
|
|
152
|
+
const handlerOptions = {
|
|
153
|
+
basePath: options?.basePath,
|
|
154
|
+
onError: options?.onError,
|
|
155
|
+
requestMiddleware: options?.requestMiddleware,
|
|
156
|
+
responseValidation: options?.responseValidation
|
|
157
|
+
};
|
|
158
|
+
const pluginExports = pluginExportsOf(api);
|
|
159
|
+
const pluginRoutes = pluginRoutesOf(api);
|
|
160
|
+
const pluginRouter = pluginRouterOf(api);
|
|
161
|
+
const hasPluginRoutes = Object.keys(pluginRoutes).length > 0;
|
|
162
|
+
const handler = async (request) => {
|
|
163
|
+
if (hasPluginRoutes) {
|
|
164
|
+
const pathname = new URL(request.url).pathname;
|
|
165
|
+
if (!(matchRoute(request.method, pathname, api.routes, options?.basePath).kind === "matched") && matchRoute(request.method, pathname, pluginRoutes, options?.basePath).kind === "matched") return handleNextRequest(request, pluginRoutes, pluginRouter, handlerOptions, guards, schemes, requestContext, pluginExports, void 0);
|
|
166
|
+
}
|
|
167
|
+
return handleNextRequest(request, api.routes, api[ROUTER_META], handlerOptions, guards, schemes, requestContext, pluginExports, mountedJobs);
|
|
168
|
+
};
|
|
169
|
+
return {
|
|
170
|
+
GET: handler,
|
|
171
|
+
HEAD: handler,
|
|
172
|
+
POST: handler,
|
|
173
|
+
PUT: handler,
|
|
174
|
+
PATCH: handler,
|
|
175
|
+
DELETE: handler,
|
|
176
|
+
OPTIONS: handler
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Serve an API on Next. What it takes is what every route is served with. Next
|
|
181
|
+
* routes by file, so mounting returns the route handlers to re-export rather
|
|
182
|
+
* than registering them on an app.
|
|
183
|
+
*
|
|
184
|
+
* @example
|
|
185
|
+
* // kizuna.config.ts
|
|
186
|
+
* export default defineConfig({
|
|
187
|
+
* adapter: nextAdapter(),
|
|
188
|
+
* routes,
|
|
189
|
+
* });
|
|
190
|
+
*
|
|
191
|
+
* // src/app/api/[...kizuna]/route.ts
|
|
192
|
+
* export const { GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS } = kizuna.api.mount({
|
|
193
|
+
* basePath: '/api',
|
|
194
|
+
* });
|
|
195
|
+
*/
|
|
196
|
+
const nextAdapter = (defaults) => ({
|
|
197
|
+
name: "next",
|
|
198
|
+
mount: (api, options) => mountNext(api, {
|
|
199
|
+
...defaults,
|
|
200
|
+
...options
|
|
201
|
+
})
|
|
202
|
+
});
|
|
203
|
+
//#endregion
|
|
204
|
+
export { NextRequest, NextResponse, handleNextRequest, mountNext, nextAdapter };
|
package/package.json
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kizunajs/next",
|
|
3
|
+
"version": "2.0.0-beta.0",
|
|
4
|
+
"description": "Next.js App Router adapter for Kizuna.js",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"kizunajs",
|
|
7
|
+
"typescript",
|
|
8
|
+
"zod",
|
|
9
|
+
"openapi",
|
|
10
|
+
"spec-driven",
|
|
11
|
+
"type-safe",
|
|
12
|
+
"rest",
|
|
13
|
+
"api",
|
|
14
|
+
"nextjs",
|
|
15
|
+
"next",
|
|
16
|
+
"app-router",
|
|
17
|
+
"adapter"
|
|
18
|
+
],
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"homepage": "https://kizunajs.com/docs/adapters/next",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/kizunajs/kizuna.git",
|
|
24
|
+
"directory": "packages/next"
|
|
25
|
+
},
|
|
26
|
+
"bugs": "https://github.com/kizunajs/kizuna/issues",
|
|
27
|
+
"type": "module",
|
|
28
|
+
"sideEffects": false,
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=24"
|
|
31
|
+
},
|
|
32
|
+
"main": "./dist/index.cjs",
|
|
33
|
+
"module": "./dist/index.mjs",
|
|
34
|
+
"types": "./dist/index.d.mts",
|
|
35
|
+
"exports": {
|
|
36
|
+
".": {
|
|
37
|
+
"import": {
|
|
38
|
+
"types": "./dist/index.d.mts",
|
|
39
|
+
"default": "./dist/index.mjs"
|
|
40
|
+
},
|
|
41
|
+
"require": {
|
|
42
|
+
"types": "./dist/index.d.cts",
|
|
43
|
+
"default": "./dist/index.cjs"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"kizuna": {
|
|
48
|
+
"entries": {
|
|
49
|
+
".": "server"
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"files": [
|
|
53
|
+
"dist"
|
|
54
|
+
],
|
|
55
|
+
"publishConfig": {
|
|
56
|
+
"access": "public"
|
|
57
|
+
},
|
|
58
|
+
"peerDependencies": {
|
|
59
|
+
"next": ">=16.0.0",
|
|
60
|
+
"zod": "^4.0.0",
|
|
61
|
+
"kizunajs": "2.0.0-beta.0"
|
|
62
|
+
},
|
|
63
|
+
"devDependencies": {
|
|
64
|
+
"next": "^16.3.3",
|
|
65
|
+
"tsdown": "^0.21.0",
|
|
66
|
+
"typescript": "^6.0.3",
|
|
67
|
+
"zod": "^4.0.0",
|
|
68
|
+
"kizunajs": "2.0.0-beta.0"
|
|
69
|
+
},
|
|
70
|
+
"scripts": {
|
|
71
|
+
"build": "tsdown src/index.ts --format esm,cjs --dts --clean --external kizunajs --external zod",
|
|
72
|
+
"typecheck": "tsc --noEmit"
|
|
73
|
+
}
|
|
74
|
+
}
|