diff --git a/go.mod b/go.mod index 2311ad8..5ae1d01 100644 --- a/go.mod +++ b/go.mod @@ -3,30 +3,31 @@ module apigo.cc/ai/ai go 1.22 require ( - github.com/dop251/goja v0.0.0-20240828124009-016eb7256539 - github.com/dop251/goja_nodejs v0.0.0-20240728170619-29b559befffc + github.com/Masterminds/semver/v3 v3.3.0 + github.com/dlclark/regexp2 v1.11.4 + github.com/dop251/base64dec v0.0.0-20231022112746-c6c9f9a96217 github.com/fsnotify/fsnotify v1.7.0 - github.com/go-resty/resty/v2 v2.15.0 + github.com/go-resty/resty/v2 v2.15.2 + github.com/go-sourcemap/sourcemap v2.1.4+incompatible github.com/golang-jwt/jwt/v5 v5.2.1 - github.com/sashabaranov/go-openai v1.29.2 + github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134 + github.com/sashabaranov/go-openai v1.30.3 github.com/ssgo/config v1.7.7 github.com/ssgo/httpclient v1.7.7 github.com/ssgo/log v1.7.7 github.com/ssgo/u v1.7.7 github.com/stretchr/testify v1.9.0 + go.uber.org/goleak v1.3.0 + golang.org/x/net v0.29.0 + golang.org/x/text v0.18.0 + gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.1 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect - github.com/dlclark/regexp2 v1.11.4 // indirect - github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect - github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134 // indirect + github.com/kr/text v0.2.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/ssgo/standard v1.7.7 // indirect - golang.org/x/net v0.29.0 // indirect golang.org/x/sys v0.25.0 // indirect - golang.org/x/text v0.18.0 // indirect ) - -replace github.com/dop251/goja v0.0.0-20240828124009-016eb7256539 => ./goja diff --git a/goja/README.md b/goja/README.md index a4d0c09..c2dbcfb 100644 --- a/goja/README.md +++ b/goja/README.md @@ -3,7 +3,7 @@ goja ECMAScript 5.1(+) implementation in Go. -[![Go Reference](https://pkg.go.dev/badge/github.com/dop251/goja.svg)](https://pkg.go.dev/github.com/dop251/goja) +[![Go Reference](https://pkg.go.dev/badge/apigo.cc/ai/ai/goja.svg)](https://pkg.go.dev/apigo.cc/ai/ai/goja) Goja is an implementation of ECMAScript 5.1 in pure Go with emphasis on standard compliance and performance. @@ -20,7 +20,7 @@ Features pass all of them. See .tc39_test262_checkout.sh for the latest working commit id. * Capable of running Babel, Typescript compiler and pretty much anything written in ES5. * Sourcemaps. - * Most of ES6 functionality, still work in progress, see https://github.com/dop251/goja/milestone/1?closed=1 + * Most of ES6 functionality, still work in progress, see https://apigo.cc/ai/ai/goja/milestone/1?closed=1 Known incompatibilities and caveats ----------------------------------- @@ -48,7 +48,7 @@ key = undefined; // Now it does The reason for it is the limitation of the Go runtime. At the time of writing (version 1.15) having a finalizer set on an object which is part of a reference cycle makes the whole cycle non-garbage-collectable. The solution above is the only reasonable way I can think of without involving finalizers. This is the third attempt -(see https://github.com/dop251/goja/issues/250 and https://github.com/dop251/goja/issues/199 for more details). +(see https://apigo.cc/ai/ai/goja/issues/250 and https://apigo.cc/ai/ai/goja/issues/199 for more details). Note, this does not have any effect on the application logic, but may cause a higher-than-expected memory usage. @@ -80,7 +80,7 @@ FAQ Although it's faster than many scripting language implementations in Go I have seen (for example it's 6-7 times faster than otto on average) it is not a replacement for V8 or SpiderMonkey or any other general-purpose JavaScript engine. -You can find some benchmarks [here](https://github.com/dop251/goja/issues/2). +You can find some benchmarks [here](https://apigo.cc/ai/ai/goja/issues/2). ### Why would I want to use it over a V8 wrapper? @@ -106,13 +106,13 @@ it's not possible to pass object values between runtimes. setTimeout() assumes concurrent execution of code which requires an execution environment, for example an event loop similar to nodejs or a browser. -There is a [separate project](https://github.com/dop251/goja_nodejs) aimed at providing some NodeJS functionality, +There is a [separate project](https://apigo.cc/ai/ai/goja_nodejs) aimed at providing some NodeJS functionality, and it includes an event loop. ### Can you implement (feature X from ES6 or higher)? I will be adding features in their dependency order and as quickly as time permits. Please do not ask -for ETAs. Features that are open in the [milestone](https://github.com/dop251/goja/milestone/1) are either in progress +for ETAs. Features that are open in the [milestone](https://apigo.cc/ai/ai/goja/milestone/1) are either in progress or will be worked on next. The ongoing work is done in separate feature branches which are merged into master when appropriate. @@ -153,13 +153,13 @@ if num := v.Export().(int64); num != 4 { Passing Values to JS -------------------- -Any Go value can be passed to JS using Runtime.ToValue() method. See the method's [documentation](https://pkg.go.dev/github.com/dop251/goja#Runtime.ToValue) for more details. +Any Go value can be passed to JS using Runtime.ToValue() method. See the method's [documentation](https://pkg.go.dev/apigo.cc/ai/ai/goja#Runtime.ToValue) for more details. Exporting Values from JS ------------------------ A JS value can be exported into its default Go representation using Value.Export() method. -Alternatively it can be exported into a specific Go variable using [Runtime.ExportTo()](https://pkg.go.dev/github.com/dop251/goja#Runtime.ExportTo) method. +Alternatively it can be exported into a specific Go variable using [Runtime.ExportTo()](https://pkg.go.dev/apigo.cc/ai/ai/goja#Runtime.ExportTo) method. Within a single export operation the same Object will be represented by the same Go value (either the same map, slice or a pointer to the same struct). This includes circular objects and makes it possible to export them. @@ -168,7 +168,7 @@ Calling JS functions from Go ---------------------------- There are 2 approaches: -- Using [AssertFunction()](https://pkg.go.dev/github.com/dop251/goja#AssertFunction): +- Using [AssertFunction()](https://pkg.go.dev/apigo.cc/ai/ai/goja#AssertFunction): ```go const SCRIPT = ` function sum(a, b) { @@ -193,7 +193,7 @@ if err != nil { fmt.Println(res) // Output: 42 ``` -- Using [Runtime.ExportTo()](https://pkg.go.dev/github.com/dop251/goja#Runtime.ExportTo): +- Using [Runtime.ExportTo()](https://pkg.go.dev/apigo.cc/ai/ai/goja#Runtime.ExportTo): ```go const SCRIPT = ` function sum(a, b) { @@ -224,7 +224,7 @@ Mapping struct field and method names ------------------------------------- By default, the names are passed through as is which means they are capitalised. This does not match the standard JavaScript naming convention, so if you need to make your JS code look more natural or if you are -dealing with a 3rd party library, you can use a [FieldNameMapper](https://pkg.go.dev/github.com/dop251/goja#FieldNameMapper): +dealing with a 3rd party library, you can use a [FieldNameMapper](https://pkg.go.dev/apigo.cc/ai/ai/goja#FieldNameMapper): ```go vm := goja.New() @@ -238,14 +238,14 @@ fmt.Println(res.Export()) // Output: 42 ``` -There are two standard mappers: [TagFieldNameMapper](https://pkg.go.dev/github.com/dop251/goja#TagFieldNameMapper) and -[UncapFieldNameMapper](https://pkg.go.dev/github.com/dop251/goja#UncapFieldNameMapper), or you can use your own implementation. +There are two standard mappers: [TagFieldNameMapper](https://pkg.go.dev/apigo.cc/ai/ai/goja#TagFieldNameMapper) and +[UncapFieldNameMapper](https://pkg.go.dev/apigo.cc/ai/ai/goja#UncapFieldNameMapper), or you can use your own implementation. Native Constructors ------------------- In order to implement a constructor function in Go use `func (goja.ConstructorCall) *goja.Object`. -See [Runtime.ToValue()](https://pkg.go.dev/github.com/dop251/goja#Runtime.ToValue) documentation for more details. +See [Runtime.ToValue()](https://pkg.go.dev/apigo.cc/ai/ai/goja#Runtime.ToValue) documentation for more details. Regular Expressions ------------------- @@ -331,4 +331,4 @@ func TestInterrupt(t *testing.T) { NodeJS Compatibility -------------------- -There is a [separate project](https://github.com/dop251/goja_nodejs) aimed at providing some of the NodeJS functionality. +There is a [separate project](https://apigo.cc/ai/ai/goja_nodejs) aimed at providing some of the NodeJS functionality. diff --git a/goja/array.go b/goja/array.go index 7a67a47..e7d0f99 100644 --- a/goja/array.go +++ b/goja/array.go @@ -7,7 +7,7 @@ import ( "reflect" "strconv" - "github.com/dop251/goja/unistring" + "apigo.cc/ai/ai/goja/unistring" ) type arrayIterObject struct { diff --git a/goja/array_sparse.go b/goja/array_sparse.go index f99afd7..e3a7809 100644 --- a/goja/array_sparse.go +++ b/goja/array_sparse.go @@ -8,7 +8,7 @@ import ( "sort" "strconv" - "github.com/dop251/goja/unistring" + "apigo.cc/ai/ai/goja/unistring" ) type sparseArrayItem struct { diff --git a/goja/ast/README.markdown b/goja/ast/README.markdown index aba088e..80ee5a1 100644 --- a/goja/ast/README.markdown +++ b/goja/ast/README.markdown @@ -1,6 +1,6 @@ # ast -- - import "github.com/dop251/goja/ast" + import "apigo.cc/ai/ai/goja/ast" Package ast declares types representing a JavaScript AST. diff --git a/goja/ast/node.go b/goja/ast/node.go index 3bec89d..90b475f 100644 --- a/goja/ast/node.go +++ b/goja/ast/node.go @@ -9,9 +9,9 @@ node types are concerned) and may change in the future. package ast import ( - "github.com/dop251/goja/file" - "github.com/dop251/goja/token" - "github.com/dop251/goja/unistring" + "apigo.cc/ai/ai/goja/file" + "apigo.cc/ai/ai/goja/token" + "apigo.cc/ai/ai/goja/unistring" ) type PropertyKind string diff --git a/goja/builtin_bigint.go b/goja/builtin_bigint.go index a50ebcf..c3de35f 100644 --- a/goja/builtin_bigint.go +++ b/goja/builtin_bigint.go @@ -9,7 +9,7 @@ import ( "strconv" "sync" - "github.com/dop251/goja/unistring" + "apigo.cc/ai/ai/goja/unistring" ) type valueBigInt big.Int diff --git a/goja/builtin_error.go b/goja/builtin_error.go index a7eae7d..3a74c6b 100644 --- a/goja/builtin_error.go +++ b/goja/builtin_error.go @@ -1,6 +1,6 @@ package goja -import "github.com/dop251/goja/unistring" +import "apigo.cc/ai/ai/goja/unistring" const propNameStack = "stack" diff --git a/goja/builtin_global.go b/goja/builtin_global.go index 2c6385a..c16b324 100644 --- a/goja/builtin_global.go +++ b/goja/builtin_global.go @@ -10,7 +10,7 @@ import ( "sync" "unicode/utf8" - "github.com/dop251/goja/unistring" + "apigo.cc/ai/ai/goja/unistring" ) const hexUpper = "0123456789ABCDEF" diff --git a/goja/builtin_json.go b/goja/builtin_json.go index cd4a7bc..9817aaf 100644 --- a/goja/builtin_json.go +++ b/goja/builtin_json.go @@ -12,7 +12,7 @@ import ( "unicode/utf16" "unicode/utf8" - "github.com/dop251/goja/unistring" + "apigo.cc/ai/ai/goja/unistring" ) const hex = "0123456789abcdef" diff --git a/goja/builtin_number.go b/goja/builtin_number.go index 43add4f..26dc43b 100644 --- a/goja/builtin_number.go +++ b/goja/builtin_number.go @@ -4,7 +4,7 @@ import ( "math" "sync" - "github.com/dop251/goja/ftoa" + "apigo.cc/ai/ai/goja/ftoa" ) func (r *Runtime) toNumber(v Value) Value { diff --git a/goja/builtin_promise.go b/goja/builtin_promise.go index d51f27d..554d743 100644 --- a/goja/builtin_promise.go +++ b/goja/builtin_promise.go @@ -1,7 +1,7 @@ package goja import ( - "github.com/dop251/goja/unistring" + "apigo.cc/ai/ai/goja/unistring" "reflect" ) @@ -605,7 +605,7 @@ func (r *Runtime) wrapPromiseReaction(fObj *Object) func(interface{}) { // NewPromise creates and returns a Promise and resolving functions for it. // // WARNING: The returned values are not goroutine-safe and must not be called in parallel with VM running. -// In order to make use of this method you need an event loop such as the one in goja_nodejs (https://github.com/dop251/goja_nodejs) +// In order to make use of this method you need an event loop such as the one in goja_nodejs (https://apigo.cc/ai/ai/goja_nodejs) // where it can be used like this: // // loop := NewEventLoop() diff --git a/goja/builtin_proxy.go b/goja/builtin_proxy.go index f589930..5d3c8de 100644 --- a/goja/builtin_proxy.go +++ b/goja/builtin_proxy.go @@ -1,7 +1,7 @@ package goja import ( - "github.com/dop251/goja/unistring" + "apigo.cc/ai/ai/goja/unistring" ) type nativeProxyHandler struct { diff --git a/goja/builtin_regexp.go b/goja/builtin_regexp.go index cdc0d9d..66880c1 100644 --- a/goja/builtin_regexp.go +++ b/goja/builtin_regexp.go @@ -1,8 +1,8 @@ package goja import ( + "apigo.cc/ai/ai/goja/parser" "fmt" - "github.com/dop251/goja/parser" "regexp" "strings" "unicode/utf16" diff --git a/goja/builtin_string.go b/goja/builtin_string.go index 067c615..e5df079 100644 --- a/goja/builtin_string.go +++ b/goja/builtin_string.go @@ -7,9 +7,9 @@ import ( "unicode/utf16" "unicode/utf8" - "github.com/dop251/goja/unistring" + "apigo.cc/ai/ai/goja/unistring" - "github.com/dop251/goja/parser" + "apigo.cc/ai/ai/goja/parser" "golang.org/x/text/collate" "golang.org/x/text/language" "golang.org/x/text/unicode/norm" diff --git a/goja/builtin_symbol.go b/goja/builtin_symbol.go index 8231b7b..72497e7 100644 --- a/goja/builtin_symbol.go +++ b/goja/builtin_symbol.go @@ -1,6 +1,6 @@ package goja -import "github.com/dop251/goja/unistring" +import "apigo.cc/ai/ai/goja/unistring" var ( SymHasInstance = newSymbol(asciiString("Symbol.hasInstance")) diff --git a/goja/builtin_typedarrays.go b/goja/builtin_typedarrays.go index 38c0376..db5c244 100644 --- a/goja/builtin_typedarrays.go +++ b/goja/builtin_typedarrays.go @@ -8,7 +8,7 @@ import ( "sync" "unsafe" - "github.com/dop251/goja/unistring" + "apigo.cc/ai/ai/goja/unistring" ) type typedArraySortCtx struct { diff --git a/goja/compiler.go b/goja/compiler.go index 2abd9ba..e9ff4f9 100644 --- a/goja/compiler.go +++ b/goja/compiler.go @@ -1,13 +1,13 @@ package goja import ( + "apigo.cc/ai/ai/goja/token" "fmt" - "github.com/dop251/goja/token" "sort" - "github.com/dop251/goja/ast" - "github.com/dop251/goja/file" - "github.com/dop251/goja/unistring" + "apigo.cc/ai/ai/goja/ast" + "apigo.cc/ai/ai/goja/file" + "apigo.cc/ai/ai/goja/unistring" ) type blockType int diff --git a/goja/compiler_expr.go b/goja/compiler_expr.go index 5245f26..ae3a7be 100644 --- a/goja/compiler_expr.go +++ b/goja/compiler_expr.go @@ -3,10 +3,10 @@ package goja import ( "math/big" - "github.com/dop251/goja/ast" - "github.com/dop251/goja/file" - "github.com/dop251/goja/token" - "github.com/dop251/goja/unistring" + "apigo.cc/ai/ai/goja/ast" + "apigo.cc/ai/ai/goja/file" + "apigo.cc/ai/ai/goja/token" + "apigo.cc/ai/ai/goja/unistring" ) type compiledExpr interface { diff --git a/goja/compiler_stmt.go b/goja/compiler_stmt.go index 2d3d83b..2f9ea2b 100644 --- a/goja/compiler_stmt.go +++ b/goja/compiler_stmt.go @@ -1,10 +1,10 @@ package goja import ( - "github.com/dop251/goja/ast" - "github.com/dop251/goja/file" - "github.com/dop251/goja/token" - "github.com/dop251/goja/unistring" + "apigo.cc/ai/ai/goja/ast" + "apigo.cc/ai/ai/goja/file" + "apigo.cc/ai/ai/goja/token" + "apigo.cc/ai/ai/goja/unistring" ) func (c *compiler) compileStatement(v ast.Statement, needResult bool) { diff --git a/goja/compiler_test.go b/goja/compiler_test.go index fd47617..b153795 100644 --- a/goja/compiler_test.go +++ b/goja/compiler_test.go @@ -6,7 +6,7 @@ import ( "testing" "unsafe" - "github.com/dop251/goja/unistring" + "apigo.cc/ai/ai/goja/unistring" ) const TESTLIB = ` diff --git a/goja/destruct.go b/goja/destruct.go index 66792dc..01d3027 100644 --- a/goja/destruct.go +++ b/goja/destruct.go @@ -1,7 +1,7 @@ package goja import ( - "github.com/dop251/goja/unistring" + "apigo.cc/ai/ai/goja/unistring" "reflect" ) diff --git a/goja/file/README.markdown b/goja/file/README.markdown index e9228c2..d3b9c86 100644 --- a/goja/file/README.markdown +++ b/goja/file/README.markdown @@ -1,6 +1,6 @@ # file -- - import "github.com/dop251/goja/file" + import "apigo.cc/ai/ai/goja/file" Package file encapsulates the file abstractions used by the ast & parser. diff --git a/goja/ftoa/ftostr.go b/goja/ftoa/ftostr.go index a9d2d24..b881ee3 100644 --- a/goja/ftoa/ftostr.go +++ b/goja/ftoa/ftostr.go @@ -4,7 +4,7 @@ import ( "math" "strconv" - "github.com/dop251/goja/ftoa/internal/fast" + "apigo.cc/ai/ai/goja/ftoa/internal/fast" ) type FToStrMode int diff --git a/goja/func.go b/goja/func.go index 727e291..272c9b2 100644 --- a/goja/func.go +++ b/goja/func.go @@ -1,11 +1,9 @@ package goja import ( + "apigo.cc/ai/ai/goja/unistring" "fmt" "reflect" - "strings" - - "github.com/dop251/goja/unistring" ) type resultType uint8 @@ -570,34 +568,34 @@ func (f *nativeFuncObject) vmCall(vm *vm, n int) { ret = _undefined } //fmt.Println(">>>>>>>>>>>>>>>>>>>>>>", ret.toString().String()) - if strings.HasPrefix(ret.toString().String(), "GoError: ") { - //fmt.Println(">>>>>>>>>>>>>>>>>>>>>>", 111) - //vm.throw(Exception{ - // val: ret.toString(), - //}) - //stack := make([]StackFrame, 0) - //for i := len(vm.callStack) - 1; i >= 0; i-- { - // frame := &vm.callStack[i] - // if frame.prg != nil || frame.sb > 0 { - // var funcName unistring.String - // if prg := frame.prg; prg != nil { - // funcName = prg.funcName - // } else { - // funcName = getFuncName(vm.stack, frame.sb) - // } - // stack = append(stack, StackFrame{prg: vm.callStack[i].prg, pc: frame.pc, funcName: funcName}) - // //prg := vm.callStack[i].prg - // //stack = append(stack, string(funcName)+" "+prg.src.Position(prg.sourceOffset(frame.pc)).String()) - // } - //} - vm.throw(&Exception{ - val: ret.ToString(), - }) - return - } else { - vm.stack[vm.sp-n-2] = ret - vm.popCtx() - } + //if strings.HasPrefix(ret.toString().String(), "GoError: ") { + // //fmt.Println(">>>>>>>>>>>>>>>>>>>>>>", 111) + // //vm.throw(Exception{ + // // val: ret.toString(), + // //}) + // //stack := make([]StackFrame, 0) + // //for i := len(vm.callStack) - 1; i >= 0; i-- { + // // frame := &vm.callStack[i] + // // if frame.prg != nil || frame.sb > 0 { + // // var funcName unistring.String + // // if prg := frame.prg; prg != nil { + // // funcName = prg.funcName + // // } else { + // // funcName = getFuncName(vm.stack, frame.sb) + // // } + // // stack = append(stack, StackFrame{prg: vm.callStack[i].prg, pc: frame.pc, funcName: funcName}) + // // //prg := vm.callStack[i].prg + // // //stack = append(stack, string(funcName)+" "+prg.src.Position(prg.sourceOffset(frame.pc)).String()) + // // } + // //} + // vm.throw(&Exception{ + // val: ret.ToString(), + // }) + // return + //} else { + vm.stack[vm.sp-n-2] = ret + vm.popCtx() + //} } else { vm.stack[vm.sp-n-2] = _undefined } diff --git a/goja/go.mod b/goja/go.mod deleted file mode 100644 index 103a3f1..0000000 --- a/goja/go.mod +++ /dev/null @@ -1,15 +0,0 @@ -module github.com/dop251/goja - -go 1.20 - -require ( - github.com/Masterminds/semver/v3 v3.2.1 - github.com/dlclark/regexp2 v1.11.4 - github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d - github.com/go-sourcemap/sourcemap v2.1.3+incompatible - github.com/google/pprof v0.0.0-20230207041349-798e818bf904 - golang.org/x/text v0.3.8 - gopkg.in/yaml.v2 v2.4.0 -) - -require github.com/kr/pretty v0.3.0 // indirect diff --git a/goja/goja/main.go b/goja/goja/main.go index 1015f46..579c4da 100644 --- a/goja/goja/main.go +++ b/goja/goja/main.go @@ -13,9 +13,9 @@ import ( "runtime/pprof" "time" - "github.com/dop251/goja" - "github.com/dop251/goja_nodejs/console" - "github.com/dop251/goja_nodejs/require" + "apigo.cc/ai/ai/goja" + "apigo.cc/ai/ai/goja_nodejs/console" + "apigo.cc/ai/ai/goja_nodejs/require" ) var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to file") diff --git a/goja/object.go b/goja/object.go index 79bd67d..5558b1a 100644 --- a/goja/object.go +++ b/goja/object.go @@ -6,7 +6,7 @@ import ( "reflect" "sort" - "github.com/dop251/goja/unistring" + "apigo.cc/ai/ai/goja/unistring" ) const ( diff --git a/goja/object_args.go b/goja/object_args.go index eb41d01..f36f0c1 100644 --- a/goja/object_args.go +++ b/goja/object_args.go @@ -1,6 +1,6 @@ package goja -import "github.com/dop251/goja/unistring" +import "apigo.cc/ai/ai/goja/unistring" type argumentsObject struct { baseObject diff --git a/goja/object_dynamic.go b/goja/object_dynamic.go index b1e3161..f634954 100644 --- a/goja/object_dynamic.go +++ b/goja/object_dynamic.go @@ -5,7 +5,7 @@ import ( "reflect" "strconv" - "github.com/dop251/goja/unistring" + "apigo.cc/ai/ai/goja/unistring" ) /* diff --git a/goja/object_goarray_reflect.go b/goja/object_goarray_reflect.go index e40364d..a2a0cae 100644 --- a/goja/object_goarray_reflect.go +++ b/goja/object_goarray_reflect.go @@ -4,7 +4,7 @@ import ( "reflect" "strconv" - "github.com/dop251/goja/unistring" + "apigo.cc/ai/ai/goja/unistring" ) type objectGoArrayReflect struct { diff --git a/goja/object_gomap.go b/goja/object_gomap.go index 82138c2..b08b900 100644 --- a/goja/object_gomap.go +++ b/goja/object_gomap.go @@ -3,7 +3,7 @@ package goja import ( "reflect" - "github.com/dop251/goja/unistring" + "apigo.cc/ai/ai/goja/unistring" ) type objectGoMapSimple struct { diff --git a/goja/object_gomap_reflect.go b/goja/object_gomap_reflect.go index d4c1a06..79b3244 100644 --- a/goja/object_gomap_reflect.go +++ b/goja/object_gomap_reflect.go @@ -4,7 +4,7 @@ import ( "fmt" "reflect" - "github.com/dop251/goja/unistring" + "apigo.cc/ai/ai/goja/unistring" ) type objectGoMapReflect struct { diff --git a/goja/object_goreflect.go b/goja/object_goreflect.go index f8ca6d0..560e4fb 100644 --- a/goja/object_goreflect.go +++ b/goja/object_goreflect.go @@ -6,8 +6,8 @@ import ( "reflect" "strings" - "github.com/dop251/goja/parser" - "github.com/dop251/goja/unistring" + "apigo.cc/ai/ai/goja/parser" + "apigo.cc/ai/ai/goja/unistring" ) // JsonEncodable allows custom JSON encoding by JSON.stringify() diff --git a/goja/object_goslice.go b/goja/object_goslice.go index 1a52207..0a7c252 100644 --- a/goja/object_goslice.go +++ b/goja/object_goslice.go @@ -6,7 +6,7 @@ import ( "reflect" "strconv" - "github.com/dop251/goja/unistring" + "apigo.cc/ai/ai/goja/unistring" ) type objectGoSlice struct { diff --git a/goja/object_goslice_reflect.go b/goja/object_goslice_reflect.go index 4c28d8c..8a6a90a 100644 --- a/goja/object_goslice_reflect.go +++ b/goja/object_goslice_reflect.go @@ -5,7 +5,7 @@ import ( "math/bits" "reflect" - "github.com/dop251/goja/unistring" + "apigo.cc/ai/ai/goja/unistring" ) type objectGoSliceReflect struct { diff --git a/goja/object_template.go b/goja/object_template.go index 6d42f9f..8b7456e 100644 --- a/goja/object_template.go +++ b/goja/object_template.go @@ -1,8 +1,8 @@ package goja import ( + "apigo.cc/ai/ai/goja/unistring" "fmt" - "github.com/dop251/goja/unistring" "math" "reflect" "sort" diff --git a/goja/parser/README.markdown b/goja/parser/README.markdown index ec1186d..b15be55 100644 --- a/goja/parser/README.markdown +++ b/goja/parser/README.markdown @@ -1,11 +1,11 @@ # parser -- - import "github.com/dop251/goja/parser" + import "apigo.cc/ai/ai/goja/parser" Package parser implements a parser for JavaScript. Borrowed from https://github.com/robertkrimen/otto/tree/master/parser import ( - "github.com/dop251/goja/parser" + "apigo.cc/ai/ai/goja/parser" ) Parse and return an AST diff --git a/goja/parser/error.go b/goja/parser/error.go index cf4d2c3..f3195ad 100644 --- a/goja/parser/error.go +++ b/goja/parser/error.go @@ -4,8 +4,8 @@ import ( "fmt" "sort" - "github.com/dop251/goja/file" - "github.com/dop251/goja/token" + "apigo.cc/ai/ai/goja/file" + "apigo.cc/ai/ai/goja/token" ) const ( diff --git a/goja/parser/expression.go b/goja/parser/expression.go index 305bed4..2fa5d66 100644 --- a/goja/parser/expression.go +++ b/goja/parser/expression.go @@ -3,10 +3,10 @@ package parser import ( "strings" - "github.com/dop251/goja/ast" - "github.com/dop251/goja/file" - "github.com/dop251/goja/token" - "github.com/dop251/goja/unistring" + "apigo.cc/ai/ai/goja/ast" + "apigo.cc/ai/ai/goja/file" + "apigo.cc/ai/ai/goja/token" + "apigo.cc/ai/ai/goja/unistring" ) func (self *_parser) parseIdentifier() *ast.Identifier { diff --git a/goja/parser/lexer.go b/goja/parser/lexer.go index 7d824be..f484cee 100644 --- a/goja/parser/lexer.go +++ b/goja/parser/lexer.go @@ -12,9 +12,9 @@ import ( "golang.org/x/text/unicode/rangetable" - "github.com/dop251/goja/file" - "github.com/dop251/goja/token" - "github.com/dop251/goja/unistring" + "apigo.cc/ai/ai/goja/file" + "apigo.cc/ai/ai/goja/token" + "apigo.cc/ai/ai/goja/unistring" ) var ( diff --git a/goja/parser/lexer_test.go b/goja/parser/lexer_test.go index 0eefa40..e97101d 100644 --- a/goja/parser/lexer_test.go +++ b/goja/parser/lexer_test.go @@ -3,9 +3,9 @@ package parser import ( "testing" - "github.com/dop251/goja/file" - "github.com/dop251/goja/token" - "github.com/dop251/goja/unistring" + "apigo.cc/ai/ai/goja/file" + "apigo.cc/ai/ai/goja/token" + "apigo.cc/ai/ai/goja/unistring" ) func TestLexer(t *testing.T) { diff --git a/goja/parser/marshal_test.go b/goja/parser/marshal_test.go index eb9a809..62fe3b5 100644 --- a/goja/parser/marshal_test.go +++ b/goja/parser/marshal_test.go @@ -7,7 +7,7 @@ import ( "strings" "testing" - "github.com/dop251/goja/ast" + "apigo.cc/ai/ai/goja/ast" ) func marshal(name string, children ...interface{}) interface{} { diff --git a/goja/parser/parser.go b/goja/parser/parser.go index 24b3802..c471f54 100644 --- a/goja/parser/parser.go +++ b/goja/parser/parser.go @@ -2,7 +2,7 @@ Package parser implements a parser for JavaScript. import ( - "github.com/dop251/goja/parser" + "apigo.cc/ai/ai/goja/parser" ) Parse and return an AST @@ -38,10 +38,10 @@ import ( "io" "os" - "github.com/dop251/goja/ast" - "github.com/dop251/goja/file" - "github.com/dop251/goja/token" - "github.com/dop251/goja/unistring" + "apigo.cc/ai/ai/goja/ast" + "apigo.cc/ai/ai/goja/file" + "apigo.cc/ai/ai/goja/token" + "apigo.cc/ai/ai/goja/unistring" ) // A Mode value is a set of flags (or 0). They control optional parser functionality. diff --git a/goja/parser/parser_test.go b/goja/parser/parser_test.go index e09a7d6..6a71df4 100644 --- a/goja/parser/parser_test.go +++ b/goja/parser/parser_test.go @@ -7,10 +7,10 @@ import ( "strings" "testing" - "github.com/dop251/goja/ast" - "github.com/dop251/goja/file" - "github.com/dop251/goja/token" - "github.com/dop251/goja/unistring" + "apigo.cc/ai/ai/goja/ast" + "apigo.cc/ai/ai/goja/file" + "apigo.cc/ai/ai/goja/token" + "apigo.cc/ai/ai/goja/unistring" ) func firstErr(err error) error { diff --git a/goja/parser/scope.go b/goja/parser/scope.go index 5e28ef4..172e062 100644 --- a/goja/parser/scope.go +++ b/goja/parser/scope.go @@ -1,8 +1,8 @@ package parser import ( - "github.com/dop251/goja/ast" - "github.com/dop251/goja/unistring" + "apigo.cc/ai/ai/goja/ast" + "apigo.cc/ai/ai/goja/unistring" ) type _scope struct { diff --git a/goja/parser/statement.go b/goja/parser/statement.go index 8ec5cde..8df8660 100644 --- a/goja/parser/statement.go +++ b/goja/parser/statement.go @@ -6,9 +6,9 @@ import ( "os" "strings" - "github.com/dop251/goja/ast" - "github.com/dop251/goja/file" - "github.com/dop251/goja/token" + "apigo.cc/ai/ai/goja/ast" + "apigo.cc/ai/ai/goja/file" + "apigo.cc/ai/ai/goja/token" "github.com/go-sourcemap/sourcemap" ) diff --git a/goja/proxy.go b/goja/proxy.go index e9bd8c9..b6afd82 100644 --- a/goja/proxy.go +++ b/goja/proxy.go @@ -4,7 +4,7 @@ import ( "fmt" "reflect" - "github.com/dop251/goja/unistring" + "apigo.cc/ai/ai/goja/unistring" ) // Proxy is a Go wrapper around ECMAScript Proxy. Calling Runtime.ToValue() on it diff --git a/goja/regexp.go b/goja/regexp.go index f70c34d..ed7b5ae 100644 --- a/goja/regexp.go +++ b/goja/regexp.go @@ -1,9 +1,9 @@ package goja import ( + "apigo.cc/ai/ai/goja/unistring" "fmt" "github.com/dlclark/regexp2" - "github.com/dop251/goja/unistring" "io" "regexp" "sort" diff --git a/goja/runtime.go b/goja/runtime.go index eff33b1..19358cc 100644 --- a/goja/runtime.go +++ b/goja/runtime.go @@ -17,10 +17,10 @@ import ( "golang.org/x/text/collate" - js_ast "github.com/dop251/goja/ast" - "github.com/dop251/goja/file" - "github.com/dop251/goja/parser" - "github.com/dop251/goja/unistring" + js_ast "apigo.cc/ai/ai/goja/ast" + "apigo.cc/ai/ai/goja/file" + "apigo.cc/ai/ai/goja/parser" + "apigo.cc/ai/ai/goja/unistring" ) const ( diff --git a/goja/runtime_test.go b/goja/runtime_test.go index c456cfc..333bf14 100644 --- a/goja/runtime_test.go +++ b/goja/runtime_test.go @@ -11,7 +11,7 @@ import ( "testing" "time" - "github.com/dop251/goja/parser" + "apigo.cc/ai/ai/goja/parser" ) func TestGlobalObjectProto(t *testing.T) { diff --git a/goja/string.go b/goja/string.go index 0eaf3ef..3f67067 100644 --- a/goja/string.go +++ b/goja/string.go @@ -6,7 +6,7 @@ import ( "strings" "unicode/utf8" - "github.com/dop251/goja/unistring" + "apigo.cc/ai/ai/goja/unistring" ) const ( diff --git a/goja/string_ascii.go b/goja/string_ascii.go index 6b13784..1f6fa81 100644 --- a/goja/string_ascii.go +++ b/goja/string_ascii.go @@ -9,7 +9,7 @@ import ( "strconv" "strings" - "github.com/dop251/goja/unistring" + "apigo.cc/ai/ai/goja/unistring" ) type asciiString string diff --git a/goja/string_imported.go b/goja/string_imported.go index 1c6cae8..43352dd 100644 --- a/goja/string_imported.go +++ b/goja/string_imported.go @@ -9,8 +9,8 @@ import ( "unicode/utf16" "unicode/utf8" - "github.com/dop251/goja/parser" - "github.com/dop251/goja/unistring" + "apigo.cc/ai/ai/goja/parser" + "apigo.cc/ai/ai/goja/unistring" "golang.org/x/text/cases" "golang.org/x/text/language" diff --git a/goja/string_unicode.go b/goja/string_unicode.go index 49e363f..af06ce8 100644 --- a/goja/string_unicode.go +++ b/goja/string_unicode.go @@ -10,8 +10,8 @@ import ( "unicode/utf16" "unicode/utf8" - "github.com/dop251/goja/parser" - "github.com/dop251/goja/unistring" + "apigo.cc/ai/ai/goja/parser" + "apigo.cc/ai/ai/goja/unistring" "golang.org/x/text/cases" "golang.org/x/text/language" ) diff --git a/goja/token/README.markdown b/goja/token/README.markdown index 66dd2ab..fa11377 100644 --- a/goja/token/README.markdown +++ b/goja/token/README.markdown @@ -1,6 +1,6 @@ # token -- - import "github.com/dop251/goja/token" + import "apigo.cc/ai/ai/goja/token" Package token defines constants representing the lexical tokens of JavaScript (ECMA5). diff --git a/goja/typedarrays.go b/goja/typedarrays.go index 67c4677..9360b0e 100644 --- a/goja/typedarrays.go +++ b/goja/typedarrays.go @@ -7,7 +7,7 @@ import ( "strconv" "unsafe" - "github.com/dop251/goja/unistring" + "apigo.cc/ai/ai/goja/unistring" ) type byteOrder bool diff --git a/goja/value.go b/goja/value.go index 6ca3d72..9c7cb70 100644 --- a/goja/value.go +++ b/goja/value.go @@ -9,8 +9,8 @@ import ( "strconv" "unsafe" - "github.com/dop251/goja/ftoa" - "github.com/dop251/goja/unistring" + "apigo.cc/ai/ai/goja/ftoa" + "apigo.cc/ai/ai/goja/unistring" ) var ( diff --git a/goja/vm.go b/goja/vm.go index 35e5594..b6b807d 100644 --- a/goja/vm.go +++ b/goja/vm.go @@ -10,7 +10,7 @@ import ( "sync/atomic" "time" - "github.com/dop251/goja/unistring" + "apigo.cc/ai/ai/goja/unistring" ) const ( diff --git a/goja/vm_test.go b/goja/vm_test.go index a88ff47..d1b9f66 100644 --- a/goja/vm_test.go +++ b/goja/vm_test.go @@ -1,9 +1,9 @@ package goja import ( - "github.com/dop251/goja/file" - "github.com/dop251/goja/parser" - "github.com/dop251/goja/unistring" + "apigo.cc/ai/ai/goja/file" + "apigo.cc/ai/ai/goja/parser" + "apigo.cc/ai/ai/goja/unistring" "testing" ) diff --git a/goja_nodejs/LICENSE b/goja_nodejs/LICENSE new file mode 100644 index 0000000..8c27a94 --- /dev/null +++ b/goja_nodejs/LICENSE @@ -0,0 +1,13 @@ +Copyright (c) 2016 Dmitry Panov + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/goja_nodejs/README.md b/goja_nodejs/README.md new file mode 100644 index 0000000..e154fc6 --- /dev/null +++ b/goja_nodejs/README.md @@ -0,0 +1,32 @@ +Nodejs compatibility library for Goja +==== + +This is a collection of Goja modules that provide nodejs compatibility. + +Example: + +```go +package main + +import ( + "apigo.cc/ai/ai/goja" + "apigo.cc/ai/ai/goja_nodejs/require" +) + +func main() { + registry := new(require.Registry) // this can be shared by multiple runtimes + + runtime := goja.New() + req := registry.Enable(runtime) + + runtime.RunString(` + var m = require("./m.js"); + m.test(); + `) + + m, err := req.Require("./m.js") + _, _ = m, err +} +``` + +More modules will be added. Contributions welcome too. diff --git a/goja_nodejs/assert.js b/goja_nodejs/assert.js new file mode 100644 index 0000000..b00076d --- /dev/null +++ b/goja_nodejs/assert.js @@ -0,0 +1,83 @@ +'use strict'; + +const assert = { + _isSameValue(a, b) { + if (a === b) { + // Handle +/-0 vs. -/+0 + return a !== 0 || 1 / a === 1 / b; + } + + // Handle NaN vs. NaN + return a !== a && b !== b; + }, + + _toString(value) { + try { + if (value === 0 && 1 / value === -Infinity) { + return '-0'; + } + + return String(value); + } catch (err) { + if (err.name === 'TypeError') { + return Object.prototype.toString.call(value); + } + + throw err; + } + }, + + sameValue(actual, expected, message) { + if (assert._isSameValue(actual, expected)) { + return; + } + if (message === undefined) { + message = ''; + } else { + message += ' '; + } + + message += 'Expected SameValue(«' + assert._toString(actual) + '», «' + assert._toString(expected) + '») to be true'; + + throw new Error(message); + }, + + throws(f, ctor, message) { + if (message === undefined) { + message = ''; + } else { + message += ' '; + } + try { + f(); + } catch (e) { + if (e.constructor !== ctor) { + throw new Error(message + "Wrong exception type was thrown: " + e.constructor.name); + } + return; + } + throw new Error(message + "No exception was thrown"); + }, + + throwsNodeError(f, ctor, code, message) { + if (message === undefined) { + message = ''; + } else { + message += ' '; + } + try { + f(); + } catch (e) { + if (e.constructor !== ctor) { + throw new Error(message + "Wrong exception type was thrown: " + e.constructor.name); + } + if (e.code !== code) { + throw new Error(message + "Wrong exception code was thrown: " + e.code); + } + return; + } + throw new Error(message + "No exception was thrown"); + } +} + +module.exports = assert; \ No newline at end of file diff --git a/goja_nodejs/buffer/buffer.go b/goja_nodejs/buffer/buffer.go new file mode 100644 index 0000000..e7c4d71 --- /dev/null +++ b/goja_nodejs/buffer/buffer.go @@ -0,0 +1,440 @@ +package buffer + +import ( + "bytes" + "encoding/base64" + "encoding/hex" + "reflect" + "strconv" + + "apigo.cc/ai/ai/goja" + "apigo.cc/ai/ai/goja_nodejs/errors" + "apigo.cc/ai/ai/goja_nodejs/require" + + "github.com/dop251/base64dec" + "golang.org/x/text/encoding/unicode" +) + +const ModuleName = "buffer" + +type Buffer struct { + r *goja.Runtime + + bufferCtorObj *goja.Object + + uint8ArrayCtorObj *goja.Object + uint8ArrayCtor goja.Constructor +} + +var ( + symApi = goja.NewSymbol("api") +) + +var ( + reflectTypeArrayBuffer = reflect.TypeOf(goja.ArrayBuffer{}) + reflectTypeString = reflect.TypeOf("") + reflectTypeInt = reflect.TypeOf(int64(0)) + reflectTypeFloat = reflect.TypeOf(0.0) + reflectTypeBytes = reflect.TypeOf(([]byte)(nil)) +) + +func Enable(runtime *goja.Runtime) { + runtime.Set("Buffer", require.Require(runtime, ModuleName).ToObject(runtime).Get("Buffer")) +} + +func Bytes(r *goja.Runtime, v goja.Value) []byte { + var b []byte + err := r.ExportTo(v, &b) + if err != nil { + return []byte(v.String()) + } + return b +} + +func mod(r *goja.Runtime) *goja.Object { + res := r.Get("Buffer") + if res == nil { + res = require.Require(r, ModuleName).ToObject(r).Get("Buffer") + } + m, ok := res.(*goja.Object) + if !ok { + panic(r.NewTypeError("Could not extract Buffer")) + } + return m +} + +func api(mod *goja.Object) *Buffer { + if s := mod.GetSymbol(symApi); s != nil { + b, _ := s.Export().(*Buffer) + return b + } + + return nil +} + +func GetApi(r *goja.Runtime) *Buffer { + return api(mod(r)) +} + +func DecodeBytes(r *goja.Runtime, arg, enc goja.Value) []byte { + switch arg.ExportType() { + case reflectTypeArrayBuffer: + return arg.Export().(goja.ArrayBuffer).Bytes() + case reflectTypeString: + var codec StringCodec + if !goja.IsUndefined(enc) { + codec = stringCodecs[enc.String()] + } + if codec == nil { + codec = utf8Codec + } + return codec.DecodeAppend(arg.String(), nil) + default: + if o, ok := arg.(*goja.Object); ok { + if o.ExportType() == reflectTypeBytes { + return o.Export().([]byte) + } + } + } + panic(errors.NewTypeError(r, errors.ErrCodeInvalidArgType, "The \"data\" argument must be of type string or an instance of Buffer, TypedArray, or DataView.")) +} + +func WrapBytes(r *goja.Runtime, data []byte) *goja.Object { + m := mod(r) + if api := api(m); api != nil { + return api.WrapBytes(data) + } + if from, ok := goja.AssertFunction(m.Get("from")); ok { + ab := r.NewArrayBuffer(data) + v, err := from(m, r.ToValue(ab)) + if err != nil { + panic(err) + } + return v.ToObject(r) + } + panic(r.NewTypeError("Buffer.from is not a function")) +} + +// EncodeBytes returns the given byte slice encoded as string with the given encoding. If encoding +// is not specified or not supported, returns a Buffer that wraps the data. +func EncodeBytes(r *goja.Runtime, data []byte, enc goja.Value) goja.Value { + var codec StringCodec + if !goja.IsUndefined(enc) { + codec = StringCodecByName(enc.String()) + } + if codec != nil { + return r.ToValue(codec.Encode(data)) + } + return WrapBytes(r, data) +} + +func (b *Buffer) WrapBytes(data []byte) *goja.Object { + return b.fromBytes(data) +} + +func (b *Buffer) ctor(call goja.ConstructorCall) (res *goja.Object) { + arg := call.Argument(0) + switch arg.ExportType() { + case reflectTypeInt, reflectTypeFloat: + panic(b.r.NewTypeError("Calling the Buffer constructor with numeric argument is not implemented yet")) + // TODO implement + } + return b._from(call.Arguments...) +} + +type StringCodec interface { + DecodeAppend(string, []byte) []byte + Encode([]byte) string +} + +type hexCodec struct{} + +func (hexCodec) DecodeAppend(s string, b []byte) []byte { + l := hex.DecodedLen(len(s)) + dst, res := expandSlice(b, l) + n, err := hex.Decode(dst, []byte(s)) + if err != nil { + res = res[:len(b)+n] + } + return res +} + +func (hexCodec) Encode(b []byte) string { + return hex.EncodeToString(b) +} + +type _utf8Codec struct{} + +func (_utf8Codec) DecodeAppend(s string, b []byte) []byte { + r, _ := unicode.UTF8.NewEncoder().String(s) + dst, res := expandSlice(b, len(r)) + copy(dst, r) + return res +} + +func (_utf8Codec) Encode(b []byte) string { + r, _ := unicode.UTF8.NewDecoder().Bytes(b) + return string(r) +} + +type base64Codec struct{} + +type base64UrlCodec struct { + base64Codec +} + +func (base64Codec) DecodeAppend(s string, b []byte) []byte { + res, _ := Base64DecodeAppend(b, s) + return res +} + +func (base64Codec) Encode(b []byte) string { + return base64.StdEncoding.EncodeToString(b) +} + +func (base64UrlCodec) Encode(b []byte) string { + return base64.RawURLEncoding.EncodeToString(b) +} + +var utf8Codec StringCodec = _utf8Codec{} + +var stringCodecs = map[string]StringCodec{ + "hex": hexCodec{}, + "utf8": utf8Codec, + "utf-8": utf8Codec, + "base64": base64Codec{}, + "base64Url": base64UrlCodec{}, +} + +func expandSlice(b []byte, l int) (dst, res []byte) { + if cap(b)-len(b) < l { + b1 := make([]byte, len(b)+l) + copy(b1, b) + dst = b1[len(b):] + res = b1 + } else { + dst = b[len(b) : len(b)+l] + res = b[:len(b)+l] + } + return +} + +func Base64DecodeAppend(dst []byte, src string) ([]byte, error) { + l := base64.RawStdEncoding.DecodedLen(len(src)) + d, res := expandSlice(dst, l) + n, err := base64dec.DecodeBase64(d, src) + + res = res[:len(dst)+n] + return res, err +} + +func (b *Buffer) fromString(str, enc string) *goja.Object { + codec := stringCodecs[enc] + if codec == nil { + codec = utf8Codec + } + return b.fromBytes(codec.DecodeAppend(str, nil)) +} + +func (b *Buffer) fromBytes(data []byte) *goja.Object { + o, err := b.uint8ArrayCtor(b.bufferCtorObj, b.r.ToValue(b.r.NewArrayBuffer(data))) + if err != nil { + panic(err) + } + return o +} + +func (b *Buffer) _from(args ...goja.Value) *goja.Object { + if len(args) == 0 { + panic(errors.NewTypeError(b.r, errors.ErrCodeInvalidArgType, "The first argument must be of type string or an instance of Buffer, ArrayBuffer, or Array or an Array-like Object. Received undefined")) + } + arg := args[0] + switch arg.ExportType() { + case reflectTypeArrayBuffer: + v, err := b.uint8ArrayCtor(b.bufferCtorObj, args...) + if err != nil { + panic(err) + } + return v + case reflectTypeString: + var enc string + if len(args) > 1 { + enc = args[1].String() + } + return b.fromString(arg.String(), enc) + default: + if o, ok := arg.(*goja.Object); ok { + if o.ExportType() == reflectTypeBytes { + bb, _ := o.Export().([]byte) + a := make([]byte, len(bb)) + copy(a, bb) + return b.fromBytes(a) + } else { + if f, ok := goja.AssertFunction(o.Get("valueOf")); ok { + valueOf, err := f(o) + if err != nil { + panic(err) + } + if valueOf != o { + args[0] = valueOf + return b._from(args...) + } + } + + if s := o.GetSymbol(goja.SymToPrimitive); s != nil { + if f, ok := goja.AssertFunction(s); ok { + str, err := f(o, b.r.ToValue("string")) + if err != nil { + panic(err) + } + args[0] = str + return b._from(args...) + } + } + } + // array-like + if v := o.Get("length"); v != nil { + length := int(v.ToInteger()) + a := make([]byte, length) + for i := 0; i < length; i++ { + item := o.Get(strconv.Itoa(i)) + if item != nil { + a[i] = byte(item.ToInteger()) + } + } + return b.fromBytes(a) + } + } + } + panic(errors.NewTypeError(b.r, errors.ErrCodeInvalidArgType, "The first argument must be of type string or an instance of Buffer, ArrayBuffer, or Array or an Array-like Object. Received %s", arg)) +} + +func (b *Buffer) from(call goja.FunctionCall) goja.Value { + return b._from(call.Arguments...) +} + +func isNumber(v goja.Value) bool { + switch v.ExportType() { + case reflectTypeInt, reflectTypeFloat: + return true + } + return false +} + +func isString(v goja.Value) bool { + return v.ExportType() == reflectTypeString +} + +func StringCodecByName(name string) StringCodec { + return stringCodecs[name] +} + +func (b *Buffer) getStringCodec(enc goja.Value) (codec StringCodec) { + if !goja.IsUndefined(enc) { + codec = stringCodecs[enc.String()] + if codec == nil { + panic(errors.NewTypeError(b.r, "ERR_UNKNOWN_ENCODING", "Unknown encoding: %s", enc)) + } + } else { + codec = utf8Codec + } + return +} + +func (b *Buffer) fill(buf []byte, fill string, enc goja.Value) []byte { + codec := b.getStringCodec(enc) + b1 := codec.DecodeAppend(fill, buf[:0]) + if len(b1) > len(buf) { + return b1[:len(buf)] + } + for i := len(b1); i < len(buf); { + i += copy(buf[i:], buf[:i]) + } + return buf +} + +func (b *Buffer) alloc(call goja.FunctionCall) goja.Value { + arg0 := call.Argument(0) + size := -1 + if isNumber(arg0) { + size = int(arg0.ToInteger()) + } + if size < 0 { + panic(errors.NewTypeError(b.r, errors.ErrCodeInvalidArgType, "The \"size\" argument must be of type number.")) + } + fill := call.Argument(1) + buf := make([]byte, size) + if !goja.IsUndefined(fill) { + if isString(fill) { + var enc goja.Value + if a := call.Argument(2); isString(a) { + enc = a + } else { + enc = goja.Undefined() + } + buf = b.fill(buf, fill.String(), enc) + } else { + fill = fill.ToNumber() + if !goja.IsNaN(fill) && !goja.IsInfinity(fill) { + fillByte := byte(fill.ToInteger()) + if fillByte != 0 { + for i := range buf { + buf[i] = fillByte + } + } + } + } + } + return b.fromBytes(buf) +} + +func (b *Buffer) proto_toString(call goja.FunctionCall) goja.Value { + bb := Bytes(b.r, call.This) + codec := b.getStringCodec(call.Argument(0)) + return b.r.ToValue(codec.Encode(bb)) +} + +func (b *Buffer) proto_equals(call goja.FunctionCall) goja.Value { + bb := Bytes(b.r, call.This) + other := call.Argument(0) + if b.r.InstanceOf(other, b.uint8ArrayCtorObj) { + otherBytes := Bytes(b.r, other) + return b.r.ToValue(bytes.Equal(bb, otherBytes)) + } + panic(errors.NewTypeError(b.r, errors.ErrCodeInvalidArgType, "The \"otherBuffer\" argument must be an instance of Buffer or Uint8Array.")) +} + +func Require(runtime *goja.Runtime, module *goja.Object) { + b := &Buffer{r: runtime} + uint8Array := runtime.Get("Uint8Array") + if c, ok := goja.AssertConstructor(uint8Array); ok { + b.uint8ArrayCtor = c + } else { + panic(runtime.NewTypeError("Uint8Array is not a constructor")) + } + uint8ArrayObj := uint8Array.ToObject(runtime) + + ctor := runtime.ToValue(b.ctor).ToObject(runtime) + ctor.SetPrototype(uint8ArrayObj) + ctor.DefineDataPropertySymbol(symApi, runtime.ToValue(b), goja.FLAG_FALSE, goja.FLAG_FALSE, goja.FLAG_FALSE) + b.bufferCtorObj = ctor + b.uint8ArrayCtorObj = uint8ArrayObj + + proto := runtime.NewObject() + proto.SetPrototype(uint8ArrayObj.Get("prototype").ToObject(runtime)) + proto.DefineDataProperty("constructor", ctor, goja.FLAG_TRUE, goja.FLAG_TRUE, goja.FLAG_FALSE) + proto.Set("equals", b.proto_equals) + proto.Set("toString", b.proto_toString) + + ctor.Set("prototype", proto) + ctor.Set("poolSize", 8192) + ctor.Set("from", b.from) + ctor.Set("alloc", b.alloc) + + exports := module.Get("exports").(*goja.Object) + exports.Set("Buffer", ctor) +} + +func init() { + require.RegisterCoreModule(ModuleName, Require) +} diff --git a/goja_nodejs/buffer/buffer_test.go b/goja_nodejs/buffer/buffer_test.go new file mode 100644 index 0000000..ef7a51a --- /dev/null +++ b/goja_nodejs/buffer/buffer_test.go @@ -0,0 +1,228 @@ +package buffer + +import ( + "testing" + + "apigo.cc/ai/ai/goja" + "apigo.cc/ai/ai/goja_nodejs/require" +) + +func TestBufferFrom(t *testing.T) { + vm := goja.New() + new(require.Registry).Enable(vm) + + _, err := vm.RunString(` + const Buffer = require("node:buffer").Buffer; + + function checkBuffer(buf) { + if (!(buf instanceof Buffer)) { + throw new Error("instanceof Buffer"); + } + + if (!(buf instanceof Uint8Array)) { + throw new Error("instanceof Uint8Array"); + } + } + + checkBuffer(Buffer.from(new ArrayBuffer(16))); + checkBuffer(Buffer.from(new Uint16Array(8))); + + { + const b = Buffer.from("\xff\xfe\xfd"); + const h = b.toString("hex") + if (h !== "c3bfc3bec3bd") { + throw new Error(h); + } + } + + { + const b = Buffer.from("0102fffdXXX", "hex"); + checkBuffer(b); + if (b.toString("hex") !== "0102fffd") { + throw new Error(b.toString("hex")); + } + } + + { + const b = Buffer.from('1ag123', 'hex'); + if (b.length !== 1 || b[0] !== 0x1a) { + throw new Error(b); + } + } + + { + const b = Buffer.from('1a7', 'hex'); + if (b.length !== 1 || b[0] !== 0x1a) { + throw new Error(b); + } + } + + { + const b = Buffer.from("\uD801", "utf-8"); + if (b.length !== 3 || b[0] !== 0xef || b[1] !== 0xbf || b[2] !== 0xbd) { + throw new Error(b); + } + } + `) + + if err != nil { + t.Fatal(err) + } +} + +func TestFromBase64(t *testing.T) { + vm := goja.New() + new(require.Registry).Enable(vm) + + _, err := vm.RunString(` + const Buffer = require("node:buffer").Buffer; + + { + let b = Buffer.from("AAA_", "base64"); + if (b.length !== 3 || b[0] !== 0 || b[1] !== 0 || b[2] !== 0x3f) { + throw new Error(b.toString("hex")); + } + + let r = b.toString("base64"); + if (r !== "AAA/") { + throw new Error("to base64: " + r); + } + for (let i = 0; i < 20; i++) { + let s = "A".repeat(i) + "_" + "A".repeat(20-i); + let s1 = "A".repeat(i) + "/" + "A".repeat(20-i); + let b = Buffer.from(s, "base64"); + let b1 = Buffer.from(s1, "base64"); + if (!b.equals(b1)) { + throw new Error(s); + } + } + } + + { + let b = Buffer.from("SQ==???", "base64"); + if (b.length !== 1 || b[0] != 0x49) { + throw new Error(b.toString("hex")); + } + } + + { + let s = Buffer.from("AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow", "base64Url").toString("base64"); + if (s !== "AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ+EstJQLr/T+1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow==") { + throw new Error(s); + } + } + `) + + if err != nil { + t.Fatal(err) + } +} + +func TestWrapBytes(t *testing.T) { + vm := goja.New() + new(require.Registry).Enable(vm) + b := []byte{1, 2, 3} + buffer := GetApi(vm) + vm.Set("b", buffer.WrapBytes(b)) + Enable(vm) + _, err := vm.RunString(` + if (typeof Buffer !== "function") { + throw new Error("Buffer is not a function: " + typeof Buffer); + } + if (!(b instanceof Buffer)) { + throw new Error("instanceof Buffer"); + } + if (b.toString("hex") !== "010203") { + throw new Error(b); + } + `) + + if err != nil { + t.Fatal(err) + } +} + +func TestBuffer_alloc(t *testing.T) { + vm := goja.New() + new(require.Registry).Enable(vm) + + _, err := vm.RunString(` + const Buffer = require("node:buffer").Buffer; + + { + const b = Buffer.alloc(2, "abc"); + if (b.toString() !== "ab") { + throw new Error(b); + } + } + + { + const b = Buffer.alloc(16, "abc"); + if (b.toString() !== "abcabcabcabcabca") { + throw new Error(b); + } + } + + { + const fill = { + valueOf() { + return 0xac; + } + } + const b = Buffer.alloc(8, fill); + if (b.toString("hex") !== "acacacacacacacac") { + throw new Error(b); + } + } + + { + const fill = { + valueOf() { + return Infinity; + } + } + const b = Buffer.alloc(2, fill); + if (b.toString("hex") !== "0000") { + throw new Error(b); + } + } + + { + const fill = { + valueOf() { + return "ac"; + } + } + const b = Buffer.alloc(2, fill); + if (b.toString("hex") !== "0000") { + throw new Error(b); + } + } + + { + const b = Buffer.alloc(2, -257.4); + if (b.toString("hex") !== "ffff") { + throw new Error(b); + } + } + + { + const b = Buffer.alloc(2, Infinity); + if (b.toString("hex") !== "0000") { + throw new Error("Infinity: " + b.toString("hex")); + } + } + + { + const b = Buffer.alloc(2, null); + if (b.toString("hex") !== "0000") { + throw new Error("Infinity: " + b.toString("hex")); + } + } + + `) + + if err != nil { + t.Fatal(err) + } +} diff --git a/goja_nodejs/console/module.go b/goja_nodejs/console/module.go new file mode 100644 index 0000000..516f69e --- /dev/null +++ b/goja_nodejs/console/module.go @@ -0,0 +1,72 @@ +package console + +import ( + "apigo.cc/ai/ai/goja" + "apigo.cc/ai/ai/goja_nodejs/require" + "apigo.cc/ai/ai/goja_nodejs/util" +) + +const ModuleName = "console" + +type Console struct { + runtime *goja.Runtime + util *goja.Object + printer Printer +} + +type Printer interface { + Log(string) + Warn(string) + Error(string) +} + +func (c *Console) log(p func(string)) func(goja.FunctionCall) goja.Value { + return func(call goja.FunctionCall) goja.Value { + if format, ok := goja.AssertFunction(c.util.Get("format")); ok { + ret, err := format(c.util, call.Arguments...) + if err != nil { + panic(err) + } + + p(ret.String()) + } else { + panic(c.runtime.NewTypeError("util.format is not a function")) + } + + return nil + } +} + +func Require(runtime *goja.Runtime, module *goja.Object) { + requireWithPrinter(defaultStdPrinter)(runtime, module) +} + +func RequireWithPrinter(printer Printer) require.ModuleLoader { + return requireWithPrinter(printer) +} + +func requireWithPrinter(printer Printer) require.ModuleLoader { + return func(runtime *goja.Runtime, module *goja.Object) { + c := &Console{ + runtime: runtime, + printer: printer, + } + + c.util = require.Require(runtime, util.ModuleName).(*goja.Object) + + o := module.Get("exports").(*goja.Object) + o.Set("log", c.log(c.printer.Log)) + o.Set("error", c.log(c.printer.Error)) + o.Set("warn", c.log(c.printer.Warn)) + o.Set("info", c.log(c.printer.Log)) + o.Set("debug", c.log(c.printer.Log)) + } +} + +func Enable(runtime *goja.Runtime) { + runtime.Set("console", require.Require(runtime, ModuleName)) +} + +func init() { + require.RegisterCoreModule(ModuleName, Require) +} diff --git a/goja_nodejs/console/module_test.go b/goja_nodejs/console/module_test.go new file mode 100644 index 0000000..2b2a8db --- /dev/null +++ b/goja_nodejs/console/module_test.go @@ -0,0 +1,78 @@ +package console + +import ( + "testing" + + "apigo.cc/ai/ai/goja" + "apigo.cc/ai/ai/goja_nodejs/require" +) + +func TestConsole(t *testing.T) { + vm := goja.New() + + new(require.Registry).Enable(vm) + Enable(vm) + + if c := vm.Get("console"); c == nil { + t.Fatal("console not found") + } + + if _, err := vm.RunString("console.log('')"); err != nil { + t.Fatal("console.log() error", err) + } + + if _, err := vm.RunString("console.error('')"); err != nil { + t.Fatal("console.error() error", err) + } + + if _, err := vm.RunString("console.warn('')"); err != nil { + t.Fatal("console.warn() error", err) + } + + if _, err := vm.RunString("console.info('')"); err != nil { + t.Fatal("console.info() error", err) + } + + if _, err := vm.RunString("console.debug('')"); err != nil { + t.Fatal("console.debug() error", err) + } +} + +func TestConsoleWithPrinter(t *testing.T) { + var stdoutStr, stderrStr string + + printer := StdPrinter{ + StdoutPrint: func(s string) { stdoutStr += s }, + StderrPrint: func(s string) { stderrStr += s }, + } + + vm := goja.New() + + registry := new(require.Registry) + registry.Enable(vm) + registry.RegisterNativeModule(ModuleName, RequireWithPrinter(printer)) + Enable(vm) + + if c := vm.Get("console"); c == nil { + t.Fatal("console not found") + } + + _, err := vm.RunString(` + console.log('a') + console.error('b') + console.warn('c') + console.debug('d') + console.info('e') + `) + if err != nil { + t.Fatal(err) + } + + if want := "ade"; stdoutStr != want { + t.Fatalf("Unexpected stdout output: got %q, want %q", stdoutStr, want) + } + + if want := "bc"; stderrStr != want { + t.Fatalf("Unexpected stderr output: got %q, want %q", stderrStr, want) + } +} diff --git a/goja_nodejs/console/std_printer.go b/goja_nodejs/console/std_printer.go new file mode 100644 index 0000000..ffb2324 --- /dev/null +++ b/goja_nodejs/console/std_printer.go @@ -0,0 +1,38 @@ +package console + +import ( + "log" + "os" +) + +var ( + stderrLogger = log.Default() // the default logger output to stderr + stdoutLogger = log.New(os.Stdout, "", log.LstdFlags) + + defaultStdPrinter Printer = &StdPrinter{ + StdoutPrint: func(s string) { stdoutLogger.Print(s) }, + StderrPrint: func(s string) { stderrLogger.Print(s) }, + } +) + +// StdPrinter implements the console.Printer interface +// that prints to the stdout or stderr. +type StdPrinter struct { + StdoutPrint func(s string) + StderrPrint func(s string) +} + +// Log prints s to the stdout. +func (p StdPrinter) Log(s string) { + p.StdoutPrint(s) +} + +// Warn prints s to the stderr. +func (p StdPrinter) Warn(s string) { + p.StderrPrint(s) +} + +// Error prints s to the stderr. +func (p StdPrinter) Error(s string) { + p.StderrPrint(s) +} diff --git a/goja_nodejs/errors/errors.go b/goja_nodejs/errors/errors.go new file mode 100644 index 0000000..fc2dac4 --- /dev/null +++ b/goja_nodejs/errors/errors.go @@ -0,0 +1,71 @@ +package errors + +import ( + "fmt" + + "apigo.cc/ai/ai/goja" +) + +const ( + ErrCodeInvalidArgType = "ERR_INVALID_ARG_TYPE" + ErrCodeInvalidArgValue = "ERR_INVALID_ARG_VALUE" + ErrCodeInvalidThis = "ERR_INVALID_THIS" + ErrCodeMissingArgs = "ERR_MISSING_ARGS" +) + +func error_toString(call goja.FunctionCall, r *goja.Runtime) goja.Value { + this := call.This.ToObject(r) + var name, msg string + if n := this.Get("name"); n != nil && !goja.IsUndefined(n) { + name = n.String() + } else { + name = "Error" + } + if m := this.Get("message"); m != nil && !goja.IsUndefined(m) { + msg = m.String() + } + if code := this.Get("code"); code != nil && !goja.IsUndefined(code) { + if name != "" { + name += " " + } + name += "[" + code.String() + "]" + } + if msg != "" { + if name != "" { + name += ": " + } + name += msg + } + return r.ToValue(name) +} + +func addProps(r *goja.Runtime, e *goja.Object, code string) { + e.Set("code", code) + e.DefineDataProperty("toString", r.ToValue(error_toString), goja.FLAG_TRUE, goja.FLAG_TRUE, goja.FLAG_FALSE) +} + +func NewTypeError(r *goja.Runtime, code string, params ...interface{}) *goja.Object { + e := r.NewTypeError(params...) + addProps(r, e, code) + return e +} + +func NewError(r *goja.Runtime, ctor *goja.Object, code string, args ...interface{}) *goja.Object { + if ctor == nil { + ctor, _ = r.Get("Error").(*goja.Object) + } + if ctor == nil { + return nil + } + msg := "" + if len(args) > 0 { + f, _ := args[0].(string) + msg = fmt.Sprintf(f, args[1:]...) + } + o, err := r.New(ctor, r.ToValue(msg)) + if err != nil { + panic(err) + } + addProps(r, o, code) + return o +} diff --git a/goja_nodejs/eventloop/eventloop.go b/goja_nodejs/eventloop/eventloop.go new file mode 100644 index 0000000..61d23b8 --- /dev/null +++ b/goja_nodejs/eventloop/eventloop.go @@ -0,0 +1,514 @@ +package eventloop + +import ( + "sync" + "sync/atomic" + "time" + + "apigo.cc/ai/ai/goja" + "apigo.cc/ai/ai/goja_nodejs/console" + "apigo.cc/ai/ai/goja_nodejs/require" +) + +type job struct { + cancel func() bool + fn func() + idx int + + cancelled bool +} + +type Timer struct { + job + timer *time.Timer +} + +type Interval struct { + job + ticker *time.Ticker + stopChan chan struct{} +} + +type Immediate struct { + job +} + +type EventLoop struct { + vm *goja.Runtime + jobChan chan func() + jobs []*job + jobCount int32 + canRun int32 + + auxJobsLock sync.Mutex + wakeupChan chan struct{} + + auxJobsSpare, auxJobs []func() + + stopLock sync.Mutex + stopCond *sync.Cond + running bool + terminated bool + + enableConsole bool + registry *require.Registry +} + +func NewEventLoop(opts ...Option) *EventLoop { + vm := goja.New() + + loop := &EventLoop{ + vm: vm, + jobChan: make(chan func()), + wakeupChan: make(chan struct{}, 1), + enableConsole: true, + } + loop.stopCond = sync.NewCond(&loop.stopLock) + + for _, opt := range opts { + opt(loop) + } + if loop.registry == nil { + loop.registry = new(require.Registry) + } + loop.registry.Enable(vm) + if loop.enableConsole { + console.Enable(vm) + } + vm.Set("setTimeout", loop.setTimeout) + vm.Set("setInterval", loop.setInterval) + vm.Set("setImmediate", loop.setImmediate) + vm.Set("clearTimeout", loop.clearTimeout) + vm.Set("clearInterval", loop.clearInterval) + vm.Set("clearImmediate", loop.clearImmediate) + + return loop +} + +type Option func(*EventLoop) + +// EnableConsole controls whether the "console" module is loaded into +// the runtime used by the loop. By default, loops are created with +// the "console" module loaded, pass EnableConsole(false) to +// NewEventLoop to disable this behavior. +func EnableConsole(enableConsole bool) Option { + return func(loop *EventLoop) { + loop.enableConsole = enableConsole + } +} + +func WithRegistry(registry *require.Registry) Option { + return func(loop *EventLoop) { + loop.registry = registry + } +} + +func (loop *EventLoop) schedule(call goja.FunctionCall, repeating bool) goja.Value { + if fn, ok := goja.AssertFunction(call.Argument(0)); ok { + delay := call.Argument(1).ToInteger() + var args []goja.Value + if len(call.Arguments) > 2 { + args = append(args, call.Arguments[2:]...) + } + f := func() { fn(nil, args...) } + loop.jobCount++ + var job *job + var ret goja.Value + if repeating { + interval := loop.newInterval(f) + interval.start(loop, time.Duration(delay)*time.Millisecond) + job = &interval.job + ret = loop.vm.ToValue(interval) + } else { + timeout := loop.newTimeout(f) + timeout.start(loop, time.Duration(delay)*time.Millisecond) + job = &timeout.job + ret = loop.vm.ToValue(timeout) + } + job.idx = len(loop.jobs) + loop.jobs = append(loop.jobs, job) + return ret + } + return nil +} + +func (loop *EventLoop) setTimeout(call goja.FunctionCall) goja.Value { + return loop.schedule(call, false) +} + +func (loop *EventLoop) setInterval(call goja.FunctionCall) goja.Value { + return loop.schedule(call, true) +} + +func (loop *EventLoop) setImmediate(call goja.FunctionCall) goja.Value { + if fn, ok := goja.AssertFunction(call.Argument(0)); ok { + var args []goja.Value + if len(call.Arguments) > 1 { + args = append(args, call.Arguments[1:]...) + } + f := func() { fn(nil, args...) } + loop.jobCount++ + return loop.vm.ToValue(loop.addImmediate(f)) + } + return nil +} + +// SetTimeout schedules to run the specified function in the context +// of the loop as soon as possible after the specified timeout period. +// SetTimeout returns a Timer which can be passed to ClearTimeout. +// The instance of goja.Runtime that is passed to the function and any Values derived +// from it must not be used outside the function. SetTimeout is +// safe to call inside or outside the loop. +// If the loop is terminated (see Terminate()) returns nil. +func (loop *EventLoop) SetTimeout(fn func(*goja.Runtime), timeout time.Duration) *Timer { + t := loop.newTimeout(func() { fn(loop.vm) }) + if loop.addAuxJob(func() { + t.start(loop, timeout) + loop.jobCount++ + t.idx = len(loop.jobs) + loop.jobs = append(loop.jobs, &t.job) + }) { + return t + } + return nil +} + +// ClearTimeout cancels a Timer returned by SetTimeout if it has not run yet. +// ClearTimeout is safe to call inside or outside the loop. +func (loop *EventLoop) ClearTimeout(t *Timer) { + loop.addAuxJob(func() { + loop.clearTimeout(t) + }) +} + +// SetInterval schedules to repeatedly run the specified function in +// the context of the loop as soon as possible after every specified +// timeout period. SetInterval returns an Interval which can be +// passed to ClearInterval. The instance of goja.Runtime that is passed to the +// function and any Values derived from it must not be used outside +// the function. SetInterval is safe to call inside or outside the +// loop. +// If the loop is terminated (see Terminate()) returns nil. +func (loop *EventLoop) SetInterval(fn func(*goja.Runtime), timeout time.Duration) *Interval { + i := loop.newInterval(func() { fn(loop.vm) }) + if loop.addAuxJob(func() { + i.start(loop, timeout) + loop.jobCount++ + i.idx = len(loop.jobs) + loop.jobs = append(loop.jobs, &i.job) + }) { + return i + } + return nil +} + +// ClearInterval cancels an Interval returned by SetInterval. +// ClearInterval is safe to call inside or outside the loop. +func (loop *EventLoop) ClearInterval(i *Interval) { + loop.addAuxJob(func() { + loop.clearInterval(i) + }) +} + +func (loop *EventLoop) setRunning() { + loop.stopLock.Lock() + defer loop.stopLock.Unlock() + if loop.running { + panic("Loop is already started") + } + loop.running = true + atomic.StoreInt32(&loop.canRun, 1) + loop.auxJobsLock.Lock() + loop.terminated = false + loop.auxJobsLock.Unlock() +} + +// Run calls the specified function, starts the event loop and waits until there are no more delayed jobs to run +// after which it stops the loop and returns. +// The instance of goja.Runtime that is passed to the function and any Values derived from it must not be used +// outside the function. +// Do NOT use this function while the loop is already running. Use RunOnLoop() instead. +// If the loop is already started it will panic. +func (loop *EventLoop) Run(fn func(*goja.Runtime)) { + loop.setRunning() + fn(loop.vm) + loop.run(false) +} + +// Start the event loop in the background. The loop continues to run until Stop() is called. +// If the loop is already started it will panic. +func (loop *EventLoop) Start() { + loop.setRunning() + go loop.run(true) +} + +// StartInForeground starts the event loop in the current goroutine. The loop continues to run until Stop() is called. +// If the loop is already started it will panic. +// Use this instead of Start if you want to recover from panics that may occur while calling native Go functions from +// within setInterval and setTimeout callbacks. +func (loop *EventLoop) StartInForeground() { + loop.setRunning() + loop.run(true) +} + +// Stop the loop that was started with Start(). After this function returns there will be no more jobs executed +// by the loop. It is possible to call Start() or Run() again after this to resume the execution. +// Note, it does not cancel active timeouts (use Terminate() instead if you want this). +// It is not allowed to run Start() (or Run()) and Stop() or Terminate() concurrently. +// Calling Stop() on a non-running loop has no effect. +// It is not allowed to call Stop() from the loop, because it is synchronous and cannot complete until the loop +// is not running any jobs. Use StopNoWait() instead. +// return number of jobs remaining +func (loop *EventLoop) Stop() int { + loop.stopLock.Lock() + for loop.running { + atomic.StoreInt32(&loop.canRun, 0) + loop.wakeup() + loop.stopCond.Wait() + } + loop.stopLock.Unlock() + return int(loop.jobCount) +} + +// StopNoWait tells the loop to stop and returns immediately. Can be used inside the loop. Calling it on a +// non-running loop has no effect. +func (loop *EventLoop) StopNoWait() { + loop.stopLock.Lock() + if loop.running { + atomic.StoreInt32(&loop.canRun, 0) + loop.wakeup() + } + loop.stopLock.Unlock() +} + +// Terminate stops the loop and clears all active timeouts and intervals. After it returns there are no +// active timers or goroutines associated with the loop. Any attempt to submit a task (by using RunOnLoop(), +// SetTimeout() or SetInterval()) will not succeed. +// After being terminated the loop can be restarted again by using Start() or Run(). +// This method must not be called concurrently with Stop*(), Start(), or Run(). +func (loop *EventLoop) Terminate() { + loop.Stop() + + loop.auxJobsLock.Lock() + loop.terminated = true + loop.auxJobsLock.Unlock() + + loop.runAux() + + for i := 0; i < len(loop.jobs); i++ { + job := loop.jobs[i] + if !job.cancelled { + job.cancelled = true + if job.cancel() { + loop.removeJob(job) + i-- + } + } + } + + for len(loop.jobs) > 0 { + (<-loop.jobChan)() + } +} + +// RunOnLoop schedules to run the specified function in the context of the loop as soon as possible. +// The order of the runs is preserved (i.e. the functions will be called in the same order as calls to RunOnLoop()) +// The instance of goja.Runtime that is passed to the function and any Values derived from it must not be used +// outside the function. It is safe to call inside or outside the loop. +// Returns true on success or false if the loop is terminated (see Terminate()). +func (loop *EventLoop) RunOnLoop(fn func(*goja.Runtime)) bool { + return loop.addAuxJob(func() { fn(loop.vm) }) +} + +func (loop *EventLoop) runAux() { + loop.auxJobsLock.Lock() + jobs := loop.auxJobs + loop.auxJobs = loop.auxJobsSpare + loop.auxJobsLock.Unlock() + for i, job := range jobs { + job() + jobs[i] = nil + } + loop.auxJobsSpare = jobs[:0] +} + +func (loop *EventLoop) run(inBackground bool) { + loop.runAux() + if inBackground { + loop.jobCount++ + } +LOOP: + for loop.jobCount > 0 { + select { + case job := <-loop.jobChan: + job() + case <-loop.wakeupChan: + loop.runAux() + if atomic.LoadInt32(&loop.canRun) == 0 { + break LOOP + } + } + } + if inBackground { + loop.jobCount-- + } + + loop.stopLock.Lock() + loop.running = false + loop.stopLock.Unlock() + loop.stopCond.Broadcast() +} + +func (loop *EventLoop) wakeup() { + select { + case loop.wakeupChan <- struct{}{}: + default: + } +} + +func (loop *EventLoop) addAuxJob(fn func()) bool { + loop.auxJobsLock.Lock() + if loop.terminated { + loop.auxJobsLock.Unlock() + return false + } + loop.auxJobs = append(loop.auxJobs, fn) + loop.auxJobsLock.Unlock() + loop.wakeup() + return true +} + +func (loop *EventLoop) newTimeout(f func()) *Timer { + t := &Timer{ + job: job{fn: f}, + } + t.cancel = t.doCancel + + return t +} + +func (t *Timer) start(loop *EventLoop, timeout time.Duration) { + t.timer = time.AfterFunc(timeout, func() { + loop.jobChan <- func() { + loop.doTimeout(t) + } + }) +} + +func (loop *EventLoop) newInterval(f func()) *Interval { + i := &Interval{ + job: job{fn: f}, + stopChan: make(chan struct{}), + } + i.cancel = i.doCancel + + return i +} + +func (i *Interval) start(loop *EventLoop, timeout time.Duration) { + // https://nodejs.org/api/timers.html#timers_setinterval_callback_delay_args + if timeout <= 0 { + timeout = time.Millisecond + } + i.ticker = time.NewTicker(timeout) + go i.run(loop) +} + +func (loop *EventLoop) addImmediate(f func()) *Immediate { + i := &Immediate{ + job: job{fn: f}, + } + loop.addAuxJob(func() { + loop.doImmediate(i) + }) + return i +} + +func (loop *EventLoop) doTimeout(t *Timer) { + loop.removeJob(&t.job) + if !t.cancelled { + t.cancelled = true + loop.jobCount-- + t.fn() + } +} + +func (loop *EventLoop) doInterval(i *Interval) { + if !i.cancelled { + i.fn() + } +} + +func (loop *EventLoop) doImmediate(i *Immediate) { + if !i.cancelled { + i.cancelled = true + loop.jobCount-- + i.fn() + } +} + +func (loop *EventLoop) clearTimeout(t *Timer) { + if t != nil && !t.cancelled { + t.cancelled = true + loop.jobCount-- + if t.doCancel() { + loop.removeJob(&t.job) + } + } +} + +func (loop *EventLoop) clearInterval(i *Interval) { + if i != nil && !i.cancelled { + i.cancelled = true + loop.jobCount-- + i.doCancel() + } +} + +func (loop *EventLoop) removeJob(job *job) { + idx := job.idx + if idx < 0 { + return + } + if idx < len(loop.jobs)-1 { + loop.jobs[idx] = loop.jobs[len(loop.jobs)-1] + loop.jobs[idx].idx = idx + } + loop.jobs[len(loop.jobs)-1] = nil + loop.jobs = loop.jobs[:len(loop.jobs)-1] + job.idx = -1 +} + +func (loop *EventLoop) clearImmediate(i *Immediate) { + if i != nil && !i.cancelled { + i.cancelled = true + loop.jobCount-- + } +} + +func (i *Interval) doCancel() bool { + close(i.stopChan) + return false +} + +func (t *Timer) doCancel() bool { + return t.timer.Stop() +} + +func (i *Interval) run(loop *EventLoop) { +L: + for { + select { + case <-i.stopChan: + i.ticker.Stop() + break L + case <-i.ticker.C: + loop.jobChan <- func() { + loop.doInterval(i) + } + } + } + loop.jobChan <- func() { + loop.removeJob(&i.job) + } +} diff --git a/goja_nodejs/eventloop/eventloop_test.go b/goja_nodejs/eventloop/eventloop_test.go new file mode 100644 index 0000000..c54cbf5 --- /dev/null +++ b/goja_nodejs/eventloop/eventloop_test.go @@ -0,0 +1,641 @@ +package eventloop + +import ( + "fmt" + "sync/atomic" + "testing" + "time" + + "apigo.cc/ai/ai/goja" + + "go.uber.org/goleak" +) + +func TestRun(t *testing.T) { + t.Parallel() + const SCRIPT = ` + var calledAt; + setTimeout(function() { + calledAt = now(); + }, 1000); + ` + + loop := NewEventLoop() + prg, err := goja.Compile("main.js", SCRIPT, false) + if err != nil { + t.Fatal(err) + } + startTime := time.Now() + loop.Run(func(vm *goja.Runtime) { + vm.Set("now", time.Now) + _, err = vm.RunProgram(prg) + }) + if err != nil { + t.Fatal(err) + } + var calledAt time.Time + loop.Run(func(vm *goja.Runtime) { + err = vm.ExportTo(vm.Get("calledAt"), &calledAt) + }) + if err != nil { + t.Fatal(err) + } + if calledAt.IsZero() { + t.Fatal("Not called") + } + if dur := calledAt.Sub(startTime); dur < time.Second { + t.Fatal(dur) + } +} + +func TestStart(t *testing.T) { + t.Parallel() + const SCRIPT = ` + var calledAt; + setTimeout(function() { + calledAt = now(); + }, 1000); + ` + + prg, err := goja.Compile("main.js", SCRIPT, false) + if err != nil { + t.Fatal(err) + } + + loop := NewEventLoop() + startTime := time.Now() + loop.Start() + + loop.RunOnLoop(func(vm *goja.Runtime) { + vm.Set("now", time.Now) + vm.RunProgram(prg) + }) + + time.Sleep(2 * time.Second) + if remainingJobs := loop.Stop(); remainingJobs != 0 { + t.Fatal(remainingJobs) + } + + var calledAt time.Time + loop.Run(func(vm *goja.Runtime) { + err = vm.ExportTo(vm.Get("calledAt"), &calledAt) + }) + if err != nil { + t.Fatal(err) + } + if calledAt.IsZero() { + t.Fatal("Not called") + } + if dur := calledAt.Sub(startTime); dur < time.Second { + t.Fatal(dur) + } +} + +func TestStartInForeground(t *testing.T) { + t.Parallel() + const SCRIPT = ` + var calledAt; + setTimeout(function() { + calledAt = now(); + }, 1000); + ` + + prg, err := goja.Compile("main.js", SCRIPT, false) + if err != nil { + t.Fatal(err) + } + + loop := NewEventLoop() + startTime := time.Now() + go loop.StartInForeground() + + loop.RunOnLoop(func(vm *goja.Runtime) { + vm.Set("now", time.Now) + vm.RunProgram(prg) + }) + + time.Sleep(2 * time.Second) + if remainingJobs := loop.Stop(); remainingJobs != 0 { + t.Fatal(remainingJobs) + } + + var calledAt time.Time + loop.Run(func(vm *goja.Runtime) { + err = vm.ExportTo(vm.Get("calledAt"), &calledAt) + }) + if err != nil { + t.Fatal(err) + } + if calledAt.IsZero() { + t.Fatal("Not called") + } + if dur := calledAt.Sub(startTime); dur < time.Second { + t.Fatal(dur) + } +} + +func TestInterval(t *testing.T) { + t.Parallel() + const SCRIPT = ` + var count = 0; + var t = setInterval(function(times) { + console.log("tick"); + if (++count > times) { + clearInterval(t); + } + }, 1000, 2); + console.log("Started"); + ` + + loop := NewEventLoop() + prg, err := goja.Compile("main.js", SCRIPT, false) + if err != nil { + t.Fatal(err) + } + + loop.Run(func(vm *goja.Runtime) { + _, err = vm.RunProgram(prg) + }) + if err != nil { + t.Fatal(err) + } + + var count int64 + loop.Run(func(vm *goja.Runtime) { + count = vm.Get("count").ToInteger() + }) + if count != 3 { + t.Fatal(count) + } +} + +func TestImmediate(t *testing.T) { + t.Parallel() + const SCRIPT = ` + let log = []; + function cb(arg) { + log.push(arg); + } + var i; + var t = setImmediate(function() { + cb("tick"); + setImmediate(cb, "tick 2"); + i = setImmediate(cb, "should not run") + }); + setImmediate(function() { + clearImmediate(i); + }); + cb("Started"); + ` + + loop := NewEventLoop() + prg, err := goja.Compile("main.js", SCRIPT, false) + if err != nil { + t.Fatal(err) + } + loop.Run(func(vm *goja.Runtime) { + _, err = vm.RunProgram(prg) + }) + if err != nil { + t.Fatal(err) + } + + loop.Run(func(vm *goja.Runtime) { + _, err = vm.RunString(` + if (log.length != 3) { + throw new Error("Invalid log length: " + log); + } + if (log[0] !== "Started" || log[1] !== "tick" || log[2] !== "tick 2") { + throw new Error("Invalid log: " + log); + } + `) + }) + if err != nil { + t.Fatal(err) + } +} + +func TestRunNoSchedule(t *testing.T) { + loop := NewEventLoop() + fired := false + loop.Run(func(vm *goja.Runtime) { // should not hang + fired = true + // do not schedule anything + }) + + if !fired { + t.Fatal("Not fired") + } +} + +func TestRunWithConsole(t *testing.T) { + const SCRIPT = ` + console.log("Started"); + ` + + loop := NewEventLoop() + prg, err := goja.Compile("main.js", SCRIPT, false) + if err != nil { + t.Fatal(err) + } + loop.Run(func(vm *goja.Runtime) { + _, err = vm.RunProgram(prg) + }) + if err != nil { + t.Fatal("Call to console.log generated an error", err) + } + + loop = NewEventLoop(EnableConsole(true)) + prg, err = goja.Compile("main.js", SCRIPT, false) + if err != nil { + t.Fatal(err) + } + loop.Run(func(vm *goja.Runtime) { + _, err = vm.RunProgram(prg) + }) + if err != nil { + t.Fatal("Call to console.log generated an error", err) + } +} + +func TestRunNoConsole(t *testing.T) { + const SCRIPT = ` + console.log("Started"); + ` + + loop := NewEventLoop(EnableConsole(false)) + prg, err := goja.Compile("main.js", SCRIPT, false) + if err != nil { + t.Fatal(err) + } + loop.Run(func(vm *goja.Runtime) { + _, err = vm.RunProgram(prg) + }) + if err == nil { + t.Fatal("Call to console.log did not generate an error", err) + } +} + +func TestClearIntervalRace(t *testing.T) { + t.Parallel() + const SCRIPT = ` + console.log("calling setInterval"); + var t = setInterval(function() { + console.log("tick"); + }, 500); + console.log("calling sleep"); + sleep(2000); + console.log("calling clearInterval"); + clearInterval(t); + ` + + loop := NewEventLoop() + prg, err := goja.Compile("main.js", SCRIPT, false) + if err != nil { + t.Fatal(err) + } + // Should not hang + loop.Run(func(vm *goja.Runtime) { + vm.Set("sleep", func(ms int) { + <-time.After(time.Duration(ms) * time.Millisecond) + }) + vm.RunProgram(prg) + }) +} + +func TestNativeTimeout(t *testing.T) { + t.Parallel() + fired := false + loop := NewEventLoop() + loop.SetTimeout(func(*goja.Runtime) { + fired = true + }, 1*time.Second) + loop.Run(func(*goja.Runtime) { + // do not schedule anything + }) + if !fired { + t.Fatal("Not fired") + } +} + +func TestNativeClearTimeout(t *testing.T) { + t.Parallel() + fired := false + loop := NewEventLoop() + timer := loop.SetTimeout(func(*goja.Runtime) { + fired = true + }, 2*time.Second) + loop.SetTimeout(func(*goja.Runtime) { + loop.ClearTimeout(timer) + }, 1*time.Second) + loop.Run(func(*goja.Runtime) { + // do not schedule anything + }) + if fired { + t.Fatal("Cancelled timer fired!") + } +} + +func TestNativeInterval(t *testing.T) { + t.Parallel() + count := 0 + loop := NewEventLoop() + var i *Interval + i = loop.SetInterval(func(*goja.Runtime) { + t.Log("tick") + count++ + if count > 2 { + loop.ClearInterval(i) + } + }, 1*time.Second) + loop.Run(func(*goja.Runtime) { + // do not schedule anything + }) + if count != 3 { + t.Fatal("Expected interval to fire 3 times, got", count) + } +} + +func TestNativeClearInterval(t *testing.T) { + t.Parallel() + count := 0 + loop := NewEventLoop() + loop.Run(func(*goja.Runtime) { + i := loop.SetInterval(func(*goja.Runtime) { + t.Log("tick") + count++ + }, 500*time.Millisecond) + <-time.After(2 * time.Second) + loop.ClearInterval(i) + }) + if count != 0 { + t.Fatal("Expected interval to fire 0 times, got", count) + } +} + +func TestSetAndClearOnStoppedLoop(t *testing.T) { + t.Parallel() + loop := NewEventLoop() + timeout := loop.SetTimeout(func(runtime *goja.Runtime) { + panic("must not run") + }, 1*time.Millisecond) + loop.ClearTimeout(timeout) + loop.Start() + time.Sleep(10 * time.Millisecond) + loop.Terminate() +} + +func TestSetTimeoutConcurrent(t *testing.T) { + t.Parallel() + loop := NewEventLoop() + loop.Start() + ch := make(chan struct{}, 1) + loop.SetTimeout(func(*goja.Runtime) { + ch <- struct{}{} + }, 100*time.Millisecond) + <-ch + loop.Stop() +} + +func TestClearTimeoutConcurrent(t *testing.T) { + t.Parallel() + loop := NewEventLoop() + loop.Start() + timer := loop.SetTimeout(func(*goja.Runtime) { + }, 100*time.Millisecond) + loop.ClearTimeout(timer) + loop.Stop() + if c := loop.jobCount; c != 0 { + t.Fatalf("jobCount: %d", c) + } +} + +func TestClearIntervalConcurrent(t *testing.T) { + t.Parallel() + loop := NewEventLoop() + loop.Start() + ch := make(chan struct{}, 1) + i := loop.SetInterval(func(*goja.Runtime) { + ch <- struct{}{} + }, 500*time.Millisecond) + + <-ch + loop.ClearInterval(i) + loop.Stop() + if c := loop.jobCount; c != 0 { + t.Fatalf("jobCount: %d", c) + } +} + +func TestRunOnStoppedLoop(t *testing.T) { + t.Parallel() + loop := NewEventLoop() + var failed int32 + done := make(chan struct{}) + go func() { + for atomic.LoadInt32(&failed) == 0 { + loop.Start() + time.Sleep(10 * time.Millisecond) + loop.Stop() + } + }() + go func() { + for atomic.LoadInt32(&failed) == 0 { + loop.RunOnLoop(func(*goja.Runtime) { + if !loop.running { + atomic.StoreInt32(&failed, 1) + close(done) + return + } + }) + time.Sleep(10 * time.Millisecond) + } + }() + select { + case <-done: + case <-time.After(5 * time.Second): + } + if atomic.LoadInt32(&failed) != 0 { + t.Fatal("running job on stopped loop") + } +} + +func TestPromise(t *testing.T) { + t.Parallel() + const SCRIPT = ` + let result; + const p = new Promise((resolve, reject) => { + setTimeout(() => {resolve("passed")}, 500); + }); + p.then(value => { + result = value; + }); + ` + + loop := NewEventLoop() + prg, err := goja.Compile("main.js", SCRIPT, false) + if err != nil { + t.Fatal(err) + } + loop.Run(func(vm *goja.Runtime) { + _, err = vm.RunProgram(prg) + }) + if err != nil { + t.Fatal(err) + } + loop.Run(func(vm *goja.Runtime) { + result := vm.Get("result") + if !result.SameAs(vm.ToValue("passed")) { + err = fmt.Errorf("unexpected result: %v", result) + } + }) + if err != nil { + t.Fatal(err) + } +} + +func TestPromiseNative(t *testing.T) { + t.Parallel() + const SCRIPT = ` + let result; + p.then(value => { + result = value; + done(); + }); + ` + + loop := NewEventLoop() + prg, err := goja.Compile("main.js", SCRIPT, false) + if err != nil { + t.Fatal(err) + } + ch := make(chan error) + loop.Start() + defer loop.Stop() + + loop.RunOnLoop(func(vm *goja.Runtime) { + vm.Set("done", func() { + ch <- nil + }) + p, resolve, _ := vm.NewPromise() + vm.Set("p", p) + _, err = vm.RunProgram(prg) + if err != nil { + ch <- err + return + } + go func() { + time.Sleep(500 * time.Millisecond) + loop.RunOnLoop(func(*goja.Runtime) { + resolve("passed") + }) + }() + }) + err = <-ch + if err != nil { + t.Fatal(err) + } + loop.RunOnLoop(func(vm *goja.Runtime) { + result := vm.Get("result") + if !result.SameAs(vm.ToValue("passed")) { + ch <- fmt.Errorf("unexpected result: %v", result) + } else { + ch <- nil + } + }) + err = <-ch + if err != nil { + t.Fatal(err) + } +} + +func TestEventLoop_StopNoWait(t *testing.T) { + t.Parallel() + loop := NewEventLoop() + var ran int32 + loop.Run(func(runtime *goja.Runtime) { + loop.SetTimeout(func(*goja.Runtime) { + atomic.StoreInt32(&ran, 1) + }, 5*time.Second) + + loop.SetTimeout(func(*goja.Runtime) { + loop.StopNoWait() + }, 500*time.Millisecond) + }) + + if atomic.LoadInt32(&ran) != 0 { + t.Fatal("ran != 0") + } +} + +func TestEventLoop_ClearRunningTimeout(t *testing.T) { + t.Parallel() + const SCRIPT = ` + var called = 0; + let aTimer; + function a() { + if (++called > 5) { + return; + } + if (aTimer) { + clearTimeout(aTimer); + } + console.log("ok"); + aTimer = setTimeout(a, 500); + } + a();` + + prg, err := goja.Compile("main.js", SCRIPT, false) + if err != nil { + t.Fatal(err) + } + + loop := NewEventLoop() + + loop.Run(func(vm *goja.Runtime) { + _, err = vm.RunProgram(prg) + }) + + if err != nil { + t.Fatal(err) + } + + var called int64 + loop.Run(func(vm *goja.Runtime) { + called = vm.Get("called").ToInteger() + }) + if called != 6 { + t.Fatal(called) + } +} + +func TestEventLoop_Terminate(t *testing.T) { + defer goleak.VerifyNone(t) + + loop := NewEventLoop() + loop.Start() + interval := loop.SetInterval(func(vm *goja.Runtime) {}, 10*time.Millisecond) + time.Sleep(500 * time.Millisecond) + loop.ClearInterval(interval) + loop.Terminate() + + if loop.SetTimeout(func(*goja.Runtime) {}, time.Millisecond) != nil { + t.Fatal("was able to SetTimeout()") + } + if loop.SetInterval(func(*goja.Runtime) {}, time.Millisecond) != nil { + t.Fatal("was able to SetInterval()") + } + if loop.RunOnLoop(func(*goja.Runtime) {}) { + t.Fatal("was able to RunOnLoop()") + } + + ch := make(chan struct{}) + loop.Start() + if !loop.RunOnLoop(func(runtime *goja.Runtime) { + close(ch) + }) { + t.Fatal("RunOnLoop() has failed after restart") + } + <-ch + loop.Terminate() +} diff --git a/goja_nodejs/process/module.go b/goja_nodejs/process/module.go new file mode 100644 index 0000000..89d26db --- /dev/null +++ b/goja_nodejs/process/module.go @@ -0,0 +1,37 @@ +package process + +import ( + "os" + "strings" + + "apigo.cc/ai/ai/goja" + "apigo.cc/ai/ai/goja_nodejs/require" +) + +const ModuleName = "process" + +type Process struct { + env map[string]string +} + +func Require(runtime *goja.Runtime, module *goja.Object) { + p := &Process{ + env: make(map[string]string), + } + + for _, e := range os.Environ() { + envKeyValue := strings.SplitN(e, "=", 2) + p.env[envKeyValue[0]] = envKeyValue[1] + } + + o := module.Get("exports").(*goja.Object) + o.Set("env", p.env) +} + +func Enable(runtime *goja.Runtime) { + runtime.Set("process", require.Require(runtime, ModuleName)) +} + +func init() { + require.RegisterCoreModule(ModuleName, Require) +} diff --git a/goja_nodejs/process/module_test.go b/goja_nodejs/process/module_test.go new file mode 100644 index 0000000..73903ee --- /dev/null +++ b/goja_nodejs/process/module_test.go @@ -0,0 +1,68 @@ +package process + +import ( + "fmt" + "os" + "strings" + "testing" + + "apigo.cc/ai/ai/goja" + "apigo.cc/ai/ai/goja_nodejs/require" +) + +func TestProcessEnvStructure(t *testing.T) { + vm := goja.New() + + new(require.Registry).Enable(vm) + Enable(vm) + + if c := vm.Get("process"); c == nil { + t.Fatal("process not found") + } + + if c, err := vm.RunString("process.env"); c == nil || err != nil { + t.Fatal("error accessing process.env") + } +} + +func TestProcessEnvValuesArtificial(t *testing.T) { + os.Setenv("GOJA_IS_AWESOME", "true") + defer os.Unsetenv("GOJA_IS_AWESOME") + + vm := goja.New() + + new(require.Registry).Enable(vm) + Enable(vm) + + jsRes, err := vm.RunString("process.env['GOJA_IS_AWESOME']") + + if err != nil { + t.Fatalf("Error executing: %s", err) + } + + if jsRes.String() != "true" { + t.Fatalf("Error executing: got %s but expected %s", jsRes, "true") + } +} + +func TestProcessEnvValuesBrackets(t *testing.T) { + vm := goja.New() + + new(require.Registry).Enable(vm) + Enable(vm) + + for _, e := range os.Environ() { + envKeyValue := strings.SplitN(e, "=", 2) + jsExpr := fmt.Sprintf("process.env['%s']", envKeyValue[0]) + + jsRes, err := vm.RunString(jsExpr) + + if err != nil { + t.Fatalf("Error executing %s: %s", jsExpr, err) + } + + if jsRes.String() != envKeyValue[1] { + t.Fatalf("Error executing %s: got %s but expected %s", jsExpr, jsRes, envKeyValue[1]) + } + } +} diff --git a/goja_nodejs/require/module.go b/goja_nodejs/require/module.go new file mode 100644 index 0000000..0bde6a2 --- /dev/null +++ b/goja_nodejs/require/module.go @@ -0,0 +1,246 @@ +package require + +import ( + "errors" + "io" + "io/fs" + "os" + "path" + "path/filepath" + "runtime" + "sync" + "syscall" + "text/template" + + js "apigo.cc/ai/ai/goja" + "apigo.cc/ai/ai/goja/parser" +) + +type ModuleLoader func(*js.Runtime, *js.Object) + +// SourceLoader represents a function that returns a file data at a given path. +// The function should return ModuleFileDoesNotExistError if the file either doesn't exist or is a directory. +// This error will be ignored by the resolver and the search will continue. Any other errors will be propagated. +type SourceLoader func(path string) ([]byte, error) + +var ( + InvalidModuleError = errors.New("Invalid module") + IllegalModuleNameError = errors.New("Illegal module name") + NoSuchBuiltInModuleError = errors.New("No such built-in module") + ModuleFileDoesNotExistError = errors.New("module file does not exist") +) + +var native, builtin map[string]ModuleLoader + +// Registry contains a cache of compiled modules which can be used by multiple Runtimes +type Registry struct { + sync.Mutex + native map[string]ModuleLoader + compiled map[string]*js.Program + + srcLoader SourceLoader + globalFolders []string +} + +type RequireModule struct { + r *Registry + runtime *js.Runtime + modules map[string]*js.Object + nodeModules map[string]*js.Object +} + +func NewRegistry(opts ...Option) *Registry { + r := &Registry{} + + for _, opt := range opts { + opt(r) + } + + return r +} + +func NewRegistryWithLoader(srcLoader SourceLoader) *Registry { + return NewRegistry(WithLoader(srcLoader)) +} + +type Option func(*Registry) + +// WithLoader sets a function which will be called by the require() function in order to get a source code for a +// module at the given path. The same function will be used to get external source maps. +// Note, this only affects the modules loaded by the require() function. If you need to use it as a source map +// loader for code parsed in a different way (such as runtime.RunString() or eval()), use (*Runtime).SetParserOptions() +func WithLoader(srcLoader SourceLoader) Option { + return func(r *Registry) { + r.srcLoader = srcLoader + } +} + +// WithGlobalFolders appends the given paths to the registry's list of +// global folders to search if the requested module is not found +// elsewhere. By default, a registry's global folders list is empty. +// In the reference Node.js implementation, the default global folders +// list is $NODE_PATH, $HOME/.node_modules, $HOME/.node_libraries and +// $PREFIX/lib/node, see +// https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders. +func WithGlobalFolders(globalFolders ...string) Option { + return func(r *Registry) { + r.globalFolders = globalFolders + } +} + +// Enable adds the require() function to the specified runtime. +func (r *Registry) Enable(runtime *js.Runtime) *RequireModule { + rrt := &RequireModule{ + r: r, + runtime: runtime, + modules: make(map[string]*js.Object), + nodeModules: make(map[string]*js.Object), + } + + runtime.Set("require", rrt.require) + return rrt +} + +func (r *Registry) RegisterNativeModule(name string, loader ModuleLoader) { + r.Lock() + defer r.Unlock() + + if r.native == nil { + r.native = make(map[string]ModuleLoader) + } + name = filepathClean(name) + r.native[name] = loader +} + +// DefaultSourceLoader is used if none was set (see WithLoader()). It simply loads files from the host's filesystem. +func DefaultSourceLoader(filename string) ([]byte, error) { + fp := filepath.FromSlash(filename) + f, err := os.Open(fp) + if err != nil { + if errors.Is(err, fs.ErrNotExist) { + err = ModuleFileDoesNotExistError + } else if runtime.GOOS == "windows" { + if errors.Is(err, syscall.Errno(0x7b)) { // ERROR_INVALID_NAME, The filename, directory name, or volume label syntax is incorrect. + err = ModuleFileDoesNotExistError + } + } + return nil, err + } + + defer f.Close() + // On some systems (e.g. plan9 and FreeBSD) it is possible to use the standard read() call on directories + // which means we cannot rely on read() returning an error, we have to do stat() instead. + if fi, err := f.Stat(); err == nil { + if fi.IsDir() { + return nil, ModuleFileDoesNotExistError + } + } else { + return nil, err + } + return io.ReadAll(f) +} + +func (r *Registry) getSource(p string) ([]byte, error) { + srcLoader := r.srcLoader + if srcLoader == nil { + srcLoader = DefaultSourceLoader + } + return srcLoader(p) +} + +func (r *Registry) getCompiledSource(p string) (*js.Program, error) { + r.Lock() + defer r.Unlock() + + prg := r.compiled[p] + if prg == nil { + buf, err := r.getSource(p) + if err != nil { + return nil, err + } + s := string(buf) + + if path.Ext(p) == ".json" { + s = "module.exports = JSON.parse('" + template.JSEscapeString(s) + "')" + } + + source := "(function(exports, require, module) {" + s + "\n})" + parsed, err := js.Parse(p, source, parser.WithSourceMapLoader(r.srcLoader)) + if err != nil { + return nil, err + } + prg, err = js.CompileAST(parsed, false) + if err == nil { + if r.compiled == nil { + r.compiled = make(map[string]*js.Program) + } + r.compiled[p] = prg + } + return prg, err + } + return prg, nil +} + +func (r *RequireModule) require(call js.FunctionCall) js.Value { + ret, err := r.Require(call.Argument(0).String()) + if err != nil { + if _, ok := err.(*js.Exception); !ok { + panic(r.runtime.NewGoError(err)) + } + panic(err) + } + return ret +} + +func filepathClean(p string) string { + return path.Clean(p) +} + +// Require can be used to import modules from Go source (similar to JS require() function). +func (r *RequireModule) Require(p string) (ret js.Value, err error) { + module, err := r.resolve(p) + if err != nil { + return + } + ret = module.Get("exports") + return +} + +func Require(runtime *js.Runtime, name string) js.Value { + if r, ok := js.AssertFunction(runtime.Get("require")); ok { + mod, err := r(js.Undefined(), runtime.ToValue(name)) + if err != nil { + panic(err) + } + return mod + } + panic(runtime.NewTypeError("Please enable require for this runtime using new(require.Registry).Enable(runtime)")) +} + +// RegisterNativeModule registers a module that isn't loaded through a SourceLoader, but rather through +// a provided ModuleLoader. Typically, this will be a module implemented in Go (although theoretically +// it can be anything, depending on the ModuleLoader implementation). +// Such modules take precedence over modules loaded through a SourceLoader, i.e. if a module name resolves as +// native, the native module is loaded, and the SourceLoader is not consulted. +// The binding is global and affects all instances of Registry. +// It should be called from a package init() function as it may not be used concurrently with require() calls. +// For registry-specific bindings see Registry.RegisterNativeModule. +func RegisterNativeModule(name string, loader ModuleLoader) { + if native == nil { + native = make(map[string]ModuleLoader) + } + name = filepathClean(name) + native[name] = loader +} + +// RegisterCoreModule registers a nodejs core module. If the name does not start with "node:", the module +// will also be loadable as "node:". Hence, for "builtin" modules (such as buffer, console, etc.) +// the name should not include the "node:" prefix, but for prefix-only core modules (such as "node:test") +// it should include the prefix. +func RegisterCoreModule(name string, loader ModuleLoader) { + if builtin == nil { + builtin = make(map[string]ModuleLoader) + } + name = filepathClean(name) + builtin[name] = loader +} diff --git a/goja_nodejs/require/module_test.go b/goja_nodejs/require/module_test.go new file mode 100644 index 0000000..5cfc3e6 --- /dev/null +++ b/goja_nodejs/require/module_test.go @@ -0,0 +1,534 @@ +package require + +import ( + "bytes" + "errors" + "fmt" + "io" + "os" + "path" + "path/filepath" + "runtime" + "testing" + + js "apigo.cc/ai/ai/goja" +) + +func mapFileSystemSourceLoader(files map[string]string) SourceLoader { + return func(path string) ([]byte, error) { + s, ok := files[path] + if !ok { + return nil, ModuleFileDoesNotExistError + } + return []byte(s), nil + } +} + +func TestRequireNativeModule(t *testing.T) { + const SCRIPT = ` + var m = require("test/m"); + m.test(); + ` + + vm := js.New() + + registry := new(Registry) + registry.Enable(vm) + + RegisterNativeModule("test/m", func(runtime *js.Runtime, module *js.Object) { + o := module.Get("exports").(*js.Object) + o.Set("test", func(call js.FunctionCall) js.Value { + return runtime.ToValue("passed") + }) + }) + + v, err := vm.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + + if !v.StrictEquals(vm.ToValue("passed")) { + t.Fatalf("Unexpected result: %v", v) + } +} + +func TestRegisterCoreModule(t *testing.T) { + vm := js.New() + + registry := new(Registry) + registry.Enable(vm) + + RegisterCoreModule("coremod", func(runtime *js.Runtime, module *js.Object) { + o := module.Get("exports").(*js.Object) + o.Set("test", func(call js.FunctionCall) js.Value { + return runtime.ToValue("passed") + }) + }) + + RegisterCoreModule("coremod1", func(runtime *js.Runtime, module *js.Object) { + o := module.Get("exports").(*js.Object) + o.Set("test", func(call js.FunctionCall) js.Value { + return runtime.ToValue("passed1") + }) + }) + + RegisterCoreModule("node:test1", func(runtime *js.Runtime, module *js.Object) { + o := module.Get("exports").(*js.Object) + o.Set("test", func(call js.FunctionCall) js.Value { + return runtime.ToValue("test1 passed") + }) + }) + + registry.RegisterNativeModule("bob", func(runtime *js.Runtime, module *js.Object) { + + }) + + _, err := vm.RunString(` + const m1 = require("coremod"); + const m2 = require("node:coremod"); + if (m1 !== m2) { + throw new Error("Modules are not equal"); + } + if (m1.test() !== "passed") { + throw new Error("m1.test() has failed"); + } + + const m3 = require("node:coremod1"); + const m4 = require("coremod1"); + if (m3 !== m4) { + throw new Error("Modules are not equal (1)"); + } + if (m3.test() !== "passed1") { + throw new Error("m3.test() has failed"); + } + + try { + require("node:bob"); + } catch (e) { + if (!e.message.includes("No such built-in module")) { + throw e; + } + } + require("bob"); + + try { + require("test1"); + throw new Error("Expected exception"); + } catch (e) { + if (!e.message.includes("Invalid module")) { + throw e; + } + } + + if (require("node:test1").test() !== "test1 passed") { + throw new Error("test1.test() has failed"); + } + `) + + if err != nil { + t.Fatal(err) + } +} + +func TestRequireRegistryNativeModule(t *testing.T) { + const SCRIPT = ` + var log = require("test/log"); + log.print('passed'); + ` + + logWithOutput := func(w io.Writer, prefix string) ModuleLoader { + return func(vm *js.Runtime, module *js.Object) { + o := module.Get("exports").(*js.Object) + o.Set("print", func(call js.FunctionCall) js.Value { + fmt.Fprint(w, prefix, call.Argument(0).String()) + return js.Undefined() + }) + } + } + + vm1 := js.New() + buf1 := &bytes.Buffer{} + + registry1 := new(Registry) + registry1.Enable(vm1) + + registry1.RegisterNativeModule("test/log", logWithOutput(buf1, "vm1 ")) + + vm2 := js.New() + buf2 := &bytes.Buffer{} + + registry2 := new(Registry) + registry2.Enable(vm2) + + registry2.RegisterNativeModule("test/log", logWithOutput(buf2, "vm2 ")) + + _, err := vm1.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + + s := buf1.String() + if s != "vm1 passed" { + t.Fatalf("vm1: Unexpected result: %q", s) + } + + _, err = vm2.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + + s = buf2.String() + if s != "vm2 passed" { + t.Fatalf("vm2: Unexpected result: %q", s) + } +} + +func TestRequire(t *testing.T) { + absPath, err := filepath.Abs("./testdata/m.js") + if err != nil { + t.Fatal(err) + } + + isWindows := runtime.GOOS == "windows" + + tests := []struct { + path string + ok bool + }{ + { + "./testdata/m.js", + true, + }, + { + "../require/testdata/m.js", + true, + }, + { + absPath, + true, + }, + { + `.\testdata\m.js`, + isWindows, + }, + { + `..\require\testdata\m.js`, + isWindows, + }, + } + + const SCRIPT = ` + var m = require(testPath); + m.test(); + ` + + for _, test := range tests { + t.Run(test.path, func(t *testing.T) { + vm := js.New() + vm.Set("testPath", test.path) + + registry := new(Registry) + registry.Enable(vm) + + v, err := vm.RunString(SCRIPT) + + ok := err == nil + + if ok != test.ok { + t.Fatalf("Expected ok to be %v, got %v (%v)", test.ok, ok, err) + } + + if !ok { + return + } + + if !v.StrictEquals(vm.ToValue("passed")) { + t.Fatalf("Unexpected result: %v", v) + } + }) + } +} + +func TestSourceLoader(t *testing.T) { + const SCRIPT = ` + var m = require("m.js"); + m.test(); + ` + + const MODULE = ` + function test() { + return "passed1"; + } + + exports.test = test; + ` + + vm := js.New() + + registry := NewRegistry(WithGlobalFolders("."), WithLoader(func(name string) ([]byte, error) { + if name == "m.js" { + return []byte(MODULE), nil + } + return nil, errors.New("Module does not exist") + })) + registry.Enable(vm) + + v, err := vm.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + + if !v.StrictEquals(vm.ToValue("passed1")) { + t.Fatalf("Unexpected result: %v", v) + } +} + +func TestStrictModule(t *testing.T) { + const SCRIPT = ` + var m = require("m.js"); + m.test(); + ` + + const MODULE = ` + "use strict"; + + function test() { + var a = "passed1"; + eval("var a = 'not passed'"); + return a; + } + + exports.test = test; + ` + + vm := js.New() + + registry := NewRegistry(WithGlobalFolders("."), WithLoader(func(name string) ([]byte, error) { + if name == "m.js" { + return []byte(MODULE), nil + } + return nil, errors.New("Module does not exist") + })) + registry.Enable(vm) + + v, err := vm.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + + if !v.StrictEquals(vm.ToValue("passed1")) { + t.Fatalf("Unexpected result: %v", v) + } +} + +func TestResolve(t *testing.T) { + testRequire := func(src, fpath string, globalFolders []string, fs map[string]string) (*js.Runtime, js.Value, error) { + vm := js.New() + r := NewRegistry(WithGlobalFolders(globalFolders...), WithLoader(mapFileSystemSourceLoader(fs))) + r.Enable(vm) + t.Logf("Require(%s)", fpath) + ret, err := vm.RunScript(path.Join(src, "test.js"), fmt.Sprintf("require('%s')", fpath)) + if err != nil { + return nil, nil, err + } + return vm, ret, nil + } + + globalFolders := []string{ + "/usr/lib/node_modules", + "/home/src/.node_modules", + } + + fs := map[string]string{ + "/home/src/app/app.js": `exports.name = "app"`, + "/home/src/app2/app2.json": `{"name": "app2"}`, + "/home/src/app3/index.js": `exports.name = "app3"`, + "/home/src/app4/index.json": `{"name": "app4"}`, + "/home/src/app5/package.json": `{"main": "app5.js"}`, + "/home/src/app5/app5.js": `exports.name = "app5"`, + "/home/src/app6/package.json": `{"main": "."}`, + "/home/src/app6/index.js": `exports.name = "app6"`, + "/home/src/app7/package.json": `{"main": "./a/b/c/file.js"}`, + "/home/src/app7/a/b/c/file.js": `exports.name = "app7"`, + "/usr/lib/node_modules/app8": `exports.name = "app8"`, + "/home/src/app9/app9.js": `exports.name = require('./a/file.js').name`, + "/home/src/app9/a/file.js": `exports.name = require('./b/file.js').name`, + "/home/src/app9/a/b/file.js": `exports.name = require('./c/file.js').name`, + "/home/src/app9/a/b/c/file.js": `exports.name = "app9"`, + "/home/src/.node_modules/app10": `exports.name = "app10"`, + "/home/src/app11/app11.js": `exports.name = require('d/file.js').name`, + "/home/src/app11/a/b/c/app11.js": `exports.name = require('d/file.js').name`, + "/home/src/app11/node_modules/d/file.js": `exports.name = "app11"`, + "/app12.js": `exports.name = require('a/file.js').name`, + "/node_modules/a/file.js": `exports.name = "app12"`, + "/app13/app13.js": `exports.name = require('b/file.js').name`, + "/node_modules/b/file.js": `exports.name = "app13"`, + "node_modules/app14/index.js": `exports.name = "app14"`, + "../node_modules/app15/index.js": `exports.name = "app15"`, + } + + for i, tc := range []struct { + src string + path string + ok bool + field string + value string + }{ + {"/home/src", "./app/app", true, "name", "app"}, + {"/home/src", "./app/app.js", true, "name", "app"}, + {"/home/src", "./app/bad.js", false, "", ""}, + {"/home/src", "./app2/app2", true, "name", "app2"}, + {"/home/src", "./app2/app2.json", true, "name", "app2"}, + {"/home/src", "./app/bad.json", false, "", ""}, + {"/home/src", "./app3", true, "name", "app3"}, + {"/home/src", "./appbad", false, "", ""}, + {"/home/src", "./app4", true, "name", "app4"}, + {"/home/src", "./appbad", false, "", ""}, + {"/home/src", "./app5", true, "name", "app5"}, + {"/home/src", "./app6", true, "name", "app6"}, + {"/home/src", "./app7", true, "name", "app7"}, + {"/home/src", "app8", true, "name", "app8"}, + {"/home/src", "./app9/app9", true, "name", "app9"}, + {"/home/src", "app10", true, "name", "app10"}, + {"/home/src", "./app11/app11.js", true, "name", "app11"}, + {"/home/src", "./app11/a/b/c/app11.js", true, "name", "app11"}, + {"/", "./app12", true, "name", "app12"}, + {"/", "./app13/app13", true, "name", "app13"}, + {".", "app14", true, "name", "app14"}, + {"..", "nonexistent", false, "", ""}, + } { + vm, mod, err := testRequire(tc.src, tc.path, globalFolders, fs) + if err != nil { + if tc.ok { + t.Errorf("%d: require() failed: %v", i, err) + } + continue + } else { + if !tc.ok { + t.Errorf("%d: expected to fail, but did not", i) + continue + } + } + f := mod.ToObject(vm).Get(tc.field) + if f == nil { + t.Errorf("%v: field %q not found", i, tc.field) + continue + } + value := f.String() + if value != tc.value { + t.Errorf("%v: got %q expected %q", i, value, tc.value) + } + } +} + +func TestRequireCycle(t *testing.T) { + vm := js.New() + r := NewRegistry(WithLoader(mapFileSystemSourceLoader(map[string]string{ + "a.js": `var b = require('./b.js'); exports.done = true;`, + "b.js": `var a = require('./a.js'); exports.done = true;`, + }))) + r.Enable(vm) + res, err := vm.RunString(` + var a = require('./a.js'); + var b = require('./b.js'); + a.done && b.done; + `) + if err != nil { + t.Fatal(err) + } + if v := res.Export(); v != true { + t.Fatalf("Unexpected result: %v", v) + } +} + +func TestErrorPropagation(t *testing.T) { + vm := js.New() + r := NewRegistry(WithLoader(mapFileSystemSourceLoader(map[string]string{ + "m.js": `throw 'test passed';`, + }))) + rr := r.Enable(vm) + _, err := rr.Require("./m") + if err == nil { + t.Fatal("Expected an error") + } + if ex, ok := err.(*js.Exception); ok { + if !ex.Value().StrictEquals(vm.ToValue("test passed")) { + t.Fatalf("Unexpected Exception: %v", ex) + } + } else { + t.Fatal(err) + } +} + +func TestSourceMapLoader(t *testing.T) { + vm := js.New() + r := NewRegistry(WithLoader(func(p string) ([]byte, error) { + switch p { + case "dir/m.js": + return []byte(`throw 'test passed'; +//# sourceMappingURL=m.js.map`), nil + case "dir/m.js.map": + return []byte(`{"version":3,"file":"m.js","sourceRoot":"","sources":["m.ts"],"names":[],"mappings":";AAAA"} +`), nil + } + return nil, ModuleFileDoesNotExistError + })) + + rr := r.Enable(vm) + _, err := rr.Require("./dir/m") + if err == nil { + t.Fatal("Expected an error") + } + if ex, ok := err.(*js.Exception); ok { + if !ex.Value().StrictEquals(vm.ToValue("test passed")) { + t.Fatalf("Unexpected Exception: %v", ex) + } + } else { + t.Fatal(err) + } +} + +func testsetup() (string, func(), error) { + name, err := os.MkdirTemp("", "goja-nodejs-require-test") + if err != nil { + return "", nil, err + } + return name, func() { + os.RemoveAll(name) + }, nil +} + +func TestDefaultModuleLoader(t *testing.T) { + workdir, teardown, err := testsetup() + if err != nil { + t.Fatal(err) + } + defer teardown() + + err = os.Chdir(workdir) + if err != nil { + t.Fatal(err) + } + err = os.Mkdir("module", 0755) + if err != nil { + t.Fatal(err) + } + err = os.WriteFile("module/index.js", []byte(`throw 'test passed';`), 0644) + if err != nil { + t.Fatal(err) + } + vm := js.New() + r := NewRegistry() + rr := r.Enable(vm) + _, err = rr.Require("./module") + if err == nil { + t.Fatal("Expected an error") + } + if ex, ok := err.(*js.Exception); ok { + if !ex.Value().StrictEquals(vm.ToValue("test passed")) { + t.Fatalf("Unexpected Exception: %v", ex) + } + } else { + t.Fatal(err) + } +} diff --git a/goja_nodejs/require/resolve.go b/goja_nodejs/require/resolve.go new file mode 100644 index 0000000..a7aec73 --- /dev/null +++ b/goja_nodejs/require/resolve.go @@ -0,0 +1,276 @@ +package require + +import ( + "encoding/json" + "errors" + "path" + "path/filepath" + "runtime" + "strings" + + js "apigo.cc/ai/ai/goja" +) + +const NodePrefix = "node:" + +// NodeJS module search algorithm described by +// https://nodejs.org/api/modules.html#modules_all_together +func (r *RequireModule) resolve(modpath string) (module *js.Object, err error) { + origPath, modpath := modpath, filepathClean(modpath) + if modpath == "" { + return nil, IllegalModuleNameError + } + + var start string + err = nil + if path.IsAbs(origPath) { + start = "/" + } else { + start = r.getCurrentModulePath() + } + + p := path.Join(start, modpath) + if isFileOrDirectoryPath(origPath) { + if module = r.modules[p]; module != nil { + return + } + module, err = r.loadAsFileOrDirectory(p) + if err == nil && module != nil { + r.modules[p] = module + } + } else { + module, err = r.loadNative(origPath) + if err == nil { + return + } else { + if err == InvalidModuleError { + err = nil + } else { + return + } + } + if module = r.nodeModules[p]; module != nil { + return + } + module, err = r.loadNodeModules(modpath, start) + if err == nil && module != nil { + r.nodeModules[p] = module + } + } + + if module == nil && err == nil { + err = InvalidModuleError + } + return +} + +func (r *RequireModule) loadNative(path string) (*js.Object, error) { + module := r.modules[path] + if module != nil { + return module, nil + } + + var ldr ModuleLoader + if ldr = r.r.native[path]; ldr == nil { + ldr = native[path] + } + + var isBuiltIn, withPrefix bool + if ldr == nil { + ldr = builtin[path] + if ldr == nil && strings.HasPrefix(path, NodePrefix) { + ldr = builtin[path[len(NodePrefix):]] + if ldr == nil { + return nil, NoSuchBuiltInModuleError + } + withPrefix = true + } + isBuiltIn = true + } + + if ldr != nil { + module = r.createModuleObject() + r.modules[path] = module + if isBuiltIn { + if withPrefix { + r.modules[path[len(NodePrefix):]] = module + } else { + if !strings.HasPrefix(path, NodePrefix) { + r.modules[NodePrefix+path] = module + } + } + } + ldr(r.runtime, module) + return module, nil + } + + return nil, InvalidModuleError +} + +func (r *RequireModule) loadAsFileOrDirectory(path string) (module *js.Object, err error) { + if module, err = r.loadAsFile(path); module != nil || err != nil { + return + } + + return r.loadAsDirectory(path) +} + +func (r *RequireModule) loadAsFile(path string) (module *js.Object, err error) { + if module, err = r.loadModule(path); module != nil || err != nil { + return + } + + p := path + ".js" + if module, err = r.loadModule(p); module != nil || err != nil { + return + } + + p = path + ".json" + return r.loadModule(p) +} + +func (r *RequireModule) loadIndex(modpath string) (module *js.Object, err error) { + p := path.Join(modpath, "index.js") + if module, err = r.loadModule(p); module != nil || err != nil { + return + } + + p = path.Join(modpath, "index.json") + return r.loadModule(p) +} + +func (r *RequireModule) loadAsDirectory(modpath string) (module *js.Object, err error) { + p := path.Join(modpath, "package.json") + buf, err := r.r.getSource(p) + if err != nil { + return r.loadIndex(modpath) + } + var pkg struct { + Main string + } + err = json.Unmarshal(buf, &pkg) + if err != nil || len(pkg.Main) == 0 { + return r.loadIndex(modpath) + } + + m := path.Join(modpath, pkg.Main) + if module, err = r.loadAsFile(m); module != nil || err != nil { + return + } + + return r.loadIndex(m) +} + +func (r *RequireModule) loadNodeModule(modpath, start string) (*js.Object, error) { + return r.loadAsFileOrDirectory(path.Join(start, modpath)) +} + +func (r *RequireModule) loadNodeModules(modpath, start string) (module *js.Object, err error) { + for _, dir := range r.r.globalFolders { + if module, err = r.loadNodeModule(modpath, dir); module != nil || err != nil { + return + } + } + for { + var p string + if path.Base(start) != "node_modules" { + p = path.Join(start, "node_modules") + } else { + p = start + } + if module, err = r.loadNodeModule(modpath, p); module != nil || err != nil { + return + } + if start == ".." { // Dir('..') is '.' + break + } + parent := path.Dir(start) + if parent == start { + break + } + start = parent + } + + return +} + +func (r *RequireModule) getCurrentModulePath() string { + var buf [2]js.StackFrame + frames := r.runtime.CaptureCallStack(2, buf[:0]) + if len(frames) < 2 { + return "." + } + return path.Dir(frames[1].SrcName()) +} + +func (r *RequireModule) createModuleObject() *js.Object { + module := r.runtime.NewObject() + module.Set("exports", r.runtime.NewObject()) + return module +} + +func (r *RequireModule) loadModule(path string) (*js.Object, error) { + module := r.modules[path] + if module == nil { + module = r.createModuleObject() + r.modules[path] = module + err := r.loadModuleFile(path, module) + if err != nil { + module = nil + delete(r.modules, path) + if errors.Is(err, ModuleFileDoesNotExistError) { + err = nil + } + } + return module, err + } + return module, nil +} + +func (r *RequireModule) loadModuleFile(path string, jsModule *js.Object) error { + + prg, err := r.r.getCompiledSource(path) + + if err != nil { + return err + } + + f, err := r.runtime.RunProgram(prg) + if err != nil { + return err + } + + if call, ok := js.AssertFunction(f); ok { + jsExports := jsModule.Get("exports") + jsRequire := r.runtime.Get("require") + + // Run the module source, with "jsExports" as "this", + // "jsExports" as the "exports" variable, "jsRequire" + // as the "require" variable and "jsModule" as the + // "module" variable (Nodejs capable). + _, err = call(jsExports, jsExports, jsRequire, jsModule) + if err != nil { + return err + } + } else { + return InvalidModuleError + } + + return nil +} + +func isFileOrDirectoryPath(path string) bool { + result := path == "." || path == ".." || + strings.HasPrefix(path, "/") || + strings.HasPrefix(path, "./") || + strings.HasPrefix(path, "../") + + if runtime.GOOS == "windows" { + result = result || + strings.HasPrefix(path, `.\`) || + strings.HasPrefix(path, `..\`) || + filepath.IsAbs(path) + } + + return result +} diff --git a/goja_nodejs/require/testdata/m.js b/goja_nodejs/require/testdata/m.js new file mode 100644 index 0000000..97d9995 --- /dev/null +++ b/goja_nodejs/require/testdata/m.js @@ -0,0 +1,7 @@ +function test() { + return "passed"; +} + +module.exports = { + test: test +} diff --git a/goja_nodejs/staticcheck.conf b/goja_nodejs/staticcheck.conf new file mode 100644 index 0000000..2441b9d --- /dev/null +++ b/goja_nodejs/staticcheck.conf @@ -0,0 +1 @@ +checks = ["all", "-ST1000", "-ST1003", "-ST1005", "-ST1006", "-ST1012", "-ST1021", "-ST1020", "-ST1008"] diff --git a/goja_nodejs/url/escape.go b/goja_nodejs/url/escape.go new file mode 100644 index 0000000..3d288c2 --- /dev/null +++ b/goja_nodejs/url/escape.go @@ -0,0 +1,134 @@ +package url + +import "strings" + +var tblEscapeURLQuery = [128]byte{ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, +} + +var tblEscapeURLQueryParam = [128]byte{ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, +} + +// The code below is mostly borrowed from the standard Go url package + +const upperhex = "0123456789ABCDEF" + +func ishex(c byte) bool { + switch { + case '0' <= c && c <= '9': + return true + case 'a' <= c && c <= 'f': + return true + case 'A' <= c && c <= 'F': + return true + } + return false +} + +func unhex(c byte) byte { + switch { + case '0' <= c && c <= '9': + return c - '0' + case 'a' <= c && c <= 'f': + return c - 'a' + 10 + case 'A' <= c && c <= 'F': + return c - 'A' + 10 + } + return 0 +} + +func escape(s string, table *[128]byte, spaceToPlus bool) string { + spaceCount, hexCount := 0, 0 + for i := 0; i < len(s); i++ { + c := s[i] + if c > 127 || table[c] == 0 { + if c == ' ' && spaceToPlus { + spaceCount++ + } else { + hexCount++ + } + } + } + + if spaceCount == 0 && hexCount == 0 { + return s + } + + var sb strings.Builder + hexBuf := [3]byte{'%', 0, 0} + + sb.Grow(len(s) + 2*hexCount) + + for i := 0; i < len(s); i++ { + switch c := s[i]; { + case c == ' ' && spaceToPlus: + sb.WriteByte('+') + case c > 127 || table[c] == 0: + hexBuf[1] = upperhex[c>>4] + hexBuf[2] = upperhex[c&15] + sb.Write(hexBuf[:]) + default: + sb.WriteByte(c) + } + } + return sb.String() +} + +func unescapeSearchParam(s string) string { + n := 0 + hasPlus := false + for i := 0; i < len(s); { + switch s[i] { + case '%': + if i+2 >= len(s) || !ishex(s[i+1]) || !ishex(s[i+2]) { + i++ + continue + } + n++ + i += 3 + case '+': + hasPlus = true + i++ + default: + i++ + } + } + + if n == 0 && !hasPlus { + return s + } + + var t strings.Builder + t.Grow(len(s) - 2*n) + for i := 0; i < len(s); i++ { + switch s[i] { + case '%': + if i+2 >= len(s) || !ishex(s[i+1]) || !ishex(s[i+2]) { + t.WriteByte('%') + } else { + t.WriteByte(unhex(s[i+1])<<4 | unhex(s[i+2])) + i += 2 + } + case '+': + t.WriteByte(' ') + default: + t.WriteByte(s[i]) + } + } + return t.String() +} diff --git a/goja_nodejs/url/module.go b/goja_nodejs/url/module.go new file mode 100644 index 0000000..ca39ff6 --- /dev/null +++ b/goja_nodejs/url/module.go @@ -0,0 +1,36 @@ +package url + +import ( + "apigo.cc/ai/ai/goja" + "apigo.cc/ai/ai/goja_nodejs/require" +) + +const ModuleName = "url" + +type urlModule struct { + r *goja.Runtime + + URLSearchParamsPrototype *goja.Object + URLSearchParamsIteratorPrototype *goja.Object +} + +func Require(runtime *goja.Runtime, module *goja.Object) { + exports := module.Get("exports").(*goja.Object) + m := &urlModule{ + r: runtime, + } + exports.Set("URL", m.createURLConstructor()) + exports.Set("URLSearchParams", m.createURLSearchParamsConstructor()) + exports.Set("domainToASCII", m.domainToASCII) + exports.Set("domainToUnicode", m.domainToUnicode) +} + +func Enable(runtime *goja.Runtime) { + m := require.Require(runtime, ModuleName).ToObject(runtime) + runtime.Set("URL", m.Get("URL")) + runtime.Set("URLSearchParams", m.Get("URLSearchParams")) +} + +func init() { + require.RegisterCoreModule(ModuleName, Require) +} diff --git a/goja_nodejs/url/nodeurl.go b/goja_nodejs/url/nodeurl.go new file mode 100644 index 0000000..538cccf --- /dev/null +++ b/goja_nodejs/url/nodeurl.go @@ -0,0 +1,148 @@ +package url + +import ( + "net/url" + "strings" +) + +type searchParam struct { + name string + value string +} + +func (sp *searchParam) Encode() string { + return sp.string(true) +} + +func escapeSearchParam(s string) string { + return escape(s, &tblEscapeURLQueryParam, true) +} + +func (sp *searchParam) string(encode bool) string { + if encode { + return escapeSearchParam(sp.name) + "=" + escapeSearchParam(sp.value) + } else { + return sp.name + "=" + sp.value + } +} + +type searchParams []searchParam + +func (s searchParams) Len() int { + return len(s) +} + +func (s searchParams) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +func (s searchParams) Less(i, j int) bool { + return strings.Compare(s[i].name, s[j].name) < 0 +} + +func (s searchParams) Encode() string { + var sb strings.Builder + for i, v := range s { + if i > 0 { + sb.WriteByte('&') + } + sb.WriteString(v.Encode()) + } + return sb.String() +} + +func (s searchParams) String() string { + var sb strings.Builder + for i, v := range s { + if i > 0 { + sb.WriteByte('&') + } + sb.WriteString(v.string(false)) + } + return sb.String() +} + +type nodeURL struct { + url *url.URL + searchParams searchParams +} + +type urlSearchParams nodeURL + +// This methods ensures that the url.URL has the proper RawQuery based on the searchParam +// structs. If a change is made to the searchParams we need to keep them in sync. +func (nu *nodeURL) syncSearchParams() { + if nu.rawQueryUpdateNeeded() { + nu.url.RawQuery = nu.searchParams.Encode() + } +} + +func (nu *nodeURL) rawQueryUpdateNeeded() bool { + return len(nu.searchParams) > 0 && nu.url.RawQuery == "" +} + +func (nu *nodeURL) String() string { + return nu.url.String() +} + +func (sp *urlSearchParams) hasName(name string) bool { + for _, v := range sp.searchParams { + if v.name == name { + return true + } + } + return false +} + +func (sp *urlSearchParams) hasValue(name, value string) bool { + for _, v := range sp.searchParams { + if v.name == name && v.value == value { + return true + } + } + return false +} + +func (sp *urlSearchParams) getValues(name string) []string { + vals := make([]string, 0, len(sp.searchParams)) + for _, v := range sp.searchParams { + if v.name == name { + vals = append(vals, v.value) + } + } + + return vals +} + +func (sp *urlSearchParams) getFirstValue(name string) (string, bool) { + for _, v := range sp.searchParams { + if v.name == name { + return v.value, true + } + } + + return "", false +} + +func parseSearchQuery(query string) (ret searchParams) { + if query == "" { + return + } + + query = strings.TrimPrefix(query, "?") + + for _, v := range strings.Split(query, "&") { + if v == "" { + continue + } + pair := strings.SplitN(v, "=", 2) + l := len(pair) + if l == 1 { + ret = append(ret, searchParam{name: unescapeSearchParam(pair[0]), value: ""}) + } else if l == 2 { + ret = append(ret, searchParam{name: unescapeSearchParam(pair[0]), value: unescapeSearchParam(pair[1])}) + } + } + + return +} diff --git a/goja_nodejs/url/testdata/url_search_params.js b/goja_nodejs/url/testdata/url_search_params.js new file mode 100644 index 0000000..97ec5c9 --- /dev/null +++ b/goja_nodejs/url/testdata/url_search_params.js @@ -0,0 +1,385 @@ +"use strict"; + +const assert = require("../../assert.js"); + +let params; + +function testCtor(value, expected) { + assert.sameValue(new URLSearchParams(value).toString(), expected); +} + +testCtor("user=abc&query=xyz", "user=abc&query=xyz"); +testCtor("?user=abc&query=xyz", "user=abc&query=xyz"); + +testCtor( + { + num: 1, + user: "abc", + query: ["first", "second"], + obj: { prop: "value" }, + b: true, + }, + "num=1&user=abc&query=first%2Csecond&obj=%5Bobject+Object%5D&b=true" +); + +const map = new Map(); +map.set("user", "abc"); +map.set("query", "xyz"); +testCtor(map, "user=abc&query=xyz"); + +testCtor( + [ + ["user", "abc"], + ["query", "first"], + ["query", "second"], + ], + "user=abc&query=first&query=second" +); + +// Each key-value pair must have exactly two elements +assert.throwsNodeError(() => new URLSearchParams([["single_value"]]), TypeError, "ERR_INVALID_TUPLE"); +assert.throwsNodeError(() => new URLSearchParams([["too", "many", "values"]]), TypeError, "ERR_INVALID_TUPLE"); + +params = new URLSearchParams("a=b&cc=d"); +params.forEach((value, name, searchParams) => { + if (name === "a") { + assert.sameValue(value, "b"); + } + if (name === "cc") { + assert.sameValue(value, "d"); + } + assert.sameValue(searchParams, params); +}); + +params.forEach((value, name, searchParams) => { + if (name === "a") { + assert.sameValue(value, "b"); + searchParams.set("cc", "d1"); + } + if (name === "cc") { + assert.sameValue(value, "d1"); + } + assert.sameValue(searchParams, params); +}); + +assert.throwsNodeError(() => params.forEach(123), TypeError, "ERR_INVALID_ARG_TYPE"); + +assert.throwsNodeError(() => params.forEach.call(1, 2), TypeError, "ERR_INVALID_THIS"); + +params = new URLSearchParams("a=1=2&b=3"); +assert.sameValue(params.size, 2); +assert.sameValue(params.get("a"), "1=2"); +assert.sameValue(params.get("b"), "3"); + +params = new URLSearchParams("&"); +assert.sameValue(params.size, 0); + +params = new URLSearchParams("& "); +assert.sameValue(params.size, 1); +assert.sameValue(params.get(" "), ""); + +params = new URLSearchParams(" &"); +assert.sameValue(params.size, 1); +assert.sameValue(params.get(" "), ""); + +params = new URLSearchParams("="); +assert.sameValue(params.size, 1); +assert.sameValue(params.get(""), ""); + +params = new URLSearchParams("&=2"); +assert.sameValue(params.size, 1); +assert.sameValue(params.get(""), "2"); + +params = new URLSearchParams("?user=abc"); +assert.throwsNodeError(() => params.append(), TypeError, "ERR_MISSING_ARGS"); +params.append("query", "first"); +assert.sameValue(params.toString(), "user=abc&query=first"); + +params = new URLSearchParams("first=one&second=two&third=three"); +assert.throwsNodeError(() => params.delete(), TypeError, "ERR_MISSING_ARGS"); +params.delete("second", "fake-value"); +assert.sameValue(params.toString(), "first=one&second=two&third=three"); +params.delete("third", "three"); +assert.sameValue(params.toString(), "first=one&second=two"); +params.delete("second"); +assert.sameValue(params.toString(), "first=one"); + +params = new URLSearchParams("user=abc&query=xyz"); +assert.throwsNodeError(() => params.get(), TypeError, "ERR_MISSING_ARGS"); +assert.sameValue(params.get("user"), "abc"); +assert.sameValue(params.get("non-existant"), null); + +params = new URLSearchParams("query=first&query=second"); +assert.throwsNodeError(() => params.getAll(), TypeError, "ERR_MISSING_ARGS"); +const all = params.getAll("query"); +assert.sameValue(all.includes("first"), true); +assert.sameValue(all.includes("second"), true); +assert.sameValue(all.length, 2); +const getAllUndefined = params.getAll(undefined); +assert.sameValue(getAllUndefined.length, 0); +const getAllNonExistant = params.getAll("does_not_exists"); +assert.sameValue(getAllNonExistant.length, 0); + +params = new URLSearchParams("user=abc&query=xyz"); +assert.throwsNodeError(() => params.has(), TypeError, "ERR_MISSING_ARGS"); +assert.sameValue(params.has(undefined), false); +assert.sameValue(params.has("user"), true); +assert.sameValue(params.has("user", "abc"), true); +assert.sameValue(params.has("user", "abc", "extra-param"), true); +assert.sameValue(params.has("user", "efg"), false); +assert.sameValue(params.has("user", undefined), true); + +params = new URLSearchParams(); +params.append("foo", "bar"); +params.append("foo", "baz"); +params.append("abc", "def"); +assert.sameValue(params.toString(), "foo=bar&foo=baz&abc=def"); +params.set("foo", "def"); +params.set("xyz", "opq"); +assert.sameValue(params.toString(), "foo=def&abc=def&xyz=opq"); + +params = new URLSearchParams("query=first&query=second&user=abc&double=first,second"); +const URLSearchIteratorPrototype = params.entries().__proto__; +assert.sameValue(typeof URLSearchIteratorPrototype, "object"); + +assert.sameValue(params[Symbol.iterator], params.entries); + +{ + const entries = params.entries(); + assert.sameValue(entries.toString(), "[object URLSearchParams Iterator]"); + assert.sameValue(entries.__proto__, URLSearchIteratorPrototype); + + let item = entries.next(); + assert.sameValue(item.value.toString(), ["query", "first"].toString()); + assert.sameValue(item.done, false); + + item = entries.next(); + assert.sameValue(item.value.toString(), ["query", "second"].toString()); + assert.sameValue(item.done, false); + + item = entries.next(); + assert.sameValue(item.value.toString(), ["user", "abc"].toString()); + assert.sameValue(item.done, false); + + item = entries.next(); + assert.sameValue(item.value.toString(), ["double", "first,second"].toString()); + assert.sameValue(item.done, false); + + item = entries.next(); + assert.sameValue(item.value, undefined); + assert.sameValue(item.done, true); +} + +params = new URLSearchParams("query=first&query=second&user=abc"); +{ + const keys = params.keys(); + assert.sameValue(keys.__proto__, URLSearchIteratorPrototype); + + let item = keys.next(); + assert.sameValue(item.value, "query"); + assert.sameValue(item.done, false); + + item = keys.next(); + assert.sameValue(item.value, "query"); + assert.sameValue(item.done, false); + + item = keys.next(); + assert.sameValue(item.value, "user"); + assert.sameValue(item.done, false); + + item = keys.next(); + assert.sameValue(item.value, undefined); + assert.sameValue(item.done, true); +} + +params = new URLSearchParams("query=first&query=second&user=abc"); +{ + const values = params.values(); + assert.sameValue(values.__proto__, URLSearchIteratorPrototype); + + let item = values.next(); + assert.sameValue(item.value, "first"); + assert.sameValue(item.done, false); + + item = values.next(); + assert.sameValue(item.value, "second"); + assert.sameValue(item.done, false); + + item = values.next(); + assert.sameValue(item.value, "abc"); + assert.sameValue(item.done, false); + + item = values.next(); + assert.sameValue(item.value, undefined); + assert.sameValue(item.done, true); +} + + +params = new URLSearchParams("query[]=abc&type=search&query[]=123"); +params.sort(); +assert.sameValue(params.toString(), "query%5B%5D=abc&query%5B%5D=123&type=search"); + +params = new URLSearchParams("query=first&query=second&user=abc"); +assert.sameValue(params.size, 3); + +params = new URLSearchParams("%"); +assert.sameValue(params.has("%"), true); +assert.sameValue(params.toString(), "%25="); + +{ + const params = new URLSearchParams(""); + assert.sameValue(params.size, 0); + assert.sameValue(params.toString(), ""); + assert.sameValue(params.get(undefined), null); + params.set(undefined, true); + assert.sameValue(params.has(undefined), true); + assert.sameValue(params.has("undefined"), true); + assert.sameValue(params.get("undefined"), "true"); + assert.sameValue(params.get(undefined), "true"); + assert.sameValue(params.getAll(undefined).toString(), ["true"].toString()); + params.delete(undefined); + assert.sameValue(params.has(undefined), false); + assert.sameValue(params.has("undefined"), false); + + assert.sameValue(params.has(null), false); + params.set(null, "nullval"); + assert.sameValue(params.has(null), true); + assert.sameValue(params.has("null"), true); + assert.sameValue(params.get(null), "nullval"); + assert.sameValue(params.get("null"), "nullval"); + params.delete(null); + assert.sameValue(params.has(null), false); + assert.sameValue(params.has("null"), false); +} + +function* functionGeneratorExample() { + yield ["user", "abc"]; + yield ["query", "first"]; + yield ["query", "second"]; +} + +params = new URLSearchParams(functionGeneratorExample()); +assert.sameValue(params.toString(), "user=abc&query=first&query=second"); + +assert.sameValue(params.__proto__.constructor, URLSearchParams); +assert.sameValue(params instanceof URLSearchParams, true); + +{ + const params = new URLSearchParams("1=2&1=3"); + assert.sameValue(params.get(1), "2"); + assert.sameValue(params.getAll(1).toString(), ["2", "3"].toString()); + assert.sameValue(params.getAll("x").toString(), [].toString()); +} + +// Sync +{ + const url = new URL("https://test.com/"); + const params = url.searchParams; + assert.sameValue(params.size, 0); + url.search = "a=1"; + assert.sameValue(params.size, 1); + assert.sameValue(params.get("a"), "1"); +} + +{ + const url = new URL("https://test.com/?a=1"); + const params = url.searchParams; + assert.sameValue(params.size, 1); + url.search = ""; + assert.sameValue(params.size, 0); + url.search = "b=2"; + assert.sameValue(params.size, 1); +} + +{ + const url = new URL("https://test.com/"); + const params = url.searchParams; + params.append("a", "1"); + assert.sameValue(url.toString(), "https://test.com/?a=1"); +} + +{ + const url = new URL("https://test.com/"); + url.searchParams.append("a", "1"); + url.searchParams.append("b", "1"); + assert.sameValue(url.toString(), "https://test.com/?a=1&b=1"); +} + +{ + const url = new URL("https://test.com/"); + const params = url.searchParams; + url.searchParams.append("a", "1"); + assert.sameValue(url.search, "?a=1"); +} + +{ + const url = new URL("https://test.com/?a=1"); + const params = url.searchParams; + params.append("a", "2"); + assert.sameValue(url.search, "?a=1&a=2"); +} + +{ + const url = new URL("https://test.com/"); + const params = url.searchParams; + params.set("a", "1"); + assert.sameValue(url.search, "?a=1"); +} + +{ + const url = new URL("https://test.com/"); + url.searchParams.set("a", "1"); + url.searchParams.set("b", "1"); + assert.sameValue(url.toString(), "https://test.com/?a=1&b=1"); +} + +{ + const url = new URL("https://test.com/?a=1&b=2"); + const params = url.searchParams; + params.delete("a"); + assert.sameValue(url.search, "?b=2"); +} + +{ + const url = new URL("https://test.com/?b=2&a=1"); + const params = url.searchParams; + params.sort(); + assert.sameValue(url.search, "?a=1&b=2"); +} + +{ + const url = new URL("https://test.com/?a=1"); + const params = url.searchParams; + params.delete("a"); + assert.sameValue(url.search, ""); + + params.set("a", 2); + assert.sameValue(url.search, "?a=2"); +} + +// FAILING: no custom properties on wrapped Go structs +/* +{ + const params = new URLSearchParams(""); + assert.sameValue(Object.isExtensible(params), true); + assert.sameValue(Reflect.defineProperty(params, "customField", {value: 42, configurable: true}), true); + assert.sameValue(params.customField, 42); + const desc = Reflect.getOwnPropertyDescriptor(params, "customField"); + assert.sameValue(desc.value, 42); + assert.sameValue(desc.writable, false); + assert.sameValue(desc.enumerable, false); + assert.sameValue(desc.configurable, true); +} +*/ + +// Escape +{ + const myURL = new URL('https://example.org/abc?fo~o=~ba r%z'); + + assert.sameValue(myURL.search, "?fo~o=~ba%20r%z"); + + // Modify the URL via searchParams... + myURL.searchParams.sort(); + + assert.sameValue(myURL.search, "?fo%7Eo=%7Eba+r%25z"); +} diff --git a/goja_nodejs/url/testdata/url_test.js b/goja_nodejs/url/testdata/url_test.js new file mode 100644 index 0000000..8410060 --- /dev/null +++ b/goja_nodejs/url/testdata/url_test.js @@ -0,0 +1,229 @@ +"use strict"; + +const assert = require("../../assert.js"); + +function testURLCtor(str, expected) { + assert.sameValue(new URL(str).toString(), expected); +} + +function testURLCtorBase(ref, base, expected, message) { + assert.sameValue(new URL(ref, base).toString(), expected, message); +} + +testURLCtorBase("https://example.org/", undefined, "https://example.org/"); +testURLCtorBase("/foo", "https://example.org/", "https://example.org/foo"); +testURLCtorBase("http://Example.com/", "https://example.org/", "http://example.com/"); +testURLCtorBase("https://Example.com/", "https://example.org/", "https://example.com/"); +testURLCtorBase("foo://Example.com/", "https://example.org/", "foo://Example.com/"); +testURLCtorBase("foo:Example.com/", "https://example.org/", "foo:Example.com/"); +testURLCtorBase("#hash", "https://example.org/", "https://example.org/#hash"); + +testURLCtor("HTTP://test.com", "http://test.com/"); +testURLCtor("HTTPS://á.com", "https://xn--1ca.com/"); +testURLCtor("HTTPS://á.com:123", "https://xn--1ca.com:123/"); +testURLCtor("https://test.com#asdfá", "https://test.com/#asdf%C3%A1"); +testURLCtor("HTTPS://á.com:123/á", "https://xn--1ca.com:123/%C3%A1"); +testURLCtor("fish://á.com", "fish://%C3%A1.com"); +testURLCtor("https://test.com/?a=1 /2", "https://test.com/?a=1%20/2"); +testURLCtor("https://test.com/á=1?á=1&ü=2#é", "https://test.com/%C3%A1=1?%C3%A1=1&%C3%BC=2#%C3%A9"); + +assert.throws(() => new URL("test"), TypeError); +assert.throws(() => new URL("ssh://EEE:ddd"), TypeError); + +{ + let u = new URL("https://example.org/"); + assert.sameValue(u.__proto__.constructor, URL); + assert.sameValue(u instanceof URL, true); +} + +{ + let u = new URL("https://example.org/"); + assert.sameValue(u.searchParams, u.searchParams); +} + +let myURL; + +// Hash +myURL = new URL("https://example.org/foo#bar"); +myURL.hash = "baz"; +assert.sameValue(myURL.href, "https://example.org/foo#baz"); + +myURL.hash = "#baz"; +assert.sameValue(myURL.href, "https://example.org/foo#baz"); + +myURL.hash = "#á=1 2"; +assert.sameValue(myURL.href, "https://example.org/foo#%C3%A1=1%202"); + +myURL.hash = "#a/#b"; +// FAILING: the second # gets escaped +//assert.sameValue(myURL.href, "https://example.org/foo#a/#b"); +assert.sameValue(myURL.search, ""); +// FAILING: the second # gets escaped +//assert.sameValue(myURL.hash, "#a/#b"); + +// Host +myURL = new URL("https://example.org:81/foo"); +myURL.host = "example.com:82"; +assert.sameValue(myURL.href, "https://example.com:82/foo"); + +// Hostname +myURL = new URL("https://example.org:81/foo"); +myURL.hostname = "example.com:82"; +assert.sameValue(myURL.href, "https://example.org:81/foo"); + +myURL.hostname = "á.com"; +assert.sameValue(myURL.href, "https://xn--1ca.com:81/foo"); + +// href +myURL = new URL("https://example.org/foo"); +myURL.href = "https://example.com/bar"; +assert.sameValue(myURL.href, "https://example.com/bar"); + +// Password +myURL = new URL("https://abc:xyz@example.com"); +myURL.password = "123"; +assert.sameValue(myURL.href, "https://abc:123@example.com/"); + +// pathname +myURL = new URL("https://example.org/abc/xyz?123"); +myURL.pathname = "/abcdef"; +assert.sameValue(myURL.href, "https://example.org/abcdef?123"); + +myURL.pathname = ""; +assert.sameValue(myURL.href, "https://example.org/?123"); + +myURL.pathname = "á"; +assert.sameValue(myURL.pathname, "/%C3%A1"); +assert.sameValue(myURL.href, "https://example.org/%C3%A1?123"); + +// port + +myURL = new URL("https://example.org:8888"); +assert.sameValue(myURL.port, "8888"); + +function testSetPort(port, expected) { + const url = new URL("https://example.org:8888"); + url.port = port; + assert.sameValue(url.port, expected); +} + +testSetPort(0, "0"); +testSetPort(-0, "0"); + +// Default ports are automatically transformed to the empty string +// (HTTPS protocol's default port is 443) +testSetPort("443", ""); +testSetPort(443, ""); + +// Empty string is the same as default port +testSetPort("", ""); + +// Completely invalid port strings are ignored +testSetPort("abcd", "8888"); +testSetPort("-123", ""); +testSetPort(-123, ""); +testSetPort(-123.45, ""); +testSetPort(undefined, "8888"); +testSetPort(null, "8888"); +testSetPort(+Infinity, "8888"); +testSetPort(-Infinity, "8888"); +testSetPort(NaN, "8888"); + +// Leading numbers are treated as a port number +testSetPort("5678abcd", "5678"); +testSetPort("a5678abcd", ""); + +// Non-integers are truncated +testSetPort(1234.5678, "1234"); + +// Out-of-range numbers which are not represented in scientific notation +// will be ignored. +testSetPort(1e10, "8888"); +testSetPort("123456", "8888"); +testSetPort(123456, "8888"); +testSetPort(4.567e21, "4"); + +// toString() takes precedence over valueOf(), even if it returns a valid integer +testSetPort( + { + toString() { + return "2"; + }, + valueOf() { + return 1; + }, + }, + "2" +); + +// Protocol +function testSetProtocol(url, protocol, expected) { + url.protocol = protocol; + assert.sameValue(url.protocol, expected); +} +testSetProtocol(new URL("https://example.org"), "ftp", "ftp:"); +testSetProtocol(new URL("https://example.org"), "ftp:", "ftp:"); +testSetProtocol(new URL("https://example.org"), "FTP:", "ftp:"); +testSetProtocol(new URL("https://example.org"), "ftp: blah", "ftp:"); +// special to non-special +testSetProtocol(new URL("https://example.org"), "foo", "https:"); +// non-special to special +testSetProtocol(new URL("fish://example.org"), "https", "fish:"); + +// Search +myURL = new URL("https://example.org/abc?123"); +myURL.search = "abc=xyz"; +assert.sameValue(myURL.href, "https://example.org/abc?abc=xyz"); + +myURL.search = "a=1 2"; +assert.sameValue(myURL.href, "https://example.org/abc?a=1%202"); + +myURL.search = "á=ú"; +assert.sameValue(myURL.search, "?%C3%A1=%C3%BA"); +assert.sameValue(myURL.href, "https://example.org/abc?%C3%A1=%C3%BA"); + +myURL.hash = "hash"; +myURL.search = "a=#b"; +assert.sameValue(myURL.href, "https://example.org/abc?a=%23b#hash"); +assert.sameValue(myURL.search, "?a=%23b"); +assert.sameValue(myURL.hash, "#hash"); + +// Username +myURL = new URL("https://abc:xyz@example.com/"); +myURL.username = "123"; +assert.sameValue(myURL.href, "https://123:xyz@example.com/"); + +// Origin, read-only +assert.throws(() => { + myURL.origin = "abc"; +}, TypeError); + +// href +myURL = new URL("https://example.org"); +myURL.href = "https://example.com"; +assert.sameValue(myURL.href, "https://example.com/"); + +assert.throws(() => { + myURL.href = "test"; +}, TypeError); + +// Search Params +myURL = new URL("https://example.com/"); +myURL.searchParams.append("user", "abc"); +assert.sameValue(myURL.toString(), "https://example.com/?user=abc"); +myURL.searchParams.append("first", "one"); +assert.sameValue(myURL.toString(), "https://example.com/?user=abc&first=one"); +myURL.searchParams.delete("user"); +assert.sameValue(myURL.toString(), "https://example.com/?first=one"); + +{ + const url = require("url"); + + assert.sameValue(url.domainToASCII('español.com'), "xn--espaol-zwa.com"); + assert.sameValue(url.domainToASCII('中文.com'), "xn--fiq228c.com"); + assert.sameValue(url.domainToASCII('xn--iñvalid.com'), ""); + + assert.sameValue(url.domainToUnicode('xn--espaol-zwa.com'), "español.com"); + assert.sameValue(url.domainToUnicode('xn--fiq228c.com'), "中文.com"); + assert.sameValue(url.domainToUnicode('xn--iñvalid.com'), ""); +} diff --git a/goja_nodejs/url/url.go b/goja_nodejs/url/url.go new file mode 100644 index 0000000..36ea5d8 --- /dev/null +++ b/goja_nodejs/url/url.go @@ -0,0 +1,407 @@ +package url + +import ( + "math" + "net/url" + "reflect" + "strconv" + "strings" + + "apigo.cc/ai/ai/goja" + "apigo.cc/ai/ai/goja_nodejs/errors" + + "golang.org/x/net/idna" +) + +const ( + URLNotAbsolute = "URL is not absolute" + InvalidURL = "Invalid URL" + InvalidBaseURL = "Invalid base URL" + InvalidHostname = "Invalid hostname" +) + +var ( + reflectTypeURL = reflect.TypeOf((*nodeURL)(nil)) + reflectTypeInt = reflect.TypeOf(int64(0)) +) + +func toURL(r *goja.Runtime, v goja.Value) *nodeURL { + if v.ExportType() == reflectTypeURL { + if u := v.Export().(*nodeURL); u != nil { + return u + } + } + + panic(errors.NewTypeError(r, errors.ErrCodeInvalidThis, `Value of "this" must be of type URL`)) +} + +func (m *urlModule) newInvalidURLError(msg, input string) *goja.Object { + o := errors.NewTypeError(m.r, "ERR_INVALID_URL", msg) + o.Set("input", m.r.ToValue(input)) + return o +} + +func (m *urlModule) defineURLAccessorProp(p *goja.Object, name string, getter func(*nodeURL) interface{}, setter func(*nodeURL, goja.Value)) { + var getterVal, setterVal goja.Value + if getter != nil { + getterVal = m.r.ToValue(func(call goja.FunctionCall) goja.Value { + return m.r.ToValue(getter(toURL(m.r, call.This))) + }) + } + if setter != nil { + setterVal = m.r.ToValue(func(call goja.FunctionCall) goja.Value { + setter(toURL(m.r, call.This), call.Argument(0)) + return goja.Undefined() + }) + } + p.DefineAccessorProperty(name, getterVal, setterVal, goja.FLAG_FALSE, goja.FLAG_TRUE) +} + +func valueToURLPort(v goja.Value) (portNum int, empty bool) { + portNum = -1 + if et := v.ExportType(); et == reflectTypeInt { + num := v.ToInteger() + if num < 0 { + empty = true + } else if num <= math.MaxUint16 { + portNum = int(num) + } + } else { + s := v.String() + if s == "" { + return 0, true + } + firstDigitIdx := -1 + for i := 0; i < len(s); i++ { + if c := s[i]; c >= '0' && c <= '9' { + firstDigitIdx = i + break + } + } + + if firstDigitIdx == -1 { + return -1, false + } + + if firstDigitIdx > 0 { + return 0, true + } + + for i := 0; i < len(s); i++ { + if c := s[i]; c >= '0' && c <= '9' { + if portNum == -1 { + portNum = 0 + } + portNum = portNum*10 + int(c-'0') + if portNum > math.MaxUint16 { + portNum = -1 + break + } + } else { + break + } + } + } + return +} + +func isDefaultURLPort(protocol string, port int) bool { + switch port { + case 21: + if protocol == "ftp" { + return true + } + case 80: + if protocol == "http" || protocol == "ws" { + return true + } + case 443: + if protocol == "https" || protocol == "wss" { + return true + } + } + return false +} + +func isSpecialProtocol(protocol string) bool { + switch protocol { + case "ftp", "file", "http", "https", "ws", "wss": + return true + } + return false +} + +func clearURLPort(u *url.URL) { + u.Host = u.Hostname() +} + +func setURLPort(nu *nodeURL, v goja.Value) { + u := nu.url + if u.Scheme == "file" { + return + } + portNum, empty := valueToURLPort(v) + if empty { + clearURLPort(u) + return + } + if portNum == -1 { + return + } + if isDefaultURLPort(u.Scheme, portNum) { + clearURLPort(u) + } else { + u.Host = u.Hostname() + ":" + strconv.Itoa(portNum) + } +} + +func (m *urlModule) parseURL(s string, isBase bool) *url.URL { + u, err := url.Parse(s) + if err != nil { + if isBase { + panic(m.newInvalidURLError(InvalidBaseURL, s)) + } else { + panic(m.newInvalidURLError(InvalidURL, s)) + } + } + if isBase && !u.IsAbs() { + panic(m.newInvalidURLError(URLNotAbsolute, s)) + } + if portStr := u.Port(); portStr != "" { + if port, err := strconv.Atoi(portStr); err != nil || isDefaultURLPort(u.Scheme, port) { + u.Host = u.Hostname() // Clear port + } + } + m.fixURL(u) + return u +} + +func fixRawQuery(u *url.URL) { + if u.RawQuery != "" { + u.RawQuery = escape(u.RawQuery, &tblEscapeURLQuery, false) + } +} + +func (m *urlModule) fixURL(u *url.URL) { + switch u.Scheme { + case "https", "http", "ftp", "wss", "ws": + if u.Path == "" { + u.Path = "/" + } + hostname := u.Hostname() + lh := strings.ToLower(hostname) + ch, err := idna.Punycode.ToASCII(lh) + if err != nil { + panic(m.newInvalidURLError(InvalidHostname, lh)) + } + if ch != hostname { + if port := u.Port(); port != "" { + u.Host = ch + ":" + port + } else { + u.Host = ch + } + } + } + fixRawQuery(u) +} + +func (m *urlModule) createURLPrototype() *goja.Object { + p := m.r.NewObject() + + // host + m.defineURLAccessorProp(p, "host", func(u *nodeURL) interface{} { + return u.url.Host + }, func(u *nodeURL, arg goja.Value) { + host := arg.String() + if _, err := url.ParseRequestURI(u.url.Scheme + "://" + host); err == nil { + u.url.Host = host + m.fixURL(u.url) + } + }) + + // hash + m.defineURLAccessorProp(p, "hash", func(u *nodeURL) interface{} { + if u.url.Fragment != "" { + return "#" + u.url.EscapedFragment() + } + return "" + }, func(u *nodeURL, arg goja.Value) { + h := arg.String() + if len(h) > 0 && h[0] == '#' { + h = h[1:] + } + u.url.Fragment = h + }) + + // hostname + m.defineURLAccessorProp(p, "hostname", func(u *nodeURL) interface{} { + return strings.Split(u.url.Host, ":")[0] + }, func(u *nodeURL, arg goja.Value) { + h := arg.String() + if strings.IndexByte(h, ':') >= 0 { + return + } + if _, err := url.ParseRequestURI(u.url.Scheme + "://" + h); err == nil { + if port := u.url.Port(); port != "" { + u.url.Host = h + ":" + port + } else { + u.url.Host = h + } + m.fixURL(u.url) + } + }) + + // href + m.defineURLAccessorProp(p, "href", func(u *nodeURL) interface{} { + return u.String() + }, func(u *nodeURL, arg goja.Value) { + u.url = m.parseURL(arg.String(), true) + }) + + // pathname + m.defineURLAccessorProp(p, "pathname", func(u *nodeURL) interface{} { + return u.url.EscapedPath() + }, func(u *nodeURL, arg goja.Value) { + p := arg.String() + if _, err := url.Parse(p); err == nil { + switch u.url.Scheme { + case "https", "http", "ftp", "ws", "wss": + if !strings.HasPrefix(p, "/") { + p = "/" + p + } + } + u.url.Path = p + } + }) + + // origin + m.defineURLAccessorProp(p, "origin", func(u *nodeURL) interface{} { + return u.url.Scheme + "://" + u.url.Hostname() + }, nil) + + // password + m.defineURLAccessorProp(p, "password", func(u *nodeURL) interface{} { + p, _ := u.url.User.Password() + return p + }, func(u *nodeURL, arg goja.Value) { + user := u.url.User + u.url.User = url.UserPassword(user.Username(), arg.String()) + }) + + // username + m.defineURLAccessorProp(p, "username", func(u *nodeURL) interface{} { + return u.url.User.Username() + }, func(u *nodeURL, arg goja.Value) { + p, has := u.url.User.Password() + if !has { + u.url.User = url.User(arg.String()) + } else { + u.url.User = url.UserPassword(arg.String(), p) + } + }) + + // port + m.defineURLAccessorProp(p, "port", func(u *nodeURL) interface{} { + return u.url.Port() + }, func(u *nodeURL, arg goja.Value) { + setURLPort(u, arg) + }) + + // protocol + m.defineURLAccessorProp(p, "protocol", func(u *nodeURL) interface{} { + return u.url.Scheme + ":" + }, func(u *nodeURL, arg goja.Value) { + s := arg.String() + pos := strings.IndexByte(s, ':') + if pos >= 0 { + s = s[:pos] + } + s = strings.ToLower(s) + if isSpecialProtocol(u.url.Scheme) == isSpecialProtocol(s) { + if _, err := url.ParseRequestURI(s + "://" + u.url.Host); err == nil { + u.url.Scheme = s + } + } + }) + + // Search + m.defineURLAccessorProp(p, "search", func(u *nodeURL) interface{} { + u.syncSearchParams() + if u.url.RawQuery != "" { + return "?" + u.url.RawQuery + } + return "" + }, func(u *nodeURL, arg goja.Value) { + u.url.RawQuery = arg.String() + fixRawQuery(u.url) + if u.searchParams != nil { + u.searchParams = parseSearchQuery(u.url.RawQuery) + if u.searchParams == nil { + u.searchParams = make(searchParams, 0) + } + } + }) + + // search Params + m.defineURLAccessorProp(p, "searchParams", func(u *nodeURL) interface{} { + if u.searchParams == nil { + sp := parseSearchQuery(u.url.RawQuery) + if sp == nil { + sp = make(searchParams, 0) + } + u.searchParams = sp + } + return m.newURLSearchParams((*urlSearchParams)(u)) + }, nil) + + p.Set("toString", m.r.ToValue(func(call goja.FunctionCall) goja.Value { + u := toURL(m.r, call.This) + u.syncSearchParams() + return m.r.ToValue(u.url.String()) + })) + + p.Set("toJSON", m.r.ToValue(func(call goja.FunctionCall) goja.Value { + u := toURL(m.r, call.This) + u.syncSearchParams() + return m.r.ToValue(u.url.String()) + })) + + return p +} + +func (m *urlModule) createURLConstructor() goja.Value { + f := m.r.ToValue(func(call goja.ConstructorCall) *goja.Object { + var u *url.URL + if baseArg := call.Argument(1); !goja.IsUndefined(baseArg) { + base := m.parseURL(baseArg.String(), true) + ref := m.parseURL(call.Argument(0).String(), false) + u = base.ResolveReference(ref) + } else { + u = m.parseURL(call.Argument(0).String(), true) + } + res := m.r.ToValue(&nodeURL{url: u}).(*goja.Object) + res.SetPrototype(call.This.Prototype()) + return res + }).(*goja.Object) + + proto := m.createURLPrototype() + f.Set("prototype", proto) + proto.DefineDataProperty("constructor", f, goja.FLAG_FALSE, goja.FLAG_FALSE, goja.FLAG_FALSE) + return f +} + +func (m *urlModule) domainToASCII(domUnicode string) string { + res, err := idna.ToASCII(domUnicode) + if err != nil { + return "" + } + return res +} + +func (m *urlModule) domainToUnicode(domASCII string) string { + res, err := idna.ToUnicode(domASCII) + if err != nil { + return "" + } + return res +} diff --git a/goja_nodejs/url/url_test.go b/goja_nodejs/url/url_test.go new file mode 100644 index 0000000..048c80b --- /dev/null +++ b/goja_nodejs/url/url_test.go @@ -0,0 +1,122 @@ +package url + +import ( + _ "embed" + "testing" + + "apigo.cc/ai/ai/goja" + "apigo.cc/ai/ai/goja_nodejs/require" +) + +func TestURL(t *testing.T) { + vm := goja.New() + new(require.Registry).Enable(vm) + Enable(vm) + + if c := vm.Get("URL"); c == nil { + t.Fatal("URL not found") + } + + script := `const url = new URL("https://user:pass@sub.example.com:8080/p/a/t/h?query=string#hash");` + + if _, err := vm.RunString(script); err != nil { + t.Fatal("Failed to process url script.", err) + } +} + +func TestGetters(t *testing.T) { + vm := goja.New() + new(require.Registry).Enable(vm) + Enable(vm) + + if c := vm.Get("URL"); c == nil { + t.Fatal("URL not found") + } + + script := ` + new URL("https://user:pass@sub.example.com:8080/p/a/t/h?query=string#hashed"); + ` + + v, err := vm.RunString(script) + if err != nil { + t.Fatal("Failed to process url script.", err) + } + + url := v.ToObject(vm) + + tests := []struct { + prop string + expected string + }{ + { + prop: "hash", + expected: "#hashed", + }, + { + prop: "host", + expected: "sub.example.com:8080", + }, + { + prop: "hostname", + expected: "sub.example.com", + }, + { + prop: "href", + expected: "https://user:pass@sub.example.com:8080/p/a/t/h?query=string#hashed", + }, + { + prop: "origin", + expected: "https://sub.example.com", + }, + { + prop: "password", + expected: "pass", + }, + { + prop: "username", + expected: "user", + }, + { + prop: "port", + expected: "8080", + }, + { + prop: "protocol", + expected: "https:", + }, + { + prop: "search", + expected: "?query=string", + }, + } + + for _, test := range tests { + v := url.Get(test.prop).String() + if v != test.expected { + t.Fatal("failed to match " + test.prop + " property. got: " + v + ", expected: " + test.expected) + } + } +} + +//go:embed testdata/url_test.js +var urlTest string + +func TestJs(t *testing.T) { + vm := goja.New() + new(require.Registry).Enable(vm) + Enable(vm) + + if c := vm.Get("URL"); c == nil { + t.Fatal("URL not found") + } + + // Script will throw an error on failed validation + + _, err := vm.RunScript("testdata/url_test.js", urlTest) + if err != nil { + if ex, ok := err.(*goja.Exception); ok { + t.Fatal(ex.String()) + } + t.Fatal("Failed to process url script.", err) + } +} diff --git a/goja_nodejs/url/urlsearchparams.go b/goja_nodejs/url/urlsearchparams.go new file mode 100644 index 0000000..2b7c382 --- /dev/null +++ b/goja_nodejs/url/urlsearchparams.go @@ -0,0 +1,390 @@ +package url + +import ( + "reflect" + "sort" + + "apigo.cc/ai/ai/goja_nodejs/errors" + + "apigo.cc/ai/ai/goja" +) + +var ( + reflectTypeURLSearchParams = reflect.TypeOf((*urlSearchParams)(nil)) + reflectTypeURLSearchParamsIterator = reflect.TypeOf((*urlSearchParamsIterator)(nil)) +) + +func newInvalidTupleError(r *goja.Runtime) *goja.Object { + return errors.NewTypeError(r, "ERR_INVALID_TUPLE", "Each query pair must be an iterable [name, value] tuple") +} + +func newMissingArgsError(r *goja.Runtime, msg string) *goja.Object { + return errors.NewTypeError(r, errors.ErrCodeMissingArgs, msg) +} + +func newInvalidArgsError(r *goja.Runtime) *goja.Object { + return errors.NewTypeError(r, "ERR_INVALID_ARG_TYPE", `The "callback" argument must be of type function.`) +} + +func toUrlSearchParams(r *goja.Runtime, v goja.Value) *urlSearchParams { + if v.ExportType() == reflectTypeURLSearchParams { + if u := v.Export().(*urlSearchParams); u != nil { + return u + } + } + panic(errors.NewTypeError(r, errors.ErrCodeInvalidThis, `Value of "this" must be of type URLSearchParams`)) +} + +func (m *urlModule) newURLSearchParams(sp *urlSearchParams) *goja.Object { + v := m.r.ToValue(sp).(*goja.Object) + v.SetPrototype(m.URLSearchParamsPrototype) + return v +} + +func (m *urlModule) createURLSearchParamsConstructor() goja.Value { + f := m.r.ToValue(func(call goja.ConstructorCall) *goja.Object { + var sp searchParams + v := call.Argument(0) + if o, ok := v.(*goja.Object); ok { + sp = m.buildParamsFromObject(o) + } else if !goja.IsUndefined(v) { + sp = parseSearchQuery(v.String()) + } + + return m.newURLSearchParams(&urlSearchParams{searchParams: sp}) + }).(*goja.Object) + + m.URLSearchParamsPrototype = m.createURLSearchParamsPrototype() + f.Set("prototype", m.URLSearchParamsPrototype) + m.URLSearchParamsPrototype.DefineDataProperty("constructor", f, goja.FLAG_FALSE, goja.FLAG_FALSE, goja.FLAG_FALSE) + + return f +} + +func (m *urlModule) buildParamsFromObject(o *goja.Object) searchParams { + var query searchParams + + if o.GetSymbol(goja.SymIterator) != nil { + return m.buildParamsFromIterable(o) + } + + for _, k := range o.Keys() { + val := o.Get(k).String() + query = append(query, searchParam{name: k, value: val}) + } + + return query +} + +func (m *urlModule) buildParamsFromIterable(o *goja.Object) searchParams { + var query searchParams + + m.r.ForOf(o, func(val goja.Value) bool { + obj := val.ToObject(m.r) + var name, value string + i := 0 + // Use ForOf to determine if the object is iterable + m.r.ForOf(obj, func(val goja.Value) bool { + if i == 0 { + name = val.String() + i++ + return true + } + if i == 1 { + value = val.String() + i++ + return true + } + // Array isn't a tuple + panic(newInvalidTupleError(m.r)) + }) + + // Ensure we have two values + if i <= 1 { + panic(newInvalidTupleError(m.r)) + } + + query = append(query, searchParam{ + name: name, + value: value, + }) + + return true + }) + + return query +} + +func (m *urlModule) createURLSearchParamsPrototype() *goja.Object { + p := m.r.NewObject() + + p.Set("append", m.r.ToValue(func(call goja.FunctionCall) goja.Value { + if len(call.Arguments) < 2 { + panic(newMissingArgsError(m.r, `The "name" and "value" arguments must be specified`)) + } + + u := toUrlSearchParams(m.r, call.This) + u.searchParams = append(u.searchParams, searchParam{ + name: call.Argument(0).String(), + value: call.Argument(1).String(), + }) + u.markUpdated() + + return goja.Undefined() + })) + + p.Set("delete", m.r.ToValue(func(call goja.FunctionCall) goja.Value { + u := toUrlSearchParams(m.r, call.This) + + if len(call.Arguments) < 1 { + panic(newMissingArgsError(m.r, `The "name" argument must be specified`)) + } + + name := call.Argument(0).String() + isValid := func(v searchParam) bool { + if len(call.Arguments) == 1 { + return v.name != name + } else if v.name == name { + arg := call.Argument(1) + if !goja.IsUndefined(arg) && v.value == arg.String() { + return false + } + } + return true + } + + j := 0 + for i, v := range u.searchParams { + if isValid(v) { + if i != j { + u.searchParams[j] = v + } + j++ + } + } + u.searchParams = u.searchParams[:j] + u.markUpdated() + + return goja.Undefined() + })) + + entries := m.r.ToValue(func(call goja.FunctionCall) goja.Value { + return m.newURLSearchParamsIterator(toUrlSearchParams(m.r, call.This), urlSearchParamsIteratorEntries) + }) + p.Set("entries", entries) + p.DefineDataPropertySymbol(goja.SymIterator, entries, goja.FLAG_TRUE, goja.FLAG_FALSE, goja.FLAG_TRUE) + + p.Set("forEach", m.r.ToValue(func(call goja.FunctionCall) goja.Value { + u := toUrlSearchParams(m.r, call.This) + + if len(call.Arguments) != 1 { + panic(newInvalidArgsError(m.r)) + } + + if fn, ok := goja.AssertFunction(call.Argument(0)); ok { + for _, pair := range u.searchParams { + // name, value, searchParams + _, err := fn( + nil, + m.r.ToValue(pair.name), + m.r.ToValue(pair.value), + call.This, + ) + + if err != nil { + panic(err) + } + } + } else { + panic(newInvalidArgsError(m.r)) + } + + return goja.Undefined() + })) + + p.Set("get", m.r.ToValue(func(call goja.FunctionCall) goja.Value { + u := toUrlSearchParams(m.r, call.This) + + if len(call.Arguments) == 0 { + panic(newMissingArgsError(m.r, `The "name" argument must be specified`)) + } + + if val, exists := u.getFirstValue(call.Argument(0).String()); exists { + return m.r.ToValue(val) + } + + return goja.Null() + })) + + p.Set("getAll", m.r.ToValue(func(call goja.FunctionCall) goja.Value { + u := toUrlSearchParams(m.r, call.This) + + if len(call.Arguments) == 0 { + panic(newMissingArgsError(m.r, `The "name" argument must be specified`)) + } + + vals := u.getValues(call.Argument(0).String()) + return m.r.ToValue(vals) + })) + + p.Set("has", m.r.ToValue(func(call goja.FunctionCall) goja.Value { + u := toUrlSearchParams(m.r, call.This) + + if len(call.Arguments) == 0 { + panic(newMissingArgsError(m.r, `The "name" argument must be specified`)) + } + + name := call.Argument(0).String() + value := call.Argument(1) + var res bool + if goja.IsUndefined(value) { + res = u.hasName(name) + } else { + res = u.hasValue(name, value.String()) + } + return m.r.ToValue(res) + })) + + p.Set("keys", m.r.ToValue(func(call goja.FunctionCall) goja.Value { + return m.newURLSearchParamsIterator(toUrlSearchParams(m.r, call.This), urlSearchParamsIteratorKeys) + })) + + p.Set("set", m.r.ToValue(func(call goja.FunctionCall) goja.Value { + u := toUrlSearchParams(m.r, call.This) + + if len(call.Arguments) < 2 { + panic(newMissingArgsError(m.r, `The "name" and "value" arguments must be specified`)) + } + + name := call.Argument(0).String() + found := false + j := 0 + for i, sp := range u.searchParams { + if sp.name == name { + if found { + continue // Remove all values + } + + u.searchParams[i].value = call.Argument(1).String() + found = true + } + if i != j { + u.searchParams[j] = sp + } + j++ + } + + if !found { + u.searchParams = append(u.searchParams, searchParam{ + name: name, + value: call.Argument(1).String(), + }) + } else { + u.searchParams = u.searchParams[:j] + } + + u.markUpdated() + + return goja.Undefined() + })) + + p.Set("sort", m.r.ToValue(func(call goja.FunctionCall) goja.Value { + u := toUrlSearchParams(m.r, call.This) + sort.Stable(u.searchParams) + u.markUpdated() + return goja.Undefined() + })) + + p.DefineAccessorProperty("size", m.r.ToValue(func(call goja.FunctionCall) goja.Value { + u := toUrlSearchParams(m.r, call.This) + return m.r.ToValue(len(u.searchParams)) + }), nil, goja.FLAG_FALSE, goja.FLAG_TRUE) + + p.Set("toString", m.r.ToValue(func(call goja.FunctionCall) goja.Value { + u := toUrlSearchParams(m.r, call.This) + str := u.searchParams.Encode() + return m.r.ToValue(str) + })) + + p.Set("values", m.r.ToValue(func(call goja.FunctionCall) goja.Value { + return m.newURLSearchParamsIterator(toUrlSearchParams(m.r, call.This), urlSearchParamsIteratorValues) + })) + + return p +} + +func (sp *urlSearchParams) markUpdated() { + if sp.url != nil && sp.url.RawQuery != "" { + sp.url.RawQuery = "" + } +} + +type urlSearchParamsIteratorType int + +const ( + urlSearchParamsIteratorKeys urlSearchParamsIteratorType = iota + urlSearchParamsIteratorValues + urlSearchParamsIteratorEntries +) + +type urlSearchParamsIterator struct { + typ urlSearchParamsIteratorType + sp *urlSearchParams + idx int +} + +func toURLSearchParamsIterator(r *goja.Runtime, v goja.Value) *urlSearchParamsIterator { + if v.ExportType() == reflectTypeURLSearchParamsIterator { + if u := v.Export().(*urlSearchParamsIterator); u != nil { + return u + } + } + + panic(errors.NewTypeError(r, errors.ErrCodeInvalidThis, `Value of "this" must be of type URLSearchParamIterator`)) +} + +func (m *urlModule) getURLSearchParamsIteratorPrototype() *goja.Object { + if m.URLSearchParamsIteratorPrototype != nil { + return m.URLSearchParamsIteratorPrototype + } + + p := m.r.NewObject() + + p.Set("next", m.r.ToValue(func(call goja.FunctionCall) goja.Value { + it := toURLSearchParamsIterator(m.r, call.This) + res := m.r.NewObject() + if it.idx < len(it.sp.searchParams) { + param := it.sp.searchParams[it.idx] + switch it.typ { + case urlSearchParamsIteratorKeys: + res.Set("value", param.name) + case urlSearchParamsIteratorValues: + res.Set("value", param.value) + default: + res.Set("value", m.r.NewArray(param.name, param.value)) + } + res.Set("done", false) + it.idx++ + } else { + res.Set("value", goja.Undefined()) + res.Set("done", true) + } + return res + })) + + p.DefineDataPropertySymbol(goja.SymToStringTag, m.r.ToValue("URLSearchParams Iterator"), goja.FLAG_FALSE, goja.FLAG_FALSE, goja.FLAG_TRUE) + + m.URLSearchParamsIteratorPrototype = p + return p +} + +func (m *urlModule) newURLSearchParamsIterator(sp *urlSearchParams, typ urlSearchParamsIteratorType) goja.Value { + it := m.r.ToValue(&urlSearchParamsIterator{ + typ: typ, + sp: sp, + }).(*goja.Object) + + it.SetPrototype(m.getURLSearchParamsIteratorPrototype()) + + return it +} diff --git a/goja_nodejs/url/urlsearchparams_test.go b/goja_nodejs/url/urlsearchparams_test.go new file mode 100644 index 0000000..ff54070 --- /dev/null +++ b/goja_nodejs/url/urlsearchparams_test.go @@ -0,0 +1,53 @@ +package url + +import ( + _ "embed" + "testing" + + "apigo.cc/ai/ai/goja" + "apigo.cc/ai/ai/goja_nodejs/console" + "apigo.cc/ai/ai/goja_nodejs/require" +) + +func createVM() *goja.Runtime { + vm := goja.New() + new(require.Registry).Enable(vm) + console.Enable(vm) + Enable(vm) + return vm +} + +func TestURLSearchParams(t *testing.T) { + vm := createVM() + + if c := vm.Get("URLSearchParams"); c == nil { + t.Fatal("URLSearchParams not found") + } + + script := `const params = new URLSearchParams();` + + if _, err := vm.RunString(script); err != nil { + t.Fatal("Failed to process url script.", err) + } +} + +//go:embed testdata/url_search_params.js +var url_search_params string + +func TestURLSearchParameters(t *testing.T) { + vm := createVM() + + if c := vm.Get("URLSearchParams"); c == nil { + t.Fatal("URLSearchParams not found") + } + + // Script will throw an error on failed validation + + _, err := vm.RunScript("testdata/url_search_params.js", url_search_params) + if err != nil { + if ex, ok := err.(*goja.Exception); ok { + t.Fatal(ex.String()) + } + t.Fatal("Failed to process url script.", err) + } +} diff --git a/goja_nodejs/util/module.go b/goja_nodejs/util/module.go new file mode 100644 index 0000000..b2b08ef --- /dev/null +++ b/goja_nodejs/util/module.go @@ -0,0 +1,104 @@ +package util + +import ( + "apigo.cc/ai/ai/goja" + "apigo.cc/ai/ai/goja_nodejs/require" + "bytes" +) + +const ModuleName = "util" + +type Util struct { + runtime *goja.Runtime +} + +func (u *Util) format(f rune, val goja.Value, w *bytes.Buffer) bool { + switch f { + case 's': + w.WriteString(val.String()) + case 'd': + w.WriteString(val.ToNumber().String()) + case 'j': + if json, ok := u.runtime.Get("JSON").(*goja.Object); ok { + if stringify, ok := goja.AssertFunction(json.Get("stringify")); ok { + res, err := stringify(json, val) + if err != nil { + panic(err) + } + w.WriteString(res.String()) + } + } + case '%': + w.WriteByte('%') + return false + default: + w.WriteByte('%') + w.WriteRune(f) + return false + } + return true +} + +func (u *Util) Format(b *bytes.Buffer, f string, args ...goja.Value) { + pct := false + argNum := 0 + for _, chr := range f { + if pct { + if argNum < len(args) { + if u.format(chr, args[argNum], b) { + argNum++ + } + } else { + b.WriteByte('%') + b.WriteRune(chr) + } + pct = false + } else { + if chr == '%' { + pct = true + } else { + b.WriteRune(chr) + } + } + } + + for _, arg := range args[argNum:] { + b.WriteByte(' ') + b.WriteString(arg.String()) + } +} + +func (u *Util) js_format(call goja.FunctionCall) goja.Value { + var b bytes.Buffer + var fmt string + + if arg := call.Argument(0); !goja.IsUndefined(arg) { + fmt = arg.String() + } + + var args []goja.Value + if len(call.Arguments) > 0 { + args = call.Arguments[1:] + } + u.Format(&b, fmt, args...) + + return u.runtime.ToValue(b.String()) +} + +func Require(runtime *goja.Runtime, module *goja.Object) { + u := &Util{ + runtime: runtime, + } + obj := module.Get("exports").(*goja.Object) + obj.Set("format", u.js_format) +} + +func New(runtime *goja.Runtime) *Util { + return &Util{ + runtime: runtime, + } +} + +func init() { + require.RegisterCoreModule(ModuleName, Require) +} diff --git a/goja_nodejs/util/module_test.go b/goja_nodejs/util/module_test.go new file mode 100644 index 0000000..fe3d736 --- /dev/null +++ b/goja_nodejs/util/module_test.go @@ -0,0 +1,74 @@ +package util + +import ( + "bytes" + "testing" + + "apigo.cc/ai/ai/goja" + "apigo.cc/ai/ai/goja_nodejs/require" +) + +func TestUtil_Format(t *testing.T) { + vm := goja.New() + util := New(vm) + + var b bytes.Buffer + util.Format(&b, "Test: %% %д %s %d, %j", vm.ToValue("string"), vm.ToValue(42), vm.NewObject()) + + if res := b.String(); res != "Test: % %д string 42, {}" { + t.Fatalf("Unexpected result: '%s'", res) + } +} + +func TestUtil_Format_NoArgs(t *testing.T) { + vm := goja.New() + util := New(vm) + + var b bytes.Buffer + util.Format(&b, "Test: %s %d, %j") + + if res := b.String(); res != "Test: %s %d, %j" { + t.Fatalf("Unexpected result: '%s'", res) + } +} + +func TestUtil_Format_LessArgs(t *testing.T) { + vm := goja.New() + util := New(vm) + + var b bytes.Buffer + util.Format(&b, "Test: %s %d, %j", vm.ToValue("string"), vm.ToValue(42)) + + if res := b.String(); res != "Test: string 42, %j" { + t.Fatalf("Unexpected result: '%s'", res) + } +} + +func TestUtil_Format_MoreArgs(t *testing.T) { + vm := goja.New() + util := New(vm) + + var b bytes.Buffer + util.Format(&b, "Test: %s %d, %j", vm.ToValue("string"), vm.ToValue(42), vm.NewObject(), vm.ToValue(42.42)) + + if res := b.String(); res != "Test: string 42, {} 42.42" { + t.Fatalf("Unexpected result: '%s'", res) + } +} + +func TestJSNoArgs(t *testing.T) { + vm := goja.New() + new(require.Registry).Enable(vm) + + if util, ok := require.Require(vm, ModuleName).(*goja.Object); ok { + if format, ok := goja.AssertFunction(util.Get("format")); ok { + res, err := format(util) + if err != nil { + t.Fatal(err) + } + if v := res.Export(); v != "" { + t.Fatalf("Unexpected result: %v", v) + } + } + } +} diff --git a/js.go b/js.go index ff05106..fc0a884 100644 --- a/js.go +++ b/js.go @@ -1,6 +1,8 @@ package ai import ( + "apigo.cc/ai/ai/goja" + "apigo.cc/ai/ai/goja_nodejs/require" "apigo.cc/ai/ai/js" "apigo.cc/ai/ai/llm" "bytes" @@ -8,8 +10,6 @@ import ( "encoding/json" "errors" "fmt" - "github.com/dop251/goja" - "github.com/dop251/goja_nodejs/require" "github.com/ssgo/u" "path/filepath" "regexp" diff --git a/js/ai.go b/js/ai.go index d6c1722..a6070e3 100644 --- a/js/ai.go +++ b/js/ai.go @@ -1,8 +1,8 @@ package js import ( + "apigo.cc/ai/ai/goja" "apigo.cc/ai/ai/llm" - "github.com/dop251/goja" "github.com/ssgo/u" "reflect" "strings" @@ -24,81 +24,96 @@ type AIGCResult struct { func RequireAI(lm llm.LLM) map[string]any { return map[string]any{ - "ask": func(args goja.FunctionCall, vm *goja.Runtime) goja.Value { + "ask": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { + args := MakeArgs(&argsIn, vm).Check(1) conf, cb := getAskArgs(args.This, vm, args.Arguments) result, usage, err := lm.Ask(makeChatMessages(args.Arguments), conf, cb) return vm.ToValue(map[string]any{"tokenUsage": toMap(usage), "result": result, "error": getErrorStr(err)}) }, - "fastAsk": func(args goja.FunctionCall, vm *goja.Runtime) goja.Value { + "fastAsk": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { + args := MakeArgs(&argsIn, vm).Check(1) _, cb := getAskArgs(args.This, vm, args.Arguments) result, usage, err := lm.FastAsk(makeChatMessages(args.Arguments), cb) return vm.ToValue(map[string]any{"tokenUsage": toMap(usage), "result": result, "error": getErrorStr(err)}) }, - "longAsk": func(args goja.FunctionCall, vm *goja.Runtime) goja.Value { + "longAsk": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { + args := MakeArgs(&argsIn, vm).Check(1) _, cb := getAskArgs(args.This, vm, args.Arguments) result, usage, err := lm.LongAsk(makeChatMessages(args.Arguments), cb) return vm.ToValue(map[string]any{"tokenUsage": toMap(usage), "result": result, "error": getErrorStr(err)}) }, - "batterAsk": func(args goja.FunctionCall, vm *goja.Runtime) goja.Value { + "batterAsk": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { + args := MakeArgs(&argsIn, vm).Check(1) _, cb := getAskArgs(args.This, vm, args.Arguments) result, usage, err := lm.BatterAsk(makeChatMessages(args.Arguments), cb) return vm.ToValue(map[string]any{"tokenUsage": toMap(usage), "result": result, "error": getErrorStr(err)}) }, - "bestAsk": func(args goja.FunctionCall, vm *goja.Runtime) goja.Value { + "bestAsk": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { + args := MakeArgs(&argsIn, vm).Check(1) _, cb := getAskArgs(args.This, vm, args.Arguments) result, usage, err := lm.BestAsk(makeChatMessages(args.Arguments), cb) return vm.ToValue(map[string]any{"tokenUsage": toMap(usage), "result": result, "error": getErrorStr(err)}) }, - "multiAsk": func(args goja.FunctionCall, vm *goja.Runtime) goja.Value { + "multiAsk": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { + args := MakeArgs(&argsIn, vm).Check(1) _, cb := getAskArgs(args.This, vm, args.Arguments) result, usage, err := lm.MultiAsk(makeChatMessages(args.Arguments), cb) return vm.ToValue(map[string]any{"tokenUsage": toMap(usage), "result": result, "error": getErrorStr(err)}) }, - "bestMultiAsk": func(args goja.FunctionCall, vm *goja.Runtime) goja.Value { + "bestMultiAsk": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { + args := MakeArgs(&argsIn, vm).Check(1) _, cb := getAskArgs(args.This, vm, args.Arguments) result, usage, err := lm.BestMultiAsk(makeChatMessages(args.Arguments), cb) return vm.ToValue(map[string]any{"tokenUsage": toMap(usage), "result": result, "error": getErrorStr(err)}) }, - "codeInterpreterAsk": func(args goja.FunctionCall, vm *goja.Runtime) goja.Value { + "codeInterpreterAsk": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { + args := MakeArgs(&argsIn, vm).Check(1) _, cb := getAskArgs(args.This, vm, args.Arguments) result, usage, err := lm.CodeInterpreterAsk(makeChatMessages(args.Arguments), cb) return vm.ToValue(map[string]any{"tokenUsage": toMap(usage), "result": result, "error": getErrorStr(err)}) }, - "webSearchAsk": func(args goja.FunctionCall, vm *goja.Runtime) goja.Value { + "webSearchAsk": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { + args := MakeArgs(&argsIn, vm).Check(1) _, cb := getAskArgs(args.This, vm, args.Arguments) result, usage, err := lm.WebSearchAsk(makeChatMessages(args.Arguments), cb) return vm.ToValue(map[string]any{"tokenUsage": toMap(usage), "result": result, "error": getErrorStr(err)}) }, - "makeImage": func(args goja.FunctionCall, vm *goja.Runtime) goja.Value { + "makeImage": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { + args := MakeArgs(&argsIn, vm).Check(1) prompt, conf := getAIGCArgs(args.Arguments) results, err := lm.MakeImage(prompt, conf) return makeAIGCResult(vm, results, nil, err) }, - "fastMakeImage": func(args goja.FunctionCall, vm *goja.Runtime) goja.Value { + "fastMakeImage": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { + args := MakeArgs(&argsIn, vm).Check(1) prompt, conf := getAIGCArgs(args.Arguments) results, err := lm.FastMakeImage(prompt, conf) return makeAIGCResult(vm, results, nil, err) }, - "bestMakeImage": func(args goja.FunctionCall, vm *goja.Runtime) goja.Value { + "bestMakeImage": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { + args := MakeArgs(&argsIn, vm).Check(1) prompt, conf := getAIGCArgs(args.Arguments) results, err := lm.BestMakeImage(prompt, conf) return makeAIGCResult(vm, results, nil, err) }, - "makeVideo": func(args goja.FunctionCall, vm *goja.Runtime) goja.Value { + "makeVideo": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { + args := MakeArgs(&argsIn, vm).Check(1) prompt, conf := getAIGCArgs(args.Arguments) results, previews, err := lm.MakeVideo(prompt, conf) return makeAIGCResult(vm, results, previews, err) }, - "fastMakeVideo": func(args goja.FunctionCall, vm *goja.Runtime) goja.Value { + "fastMakeVideo": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { + args := MakeArgs(&argsIn, vm).Check(1) prompt, conf := getAIGCArgs(args.Arguments) results, previews, err := lm.FastMakeVideo(prompt, conf) return makeAIGCResult(vm, results, previews, err) }, - "bestMakeVideo": func(args goja.FunctionCall, vm *goja.Runtime) goja.Value { + "bestMakeVideo": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { + args := MakeArgs(&argsIn, vm).Check(1) prompt, conf := getAIGCArgs(args.Arguments) results, previews, err := lm.BestMakeVideo(prompt, conf) return makeAIGCResult(vm, results, previews, err) diff --git a/js/common.go b/js/common.go index f910578..56487ba 100644 --- a/js/common.go +++ b/js/common.go @@ -1,8 +1,8 @@ package js import ( + "apigo.cc/ai/ai/goja" "errors" - "github.com/dop251/goja" "github.com/ssgo/u" "path/filepath" ) @@ -25,11 +25,11 @@ func MakeArgs(args *goja.FunctionCall, vm *goja.Runtime) *Args { } } -func (args *Args) Check(num int) goja.Value { +func (args *Args) Check(num int) *Args { if len(args.Arguments) < num { - return args.VM.NewGoError(errors.New("arguments need " + u.String(num) + ", but given " + u.String(len(args.Arguments)))) + panic(args.VM.NewGoError(errors.New("arguments need " + u.String(num) + ", but given " + u.String(len(args.Arguments))))) } - return nil + return args } func (args *Args) Int(index int) int { diff --git a/js/console.go b/js/console.go index e585ec3..893ec64 100644 --- a/js/console.go +++ b/js/console.go @@ -1,8 +1,8 @@ package js import ( + "apigo.cc/ai/ai/goja" "fmt" - "github.com/dop251/goja" "github.com/ssgo/u" "strings" ) diff --git a/js/file.go b/js/file.go index e31acd8..e754680 100644 --- a/js/file.go +++ b/js/file.go @@ -1,54 +1,43 @@ package js import ( - "errors" - "github.com/dop251/goja" + "apigo.cc/ai/ai/goja" "github.com/ssgo/u" ) func RequireFile() map[string]any { return map[string]any{ - "read": func(args goja.FunctionCall, vm *goja.Runtime) goja.Value { - if len(args.Arguments) < 1 { - return vm.NewGoError(errors.New("arguments need 1, but given " + u.String(len(args.Arguments)))) - } - if r, err := u.ReadFile(findPath(vm, u.String(args.Arguments[0].Export()))); err == nil { + "read": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { + args := MakeArgs(&argsIn, vm).Check(1) + if r, err := u.ReadFile(findPath(vm, args.Str(0))); err == nil { return vm.ToValue(r) } else { panic(vm.NewGoError(err)) } }, - "write": func(args goja.FunctionCall, vm *goja.Runtime) goja.Value { - if len(args.Arguments) < 2 { - return vm.NewGoError(errors.New("arguments need 2, but given " + u.String(len(args.Arguments)))) - } - if err := u.WriteFileBytes(findPath(vm, u.String(args.Arguments[0].Export())), u.Bytes(args.Arguments[0].Export())); err == nil { + "write": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { + args := MakeArgs(&argsIn, vm).Check(2) + if err := u.WriteFileBytes(findPath(vm, args.Str(0)), u.Bytes(args.Any(0))); err == nil { return nil } else { - return vm.NewGoError(err) + panic(vm.NewGoError(err)) } }, - "dir": func(args goja.FunctionCall, vm *goja.Runtime) goja.Value { - if len(args.Arguments) < 1 { - return vm.NewGoError(errors.New("arguments need 1, but given " + u.String(len(args.Arguments)))) - } - if r, err := u.ReadDir(findPath(vm, u.String(args.Arguments[0].Export()))); err == nil { + "dir": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { + args := MakeArgs(&argsIn, vm).Check(1) + if r, err := u.ReadDir(findPath(vm, args.Str(0))); err == nil { return vm.ToValue(r) } else { - return vm.NewGoError(err) + panic(vm.NewGoError(err)) } }, - "stat": func(args goja.FunctionCall, vm *goja.Runtime) goja.Value { - if len(args.Arguments) < 1 { - return vm.NewGoError(errors.New("arguments need 1, but given " + u.String(len(args.Arguments)))) - } - return vm.ToValue(u.GetFileInfo(findPath(vm, u.String(args.Arguments[0].Export())))) + "stat": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { + args := MakeArgs(&argsIn, vm).Check(1) + return vm.ToValue(u.GetFileInfo(findPath(vm, args.Str(0)))) }, - "find": func(args goja.FunctionCall, vm *goja.Runtime) goja.Value { - if len(args.Arguments) < 1 { - return vm.NewGoError(errors.New("arguments need 1, but given " + u.String(len(args.Arguments)))) - } - return vm.ToValue(findPath(vm, u.String(args.Arguments[0].Export()))) + "find": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { + args := MakeArgs(&argsIn, vm).Check(1) + return vm.ToValue(findPath(vm, args.Str(0))) }, } } diff --git a/js/http.go b/js/http.go index 9d55032..cb0b48f 100644 --- a/js/http.go +++ b/js/http.go @@ -1,7 +1,7 @@ package js import ( - "github.com/dop251/goja" + "apigo.cc/ai/ai/goja" "github.com/ssgo/httpclient" "github.com/ssgo/u" ) @@ -41,7 +41,7 @@ func RequireHTTP() map[string]any { func makeResult(r *httpclient.Result, vm *goja.Runtime) goja.Value { if r.Error != nil { - return vm.NewGoError(r.Error) + panic(vm.NewGoError(r.Error)) } headers := map[string]string{} for k, v := range r.Response.Header { @@ -57,50 +57,32 @@ func makeResult(r *httpclient.Result, vm *goja.Runtime) goja.Value { } func (hc *Http) Get(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { - args := MakeArgs(&argsIn, vm) - if err := args.Check(1); err != nil { - return err - } + args := MakeArgs(&argsIn, vm).Check(1) return makeResult(hc.client.Get(args.Str(0), args.Map2StrArr(1)...), vm) } func (hc *Http) Head(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { - args := MakeArgs(&argsIn, vm) - if err := args.Check(1); err != nil { - return err - } + args := MakeArgs(&argsIn, vm).Check(1) return makeResult(hc.client.Head(args.Str(0), args.Map2StrArr(1)...), vm) } func (hc *Http) Post(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { - args := MakeArgs(&argsIn, vm) - if err := args.Check(2); err != nil { - return err - } + args := MakeArgs(&argsIn, vm).Check(2) return makeResult(hc.client.Post(args.Str(0), args.Any(1), args.Map2StrArr(2)...), vm) } func (hc *Http) Put(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { - args := MakeArgs(&argsIn, vm) - if err := args.Check(2); err != nil { - return err - } + args := MakeArgs(&argsIn, vm).Check(2) return makeResult(hc.client.Put(args.Str(0), args.Any(1), args.Map2StrArr(2)...), vm) } func (hc *Http) Delete(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { - args := MakeArgs(&argsIn, vm) - if err := args.Check(2); err != nil { - return err - } + args := MakeArgs(&argsIn, vm).Check(2) return makeResult(hc.client.Delete(args.Str(0), args.Any(1), args.Map2StrArr(2)...), vm) } func (hc *Http) Do(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { - args := MakeArgs(&argsIn, vm) - if err := args.Check(3); err != nil { - return err - } + args := MakeArgs(&argsIn, vm).Check(3) if len(argsIn.Arguments) == 3 { argsIn.Arguments = append(argsIn.Arguments, vm.ToValue(nil)) } @@ -122,10 +104,7 @@ func (hc *Http) Do(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { } func (hc *Http) Upload(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { - args := MakeArgs(&argsIn, vm) - if err := args.Check(2); err != nil { - return err - } + args := MakeArgs(&argsIn, vm).Check(2) postData := map[string]string{} postFiles := map[string]any{} u.Convert(args.Any(1), &postData) @@ -137,10 +116,7 @@ func (hc *Http) Upload(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { } func (hc *Http) Download(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { - args := MakeArgs(&argsIn, vm) - if err := args.Check(2); err != nil { - return err - } + args := MakeArgs(&argsIn, vm).Check(2) var r *httpclient.Result var callback goja.Callable if len(argsIn.Arguments) > 2 { diff --git a/js/util.go b/js/util.go index e11a5cc..bf246be 100644 --- a/js/util.go +++ b/js/util.go @@ -1,35 +1,28 @@ package js import ( + "apigo.cc/ai/ai/goja" "bytes" "encoding/hex" "encoding/json" - "github.com/dop251/goja" "github.com/ssgo/u" "gopkg.in/yaml.v3" + "strings" "text/template" ) func RequireUtil() map[string]any { return map[string]any{ "json": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { - args := MakeArgs(&argsIn, vm) - if err := args.Check(1); err != nil { - return err - } - + args := MakeArgs(&argsIn, vm).Check(1) if r, err := json.Marshal(args.Arguments[0].Export()); err == nil { return vm.ToValue(string(r)) } else { - return vm.NewGoError(err) + panic(vm.NewGoError(err)) } }, "jsonP": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { - args := MakeArgs(&argsIn, vm) - if err := args.Check(1); err != nil { - return err - } - + args := MakeArgs(&argsIn, vm).Check(1) if r, err := json.Marshal(args.Arguments[0].Export()); err == nil { r1 := bytes.Buffer{} if err2 := json.Indent(&r1, r, "", " "); err2 == nil { @@ -38,145 +31,131 @@ func RequireUtil() map[string]any { return vm.ToValue(string(r)) } } else { - return vm.NewGoError(err) + panic(vm.NewGoError(err)) } }, "unJson": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { - args := MakeArgs(&argsIn, vm) - if err := args.Check(1); err != nil { - return err - } - + args := MakeArgs(&argsIn, vm).Check(1) var r any if err := json.Unmarshal(u.Bytes(args.Arguments[0].Export()), &r); err == nil { return vm.ToValue(r) } else { - return vm.NewGoError(err) + panic(vm.NewGoError(err)) } }, "yaml": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { - args := MakeArgs(&argsIn, vm) - if err := args.Check(1); err != nil { - return err - } - + args := MakeArgs(&argsIn, vm).Check(1) if r, err := yaml.Marshal(args.Arguments[0].Export()); err == nil { return vm.ToValue(string(r)) } else { - return vm.NewGoError(err) + panic(vm.NewGoError(err)) } }, "unYaml": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { - args := MakeArgs(&argsIn, vm) - if err := args.Check(1); err != nil { - return err - } - + args := MakeArgs(&argsIn, vm).Check(1) var r any if err := yaml.Unmarshal(u.Bytes(args.Arguments[0].Export()), &r); err == nil { return vm.ToValue(r) } else { - return vm.NewGoError(err) + panic(vm.NewGoError(err)) + } + }, + "save": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { + args := MakeArgs(&argsIn, vm).Check(2) + filename := args.Str(0) + var data []byte + var err error + if strings.HasSuffix(filename, ".yml") || strings.HasSuffix(filename, ".yaml") { + data, err = yaml.Marshal(args.Any(1)) + } else { + data, err = json.Marshal(args.Any(1)) + } + if err == nil { + if err = u.WriteFileBytes(findPath(vm, filename), data); err != nil { + panic(vm.NewGoError(err)) + } + return nil + } else { + panic(vm.NewGoError(err)) + } + }, + "load": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { + args := MakeArgs(&argsIn, vm).Check(1) + filename := args.Str(0) + if data, err := u.ReadFileBytes(findPath(vm, filename)); err == nil { + var r any + if strings.HasSuffix(filename, ".yml") || strings.HasSuffix(filename, ".yaml") { + err = yaml.Unmarshal(data, &r) + } else { + err = json.Unmarshal(data, &r) + } + if err == nil { + return vm.ToValue(r) + } else { + panic(vm.NewGoError(err)) + } + } else { + panic(vm.NewGoError(err)) } }, "base64": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { - args := MakeArgs(&argsIn, vm) - if err := args.Check(1); err != nil { - return err - } - + args := MakeArgs(&argsIn, vm).Check(1) return vm.ToValue(u.Base64(u.Bytes(args.Arguments[0].Export()))) }, "unBase64": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { - args := MakeArgs(&argsIn, vm) - if err := args.Check(1); err != nil { - return err - } - + args := MakeArgs(&argsIn, vm).Check(1) return vm.ToValue(u.UnBase64String(args.Str(0))) }, "urlBase64": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { - args := MakeArgs(&argsIn, vm) - if err := args.Check(1); err != nil { - return err - } - + args := MakeArgs(&argsIn, vm).Check(1) return vm.ToValue(u.UrlBase64String(args.Str(0))) }, "unUrlBase64": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { - args := MakeArgs(&argsIn, vm) - if err := args.Check(1); err != nil { - return err - } - + args := MakeArgs(&argsIn, vm).Check(1) return vm.ToValue(u.UnUrlBase64String(args.Str(0))) }, "hex": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { - args := MakeArgs(&argsIn, vm) - if err := args.Check(1); err != nil { - return err - } - + args := MakeArgs(&argsIn, vm).Check(1) return vm.ToValue(hex.EncodeToString(u.Bytes(args.Arguments[0].Export()))) }, "unHex": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { - args := MakeArgs(&argsIn, vm) - if err := args.Check(1); err != nil { - return err - } - + args := MakeArgs(&argsIn, vm).Check(1) if r, err := hex.DecodeString(args.Str(0)); err == nil { return vm.ToValue(string(r)) } else { - return vm.NewGoError(err) + panic(vm.NewGoError(err)) } }, "aes": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { - args := MakeArgs(&argsIn, vm) - if err := args.Check(3); err != nil { - return err - } - + args := MakeArgs(&argsIn, vm).Check(3) if r, err := u.EncryptAesBytes(u.Bytes(args.Arguments[0].Export()), u.Bytes(args.Arguments[1].Export()), u.Bytes(args.Arguments[2].Export())); err == nil { return vm.ToValue(string(r)) } else { - return vm.NewGoError(err) + panic(vm.NewGoError(err)) } }, "unAes": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { - args := MakeArgs(&argsIn, vm) - if err := args.Check(3); err != nil { - return err - } - + args := MakeArgs(&argsIn, vm).Check(3) if r, err := u.DecryptAesBytes(u.Bytes(args.Arguments[0].Export()), u.Bytes(args.Arguments[1].Export()), u.Bytes(args.Arguments[2].Export())); err == nil { return vm.ToValue(string(r)) } else { - return vm.NewGoError(err) + panic(vm.NewGoError(err)) } }, "gzip": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { - args := MakeArgs(&argsIn, vm) - if err := args.Check(1); err != nil { - return err - } - + args := MakeArgs(&argsIn, vm).Check(1) if r, err := u.Gzip(u.Bytes(args.Arguments[0].Export())); err == nil { return vm.ToValue(string(r)) } else { - return vm.NewGoError(err) + panic(vm.NewGoError(err)) } }, "gunzip": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { - args := MakeArgs(&argsIn, vm) - if err := args.Check(1); err != nil { - return err - } - + args := MakeArgs(&argsIn, vm).Check(1) if r, err := u.Gunzip(u.Bytes(args.Arguments[0].Export())); err == nil { return vm.ToValue(string(r)) } else { - return vm.NewGoError(err) + panic(vm.NewGoError(err)) } }, "id": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { @@ -193,43 +172,23 @@ func RequireUtil() map[string]any { return vm.ToValue(u.MakeToken(size)) }, "md5": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { - args := MakeArgs(&argsIn, vm) - if err := args.Check(1); err != nil { - return err - } - + args := MakeArgs(&argsIn, vm).Check(1) return vm.ToValue(u.MD5(u.Bytes(args.Arguments[0].Export()))) }, "sha1": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { - args := MakeArgs(&argsIn, vm) - if err := args.Check(1); err != nil { - return err - } - + args := MakeArgs(&argsIn, vm).Check(1) return vm.ToValue(u.Sha1(u.Bytes(args.Arguments[0].Export()))) }, "sha256": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { - args := MakeArgs(&argsIn, vm) - if err := args.Check(1); err != nil { - return err - } - + args := MakeArgs(&argsIn, vm).Check(1) return vm.ToValue(u.Sha256(u.Bytes(args.Arguments[0].Export()))) }, "sha512": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { - args := MakeArgs(&argsIn, vm) - if err := args.Check(1); err != nil { - return err - } - + args := MakeArgs(&argsIn, vm).Check(1) return vm.ToValue(u.Sha512(u.Bytes(args.Arguments[0].Export()))) }, "tpl": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { - args := MakeArgs(&argsIn, vm) - if err := args.Check(2); err != nil { - return err - } - + args := MakeArgs(&argsIn, vm).Check(2) var functions = map[string]any{} if len(argsIn.Arguments) > 2 { obj := argsIn.Arguments[2].ToObject(vm) @@ -256,16 +215,12 @@ func RequireUtil() map[string]any { err = tpl.Execute(buf, args.Arguments[1].Export()) } if err != nil { - return vm.NewGoError(err) + panic(vm.NewGoError(err)) } return vm.ToValue(buf.String()) }, "shell": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { - args := MakeArgs(&argsIn, vm) - if err := args.Check(1); err != nil { - return err - } - + args := MakeArgs(&argsIn, vm).Check(1) a := make([]string, len(args.Arguments)-1) for i := 1; i < len(args.Arguments); i++ { a[i-1] = args.Str(i) @@ -274,7 +229,7 @@ func RequireUtil() map[string]any { if r, err := u.RunCommand(args.Str(0), a...); err == nil { return vm.ToValue(r) } else { - return vm.NewGoError(err) + panic(vm.NewGoError(err)) } }, }