SkillAgentSearch skills...

Fjs

A high-performance JavaScript runtime for Flutter applications, built with Rust and powered by QuickJS.

Install / Use

/learn @fluttercandies/Fjs
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

<div align="center"> <img src="fjs.png" alt="FJS Logo" width="240">

🚀 FJS - Flutter JavaScript Engine

High-performance JavaScript runtime for Flutter ⚡ Built with Rust and powered by QuickJS 🦀

pub package GitHub stars License

🌏 中文文档

</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

  • JsEngine wraps an existing JsAsyncContext; disposing the engine does not dispose the underlying context or runtime
  • dispose() detaches the fjs bridge object, drains pending runtime work, and then runs GC before the engine becomes unusable
  • clearPendingModules() only removes dynamic modules that have not been loaded into the current context yet
  • declareNewModules() and declareNewBytecodeModules() 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

View on GitHub
GitHub Stars90
CategoryDevelopment
Updated2d ago
Forks7

Languages

Dart

Security Score

100/100

Audited on Mar 31, 2026

No findings