1
0
Fork 0

Adding upstream version 0.0~git20250409.f7acab6.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-05-22 11:36:18 +02:00
parent b9b5d88025
commit 21b930d007
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
51 changed files with 11229 additions and 0 deletions

View file

@ -0,0 +1,48 @@
name: Publish type definitions to npm
inputs:
VERSION:
description: Packages version
required: true
NODE_AUTH_TOKEN:
description: Node auth token
required: true
runs:
using: composite
steps:
- name: Set variables
id: vars
shell: bash
run: |
echo "PKGS=buffer url" >> "${GITHUB_OUTPUT}"
- name: Set versions
shell: bash
run: |
jq --arg version "${{ inputs.VERSION }}" '.version = $version' global-types/package.json > global-types/tmp.json && mv global-types/tmp.json global-types/package.json
for pkg in ${{ steps.vars.outputs.PKGS }}; do
jq --arg version "${{ inputs.VERSION }}" '.version = $version' $pkg/types/package.json > $pkg/types/tmp.json && mv $pkg/types/tmp.json $pkg/types/package.json
done
- name: Update dependency versions
shell: bash
run: |
for pkg in ${{ steps.vars.outputs.PKGS }}; do
jq --arg version "${{ inputs.VERSION }}" '.dependencies."@dop251/types-goja_nodejs-global" = $version' $pkg/types/package.json > $pkg/types/tmp.json && mv $pkg/types/tmp.json $pkg/types/package.json
done
- name: Setup nodejs
uses: actions/setup-node@v2
with:
node-version: '22'
registry-url: 'https://registry.npmjs.org'
- name: Publish the packages
shell: bash
run: |
cd global-types
npm publish --access=public
cd -
for pkg in ${{ steps.vars.outputs.PKGS }}; do
cd $pkg/types
npm publish --access=public
cd -
done
env:
NODE_AUTH_TOKEN: ${{ inputs.NODE_AUTH_TOKEN }}

79
.github/workflows/main.yml vendored Normal file
View file

@ -0,0 +1,79 @@
on: [push, pull_request]
name: Test
jobs:
test:
strategy:
matrix:
go-version: [1.20.x, 1.x]
os: [ubuntu-latest, windows-latest]
arch: ["", "386"]
fail-fast: false
runs-on: ${{ matrix.os }}
steps:
- name: Install Go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
- name: Checkout code
uses: actions/checkout@v3
- name: Check formatting
run: diff -u <(echo -n) <(gofmt -d .)
if: runner.os != 'Windows'
- name: Run go vet
env:
GOARCH: ${{ matrix.arch }}
run: go vet ./...
- name: Run staticcheck
uses: dominikh/staticcheck-action@v1.3.1
with:
version: "2025.1.1"
install-go: false
cache-key: ${{ matrix.go-version }}
if: ${{ matrix.go-version == '1.x' }}
- name: Run tests
env:
GOARCH: ${{ matrix.arch }}
run: go test -vet=off ./...
test-types:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: |
npm ci
npm test --workspaces
publish-types-tagged:
name: 'Publish type definitions to npm (tagged)'
needs: [test, test-types]
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')
steps:
- uses: actions/checkout@v4
- name: Extract version
run: |
VERSION=$(echo "${{ github.ref }}" | sed 's|refs/tags/v||')
if [[ "$VERSION" == [0-9].* ]]; then
echo "version=$VERSION" >> $GITHUB_ENV
fi
- uses: './.github/actions/publish-types'
if: env.version != ''
with:
VERSION: ${{ env.version }}
NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }}
publish-types-untagged:
name: 'Publish type definitions to npm (untagged master)'
needs: [ test, test-types ]
runs-on: ubuntu-latest
if: "!startsWith(github.ref, 'refs/tags/v') && github.ref_name == 'master'"
steps:
- uses: actions/checkout@v4
- name: Extract version
run: |
VERSION=0.0.0-$(git log -1 --format=%cd --date=format:%Y%m%d%H%M%S)-$(git rev-parse --short=12 HEAD)
echo "version=$VERSION" >> $GITHUB_ENV
- uses: './.github/actions/publish-types'
with:
VERSION: ${{ env.version }}
NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }}

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
.idea
*.iml
vendor/*
node_modules

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

46
README.md Normal file
View file

@ -0,0 +1,46 @@
Nodejs compatibility library for Goja
====
This is a collection of [Goja](https://github.com/dop251/goja) modules that provide nodejs compatibility.
Example:
```go
package main
import (
"github.com/dop251/goja"
"github.com/dop251/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
}
```
Type Definitions
---
Type definitions are published to https://npmjs.com as @dop251/types-goja_nodejs-MODULE.
They only include what's been implemented so far.
To make use of them you need to install the appropriate modules and add `node_modules/@dop251` to `typeRoots` in `tsconfig.json`.
I didn't want to add those to DefinitelyTyped partly because I don't think they really belong there,
and partly because I'd like to fully control the release cycle, i.e. publish the modules by an automated CI job and
exactly at the same time as the Go code is released.
And the reason for splitting them into different packages is that the modules can be enabled or disabled individually, unlike in nodejs.
More modules will be added. Contributions welcome too.

129
assert.js Normal file
View file

@ -0,0 +1,129 @@
'use strict';
const assert = {
_isSameValue(a, b) {
if (this._isNumber(a)) {
return this._numberEquals(a, b);
}
return a === b;
},
_isNumber(val) {
return typeof val === "number";
},
_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;
}
},
_numberEquals(a, b, precision = 1e-6) {
if (!this._isNumber(b)) {
return false;
}
// Handle NaN vs. NaN
if (a !== a && b !== b) {
return true; // Both are NaN
}
// If only one is NaN, they're not equal
if (a !== a || b !== b) {
return false;
}
if (a === b) {
// Handle +/-0 vs. -/+0
return a !== 0 || 1 / a === 1 / b;
}
// Use relative error for larger numbers, absolute for smaller ones
if (Math.abs(a) > 1 || Math.abs(b) > 1) {
return Math.abs((a - b) / Math.max(Math.abs(a), Math.abs(b))) < precision;
}
// Absolute error for small numbers
return Math.abs(a - b) < precision;
},
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, checks, message) {
if (message === undefined) {
message = '';
} else {
message += ' ';
}
try {
f();
} catch (e) {
for (const check of checks) {
check(e, message);
}
return;
}
throw new Error(message + "No exception was thrown");
},
_sameErrorType(expected){
return function(e, message) {
assert.sameValue(e.constructor, expected, `${message}Wrong exception type was thrown:`);
}
},
_sameErrorCode(expected){
return function(e, message) {
assert.sameValue(e.code, expected, `${message}Wrong exception code was thrown:`);
}
},
_sameErrorMessage(expected){
return function(e, message) {
assert.sameValue(e.message, expected, `${message}Wrong exception message was thrown:`);
}
},
throws(f, ctor, message) {
return this._throws(f, [
this._sameErrorType(ctor)
], message);
},
throwsNodeError(f, ctor, code, message) {
return this._throws(f, [
this._sameErrorType(ctor),
this._sameErrorCode(code)
], message);
},
throwsNodeErrorWithMessage(f, ctor, code, errorMessage, message) {
return this._throws(f, [
this._sameErrorType(ctor),
this._sameErrorCode(code),
this._sameErrorMessage(errorMessage)
], message);
}
}
module.exports = assert;

1198
buffer/buffer.go Normal file

File diff suppressed because it is too large Load diff

2430
buffer/buffer_test.go Normal file

File diff suppressed because it is too large Load diff

34
buffer/testdata/assertions.js vendored Normal file
View file

@ -0,0 +1,34 @@
/**
* Assertion helper functions for Buffer tests
*/
"use strict";
const assert = require("../../assert.js");
function assertValueRead(actual, expected) {
assert.sameValue(actual, expected, "value read does not match; ")
}
function assertBytesWritten(actual, expected) {
assert.sameValue(actual, expected, "bytesWritten does not match; ")
}
function assertBufferWriteRead(buffer, writeMethod, readMethod, value, offset = 0) {
const bytesWritten = buffer[writeMethod](value, offset);
const bytesPerElement = getBufferElementSize(writeMethod);
assertBytesWritten(bytesWritten, offset + bytesPerElement);
const readValue = buffer[readMethod](offset);
assertValueRead(readValue, value);
}
// getBufferElementSize determines the number of bytes per type based on method name
function getBufferElementSize(methodName) {
if (methodName.includes('64')) return 8;
if (methodName.includes('Double')) return 8;
if (methodName.includes('32')) return 4;
if (methodName.includes('Float')) return 4;
if (methodName.includes('16')) return 2;
if (methodName.includes('8')) return 1;
return 1;
}

10
buffer/types/README.md Normal file
View file

@ -0,0 +1,10 @@
## Type definitions for the goja_nodejs buffer module.
This package contains type definitions which only include features
currently implemented by the goja_nodejs buffer module.
### Install
```shell
npm install --save-dev @dop251/types-goja_nodejs-buffer
```

210
buffer/types/buffer.buffer.d.ts vendored Normal file
View file

@ -0,0 +1,210 @@
declare module "buffer" {
type ImplicitArrayBuffer<T extends WithImplicitCoercion<ArrayBufferLike>> = T extends
{ valueOf(): infer V extends ArrayBufferLike } ? V : T;
global {
interface BufferConstructor {
// see buffer.d.ts for implementation shared with all TypeScript versions
/**
* Allocates a new buffer containing the given {str}.
*
* @param str String to store in buffer.
* @param encoding encoding to use, optional. Default is 'utf8'
* @deprecated since v10.0.0 - Use `Buffer.from(string[, encoding])` instead.
*/
new(str: string, encoding?: BufferEncoding): Buffer<ArrayBuffer>;
/**
* Allocates a new buffer containing the given {array} of octets.
*
* @param array The octets to store.
* @deprecated since v10.0.0 - Use `Buffer.from(array)` instead.
*/
new(array: ArrayLike<number>): Buffer<ArrayBuffer>;
/**
* Produces a Buffer backed by the same allocated memory as
* the given {ArrayBuffer}/{SharedArrayBuffer}.
*
* @param arrayBuffer The ArrayBuffer with which to share memory.
* @deprecated since v10.0.0 - Use `Buffer.from(arrayBuffer[, byteOffset[, length]])` instead.
*/
new<TArrayBuffer extends ArrayBufferLike = ArrayBuffer>(arrayBuffer: TArrayBuffer): Buffer<TArrayBuffer>;
/**
* Allocates a new `Buffer` using an `array` of bytes in the range `0` `255`.
* Array entries outside that range will be truncated to fit into it.
*
* ```js
* import { Buffer } from 'node:buffer';
*
* // Creates a new Buffer containing the UTF-8 bytes of the string 'buffer'.
* const buf = Buffer.from([0x62, 0x75, 0x66, 0x66, 0x65, 0x72]);
* ```
*
* If `array` is an `Array`-like object (that is, one with a `length` property of
* type `number`), it is treated as if it is an array, unless it is a `Buffer` or
* a `Uint8Array`. This means all other `TypedArray` variants get treated as an
* `Array`. To create a `Buffer` from the bytes backing a `TypedArray`, use
* `Buffer.copyBytesFrom()`.
*
* A `TypeError` will be thrown if `array` is not an `Array` or another type
* appropriate for `Buffer.from()` variants.
*
* `Buffer.from(array)` and `Buffer.from(string)` may also use the internal
* `Buffer` pool like `Buffer.allocUnsafe()` does.
* @since v5.10.0
*/
from(array: WithImplicitCoercion<ArrayLike<number>>): Buffer<ArrayBuffer>;
/**
* This creates a view of the `ArrayBuffer` without copying the underlying
* memory. For example, when passed a reference to the `.buffer` property of a
* `TypedArray` instance, the newly created `Buffer` will share the same
* allocated memory as the `TypedArray`'s underlying `ArrayBuffer`.
*
* ```js
* import { Buffer } from 'node:buffer';
*
* const arr = new Uint16Array(2);
*
* arr[0] = 5000;
* arr[1] = 4000;
*
* // Shares memory with `arr`.
* const buf = Buffer.from(arr.buffer);
*
* console.log(buf);
* // Prints: <Buffer 88 13 a0 0f>
*
* // Changing the original Uint16Array changes the Buffer also.
* arr[1] = 6000;
*
* console.log(buf);
* // Prints: <Buffer 88 13 70 17>
* ```
*
* The optional `byteOffset` and `length` arguments specify a memory range within
* the `arrayBuffer` that will be shared by the `Buffer`.
*
* ```js
* import { Buffer } from 'node:buffer';
*
* const ab = new ArrayBuffer(10);
* const buf = Buffer.from(ab, 0, 2);
*
* console.log(buf.length);
* // Prints: 2
* ```
*
* A `TypeError` will be thrown if `arrayBuffer` is not an `ArrayBuffer` or a
* `SharedArrayBuffer` or another type appropriate for `Buffer.from()`
* variants.
*
* It is important to remember that a backing `ArrayBuffer` can cover a range
* of memory that extends beyond the bounds of a `TypedArray` view. A new
* `Buffer` created using the `buffer` property of a `TypedArray` may extend
* beyond the range of the `TypedArray`:
*
* ```js
* import { Buffer } from 'node:buffer';
*
* const arrA = Uint8Array.from([0x63, 0x64, 0x65, 0x66]); // 4 elements
* const arrB = new Uint8Array(arrA.buffer, 1, 2); // 2 elements
* console.log(arrA.buffer === arrB.buffer); // true
*
* const buf = Buffer.from(arrB.buffer);
* console.log(buf);
* // Prints: <Buffer 63 64 65 66>
* ```
* @since v5.10.0
* @param arrayBuffer An `ArrayBuffer`, `SharedArrayBuffer`, for example the
* `.buffer` property of a `TypedArray`.
* @param byteOffset Index of first byte to expose. **Default:** `0`.
* @param length Number of bytes to expose. **Default:**
* `arrayBuffer.byteLength - byteOffset`.
*/
from<TArrayBuffer extends WithImplicitCoercion<ArrayBufferLike>>(
arrayBuffer: TArrayBuffer,
byteOffset?: number,
length?: number,
): Buffer<ImplicitArrayBuffer<TArrayBuffer>>;
/**
* Creates a new `Buffer` containing `string`. The `encoding` parameter identifies
* the character encoding to be used when converting `string` into bytes.
*
* ```js
* import { Buffer } from 'node:buffer';
*
* const buf1 = Buffer.from('this is a tést');
* const buf2 = Buffer.from('7468697320697320612074c3a97374', 'hex');
*
* console.log(buf1.toString());
* // Prints: this is a tést
* console.log(buf2.toString());
* // Prints: this is a tést
* console.log(buf1.toString('latin1'));
* // Prints: this is a tést
* ```
*
* A `TypeError` will be thrown if `string` is not a string or another type
* appropriate for `Buffer.from()` variants.
*
* `Buffer.from(string)` may also use the internal `Buffer` pool like
* `Buffer.allocUnsafe()` does.
* @since v5.10.0
* @param string A string to encode.
* @param encoding The encoding of `string`. **Default:** `'utf8'`.
*/
from(string: WithImplicitCoercion<string>, encoding?: BufferEncoding): Buffer<ArrayBuffer>;
/**
* Allocates a new `Buffer` of `size` bytes. If `fill` is `undefined`, the`Buffer` will be zero-filled.
*
* ```js
* import { Buffer } from 'node:buffer';
*
* const buf = Buffer.alloc(5);
*
* console.log(buf);
* // Prints: <Buffer 00 00 00 00 00>
* ```
*
* If `size` is larger than {@link constants.MAX_LENGTH} or smaller than 0, `ERR_OUT_OF_RANGE` is thrown.
*
* If `fill` is specified, the allocated `Buffer` will be initialized by calling `buf.fill(fill)`.
*
* ```js
* import { Buffer } from 'node:buffer';
*
* const buf = Buffer.alloc(5, 'a');
*
* console.log(buf);
* // Prints: <Buffer 61 61 61 61 61>
* ```
*
* If both `fill` and `encoding` are specified, the allocated `Buffer` will be
* initialized by calling `buf.fill(fill, encoding)`.
*
* ```js
* import { Buffer } from 'node:buffer';
*
* const buf = Buffer.alloc(11, 'aGVsbG8gd29ybGQ=', 'base64');
*
* console.log(buf);
* // Prints: <Buffer 68 65 6c 6c 6f 20 77 6f 72 6c 64>
* ```
*
* Calling `Buffer.alloc()` can be measurably slower than the alternative `Buffer.allocUnsafe()` but ensures that the newly created `Buffer` instance
* contents will never contain sensitive data from previous allocations, including
* data that might not have been allocated for `Buffer`s.
*
* A `TypeError` will be thrown if `size` is not a number.
* @since v5.10.0
* @param size The desired length of the new `Buffer`.
* @param [fill=0] A value to pre-fill the new `Buffer` with.
* @param [encoding='utf8'] If `fill` is a string, this is its encoding.
*/
alloc(size: number, fill?: string | Uint8Array | number, encoding?: BufferEncoding): Buffer<ArrayBuffer>;
}
interface Buffer<TArrayBuffer extends ArrayBufferLike = ArrayBufferLike> extends Uint8Array<TArrayBuffer> {
// see buffer.d.ts for implementation shared with all TypeScript versions
}
}
}

1318
buffer/types/buffer.d.ts vendored Normal file

File diff suppressed because it is too large Load diff

19
buffer/types/package.json Normal file
View file

@ -0,0 +1,19 @@
{
"name": "@dop251/types-goja_nodejs-buffer",
"version": "0.0.1-rc2",
"types": "buffer.d.ts",
"scripts": {
"test": "tsc"
},
"repository": {
"type": "git",
"url": "git+https://github.com/dop251/goja_nodejs.git"
},
"dependencies": {
"@dop251/types-goja_nodejs-global": "0.0.1-rc2"
},
"devDependencies": {
"typescript": "next"
},
"private": false
}

View file

@ -0,0 +1,16 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "esnext",
"lib": [
"es6",
"dom"
],
"noImplicitAny": true,
"noImplicitThis": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noEmit": true,
"forceConsistentCasingInFileNames": true
}
}

72
console/module.go Normal file
View file

@ -0,0 +1,72 @@
package console
import (
"github.com/dop251/goja"
"github.com/dop251/goja_nodejs/require"
"github.com/dop251/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)
}

78
console/module_test.go Normal file
View file

@ -0,0 +1,78 @@
package console
import (
"testing"
"github.com/dop251/goja"
"github.com/dop251/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)
}
}

38
console/std_printer.go Normal file
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)
}

97
errors/errors.go Normal file
View file

@ -0,0 +1,97 @@
package errors
import (
"fmt"
"github.com/dop251/goja"
)
const (
ErrCodeInvalidArgType = "ERR_INVALID_ARG_TYPE"
ErrCodeInvalidArgValue = "ERR_INVALID_ARG_VALUE"
ErrCodeInvalidThis = "ERR_INVALID_THIS"
ErrCodeMissingArgs = "ERR_MISSING_ARGS"
ErrCodeOutOfRange = "ERR_OUT_OF_RANGE"
)
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 NewRangeError(r *goja.Runtime, code string, params ...interface{}) *goja.Object {
ctor, _ := r.Get("RangeError").(*goja.Object)
return NewError(r, ctor, code, params...)
}
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
}
func NewArgumentNotBigIntTypeError(r *goja.Runtime, name string) *goja.Object {
return NewNotCorrectTypeError(r, name, "BigInt")
}
func NewArgumentNotStringTypeError(r *goja.Runtime, name string) *goja.Object {
return NewNotCorrectTypeError(r, name, "string")
}
func NewArgumentNotNumberTypeError(r *goja.Runtime, name string) *goja.Object {
return NewNotCorrectTypeError(r, name, "number")
}
func NewNotCorrectTypeError(r *goja.Runtime, name, _type string) *goja.Object {
return NewTypeError(r, ErrCodeInvalidArgType, "The \"%s\" argument must be of type %s.", name, _type)
}
func NewArgumentOutOfRangeError(r *goja.Runtime, name string, v any) *goja.Object {
return NewRangeError(r, ErrCodeOutOfRange, "The value of \"%s\" %v is out of range.", name, v)
}

514
eventloop/eventloop.go Normal file
View file

@ -0,0 +1,514 @@
package eventloop
import (
"sync"
"sync/atomic"
"time"
"github.com/dop251/goja"
"github.com/dop251/goja_nodejs/console"
"github.com/dop251/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)
}
}

641
eventloop/eventloop_test.go Normal file
View file

@ -0,0 +1,641 @@
package eventloop
import (
"fmt"
"sync/atomic"
"testing"
"time"
"github.com/dop251/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()
}

3
global-types/README.md Normal file
View file

@ -0,0 +1,3 @@
## Core type definitions for goja_nodejs.
This package is used by other type definition packages for goja_nodejs. You probably do not need to install it directly.

16
global-types/globals.d.ts vendored Normal file
View file

@ -0,0 +1,16 @@
export {};
declare global {
namespace GojaNodeJS {
interface Iterator<T, TReturn = any, TNext = any> extends IteratorObject<T, TReturn, TNext> {
[Symbol.iterator](): GojaNodeJS.Iterator<T, TReturn, TNext>;
}
// Polyfill for TS 5.6's instrinsic BuiltinIteratorReturn type, required for DOM-compatible iterators
type BuiltinIteratorReturn = ReturnType<any[][typeof Symbol.iterator]> extends
globalThis.Iterator<any, infer TReturn> ? TReturn
: any;
}
}

16
global-types/package.json Normal file
View file

@ -0,0 +1,16 @@
{
"name": "@dop251/types-goja_nodejs-global",
"version": "0.0.1-rc2",
"types": "globals.d.ts",
"scripts": {
"test": "tsc --noEmit"
},
"devDependencies": {
"typescript": "next"
},
"repository": {
"type": "git",
"url": "git+https://github.com/dop251/goja_nodejs.git"
},
"private": false
}

View file

@ -0,0 +1,18 @@
{
"files": ["globals.d.ts"],
"compilerOptions": {
"module": "commonjs",
"target": "esnext",
"lib": [
"es6",
"dom"
],
"noImplicitAny": true,
"noImplicitThis": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"types": [],
"noEmit": true,
"forceConsistentCasingInFileNames": true
}
}

17
go.mod Normal file
View file

@ -0,0 +1,17 @@
module github.com/dop251/goja_nodejs
go 1.20
require (
github.com/dop251/base64dec v0.0.0-20231022112746-c6c9f9a96217
github.com/dop251/goja v0.0.0-20250309171923-bcd7cc6bf64c
go.uber.org/goleak v1.3.0
golang.org/x/net v0.27.0
golang.org/x/text v0.16.0
)
require (
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-20240727154555-813a5fbdbec8 // indirect
)

22
go.sum Normal file
View file

@ -0,0 +1,22 @@
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dop251/base64dec v0.0.0-20231022112746-c6c9f9a96217 h1:16iT9CBDOniJwFGPI41MbUDfEk74hFaKTqudrX8kenY=
github.com/dop251/base64dec v0.0.0-20231022112746-c6c9f9a96217/go.mod h1:eIb+f24U+eWQCIsj9D/ah+MD9UP+wdxuqzsdLD+mhGM=
github.com/dop251/goja v0.0.0-20250309171923-bcd7cc6bf64c h1:mxWGS0YyquJ/ikZOjSrRjjFIbUqIP9ojyYQ+QZTU3Rg=
github.com/dop251/goja v0.0.0-20250309171923-bcd7cc6bf64c/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4=
github.com/go-sourcemap/sourcemap v2.1.4+incompatible h1:a+iTbH5auLKxaNwQFg0B+TCYl6lbukKPc7b5x0n1s6Q=
github.com/go-sourcemap/sourcemap v2.1.4+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 h1:FKHo8hFI3A+7w0aUQuYXQ+6EN5stWmeY/AZqtM8xk9k=
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

84
goutil/argtypes.go Normal file
View file

@ -0,0 +1,84 @@
package goutil
import (
"math/big"
"github.com/dop251/goja"
"github.com/dop251/goja_nodejs/errors"
)
func RequiredIntegerArgument(r *goja.Runtime, call goja.FunctionCall, name string, argIndex int) int64 {
arg := call.Argument(argIndex)
if goja.IsNumber(arg) {
return arg.ToInteger()
}
if goja.IsUndefined(arg) {
panic(errors.NewTypeError(r, errors.ErrCodeInvalidArgType, "The \"%s\" argument is required.", name))
}
panic(errors.NewArgumentNotNumberTypeError(r, name))
}
func RequiredFloatArgument(r *goja.Runtime, call goja.FunctionCall, name string, argIndex int) float64 {
arg := call.Argument(argIndex)
if goja.IsNumber(arg) {
return arg.ToFloat()
}
if goja.IsUndefined(arg) {
panic(errors.NewTypeError(r, errors.ErrCodeInvalidArgType, "The \"%s\" argument is required.", name))
}
panic(errors.NewArgumentNotNumberTypeError(r, name))
}
func CoercedIntegerArgument(call goja.FunctionCall, argIndex int, defaultValue int64, typeMistMatchValue int64) int64 {
arg := call.Argument(argIndex)
if goja.IsNumber(arg) {
return arg.ToInteger()
}
if goja.IsUndefined(arg) {
return defaultValue
}
return typeMistMatchValue
}
func OptionalIntegerArgument(r *goja.Runtime, call goja.FunctionCall, name string, argIndex int, defaultValue int64) int64 {
arg := call.Argument(argIndex)
if goja.IsNumber(arg) {
return arg.ToInteger()
}
if goja.IsUndefined(arg) {
return defaultValue
}
panic(errors.NewArgumentNotNumberTypeError(r, name))
}
func RequiredBigIntArgument(r *goja.Runtime, call goja.FunctionCall, name string, argIndex int) *big.Int {
arg := call.Argument(argIndex)
if goja.IsUndefined(arg) {
panic(errors.NewTypeError(r, errors.ErrCodeInvalidArgType, "The \"%s\" argument is required.", name))
}
if !goja.IsBigInt(arg) {
panic(errors.NewArgumentNotBigIntTypeError(r, name))
}
n, _ := arg.Export().(*big.Int)
if n == nil {
n = new(big.Int)
}
return n
}
func RequiredStringArgument(r *goja.Runtime, call goja.FunctionCall, name string, argIndex int) string {
arg := call.Argument(argIndex)
if goja.IsString(arg) {
return arg.String()
}
if goja.IsUndefined(arg) {
panic(errors.NewTypeError(r, errors.ErrCodeInvalidArgType, "The \"%s\" argument is required.", name))
}
panic(errors.NewArgumentNotStringTypeError(r, name))
}

67
package-lock.json generated Normal file
View file

@ -0,0 +1,67 @@
{
"name": "goja_nodejs",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"workspaces": [
"global-types",
"url/types",
"buffer/types"
]
},
"buffer/types": {
"name": "@dop251/types-goja_nodejs-buffer",
"version": "0.0.1-rc2",
"dependencies": {
"@dop251/types-goja_nodejs-global": "0.0.1-rc2"
},
"devDependencies": {
"typescript": "next"
}
},
"global-types": {
"name": "@dop251/types-goja_nodejs-global",
"version": "0.0.1-rc2",
"devDependencies": {
"typescript": "next"
}
},
"node_modules/@dop251/types-goja_nodejs-buffer": {
"resolved": "buffer/types",
"link": true
},
"node_modules/@dop251/types-goja_nodejs-global": {
"resolved": "global-types",
"link": true
},
"node_modules/@dop251/types-goja_nodejs-url": {
"resolved": "url/types",
"link": true
},
"node_modules/typescript": {
"version": "5.9.0-dev.20250314",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.0-dev.20250314.tgz",
"integrity": "sha512-b9eLo5FjlR0BRMsYIxZYCrtTTUu97N1bh+DpQFCEm5OfRGzUg/Oc09fgct4jA4NF7R5Yg9oxWqVT90uto1TsvA==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"url/types": {
"name": "@dop251/types-goja_nodejs-url",
"version": "0.0.1-rc2",
"dependencies": {
"@dop251/types-goja_nodejs-global": "0.0.1-rc2"
},
"devDependencies": {
"typescript": "next"
}
}
}
}

8
package.json Normal file
View file

@ -0,0 +1,8 @@
{
"private": true,
"workspaces": [
"global-types",
"url/types",
"buffer/types"
]
}

37
process/module.go Normal file
View file

@ -0,0 +1,37 @@
package process
import (
"os"
"strings"
"github.com/dop251/goja"
"github.com/dop251/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)
}

68
process/module_test.go Normal file
View file

@ -0,0 +1,68 @@
package process
import (
"fmt"
"os"
"strings"
"testing"
"github.com/dop251/goja"
"github.com/dop251/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])
}
}
}

276
require/module.go Normal file
View file

@ -0,0 +1,276 @@
package require
import (
"errors"
"io"
"io/fs"
"os"
"path"
"path/filepath"
"runtime"
"sync"
"syscall"
"text/template"
js "github.com/dop251/goja"
"github.com/dop251/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)
// PathResolver is a function that should return a canonical path of the path parameter relative to the base. The base
// is expected to be already canonical as it would be a result of a previous call to the PathResolver for all cases
// except for the initial evaluation, but it's a responsibility of the caller to ensure that the name of the script
// is a canonical path. To match Node JS behaviour, it should resolve symlinks.
// The path parameter is the argument of the require() call. The returned value will be supplied to the SourceLoader.
type PathResolver func(base, path string) string
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
pathResolver PathResolver
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
}
}
// WithPathResolver sets a function which will be used to resolve paths (see PathResolver). If not specified, the
// DefaultPathResolver is used.
func WithPathResolver(pathResolver PathResolver) Option {
return func(r *Registry) {
r.pathResolver = pathResolver
}
}
// 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) {
f, err := os.Open(filename)
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)
}
// DefaultPathResolver is used if none was set (see WithPathResolver). It converts the path using filepath.FromSlash(),
// then joins it with base and resolves symlinks on the resulting path.
// Note, it does not make the path absolute, so to match nodejs behaviour, the initial script name should be set
// to an absolute path.
// The implementation is somewhat suboptimal because it runs filepath.EvalSymlinks() on the joint path, not using the
// fact that the base path is already resolved. This is because there is no way to resolve symlinks only in a portion
// of a path without re-implementing a significant part of filepath.FromSlash().
func DefaultPathResolver(base, path string) string {
p := filepath.Join(base, filepath.FromSlash(path))
if resolved, err := filepath.EvalSymlinks(p); err == nil {
p = resolved
}
return p
}
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 filepath.Ext(p) == ".json" {
s = "module.exports = JSON.parse('" + template.JSEscapeString(s) + "')"
}
source := "(function(exports,require,module,__filename,__dirname){" + 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
}

587
require/module_test.go Normal file
View file

@ -0,0 +1,587 @@
package require
import (
"bytes"
"errors"
"fmt"
"io"
"os"
"path"
"path/filepath"
"runtime"
"syscall"
"testing"
js "github.com/dop251/goja"
)
func mapFileSystemSourceLoader(files map[string]string) SourceLoader {
return func(p string) ([]byte, error) {
s, ok := files[filepath.ToSlash(p)]
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 filepath.ToSlash(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)
}
}
func TestDefaultPathResolver(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("node_modules", 0755)
if err != nil {
t.Fatal(err)
}
err = os.Mkdir("node_modules/a", 0755)
if err != nil {
t.Fatal(err)
}
err = os.Mkdir("node_modules/a/node_modules", 0755)
if err != nil {
t.Fatal(err)
}
err = os.Mkdir("node_modules/b", 0755)
if err != nil {
t.Fatal(err)
}
err = os.Symlink("../../b", "node_modules/a/node_modules/b")
if err != nil {
if runtime.GOOS == "windows" && errors.Is(err, syscall.Errno(1314)) { // ERROR_PRIVILEGE_NOT_HELD
t.Skip("Creating symlinks on Windows requires admin privileges")
}
t.Fatal(err)
}
err = os.WriteFile("node_modules/b/index.js", []byte(``), 0644)
if err != nil {
t.Fatal(err)
}
err = os.WriteFile("node_modules/a/index.js", []byte(`require('b')`), 0644)
if err != nil {
t.Fatal(err)
}
vm := js.New()
r := NewRegistry()
rr := r.Enable(vm)
_, err = rr.Require("a")
if err != nil {
t.Fatal(err)
}
}

275
require/resolve.go Normal file
View file

@ -0,0 +1,275 @@
package require
import (
"encoding/json"
"errors"
"path/filepath"
"runtime"
"strings"
js "github.com/dop251/goja"
)
const NodePrefix = "node:"
func (r *RequireModule) resolvePath(base, name string) string {
if r.r.pathResolver != nil {
return r.r.pathResolver(base, name)
}
return DefaultPathResolver(base, name)
}
// 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) {
var start string
err = nil
if !filepath.IsAbs(modpath) {
start = r.getCurrentModulePath()
}
p := r.resolvePath(start, modpath)
if isFileOrDirectoryPath(modpath) {
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(modpath)
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 := r.resolvePath(modpath, "index.js")
if module, err = r.loadModule(p); module != nil || err != nil {
return
}
p = r.resolvePath(modpath, "index.json")
return r.loadModule(p)
}
func (r *RequireModule) loadAsDirectory(modpath string) (module *js.Object, err error) {
p := r.resolvePath(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 := r.resolvePath(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(r.resolvePath(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 filepath.Base(start) != "node_modules" {
p = filepath.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 := filepath.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 filepath.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, r.runtime.ToValue(path), r.runtime.ToValue(filepath.Dir(path)))
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
require/testdata/m.js vendored Normal file
View file

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

1
staticcheck.conf Normal file
View file

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

134
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
url/module.go Normal file
View file

@ -0,0 +1,36 @@
package url
import (
"github.com/dop251/goja"
"github.com/dop251/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
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
}

387
url/testdata/url_search_params.js vendored Normal file
View file

@ -0,0 +1,387 @@
"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(URLSearchIteratorPrototype.__proto__, [][Symbol.iterator]().__proto__.__proto__);
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);
assert.sameValue(Array.from(params.keys()).length, 3);
}
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/");
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");
}

262
url/testdata/url_test.js vendored Normal file
View file

@ -0,0 +1,262 @@
"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");
assert.sameValue(myURL.toString(), "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");
myURL = new URL("file:///./abc");
assert.sameValue(myURL.pathname, "/abc");
myURL = new URL("fish://host/.");
assert.sameValue(myURL.pathname, "/");
myURL.pathname = ".";
assert.sameValue(myURL.pathname, "/");
myURL = new URL("fish://host/a/../b");
assert.sameValue(myURL.pathname, '/b');
myURL.pathname = 'a/../c';
assert.sameValue(myURL.pathname, '/c');
myURL = new URL("file://");
assert.sameValue(myURL.pathname, "/");
assert.sameValue(myURL.href, "file:///");
assert.throwsNodeError(() => {
new URL("http://");
}, TypeError, "ERR_INVALID_URL");
// myURL = new URL("fish://");
// assert.sameValue(myURL.pathname, "");
// Currently returns "fish:"
// assert.sameValue(myURL.href, "fish://");
// 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'), "");
}
{
const url = new URL("otpauth://totp");
url.pathname = 'domain.com Domain:user@domain.com';
assert.sameValue(url.toString(), 'otpauth://totp/domain.com%20Domain:user@domain.com');
}

10
url/types/README.md Normal file
View file

@ -0,0 +1,10 @@
## Type definitions for the goja_nodejs url module.
This package contains type definitions which only include features
currently implemented by the goja_nodejs url module.
### Install
```shell
npm install --save-dev @dop251/types-goja_nodejs-url
```

19
url/types/package.json Normal file
View file

@ -0,0 +1,19 @@
{
"name": "@dop251/types-goja_nodejs-url",
"version": "0.0.1-rc2",
"types": "url.d.ts",
"scripts": {
"test": "tsc --noEmit"
},
"repository": {
"type": "git",
"url": "git+https://github.com/dop251/goja_nodejs.git"
},
"dependencies": {
"@dop251/types-goja_nodejs-global": "0.0.1-rc2"
},
"devDependencies": {
"typescript": "next"
},
"private": false
}

16
url/types/tsconfig.json Normal file
View file

@ -0,0 +1,16 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "esnext",
"lib": [
"es6",
"dom"
],
"noImplicitAny": true,
"noImplicitThis": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noEmit": true,
"forceConsistentCasingInFileNames": true
}
}

543
url/types/url.d.ts vendored Normal file
View file

@ -0,0 +1,543 @@
/// <reference types="@dop251/types-goja_nodejs-global" />
declare module 'url' {
/**
* Returns the [Punycode](https://tools.ietf.org/html/rfc5891#section-4.4) ASCII serialization of the `domain`. If `domain` is an
* invalid domain, the empty string is returned.
*
* It performs the inverse operation to {@link domainToUnicode}.
*
* ```js
* import url from 'node:url';
*
* console.log(url.domainToASCII('español.com'));
* // Prints xn--espaol-zwa.com
* console.log(url.domainToASCII('中文.com'));
* // Prints xn--fiq228c.com
* console.log(url.domainToASCII('xn--iñvalid.com'));
* // Prints an empty string
* ```
* @since v7.4.0, v6.13.0
*/
function domainToASCII(domain: string): string;
/**
* Returns the Unicode serialization of the `domain`. If `domain` is an invalid
* domain, the empty string is returned.
*
* It performs the inverse operation to {@link domainToASCII}.
*
* ```js
* import url from 'node:url';
*
* console.log(url.domainToUnicode('xn--espaol-zwa.com'));
* // Prints español.com
* console.log(url.domainToUnicode('xn--fiq228c.com'));
* // Prints 中文.com
* console.log(url.domainToUnicode('xn--iñvalid.com'));
* // Prints an empty string
* ```
* @since v7.4.0, v6.13.0
*/
function domainToUnicode(domain: string): string;
/**
* Browser-compatible `URL` class, implemented by following the WHATWG URL
* Standard. [Examples of parsed URLs](https://url.spec.whatwg.org/#example-url-parsing) may be found in the Standard itself.
* The `URL` class is also available on the global object.
*
* In accordance with browser conventions, all properties of `URL` objects
* are implemented as getters and setters on the class prototype, rather than as
* data properties on the object itself. Thus, unlike `legacy urlObject`s,
* using the `delete` keyword on any properties of `URL` objects (e.g. `delete myURL.protocol`, `delete myURL.pathname`, etc) has no effect but will still
* return `true`.
* @since v7.0.0, v6.13.0
*/
class URL {
constructor(input: string | { toString: () => string }, base?: string | URL);
/**
* Gets and sets the fragment portion of the URL.
*
* ```js
* const myURL = new URL('https://example.org/foo#bar');
* console.log(myURL.hash);
* // Prints #bar
*
* myURL.hash = 'baz';
* console.log(myURL.href);
* // Prints https://example.org/foo#baz
* ```
*
* Invalid URL characters included in the value assigned to the `hash` property
* are `percent-encoded`. The selection of which characters to
* percent-encode may vary somewhat from what the {@link parse} and {@link format} methods would produce.
*/
hash: string;
/**
* Gets and sets the host portion of the URL.
*
* ```js
* const myURL = new URL('https://example.org:81/foo');
* console.log(myURL.host);
* // Prints example.org:81
*
* myURL.host = 'example.com:82';
* console.log(myURL.href);
* // Prints https://example.com:82/foo
* ```
*
* Invalid host values assigned to the `host` property are ignored.
*/
host: string;
/**
* Gets and sets the host name portion of the URL. The key difference between`url.host` and `url.hostname` is that `url.hostname` does _not_ include the
* port.
*
* ```js
* const myURL = new URL('https://example.org:81/foo');
* console.log(myURL.hostname);
* // Prints example.org
*
* // Setting the hostname does not change the port
* myURL.hostname = 'example.com';
* console.log(myURL.href);
* // Prints https://example.com:81/foo
*
* // Use myURL.host to change the hostname and port
* myURL.host = 'example.org:82';
* console.log(myURL.href);
* // Prints https://example.org:82/foo
* ```
*
* Invalid host name values assigned to the `hostname` property are ignored.
*/
hostname: string;
/**
* Gets and sets the serialized URL.
*
* ```js
* const myURL = new URL('https://example.org/foo');
* console.log(myURL.href);
* // Prints https://example.org/foo
*
* myURL.href = 'https://example.com/bar';
* console.log(myURL.href);
* // Prints https://example.com/bar
* ```
*
* Getting the value of the `href` property is equivalent to calling {@link toString}.
*
* Setting the value of this property to a new value is equivalent to creating a
* new `URL` object using `new URL(value)`. Each of the `URL` object's properties will be modified.
*
* If the value assigned to the `href` property is not a valid URL, a `TypeError` will be thrown.
*/
href: string;
/**
* Gets the read-only serialization of the URL's origin.
*
* ```js
* const myURL = new URL('https://example.org/foo/bar?baz');
* console.log(myURL.origin);
* // Prints https://example.org
* ```
*
* ```js
* const idnURL = new URL('https://測試');
* console.log(idnURL.origin);
* // Prints https://xn--g6w251d
*
* console.log(idnURL.hostname);
* // Prints xn--g6w251d
* ```
*/
readonly origin: string;
/**
* Gets and sets the password portion of the URL.
*
* ```js
* const myURL = new URL('https://abc:xyz@example.com');
* console.log(myURL.password);
* // Prints xyz
*
* myURL.password = '123';
* console.log(myURL.href);
* // Prints https://abc:123@example.com/
* ```
*
* Invalid URL characters included in the value assigned to the `password` property
* are `percent-encoded`. The selection of which characters to
* percent-encode may vary somewhat from what the {@link parse} and {@link format} methods would produce.
*/
password: string;
/**
* Gets and sets the path portion of the URL.
*
* ```js
* const myURL = new URL('https://example.org/abc/xyz?123');
* console.log(myURL.pathname);
* // Prints /abc/xyz
*
* myURL.pathname = '/abcdef';
* console.log(myURL.href);
* // Prints https://example.org/abcdef?123
* ```
*
* Invalid URL characters included in the value assigned to the `pathname` property are `percent-encoded`. The selection of which characters
* to percent-encode may vary somewhat from what the {@link parse} and {@link format} methods would produce.
*/
pathname: string;
/**
* Gets and sets the port portion of the URL.
*
* The port value may be a number or a string containing a number in the range `0` to `65535` (inclusive). Setting the value to the default port of the `URL` objects given `protocol` will
* result in the `port` value becoming
* the empty string (`''`).
*
* The port value can be an empty string in which case the port depends on
* the protocol/scheme:
*
* <omitted>
*
* Upon assigning a value to the port, the value will first be converted to a
* string using `.toString()`.
*
* If that string is invalid but it begins with a number, the leading number is
* assigned to `port`.
* If the number lies outside the range denoted above, it is ignored.
*
* ```js
* const myURL = new URL('https://example.org:8888');
* console.log(myURL.port);
* // Prints 8888
*
* // Default ports are automatically transformed to the empty string
* // (HTTPS protocol's default port is 443)
* myURL.port = '443';
* console.log(myURL.port);
* // Prints the empty string
* console.log(myURL.href);
* // Prints https://example.org/
*
* myURL.port = 1234;
* console.log(myURL.port);
* // Prints 1234
* console.log(myURL.href);
* // Prints https://example.org:1234/
*
* // Completely invalid port strings are ignored
* myURL.port = 'abcd';
* console.log(myURL.port);
* // Prints 1234
*
* // Leading numbers are treated as a port number
* myURL.port = '5678abcd';
* console.log(myURL.port);
* // Prints 5678
*
* // Non-integers are truncated
* myURL.port = 1234.5678;
* console.log(myURL.port);
* // Prints 1234
*
* // Out-of-range numbers which are not represented in scientific notation
* // will be ignored.
* myURL.port = 1e10; // 10000000000, will be range-checked as described below
* console.log(myURL.port);
* // Prints 1234
* ```
*
* Numbers which contain a decimal point,
* such as floating-point numbers or numbers in scientific notation,
* are not an exception to this rule.
* Leading numbers up to the decimal point will be set as the URL's port,
* assuming they are valid:
*
* ```js
* myURL.port = 4.567e21;
* console.log(myURL.port);
* // Prints 4 (because it is the leading number in the string '4.567e21')
* ```
*/
port: string;
/**
* Gets and sets the protocol portion of the URL.
*
* ```js
* const myURL = new URL('https://example.org');
* console.log(myURL.protocol);
* // Prints https:
*
* myURL.protocol = 'ftp';
* console.log(myURL.href);
* // Prints ftp://example.org/
* ```
*
* Invalid URL protocol values assigned to the `protocol` property are ignored.
*/
protocol: string;
/**
* Gets and sets the serialized query portion of the URL.
*
* ```js
* const myURL = new URL('https://example.org/abc?123');
* console.log(myURL.search);
* // Prints ?123
*
* myURL.search = 'abc=xyz';
* console.log(myURL.href);
* // Prints https://example.org/abc?abc=xyz
* ```
*
* Any invalid URL characters appearing in the value assigned the `search` property will be `percent-encoded`. The selection of which
* characters to percent-encode may vary somewhat from what the {@link parse} and {@link format} methods would produce.
*/
search: string;
/**
* Gets the `URLSearchParams` object representing the query parameters of the
* URL. This property is read-only but the `URLSearchParams` object it provides
* can be used to mutate the URL instance; to replace the entirety of query
* parameters of the URL, use the {@link search} setter. See `URLSearchParams` documentation for details.
*
* Use care when using `.searchParams` to modify the `URL` because,
* per the WHATWG specification, the `URLSearchParams` object uses
* different rules to determine which characters to percent-encode. For
* instance, the `URL` object will not percent encode the ASCII tilde (`~`)
* character, while `URLSearchParams` will always encode it:
*
* ```js
* const myURL = new URL('https://example.org/abc?foo=~bar');
*
* console.log(myURL.search); // prints ?foo=~bar
*
* // Modify the URL via searchParams...
* myURL.searchParams.sort();
*
* console.log(myURL.search); // prints ?foo=%7Ebar
* ```
*/
readonly searchParams: URLSearchParams;
/**
* Gets and sets the username portion of the URL.
*
* ```js
* const myURL = new URL('https://abc:xyz@example.com');
* console.log(myURL.username);
* // Prints abc
*
* myURL.username = '123';
* console.log(myURL.href);
* // Prints https://123:xyz@example.com/
* ```
*
* Any invalid URL characters appearing in the value assigned the `username` property will be `percent-encoded`. The selection of which
* characters to percent-encode may vary somewhat from what the {@link parse} and {@link format} methods would produce.
*/
username: string;
/**
* The `toString()` method on the `URL` object returns the serialized URL. The
* value returned is equivalent to that of {@link href} and {@link toJSON}.
*/
toString(): string;
/**
* The `toJSON()` method on the `URL` object returns the serialized URL. The
* value returned is equivalent to that of {@link href} and {@link toString}.
*
* This method is automatically called when an `URL` object is serialized
* with [`JSON.stringify()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify).
*
* ```js
* const myURLs = [
* new URL('https://www.example.com'),
* new URL('https://test.example.org'),
* ];
* console.log(JSON.stringify(myURLs));
* // Prints ["https://www.example.com/","https://test.example.org/"]
* ```
*/
toJSON(): string;
}
interface URLSearchParamsIterator<T> extends GojaNodeJS.Iterator<T, GojaNodeJS.BuiltinIteratorReturn, unknown> {
[Symbol.iterator](): URLSearchParamsIterator<T>;
}
/**
* The `URLSearchParams` API provides read and write access to the query of a `URL`. The `URLSearchParams` class can also be used standalone with one of the
* four following constructors.
* The `URLSearchParams` class is also available on the global object.
*
* The WHATWG `URLSearchParams` interface and the `querystring` module have
* similar purpose, but the purpose of the `querystring` module is more
* general, as it allows the customization of delimiter characters (`&#x26;` and `=`).
* On the other hand, this API is designed purely for URL query strings.
*
* ```js
* const myURL = new URL('https://example.org/?abc=123');
* console.log(myURL.searchParams.get('abc'));
* // Prints 123
*
* myURL.searchParams.append('abc', 'xyz');
* console.log(myURL.href);
* // Prints https://example.org/?abc=123&#x26;abc=xyz
*
* myURL.searchParams.delete('abc');
* myURL.searchParams.set('a', 'b');
* console.log(myURL.href);
* // Prints https://example.org/?a=b
*
* const newSearchParams = new URLSearchParams(myURL.searchParams);
* // The above is equivalent to
* // const newSearchParams = new URLSearchParams(myURL.search);
*
* newSearchParams.append('a', 'c');
* console.log(myURL.href);
* // Prints https://example.org/?a=b
* console.log(newSearchParams.toString());
* // Prints a=b&#x26;a=c
*
* // newSearchParams.toString() is implicitly called
* myURL.search = newSearchParams;
* console.log(myURL.href);
* // Prints https://example.org/?a=b&#x26;a=c
* newSearchParams.delete('a');
* console.log(myURL.href);
* // Prints https://example.org/?a=b&#x26;a=c
* ```
* @since v7.5.0, v6.13.0
*/
class URLSearchParams implements Iterable<[string, string]> {
constructor(
init?:
| URLSearchParams
| string
| Record<string, string | readonly string[]>
| Iterable<[string, string]>
| ReadonlyArray<[string, string]>,
);
/**
* Append a new name-value pair to the query string.
*/
append(name: string, value: string): void;
/**
* If `value` is provided, removes all name-value pairs
* where name is `name` and value is `value`.
*
* If `value` is not provided, removes all name-value pairs whose name is `name`.
*/
delete(name: string, value?: string): void;
/**
* Returns an ES6 `Iterator` over each of the name-value pairs in the query.
* Each item of the iterator is a JavaScript `Array`. The first item of the `Array` is the `name`, the second item of the `Array` is the `value`.
*
* Alias for `urlSearchParams[@@iterator]()`.
*/
entries(): URLSearchParamsIterator<[string, string]>;
/**
* Iterates over each name-value pair in the query and invokes the given function.
*
* ```js
* const myURL = new URL('https://example.org/?a=b&#x26;c=d');
* myURL.searchParams.forEach((value, name, searchParams) => {
* console.log(name, value, myURL.searchParams === searchParams);
* });
* // Prints:
* // a b true
* // c d true
* ```
* @param fn Invoked for each name-value pair in the query
* @param thisArg To be used as `this` value for when `fn` is called
*/
forEach<TThis = this>(
fn: (this: TThis, value: string, name: string, searchParams: URLSearchParams) => void,
thisArg?: TThis,
): void;
/**
* Returns the value of the first name-value pair whose name is `name`. If there
* are no such pairs, `null` is returned.
* @return or `null` if there is no name-value pair with the given `name`.
*/
get(name: string): string | null;
/**
* Returns the values of all name-value pairs whose name is `name`. If there are
* no such pairs, an empty array is returned.
*/
getAll(name: string): string[];
/**
* Checks if the `URLSearchParams` object contains key-value pair(s) based on `name` and an optional `value` argument.
*
* If `value` is provided, returns `true` when name-value pair with
* same `name` and `value` exists.
*
* If `value` is not provided, returns `true` if there is at least one name-value
* pair whose name is `name`.
*/
has(name: string, value?: string): boolean;
/**
* Returns an ES6 `Iterator` over the names of each name-value pair.
*
* ```js
* const params = new URLSearchParams('foo=bar&#x26;foo=baz');
* for (const name of params.keys()) {
* console.log(name);
* }
* // Prints:
* // foo
* // foo
* ```
*/
keys(): URLSearchParamsIterator<string>;
/**
* Sets the value in the `URLSearchParams` object associated with `name` to `value`. If there are any pre-existing name-value pairs whose names are `name`,
* set the first such pair's value to `value` and remove all others. If not,
* append the name-value pair to the query string.
*
* ```js
* const params = new URLSearchParams();
* params.append('foo', 'bar');
* params.append('foo', 'baz');
* params.append('abc', 'def');
* console.log(params.toString());
* // Prints foo=bar&#x26;foo=baz&#x26;abc=def
*
* params.set('foo', 'def');
* params.set('xyz', 'opq');
* console.log(params.toString());
* // Prints foo=def&#x26;abc=def&#x26;xyz=opq
* ```
*/
set(name: string, value: string): void;
/**
* The total number of parameter entries.
* @since v19.8.0
*/
readonly size: number;
/**
* Sort all existing name-value pairs in-place by their names. Sorting is done
* with a [stable sorting algorithm](https://en.wikipedia.org/wiki/Sorting_algorithm#Stability), so relative order between name-value pairs
* with the same name is preserved.
*
* This method can be used, in particular, to increase cache hits.
*
* ```js
* const params = new URLSearchParams('query[]=abc&#x26;type=search&#x26;query[]=123');
* params.sort();
* console.log(params.toString());
* // Prints query%5B%5D=abc&#x26;query%5B%5D=123&#x26;type=search
* ```
* @since v7.7.0, v6.13.0
*/
sort(): void;
/**
* Returns the search parameters serialized as a string, with characters
* percent-encoded where necessary.
*/
toString(): string;
/**
* Returns an ES6 `Iterator` over the values of each name-value pair.
*/
values(): URLSearchParamsIterator<string>;
[Symbol.iterator](): URLSearchParamsIterator<[string, string]>;
}
}
declare module "node:url" {
export * from "url";
}

417
url/url.go Normal file
View file

@ -0,0 +1,417 @@
package url
import (
"math"
"net/url"
"path"
"reflect"
"strconv"
"strings"
"github.com/dop251/goja"
"github.com/dop251/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 isSpecialNetProtocol(protocol string) bool {
switch protocol {
case "https", "http", "ftp", "wss", "ws":
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 isSpecialNetProtocol(u.Scheme) && u.Host == "" && u.Path == "" {
panic(m.newInvalidURLError(InvalidURL, 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 cleanPath(p, proto string) string {
if !strings.HasPrefix(p, "/") && (isSpecialProtocol(proto) || p != "") {
p = "/" + p
}
if p != "" {
return path.Clean(p)
}
return ""
}
func (m *urlModule) fixURL(u *url.URL) {
u.Path = cleanPath(u.Path, u.Scheme)
if isSpecialNetProtocol(u.Scheme) {
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) {
u.url.Path = cleanPath(arg.String(), u.url.Scheme)
})
// 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
url/url_test.go Normal file
View file

@ -0,0 +1,122 @@
package url
import (
_ "embed"
"testing"
"github.com/dop251/goja"
"github.com/dop251/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)
}
}

408
url/urlsearchparams.go Normal file
View file

@ -0,0 +1,408 @@
package url
import (
"reflect"
"sort"
"github.com/dop251/goja_nodejs/errors"
"github.com/dop251/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 newInvalidCallbackTypeError(r *goja.Runtime) *goja.Object {
return errors.NewNotCorrectTypeError(r, "callback", "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 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(newInvalidCallbackTypeError(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 getIteratorPrototype(r *goja.Runtime) (iteratorProto *goja.Object) {
ar := r.NewArray()
if fn, ok := goja.AssertFunction(ar.GetSymbol(goja.SymIterator)); ok {
iter, err := fn(ar)
if err != nil {
panic(err)
}
iteratorProto = iter.ToObject(r).Prototype()
if iteratorProto == nil {
panic(r.NewTypeError("[][Symbol.iterator().__proto__ is null"))
}
iteratorProto = iteratorProto.Prototype()
if iteratorProto == nil {
panic(r.NewTypeError("[][Symbol.iterator().__proto__.__proto__ is null"))
}
} else {
panic(r.NewTypeError("[][Symbol.iterator is not a function"))
}
return
}
func (m *urlModule) getURLSearchParamsIteratorPrototype() *goja.Object {
if m.URLSearchParamsIteratorPrototype != nil {
return m.URLSearchParamsIteratorPrototype
}
p := m.r.NewObject()
p.SetPrototype(getIteratorPrototype(m.r))
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"
"github.com/dop251/goja"
"github.com/dop251/goja_nodejs/console"
"github.com/dop251/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
util/module.go Normal file
View file

@ -0,0 +1,104 @@
package util
import (
"bytes"
"github.com/dop251/goja"
"github.com/dop251/goja_nodejs/require"
)
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)
}

74
util/module_test.go Normal file
View file

@ -0,0 +1,74 @@
package util
import (
"bytes"
"testing"
"github.com/dop251/goja"
"github.com/dop251/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)
}
}
}
}