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} */ 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 } 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); } })();