2024-05-06 17:02:16 -04:00

188 lines
6.0 KiB
JavaScript

const path = require('path');
const os = require('os');
const fs = require('fs');
const { inspect } = require('util');
const originaStdoutlWrite = process.stdout.write;
const originaStderrlWrite = process.stderr.write;
// Overwrite process.stdout.write
process.stdout.write = (chunk, encoding, callback) => {
try {
// Attempt to parse chunk as JSON
const obj = JSON.parse(chunk.trim());
// Utilize util.inspect to improve the readability of the object
const formatted = inspect(obj, { colors: true, depth: null });
// Call the original stdout.write with the formatted string
originaStdoutlWrite.call(process.stdout, formatted + '\n', encoding, callback);
} catch (error) {
// If it's not JSON, just output the original message
originaStdoutlWrite.call(process.stdout, chunk, encoding, callback);
}
};
// Overwrite process.stdout.write
process.stderr.write = (chunk, encoding, callback) => {
try {
// Attempt to parse chunk as JSON
const obj = JSON.parse(chunk.trim());
// inspect without color because stderr is red
const formatted = inspect(obj, { colors: false, depth: null });
// Call the original stdout.write with the formatted string
originaStderrlWrite.call(process.stderr, formatted + '\n', encoding, callback);
} catch (error) {
// If it's not JSON, just output the original message
originaStderrlWrite.call(process.stderr, chunk, encoding, callback);
}
};
/**
* Custom log function that formats objects with colors regardless of their depth.
*
* @param {*} originalFunction Original console function to be modified
* @returns
*/
function customLog(originalFunction) {
return function (...args) {
args = args.map((arg) => {
if (typeof arg === 'object' && arg !== null) {
// Format the object with colors and unlimited depth
return inspect(arg, { colors: true, depth: null });
}
return arg;
});
originalFunction.apply(console, args);
};
}
/**
* We apply the custom log function only to the console.log and console.info functions.
* console.error and console.warn are printed in red and handled by the parent process.
* We don't need to handle process.stdout.write and process.stderr.write
* Because they can't don't accept objects anyway.
*/
console.log = customLog(console.log);
console.info = customLog(console.info);
/**
* Saves the invocation result to a temporary file in JSON format.
* The file is named with the process ID to avoid conflicts between concurrent invocations.
* The result includes both the response and the error if any.
*
* @param {*} result
* @returns {void}
*/
const saveInvocationResult = (result) => {
const filePath = path.join(os.tmpdir(), `sls_${process.pid}.json`);
const stringifiedResult = JSON.stringify(result, null, 2);
fs.writeFileSync(filePath, stringifiedResult);
};
/**
* Imports the user handler function from the specified file path.
* We first try to require the file, and if it fails with ERR_REQUIRE_ESM, we import it.
* This supports both CommonJS and ESM handler files.
* If the file is already cached, it is deleted to ensure the latest version is imported
* as the user makes changes during the dev mode session
*
* @param {string} handlerFileAbsolutePath - Path to handler file
* @param {string} handlerName - The Handler Name defined in the config file
* @returns {Promise<Function>}
*/
const importFunction = async (handlerFileAbsolutePath, handlerName) => {
let handlerFunction;
try {
handlerFunction = require(handlerFileAbsolutePath)[handlerName];
} catch (error) {
if (error.code === 'ERR_REQUIRE_ESM') {
handlerFunction = await import(handlerFileAbsolutePath)[handlerName];
}
throw error;
}
// Make sure the handler is a function
if (typeof handlerFunction !== 'function') {
throw new Error(`Handler is not a function`);
}
return handlerFunction;
};
/**
* Invoke the handler function with the event, context and callback.
* We add context functions to the context object we received via WebSockets.
*
* @param {*} handlerFunction The handler function to invoke
* @param {{ event: any, partialContext: any }} options
* @returns <Promise<any>} a promise that resolves with the response from the handler function
* @throws {Error} if the handler function is not a function, or if the handler function throws an error
*/
const invokeFunction = async (handlerFunction, { event, partialContext }) => {
return new Promise(async (resolve, reject) => {
const callback = (error, response) => {
if (error) {
reject(error);
} else {
resolve(response);
}
};
const startTime = new Date();
const context = {
...partialContext,
succeed(result) {
callback(null, result);
},
fail(error) {
callback(error, null);
},
done(error, result) {
callback(error, result);
},
getRemainingTimeInMillis() {
return Math.max(context.timeout * 1000 - (new Date().valueOf() - startTime.valueOf()), 0);
},
};
try {
const response = await handlerFunction(event, context, callback);
resolve(response);
} catch (error) {
reject(error);
}
});
};
/**
* We parse all the data passed in by the parent process
*/
const inputArgs = JSON.parse(process.argv[2]);
const { handlerFileAbsolutePath, handlerName, event, partialContext } = inputArgs;
(async () => {
try {
// Import and invoke the handler function
const handler = await importFunction(handlerFileAbsolutePath, handlerName);
const response = await invokeFunction(handler, { event, partialContext });
// Save the response for the parent process to fetch and return to Lambda
saveInvocationResult({ response, error: null });
} catch (error) {
// Save the error for the parent process to fetch and return to Lambda
saveInvocationResult({
response: null,
error: { name: error.name, message: error.message, stack: error.stack },
});
// Exit the child process in case of errors
console.error(error);
process.exit(1);
}
})();