Skip to content

Commit

Permalink
tests: add test setup for edge functions (#84)
Browse files Browse the repository at this point in the history
* tests: add test setup for edge functions

chore: update

* chore: update

* chore: install deno on CI

* chore: install specific deno version to match edge-bundler

* chore: update test sharding

* chore: update path

* chore: update

* chore: increase timings

* chore: fight flakyness with more hardware

* chore: fix sharding

* chore: increase timeout
  • Loading branch information
lukasholzer authored Nov 29, 2023
1 parent a31816e commit 90bb11b
Show file tree
Hide file tree
Showing 26 changed files with 985 additions and 59 deletions.
7 changes: 6 additions & 1 deletion .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
shard: [1/3, 2/3, 3/3]
shard: [1/5, 2/5, 3/5, 4/5, 5/5]
steps:
- uses: actions/checkout@v4
- name: 'Install Node'
Expand All @@ -59,6 +59,11 @@ jobs:
node-version: '18.x'
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
- name: Install Deno
uses: denoland/setup-deno@v1
with:
# Should match the `DENO_VERSION_RANGE` from https://github.com/netlify/edge-bundler/blob/e55f825bd985d3c92e21d1b765d71e70d5628fba/node/bridge.ts#L17
deno-version: v1.37.0
- name: 'Install dependencies'
run: npm ci
- name: 'Build'
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ dist/
/playwright-report/
/blob-report/
/playwright/.cache/

deno.lock
4 changes: 4 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"deno.enablePaths": ["tools/deno", "edge-runtime"],
"deno.unstable": true
}
7 changes: 7 additions & 0 deletions deno.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"lint": {
"files": {
"include": ["edge-runtime/middleware.ts"]
}
}
}
22 changes: 22 additions & 0 deletions edge-runtime/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { Config, Context } from '@netlify/edge-functions'

// import nextConfig from '../edge-shared/nextConfig.json' assert { type: 'json' }

export async function handleMiddleware(req: Request, context: Context, nextHandler: () => any) {
// Don't run in dev
if (Deno.env.get('NETLIFY_DEV')) {
return
}

const url = new URL(req.url)
console.log('from handleMiddleware', url)
// const req = new IncomingMessage(internalEvent);
// const res = new ServerlessResponse({
// method: req.method ?? "GET",
// headers: {},
// });
//
// const request = buildNextRequest(req, context, nextConfig)

return Response.json({ success: true })
}
13 changes: 13 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"type": "module",
"files": [
"dist",
"edge-runtime",
"manifest.yml"
],
"engines": {
Expand Down Expand Up @@ -50,6 +51,7 @@
"p-limit": "^4.0.0"
},
"devDependencies": {
"@netlify/edge-functions": "^2.2.0",
"@netlify/eslint-config-node": "^7.0.1",
"@netlify/serverless-functions-api": "^1.10.1",
"@netlify/zip-it-and-ship-it": "^9.25.5",
Expand Down
8 changes: 8 additions & 0 deletions src/build/config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { NetlifyPluginConstants, NetlifyPluginOptions } from '@netlify/build'
import type { PrerenderManifest } from 'next/dist/build/index.js'
import { MiddlewareManifest } from 'next/dist/build/webpack/plugins/middleware-plugin.js'
import { readFile } from 'node:fs/promises'
import { resolve } from 'node:path'
import { SERVER_HANDLER_NAME } from './constants.js'
Expand All @@ -10,6 +11,13 @@ export const getPrerenderManifest = async ({
return JSON.parse(await readFile(resolve(PUBLISH_DIR, 'prerender-manifest.json'), 'utf-8'))
}

/**
* Get Next.js middleware config from the build output
*/
export const getMiddlewareManifest = async (): Promise<MiddlewareManifest> => {
return JSON.parse(await readFile(resolve('.next/server/middleware-manifest.json'), 'utf-8'))
}

/**
* Enable Next.js standalone mode at build time
*/
Expand Down
154 changes: 143 additions & 11 deletions src/build/functions/edge.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,144 @@
import { mkdir, rm } from 'node:fs/promises'
import { join } from 'node:path'
import { EDGE_HANDLER_DIR } from '../constants.js'

/**
* Create a Netlify edge function to run the Next.js server
*/
export const createEdgeHandler = async () => {
// reset the handler directory
await rm(join(process.cwd(), EDGE_HANDLER_DIR), { recursive: true, force: true })
await mkdir(join(process.cwd(), EDGE_HANDLER_DIR), { recursive: true })
import type { EdgeFunctionDefinition as NextDefinition } from 'next/dist/build/webpack/plugins/middleware-plugin.js'
import { cp, mkdir, readFile, rm, writeFile } from 'node:fs/promises'
import { dirname, join, relative, resolve } from 'node:path'
import { getMiddlewareManifest } from '../config.js'
import {
EDGE_FUNCTIONS_DIR,
EDGE_HANDLER_NAME,
PLUGIN_DIR,
PLUGIN_NAME,
PLUGIN_VERSION,
} from '../constants.js'

interface NetlifyManifest {
version: 1
functions: NetlifyDefinition[]
}

type NetlifyDefinition =
| {
function: string
name?: string
path: string
cache?: 'manual'
generator: string
}
| {
function: string
name?: string
pattern: string
cache?: 'manual'
generator: string
}

const getHandlerName = ({ name }: NextDefinition) =>
EDGE_HANDLER_NAME.replace('{{name}}', name.replace(/\W/g, '-'))

const buildHandlerDefinitions = (
{ name: definitionName, matchers, page }: NextDefinition,
handlerName: string,
): NetlifyDefinition[] => {
return definitionName === 'middleware'
? [
{
function: handlerName,
name: 'Next.js Middleware Handler',
path: '/*',
generator: `${PLUGIN_NAME}@${PLUGIN_VERSION}`,
} as any,
]
: matchers.map((matcher) => ({
function: handlerName,
name: `Next.js Edge Handler: ${page}`,
pattern: matcher.regexp,
cache: 'manual',
generator: `${PLUGIN_NAME}@${PLUGIN_VERSION}`,
}))
}

const copyHandlerDependencies = async (
{ name: definitionName, files }: NextDefinition,
handlerName: string,
) => {
await Promise.all(
files.map(async (file) => {
const srcDir = join(process.cwd(), '.next/standalone/.next')
const destDir = join(process.cwd(), EDGE_FUNCTIONS_DIR, handlerName)

if (file === `server/${definitionName}.js`) {
const entrypoint = await readFile(join(srcDir, file), 'utf8')
// const exports = ``
const exports = `
export default _ENTRIES["middleware_${definitionName}"].default;
// export default () => {
// console.log('here', _ENTRIES)
// }
`
await mkdir(dirname(join(destDir, file)), { recursive: true })
await writeFile(
join(destDir, file),
`
import './edge-runtime-webpack.js';
var _ENTRIES = {};\n`.concat(entrypoint, '\n', exports),
)
return
}

await cp(join(srcDir, file), join(destDir, file))
}),
)
}

const writeHandlerFile = async ({ name: definitionName }: NextDefinition, handlerName: string) => {
const handlerFile = resolve(EDGE_FUNCTIONS_DIR, handlerName, `${handlerName}.js`)
const rel = relative(handlerFile, join(PLUGIN_DIR, 'dist/run/handlers/middleware.js'))
await cp(
join(PLUGIN_DIR, 'edge-runtime'),
resolve(EDGE_FUNCTIONS_DIR, handlerName, 'edge-runtime'),
{
recursive: true,
},
)
await writeFile(
resolve(EDGE_FUNCTIONS_DIR, handlerName, `${handlerName}.js`),
`import {handleMiddleware} from './edge-runtime/middleware.ts';
import handler from './server/${definitionName}.js';
export default (req, context) => handleMiddleware(req, context, handler);
export const config = {path: "/*"}`,
)
}

const writeEdgeManifest = async (manifest: NetlifyManifest) => {
await mkdir(resolve(EDGE_FUNCTIONS_DIR), { recursive: true })
await writeFile(resolve(EDGE_FUNCTIONS_DIR, 'manifest.json'), JSON.stringify(manifest, null, 2))
}

export const createEdgeHandlers = async () => {
await rm(EDGE_FUNCTIONS_DIR, { recursive: true, force: true })

const nextManifest = await getMiddlewareManifest()
const nextDefinitions = [
...Object.values(nextManifest.middleware),
// ...Object.values(nextManifest.functions)
]
const netlifyManifest: NetlifyManifest = {
version: 1,
functions: await nextDefinitions.reduce(
async (netlifyDefinitions: Promise<NetlifyDefinition[]>, nextDefinition: NextDefinition) => {
const handlerName = getHandlerName(nextDefinition)
await copyHandlerDependencies(nextDefinition, handlerName)
await writeHandlerFile(nextDefinition, handlerName)
return [
...(await netlifyDefinitions),
...buildHandlerDefinitions(nextDefinition, handlerName),
]
},
Promise.resolve([]),
),
}

await writeEdgeManifest(netlifyManifest)
}
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
uploadStaticContent,
} from './build/content/static.js'
import { createServerHandler } from './build/functions/server.js'
import { createEdgeHandlers } from './build/functions/edge.js'

export const onPreBuild = async ({ constants, utils }: NetlifyPluginOptions) => {
setPreBuildConfig()
Expand All @@ -23,7 +24,7 @@ export const onBuild = async ({ constants, utils }: NetlifyPluginOptions) => {
uploadStaticContent({ constants }),
uploadPrerenderedContent({ constants }),
createServerHandler({ constants }),
// createEdgeHandler(),
createEdgeHandlers(),
])
}

Expand Down
12 changes: 12 additions & 0 deletions tests/fixtures/middleware/app/layout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export const metadata = {
title: 'Simple Next App',
description: 'Description for Simple Next App',
}

export default function RootLayout({ children }) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}
7 changes: 7 additions & 0 deletions tests/fixtures/middleware/app/other/page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function Home() {
return (
<main>
<h1>Other</h1>
</main>
)
}
7 changes: 7 additions & 0 deletions tests/fixtures/middleware/app/page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function Home() {
return (
<main>
<h1>Home</h1>
</main>
)
}
10 changes: 10 additions & 0 deletions tests/fixtures/middleware/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
return NextResponse.redirect(new URL('/other', request.url))
}

export const config = {
matcher: '/test',
}
9 changes: 9 additions & 0 deletions tests/fixtures/middleware/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone',
eslint: {
ignoreDuringBuilds: true,
},
}

module.exports = nextConfig
Loading

0 comments on commit 90bb11b

Please sign in to comment.