From 82a720edd897838239a9a3037be4013558f830a7 Mon Sep 17 00:00:00 2001 From: Kiko Beats Date: Fri, 5 May 2023 15:38:55 +0000 Subject: [PATCH 01/16] add nodejs-web runtime --- packages/node/package.json | 2 ++ .../serverless-handler.mts | 24 +++++++++++++++++- packages/node/src/utils.ts | 1 + packages/node/test/dev-fixtures/nodejs-web.js | 12 +++++++++ packages/node/test/unit/dev.test.ts | 25 +++++++++++++++++++ pnpm-lock.yaml | 7 +++++- 6 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 packages/node/test/dev-fixtures/nodejs-web.js diff --git a/packages/node/package.json b/packages/node/package.json index 9f3afe93113..7bf399356c0 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -19,6 +19,8 @@ "dist" ], "dependencies": { + "@edge-runtime/node-utils": "2.0.3", + "@edge-runtime/primitives": "2.1.2", "@edge-runtime/vm": "2.0.0", "@types/node": "14.18.33", "@types/node-fetch": "2.6.3", diff --git a/packages/node/src/serverless-functions/serverless-handler.mts b/packages/node/src/serverless-functions/serverless-handler.mts index 3bb9edb984f..8e9b8e3828b 100644 --- a/packages/node/src/serverless-functions/serverless-handler.mts +++ b/packages/node/src/serverless-functions/serverless-handler.mts @@ -1,12 +1,17 @@ import { addHelpers } from './helpers.js'; +import { buildToNodeHandler } from '@edge-runtime/node-utils'; import { createServer } from 'http'; +import { getConfig } from '@vercel/static-config'; +import { Project } from 'ts-morph'; import { serializeBody } from '../utils.js'; import { streamToBuffer } from '@vercel/build-utils'; +import primitives from '@edge-runtime/primitives' import exitHook from 'exit-hook'; import fetch from 'node-fetch'; import { listen } from 'async-listen'; import { isAbsolute } from 'path'; import { pathToFileURL } from 'url'; +import type { BuildDependencies } from '@edge-runtime/node-utils'; import type { ServerResponse, IncomingMessage } from 'http'; import type { VercelProxyResponse } from '../types.js'; import type { VercelRequest, VercelResponse } from './helpers.js'; @@ -21,6 +26,9 @@ type ServerlessFunctionSignature = ( res: ServerResponse | VercelResponse ) => void; +const parseConfig = (entryPointPath: string) => + getConfig(new Project(), entryPointPath); + async function createServerlessServer( userCode: ServerlessFunctionSignature, options: ServerlessServerOptions @@ -46,7 +54,21 @@ async function compileUserCode(entrypointPath: string) { if (fn.default) fn = fn.default; } - return fn; + const staticConfig = parseConfig(entrypointPath); + if (staticConfig?.runtime !== 'nodejs-web') return fn + + return buildToNodeHandler( + { + Headers: primitives.Headers as BuildDependencies['Headers'], + ReadableStream: primitives.ReadableStream, + Request: primitives.Request as BuildDependencies['Request'], + Uint8Array: Uint8Array, + FetchEvent: primitives.FetchEvent + }, + /** fallback when headers.host is missing for creating the absolute `req.url` URL */ + { defaultOrigin: 'https://vercel.com' } + )(fn) + } export async function createServerlessEventHandler( diff --git a/packages/node/src/utils.ts b/packages/node/src/utils.ts index 7a7a3c91c0b..a105f29f143 100644 --- a/packages/node/src/utils.ts +++ b/packages/node/src/utils.ts @@ -72,6 +72,7 @@ export function logError(error: Error) { export enum EdgeRuntimes { Edge = 'edge', ExperimentalEdge = 'experimental-edge', + NodeJsWeb = 'nodejs-web', } export function isEdgeRuntime(runtime?: string): runtime is EdgeRuntimes { diff --git a/packages/node/test/dev-fixtures/nodejs-web.js b/packages/node/test/dev-fixtures/nodejs-web.js new file mode 100644 index 00000000000..7ef67e471be --- /dev/null +++ b/packages/node/test/dev-fixtures/nodejs-web.js @@ -0,0 +1,12 @@ +/* global Response */ + +export const config = { runtime: 'nodejs-web' }; + +const baseUrl = ({ headers }) => + `${headers.get('x-forwarded-proto')}://${headers.get('x-forwarded-host')}`; + +export default request => { + const { searchParams } = new URL(request.url, baseUrl(request)); + const name = searchParams.get('name'); + return new Response(`Greetings, ${name}`); +}; diff --git a/packages/node/test/unit/dev.test.ts b/packages/node/test/unit/dev.test.ts index b5de91cda1c..cb3785bc57d 100644 --- a/packages/node/test/unit/dev.test.ts +++ b/packages/node/test/unit/dev.test.ts @@ -24,6 +24,31 @@ function testForkDevServer(entrypoint: string) { }); } +test('runs a nodejs-web function', async () => { + const child = testForkDevServer('./nodejs-web.js'); + try { + const result = await readMessage(child); + if (result.state !== 'message') { + throw new Error('Exited. error: ' + JSON.stringify(result.value)); + } + + const { address, port } = result.value; + const response = await fetch( + `http://${address}:${port}/api/nodejs-web?name=Vercel` + ); + + expect({ + status: response.status, + body: await response.text(), + }).toEqual({ + status: 200, + body: 'Greetings, Vercel', + }); + } finally { + child.kill(9); + } +}); + test('runs an edge function that uses `WebSocket`', async () => { const child = testForkDevServer('./edge-websocket.js'); try { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9e98b3868e6..e7978eca13e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1189,6 +1189,12 @@ importers: packages/node: dependencies: + '@edge-runtime/node-utils': + specifier: 2.0.3 + version: 2.0.3 + '@edge-runtime/primitives': + specifier: 2.1.2 + version: 2.1.2 '@edge-runtime/vm': specifier: 2.0.0 version: 2.0.0 @@ -3152,7 +3158,6 @@ packages: /@edge-runtime/node-utils@2.0.3: resolution: {integrity: sha512-JUSbi5xu/A8+D2t9B9wfirCI1J8n8q0660FfmqZgA+n3RqxD3y7SnamL1sKRE5/AbHsKs9zcqCbK2YDklbc9Bg==} engines: {node: '>=14'} - dev: true /@edge-runtime/primitives@2.0.0: resolution: {integrity: sha512-AXqUq1zruTJAICrllUvZcgciIcEGHdF6KJ3r6FM0n4k8LpFxZ62tPWVIJ9HKm+xt+ncTBUZxwgUaQ73QMUQEKw==} From 9ae470e52e2b869ebf377b4398416ff98dad87a9 Mon Sep 17 00:00:00 2001 From: Kiko Beats Date: Fri, 12 May 2023 10:03:48 +0000 Subject: [PATCH 02/16] [node] serverless named exports --- .../serverless-handler.mts | 88 ++++++++++++------- packages/node/src/utils.ts | 1 - .../{nodejs-web.js => serverless-web.js} | 6 +- packages/node/test/unit/dev.test.ts | 6 +- 4 files changed, 61 insertions(+), 40 deletions(-) rename packages/node/test/dev-fixtures/{nodejs-web.js => serverless-web.js} (78%) diff --git a/packages/node/src/serverless-functions/serverless-handler.mts b/packages/node/src/serverless-functions/serverless-handler.mts index 8e9b8e3828b..8ae5b7c3438 100644 --- a/packages/node/src/serverless-functions/serverless-handler.mts +++ b/packages/node/src/serverless-functions/serverless-handler.mts @@ -1,11 +1,9 @@ import { addHelpers } from './helpers.js'; import { buildToNodeHandler } from '@edge-runtime/node-utils'; import { createServer } from 'http'; -import { getConfig } from '@vercel/static-config'; -import { Project } from 'ts-morph'; import { serializeBody } from '../utils.js'; import { streamToBuffer } from '@vercel/build-utils'; -import primitives from '@edge-runtime/primitives' +import primitives from '@edge-runtime/primitives'; import exitHook from 'exit-hook'; import fetch from 'node-fetch'; import { listen } from 'async-listen'; @@ -26,57 +24,83 @@ type ServerlessFunctionSignature = ( res: ServerResponse | VercelResponse ) => void; -const parseConfig = (entryPointPath: string) => - getConfig(new Project(), entryPointPath); +/* https://nextjs.org/docs/app/building-your-application/routing/router-handlers#supported-http-methods */ +const HTTP_METHODS = [ + 'GET', + 'HEAD', + 'OPTIONS', + 'POST', + 'PUT', + 'DELETE', + 'PATCH', +] as const; -async function createServerlessServer( - userCode: ServerlessFunctionSignature, - options: ServerlessServerOptions -) { - const server = createServer(async (req, res) => { - if (options.shouldAddHelpers) await addHelpers(req, res); - return userCode(req, res); - }); +type HTTP_METHOD = typeof HTTP_METHODS[number]; + +function isHTTPMethod(maybeMethod: string): maybeMethod is HTTP_METHOD { + return HTTP_METHODS.includes(maybeMethod as HTTP_METHOD); +} + +const defaultHttpHandler: ServerlessFunctionSignature = (_, res) => { + res.statusCode = 405; + res.end(); +}; + +async function createServerlessServer(userCode: ServerlessFunctionSignature) { + const server = createServer(userCode); exitHook(() => server.close()); return { url: await listen(server) }; } -async function compileUserCode(entrypointPath: string) { +async function compileUserCode( + entrypointPath: string, + options: ServerlessServerOptions +) { const id = isAbsolute(entrypointPath) ? pathToFileURL(entrypointPath).href : entrypointPath; - let fn = await import(id); + let listener = await import(id); /** * In some cases we might have nested default props due to TS => JS */ for (let i = 0; i < 5; i++) { - if (fn.default) fn = fn.default; + if (listener.default) listener = listener.default; } - const staticConfig = parseConfig(entrypointPath); - if (staticConfig?.runtime !== 'nodejs-web') return fn - - return buildToNodeHandler( - { - Headers: primitives.Headers as BuildDependencies['Headers'], - ReadableStream: primitives.ReadableStream, - Request: primitives.Request as BuildDependencies['Request'], - Uint8Array: Uint8Array, - FetchEvent: primitives.FetchEvent - }, - /** fallback when headers.host is missing for creating the absolute `req.url` URL */ - { defaultOrigin: 'https://vercel.com' } - )(fn) + if (Object.keys(listener).every(key => !isHTTPMethod(key))) { + return async (req: IncomingMessage, res: ServerResponse) => { + if (options.shouldAddHelpers) await addHelpers(req, res); + return listener(req, res); + }; + } + + HTTP_METHODS.forEach(httpMethod => { + listener[httpMethod] = + listener[httpMethod] === undefined + ? defaultHttpHandler + : buildToNodeHandler( + { + Headers: primitives.Headers as BuildDependencies['Headers'], + ReadableStream: primitives.ReadableStream, + Request: primitives.Request as BuildDependencies['Request'], + Uint8Array: Uint8Array, + FetchEvent: primitives.FetchEvent, + }, + { defaultOrigin: 'https://vercel.com' } + )(listener[httpMethod]); + }); + return (req: IncomingMessage, res: ServerResponse) => + listener[req.method ?? 'GET'](req, res); } export async function createServerlessEventHandler( entrypointPath: string, options: ServerlessServerOptions ): Promise<(request: IncomingMessage) => Promise> { - const userCode = await compileUserCode(entrypointPath); - const server = await createServerlessServer(userCode, options); + const userCode = await compileUserCode(entrypointPath, options); + const server = await createServerlessServer(userCode); return async function (request: IncomingMessage) { const url = new URL(request.url ?? '/', server.url); diff --git a/packages/node/src/utils.ts b/packages/node/src/utils.ts index a105f29f143..7a7a3c91c0b 100644 --- a/packages/node/src/utils.ts +++ b/packages/node/src/utils.ts @@ -72,7 +72,6 @@ export function logError(error: Error) { export enum EdgeRuntimes { Edge = 'edge', ExperimentalEdge = 'experimental-edge', - NodeJsWeb = 'nodejs-web', } export function isEdgeRuntime(runtime?: string): runtime is EdgeRuntimes { diff --git a/packages/node/test/dev-fixtures/nodejs-web.js b/packages/node/test/dev-fixtures/serverless-web.js similarity index 78% rename from packages/node/test/dev-fixtures/nodejs-web.js rename to packages/node/test/dev-fixtures/serverless-web.js index 7ef67e471be..b88c0a61c8a 100644 --- a/packages/node/test/dev-fixtures/nodejs-web.js +++ b/packages/node/test/dev-fixtures/serverless-web.js @@ -1,12 +1,10 @@ /* global Response */ -export const config = { runtime: 'nodejs-web' }; - const baseUrl = ({ headers }) => `${headers.get('x-forwarded-proto')}://${headers.get('x-forwarded-host')}`; -export default request => { +export function GET(request) { const { searchParams } = new URL(request.url, baseUrl(request)); const name = searchParams.get('name'); return new Response(`Greetings, ${name}`); -}; +} diff --git a/packages/node/test/unit/dev.test.ts b/packages/node/test/unit/dev.test.ts index cb3785bc57d..2759b82670d 100644 --- a/packages/node/test/unit/dev.test.ts +++ b/packages/node/test/unit/dev.test.ts @@ -24,8 +24,8 @@ function testForkDevServer(entrypoint: string) { }); } -test('runs a nodejs-web function', async () => { - const child = testForkDevServer('./nodejs-web.js'); +test('runs an serverless function that exports GET', async () => { + const child = testForkDevServer('./serverless-web.js'); try { const result = await readMessage(child); if (result.state !== 'message') { @@ -34,7 +34,7 @@ test('runs a nodejs-web function', async () => { const { address, port } = result.value; const response = await fetch( - `http://${address}:${port}/api/nodejs-web?name=Vercel` + `http://${address}:${port}/api/serverless-web?name=Vercel` ); expect({ From c4d70963fff5744e19535a4a407ac4587032ad3c Mon Sep 17 00:00:00 2001 From: Kiko Beats Date: Fri, 12 May 2023 17:49:31 +0000 Subject: [PATCH 03/16] use server address instead of localhost there is a bug in node-fetch that requires to don't use localhost node-fetch/node-fetch#1624 --- packages/cli/src/util/extension/proxy.ts | 5 +---- packages/node/test/unit/dev.test.ts | 20 ++++++++------------ 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/packages/cli/src/util/extension/proxy.ts b/packages/cli/src/util/extension/proxy.ts index dc9c6fcec7d..fe71fc2f29c 100644 --- a/packages/cli/src/util/extension/proxy.ts +++ b/packages/cli/src/util/extension/proxy.ts @@ -8,10 +8,7 @@ import { import type { Server } from 'http'; import type Client from '../client'; -const toHeaders = buildToHeaders({ - // @ts-expect-error - `node-fetch` Headers is missing `getAll()` - Headers, -}); +const toHeaders = buildToHeaders({ Headers }); export function createProxy(client: Client): Server { return createServer(async (req, res) => { diff --git a/packages/node/test/unit/dev.test.ts b/packages/node/test/unit/dev.test.ts index 2759b82670d..0fd1e4fe72d 100644 --- a/packages/node/test/unit/dev.test.ts +++ b/packages/node/test/unit/dev.test.ts @@ -82,9 +82,8 @@ test('runs an edge function that uses `buffer`', async () => { throw new Error('Exited. error: ' + JSON.stringify(result.value)); } - const response = await fetch( - `http://localhost:${result.value.port}/api/edge-buffer` - ); + const { address, port } = result.value; + const response = await fetch(`http://${address}:${port}/api/edge-buffer`); expect({ status: response.status, json: await response.json(), @@ -109,9 +108,8 @@ test('runs a mjs endpoint', async () => { throw new Error('Exited. error: ' + JSON.stringify(result.value)); } - const response = await fetch( - `http://localhost:${result.value.port}/api/hello` - ); + const { address, port } = result.value; + const response = await fetch(`http://${address}:${port}/api/hello`); expect({ status: response.status, headers: Object.fromEntries(response.headers), @@ -142,9 +140,8 @@ test('runs a esm typescript endpoint', async () => { throw new Error('Exited. error: ' + JSON.stringify(result.value)); } - const response = await fetch( - `http://localhost:${result.value.port}/api/hello` - ); + const { address, port } = result.value; + const response = await fetch(`http://${address}:${port}/api/hello`); expect({ status: response.status, headers: Object.fromEntries(response.headers), @@ -175,9 +172,8 @@ test('allow setting multiple cookies with same name', async () => { throw new Error(`Exited. error: ${JSON.stringify(result.value)}`); } - const response = await fetch( - `http://localhost:${result.value.port}/api/hello` - ); + const { address, port } = result.value; + const response = await fetch(`http://${address}:${port}/api/hello`); expect({ status: response.status, text: await response.text(), From d40ac950b6d41ebe625c436f360a034ac7bdf917 Mon Sep 17 00:00:00 2001 From: Kiko Beats Date: Wed, 24 May 2023 13:01:11 +0100 Subject: [PATCH 04/16] refactor: move into helpers-web --- .../src/serverless-functions/helpers-web.ts | 76 +++++++++++++++++++ .../serverless-handler.mts | 49 +++--------- 2 files changed, 87 insertions(+), 38 deletions(-) create mode 100644 packages/node/src/serverless-functions/helpers-web.ts diff --git a/packages/node/src/serverless-functions/helpers-web.ts b/packages/node/src/serverless-functions/helpers-web.ts new file mode 100644 index 00000000000..edf1ccbbddb --- /dev/null +++ b/packages/node/src/serverless-functions/helpers-web.ts @@ -0,0 +1,76 @@ +import type { ServerResponse, IncomingMessage } from 'http'; +import type { NodeHandler } from '@edge-runtime/node-utils'; +import { buildToNodeHandler } from '@edge-runtime/node-utils'; + +class FetchEvent { + public request: Request; + public awaiting: Set>; + public response: Response | null; + + constructor(request: Request) { + this.request = request; + this.response = null; + this.awaiting = new Set(); + } + + respondWith(response: Response) { + this.response = response; + } + + waitUntil() { + throw new Error('waitUntil is not implemented yet for Node.js'); + } +} + +const webHandlerToNodeHandler = buildToNodeHandler( + { + Headers, + ReadableStream, + Request: class extends Request { + constructor(input: RequestInfo | URL, init?: RequestInit | undefined) { + super(input, addDuplexToInit(init)); + } + }, + Uint8Array: Uint8Array, + FetchEvent: FetchEvent, + }, + { defaultOrigin: 'https://vercel.com' } +); + +/** + * When users export at least one HTTP handler, we will generate + * a generic handler routing to the right method. If there is no + * handler function exported returns null. + */ +export function getWebExportsHandler(listener: any, methods: string[]) { + const handlerByMethod: { [key: string]: NodeHandler } = {}; + + for (const key of methods) { + handlerByMethod[key] = + typeof listener[key] !== 'undefined' + ? webHandlerToNodeHandler(listener[key]) + : defaultHttpHandler; + } + + return (req: IncomingMessage, res: ServerResponse) => { + const method = req.method ?? 'GET'; + handlerByMethod[method](req, res); + }; +} + +/** + * Add `duplex: 'half'` by default to all requests + * https://github.com/vercel/edge-runtime/blob/bf167c418247a79d3941bfce4a5d43c37f512502/packages/primitives/src/primitives/fetch.js#L22-L26 + * https://developer.chrome.com/articles/fetch-streaming-requests/#streaming-request-bodies + */ +function addDuplexToInit(init: RequestInit | undefined) { + if (typeof init === 'undefined' || typeof init === 'object') { + return { duplex: 'half', ...init }; + } + return init; +} + +function defaultHttpHandler(_: IncomingMessage, res: ServerResponse) { + res.statusCode = 405; + res.end(); +} diff --git a/packages/node/src/serverless-functions/serverless-handler.mts b/packages/node/src/serverless-functions/serverless-handler.mts index 8ae5b7c3438..3f545cefe6e 100644 --- a/packages/node/src/serverless-functions/serverless-handler.mts +++ b/packages/node/src/serverless-functions/serverless-handler.mts @@ -1,15 +1,12 @@ import { addHelpers } from './helpers.js'; -import { buildToNodeHandler } from '@edge-runtime/node-utils'; import { createServer } from 'http'; import { serializeBody } from '../utils.js'; import { streamToBuffer } from '@vercel/build-utils'; -import primitives from '@edge-runtime/primitives'; import exitHook from 'exit-hook'; import fetch from 'node-fetch'; import { listen } from 'async-listen'; import { isAbsolute } from 'path'; import { pathToFileURL } from 'url'; -import type { BuildDependencies } from '@edge-runtime/node-utils'; import type { ServerResponse, IncomingMessage } from 'http'; import type { VercelProxyResponse } from '../types.js'; import type { VercelRequest, VercelResponse } from './helpers.js'; @@ -24,6 +21,8 @@ type ServerlessFunctionSignature = ( res: ServerResponse | VercelResponse ) => void; +const [nodeMajor] = process.versions.node.split('.').map(v => Number(v)); + /* https://nextjs.org/docs/app/building-your-application/routing/router-handlers#supported-http-methods */ const HTTP_METHODS = [ 'GET', @@ -33,18 +32,7 @@ const HTTP_METHODS = [ 'PUT', 'DELETE', 'PATCH', -] as const; - -type HTTP_METHOD = typeof HTTP_METHODS[number]; - -function isHTTPMethod(maybeMethod: string): maybeMethod is HTTP_METHOD { - return HTTP_METHODS.includes(maybeMethod as HTTP_METHOD); -} - -const defaultHttpHandler: ServerlessFunctionSignature = (_, res) => { - res.statusCode = 405; - res.end(); -}; +]; async function createServerlessServer(userCode: ServerlessFunctionSignature) { const server = createServer(userCode); @@ -68,31 +56,16 @@ async function compileUserCode( if (listener.default) listener = listener.default; } - if (Object.keys(listener).every(key => !isHTTPMethod(key))) { - return async (req: IncomingMessage, res: ServerResponse) => { - if (options.shouldAddHelpers) await addHelpers(req, res); - return listener(req, res); - }; + if (HTTP_METHODS.some(method => typeof listener[method] === 'function')) { + if (nodeMajor < 18) throw new Error('Node.js v18 or above is needed') + const { getWebExportsHandler } = await import('./helpers-web.js'); + return getWebExportsHandler(listener, HTTP_METHODS); } - HTTP_METHODS.forEach(httpMethod => { - listener[httpMethod] = - listener[httpMethod] === undefined - ? defaultHttpHandler - : buildToNodeHandler( - { - Headers: primitives.Headers as BuildDependencies['Headers'], - ReadableStream: primitives.ReadableStream, - Request: primitives.Request as BuildDependencies['Request'], - Uint8Array: Uint8Array, - FetchEvent: primitives.FetchEvent, - }, - { defaultOrigin: 'https://vercel.com' } - )(listener[httpMethod]); - }); - - return (req: IncomingMessage, res: ServerResponse) => - listener[req.method ?? 'GET'](req, res); + return async (req: IncomingMessage, res: ServerResponse) => { + if (options.shouldAddHelpers) await addHelpers(req, res); + return listener(req, res); + }; } export async function createServerlessEventHandler( From 706699e95033b67c14c8e62bd17918bd4db6e9be Mon Sep 17 00:00:00 2001 From: Kiko Beats Date: Wed, 24 May 2023 13:30:00 +0100 Subject: [PATCH 05/16] add named export for Edge --- .../edge-functions/edge-handler-template.js | 34 ++++++++++++++----- .../node/src/edge-functions/edge-handler.mts | 9 ++--- .../serverless-handler.mts | 4 +-- 3 files changed, 29 insertions(+), 18 deletions(-) diff --git a/packages/node/src/edge-functions/edge-handler-template.js b/packages/node/src/edge-functions/edge-handler-template.js index 33348e0ecf1..2b385869bc2 100644 --- a/packages/node/src/edge-functions/edge-handler-template.js +++ b/packages/node/src/edge-functions/edge-handler-template.js @@ -10,14 +10,14 @@ function getUrl(url, headers) { return urlObj.toString(); } -async function respond(userEdgeHandler, event, options, dependencies) { +async function respond(handler, event, options, dependencies) { const { Request, Response } = dependencies; const { isMiddleware } = options; event.request.headers.set( 'host', event.request.headers.get('x-forwarded-host') ); - let response = await userEdgeHandler( + let response = await handler( new Request( getUrl(event.request.url, event.request.headers), event.request @@ -62,15 +62,31 @@ async function parseRequestEvent(event) { // This will be invoked by logic using this template // eslint-disable-next-line @typescript-eslint/no-unused-vars -function registerFetchListener(userEdgeHandler, options, dependencies) { +function registerFetchListener(module, options, dependencies) { addEventListener('fetch', async event => { + let handler; + if (typeof module.default === 'function') { + handler = module.default; + } else { + if ( + ['GET', 'HEAD', 'OPTIONS', 'POST', 'PUT', 'DELETE', 'PATCH'].some( + method => typeof module[method] === 'function' + ) + ) { + const method = event.request.method ?? 'GET'; + handler = + typeof module[method] === 'function' + ? module[method] + : () => new dependencies.Response(null, { status: 405 }); + } else { + throw new Error( + 'No default export was found. Add a default export to handle requests. Learn more: https://vercel.link/creating-edge-middleware' + ); + } + } + try { - const response = await respond( - userEdgeHandler, - event, - options, - dependencies - ); + const response = await respond(handler, event, options, dependencies); return event.respondWith(response); } catch (error) { event.respondWith(toResponseError(error, dependencies.Response)); diff --git a/packages/node/src/edge-functions/edge-handler.mts b/packages/node/src/edge-functions/edge-handler.mts index 2bcf431fead..afe873040c2 100644 --- a/packages/node/src/edge-functions/edge-handler.mts +++ b/packages/node/src/edge-functions/edge-handler.mts @@ -89,12 +89,7 @@ async function compileUserCode( // user code ${compiledFile.text}; - const userEdgeHandler = module.exports.default; - if (!userEdgeHandler) { - throw new Error( - 'No default export was found. Add a default export to handle requests. Learn more: https://vercel.link/creating-edge-middleware' - ); - } + const handler = module.exports; // request metadata const isMiddleware = ${isMiddleware}; @@ -104,7 +99,7 @@ async function compileUserCode( ${edgeHandlerTemplate}; const dependencies = { Request, Response }; const options = { isMiddleware, entrypointLabel }; - registerFetchListener(userEdgeHandler, options, dependencies); + registerFetchListener(handler, options, dependencies); `; return { diff --git a/packages/node/src/serverless-functions/serverless-handler.mts b/packages/node/src/serverless-functions/serverless-handler.mts index 3f545cefe6e..e31f384f5e7 100644 --- a/packages/node/src/serverless-functions/serverless-handler.mts +++ b/packages/node/src/serverless-functions/serverless-handler.mts @@ -21,7 +21,7 @@ type ServerlessFunctionSignature = ( res: ServerResponse | VercelResponse ) => void; -const [nodeMajor] = process.versions.node.split('.').map(v => Number(v)); +const [NODE_MAJOR] = process.versions.node.split('.').map(v => Number(v)); /* https://nextjs.org/docs/app/building-your-application/routing/router-handlers#supported-http-methods */ const HTTP_METHODS = [ @@ -57,7 +57,7 @@ async function compileUserCode( } if (HTTP_METHODS.some(method => typeof listener[method] === 'function')) { - if (nodeMajor < 18) throw new Error('Node.js v18 or above is needed') + if (NODE_MAJOR < 18) throw new Error('Node.js v18 or above is needed'); const { getWebExportsHandler } = await import('./helpers-web.js'); return getWebExportsHandler(listener, HTTP_METHODS); } From 43df4a67ce11823d3921439f11593f9cd177405c Mon Sep 17 00:00:00 2001 From: Kiko Beats Date: Thu, 25 May 2023 09:38:43 +0100 Subject: [PATCH 06/16] test: only for node18 or above --- packages/node/test/unit/dev.test.ts | 56 +++++++++++++++++------------ 1 file changed, 34 insertions(+), 22 deletions(-) diff --git a/packages/node/test/unit/dev.test.ts b/packages/node/test/unit/dev.test.ts index 0fd1e4fe72d..0f88af44461 100644 --- a/packages/node/test/unit/dev.test.ts +++ b/packages/node/test/unit/dev.test.ts @@ -4,6 +4,8 @@ import fetch from 'node-fetch'; jest.setTimeout(20 * 1000); +const [NODE_MAJOR] = process.versions.node.split('.').map(v => Number(v)); + function testForkDevServer(entrypoint: string) { const ext = extname(entrypoint); const isTypeScript = ext === '.ts'; @@ -24,30 +26,40 @@ function testForkDevServer(entrypoint: string) { }); } -test('runs an serverless function that exports GET', async () => { - const child = testForkDevServer('./serverless-web.js'); - try { - const result = await readMessage(child); - if (result.state !== 'message') { - throw new Error('Exited. error: ' + JSON.stringify(result.value)); +(NODE_MAJOR < 18 ? test.skip : test)( + 'runs an serverless function that exports GET', + async () => { + const child = testForkDevServer('./serverless-web.js'); + try { + const result = await readMessage(child); + if (result.state !== 'message') { + throw new Error('Exited. error: ' + JSON.stringify(result.value)); + } + + const { address, port } = result.value; + + { + const response = await fetch( + `http://${address}:${port}/api/serverless-web?name=Vercel` + ); + expect({ + status: response.status, + body: await response.text(), + }).toEqual({ status: 200, body: 'Greetings, Vercel' }); + } + + { + const response = await fetch( + `http://${address}:${port}/api/serverless-web?name=Vercel`, + { method: 'HEAD' } + ); + expect({ status: response.status }).toEqual({ status: 405 }); + } + } finally { + child.kill(9); } - - const { address, port } = result.value; - const response = await fetch( - `http://${address}:${port}/api/serverless-web?name=Vercel` - ); - - expect({ - status: response.status, - body: await response.text(), - }).toEqual({ - status: 200, - body: 'Greetings, Vercel', - }); - } finally { - child.kill(9); } -}); +); test('runs an edge function that uses `WebSocket`', async () => { const child = testForkDevServer('./edge-websocket.js'); From 8503358350c2741c41fed77be825116480f13841 Mon Sep 17 00:00:00 2001 From: Kiko Beats Date: Thu, 25 May 2023 09:39:35 +0100 Subject: [PATCH 07/16] Update packages/node/src/edge-functions/edge-handler-template.js Co-authored-by: Sean Massa --- packages/node/src/edge-functions/edge-handler-template.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/node/src/edge-functions/edge-handler-template.js b/packages/node/src/edge-functions/edge-handler-template.js index 2b385869bc2..feeb088229c 100644 --- a/packages/node/src/edge-functions/edge-handler-template.js +++ b/packages/node/src/edge-functions/edge-handler-template.js @@ -80,7 +80,7 @@ function registerFetchListener(module, options, dependencies) { : () => new dependencies.Response(null, { status: 405 }); } else { throw new Error( - 'No default export was found. Add a default export to handle requests. Learn more: https://vercel.link/creating-edge-middleware' + `No default export was found at ${event.request.url}. Add a default export to handle requests. Learn more: https://vercel.link/creating-edge-middleware` ); } } From 1a2c62e26af39364637d56e9278abf76f0e01b4c Mon Sep 17 00:00:00 2001 From: Kiko Beats Date: Thu, 25 May 2023 09:40:30 +0100 Subject: [PATCH 08/16] rename handler into userModule --- packages/node/src/edge-functions/edge-handler.mts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/node/src/edge-functions/edge-handler.mts b/packages/node/src/edge-functions/edge-handler.mts index afe873040c2..af004aca668 100644 --- a/packages/node/src/edge-functions/edge-handler.mts +++ b/packages/node/src/edge-functions/edge-handler.mts @@ -89,7 +89,7 @@ async function compileUserCode( // user code ${compiledFile.text}; - const handler = module.exports; + const userModule = module.exports; // request metadata const isMiddleware = ${isMiddleware}; @@ -99,7 +99,7 @@ async function compileUserCode( ${edgeHandlerTemplate}; const dependencies = { Request, Response }; const options = { isMiddleware, entrypointLabel }; - registerFetchListener(handler, options, dependencies); + registerFetchListener(userModule, options, dependencies); `; return { From b923bc66c18bd9f6531c9bd3cbd849f6a2fea3ad Mon Sep 17 00:00:00 2001 From: Kiko Beats Date: Mon, 5 Jun 2023 19:15:33 +0000 Subject: [PATCH 09/16] fix: store error into a variable --- .../edge-functions/edge-handler-template.js | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/node/src/edge-functions/edge-handler-template.js b/packages/node/src/edge-functions/edge-handler-template.js index feeb088229c..2a36cb8a230 100644 --- a/packages/node/src/edge-functions/edge-handler-template.js +++ b/packages/node/src/edge-functions/edge-handler-template.js @@ -63,8 +63,9 @@ async function parseRequestEvent(event) { // This will be invoked by logic using this template // eslint-disable-next-line @typescript-eslint/no-unused-vars function registerFetchListener(module, options, dependencies) { + let handler; + addEventListener('fetch', async event => { - let handler; if (typeof module.default === 'function') { handler = module.default; } else { @@ -78,19 +79,18 @@ function registerFetchListener(module, options, dependencies) { typeof module[method] === 'function' ? module[method] : () => new dependencies.Response(null, { status: 405 }); - } else { - throw new Error( - `No default export was found at ${event.request.url}. Add a default export to handle requests. Learn more: https://vercel.link/creating-edge-middleware` - ); } } - try { - const response = await respond(handler, event, options, dependencies); - return event.respondWith(response); - } catch (error) { - event.respondWith(toResponseError(error, dependencies.Response)); + if (!handler) { + const error = new Error( + `No default export was found at ${event.request.url}. Add a default export to handle requests. Learn more: https://vercel.link/creating-edge-middleware` + ); + return event.respondWith(toResponseError(error, dependencies.Response)); } + + const response = await respond(handler, event, options, dependencies); + event.respondWith(response); }); } From db77f8b372f9c7b2ca9b5e31c472965c98608591 Mon Sep 17 00:00:00 2001 From: Kiko Beats Date: Tue, 6 Jun 2023 10:36:13 +0200 Subject: [PATCH 10/16] Create young-rats-smoke.md --- .changeset/young-rats-smoke.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/young-rats-smoke.md diff --git a/.changeset/young-rats-smoke.md b/.changeset/young-rats-smoke.md new file mode 100644 index 00000000000..f73489e0346 --- /dev/null +++ b/.changeset/young-rats-smoke.md @@ -0,0 +1,6 @@ +--- +"vercel": minor +"@vercel/node": minor +--- + +[node] Add isomorphic functions From 0ae95e4e1e5e974079f15632112ff872bd86a1a7 Mon Sep 17 00:00:00 2001 From: Kiko Beats Date: Tue, 6 Jun 2023 11:05:13 +0200 Subject: [PATCH 11/16] keep try/catch --- .../edge-functions/edge-handler-template.js | 48 ++++++++++--------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/packages/node/src/edge-functions/edge-handler-template.js b/packages/node/src/edge-functions/edge-handler-template.js index 2a36cb8a230..42b5adc6351 100644 --- a/packages/node/src/edge-functions/edge-handler-template.js +++ b/packages/node/src/edge-functions/edge-handler-template.js @@ -66,31 +66,35 @@ function registerFetchListener(module, options, dependencies) { let handler; addEventListener('fetch', async event => { - if (typeof module.default === 'function') { - handler = module.default; - } else { - if ( - ['GET', 'HEAD', 'OPTIONS', 'POST', 'PUT', 'DELETE', 'PATCH'].some( - method => typeof module[method] === 'function' - ) - ) { - const method = event.request.method ?? 'GET'; - handler = - typeof module[method] === 'function' - ? module[method] - : () => new dependencies.Response(null, { status: 405 }); + try { + if (typeof module.default === 'function') { + handler = module.default; + } else { + if ( + ['GET', 'HEAD', 'OPTIONS', 'POST', 'PUT', 'DELETE', 'PATCH'].some( + method => typeof module[method] === 'function' + ) + ) { + const method = event.request.method ?? 'GET'; + handler = + typeof module[method] === 'function' + ? module[method] + : () => new dependencies.Response(null, { status: 405 }); + } } - } - if (!handler) { - const error = new Error( - `No default export was found at ${event.request.url}. Add a default export to handle requests. Learn more: https://vercel.link/creating-edge-middleware` - ); - return event.respondWith(toResponseError(error, dependencies.Response)); - } + if (!handler) { + const error = new Error( + `No default export was found at ${event.request.url}. Add a default export to handle requests. Learn more: https://vercel.link/creating-edge-middleware` + ); + return event.respondWith(toResponseError(error, dependencies.Response)); + } - const response = await respond(handler, event, options, dependencies); - event.respondWith(response); + const response = await respond(handler, event, options, dependencies); + event.respondWith(response); + } catch (error) { + event.respondWith(toResponseError(error, dependencies.Response)); + } }); } From 45d2da10163084a6c98abd050c5402c4d38d6527 Mon Sep 17 00:00:00 2001 From: Kiko Beats Date: Tue, 6 Jun 2023 11:59:10 +0200 Subject: [PATCH 12/16] update test assertion --- packages/cli/test/dev/integration-1.test.ts | 10 ++++++---- .../node/src/edge-functions/edge-handler-template.js | 8 +++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/cli/test/dev/integration-1.test.ts b/packages/cli/test/dev/integration-1.test.ts index 4db49983596..20cec65c509 100644 --- a/packages/cli/test/dev/integration-1.test.ts +++ b/packages/cli/test/dev/integration-1.test.ts @@ -322,14 +322,16 @@ test('[vercel dev] should handle missing handler errors thrown in edge functions ); validateResponseHeaders(res); - const { stderr } = await dev.kill(); + const { stdout } = await dev.kill(); expect(await res.text()).toMatch( /500<\/strong>: INTERNAL_SERVER_ERROR/g ); - expect(stderr).toMatch( - /No default export was found. Add a default export to handle requests./g - ); + expect(stdout).toMatchInlineSnapshot(` + "Error from API Route /api/edge-error-no-handler: No default export was found at http://localhost:${port}/api/edge-error-no-handler. Add a default export to handle requests. Learn more: https://vercel.link/creating-edge-middleware + at (api/edge-error-no-handler.js) + " + `); } finally { await dev.kill(); } diff --git a/packages/node/src/edge-functions/edge-handler-template.js b/packages/node/src/edge-functions/edge-handler-template.js index 42b5adc6351..a3818eba601 100644 --- a/packages/node/src/edge-functions/edge-handler-template.js +++ b/packages/node/src/edge-functions/edge-handler-template.js @@ -82,14 +82,12 @@ function registerFetchListener(module, options, dependencies) { : () => new dependencies.Response(null, { status: 405 }); } } - if (!handler) { - const error = new Error( - `No default export was found at ${event.request.url}. Add a default export to handle requests. Learn more: https://vercel.link/creating-edge-middleware` + const url = getUrl(event.request.url, event.request.headers); + throw new Error( + `No default export was found at ${url}. Add a default export to handle requests. Learn more: https://vercel.link/creating-edge-middleware` ); - return event.respondWith(toResponseError(error, dependencies.Response)); } - const response = await respond(handler, event, options, dependencies); event.respondWith(response); } catch (error) { From 0163cc8eeac3f9ef6f214a0c0e691dc21aacb30c Mon Sep 17 00:00:00 2001 From: Sean Massa Date: Tue, 6 Jun 2023 13:44:43 -0500 Subject: [PATCH 13/16] Update packages/node/src/serverless-functions/serverless-handler.mts --- packages/node/src/serverless-functions/serverless-handler.mts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/node/src/serverless-functions/serverless-handler.mts b/packages/node/src/serverless-functions/serverless-handler.mts index e31f384f5e7..8a5307d3259 100644 --- a/packages/node/src/serverless-functions/serverless-handler.mts +++ b/packages/node/src/serverless-functions/serverless-handler.mts @@ -57,7 +57,9 @@ async function compileUserCode( } if (HTTP_METHODS.some(method => typeof listener[method] === 'function')) { - if (NODE_MAJOR < 18) throw new Error('Node.js v18 or above is needed'); + if (NODE_MAJOR < 18) { + throw new Error('Node.js v18 or above is required to use HTTP method exports in your functions.'); + } const { getWebExportsHandler } = await import('./helpers-web.js'); return getWebExportsHandler(listener, HTTP_METHODS); } From b7e06c45b640677714a955d45ee21f98eceaf2e0 Mon Sep 17 00:00:00 2001 From: Sean Massa Date: Tue, 6 Jun 2023 13:48:29 -0500 Subject: [PATCH 14/16] update handler not found message --- packages/cli/test/dev/integration-1.test.ts | 3 ++- packages/node/src/edge-functions/edge-handler-template.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/cli/test/dev/integration-1.test.ts b/packages/cli/test/dev/integration-1.test.ts index 20cec65c509..319ad9850c5 100644 --- a/packages/cli/test/dev/integration-1.test.ts +++ b/packages/cli/test/dev/integration-1.test.ts @@ -327,8 +327,9 @@ test('[vercel dev] should handle missing handler errors thrown in edge functions expect(await res.text()).toMatch( /500<\/strong>: INTERNAL_SERVER_ERROR/g ); + const url = `http://localhost:${port}/api/edge-error-no-handler`; expect(stdout).toMatchInlineSnapshot(` - "Error from API Route /api/edge-error-no-handler: No default export was found at http://localhost:${port}/api/edge-error-no-handler. Add a default export to handle requests. Learn more: https://vercel.link/creating-edge-middleware + "Error from API Route /api/edge-error-no-handler: No default or HTTP-named export was found at ${url}. Add one to handle requests. Learn more: https://vercel.link/creating-edge-middleware at (api/edge-error-no-handler.js) " `); diff --git a/packages/node/src/edge-functions/edge-handler-template.js b/packages/node/src/edge-functions/edge-handler-template.js index a3818eba601..5bf4da40144 100644 --- a/packages/node/src/edge-functions/edge-handler-template.js +++ b/packages/node/src/edge-functions/edge-handler-template.js @@ -85,7 +85,7 @@ function registerFetchListener(module, options, dependencies) { if (!handler) { const url = getUrl(event.request.url, event.request.headers); throw new Error( - `No default export was found at ${url}. Add a default export to handle requests. Learn more: https://vercel.link/creating-edge-middleware` + `No default or HTTP-named export was found at ${url}. Add one to handle requests. Learn more: https://vercel.link/creating-edge-middleware` ); } const response = await respond(handler, event, options, dependencies); From 97ce530a7506cd68ee474e6d1f21f0bc001b33aa Mon Sep 17 00:00:00 2001 From: Sean Massa Date: Tue, 6 Jun 2023 14:07:09 -0500 Subject: [PATCH 15/16] make error messages look more like errors --- packages/node/src/utils.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/node/src/utils.ts b/packages/node/src/utils.ts index 7a7a3c91c0b..2fb3b5c026e 100644 --- a/packages/node/src/utils.ts +++ b/packages/node/src/utils.ts @@ -59,7 +59,12 @@ export function entrypointToOutputPath( } export function logError(error: Error) { - console.error(error.message); + let message = error.message; + if (!message.startsWith('Error:')) { + message = `Error: ${message}`; + } + console.error(message); + if (error.stack) { // only show the stack trace if debug is enabled // because it points to internals, not user code From 9ad83181292d767ef329218b6b0c0bdbd757d52f Mon Sep 17 00:00:00 2001 From: Sean Massa Date: Tue, 6 Jun 2023 14:11:35 -0500 Subject: [PATCH 16/16] run prettier --- packages/node/src/serverless-functions/serverless-handler.mts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/node/src/serverless-functions/serverless-handler.mts b/packages/node/src/serverless-functions/serverless-handler.mts index 8a5307d3259..d48d3aa5e63 100644 --- a/packages/node/src/serverless-functions/serverless-handler.mts +++ b/packages/node/src/serverless-functions/serverless-handler.mts @@ -58,7 +58,9 @@ async function compileUserCode( if (HTTP_METHODS.some(method => typeof listener[method] === 'function')) { if (NODE_MAJOR < 18) { - throw new Error('Node.js v18 or above is required to use HTTP method exports in your functions.'); + throw new Error( + 'Node.js v18 or above is required to use HTTP method exports in your functions.' + ); } const { getWebExportsHandler } = await import('./helpers-web.js'); return getWebExportsHandler(listener, HTTP_METHODS);