├── LICENSE ├── README.md ├── .gitignore ├── js_wrapping_generator ├── CHANGELOG.md ├── README.md ├── lib │ ├── builder.dart │ └── src │ │ ├── transfomation.dart │ │ └── generator.dart ├── build.yaml ├── pubspec.yaml ├── example │ └── gmaps │ │ └── map-simple │ │ ├── map.html │ │ ├── page.dart │ │ └── page.js.g.dart ├── LICENSE ├── analysis_options.yaml └── test │ └── builder_test.dart └── js_wrapping ├── AUTHORS ├── pubspec.yaml ├── lib └── js_wrapping.dart ├── LICENSE ├── CHANGELOG.md ├── analysis_options.yaml └── README.md /LICENSE: -------------------------------------------------------------------------------- 1 | js_wrapping/LICENSE -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | js_wrapping/README.md -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .packages 2 | .dart_tool 3 | pubspec.lock 4 | .buildlog 5 | .pub/ 6 | .idea/ -------------------------------------------------------------------------------- /js_wrapping_generator/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | See CHANGELOG of the [js_wrapping](https://pub.dartlang.org/packages/js_wrapping) 2 | package for details. 3 | -------------------------------------------------------------------------------- /js_wrapping_generator/README.md: -------------------------------------------------------------------------------- 1 | # Dart Js Wrapping generator 2 | 3 | A generator of Js wrappers. See [js_wrapping](https://pub.dartlang.org/packages/js_wrapping) 4 | package for details. 5 | -------------------------------------------------------------------------------- /js_wrapping/AUTHORS: -------------------------------------------------------------------------------- 1 | # Below is a list of people and organizations that have contributed 2 | # to the Dart project. Names should be added to the list like so: 3 | # 4 | # Name/Organization 5 | 6 | Alexandre Ardhuin 7 | -------------------------------------------------------------------------------- /js_wrapping/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: js_wrapping 2 | version: 0.7.4 3 | description: That package contains elements to easilly wrap JS library in Dart. 4 | homepage: https://github.com/a14n/dart-js-wrapping 5 | environment: 6 | sdk: '>=2.12.0 <3.0.0' 7 | dependencies: 8 | js: ^0.6.3 9 | -------------------------------------------------------------------------------- /js_wrapping_generator/lib/builder.dart: -------------------------------------------------------------------------------- 1 | import 'package:build/build.dart'; 2 | import 'package:source_gen/source_gen.dart'; 3 | 4 | import 'src/generator.dart'; 5 | 6 | Builder jsWrappingBuilder(BuilderOptions options) => 7 | LibraryBuilder(JsWrappingGenerator(), 8 | generatedExtension: '.js.g.dart'); 9 | -------------------------------------------------------------------------------- /js_wrapping_generator/build.yaml: -------------------------------------------------------------------------------- 1 | targets: 2 | $default: 3 | builders: 4 | js_wrapping_generator: 5 | generate_for: 6 | include: 7 | - example/** 8 | 9 | builders: 10 | js_wrapping_generator: 11 | import: "package:js_wrapping_generator/builder.dart" 12 | builder_factories: ["jsWrappingBuilder"] 13 | build_extensions: {".dart": [".js.g.dart"]} 14 | auto_apply: dependents 15 | build_to: source 16 | -------------------------------------------------------------------------------- /js_wrapping_generator/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: js_wrapping_generator 2 | version: 0.7.4 3 | description: > 4 | With that package you will be able to easilly generate wrapper for JS library 5 | in Dart. 6 | homepage: https://github.com/a14n/dart-js-wrapping 7 | environment: 8 | sdk: '>=2.12.0 <3.0.0' 9 | dependencies: 10 | analyzer: ^1.0.0 11 | build: ^2.1.0 12 | collection: ^1.15.0 13 | js_wrapping: 0.7.4 14 | source_gen: ^1.0.0 15 | dev_dependencies: 16 | build_runner: 17 | build_test: 18 | build_web_compilers: ^3.0.0 19 | test: 20 | # dependency_overrides: 21 | # js_wrapping: 22 | # path: ../js_wrapping 23 | -------------------------------------------------------------------------------- /js_wrapping_generator/example/gmaps/map-simple/map.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Google Maps JavaScript API v3 Example: Map Simple 5 | 6 | 7 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /js_wrapping/lib/js_wrapping.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015, Alexandre Ardhuin. All rights reserved. Use of this 2 | // source code is governed by a BSD-style license that can be found in the 3 | // LICENSE file. 4 | 5 | /// The js library allows Dart library authors to define Dart interfaces for 6 | /// JavaScript objects. 7 | library js_wrapping; 8 | 9 | export 'package:js/js.dart'; 10 | export 'package:js/js_util.dart'; 11 | 12 | /// A metadata annotation that allows to customize the name used for method call 13 | /// or attribute access on the javascript side. 14 | /// 15 | /// You can use it on libraries, classes, members. 16 | class JsName { 17 | const JsName([this.name]); 18 | final String? name; 19 | } 20 | 21 | /// A placeholder for generation of factory contructors and static members. 22 | T $js() => const Object() as T; 23 | -------------------------------------------------------------------------------- /js_wrapping/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Alexandre Ardhuin. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /js_wrapping_generator/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Alexandre Ardhuin. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /js_wrapping/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v0.7.4 (2022-01-13) 2 | 3 | - handle list cast in closures. 4 | 5 | # v0.7.3 (2021-08-30) 6 | 7 | - handle enum to avoid cast issue in closures. 8 | 9 | # v0.7.2 (2021-08-12) 10 | 11 | - Bind functions for Function returned by getters. 12 | 13 | # v0.7.1 (2021-08-10) 14 | 15 | - Bind functions for Function fields 16 | 17 | # v0.7.0 (2021-03-09) 18 | 19 | - migrate to null-safety 20 | 21 | # v0.6.0 (2018-07-27) 22 | 23 | - migration from `dart:js` to `package:js`. 24 | 25 | # v0.5.0 (2018-07-27) 26 | 27 | - migration to Dart 2. 28 | 29 | # v0.4.9 (2018-07-06) 30 | 31 | - Bump dependencies. 32 | 33 | # v0.4.8 (2018-06-26) 34 | 35 | - Fix issue in `JsList` and `JsMap` without codec. 36 | 37 | # v0.4.7 (2018-04-13) 38 | 39 | - Revert changes introduce in 0.4.6. 40 | 41 | # v0.4.6 (2018-04-13) 42 | 43 | - Fix `IdentityConverter` for dart2. 44 | 45 | # v0.4.5 (2018-04-07) 46 | 47 | - Remove unused codec in generated code. 48 | 49 | # v0.4.4 (2018-04-06) 50 | 51 | - Migrate to new build_runner system. 52 | 53 | # v0.4.3 (2018-03-14) 54 | 55 | - Fix runtime cast failure in `JsObjectAsMap.keys` for dart2. 56 | 57 | # v0.4.2 (2017-06-30) 58 | 59 | - Fix issue with js function unwrapping. 60 | 61 | # v0.4.1 (2017-08-28) 62 | 63 | - upgrade dependencies 64 | 65 | # v0.4.0 (2017-06-20) 66 | 67 | - Fix issue with [dart-lang/sdk#28371](https://github.com/dart-lang/sdk/issues/28371). 68 | 69 | **Breaking change**: use `factory _MyClass() => null;` instead of 70 | `external factory _MyClass()` for your construtor templates. 71 | - remove comment generics syntax 72 | 73 | # v0.2.0+1 (2015-06-01) 74 | 75 | Fix issue with callback returning `void`. 76 | 77 | # v0.2.0 (2015-05-28) 78 | 79 | Total rewrite on top of the [source_gen](https://pub.dartlang.org/packages/source_gen) 80 | package. 81 | 82 | # Semantic Version Conventions 83 | 84 | http://semver.org/ 85 | 86 | - *Stable*: All even numbered minor versions are considered API stable: 87 | i.e.: v1.0.x, v1.2.x, and so on. 88 | - *Development*: All odd numbered minor versions are considered API unstable: 89 | i.e.: v0.9.x, v1.1.x, and so on. 90 | -------------------------------------------------------------------------------- /js_wrapping_generator/example/gmaps/map-simple/page.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015, Alexandre Ardhuin. All rights reserved. Use of this 2 | // source code is governed by a BSD-style license that can be found in the 3 | // LICENSE file. 4 | 5 | @JS('google.maps') 6 | library google_maps.sample.simple; 7 | 8 | import 'dart:html'; 9 | 10 | import 'package:js_wrapping/js_wrapping.dart'; 11 | 12 | @JsName('Map') 13 | abstract class GMap { 14 | factory GMap(Node? mapDiv, [MapOptions? opts]) => $js(); 15 | 16 | @JsName('getZoom') 17 | num? _getZoom(); 18 | num? get zoom => _getZoom(); 19 | } 20 | 21 | @JsName() 22 | abstract class LatLng { 23 | factory LatLng(num? lat, num? lng, [bool? noWrap]) => $js(); 24 | 25 | bool? equals(LatLng other); 26 | num? get lat => _lat(); 27 | @JsName('lat') 28 | num? _lat(); 29 | num? get lng => _lng(); 30 | @JsName('lng') 31 | num? _lng(); 32 | String toString(); 33 | String? toUrlValue([num precision]); 34 | } 35 | 36 | @JsName() 37 | @anonymous 38 | abstract class MapOptions { 39 | factory MapOptions() => $js(); 40 | 41 | int? zoom; 42 | LatLng? center; 43 | MapTypeId? mapTypeId; 44 | } 45 | 46 | @JsName('MapTypeId') 47 | enum MapTypeId { 48 | HYBRID, 49 | ROADMAP, 50 | SATELLITE, 51 | TERRAIN, 52 | } 53 | 54 | @JS('event') 55 | external Object get _Event$namespace; 56 | 57 | class Event { 58 | static MapsEventListener addDomListener( 59 | Object instance, String eventName, Function handler, 60 | [bool? capture]) => 61 | callMethod(_Event$namespace, 'addDomListener', 62 | [instance, eventName, allowInterop(handler), capture]); 63 | static MapsEventListener addDomListenerOnce( 64 | Object instance, String eventName, Function handler, 65 | [bool? capture]) => 66 | callMethod(_Event$namespace, 'addDomListenerOnce', 67 | [instance, eventName, allowInterop(handler), capture]); 68 | static MapsEventListener addListener( 69 | Object instance, String eventName, Function handler) => 70 | callMethod(_Event$namespace, 'addListener', 71 | [instance, eventName, allowInterop(handler)]); 72 | static MapsEventListener addListenerOnce( 73 | Object instance, String eventName, Function handler) => 74 | callMethod(_Event$namespace, 'addListenerOnce', 75 | [instance, eventName, allowInterop(handler)]); 76 | static void clearInstanceListeners(Object instance) => 77 | callMethod(_Event$namespace, 'clearInstanceListeners', [instance]); 78 | static void clearListeners(Object instance, String eventName) => 79 | callMethod(_Event$namespace, 'clearListeners', [instance, eventName]); 80 | static void removeListener(MapsEventListener listener) => 81 | callMethod(_Event$namespace, 'removeListener', [listener]); 82 | 83 | static void trigger( 84 | Object instance, String eventName, List? eventArgs) => 85 | callMethod( 86 | _Event$namespace, 'trigger', [instance, eventName, ...?eventArgs]); 87 | } 88 | 89 | @JsName() 90 | @anonymous 91 | abstract class MapsEventListener { 92 | factory MapsEventListener() => $js(); 93 | void remove(); 94 | } 95 | 96 | void main() { 97 | final mapOptions = MapOptions() 98 | ..zoom = 8 99 | ..center = LatLng(-34.397, 150.644) 100 | ..mapTypeId = MapTypeId.ROADMAP; 101 | final map = GMap(querySelector('#map_canvas'), mapOptions); 102 | Event.addListener(map, 'zoom_changed', () => print(map.zoom)); 103 | } 104 | -------------------------------------------------------------------------------- /js_wrapping_generator/lib/src/transfomation.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015, Alexandre Ardhuin. All rights reserved. Use of this 2 | // source code is governed by a BSD-style license that can be found in the 3 | // LICENSE file. 4 | 5 | library js_wrapping_generator.util; 6 | 7 | import 'package:analyzer/dart/analysis/results.dart'; 8 | import 'package:analyzer/dart/ast/ast.dart'; 9 | import 'package:analyzer/dart/ast/token.dart'; 10 | import 'package:analyzer/dart/element/element.dart'; 11 | 12 | String getSourceCode(Element element) { 13 | final node = getNode(element); 14 | return element.source!.contents.data.substring(node.offset, node.end); 15 | } 16 | 17 | AstNode getNode(Element element) => 18 | (element.session!.getParsedLibraryByElement2(element.library!) 19 | as ParsedLibraryResult) 20 | .getElementDeclaration(element)! 21 | .node; 22 | 23 | class SourceTransformation { 24 | SourceTransformation(this.begin, this.end, this.content); 25 | SourceTransformation.removal(this.begin, this.end) : content = ''; 26 | SourceTransformation.insertion(int index, this.content) 27 | : begin = index, 28 | end = index; 29 | 30 | int begin; 31 | int end; 32 | final String content; 33 | 34 | void shift(int value) { 35 | begin += value; 36 | end += value; 37 | } 38 | } 39 | 40 | class Transformer { 41 | final _transformations = []; 42 | 43 | void insertAt(int index, String content) => 44 | _transformations.add(SourceTransformation.insertion(index, content)); 45 | 46 | void removeBetween(int begin, int end) => 47 | _transformations.add(SourceTransformation.removal(begin, end)); 48 | 49 | void removeNode(AstNode node) => 50 | _transformations.add(SourceTransformation.removal(node.offset, node.end)); 51 | 52 | void removeToken(Token token) => _transformations 53 | .add(SourceTransformation.removal(token.offset, token.end)); 54 | 55 | void replace(int begin, int end, String content) => 56 | _transformations.add(SourceTransformation(begin, end, content)); 57 | 58 | String applyOnElement(Element element) { 59 | var code = getSourceCode(element); 60 | final initialPadding = -getNode(element).offset; 61 | for (final transformation in _transformations) { 62 | transformation.shift(initialPadding); 63 | } 64 | for (var i = 0; i < _transformations.length; i++) { 65 | final t = _transformations[i]; 66 | code = code.substring(0, t.begin) + t.content + code.substring(t.end); 67 | for (final transformation in _transformations.skip(i + 1)) { 68 | if (transformation.end <= t.begin) continue; 69 | if (t.end <= transformation.begin) { 70 | transformation.shift(t.content.length - (t.end - t.begin)); 71 | continue; 72 | } 73 | throw StateError('Colision in transformations'); 74 | } 75 | } 76 | return code; 77 | } 78 | 79 | String applyOn(String code, {int padding = 0}) { 80 | var result = code; 81 | for (final transformation in _transformations) { 82 | transformation.shift(padding); 83 | } 84 | for (var i = 0; i < _transformations.length; i++) { 85 | final t = _transformations[i]; 86 | result = 87 | result.substring(0, t.begin) + t.content + result.substring(t.end); 88 | for (final transformation in _transformations.skip(i + 1)) { 89 | if (transformation.end <= t.begin) continue; 90 | if (t.end <= transformation.begin) { 91 | transformation.shift(t.content.length - (t.end - t.begin)); 92 | continue; 93 | } 94 | throw StateError('Colision in transformations'); 95 | } 96 | } 97 | return result; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /js_wrapping_generator/example/gmaps/map-simple/page.js.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | // ************************************************************************** 4 | // JsWrappingGenerator 5 | // ************************************************************************** 6 | 7 | // Copyright (c) 2015, Alexandre Ardhuin. All rights reserved. Use of this 8 | // source code is governed by a BSD-style license that can be found in the 9 | // LICENSE file. 10 | 11 | @JS('google.maps') 12 | library google_maps.sample.simple; 13 | 14 | import 'dart:html'; 15 | 16 | import 'package:js_wrapping/js_wrapping.dart'; 17 | 18 | @JS('Map') 19 | class GMap { 20 | external GMap(Node? mapDiv, [MapOptions? opts]); 21 | } 22 | 23 | extension GMap$Ext on GMap { 24 | num? get zoom => _getZoom(); 25 | 26 | num? _getZoom() => callMethod(this, 'getZoom', []); 27 | } 28 | 29 | @JS() 30 | class LatLng { 31 | external LatLng(num? lat, num? lng, [bool? noWrap]); 32 | 33 | external bool? equals(LatLng other); 34 | 35 | external String toString(); 36 | 37 | external String? toUrlValue([num precision]); 38 | } 39 | 40 | extension LatLng$Ext on LatLng { 41 | num? get lat => _lat(); 42 | num? get lng => _lng(); 43 | 44 | num? _lat() => callMethod(this, 'lat', []); 45 | 46 | num? _lng() => callMethod(this, 'lng', []); 47 | } 48 | 49 | @JS() 50 | @anonymous 51 | class MapOptions { 52 | external factory MapOptions(); 53 | 54 | external int? get zoom; 55 | 56 | external set zoom(int? value); 57 | 58 | external LatLng? get center; 59 | 60 | external set center(LatLng? value); 61 | 62 | external MapTypeId? get mapTypeId; 63 | 64 | external set mapTypeId(MapTypeId? value); 65 | } 66 | 67 | @JS('MapTypeId') 68 | class MapTypeId { 69 | external static MapTypeId get HYBRID; 70 | external static MapTypeId get ROADMAP; 71 | external static MapTypeId get SATELLITE; 72 | external static MapTypeId get TERRAIN; 73 | } 74 | 75 | @JS('event') 76 | external Object get _Event$namespace; 77 | 78 | class Event { 79 | static MapsEventListener addDomListener( 80 | Object instance, String eventName, Function handler, 81 | [bool? capture]) => 82 | callMethod(_Event$namespace, 'addDomListener', 83 | [instance, eventName, allowInterop(handler), capture]); 84 | static MapsEventListener addDomListenerOnce( 85 | Object instance, String eventName, Function handler, 86 | [bool? capture]) => 87 | callMethod(_Event$namespace, 'addDomListenerOnce', 88 | [instance, eventName, allowInterop(handler), capture]); 89 | static MapsEventListener addListener( 90 | Object instance, String eventName, Function handler) => 91 | callMethod(_Event$namespace, 'addListener', 92 | [instance, eventName, allowInterop(handler)]); 93 | static MapsEventListener addListenerOnce( 94 | Object instance, String eventName, Function handler) => 95 | callMethod(_Event$namespace, 'addListenerOnce', 96 | [instance, eventName, allowInterop(handler)]); 97 | static void clearInstanceListeners(Object instance) => 98 | callMethod(_Event$namespace, 'clearInstanceListeners', [instance]); 99 | static void clearListeners(Object instance, String eventName) => 100 | callMethod(_Event$namespace, 'clearListeners', [instance, eventName]); 101 | static void removeListener(MapsEventListener listener) => 102 | callMethod(_Event$namespace, 'removeListener', [listener]); 103 | 104 | static void trigger( 105 | Object instance, String eventName, List? eventArgs) => 106 | callMethod( 107 | _Event$namespace, 'trigger', [instance, eventName, ...?eventArgs]); 108 | } 109 | 110 | @JS() 111 | @anonymous 112 | class MapsEventListener { 113 | external factory MapsEventListener(); 114 | 115 | external void remove(); 116 | } 117 | 118 | void main() { 119 | final mapOptions = MapOptions() 120 | ..zoom = 8 121 | ..center = LatLng(-34.397, 150.644) 122 | ..mapTypeId = MapTypeId.ROADMAP; 123 | final map = GMap(querySelector('#map_canvas'), mapOptions); 124 | Event.addListener(map, 'zoom_changed', () => print(map.zoom)); 125 | } 126 | -------------------------------------------------------------------------------- /js_wrapping_generator/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | analyzer: 2 | exclude: 3 | linter: 4 | rules: 5 | - always_declare_return_types 6 | # - always_put_control_body_on_new_line # conflict with dartfmt 7 | - always_put_required_named_parameters_first 8 | - always_require_non_null_named_parameters 9 | # - always_specify_types # we don't want that 10 | - annotate_overrides 11 | # - avoid_annotating_with_dynamic # conflict with always_declare_return_types 12 | # - avoid_as # conflict with implicit-casts: false 13 | - avoid_bool_literals_in_conditional_expressions 14 | - avoid_catches_without_on_clauses 15 | - avoid_catching_errors 16 | - avoid_classes_with_only_static_members 17 | - avoid_double_and_int_checks 18 | - avoid_empty_else 19 | - avoid_field_initializers_in_const_classes 20 | - avoid_function_literals_in_foreach_calls 21 | - avoid_init_to_null 22 | - avoid_js_rounded_ints 23 | - avoid_null_checks_in_equality_operators 24 | - avoid_positional_boolean_parameters 25 | - avoid_private_typedef_functions 26 | - avoid_relative_lib_imports 27 | - avoid_renaming_method_parameters 28 | - avoid_return_types_on_setters 29 | - avoid_returning_null 30 | - avoid_returning_this 31 | - avoid_setters_without_getters 32 | - avoid_single_cascade_in_expression_statements 33 | - avoid_slow_async_io 34 | - avoid_types_as_parameter_names 35 | - avoid_types_on_closure_parameters 36 | - avoid_unused_constructor_parameters 37 | - await_only_futures 38 | - camel_case_types 39 | - cancel_subscriptions 40 | - cascade_invocations 41 | - close_sinks 42 | - comment_references 43 | - constant_identifier_names 44 | - control_flow_in_finally 45 | - directives_ordering 46 | - empty_catches 47 | - empty_constructor_bodies 48 | - empty_statements 49 | - file_names 50 | - hash_and_equals 51 | # - implementation_imports # we use some analyzer internal class 52 | - invariant_booleans 53 | - iterable_contains_unrelated_type 54 | - join_return_with_assignment 55 | - library_names 56 | - library_prefixes 57 | - list_remove_unrelated_type 58 | - literal_only_boolean_expressions 59 | - no_adjacent_strings_in_list 60 | - no_duplicate_case_values 61 | - non_constant_identifier_names 62 | - omit_local_variable_types 63 | - one_member_abstracts 64 | - only_throw_errors 65 | - overridden_fields 66 | # - package_api_docs # not yet enabled 67 | - package_names 68 | - package_prefixed_library_names 69 | - parameter_assignments 70 | - prefer_adjacent_string_concatenation 71 | - prefer_asserts_in_initializer_lists 72 | - prefer_collection_literals 73 | - prefer_conditional_assignment 74 | - prefer_const_constructors 75 | - prefer_const_constructors_in_immutables 76 | - prefer_const_declarations 77 | - prefer_const_literals_to_create_immutables 78 | - prefer_constructors_over_static_methods 79 | - prefer_contains 80 | - prefer_equal_for_default_values 81 | - prefer_expression_function_bodies 82 | - prefer_final_fields 83 | - prefer_final_locals 84 | - prefer_foreach 85 | - prefer_function_declarations_over_variables 86 | - prefer_generic_function_type_aliases 87 | - prefer_initializing_formals 88 | - prefer_interpolation_to_compose_strings 89 | - prefer_is_empty 90 | - prefer_is_not_empty 91 | - prefer_iterable_whereType 92 | - prefer_single_quotes 93 | - prefer_typing_uninitialized_variables 94 | # - public_member_api_docs # not yet enabled 95 | - recursive_getters 96 | - slash_for_doc_comments 97 | - sort_constructors_first 98 | - sort_unnamed_constructors_first 99 | - test_types_in_equals 100 | - throw_in_finally 101 | # - type_annotate_public_apis # not yet enabled 102 | - type_init_formals 103 | - unawaited_futures 104 | - unnecessary_brace_in_string_interps 105 | - unnecessary_const 106 | - unnecessary_getters_setters 107 | - unnecessary_lambdas 108 | - unnecessary_new 109 | - unnecessary_null_aware_assignments 110 | - unnecessary_null_in_if_null_operators 111 | - unnecessary_overrides 112 | - unnecessary_parenthesis 113 | - unnecessary_statements 114 | - unnecessary_this 115 | - unrelated_type_equality_checks 116 | - use_rethrow_when_possible 117 | - use_setters_to_change_properties 118 | - use_string_buffers 119 | - use_to_and_as_if_applicable 120 | - valid_regexps 121 | - void_checks 122 | -------------------------------------------------------------------------------- /js_wrapping/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | analyzer: 2 | 3 | linter: 4 | rules: 5 | - always_declare_return_types 6 | # - always_put_control_body_on_new_line # conflict with dartfmt 7 | - always_put_required_named_parameters_first 8 | - always_require_non_null_named_parameters 9 | # - always_specify_types # we don't want that 10 | - annotate_overrides 11 | # - avoid_annotating_with_dynamic # conflict with always_declare_return_types 12 | # - avoid_as # conflict with implicit-casts: false 13 | - avoid_bool_literals_in_conditional_expressions 14 | - avoid_catches_without_on_clauses 15 | - avoid_catching_errors 16 | - avoid_classes_with_only_static_members 17 | - avoid_double_and_int_checks 18 | - avoid_empty_else 19 | - avoid_field_initializers_in_const_classes 20 | - avoid_function_literals_in_foreach_calls 21 | - avoid_init_to_null 22 | - avoid_js_rounded_ints 23 | - avoid_null_checks_in_equality_operators 24 | - avoid_positional_boolean_parameters 25 | - avoid_private_typedef_functions 26 | - avoid_relative_lib_imports 27 | - avoid_renaming_method_parameters 28 | - avoid_return_types_on_setters 29 | - avoid_returning_null 30 | - avoid_returning_this 31 | - avoid_setters_without_getters 32 | - avoid_single_cascade_in_expression_statements 33 | - avoid_slow_async_io 34 | - avoid_types_as_parameter_names 35 | - avoid_types_on_closure_parameters 36 | - avoid_unused_constructor_parameters 37 | - await_only_futures 38 | - camel_case_types 39 | - cancel_subscriptions 40 | - cascade_invocations 41 | - close_sinks 42 | - comment_references 43 | - constant_identifier_names 44 | - control_flow_in_finally 45 | - directives_ordering 46 | - empty_catches 47 | - empty_constructor_bodies 48 | - empty_statements 49 | - file_names 50 | - hash_and_equals 51 | - implementation_imports 52 | - invariant_booleans 53 | - iterable_contains_unrelated_type 54 | - join_return_with_assignment 55 | - library_names 56 | - library_prefixes 57 | - list_remove_unrelated_type 58 | - literal_only_boolean_expressions 59 | - no_adjacent_strings_in_list 60 | - no_duplicate_case_values 61 | - non_constant_identifier_names 62 | - omit_local_variable_types 63 | - one_member_abstracts 64 | - only_throw_errors 65 | - overridden_fields 66 | # - package_api_docs # not yet enabled 67 | - package_names 68 | - package_prefixed_library_names 69 | - parameter_assignments 70 | - prefer_adjacent_string_concatenation 71 | - prefer_asserts_in_initializer_lists 72 | - prefer_bool_in_asserts 73 | - prefer_collection_literals 74 | - prefer_conditional_assignment 75 | - prefer_const_constructors 76 | - prefer_const_constructors_in_immutables 77 | - prefer_const_declarations 78 | - prefer_const_literals_to_create_immutables 79 | - prefer_constructors_over_static_methods 80 | - prefer_contains 81 | - prefer_equal_for_default_values 82 | - prefer_expression_function_bodies 83 | - prefer_final_fields 84 | - prefer_final_locals 85 | - prefer_foreach 86 | - prefer_function_declarations_over_variables 87 | - prefer_generic_function_type_aliases 88 | - prefer_initializing_formals 89 | - prefer_interpolation_to_compose_strings 90 | - prefer_is_empty 91 | - prefer_is_not_empty 92 | - prefer_iterable_whereType 93 | - prefer_single_quotes 94 | - prefer_typing_uninitialized_variables 95 | # - public_member_api_docs # not yet enabled 96 | - recursive_getters 97 | - slash_for_doc_comments 98 | - sort_constructors_first 99 | - sort_unnamed_constructors_first 100 | - super_goes_last 101 | - test_types_in_equals 102 | - throw_in_finally 103 | # - type_annotate_public_apis # not yet enabled 104 | - type_init_formals 105 | - unawaited_futures 106 | - unnecessary_brace_in_string_interps 107 | - unnecessary_const 108 | - unnecessary_getters_setters 109 | - unnecessary_lambdas 110 | - unnecessary_new 111 | - unnecessary_null_aware_assignments 112 | - unnecessary_null_in_if_null_operators 113 | - unnecessary_overrides 114 | - unnecessary_parenthesis 115 | - unnecessary_statements 116 | - unnecessary_this 117 | - unrelated_type_equality_checks 118 | - use_rethrow_when_possible 119 | - use_setters_to_change_properties 120 | - use_string_buffers 121 | - use_to_and_as_if_applicable 122 | - valid_regexps 123 | - void_checks 124 | -------------------------------------------------------------------------------- /js_wrapping/README.md: -------------------------------------------------------------------------------- 1 | # Dart Js Wrapping 2 | 3 | This package allows developers to define well-typed interfaces for JavaScript 4 | objects. Typed JavaScript Interfaces are classes 5 | that describes a JavaScript object and have a well-defined Dart API, complete 6 | with type annotations, constructors, even optional and named parameters. 7 | 8 | ## Writing Js wrapper 9 | 10 | Here's a quick example to show the package in action. 11 | 12 | Given a JS _class_ like : 13 | 14 | ```js 15 | LatLng = function(lat, lng) { 16 | this.lat = lat; 17 | this.lng = lng; 18 | } 19 | LatLng.prototype.equals = function(other) { 20 | return this.lat === other.lat && this.lng === other.lng; 21 | } 22 | ``` 23 | 24 | You can create a wrapper like : 25 | 26 | ```dart 27 | @JsName() 28 | abstract class LatLng { 29 | factory LatLng(num lat, num lng, [bool noWrap]) => $js; 30 | 31 | bool equals(LatLng other); 32 | num get lat => _lat(); 33 | @JsName('lat') 34 | num _lat(); 35 | num get lng => _lng(); 36 | @JsName('lng') 37 | num _lng(); 38 | String toString(); 39 | String toUrlValue([num precision]); 40 | } 41 | ``` 42 | 43 | Once the generator executed you will be able to use a `LatLng` that wraps a js `LatLng`. 44 | 45 | ## Configuration and Initialization 46 | 47 | ### Adding the dependency 48 | 49 | Add the following to your `pubspec.yaml`: 50 | 51 | ```yaml 52 | dependencies: 53 | js_wrapping: ^0.6.0 54 | dev_dependencies: 55 | js_wrapping_generator: ^0.6.0 56 | ``` 57 | 58 | ### Running the generator 59 | 60 | See the [Running generators section of the source_gen package](https://github.com/dart-lang/source_gen#running-generators). 61 | 62 | ## Usage 63 | 64 | **Warning: The API is still changing rapidly. Not for the faint of heart** 65 | 66 | ### Defining Typed JavaScript Interfaces 67 | 68 | To create a Typed JavaScript Interface you will start by creating a class marked with `@JsName()`. It will be the template used to create a js interface. 69 | 70 | ```dart 71 | @JS('google.maps') 72 | library google_maps.sample.simple; 73 | 74 | import 'package:js_wrapping/js_wrapping.dart'; 75 | 76 | @JsName() 77 | abstract class LatLng { 78 | factory LatLng(num lat, num lng, [bool noWrap]) => $js; 79 | 80 | bool equals(LatLng other); 81 | num get lat => _lat(); 82 | @JsName('lat') 83 | num _lat(); 84 | num get lng => _lng(); 85 | @JsName('lng') 86 | num _lng(); 87 | String toString(); 88 | String toUrlValue([num precision]); 89 | } 90 | ``` 91 | 92 | The generator will provide a new library `mylib.g.dart` containing : 93 | 94 | ```dart 95 | // GENERATED CODE - DO NOT MODIFY BY HAND 96 | 97 | // ************************************************************************** 98 | // JsWrappingGenerator 99 | // ************************************************************************** 100 | 101 | // Copyright (c) 2015, Alexandre Ardhuin. All rights reserved. Use of this 102 | // source code is governed by a BSD-style license that can be found in the 103 | // LICENSE file. 104 | 105 | @JS('google.maps') 106 | library google_maps.sample.simple; 107 | 108 | import 'package:js_wrapping/js_wrapping.dart'; 109 | 110 | @JS() 111 | class LatLng { 112 | external LatLng(num lat, num lng, [bool noWrap]); 113 | 114 | external bool equals(LatLng other); 115 | 116 | external String toString(); 117 | 118 | external String toUrlValue([num precision]); 119 | } 120 | 121 | extension LatLng$Ext on LatLng { 122 | num get lat => _lat(); 123 | num get lng => _lng(); 124 | 125 | num _lat() => callMethod(this, 'lat', []); 126 | 127 | num _lng() => callMethod(this, 'lng', []); 128 | } 129 | ``` 130 | 131 | ### Constructors to create js object 132 | 133 | If `LatLng` is a js object/function you can create a new instance in js with 134 | `LatLng()`. To make it possible to create such js instance from the Dart-side 135 | you have to define a `factory` constructor: 136 | 137 | ```dart 138 | @JsName() 139 | abstract class LatLng { 140 | factory LatLng(num lat, num lng, [bool noWrap]) => $js; 141 | } 142 | ``` 143 | 144 | This will provide: 145 | 146 | ```dart 147 | @JS() 148 | class LatLng { 149 | external LatLng(num lat, num lng, [bool noWrap]); 150 | } 151 | ``` 152 | 153 | It's now possible to instantiate js object from Dart with `LatLng()`. 154 | 155 | ### Properties and accessors 156 | 157 | Properties or abstract getters/setters can be added to the class and 158 | will generate getters and setters to access to the properties of the underlying 159 | js object. 160 | 161 | ```dart 162 | @JsName() 163 | abstract class Person { 164 | factory Person() => $js; 165 | String firstname, lastname; 166 | int get age; 167 | void set email(String email); 168 | } 169 | ``` 170 | 171 | This will provide: 172 | 173 | ```dart 174 | @JS() 175 | class Person { 176 | external Person(); 177 | external String get firstname; 178 | external set firstname(String value); 179 | external String get lastname; 180 | external set lastname(String value); 181 | external int get age; 182 | external void set email(String email); 183 | } 184 | ``` 185 | 186 | ### Methods 187 | 188 | The abstract methods will be implemented the same way : 189 | 190 | ```dart 191 | @JsName() 192 | abstract class Person { 193 | factory Person() => $js; 194 | String sayHelloTo(String other); 195 | void fall(); 196 | } 197 | ``` 198 | 199 | This will provide: 200 | 201 | ```dart 202 | @JS() 203 | class Person { 204 | external Person(); 205 | 206 | external String sayHelloTo(String other); 207 | 208 | external void fall(); 209 | } 210 | ``` 211 | 212 | ### Names used 213 | 214 | #### constructors 215 | 216 | You can override this name by providing a `JsName('MyClassName')` on the class. 217 | 218 | ```dart 219 | @JsName('People') 220 | abstract class Person { 221 | factory Person() => $js; 222 | String sayHelloTo(Person other); 223 | Person get father; 224 | } 225 | ``` 226 | 227 | #### members 228 | 229 | You can override this name by providing a `JsName('myMemberName')` on the member. 230 | 231 | ```dart 232 | @JsName() 233 | abstract class Person { 234 | @JsName('daddy') Person get father; 235 | } 236 | ``` 237 | 238 | ### Tips & Tricks 239 | 240 | #### anonymous objects 241 | 242 | It's common to instantiate anonymous Js object. If your private classe maps an 243 | anonymous object you can add `@anonymous` on it. 244 | 245 | ```dart 246 | @JsName() 247 | @anonymous 248 | abstract class Foo { 249 | factory Foo() => $js; 250 | } 251 | ``` 252 | 253 | This generates: 254 | 255 | ```dart 256 | @JS() 257 | @anonymous 258 | class Foo { 259 | external factory Foo(); 260 | } 261 | ``` 262 | 263 | #### create getter from method 264 | 265 | If a js object as a `getXxx()` function you would like to map on the dart side 266 | with a `get xxx` you can do something like that: 267 | 268 | ```dart 269 | abstract class Person { 270 | String get firstname => _getFirstname(); 271 | String _getFirstname(); 272 | } 273 | @JsName() 274 | abstract class Person { 275 | factory Person() => $js; 276 | 277 | String get firstname => _getFirstname(); 278 | @JsName('getFirstname') 279 | String _getFirstname(); 280 | } 281 | ``` 282 | 283 | This can be applied to any redirection you'd like to do. 284 | -------------------------------------------------------------------------------- /js_wrapping_generator/test/builder_test.dart: -------------------------------------------------------------------------------- 1 | @TestOn('vm') 2 | import 'package:build_test/build_test.dart'; 3 | import 'package:js_wrapping_generator/src/generator.dart'; 4 | import 'package:source_gen/source_gen.dart'; 5 | import 'package:test/test.dart'; 6 | 7 | void main() { 8 | tearDown(() { 9 | // Increment this after each test so the next test has it's own package 10 | _pkgCacheCount++; 11 | }); 12 | 13 | test('simple class', () async { 14 | await testGeneration( 15 | r''' 16 | @JsName() 17 | class A {} 18 | ''', 19 | r''' 20 | @JS() 21 | class A {} 22 | ''', 23 | ); 24 | }); 25 | 26 | test('class rename', () async { 27 | await testGeneration( 28 | r''' 29 | @JsName('B') 30 | class A {} 31 | ''', 32 | r''' 33 | @JS('B') 34 | class A {} 35 | ''', 36 | ); 37 | }); 38 | 39 | test('constructor', () async { 40 | await testGeneration( 41 | r''' 42 | @JsName() 43 | class A { 44 | factory A() => $js; 45 | } 46 | ''', 47 | r''' 48 | @JS() 49 | class A { 50 | external A(); 51 | } 52 | ''', 53 | ); 54 | }); 55 | 56 | test('anonymous constructor', () async { 57 | await testGeneration( 58 | r''' 59 | @JsName() 60 | @anonymous 61 | class A { 62 | factory A() => $js; 63 | } 64 | ''', 65 | r''' 66 | @JS() 67 | @anonymous 68 | class A { 69 | external factory A(); 70 | } 71 | ''', 72 | ); 73 | }); 74 | 75 | test('field to getter/setter', () async { 76 | await testGeneration( 77 | r''' 78 | @JsName() 79 | class A { 80 | int i; 81 | } 82 | ''', 83 | r''' 84 | @JS() 85 | class A { 86 | external int get i; 87 | 88 | external set i(int value); 89 | } 90 | ''', 91 | ); 92 | }); 93 | 94 | test('non-abstract getter', () async { 95 | await testGeneration( 96 | r''' 97 | @JsName() 98 | class A { 99 | int get i => 1; 100 | } 101 | ''', 102 | r''' 103 | @JS() 104 | class A {} 105 | 106 | extension A$Ext on A { 107 | int get i => 1; 108 | } 109 | ''', 110 | ); 111 | }); 112 | 113 | test('non-abstract setter', () async { 114 | await testGeneration( 115 | r''' 116 | @JsName() 117 | class A { 118 | set i(int value) => throw ''; 119 | } 120 | ''', 121 | r''' 122 | @JS() 123 | class A {} 124 | 125 | extension A$Ext on A { 126 | set i(int value) => throw ''; 127 | } 128 | ''', 129 | ); 130 | }); 131 | 132 | test('non-abstract method', () async { 133 | await testGeneration( 134 | r''' 135 | @JsName() 136 | class A { 137 | String m() => ''; 138 | } 139 | ''', 140 | r''' 141 | @JS() 142 | class A {} 143 | 144 | extension A$Ext on A { 145 | String m() => ''; 146 | } 147 | ''', 148 | ); 149 | }); 150 | 151 | test('rename field', () async { 152 | await testGeneration( 153 | r''' 154 | @JsName() 155 | class A { 156 | @JsName('b') 157 | String a; 158 | } 159 | ''', 160 | r''' 161 | @JS() 162 | class A {} 163 | 164 | extension A$Ext on A { 165 | String get a => getProperty(this, 'b'); 166 | 167 | set a(String value) { 168 | setProperty(this, 'b', value); 169 | } 170 | } 171 | ''', 172 | ); 173 | }); 174 | 175 | test('rename getter', () async { 176 | await testGeneration( 177 | r''' 178 | @JsName() 179 | class A { 180 | @JsName('b') 181 | String get a; 182 | } 183 | ''', 184 | r''' 185 | @JS() 186 | class A {} 187 | 188 | extension A$Ext on A { 189 | String get a => getProperty(this, 'b'); 190 | } 191 | ''', 192 | ); 193 | }); 194 | 195 | test('rename setter', () async { 196 | await testGeneration( 197 | r''' 198 | @JsName() 199 | class A { 200 | @JsName('b') 201 | set a(String value); 202 | } 203 | ''', 204 | r''' 205 | @JS() 206 | class A {} 207 | 208 | extension A$Ext on A { 209 | set a(String value) { 210 | setProperty(this, 'b', value); 211 | } 212 | } 213 | ''', 214 | ); 215 | }); 216 | 217 | test('rename method', () async { 218 | await testGeneration( 219 | r''' 220 | @JsName() 221 | class A { 222 | @JsName('f') 223 | String _m(); 224 | } 225 | ''', 226 | r''' 227 | @JS() 228 | class A {} 229 | 230 | extension A$Ext on A { 231 | String _m() => callMethod(this, 'f', []); 232 | } 233 | ''', 234 | ); 235 | }); 236 | 237 | test('return List', () async { 238 | await testGeneration( 239 | r''' 240 | @JsName() 241 | class A { 242 | List m(); 243 | } 244 | ''', 245 | r''' 246 | @JS() 247 | class A {} 248 | 249 | extension A$Ext on A { 250 | List m() => callMethod(this, 'm', [])?.cast(); 251 | } 252 | ''', 253 | ); 254 | }); 255 | 256 | test('accepting functions as parameter', () async { 257 | await testGeneration( 258 | r''' 259 | @JsName() 260 | class A { 261 | void m1(Function f); 262 | void m2(void Function() f); 263 | } 264 | ''', 265 | r''' 266 | @JS() 267 | class A {} 268 | 269 | extension A$Ext on A { 270 | void m1(Function f) { 271 | callMethod(this, 'm1', [allowInterop(f)]); 272 | } 273 | 274 | void m2(void Function() f) { 275 | callMethod(this, 'm2', [allowInterop(f)]); 276 | } 277 | } 278 | ''', 279 | ); 280 | }); 281 | 282 | test('return Future', () async { 283 | await testGeneration( 284 | r''' 285 | @JsName() 286 | class A { 287 | Future m(); 288 | } 289 | ''', 290 | r''' 291 | @JS() 292 | class A {} 293 | 294 | extension A$Ext on A { 295 | Future m() => promiseToFuture(callMethod(this, 'm', [])); 296 | } 297 | ''', 298 | ); 299 | }); 300 | 301 | test('field with Function', () async { 302 | await testGeneration( 303 | r''' 304 | @JsName() 305 | class A { 306 | int Function() a; 307 | } 308 | ''', 309 | r''' 310 | @JS() 311 | class A {} 312 | 313 | extension A$Ext on A { 314 | int Function() get a => callMethod(getProperty(this, 'a'), 'bind', [this]); 315 | 316 | set a(int Function() value) { 317 | setProperty(this, 'a', allowInterop(value)); 318 | } 319 | } 320 | ''', 321 | ); 322 | }); 323 | 324 | test('getter with Function', () async { 325 | await testGeneration( 326 | r''' 327 | @JsName() 328 | class A { 329 | int Function() get a; 330 | } 331 | ''', 332 | r''' 333 | @JS() 334 | class A {} 335 | 336 | extension A$Ext on A { 337 | int Function() get a => callMethod(getProperty(this, 'a'), 'bind', [this]); 338 | } 339 | ''', 340 | ); 341 | }); 342 | 343 | test('setter with Function', () async { 344 | await testGeneration( 345 | r''' 346 | @JsName() 347 | class A { 348 | set a(int Function() v); 349 | } 350 | ''', 351 | r''' 352 | @JS() 353 | class A {} 354 | 355 | extension A$Ext on A { 356 | set a(int Function() v) { 357 | setProperty(this, 'a', allowInterop(v)); 358 | } 359 | } 360 | ''', 361 | ); 362 | }); 363 | 364 | test('simple enum', () async { 365 | await testGeneration( 366 | r''' 367 | @JsName() 368 | enum MyEnum { 369 | a, 370 | b, 371 | } 372 | ''', 373 | r''' 374 | @JS() 375 | class MyEnum { 376 | external static MyEnum get a; 377 | external static MyEnum get b; 378 | } 379 | 380 | MyEnum? MyEnum$cast(value) { 381 | if (value == MyEnum.a) return MyEnum.a; 382 | if (value == MyEnum.b) return MyEnum.b; 383 | return null; 384 | } 385 | ''', 386 | ); 387 | }); 388 | } 389 | 390 | Future testGeneration(String input, String output) async { 391 | final srcs = { 392 | 'js|lib/js.dart': ''' 393 | library js; 394 | export 'dart:js' show allowInterop, allowInteropCaptureThis; 395 | class JS { 396 | final String name; 397 | const JS([this.name]); 398 | } 399 | class _Anonymous { 400 | const _Anonymous(); 401 | } 402 | const _Anonymous anonymous = _Anonymous(); 403 | ''', 404 | 'js_wrapping|lib/js_wrapping.dart': r''' 405 | library js_wrapping; 406 | export 'package:js/js.dart'; 407 | export 'package:js/js_util.dart'; 408 | class JsName { 409 | const JsName([this.name]); 410 | final String name; 411 | } 412 | const $js = Object(); 413 | ''', 414 | '$_pkgName|lib/test_lib.dart': ''' 415 | import 'package:js_wrapping/js_wrapping.dart'; 416 | $input 417 | ''', 418 | }; 419 | final builder = 420 | LibraryBuilder(JsWrappingGenerator(), generatedExtension: '.js.g.dart'); 421 | 422 | await testBuilder( 423 | builder, 424 | srcs, 425 | generateFor: {'$_pkgName|lib/test_lib.dart'}, 426 | outputs: { 427 | '$_pkgName|lib/test_lib.js.g.dart': decodedMatches(''' 428 | // GENERATED CODE - DO NOT MODIFY BY HAND 429 | 430 | // ************************************************************************** 431 | // JsWrappingGenerator 432 | // ************************************************************************** 433 | 434 | import 'package:js_wrapping/js_wrapping.dart'; 435 | 436 | ${output.trim()} 437 | '''), 438 | }, 439 | ); 440 | } 441 | 442 | // Ensure every test gets its own unique package name 443 | String get _pkgName => 'pkg$_pkgCacheCount'; 444 | int _pkgCacheCount = 1; 445 | -------------------------------------------------------------------------------- /js_wrapping_generator/lib/src/generator.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015, Alexandre Ardhuin. All rights reserved. Use of this 2 | // source code is governed by a BSD-style license that can be found in the 3 | // LICENSE file. 4 | 5 | import 'package:analyzer/dart/ast/ast.dart'; 6 | import 'package:analyzer/dart/constant/value.dart'; 7 | import 'package:analyzer/dart/element/element.dart'; 8 | import 'package:analyzer/dart/element/type.dart'; 9 | import 'package:analyzer/dart/element/type_system.dart'; 10 | import 'package:analyzer/src/generated/source.dart'; 11 | import 'package:build/build.dart'; 12 | import 'package:collection/collection.dart'; 13 | import 'package:js_wrapping_generator/src/transfomation.dart'; 14 | import 'package:source_gen/source_gen.dart'; 15 | 16 | class JsWrappingGenerator extends Generator { 17 | @override 18 | Future generate(LibraryReader library, BuildStep buildStep) async { 19 | final transformer = Transformer(); 20 | final definingCompilationUnitContent = library.element.source.contents.data; 21 | 22 | // remove parts 23 | // BUG : getNode fail when the definingCompilationUnit don't have elements 24 | // final libraryNode = getNode(library.element.definingCompilationUnit).root 25 | // as CompilationUnit; 26 | // libraryNode.directives 27 | // .whereType() 28 | // .forEach(transformer.removeNode); 29 | RegExp(r'^part .*;$', multiLine: true) 30 | .allMatches(definingCompilationUnitContent) 31 | .forEach((e) { 32 | transformer.removeBetween(e.start, e.end); 33 | }); 34 | 35 | final partsContent = []; 36 | for (var element 37 | in library.allElements.where((element) => !element.isSynthetic)) { 38 | if (element.thisOrAncestorOfType() == 39 | library.element.definingCompilationUnit) { 40 | if (element is ClassElement && hasJsNameAnnotation(element)) { 41 | final node = getNode(element); 42 | transformer.replace( 43 | node.offset, node.end, generateForTemplate(element)); 44 | } 45 | } else { 46 | partsContent.add(element is ClassElement && hasJsNameAnnotation(element) 47 | ? generateForTemplate(element) 48 | : getSourceCode(element)); 49 | } 50 | } 51 | 52 | return transformer.applyOn(definingCompilationUnitContent) + 53 | partsContent.join('\n'); 54 | } 55 | 56 | String generateForTemplate(ClassElement clazzTemplate) => clazzTemplate.isEnum 57 | ? generateForEnum(clazzTemplate) 58 | : generateForClass(clazzTemplate); 59 | 60 | String generateForEnum(ClassElement clazzTemplate) { 61 | final doc = getDoc(clazzTemplate) ?? ''; 62 | final jsName = getJsName(clazzTemplate); 63 | final values = clazzTemplate.accessors 64 | .where((e) => e.returnType == clazzTemplate.thisType) 65 | .map((e) => e.name); 66 | final getters = values 67 | .map((e) => 'external static ${clazzTemplate.name} get $e;') 68 | .join('\n'); 69 | final castMethod = [ 70 | '${clazzTemplate.name}? ${clazzTemplate.name}\$cast(value) {', 71 | ...values.map((e) => 72 | ' if (value == ${clazzTemplate.name}.$e) return ${clazzTemplate.name}.$e;'), 73 | ' return null;', 74 | '}', 75 | ].join('\n'); 76 | return ''' 77 | $doc 78 | @JS(${jsName != null ? "'$jsName'" : ""}) 79 | class ${clazzTemplate.name} { 80 | $getters 81 | } 82 | $castMethod 83 | '''; 84 | } 85 | 86 | String generateForClass(ClassElement clazzTemplate) { 87 | final typeSystem = clazzTemplate.library.typeSystem; 88 | final source = clazzTemplate.source; 89 | final jsName = getJsName(clazzTemplate); 90 | 91 | final isAnonymous = hasAnonymousAnnotation(clazzTemplate); 92 | final doc = getDoc(clazzTemplate) ?? ''; 93 | final classContent = StringBuffer(); 94 | final extensionContent = StringBuffer(); 95 | 96 | for (final constructor in clazzTemplate.constructors) { 97 | if (constructor.isSynthetic || constructor.name != '') { 98 | continue; 99 | } 100 | final node = getNode(constructor) as ConstructorDeclaration; 101 | final signature = constructor.source.contents.data 102 | .substring(node.parameters.offset, node.parameters.end); 103 | classContent 104 | ..writeln(getDoc(constructor) ?? '') 105 | ..write('external ') 106 | ..write(isAnonymous ? 'factory ' : '') 107 | ..writeln('${clazzTemplate.name}$signature;'); 108 | } 109 | 110 | for (final field in clazzTemplate.fields) { 111 | if (field.isSynthetic) { 112 | continue; 113 | } 114 | final node = getNode(field) as VariableDeclaration; 115 | final type = (node.parent as VariableDeclarationList).type; 116 | final dartType = field.type; 117 | final typeAsString = field.source!.contents.data 118 | .substring(type!.offset, type.endToken.next!.offset); 119 | final jsNameMetadata = getJsName(field); 120 | if (field.hasInitializer) { 121 | } else if (jsNameMetadata != null || 122 | _needReturnTypeConversion(dartType, false)) { 123 | final nameDart = field.name; 124 | final nameJs = jsNameMetadata ?? field.name; 125 | final getterContent = _convertReturnValue( 126 | dartType, 127 | false, 128 | "getProperty(this, '$nameJs')", 129 | source, 130 | type, 131 | bindThisToFunction: true, 132 | ); 133 | const paramName = 'value'; 134 | final setterParam = 135 | _convertParam(dartType, typeSystem, paramName, source); 136 | extensionContent 137 | ..writeln(getDoc(field) ?? '') 138 | ..writeln('$typeAsString get $nameDart => $getterContent;') 139 | ..writeln(getDoc(field) ?? '') 140 | ..writeln('set $nameDart($typeAsString $paramName)' 141 | "{setProperty(this, '$nameJs', $setterParam);}"); 142 | } else { 143 | classContent 144 | ..writeln(getDoc(field) ?? '') 145 | ..writeln('external $typeAsString get ${field.name};') 146 | ..writeln(getDoc(field) ?? '') 147 | ..writeln('external set ${field.name}($typeAsString value);'); 148 | } 149 | } 150 | 151 | for (final method in clazzTemplate.accessors) { 152 | if (method.isSynthetic) { 153 | continue; 154 | } 155 | final node = getNode(method) as MethodDeclaration; 156 | final jsNameMetadata = getJsName(method); 157 | if (node.body is! EmptyFunctionBody) { 158 | extensionContent.writeln( 159 | method.source.contents.data.substring(node.offset, node.end)); 160 | } else if (method.isOperator) { 161 | } else if (method.isSetter && 162 | (jsNameMetadata != null || 163 | method.parameters.first.type is FunctionType || 164 | method.parameters.first.type.isDartCoreFunction)) { 165 | // name of setter end with an "=" sign 166 | final nameJs = 167 | jsNameMetadata ?? method.name.substring(0, method.name.length - 1); 168 | final signature = method.source.contents.data.substring( 169 | node.firstTokenAfterCommentAndMetadata.offset, node.body.offset); 170 | final param = method.parameters.first; 171 | final paramValue = _convertParam( 172 | param.type, 173 | typeSystem, 174 | param.name, 175 | source, 176 | ); 177 | extensionContent 178 | ..writeln(getDoc(method) ?? '') 179 | ..writeln(signature) 180 | ..writeln("{setProperty(this, '$nameJs', $paramValue);}"); 181 | } else if (method.isGetter && 182 | (jsNameMetadata != null || 183 | _needReturnTypeConversion(method.returnType, false))) { 184 | final nameJs = jsNameMetadata ?? method.name; 185 | final signature = method.source.contents.data.substring( 186 | node.firstTokenAfterCommentAndMetadata.offset, node.body.offset); 187 | final getterContent = _convertReturnValue( 188 | method.returnType, 189 | false, 190 | "getProperty(this, '$nameJs')", 191 | source, 192 | node.returnType, 193 | bindThisToFunction: true, 194 | ); 195 | extensionContent 196 | ..writeln(getDoc(method) ?? '') 197 | ..writeln('$signature => $getterContent;'); 198 | } else { 199 | final signature = method.source.contents.data.substring( 200 | node.firstTokenAfterCommentAndMetadata.offset, node.body.end); 201 | classContent.writeln(getDoc(method) ?? ''); 202 | if (!method.isExternal) { 203 | classContent.write('external '); 204 | } 205 | classContent.writeln(signature); 206 | } 207 | } 208 | 209 | for (final method in clazzTemplate.methods) { 210 | if (method.isSynthetic) { 211 | continue; 212 | } 213 | final node = getNode(method) as MethodDeclaration; 214 | 215 | if (node.body is! EmptyFunctionBody) { 216 | extensionContent.writeln( 217 | method.source.contents.data.substring(node.offset, node.end)); 218 | continue; 219 | } 220 | 221 | if (method.isOperator) { 222 | continue; 223 | } 224 | 225 | if (method.isPrivate || 226 | method.parameters.any((p) => _isFunctionType(p.type)) || 227 | _needReturnTypeConversion(method.returnType, false)) { 228 | final name = getJsName(method) ?? method.name; 229 | final signature = method.source.contents.data.substring( 230 | node.firstTokenAfterCommentAndMetadata.offset, node.body.offset); 231 | final args = method.parameters 232 | .map((v) => _convertParam( 233 | v.type, 234 | typeSystem, 235 | v.name, 236 | source, 237 | )) 238 | .join(','); 239 | final content = _convertReturnValue( 240 | method.returnType, 241 | false, 242 | "callMethod(this, '$name', [$args])", 243 | source, 244 | node.returnType, 245 | bindThisToFunction: false, 246 | ); 247 | extensionContent 248 | ..writeln(getDoc(method) ?? '') 249 | ..writeln(signature) 250 | ..writeln( 251 | method.returnType.isVoid ? '{ $content; }' : ' => $content;'); 252 | continue; 253 | } 254 | 255 | final signature = method.source.contents.data.substring( 256 | node.firstTokenAfterCommentAndMetadata.offset, node.body.end); 257 | classContent.writeln(getDoc(method) ?? ''); 258 | if (!method.isExternal) { 259 | classContent.write('external '); 260 | } 261 | classContent.writeln(signature); 262 | } 263 | 264 | final classNode = getNode(clazzTemplate) as ClassDeclaration; 265 | final classDeclaration = clazzTemplate.source.contents.data 266 | .substring(classNode.name.offset, classNode.leftBracket.offset); 267 | 268 | var result = ''' 269 | $doc 270 | @JS(${jsName != null ? "'$jsName'" : ""}) 271 | ${isAnonymous ? '@anonymous' : ''} 272 | class $classDeclaration { 273 | $classContent 274 | } 275 | '''; 276 | if (extensionContent.isNotEmpty) { 277 | final typeParameters = clazzTemplate.typeParameters.isEmpty 278 | ? '' 279 | : '<${clazzTemplate.typeParameters.map((e) => e.name).join(',')}>'; 280 | result += ''' 281 | extension ${clazzTemplate.name}\$Ext$typeParameters on ${clazzTemplate.name}$typeParameters { 282 | $extensionContent 283 | } 284 | '''; 285 | } 286 | //print(result); 287 | return result; 288 | } 289 | 290 | String _convertReturnValue( 291 | DartType dartType, 292 | bool handleEnums, 293 | String initialValue, 294 | Source source, 295 | TypeAnnotation? type, { 296 | required bool bindThisToFunction, 297 | }) => 298 | _isCoreListWithTypeParameter(dartType) 299 | ? '$initialValue?.cast<${_getTypeParameterOfList(source, type, dartType)}>()' 300 | : bindThisToFunction && _isFunctionType(dartType) 301 | ? "callMethod($initialValue, 'bind', [this])" 302 | : dartType.isDartAsyncFuture 303 | ? 'promiseToFuture($initialValue)' 304 | : handleEnums && isEnum(dartType) 305 | ? '${dartType.getDisplayString(withNullability: false)}\$cast($initialValue)' 306 | : initialValue; 307 | 308 | bool _needReturnTypeConversion(DartType dartType, bool handleEnums) => 309 | _isCoreListWithTypeParameter(dartType) || 310 | _isFunctionType(dartType) || 311 | dartType.isDartAsyncFuture || 312 | handleEnums && isEnum(dartType); 313 | 314 | bool isEnum(DartType dartType) { 315 | final element = dartType.element; 316 | return element is ClassElement && element.isEnum; 317 | } 318 | 319 | String _convertParam( 320 | DartType dartType, 321 | TypeSystem typeSystem, 322 | String paramName, 323 | Source source, 324 | ) { 325 | if (_isFunctionType(dartType)) { 326 | var function = paramName; 327 | if (dartType is FunctionType) { 328 | var hasParamChange = false; 329 | final declarationParams = []; 330 | final callParams = []; 331 | for (var i = 0; i < dartType.parameters.length; i++) { 332 | final param = dartType.parameters[i]; 333 | final p = 'p$i'; 334 | declarationParams.add(p); 335 | final callParam = _convertReturnValue( 336 | param.type, 337 | true, // required by webdev to avoid type error 338 | p, 339 | source, 340 | null, 341 | bindThisToFunction: false, 342 | ); 343 | callParams.add(callParam); 344 | hasParamChange |= callParam != p; 345 | } 346 | if (hasParamChange) { 347 | function = 348 | '(${declarationParams.join(',')}) => $paramName(${callParams.join(',')})'; 349 | } 350 | } 351 | if (typeSystem.isNullable(dartType)) { 352 | return '$paramName == null ? null : allowInterop($function)'; 353 | } else { 354 | return 'allowInterop($function)'; 355 | } 356 | } else { 357 | return paramName; 358 | } 359 | } 360 | 361 | bool _isCoreListWithTypeParameter(DartType dartType) => 362 | dartType is InterfaceType && 363 | dartType.element.library.name == 'dart.core' && 364 | dartType.element.name == 'List' && 365 | dartType.typeArguments.isNotEmpty; 366 | 367 | bool _isFunctionType(DartType dartType) => 368 | dartType is FunctionType || dartType.isDartCoreFunction; 369 | 370 | String _getTypeParameterOfList( 371 | Source source, 372 | TypeAnnotation? type, 373 | DartType dartType, 374 | ) { 375 | if (type != null) { 376 | return source.contents.data.substring(type.offset + 'List<'.length, 377 | type.end - (type.question == null ? 0 : 1) - '>'.length); 378 | } 379 | return (dartType as ParameterizedType).typeArguments.first.toString(); 380 | } 381 | } 382 | 383 | String? getDoc(Element element) { 384 | final node = getNode(element); 385 | if (node is AnnotatedNode) { 386 | final documentationComment = node.documentationComment; 387 | if (documentationComment != null) { 388 | return element.source!.contents.data 389 | .substring(documentationComment.offset, documentationComment.end); 390 | } 391 | } 392 | return null; 393 | } 394 | 395 | Iterable getAnnotations( 396 | LibraryElement library, 397 | List metadata, 398 | String libraryName, 399 | String className, 400 | ) => 401 | metadata.map((a) => a.computeConstantValue()).whereNotNull().where((e) => 402 | library.typeSystem.isAssignableTo( 403 | e.type!, getType(library, libraryName, className)!.thisType)); 404 | 405 | bool hasAnonymousAnnotation(ClassElement clazz) => clazz.metadata 406 | .where((a) => 407 | a.element!.library!.name == 'js' && a.element!.name == 'anonymous') 408 | .isNotEmpty; 409 | 410 | // getAnnotations(clazz.library, clazz.metadata, 'js', '_Anonymous') 411 | // .isNotEmpty; 412 | 413 | Iterable getJsNameAnnotations( 414 | LibraryElement library, List metadata) => 415 | getAnnotations(library, metadata, 'js_wrapping', 'JsName'); 416 | 417 | bool hasJsNameAnnotation(ClassElement clazz) => 418 | getJsNameAnnotations(clazz.library, clazz.metadata).isNotEmpty; 419 | 420 | String? getJsName(Element element) => 421 | getJsNameAnnotations(element.library!, element.metadata) 422 | .firstOrNull 423 | ?.getField('name') 424 | ?.toStringValue(); 425 | 426 | ClassElement? getType( 427 | LibraryElement libElement, String libName, String className) { 428 | final lib = 429 | libElement.importedLibraries.firstWhereOrNull((l) => l.name == libName); 430 | return lib?.getType(className); 431 | } 432 | --------------------------------------------------------------------------------