Skip to content

guweigang/vjsx

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

30 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

VJSX

VJSX brand

V bindings to QuickJS javascript engine. Run JS in V.

The first version of this project was derived from herudi/vjs. Thanks to the original author for the foundational work that helped kick off vjsx.

Features

  • Evaluate js (code, file, module, etc).
  • Multi evaluate support.
  • Callback function support.
  • Set-Globals support.
  • Set-Module support.
  • Call V from JS.
  • Call JS from V.
  • Top-Level await support. using vjsx.type_module.

Install

v install vjsx

Build With Local QuickJS Source

If you already have a local QuickJS checkout, you can compile vjsx against the source tree directly instead of the bundled prebuilt archives.

This is useful when:

  • you are on an unsupported architecture such as macOS arm64
  • you want to use a newer QuickJS version
  • you do not want to maintain extra prebuilt .a files inside this repo

Example:

VJS_QUICKJS_PATH=/Users/guweigang/Source/quickjs \
v -d build_quickjs run main.v

Notes:

  • VJS_QUICKJS_PATH should point to the QuickJS source root that contains quickjs.c, quickjs-libc.c, quickjs.h, and quickjs-libc.h.
  • In this mode vjsx compiles QuickJS C sources directly.
  • Without -d build_quickjs, vjsx uses the bundled headers under libs/include/ together with the prebuilt archives in libs/.

Basic Usage

Create file main.v and copy-paste this code.

import vjsx

fn main() {
  mut session := vjsx.new_runtime_session()
  defer {
    session.close()
  }
  ctx := session.context()

  value := ctx.eval('1 + 2') or { panic(err) }
  ctx.end()
  defer {
    value.free()
  }

  assert value.is_number() == true
  assert value.is_string() == false
  assert value.to_int() == 3

println(value)
  // 3
}

Embedded Host Quick Start

If you are embedding vjsx into another V project, start from runtimejs.ExtensionSession rather than lower-level runtime plumbing.

import runtimejs
import vjsx

fn host_api() vjsx.HostValueBuilder {
	return vjsx.host_object(vjsx.HostObjectField{
		name:  'app'
		value: vjsx.host_object(vjsx.HostObjectField{
			name:  'name'
			value: vjsx.host_value('demo-host')
		})
	})
}

fn main() {
	mut extension_session := runtimejs.new_node_extension_session(
		vjsx.ContextConfig{},
		vjsx.NodeRuntimeConfig{
			process_args: ['extension.mjs']
		},
		vjsx.HostApiConfig{},
		host_api(),
	)
	defer {
		extension_session.close()
	}

	mut extension := extension_session.load_extension('./examples/js/host_extension.mjs',
		vjsx.ScriptPluginHooks{}) or { panic(err) }
	defer {
		extension.close()
	}

	result := extension.call_export('greet', 'world') or { panic(err) }
	defer {
		result.free()
	}
	println(result.to_string())
}

For the full host-first embedding guidance, see EMBEDDING.md.

Run

v run main.v

With a local QuickJS checkout:

VJS_QUICKJS_PATH=/Users/guweigang/Source/quickjs \
v -d build_quickjs run main.v

Explore examples

If you want the smallest file-based example, see examples/run_file.v together with examples/js/foo.js.

If you want the recommended embedded-host flow, see examples/embedding_extension.v together with examples/js/host_extension.mjs.

If you also want an example that shows host modules plus manifest-defined hook names, see examples/embedding_extension_manifest.v together with examples/js/host_extension_manifest.mjs.

CLI

You can also run JS files directly from the repository:

./vjsx ./tests/test.js

Module mode:

./vjsx --module ./examples/js/main.js

TypeScript entry files are also supported:

./vjsx ./tests/ts_basic.ts
./vjsx --module ./tests/ts_module_runtime.mts

TypeScript module graphs are also supported, including:

  • relative .ts / .mts imports
  • nearest tsconfig.json, including extends
  • compilerOptions.baseUrl and paths
  • bare package imports resolved from local node_modules
  • package exports root and explicit subpath entries

Options:

  • --module, -m: run the file as an ES module

This is runtime transpilation backed by the bundled typescript.js, and the same loader is now also available from the vjsx API through ctx.install_typescript_runtime() and ctx.run_runtime_entry(...). It is a good fit for standalone .ts scripts, .mts modules, and small local module graphs. Project-wide features like full tsc diagnostics, references, and broader Node compatibility are still out of scope for now.

When embedding vjsx in a long-lived process, always pair each created Runtime/Context with an explicit free(). Repeated TypeScript bootstrap work in the same process assumes those runtimes are torn down deliberately; leaking them can surface later as hard-to-diagnose bootstrap failures.

If you want one owner object for embedded use, prefer vjsx.new_runtime_session() and session.close(), which tear down the Context and Runtime together. For Node-style hosts, that teardown also closes tracked sqlite / mysql connections that were left open by JS code.

If you also want TypeScript/module-aware file loading from the same session, use runtimejs.new_script_runtime_session(...) or runtimejs.new_node_runtime_session(...). Those session helpers install the runtime bridge so embedders can call higher-level methods like:

  • session.run(path)
  • session.run_script(path)
  • session.run_module(path)
  • session.load_module(path)
  • session.import_module(path)
  • session.import_module_with_host(path, host_api)
  • session.load_plugin(path, hooks)
  • session.load_plugin_with_host(path, hooks, host_api)
  • session.call_module_export(path, export_name, ...)
  • session.call_module_export_with_host(path, export_name, host_api, ...)
  • session.call_module_method(path, export_name, method_name, ...)
  • session.call_module_method_with_host(path, export_name, method_name, host_api, ...)
  • session.call_default_export_method(path, method_name, ...)
  • session.call_default_export_method_with_host(path, method_name, host_api, ...)

For embedded host use, the recommended abstraction ladder is now:

  • vjsx.RuntimeSession: core lifecycle and loading
  • runtimejs.ExtensionSession: default embedder-facing session
  • runtimejs.ExtensionHandle: one loaded extension instance with lifecycle hooks plus regular export calls

That path is documented in EMBEDDING.md, together with:

  • the recommended stopping point to avoid over-design
  • API surface guidance for default vs advanced helpers
  • stability notes for likely long-term vs de-emphasized APIs
  • a convergence checklist for future cleanup without more abstraction growth
  • host API shape guidance
  • load_extension(...) usage
  • optional JS/TS manifest support
  • optional manifest services support

vjsx.new_runtime() and rt.new_context() are still available for advanced manual ownership cases, but then the caller is responsible for pairing them with ctx.free() and rt.free() correctly.

The wrapper script will use VJS_QUICKJS_PATH when it is set. If it is not set, it will try ../quickjs relative to the repository root as a local convenience fallback.

Currently support linux/mac/win (x64).

in windows, requires -cc gcc.

Host Profiles

The runtime is now split into clearer layers:

  • ctx.install_runtime_globals(...): reusable globals like Buffer, timers, URL, and URLPattern
  • ctx.install_node_compat(...): Node-like host features such as console, fs, path, os, child_process, process, standard fetch globals, sqlite, and optional mysql
  • web.inject_browser_host(ctx, ...): browser-style host features under web/, including window, DOM bootstrap, and Web APIs

web.inject_browser_host(...) is now configurable, so you can expose only the browser-facing modules you want, while still letting higher-level features like fetch pull in their required Web API dependencies.

The legacy ctx.install_host(...) entrypoint still works as a compatibility wrapper around install_node_compat(...).

For the embedding ownership, event-loop, timer, diagnostics, limits, and profile contracts, see docs/RUNTIME_CONTRACT.md.

For embedders, ctx.install_host_api(...) provides a more explicit way to expose host globals and modules to JS/TS extension code without hand-rolling js_module(...).create() at every call site.

Useful embedders helpers include:

  • vjsx.host_value(...)
  • vjsx.host_object(...)
  • vjsx.host_module_exports(...)
  • vjsx.host_module_object(...)

Database host modules:

  • import { open } from "sqlite" is available in the default Node-style host profile
  • import { connect } from "mysql" is also exposed, but the real V MySQL backend is only compiled when you pass -d vjsx_mysql
  • The CLI forwards extra V compiler flags through VJS_V_FLAGS, for example: VJS_V_FLAGS='-d vjsx_mysql' ./vjsx --module app.mjs
  • End-to-end example files live under examples/db/

SQLite example:

import { open } from "sqlite";

const db = await open({ path: "./app.db", busyTimeout: 1000 });
await db.exec("create table if not exists users (id integer primary key, name text)");
await db.execMany("insert into users(name) values (?)", [["alice"], ["bob"]]);
const firstUser = await db.queryOne("select id, name from users order by id");
const userCount = await db.scalar("select count(*) from users");
console.log(firstUser ? firstUser.name : "null", userCount);
await db.close();

MySQL example:

import { connect } from "mysql";

const db = await connect({
  host: "127.0.0.1",
  port: 3306,
  user: "root",
  password: "",
  database: "mysql",
});
const stmt = await db.prepareCached("select id, name from users where name <> ? order by id");
const rows = await stmt.query(["carol"]);
console.log(rows.length);
await stmt.close();
await db.close();

DB host API shape:

  • sqlite.open({ path, busyTimeout? })
  • mysql.connect({ host?, port?, user?|username?, password?, database?|dbname? })
  • db.query(sql, params?)
  • db.queryOne(sql, params?)
  • db.scalar(sql, params?)
  • db.queryMany(sql, [[...], [...]])
  • db.exec(sql, params?)
  • db.execMany(sql, [[...], [...]])
  • await db.prepareCached(sql) reuses the same prepared statement for repeated SQL text until that statement is closed
  • stmt.close() and db.close() are idempotent, and db.close() also marks cached/reusable statements as closed
  • db.begin()
  • db.commit()
  • db.rollback()
  • db.transaction(async (tx) => { ... })
  • await db.prepare(sql) returning a reusable statement with query(params?), queryOne(params?), scalar(params?), queryMany([[...], [...]]), exec(params?), execMany([[...], [...]]), and close()
  • db.close()
  • mysql connections also expose db.ping()
  • db.driver identifies the backend, for example sqlite or mysql
  • db.supportsTransactions tells you whether transaction helpers are available
  • db.inTransaction reflects the host connection's current transaction state
  • db.toString() and stmt.toString() provide compact debug-friendly summaries
  • db.exec(...) returns rows, changes, rowsAffected, lastInsertRowid, and insertId

process.env is exposed as a live host view, so reads reflect environment variable changes made by the embedding process after the runtime was installed.

  • statements expose driver, supportsTransactions, sql, kind, and closed

When params are provided to mysql.query(...) or mysql.exec(...), vjsx now routes them through V's prepared statement support instead of expanding SQL placeholders in user space.

For lifecycle-sensitive code, cached statements are scoped to the connection: prepareCached(...) returns the same statement for repeated SQL text until that statement is closed, and db.close() marks all cached/reusable statements as closed.

For local or CI integration tests against a live MySQL server, the optional tests/host_mysql_runtime_test.v probe reads VJS_TEST_MYSQL_HOST, VJS_TEST_MYSQL_PORT, VJS_TEST_MYSQL_USER, VJS_TEST_MYSQL_PASSWORD, VJS_TEST_MYSQL_DBNAME, and VJS_TEST_MYSQL_TABLE.

Useful presets:

  • vjsx.runtime_globals_full()
  • vjsx.runtime_globals_minimal()
  • vjsx.node_compat_full(fs_roots, process_args)
  • vjsx.node_compat_minimal(fs_roots, process_args)
  • web.browser_host_full()
  • web.browser_host_minimal()

Higher-level runtime entrypoints:

  • ctx.install_script_runtime(...)
  • ctx.install_node_runtime(...)
  • web.inject_browser_runtime(ctx)
  • web.inject_browser_runtime_minimal(ctx)

CLI runtime profiles:

  • ./vjsx --runtime node ...
  • ./vjsx --runtime script ...
  • ./vjsx --runtime browser --module ...

The CLI defaults to --runtime node for backwards compatibility. browser is intentionally a pure browser-style host profile and currently requires --module. The current CLI browser profile exposes browser-like globals such as window, self, EventTarget, URL, timers, streams, Blob, and FormData, while intentionally leaving out Node globals like process, Buffer, and modules such as fs.

Example:

import vjsx
import herudi.vjsx.web

fn main() {
  mut session := vjsx.new_script_runtime_session(vjsx.ContextConfig{}, vjsx.ScriptRuntimeConfig{
    process_args: ['inline.js']
  })
  defer {
    session.close()
  }
  ctx := session.context()

  web.inject_browser_runtime_minimal(ctx)
}

Multi Evaluate

ctx.eval('const sum = (a, b) => a + b') or { panic(err) }
ctx.eval('const mul = (a, b) => a * b') or { panic(err) }

sum := ctx.eval('sum(${1}, ${2})') or { panic(err) }
mul := ctx.eval('mul(${1}, ${2})') or { panic(err) }

ctx.end()

println(sum)
// 3

println(mul)
// 2

Add Global

glob := ctx.js_global()
glob.set('foo', 'bar')

value := ctx.eval('foo') or { panic(err) }
ctx.end()

println(value)
// bar

Add Module

mut mod := ctx.js_module('my-module')
mod.export('foo', 'foo')
mod.export('bar', 'bar')
mod.export_default(mod.to_object())
mod.create()

code := '
  import mod, { foo, bar } from "my-module";

  console.log(foo, bar);

  console.log(mod);
'

ctx.eval(code, vjsx.type_module) or { panic(err) }
ctx.end()

Install Host API

import vjsx

mut session := vjsx.new_runtime_session()
defer {
  session.close()
}
ctx := session.context()

ctx.install_host_api(
  globals: [
    vjsx.HostGlobalBinding{
      name:  'appName'
      value: vjsx.host_value('demo')
    },
  ]
  modules: [
    vjsx.HostModuleBinding{
      name: 'host-tools'
      install: vjsx.host_module_exports(
        vjsx.HostModuleExport{
          name:  'answer'
          value: vjsx.host_value(42)
        },
        vjsx.HostModuleExport{
          name: 'describe'
          value: fn [ctx] (ctx2 &vjsx.Context) vjsx.Value {
            return ctx.js_function(fn [ctx] (args []vjsx.Value) vjsx.Value {
              return ctx.js_string('host:' + args[0].str())
            })
          }
        },
      )
    },
  ]
)

ctx.eval('
  import hostTools, { answer, describe } from "host-tools";
  globalThis.result = [
    appName,
    String(answer),
    describe("ok"),
    String(hostTools.answer)
  ].join("|");
', vjsx.type_module) or { panic(err) }

Install Host Object

ctx.install_host_api(
  globals: [
    vjsx.HostGlobalBinding{
      name: 'host'
      value: vjsx.host_object(
        vjsx.HostObjectField{
          name:  'name'
          value: vjsx.host_value('demo')
        },
        vjsx.HostObjectField{
          name: 'math'
          value: vjsx.host_object(
            vjsx.HostObjectField{
              name: 'add'
              value: fn [ctx] (ctx2 &vjsx.Context) vjsx.Value {
                return ctx.js_function(fn [ctx] (args []vjsx.Value) vjsx.Value {
                  return ctx.js_int(args[0].to_int() + args[1].to_int())
                })
              }
            },
          )
        },
      )
    },
  ]
  modules: [
    vjsx.HostModuleBinding{
      name: 'host-service'
      install: vjsx.host_module_object(
        vjsx.HostObjectField{
          name:  'version'
          value: vjsx.host_value('v1')
        },
        vjsx.HostObjectField{
          name: 'greet'
          value: fn [ctx] (ctx2 &vjsx.Context) vjsx.Value {
            return ctx.js_function(fn [ctx] (args []vjsx.Value) vjsx.Value {
              return ctx.js_string('hello:' + args[0].str())
            })
          }
        },
      )
    },
  ]
)

Web Platform APIs

Inject Web API to vjsx.

import vjsx
import herudi.vjsx.web

fn main() {
  mut session := vjsx.new_runtime_session()
  defer {
    session.close()
  }
  ctx := session.context()

  // inject all browser host features
  web.inject_browser_host(ctx)

  // or inject one by one
  // web.console_api(ctx)
  // web.encoding_api(ctx)
  // more..

  ...
}

List Web Platform APIs

Current SubtleCrypto scope:

Area Current support
digest SHA-1, SHA-256, SHA-384, SHA-512
HMAC generateKey, importKey('raw'), exportKey('raw'), sign, verify
AES-CBC generateKey, importKey('raw'), exportKey('raw'), encrypt, decrypt
AES-CTR generateKey, importKey('raw'), exportKey('raw'), encrypt, decrypt with length = 128 only
PBKDF2 importKey('raw'), deriveBits, deriveKey with SHA-256, SHA-384, SHA-512
Ed25519 generateKey, sign, verify, importKey('raw') for public keys, exportKey('raw') for generated public keys
ECDSA generateKey, sign, verify, exportKey('raw') for generated public keys

Notes:

  • AES-GCM is not implemented yet.
  • ECDSA currently supports generated key pairs only; full importKey()/structured export formats are not implemented yet.
  • Ed25519 and ECDSA support in exportKey('raw') is intentionally limited to public keys.
  • PBKDF2 is a base-key flow only; use importKey('raw', ...) before deriveBits() or deriveKey().

Minimal examples:

These snippets assume you are running with the browser-style host profile, so crypto.subtle and TextEncoder are already available.

Runnable copies of these snippets live under examples/webcrypto/ and can be run with:

./vjsx --runtime browser --module ./examples/webcrypto/<file>.mjs

See also: examples/webcrypto/README.md

HMAC sign/verify:

File: examples/webcrypto/hmac_sign_verify.mjs

const text = new TextEncoder().encode("hello");
const key = await crypto.subtle.importKey(
  "raw",
  new Uint8Array([1, 2, 3, 4]),
  { name: "HMAC", hash: "SHA-256" },
  false,
  ["sign", "verify"],
);

const sig = await crypto.subtle.sign("HMAC", key, text);
const ok = await crypto.subtle.verify("HMAC", key, sig, text);
console.log(sig.byteLength, ok);

AES-CBC encrypt/decrypt:

File: examples/webcrypto/aes_cbc_encrypt_decrypt.mjs

const text = new TextEncoder().encode("hello");
const iv = new Uint8Array([15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0]);
const key = await crypto.subtle.importKey(
  "raw",
  new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]),
  "AES-CBC",
  true,
  ["encrypt", "decrypt"],
);

const encrypted = await crypto.subtle.encrypt({ name: "AES-CBC", iv }, key, text);
const decrypted = await crypto.subtle.decrypt({ name: "AES-CBC", iv }, key, encrypted);
console.log(encrypted.byteLength, new TextDecoder().decode(decrypted));

PBKDF2 derive an AES key:

File: examples/webcrypto/pbkdf2_derive_aes.mjs

const password = new TextEncoder().encode("password");
const baseKey = await crypto.subtle.importKey(
  "raw",
  password,
  "PBKDF2",
  false,
  ["deriveBits", "deriveKey"],
);

const aesKey = await crypto.subtle.deriveKey(
  {
    name: "PBKDF2",
    salt: new TextEncoder().encode("salt"),
    iterations: 1000,
    hash: "SHA-256",
  },
  baseKey,
  { name: "AES-CBC", length: 128 },
  true,
  ["encrypt", "decrypt"],
);

console.log(aesKey.algorithm.name, aesKey.algorithm.length);

Ed25519 and ECDSA:

File: examples/webcrypto/signatures.mjs

const text = new TextEncoder().encode("hello");

const ed = await crypto.subtle.generateKey("Ed25519", false, ["sign", "verify"]);
const edSig = await crypto.subtle.sign("Ed25519", ed.privateKey, text);
console.log(await crypto.subtle.verify("Ed25519", ed.publicKey, edSig, text));

const ec = await crypto.subtle.generateKey(
  { name: "ECDSA", namedCurve: "P-256" },
  false,
  ["sign", "verify"],
);
const ecSig = await crypto.subtle.sign(
  { name: "ECDSA", hash: "SHA-256" },
  ec.privateKey,
  text,
);
console.log(await crypto.subtle.verify(
  { name: "ECDSA", hash: "SHA-256" },
  ec.publicKey,
  ecSig,
  text,
));

It's Fun Project. PRs Wellcome :)

About

V bindings to quickjs javascript engine. Run JS in V.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors