jsf 1.0.0 copy "jsf: ^1.0.0" to clipboard
jsf: ^1.0.0 copied to clipboard

A high performance JavaScript engine, available out of the box in Flutter.

JSF #

English | 中文文档

A high-performance JavaScript engine available out of the box in Flutter.

Features #

  1. Up-to-date QuickJS support.
  2. High-performance build strategy enabled by default.
  3. big number and related features enabled by default.
  4. Automatic type conversion with Dart and JavaScript interop.
  5. Full platform support, including Web and OHOS.

[pic.png]

Basic Usage #

import 'package:jsf/jsf.dart';

final js = JsRuntime();
print(js.eval('40 + 2')); // 42

Runtime Options #

final js = JsRuntime(
  options: const JsRuntimeOptions(
    memoryLimitBytes: 64 * 1024 * 1024,
    maxStackSizeBytes: 1024 * 1024,
    timeout: Duration(seconds: 2),
  ),
);

timeout is enforced by QuickJS' interrupt handler. Clear or replace it with:

js.clearTimeout();
js.setTimeout(const Duration(milliseconds: 500));

Values And Handles #

Use eval() when you want a Dart value. eval() executes JavaScript, converts the result to Dart, and releases the temporary JavaScript handle:

final data = js.eval('({id: 1n, tags: ["a", "b"]})');
// {'id': BigInt.one, 'tags': ['a', 'b']}

Automatic conversion rules:

JavaScript Dart
undefined jsUndefined
null null
boolean bool
integer number int
floating-point number double
bigint BigInt
string String
Array List<dynamic>
plain object Map<String, dynamic>
Date DateTime
Map / Set Map<Object?, Object?> / Set<Object?>
RegExp / Error JsRegExp / JsErrorDetails
ArrayBuffer / TypedArray Uint8List / JsTypedArray
NaN / Infinity / -Infinity Dart special double values

Objects and arrays are converted recursively, so id: 1n becomes Dart BigInt.one, and tags becomes a Dart list. Sparse array holes become jsArrayHole. Passing Dart values into JS supports null, jsUndefined, bool, int, double, String, BigInt, DateTime, Uint8List, JsRegExp, JsErrorDetails, JsTypedArray, Set, List, and Map<String, Object?>.

eval() is for one-shot results and does not preserve JavaScript object identity. Use evalValue() when you need a JsValue handle:

  • Calling JavaScript functions or object methods.
  • Reading or writing object properties or array indexes.
  • Awaiting an existing Promise.
  • Handling circular references, class instances, DOM/host objects, TypedArray/ArrayBuffer, or values that cannot reliably become plain Dart maps/lists.
  • Keeping the same JavaScript object identity across multiple calls.

Use evalValue() when you need to keep a JavaScript object/function handle:

final object = js.evalValue('({count: 2, items: [3, 4]})');
try {
  final count = object.getPropertyValue('count');
  try {
    print(count.toDart()); // 2
  } finally {
    count.dispose();
  }
} finally {
  object.dispose();
}

JsValue.toDart() uses the same conversion rules as eval(). Circular objects cannot be converted automatically:

final circular = js.evalValue('const v = {}; v.self = v; v');
try {
  circular.toDart(); // throws JsException
} finally {
  circular.dispose();
}

Owned JsValue handles must be disposed. Handles received inside registerHandleFunction are borrowed and only valid for the callback duration. Call duplicate() if you need to keep one.

Calling JavaScript #

final add = js.evalValue('(function(a, b) { return a + b; })');
try {
  final result = js.callValue(add, [20, 22]);
  try {
    print(result.toDart()); // 42
  } finally {
    result.dispose();
  }
} finally {
  add.dispose();
}

For simple calls:

js.execInitScript('function join(prefix, values) { return prefix + values.join(","); }');
print(js.call('join', ['v:', [1, 2, 3]])); // v:1,2,3

Calling Dart From JavaScript #

Value callback:

js.registerFunction('dartSum', (args) {
  return args.cast<num>().reduce((a, b) => a + b);
});

print(js.eval('dartSum(4, 5, 6)')); // 15

Callbacks may return Future; JavaScript receives a Promise:

js.registerFunction('loadUser', (args) async {
  return {'id': 1, 'name': 'Ada'};
});

final user = await js.evalAsync('loadUser().then((user) => user.name)');

Handle callback:

js.registerHandleFunction('readModel', (args) {
  final model = args.first;
  final count = model.getPropertyValue('count');
  try {
    return count.toDart();
  } finally {
    count.dispose();
  }
});

Promises #

final value = await js.evalAsync('Promise.resolve({ok: true})');
print(value); // {'ok': true}

You can also await an existing JsValue:

final promise = js.evalValue('Promise.resolve(42)');
try {
  print(await js.awaitValue(promise));
} finally {
  promise.dispose();
}

ES Modules #

Register modules in memory:

js.registerModules({
  'math': 'export const answer = 42; export function inc(v) { return v + 1; }',
  'consumer': 'import { answer, inc } from "math"; export const result = inc(answer);',
  'pkg/relative': 'import { answer } from "../math"; export const result = answer;',
});

js.registerImportMap({'@math': 'math'});

js.eval(
  'import { result } from "consumer"; globalThis.result = result;',
  filename: 'main',
  module: true,
);

print(js.eval('result')); // 43
print(await js.evalAsync('import("@math").then((m) => m.inc(m.answer))')); // 43

Register a Flutter asset as a module:

await js.registerModuleFromAsset('app/config', 'assets/config.js');

Native platforms use the QuickJS module loader. Web includes JSF's registry loader behind the same Dart API and supports in-memory modules, import maps, relative resolution, module caching, static import, named/default/namespace exports, re-exports, and literal dynamic import("module"). evalAsync(..., module: true) and dynamic import() on Web use the browser's native ES Module/Blob loader, so browser-supported ESM syntax works, including export * from, export { x } from, export * as ns from, and top-level await. The in-memory module loader is intended for application scripts and Flutter asset modules; it does not fetch network module URLs.

Errors #

JavaScript exceptions are thrown as JsException:

try {
  js.eval('throw new Error("boom")');
} on JsException catch (error) {
  print(error.message);
}

Threading And Lifecycle #

  • A JsRuntime owns one QuickJS runtime and one context.
  • Use a runtime from the same Dart isolate that created it.
  • Dispose every runtime with dispose().
  • Dispose owned JsValue handles when you are done.
  • Do not use handles after their runtime is disposed.
  • Disposing a runtime also releases owned JsValue handles still registered under that runtime, but explicit dispose() remains recommended to control memory peaks.

Platform Notes #

  • Native platforms use QuickJS through FFI.
  • Web uses the browser JavaScript runtime and supports eval, evalValue, call, setGlobal, execInitScript, Promises, value callbacks, handle-style access, and JSF registry ES Modules. Memory limits, stack limits, and synchronous eval timeouts are browser platform limits; web prints debug warnings and keeps business code running where possible.
  • iOS/macOS use CocoaPods source forwarding.
  • Windows ships a prebuilt windows/jsf.dll; ordinary users do not need a C/C++ toolchain.

Testing #

Integration tests live in example/integration_test:

cd example
flutter drive --driver=integration_test/driver.dart --target=integration_test/js_runtime_test.dart -d macos

The test suite covers primitive conversion, BigInt, object/array conversion, handle calls, Dart callbacks, promise waiting, module loading, exceptions, timeouts, Unicode, typed arrays, circular objects, and multiple runtimes.

16
likes
0
points
2.77k
downloads

Publisher

verified publisherabandoft.com

Weekly Downloads

A high performance JavaScript engine, available out of the box in Flutter.

Repository (GitHub)
View/report issues

Topics

#javascript #runtime #qjs #quickjs #engine

License

unknown (license)

Dependencies

ffi, flutter, flutter_web_plugins

More

Packages that depend on jsf

Packages that implement jsf