Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

URLSearchParams Implementation #54

Merged
merged 20 commits into from
Sep 12, 2023
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
a61bb24
Bump to latest goja version
etiennemartin Jun 6, 2023
ee4dffc
Implement URLSearchParams class along side URL
etiennemartin Jun 6, 2023
d89596d
Remove Has() method declared in later golang version
etiennemartin Jun 7, 2023
ddd22e9
Converted type reflection to match codebase
etiennemartin Jun 7, 2023
9ffbbb6
Refactored module to use nodeURL instead of native url.URL
etiennemartin Jun 12, 2023
42e1c7a
More stable tests
etiennemartin Jun 13, 2023
22f157c
Remove use of custom struct for nodeURL
etiennemartin Jun 13, 2023
685aa3a
Comment out pre-failing tests from before
etiennemartin Jun 13, 2023
f27e615
Removed the use of Export()
etiennemartin Jul 4, 2023
36b813c
Lazy load Search Parameters on URL
etiennemartin Jul 4, 2023
ec4daaa
Simplified and cleaned up some of the methods
etiennemartin Jul 31, 2023
cc55957
Removed the use of a String array from the search param struct
etiennemartin Jul 31, 2023
8c4d82b
Re-enable tests
etiennemartin Jul 31, 2023
8b99b02
Fixed parsing issue where it would excessivily split.
etiennemartin Jul 31, 2023
403311a
Removed several uses of direct access into the call.Arguments array
etiennemartin Aug 2, 2023
b3b7721
Added a few extra tests
etiennemartin Aug 2, 2023
0364a97
Removed use of Try and simplified methods that did extra work for not…
etiennemartin Aug 31, 2023
31e0720
Inline deletion of parameters
etiennemartin Aug 31, 2023
1ddbb4f
Remove the use of stringFromValue for String()
etiennemartin Sep 7, 2023
c6c0ef7
Add support for Function Generators
etiennemartin Sep 7, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/dop251/goja_nodejs
go 1.16

require (
github.com/dop251/goja v0.0.0-20230531210528-d7324b2d74f7
github.com/dop251/goja v0.0.0-20230626124041-ba8a63e79201
golang.org/x/net v0.10.0
golang.org/x/text v0.9.0
)
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnm
github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk=
github.com/dop251/goja v0.0.0-20230531210528-d7324b2d74f7 h1:cVGkvrdHgyBkYeB6kMCaF5j2d9Bg4trgbIpcUrKrvk4=
github.com/dop251/goja v0.0.0-20230531210528-d7324b2d74f7/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4=
github.com/dop251/goja v0.0.0-20230605162241-28ee0ee714f3 h1:+3HCtB74++ClLy8GgjUQYeC8R4ILzVcIe8+5edAJJnE=
github.com/dop251/goja v0.0.0-20230605162241-28ee0ee714f3/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4=
github.com/dop251/goja v0.0.0-20230626124041-ba8a63e79201 h1:+9NRIliCUhliHMCixEO0mcXmrv3HYwxs9oxM1Z+qnYM=
github.com/dop251/goja v0.0.0-20230626124041-ba8a63e79201/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4=
github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y=
github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM=
github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU=
Expand Down
326 changes: 5 additions & 321 deletions url/module.go
Original file line number Diff line number Diff line change
@@ -1,113 +1,22 @@
package url

import (
"math"
"net/url"
"reflect"
"strconv"
"strings"

"golang.org/x/net/idna"

"github.com/dop251/goja"
"github.com/dop251/goja_nodejs/require"
)

const ModuleName = "node:url"

var (
reflectTypeURL = reflect.TypeOf((*url.URL)(nil))
reflectTypeInt = reflect.TypeOf(0)
)

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 valueToURLPort(v goja.Value) (portNum int, empty bool) {
portNum = -1
if et := v.ExportType(); et == reflectTypeInt {
if num := v.ToInteger(); num >= 0 && num <= math.MaxUint16 {
portNum = int(num)
}
} else {
s := v.String()
if s == "" {
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 setURLPort(u *url.URL, v goja.Value) {
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 toURL(r *goja.Runtime, v goja.Value) *url.URL {
func toURL(r *goja.Runtime, v goja.Value) *nodeURL {
if v.ExportType() == reflectTypeURL {
if u := v.Export().(*url.URL); u != nil {
if u := v.Export().(*nodeURL); u != nil {
return u
}
}
panic(r.NewTypeError("Expected URL"))
}

func defineURLAccessorProp(r *goja.Runtime, p *goja.Object, name string, getter func(*url.URL) interface{}, setter func(*url.URL, goja.Value)) {
func defineURLAccessorProp(r *goja.Runtime, p *goja.Object, name string, getter func(*nodeURL) interface{}, setter func(*nodeURL, goja.Value)) {
var getterVal, setterVal goja.Value
if getter != nil {
getterVal = r.ToValue(func(call goja.FunctionCall) goja.Value {
Expand All @@ -123,240 +32,15 @@ func defineURLAccessorProp(r *goja.Runtime, p *goja.Object, name string, getter
p.DefineAccessorProperty(name, getterVal, setterVal, goja.FLAG_FALSE, goja.FLAG_TRUE)
}

func createURLPrototype(r *goja.Runtime) *goja.Object {
p := r.NewObject()

// host
defineURLAccessorProp(r, p, "host", func(u *url.URL) interface{} {
return u.Host
}, func(u *url.URL, arg goja.Value) {
host := arg.String()
if _, err := url.ParseRequestURI(u.Scheme + "://" + host); err == nil {
u.Host = host
fixURL(r, u)
}
})

// hash
defineURLAccessorProp(r, p, "hash", func(u *url.URL) interface{} {
if u.Fragment != "" {
return "#" + u.EscapedFragment()
}
return ""
}, func(u *url.URL, arg goja.Value) {
h := arg.String()
if len(h) > 0 && h[0] == '#' {
h = h[1:]
}
u.Fragment = h
})

// hostname
defineURLAccessorProp(r, p, "hostname", func(u *url.URL) interface{} {
return strings.Split(u.Host, ":")[0]
}, func(u *url.URL, arg goja.Value) {
h := arg.String()
if strings.IndexByte(h, ':') >= 0 {
return
}
if _, err := url.ParseRequestURI(u.Scheme + "://" + h); err == nil {
if port := u.Port(); port != "" {
u.Host = h + ":" + port
} else {
u.Host = h
}
fixURL(r, u)
}
})

// href
defineURLAccessorProp(r, p, "href", func(u *url.URL) interface{} {
return u.String()
}, func(u *url.URL, arg goja.Value) {
url := parseURL(r, arg.String(), true)
*u = *url
})

// pathname
defineURLAccessorProp(r, p, "pathname", func(u *url.URL) interface{} {
return u.EscapedPath()
}, func(u *url.URL, arg goja.Value) {
p := arg.String()
if _, err := url.Parse(p); err == nil {
switch u.Scheme {
case "https", "http", "ftp", "ws", "wss":
if !strings.HasPrefix(p, "/") {
p = "/" + p
}
}
u.Path = p
}
})

// origin
defineURLAccessorProp(r, p, "origin", func(u *url.URL) interface{} {
return u.Scheme + "://" + u.Hostname()
}, nil)

// password
defineURLAccessorProp(r, p, "password", func(u *url.URL) interface{} {
p, _ := u.User.Password()
return p
}, func(u *url.URL, arg goja.Value) {
user := u.User
u.User = url.UserPassword(user.Username(), arg.String())
})

// username
defineURLAccessorProp(r, p, "username", func(u *url.URL) interface{} {
return u.User.Username()
}, func(u *url.URL, arg goja.Value) {
p, has := u.User.Password()
if !has {
u.User = url.User(arg.String())
} else {
u.User = url.UserPassword(arg.String(), p)
}
})

// port
defineURLAccessorProp(r, p, "port", func(u *url.URL) interface{} {
return u.Port()
}, func(u *url.URL, arg goja.Value) {
setURLPort(u, arg)
})

// protocol
defineURLAccessorProp(r, p, "protocol", func(u *url.URL) interface{} {
return u.Scheme + ":"
}, func(u *url.URL, arg goja.Value) {
s := arg.String()
pos := strings.IndexByte(s, ':')
if pos >= 0 {
s = s[:pos]
}
s = strings.ToLower(s)
if isSpecialProtocol(u.Scheme) == isSpecialProtocol(s) {
if _, err := url.ParseRequestURI(s + "://" + u.Host); err == nil {
u.Scheme = s
}
}
})

// Search
defineURLAccessorProp(r, p, "search", func(u *url.URL) interface{} {
if u.RawQuery != "" {
return "?" + u.RawQuery
}
return ""
}, func(u *url.URL, arg goja.Value) {
u.RawQuery = arg.String()
fixRawQuery(u)
})

p.Set("toString", r.ToValue(func(call goja.FunctionCall) goja.Value {
return r.ToValue(toURL(r, call.This).String())
}))

p.Set("toJSON", r.ToValue(func(call goja.FunctionCall) goja.Value {
return r.ToValue(toURL(r, call.This).String())
}))

return p
}

const (
URLNotAbsolute = "URL is not absolute"
InvalidURL = "Invalid URL"
InvalidBaseURL = "Invalid base URL"
InvalidHostname = "Invalid hostname"
)

func newInvalidURLError(r *goja.Runtime, msg, input string) *goja.Object {
// when node's error module is added this should return a NodeError
o := r.NewTypeError(msg)
o.Set("input", r.ToValue(input))
return o
}

func fixRawQuery(u *url.URL) {
if u.RawQuery != "" {
var u1 url.URL
u1.Fragment = u.RawQuery
u.RawQuery = u1.EscapedFragment()
}
}

func fixURL(r *goja.Runtime, 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(newInvalidURLError(r, InvalidHostname, lh))
}
if ch != hostname {
if port := u.Port(); port != "" {
u.Host = ch + ":" + port
} else {
u.Host = ch
}
}
fixRawQuery(u)
}
}

func parseURL(r *goja.Runtime, s string, isBase bool) *url.URL {
u, err := url.Parse(s)
if err != nil {
if isBase {
panic(newInvalidURLError(r, InvalidBaseURL, s))
} else {
panic(newInvalidURLError(r, InvalidURL, s))
}
}
if isBase && !u.IsAbs() {
panic(newInvalidURLError(r, URLNotAbsolute, s))
}
if portStr := u.Port(); portStr != "" {
if port, err := strconv.Atoi(portStr); err != nil || isDefaultURLPort(u.Scheme, port) {
clearURLPort(u)
}
}
fixURL(r, u)
return u
}

func createURLConstructor(r *goja.Runtime) goja.Value {
f := r.ToValue(func(call goja.ConstructorCall) *goja.Object {
var u *url.URL
if baseArg := call.Argument(1); !goja.IsUndefined(baseArg) {
base := parseURL(r, baseArg.String(), true)
ref := parseURL(r, call.Arguments[0].String(), false)
u = base.ResolveReference(ref)
} else {
u = parseURL(r, call.Argument(0).String(), true)
}
res := r.ToValue(u).(*goja.Object)
res.SetPrototype(call.This.Prototype())
return res
}).(*goja.Object)

f.Set("prototype", createURLPrototype(r))
return f
}

func Require(runtime *goja.Runtime, module *goja.Object) {
exports := module.Get("exports").(*goja.Object)
exports.Set("URL", createURLConstructor(runtime))
exports.Set("URLSearchParams", createURLSearchParamsConstructor(runtime))
}

func Enable(runtime *goja.Runtime) {
runtime.Set("URL", require.Require(runtime, ModuleName).ToObject(runtime).Get("URL"))
runtime.Set("URLSearchParams", require.Require(runtime, ModuleName).ToObject(runtime).Get("URLSearchParams"))
}

func init() {
Expand Down
Loading
Loading