From 5c51f70f7f9474391dedf296b673913b11f8f8aa Mon Sep 17 00:00:00 2001 From: Fabian Gruber Date: Tue, 2 Sep 2025 11:42:14 +0200 Subject: [PATCH 1/3] feat(objects): add option to drop undefined keys instead of converting to nil. We have the following code that constructs `undefined` keys inside an object and calls some Go binding: ```js const input = { foo: "bar" }; const { foo, undefinedKey } = input; const newInput = { foo, undefinedKey }; actions.output(newInput); ``` Currently this exports the input to `{"foo": "bar", "undefinedKey": nil}` in Go. With the new VM option it would export to `{"foo": "bar"}`. --- object.go | 16 ++++++++++++++++ object_test.go | 20 ++++++++++++++++++++ runtime.go | 9 +++++++++ 3 files changed, 45 insertions(+) diff --git a/object.go b/object.go index 79bd67df..2abcda32 100644 --- a/object.go +++ b/object.go @@ -967,6 +967,9 @@ func (o *baseObject) export(ctx *objectExportCtx) interface{} { itemNameStr := itemName.String() v := o.val.self.getStr(itemName.string(), nil) if v != nil { + if IsUndefined(v) && o.val.runtime.exportOptions.dropUndefinedKeys { + continue + } m[itemNameStr] = exportValue(v, ctx) } else { m[itemNameStr] = nil @@ -1822,3 +1825,16 @@ func (i *privateId) String() string { func (i *privateId) string() unistring.String { return privateIdString(i.name) } + +type exportOptions struct { + dropUndefinedKeys bool +} + +type ExportOptions func(*exportOptions) + +// WithDropUndefinedKeys configures object exports to drop undefined keys instead of converting to nil. +func WithDropUndefinedKeys() ExportOptions { + return func(opts *exportOptions) { + opts.dropUndefinedKeys = true + } +} diff --git a/object_test.go b/object_test.go index 45b95bc5..5a46ad90 100644 --- a/object_test.go +++ b/object_test.go @@ -314,6 +314,26 @@ func TestExportToSliceNonIterable(t *testing.T) { } } +func TestExportDropUndefinedKeys(t *testing.T) { + vm := New() + vm.SetExportOptions(WithDropUndefinedKeys()) + + o := vm.NewObject() + o.Set("foo", vm.ToValue("bar")) + o.Set("baz", _undefined) + var a map[string]any + err := vm.ExportTo(o, &a) + if err != nil { + t.Fatal(err) + } + if len(a) != 1 { + t.Fatalf("a: %v", a) + } + if a["foo"] != "bar" { + t.Fatalf("Unexpected a[foo]: %v", a["foo"]) + } +} + func ExampleRuntime_ExportTo_iterableToSlice() { vm := New() v, err := vm.RunString(` diff --git a/runtime.go b/runtime.go index 9cccb469..e4fb94a3 100644 --- a/runtime.go +++ b/runtime.go @@ -193,6 +193,8 @@ type Runtime struct { fieldNameMapper FieldNameMapper + exportOptions exportOptions + vm *vm hash *maphash.Hash idSeq uint64 @@ -2441,6 +2443,13 @@ func (r *Runtime) SetParserOptions(opts ...parser.Option) { r.parserOptions = opts } +// SetExportOptions sets the export options for this Runtime. +func (r *Runtime) SetExportOptions(opts ...ExportOptions) { + for _, o := range opts { + o(&r.exportOptions) + } +} + // SetMaxCallStackSize sets the maximum function call depth. When exceeded, a *StackOverflowError is thrown and // returned by RunProgram or by a Callable call. This is useful to prevent memory exhaustion caused by an // infinite recursion. The default value is math.MaxInt32. From 3d0e640a9485c65e3fbf03dda7336d713d7d6309 Mon Sep 17 00:00:00 2001 From: Fabian Gruber Date: Mon, 3 Nov 2025 17:06:08 +0100 Subject: [PATCH 2/3] fix(date): missing timeZone option handling for toLocaleString, toLocaleDateString, toLocaleTimeString AB#115818 --- builtin_date.go | 28 ++++++++++++++++++++++------ date_test.go | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 6 deletions(-) diff --git a/builtin_date.go b/builtin_date.go index 84a80ac0..868713ec 100644 --- a/builtin_date.go +++ b/builtin_date.go @@ -198,11 +198,25 @@ func (r *Runtime) dateproto_toTimeString(call FunctionCall) Value { panic(r.NewTypeError("Method Date.prototype.toTimeString is called on incompatible receiver")) } +func (d *dateObject) withTimeZoneOpts(r *Runtime, call FunctionCall) time.Time { + t := d.time() + if arg1 := call.Argument(1); arg1 != _undefined { + opts := r.toObject(arg1) + if tz := opts.Get("timeZone"); tz != _undefined { + if loc, err := time.LoadLocation(tz.String()); err == nil { + return t.In(loc) + } + } + } + return t +} + func (r *Runtime) dateproto_toLocaleString(call FunctionCall) Value { obj := r.toObject(call.This) if d, ok := obj.self.(*dateObject); ok { if d.isSet() { - return asciiString(d.time().Format(datetimeLayout_en_GB)) + t := d.withTimeZoneOpts(r, call) + return asciiString(t.Format(datetimeLayout_en_GB)) } else { return stringInvalidDate } @@ -214,7 +228,8 @@ func (r *Runtime) dateproto_toLocaleDateString(call FunctionCall) Value { obj := r.toObject(call.This) if d, ok := obj.self.(*dateObject); ok { if d.isSet() { - return asciiString(d.time().Format(dateLayout_en_GB)) + t := d.withTimeZoneOpts(r, call) + return asciiString(t.Format(dateLayout_en_GB)) } else { return stringInvalidDate } @@ -226,7 +241,8 @@ func (r *Runtime) dateproto_toLocaleTimeString(call FunctionCall) Value { obj := r.toObject(call.This) if d, ok := obj.self.(*dateObject); ok { if d.isSet() { - return asciiString(d.time().Format(timeLayout_en_GB)) + t := d.withTimeZoneOpts(r, call) + return asciiString(t.Format(timeLayout_en_GB)) } else { return stringInvalidDate } @@ -989,9 +1005,9 @@ func createDateProtoTemplate() *objectTemplate { t.putStr("toString", func(r *Runtime) Value { return r.methodProp(r.dateproto_toString, "toString", 0) }) t.putStr("toDateString", func(r *Runtime) Value { return r.methodProp(r.dateproto_toDateString, "toDateString", 0) }) t.putStr("toTimeString", func(r *Runtime) Value { return r.methodProp(r.dateproto_toTimeString, "toTimeString", 0) }) - t.putStr("toLocaleString", func(r *Runtime) Value { return r.methodProp(r.dateproto_toLocaleString, "toLocaleString", 0) }) - t.putStr("toLocaleDateString", func(r *Runtime) Value { return r.methodProp(r.dateproto_toLocaleDateString, "toLocaleDateString", 0) }) - t.putStr("toLocaleTimeString", func(r *Runtime) Value { return r.methodProp(r.dateproto_toLocaleTimeString, "toLocaleTimeString", 0) }) + t.putStr("toLocaleString", func(r *Runtime) Value { return r.methodProp(r.dateproto_toLocaleString, "toLocaleString", 2) }) + t.putStr("toLocaleDateString", func(r *Runtime) Value { return r.methodProp(r.dateproto_toLocaleDateString, "toLocaleDateString", 2) }) + t.putStr("toLocaleTimeString", func(r *Runtime) Value { return r.methodProp(r.dateproto_toLocaleTimeString, "toLocaleTimeString", 2) }) t.putStr("valueOf", func(r *Runtime) Value { return r.methodProp(r.dateproto_valueOf, "valueOf", 0) }) t.putStr("getTime", func(r *Runtime) Value { return r.methodProp(r.dateproto_getTime, "getTime", 0) }) t.putStr("getFullYear", func(r *Runtime) Value { return r.methodProp(r.dateproto_getFullYear, "getFullYear", 0) }) diff --git a/date_test.go b/date_test.go index a3ef7bab..d62224fe 100644 --- a/date_test.go +++ b/date_test.go @@ -130,6 +130,44 @@ func TestTimezoneOffset(t *testing.T) { testScript(SCRIPT, intToValue(-60), t) } +func TestLocaleTime(t *testing.T) { + const SCRIPT = ` + var d = new Date(Date.UTC(2012, 11, 20, 3, 0, 0)); + d.toLocaleString("en-GB"); + ` + + l := time.Local + defer func() { + time.Local = l + }() + var err error + time.Local, err = time.LoadLocation("Europe/London") + if err != nil { + t.Fatal(err) + } + + testScript(SCRIPT, asciiString("12/20/2012, 03:00:00"), t) +} + +func TestLocaleTimeZones(t *testing.T) { + const SCRIPT = ` + var d = new Date(Date.UTC(2012, 11, 20, 3, 0, 0)); + d.toLocaleString("en-GB", { timeZone: "Europe/Berlin" }); + ` + + l := time.Local + defer func() { + time.Local = l + }() + var err error + time.Local, err = time.LoadLocation("Europe/London") + if err != nil { + t.Fatal(err) + } + + testScript(SCRIPT, asciiString("12/20/2012, 04:00:00"), t) +} + func TestDateValueOf(t *testing.T) { const SCRIPT = ` var d9 = new Date(1.23e15); From 266f7f8dcfe0237f000fb8bde0b7daa03f024676 Mon Sep 17 00:00:00 2001 From: Fabian Gruber Date: Wed, 26 Nov 2025 12:35:33 +0100 Subject: [PATCH 3/3] fix(date): fix missing nil check for timeZone handling --- builtin_date.go | 2 +- date_test.go | 23 +++++++++++++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/builtin_date.go b/builtin_date.go index 868713ec..d676ddd9 100644 --- a/builtin_date.go +++ b/builtin_date.go @@ -202,7 +202,7 @@ func (d *dateObject) withTimeZoneOpts(r *Runtime, call FunctionCall) time.Time { t := d.time() if arg1 := call.Argument(1); arg1 != _undefined { opts := r.toObject(arg1) - if tz := opts.Get("timeZone"); tz != _undefined { + if tz := opts.Get("timeZone"); tz != nil && tz != _undefined { if loc, err := time.LoadLocation(tz.String()); err == nil { return t.In(loc) } diff --git a/date_test.go b/date_test.go index d62224fe..ed000dfc 100644 --- a/date_test.go +++ b/date_test.go @@ -130,7 +130,7 @@ func TestTimezoneOffset(t *testing.T) { testScript(SCRIPT, intToValue(-60), t) } -func TestLocaleTime(t *testing.T) { +func TestDateLocaleTime(t *testing.T) { const SCRIPT = ` var d = new Date(Date.UTC(2012, 11, 20, 3, 0, 0)); d.toLocaleString("en-GB"); @@ -149,7 +149,7 @@ func TestLocaleTime(t *testing.T) { testScript(SCRIPT, asciiString("12/20/2012, 03:00:00"), t) } -func TestLocaleTimeZones(t *testing.T) { +func TestDateLocaleTimeZones(t *testing.T) { const SCRIPT = ` var d = new Date(Date.UTC(2012, 11, 20, 3, 0, 0)); d.toLocaleString("en-GB", { timeZone: "Europe/Berlin" }); @@ -168,6 +168,25 @@ func TestLocaleTimeZones(t *testing.T) { testScript(SCRIPT, asciiString("12/20/2012, 04:00:00"), t) } +func TestDateUnsupportedOptions(t *testing.T) { + const SCRIPT = ` + var d = new Date(Date.UTC(2012, 11, 20, 3, 0, 0)); + d.toLocaleString("en-GB", { foo: "bar" }); + ` + + l := time.Local + defer func() { + time.Local = l + }() + var err error + time.Local, err = time.LoadLocation("Europe/London") + if err != nil { + t.Fatal(err) + } + + testScript(SCRIPT, asciiString("12/20/2012, 03:00:00"), t) +} + func TestDateValueOf(t *testing.T) { const SCRIPT = ` var d9 = new Date(1.23e15);