VJSX
V
The first version of this project was derived from
herudi/vjs
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
awaitsupport. 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
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
.afiles inside this repo
Example:
VJS_QUICKJS_PATH=/Users/guweigang/Source/quickjs \
v -d build_quickjs run main.v
Notes:
-
VJS_QUICKJS_PATHshould point to the QuickJS source root that contains quickjs.c, quickjs-libc.c, quickjs.h, and quickjs-libc.h. - In this mode
vjsxcompiles QuickJS C sources directly. - Without
-d build_quickjs, vjsxuses the bundled headers under libs/include/together with the prebuilt archives in libs/.
Basic Usage
Create file
main.v
import vjsx
fn main() {
rt := vjsx.new_runtime()
ctx := rt.new_context()
value := ctx.eval('1 + 2') or { panic(err) }
ctx.end()
assert value.is_number() == true
assert value.is_string() == false
assert value.to_int() == 3
println(value)
// 3
// free
value.free()
ctx.free()
rt.free()
}
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
examples/js/foo.js
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/ .mtsimports - nearest
tsconfig.json, including extends -
compilerOptions.baseUrland paths - bare package imports resolved from local
node_modules - package
exportsroot and explicit subpath entries
Options:
-
--module, -m: run the file as an ES module
This is runtime transpilation backed by the bundled
typescript.js
vjsx
ctx.install_typescript_runtime()
ctx.run_runtime_entry(...)
.ts
.mts
tsc
references
The wrapper script will use
VJS_QUICKJS_PATH
../quickjs
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, process, 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(...)
fetch
The legacy
ctx.install_host(...)
install_node_compat(...)
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() -
mysqlconnections also expose db.ping() -
db.driveridentifies the backend, for example sqliteor mysql -
db.supportsTransactionstells you whether transaction helpers are available -
db.inTransactionreflects 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 - statements expose
driver, supportsTransactions, sql, kind, and closed
When
params
mysql.query(...)
mysql.exec(...)
For lifecycle-sensitive code, cached statements are scoped to the connection:
prepareCached(...)
db.close()
For local or CI integration tests against a live MySQL server, the optional
tests/host_mysql_runtime_test.v
VJS_TEST_MYSQL_HOST
VJS_TEST_MYSQL_PORT
VJS_TEST_MYSQL_USER
VJS_TEST_MYSQL_PASSWORD
VJS_TEST_MYSQL_DBNAME
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
browser
--module
window
self
EventTarget
URL
Blob
FormData
process
Buffer
fs
Example:
import vjsx
import herudi.vjsx.web
fn main() {
rt := vjsx.new_runtime()
ctx := rt.new_context()
ctx.install_script_runtime(
process_args: ['inline.js']
)
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()
Web Platform APIs
Inject Web API to vjsx.
import vjsx
import herudi.vjsx.web
fn main() {
rt := vjsx.new_runtime()
ctx := rt.new_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
- Console
-
setTimeout
, clearTimeout -
setInterval
, clearInterval -
btoa
, atob - URL
- URLSearchParams
- URLPattern
- Encoding API
- Crypto API
-
SubtleCrypto
- digest
-
CryptoKey -
generateKey()for HMAC, Ed25519, ECDSA, AES-CBC, and AES-CTR -
importKey('raw')for HMAC, PBKDF2, AES-CBC, AES-CTR, and Ed25519public keys -
exportKey('raw')for extractable HMAC/ AESkeys, and generated Ed25519/ ECDSApublic keys -
deriveBits()for PBKDF2( SHA-256/384/512) -
deriveKey()for PBKDF2-> HMAC/ AES-CBC/ AES-CTR -
encrypt (
AES-CBC, AES-CTRwith length = 128) -
decrypt (
AES-CBC, AES-CTRwith length = 128) -
sign (
HMAC, Ed25519, ECDSA) -
verify (
HMAC, Ed25519, ECDSA)
Current
SubtleCrypto
| 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
length = 128
|
PBKDF2 |
importKey('raw')
deriveBits
deriveKey
SHA-256
SHA-384
SHA-512 |
Ed25519 |
generateKey
sign
verify
importKey('raw')
exportKey('raw')
|
ECDSA |
generateKey
sign
verify
exportKey('raw')
|
Notes:
-
AES-GCMis not implemented yet. -
ECDSAcurrently supports generated key pairs only; full importKey()/structured export formats are not implemented yet. -
Ed25519and ECDSAsupport in exportKey('raw')is intentionally limited to public keys. -
PBKDF2is 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
TextEncoder
Runnable copies of these snippets live under
examples/crypto/
./vjsx --runtime browser --module ./examples/crypto/<file>.mjs
See also:
examples/crypto/README.md
HMAC sign/verify:
File:
examples/crypto/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/crypto/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/crypto/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/crypto/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,
));