diff --git a/.gitignore b/.gitignore index 3df4733..c131cf1 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ build/ .idea/ .vscode/settings.json + +publish.cmd diff --git a/CHANGELOG.md b/CHANGELOG.md index d24048f..9cfb5d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,8 +3,12 @@ * @Author: ekibun * @Date: 2020-08-08 08:16:50 * @LastEditors: ekibun - * @LastEditTime: 2020-08-26 23:37:16 + * @LastEditTime: 2020-08-27 20:42:32 --> +## 0.0.5 + +* add js module. + ## 0.0.4 * remove C++ std limitation for linux and android. diff --git a/README.md b/README.md index 3baca3b..32861d5 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ * @Author: ekibun * @Date: 2020-08-08 08:16:50 * @LastEditors: ekibun - * @LastEditTime: 2020-08-26 23:02:05 + * @LastEditTime: 2020-08-27 20:55:04 --> # flutter_qjs @@ -13,9 +13,11 @@ A quickjs engine for flutter. This plugin is a simple js engine for flutter using the `quickjs` project. Plugin currently supports Windows, Linux, and Android. -Each `FlutterJs` object creates a new thread that runs a simple js loop. A global async function `dart` is presented to invoke dart function, and `Promise` is supported so that you can use `await` or `then` to get external result from `dart`. +Each `FlutterJs` object creates a new thread that runs a simple js loop. -Data convertion between dart and js are implemented as follow: +ES6 module with `import` function is supported and can manage in dart with `setModuleHandler`. + +A global async function `dart` is presented to invoke dart function, and `Promise` is supported so that you can use `await` or `then` to get external result from `dart`. Data convertion between dart and js are implemented as follow: | dart | js | | --- | --- | @@ -48,7 +50,7 @@ engine = null; 2. Call `setMethodHandler` to implements `dart` interaction. For example, you can use `Dio` to implements http in js: ```dart -engine.setMethodHandler((String method, List arg) async { +await engine.setMethodHandler((String method, List arg) async { switch (method) { case "http": Response response = await Dio().get(arg[0]); @@ -65,7 +67,21 @@ and in javascript, call `dart` function to get data: dart("http", "http://example.com/"); ``` -3. Use `evaluate` to run js script, and try-catch is needed to capture exception. +3. Call `setModuleHandler` to resolve js module. For example, you can use assets files as module: + +```dart +await engine.setModuleHandler((String module) async { + return await rootBundle.loadString("js/" + module.replaceFirst(new RegExp(r".js$"), "") + ".js"); +}); +``` + +and in javascript, call `import` function to get module: + +```javascript +import("hello").then(({default: greet}) => greet("world")); +``` + +4. Use `evaluate` to run js script, and try-catch is needed to capture js exception. ```dart try { @@ -75,4 +91,4 @@ try { } ``` -[This example](example/lib/test.dart) contains a complete demonstration on how to use this plugin. +[This example](example/lib/main.dart) contains a complete demonstration on how to use this plugin. diff --git a/cxx/js_dart_promise.hpp b/cxx/js_dart_promise.hpp index 672d8fc..e68b1f9 100644 --- a/cxx/js_dart_promise.hpp +++ b/cxx/js_dart_promise.hpp @@ -3,7 +3,7 @@ * @Author: ekibun * @Date: 2020-08-07 13:55:52 * @LastEditors: ekibun - * @LastEditTime: 2020-08-25 16:07:29 + * @LastEditTime: 2020-08-27 20:31:25 */ #pragma once #include "quickjs/quickjspp.hpp" @@ -59,6 +59,34 @@ namespace qjs DartChannel channel; } JSThreadState; + JSModuleDef *js_module_loader( + JSContext *ctx, + const char *module_name, void *opaque) + { + JSRuntime *rt = JS_GetRuntime(ctx); + JSThreadState *ts = (JSThreadState *)JS_GetRuntimeOpaque(rt); + auto promise = ts->channel("__dart_load_module__", Value{ctx, JS_NewString(ctx, module_name)}); + JSOSFutureArgv argv = promise->get_future().get()(ctx); + if (argv.count > 0) + { + const char *str = JS_ToCString(ctx, argv.argv[0]); + JSValue func_val = JS_Eval(ctx, str, strlen(str), module_name, JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_COMPILE_ONLY); + JS_FreeCString(ctx, str); + JS_FreeValue(ctx, argv.argv[0]); + if (JS_IsException(func_val)) + return NULL; + /* the module is already referenced, so we must free it */ + JSModuleDef *m = (JSModuleDef *)JS_VALUE_GET_PTR(func_val); + JS_FreeValue(ctx, func_val); + return m; + } + else + { + JS_Throw(ctx, argv.argv[0]); + return NULL; + } + } + JSValue js_add_ref(Value val) { JSRuntime *rt = JS_GetRuntime(val.ctx); diff --git a/cxx/js_engine.hpp b/cxx/js_engine.hpp index e02d423..3535e96 100644 --- a/cxx/js_engine.hpp +++ b/cxx/js_engine.hpp @@ -3,7 +3,7 @@ * @Author: ekibun * @Date: 2020-08-08 10:30:59 * @LastEditors: ekibun - * @LastEditTime: 2020-08-26 23:35:20 + * @LastEditTime: 2020-08-27 18:55:57 */ #pragma once @@ -76,6 +76,7 @@ namespace qjs __DartImpl.__invoke(res, rej, method, args)); )xxx", "", JS_EVAL_TYPE_MODULE); + JS_SetModuleLoaderFunc(rt.rt, nullptr, js_module_loader, nullptr); std::vector unresolvedTask; Value promiseWrapper = ctx.eval( R"xxx( diff --git a/example/js/hello.js b/example/js/hello.js new file mode 100644 index 0000000..6578d69 --- /dev/null +++ b/example/js/hello.js @@ -0,0 +1,8 @@ +/* + * @Description: module example + * @Author: ekibun + * @Date: 2020-08-27 19:06:30 + * @LastEditors: ekibun + * @LastEditTime: 2020-08-27 20:39:11 + */ +export default (name) => `hello ${name}!`; \ No newline at end of file diff --git a/example/lib/code/editor.dart b/example/lib/code/editor.dart deleted file mode 100644 index b99d493..0000000 --- a/example/lib/code/editor.dart +++ /dev/null @@ -1,34 +0,0 @@ -/* - * @Description: - * @Author: ekibun - * @Date: 2020-08-01 13:20:06 - * @LastEditors: ekibun - * @LastEditTime: 2020-08-08 17:52:22 - */ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'highlight.dart'; - -class CodeEditor extends StatefulWidget { - final void Function(String) onChanged; - - const CodeEditor({Key key, this.onChanged}) : super(key: key); - @override - _CodeEditorState createState() => _CodeEditorState(); -} - -class _CodeEditorState extends State { - CodeInputController _controller = CodeInputController(); - - @override - Widget build(BuildContext context) { - return TextField( - autofocus: true, - controller: _controller, - textCapitalization: TextCapitalization.none, - decoration: null, - maxLines: null, - onChanged: this.widget.onChanged, - ); - } -} diff --git a/example/lib/code/highlight.dart b/example/lib/highlight.dart similarity index 100% rename from example/lib/code/highlight.dart rename to example/lib/highlight.dart diff --git a/example/lib/main.dart b/example/lib/main.dart index 8a29df7..29d4fb8 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -3,15 +3,16 @@ * @Author: ekibun * @Date: 2020-08-08 08:16:51 * @LastEditors: ekibun - * @LastEditTime: 2020-08-24 22:26:03 + * @LastEditTime: 2020-08-27 20:39:32 */ import 'package:flutter/material.dart'; import 'dart:typed_data'; import 'package:dio/dio.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_qjs/flutter_qjs.dart'; -import 'code/editor.dart'; +import 'highlight.dart'; void main() { runApp(MyApp()); @@ -44,9 +45,11 @@ class TestPage extends StatefulWidget { } class _TestPageState extends State { - String code, resp; + String resp; FlutterJs engine; + CodeInputController _controller = CodeInputController(); + _createEngine() async { if (engine != null) return; engine = FlutterJs(); @@ -72,6 +75,10 @@ class _TestPageState extends State { return JsMethodHandlerNotImplement(); } }); + await engine.setModuleHandler((String module) async { + if(module == "test") return "export default '${new DateTime.now()}'"; + return await rootBundle.loadString("js/" + module.replaceFirst(new RegExp(r".js$"), "") + ".js"); + }); } @override @@ -89,8 +96,7 @@ class _TestPageState extends State { scrollDirection: Axis.horizontal, child: Row( children: [ - FlatButton( - child: Text("create engine"), onPressed: _createEngine), + FlatButton(child: Text("create engine"), onPressed: _createEngine), FlatButton( child: Text("evaluate"), onPressed: () async { @@ -99,8 +105,8 @@ class _TestPageState extends State { return; } try { - resp = (await engine.evaluate(code ?? '', "")) - .toString(); + resp = + (await engine.evaluate(_controller.text ?? '', "")).toString(); } catch (e) { resp = e.toString(); } @@ -120,10 +126,12 @@ class _TestPageState extends State { padding: const EdgeInsets.all(12), color: Colors.grey.withOpacity(0.1), constraints: BoxConstraints(minHeight: 200), - child: CodeEditor( - onChanged: (v) { - code = v; - }, + child: TextField( + autofocus: true, + controller: _controller, + decoration: null, + expands: true, + maxLines: null ), ), SizedBox(height: 16), diff --git a/example/pubspec.lock b/example/pubspec.lock index b1839d1..81d56b1 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -75,7 +75,7 @@ packages: path: ".." relative: true source: path - version: "0.0.4" + version: "0.0.5" flutter_test: dependency: "direct dev" description: flutter diff --git a/example/pubspec.yaml b/example/pubspec.yaml index a248b35..ab8b51c 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -40,7 +40,8 @@ flutter: uses-material-design: true # To add assets to your application, add an assets section, like this: - # assets: + assets: + - js/ # - images/a_dot_burr.jpeg # - images/a_dot_ham.jpeg diff --git a/lib/flutter_qjs.dart b/lib/flutter_qjs.dart index 7f8bd8b..853ccb8 100644 --- a/lib/flutter_qjs.dart +++ b/lib/flutter_qjs.dart @@ -3,7 +3,7 @@ * @Author: ekibun * @Date: 2020-08-08 08:29:09 * @LastEditors: ekibun - * @LastEditTime: 2020-08-26 23:11:10 + * @LastEditTime: 2020-08-27 18:23:47 */ import 'dart:async'; import 'dart:io'; @@ -12,6 +12,9 @@ import 'package:flutter/services.dart'; /// Handle function to manage js call with `dart(method, ...args)` function. typedef JsMethodHandler = Future Function(String method, List args); +/// Handle function to manage js module. +typedef JsModuleHandler = Future Function(String name); + /// return this in [JsMethodHandler] to mark method not implemented. class JsMethodHandlerNotImplement {} @@ -30,14 +33,19 @@ class FlutterJs { /// Set a handler to manage js call with `dart(method, ...args)` function. setMethodHandler(JsMethodHandler handler) async { await _ensureEngine(); - _FlutterJs.instance.methodHandlers[_engine] = handler; + _FlutterJs.instance._methodHandlers[_engine] = handler; + } + + /// Set a handler to manage js module. + setModuleHandler(JsModuleHandler handler) async { + await _ensureEngine(); + _FlutterJs.instance._moduleHandlers[_engine] = handler; } /// Terminate thread and release memory. destroy() async { if (_engine != null) { - await _FlutterJs.instance._channel - .invokeMethod("close", _engine); + await _FlutterJs.instance._channel.invokeMethod("close", _engine); _engine = null; } } @@ -47,8 +55,7 @@ class FlutterJs { await _ensureEngine(); var arguments = {"engine": _engine, "script": command, "name": name}; return _FlutterJs.instance._wrapFunctionArguments( - await _FlutterJs.instance._channel.invokeMethod("evaluate", arguments), - _engine); + await _FlutterJs.instance._channel.invokeMethod("evaluate", arguments), _engine); } } @@ -57,17 +64,23 @@ class _FlutterJs { static _FlutterJs get instance => _getInstance(); static _FlutterJs _instance; MethodChannel _channel = const MethodChannel('soko.ekibun.flutter_qjs'); - Map methodHandlers = - Map(); + Map _methodHandlers = Map(); + Map _moduleHandlers = Map(); _FlutterJs._internal() { _channel.setMethodCallHandler((call) async { var engine = call.arguments["engine"]; var args = call.arguments["args"]; - if (methodHandlers[engine] == null) return call.noSuchMethod(null); - var ret = await methodHandlers[engine]( - call.method, _wrapFunctionArguments(args, engine)); - if (ret is JsMethodHandlerNotImplement) return call.noSuchMethod(null); - return ret; + if (args is List) { + if (_methodHandlers[engine] == null) return call.noSuchMethod(null); + var ret = await _methodHandlers[engine](call.method, _wrapFunctionArguments(args, engine)); + if (ret is JsMethodHandlerNotImplement) return call.noSuchMethod(null); + return ret; + } else { + if (_moduleHandlers[engine] == null) return call.noSuchMethod(null); + var ret = await _moduleHandlers[engine](args); + if (ret is JsMethodHandlerNotImplement) return call.noSuchMethod(null); + return ret; + } }); } dynamic _wrapFunctionArguments(dynamic val, dynamic engine) { @@ -83,13 +96,8 @@ class _FlutterJs { if (val["__js_function__"] != null) { var functionId = val["__js_function__"]; return (List args) async { - var arguments = { - "engine": engine, - "function": functionId, - "arguments": args - }; - return _wrapFunctionArguments( - await _channel.invokeMethod("call", arguments), engine); + var arguments = {"engine": engine, "function": functionId, "arguments": args}; + return _wrapFunctionArguments(await _channel.invokeMethod("call", arguments), engine); }; } else for (var key in val.keys) { diff --git a/pubspec.yaml b/pubspec.yaml index cf435dd..608da56 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_qjs description: This plugin is a simple js engine for flutter using the `quickjs` project. Plugin currently supports Windows, Linux, and Android. -version: 0.0.4 +version: 0.0.5 homepage: https://github.com/ekibun/flutter_qjs environment: