diff --git a/bridge/core/arraybuffer.go b/bridge/core/arraybuffer.go index a99b224..41b9eb8 100644 --- a/bridge/core/arraybuffer.go +++ b/bridge/core/arraybuffer.go @@ -14,6 +14,43 @@ func ToArrayBuffer(vm *sobek.Runtime, data []byte) sobek.Value { return vm.ToValue(vm.NewArrayBuffer(data)) } +// ToUint8Array converts a Go byte slice to a JavaScript Uint8Array. +// The returned Uint8Array is a copy of the original data, meaning modifications +// in JavaScript will not affect the Go slice. +// +// If the Uint8Array constructor is not available in the global scope, it gracefully +// falls back to returning an ArrayBuffer. +func ToUint8Array(vm *sobek.Runtime, data []byte) sobek.Value { + if len(data) == 0 { + buf := vm.NewArrayBuffer(nil) + ctor := vm.Get("Uint8Array") + if ctor != nil && !sobek.IsNull(ctor) && !sobek.IsUndefined(ctor) { + typedArray, err := vm.New(ctor.ToObject(vm), vm.ToValue(buf)) + if err == nil && typedArray != nil { + return typedArray + } + } + return vm.ToValue(buf) + } + + copiedData := make([]byte, len(data)) + copy(copiedData, data) + + buf := vm.NewArrayBuffer(copiedData) + + ctor := vm.Get("Uint8Array") + if ctor == nil || sobek.IsNull(ctor) || sobek.IsUndefined(ctor) { + return vm.ToValue(buf) + } + + typedArray, err := vm.New(ctor.ToObject(vm), vm.ToValue(buf)) + if err != nil || typedArray == nil { + return vm.ToValue(buf) + } + + return typedArray +} + // MapSharedBuffer exposes a Go byte slice as a global JavaScript TypedArray. // The backing memory is shared between Go and JavaScript, meaning modifications // from either side are immediately visible to the other. diff --git a/bridge/core/reflection.go b/bridge/core/reflection.go index b84aa2c..50ee2ae 100644 --- a/bridge/core/reflection.go +++ b/bridge/core/reflection.go @@ -3,6 +3,7 @@ package core import ( "fmt" "reflect" + "strconv" "sync" "github.com/grafana/sobek" @@ -337,6 +338,21 @@ func wrapJSCallback(vm *sobek.Runtime, callable sobek.Callable, goType reflect.T } func bindSlice(vm *sobek.Runtime, v reflect.Value, visited map[uintptr]sobek.Value) (sobek.Value, error) { + // @optimized: Fast path for []byte using ToUint8Array to avoid repeated reflection and per-element allocation. + if v.Type().Elem() == reflect.TypeOf(byte(0)) { + var bytes []byte + if v.CanAddr() || v.Kind() == reflect.Slice { + bytes = v.Bytes() + } else { + l := v.Len() + bytes = make([]byte, l) + for i := 0; i < l; i++ { + bytes[i] = byte(v.Index(i).Uint()) + } + } + return ToUint8Array(vm, bytes), nil + } + // @optimized: Pre-allocate slice and use NewArray(vals...) to avoid repeated Set calls. l := v.Len() vals := make([]interface{}, l) @@ -352,16 +368,26 @@ func bindSlice(vm *sobek.Runtime, v reflect.Value, visited map[uintptr]sobek.Val func bindMap(vm *sobek.Runtime, v reflect.Value, visited map[uintptr]sobek.Value) (sobek.Value, error) { obj := vm.NewObject() - for _, key := range v.MapKeys() { + + // @optimized: Use MapRange to avoid allocating a slice of keys. + iter := v.MapRange() + for iter.Next() { + key := iter.Key() var keyStr string - // @optimized: Avoid Sprintf if key is already a string. - if key.Kind() == reflect.String { + + // @optimized: Fast path string formatting for common map key types to avoid fmt.Sprint allocation overhead. + switch key.Kind() { + case reflect.String: keyStr = key.String() - } else { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + keyStr = strconv.FormatInt(key.Int(), 10) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + keyStr = strconv.FormatUint(key.Uint(), 10) + default: keyStr = fmt.Sprint(key.Interface()) } - val, err := bindValue(vm, v.MapIndex(key), visited) + val, err := bindValue(vm, iter.Value(), visited) if err != nil { return nil, err } diff --git a/examples/projects/link-shortener/typego.lock b/examples/projects/link-shortener/typego.lock deleted file mode 100644 index 30c3244..0000000 --- a/examples/projects/link-shortener/typego.lock +++ /dev/null @@ -1,8 +0,0 @@ -{ - "lockfileVersion": 1, - "resolved": { - "github.com/gin-gonic/gin": { - "version": "v1.11.0" - } - } -} \ No newline at end of file diff --git a/examples/projects/link-shortener/typego.modules.json b/examples/projects/link-shortener/typego.modules.json index 81ae0d8..3d7b8f8 100644 --- a/examples/projects/link-shortener/typego.modules.json +++ b/examples/projects/link-shortener/typego.modules.json @@ -1,6 +1,6 @@ { "dependencies": { - "github.com/gin-gonic/gin": "latest", + "github.com/gin-gonic/gin": "v1.11.0", "github.com/teris-io/shortid": "latest" }, "compiler": { diff --git a/pkg/cli/cmd/build.go b/pkg/cli/cmd/build.go index aa1452b..d8550c9 100644 --- a/pkg/cli/cmd/build.go +++ b/pkg/cli/cmd/build.go @@ -56,6 +56,7 @@ var BuildCmd = &cobra.Command{ virtualModules := make(map[string]string) var bindBlock string + inspectedModules := make(map[string]bool) if res != nil { for _, imp := range res.Imports { @@ -71,6 +72,7 @@ var BuildCmd = &cobra.Command{ continue } + inspectedModules[cleanImp] = true bindBlock += linker.GenerateShim(info, "pkg_"+info.Name) var vmContent strings.Builder @@ -100,6 +102,12 @@ var BuildCmd = &cobra.Command{ if cleanImp == "fmt" || cleanImp == "os" { continue } + // Skip standard library bypass (which don't need fetcher but still need imports) + // or external modules that successfully inspected. + // If it's an external module that failed inspection, don't import it to prevent "imported and not used". + if !inspectedModules[cleanImp] && strings.Contains(cleanImp, ".") { + continue + } importBlock.WriteString(fmt.Sprintf("\t\"%s\"\n", cleanImp)) } } diff --git a/pkg/cli/cmd/run.go b/pkg/cli/cmd/run.go index baffa45..a8cf9f0 100644 --- a/pkg/cli/cmd/run.go +++ b/pkg/cli/cmd/run.go @@ -86,6 +86,7 @@ func runStandalone(filename string) { virtualModules := make(map[string]string) var bindBlock string + inspectedModules := make(map[string]bool) if res != nil { for _, imp := range res.Imports { @@ -98,6 +99,7 @@ func runStandalone(filename string) { } if err := fetcher.Get(cleanImp); err == nil { if info, err := linker.Inspect(cleanImp, fetcher.TempDir); err == nil { + inspectedModules[cleanImp] = true bindBlock += linker.GenerateShim(info, "pkg_"+info.Name) var vmContent strings.Builder for _, fn := range info.Exports { @@ -125,6 +127,9 @@ func runStandalone(filename string) { case "fmt", "os", "sync", "net/http", "memory": continue } + if !inspectedModules[cleanImp] && strings.Contains(cleanImp, ".") { + continue + } importBlock.WriteString(fmt.Sprintf("\t\"%s\"\n", cleanImp)) } }