diff --git a/packages/javascript/javascript/README.md b/packages/javascript/javascript/README.md index d78427a..d747e7c 100644 --- a/packages/javascript/javascript/README.md +++ b/packages/javascript/javascript/README.md @@ -1,6 +1,32 @@ -# Javascript +# JavaScript -Enable your app to evaluate javascript programs. A simple library that provides a javascript runtime where you can run your javascript. +Enable your app to evaluate javascript programs. A simple library that provides a javascript environment where you can run your javascript code. + +## Advantages + +For applications requiring non-interactive JavaScript evaluation, using this JavaScript library has the following advantages: + +- Lower resource consumption, since there is no need to allocate a WebView instance. +- Multiple isolated javascript environments with low overhead, enabling the application to run several JavaScript snippets simultaneously. +- Provides implementation for setTimeout in the javascript environment. +- Support for javascript channels for sending asynchronous messages from javascript environment to host and then receive a reply asynchronously as Promise. +- Supports loading javaScript code from a file and then evaluate it for efficient evaluation of large scripts that may be expensive to pass as a String. + +## Implementation + +### Android + +The Android plugin [javascript_android] implementation uses the [JavaScriptIsolate](https://developer.android.com/reference/kotlin/androidx/javascriptengine/JavaScriptIsolate) as the javascript environment from the [Android Jetpack JavaScript Engine](https://developer.android.com/reference/kotlin/androidx/javascriptengine) library. + +### iOS & MacOS + +The iOS, & MacOS plugin [javascript_darwin] implementation uses [JSContext](https://developer.apple.com/documentation/javascriptcore/jscontext) from [Apple's JavaScriptCore](https://developer.apple.com/documentation/javascriptcore) framework. + +## Using + +The easiest way to use this library is via the high-level interface defined by [JavaScript] class. + +___ Developed with 💙 by [Reclaim Protocol Team][reclaimprotocol_link] ✨ diff --git a/packages/javascript/javascript/example/ios/Runner.xcodeproj/project.pbxproj b/packages/javascript/javascript/example/ios/Runner.xcodeproj/project.pbxproj index f73e4c0..50299ad 100644 --- a/packages/javascript/javascript/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/javascript/javascript/example/ios/Runner.xcodeproj/project.pbxproj @@ -14,6 +14,8 @@ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + C18B3414394AF2B7D715038F /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE53F133A9F3BB6133AF9097 /* Pods_Runner.framework */; }; + F4C7252B52516FA16F1A222C /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 35AEE5AE5D4DD0190C00785B /* Pods_RunnerTests.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -40,11 +42,16 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 0CCFC7F62B43C040398F1023 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 310D22F9A35A3CF9422D69AE /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 35AEE5AE5D4DD0190C00785B /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 47634C540BFF1FDF01E5B0C0 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + 71F017915446B3AE64C7186C /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; @@ -55,13 +62,25 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 9B56F23C0F6328AB5ED79AA8 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + C7AD750105693F3956607AF2 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + CE53F133A9F3BB6133AF9097 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 562F4CC1C5DFE72356BA0E49 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + F4C7252B52516FA16F1A222C /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 97C146EB1CF9000F007C117D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + C18B3414394AF2B7D715038F /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -94,6 +113,8 @@ 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, 331C8082294A63A400263BE5 /* RunnerTests */, + F5C7AB9347B53C7AD92D7BE3 /* Pods */, + D79F224981C525B33C0BC556 /* Frameworks */, ); sourceTree = ""; }; @@ -121,6 +142,29 @@ path = Runner; sourceTree = ""; }; + D79F224981C525B33C0BC556 /* Frameworks */ = { + isa = PBXGroup; + children = ( + CE53F133A9F3BB6133AF9097 /* Pods_Runner.framework */, + 35AEE5AE5D4DD0190C00785B /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + F5C7AB9347B53C7AD92D7BE3 /* Pods */ = { + isa = PBXGroup; + children = ( + 9B56F23C0F6328AB5ED79AA8 /* Pods-Runner.debug.xcconfig */, + C7AD750105693F3956607AF2 /* Pods-Runner.release.xcconfig */, + 71F017915446B3AE64C7186C /* Pods-Runner.profile.xcconfig */, + 0CCFC7F62B43C040398F1023 /* Pods-RunnerTests.debug.xcconfig */, + 310D22F9A35A3CF9422D69AE /* Pods-RunnerTests.release.xcconfig */, + 47634C540BFF1FDF01E5B0C0 /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -128,8 +172,10 @@ isa = PBXNativeTarget; buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( + 07B0E66F96123F84A12BE111 /* [CP] Check Pods Manifest.lock */, 331C807D294A63A400263BE5 /* Sources */, 331C807F294A63A400263BE5 /* Resources */, + 562F4CC1C5DFE72356BA0E49 /* Frameworks */, ); buildRules = ( ); @@ -145,12 +191,14 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + 34771D4095194AD893A4FF68 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 6D85BD65D456EC00FD8C7CF9 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -222,6 +270,50 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 07B0E66F96123F84A12BE111 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 34771D4095194AD893A4FF68 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -238,6 +330,23 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; + 6D85BD65D456EC00FD8C7CF9 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -379,6 +488,7 @@ }; 331C8088294A63A400263BE5 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 0CCFC7F62B43C040398F1023 /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -396,6 +506,7 @@ }; 331C8089294A63A400263BE5 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 310D22F9A35A3CF9422D69AE /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -411,6 +522,7 @@ }; 331C808A294A63A400263BE5 /* Profile */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 47634C540BFF1FDF01E5B0C0 /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; diff --git a/packages/javascript/javascript/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/javascript/javascript/example/ios/Runner.xcworkspace/contents.xcworkspacedata index 1d526a1..21a3cc1 100644 --- a/packages/javascript/javascript/example/ios/Runner.xcworkspace/contents.xcworkspacedata +++ b/packages/javascript/javascript/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -4,4 +4,7 @@ + + diff --git a/packages/javascript/javascript/example/lib/json.dart b/packages/javascript/javascript/example/lib/json.dart deleted file mode 100644 index da7c5b0..0000000 --- a/packages/javascript/javascript/example/lib/json.dart +++ /dev/null @@ -1,60 +0,0 @@ - -import 'dart:convert'; -import 'dart:typed_data'; - -final RegExp _base64 = RegExp(r'^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=|[A-Za-z0-9+\/]{4})$'); - -/// check if a string is base64 encoded -bool _isBase64(String str) { - return _base64.hasMatch(str); -} - -Object? bytesToBase64Json(Uint8List bytes) { - final base64String = base64.encode(bytes); - return {'type': 'uint8array', 'value': base64String}; -} - -Object? _base64JsonReviver(Object? key, Object? value) { - if (value is Map) { - final hasStringKeys = value.keys.every((key) => key is String); - final hasIntValues = value.values.every((value) => value is int); - if (hasStringKeys && hasIntValues) { - final bytes = Uint8List(value.length); - for (final entry in value.entries) { - bytes[int.parse(entry.key)] = entry.value as int; - } - final base64String = base64.encode(bytes); - return {'type': 'uint8array', 'value': base64String}; - } - } - if (value is String && _isBase64(value)) { - return {'type': 'uint8array', 'value': value}; - } - return value; -} - -String reformatJsonStringForRPC(String jsonString) { - return json.encode(json.decode(jsonString, reviver: _base64JsonReviver)); -} - -Object? replaceBase64Json(Object? value) { - if (value is Map) { - if (value['type'] == 'uint8array') { - return value['value']; - } - final newMap = {}; - for (final entry in value.entries) { - newMap[entry.key] = replaceBase64Json(entry.value); - } - return newMap; - } - if (value is List) { - final newList = []; - for (final entry in value) { - newList.add(replaceBase64Json(entry)); - } - return newList; - } - - return value; -} \ No newline at end of file diff --git a/packages/javascript/javascript/example/lib/main.dart b/packages/javascript/javascript/example/lib/main.dart index 080dfac..cfda069 100644 --- a/packages/javascript/javascript/example/lib/main.dart +++ b/packages/javascript/javascript/example/lib/main.dart @@ -1,16 +1,35 @@ -import 'dart:convert'; - -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; -import 'package:javascript/javascript.dart'; -import 'package:javascript_example/runtime_service.dart'; +import 'package:javascript_example/src/runtime_service.dart'; +import 'package:logging/logging.dart'; + +import 'src/model.dart'; +import 'src/ui/command_input.dart'; +import 'src/ui/console_output.dart'; +import 'src/ui/url_input.dart'; void main() { WidgetsFlutterBinding.ensureInitialized(); + + // Load fonts GoogleFonts.config.allowRuntimeFetching = true; GoogleFonts.pendingFonts([GoogleFonts.sourceCodePro()]); + // setup logging + hierarchicalLoggingEnabled = true; + Logger.root.level = Level.ALL; + Logger.root.onRecord.listen((record) { + debugPrint( + '(${record.sequenceNumber.toString().padLeft(4)}) ${record.level.name.padLeft(7)} [${record.time}]: ${record.message}', + ); + if (record.error != null) { + debugPrint(record.error.toString()); + if (record.stackTrace != null) { + debugPrintStack(stackTrace: record.stackTrace); + } + } + }); + runApp(const JsInterpreterApp()); } @@ -40,228 +59,6 @@ class JsInterpreterApp extends StatelessWidget { } } -// Repository Layer -class JsCodeRepository { - final JsRuntimeService _jsService; - - JsCodeRepository(this._jsService); - - Future executeCode(String code) async { - return await _jsService.evaluate(code); - } - - Future loadFromUrl(String url) async { - return await _jsService.loadJsFromUrl(url); - } - - void setConsole({ - required void Function(String message) log, - required void Function(String message) error, - required void Function(String message) info, - required void Function(String message) warn, - }) { - return _jsService.setConsole( - log: log, - error: error, - info: info, - warn: warn, - ); - } - - void setRPCMessageHandler( - String module, - void Function(dynamic args) handler, - ) { - return _jsService.setRPCMessageHandler(module, handler); - } -} - -// Use Case Layer -class ExecuteJsUseCase { - final JsCodeRepository _repository; - - ExecuteJsUseCase(this._repository); - - Future execute(String code) async { - return await _repository.executeCode(code); - } - - Future loadFromUrl(String url) async { - return await _repository.loadFromUrl(url); - } - - void setConsole({ - required void Function(String message) log, - required void Function(String message) error, - required void Function(String message) info, - required void Function(String message) warn, - }) { - return _repository.setConsole( - log: log, - error: error, - info: info, - warn: warn, - ); - } - - void setRPCMessageHandler( - String module, - void Function(dynamic args) handler, - ) { - return _repository.setRPCMessageHandler(module, handler); - } -} - -// ViewModel -class JsInterpreterViewModel extends ChangeNotifier { - final ExecuteJsUseCase _useCase; - - Object? _result; - Object? _error; - bool _isLoading = false; - final List _consoleHistory = []; - final TextEditingController _inputController = TextEditingController(); - final TextEditingController _urlController = TextEditingController(); - - JsInterpreterViewModel(this._useCase) { - _useCase.setConsole( - log: (message) => _addToHistory(ConsoleEntry.output(message)), - error: (message) => _addToHistory(ConsoleEntry.error(message)), - info: (message) => _addToHistory(ConsoleEntry.info(message)), - warn: (message) => _addToHistory(ConsoleEntry.warn(message)), - ); - _useCase.setRPCMessageHandler('attestor-core', (args) { - try { - _addToHistory( - ConsoleEntry.output(args is String ? args : json.encode(args)), - ); - } catch (e, s) { - _addToHistory(ConsoleEntry.error('Error parsing RPC message: $e\n$s')); - } - }); - } - - Object? get result => _result; - Object? get error => _error; - bool get isLoading => _isLoading; - List get consoleHistory => _consoleHistory; - TextEditingController get inputController => _inputController; - TextEditingController get urlController => _urlController; - - Future executeCode() async { - final code = _inputController.text.trim(); - if (code.isEmpty) { - if (kDebugMode) { - print('no code entered'); - } - return; - } - - _addToHistory(ConsoleEntry.input(code)); - _setLoading(true); - _clearError(); - - try { - _result = await _useCase.execute(code); - _addToHistory(ConsoleEntry.output(_result.toString())); - clearInput(); - } catch (e) { - _error = e; - _addToHistory( - ConsoleEntry.error( - e is JavaScriptExecutionException ? e.message : e.toString(), - ), - ); - } finally { - _setLoading(false); - } - } - - Future loadFromUrl() async { - final url = _urlController.text.trim(); - if (url.isEmpty) return; - - _setLoading(true); - _clearError(); - - try { - final code = await _useCase.loadFromUrl(url); - _inputController.text = code; - _addToHistory(ConsoleEntry.info('Loaded code from: $url')); - } catch (e) { - _error = e; - _addToHistory( - ConsoleEntry.error('Failed to load from URL: ${e.toString()}'), - ); - } finally { - _setLoading(false); - } - } - - void clearAll() { - _inputController.clear(); - _urlController.clear(); - _clearResult(); - _clearError(); - _consoleHistory.clear(); - notifyListeners(); - } - - void clearInput() { - _inputController.clear(); - notifyListeners(); - } - - void _setLoading(bool loading) { - _isLoading = loading; - notifyListeners(); - } - - void _clearError() { - _error = null; - notifyListeners(); - } - - void _clearResult() { - _result = null; - notifyListeners(); - } - - void _addToHistory(ConsoleEntry entry) { - _consoleHistory.add(entry); - notifyListeners(); - } - - @override - void dispose() { - _inputController.dispose(); - _urlController.dispose(); - super.dispose(); - } -} - -// Models -class ConsoleEntry { - final String content; - final ConsoleEntryType type; - final DateTime timestamp; - - ConsoleEntry(this.content, this.type) : timestamp = DateTime.now(); - - factory ConsoleEntry.input(String content) => - ConsoleEntry(content, ConsoleEntryType.input); - factory ConsoleEntry.output(String content) => - ConsoleEntry(content, ConsoleEntryType.output); - factory ConsoleEntry.error(String content) => - ConsoleEntry(content, ConsoleEntryType.error); - factory ConsoleEntry.info(String content) => - ConsoleEntry(content, ConsoleEntryType.info); - factory ConsoleEntry.warn(String content) => - ConsoleEntry(content, ConsoleEntryType.warn); -} - -enum ConsoleEntryType { input, output, error, info, warn } - // Main Page class JsInterpreterPage extends StatefulWidget { const JsInterpreterPage({super.key}); @@ -271,25 +68,26 @@ class JsInterpreterPage extends StatefulWidget { } class _JsInterpreterPageState extends State { - JsInterpreterViewModel? _viewModel; - JsRuntimeService? _jsService; - ExecuteJsUseCase? _useCase; - JsCodeRepository? _repository; + final JsRuntimeService _jsService = JsRuntimeService(); + late final JsInterpreterViewModel _viewModel = JsInterpreterViewModel( + _jsService, + ); final ScrollController _scrollController = ScrollController(); @override void initState() { super.initState(); + _viewModel.addListener(_onViewModelChanged); _initializeServices(); } + bool _isInitialized = false; + Future _initializeServices() async { - _jsService = JsRuntimeService(); - await _jsService!.initialize(); - _repository = JsCodeRepository(_jsService!); - _useCase = ExecuteJsUseCase(_repository!); - _viewModel = JsInterpreterViewModel(_useCase!); - _viewModel!.addListener(_onViewModelChanged); + if (_isInitialized) return; + await _jsService.initialize(); + await _viewModel.initialize(); + _isInitialized = true; setState(() {}); } @@ -310,15 +108,15 @@ class _JsInterpreterPageState extends State { @override void dispose() { - _viewModel?.dispose(); - _jsService?.dispose(); + _viewModel.dispose(); + _jsService.dispose(); _scrollController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { - if (_viewModel == null) { + if (!_isInitialized) { return const Scaffold( body: Center(child: CircularProgressIndicator(color: Colors.green)), ); @@ -329,7 +127,7 @@ class _JsInterpreterPageState extends State { title: const Text('JS Interpreter Console'), actions: [ IconButton( - onPressed: _viewModel?.clearAll, + onPressed: _viewModel.clearAll, icon: const Icon(Icons.clear_all), tooltip: 'Clear All', ), @@ -337,269 +135,16 @@ class _JsInterpreterPageState extends State { ), body: Column( children: [ - // URL Input Section - UrlInputSection(viewModel: _viewModel!), + UrlInputSection(viewModel: _viewModel), - // Console Output Expanded( child: ConsoleOutputSection( - viewModel: _viewModel!, + viewModel: _viewModel, scrollController: _scrollController, ), ), - // Command Input Section - CommandInputSection(viewModel: _viewModel!), - ], - ), - ); - } -} - -// Widget Components -class UrlInputSection extends StatelessWidget { - final JsInterpreterViewModel viewModel; - - const UrlInputSection({super.key, required this.viewModel}); - - @override - Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: const Color(0xFF161B22), - border: Border(bottom: BorderSide(color: Colors.grey[800]!, width: 1)), - ), - child: Row( - children: [ - const Icon(Icons.link, color: Colors.green, size: 20), - const SizedBox(width: 8), - Expanded( - child: TextField( - controller: viewModel.urlController, - style: const TextStyle(color: Colors.white, fontSize: 14), - decoration: InputDecoration( - hintText: 'Enter JS source URL (optional)', - hintStyle: TextStyle(color: Colors.grey[600]), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide(color: Colors.grey[700]!), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: const BorderSide(color: Colors.green), - ), - contentPadding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 8, - ), - ), - onSubmitted: (_) => viewModel.loadFromUrl(), - ), - ), - const SizedBox(width: 8), - IconButton( - onPressed: viewModel.loadFromUrl, - icon: const Icon(Icons.download, color: Colors.green), - tooltip: 'Load from URL', - ), - ], - ), - ); - } -} - -class ConsoleOutputSection extends StatelessWidget { - final JsInterpreterViewModel viewModel; - final ScrollController scrollController; - - const ConsoleOutputSection({ - super.key, - required this.viewModel, - required this.scrollController, - }); - - @override - Widget build(BuildContext context) { - return Container( - color: const Color(0xFF0D1117), - child: ListView.builder( - controller: scrollController, - padding: const EdgeInsets.all(16), - itemCount: viewModel.consoleHistory.length, - itemBuilder: (context, index) { - final entry = viewModel.consoleHistory[index]; - return ConsoleEntryWidget(entry: entry); - }, - ), - ); - } -} - -class ConsoleEntryWidget extends StatelessWidget { - final ConsoleEntry entry; - - const ConsoleEntryWidget({super.key, required this.entry}); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 2), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildPrefix(), - const SizedBox(width: 8), - Expanded( - child: ConstrainedBox( - constraints: BoxConstraints(maxHeight: 1.2 * 14 * 20), - child: Text( - entry.content, - style: _getTextStyle(), - softWrap: true, - ), - ), - ), - ], - ), - ); - } - - Widget _buildPrefix() { - switch (entry.type) { - case ConsoleEntryType.input: - return const Text( - '> ', - style: TextStyle(color: Colors.green, fontWeight: FontWeight.bold), - ); - case ConsoleEntryType.output: - return const Text( - '← ', - style: TextStyle(color: Colors.blue, fontWeight: FontWeight.bold), - ); - case ConsoleEntryType.error: - return const Text( - '✗ ', - style: TextStyle(color: Colors.red, fontWeight: FontWeight.bold), - ); - case ConsoleEntryType.warn: - return const Text( - '! ', - style: TextStyle(color: Colors.orange, fontWeight: FontWeight.bold), - ); - case ConsoleEntryType.info: - return const Text( - 'ℹ ', - style: TextStyle(color: Colors.yellow, fontWeight: FontWeight.bold), - ); - } - } - - TextStyle _getTextStyle() { - final baseStyle = GoogleFonts.sourceCodePro(fontSize: 14); - - switch (entry.type) { - case ConsoleEntryType.input: - return baseStyle.copyWith(color: Colors.green); - case ConsoleEntryType.output: - return baseStyle.copyWith(color: Colors.white); - case ConsoleEntryType.error: - return baseStyle.copyWith(color: Colors.red); - case ConsoleEntryType.warn: - return baseStyle.copyWith(color: Colors.orange); - case ConsoleEntryType.info: - return baseStyle.copyWith(color: Colors.yellow); - } - } -} - -class CommandInputSection extends StatelessWidget { - final JsInterpreterViewModel viewModel; - - const CommandInputSection({super.key, required this.viewModel}); - - @override - Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: const Color(0xFF161B22), - border: Border(top: BorderSide(color: Colors.grey[800]!, width: 1)), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only(top: 12.0), - child: const Icon( - Icons.chevron_right, - color: Colors.green, - size: 16, - ), - ), - Expanded( - child: TextField( - controller: viewModel.inputController, - style: const TextStyle( - color: Colors.white, - fontSize: 16, - fontFamily: 'monospace', - ), - decoration: const InputDecoration( - border: InputBorder.none, - hintText: 'Enter JavaScript code...', - hintStyle: TextStyle(color: Colors.grey), - ), - onSubmitted: (_) => viewModel.executeCode(), - maxLines: 5, - textCapitalization: TextCapitalization.none, - keyboardType: TextInputType.text, - textInputAction: TextInputAction.done, - ), - ), - const SizedBox(width: 8), - if (viewModel.isLoading) - const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - color: Colors.green, - ), - ) - else - Column( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - onPressed: viewModel.executeCode, - icon: const Icon(Icons.play_arrow, color: Colors.green), - tooltip: 'Execute (Enter)', - ), - ], - ), - ], - ), - Row( - children: [ - AnimatedBuilder( - animation: viewModel._inputController, - builder: (context, child) { - if (viewModel._inputController.text.isEmpty) { - return const SizedBox(height: kMinInteractiveDimension); - } - return OutlinedButton.icon( - onPressed: viewModel.clearInput, - icon: const Icon(Icons.close, color: Colors.green), - label: const Text('Clear'), - ); - }, - ), - ], - ), + CommandInputSection(viewModel: _viewModel), ], ), ); diff --git a/packages/javascript/javascript/example/lib/runtime.dart b/packages/javascript/javascript/example/lib/runtime.dart deleted file mode 100644 index 73e7a31..0000000 --- a/packages/javascript/javascript/example/lib/runtime.dart +++ /dev/null @@ -1,70 +0,0 @@ -import 'dart:convert'; - -import 'package:flutter/foundation.dart'; -import 'package:javascript/javascript.dart'; - -abstract interface class JsRuntimeDelegate { - factory JsRuntimeDelegate() { - return JsRuntimeDefault(); - } - - Future initialize(); - - Future evaluate(String source); - dynamic onMessage(String channelName, dynamic Function(dynamic args) fn); - - void dispose(); -} - -final class JsRuntimeDefault implements JsRuntimeDelegate { - JsRuntimeDefault(); - - late final JavaScript _jsRuntime; - - @override - Future initialize() async { - try { - _jsRuntime = await JavaScript.createNew(); - if (kDebugMode) { - print('kDebugMode: $kDebugMode, ${_jsRuntime.runtimeType}'); - } - _jsRuntime.setIsInspectable(kDebugMode); - _jsRuntime.runJavaScriptReturningResult(""" - console.info = function() { - sendMessage('ConsoleLog', JSON.stringify(['info', ...arguments])); - } - """); - } catch (e, stackTrace) { - if (kDebugMode) { - print('Error initializing JS runtime: $e'); - print('Stack trace: $stackTrace'); - } - rethrow; - } - } - - @override - Future evaluate(String source) async { - final value = await _jsRuntime.runJavaScriptReturningResult(source); - print('evaluate: $value'); - return value; - } - - @override - dynamic onMessage(String channelName, dynamic Function(dynamic args) fn) { - return _jsRuntime.addJavaScriptChannel( - JavaScriptChannelParams( - name: channelName, - onMessageReceived: (message) { - final data = fn(message.message); - return JavaScriptReply(message: json.encode(data)); - }, - ), - ); - } - - @override - void dispose() { - _jsRuntime.dispose(); - } -} diff --git a/packages/javascript/javascript/example/lib/runtime_service.dart b/packages/javascript/javascript/example/lib/runtime_service.dart deleted file mode 100644 index a94cb37..0000000 --- a/packages/javascript/javascript/example/lib/runtime_service.dart +++ /dev/null @@ -1,320 +0,0 @@ -import 'dart:convert'; -import 'dart:math'; - -import 'package:flutter/foundation.dart'; -import 'package:http/http.dart' as http; -import 'package:javascript_example/json.dart'; -import 'package:javascript_example/runtime.dart'; -import 'package:web_socket/web_socket.dart'; - -// Service Layer -class JsRuntimeService { - late JsRuntimeDelegate _jsRuntime; - late http.Client _httpClient; - - Future initialize() async { - _jsRuntime = JsRuntimeDelegate(); - _httpClient = http.Client(); - return _jsRuntime.initialize(); - } - - Future evaluate(String source) async { - return await _jsRuntime.evaluate(source); - } - - Future loadJsFromUrl(String url) async { - if (url.isEmpty) { - throw Exception('URL is empty'); - } - final response = await _httpClient.get(Uri.parse(url)); - if (response.statusCode >= 200 && response.statusCode < 300) { - return response.body; - } else { - throw Exception('Failed to load JS from URL: ${response.statusCode}'); - } - } - - void dispose() { - _httpClient.close(); - _jsRuntime.dispose(); - } - - void setConsole({ - required void Function(String message) log, - required void Function(String message) info, - required void Function(String message) warn, - required void Function(String message) error, - }) { - try { - debugPrint('Setting console'); - _jsRuntime.evaluate(""" - console.log = function() { - sendMessage('AppConsoleLog', JSON.stringify(['log', ...arguments])); - } - console.info = function() { - sendMessage('AppConsoleLog', JSON.stringify(['info', ...arguments])); - } - console.warn = function() { - sendMessage('AppConsoleLog', JSON.stringify(['warn', ...arguments])); - } - console.error = function() { - sendMessage('AppConsoleLog', JSON.stringify(['error', ...arguments])); - } - """); - _jsRuntime.onMessage('AppConsoleLog', (dynamic args) { - // if (kDebugMode) { - // print('ConsoleLog: ${args.runtimeType}:$args'); - // } - final data = args is List - ? args - : (args is String - ? json.decode(args) as List - : throw FormatException('Invalid console log format')); - final logType = data.removeAt(0); - String output = data.join(' '); - debugPrint('jsconsole [$logType]: $output'); - switch (logType) { - case 'info': - info(output); - break; - case 'warn': - warn(output); - break; - case 'error': - error(output); - break; - case 'log': - default: - log(output); - break; - } - }); - } catch (e, s) { - if (kDebugMode) { - print('Error setting console: $e'); - print('Stack trace: $s'); - } - rethrow; - } - } - - final Map _sockets = {}; - - void _addSocket(String id, WebSocket ws) { - _sockets[id] = ws; - } - - bool _isSetDebug = false; - - void setRPCMessageHandler( - String module, - void Function(dynamic args) handler, - ) { - if (kDebugMode) { - print('setRPCMessageHandler: $module'); - } - _jsRuntime.onMessage(module, (args) async { - try { - final cmd = args is Map - ? args - : (args is String - ? json.decode(args) as Map - : throw FormatException('Invalid RPC message format')); - final response = await (args) async { - try { - debugPrint('jsconsole-rpc [$module]: $args'); - handler(args); - switch (cmd['type']) { - case 'connectWs': - if (!_isSetDebug) { - _isSetDebug = true; - sendRPCMessage( - requestId: generateRequestId(), - type: 'setLogLevel', - request: {'logLevel': 'debug', 'sendLogsToApp': true}, - ); - await Future.delayed(const Duration(seconds: 1)); - } - - final request = cmd['request'] as Map?; - if (request == null) { - throw Exception('Invalid request: $cmd'); - } - final id = request['id'] as String?; - final url = request['url'] as String?; - if (id == null || url == null) { - throw Exception('Invalid request: $cmd'); - } - final ws = await WebSocket.connect(Uri.parse(url)); - _addSocket(id, ws); - ws.events.listen((ev) { - switch (ev) { - case TextDataReceived(): - final data = ev.text; - debugPrint('jsconsole-rpc [$module]: TEXT RECEIVED: $ev'); - sendRPCMessage( - requestId: generateRequestId(), - type: 'sendWsMessage', - request: {'id': id, 'data': data}, - ); - break; - case BinaryDataReceived(): - final data = ev.data; - debugPrint( - 'jsconsole-rpc [$module]: BINARY RECEIVED: $ev', - ); - sendRPCMessage( - requestId: generateRequestId(), - type: 'sendWsMessage', - request: {'id': id, 'data': bytesToBase64Json(data)}, - ); - break; - case CloseReceived(): - debugPrint( - 'jsconsole-rpc [$module]: CLOSE RECEIVED: $ev', - ); - sendRPCMessage( - requestId: generateRequestId(), - type: 'disconnectWs', - request: {'id': id}, - ); - _sockets.remove(id); - break; - } - }); - return {}; - case 'setLogLevelDone': - case 'createClaimDone': - case 'log': - case 'console': - debugPrint('jsconsole-rpc [$module]: console: $cmd'); - return null; - case 'error': - debugPrint('jsconsole-rpc [$module]: err: $cmd'); - return null; - case 'disconnectWs': - final request = cmd['request'] as Map; - final id = request['id'] as String; - final ws = _sockets[id]; - if (ws == null) { - throw Exception('WebSocket not found'); - } - ws.close(); - _sockets.remove(id); - return {}; - case 'sendWsMessage': - final request = cmd['request'] as Map; - final id = request['id'] as String; - final data = request['data']; - final ws = _sockets[id]; - debugPrint( - 'jsconsole-rpc [$module]: sendWsMessage SENDING TO WS: $id => (${data.runtimeType}) $data', - ); - if (ws == null) { - throw Exception('WebSocket not found'); - } - if (data is String) { - ws.sendText(data); - } else if (data is Map && data['type'] == 'uint8array') { - final bytes = base64.decode(data['value'] as String); - ws.sendBytes(bytes); - } else { - throw Exception('Invalid data type: ${data.runtimeType}'); - } - return {}; - case 'createClaimStep': - debugPrint('jsconsole-rpc [$module]: createClaimStep'); - return null; - default: - debugPrint('jsconsole-rpc [$module]: UNKNOWN COMMAND: $cmd'); - throw UnsupportedError('Unknown RPC command: ${cmd['type']}'); - } - } catch (e, s) { - debugPrint('jsconsole-rpc [$module]: $e\n$s'); - debugPrintStack(stackTrace: s); - rethrow; - } - }(args); - if (response == null) { - return; - } - sendRPCResponse( - requestId: cmd['id'] ?? generateRequestId(), - type: cmd['type'] ?? 'unknown', - response: response, - ); - } catch (e, s) { - debugPrint('jsconsole-rpc ERROR [$module]: $e\n$s'); - debugPrintStack(stackTrace: s); - sendRPCErrorResponse( - requestId: generateRequestId(), - message: e.toString(), - stack: s.toString(), - ); - } - }); - if (kDebugMode) { - print('setRPCMessageHandler: $module: done'); - } - } - - void sendRPCMessage({ - required String requestId, - required String type, - required Map request, - }) { - _jsRuntime.evaluate(''' - handleIncomingMessage(${json.encode(json.encode({ - // this is a random ID you generate, - // use to match the response to the request - 'id': requestId, - // the type of request you want to make - 'type': type, - 'request': request, - }))}); -'''); - } - - void sendRPCResponse({ - required String requestId, - required String type, - required Map response, - }) { - _jsRuntime.evaluate(''' - handleIncomingMessage(${json.encode({ - // this is a random ID you generate, - // use to match the response to the request - 'id': requestId, - // the type of request you want to make - 'type': '${type}Done', - 'isResponse': true, - 'response': response, - })}); -'''); - } - - void sendRPCErrorResponse({ - required String requestId, - required String message, - required String stack, - }) { - _jsRuntime.evaluate(''' - handleIncomingMessage(${json.encode({ - // this is a random ID you generate, - // use to match the response to the request - 'id': requestId, - // the type of request you want to make - 'type': 'error', - 'isResponse': true, - 'data': {'message': message, 'stack': stack}, - })}); -'''); - } - - static String generateRequestId() { - final random = Random.secure(); - final randomNumber = random.nextDouble(); - final hexString = randomNumber.toStringAsFixed(16).substring(2); - return hexString; - } -} diff --git a/packages/javascript/javascript/example/lib/src/file.dart b/packages/javascript/javascript/example/lib/src/file.dart new file mode 100644 index 0000000..9011f9f --- /dev/null +++ b/packages/javascript/javascript/example/lib/src/file.dart @@ -0,0 +1,88 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:crypto/crypto.dart'; +import 'package:logging/logging.dart'; +import 'package:http/http.dart' as http; + +Future getFileForUrl(String url, [String extension = '']) async { + final dir = Directory.systemTemp; + await dir.create(recursive: true); + final fileHash = sha1.convert(utf8.encode(url)).toString(); + final file = File('${dir.path}/$fileHash$extension'); + if (file.existsSync()) { + await file.delete(); + } + await file.create(recursive: true); + return file; +} + +Future readResponseToFile( + http.StreamedResponse response, { + String extension = '', +}) async { + final logger = Logger('readResponseToFile'); + + final request = response.request; + if (request == null) { + throw Exception('request is null'); + } + if (response.statusCode < 200 || response.statusCode >= 300) { + throw Exception('response status code is not 2xx'); + } + final file = await getFileForUrl(request.url.toString(), extension); + + final sink = file.openWrite(mode: FileMode.writeOnlyAppend); + + bool isFileClosed = false; + + try { + final doneCompleter = Completer(); + + response.stream.listen( + sink.add, + onError: (e, s) { + isFileClosed = false; + doneCompleter.completeError(e, s); + sink.close(); + }, + onDone: () async { + try { + await sink.flush(); + await sink.close(); + isFileClosed = true; + doneCompleter.complete(); + } catch (e, s) { + logger.severe('Error flushing file', e, s); + doneCompleter.completeError(e, s); + } + }, + cancelOnError: true, + ); + + await doneCompleter.future; + } catch (e, s) { + logger.severe('Error downloading file', e, s); + rethrow; + } finally { + try { + if (!isFileClosed) { + await sink.close(); + } + } catch (e, s) { + logger.severe('Error closing file', e, s); + } + } + + return file; +} + +final _client = http.Client(); + +Future downloadFromUrl(String url, {String extension = ''}) async { + final response = await _client.send(http.Request('GET', Uri.parse(url))); + final file = await readResponseToFile(response, extension: extension); + + return file; +} diff --git a/packages/javascript/javascript/example/lib/src/model.dart b/packages/javascript/javascript/example/lib/src/model.dart new file mode 100644 index 0000000..9dbb77d --- /dev/null +++ b/packages/javascript/javascript/example/lib/src/model.dart @@ -0,0 +1,208 @@ +import 'dart:convert'; + +import 'package:flutter/widgets.dart'; +import 'package:javascript/javascript.dart'; +import 'package:logging/logging.dart'; + +import 'runtime_service.dart'; + +final Logger _log = Logger('JsInterpreterViewModel'); + +class JsInterpreterViewModel extends ChangeNotifier { + final JsRuntimeService _service; + + Object? _result; + Object? _error; + bool _isLoading = false; + final List _consoleHistory = []; + final TextEditingController _inputController = TextEditingController(); + final TextEditingController _urlController = TextEditingController(); + + JsInterpreterViewModel(this._service); + + Future initialize() async { + await _service.useJsRuntime((js) async { + await js.runJavaScriptReturningResult(""" + console.log = function() { + sendMessage('AppConsoleLog', JSON.stringify(['log', ...arguments])); + } + console.info = function() { + sendMessage('AppConsoleLog', JSON.stringify(['info', ...arguments])); + } + console.warn = function() { + sendMessage('AppConsoleLog', JSON.stringify(['warn', ...arguments])); + } + console.error = function() { + sendMessage('AppConsoleLog', JSON.stringify(['error', ...arguments])); + } + """); + await js.addJavaScriptChannel( + JavaScriptChannelParams( + name: 'AppConsoleLog', + onMessageReceived: (JavaScriptMessage message) async { + final data = json.decode(message.message ?? '[]') as List; + final logType = data.removeAt(0); + final output = data.join(' '); + _log.info('jsconsole [$logType]: $output'); + + final ConsoleEntry entry = switch (logType) { + 'info' => ConsoleEntry.info(output), + 'warn' => ConsoleEntry.warn(output), + 'error' => ConsoleEntry.error(output), + _ => ConsoleEntry.output(output), + }; + + _addToHistory(entry); + + return JavaScriptReply(message: ''); + }, + ), + ); + + await js.addJavaScriptChannel( + JavaScriptChannelParams( + name: 'Sum', + onMessageReceived: (message) { + final data = json.decode(message.message!); + return JavaScriptReply(message: json.encode(data['a'] + data['b'])); + }, + ), + ); + + /// Add your own code to evaluate javascript or add javascript channels here + await js.addJavaScriptChannel( + JavaScriptChannelParams( + name: 'ExampleChannel', + onMessageReceived: (message) { + _log.info( + 'Received message from ExampleChannel: ${message.message}', + ); + return JavaScriptReply( + message: 'replying from host: ${message.message}', + ); + }, + ), + ); + }); + } + + Object? get result => _result; + Object? get error => _error; + bool get isLoading => _isLoading; + List get consoleHistory => _consoleHistory; + TextEditingController get inputController => _inputController; + TextEditingController get urlController => _urlController; + + Future executeCode() async { + final code = _inputController.text.trim(); + if (code.isEmpty) { + _log.info('no code entered'); + return; + } + + _addToHistory(ConsoleEntry.input(code)); + _setLoading(true); + _clearError(); + + try { + _result = await _service.evaluate(code); + _addToHistory(ConsoleEntry.output(_result.toString())); + clearInput(); + } catch (e) { + _error = e; + _addToHistory( + ConsoleEntry.error( + e is JavaScriptExecutionException ? e.message : e.toString(), + ), + ); + } finally { + _setLoading(false); + } + } + + Future loadFromUrl() async { + final url = _urlController.text.trim(); + if (url.isEmpty) return; + + _addToHistory(ConsoleEntry.info('Loading from URL: $url')); + _setLoading(true); + _clearError(); + + try { + _result = await _service.loadJsFromUrlAndEvaluate(url); + _addToHistory(ConsoleEntry.output(_result.toString())); + _addToHistory(ConsoleEntry.info('Loaded code & evaluated from: $url')); + } catch (e) { + _error = e; + _addToHistory( + ConsoleEntry.error( + 'Failed to load from URL: ${e is JavaScriptExecutionException ? e.message : e.toString()}', + ), + ); + } finally { + _setLoading(false); + } + } + + void clearAll() { + _inputController.clear(); + _urlController.clear(); + _clearResult(); + _clearError(); + _consoleHistory.clear(); + notifyListeners(); + } + + void clearInput() { + _inputController.clear(); + notifyListeners(); + } + + void _setLoading(bool loading) { + _isLoading = loading; + notifyListeners(); + } + + void _clearError() { + _error = null; + notifyListeners(); + } + + void _clearResult() { + _result = null; + notifyListeners(); + } + + void _addToHistory(ConsoleEntry entry) { + _consoleHistory.add(entry); + notifyListeners(); + } + + @override + void dispose() { + _inputController.dispose(); + _urlController.dispose(); + super.dispose(); + } +} + +class ConsoleEntry { + final String content; + final ConsoleEntryType type; + final DateTime timestamp; + + ConsoleEntry(this.content, this.type) : timestamp = DateTime.now(); + + factory ConsoleEntry.input(String content) => + ConsoleEntry(content, ConsoleEntryType.input); + factory ConsoleEntry.output(String content) => + ConsoleEntry(content, ConsoleEntryType.output); + factory ConsoleEntry.error(String content) => + ConsoleEntry(content, ConsoleEntryType.error); + factory ConsoleEntry.info(String content) => + ConsoleEntry(content, ConsoleEntryType.info); + factory ConsoleEntry.warn(String content) => + ConsoleEntry(content, ConsoleEntryType.warn); +} + +enum ConsoleEntryType { input, output, error, info, warn } diff --git a/packages/javascript/javascript/example/lib/src/runtime_service.dart b/packages/javascript/javascript/example/lib/src/runtime_service.dart new file mode 100644 index 0000000..1f7773b --- /dev/null +++ b/packages/javascript/javascript/example/lib/src/runtime_service.dart @@ -0,0 +1,40 @@ +import 'package:http/http.dart' as http; +import 'package:javascript/javascript.dart'; +import 'package:logging/logging.dart'; + +import 'file.dart'; + +class JsRuntimeService { + final _log = Logger('JsRuntimeService'); + + late JavaScript _jsRuntime; + late http.Client _httpClient; + + Future initialize() async { + _jsRuntime = await JavaScript.createNew(); + _httpClient = http.Client(); + } + + Future evaluate(String source) { + return _jsRuntime.runJavaScriptReturningResult(source); + } + + Future loadJsFromUrlAndEvaluate(String url) async { + final file = await downloadFromUrl(url); + + return _jsRuntime.runJavaScriptFromFileReturningResult(file.absolute.path); + } + + void dispose() { + _httpClient.close(); + _jsRuntime.dispose(); + } + + Future useJsRuntime(Future Function(JavaScript) onEngine) async { + try { + return await onEngine(_jsRuntime); + } catch (e, s) { + _log.severe('Error when running a callback with [useJsRuntime]', e, s); + } + } +} diff --git a/packages/javascript/javascript/example/lib/src/ui/command_input.dart b/packages/javascript/javascript/example/lib/src/ui/command_input.dart new file mode 100644 index 0000000..1700ef8 --- /dev/null +++ b/packages/javascript/javascript/example/lib/src/ui/command_input.dart @@ -0,0 +1,96 @@ +import 'package:flutter/material.dart'; + +import '../model.dart'; + +class CommandInputSection extends StatelessWidget { + final JsInterpreterViewModel viewModel; + + const CommandInputSection({super.key, required this.viewModel}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFF161B22), + border: Border(top: BorderSide(color: Colors.grey[800]!, width: 1)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(top: 12.0), + child: const Icon( + Icons.chevron_right, + color: Colors.green, + size: 16, + ), + ), + Expanded( + child: TextField( + controller: viewModel.inputController, + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontFamily: 'monospace', + ), + decoration: const InputDecoration( + border: InputBorder.none, + hintText: 'Enter JavaScript code...', + hintStyle: TextStyle(color: Colors.grey), + ), + onSubmitted: (_) => viewModel.executeCode(), + maxLines: 5, + textCapitalization: TextCapitalization.none, + keyboardType: TextInputType.text, + textInputAction: TextInputAction.done, + ), + ), + const SizedBox(width: 8), + if (viewModel.isLoading) + const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.green, + ), + ) + else + Column( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + onPressed: viewModel.executeCode, + icon: const Icon(Icons.play_arrow, color: Colors.green), + tooltip: 'Execute (Enter)', + ), + ], + ), + ], + ), + Row( + children: [ + AnimatedBuilder( + animation: viewModel.inputController, + builder: (context, child) { + if (viewModel.inputController.text.isEmpty) { + return const SizedBox(height: kMinInteractiveDimension); + } + return OutlinedButton.icon( + onPressed: viewModel.clearInput, + icon: const Icon(Icons.close, color: Colors.green), + label: const Text('Clear'), + ); + }, + ), + ], + ), + ], + ), + ); + } +} diff --git a/packages/javascript/javascript/example/lib/src/ui/console_output.dart b/packages/javascript/javascript/example/lib/src/ui/console_output.dart new file mode 100644 index 0000000..b6edee6 --- /dev/null +++ b/packages/javascript/javascript/example/lib/src/ui/console_output.dart @@ -0,0 +1,107 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:javascript_example/src/model.dart'; + +class ConsoleOutputSection extends StatelessWidget { + final JsInterpreterViewModel viewModel; + final ScrollController scrollController; + + const ConsoleOutputSection({ + super.key, + required this.viewModel, + required this.scrollController, + }); + + @override + Widget build(BuildContext context) { + return Container( + color: const Color(0xFF0D1117), + child: ListView.builder( + controller: scrollController, + padding: const EdgeInsets.all(16), + itemCount: viewModel.consoleHistory.length, + itemBuilder: (context, index) { + final entry = viewModel.consoleHistory[index]; + return ConsoleEntryWidget(entry: entry); + }, + ), + ); + } +} + +class ConsoleEntryWidget extends StatelessWidget { + final ConsoleEntry entry; + + const ConsoleEntryWidget({super.key, required this.entry}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 2), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildPrefix(), + const SizedBox(width: 8), + Expanded( + child: ConstrainedBox( + constraints: BoxConstraints(maxHeight: 1.2 * 14 * 20), + child: Text( + entry.content, + style: _getTextStyle(), + softWrap: true, + ), + ), + ), + ], + ), + ); + } + + Widget _buildPrefix() { + switch (entry.type) { + case ConsoleEntryType.input: + return const Text( + '> ', + style: TextStyle(color: Colors.green, fontWeight: FontWeight.bold), + ); + case ConsoleEntryType.output: + return const Text( + '← ', + style: TextStyle(color: Colors.blue, fontWeight: FontWeight.bold), + ); + case ConsoleEntryType.error: + return const Text( + '✗ ', + style: TextStyle(color: Colors.red, fontWeight: FontWeight.bold), + ); + case ConsoleEntryType.warn: + return const Text( + '! ', + style: TextStyle(color: Colors.orange, fontWeight: FontWeight.bold), + ); + case ConsoleEntryType.info: + return const Text( + 'ℹ ', + style: TextStyle(color: Colors.yellow, fontWeight: FontWeight.bold), + ); + } + } + + TextStyle _getTextStyle() { + final baseStyle = GoogleFonts.sourceCodePro(fontSize: 14); + + switch (entry.type) { + case ConsoleEntryType.input: + return baseStyle.copyWith(color: Colors.green); + case ConsoleEntryType.output: + return baseStyle.copyWith(color: Colors.white); + case ConsoleEntryType.error: + return baseStyle.copyWith(color: Colors.red); + case ConsoleEntryType.warn: + return baseStyle.copyWith(color: Colors.orange); + case ConsoleEntryType.info: + return baseStyle.copyWith(color: Colors.yellow); + } + } +} diff --git a/packages/javascript/javascript/example/lib/src/ui/url_input.dart b/packages/javascript/javascript/example/lib/src/ui/url_input.dart new file mode 100644 index 0000000..216d8a6 --- /dev/null +++ b/packages/javascript/javascript/example/lib/src/ui/url_input.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:javascript_example/src/model.dart'; + +class UrlInputSection extends StatelessWidget { + final JsInterpreterViewModel viewModel; + + const UrlInputSection({super.key, required this.viewModel}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFF161B22), + border: Border(bottom: BorderSide(color: Colors.grey[800]!, width: 1)), + ), + child: Row( + children: [ + const Icon(Icons.link, color: Colors.green, size: 20), + const SizedBox(width: 8), + Expanded( + child: TextField( + controller: viewModel.urlController, + style: const TextStyle(color: Colors.white, fontSize: 14), + decoration: InputDecoration( + hintText: 'Enter JS source URL (optional)', + hintStyle: TextStyle(color: Colors.grey[600]), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide(color: Colors.grey[700]!), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: const BorderSide(color: Colors.green), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + ), + onSubmitted: (_) => viewModel.loadFromUrl(), + ), + ), + const SizedBox(width: 8), + IconButton( + onPressed: viewModel.loadFromUrl, + icon: const Icon(Icons.download, color: Colors.green), + tooltip: 'Load from URL', + ), + ], + ), + ); + } +} diff --git a/packages/javascript/javascript/example/macos/Runner.xcodeproj/project.pbxproj b/packages/javascript/javascript/example/macos/Runner.xcodeproj/project.pbxproj index 6e1d34d..a34b90f 100644 --- a/packages/javascript/javascript/example/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/javascript/javascript/example/macos/Runner.xcodeproj/project.pbxproj @@ -27,6 +27,8 @@ 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + BFBAC3B16D53ABFF5C6010E4 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A6547DC87827AD27ACE252A1 /* Pods_Runner.framework */; }; + DF9FBC1EF83BCFBD15188341 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 755B3F1CAC14D191EC1E0EA5 /* Pods_RunnerTests.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -60,11 +62,12 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 14FD868C341BBC91A8708987 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* _example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "_example.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10ED2044A3C60003C045 /* _example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = _example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; @@ -76,8 +79,15 @@ 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 6180346A1182816A5005AED2 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 666B6E5BAEFF77984EEF17DA /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 755B3F1CAC14D191EC1E0EA5 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + A6547DC87827AD27ACE252A1 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D8FC23FD36FE2E7C26B23A57 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + EFFE11C9CB78D133520718F3 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + F25B4AE472230037926A3740 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -85,6 +95,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + DF9FBC1EF83BCFBD15188341 /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -92,6 +103,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + BFBAC3B16D53ABFF5C6010E4 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -125,6 +137,7 @@ 331C80D6294CF71000263BE5 /* RunnerTests */, 33CC10EE2044A3C60003C045 /* Products */, D73912EC22F37F3D000D13A0 /* Frameworks */, + B3E19D78F2422C6236282864 /* Pods */, ); sourceTree = ""; }; @@ -172,9 +185,25 @@ path = Runner; sourceTree = ""; }; + B3E19D78F2422C6236282864 /* Pods */ = { + isa = PBXGroup; + children = ( + 14FD868C341BBC91A8708987 /* Pods-Runner.debug.xcconfig */, + 6180346A1182816A5005AED2 /* Pods-Runner.release.xcconfig */, + EFFE11C9CB78D133520718F3 /* Pods-Runner.profile.xcconfig */, + D8FC23FD36FE2E7C26B23A57 /* Pods-RunnerTests.debug.xcconfig */, + 666B6E5BAEFF77984EEF17DA /* Pods-RunnerTests.release.xcconfig */, + F25B4AE472230037926A3740 /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; D73912EC22F37F3D000D13A0 /* Frameworks */ = { isa = PBXGroup; children = ( + A6547DC87827AD27ACE252A1 /* Pods_Runner.framework */, + 755B3F1CAC14D191EC1E0EA5 /* Pods_RunnerTests.framework */, ); name = Frameworks; sourceTree = ""; @@ -186,6 +215,7 @@ isa = PBXNativeTarget; buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( + FB52D41D073454F57E72BEBD /* [CP] Check Pods Manifest.lock */, 331C80D1294CF70F00263BE5 /* Sources */, 331C80D2294CF70F00263BE5 /* Frameworks */, 331C80D3294CF70F00263BE5 /* Resources */, @@ -204,11 +234,13 @@ isa = PBXNativeTarget; buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + E749E1299F7CFA72A3083211 /* [CP] Check Pods Manifest.lock */, 33CC10E92044A3C60003C045 /* Sources */, 33CC10EA2044A3C60003C045 /* Frameworks */, 33CC10EB2044A3C60003C045 /* Resources */, 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, + 5365244B2192E9C98378034D /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -329,6 +361,67 @@ shellPath = /bin/sh; shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; }; + 5365244B2192E9C98378034D /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + E749E1299F7CFA72A3083211 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + FB52D41D073454F57E72BEBD /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -380,6 +473,7 @@ /* Begin XCBuildConfiguration section */ 331C80DB294CF71000263BE5 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = D8FC23FD36FE2E7C26B23A57 /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; @@ -394,6 +488,7 @@ }; 331C80DC294CF71000263BE5 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 666B6E5BAEFF77984EEF17DA /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; @@ -408,6 +503,7 @@ }; 331C80DD294CF71000263BE5 /* Profile */ = { isa = XCBuildConfiguration; + baseConfigurationReference = F25B4AE472230037926A3740 /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; diff --git a/packages/javascript/javascript/example/macos/Runner.xcworkspace/contents.xcworkspacedata b/packages/javascript/javascript/example/macos/Runner.xcworkspace/contents.xcworkspacedata index 1d526a1..21a3cc1 100644 --- a/packages/javascript/javascript/example/macos/Runner.xcworkspace/contents.xcworkspacedata +++ b/packages/javascript/javascript/example/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -4,4 +4,7 @@ + + diff --git a/packages/javascript/javascript/example/pubspec.yaml b/packages/javascript/javascript/example/pubspec.yaml index d1594f8..a4adda2 100644 --- a/packages/javascript/javascript/example/pubspec.yaml +++ b/packages/javascript/javascript/example/pubspec.yaml @@ -1,36 +1,19 @@ name: javascript_example -description: "A new Flutter project." -# The following line prevents the package from being accidentally published to -# pub.dev using `flutter pub publish`. This is preferred for private packages. +description: A example application for the javascript package. publish_to: 'none' # Remove this line if you wish to publish to pub.dev -# The following defines the version and build number for your application. -# A version number is three numbers separated by dots, like 1.2.43 -# followed by an optional build number separated by a +. -# Both the version and the builder number may be overridden in flutter -# build by specifying --build-name and --build-number, respectively. -# In Android, build-name is used as versionName while build-number used as versionCode. -# Read more about Android versioning at https://developer.android.com/studio/publish/versioning -# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. -# Read more about iOS versioning at -# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -# In Windows, build-name is used as the major, minor, and patch parts -# of the product and file versions while build-number is used as the build suffix. version: 1.0.0+1 environment: sdk: ^3.8.1 -# Dependencies specify other packages that your package needs in order to work. -# To automatically upgrade your package dependencies to the latest versions -# consider running `flutter pub upgrade --major-versions`. Alternatively, -# dependencies can be manually updated by changing the version numbers below to -# the latest version available on pub.dev. To see which dependencies have newer -# versions available, run `flutter pub outdated`. dependencies: + crypto: ^3.0.6 flutter: sdk: flutter google_fonts: ^6.3.0 + http: ^1.5.0 + logging: ^1.3.0 javascript: # When depending on this package from a real application you should use: # javascript: ^x.y.z @@ -38,7 +21,6 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../ - web_socket: ^1.0.1 dev_dependencies: flutter_driver: @@ -49,44 +31,5 @@ dev_dependencies: sdk: flutter flutter_lints: ^6.0.0 -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter packages. flutter: - - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. uses-material-design: true - - # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/to/resolution-aware-images - - # For details regarding adding assets from package dependencies, see - # https://flutter.dev/to/asset-from-package - - # To add custom fonts to your application, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.dev/to/font-from-package diff --git a/packages/javascript/javascript/example/test/widget_test.dart b/packages/javascript/javascript/example/test/widget_test.dart index e69de29..8b13789 100644 --- a/packages/javascript/javascript/example/test/widget_test.dart +++ b/packages/javascript/javascript/example/test/widget_test.dart @@ -0,0 +1 @@ + diff --git a/packages/javascript/javascript/lib/impl.dart b/packages/javascript/javascript/lib/impl.dart index 1ceae57..91b8c57 100644 --- a/packages/javascript/javascript/lib/impl.dart +++ b/packages/javascript/javascript/lib/impl.dart @@ -32,4 +32,9 @@ class JavaScriptImpl implements JavaScript { Future runJavaScriptReturningResult(String javaScript) { return _platform.runJavaScriptReturningResult(_engineId, javaScript); } + + @override + Future runJavaScriptFromFileReturningResult(String javaScriptFilePath) { + return _platform.runJavaScriptFromFileReturningResult(_engineId, javaScriptFilePath); + } } diff --git a/packages/javascript/javascript/lib/javascript.dart b/packages/javascript/javascript/lib/javascript.dart index 32b37e4..d44970c 100644 --- a/packages/javascript/javascript/lib/javascript.dart +++ b/packages/javascript/javascript/lib/javascript.dart @@ -4,41 +4,95 @@ export 'package:javascript_platform_interface/javascript_platform_interface.dart part 'impl.dart'; +/// Represents a connection to an environment where JavaScript code can be evaluated. +/// +/// Each [JavaScript] instance has its own isolated state, and cannot interact with other [JavaScript] instances. +/// +/// There is no guarantee of security boundary between [JavaScript] instances in a single application. +/// If the code in one [JavaScript] instance is able to compromise the security, then it may be able +/// to observe or manipulate other [JavaScript] instances abstract interface class JavaScript { - /// Creates a new JavaScript engine. + /// Creates & returns a new [JavaScript] instance by the [JavaScriptPlatform]. /// - /// The [platform] is used to start a engine. If not provided, the default - /// platform is used. + /// The [platform] manages the control of the underlying javascript environment. + /// If not provided, the default platform is used. static Future createNew({JavaScriptPlatform? platform}) async { final p = platform ?? JavaScriptPlatform.instance; + + // A unique id for identifying the javascript environment. final id = Object().hashCode.toString(); + // Start a new javascript environment using the platform. await p.startJavaScriptEngine(id); return JavaScriptImpl._(p, id); } - /// Disposes of the JavaScript runtime. + /// Disposes of the underlying javascript environment. Future dispose(); - /// Sets whether the JavaScript engine is inspectable. + /// Sets whether the underlying javascript environment is inspectable. + /// + /// This is only applicable to some runtime engines. Future setIsInspectable(bool isInspectable); - /// Adds a new JavaScript channel to the set of enabled channels. + /// Adds a new JavaScript channel to the set of enabled channels for the context of the current [JavaScript] instance. + /// + /// The JavaScript code can then call `sendMessage('channelName', JSON.stringify("data"))` to send a message that + /// will be passed to [JavaScriptChannelParams.onMessageReceived] and a reply will be sent back to the JavaScript code. + /// + /// For example, after adding the following JavaScript channel: + /// ```dart + /// engine.addJavaScriptChannel( + /// JavaScriptChannelParams( + /// name: 'Sum', + /// onMessageReceived: (message) { + /// final data = json.decode(message.message!); + /// return JavaScriptReply( + /// message: json.encode(data['a'] + data['b']), + /// ); + /// }, + /// ), + /// ); + /// ``` + /// + /// The JavaScript code can then call: + /// ```dart + /// sendMessage('Sum', JSON.stringify({a: 1, b: 2})) + /// ``` + /// + /// to asynchronously send a message that will be passed to [JavaScriptChannelParams.onMessageReceived] and a reply + /// will be sent back to the JavaScript code. + /// + /// As per the example, the reply back to javascript will be a promise that resolves to `"3"`. Future addJavaScriptChannel( JavaScriptChannelParams javaScriptChannelParams, ); /// Removes the JavaScript channel with the matching name from the set of - /// enabled channels. + /// enabled channels for the context of the current [JavaScript] instance. /// /// This disables the channel with the matching name if it was previously /// enabled through the [addJavaScriptChannel]. Future removeJavaScriptChannel(String javaScriptChannelName); - /// Runs the given JavaScript in the context of the current page, and returns the result. + /// Evaluates the given JavaScript code [javaScript] in the context of the current [JavaScript] instance, and returns the result. + /// + /// {@template javascript_run_javascript_returning_result_behavior} + /// There are multiple possible behaviors based on the output of the expression: + /// 1. If the JS expression evaluates to a JS String or a Promise of a JS String, then the [Future] resolves to a [Map], [List], [String], [num], [bool], or [Null] if the string can be decoded as JSON, otherwise the [Future] resolves to the string. + /// 2. If the JS expression evaluates to another data type, then the [Future] resolves to an **empty** [String] on some platforms. + /// 3. The [Future] completes with an error if a JavaScript error occurred. /// - /// The Future completes with an error if a JavaScript error occurred, or if the - /// type the given expression evaluates to is unsupported. + /// The global variables set by one evaluation are visible for later evaluations. This is similar to adding multiple `