diff --git a/compiler/lib/src/post_builder.dart b/compiler/lib/src/post_builder.dart index 3985de84..f8986b69 100644 --- a/compiler/lib/src/post_builder.dart +++ b/compiler/lib/src/post_builder.dart @@ -14,6 +14,7 @@ import 'package:crypto/crypto.dart' show md5; import 'package:fair_compiler/src/state_transfer.dart'; import 'package:path/path.dart' as path; import 'package:fair_dart2js/index.dart' as dart2js; +import 'package:yaml/yaml.dart'; import 'helper.dart' show FlatCompiler, ModuleNameHelper; class ArchiveBuilder extends PostProcessBuilder with FlatCompiler { @@ -56,7 +57,24 @@ class ArchiveBuilder extends PostProcessBuilder with FlatCompiler { print('\u001b[33m [Fair Dart2JS] partPath => ${partPath} \u001b[0m'); if (File(partPath).existsSync()) { try { - var result = await dart2js.convertFile(partPath, true); + var uglify = true; + + var optionsYamlPath = + path.join(Directory.current.path, 'fair_compiler_options.yaml'); + + var optionYamlFile = File(optionsYamlPath); + if (optionYamlFile.existsSync()) { + var optionsYaml = + loadYaml(optionYamlFile.readAsStringSync()) as YamlMap?; + + if (optionsYaml != null && optionsYaml.containsKey('uglify')) { + uglify = optionsYaml['uglify']; + } + } + + print('\u001b[33m [Fair Dart2JS] uglify option: => $uglify \u001b[0m'); + + var result = await dart2js.convertFile(partPath, uglify); File(jsName)..writeAsStringSync(result); } catch (e) { print('[Fair Dart2JS] e => ${e}'); diff --git a/compiler/pubspec.yaml b/compiler/pubspec.yaml index 3b806c05..2ee5c880 100644 --- a/compiler/pubspec.yaml +++ b/compiler/pubspec.yaml @@ -22,6 +22,7 @@ dependencies: glob: ^2.0.1 pedantic: ^1.11.1 http: ^0.13.3 + yaml: ^3.0.0 fair_annotation: ^2.3.0 # fair_annotation: diff --git a/fair/android/src/main/java/com/wuba/fair/channel/FairFfi.java b/fair/android/src/main/java/com/wuba/fair/channel/FairFfi.java index 68d4d208..ded5a7d0 100644 --- a/fair/android/src/main/java/com/wuba/fair/channel/FairFfi.java +++ b/fair/android/src/main/java/com/wuba/fair/channel/FairFfi.java @@ -14,6 +14,9 @@ import androidx.annotation.Keep; +import org.json.JSONException; +import org.json.JSONObject; + /** * dart ffi */ @@ -66,6 +69,18 @@ public void runTask() { return result[0].toString(); } } + try { + //when an exception occurs, return the function name + JSONObject jsonObject = new JSONObject(args); + if (jsonObject.has("args")) { + JSONObject funObject = new JSONObject(jsonObject.optString("args")); + if(funObject.has("funcName")){ + return String.format("Runtime error while invoke JavaScript method:%s()", funObject.optString("funcName")); + } + } + } catch (JSONException e) { + e.printStackTrace(); + } return "jsAppObj is null"; } diff --git a/fair/android/src/main/java/com/wuba/fair/core/FairV8JsLoader.java b/fair/android/src/main/java/com/wuba/fair/core/FairV8JsLoader.java index b41b45d9..86c02c04 100644 --- a/fair/android/src/main/java/com/wuba/fair/core/FairV8JsLoader.java +++ b/fair/android/src/main/java/com/wuba/fair/core/FairV8JsLoader.java @@ -11,6 +11,7 @@ import com.eclipsesource.v8.V8Array; import com.eclipsesource.v8.V8Function; import com.eclipsesource.v8.V8Object; +import com.eclipsesource.v8.V8ScriptCompilationException; import com.wuba.fair.FairPlugin; import com.wuba.fair.callback.JsResultCallback; import com.wuba.fair.constant.Constant; @@ -125,6 +126,22 @@ public void loadMainJs(Object arguments, JsResultCallback callback) { getV8JsExecutor().loadJS(jsName, jsLocalPath, callback); } catch (Exception e) { e.printStackTrace(); + //avoid loading all the time + if (callback != null) { + try { + JSONObject errorResult = new JSONObject(); + errorResult.put("status","error"); + if (e instanceof V8ScriptCompilationException){ + errorResult.put("errorInfo",e.toString()); + errorResult.put("lineNumber",((V8ScriptCompilationException) e).getLineNumber()); + }else{ + errorResult.put("errorInfo",e.getLocalizedMessage()); + } + callback.call(errorResult.toString()); + } catch (JSONException ex) { + callback.call("result"); + } + } } } diff --git a/fair/android/src/main/java/com/wuba/fair/thread/FairTask.java b/fair/android/src/main/java/com/wuba/fair/thread/FairTask.java index 3a204164..4dc10553 100644 --- a/fair/android/src/main/java/com/wuba/fair/thread/FairTask.java +++ b/fair/android/src/main/java/com/wuba/fair/thread/FairTask.java @@ -11,7 +11,6 @@ public abstract class FairTask implements Runnable { @Override public void run() { - FairLogger.d("当前的线程名称" + Thread.currentThread()); runTask(); diff --git a/fair/ios/Classes/FairDynamicJSPlugin/FairDartBridge.m b/fair/ios/Classes/FairDynamicJSPlugin/FairDartBridge.m index 233b3ef3..463fe9fe 100644 --- a/fair/ios/Classes/FairDynamicJSPlugin/FairDartBridge.m +++ b/fair/ios/Classes/FairDynamicJSPlugin/FairDartBridge.m @@ -69,6 +69,24 @@ - (void)setDartListener { if ([method isEqualToString:@"loadMainJs"]) { if ([strongSelf.delegate respondsToSelector:@selector(injectionJSScriptWtihJSScript: callback:)]) { [strongSelf.delegate injectionJSScriptWtihJSScript:model.path callback:^(id result, NSError *error) { + JSValue *value = result; + if (value && [value isKindOfClass:[JSValue class]]) { + NSString *str = value.toString; + if([str isEqualToString:@"undefined"]){ + NSMutableDictionary *result=[NSMutableDictionary dictionary]; + result[@"status"] = @"error"; + result[@"errorInfo"]=@"load JavaScript error"; + result[@"lineNumber"]=@-1; + + NSError *error = nil; + NSData *jsonData = [NSJSONSerialization dataWithJSONObject:result options:NSJSONWritingPrettyPrinted error:&error]; + NSString *jsonStr = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + + + callback(jsonStr); + return; + } + } callback(@"success"); }]; } @@ -163,6 +181,23 @@ - (const char *)executeScriptSyncImpl:(char *)args } NSString *result = [NSString stringWithFormat:@"%@", obj.toString]; FairLog(@"result:%@", result); + if([result isEqualToString:@"undefined"]){ + //取args中的funcName字段 + //arg ===> "{\"pageName\":\"null#0\",\"type\":\"method\",\"args\":{\"funcName\":\"_getAuth\",\"args\":null}}" + NSString *str = [NSString stringWithUTF8String:args]; + NSData *jsonData = [str dataUsingEncoding:NSUTF8StringEncoding]; + + NSDictionary *dic = [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingMutableContainers error:nil]; + + NSDictionary *args = dic[@"args"]; + NSString *funcName = args[@"funcName"]; + + FairLog(@"invoke funcName:%@",funcName); + + NSString *errorResult = [NSString stringWithFormat:@"Runtime error while invoke JavaScript method:%@()", funcName]; + + return errorResult.UTF8String; + } return result.UTF8String; } diff --git a/fair/lib/src/internal/bind_data.dart b/fair/lib/src/internal/bind_data.dart index 1247d413..cd925f15 100644 --- a/fair/lib/src/internal/bind_data.dart +++ b/fair/lib/src/internal/bind_data.dart @@ -68,8 +68,12 @@ class BindingData { } else { result = _functions?['runtimeInvokeMethodSync']?.call(funcName); } - var value = jsonDecode(result); - return value['result']['result']; + try { + var value = jsonDecode(result); + return value['result']['result']; + } catch (e) { + throw RuntimeError(errorMsg: result); + } } else { return _functions?[funcName]; } @@ -158,3 +162,14 @@ class BindingData { _functions?.clear(); } } + +class RuntimeError extends Error { + final String errorMsg; + + RuntimeError({required this.errorMsg}); + + @override + String toString() { + return errorMsg; + } +} diff --git a/fair/lib/src/internal/error_tips.dart b/fair/lib/src/internal/error_tips.dart index c077bfef..f29a66f4 100644 --- a/fair/lib/src/internal/error_tips.dart +++ b/fair/lib/src/internal/error_tips.dart @@ -4,8 +4,8 @@ * found in the LICENSE file. */ +import 'package:fair/src/internal/stack_trace_detail.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'warning_dialog_widget.dart'; @@ -15,9 +15,21 @@ class WarningWidget extends StatelessWidget { final String? solution; final dynamic error; final BuildContext? parentContext; + final String? stackTrace; + final Map? errorBlock; + final List? highlightLines; - const WarningWidget({Key? key, this.name, this.url, this.error, this.solution,this.parentContext}) + const WarningWidget( + {Key? key, + this.name, + this.url, + this.error, + this.solution, + this.parentContext, + this.stackTrace, + this.errorBlock, + this.highlightLines}) : super(key: key); @override @@ -65,6 +77,20 @@ class WarningWidget extends StatelessWidget { cancelFun: (){ Navigator.pop(parentContext!); }, + stackTraceVisible: stackTrace != null, + viewStackTrace: () { + Navigator.push( + parentContext!, + MaterialPageRoute( + builder: (context) => + StackTraceDetailPage( + stackTrace: stackTrace!, + name: name, + errorJson: errorBlock, + error: error, + highlightLines: highlightLines, + ))); + }, ); } ); diff --git a/fair/lib/src/internal/stack_trace_detail.dart b/fair/lib/src/internal/stack_trace_detail.dart new file mode 100644 index 00000000..52b90c71 --- /dev/null +++ b/fair/lib/src/internal/stack_trace_detail.dart @@ -0,0 +1,159 @@ +import 'dart:convert'; + +import 'package:fair/src/extension.dart'; +import 'package:flutter/material.dart'; + +class StackTraceDetailPage extends StatelessWidget { + final String stackTrace; + final String? name; + final Map? errorJson; + final dynamic error; + final List? highlightLines; + + StackTraceDetailPage( + {required this.stackTrace, this.name, this.errorJson, this.error,this.highlightLines}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('StackTrace'), + leading: InkWell( + child: Icon(Icons.arrow_back), + onTap: () { + Navigator.pop(context); + }, + ), + ), + body: SingleChildScrollView( + child: Column( + children: [ + _stackTraceHeader(), + _stackTraceDetails(), + ], + ), + ), + ); + } + + Widget _stackTraceHeader() => RichText( + text: TextSpan( + text: '══╡ EXCEPTION CAUGHT BY FAIR RUNTIME ╞══\n', + style: TextStyle( + fontSize: 15.0, + color: Colors.black, + ), + children: [ + ..._getErrorTagNode(), + TextSpan( + text: '\n\nRuntime Error:\n', + style: TextStyle( + fontSize: 22.0, + color: Colors.black, + ), + ), + TextSpan( + text: '$error', + style: TextStyle( + fontSize: 15.0, + color: Colors.black, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ); + + List _getErrorTagNode() { + if (errorJson == null) return []; + + return [ + TextSpan( + text: '\nError Tag:', + style: TextStyle( + fontSize: 15.0, + color: Colors.black, + ), + ), + TextSpan( + text: name, + style: TextStyle( + fontWeight: FontWeight.bold, + background: Paint()..color = Colors.redAccent, + )), + TextSpan( + text: ', while parsing:\n', + style: TextStyle( + fontSize: 15.0, + color: Colors.black, + ), + ), + _buildFormattedJsonSpan() + ]; + } + + TextSpan _buildFormattedJsonSpan() { + var encoder = JsonEncoder.withIndent(' '); + final formattedJson = encoder.convert(errorJson); + return TextSpan( + text: formattedJson, + style: TextStyle( + fontSize: 15.0, + color: Colors.black, + fontWeight: FontWeight.bold, + ), + ); + } + + final String PKG_PREFIX = 'package:'; + final String DART_SUFFIX = '.dart'; + + RichText _stackTraceDetails() { + final splitter = LineSplitter(); + final stackTraceLines = splitter.convert(stackTrace); + + final defaultTextStyle = + TextStyle(color: Color(0xff333333), fontWeight: FontWeight.w500); + + + final spans = stackTraceLines.mapEach((index, e) { + if (highlightLines?.isNotEmpty == true) { + //lineNumber => index+1 + if (highlightLines?.contains(index + 1) == true) { + //highlight + return TextSpan( + text: '$e\n', + style: TextStyle( + fontWeight: FontWeight.bold, + background: Paint()..color = Colors.redAccent, + )); + } else { + return TextSpan(text: '$e\n'); + } + } else { + if (e.contains(PKG_PREFIX)) { + final span1 = e.substring(0, e.indexOf(PKG_PREFIX)); + final highlightSpan = e.substring( + e.indexOf(PKG_PREFIX), e.lastIndexOf(DART_SUFFIX) + 5); + final span2 = e.substring(e.lastIndexOf(DART_SUFFIX) + 5); + + return TextSpan(text: span1, style: defaultTextStyle, children: [ + TextSpan( + text: highlightSpan, + style: TextStyle(color: Color(0xFF3978C4))), + TextSpan(text: span2, style: defaultTextStyle), + TextSpan(text: '\n', style: defaultTextStyle), + ]); + } else { + return TextSpan(text: '$e\n'); + } + } + }).toList(); + + return RichText( + text: TextSpan( + text: '\nWhen the exception was thrown, this was the stack:\n\n', + children: spans, + style: defaultTextStyle)); + } +} diff --git a/fair/lib/src/internal/warning_dialog_widget.dart b/fair/lib/src/internal/warning_dialog_widget.dart index bf43f1b4..c3ea29a8 100644 --- a/fair/lib/src/internal/warning_dialog_widget.dart +++ b/fair/lib/src/internal/warning_dialog_widget.dart @@ -6,6 +6,8 @@ class DialogWidget extends Dialog { final String? solution ; //是否须要"取消"按钮 final dynamic error ; //错误 void Function()? cancelFun; //取消 + void Function()? viewStackTrace; //查看堆栈 + final bool? stackTraceVisible;//堆栈按钮是否可见 DialogWidget({ @@ -15,6 +17,8 @@ class DialogWidget extends Dialog { this.solution, this.error, this.cancelFun, + this.viewStackTrace, + this.stackTraceVisible, }) : super(key: key); @override @@ -56,14 +60,16 @@ class DialogWidget extends Dialog { mainAxisAlignment: MainAxisAlignment.center, children: [ - Text( - 'Tag: $name', - style: TextStyle( - fontWeight: FontWeight.bold, - color: Color(0xffff0000), - fontSize: 20.0, - ), - ), + Visibility( + visible: name != null, + child: Text( + 'Tag: $name', + style: TextStyle( + fontWeight: FontWeight.bold, + color: Color(0xffff0000), + fontSize: 20.0, + ), + )), SizedBox(height: 10), Text( 'Bundle: $url', @@ -111,9 +117,11 @@ class DialogWidget extends Dialog { var widgets = []; widgets.add(_buildBottomCancelButton()); widgets.add(_buildBottomOnline()); + widgets.add(_buildBottomStackTraceButton()); return Flex( direction: Axis.horizontal, + mainAxisAlignment: MainAxisAlignment.spaceAround, children: widgets, ); } @@ -128,7 +136,7 @@ class DialogWidget extends Dialog { Widget _buildBottomCancelButton() { return Flexible( - fit: FlexFit.tight, + fit: FlexFit.loose, child:InkWell( onTap: this.cancelFun, child: Text('Cancel', style: TextStyle(color: Color(0xff666666))), @@ -136,4 +144,16 @@ class DialogWidget extends Dialog { ); } + Widget _buildBottomStackTraceButton() { + if (stackTraceVisible == null || !stackTraceVisible!) { + return Container(); + } + return Flexible( + fit: FlexFit.loose, + child: InkWell( + onTap: viewStackTrace, + child: Text('StackTrace', style: TextStyle(color: Color(0xFF6888DE))), + ), + ); + } } \ No newline at end of file diff --git a/fair/lib/src/render/builder.dart b/fair/lib/src/render/builder.dart index d1fd30cd..ec3b7042 100644 --- a/fair/lib/src/render/builder.dart +++ b/fair/lib/src/render/builder.dart @@ -7,6 +7,8 @@ * found in the LICENSE file. */ +import 'dart:convert'; +import 'dart:developer' as developer; import 'package:fair/fair.dart'; import 'package:fair/src/type.dart'; import 'package:flutter/material.dart'; @@ -48,7 +50,6 @@ class DynamicWidgetBuilder extends DynamicBuilder { dynamic convert(BuildContext context, Map map, Map? methodMap, {Domain? domain}) { var name = map[tag]; - print('name:$name'); if (name == null) { return WarningWidget( parentContext: context, @@ -159,12 +160,14 @@ class DynamicWidgetBuilder extends DynamicBuilder { return children.asListOf() ?? children; } return block(map, methodMap, context, domain, mapper, name, isWidget); - } catch (e) { + } catch (e, stack) { return WarningWidget( parentContext: context, name: name, error: e, url: bundle, + stackTrace: stack.toString(), + errorBlock: map, solution: "Tag name not supported yet,You need to use the @FairBinding annotation to tag the local Widget component"); } @@ -198,10 +201,33 @@ class DynamicWidgetBuilder extends DynamicBuilder { stack: stack, context: ErrorSummary('while parsing widget of $name, $fun'), )); - throw ArgumentError('name===$name,fun===$fun, error===$e, map===$map'); + + //print StackTrack in console + _dumpErrorToConsole(name, map, e, stack); + + rethrow; } } + void _dumpErrorToConsole(String name, Map map, Object e, StackTrace stack) { + var encoder = JsonEncoder.withIndent(' '); + final formattedJson = encoder.convert(map); + + final errorFormatText = ''' + + ══╡ EXCEPTION CAUGHT BY FAIR RUNTIME ╞══════════════════════════════════════════════════════════════ + Error Tag:$name, while parsing: + +$formattedJson + + $e + + When the exception was thrown, this was the stack: + '''; + + developer.log('', error: errorFormatText, level: 900, stackTrace: stack); + } + W positioned( dynamic paMap, Map? methodMap, BuildContext context, Domain? domain) { var pa = []; diff --git a/fair/lib/src/widget.dart b/fair/lib/src/widget.dart index c3e8b427..2e43a5af 100644 --- a/fair/lib/src/widget.dart +++ b/fair/lib/src/widget.dart @@ -6,6 +6,7 @@ import 'dart:convert'; +import 'package:fair/src/internal/error_tips.dart'; import 'package:fair/src/runtime/fair_message_dispatcher.dart'; import 'package:fair/src/runtime/runtime_fair_delegate.dart'; import 'package:flutter/foundation.dart'; @@ -98,6 +99,11 @@ class FairState extends State with Loader, AutomaticKeepAliveClientM FairApp? _fairApp; String? bundleType; late String state2key; + bool isLoadJsError = false; + String? loadJsErrorInfo; + String? jsSource; + String? rawJsPath; + int? loadJsErrorLineNumber; // None nullable late FairDelegate delegate; @@ -128,7 +134,36 @@ class FairState extends State with Loader, AutomaticKeepAliveClientM } // if it's not in a tree, it's not unnecessary to load js any more. if(mounted) { - await Future.wait([_mFairApp.runtime.addScript(state2key, resolveJS, widget.data), _mFairApp.register(this)]); + final results = await Future.wait([ + _mFairApp.runtime.addScript(state2key, resolveJS, widget.data), + _mFairApp.register(this) + ]); + + //debug mode throw error message to WarningWidget + if (!kReleaseMode && results.isNotEmpty) { + print('addScript Result:${results.first}'); + try { + final addScriptResult = results.first.toString(); + var errorResult = jsonDecode(addScriptResult); + isLoadJsError = (errorResult['status'] == 'error'); + + if (isLoadJsError) { + loadJsErrorInfo = errorResult['errorInfo'] ?? ''; + loadJsErrorLineNumber = errorResult['lineNumber']; + + //debug mode use json + if(widget.path?.endsWith('.fair.json') == true){ + rawJsPath = widget.path?.replaceFirst('.fair.json', '.fair.js') ?? + widget.path; + + jsSource = await rootBundle.loadString(rawJsPath!); + } + } + } catch (e) { + print(e); + } + } + if(mounted) { delegate.didChangeDependencies(); _reload(); @@ -164,6 +199,19 @@ class FairState extends State with Loader, AutomaticKeepAliveClientM Widget build(BuildContext context) { super.build(context); assert(_fairApp != null, 'FairWidget must be descendant of FairApp'); + if (!kReleaseMode && isLoadJsError) { + return Scaffold( + body: WarningWidget( + parentContext: context, + url: rawJsPath, + error: loadJsErrorInfo, + stackTrace: jsSource, + highlightLines: [loadJsErrorLineNumber ?? -1], + solution: + 'Unsupported JavaScript syntax, please check and correct your Dart method coding!', + ), + ); + } var builder = widget.holder ?? _fairApp?.placeholderBuilder; var result = _child ?? builder?.call(context); if (!kReleaseMode && _fairApp!.debugShowFairBanner) { diff --git a/fair/pubspec.yaml b/fair/pubspec.yaml index c35686d4..013826d1 100644 --- a/fair/pubspec.yaml +++ b/fair/pubspec.yaml @@ -22,6 +22,7 @@ dependencies: flat_buffers: ^2.0.5 url_launcher: ^6.0.10 http: ^0.13.3 + logging: ^1.2.0 dev_dependencies: flutter_test: