Skip to content

Commit

Permalink
Edge Functions / Middleware support (#9)
Browse files Browse the repository at this point in the history
* experiment: use from npm so we can use next 12 middleware

* use local @happykit/flags

* use @happykit/flags through workspace

* use req.cookies on server

* experimental middleware support

* add middleware pages for all variants

* move Content to components

* Content -> EdgeFunctionContent

* bring back dynamic recompliation of package

upgrades @preconstruct/next to a version supporting Next.js 12

* use response.cookie

response.cookie was originally broken when Expires was present on the cookie: vercel/next.js#30430

This will be fixed in Next.js v12.0.2, so we use the canary of that for now, and the release of happykit might have to be a breaking change since we then no longer support older Next.js versions theoretically.

We need to list next >=12.0.2 as a peerDependency once v12.0.2 is out.

* use getCookie

* update dependencies

* pre -> code

* use differt header access methods

* switch prebuild to preconstruct build

* use next.js v12.0.2

* upgrade dependencies

* add @tailwindui/react

* upgrade @babel/preset-env

* disable dynamic recompilation

Did not work, so reverting back to triggering package builds manually.

* add reload button to middleware

* rename happykit.config to flags.config

* increase maxAge

* add flagBag.cookie to getEdgeFlags

* avoid eval/new Function warning

Middleware does not accept eval or new Function, but the way in which we create our custom errors uses that under the hood. So we need to throw native errors directly without extending the error class.

* keep bottom bar on bottom

even with short content like on the middleware demo page

* list middleware support as key feature

* add getEdgeFlags to README

* remove InvalidConfigurationError

* reenable preconstruct dev

* Revert "reenable preconstruct dev"

This reverts commit 2171933.

* recreate cookieOptions object on every request

This avoids vercel/next.js#31666

* explain middleware behaviour in EdgeFunctionContent

* consistently use flags.config

* make flagBag.cookie iterable

* Revert "make flagBag.cookie iterable"

This reverts commit 77bfb6f.

* reword key features

* remove jest-expect-message

* add edge tests

* v2.0.0
  • Loading branch information
dferber90 committed Dec 4, 2021
1 parent ffe2eb3 commit 1dc1653
Show file tree
Hide file tree
Showing 30 changed files with 2,398 additions and 1,275 deletions.
3 changes: 2 additions & 1 deletion README.md
Expand Up @@ -27,7 +27,8 @@ Add Feature Flags to your Next.js application with a single React Hook. This pac
- only 5 kB in size
- extremely fast flag responses (~50ms)
- supports *user targeting*, *custom rules* and *rollouts*
- support *server-side rendering* and *static site generation*
- supports *server-side rendering* and *static site generation*
- supports *_middleware (edge functions)*

<br />

Expand Down
63 changes: 63 additions & 0 deletions example/components/EdgeFunctionContent.tsx
@@ -0,0 +1,63 @@
import React from "react";
import { AppFlags } from "../types/AppFlags";

export function EdgeFunctionContent(props: {
checkoutVariant: AppFlags["checkout"];
}) {
return (
<React.Fragment>
<p>
This demo shows how to use <code>@happykit/flags</code> in{" "}
<a
href="https://nextjs.org/blog/next-12#introducing-middleware"
rel="noreferrer noopener"
>
Next.js Middleware
</a>
.
</p>
<pre>
You have been served the "<code>{props.checkoutVariant}</code>" checkout
variant.
</pre>
<button
type="button"
className="inline-flex items-center px-2.5 py-1.5 border border-transparent text-xs font-medium rounded shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
onClick={() => {
document.cookie =
"hkvk=; path=/; maxAge=0; expires=Thu, 01 Jan 1970 00:00:01 GMT";
window.location.reload();
}}
>
Remove cookie and reload
</button>
<p>
This example uses a <code>_middleware</code> file at{" "}
<code>pages/demo/middleware</code> to statically render different
variants for the <code>/demo/middleware</code> path. The different
variants live under <code>pages/demo/middleware/variant-*.tsx</code>.
</p>
<p>
The middleware loads the flags and rewrites the incoming request either
to <code>variant-short.tsx</code>, <code>variant-medium.tsx</code> or{" "}
<code>variant-full.tsx</code> depending on the resolved flag variant.
</p>
<p>
Since resulting page is served statically from the edge, rendering will
use no visitor key. This is necessary as the concept of a visitor does
not exist during static site generation. Thus all rules and
percentage-based rollouts targeting a visitor resolve to{" "}
<code>null</code>.
</p>
<p>
The middleware loads the flags purely to decide where to rewrite the
request to. It does not send any resolved flags into the application
itself.
</p>
<p>
You are however free to call <code>useFlags()</code> on the client and
combine this approach with the middleware.
</p>
</React.Fragment>
);
}
4 changes: 2 additions & 2 deletions example/components/Layout.tsx
Expand Up @@ -241,10 +241,10 @@ export function Layout(props: {
</button>
</div>
<main
className="flex-1 relative z-0 overflow-y-auto focus:outline-none"
className="flex-1 relative z-0 overflow-y-auto focus:outline-none flex flex-col min-h-screen"
tabIndex={0}
>
<div className="py-6 max-w-prose">
<div className="py-6 max-w-prose flex-auto">
<div className="max-w-7xl mx-auto px-4 sm:px-6 md:px-8">
<h1 className="text-2xl font-semibold text-gray-900">
{props.title}
Expand Down
8 changes: 7 additions & 1 deletion example/components/Nav.tsx
Expand Up @@ -8,7 +8,10 @@ function NavLink(props: {
indent?: boolean;
}) {
const router = useRouter();
const active = router.asPath === props.href;
const active =
router.asPath === props.href ||
// TODO this is a workaround since the _middeware's rewrite messes with asPath
(props.href === "/demo/middleware" && router.asPath.startsWith(props.href));
return (
<Link href={props.href}>
<a
Expand Down Expand Up @@ -54,6 +57,9 @@ export function Nav() {
<NavLink indent href="/demo/static-site-generation-hybrid">
Static Site Generation (Hybrid)
</NavLink>
<NavLink indent href="/demo/middleware">
Middleware
</NavLink>
</div>
<div className="bg-white text-gray-600 group w-full flex items-center pl-7 pr-2 py-2 text-sm font-medium rounded-md">
Targeting and Rules
Expand Down
9 changes: 9 additions & 0 deletions example/flags.config.ts
@@ -0,0 +1,9 @@
import { configure } from "@happykit/flags/config";

configure({
envKey: process.env.NEXT_PUBLIC_FLAGS_ENV_KEY!,

// You can just delete this line in your own application.
// It's only here because we use it while working on @happykit/flags itself.
endpoint: process.env.NEXT_PUBLIC_FLAGS_ENDPOINT,
});
3 changes: 0 additions & 3 deletions example/next.config.js

This file was deleted.

23 changes: 12 additions & 11 deletions example/package.json
Expand Up @@ -4,25 +4,26 @@
"private": true,
"scripts": {
"dev": "next dev",
"prebuild": "yarn workspace @happykit/flags preconstruct dev",
"prebuild": "yarn workspace @happykit/flags preconstruct build",
"build": "next build",
"start": "next start"
},
"dependencies": {
"@headlessui/react": "1.1.1",
"@preconstruct/next": "3.0.0",
"@tailwindcss/typography": "0.4.0",
"@headlessui/react": "1.4.1",
"@preconstruct/next": "3.0.1",
"@tailwindcss/typography": "0.4.1",
"@tailwindui/react": "0.1.1",
"@types/dedent": "0.7.0",
"@types/node": "16.7.10",
"@types/react": "17.0.19",
"autoprefixer": "10.2.5",
"@types/node": "16.11.9",
"@types/react": "17.0.35",
"autoprefixer": "10.4.0",
"clsx": "1.1.1",
"dedent": "0.7.0",
"next": "11.1.2",
"postcss": "8.2.14",
"next": "12.0.4",
"postcss": "8.3.11",
"react": "17.0.2",
"react-dom": "17.0.2",
"tailwindcss": "2.1.2",
"typescript": "4.4.2"
"tailwindcss": "2.2.19",
"typescript": "4.5.2"
}
}
11 changes: 2 additions & 9 deletions example/pages/_app.tsx
Expand Up @@ -2,16 +2,9 @@ import * as React from "react";
import Head from "next/head";
import type { AppProps } from "next/app";
import "tailwindcss/tailwind.css";
import { configure } from "@happykit/flags/config";
import Script from "next/script";

configure({
envKey: process.env.NEXT_PUBLIC_FLAGS_ENV_KEY!,

// You can just delete this line in your own application.
// It's only here because we use it while working on @happykit/flags itself.
endpoint: process.env.NEXT_PUBLIC_FLAGS_ENDPOINT,
});
// Import HappyKit configuration
import "../flags.config";

function MyApp({ Component, pageProps }: AppProps) {
return (
Expand Down
17 changes: 17 additions & 0 deletions example/pages/demo/middleware/_middleware.ts
@@ -0,0 +1,17 @@
import { NextRequest, NextResponse } from "next/server";
import type { AppFlags } from "../../../types/AppFlags";
// Importing the config is necessary to configure getEdgeFlags
import "../../../flags.config";
import { getEdgeFlags } from "@happykit/flags/edge";

export async function middleware(request: NextRequest) {
const flagBag = await getEdgeFlags<AppFlags>({ request });

const response = NextResponse.rewrite(
`/demo/middleware/variant-${flagBag.flags?.checkout || "full"}`
);

if (flagBag.cookie) response.cookie(...flagBag.cookie.args);

return response;
}
17 changes: 17 additions & 0 deletions example/pages/demo/middleware/variant-full.tsx
@@ -0,0 +1,17 @@
import * as React from "react";
import { Layout } from "../../../components/Layout";
import { EdgeFunctionContent } from "../../../components/EdgeFunctionContent";

export default function Page() {
return (
<Layout
title="Middleware"
source={`https://github.com/happykit/flags/blob/${process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_REF}/example/pages/demo/middleware`}
flagBag={null}
>
<article className="py-4 prose max-w-prose">
<EdgeFunctionContent checkoutVariant="full" />
</article>
</Layout>
);
}
17 changes: 17 additions & 0 deletions example/pages/demo/middleware/variant-medium.tsx
@@ -0,0 +1,17 @@
import * as React from "react";
import { Layout } from "../../../components/Layout";
import { EdgeFunctionContent } from "../../../components/EdgeFunctionContent";

export default function Page() {
return (
<Layout
title="Middleware"
source={`https://github.com/happykit/flags/blob/${process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_REF}/example/pages/demo/middleware`}
flagBag={null}
>
<article className="py-4 prose max-w-prose">
<EdgeFunctionContent checkoutVariant="medium" />
</article>
</Layout>
);
}
17 changes: 17 additions & 0 deletions example/pages/demo/middleware/variant-short.tsx
@@ -0,0 +1,17 @@
import * as React from "react";
import { Layout } from "../../../components/Layout";
import { EdgeFunctionContent } from "../../../components/EdgeFunctionContent";

export default function Page() {
return (
<Layout
title="Middleware"
source={`https://github.com/happykit/flags/blob/${process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_REF}/example/pages/demo/middleware`}
flagBag={null}
>
<article className="py-4 prose max-w-prose">
<EdgeFunctionContent checkoutVariant="short" />
</article>
</Layout>
);
}
2 changes: 1 addition & 1 deletion example/pages/docs/public-api.tsx
Expand Up @@ -76,7 +76,7 @@ export default function Page() {
<h2>Endpoint</h2>
<p>
Requests need to be sent as a <code>POST</code> request to:
<pre>https://happykit.dev/api/flags/&lt;flags key&gt;</pre> You can
<code>https://happykit.dev/api/flags/&lt;flags key&gt;</code> You can
find the <i>flag key</i> for each stage in your project's settings on{" "}
<a href="https://happykit.dev/">happykit.dev</a>.
</p>
Expand Down
6 changes: 3 additions & 3 deletions package.json
Expand Up @@ -7,7 +7,7 @@
"example:dev": "yarn workspace example dev",
"example:build": "yarn workspace example build",
"example:start": "yarn workspace example next start",
"package:dev": "yarn workspace @happykit/flags preconstruct dev",
"package:dev": "yarn workspace @happykit/flags preconstruct build",
"package:test": "yarn workspace @happykit/flags test",
"package:build": "yarn workspace @happykit/flags build"
},
Expand All @@ -16,8 +16,8 @@
"example"
],
"dependencies": {
"@preconstruct/cli": "2.1.0",
"next": "11.1.2",
"@preconstruct/cli": "2.1.5",
"next": "12.0.4",
"react": "17.0.2",
"react-dom": "17.0.2"
},
Expand Down

1 comment on commit 1dc1653

@vercel
Copy link

@vercel vercel bot commented on 1dc1653 Dec 4, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.