diff --git a/build.mjs b/build.mjs index 728af5a633..b828120dc6 100644 --- a/build.mjs +++ b/build.mjs @@ -69,6 +69,7 @@ const ES_BUILD_OPTIONS = { "xml", "net", "navigator", + "util", "url", "performance", ], diff --git a/llrt_core/src/modules/console.rs b/llrt_core/src/modules/console.rs index 3a11819c57..0e8a8a61d4 100644 --- a/llrt_core/src/modules/console.rs +++ b/llrt_core/src/modules/console.rs @@ -133,7 +133,7 @@ pub fn init(ctx: &Ctx<'_>) -> Result<()> { Ok(()) } -const NEWLINE_LOOKUP: [char; 2] = [NEWLINE, CARRIAGE_RETURN]; +pub const NEWLINE_LOOKUP: [char; 2] = [NEWLINE, CARRIAGE_RETURN]; const COLOR_RESET: &str = "\x1b[0m"; const COLOR_BLACK: &str = "\x1b[30m"; const COLOR_GREEN: &str = "\x1b[32m"; @@ -240,7 +240,7 @@ fn is_primitive_like_or_void(typeof_value: Type) -> bool { } #[inline(always)] -fn stringify_value<'js>( +pub fn stringify_value<'js>( result: &mut String, ctx: &Ctx<'js>, obj: Value<'js>, @@ -579,21 +579,30 @@ fn format_plain<'js>(ctx: Ctx<'js>, args: Rest>) -> Result { format_values(&ctx, args, false) } -fn format_values_internal<'js>( +pub fn format_values_internal<'js>( result: &mut String, ctx: &Ctx<'js>, args: Rest>, tty: bool, newline_char: char, ) -> Result<()> { - let mut write_space = false; - for arg in args.0.into_iter() { - if write_space { - result.push(' '); + // Parse arguments + let mut str_pattern_option_value: String = String::with_capacity(64); + let mut replacements = Vec::with_capacity(args.len() - 1); + for (index, arg) in args.0.into_iter().enumerate() { + if index == 0 { + stringify_value(&mut str_pattern_option_value, ctx, arg, tty, newline_char)?; + } else { + let mut result = String::with_capacity(64); + stringify_value(&mut result, ctx, arg, tty, newline_char)?; + replacements.push(result) } - stringify_value(result, ctx, arg, tty, newline_char)?; - write_space = true } + + result.push_str(&format_string( + str_pattern_option_value.as_str(), + &replacements, + )); Ok(()) } @@ -617,6 +626,63 @@ pub(crate) fn log_std_err<'js>( write_log(stderr(), ctx, args, level) } +fn format_string(format_str: &str, replacements: &[String]) -> String { + // Quick return if we don't have anything to replace + if replacements.is_empty() { + return format_str.to_owned(); + } + + // Define capacity + let mut capacity = format_str.len() + replacements.len(); // add the number of replacements in case we have more to account for spaces + for replacement in replacements { + capacity += replacement.len(); + } + let mut result = String::with_capacity(capacity); + let mut replacement_idx = 0; + + // Iterate over chars to find patterns + let mut chars = format_str.chars().peekable(); + while let Some(current_char) = chars.next() { + if current_char == '%' { + if let Some(next_char) = chars.next() { + // Handle the case of %s, %d, etc, we keep it simple and only replaces string + let need_to_put_back_chars = match next_char { + 's' | 'd' | 'i' | 'f' | 'o' | 'O' | 'j' => { + if replacement_idx < replacements.len() { + result.push_str(&replacements[replacement_idx]); + replacement_idx += 1; + false + } else { + true + } + }, + '%' => { + // Handle the case of %% where we want to push only one % + result.push(current_char); + false + }, + _ => true, + }; + // Nothing was replaced, just add back what we found + if need_to_put_back_chars { + result.push(current_char); + result.push(next_char); + } + } + } else { + result.push(current_char); + } + } + + // Add what remains + if replacement_idx < replacements.len() { + result.push(' '); + result.push_str(&replacements[replacement_idx..].join(" ")); + } + + result +} + #[allow(clippy::unused_io_amount)] fn write_log<'js, T>( mut output: T, @@ -754,19 +820,29 @@ fn write_lambda_log<'js>( let mut exception = None; - let mut write_space = false; - for arg in args.0.into_iter() { - if write_space { - values_string.push(' '); - } + // Parse arguments + let mut str_pattern_option_value: String = String::with_capacity(64); + let mut replacements = Vec::with_capacity(args.len() - 1); + for (index, arg) in args.0.into_iter().enumerate() { if arg.is_error() && exception.is_none() { let exception_value = arg.clone(); exception = Some(exception_value.into_exception().unwrap()); } - stringify_value(&mut values_string, ctx, arg, is_tty, NEWLINE)?; - write_space = true + + if index == 0 { + stringify_value(&mut str_pattern_option_value, ctx, arg, is_tty, NEWLINE)?; + } else { + let mut result = String::with_capacity(64); + stringify_value(&mut result, ctx, arg, is_tty, NEWLINE)?; + replacements.push(result) + } } + values_string.push_str(&format_string( + str_pattern_option_value.as_str(), + &replacements, + )); + result.push_str(&escape_json(values_string.as_bytes())); result.push('\"'); if let Some(exception) = exception { diff --git a/llrt_core/src/modules/util.rs b/llrt_core/src/modules/util.rs index f53b44fa58..d2e0651302 100644 --- a/llrt_core/src/modules/util.rs +++ b/llrt_core/src/modules/util.rs @@ -1,9 +1,12 @@ +use rquickjs::function::{Func, Rest}; use rquickjs::{ cstr, module::{Declarations, Exports, ModuleDef}, - Ctx, Function, Result, + Ctx, Function, Result, Value, }; +use std::sync::atomic::Ordering; +use crate::modules::console::{format_values_internal, AWS_LAMBDA_MODE, NEWLINE_LOOKUP}; use crate::{module_builder::ModuleInfo, modules::module::export_default}; pub struct UtilModule; @@ -12,6 +15,7 @@ impl ModuleDef for UtilModule { fn declare(declare: &mut Declarations) -> Result<()> { declare.declare(stringify!(TextDecoder))?; declare.declare(stringify!(TextEncoder))?; + declare.declare(stringify!(format))?; declare.declare_static(cstr!("default"))?; Ok(()) } @@ -25,6 +29,7 @@ impl ModuleDef for UtilModule { default.set(stringify!(TextEncoder), encoder)?; default.set(stringify!(TextDecoder), decoder)?; + default.set("format", Func::from(format))?; Ok(()) }) @@ -39,3 +44,11 @@ impl From for ModuleInfo { } } } + +fn format<'js>(ctx: Ctx<'js>, args: Rest>) -> Result { + let mut result = String::with_capacity(64); + let newline_char = NEWLINE_LOOKUP[AWS_LAMBDA_MODE.load(Ordering::Relaxed) as usize]; + format_values_internal(&mut result, &ctx, args, false, newline_char)?; + + Ok(result) +} diff --git a/tests/unit/console.test.ts b/tests/unit/console.test.ts index 6b878000d6..5ea516274c 100644 --- a/tests/unit/console.test.ts +++ b/tests/unit/console.test.ts @@ -1,11 +1,27 @@ import * as timers from "timers"; import { Console as NodeConsole } from "node:console"; import { Console } from "console"; +import util from "util"; function log(...args: any[]) { return (console as any).__formatPlain(...args); } +it("should format strings correctly", () => { + + expect(util.format('%s:%s', 'foo', "bar")).toEqual("foo:bar") + expect(util.format(1, 2, 3)).toEqual("1 2 3") + expect(util.format("%% %s")).toEqual("%% %s") + expect(util.format("%s:%s", "foo")).toEqual("foo:%s") + expect(util.format("Hello %%, %s! How are you, %s?", "Alice", "Bob")).toEqual("Hello %, Alice! How are you, Bob?") + expect(util.format("The %s %d %f.", "quick", "42", "3.14")).toEqual("The quick 42 3.14.") + expect(util.format("Unmatched placeholders: %s %x %% %q", "one", "two")).toEqual("Unmatched placeholders: one %x % %q two") + expect(util.format("Unmatched placeholders: %s", "one", "two","three")).toEqual("Unmatched placeholders: one two three") + + // Should not throw any exceptions + console.log('%s:%s', 'foo', "bar") +}) + it("should log module", () => { let module = log(timers);