Skip to content

Commit

Permalink
Support Next.js 15 (#721)
Browse files Browse the repository at this point in the history
## Summary
<!-- Succinctly describe your change, providing context, what you've
changed, and why. -->

Ensures we support Next.js by being looser with second argument typing
for `GET`, `POST`, and `PUT` endpoints.

```
$ pnpm build

> [email protected] build /home/nixos/scratch/inngest-nextjs15-bug
> next build

 ⚠ Configuration with next.config.ts is currently an experimental feature, use with caution.
  ▲ Next.js 15.0.0-canary.177

   Creating an optimized production build ...
 ✓ Compiled successfully
   Linting and checking validity of types  ...Failed to compile.

src/app/api/inngest/route.ts
Type error: Route "src/app/api/inngest/route.ts" has an invalid "GET" export:
  Type "ServerResponse<IncomingMessage> & { send: Send<any>; json: Send<any>; ... 5 more ...; revalidate: (urlPath: string, opts?: { ...; } | undefined) => Promise<...>; }" is not a valid type for the function's second argument.
```

This is due to Next.js majors (and execution environments) all requiring
different typing for that second argument, from `undefined`, to
`NextApiResponse`, to `RouteContext`, which is _enforced_ by build-step
type checking. Instead we set `unknown` and handle the difference
manually at runtime.

## Checklist
<!-- Tick these items off as you progress. -->
<!-- If an item isn't applicable, ideally please strikeout the item by
wrapping it in "~~"" and suffix it with "N/A My reason for skipping
this." -->
<!-- e.g. "- [ ] ~~Added tests~~ N/A Only touches docs" -->

- [ ] ~Added a [docs PR](https://github.com/inngest/website) that
references this PR~ N/A Bug fix
- [x] Added unit/integration tests
- [x] Added changesets if applicable

## Related
<!-- A space for any related links, issues, or PRs. -->
<!-- Linear issues are autolinked. -->
<!-- e.g. - INN-123 -->
<!-- GitHub issues/PRs can be linked using shorthand. -->
<!-- e.g. "- inngest/inngest#123" -->
<!-- Feel free to remove this section if there are no applicable related
links.-->
- Fixes #717
  • Loading branch information
jpwilliams authored Oct 17, 2024
1 parent 4f2ce07 commit 59fa466
Show file tree
Hide file tree
Showing 2 changed files with 63 additions and 13 deletions.
5 changes: 5 additions & 0 deletions .changeset/clever-pets-relax.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"inngest": patch
---

Support Next.js 15 in serve handler typing
71 changes: 58 additions & 13 deletions packages/inngest/src/next.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,40 @@ import { type SupportedFrameworkName } from "./types";
*/
export const frameworkName: SupportedFrameworkName = "nextjs";

/**
* The shape of a request handler, supporting Next.js 12+.
*
* We are intentionally abstract with the arguments here, as Next.js's type
* checking when building varies wildly between major versions; specifying
* different types (even optional types) here can cause issues with the build.
*
* This change was initially made for Next.js 15, which specifies the second
* argument as `RouteContext`, whereas Next.js 13 and 14 omit it and Next.js 12
* provides a `NextApiResponse`, which is varies based on the execution
* environment used (edge vs serverless).
*/
export type RequestHandler = (
expectedReq: NextRequest,
res: unknown
) => Promise<Response>;

const isRecord = (val: unknown): val is Record<string, unknown> => {
return typeof val === "object" && val !== null;
};

const isFunction = (val: unknown): val is (...args: unknown[]) => unknown => {
return typeof val === "function";
};

const isNext12ApiResponse = (val: unknown): val is NextApiResponse => {
return (
isRecord(val) &&
isFunction(val.setHeader) &&
isFunction(val.status) &&
isFunction(val.send)
);
};

/**
* In Next.js, serve and register any declared functions with Inngest, making
* them available to be triggered by events.
Expand All @@ -60,19 +94,19 @@ export const frameworkName: SupportedFrameworkName = "nextjs";
// Has explicit return type to avoid JSR-defined "slow types"
export const serve = (
options: ServeHandlerOptions
): ((expectedReq: NextRequest, res: NextApiResponse) => Promise<Response>) & {
GET: (expectedReq: NextRequest, res: NextApiResponse) => Promise<Response>;
POST: (expectedReq: NextRequest, res: NextApiResponse) => Promise<Response>;
PUT: (expectedReq: NextRequest, res: NextApiResponse) => Promise<Response>;
): RequestHandler & {
GET: RequestHandler;
POST: RequestHandler;
PUT: RequestHandler;
} => {
const handler = new InngestCommHandler({
frameworkName,
...options,
handler: (
reqMethod: "GET" | "POST" | "PUT" | undefined,
expectedReq: NextRequest,
res: NextApiResponse
...args: Parameters<RequestHandler>
) => {
const [expectedReq, res] = args;
const req = expectedReq as Either<NextApiRequest, NextRequest>;

const getHeader = (key: string): string | null | undefined => {
Expand Down Expand Up @@ -173,18 +207,22 @@ export const serve = (
/**
* Carefully attempt to set headers and data on the response object
* for Next.js 12 support.
*
* This also assumes that we're not using Next.js 15, where the `res`
* object is repopulated as a `RouteContext` object. We expect these
* methods to NOT be defined in Next.js 15.
*
* We could likely use `instanceof ServerResponse` to better check the
* type of this, though Next.js 12 had issues with this due to not
* instantiating the response correctly.
*/
if (typeof res?.setHeader === "function") {
if (isNext12ApiResponse(res)) {
for (const [key, value] of Object.entries(headers)) {
res.setHeader(key, value);
}
}

if (
typeof res?.status === "function" &&
typeof res?.send === "function"
) {
res.status(status).send(body);
res.status(status);
res.send(body);

/**
* If we're here, we're in a serverless endpoint (not edge), so
Expand Down Expand Up @@ -238,6 +276,13 @@ export const serve = (
const baseFn = handler.createHandler();

const fn = baseFn.bind(null, undefined);

/**
* Ensure we have a non-variadic length to avoid issues with forced type
* checking.
*/
Object.defineProperty(fn, "length", { value: 1 });

type Fn = typeof fn;

const handlerFn = Object.defineProperties(fn, {
Expand Down

0 comments on commit 59fa466

Please sign in to comment.