Skip to content

Commit

Permalink
[node] serverless named exports
Browse files Browse the repository at this point in the history
  • Loading branch information
Kikobeats committed May 12, 2023
1 parent 5400851 commit 7d73a94
Show file tree
Hide file tree
Showing 4 changed files with 61 additions and 40 deletions.
88 changes: 56 additions & 32 deletions packages/node/src/serverless-functions/serverless-handler.mts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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<VercelProxyResponse>> {
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);
Expand Down
1 change: 0 additions & 1 deletion packages/node/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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}`);
};
}
6 changes: 3 additions & 3 deletions packages/node/test/unit/dev.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand All @@ -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({
Expand Down

0 comments on commit 7d73a94

Please sign in to comment.