fjs 2.1.0
fjs: ^2.1.0 copied to clipboard
A high-performance JavaScript runtime for Flutter applications, built with Rust and powered by QuickJS.
π FJS - Flutter JavaScript Engine #
High-performance JavaScript runtime for Flutter β‘ Built with Rust and powered by QuickJS π¦
β¨ Why FJS? #
- High Performance - Rust-powered, optimized for mobile platforms
- ES6 Modules - Full support for import/export syntax
- Async/Await - Native async JavaScript execution
- Type Safe - Strongly typed Dart API with sealed classes
- Bridge Communication - Bidirectional Dart-JS communication
- Cross Platform - Android, iOS, Linux, macOS, Windows
- Memory Safe - Built-in GC with configurable limits
π― Real-world Usage #
Mikan Flutter - A Flutter client for Mikan Project, an anime subscription and management platform. FJS powers its core JavaScript execution engine.
π¦ Installation #
dependencies:
fjs: any
π Quick Start #
import 'package:fjs/fjs.dart';
void main() async {
await LibFjs.init();
// Create runtime with builtin modules
final runtime = await JsAsyncRuntime.withOptions(
builtin: JsBuiltinOptions(
console: true,
fetch: true,
timers: true,
),
);
// Create context
final context = await JsAsyncContext.from(runtime: runtime);
// Create engine
final engine = JsEngine(context: context);
await engine.init(bridge: (jsValue) {
return JsResult.ok(JsValue.string('Hello from Dart'));
});
// Execute JavaScript
final result = await engine.eval(source: JsCode.code('''
console.log('Hello from FJS!');
1 + 2
'''));
print(result.value); // 3
await engine.dispose();
}
ποΈ Runtime & Context APIs #
// Create an async runtime with web-style builtins and one extra ES module.
final runtime = await JsAsyncRuntime.withOptions(
builtin: JsBuiltinOptions.web(),
additional: [
JsModule.code(
module: 'app/math',
code: 'export function add(a, b) { return a + b; }',
),
],
);
// Apply runtime-level safety and diagnostic limits.
await runtime.setInfo(info: 'main-runtime');
await runtime.setMemoryLimit(limit: BigInt.from(64 * 1024 * 1024));
await runtime.setGcThreshold(threshold: BigInt.from(8 * 1024 * 1024));
await runtime.setMaxStackSize(limit: BigInt.from(512 * 1024));
// Create a context from that runtime.
final context = await JsAsyncContext.from(runtime: runtime);
// Evaluate a simple expression and read the structured JsResult.
final evalResult = await context.eval(code: '21 + 21');
print(evalResult.ok.value); // 42
// Enable top-level await / Promise handling explicitly.
final asyncResult = await context.evalWithOptions(
code: 'await Promise.resolve(40 + 2)',
options: JsEvalOptions.withPromise(),
);
print(asyncResult.ok.value); // 42
// Load code from disk.
final fileResult = await context.evalFile(path: '/absolute/path/to/script.js');
final strictFileResult = await context.evalFileWithOptions(
path: '/absolute/path/to/script.js',
options: JsEvalOptions.defaults(),
);
// Call an exported function from a module that is already available in the runtime.
final functionResult = await context.evalFunction(
module: 'app/math',
method: 'add',
params: [JsValue.integer(2), JsValue.integer(3)],
);
print(functionResult.ok.value); // 5
// Inspect which modules the context can currently import.
final availableModules = await context.getAvailableModules();
print(availableModules);
// Advance or fully drain pending async work when you need explicit control.
if (await runtime.isJobPending()) {
await runtime.executePendingJob();
}
await runtime.idle();
Low-level context APIs return JsResult, which is useful when you want structured success or error handling instead of exceptions.
Synchronous Runtime & Context #
// Build a synchronous runtime when you do not need async JavaScript execution.
final runtime = await JsRuntime.withOptions(
builtin: JsBuiltinOptions.essential(),
);
final context = JsContext.from(runtime: runtime);
// Sync contexts return JsResult directly.
final result = context.eval(code: '6 * 7');
print(result.ok.value); // 42
// Apply eval flags such as strict mode.
final strictResult = context.evalWithOptions(
code: '"use strict"; 8 * 8',
options: JsEvalOptions.defaults(),
);
print(strictResult.ok.value); // 64
// File-based sync evaluation uses the same JsResult shape.
final fileResult = context.evalFile(path: '/absolute/path/to/script.js');
final fileWithOptions = context.evalFileWithOptions(
path: '/absolute/path/to/script.js',
options: JsEvalOptions.defaults(),
);
// Introspect the modules visible to this context.
final modules = context.getAvailableModules();
print(modules);
// Pump the QuickJS job queue manually in sync mode.
while (runtime.isJobPending()) {
runtime.executePendingJob();
}
// Configure runtime limits and collect memory statistics.
runtime.setMemoryLimit(limit: BigInt.from(32 * 1024 * 1024));
runtime.setGcThreshold(threshold: BigInt.from(4 * 1024 * 1024));
runtime.setMaxStackSize(limit: BigInt.from(256 * 1024));
runtime.setInfo(info: 'sync-runtime');
print(runtime.memoryUsage().summary());
runtime.runGc();
π§± Source Inputs & Eval Options #
import 'dart:convert';
import 'dart:typed_data';
// Source code can come from a string, a file path, or UTF-8 bytes.
final inlineCode = JsCode.code('1 + 1');
final fileCode = JsCode.path('/absolute/path/to/script.js');
final bytesCode = JsCode.bytes(Uint8List.fromList(utf8.encode('2 + 2')));
// Modules support the same three source forms.
final inlineModule = JsModule.code(
module: 'feature/inline',
code: 'export const enabled = true;',
);
final fileModule = JsModule.path(
module: 'feature/file',
path: '/absolute/path/to/feature.js',
);
final bytesModule = JsModule.bytes(
module: 'feature/bytes',
bytes: utf8.encode('export const answer = 42;'),
);
// Eval options control whether code runs as global script, async code, or module-style code.
final defaultEval = JsEvalOptions.defaults();
final asyncEval = JsEvalOptions.withPromise();
final moduleEval = JsEvalOptions.module();
π¦ ES6 Modules #
// Declare modules
await engine.declareNewModule(
module: JsModule.code(module: 'math', code: '''
export const add = (a, b) => a + b;
export const multiply = (a, b) => a * b;
'''),
);
// Use modules
await engine.eval(source: JsCode.code('''
const { add, multiply } = await import('math');
console.log(add(2, 3)); // 5
console.log(multiply(4, 5)); // 20
'''));
// Or call an exported function directly without writing an import wrapper yourself.
final sum = await engine.call(
module: 'math',
method: 'add',
params: [JsValue.integer(2), JsValue.integer(3)],
);
print(sum.value); // 5
// Batch-register multiple modules in one request.
await engine.declareNewModules(modules: [
JsModule.code(
module: 'numbers/double',
code: 'export const double = (value) => value * 2;',
),
JsModule.code(
module: 'numbers/triple',
code: 'export const triple = (value) => value * 3;',
),
]);
// Execute a module immediately and leave it cached in the current context.
await engine.evaluateModule(
module: JsModule.code(
module: 'startup',
code: 'globalThis.started = true; export default "ready";',
),
);
// Inspect the dynamic modules declared on this engine.
final declaredModules = await engine.getDeclaredModules();
final hasMath = await engine.isModuleDeclared(moduleName: 'math');
print(declaredModules);
print(hasMath); // true
Dynamic modules can be cleared only before they are loaded. After a module has been imported or evaluated in a context, recreate the context to replace it.
π Module Inventory #
final modules = await engine.getAvailableModules();
print(modules);
final hasConsole = await engine.isModuleAvailable(moduleName: 'console');
final hasXml = await engine.isModuleAvailable(moduleName: 'llrt:xml');
print('console: $hasConsole, llrt:xml: $hasXml');
π Engine Lifecycle Notes #
JsEnginewraps an existingJsAsyncContext; disposing the engine does not dispose the underlying context or runtimedispose()detaches thefjsbridge object, drains pending runtime work, and then runs GC before the engine becomes unusableclearPendingModules()only removes dynamic modules that have not been loaded into the current context yetdeclareNewModules()anddeclareNewBytecodeModules()reject duplicate module names in a single request
π¦ Module Bytecode #
// Compile an ES module into QuickJS bytecode without touching the current engine.
final bytecode = await JsBytecode.compile(
module: JsModule.code(
module: 'plugin/main.js',
code: 'export function run() { return "ready"; }',
),
options: JsModuleBytecodeOptions.defaults(),
);
// Validate the bytecode payload before declaring it.
await JsBytecode.validate(module: bytecode);
// Register the precompiled module on the engine.
await engine.declareNewBytecodeModule(module: bytecode);
// Import and execute the declared module like any other ES module.
final result = await engine.eval(source: JsCode.code('''
const { run } = await import('plugin/main.js');
run();
'''));
// Reconstruct bytecode from persisted bytes when loading from storage.
final restored = JsModuleBytecode(
name: bytecode.name,
bytes: bytecode.bytes,
);
JsBytecode.validateSync(module: restored);
JsBytecode.compile() runs in an isolated QuickJS context, so compiling does not declare or cache the module inside the current engine. JsBytecode.validate() only checks structural validity and embedded module name; it does not execute the module. compileSync() / validateSync() are also available for synchronous callers, but the async variants are safer on the main isolate.
QuickJS bytecode is version-specific and must be treated as trusted input. Recompile bytecode whenever the embedded QuickJS version changes.
Bytecode Bundles #
// Compile a full module graph into one distributable bundle.
final bundle = await JsBytecode.compileModuleBundle(
entry: 'plugins/main.js',
modules: [
JsModule.code(
module: 'plugins/deps/math.js',
code: 'export const double = (value) => value * 2;',
),
JsModule.code(
module: 'plugins/main.js',
code: '''
import { double } from './deps/math.js';
export default { ready: true, answer: double(21) };
''',
),
],
);
// Validate the bundle structure before loading it.
await JsBytecode.validateBundle(bundle: bundle);
// Execute the bundle entry and cache the involved modules.
await engine.evaluateBytecodeBundle(bundle: bundle);
// Read exports by importing the entry module afterwards.
final result = await engine.eval(source: JsCode.code('''
const { default: plugin } = await import('plugins/main.js');
plugin
'''));
print(result.value); // { ready: true, answer: 42 }
Bundles are useful when a plugin ships as a module graph instead of a single file. Relative imports are preserved inside the compiled bundle, and declareNewBytecodeBundle() is available when you want to register the bundle without executing its entry yet. validateBundle() is structural: it checks entry presence, duplicate names, and that each payload is readable by the embedded QuickJS version. evaluateBytecodeBundle() executes the entry and populates the module cache; import the entry afterwards to read its exports.
Classic Script Bytecode #
// Compile classic script source into non-module bytecode.
final script = await JsBytecode.compileScript(
name: 'startup.js',
source: JsCode.code('''
await Promise.resolve();
globalThis.launchCount = (globalThis.launchCount ?? 0) + 1;
({ mode: 'script', launchCount: globalThis.launchCount })
'''),
options: const JsScriptBytecodeOptions(
promise: true,
strict: true,
stripSource: true,
stripDebug: true,
endianness: JsBytecodeEndianness.little,
),
);
// Validate before evaluation.
await JsBytecode.validateScript(script: script);
// Execute the script bytecode and read its completion value.
final result = await engine.evaluateScriptBytecode(script: script);
print(result.value); // { mode: 'script', launchCount: 1 }
Script bytecode is the non-module counterpart to ES module bytecode. validateScript() is structural only: it ensures the bytes decode as executable non-module bytecode under the embedded QuickJS version. QuickJS does not expose an embedded script name to verify at load time, so the name acts as compile-time metadata and the source filename shown in stack traces.
π§Ύ Values, Results, and Errors #
import 'dart:typed_data';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
// Convert common Dart values into structured JsValue instances automatically.
final payload = JsValue.from({
'enabled': true,
'count': 3,
'tags': ['a', 'b'],
'buffer': Uint8List.fromList([1, 2, 3]),
});
print(payload.typeName()); // Object
print(payload.value); // Dart Map<String, dynamic>
// Or build typed JsValue trees yourself when you need exact control.
final typed = JsValue.object({
'big': JsValue.bigint('9007199254740993'),
'createdAt': JsValue.date(DateTime.now().millisecondsSinceEpoch),
});
print(typed.value);
// Low-level context APIs return JsResult instead of throwing.
final result = await context.eval(code: '40 + 2');
if (result.isOk) {
print(result.ok.value); // 42
} else {
print('${result.err.code()}: ${result.err}');
}
// JsError values are useful for structured error handling and retry decisions.
const syntaxError = JsError.syntax(
message: 'Unexpected token',
line: 1,
column: 10,
);
print(syntaxError.code());
print(syntaxError.isRecoverable());
// High-level execution APIs still throw AnyhowException on failure.
try {
await engine.eval(source: JsCode.code('invalid.code()'));
} on AnyhowException catch (e) {
print('Execution failed: ${e.message}');
}
JsError is returned inside JsResult.err(...) for structured bridge and low-level context results. Public execution APIs like eval() and call() currently surface Rust-side failures as AnyhowException.
π Bridge Communication #
// The bridge receives a JsValue and returns a JsResult back to JavaScript.
await engine.init(bridge: (jsValue) async {
final data = jsValue.value;
if (data is Map && data['action'] == 'fetchUser') {
final user = await fetchUser(data['id']);
return JsResult.ok(JsValue.from(user));
}
return JsResult.ok(JsValue.none());
});
// In JavaScript, call back into Dart through the injected fjs object.
await engine.eval(source: JsCode.code('''
const user = await fjs.bridge_call({ action: 'fetchUser', id: 123 });
console.log(user);
'''));
π§ Memory Management #
// Set runtime safety limits.
await runtime.setMemoryLimit(limit: BigInt.from(50 * 1024 * 1024)); // 50MB
await runtime.setGcThreshold(threshold: BigInt.from(10 * 1024 * 1024)); // 10MB
await runtime.setMaxStackSize(limit: BigInt.from(512 * 1024)); // 512KB
// Inspect current memory usage.
final usage = await runtime.memoryUsage();
print(usage.summary());
// Force a garbage collection pass when you need immediate cleanup.
await runtime.runGc();
π Core API #
JsAsyncRuntime & JsAsyncContext #
abstract class JsAsyncRuntime {
factory JsAsyncRuntime();
static Future<JsAsyncRuntime> withOptions({
JsBuiltinOptions? builtin,
List<JsModule>? additional,
});
Future<bool> isJobPending();
Future<bool> executePendingJob();
Future<void> idle();
Future<MemoryUsage> memoryUsage();
Future<void> setMemoryLimit({required BigInt limit});
Future<void> setGcThreshold({required BigInt threshold});
Future<void> setMaxStackSize({required BigInt limit});
Future<void> setInfo({required String info});
Future<void> runGc();
}
abstract class JsAsyncContext {
static Future<JsAsyncContext> from({required JsAsyncRuntime runtime});
Future<JsResult> eval({required String code});
Future<JsResult> evalWithOptions({required String code, required JsEvalOptions options});
Future<JsResult> evalFile({required String path});
Future<JsResult> evalFileWithOptions({required String path, required JsEvalOptions options});
Future<JsResult> evalFunction({
required String module,
required String method,
List<JsValue>? params,
});
Future<List<String>> getAvailableModules();
}
JsRuntime & JsContext #
abstract class JsRuntime {
factory JsRuntime();
static Future<JsRuntime> withOptions({
JsBuiltinOptions? builtin,
List<JsModule>? additional,
});
bool isJobPending();
bool executePendingJob();
MemoryUsage memoryUsage();
void setMemoryLimit({required BigInt limit});
void setGcThreshold({required BigInt threshold});
void setMaxStackSize({required BigInt limit});
void setInfo({required String info});
void runGc();
}
abstract class JsContext {
static JsContext from({required JsRuntime runtime});
JsResult eval({required String code});
JsResult evalWithOptions({required String code, required JsEvalOptions options});
JsResult evalFile({required String path});
JsResult evalFileWithOptions({required String path, required JsEvalOptions options});
List<String> getAvailableModules();
}
JsEngine #
class JsEngine {
factory JsEngine({required JsAsyncContext context});
Future<void> init({required FutureOr<JsResult> Function(JsValue) bridge});
Future<void> initWithoutBridge();
Future<JsValue> eval({required JsCode source, JsEvalOptions? options});
Future<JsValue> call({required String module, required String method, List<JsValue>? params});
Future<void> declareNewModule({required JsModule module});
Future<void> declareNewModules({required List<JsModule> modules}); // rejects duplicate names in one request
Future<void> declareNewBytecodeBundle({required JsModuleBytecodeBundle bundle});
Future<void> declareNewBytecodeModule({required JsModuleBytecode module});
Future<void> declareNewBytecodeModules({required List<JsModuleBytecode> modules}); // rejects duplicate names in one request
Future<void> clearPendingModules();
Future<List<String>> getAvailableModules();
Future<bool> isModuleDeclared({required String moduleName});
Future<bool> isModuleAvailable({required String moduleName});
Future<List<String>> getDeclaredModules();
Future<JsValue> evaluateBytecodeBundle({required JsModuleBytecodeBundle bundle});
Future<JsValue> evaluateModule({required JsModule module});
Future<JsValue> evaluateBytecodeModule({required JsModuleBytecode module});
Future<JsValue> evaluateScriptBytecode({required JsScriptBytecode script});
Future<void> dispose(); // drains pending runtime work, then runs GC
bool get running;
bool get disposed;
JsAsyncContext get context; // engine does not own the returned context
}
MemoryUsage #
abstract class MemoryUsage {
int get totalMemory;
int get totalAllocations;
int get mallocSize;
int get objCount;
int get strCount;
String summary();
}
JsValue #
sealed class JsValue {
const factory JsValue.none();
const factory JsValue.boolean(bool value);
const factory JsValue.integer(PlatformInt64 value);
const factory JsValue.float(double value);
const factory JsValue.bigint(String value);
const factory JsValue.string(String value);
const factory JsValue.bytes(Uint8List value);
const factory JsValue.array(List<JsValue> value);
const factory JsValue.object(Map<String, JsValue> value);
const factory JsValue.date(PlatformInt64 value);
const factory JsValue.symbol(String value);
const factory JsValue.function(String value);
static JsValue from(Object? any);
String typeName();
dynamic get value;
}
JsBuiltinOptions & JsEvalOptions #
sealed class JsBuiltinOptions {
const factory JsBuiltinOptions({
bool? console,
bool? fetch,
bool? timers,
bool? crypto,
bool? fs,
bool? url,
bool? process,
bool? path,
bool? util,
bool? intl,
bool? temporal,
// ... other builtin toggles
});
static JsBuiltinOptions none();
static JsBuiltinOptions essential();
static JsBuiltinOptions web();
static JsBuiltinOptions node();
static JsBuiltinOptions all();
}
sealed class JsEvalOptions {
factory JsEvalOptions({
bool? global,
bool? strict,
bool? backtraceBarrier,
bool? promise,
});
static JsEvalOptions defaults();
static JsEvalOptions withPromise();
static JsEvalOptions module();
}
JsCode, JsModule, and Bytecode #
sealed class JsCode {
const factory JsCode.code(String value); // Inline code
const factory JsCode.path(String value); // File path
const factory JsCode.bytes(Uint8List value); // Raw UTF-8 source bytes
}
sealed class JsModule {
static JsModule code({required String module, required String code});
static JsModule path({required String module, required String path});
static JsModule bytes({required String module, required List<int> bytes}); // UTF-8 source bytes
}
sealed class JsModuleBytecode {
factory JsModuleBytecode({required String name, required List<int> bytes});
}
sealed class JsModuleBytecodeBundle {
factory JsModuleBytecodeBundle({
String? entry,
required List<JsModuleBytecode> modules,
});
}
sealed class JsScriptBytecode {
factory JsScriptBytecode({required String name, required List<int> bytes});
}
class JsBytecode {
static JsModuleBytecode compileSync({
required JsModule module,
JsModuleBytecodeOptions? options,
});
static Future<JsModuleBytecode> compile({
required JsModule module,
JsModuleBytecodeOptions? options,
});
static JsModuleBytecodeBundle compileModuleBundleSync({
required List<JsModule> modules,
String? entry,
JsModuleBytecodeOptions? options,
});
static Future<JsModuleBytecodeBundle> compileModuleBundle({
required List<JsModule> modules,
String? entry,
JsModuleBytecodeOptions? options,
});
static JsScriptBytecode compileScriptSync({
required String name,
required JsCode source,
JsScriptBytecodeOptions? options,
});
static Future<JsScriptBytecode> compileScript({
required String name,
required JsCode source,
JsScriptBytecodeOptions? options,
});
static void validateSync({required JsModuleBytecode module});
static Future<void> validate({required JsModuleBytecode module});
static void validateBundleSync({required JsModuleBytecodeBundle bundle});
static Future<void> validateBundle({required JsModuleBytecodeBundle bundle});
static void validateScriptSync({required JsScriptBytecode script});
static Future<void> validateScript({required JsScriptBytecode script});
}
sealed class JsModuleBytecodeOptions {
const factory JsModuleBytecodeOptions({
JsBytecodeEndianness? endianness,
bool? stripSource,
bool? stripDebug,
});
static JsModuleBytecodeOptions defaults();
}
sealed class JsScriptBytecodeOptions {
const factory JsScriptBytecodeOptions({
JsBytecodeEndianness? endianness,
bool? stripSource,
bool? stripDebug,
bool? strict,
bool? backtraceBarrier,
bool? promise,
});
static JsScriptBytecodeOptions defaults();
}
JsResult & JsError #
sealed class JsResult {
const factory JsResult.ok(JsValue value);
const factory JsResult.err(JsError error);
bool get isOk;
bool get isErr;
JsValue get ok;
JsError get err;
}
sealed class JsError {
const factory JsError.promise(String message);
const factory JsError.module({String? module, String? method, required String message});
const factory JsError.context(String message);
const factory JsError.storage(String message);
const factory JsError.io({String? path, required String message});
const factory JsError.runtime(String message);
const factory JsError.generic(String message);
const factory JsError.engine(String message);
const factory JsError.bridge(String message);
const factory JsError.conversion({
required String from,
required String to,
required String message,
});
const factory JsError.timeout({
required String operation,
required BigInt timeoutMs,
});
const factory JsError.memoryLimit({
required BigInt current,
required BigInt limit,
});
const factory JsError.stackOverflow(String message);
const factory JsError.syntax({int? line, int? column, required String message});
const factory JsError.reference(String message);
const factory JsError.type(String message);
const factory JsError.cancelled(String message);
String code();
bool isRecoverable();
}
π§© Built-in Runtime Features #
Some builtin options expose importable modules, and some install globals directly on the runtime.
| Option | Description |
|---|---|
abort |
AbortController and abort-related globals |
assert |
Assertion helpers |
asyncHooks |
Async lifecycle tracking |
buffer |
Buffer utilities for binary data |
childProcess |
Child process spawning |
console |
Console logging (console.log, console.error, etc.) |
crypto |
Cryptographic functions and Web Crypto globals |
dgram |
UDP sockets |
dns |
DNS resolution |
events |
EventEmitter support |
exceptions |
Exception helpers installed globally |
fetch |
Fetch API globals |
fs |
File system operations |
https |
HTTPS client module |
intl |
Lightweight Intl.DateTimeFormat timezone support |
navigator |
Navigator globals |
net |
TCP sockets |
os |
Operating system utilities (not available on iOS) |
path |
Path manipulation (POSIX/Windows) |
perfHooks |
Performance measurement APIs |
process |
Process information and environment |
streamWeb |
Web Streams API |
stringDecoder |
String decoding from buffers |
temporal |
Temporal global |
timers |
Timer functions (setTimeout, setInterval, setImmediate) |
tty |
Terminal utilities |
url |
URL parsing and formatting |
util |
Utility functions |
zlib |
Compression/decompression (gzip, deflate) |
json |
JSON static method compatibility helpers |
Quick Presets #
// Essential: console, timers, buffer, util, json
JsBuiltinOptions.essential()
// Web: console, timers, fetch, url, crypto, streamWeb, navigator, exceptions, intl, json
JsBuiltinOptions.web()
// Node.js: Most Node-compatible modules, plus https and intl
JsBuiltinOptions.node()
// All modules
JsBuiltinOptions.all()
// Custom selection
JsBuiltinOptions(
console: true,
fetch: true,
timers: true,
// ... other options
)
β‘ Performance Tips #
- Reuse Engines - Create once, use many times
- Set Memory Limits - Configure appropriate limits
- Use Source Bytes - Prefer
JsCode.bytes()/JsModule.bytes()when your JavaScript source is already in UTF-8 bytes - Batch Operations - Group related operations
π License #
MIT License - see LICENSE file.