// @ts-check // this is not TS file because it's used both directly inside test process // as well as child process that lacks TS on-the-fly transpilation import { join } from 'node:path' import { BLOB_TOKEN } from './constants.mjs' import { execute as untypedExecute } from 'lambda-local' const SERVER_HANDLER_NAME = '___netlify-server-handler' /** * @typedef {import('./contexts').FixtureTestContext} FixtureTestContext * * @typedef {Awaited<ReturnType<ReturnType<typeof import('@netlify/serverless-functions-api').getLambdaHandler>>>} LambdaResult * * @typedef {Object} FunctionInvocationOptions * @property {Record<string, string>} [env] Environment variables that should be set during the invocation * @property {string} [httpMethod] The http method that is used for the invocation. Defaults to 'GET' * @property {string} [url] TThe relative path that should be requested. Defaults to '/' * @property {Record<string, string>} [headers] The headers used for the invocation * @property {Record<string, unknown>} [flags] Feature flags that should be set during the invocation */ /** * This is helper to get LambdaLocal's execute to actually provide result type instead of `unknown` * Because jsdoc doesn't seem to have equivalent of `as` in TS and trying to assign `LambdaResult` type * to return value of `execute` leading to `Type 'unknown' is not assignable to type 'LambdaResult'` * error, this types it as `any` instead which allow to later type it as `LambdaResult`. * @param {Parameters<typeof untypedExecute>} args * @returns {Promise<LambdaResult>} */ async function execute(...args) { /** * @type {any} */ const anyResult = await untypedExecute(...args) return anyResult } /** * @param {FixtureTestContext} ctx */ export const createBlobContext = (ctx) => Buffer.from( JSON.stringify({ edgeURL: `http://${ctx.blobStoreHost}`, uncachedEdgeURL: `http://${ctx.blobStoreHost}`, token: BLOB_TOKEN, siteID: ctx.siteID, deployID: ctx.deployID, primaryRegion: 'us-test-1', }), ).toString('base64') /** * Converts a readable stream to a buffer * @param {NodeJS.ReadableStream} stream * @returns {Promise<Buffer>} */ function streamToBuffer(stream) { /** * @type {Buffer[]} */ const chunks = [] return new Promise((resolve, reject) => { stream.on('data', (chunk) => chunks.push(Buffer.from(chunk))) stream.on('error', (err) => reject(err)) stream.on('end', () => resolve(Buffer.concat(chunks))) }) } /** * @param {FixtureTestContext} ctx * @param {Record<string, string>} [env] */ function temporarilySetEnv(ctx, env) { const environment = { NODE_ENV: 'production', NETLIFY_BLOBS_CONTEXT: createBlobContext(ctx), ...(env || {}), } const envVarsToRestore = {} // We are not using lambda-local's environment variable setting because it cleans up // environment vars to early (before stream is closed) Object.keys(environment).forEach(function (key) { if (typeof process.env[key] !== 'undefined') { envVarsToRestore[key] = process.env[key] } process.env[key] = environment[key] }) return function restoreEnvironment() { Object.keys(environment).forEach(function (key) { if (typeof envVarsToRestore[key] !== 'undefined') { process.env[key] = envVarsToRestore[key] } else { delete process.env[key] } }) } } const DEFAULT_FLAGS = {} /** * @param {FixtureTestContext} ctx * @param {FunctionInvocationOptions} options */ export async function loadAndInvokeFunctionImpl( ctx, { headers, httpMethod, flags, url, env } = {}, ) { const { handler } = await import( 'file:///' + join(ctx.functionDist, SERVER_HANDLER_NAME, '___netlify-entry-point.mjs') ) const restoreEnvironment = temporarilySetEnv(ctx, env) let resolveInvocation, rejectInvocation const invocationPromise = new Promise((resolve, reject) => { resolveInvocation = resolve rejectInvocation = reject }) const response = await execute({ event: { headers: headers || {}, httpMethod: httpMethod || 'GET', rawUrl: new URL(url || '/', 'https://example.netlify').href, flags: flags ?? DEFAULT_FLAGS, }, lambdaFunc: { handler }, timeoutMs: 4_000, onInvocationEnd: (error) => { // lambda-local resolve promise return from execute when response is closed // but we should wait for tracked background work to finish // before resolving the promise to allow background work to finish if (error) { rejectInvocation(error) } else { resolveInvocation() } }, }) await invocationPromise if (!response) { throw new Error('No response from lambda-local') } const responseHeaders = Object.entries(response.multiValueHeaders || {}).reduce( (prev, [key, value]) => ({ ...prev, [key]: value.length === 1 ? `${value}` : value.join(', '), }), response.headers || {}, ) const bodyBuffer = await streamToBuffer(response.body) restoreEnvironment() return { statusCode: response.statusCode, bodyBuffer, body: bodyBuffer.toString('utf-8'), headers: responseHeaders, isBase64Encoded: response.isBase64Encoded, } } /** * @typedef {typeof loadAndInvokeFunctionImpl} InvokeFunction * @typedef {Promise<Awaited<ReturnType<InvokeFunction>>>} InvokeFunctionResult */