Fjs
A high-performance JavaScript runtime for Flutter applications, built with Rust and powered by QuickJS.
Install / Use
/learn @fluttercandies/FjsREADME
🚀 FJS - Flutter JavaScript Engine
High-performance JavaScript runtime for Flutter ⚡ Built with Rust and powered by QuickJS 🦀
</div>✨ 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 th
Related Skills
himalaya
345.4kCLI to manage emails via IMAP/SMTP. Use `himalaya` to list, read, write, reply, forward, search, and organize emails from the terminal. Supports multiple accounts and message composition with MML (MIME Meta Language).
node-connect
345.4kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
104.6kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
coding-agent
345.4kDelegate coding tasks to Codex, Claude Code, or Pi agents via background process
