Skip to content

Latest commit

 

History

130 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

goja_ext

goja_ext 是一组可按需组合的 Go 模块,为 Goja 提供 Node.js 风格的 CommonJS 模块、Web Platform API,以及文件系统、事件循环和网络等宿主能力。

它不是完整的 Node.js 运行时,也不包含 V8、libuv 或 npm。这里的 Node 兼容指的是 “在 Goja 中提供一组常用、可嵌入的 Node API 子集”。每个模块都可以单独启用;带有 异步行为的模块必须绑定到创建该 Goja runtime 的事件循环。

本文先面向只阅读 JavaScript API 的使用者,再介绍 Go 宿主如何创建 runtime 和配置 模块。模块目录下还有更细的说明:fetch/README.mdfs/README.mdstreams/README.mdwebsocket/README.md 等。

快速判断

  • 只需要 URL、事件、Buffer、路径等同步能力:启用对应模块即可,不需要 event loop。
  • 需要 setTimeout、Promise timers、Fetch、SSE、FS 异步 API、WebSocket 或流式 网络处理:使用 eventloop.NewEventLoop,并在 loop 所属 runtime 中安装模块。
  • 需要文件分块读写:为 FS 设置 fs.WithStreams(true),使用 FsFile.readable/writablewriteFile(ReadableStream)
  • 需要完整 Node 内置模块、npm 包、process.argv、child_process 等能力:本项目 不是合适的替代品,应使用真正的 Node.js 运行时。

JavaScript API 使用者

模块总览

所有下表中的模块都支持列出的 require() 入口。多数 Node 兼容模块还支持 node: 别名;fs 明确只提供 Deno 风格入口。global 一栏表示调用 Go 的 Enable 后会额外安装的全局对象。

模块 JavaScript 入口 提供的功能 与 Node.js 的主要区别
require require("./x")、require("pkg") CommonJS、.js/.json、node_modules、Go 原生模块 只有宿主提供的 source loader 和文件内容可被加载;不自动执行 npm 安装,也没有 Node 的全部内置模块
events require("events") EventEmitter、once、on、监听器管理、Abort signal 集成 基于 bare-events 改编;行为以项目实现为准,带 signal 的部分 API 需要 abort
buffer require("buffer");global: Buffer Buffer 构造、from、alloc、concat、数字读写和编码转换 只承诺当前 codec 表;Web Streams 和 FS 字节结果仍是 Uint8Array,不会自动变成 Buffer
console require("console");global: console 常用输出、assert/dir、计数、计时、分组和 trace 输出交给 Go 的 Printer;没有 Console 流构造器、table、profile 和浏览器可展开对象视图
abort require("abort");global: AbortController、AbortSignal 取消控制、timeout、throwIfAborted 面向 Fetch 等模块的轻量实现,不承诺浏览器/Node 完整 Abort API
fetch require("fetch")、require("@microsoft/fetch-event-source");global: fetch、Headers、Request、Response、FormData HTTP Fetch、请求/响应体 Web Stream、POST SSE、超时、代理、取消 HTTP 使用 Resty 配置的 Go client;流式上传不可用于 307/308 重放,必须有 event loop
fs require("fs")、require("fs/promises");global: fs Afero-backed Deno 风格文件 API、FsFile、WHATWG 文件流 不提供 node:fs、callback、Node flags/Stats;symlink/hard link 等能力必须显式提供;字节为 Uint8Array
path require("path");另有 posix、win32 路径拼接、解析、规范化、相对路径、盘符和 UNC resolve 读取 runtime-local cwd,不调用宿主进程 chdir;只实现列出的 path API
process require("process");global: process env 快照、cwd、runtime-local chdir 当前不含 nextTick、argv、platform 等进程控制信息;不会修改宿主进程 cwd
streams require("streams")、require("stream/web")、require("node:stream/web");global: Web Streams WHATWG ReadableStream、WritableStream、TransformStream、BYOB、reader/writer、文本流 使用嵌入的 web-streams-polyfill;不提供 Node classic stream;流块通常是原生 Uint8Array
string_decoder require("string_decoder") 跨 chunk 的 utf8、utf16le/ucs2、base64、hex、latin1 解码 独立的 Go 实现,不接入 Web Streams 内部
timers require("timers")、require("timers/promises") timeout、interval、immediate、Promise timers、async iterator、scheduler.wait 必须有 event loop;顶层脚本中解构 Promise timers 的 setTimeout 可能触发 Goja TDZ 冲突
url require("url");global: URL、URLSearchParams URL 解析、查询参数、域名 ASCII/Unicode 转换 Go 原生 Node 风格子集;默认端口会规范化,完整 WHATWG 边界行为不以 Node 版本兼容为承诺
util require("util") Node 常用 format 占位符、对象 inspect、内建类型、循环引用和深度控制 不支持颜色、自定义 inspect hook 等完整选项,复杂输出不保证逐字符一致
structuredclone require("structuredclone");global: structuredClone 复制数组、对象、Map、Set、Date、RegExp、ArrayBuffer、TypedArray 和循环引用 不支持函数、Symbol、WeakMap、WeakSet、Promise 及未知宿主对象,会抛 DataCloneError
websocket global: WebSocket WebSocket 客户端、事件监听、文本消息、协议、关闭和连接管理 不是 require core module;底层是 Gorilla WebSocket,主要覆盖客户端基础生命周期,必须有 event loop
cloudflarekv global: KVNamespace、KV、SyncKV Cloudflare Workers KV 兼容的 get/put/delete/list、metadata、过期时间、Web Stream 不是 require core module;存储后端由 Go 宿主实现 store.NamespaceStore 注入;同步绑定(SyncKV)不支持 ReadableStream

常用 JavaScript 写法

启用模块后,JavaScript 侧的调用方式尽量保持 Node/Web API 的习惯:

const { URL, URLSearchParams } = require("url");
const { EventEmitter } = require("events");
const { Buffer } = require("buffer");
const { inspect } = require("util");

const url = new URL("https://example.com/search?q=goja");
const query = new URLSearchParams(url.search);
const emitter = new EventEmitter();
emitter.on("message", (value) => console.log(value));

const bytes = Buffer.from("hello", "utf8");
console.log(query.get("q"), inspect(bytes));

启用 abort 和 fetch 后可以取消异步请求:

fetch.Enable 会安装 Headers、Request、Response、FormData;真正的 fetch 函数需要 Go 宿主再调用 EnableFetch,并提供 event loop。SSE 客户端由 require("@microsoft/fetch-event-source") 提供。

const controller = new AbortController();

fetch("https://example.com/data", { signal: controller.signal })
  .then((response) => response.json())
  .then((data) => console.log(data))
  .catch((reason) => console.error(reason));

setTimeout(() => controller.abort("request cancelled"), 1000);

Fetch 的 Response.body 是 Web ReadableStream,读取时得到 Uint8Array:

(async () => {
  const response = await fetch("https://example.com/data");
  const reader = response.body.getReader();

  for (;;) {
    const { done, value } = await reader.read();
    if (done) break;
    // value 是 Uint8Array;需要字符串时显式使用 TextDecoder 或 Buffer。
    console.log(value.byteLength);
  }
})();

各模块的使用要点

require 与原生模块

require 支持相对路径、绝对路径、node_modules 查找、package.json 的 main, 以及 .js 和 .json 文件。Go 宿主可把自己的能力注册成原生模块,JavaScript 侧像普通 CommonJS 模块一样使用:

const host = require("host");
host.log("hello from JavaScript");

它不会替你安装或解析 npm 依赖;默认 source loader 读取宿主文件系统,沙箱场景应 使用自定义 WithLoader 和 WithPathResolver。

events、abort 与 console

events 提供 canonical EventEmitter。require("events").EventEmitter 与 Node streams facade 使用同一个构造器,因此可以安全地混用事件和流对象。带 signal 的 events.once/events.on 需要先安装 abort。

console 的输出通过 Go Printer 接口接收;eventloop.NewEventLoop() 默认安装 console,也可以用 eventloop.EnableConsole(false) 关闭 runtime 内的全局对象。对象参数 会自动使用 util.inspect 格式化,因此直接输出 KV 返回值时能看到嵌套字段;文本后端不具备 浏览器开发者工具的交互式展开能力。

buffer 与字节边界

需要 Node Buffer 语义时显式启用 buffer,并优先使用 Buffer.alloc()、 Buffer.from()。但是 Web Streams、Fetch 响应体和 FS 读操作的结果是原生 Uint8Array;它们不会因为 Buffer 模块已加载就自动转换。

fs、path 与 process

fs.Enable*require("fs")require("fs/promises") 使用同一份 Deno 风格 文件系统状态,包括同步/异步文件操作、目录、临时文件、FsFile 和 WHATWG 流。 异步调用只返回 Promise/AsyncIterable,不接受 Node callback。

path.resolve()、fs.cwd() 和 process.cwd() 共享 runtime-local cwd。调用 process.chdir() 或 fs.chdir() 不会改变 Go 进程的工作目录,也不会影响其他 runtime。

Web Streams

streams.Enable(rt) 安装 Web Streams 全局构造器;require("streams")require("stream/web")require("node:stream/web") 返回同一份 canonical 导出。 streamnode:streamnode:streams 不受支持。

timers

event loop 会安装全局 setTimeout、setInterval、setImmediate、对应的清理函数 和 queueMicrotask。require("timers/promises") 只是对这些调度器的 Promise 封装, 不会创建第二套计时器。

url、util 与 structuredclone

这些模块适合常见脚本和数据处理,但不是完整标准库替代品。特别是 util.inspect 的输出格式、structuredClone 的可复制对象范围和 URL 的边界解析,应按项目当前 测试和源码行为验证,不要仅凭 Node 文档推断全部细节。

websocket

new WebSocket(url) 创建客户端连接,事件通过 open、message、close、error 触发。可以使用 addEventListener/removeEventListener 或 onopen 等属性。 默认 TLS 会校验证书和主机名;私有 CA、客户端证书或测试拨号器要由 Go 宿主注入。

Go 宿主接入

安装与版本

本仓库保持 github.com/dop251/goja_nodejs 模块身份,以便调用方将直接依赖和 第三方库的间接依赖统一替换到 goja_ext。请在调用方的 go.mod 中配置:

require github.com/dop251/goja_nodejs <upstream-version>

replace github.com/dop251/goja_nodejs => github.com/sealdice/goja_ext <goja-ext-version>

然后仍使用原模块路径安装和导入:

go get github.com/dop251/goja_nodejs/eventloop

项目 go.mod 声明 Go 1.25;开发和部署需要使用 Go 1.25 或更新版本。

创建一个组合 runtime

下面的代码安装 CommonJS、Abort、Fetch、Process 和 Web Streams,并在同一个事件循环 中运行 JavaScript:

package main

import (
    "github.com/dop251/goja"
    "github.com/dop251/goja_nodejs/abort"
    "github.com/dop251/goja_nodejs/eventloop"
    "github.com/dop251/goja_nodejs/fetch"
    "github.com/dop251/goja_nodejs/process"
    "github.com/dop251/goja_nodejs/require"
    "github.com/dop251/goja_nodejs/streams"
)

func main() {
    registry := require.NewRegistry()
    loop := eventloop.NewEventLoop(
        eventloop.EnableConsole(true),
        eventloop.WithRegistry(registry),
    )

    loop.Run(func(rt *goja.Runtime) {
        abort.Enable(rt)
        fetch.Enable(rt)
        process.Enable(rt)
        streams.Enable(rt)

        if err := fetch.EnableFetch(rt, loop); err != nil {
            panic(err)
        }

        value, err := rt.RunString(`
            fetch("https://example.com/")
              .then((response) => response.text())
              .then((text) => console.log(text.length));
        `)
        if err != nil {
            panic(err)
        }
        _ = value
    })
}

实际服务通常使用 loop.Start()/loop.StartInForeground(),再通过 loop.RunOnLoop(func(*goja.Runtime) { ... }) 调度工作:

loop := eventloop.NewEventLoop(eventloop.EnableConsole(false))
loop.Start()
defer loop.Stop()

if ok := loop.RunOnLoop(func(rt *goja.Runtime) {
    // 安装模块、调用 RunString、读取或修改 Goja 值
}); !ok {
    panic("event loop is not running")
}

模块安装方式

大多数 core module 在 Go 包的 init() 中已注册到全局 core module 表。宿主只需在 所属 runtime 中调用 Enable,或直接使用 require:

abort.Enable(rt)
buffer.Enable(rt)
url.Enable(rt)

// 等价地,使用 require("abort")、require("buffer")、require("url")。

Enable 和 require(...) 在同一 runtime 内共享 canonical 构造器和导出对象,避免 AbortController、ReadableStream、EventEmitter 等出现多份身份不一致的实现。

Fetch 和 WebSocket 等需要调度器的能力必须传入所属 loop:

fetch.Enable(rt)
if err := fetch.EnableFetch(rt, loop); err != nil {
    return err
}

if err := websocket.Enable(rt, loop); err != nil {
    return err
}

接入 Cloudflare KV 兼容绑定

cloudflarekv 提供一个 Cloudflare Workers KV 风格的 JS 接口,存储后端由 Go 宿主实现 store.NamespaceStore 注入,桥接层负责 JS 值与存储记录之间的转换 (字符串/JSON/ArrayBuffer/ReadableStream、metadata、过期时间、list 分页)。

NamespaceStore 是存储适配器必须实现的基础接口:

type NamespaceStore interface {
    Put(ctx context.Context, key string, value []byte, options store.PutOptions) error
    Get(ctx context.Context, key string) (store.Record, bool, error)
    Delete(ctx context.Context, key string) error
    List(ctx context.Context, options store.ListOptions) (store.ListResult, error)
}

基础接口保持向后兼容,但其中的 Get / Put 必然完整物化 []byte。需要真正的 分块流读写或高效批量读取时,后端可以额外实现以下窄接口:

type StreamGetter interface {
    GetStream(ctx context.Context, key string) (store.StreamRecord, bool, error)
}

type StreamPutter interface {
    PutStream(ctx context.Context, key string, body io.Reader, options store.PutOptions) error
}

type BulkGetter interface {
    GetMany(ctx context.Context, keys []string) (map[string]store.Record, error)
}

StreamPutter 必须保证原子可见性:只有完整读到 EOF 且返回 nil 后才能提交值;取消、 超限或读取错误不能留下半个值。未实现可选接口时 bridge 会回退到基础接口,因此 JS API 仍可用,但 stream 回退路径会在内存中完整缓冲。

绑定方式有五种:

  • BindNamespace:将指定的 store 以 Promise API 绑定为全局对象(如 KV);
  • BindSyncNamespace:将指定的 store 以同步 API 绑定为全局对象(如 SyncKV), 便于测试或脚本场景;
  • BindNamespaceObject:将 Promise API 安装到已有 JS 对象,不创建全局变量;
  • BindSyncNamespaceObject:将同步 API 安装到已有 JS 对象,不创建全局变量;
  • InstallConstructor:安装 KVNamespace 构造器,由 resolver 按 namespace 名称解析对应的 store,模拟 Workers 的多命名空间绑定。
kv := myBackendStore // 实现 store.NamespaceStore

// 异步 API(Promise),需要 event loop:
if err := cloudflarekv.BindNamespace(rt, loop, "KV", kv); err != nil {
    return err
}

// 同步 API(无 Promise,不需要 event loop):
if err := cloudflarekv.BindSyncNamespace(rt, "SyncKV", kv); err != nil {
    return err
}

// 安装到已有对象,适用于 storage.kv / storage.synckv 等嵌套绑定:
storage := rt.NewObject()
asyncObject := rt.NewObject()
if err := cloudflarekv.BindNamespaceObject(rt, loop, asyncObject, kv); err != nil {
    return err
}
syncObject := rt.NewObject()
if err := cloudflarekv.BindSyncNamespaceObject(rt, syncObject, kv); err != nil {
    return err
}
if err := storage.Set("kv", asyncObject); err != nil {
    return err
}
if err := storage.Set("synckv", syncObject); err != nil {
    return err
}

// 多命名空间构造器:
resolver := func(namespace string) (store.NamespaceStore, error) {
    switch namespace {
    case "prod":
        return prodStore, nil
    case "staging":
        return stagingStore, nil
    }
    return nil, fmt.Errorf("unknown namespace %q", namespace)
}
if err := cloudflarekv.InstallConstructor(rt, loop, resolver); err != nil {
    return err
}

JS 侧的用法与 Cloudflare Workers KV 保持一致:get 支持 "text"(默认)、"json""arrayBuffer""stream" 四种取值类型, put 支持 expiration / expirationTtl / metadata 选项,list 返回 keyslist_complete 与可选 cursor 用于分页:

await KV.put("user:1", JSON.stringify({ name: "Ada" }), {
  expirationTtl: 3600,
  metadata: { role: "admin" },
});

const value = await KV.get("user:1", "json");        // { name: "Ada" }
const binary = await KV.get("logo.png", "arrayBuffer");
const stream = await KV.get("logs.txt", "stream");   // ReadableStream

const values = await KV.get(["user:1", "user:2"], "json");
console.log(values.get("user:1"));                    // object or null

const page = await KV.list({ prefix: "user:", limit: 100 });
for (const key of page.keys) {
  console.log(key.name, key.metadata, key.expiration);
}

await KV.delete("user:1");

JS bridge 默认执行 Cloudflare KV 的形状限制(512-byte key、25 MiB value、 1024-byte metadata、最多 100 个 bulk key、list limit 1000、过期时间至少 60 秒), 并对同一个 key 的连续 put 应用 1 秒间隔。它还带有每个 binding 独立的 64 MiB LRU 正/负缓存,默认 TTL 为 60 秒;JS 的 cacheTtl 可覆盖 TTL,但不得低于 30 秒。 这些都是 bridge 策略,Go 代码直接调用 NamespaceStore 不受影响。宿主可以按需调整或关闭:

limits := cloudflarekv.CloudflareLimits()
limits.MaxValueBytes = 8 * 1024 * 1024

err := cloudflarekv.BindNamespace(rt, loop, "KV", kv,
    cloudflarekv.WithLimits(limits),
    cloudflarekv.WithCacheCapacity(0),
    cloudflarekv.WithWriteRateLimit(0),
)

SyncKV 提供相同的同步方法(get/getWithMetadata/put/delete/list),但 get(..., "stream") 与 ReadableStream 写入不受支持,会直接抛出异常。 getWithMetadata 在键不存在时返回 { value: null, metadata: null }

Fetch 的 WithTimeout、WithProxy、WithHTTPClient、WithTransport 和 WithRestyClient 只影响 Go 侧 HTTP 执行器,不会暴露为 JavaScript 全局属性。WithTimeout 对应 Go http.Client 的请求总超时,包含完整响应体读取时间;默认值为 0,即不启用超时。SSE 等长连接不应设置 WithTimeout,应使用 AbortSignal 或 AbortSignal.timeout 主动取消。 WebSocket 的 TLS、拨号器、日志器和连接管理器可通过 EnableWithOptions 注入;默认 TLS 会校验证书和主机名。

配置 FS 与 Node 文件流

FS 后端由 Afero 注入,默认是 afero.OsFs;内存测试或沙箱常用 afero.NewMemMapFs()。配置应在同一 runtime/registry 中保持一致:

backend := afero.NewOsFs()
options := []fs.Option{
    fs.WithFS(backend),
    fs.WithCwd("/workspace"),
    fs.WithStreams(false),
}

registry := require.NewRegistry()
loop := eventloop.NewEventLoop(
    eventloop.EnableConsole(false),
    eventloop.WithRegistry(registry),
)
fs.RegisterWithLoop(registry, loop, options...)

loop.Run(func(rt *goja.Runtime) {
    if err := fs.EnableWithLoop(rt, loop, options...); err != nil {
        panic(err)
    }
    process.Enable(rt)
})

需要 FsFile.readable/writablewriteFile(ReadableStream) 时:

options := []fs.Option{
    fs.WithFS(backend),
    fs.WithCwd("/workspace"),
    fs.WithStreams(true),
}

fs.WithExtraCapabilities 可以显式注入 lstat、readlink、symlink 和 hard link 能力;Afero 不支持这些接口时,相关调用返回 ENOSYS,不会伪造 Node 结果。

注册自定义 CommonJS/Go 模块

使用 registry 可以让原生模块只对指定 runtime 集合可见;使用包级函数注册则是全局 core module:

registry.RegisterNativeModule("host", func(rt *goja.Runtime, module *goja.Object) {
    exports := module.Get("exports").ToObject(rt)
    _ = exports.Set("log", func(call goja.FunctionCall) goja.Value {
        fmt.Println(call.Argument(0).String())
        return goja.Undefined()
    })
})

require.NewRegistry 还可配置 WithLoader、WithPathResolver 和 WithGlobalFolders, 适合把模块文件放在内存、数据库或受控沙箱中。

仅供 Go 使用的支持包

  • eventloop:创建 runtime、调度定时器和 microtask、串行化 Goja 调用,并提供 logger 和生命周期管理;它本身不是 JavaScript 模块。
  • runtimehost:保存 runtime-local 的 scheduler、cwd 和模块状态,适合自定义宿主 实现 runtimehost.Scheduler 或复用共享 cwd。
  • errors:生成带 Node 风格 code 的 TypeError、RangeError 等 JS 错误。
  • goutil:读取和校验 Goja 参数,并把 JavaScript 值转换为 Go 类型。

这些包是模块实现和宿主适配的基础,不会额外向 JavaScript 暴露对象。

TypeScript 类型定义

仓库提供拆分的类型包,当前包括:

  • @dop251/types-goja_nodejs-global
  • @dop251/types-goja_nodejs-url
  • @dop251/types-goja_nodejs-buffer

在 TypeScript 项目中安装所需包,并把 node_modules/@dop251 纳入 typeRoots。类型 包按模块拆分,和 Go 侧按需启用的模型一致;它们只描述已实现的 API 子集,不代表完整 Node 类型。

运行时和并发注意事项

Goja runtime 所有权

Goja 不是 goroutine-safe。以下操作都必须在拥有 runtime 的 loop 线程执行:

  • 调用 rt.RunString、rt.RunProgram 或 Goja 函数;
  • 创建、读取、修改或传递 Goja Value;
  • 安装模块和设置全局变量;
  • 处理异步模块回调。

网络、FS 和 WebSocket 的后台工作可以在 Go 协程中进行,但完成回调会回到同一个 runtime-bound event loop。不要把一个 loop 传给另一个 runtime。

配置必须一致

同一个 runtime 的 scheduler、FS backend、cwd、stream chunk size 和 capability 配置只能等价地重复安装。发现冲突时模块会返回错误,不采用 first-call-wins;这样 可以避免 require("fs")、全局 fs 和 process 看到不同的文件系统状态。

Node 兼容边界

  • 本项目没有 Node 的完整内置模块集合,也没有 node:test、child_process、worker_threads、 cluster 等运行时服务。
  • process.env 是初始化时的环境变量快照;process.chdir 是 runtime-local 操作。
  • Fetch 的 body 只能按 Web API 语义消费一次;Response.body、FS 流和 Web Streams 使用 Uint8Array,需要 Buffer 时必须显式转换。
  • streamnode:streamnode:fsnode:fs/promises 不属于公开 API。
  • FS 的 symlink、hard link、lstat 等能力来自宿主 capability;不能假定任意 Afero backend 都具备这些操作。
  • WebSocket 默认验证 TLS;不要为了绕过证书问题在生产环境设置 InsecureSkipVerify。
  • timers/promises 依赖 event loop;在顶层脚本把导出的 setTimeout 用 const 解构时,可能和全局 timer 产生 Goja 的暂时性死区冲突,放在 CommonJS 模块内部或 使用别名可规避。

生命周期

宿主应在退出前停止 event loop,并关闭自己持有的 HTTP/WebSocket 资源。使用独立的 websocket.WebSocketManager 时调用 CloseAll();不要在 loop 已停止后继续向其 RunOnLoop 提交 Goja 回调。

示例与验证

仓库中的可运行示例:

go run ./examples/composed_runtime
go run ./examples/fetch_abort
go run ./examples/fs_runtime
go run ./examples/streams
go run ./examples/websocket_tls

常用验证命令:

go test -count=1 ./...
go test -count=1 -race ./...
go vet ./...
go build ./...
git diff --check

更细的模块行为、边界和测试用例请直接查看对应目录的 README 和 *_test.go 文件。

许可证

本项目使用 LICENSE 中声明的许可证。嵌入的 Web Streams、Node streams 和 events 实现还带有各自上游项目的许可证说明,详见对应目录中的 LICENSE/README。

About

Nodejs compatibility library for Goja, with some sealdice-core fix.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages