update throw exception

add util.load and util.save
This commit is contained in:
Star 2024-09-24 13:34:32 +08:00
parent 5faf209709
commit 767d87ac3e
96 changed files with 5755 additions and 375 deletions

23
go.mod
View File

@ -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

View File

@ -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.

View File

@ -7,7 +7,7 @@ import (
"reflect"
"strconv"
"github.com/dop251/goja/unistring"
"apigo.cc/ai/ai/goja/unistring"
)
type arrayIterObject struct {

View File

@ -8,7 +8,7 @@ import (
"sort"
"strconv"
"github.com/dop251/goja/unistring"
"apigo.cc/ai/ai/goja/unistring"
)
type sparseArrayItem struct {

View File

@ -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.

View File

@ -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

View File

@ -9,7 +9,7 @@ import (
"strconv"
"sync"
"github.com/dop251/goja/unistring"
"apigo.cc/ai/ai/goja/unistring"
)
type valueBigInt big.Int

View File

@ -1,6 +1,6 @@
package goja
import "github.com/dop251/goja/unistring"
import "apigo.cc/ai/ai/goja/unistring"
const propNameStack = "stack"

View File

@ -10,7 +10,7 @@ import (
"sync"
"unicode/utf8"
"github.com/dop251/goja/unistring"
"apigo.cc/ai/ai/goja/unistring"
)
const hexUpper = "0123456789ABCDEF"

View File

@ -12,7 +12,7 @@ import (
"unicode/utf16"
"unicode/utf8"
"github.com/dop251/goja/unistring"
"apigo.cc/ai/ai/goja/unistring"
)
const hex = "0123456789abcdef"

View File

@ -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 {

View File

@ -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()

View File

@ -1,7 +1,7 @@
package goja
import (
"github.com/dop251/goja/unistring"
"apigo.cc/ai/ai/goja/unistring"
)
type nativeProxyHandler struct {

View File

@ -1,8 +1,8 @@
package goja
import (
"apigo.cc/ai/ai/goja/parser"
"fmt"
"github.com/dop251/goja/parser"
"regexp"
"strings"
"unicode/utf16"

View File

@ -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"

View File

@ -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"))

View File

@ -8,7 +8,7 @@ import (
"sync"
"unsafe"
"github.com/dop251/goja/unistring"
"apigo.cc/ai/ai/goja/unistring"
)
type typedArraySortCtx struct {

View File

@ -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

View File

@ -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 {

View File

@ -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) {

View File

@ -6,7 +6,7 @@ import (
"testing"
"unsafe"
"github.com/dop251/goja/unistring"
"apigo.cc/ai/ai/goja/unistring"
)
const TESTLIB = `

View File

@ -1,7 +1,7 @@
package goja
import (
"github.com/dop251/goja/unistring"
"apigo.cc/ai/ai/goja/unistring"
"reflect"
)

View File

@ -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.

View File

@ -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

View File

@ -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(),
//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(),
// })
//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
// return
//} 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
}

View File

@ -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

View File

@ -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")

View File

@ -6,7 +6,7 @@ import (
"reflect"
"sort"
"github.com/dop251/goja/unistring"
"apigo.cc/ai/ai/goja/unistring"
)
const (

View File

@ -1,6 +1,6 @@
package goja
import "github.com/dop251/goja/unistring"
import "apigo.cc/ai/ai/goja/unistring"
type argumentsObject struct {
baseObject

View File

@ -5,7 +5,7 @@ import (
"reflect"
"strconv"
"github.com/dop251/goja/unistring"
"apigo.cc/ai/ai/goja/unistring"
)
/*

View File

@ -4,7 +4,7 @@ import (
"reflect"
"strconv"
"github.com/dop251/goja/unistring"
"apigo.cc/ai/ai/goja/unistring"
)
type objectGoArrayReflect struct {

View File

@ -3,7 +3,7 @@ package goja
import (
"reflect"
"github.com/dop251/goja/unistring"
"apigo.cc/ai/ai/goja/unistring"
)
type objectGoMapSimple struct {

View File

@ -4,7 +4,7 @@ import (
"fmt"
"reflect"
"github.com/dop251/goja/unistring"
"apigo.cc/ai/ai/goja/unistring"
)
type objectGoMapReflect struct {

View File

@ -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()

View File

@ -6,7 +6,7 @@ import (
"reflect"
"strconv"
"github.com/dop251/goja/unistring"
"apigo.cc/ai/ai/goja/unistring"
)
type objectGoSlice struct {

View File

@ -5,7 +5,7 @@ import (
"math/bits"
"reflect"
"github.com/dop251/goja/unistring"
"apigo.cc/ai/ai/goja/unistring"
)
type objectGoSliceReflect struct {

View File

@ -1,8 +1,8 @@
package goja
import (
"apigo.cc/ai/ai/goja/unistring"
"fmt"
"github.com/dop251/goja/unistring"
"math"
"reflect"
"sort"

View File

@ -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

View File

@ -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 (

View File

@ -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 {

View File

@ -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 (

View File

@ -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) {

View File

@ -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{} {

View File

@ -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.

View File

@ -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 {

View File

@ -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 {

View File

@ -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"
)

View File

@ -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

View File

@ -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"

View File

@ -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 (

View File

@ -11,7 +11,7 @@ import (
"testing"
"time"
"github.com/dop251/goja/parser"
"apigo.cc/ai/ai/goja/parser"
)
func TestGlobalObjectProto(t *testing.T) {

View File

@ -6,7 +6,7 @@ import (
"strings"
"unicode/utf8"
"github.com/dop251/goja/unistring"
"apigo.cc/ai/ai/goja/unistring"
)
const (

View File

@ -9,7 +9,7 @@ import (
"strconv"
"strings"
"github.com/dop251/goja/unistring"
"apigo.cc/ai/ai/goja/unistring"
)
type asciiString string

View File

@ -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"

View File

@ -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"
)

View File

@ -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).

View File

@ -7,7 +7,7 @@ import (
"strconv"
"unsafe"
"github.com/dop251/goja/unistring"
"apigo.cc/ai/ai/goja/unistring"
)
type byteOrder bool

View File

@ -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 (

View File

@ -10,7 +10,7 @@ import (
"sync/atomic"
"time"
"github.com/dop251/goja/unistring"
"apigo.cc/ai/ai/goja/unistring"
)
const (

View File

@ -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"
)

13
goja_nodejs/LICENSE Normal file
View File

@ -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.

32
goja_nodejs/README.md Normal file
View File

@ -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.

83
goja_nodejs/assert.js Normal file
View File

@ -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;

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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()
}

View File

@ -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)
}

View File

@ -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])
}
}
}

View File

@ -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:<name>". 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
}

View File

@ -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)
}
}

View File

@ -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
}

7
goja_nodejs/require/testdata/m.js vendored Normal file
View File

@ -0,0 +1,7 @@
function test() {
return "passed";
}
module.exports = {
test: test
}

View File

@ -0,0 +1 @@
checks = ["all", "-ST1000", "-ST1003", "-ST1005", "-ST1006", "-ST1012", "-ST1021", "-ST1020", "-ST1008"]

134
goja_nodejs/url/escape.go Normal file
View File

@ -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()
}

36
goja_nodejs/url/module.go Normal file
View File

@ -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)
}

148
goja_nodejs/url/nodeurl.go Normal file
View File

@ -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
}

View File

@ -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");
}

229
goja_nodejs/url/testdata/url_test.js vendored Normal file
View File

@ -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'), "");
}

407
goja_nodejs/url/url.go Normal file
View File

@ -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
}

122
goja_nodejs/url/url_test.go Normal file
View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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)
}
}

104
goja_nodejs/util/module.go Normal file
View File

@ -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)
}

View File

@ -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)
}
}
}
}

4
js.go
View File

@ -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"

View File

@ -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)

View File

@ -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 {

View File

@ -1,8 +1,8 @@
package js
import (
"apigo.cc/ai/ai/goja"
"fmt"
"github.com/dop251/goja"
"github.com/ssgo/u"
"strings"
)

View File

@ -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)))
},
}
}

View File

@ -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 {

View File

@ -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))
}
},
}